新增对群聊黑白名单和启禁用私聊的功能,添加对全局回复模式、语音角色和主动打招呼的指令配置。 (#341)

* feat: add support for ‘greeting’ and ‘global reply mode’ commands, improve variable naming and remove unnecessary backend output.

* feat: Add support for black and white lists, global reply mode and voice role settings, private chat switch, and active greeting configuration. Refactor some variable names and comment out redundant code for better readability and reduced backend output.

* feat: 为新功能完善了帮助面板

* docs: 完善了‘打招呼’的帮助说明

* feature:Add custom configuration for voice filtering regex.

---------

Co-authored-by: Sean <1519059137@qq.com>
This commit is contained in:
Sean Murphy 2023-04-13 22:36:58 +08:00 committed by GitHub
parent 11a62097f0
commit 4b29e261a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1190 additions and 802 deletions

View file

@ -14,7 +14,7 @@ import {
randomString, randomString,
completeJSON, completeJSON,
isImage, isImage,
getDefaultUserSetting, isCN, getMasterQQ getDefaultReplySetting, isCN, getMasterQQ
} from '../utils/common.js' } from '../utils/common.js'
import { ChatGPTPuppeteer } from '../utils/browser.js' import { ChatGPTPuppeteer } from '../utils/browser.js'
import { KeyvFile } from 'keyv-file' import { KeyvFile } from 'keyv-file'
@ -57,10 +57,10 @@ try {
const defaultPropmtPrefix = ', a large language model trained by OpenAI. You answer as concisely as possible for each response (e.g. dont be verbose). It is very important that you answer as concisely as possible, so please remember this. If you are generating a list, do not have too many items. Keep the number of items short.' const defaultPropmtPrefix = ', a large language model trained by OpenAI. You answer as concisely as possible for each response (e.g. dont be verbose). It is very important that you answer as concisely as possible, so please remember this. If you are generating a list, do not have too many items. Keep the number of items short.'
const newFetch = (url, options = {}) => { const newFetch = (url, options = {}) => {
const defaultOptions = Config.proxy const defaultOptions = Config.proxy
? { ? {
agent: proxy(Config.proxy) agent: proxy(Config.proxy)
} }
: {} : {}
const mergedOptions = { const mergedOptions = {
...defaultOptions, ...defaultOptions,
...options ...options
@ -452,22 +452,22 @@ export class chatgpt extends plugin {
} }
async switch2Picture (e) { async switch2Picture (e) {
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`) let userReplySetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
if (!userSetting) { if (!userReplySetting) {
userSetting = getDefaultUserSetting() userReplySetting = getDefaultReplySetting()
} else { } else {
userSetting = JSON.parse(userSetting) userReplySetting = JSON.parse(userReplySetting)
} }
userSetting.usePicture = true userReplySetting.usePicture = true
userSetting.useTTS = false userReplySetting.useTTS = false
await redis.set(`CHATGPT:USER:${e.sender.user_id}`, JSON.stringify(userSetting)) await redis.set(`CHATGPT:USER:${e.sender.user_id}`, JSON.stringify(userReplySetting))
await this.reply('ChatGPT回复已转换为图片模式') await this.reply('ChatGPT回复已转换为图片模式')
} }
async switch2Text (e) { async switch2Text (e) {
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`) let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
if (!userSetting) { if (!userSetting) {
userSetting = getDefaultUserSetting() userSetting = getDefaultReplySetting()
} else { } else {
userSetting = JSON.parse(userSetting) userSetting = JSON.parse(userSetting)
} }
@ -484,7 +484,7 @@ export class chatgpt extends plugin {
} }
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`) let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
if (!userSetting) { if (!userSetting) {
userSetting = getDefaultUserSetting() userSetting = getDefaultReplySetting()
} else { } else {
userSetting = JSON.parse(userSetting) userSetting = JSON.parse(userSetting)
} }
@ -500,7 +500,7 @@ export class chatgpt extends plugin {
} }
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`) let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
if (!userSetting) { if (!userSetting) {
userSetting = getDefaultUserSetting() userSetting = getDefaultReplySetting()
} else { } else {
userSetting = JSON.parse(userSetting) userSetting = JSON.parse(userSetting)
} }
@ -520,6 +520,20 @@ export class chatgpt extends plugin {
* #chatgpt * #chatgpt
*/ */
async chatgpt (e) { async chatgpt (e) {
if (!e.isMaster && e.isPrivate && !Config.enablePrivateChat) {
this.reply('ChatGpt私聊通道已关闭。')
return false
}
if (e.isGroup) {
const whitelist = Config.groupWhitelist.filter(group => group.trim())
if (whitelist.length > 0 && !whitelist.includes(e.group_id.toString())) {
return false
}
const blacklist = Config.groupBlacklist.filter(group => group.trim())
if (blacklist.length > 0 && blacklist.includes(e.group_id.toString())) {
return false
}
}
let prompt let prompt
if (this.toggleMode === 'at') { if (this.toggleMode === 'at') {
if (!e.raw_message || e.msg?.startsWith('#')) { if (!e.raw_message || e.msg?.startsWith('#')) {
@ -568,7 +582,7 @@ export class chatgpt extends plugin {
userSetting.useTTS = Config.defaultUseTTS userSetting.useTTS = Config.defaultUseTTS
} }
} else { } else {
userSetting = getDefaultUserSetting() userSetting = getDefaultReplySetting()
} }
let useTTS = !!userSetting.useTTS let useTTS = !!userSetting.useTTS
let speaker = convertSpeaker(userSetting.ttsRole || Config.defaultTTSRole) let speaker = convertSpeaker(userSetting.ttsRole || Config.defaultTTSRole)
@ -830,8 +844,18 @@ export class chatgpt extends plugin {
this.reply(`建议的回复:\n${chatMessage.suggestedResponses}`) this.reply(`建议的回复:\n${chatMessage.suggestedResponses}`)
} }
} }
// 过滤‘括号’的内容不读,减少违和感 // 处理tts输入文本
let ttsResponse = response.replace(/[(\[{<【《「『【〖【【【“‘'"@][^()\]}>】》」』】〗】】”’'@]*[)\]}>】》」』】〗】】”’'@]/g, '') let ttsResponse, ttsRegex
const regex = /^\/(.*)\/([gimuy]*)$/
const match = Config.ttsRegex.match(regex)
if (match) {
const pattern = match[1]
const flags = match[2]
ttsRegex = new RegExp(pattern, flags) // 返回新的正则表达式对象
} else {
ttsRegex = ''
}
ttsResponse = response.replace(ttsRegex, '')
if (Config.ttsSpace && ttsResponse.length <= Config.ttsAutoFallbackThreshold) { if (Config.ttsSpace && ttsResponse.length <= Config.ttsAutoFallbackThreshold) {
try { try {
let wav = await generateAudio(ttsResponse, speaker, '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)') let wav = await generateAudio(ttsResponse, speaker, '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)')
@ -1436,19 +1460,19 @@ export class chatgpt extends plugin {
Authorization: 'Bearer ' + Config.apiKey Authorization: 'Bearer ' + Config.apiKey
} }
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.error) { if (data.error) {
this.reply('获取失败:' + data.error.code) this.reply('获取失败:' + data.error.code)
return false return false
} else { } else {
let total_granted = data.total_granted.toFixed(2) let total_granted = data.total_granted.toFixed(2)
let total_used = data.total_used.toFixed(2) let total_used = data.total_used.toFixed(2)
let total_available = data.total_available.toFixed(2) let total_available = data.total_available.toFixed(2)
let expires_at = new Date(data.grants.data[0].expires_at * 1000).toLocaleDateString().replace(/\//g, '-') let expires_at = new Date(data.grants.data[0].expires_at * 1000).toLocaleDateString().replace(/\//g, '-')
this.reply('总额度:$' + total_granted + '\n已经使用额度$' + total_used + '\n当前剩余额度$' + total_available + '\n到期日期(UTC)' + expires_at) this.reply('总额度:$' + total_granted + '\n已经使用额度$' + total_used + '\n当前剩余额度$' + total_available + '\n到期日期(UTC)' + expires_at)
} }
}) })
} }
/** /**

View file

@ -24,10 +24,15 @@ export class Entertainment extends plugin {
priority: 500, priority: 500,
rule: [ rule: [
{ {
reg: '^#(chatgpt|ChatGPT)打招呼', reg: '^#chatgpt打招呼(帮助)?',
fnc: 'sendMessage', fnc: 'sendMessage',
permission: 'master' permission: 'master'
}, },
{
reg: '^#chatgpt(查看|设置|删除)打招呼',
fnc: 'handleSentMessage',
permission: 'master'
},
{ {
reg: `^(${emojiRegex()}){2}$`, reg: `^(${emojiRegex()}){2}$`,
fnc: 'combineEmoj' fnc: 'combineEmoj'
@ -36,8 +41,9 @@ export class Entertainment extends plugin {
}) })
this.task = [ this.task = [
{ {
// 每半小时 // 设置十分钟左右的浮动
cron: '*/30 * * * ?', cron: '0 ' + Math.ceil(Math.random() * 10) + ' 7-23/' + Config.helloInterval + ' * * ?',
// cron: '0 ' + '*/' + Config.helloInterval + ' * * * ?',
name: 'ChatGPT主动随机说话', name: 'ChatGPT主动随机说话',
fnc: this.sendRandomMessage.bind(this) fnc: this.sendRandomMessage.bind(this)
} }
@ -93,7 +99,16 @@ export class Entertainment extends plugin {
} }
async sendMessage (e) { async sendMessage (e) {
let groupId = e.msg.replace(/^#(chatgpt|ChatGPT)打招呼/, '') if (e.msg.match(/^#chatgpt打招呼帮助/) !== null) {
await this.reply('设置主动打招呼的群聊名单,群号之间以,隔开,参数之间空格隔开\n' +
'#chatgpt打招呼+群号:立即在指定群聊发起打招呼' +
'#chatgpt查看打招呼\n' +
'#chatgpt删除打招呼删除主动打招呼群聊可指定若干个群号\n' +
'#chatgpt设置打招呼可指定1-3个参数依次是更新打招呼列表、打招呼间隔时间和触发概率、更新打招呼所有配置项')
return false
}
let groupId = e.msg.replace(/^#chatgpt打招呼/, '')
logger.info(groupId)
groupId = parseInt(groupId) groupId = parseInt(groupId)
if (groupId && !Bot.getGroupList().get(groupId)) { if (groupId && !Bot.getGroupList().get(groupId)) {
await e.reply('机器人不在这个群里!') await e.reply('机器人不在这个群里!')
@ -125,9 +140,8 @@ export class Entertainment extends plugin {
} }
let groupId = parseInt(toSend[i]) let groupId = parseInt(toSend[i])
if (Bot.getGroupList().get(groupId)) { if (Bot.getGroupList().get(groupId)) {
// 5%的概率打招呼 // 打招呼概率
if (Math.floor(Math.random() * 100) < 5 && !(await redis.get(`CHATGPT:HELLO_GROUP:${groupId}`))) { if (Math.floor(Math.random() * 100) < Config.helloProbability) {
await redis.set(`CHATGPT:HELLO_GROUP:${groupId}`, '1', { EX: 3600 * 6 })
let message = await generateHello() let message = await generateHello()
logger.info(`打招呼给群聊${groupId}` + message) logger.info(`打招呼给群聊${groupId}` + message)
if (Config.defaultUseTTS) { if (Config.defaultUseTTS) {
@ -148,4 +162,80 @@ export class Entertainment extends plugin {
} }
} }
} }
async handleSentMessage (e) {
const addReg = /^#chatgpt设置打招呼[:]?\s?(\S+)(?:\s+(\d+))?(?:\s+(\d+))?$/
const delReg = /^#chatgpt删除打招呼[:\s]?(\S+)/
const checkReg = /^#chatgpt查看打招呼$/
let replyMsg = ''
Config.initiativeChatGroups = Config.initiativeChatGroups.filter(group => group.trim() !== '')
if (e.msg.match(checkReg)) {
if (Config.initiativeChatGroups.length === 0) {
replyMsg = '当前没有需要打招呼的群聊'
} else {
replyMsg = `当前打招呼设置为:\n${!e.isGroup ? '群号:' + Config.initiativeChatGroups.join(', ') + '\n' : ''}间隔时间:${Config.helloInterval}小时\n触发概率:${Config.helloProbability}%`
}
} else if (e.msg.match(delReg)) {
const groupsToDelete = e.msg.trim().match(delReg)[1].split(/[,]\s?/).filter(group => group.trim() !== '')
let deletedGroups = []
for (const element of groupsToDelete) {
if (!/^[1-9]\d{8,9}$/.test(element)) {
await this.reply(`群号${element}不合法请输入9-10位不以0开头的数字`, true)
return false
}
if (!Config.initiativeChatGroups.includes(element)) {
continue
}
Config.initiativeChatGroups.splice(Config.initiativeChatGroups.indexOf(element), 1)
deletedGroups.push(element)
}
Config.initiativeChatGroups = Config.initiativeChatGroups.filter(group => group.trim() !== '')
if (deletedGroups.length === 0) {
replyMsg = '没有可删除的群号,请输入正确的群号\n'
} else {
replyMsg = `已删除打招呼群号:${deletedGroups.join(', ')}\n`
}
replyMsg += `当前打招呼设置为:\n${!e.isGroup ? '群号:' + Config.initiativeChatGroups.join(', ') + '\n' : ''}间隔时间:${Config.helloInterval}小时\n触发概率:${Config.helloProbability}%`
} else if (e.msg.match(addReg)) {
let paramArray = e.msg.match(addReg)
if (typeof paramArray[3] === 'undefined' && typeof paramArray[2] !== 'undefined') {
Config.helloInterval = Math.min(Math.max(parseInt(paramArray[1]), 1), 24)
Config.helloProbability = Math.min(Math.max(parseInt(paramArray[2]), 0), 100)
replyMsg = `已更新打招呼设置:\n${!e.isGroup ? '群号:' + Config.initiativeChatGroups.join(', ') + '\n' : ''}间隔时间:${Config.helloInterval}小时\n触发概率:${Config.helloProbability}%`
} else {
const validGroups = []
const groups = paramArray ? paramArray[1].split(/[,]\s?/) : []
for (const element of groups) {
if (!/^[1-9]\d{8,9}$/.test(element)) {
await this.reply(`群号${element}不合法请输入9-10位不以0开头的数字`, true)
return false
}
if (Config.initiativeChatGroups.includes(element)) {
continue
}
validGroups.push(element)
}
if (validGroups.length === 0) {
await this.reply('没有可添加的群号,请输入新的群号')
return false
} else {
Config.initiativeChatGroups = Config.initiativeChatGroups
.filter(group => group.trim() !== '')
.concat(validGroups)
}
if (typeof paramArray[2] === 'undefined' && typeof paramArray[3] === 'undefined') {
replyMsg = `已更新打招呼设置:\n${!e.isGroup ? '群号:' + Config.initiativeChatGroups.join(', ') + '\n' : ''}间隔时间:${Config.helloInterval}小时\n触发概率:${Config.helloProbability}%`
} else {
Config.helloInterval = Math.min(Math.max(parseInt(paramArray[2]), 1), 24)
Config.helloProbability = Math.min(Math.max(parseInt(paramArray[3]), 0), 100)
replyMsg = `已更新打招呼设置:\n${!e.isGroup ? '群号:' + Config.initiativeChatGroups.join(', ') + '\n' : ''}间隔时间:${Config.helloInterval}小时\n触发概率:${Config.helloProbability}%`
}
}
} else {
replyMsg = '无效的打招呼设置,请输入正确的命令。\n可发送”#chatgpt打招呼帮助“获取打招呼指北。'
}
await this.reply(replyMsg)
return false
}
} }

View file

@ -150,6 +150,21 @@ let helpData = [
icon: 'confirm', icon: 'confirm',
title: '#chatgpt必应(开启|关闭)建议回复', title: '#chatgpt必应(开启|关闭)建议回复',
desc: '开关Bing模式下的建议回复。' desc: '开关Bing模式下的建议回复。'
},
{
icon: 'list',
title: '#(关闭|打开)群聊上下文',
desc: '开启后将会发送近期群聊中的对话给机器人提供参考'
},
{
icon: 'switch',
title: '#chatgpt(允许|禁止|打开|关闭|同意)私聊',
desc: '开启后将关闭本插件的私聊通道。(主人不影响)'
},
{
icon: 'token',
title: '#chatgpt(设置|添加)群聊[白黑]名单',
desc: '白名单配置后只有白名单内的群可使用本插件,配置黑名单则会在对应群聊禁用本插件'
} }
] ]
}, },
@ -258,7 +273,7 @@ let helpData = [
list: [ list: [
{ {
icon: 'smiley-wink', icon: 'smiley-wink',
title: '#chatgpt打招呼(群号)', title: '#chatgpt打招呼(群号|帮助)',
desc: '让AI随机到某个群去打招呼' desc: '让AI随机到某个群去打招呼'
}, },
{ {
@ -266,6 +281,11 @@ let helpData = [
title: '#chatgpt模式帮助', title: '#chatgpt模式帮助',
desc: '查看多种聊天模式的区别及当前使用的模式' desc: '查看多种聊天模式的区别及当前使用的模式'
}, },
{
icon: 'help',
title: '#chatgpt全局回复帮助',
desc: '获取配置全局回复模式和全局语音角色的命令帮助'
},
{ {
icon: 'help', icon: 'help',
title: '#chatgpt帮助', title: '#chatgpt帮助',
@ -296,15 +316,11 @@ export class help extends plugin {
} }
async help (e) { async help (e) {
if (Config.preview) if (Config.preview) { await renderUrl(e, `http://127.0.0.1:${Config.serverPort || 3321}/help/`, { Viewport: { width: 800, height: 600 } }) } else { await render(e, 'chatgpt-plugin', 'help/index', { helpData, version }) }
await renderUrl(e, `http://127.0.0.1:${Config.serverPort || 3321}/help/`, {Viewport: {width: 800, height: 600}})
else
await render(e, 'chatgpt-plugin', 'help/index', { helpData, version })
} }
async newHelp (e) { async newHelp (e) {
let use = e.msg.replace(/^#帮助-/, '').toUpperCase().trim() let use = e.msg.replace(/^#帮助-/, '').toUpperCase().trim()
await renderUrl(e, `http://127.0.0.1:${Config.serverPort || 3321}/help/` + use, {Viewport: {width: 800, height: 600}}) await renderUrl(e, `http://127.0.0.1:${Config.serverPort || 3321}/help/` + use, {Viewport: {width: 800, height: 600}})
} }
} }

View file

@ -3,7 +3,8 @@ import { Config } from '../utils/config.js'
import { exec } from 'child_process' import { exec } from 'child_process'
import { checkPnpm, formatDuration, parseDuration, getPublicIP } from '../utils/common.js' import { checkPnpm, formatDuration, parseDuration, getPublicIP } from '../utils/common.js'
import SydneyAIClient from '../utils/SydneyAIClient.js' import SydneyAIClient from '../utils/SydneyAIClient.js'
import { convertSpeaker, speakers } from '../utils/tts.js'
let isWhiteList = true
export class ChatgptManagement extends plugin { export class ChatgptManagement extends plugin {
constructor (e) { constructor (e) {
super({ super({
@ -135,13 +136,38 @@ export class ChatgptManagement extends plugin {
fnc: 'queryBingPromptPrefix', fnc: 'queryBingPromptPrefix',
permission: 'master' permission: 'master'
}, },
{
reg: '^#chatgpt(打开|关闭|设置)?全局((图片模式|语音模式|(语音角色|角色语音|角色).*)|回复帮助)$',
fnc: 'setDefaultReplySetting',
permission: 'master'
},
{ {
/** 命令正则匹配 */ /** 命令正则匹配 */
reg: '^#(关闭|打开)群聊上下文', reg: '^#(关闭|打开)群聊上下文$',
/** 执行方法 */ /** 执行方法 */
fnc: 'enableGroupContext', fnc: 'enableGroupContext',
permission: 'master' permission: 'master'
}, },
{
reg: '^#chatgpt(允许|禁止|打开|关闭|同意)私聊$',
fnc: 'enablePrivateChat',
permission: 'master'
},
{
reg: '^#chatgpt(设置|添加)群聊[白黑]名单$',
fnc: 'setList',
permission: 'master'
},
{
reg: '^#chatgpt查看群聊[白黑]名单$',
fnc: 'checkGroupList',
permission: 'master'
},
{
reg: '^#chatgpt(删除|移除)群聊[白黑]名单$',
fnc: 'delGroupList',
permission: 'master'
},
{ {
reg: '^#(设置|修改)管理密码', reg: '^#(设置|修改)管理密码',
fnc: 'setAdminPassword', fnc: 'setAdminPassword',
@ -155,23 +181,198 @@ export class ChatgptManagement extends plugin {
] ]
}) })
} }
async setList (e) {
this.setContext('saveList')
isWhiteList = e.msg.includes('白')
const listType = isWhiteList ? '白名单' : '黑名单'
await this.reply(`请发送需要设置的群聊${listType},群号间使用,隔开`, e.isGroup)
return false
}
async saveList (e) {
if (!this.e.msg) return
const groupNums = this.e.msg.match(/\d+/g)
const groupList = Array.isArray(groupNums) ? this.e.msg.match(/\d+/g).filter(value => /^[1-9]\d{8,9}/.test(value)) : []
if (!groupList.length) {
await this.reply('没有可添加的群号,请检查群号是否正确', e.isGroup)
return false
}
let whitelist = []
let blacklist = []
for (const element of groupList) {
if (isWhiteList) {
Config.groupWhitelist = Config.groupWhitelist.filter(item => item !== element)
whitelist.push(element)
} else {
Config.groupBlacklist = Config.groupBlacklist.filter(item => item !== element)
blacklist.push(element)
}
}
if (!(whitelist.length || blacklist.length)) {
await this.reply('没有可添加的群号,请检查群号是否正确或重复添加', e.isGroup)
this.finish('saveList')
return false
} else {
if (isWhiteList) {
Config.groupWhitelist = Config.groupWhitelist
.filter(group => group.trim() !== '')
.concat(whitelist)
} else {
Config.groupBlacklist = Config.groupBlacklist
.filter(group => group.trim() !== '')
.concat(blacklist)
}
}
await this.reply(`群聊${isWhiteList ? '白' : '黑'}名单已更新,可通过\n'#chatgpt查看群聊${isWhiteList ? '白' : '黑'}名单'查看最新名单\n#chatgpt移除群聊${isWhiteList ? '白' : '黑'}名单'管理名单`, e.isGroup)
this.finish('saveList')
}
async checkGroupList (e) {
isWhiteList = e.msg.includes('白')
const list = isWhiteList ? Config.groupWhitelist : Config.groupBlacklist
const listType = isWhiteList ? '白名单' : '黑名单'
const replyMsg = list.length ? `当前群聊${listType}为:${list.join('')}` : `当前没有设置任何${listType}`
this.reply(replyMsg, e.isGroup)
return false
}
async delGroupList (e) {
isWhiteList = e.msg.includes('白')
const listType = isWhiteList ? '白名单' : '黑名单'
let replyMsg = ''
if (Config.groupWhitelist.length && Config.groupBlacklist.length) {
replyMsg = `当前群聊(白|黑)名单为空,请先添加${listType}吧~`
} else if ((isWhiteList && !Config.groupWhitelist.length) || (!isWhiteList && !Config.groupBlacklist.length)) {
replyMsg = `当前群聊${listType}为空,请先添加吧~`
}
if (replyMsg) {
await this.reply(replyMsg, e.isGroup)
return false
}
this.setContext('confirmDelGroup')
await this.reply(`请发送需要删除的群聊${listType},群号间使用,隔开。输入‘全部删除’清空${listType}`, e.isGroup)
return false
}
async confirmDelGroup (e) {
if (!this.e.msg) return
const isAllDeleted = this.e.msg.trim() === '全部删除'
const groupNumRegex = /^[1-9]\d{8,9}$/
const groupNums = this.e.msg.match(/\d+/g)
const validGroups = Array.isArray(groupNums) ? groupNums.filter(groupNum => groupNumRegex.test(groupNum)) : []
if (isAllDeleted) {
Config.groupWhitelist = isWhiteList ? [] : Config.groupWhitelist
Config.groupBlacklist = !isWhiteList ? [] : Config.groupBlacklist
} else {
if (!validGroups.length) {
await this.reply('没有可删除的群号,请检查输入的群号是否正确', e.isGroup)
return false
} else {
for (const element of validGroups) {
if (isWhiteList) {
Config.groupWhitelist = Config.groupWhitelist.filter(item => item !== element)
} else {
Config.groupBlacklist = Config.groupBlacklist.filter(item => item !== element)
}
}
}
}
const groupType = isWhiteList ? '白' : '黑'
await this.reply(`群聊${groupType}名单已更新,可通过'#chatgpt查看群聊${groupType}名单'命令查看最新名单`)
this.finish('confirmDelGroup')
}
async enablePrivateChat (e) {
Config.enablePrivateChat = !!e.msg.match(/(允许|打开|同意)/)
await this.reply('设置成功', e.isGroup)
return false
}
async enableGroupContext (e) { async enableGroupContext (e) {
const re = /#(关闭|打开)/ const reg = /(关闭|打开)/
const match = e.msg.match(re) const match = e.msg.match(reg)
//logger.info(match)
if (match) { if (match) {
const action = match[1] const action = match[1]
if (action === '关闭') { if (action === '关闭') {
Config.enableGroupContext = false // 关闭 Config.enableGroupContext = false // 关闭
await this.reply('已关闭群聊上下文功能', true) await this.reply('已关闭群聊上下文功能', true)
} else { } else {
Config.enableGroupContext = true // 打开 Config.enableGroupContext = true // 打开
await this.reply('已打开群聊上下文功能', true) await this.reply('已打开群聊上下文功能', true)
} }
} }
return false return false
} }
async setDefaultReplySetting (e) {
const reg = /^#chatgpt(打开|关闭|设置)?全局((图片模式|语音模式|(语音角色|角色语音|角色).*)|回复帮助)/
const matchCommand = e.msg.match(reg)
const settingType = matchCommand[2]
let replyMsg = ''
switch (settingType) {
case '图片模式':
if (matchCommand[1] === '打开') {
Config.defaultUsePicture = true
Config.defaultUseTTS = false
replyMsg = 'ChatGPT将默认以图片回复'
} else if (matchCommand[1] === '关闭') {
Config.defaultUsePicture = false
if (Config.defaultUseTTS) {
replyMsg = 'ChatGPT将默认以语音回复'
} else {
replyMsg = 'ChatGPT将默认以文本回复'
}
} else if (matchCommand[1] === '设置') {
replyMsg = '请使用“#chatgpt打开全局图片模式”或“#chatgpt关闭全局图片模式”命令来设置回复模式'
} break
case '语音模式':
if (!Config.ttsSpace) {
replyMsg = '您没有配置VITS API请前往锅巴面板进行配置'
break
}
if (matchCommand[1] === '打开') {
Config.defaultUseTTS = true
Config.defaultUsePicture = false
replyMsg = 'ChatGPT将默认以语音回复'
} else if (matchCommand[1] === '关闭') {
Config.defaultUseTTS = false
if (Config.defaultUsePicture) {
replyMsg = 'ChatGPT将默认以图片回复'
} else {
replyMsg = 'ChatGPT将默认以文本回复'
}
} else if (matchCommand[1] === '设置') {
replyMsg = '请使用“#chatgpt打开全局语音模式”或“#chatgpt关闭全局语音模式”命令来设置回复模式'
} break
case '回复帮助':
replyMsg = '可使用以下命令配置全局回复:\n#chatgpt(打开/关闭)全局(语音/图片)模式\n#chatgpt设置全局(语音角色|角色语音|角色)+角色名称(留空则为随机)'
break
default:
if (!Config.ttsSpace) {
replyMsg = '您没有配置VITS API请前往锅巴面板进行配置'
break
}
if (settingType.match(/(语音角色|角色语音|角色)/)) {
const speaker = matchCommand[2].replace(/(语音角色|角色语音|角色)/, '').trim() || ''
if (!speaker.length) {
replyMsg = 'ChatGpt将随机挑选角色回复'
Config.defaultTTSRole = ''
} else {
const ttsRole = convertSpeaker(speaker)
if (speakers.includes(ttsRole)) {
Config.defaultTTSRole = ttsRole
replyMsg = `ChatGPT默认语音角色已被设置为“${ttsRole}`
} else {
replyMsg = `抱歉,我还不认识“${ttsRole}”这个语音角色`
}
}
} else {
replyMsg = "无法识别的设置类型\n请使用'#chatgpt全局回复帮助'查看正确命令"
}
}
await this.reply(replyMsg, true)
}
async turnOnConfirm (e) { async turnOnConfirm (e) {
await redis.set('CHATGPT:CONFIRM', 'on') await redis.set('CHATGPT:CONFIRM', 'on')
await this.reply('已开启消息确认', true) await this.reply('已开启消息确认', true)

View file

@ -57,6 +57,8 @@
"initiativeChatGroups": [], "initiativeChatGroups": [],
"enableDraw": true, "enableDraw": true,
"helloPrompt": "写一段话让大家来找我聊天。类似于“有人找我聊天吗“这种风格轻松随意一点控制在20个字以内", "helloPrompt": "写一段话让大家来找我聊天。类似于“有人找我聊天吗“这种风格轻松随意一点控制在20个字以内",
"helloInterval": 3,
"helloProbability": 50,
"chatglmBaseUrl": "http://localhost:8080", "chatglmBaseUrl": "http://localhost:8080",
"allowOtherMode": true, "allowOtherMode": true,
"sydneyContext": "", "sydneyContext": "",
@ -66,5 +68,9 @@
"enableRobotAt": true, "enableRobotAt": true,
"maxNumUserMessagesInConversation": 20, "maxNumUserMessagesInConversation": 20,
"sydneyApologyIgnored": true, "sydneyApologyIgnored": true,
"enforceMaster": false "enforceMaster": false,
"enablePrivateChat": false,
"groupWhitelist": [],
"groupBlacklist": [],
"ttsRegex": "/匹配规则/匹配模式"
} }

View file

@ -38,12 +38,30 @@ export function supportGuoba () {
bottomHelpMessage: '检查输入结果中是否有违禁词,如果存在黑名单中的违禁词则不输出。英文逗号隔开', bottomHelpMessage: '检查输入结果中是否有违禁词,如果存在黑名单中的违禁词则不输出。英文逗号隔开',
component: 'InputTextArea' component: 'InputTextArea'
}, },
{
field: 'groupWhitelist',
label: '群聊白名单',
bottomHelpMessage: '设置后只有白名单内的群可以使用本插件。用英文逗号隔开',
component: 'Input'
},
{
field: 'groupBlacklist',
label: '群聊黑名单',
bottomHelpMessage: '设置后名单内的群禁止使用本插件。用英文逗号隔开',
component: 'Input'
},
{},
{ {
field: 'imgOcr', field: 'imgOcr',
label: '图片识别', label: '图片识别',
bottomHelpMessage: '是否识别消息中图片的文字内容,需要同时包含图片和消息才生效', bottomHelpMessage: '是否识别消息中图片的文字内容,需要同时包含图片和消息才生效',
component: 'Switch' component: 'Switch'
}, },
{
field: 'enablePrivateChat',
label: '是否允许私聊机器人',
component: 'Switch'
},
{ {
field: 'defaultUsePicture', field: 'defaultUsePicture',
label: '全局图片模式', label: '全局图片模式',
@ -65,6 +83,12 @@ export function supportGuoba () {
options: speakers.concat('随机').map(s => { return { label: s, value: s } }) options: speakers.concat('随机').map(s => { return { label: s, value: s } })
} }
}, },
{
field: 'ttsRegex',
label: '语音过滤正则表达式',
bottomHelpMessage: '语音模式下配置此项以过滤不想被读出来的内容。表达式测试地址https://www.runoob.com/regexp/regexp-syntax.html',
component: 'Input'
},
{ {
field: 'ttsAutoFallbackThreshold', field: 'ttsAutoFallbackThreshold',
label: '语音转文字阈值', label: '语音转文字阈值',
@ -303,7 +327,7 @@ export function supportGuoba () {
// component: 'InputTextArea' // component: 'InputTextArea'
// }, // },
{ {
field: 'groupContextLength', field: 'groupContextLength',
label: '允许机器人读取近期的最多群聊聊天记录条数。', label: '允许机器人读取近期的最多群聊聊天记录条数。',
bottomHelpMessage: '允许机器人读取近期的最多群聊聊天记录条数。太多可能会超。默认50', bottomHelpMessage: '允许机器人读取近期的最多群聊聊天记录条数。太多可能会超。默认50',
component: 'InputNumber' component: 'InputNumber'
@ -488,10 +512,29 @@ export function supportGuoba () {
}, },
{ {
field: 'helloPrompt', field: 'helloPrompt',
label: '打招呼所说文字的引导文字', label: '打招呼prompt',
bottomHelpMessage: '将会用这段文字询问ChatGPT由ChatGPT给出随机的打招呼文字', bottomHelpMessage: '将会用这段文字询问ChatGPT由ChatGPT给出随机的打招呼文字',
component: 'Input' component: 'Input'
}, },
{
field: 'helloInterval',
label: '打招呼间隔(小时)',
component: 'InputNumber',
componentProps: {
min: 1,
max: 24
}
},
{
field: 'helloProbability',
label: '打招呼的触发概率(%)',
bottomHelpMessage: '设置为100则每次经过间隔时间必定触发主动打招呼事件。',
component: 'InputNumber',
componentProps: {
min: 0,
max: 100
}
},
{ {
field: 'emojiBaseURL', field: 'emojiBaseURL',
label: '合成emoji的API地址默认谷歌厨房', label: '合成emoji的API地址默认谷歌厨房',
@ -511,7 +554,7 @@ export function supportGuoba () {
field: 'serverPort', field: 'serverPort',
label: '系统Api服务端口', label: '系统Api服务端口',
bottomHelpMessage: '系统Api服务开启的端口号如需外网访问请将系统防火墙和服务器防火墙对应端口开放,修改后请重启', bottomHelpMessage: '系统Api服务开启的端口号如需外网访问请将系统防火墙和服务器防火墙对应端口开放,修改后请重启',
component: 'InputNumber', component: 'InputNumber'
}, },
{ {
field: 'serverHost', field: 'serverHost',
@ -529,14 +572,14 @@ export function supportGuoba () {
field: 'chatViewWidth', field: 'chatViewWidth',
label: '图片渲染宽度', label: '图片渲染宽度',
bottomHelpMessage: '聊天页面渲染窗口的宽度', bottomHelpMessage: '聊天页面渲染窗口的宽度',
component: 'InputNumber', component: 'InputNumber'
}, },
{ {
field: 'chatViewBotName', field: 'chatViewBotName',
label: 'Bot命名', label: 'Bot命名',
bottomHelpMessage: '新渲染模式强制修改Bot命名', bottomHelpMessage: '新渲染模式强制修改Bot命名',
component: 'Input' component: 'Input'
}, }
], ],
// 获取配置数据方法(用于前端填充显示数据) // 获取配置数据方法(用于前端填充显示数据)
getConfigData () { getConfigData () {
@ -553,4 +596,4 @@ export function supportGuoba () {
} }
} }
} }
} }

View file

@ -11,6 +11,7 @@ import schedule from 'node-schedule'
import { Config } from '../utils/config.js' import { Config } from '../utils/config.js'
import { randomString, getPublicIP } from '../utils/common.js' import { randomString, getPublicIP } from '../utils/common.js'
const __dirname = path.resolve() const __dirname = path.resolve()
const server = fastify({ const server = fastify({
logger: Config.debug logger: Config.debug
@ -208,6 +209,7 @@ export async function createServer() {
if(request.method == 'GET') if(request.method == 'GET')
Statistics.WebAccess.count += 1 Statistics.WebAccess.count += 1
done() done()
}) })
//定时任务 //定时任务
var rule = new schedule.RecurrenceRule(); var rule = new schedule.RecurrenceRule();
@ -235,4 +237,4 @@ export async function createServer() {
} }
server.log.info(`server listening on ${server.server.address().port}`) server.log.info(`server listening on ${server.server.address().port}`)
}) })
} }

