mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 13:27:08 +00:00
新增对群聊黑白名单和启禁用私聊的功能,添加对全局回复模式、语音角色和主动打招呼的指令配置。 (#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:
parent
11a62097f0
commit
4b29e261a0
10 changed files with 1190 additions and 802 deletions
82
apps/chat.js
82
apps/chat.js
|
|
@ -14,7 +14,7 @@ import {
|
|||
randomString,
|
||||
completeJSON,
|
||||
isImage,
|
||||
getDefaultUserSetting, isCN, getMasterQQ
|
||||
getDefaultReplySetting, isCN, getMasterQQ
|
||||
} from '../utils/common.js'
|
||||
import { ChatGPTPuppeteer } from '../utils/browser.js'
|
||||
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. don’t 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 defaultOptions = Config.proxy
|
||||
? {
|
||||
? {
|
||||
agent: proxy(Config.proxy)
|
||||
}
|
||||
: {}
|
||||
: {}
|
||||
const mergedOptions = {
|
||||
...defaultOptions,
|
||||
...options
|
||||
|
|
@ -452,22 +452,22 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
|
||||
async switch2Picture (e) {
|
||||
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||
if (!userSetting) {
|
||||
userSetting = getDefaultUserSetting()
|
||||
let userReplySetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||
if (!userReplySetting) {
|
||||
userReplySetting = getDefaultReplySetting()
|
||||
} else {
|
||||
userSetting = JSON.parse(userSetting)
|
||||
userReplySetting = JSON.parse(userReplySetting)
|
||||
}
|
||||
userSetting.usePicture = true
|
||||
userSetting.useTTS = false
|
||||
await redis.set(`CHATGPT:USER:${e.sender.user_id}`, JSON.stringify(userSetting))
|
||||
userReplySetting.usePicture = true
|
||||
userReplySetting.useTTS = false
|
||||
await redis.set(`CHATGPT:USER:${e.sender.user_id}`, JSON.stringify(userReplySetting))
|
||||
await this.reply('ChatGPT回复已转换为图片模式')
|
||||
}
|
||||
|
||||
async switch2Text (e) {
|
||||
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||
if (!userSetting) {
|
||||
userSetting = getDefaultUserSetting()
|
||||
userSetting = getDefaultReplySetting()
|
||||
} else {
|
||||
userSetting = JSON.parse(userSetting)
|
||||
}
|
||||
|
|
@ -484,7 +484,7 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||
if (!userSetting) {
|
||||
userSetting = getDefaultUserSetting()
|
||||
userSetting = getDefaultReplySetting()
|
||||
} else {
|
||||
userSetting = JSON.parse(userSetting)
|
||||
}
|
||||
|
|
@ -500,7 +500,7 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||
if (!userSetting) {
|
||||
userSetting = getDefaultUserSetting()
|
||||
userSetting = getDefaultReplySetting()
|
||||
} else {
|
||||
userSetting = JSON.parse(userSetting)
|
||||
}
|
||||
|
|
@ -520,6 +520,20 @@ export class chatgpt extends plugin {
|
|||
* #chatgpt
|
||||
*/
|
||||
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
|
||||
if (this.toggleMode === 'at') {
|
||||
if (!e.raw_message || e.msg?.startsWith('#')) {
|
||||
|
|
@ -568,7 +582,7 @@ export class chatgpt extends plugin {
|
|||
userSetting.useTTS = Config.defaultUseTTS
|
||||
}
|
||||
} else {
|
||||
userSetting = getDefaultUserSetting()
|
||||
userSetting = getDefaultReplySetting()
|
||||
}
|
||||
let useTTS = !!userSetting.useTTS
|
||||
let speaker = convertSpeaker(userSetting.ttsRole || Config.defaultTTSRole)
|
||||
|
|
@ -830,8 +844,18 @@ export class chatgpt extends plugin {
|
|||
this.reply(`建议的回复:\n${chatMessage.suggestedResponses}`)
|
||||
}
|
||||
}
|
||||
// 过滤‘括号’的内容不读,减少违和感
|
||||
let ttsResponse = response.replace(/[((\[{<【《「『【〖【【【“‘'"@][^()()\]}>】》」』】〗】】”’'@]*[))\]}>】》」』】〗】】”’'@]/g, '')
|
||||
// 处理tts输入文本
|
||||
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) {
|
||||
try {
|
||||
let wav = await generateAudio(ttsResponse, speaker, '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)')
|
||||
|
|
@ -1436,19 +1460,19 @@ export class chatgpt extends plugin {
|
|||
Authorization: 'Bearer ' + Config.apiKey
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
this.reply('获取失败:' + data.error.code)
|
||||
return false
|
||||
} else {
|
||||
let total_granted = data.total_granted.toFixed(2)
|
||||
let total_used = data.total_used.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, '-')
|
||||
this.reply('总额度:$' + total_granted + '\n已经使用额度:$' + total_used + '\n当前剩余额度:$' + total_available + '\n到期日期(UTC):' + expires_at)
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
this.reply('获取失败:' + data.error.code)
|
||||
return false
|
||||
} else {
|
||||
let total_granted = data.total_granted.toFixed(2)
|
||||
let total_used = data.total_used.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, '-')
|
||||
this.reply('总额度:$' + total_granted + '\n已经使用额度:$' + total_used + '\n当前剩余额度:$' + total_available + '\n到期日期(UTC):' + expires_at)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -24,10 +24,15 @@ export class Entertainment extends plugin {
|
|||
priority: 500,
|
||||
rule: [
|
||||
{
|
||||
reg: '^#(chatgpt|ChatGPT)打招呼',
|
||||
reg: '^#chatgpt打招呼(帮助)?',
|
||||
fnc: 'sendMessage',
|
||||
permission: 'master'
|
||||
},
|
||||
{
|
||||
reg: '^#chatgpt(查看|设置|删除)打招呼',
|
||||
fnc: 'handleSentMessage',
|
||||
permission: 'master'
|
||||
},
|
||||
{
|
||||
reg: `^(${emojiRegex()}){2}$`,
|
||||
fnc: 'combineEmoj'
|
||||
|
|
@ -36,8 +41,9 @@ export class Entertainment extends plugin {
|
|||
})
|
||||
this.task = [
|
||||
{
|
||||
// 每半小时
|
||||
cron: '*/30 * * * ?',
|
||||
// 设置十分钟左右的浮动
|
||||
cron: '0 ' + Math.ceil(Math.random() * 10) + ' 7-23/' + Config.helloInterval + ' * * ?',
|
||||
// cron: '0 ' + '*/' + Config.helloInterval + ' * * * ?',
|
||||
name: 'ChatGPT主动随机说话',
|
||||
fnc: this.sendRandomMessage.bind(this)
|
||||
}
|
||||
|
|
@ -93,7 +99,16 @@ export class Entertainment extends plugin {
|
|||
}
|
||||
|
||||
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)
|
||||
if (groupId && !Bot.getGroupList().get(groupId)) {
|
||||
await e.reply('机器人不在这个群里!')
|
||||
|
|
@ -125,9 +140,8 @@ export class Entertainment extends plugin {
|
|||
}
|
||||
let groupId = parseInt(toSend[i])
|
||||
if (Bot.getGroupList().get(groupId)) {
|
||||
// 5%的概率打招呼
|
||||
if (Math.floor(Math.random() * 100) < 5 && !(await redis.get(`CHATGPT:HELLO_GROUP:${groupId}`))) {
|
||||
await redis.set(`CHATGPT:HELLO_GROUP:${groupId}`, '1', { EX: 3600 * 6 })
|
||||
// 打招呼概率
|
||||
if (Math.floor(Math.random() * 100) < Config.helloProbability) {
|
||||
let message = await generateHello()
|
||||
logger.info(`打招呼给群聊${groupId}:` + message)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
apps/help.js
28
apps/help.js
|
|
@ -150,6 +150,21 @@ let helpData = [
|
|||
icon: 'confirm',
|
||||
title: '#chatgpt必应(开启|关闭)建议回复',
|
||||
desc: '开关Bing模式下的建议回复。'
|
||||
},
|
||||
{
|
||||
icon: 'list',
|
||||
title: '#(关闭|打开)群聊上下文',
|
||||
desc: '开启后将会发送近期群聊中的对话给机器人提供参考'
|
||||
},
|
||||
{
|
||||
icon: 'switch',
|
||||
title: '#chatgpt(允许|禁止|打开|关闭|同意)私聊',
|
||||
desc: '开启后将关闭本插件的私聊通道。(主人不影响)'
|
||||
},
|
||||
{
|
||||
icon: 'token',
|
||||
title: '#chatgpt(设置|添加)群聊[白黑]名单',
|
||||
desc: '白名单配置后只有白名单内的群可使用本插件,配置黑名单则会在对应群聊禁用本插件'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -258,7 +273,7 @@ let helpData = [
|
|||
list: [
|
||||
{
|
||||
icon: 'smiley-wink',
|
||||
title: '#chatgpt打招呼(群号)',
|
||||
title: '#chatgpt打招呼(群号|帮助)',
|
||||
desc: '让AI随机到某个群去打招呼'
|
||||
},
|
||||
{
|
||||
|
|
@ -266,6 +281,11 @@ let helpData = [
|
|||
title: '#chatgpt模式帮助',
|
||||
desc: '查看多种聊天模式的区别及当前使用的模式'
|
||||
},
|
||||
{
|
||||
icon: 'help',
|
||||
title: '#chatgpt全局回复帮助',
|
||||
desc: '获取配置全局回复模式和全局语音角色的命令帮助'
|
||||
},
|
||||
{
|
||||
icon: 'help',
|
||||
title: '#chatgpt帮助',
|
||||
|
|
@ -296,15 +316,11 @@ export class help extends plugin {
|
|||
}
|
||||
|
||||
async help (e) {
|
||||
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 })
|
||||
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 }) }
|
||||
}
|
||||
|
||||
async newHelp (e) {
|
||||
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}})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { Config } from '../utils/config.js'
|
|||
import { exec } from 'child_process'
|
||||
import { checkPnpm, formatDuration, parseDuration, getPublicIP } from '../utils/common.js'
|
||||
import SydneyAIClient from '../utils/SydneyAIClient.js'
|
||||
|
||||
import { convertSpeaker, speakers } from '../utils/tts.js'
|
||||
let isWhiteList = true
|
||||
export class ChatgptManagement extends plugin {
|
||||
constructor (e) {
|
||||
super({
|
||||
|
|
@ -135,13 +136,38 @@ export class ChatgptManagement extends plugin {
|
|||
fnc: 'queryBingPromptPrefix',
|
||||
permission: 'master'
|
||||
},
|
||||
{
|
||||
reg: '^#chatgpt(打开|关闭|设置)?全局((图片模式|语音模式|(语音角色|角色语音|角色).*)|回复帮助)$',
|
||||
fnc: 'setDefaultReplySetting',
|
||||
permission: 'master'
|
||||
},
|
||||
{
|
||||
/** 命令正则匹配 */
|
||||
reg: '^#(关闭|打开)群聊上下文',
|
||||
reg: '^#(关闭|打开)群聊上下文$',
|
||||
/** 执行方法 */
|
||||
fnc: 'enableGroupContext',
|
||||
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: '^#(设置|修改)管理密码',
|
||||
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) {
|
||||
const re = /#(关闭|打开)/
|
||||
const match = e.msg.match(re)
|
||||
//logger.info(match)
|
||||
const reg = /(关闭|打开)/
|
||||
const match = e.msg.match(reg)
|
||||
if (match) {
|
||||
const action = match[1]
|
||||
if (action === '关闭') {
|
||||
Config.enableGroupContext = false // 关闭
|
||||
await this.reply('已关闭群聊上下文功能', true)
|
||||
await this.reply('已关闭群聊上下文功能', true)
|
||||
} else {
|
||||
Config.enableGroupContext = true // 打开
|
||||
await this.reply('已打开群聊上下文功能', true)
|
||||
await this.reply('已打开群聊上下文功能', true)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
await redis.set('CHATGPT:CONFIRM', 'on')
|
||||
await this.reply('已开启消息确认', true)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@
|
|||
"initiativeChatGroups": [],
|
||||
"enableDraw": true,
|
||||
"helloPrompt": "写一段话让大家来找我聊天。类似于“有人找我聊天吗?“这种风格,轻松随意一点控制在20个字以内",
|
||||
"helloInterval": 3,
|
||||
"helloProbability": 50,
|
||||
"chatglmBaseUrl": "http://localhost:8080",
|
||||
"allowOtherMode": true,
|
||||
"sydneyContext": "",
|
||||
|
|
@ -66,5 +68,9 @@
|
|||
"enableRobotAt": true,
|
||||
"maxNumUserMessagesInConversation": 20,
|
||||
"sydneyApologyIgnored": true,
|
||||
"enforceMaster": false
|
||||
"enforceMaster": false,
|
||||
"enablePrivateChat": false,
|
||||
"groupWhitelist": [],
|
||||
"groupBlacklist": [],
|
||||
"ttsRegex": "/匹配规则/匹配模式"
|
||||
}
|
||||
|
|
@ -38,12 +38,30 @@ export function supportGuoba () {
|
|||
bottomHelpMessage: '检查输入结果中是否有违禁词,如果存在黑名单中的违禁词则不输出。英文逗号隔开',
|
||||
component: 'InputTextArea'
|
||||
},
|
||||
{
|
||||
field: 'groupWhitelist',
|
||||
label: '群聊白名单',
|
||||
bottomHelpMessage: '设置后只有白名单内的群可以使用本插件。用英文逗号隔开',
|
||||
component: 'Input'
|
||||
},
|
||||
{
|
||||
field: 'groupBlacklist',
|
||||
label: '群聊黑名单',
|
||||
bottomHelpMessage: '设置后名单内的群禁止使用本插件。用英文逗号隔开',
|
||||
component: 'Input'
|
||||
},
|
||||
{},
|
||||
{
|
||||
field: 'imgOcr',
|
||||
label: '图片识别',
|
||||
bottomHelpMessage: '是否识别消息中图片的文字内容,需要同时包含图片和消息才生效',
|
||||
component: 'Switch'
|
||||
},
|
||||
{
|
||||
field: 'enablePrivateChat',
|
||||
label: '是否允许私聊机器人',
|
||||
component: 'Switch'
|
||||
},
|
||||
{
|
||||
field: 'defaultUsePicture',
|
||||
label: '全局图片模式',
|
||||
|
|
@ -65,6 +83,12 @@ export function supportGuoba () {
|
|||
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',
|
||||
label: '语音转文字阈值',
|
||||
|
|
@ -303,7 +327,7 @@ export function supportGuoba () {
|
|||
// component: 'InputTextArea'
|
||||
// },
|
||||
{
|
||||
field: 'groupContextLength',
|
||||
field: 'groupContextLength',
|
||||
label: '允许机器人读取近期的最多群聊聊天记录条数。',
|
||||
bottomHelpMessage: '允许机器人读取近期的最多群聊聊天记录条数。太多可能会超。默认50',
|
||||
component: 'InputNumber'
|
||||
|
|
@ -488,10 +512,29 @@ export function supportGuoba () {
|
|||
},
|
||||
{
|
||||
field: 'helloPrompt',
|
||||
label: '打招呼所说文字的引导文字',
|
||||
label: '打招呼prompt',
|
||||
bottomHelpMessage: '将会用这段文字询问ChatGPT,由ChatGPT给出随机的打招呼文字',
|
||||
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',
|
||||
label: '合成emoji的API地址,默认谷歌厨房',
|
||||
|
|
@ -511,7 +554,7 @@ export function supportGuoba () {
|
|||
field: 'serverPort',
|
||||
label: '系统Api服务端口',
|
||||
bottomHelpMessage: '系统Api服务开启的端口号,如需外网访问请将系统防火墙和服务器防火墙对应端口开放,修改后请重启',
|
||||
component: 'InputNumber',
|
||||
component: 'InputNumber'
|
||||
},
|
||||
{
|
||||
field: 'serverHost',
|
||||
|
|
@ -529,14 +572,14 @@ export function supportGuoba () {
|
|||
field: 'chatViewWidth',
|
||||
label: '图片渲染宽度',
|
||||
bottomHelpMessage: '聊天页面渲染窗口的宽度',
|
||||
component: 'InputNumber',
|
||||
component: 'InputNumber'
|
||||
},
|
||||
{
|
||||
field: 'chatViewBotName',
|
||||
label: 'Bot命名',
|
||||
bottomHelpMessage: '新渲染模式强制修改Bot命名',
|
||||
component: 'Input'
|
||||
},
|
||||
}
|
||||
],
|
||||
// 获取配置数据方法(用于前端填充显示数据)
|
||||
getConfigData () {
|
||||
|
|
@ -553,4 +596,4 @@ export function supportGuoba () {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import schedule from 'node-schedule'
|
|||
import { Config } from '../utils/config.js'
|
||||
import { randomString, getPublicIP } from '../utils/common.js'
|
||||
|
||||
|
||||
const __dirname = path.resolve()
|
||||
const server = fastify({
|
||||
logger: Config.debug
|
||||
|
|
@ -208,6 +209,7 @@ export async function createServer() {
|
|||
if(request.method == 'GET')
|
||||
Statistics.WebAccess.count += 1
|
||||
done()
|
||||
|
||||
})
|
||||
//定时任务
|
||||
var rule = new schedule.RecurrenceRule();
|
||||
|
|
@ -235,4 +237,4 @@ export async function createServer() {
|
|||
}
|
||||
server.log.info(`server listening on ${server.server.address().port}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -112,7 +112,7 @@ export async function pTimeout (
|
|||
const cancelablePromise = new Promise((resolve, reject) => {
|
||||
if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) {
|
||||
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 =
|
||||
typeof message === 'string'
|
||||
? message
|
||||
: `Promise timed out after ${milliseconds} milliseconds`
|
||||
typeof message === 'string'
|
||||
? message
|
||||
: `Promise timed out after ${milliseconds} milliseconds`
|
||||
const timeoutError =
|
||||
message instanceof Error ? message : new Error(errorMessage)
|
||||
message instanceof Error ? message : new Error(errorMessage)
|
||||
|
||||
if (typeof promise.cancel === 'function') {
|
||||
promise.cancel()
|
||||
|
|
@ -179,19 +179,19 @@ export async function pTimeout (
|
|||
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) {
|
||||
const reason =
|
||||
signal.reason === undefined
|
||||
? getDOMException('This operation was aborted.')
|
||||
: signal.reason
|
||||
signal.reason === undefined
|
||||
? getDOMException('This operation was aborted.')
|
||||
: signal.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) {
|
||||
return globalThis.DOMException === undefined
|
||||
? new Error(errorMessage)
|
||||
|
|
@ -239,18 +239,18 @@ export async function getMasterQQ () {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pluginKey plugin key
|
||||
* @param htmlPath html文件路径,相对于plugin resources目录
|
||||
* @param data 渲染数据
|
||||
* @param renderCfg 渲染配置
|
||||
* @param renderCfg.retType 返回值类型
|
||||
* * default/空:自动发送图片,返回true
|
||||
* * msgId:自动发送图片,返回msg id
|
||||
* * base64: 不自动发送图像,返回图像base64数据
|
||||
* @param renderCfg.beforeRender({data}) 可改写渲染的data数据
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
*
|
||||
* @param pluginKey plugin key
|
||||
* @param htmlPath html文件路径,相对于plugin resources目录
|
||||
* @param data 渲染数据
|
||||
* @param renderCfg 渲染配置
|
||||
* @param renderCfg.retType 返回值类型
|
||||
* * default/空:自动发送图片,返回true
|
||||
* * msgId:自动发送图片,返回msg id
|
||||
* * base64: 不自动发送图像,返回图像base64数据
|
||||
* @param renderCfg.beforeRender({data}) 可改写渲染的data数据
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function render (e, pluginKey, htmlPath, data = {}, renderCfg = {}) {
|
||||
// 处理传入的path
|
||||
htmlPath = htmlPath.replace(/.html$/, '')
|
||||
|
|
@ -316,7 +316,7 @@ export async function renderUrl (e, url, renderCfg = {}) {
|
|||
width: 1280,
|
||||
height: 720
|
||||
})
|
||||
let buff = base64 = await page.screenshot({fullPage:true})
|
||||
let buff = base64 = await page.screenshot({ fullPage: true })
|
||||
base64 = segment.image(buff)
|
||||
await page.close().catch((err) => logger.error(err))
|
||||
} catch (error) {
|
||||
|
|
@ -338,7 +338,7 @@ export async function renderUrl (e, url, renderCfg = {}) {
|
|||
return renderCfg.retType === 'msgId' ? ret : true
|
||||
}
|
||||
|
||||
export function getDefaultUserSetting () {
|
||||
export function getDefaultReplySetting () {
|
||||
return {
|
||||
usePicture: Config.defaultUsePicture,
|
||||
useTTS: Config.defaultUseTTS,
|
||||
|
|
@ -522,7 +522,7 @@ export function maskQQ (qq) {
|
|||
return newqq
|
||||
}
|
||||
|
||||
export function completeJSON(input) {
|
||||
export function completeJSON (input) {
|
||||
let result = {}
|
||||
|
||||
let inJson = false
|
||||
|
|
@ -533,7 +533,7 @@ export function completeJSON(input) {
|
|||
let tempValue = ''
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
// 获取当前字符
|
||||
let char = input[i];
|
||||
let char = input[i]
|
||||
// 获取到json头
|
||||
if (!inJson && char === '{') {
|
||||
inJson = true
|
||||
|
|
@ -566,7 +566,7 @@ export function completeJSON(input) {
|
|||
// 结束结构追加数据
|
||||
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
|
||||
inQuote = false
|
||||
|
|
@ -577,12 +577,12 @@ export function completeJSON(input) {
|
|||
}
|
||||
// 处理截断的json数据
|
||||
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
|
||||
}
|
||||
|
||||
export async function isImage(link) {
|
||||
export async function isImage (link) {
|
||||
try {
|
||||
let response = await fetch(link)
|
||||
let body = await response.arrayBuffer()
|
||||
|
|
@ -606,4 +606,4 @@ export async function getPublicIP() {
|
|||
return '127.0.0.1'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ const defaultConfig = {
|
|||
initiativeChatGroups: [],
|
||||
enableDraw: true,
|
||||
helloPrompt: '写一段话让大家来找我聊天。类似于“有人找我聊天吗?"这种风格,轻松随意一点控制在20个字以内',
|
||||
helloInterval: 3,
|
||||
helloProbability: 50,
|
||||
chatglmBaseUrl: 'http://localhost:8080',
|
||||
allowOtherMode: true,
|
||||
sydneyContext: '',
|
||||
|
|
@ -85,6 +87,10 @@ const defaultConfig = {
|
|||
viewHost: '',
|
||||
chatViewWidth: 1280,
|
||||
chatViewBotName: '',
|
||||
enablePrivateChat: false,
|
||||
groupWhitelist: [],
|
||||
groupBlacklist: [],
|
||||
ttsRegex: '/匹配规则/匹配模式',
|
||||
version: 'v2.5.1'
|
||||
}
|
||||
const _path = process.cwd()
|
||||
|
|
@ -150,4 +156,4 @@ export const Config = new Proxy(config, {
|
|||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue