命令别名增强,可以在Web UI中进行设置,优化群聊学习

This commit is contained in:
CMHopeSunshine 2022-11-20 19:51:47 +08:00
parent a850fdbfc7
commit 512685a707
21 changed files with 362 additions and 525 deletions

View File

@ -1,2 +1,3 @@
from .config.manage import ConfigManager, ConfigModel, config from .config.manage import ConfigManager, ConfigModel, config
from .plugin.manage import PluginManager, HIDDEN_PLUGINS, MatcherInfo, PluginInfo from .plugin.manage import PluginManager, HIDDEN_PLUGINS, MatcherInfo, PluginInfo
from .command import handle_command_alias

View File

@ -0,0 +1,46 @@
import re
from typing import List
from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent
from nonebot.message import event_preprocessor
from tortoise.queryset import Q
from LittlePaimon.config import config
from LittlePaimon.database import CommandAlias, AliasMode
@event_preprocessor
async def handle_command_alias(event: MessageEvent):
if not config.command_alias_enable:
return
msgs = event.get_message()
if len(msgs) < 1 or msgs[0].type != 'text':
return
msg = str(msgs[0]).lstrip()
if not msg:
return
if isinstance(event, GroupMessageEvent):
filter_arg = Q(group_id=str(event.group_id)) | Q(group_id='all')
else:
filter_arg = Q(group_id='all')
all_alias = await CommandAlias.filter(filter_arg).order_by('priority')
new_msg = modify_msg(all_alias, msg)
event.message[0].data['text'] = new_msg
def combine_msg(new_command: str, extra_msg: str, is_reverse: bool):
return (new_command + extra_msg) if not is_reverse else (extra_msg + new_command)
def modify_msg(all_alias: List[CommandAlias], msg: str):
for alias in all_alias:
if alias.is_regex:
msg = re.sub(alias.alias, alias.command, msg)
else:
if alias.mode == AliasMode.prefix and msg.startswith(alias.alias):
msg = combine_msg(alias.command, msg[len(alias.alias):], alias.is_reverse)
elif alias.mode == AliasMode.suffix and msg.endswith(alias.alias):
msg = combine_msg(msg[:-len(alias.alias)], alias.command, alias.is_reverse)
elif alias.mode == AliasMode.full_match and msg == alias.alias:
msg = alias.command
return msg

View File

@ -45,6 +45,8 @@ class ConfigModel(BaseModel):
secret_key: str = Field('49c294d32f69b732ef6447c18379451ce1738922a75cd1d4812ef150318a2ed0', alias='Web端token密钥') secret_key: str = Field('49c294d32f69b732ef6447c18379451ce1738922a75cd1d4812ef150318a2ed0', alias='Web端token密钥')
admin_theme: Literal['default', 'antd', 'ang', 'dark'] = Field('default', alias='Web端主题') admin_theme: Literal['default', 'antd', 'ang', 'dark'] = Field('default', alias='Web端主题')
command_alias_enable: bool = Field(True, alias='启用命令别名')
@property @property
def alias_dict(self): def alias_dict(self):
return {v.alias: k for k, v in self.__fields__.items()} return {v.alias: k for k, v in self.__fields__.items()}

View File

@ -4,66 +4,75 @@ from pathlib import Path
from tortoise import Tortoise from tortoise import Tortoise
from nonebot.log import logger from nonebot.log import logger
from LittlePaimon.utils import scheduler, logger as my_logger from LittlePaimon.utils import scheduler, logger as my_logger
from LittlePaimon.utils.path import GENSHIN_DB_PATH, SUB_DB_PATH, GENSHIN_VOICE_DB_PATH, MANAGER_DB_PATH, YSC_TEMP_IMG_PATH from LittlePaimon.utils.path import GENSHIN_DB_PATH, SUB_DB_PATH, GENSHIN_VOICE_DB_PATH, MANAGER_DB_PATH, \
YSC_TEMP_IMG_PATH
from .models import * from .models import *
DATABASE = { DATABASE = {
"connections": { 'connections': {
"paimon_genshin": { 'paimon_genshin': {
"engine": "tortoise.backends.sqlite", 'engine': 'tortoise.backends.sqlite',
"credentials": {"file_path": GENSHIN_DB_PATH}, 'credentials': {'file_path': GENSHIN_DB_PATH},
}, },
"paimon_subscription": { 'paimon_subscription': {
"engine": "tortoise.backends.sqlite", 'engine': 'tortoise.backends.sqlite',
"credentials": {"file_path": SUB_DB_PATH}, 'credentials': {'file_path': SUB_DB_PATH},
}, },
'paimon_genshin_voice': { 'paimon_genshin_voice': {
"engine": "tortoise.backends.sqlite", 'engine': 'tortoise.backends.sqlite',
"credentials": {"file_path": GENSHIN_VOICE_DB_PATH}, 'credentials': {'file_path': GENSHIN_VOICE_DB_PATH},
}, },
'paimon_manager': { 'paimon_manage': {
"engine": "tortoise.backends.sqlite", 'engine': 'tortoise.backends.sqlite',
"credentials": {"file_path": MANAGER_DB_PATH}, 'credentials': {'file_path': MANAGER_DB_PATH},
}
}, },
"apps": { # 'memory_db': 'sqlite://:memory:'
"paimon_genshin": {
"models": ['LittlePaimon.database.models.player_info',
'LittlePaimon.database.models.abyss_info',
'LittlePaimon.database.models.character',
'LittlePaimon.database.models.cookie'],
"default_connection": "paimon_genshin",
}, },
"paimon_subscription": { 'apps': {
"models": ['LittlePaimon.database.models.subscription'], 'paimon_genshin': {
"default_connection": "paimon_subscription", 'models': [player_info.__name__,
abyss_info.__name__,
character.__name__,
cookie.__name__],
'default_connection': 'paimon_genshin',
}, },
"paimon_genshin_voice": { 'paimon_subscription': {
"models": ['LittlePaimon.database.models.genshin_voice'], 'models': [subscription.__name__],
"default_connection": "paimon_genshin_voice", 'default_connection': 'paimon_subscription',
}, },
"paimon_manager": { 'paimon_genshin_voice': {
"models": ['LittlePaimon.database.models.manager'], 'models': [genshin_voice.__name__],
"default_connection": "paimon_manager", 'default_connection': 'paimon_genshin_voice',
}
}, },
'paimon_manage': {
'models': [manage.__name__],
'default_connection': 'paimon_manage',
},
# 'memory_db': {
# 'models': [memory_db.__name__],
# 'default_connection': 'memory_db',
# }
},
'use_tz': False,
'timezone': 'Asia/Shanghai'
} }
def register_database(db_name: str, models: List[Union[str, Path]], db_path: Optional[Union[str, Path]]): def register_database(db_name: str, models: str, db_path: Optional[Union[str, Path]]):
""" """
注册数据库 注册数据库
""" """
if db_name in DATABASE['connections'] and db_name in DATABASE['apps']: if db_name in DATABASE['connections'] and db_name in DATABASE['apps']:
DATABASE['apps'][db_name]['models'].extend(models) DATABASE['apps'][db_name]['models'].append(models)
else: else:
DATABASE['connections'][db_name] = { DATABASE['connections'][db_name] = {
"engine": "tortoise.backends.sqlite", 'engine': 'tortoise.backends.sqlite',
"credentials": {"file_path": db_path}, 'credentials': {'file_path': db_path},
} }
DATABASE['apps'][db_name] = { DATABASE['apps'][db_name] = {
"models": models, 'models': [models],
"default_connection": db_name, 'default_connection': db_name,
} }
@ -74,9 +83,9 @@ async def connect():
try: try:
await Tortoise.init(DATABASE) await Tortoise.init(DATABASE)
await Tortoise.generate_schemas() await Tortoise.generate_schemas()
logger.opt(colors=True).success("<u><y>[数据库]</y></u><g>连接成功</g>") logger.opt(colors=True).success('<u><y>[数据库]</y></u><g>连接成功</g>')
except Exception as e: except Exception as e:
logger.opt(colors=True).warning(f"<u><y>[数据库]</y></u><r>连接失败:{e}</r>") logger.opt(colors=True).warning(f'<u><y>[数据库]</y></u><r>连接失败:{e}</r>')
raise e raise e
@ -85,7 +94,7 @@ async def disconnect():
断开数据库连接 断开数据库连接
""" """
await Tortoise.close_connections() await Tortoise.close_connections()
logger.opt(colors=True).success("<u><y>[数据库]</y></u><r>连接已断开</r>") logger.opt(colors=True).success('<u><y>[数据库]</y></u><r>连接已断开</r>')
@scheduler.scheduled_job('cron', hour=0, minute=0, misfire_grace_time=10) @scheduler.scheduled_job('cron', hour=0, minute=0, misfire_grace_time=10)
@ -112,3 +121,5 @@ async def daily_reset():
if YSC_TEMP_IMG_PATH.exists(): if YSC_TEMP_IMG_PATH.exists():
shutil.rmtree(YSC_TEMP_IMG_PATH) shutil.rmtree(YSC_TEMP_IMG_PATH)
YSC_TEMP_IMG_PATH.mkdir(parents=True, exist_ok=True) YSC_TEMP_IMG_PATH.mkdir(parents=True, exist_ok=True)
await MysAuthKey.filter()

View File

@ -3,6 +3,8 @@ from .cookie import *
from .player_info import * from .player_info import *
from .subscription import * from .subscription import *
from .genshin_voice import * from .genshin_voice import *
from .manager import * from .manage import *
from .abyss_info import * from .abyss_info import *
from .learning_chat import * # from .memory_db import *
from . import abyss_info, character, cookie, genshin_voice, manage, other, player_info, subscription

View File

@ -1,171 +0,0 @@
import time
from typing import List, Optional, Iterator
from pydantic import BaseModel
try:
import ujson as json
except ImportError:
import json
from tortoise import fields
from tortoise.models import Model
class BanWord(BaseModel):
keywords: str
group_id: int
reason: Optional[str]
time: Optional[int]
class BanWords(BaseModel):
bans: List[BanWord] = []
def __len__(self):
return len(self.bans)
def __getitem__(self, item):
return self.bans[item]
def __setitem__(self, key, value):
self.bans[key] = value
def __delitem__(self, key):
del self.bans[key]
def __iter__(self) -> Iterator[BanWord]:
return iter(self.bans)
def __reversed__(self):
return reversed(self.bans)
def append(self, ban: BanWord):
self.bans.append(ban)
def index(self, ban: BanWord) -> int:
return self.bans.index(ban)
# @staticmethod
# def tortoise_decoder(text: str) -> List["BanWord"]:
# print('ban_decoder', text)
# return [BanWord.parse_obj(item) for item in json.loads(text)]
#
# @staticmethod
# def tortoise_encoder(models: List["BanWord"]) -> str:
# print('ban_encoder', models)
# if not models:
# return ''
# elif isinstance(models[0], BanWord):
# return json.dumps([model.dict() for model in models])
class Answer(BaseModel):
keywords: str
group_id: int
count: int
time: int
messages: List[str]
# @staticmethod
# def tortoise_decoder(text: str) -> List["Answer"]:
# print('answer_decoder', text)
# return [Answer.parse_obj(item) for item in json.loads(text)]
#
# @staticmethod
# def tortoise_encoder(models: List["Answer"]) -> str:
# print('answer_encoder', models)
# if not models:
# return ''
# elif isinstance(models[0], BanWord):
# return json.dumps([model.dict() for model in models])
class Answers(BaseModel):
answers: List[Answer] = []
def __len__(self):
return len(self.answers)
def __getitem__(self, item):
return self.answers[item]
def __setitem__(self, key, value):
self.answers[key] = value
def __delitem__(self, key):
del self.answers[key]
def __iter__(self) -> Iterator[Answer]:
return iter(self.answers)
def __reversed__(self):
return reversed(self.answers)
def append(self, answer: Answer):
self.answers.append(answer)
def index(self, answer: Answer) -> int:
return self.answers.index(answer)
class Message(Model):
id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增主键"""
group_id: int = fields.IntField()
"""群id"""
user_id: int = fields.IntField()
"""用户id"""
raw_message: str = fields.TextField()
"""原始消息"""
is_plain_text: bool = fields.BooleanField()
"""是否为纯文本"""
plain_text: str = fields.TextField()
"""纯文本"""
keywords: str = fields.TextField()
"""关键词"""
time: int = fields.IntField()
"""时间戳"""
class Meta:
table = 'Message'
indexes = ('time',)
ordering = ['-time']
class Context(Model):
id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增主键"""
keywords: str = fields.TextField()
"""关键词"""
time: int = fields.IntField(default=int(time.time()))
"""时间戳"""
count: int = fields.IntField(default=1)
"""次数"""
answers: Answers = fields.JSONField(encoder=Answers.json, decoder=Answers.parse_raw, default=Answers())
"""答案列表"""
clear_time: Optional[int] = fields.IntField(null=True)
"""清除时间戳"""
ban: BanWords = fields.JSONField(encoder=BanWords.json, decoder=BanWords.parse_raw, default=BanWords())
"""禁用词列表"""
class Meta:
table = 'Context'
indexes = ('keywords', 'count', 'time')
ordering = ['-time', '-count']
class BlackList(Model):
id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增主键"""
group_id: int = fields.IntField()
"""群id"""
answers: List[str] = fields.JSONField(default=[])
"""答案"""
answers_reserve: List[str] = fields.JSONField(default=[])
"""备用答案"""
class Meta:
table = 'BlackList'
indexes = ('group_id',)

View File

@ -1,5 +1,6 @@
import datetime import datetime
from typing import List from typing import List
from enum import Enum
from tortoise import fields from tortoise import fields
from tortoise.models import Model from tortoise.models import Model
@ -43,3 +44,30 @@ class PluginStatistics(Model):
class Meta: class Meta:
table = 'plugin_statistics' table = 'plugin_statistics'
class AliasMode(Enum):
prefix: str = '前缀'
suffix: str = '后缀'
full_match: str = '全匹配'
class CommandAlias(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
command: str = fields.TextField()
"""目标命令"""
alias: str = fields.TextField()
"""命令别名"""
mode: AliasMode = fields.CharEnumField(AliasMode, max_length=10)
"""别名模式"""
is_regex: bool = fields.BooleanField(default=False)
"""是否为正则表达式"""
is_reverse: bool = fields.BooleanField(default=False)
"""是否反转"""
group_id: str = fields.CharField(max_length=30)
"""启用的群idall为全局启用"""
priority: int = fields.IntField(default=99)
"""优先级,数字越大优先级越高"""
class Meta:
table = 'command_alias'

View File

@ -0,0 +1,16 @@
import datetime
from tortoise import fields
from tortoise.models import Model
class MysAuthKey(Model):
id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
user_id: str = fields.TextField()
"""用户id"""
uid: str = fields.TextField()
"""原神uid"""
authkey: str = fields.TextField()
"""authkey"""
generate_time: datetime.datetime = fields.DatetimeField(auto_now_add=True)
"""生成时间"""

View File

@ -67,32 +67,40 @@ class LearningChat:
self.role = 'superuser' if event.user_id in SUPERUSERS else event.sender.role self.role = 'superuser' if event.user_id in SUPERUSERS else event.sender.role
self.config = config_manager.get_group_config(self.data.group_id) self.config = config_manager.get_group_config(self.data.group_id)
self.ban_users = set(chat_config.ban_users + self.config.ban_users) self.ban_users = set(chat_config.ban_users + self.config.ban_users)
self.ban_words = set(chat_config.ban_words + self.config.ban_words)
async def _learn(self) -> Result: async def _learn(self) -> Result:
# logger.debug('群聊学习', f'收到来自群<m>{self.data.group_id}</m>的消息<m>{self.data.message}</m>') # logger.debug('群聊学习', f'收到来自群<m>{self.data.group_id}</m>的消息<m>{self.data.message}</m>')
if not chat_config.total_enable or not self.config.enable or self.data.user_id in self.ban_users: if not chat_config.total_enable or not self.config.enable:
# 如果未开启群聊学习或者发言人在屏蔽列表中,跳过 logger.debug('群聊学习', f'➤该群<m>{self.data.group_id}</m>未开启群聊学习,跳过')
# 如果未开启群聊学习,跳过
return Result.Pass return Result.Pass
elif self.data.user_id in self.ban_users:
# 发言人在屏蔽列表中,跳过
logger.debug('群聊学习', f'➤发言人<m>{self.data.user_id}</m>在屏蔽列表中,跳过')
elif self.to_me and '不可以' in self.data.message: elif self.to_me and '不可以' in self.data.message:
# 如果是对某句话进行禁言 # 如果是对某句话进行禁言
return Result.Ban return Result.Ban
elif self.to_me and any(w in self.data.message for w in {'学说话', '快学', '开启学习', '闭嘴', '别学', '关闭学习'}): elif self.to_me and any(
w in self.data.message for w in {'学说话', '快学', '开启学习', '闭嘴', '别学', '关闭学习'}):
return Result.SetEnable return Result.SetEnable
elif not await self._check_allow(self.data): elif not await self._check_allow(self.data):
# 本消息不合法,跳过 # 本消息不合法,跳过
logger.debug('群聊学习', f'➤消息未通过校验,跳过')
return Result.Pass return Result.Pass
elif self.reply: elif self.reply:
# 如果是回复消息 # 如果是回复消息
if not (message := await ChatMessage.get_or_none(message_id=self.reply.message_id)): if not (message := await ChatMessage.get_or_none(message_id=self.reply.message_id)):
# 回复的消息在数据库中有记录 # 回复的消息在数据库中有记录
logger.debug('群聊学习', '是否学习:回复的消息不在数据库中,不学习') logger.debug('群聊学习', '回复的消息不在数据库中,跳过')
return Result.Pass return Result.Pass
if message.user_id in self.ban_users: if message.user_id in self.ban_users:
# 且回复的人不在屏蔽列表中 # 且回复的人不在屏蔽列表中
logger.debug('群聊学习', '➤回复的人在屏蔽列表中,跳过')
return Result.Pass return Result.Pass
if not await self._check_allow(message): if not await self._check_allow(message):
# 且回复的内容通过校验 # 且回复的内容通过校验
logger.debug('群聊学习', '是否学习:回复的消息未通过校验,不学习') logger.debug('群聊学习', '回复的消息未通过校验,跳过')
return Result.Pass return Result.Pass
# 则将该回复作为该消息的答案 # 则将该回复作为该消息的答案
await self._set_answer(message) await self._set_answer(message)
@ -102,7 +110,7 @@ class LearningChat:
# 获取本群一个小时内的最后5条消息 # 获取本群一个小时内的最后5条消息
if messages[0].message == self.data.message: if messages[0].message == self.data.message:
# 判断是否为复读中 # 判断是否为复读中
logger.debug('群聊学习', '是否学习:复读中,不学习') logger.debug('群聊学习', '复读中,跳过')
return Result.Repeat return Result.Repeat
for message in messages: for message in messages:
# 如果5条内有相关信息就作为该消息的答案 # 如果5条内有相关信息就作为该消息的答案
@ -114,7 +122,7 @@ class LearningChat:
# 如果没有相关信息 # 如果没有相关信息
if messages[0].user_id in self.ban_users or not await self._check_allow(messages[0]): if messages[0].user_id in self.ban_users or not await self._check_allow(messages[0]):
# 且最后一条消息的发送者不在屏蔽列表中并通过校验 # 且最后一条消息的发送者不在屏蔽列表中并通过校验
logger.debug('群聊学习', '是否学习:最后一条消息未通过校验,不学习') logger.debug('群聊学习', '最后一条消息未通过校验,跳过')
return Result.Pass return Result.Pass
# 则作为最后一条消息的答案 # 则作为最后一条消息的答案
await self._set_answer(messages[0]) await self._set_answer(messages[0])
@ -162,23 +170,24 @@ class LearningChat:
elif result == Result.Repeat and (messages := await ChatMessage.filter(group_id=self.data.group_id, elif result == Result.Repeat and (messages := await ChatMessage.filter(group_id=self.data.group_id,
time__gte=self.data.time - 3600).limit( time__gte=self.data.time - 3600).limit(
self.config.repeat_threshold)): self.config.repeat_threshold)):
# 如果达到阈值,进行复读 # 如果达到阈值,且bot没有回复过且不是全都为同一个人在说进行复读
if len(messages) >= self.config.repeat_threshold and all( if len(messages) >= self.config.repeat_threshold and all(
message.message == self.data.message and message.user_id != self.bot_id for message in message.message == self.data.message and message.user_id != self.bot_id
messages): for message in messages) and not all(
message.user_id == messages[0].user_id for message in messages):
if random.random() < self.config.break_probability: if random.random() < self.config.break_probability:
logger.debug('群聊学习', f'➤➤是否回复:达到复读阈值,打断复读!') logger.debug('群聊学习', f'➤➤达到复读阈值,打断复读!')
return [random.choice(BREAK_REPEAT_WORDS)] return [random.choice(BREAK_REPEAT_WORDS)]
else: else:
logger.debug('群聊学习', f'➤➤是否回复:达到复读阈值,复读<m>{messages[0].message}</m>') logger.debug('群聊学习', f'➤➤达到复读阈值,复读<m>{messages[0].message}</m>')
return [self.data.message] return [self.data.message]
else: else:
# 回复 # 回复
if self.data.is_plain_text and len(self.data.plain_text) <= 1: if self.data.is_plain_text and len(self.data.plain_text) <= 1:
logger.debug('群聊学习', '➤➤是否回复:消息过短,不回复') logger.debug('群聊学习', '➤➤消息过短,不回复')
return None return None
if not (context := await ChatContext.get_or_none(keywords=self.data.keywords)): if not (context := await ChatContext.get_or_none(keywords=self.data.keywords)):
logger.debug('群聊学习', '➤➤是否回复:尚未有已学习的回复,不回复') logger.debug('群聊学习', '➤➤尚未有已学习的回复,不回复')
return None return None
# 获取回复阈值 # 获取回复阈值
@ -195,7 +204,7 @@ class LearningChat:
else: else:
answer_count_threshold = 1 answer_count_threshold = 1
cross_group_threshold = 1 cross_group_threshold = 1
logger.debug('群聊学习', f'➤➤本次回复阈值为<m>{answer_count_threshold}</m>,跨群阈值为<m>{cross_group_threshold}</m>')
# 获取满足跨群条件的回复 # 获取满足跨群条件的回复
answers_cross = await ChatAnswer.filter(context=context, count__gte=answer_count_threshold, answers_cross = await ChatAnswer.filter(context=context, count__gte=answer_count_threshold,
keywords__in=await ChatAnswer.annotate( keywords__in=await ChatAnswer.annotate(
@ -215,7 +224,7 @@ class LearningChat:
# answer.count -= answer_count_threshold - 1 # answer.count -= answer_count_threshold - 1
candidate_answers.append(answer) candidate_answers.append(answer)
if not candidate_answers: if not candidate_answers:
logger.debug('群聊学习', '➤➤是否回复:没有符合条件的候选回复') logger.debug('群聊学习', '➤➤没有符合条件的候选回复')
return None return None
# 从候选回复中进行选择 # 从候选回复中进行选择
@ -225,13 +234,13 @@ class LearningChat:
per_list.append(1 - sum(per_list)) per_list.append(1 - sum(per_list))
answer_dict = tuple(zip(candidate_answers, per_list)) answer_dict = tuple(zip(candidate_answers, per_list))
logger.debug('群聊学习', logger.debug('群聊学习',
f'➤➤是否回复:候选回复有<m>{"|".join([f"""{a.keywords}({round(p, 3)})""" for a, p in answer_dict])}|不回复({round(per_list[-1], 3)})</m>') f'➤➤候选回复有<m>{"|".join([f"""{a.keywords}({round(p, 3)})""" for a, p in answer_dict])}|不回复({round(per_list[-1], 3)})</m>')
if (result := random.choices(candidate_answers + [None], weights=per_list)[0]) is None: if (result := random.choices(candidate_answers + [None], weights=per_list)[0]) is None:
logger.debug('群聊学习', '➤➤是否回复:但不进行回复') logger.debug('群聊学习', '➤➤但不进行回复')
return None return None
result_message = random.choice(result.messages) result_message = random.choice(result.messages)
logger.debug('群聊学习', f'➤➤是否回复:将回复<m>{result_message}</m>') logger.debug('群聊学习', f'➤➤将回复<m>{result_message}</m>')
return [result_message] return [result_message]
async def _ban(self, message_id: Optional[int] = None) -> bool: async def _ban(self, message_id: Optional[int] = None) -> bool:
@ -345,9 +354,13 @@ class LearningChat:
popularity: List[Tuple[int, List[ChatMessage]]] = sorted(total_messages.items(), popularity: List[Tuple[int, List[ChatMessage]]] = sorted(total_messages.items(),
key=cmp_to_key(group_popularity_cmp)) key=cmp_to_key(group_popularity_cmp))
logger.debug('群聊学习', f'主动发言:群热度排行<m>{">".join([str(g[0]) for g in popularity])}</m>') logger.debug('群聊学习', f'主动发言:群热度排行<m>{">>".join([str(g[0]) for g in popularity])}</m>')
for group_id, messages in popularity: for group_id, messages in popularity:
if len(messages) < 30:
continue
config = config_manager.get_group_config(group_id) config = config_manager.get_group_config(group_id)
ban_words = set(chat_config.ban_words + config.ban_words + ['[CQ:xml', '[CQ:json', '[CQ:at', '[CQ:video', '[CQ:record', '[CQ:share'])
# 是否开启了主动发言 # 是否开启了主动发言
if not config.speak_enable: if not config.speak_enable:
@ -378,6 +391,12 @@ class LearningChat:
weights=[answer.count + 1 if answer.time >= today_time else answer.count weights=[answer.count + 1 if answer.time >= today_time else answer.count
for answer in answers])[0] for answer in answers])[0]
message = random.choice(answer.messages) message = random.choice(answer.messages)
if len(message) < 2:
continue
if message.startswith('&#91;') and message.endswith('&#93;'):
continue
if any(word in message for word in ban_words):
continue
speak_list.append(message) speak_list.append(message)
while random.random() < config.speak_continuously_probability and len( while random.random() < config.speak_continuously_probability and len(
speak_list) < config.speak_continuously_max_len: speak_list) < config.speak_continuously_max_len:
@ -390,6 +409,12 @@ class LearningChat:
weights=[a.count + 1 if a.time >= today_time else a.count weights=[a.count + 1 if a.time >= today_time else a.count
for a in follow_answers])[0] for a in follow_answers])[0]
message = random.choice(answer.messages) message = random.choice(answer.messages)
if len(message) < 2:
continue
if message.startswith('&#91;') and message.endswith('&#93;'):
continue
if any(word in message for word in ban_words):
continue
speak_list.append(message) speak_list.append(message)
else: else:
break break
@ -439,18 +464,19 @@ class LearningChat:
async def _check_allow(self, message: Union[ChatMessage, ChatAnswer]) -> bool: async def _check_allow(self, message: Union[ChatMessage, ChatAnswer]) -> bool:
raw_message = message.message if isinstance(message, ChatMessage) else message.messages[0] raw_message = message.message if isinstance(message, ChatMessage) else message.messages[0]
keywords = message.keywords if len(raw_message) < 2:
return False
if any(i in raw_message for i in if any(i in raw_message for i in
{'[CQ:xml', '[CQ:json', '[CQ:at', '[CQ:video', '[CQ:record', '[CQ:share'}): {'[CQ:xml', '[CQ:json', '[CQ:at', '[CQ:video', '[CQ:record', '[CQ:share'}):
# logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>') # logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>')
return False return False
if any(i in raw_message for i in self.config.ban_words): if any(i in raw_message for i in self.ban_words):
# logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>') # logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>')
return False return False
if raw_message.startswith('&#91;') and raw_message.endswith('&#93;'): if raw_message.startswith('&#91;') and raw_message.endswith('&#93;'):
# logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>') # logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>')
return False return False
if ban_word := await ChatBlackList.get_or_none(keywords=keywords): if ban_word := await ChatBlackList.get_or_none(keywords=message.keywords):
if ban_word.global_ban or message.group_id in ban_word.ban_group_id: if ban_word.global_ban or message.group_id in ban_word.ban_group_id:
# logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>') # logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>')
return False return False

View File

@ -18,6 +18,7 @@ from tortoise.models import Model
from LittlePaimon.database import register_database from LittlePaimon.database import register_database
from LittlePaimon.utils.path import DATABASE_PATH from LittlePaimon.utils.path import DATABASE_PATH
from .config import config_manager from .config import config_manager
config = config_manager.config config = config_manager.config
JSON_DUMPS = functools.partial(json.dumps, ensure_ascii=False) JSON_DUMPS = functools.partial(json.dumps, ensure_ascii=False)
@ -125,4 +126,4 @@ class ChatBlackList(Model):
indexes = ('keywords',) indexes = ('keywords',)
register_database(db_name='LearningChat', models=['LittlePaimon.plugins.Learning_Chat.models'], db_path=DATABASE_PATH / 'LearningChat.db') register_database(db_name='LearningChat', models=__name__, db_path=DATABASE_PATH / 'LearningChat.db')

View File

@ -36,7 +36,7 @@ global_config_form = Form(
actions=[Action(label='保存', level=LevelEnum.success, type='submit'), actions=[Action(label='保存', level=LevelEnum.success, type='submit'),
Action(label='重置', level=LevelEnum.warning, type='reset')] Action(label='重置', level=LevelEnum.warning, type='reset')]
) )
group_select = Select(label='分群配置', name='group_id', source='/LittlePaimon/api/get_group_list', group_select = Select(label='分群配置', name='group_id', source='${group_list}',
placeholder='选择群') placeholder='选择群')
group_config_form = Form( group_config_form = Form(
title='分群配置', title='分群配置',
@ -106,9 +106,10 @@ blacklist_table = TableCRUD(mode='table',
api='delete:/LittlePaimon/api/delete_chat?type=blacklist&id=${id}') api='delete:/LittlePaimon/api/delete_chat?type=blacklist&id=${id}')
], ],
footable=True, footable=True,
columns=[TableColumn(type='tpl', tpl='${keywords|truncate:15}', label='内容/关键词', columns=[TableColumn(type='tpl', tpl='${keywords|truncate:20}', label='内容/关键词',
name='keywords', name='keywords',
searchable=True, popOver={'mode': 'dialog', 'title': '全文', searchable=True, popOver={'mode': 'dialog', 'title': '全文',
'className': 'break-all',
'body': {'type': 'tpl', 'body': {'type': 'tpl',
'tpl': '${keywords}'}}), 'tpl': '${keywords}'}}),
TableColumn(label='已禁用的群', name='bans', searchable=True), TableColumn(label='已禁用的群', name='bans', searchable=True),
@ -117,7 +118,7 @@ message_table = TableCRUD(mode='table',
title='', title='',
syncLocation=False, syncLocation=False,
api='/LittlePaimon/api/get_chat_messages', api='/LittlePaimon/api/get_chat_messages',
interval=6000, interval=12000,
headerToolbar=[ActionType.Ajax(label='删除所有聊天记录', headerToolbar=[ActionType.Ajax(label='删除所有聊天记录',
level=LevelEnum.warning, level=LevelEnum.warning,
confirmText='确定要删除所有聊天记录吗?', confirmText='确定要删除所有聊天记录吗?',
@ -135,8 +136,9 @@ message_table = TableCRUD(mode='table',
columns=[TableColumn(label='消息ID', name='message_id'), columns=[TableColumn(label='消息ID', name='message_id'),
TableColumn(label='群ID', name='group_id', searchable=True), TableColumn(label='群ID', name='group_id', searchable=True),
TableColumn(label='用户ID', name='user_id', searchable=True), TableColumn(label='用户ID', name='user_id', searchable=True),
TableColumn(type='tpl', tpl='${message|truncate:15}', label='消息', name='message', TableColumn(type='tpl', tpl='${raw_message|truncate:20}', label='消息', name='message',
searchable=True, popOver={'mode': 'dialog', 'title': '消息全文', searchable=True, popOver={'mode': 'dialog', 'title': '消息全文',
'className': 'break-all',
'body': {'type': 'tpl', 'body': {'type': 'tpl',
'tpl': '${raw_message}'}}), 'tpl': '${raw_message}'}}),
TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}', label='时间', TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}', label='时间',
@ -147,7 +149,7 @@ answer_table = TableCRUD(
syncLocation=False, syncLocation=False,
footable=True, footable=True,
api='/LittlePaimon/api/get_chat_answers', api='/LittlePaimon/api/get_chat_answers',
interval=6000, interval=12000,
headerToolbar=[ActionType.Ajax(label='删除所有已学习的回复', headerToolbar=[ActionType.Ajax(label='删除所有已学习的回复',
level=LevelEnum.warning, level=LevelEnum.warning,
confirmText='确定要删除所有已学习的回复吗?', confirmText='确定要删除所有已学习的回复吗?',
@ -162,8 +164,8 @@ answer_table = TableCRUD(
api='delete:/LittlePaimon/api/delete_chat?type=answer&id=${id}')], api='delete:/LittlePaimon/api/delete_chat?type=answer&id=${id}')],
columns=[TableColumn(label='ID', name='id', visible=False), columns=[TableColumn(label='ID', name='id', visible=False),
TableColumn(label='群ID', name='group_id', searchable=True), TableColumn(label='群ID', name='group_id', searchable=True),
TableColumn(type='tpl', tpl='${keywords|truncate:15}', label='内容/关键词', name='keywords', TableColumn(type='tpl', tpl='${keywords|truncate:20}', label='内容/关键词', name='keywords',
searchable=True, popOver={'mode': 'dialog', 'title': '内容全文', searchable=True, popOver={'mode': 'dialog', 'title': '内容全文', 'className': 'break-all',
'body': {'type': 'tpl', 'tpl': '${keywords}'}}), 'body': {'type': 'tpl', 'tpl': '${keywords}'}}),
TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}', label='最后学习时间', name='time', TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}', label='最后学习时间', name='time',
sortable=True), sortable=True),
@ -176,7 +178,7 @@ answer_table_on_context = TableCRUD(
syncLocation=False, syncLocation=False,
footable=True, footable=True,
api='/LittlePaimon/api/get_chat_answers?context_id=${id}&page=${page}&perPage=${perPage}&orderBy=${orderBy}&orderDir=${orderDir}', api='/LittlePaimon/api/get_chat_answers?context_id=${id}&page=${page}&perPage=${perPage}&orderBy=${orderBy}&orderDir=${orderDir}',
interval=6000, interval=12000,
headerToolbar=[ActionType.Ajax(label='删除该内容所有回复', headerToolbar=[ActionType.Ajax(label='删除该内容所有回复',
level=LevelEnum.warning, level=LevelEnum.warning,
confirmText='确定要删除该条内容已学习的回复吗?', confirmText='确定要删除该条内容已学习的回复吗?',
@ -191,8 +193,8 @@ answer_table_on_context = TableCRUD(
api='delete:/LittlePaimon/api/delete_chat?type=answer&id=${id}')], api='delete:/LittlePaimon/api/delete_chat?type=answer&id=${id}')],
columns=[TableColumn(label='ID', name='id', visible=False), columns=[TableColumn(label='ID', name='id', visible=False),
TableColumn(label='群ID', name='group_id'), TableColumn(label='群ID', name='group_id'),
TableColumn(type='tpl', tpl='${keywords|truncate:15}', label='内容/关键词', name='keywords', TableColumn(type='tpl', tpl='${keywords|truncate:20}', label='内容/关键词', name='keywords',
searchable=True, popOver={'mode': 'dialog', 'title': '内容全文', searchable=True, popOver={'mode': 'dialog', 'title': '内容全文', 'className': 'break-all',
'body': {'type': 'tpl', 'tpl': '${keywords}'}}), 'body': {'type': 'tpl', 'tpl': '${keywords}'}}),
TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}', label='最后学习时间', name='time', TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}', label='最后学习时间', name='time',
sortable=True), sortable=True),
@ -204,7 +206,7 @@ context_table = TableCRUD(mode='table',
title='', title='',
syncLocation=False, syncLocation=False,
api='/LittlePaimon/api/get_chat_contexts', api='/LittlePaimon/api/get_chat_contexts',
interval=6000, interval=12000,
headerToolbar=[ActionType.Ajax(label='删除所有学习内容', headerToolbar=[ActionType.Ajax(label='删除所有学习内容',
level=LevelEnum.warning, level=LevelEnum.warning,
confirmText='确定要删除所有已学习的内容吗?', confirmText='确定要删除所有已学习的内容吗?',
@ -225,9 +227,9 @@ context_table = TableCRUD(mode='table',
], ],
footable=True, footable=True,
columns=[TableColumn(label='ID', name='id', visible=False), columns=[TableColumn(label='ID', name='id', visible=False),
TableColumn(type='tpl', tpl='${keywords|truncate:15}', label='内容/关键词', TableColumn(type='tpl', tpl='${keywords|truncate:20}', label='内容/关键词',
name='keywords', searchable=True, name='keywords', searchable=True,
popOver={'mode': 'dialog', 'title': '内容全文', popOver={'mode': 'dialog', 'title': '内容全文', 'className': 'break-all',
'body': {'type': 'tpl', 'tpl': '${keywords}'}}), 'body': {'type': 'tpl', 'tpl': '${keywords}'}}),
TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}', TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}',
label='最后学习时间', name='time', sortable=True), label='最后学习时间', name='time', sortable=True),
@ -271,6 +273,6 @@ blacklist_page = PageSchema(url='/chat/blacklist', icon='fa fa-ban', label='禁
database_page = PageSchema(label='数据库', icon='fa fa-database', database_page = PageSchema(label='数据库', icon='fa fa-database',
children=[message_page, context_page, answer_page, blacklist_page]) children=[message_page, context_page, answer_page, blacklist_page])
config_page = PageSchema(url='/chat/configs', icon='fa fa-wrench', label='配置', config_page = PageSchema(url='/chat/configs', icon='fa fa-wrench', label='配置',
schema=Page(title='配置', body=[global_config_form, group_select, group_config_form])) schema=Page(title='配置', initApi='/LittlePaimon/api/get_group_list', body=[global_config_form, group_select, group_config_form]))
chat_page = PageSchema(label='群聊学习', icon='fa fa-wechat (alias)', children=[config_page, database_page]) chat_page = PageSchema(label='群聊学习', icon='fa fa-wechat (alias)', children=[config_page, database_page])
admin_app.pages[0].children.append(chat_page) admin_app.pages[0].children.append(chat_page)

View File

@ -1,127 +0,0 @@
from argparse import Namespace
from nonebot import on_shell_command
from nonebot.rule import ArgumentParser
from nonebot.plugin import PluginMetadata
from nonebot.params import ShellCommandArgs
from nonebot.adapters.onebot.v11 import Bot, MessageEvent, PrivateMessageEvent
from .handler import handle, get_id
from .alias_list import aliases
__plugin_meta__ = PluginMetadata(
name="命令别名",
description="为机器人指令创建别名",
usage=(
"添加别名alias {name}={command}\n"
"查看别名alias {name}\n"
"别名列表alias -p\n"
"删除别名unalias {name}\n"
"清空别名unalias -a"
),
extra={
"unique_name": "alias",
"example": "alias '喷水'='echo 呼风唤雨'\nunalias '喷水'",
"author": "meetwq <meetwq@gmail.com>",
"version": "0.3.2",
},
)
alias_parser = ArgumentParser()
alias_parser.add_argument("-p", "--print", action="store_true")
alias_parser.add_argument("-g", "--globally", action="store_true")
alias_parser.add_argument("names", nargs="*")
alias = on_shell_command("alias", parser=alias_parser, priority=10, block=True)
unalias_parser = ArgumentParser()
unalias_parser.add_argument("-a", "--all", action="store_true")
unalias_parser.add_argument("-g", "--globally", action="store_true")
unalias_parser.add_argument("names", nargs="*")
unalias = on_shell_command("unalias", parser=unalias_parser, priority=10)
@alias.handle()
async def _(bot: Bot, event: MessageEvent, args: Namespace = ShellCommandArgs()):
gl = args.globally
id = "global" if gl else get_id(event)
word = "全局别名" if gl else "别名"
if args.print:
message = "全局别名:" if gl else ""
alias_all = aliases.get_alias_all(id)
for name in sorted(alias_all):
message += f"\n{name}='{alias_all[name]}'"
if not gl:
alias_all_gl = aliases.get_alias_all("global")
if alias_all_gl:
message += "\n全局别名:"
for name in sorted(alias_all_gl):
message += f"\n{name}='{alias_all_gl[name]}'"
message = message.strip()
if message:
await alias.finish(message)
else:
await alias.finish(f"尚未添加任何{word}")
is_admin = event.sender.role in ["admin", "owner"]
is_superuser = str(event.user_id) in bot.config.superusers
is_private = isinstance(event, PrivateMessageEvent)
if gl and not is_superuser:
await alias.finish("管理全局别名需要超级用户权限!")
if not (is_admin or is_superuser or is_private):
await alias.finish("管理别名需要群管理员权限!")
message = ""
names = args.names
for name in names:
if "=" in name:
name, command = name.split("=", 1)
if name and command and aliases.add_alias(id, name, command):
message += f"成功添加{word}{name}='{command}'\n"
else:
command = aliases.get_alias(id, name)
if command:
message += f"{name}='{command}'\n"
else:
message += f"不存在的{word}{name}\n"
message = message.strip()
if message:
await alias.send(message)
@unalias.handle()
async def _(bot: Bot, event: MessageEvent, args: Namespace = ShellCommandArgs()):
gl = args.globally
id = "global" if gl else get_id(event)
word = "全局别名" if gl else "别名"
is_admin = event.sender.role in ["admin", "owner"]
is_superuser = str(event.user_id) in bot.config.superusers
is_private = isinstance(event, PrivateMessageEvent)
if gl and not is_superuser:
await alias.finish("管理全局别名需要超级用户权限!")
if not (is_admin or is_superuser or is_private):
await alias.finish("管理别名需要群管理员权限!")
if args.all and aliases.del_alias_all(id):
await unalias.finish(f"成功删除所有{word}")
message = ""
names = args.names
for name in names:
if aliases.get_alias(id, name):
if aliases.del_alias(id, name):
message += f"成功删除{word}{name}\n"
else:
message += f"不存在的{word}{name}\n"
message = message.strip()
if message:
await unalias.send(message)

View File

@ -1,61 +0,0 @@
import json
from pathlib import Path
data_path = Path("data/alias")
if not data_path.exists():
data_path.mkdir(parents=True)
class AliasList:
def __init__(self, path: Path):
self.path = path
self.list = self._load_alias()
def _load_alias(self) -> dict:
if self.path.exists():
return json.load(self.path.open("r", encoding="utf-8"))
else:
return {}
def _dump_alias(self) -> bool:
json.dump(
self.list,
self.path.open("w", encoding="utf-8"),
indent=4,
separators=(",", ": "),
ensure_ascii=False,
)
return True
def add_alias(self, id: str, name: str, command: str) -> bool:
if id not in self.list:
self.list[id] = {}
self.list[id][name] = command
return self._dump_alias()
def del_alias(self, id: str, name: str) -> bool:
if id not in self.list:
return False
self.list[id].pop(name, "")
if not self.list[id]:
self.list.pop(id, {})
return self._dump_alias()
def del_alias_all(self, id: str) -> bool:
self.list.pop(id, {})
return self._dump_alias()
def get_alias(self, id: str, name: str) -> str:
if id not in self.list:
return ""
if name not in self.list[id]:
return ""
return self.list[id][name]
def get_alias_all(self, id: str) -> dict:
if id not in self.list:
return {}
return self.list[id].copy()
aliases = AliasList(data_path / "aliases.json")

View File

@ -1,30 +0,0 @@
from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent
from nonebot.message import event_preprocessor
from .parser import parse_msg
@event_preprocessor
async def handle(event: MessageEvent):
msgs = event.get_message()
if len(msgs) < 1 or msgs[0].type != "text":
return
msg = str(msgs[0]).lstrip()
if not msg:
return
try:
msg = parse_msg(msg, get_id(event))
event.message[0].data["text"] = msg
except Exception:
return
def get_id(event: MessageEvent) -> str:
if event.message_type == 'group':
return 'group_' + str(event.group_id)
elif event.message_type == 'guild':
return 'guild_' + str(event.guild_id)
else:
return 'private_' + str(event.user_id)

View File

@ -1,48 +0,0 @@
import shlex
from expandvars import expand, ExpandvarsException
from .alias_list import aliases
def parse_msg(msg: str, id: str) -> str:
alias_all = aliases.get_alias_all(id)
alias_all_gl = aliases.get_alias_all("global")
alias_all_gl.update(alias_all)
for name in sorted(alias_all_gl, reverse=True):
if msg.startswith(name):
return replace_msg(name, msg, alias_all_gl[name])
return msg
def replace_msg(cmd: str, msg: str, alias: str) -> str:
if "$" not in alias:
return alias + msg[len(cmd):]
args = parse_args(cmd, msg)
env = set_env(args)
return parse_alias(alias, env)
def parse_args(cmd: str, msg: str) -> list:
if cmd.strip() == msg.strip():
return []
arg = msg[len(cmd):]
try:
return shlex.split(arg)
except ValueError:
return [arg]
def set_env(args: list) -> dict:
env = {}
for i, arg in enumerate(args, start=1):
env[str(i)] = arg
env["a"] = " ".join(args)
return env
def parse_alias(alias: str, env: dict = {}) -> str:
try:
return expand(alias, environ=env)
except ExpandvarsException:
return alias

View File

@ -6,6 +6,7 @@ from .login import route as login_route
from .plugin import route as plugin_route from .plugin import route as plugin_route
from .status import route as status_route from .status import route as status_route
from .utils import authentication from .utils import authentication
from .command_alias import route as command_alias_route
BaseApiRouter = APIRouter(prefix='/LittlePaimon/api') BaseApiRouter = APIRouter(prefix='/LittlePaimon/api')
@ -14,3 +15,4 @@ BaseApiRouter.include_router(plugin_route)
BaseApiRouter.include_router(bot_info_route) BaseApiRouter.include_router(bot_info_route)
BaseApiRouter.include_router(status_route) BaseApiRouter.include_router(status_route)
BaseApiRouter.include_router(login_route) BaseApiRouter.include_router(login_route)
BaseApiRouter.include_router(command_alias_route)

View File

@ -19,10 +19,20 @@ route = APIRouter()
@route.get('/get_group_list', response_class=JSONResponse, dependencies=[authentication()]) @route.get('/get_group_list', response_class=JSONResponse, dependencies=[authentication()])
async def get_group_list(): @cache(datetime.timedelta(minutes=3))
async def get_group_list(include_all: bool = False):
try: try:
group_list = await get_bot().get_group_list() group_list = await get_bot().get_group_list()
return [{'label': f'{group["group_name"]}({group["group_id"]})', 'value': group['group_id']} for group in group_list] group_list = [{'label': f'{group["group_name"]}({group["group_id"]})', 'value': group['group_id']} for group in group_list]
if include_all:
group_list.insert(0, {'label': '全局', 'value': 'all'})
return {
'status': 0,
'msg': 'ok',
'data': {
'group_list': group_list
}
}
except ValueError: except ValueError:
return { return {
'status': -100, 'status': -100,
@ -31,6 +41,7 @@ async def get_group_list():
@route.get('/get_group_members', response_class=JSONResponse, dependencies=[authentication()]) @route.get('/get_group_members', response_class=JSONResponse, dependencies=[authentication()])
@cache(datetime.timedelta(minutes=3))
async def get_group_members(group_id: int): async def get_group_members(group_id: int):
try: try:
return await get_bot().get_group_member_list(group_id=group_id) return await get_bot().get_group_member_list(group_id=group_id)
@ -42,7 +53,7 @@ async def get_group_members(group_id: int):
@route.get('/get_groups_and_members', response_class=JSONResponse, dependencies=[authentication()]) @route.get('/get_groups_and_members', response_class=JSONResponse, dependencies=[authentication()])
@cache(datetime.timedelta(minutes=10)) @cache(datetime.timedelta(minutes=3))
async def get_groups_and_members(): async def get_groups_and_members():
result = [] result = []
try: try:
@ -87,7 +98,7 @@ async def get_groups_and_members():
@route.get('/get_friend_list', response_class=JSONResponse, dependencies=[authentication()]) @route.get('/get_friend_list', response_class=JSONResponse, dependencies=[authentication()])
@cache(datetime.timedelta(minutes=10)) @cache(datetime.timedelta(minutes=3))
async def get_friend_list(): async def get_friend_list():
try: try:
bot: Bot = get_bot() bot: Bot = get_bot()

View File

@ -0,0 +1,55 @@
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from tortoise.queryset import Q
from LittlePaimon.database import CommandAlias
from LittlePaimon.config import ConfigManager
from LittlePaimon.config.command import modify_msg
from .utils import authentication
route = APIRouter()
@route.get('/command_alias', response_class=JSONResponse, dependencies=[authentication()])
async def get_command_alias():
alias = await CommandAlias.all().order_by('priority').values()
return {
'status': 0,
'msg': 'ok',
'data': {
'command_alias_enable': ConfigManager.config.command_alias_enable,
'items': alias,
}
}
@route.post('/command_alias', response_class=JSONResponse, dependencies=[authentication()])
async def add_command_alias(data: dict):
ConfigManager.config.update(command_alias_enable=data['command_alias_enable'])
ConfigManager.save()
data = data['items']
await CommandAlias.filter(id__not_in=[a['id'] for a in data if a.get('id')]).delete()
for alias in data:
alias['priority'] = data.index(alias)
if alias.get('id'):
await CommandAlias.filter(id=alias.pop('id')).update(**alias)
else:
await CommandAlias.create(**alias)
return {
'status': 0,
'msg': '命令别名保存成功'
}
@route.get('/test_command_alias', response_class=JSONResponse, dependencies=[authentication()])
async def test_command_alias(group_id: str, message: str):
all_alias = await CommandAlias.filter(Q(group_id=group_id) | Q(group_id='all')).order_by('priority')
msg = modify_msg(all_alias, message)
return {
'status': 0,
'msg': '测试成功',
'data': {
'new_msg': msg
}
}

View File

@ -0,0 +1,71 @@
from amis import Form, Switch, InputSubForm, Hidden, ButtonGroupSelect, InputText, Radios, Checkbox, Select, Static, Alert, Html, PageSchema, Page
main_form = Form(
title='命令别名',
initApi='get:/LittlePaimon/api/command_alias',
api='post:/LittlePaimon/api/command_alias',
submitText='保存',
body=[
Switch(name='command_alias_enable', label='功能开关', onText='全局启用', offText='全局关闭'),
InputSubForm(
name='items',
label='已设置的命令别名',
multiple=True,
btnLabel='${alias} >> ${command}',
draggable=True,
addable=True,
removable=True,
addButtonText='添加命令别名',
showErrorMsg=False,
form=Form(
title='命令别名',
body=[
Hidden(name='id'),
ButtonGroupSelect(name='is_regex', label='匹配模式', value=False,
options=[
{'label': '普通匹配', 'value': False},
{'label': '正则匹配', 'value': True}
]),
InputText(name='alias', label='命令别名', required=True),
InputText(name='command', label='原命令', required=True),
Radios(name='mode', label='匹配位置', value='前缀', hiddenOn='${is_regex == true}', required=True,
options=[
{
'label': '前缀',
'value': '前缀'
},
{
'label': '后缀',
'value': '后缀'
},
{
'label': '全匹配',
'value': '全匹配'
}
]),
Checkbox(name='is_reverse', label='是否反转', hiddenOn='${is_regex == true}'),
Select(name='group_id', label='设置群', value='all', required=True,
source='${group_list}')
]
)
)
]
)
test_form = Form(
title='测试',
api='get:/LittlePaimon/api/test_command_alias?group_id=${group_id}&message=${message}',
submitText='测试',
body=[
Select(name='group_id', label='测试群', value='all', required=True, source='${group_list}'),
InputText(name='message', label='测试消息', required=True),
Static(className='text-red-600', name='new_msg', label='命令别名修改后消息', visibleOn="typeof data.new_msg !== 'undefined'")
]
)
tips = Alert(level='info',
body=Html(html='命令别名的详细用法和配置例子可以在<a href="https://docs.paimon.cherishmoon.fun/configs/manage/plugin-manage.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%91%BD%E4%BB%A4%E5%88%AB%E5%90%8D" target="_blank">文档</a>中查看,配置保存后,可以在下方的"测试"一栏进行实时测试。'))
page = PageSchema(url='/bot_config/command_alias', icon='fa fa-angle-double-right', label='命令别名',
schema=Page(title='', initApi='/LittlePaimon/api/get_group_list?include_all=true', body=[tips, main_form, test_form]))

View File

@ -5,6 +5,7 @@ from .home_page import page as home_page
from .plugin_manage import page as plugin_manage_page from .plugin_manage import page as plugin_manage_page
from .private_cookie import page as private_cookie_page from .private_cookie import page as private_cookie_page
from .public_cookie import page as public_cookie_page from .public_cookie import page as public_cookie_page
from .command_alias import page as command_alias_page
github_logo = Tpl(className='w-full', github_logo = Tpl(className='w-full',
@ -21,7 +22,7 @@ admin_app = App(brandName='LittlePaimon',
PageSchema(label='Cookie管理', icon='fa fa-key', PageSchema(label='Cookie管理', icon='fa fa-key',
children=[public_cookie_page, private_cookie_page]), children=[public_cookie_page, private_cookie_page]),
PageSchema(label='机器人配置', icon='fa fa-wrench', PageSchema(label='机器人配置', icon='fa fa-wrench',
children=[plugin_manage_page, config_page]) children=[plugin_manage_page, config_page, command_alias_page])
]}], ]}],
footer=f'<div class="p-2 text-center bg-blue-100">Copyright © 2021 - 2022 <a href="https://github.com/CMHopeSunshine/LittlePaimon" target="_blank" class="link-secondary">LittlePaimon v{__version__}</a> X<a target="_blank" href="https://github.com/baidu/amis" class="link-secondary" rel="noopener"> amis v2.2.0</a></div>') footer=f'<div class="p-2 text-center bg-blue-100">Copyright © 2021 - 2022 <a href="https://github.com/CMHopeSunshine/LittlePaimon" target="_blank" class="link-secondary">LittlePaimon v{__version__}</a> X<a target="_blank" href="https://github.com/baidu/amis" class="link-secondary" rel="noopener"> amis v2.2.0</a></div>')

View File

@ -121,12 +121,11 @@ cards_curd = CardsCRUD(mode='cards',
syncLocation=False, syncLocation=False,
api='/LittlePaimon/api/get_plugins', api='/LittlePaimon/api/get_plugins',
loadDataOnce=True, loadDataOnce=True,
source='${rows | filter:name:match:keywords_name | filter:description:match:keywords_description | filter:status:match:status}', source='${rows | filter:name:match:keywords_name | filter:description:match:keywords_description}',
filter={ filter={
'body': [ 'body': [
InputText(name='keywords_name', label='插件名'), InputText(name='keywords_name', label='插件名'),
InputText(name='keywords_description', label='插件描述'), InputText(name='keywords_description', label='插件描述')
Switch(name='status', label='插件状态', onText='启用', offText='禁用')
] ]
}, },
perPage=12, perPage=12,