File diff suppressed because it is too large Load diff

View file

@ -112,7 +112,7 @@ export async function pTimeout (
const cancelablePromise = new Promise((resolve, reject) => { const cancelablePromise = new Promise((resolve, reject) => {
if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) { if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) {
throw new TypeError( throw new TypeError(
`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\`` `Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``
) )
} }
@ -146,11 +146,11 @@ export async function pTimeout (
} }
const errorMessage = const errorMessage =
typeof message === 'string' typeof message === 'string'
? message ? message
: `Promise timed out after ${milliseconds} milliseconds` : `Promise timed out after ${milliseconds} milliseconds`
const timeoutError = const timeoutError =
message instanceof Error ? message : new Error(errorMessage) message instanceof Error ? message : new Error(errorMessage)
if (typeof promise.cancel === 'function') { if (typeof promise.cancel === 'function') {
promise.cancel() promise.cancel()
@ -179,19 +179,19 @@ export async function pTimeout (
return cancelablePromise return cancelablePromise
} }
/** /**
TODO: Remove below function and just 'reject(signal.reason)' when targeting Node 18. TODO: Remove below function and just 'reject(signal.reason)' when targeting Node 18.
*/ */
function getAbortedReason (signal) { function getAbortedReason (signal) {
const reason = const reason =
signal.reason === undefined signal.reason === undefined
? getDOMException('This operation was aborted.') ? getDOMException('This operation was aborted.')
: signal.reason : signal.reason
return reason instanceof Error ? reason : getDOMException(reason) return reason instanceof Error ? reason : getDOMException(reason)
} }
/** /**
TODO: Remove AbortError and just throw DOMException when targeting Node 18. TODO: Remove AbortError and just throw DOMException when targeting Node 18.
*/ */
function getDOMException (errorMessage) { function getDOMException (errorMessage) {
return globalThis.DOMException === undefined return globalThis.DOMException === undefined
? new Error(errorMessage) ? new Error(errorMessage)
@ -239,18 +239,18 @@ export async function getMasterQQ () {
} }
/** /**
* *
* @param pluginKey plugin key * @param pluginKey plugin key
* @param htmlPath html文件路径相对于plugin resources目录 * @param htmlPath html文件路径相对于plugin resources目录
* @param data 渲染数据 * @param data 渲染数据
* @param renderCfg 渲染配置 * @param renderCfg 渲染配置
* @param renderCfg.retType 返回值类型 * @param renderCfg.retType 返回值类型
* * default/true * * default/true
* * msgId自动发送图片返回msg id * * msgId自动发送图片返回msg id
* * base64: 不自动发送图像返回图像base64数据 * * base64: 不自动发送图像返回图像base64数据
* @param renderCfg.beforeRender({data}) 可改写渲染的data数据 * @param renderCfg.beforeRender({data}) 可改写渲染的data数据
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
export async function render (e, pluginKey, htmlPath, data = {}, renderCfg = {}) { export async function render (e, pluginKey, htmlPath, data = {}, renderCfg = {}) {
// 处理传入的path // 处理传入的path
htmlPath = htmlPath.replace(/.html$/, '') htmlPath = htmlPath.replace(/.html$/, '')
@ -316,7 +316,7 @@ export async function renderUrl (e, url, renderCfg = {}) {
width: 1280, width: 1280,
height: 720 height: 720
}) })
let buff = base64 = await page.screenshot({fullPage:true}) let buff = base64 = await page.screenshot({ fullPage: true })
base64 = segment.image(buff) base64 = segment.image(buff)
await page.close().catch((err) => logger.error(err)) await page.close().catch((err) => logger.error(err))
} catch (error) { } catch (error) {
@ -338,7 +338,7 @@ export async function renderUrl (e, url, renderCfg = {}) {
return renderCfg.retType === 'msgId' ? ret : true return renderCfg.retType === 'msgId' ? ret : true
} }
export function getDefaultUserSetting () { export function getDefaultReplySetting () {
return { return {
usePicture: Config.defaultUsePicture, usePicture: Config.defaultUsePicture,
useTTS: Config.defaultUseTTS, useTTS: Config.defaultUseTTS,
@ -522,7 +522,7 @@ export function maskQQ (qq) {
return newqq return newqq
} }
export function completeJSON(input) { export function completeJSON (input) {
let result = {} let result = {}
let inJson = false let inJson = false
@ -533,7 +533,7 @@ export function completeJSON(input) {
let tempValue = '' let tempValue = ''
for (let i = 0; i < input.length; i++) { for (let i = 0; i < input.length; i++) {
// 获取当前字符 // 获取当前字符
let char = input[i]; let char = input[i]
// 获取到json头 // 获取到json头
if (!inJson && char === '{') { if (!inJson && char === '{') {
inJson = true inJson = true
@ -566,7 +566,7 @@ export function completeJSON(input) {
// 结束结构追加数据 // 结束结构追加数据
if (!inQuote && onStructure && char === ',') { if (!inQuote && onStructure && char === ',') {
// 追加结构 // 追加结构
result[tempKey] = tempValue.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, "\t") result[tempKey] = tempValue.replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\t/g, '\t')
// 结束结构清除数据 // 结束结构清除数据
onStructure = false onStructure = false
inQuote = false inQuote = false
@ -577,12 +577,12 @@ export function completeJSON(input) {
} }
// 处理截断的json数据 // 处理截断的json数据
if (onStructure && tempKey != '') { if (onStructure && tempKey != '') {
result[tempKey] = tempValue.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, "\t") result[tempKey] = tempValue.replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\t/g, '\t')
} }
return result return result
} }
export async function isImage(link) { export async function isImage (link) {
try { try {
let response = await fetch(link) let response = await fetch(link)
let body = await response.arrayBuffer() let body = await response.arrayBuffer()
@ -606,4 +606,4 @@ export async function getPublicIP() {
return '127.0.0.1' return '127.0.0.1'
} }
} }

View file

@ -68,6 +68,8 @@ const defaultConfig = {
initiativeChatGroups: [], initiativeChatGroups: [],
enableDraw: true, enableDraw: true,
helloPrompt: '写一段话让大家来找我聊天。类似于“有人找我聊天吗?"这种风格轻松随意一点控制在20个字以内', helloPrompt: '写一段话让大家来找我聊天。类似于“有人找我聊天吗?"这种风格轻松随意一点控制在20个字以内',
helloInterval: 3,
helloProbability: 50,
chatglmBaseUrl: 'http://localhost:8080', chatglmBaseUrl: 'http://localhost:8080',
allowOtherMode: true, allowOtherMode: true,
sydneyContext: '', sydneyContext: '',
@ -85,6 +87,10 @@ const defaultConfig = {
viewHost: '', viewHost: '',
chatViewWidth: 1280, chatViewWidth: 1280,
chatViewBotName: '', chatViewBotName: '',
enablePrivateChat: false,
groupWhitelist: [],
groupBlacklist: [],
ttsRegex: '/匹配规则/匹配模式',
version: 'v2.5.1' version: 'v2.5.1'
} }
const _path = process.cwd() const _path = process.cwd()
@ -150,4 +156,4 @@ export const Config = new Proxy(config, {
} }
return true return true
} }
}) })