mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-18 06:17:06 +00:00
Merge branch 'v2' of github.com:ikechan8370/chatgpt-plugin into v2
This commit is contained in:
commit
5b94b49536
7 changed files with 190 additions and 87 deletions
29
apps/chat.js
29
apps/chat.js
|
|
@ -26,8 +26,8 @@ import { convertSpeaker, generateAudio, speakers } from '../utils/tts.js'
|
||||||
import ChatGLMClient from '../utils/chatglm.js'
|
import ChatGLMClient from '../utils/chatglm.js'
|
||||||
import { convertFaces } from '../utils/face.js'
|
import { convertFaces } from '../utils/face.js'
|
||||||
import uploadRecord from '../utils/uploadRecord.js'
|
import uploadRecord from '../utils/uploadRecord.js'
|
||||||
import {SlackClaudeClient} from "../utils/slack/slackClient.js";
|
import { SlackClaudeClient } from "../utils/slack/slackClient.js"
|
||||||
import {getPromptByName} from "../utils/prompts.js";
|
import { ChatgptManagement } from './management.js'
|
||||||
try {
|
try {
|
||||||
await import('keyv')
|
await import('keyv')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -76,9 +76,9 @@ export class chatgpt extends plugin {
|
||||||
let toggleMode = Config.toggleMode
|
let toggleMode = Config.toggleMode
|
||||||
super({
|
super({
|
||||||
/** 功能名称 */
|
/** 功能名称 */
|
||||||
name: 'chatgpt',
|
name: 'ChatGpt 对话',
|
||||||
/** 功能描述 */
|
/** 功能描述 */
|
||||||
dsc: 'chatgpt from openai',
|
dsc: '与人工智能对话,畅聊无限可能~',
|
||||||
event: 'message',
|
event: 'message',
|
||||||
/** 优先级,数字越小等级越高 */
|
/** 优先级,数字越小等级越高 */
|
||||||
priority: 1144,
|
priority: 1144,
|
||||||
|
|
@ -549,15 +549,18 @@ export class chatgpt extends plugin {
|
||||||
*/
|
*/
|
||||||
async chatgpt (e) {
|
async chatgpt (e) {
|
||||||
if (!e.isMaster && e.isPrivate && !Config.enablePrivateChat) {
|
if (!e.isMaster && e.isPrivate && !Config.enablePrivateChat) {
|
||||||
this.reply('ChatGpt私聊通道已关闭。')
|
await this.reply('ChatGpt私聊通道已关闭。')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (e.isGroup) {
|
if (e.isGroup) {
|
||||||
const whitelist = Config.groupWhitelist.filter(group => group.trim())
|
let cm = new ChatgptManagement()
|
||||||
|
let [groupWhitelist, groupBlacklist] = await cm.processList(Config.groupWhitelist, Config.groupBlacklist)
|
||||||
|
// logger.info('groupWhitelist:', Config.groupWhitelist, 'groupBlacklist', Config.groupBlacklist)
|
||||||
|
const whitelist = groupWhitelist.filter(group => group.trim())
|
||||||
if (whitelist.length > 0 && !whitelist.includes(e.group_id.toString())) {
|
if (whitelist.length > 0 && !whitelist.includes(e.group_id.toString())) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const blacklist = Config.groupBlacklist.filter(group => group.trim())
|
const blacklist = groupBlacklist.filter(group => group.trim())
|
||||||
if (blacklist.length > 0 && blacklist.includes(e.group_id.toString())) {
|
if (blacklist.length > 0 && blacklist.includes(e.group_id.toString())) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -974,6 +977,10 @@ export class chatgpt extends plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
async chatgpt1 (e) {
|
async chatgpt1 (e) {
|
||||||
|
if (!e.isMaster && e.isPrivate && !Config.enablePrivateChat) {
|
||||||
|
await this.reply('ChatGpt私聊通道已关闭。')
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (!Config.allowOtherMode) {
|
if (!Config.allowOtherMode) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -993,6 +1000,10 @@ export class chatgpt extends plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
async chatgpt3 (e) {
|
async chatgpt3 (e) {
|
||||||
|
if (!e.isMaster && e.isPrivate && !Config.enablePrivateChat) {
|
||||||
|
await this.reply('ChatGpt私聊通道已关闭。')
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (!Config.allowOtherMode) {
|
if (!Config.allowOtherMode) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -1031,6 +1042,10 @@ export class chatgpt extends plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
async bing (e) {
|
async bing (e) {
|
||||||
|
if (!e.isMaster && e.isPrivate && !Config.enablePrivateChat) {
|
||||||
|
await this.reply('ChatGpt私聊通道已关闭。')
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (!Config.allowOtherMode) {
|
if (!Config.allowOtherMode) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export class Entertainment extends plugin {
|
||||||
constructor (e) {
|
constructor (e) {
|
||||||
super({
|
super({
|
||||||
name: 'ChatGPT-Plugin 娱乐小功能',
|
name: 'ChatGPT-Plugin 娱乐小功能',
|
||||||
dsc: 'ChatGPT-Plugin娱乐小功能',
|
dsc: '让你的聊天更有趣!现已支持主动打招呼和表情合成小功能!',
|
||||||
event: 'message',
|
event: 'message',
|
||||||
priority: 500,
|
priority: 500,
|
||||||
rule: [
|
rule: [
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,7 @@ export class help extends plugin {
|
||||||
constructor (e) {
|
constructor (e) {
|
||||||
super({
|
super({
|
||||||
name: 'ChatGPT-Plugin 帮助',
|
name: 'ChatGPT-Plugin 帮助',
|
||||||
dsc: 'ChatGPT-Plugin帮助',
|
dsc: 'ChatGPT-Plugin 帮助面板',
|
||||||
event: 'message',
|
event: 'message',
|
||||||
priority: 500,
|
priority: 500,
|
||||||
rule: [
|
rule: [
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,12 @@ export class history extends plugin {
|
||||||
constructor (e) {
|
constructor (e) {
|
||||||
super({
|
super({
|
||||||
name: 'ChatGPT-Plugin 聊天记录',
|
name: 'ChatGPT-Plugin 聊天记录',
|
||||||
dsc: 'ChatGPT-Plugin聊天记录提取',
|
dsc: '让你的聊天更加便捷!本插件支持以图片的形式导出本次对话的聊天记录,方便随时分享精彩瞬间!',
|
||||||
event: 'message',
|
event: 'message',
|
||||||
priority: 500,
|
priority: 500,
|
||||||
rule: [
|
rule: [
|
||||||
{
|
{
|
||||||
reg: '^#(chatgpt|ChatGPT)(导出)?聊天记录',
|
reg: '^#(chatgpt|ChatGPT)(导出)?聊天记录$',
|
||||||
fnc: 'history'
|
fnc: 'history'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ import { convertSpeaker, speakers } from '../utils/tts.js'
|
||||||
import md5 from 'md5'
|
import md5 from 'md5'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import loader from '../../../lib/plugins/loader.js'
|
||||||
let isWhiteList = true
|
let isWhiteList = true
|
||||||
export class ChatgptManagement extends plugin {
|
export class ChatgptManagement extends plugin {
|
||||||
constructor (e) {
|
constructor (e) {
|
||||||
super({
|
super({
|
||||||
name: 'ChatGPT-Plugin 管理',
|
name: 'ChatGPT-Plugin 管理',
|
||||||
dsc: 'ChatGPT-Plugin管理',
|
dsc: '插件的管理项配置,让你轻松掌控各个功能的开闭和管理。包含各种实用的配置选项,让你的聊天更加便捷和高效!',
|
||||||
event: 'message',
|
event: 'message',
|
||||||
priority: 500,
|
priority: 500,
|
||||||
rule: [
|
rule: [
|
||||||
|
|
@ -204,11 +204,88 @@ export class ChatgptManagement extends plugin {
|
||||||
{
|
{
|
||||||
reg: '^#chatgpt用户(设置|配置|管理)',
|
reg: '^#chatgpt用户(设置|配置|管理)',
|
||||||
fnc: 'userPage'
|
fnc: 'userPage'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reg: '^#chatgpt(对话|管理|娱乐|绘图|人物设定|聊天记录)?指令表(帮助)?',
|
||||||
|
fnc: 'commandHelp',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async commandHelp (e) {
|
||||||
|
if (!this.e.isMaster) { return this.reply('你没有权限') }
|
||||||
|
if (e.msg.trim() === '#chatgpt指令表帮助') {
|
||||||
|
await this.reply('#chatgpt指令表: 查看本插件的所有指令\n' +
|
||||||
|
'#chatgpt(对话|管理|娱乐|绘图|人物设定|聊天记录)指令表: 查看对应功能分类的指令表')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const categories = {
|
||||||
|
对话: '对话',
|
||||||
|
管理: '管理',
|
||||||
|
娱乐: '娱乐',
|
||||||
|
绘图: '绘图',
|
||||||
|
人物设定: '人物设定',
|
||||||
|
聊天记录: '聊天记录'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategory (e, plugin) {
|
||||||
|
for (const key in categories) {
|
||||||
|
if (e.msg.includes(key) && plugin.name.includes(categories[key])) {
|
||||||
|
return '功能名称: '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const commandSet = []
|
||||||
|
const plugins = await Promise.all(loader.priority.map(p => new p.class()))
|
||||||
|
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
const name = plugin.name
|
||||||
|
const rule = plugin.rule
|
||||||
|
if (/^chatgpt/i.test(name) && rule) {
|
||||||
|
commandSet.push({ name, dsc: plugin.dsc, rule })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatePrompt = (plugin, command) => {
|
||||||
|
const category = getCategory(e, plugin)
|
||||||
|
const commandsStr = command.length ? `正则指令:\n${command.join('\n')}\n` : '正则指令: 无\n'
|
||||||
|
const description = `功能介绍:${plugin.dsc}\n`
|
||||||
|
return `${category}${plugin.name}\n${description}${commandsStr}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompts = []
|
||||||
|
for (const plugin of commandSet) {
|
||||||
|
const commands = plugin.rule.map(v => v.reg.includes('[#*0-9]') ? '表情合成功能只需要发送两个emoji表情即可' : v.reg)
|
||||||
|
const category = getCategory(e, plugin)
|
||||||
|
if (category || (!e.msg.includes('对话') && !e.msg.includes('管理') && !e.msg.includes('娱乐') && !e.msg.includes('绘图') && !e.msg.includes('人物设定') && !e.msg.includes('聊天记录'))) {
|
||||||
|
prompts.push(generatePrompt(plugin, commands))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.reply(prompts.join('\n'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对原始黑白名单进行去重和去除无效群号处理
|
||||||
|
* @param whitelist
|
||||||
|
* @param blacklist
|
||||||
|
* @returns {Promise<any[][]>}
|
||||||
|
*/
|
||||||
|
async processList (whitelist, blacklist) {
|
||||||
|
let groupWhitelist = Array.isArray(whitelist)
|
||||||
|
? whitelist
|
||||||
|
: String(whitelist).split(/[,,]/)
|
||||||
|
let groupBlacklist = !Array.isArray(blacklist)
|
||||||
|
? blacklist
|
||||||
|
: String(blacklist).split(/[,,]/)
|
||||||
|
groupWhitelist = Array.from(new Set(groupWhitelist)).filter(value => /^[1-9]\d{8,9}$/.test(value))
|
||||||
|
groupBlacklist = Array.from(new Set(groupBlacklist)).filter(value => /^[1-9]\d{8,9}$/.test(value))
|
||||||
|
return [groupWhitelist, groupBlacklist]
|
||||||
|
}
|
||||||
|
|
||||||
async setList (e) {
|
async setList (e) {
|
||||||
this.setContext('saveList')
|
this.setContext('saveList')
|
||||||
isWhiteList = e.msg.includes('白')
|
isWhiteList = e.msg.includes('白')
|
||||||
|
|
@ -219,39 +296,45 @@ export class ChatgptManagement extends plugin {
|
||||||
|
|
||||||
async saveList (e) {
|
async saveList (e) {
|
||||||
if (!this.e.msg) return
|
if (!this.e.msg) return
|
||||||
const groupNums = this.e.msg.match(/\d+/g)
|
const listType = isWhiteList ? '白名单' : '黑名单'
|
||||||
const groupList = Array.isArray(groupNums) ? this.e.msg.match(/\d+/g).filter(value => /^[1-9]\d{8,9}/.test(value)) : []
|
const inputMatch = this.e.msg.match(/\d+/g)
|
||||||
if (!groupList.length) {
|
let [groupWhitelist, groupBlacklist] = await this.processList(Config.groupWhitelist, Config.groupBlacklist)
|
||||||
await this.reply('没有可添加的群号,请检查群号是否正确', e.isGroup)
|
let inputList = Array.isArray(inputMatch) ? this.e.msg.match(/\d+/g).filter(value => /^[1-9]\d{8,9}$/.test(value)) : []
|
||||||
|
if (!inputList.length) {
|
||||||
|
await this.reply('无效输入,请在检查群号是否正确后重新输入', e.isGroup)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
inputList = Array.from(new Set(inputList))
|
||||||
let whitelist = []
|
let whitelist = []
|
||||||
let blacklist = []
|
let blacklist = []
|
||||||
for (const element of groupList) {
|
for (const element of inputList) {
|
||||||
if (isWhiteList) {
|
if (listType === '白名单') {
|
||||||
Config.groupWhitelist = Config.groupWhitelist.filter(item => item !== element)
|
groupWhitelist = groupWhitelist.filter(item => item !== element)
|
||||||
whitelist.push(element)
|
whitelist.push(element)
|
||||||
} else {
|
} else {
|
||||||
Config.groupBlacklist = Config.groupBlacklist.filter(item => item !== element)
|
groupBlacklist = groupBlacklist.filter(item => item !== element)
|
||||||
blacklist.push(element)
|
blacklist.push(element)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!(whitelist.length || blacklist.length)) {
|
if (!(whitelist.length || blacklist.length)) {
|
||||||
await this.reply('没有可添加的群号,请检查群号是否正确或重复添加', e.isGroup)
|
await this.reply('无效输入,请在检查群号是否正确或重复添加后重新输入', e.isGroup)
|
||||||
this.finish('saveList')
|
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
if (isWhiteList) {
|
if (listType === '白名单') {
|
||||||
Config.groupWhitelist = Config.groupWhitelist
|
Config.groupWhitelist = groupWhitelist
|
||||||
.filter(group => group.trim() !== '')
|
.filter(group => group !== '')
|
||||||
.concat(whitelist)
|
.concat(whitelist)
|
||||||
} else {
|
} else {
|
||||||
Config.groupBlacklist = Config.groupBlacklist
|
Config.groupBlacklist = groupBlacklist
|
||||||
.filter(group => group.trim() !== '')
|
.filter(group => group !== '')
|
||||||
.concat(blacklist)
|
.concat(blacklist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.reply(`群聊${isWhiteList ? '白' : '黑'}名单已更新,可通过\n'#chatgpt查看群聊${isWhiteList ? '白' : '黑'}名单'查看最新名单\n#chatgpt移除群聊${isWhiteList ? '白' : '黑'}名单'管理名单`, e.isGroup)
|
let replyMsg = `群聊${listType}已更新,可通过\n'#chatgpt查看群聊${listType}'查看最新名单\n'#chatgpt移除群聊${listType}'管理名单`
|
||||||
|
if (e.isPrivate) {
|
||||||
|
replyMsg += `\n当前群聊${listType}为:${listType === '白名单' ? Config.groupWhitelist : Config.groupBlacklist}`
|
||||||
|
}
|
||||||
|
await this.reply(replyMsg, e.isGroup)
|
||||||
this.finish('saveList')
|
this.finish('saveList')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,8 +342,8 @@ export class ChatgptManagement extends plugin {
|
||||||
isWhiteList = e.msg.includes('白')
|
isWhiteList = e.msg.includes('白')
|
||||||
const list = isWhiteList ? Config.groupWhitelist : Config.groupBlacklist
|
const list = isWhiteList ? Config.groupWhitelist : Config.groupBlacklist
|
||||||
const listType = isWhiteList ? '白名单' : '黑名单'
|
const listType = isWhiteList ? '白名单' : '黑名单'
|
||||||
const replyMsg = list.length ? `当前群聊${listType}为:${list.join(',')}` : `当前没有设置任何${listType}`
|
const replyMsg = list.length ? `当前群聊${listType}为:${list}` : `当前没有设置任何群聊${listType}`
|
||||||
this.reply(replyMsg, e.isGroup)
|
await this.reply(replyMsg, e.isGroup)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,9 +351,9 @@ export class ChatgptManagement extends plugin {
|
||||||
isWhiteList = e.msg.includes('白')
|
isWhiteList = e.msg.includes('白')
|
||||||
const listType = isWhiteList ? '白名单' : '黑名单'
|
const listType = isWhiteList ? '白名单' : '黑名单'
|
||||||
let replyMsg = ''
|
let replyMsg = ''
|
||||||
if (Config.groupWhitelist.length && Config.groupBlacklist.length) {
|
if (Config.groupWhitelist.length === 0 && Config.groupBlacklist.length === 0) {
|
||||||
replyMsg = `当前群聊(白|黑)名单为空,请先添加${listType}吧~`
|
replyMsg = `当前群聊(白|黑)名单为空,请先添加${listType}吧~`
|
||||||
} else if ((isWhiteList && !Config.groupWhitelist.length) || (!isWhiteList && !Config.groupBlacklist.length)) {
|
} else if ((listType === '白名单' && !Config.groupWhitelist.length) || (listType === '黑名单' && !Config.groupBlacklist.length)) {
|
||||||
replyMsg = `当前群聊${listType}为空,请先添加吧~`
|
replyMsg = `当前群聊${listType}为空,请先添加吧~`
|
||||||
}
|
}
|
||||||
if (replyMsg) {
|
if (replyMsg) {
|
||||||
|
|
@ -286,27 +369,32 @@ export class ChatgptManagement extends plugin {
|
||||||
if (!this.e.msg) return
|
if (!this.e.msg) return
|
||||||
const isAllDeleted = this.e.msg.trim() === '全部删除'
|
const isAllDeleted = this.e.msg.trim() === '全部删除'
|
||||||
const groupNumRegex = /^[1-9]\d{8,9}$/
|
const groupNumRegex = /^[1-9]\d{8,9}$/
|
||||||
const groupNums = this.e.msg.match(/\d+/g)
|
const inputMatch = this.e.msg.match(/\d+/g)
|
||||||
const validGroups = Array.isArray(groupNums) ? groupNums.filter(groupNum => groupNumRegex.test(groupNum)) : []
|
const validGroups = Array.isArray(inputMatch) ? inputMatch.filter(groupNum => groupNumRegex.test(groupNum)) : []
|
||||||
|
let [groupWhitelist, groupBlacklist] = await this.processList(Config.groupWhitelist, Config.groupBlacklist)
|
||||||
if (isAllDeleted) {
|
if (isAllDeleted) {
|
||||||
Config.groupWhitelist = isWhiteList ? [] : Config.groupWhitelist
|
Config.groupWhitelist = isWhiteList ? [] : groupWhitelist
|
||||||
Config.groupBlacklist = !isWhiteList ? [] : Config.groupBlacklist
|
Config.groupBlacklist = !isWhiteList ? [] : groupBlacklist
|
||||||
} else {
|
} else {
|
||||||
if (!validGroups.length) {
|
if (!validGroups.length) {
|
||||||
await this.reply('没有可删除的群号,请检查输入的群号是否正确', e.isGroup)
|
await this.reply('无效输入,请在检查群号是否正确后重新输入', e.isGroup)
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
for (const element of validGroups) {
|
for (const element of validGroups) {
|
||||||
if (isWhiteList) {
|
if (isWhiteList) {
|
||||||
Config.groupWhitelist = Config.groupWhitelist.filter(item => item !== element)
|
Config.groupWhitelist = groupWhitelist.filter(item => item !== element)
|
||||||
} else {
|
} else {
|
||||||
Config.groupBlacklist = Config.groupBlacklist.filter(item => item !== element)
|
Config.groupBlacklist = groupBlacklist.filter(item => item !== element)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const groupType = isWhiteList ? '白' : '黑'
|
const listType = isWhiteList ? '白名单' : '黑名单'
|
||||||
await this.reply(`群聊${groupType}名单已更新,可通过'#chatgpt查看群聊${groupType}名单'命令查看最新名单`)
|
let replyMsg = `群聊${listType}已更新,可通过'#chatgpt查看群聊${listType}'命令查看最新名单`
|
||||||
|
if (e.isPrivate) {
|
||||||
|
replyMsg += `\n当前群聊${listType}为:${listType === '白名单' ? Config.groupWhitelist : Config.groupBlacklist}`
|
||||||
|
}
|
||||||
|
await this.reply(replyMsg, e.isGroup)
|
||||||
this.finish('confirmDelGroup')
|
this.finish('confirmDelGroup')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import { deleteOnePrompt, getPromptByName, readPrompts, saveOnePrompt } from '..
|
||||||
export class help extends plugin {
|
export class help extends plugin {
|
||||||
constructor (e) {
|
constructor (e) {
|
||||||
super({
|
super({
|
||||||
name: 'ChatGPT-Plugin 设定管理',
|
name: 'ChatGPT-Plugin 人物设定',
|
||||||
dsc: 'ChatGPT-Plugin 设定管理',
|
dsc: '让你的聊天更加有趣!本插件支持丰富的人物设定拓展,可以在线浏览并导入喜欢的设定和上传自己的设定。让你的聊天更加生动有趣!',
|
||||||
event: 'message',
|
event: 'message',
|
||||||
priority: 500,
|
priority: 500,
|
||||||
rule: [
|
rule: [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue