🎨 格式化代码,适配新amis

This commit is contained in:
CMHopeSunshine 2023-06-10 17:22:43 +08:00
parent 7dad2158d7
commit ebe4b67e6c
6 changed files with 1243 additions and 634 deletions

View File

@ -16,51 +16,63 @@ from .config import config_manager
from . import web_api, web_page
__plugin_meta__ = PluginMetadata(
name='群聊学习',
description='群聊学习',
usage='群聊学习',
name="群聊学习",
description="群聊学习",
usage="群聊学习",
extra={
'author': '惜月',
'priority': 16,
}
"author": "惜月",
"priority": 16,
},
)
async def ChatRule(event: GroupMessageEvent, state: T_State) -> bool:
if answers := await LearningChat(event).answer():
state['answers'] = answers
state["answers"] = answers
return True
return False
learning_chat = on_message(priority=99, block=False, rule=Rule(ChatRule), permission=GROUP, state={
'pm_name': '群聊学习',
'pm_description': '(被动技能)bot会学习群友们的发言',
'pm_usage': '群聊学习',
'pm_priority': 1
})
learning_chat = on_message(
priority=99,
block=False,
rule=Rule(ChatRule),
permission=GROUP,
state={
"pm_name": "群聊学习",
"pm_description": "(被动技能)bot会学习群友们的发言",
"pm_usage": "群聊学习",
"pm_priority": 1,
},
)
@learning_chat.handle()
async def _(event: GroupMessageEvent, answers=Arg('answers')):
async def _(event: GroupMessageEvent, answers=Arg("answers")):
for answer in answers:
try:
logger.info('群聊学习', f'{NICKNAME}将向群<m>{event.group_id}</m>回复<m>"{answer}"</m>')
logger.info(
"群聊学习", f'{NICKNAME}将向群<m>{event.group_id}</m>回复<m>"{answer}"</m>'
)
msg = await learning_chat.send(Message(answer))
await ChatMessage.create(group_id=event.group_id,
user_id=event.self_id,
message_id=msg['message_id'],
message=answer,
raw_message=answer,
time=int(time.time()),
plain_text=Message(answer).extract_plain_text())
await ChatMessage.create(
group_id=event.group_id,
user_id=event.self_id,
message_id=msg["message_id"],
message=answer,
raw_message=answer,
time=int(time.time()),
plain_text=Message(answer).extract_plain_text(),
)
await asyncio.sleep(random.random() + 0.5)
except ActionFailed:
logger.info('群聊学习', f'{NICKNAME}向群<m>{event.group_id}</m>的回复<m>"{answer}"</m>发送<r>失败,可能处于风控中</r>')
logger.info(
"群聊学习",
f'{NICKNAME}向群<m>{event.group_id}</m>的回复<m>"{answer}"</m>发送<r>失败,可能处于风控中</r>',
)
@scheduler.scheduled_job('interval', minutes=3, misfire_grace_time=5)
@scheduler.scheduled_job("interval", minutes=3, misfire_grace_time=5)
async def speak_up():
if not config_manager.config.total_enable:
return
@ -73,15 +85,22 @@ async def speak_up():
group_id, messages = speak
for msg in messages:
try:
logger.info('群聊学习', f'{NICKNAME}向群<m>{group_id}</m>主动发言<m>"{msg}"</m>')
send_result = await bot.send_group_msg(group_id=group_id, message=Message(msg))
await ChatMessage.create(group_id=group_id,
user_id=int(bot.self_id),
message_id=send_result['message_id'],
message=msg,
raw_message=msg,
time=int(time.time()),
plain_text=Message(msg).extract_plain_text())
logger.info("群聊学习", f'{NICKNAME}向群<m>{group_id}</m>主动发言<m>"{msg}"</m>')
send_result = await bot.send_group_msg(
group_id=group_id, message=Message(msg)
)
await ChatMessage.create(
group_id=group_id,
user_id=int(bot.self_id),
message_id=send_result["message_id"],
message=msg,
raw_message=msg,
time=int(time.time()),
plain_text=Message(msg).extract_plain_text(),
)
await asyncio.sleep(random.randint(2, 4))
except ActionFailed:
logger.info('群聊学习', f'{NICKNAME}向群<m>{group_id}</m>主动发言<m>"{msg}"</m><r>发送失败,可能处于风控中</r>')
logger.info(
"群聊学习",
f'{NICKNAME}向群<m>{group_id}</m>主动发言<m>"{msg}"</m><r>发送失败,可能处于风控中</r>',
)

View File

@ -7,19 +7,19 @@ from LittlePaimon.utils.files import load_yaml, save_yaml
class ChatGroupConfig(BaseModel):
enable: bool = Field(True, alias='群聊学习开关')
ban_words: List[str] = Field([], alias='屏蔽词')
ban_users: List[int] = Field([], alias='屏蔽用户')
answer_threshold: int = Field(4, alias='回复阈值')
answer_threshold_weights: List[int] = Field([10, 30, 60], alias='回复阈值权重')
repeat_threshold: int = Field(3, alias='复读阈值')
break_probability: float = Field(0.25, alias='打断复读概率')
speak_enable: bool = Field(True, alias='主动发言开关')
speak_threshold: int = Field(5, alias='主动发言阈值')
speak_min_interval: int = Field(300, alias='主动发言最小间隔')
speak_continuously_probability: float = Field(0.5, alias='连续主动发言概率')
speak_continuously_max_len: int = Field(3, alias='最大连续主动发言句数')
speak_poke_probability: float = Field(0.5, alias='主动发言附带戳一戳概率')
enable: bool = Field(default=True, alias="群聊学习开关")
ban_words: List[str] = Field(default_factory=list, alias="屏蔽词")
ban_users: List[int] = Field(default_factory=list, alias="屏蔽用户")
answer_threshold: int = Field(default=4, alias="回复阈值")
answer_threshold_weights: List[int] = Field(default=[10, 30, 60], alias="回复阈值权重")
repeat_threshold: int = Field(default=3, alias="复读阈值")
break_probability: float = Field(default=0.25, alias="打断复读概率")
speak_enable: bool = Field(default=True, alias="主动发言开关")
speak_threshold: int = Field(default=5, alias="主动发言阈值")
speak_min_interval: int = Field(default=300, alias="主动发言最小间隔")
speak_continuously_probability: float = Field(default=0.5, alias="连续主动发言概率")
speak_continuously_max_len: int = Field(default=3, alias="最大连续主动发言句数")
speak_poke_probability: float = Field(default=0.5, alias="主动发言附带戳一戳概率")
def update(self, **kwargs):
for key, value in kwargs.items():
@ -28,14 +28,14 @@ class ChatGroupConfig(BaseModel):
class ChatConfig(BaseModel):
total_enable: bool = Field(True, alias='群聊学习总开关')
ban_words: List[str] = Field([], alias='全局屏蔽词')
ban_users: List[int] = Field([], alias='全局屏蔽用户')
KEYWORDS_SIZE: int = Field(3, alias='单句关键词分词数量')
cross_group_threshold: int = Field(3, alias='跨群回复阈值')
learn_max_count: int = Field(6, alias='最高学习次数')
dictionary: List[str] = Field([], alias='自定义词典')
group_config: Dict[int, ChatGroupConfig] = Field({}, alias='分群配置')
total_enable: bool = Field(default=True, alias="群聊学习总开关")
ban_words: List[str] = Field(default_factory=list, alias="全局屏蔽词")
ban_users: List[int] = Field(default_factory=list, alias="全局屏蔽用户")
KEYWORDS_SIZE: int = Field(default=3, alias="单句关键词分词数量")
cross_group_threshold: int = Field(default=3, alias="跨群回复阈值")
learn_max_count: int = Field(default=6, alias="最高学习次数")
dictionary: List[str] = Field(default_factory=list, alias="自定义词典")
group_config: Dict[int, ChatGroupConfig] = Field(default_factory=dict, alias="分群配置")
def update(self, **kwargs):
for key, value in kwargs.items():
@ -44,7 +44,6 @@ class ChatConfig(BaseModel):
class ChatConfigManager:
def __init__(self):
self.file_path = LEARNING_CHAT_CONFIG
if self.file_path.exists():

View File

@ -21,16 +21,27 @@ from .config import config_manager
chat_config = config_manager.config
command_start_ = command_start.copy()
if '' in command_start_:
command_start_.remove('')
if "" in command_start_:
command_start_.remove("")
NO_PERMISSION_WORDS = [f'{NICKNAME}就喜欢说这个,哼!', f'你管得着{NICKNAME}吗!']
ENABLE_WORDS = [f'{NICKNAME}会尝试学你们说怪话!', f'好的呢,让{NICKNAME}学学你们的说话方式~']
DISABLE_WORDS = [f'好好好,{NICKNAME}不学说话就是了!', f'果面呐噻,{NICKNAME}以后不学了...']
SORRY_WORDS = [f'{NICKNAME}知道错了...达咩!', f'{NICKNAME}不会再这么说了...', f'果面呐噻,{NICKNAME}说错话了...']
DOUBT_WORDS = [f'{NICKNAME}有说什么奇怪的话吗?']
BREAK_REPEAT_WORDS = ['打断复读', '打断!']
ALL_WORDS = NO_PERMISSION_WORDS + SORRY_WORDS + DOUBT_WORDS + ENABLE_WORDS + DISABLE_WORDS + BREAK_REPEAT_WORDS
NO_PERMISSION_WORDS = [f"{NICKNAME}就喜欢说这个,哼!", f"你管得着{NICKNAME}吗!"]
ENABLE_WORDS = [f"{NICKNAME}会尝试学你们说怪话!", f"好的呢,让{NICKNAME}学学你们的说话方式~"]
DISABLE_WORDS = [f"好好好,{NICKNAME}不学说话就是了!", f"果面呐噻,{NICKNAME}以后不学了..."]
SORRY_WORDS = [
f"{NICKNAME}知道错了...达咩!",
f"{NICKNAME}不会再这么说了...",
f"果面呐噻,{NICKNAME}说错话了...",
]
DOUBT_WORDS = [f"{NICKNAME}有说什么奇怪的话吗?"]
BREAK_REPEAT_WORDS = ["打断复读", "打断!"]
ALL_WORDS = (
NO_PERMISSION_WORDS
+ SORRY_WORDS
+ DOUBT_WORDS
+ ENABLE_WORDS
+ DISABLE_WORDS
+ BREAK_REPEAT_WORDS
)
class Result(IntEnum):
@ -50,92 +61,106 @@ class LearningChat:
group_id=event.group_id,
user_id=event.user_id,
message_id=event.message_id,
message=re.sub(r'(\[CQ:at,qq=.+])|(\[CQ:reply,id=.+])', '',
re.sub(r'(,subType=\d+,url=.+])', r']', event.raw_message)).strip(),
message=re.sub(
r"(\[CQ:at,qq=.+])|(\[CQ:reply,id=.+])",
"",
re.sub(r"(,subType=\d+,url=.+])", r"]", event.raw_message),
).strip(),
raw_message=event.raw_message,
plain_text=event.get_plaintext(),
time=event.time
time=event.time,
)
else:
self.data = ChatMessage(
group_id=event.group_id,
user_id=event.user_id,
message_id=event.message_id,
message=re.sub(r'(\[CQ:at,qq=.+])', '',
re.sub(r'(,subType=\d+,url=.+])', r']', event.raw_message)).strip(),
message=re.sub(
r"(\[CQ:at,qq=.+])",
"",
re.sub(r"(,subType=\d+,url=.+])", r"]", event.raw_message),
).strip(),
raw_message=event.raw_message,
plain_text=event.get_plaintext(),
time=event.time
time=event.time,
)
self.reply = None
self.bot_id = event.self_id
self.to_me = event.to_me or NICKNAME in self.data.message
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.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:
if self.to_me and any(
w in self.data.message for w in {'学说话', '快学', '开启学习'}):
if self.to_me and any(w in self.data.message for w in {"学说话", "快学", "开启学习"}):
return Result.SetEnable
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.SetDisable
elif not chat_config.total_enable or not self.config.enable:
logger.debug('群聊学习', f'➤该群<m>{self.data.group_id}</m>未开启群聊学习,跳过')
logger.debug("群聊学习", f"➤该群<m>{self.data.group_id}</m>未开启群聊学习,跳过")
# 如果未开启群聊学习,跳过
return Result.Pass
elif command_start_ and self.data.message.startswith(tuple(command_start_)):
# 以命令前缀开头的消息,跳过
logger.debug('群聊学习', '➤该消息以命令前缀开头,跳过')
logger.debug("群聊学习", "➤该消息以命令前缀开头,跳过")
return Result.Pass
elif self.data.user_id in self.ban_users:
# 发言人在屏蔽列表中,跳过
logger.debug('群聊学习', f'➤发言人<m>{self.data.user_id}</m>在屏蔽列表中,跳过')
logger.debug("群聊学习", f"➤发言人<m>{self.data.user_id}</m>在屏蔽列表中,跳过")
return Result.Pass
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.Ban
elif not await self._check_allow(self.data):
# 本消息不合法,跳过
logger.debug('群聊学习', '➤消息未通过校验,跳过')
logger.debug("群聊学习", "➤消息未通过校验,跳过")
return Result.Pass
elif self.reply:
# 如果是回复消息
if not (message := await ChatMessage.filter(message_id=self.reply.message_id).first()):
if not (
message := await ChatMessage.filter(
message_id=self.reply.message_id
).first()
):
# 回复的消息在数据库中有记录
logger.debug('群聊学习', '➤回复的消息不在数据库中,跳过')
logger.debug("群聊学习", "➤回复的消息不在数据库中,跳过")
return Result.Pass
if message.user_id in self.ban_users:
# 且回复的人不在屏蔽列表中
logger.debug('群聊学习', '➤回复的人在屏蔽列表中,跳过')
logger.debug("群聊学习", "➤回复的人在屏蔽列表中,跳过")
return Result.Pass
if not await self._check_allow(message):
# 且回复的内容通过校验
logger.debug('群聊学习', '➤回复的消息未通过校验,跳过')
logger.debug("群聊学习", "➤回复的消息未通过校验,跳过")
return Result.Pass
# 则将该回复作为该消息的答案
await self._set_answer(message)
return Result.Learn
elif messages := await ChatMessage.filter(group_id=self.data.group_id, time__gte=self.data.time - 3600).limit(
5):
elif messages := await ChatMessage.filter(
group_id=self.data.group_id, time__gte=self.data.time - 3600
).limit(5):
# 获取本群一个小时内的最后5条消息
if messages[0].message == self.data.message:
# 判断是否为复读中
logger.debug('群聊学习', '➤复读中,跳过')
logger.debug("群聊学习", "➤复读中,跳过")
return Result.Repeat
for message in messages:
# 如果5条内有相关信息就作为该消息的答案
if message.user_id not in self.ban_users and set(self.data.keyword_list) & set(
message.keyword_list) and self.data.keyword_list != message.keyword_list and await self._check_allow(
message):
if (
message.user_id not in self.ban_users
and set(self.data.keyword_list) & set(message.keyword_list)
and self.data.keyword_list != message.keyword_list
and await self._check_allow(message)
):
await self._set_answer(message)
return Result.Learn
# 如果没有相关信息
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
# 则作为最后一条消息的答案
await self._set_answer(messages[0])
@ -150,7 +175,7 @@ class LearningChat:
await self.data.save()
if result == Result.Ban:
# 禁用某句话
if self.role not in {'superuser', 'admin', 'owner'}:
if self.role not in {"superuser", "admin", "owner"}:
# 检查权限
return [random.choice(NO_PERMISSION_WORDS)]
if self.reply:
@ -163,57 +188,79 @@ class LearningChat:
return [random.choice(DOUBT_WORDS)]
elif result in [Result.SetEnable, Result.SetDisable]:
# 检查权限
if self.role not in {'superuser', 'admin', 'owner'}:
if self.role not in {"superuser", "admin", "owner"}:
return [random.choice(NO_PERMISSION_WORDS)]
self.config.update(enable=(result == Result.SetEnable))
config_manager.config.group_config[self.data.group_id] = self.config
config_manager.save()
logger.info('群聊学习',
f'群<m>{self.data.group_id}</m>{"开启" if result == Result.SetEnable else "关闭"}学习功能')
return [random.choice(ENABLE_WORDS if result == Result.SetEnable else DISABLE_WORDS)]
logger.info(
"群聊学习",
f'群<m>{self.data.group_id}</m>{"开启" if result == Result.SetEnable else "关闭"}学习功能',
)
return [
random.choice(
ENABLE_WORDS if result == Result.SetEnable else DISABLE_WORDS
)
]
elif result == Result.Pass:
# 跳过
return None
elif result == Result.Repeat:
if await ChatMessage.filter(group_id=self.data.group_id,
time__gte=self.data.time - 3600).limit(
self.config.repeat_threshold + 5).filter(
user_id=self.bot_id, message=self.data.message).exists():
if (
await ChatMessage.filter(
group_id=self.data.group_id, time__gte=self.data.time - 3600
)
.limit(self.config.repeat_threshold + 5)
.filter(user_id=self.bot_id, message=self.data.message)
.exists()
):
# 如果在阈值+5条消息内bot已经回复过这句话则跳过
logger.debug('群聊学习', '➤➤已经复读过了,跳过')
logger.debug("群聊学习", "➤➤已经复读过了,跳过")
return None
if not (messages := await ChatMessage.filter(
group_id=self.data.group_id,
time__gte=self.data.time - 3600).limit(self.config.repeat_threshold)):
if not (
messages := await ChatMessage.filter(
group_id=self.data.group_id, time__gte=self.data.time - 3600
).limit(self.config.repeat_threshold)
):
return None
# 如果达到阈值,且不是全都为同一个人在说,则进行复读
if len(messages) >= self.config.repeat_threshold and all(
message.message == self.data.message
for message in messages) and any(message.user_id != self.data.user_id
for message in messages):
if (
len(messages) >= self.config.repeat_threshold
and all(message.message == self.data.message for message in messages)
and any(message.user_id != self.data.user_id for message in messages)
):
if random.random() < self.config.break_probability:
logger.debug('群聊学习', '➤➤达到复读阈值,打断复读!')
logger.debug("群聊学习", "➤➤达到复读阈值,打断复读!")
return [random.choice(BREAK_REPEAT_WORDS)]
else:
logger.debug('群聊学习', f'➤➤达到复读阈值,复读<m>{messages[0].message}</m>')
logger.debug("群聊学习", f"➤➤达到复读阈值,复读<m>{messages[0].message}</m>")
return [self.data.message]
return None
else:
# 回复
if self.data.is_plain_text and len(self.data.plain_text) <= 1:
logger.debug('群聊学习', '➤➤消息过短,不回复')
logger.debug("群聊学习", "➤➤消息过短,不回复")
return None
if not (context := await ChatContext.filter(keywords=self.data.keywords).first()):
logger.debug('群聊学习', '➤➤尚未有已学习的回复,不回复')
if not (
context := await ChatContext.filter(keywords=self.data.keywords).first()
):
logger.debug("群聊学习", "➤➤尚未有已学习的回复,不回复")
return None
# 获取回复阈值
if not self.to_me:
answer_choices = list(
range(self.config.answer_threshold - len(self.config.answer_threshold_weights) + 1,
self.config.answer_threshold + 1))
range(
self.config.answer_threshold
- len(self.config.answer_threshold_weights)
+ 1,
self.config.answer_threshold + 1,
)
)
answer_count_threshold = random.choices(answer_choices, weights=self.config.answer_threshold_weights)[0]
answer_count_threshold = random.choices(
answer_choices, weights=self.config.answer_threshold_weights
)[0]
if len(self.data.keyword_list) == chat_config.KEYWORDS_SIZE:
answer_count_threshold -= 1
@ -221,17 +268,25 @@ class LearningChat:
else:
answer_count_threshold = 1
cross_group_threshold = 1
logger.debug('群聊学习',
f'➤➤本次回复阈值为<m>{answer_count_threshold}</m>,跨群阈值为<m>{cross_group_threshold}</m>')
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,
keywords__in=await ChatAnswer.annotate(
cross=Count('keywords')).group_by('keywords').filter(
cross__gte=cross_group_threshold).values_list('keywords',
flat=True))
answers_cross = await ChatAnswer.filter(
context=context,
count__gte=answer_count_threshold,
keywords__in=await ChatAnswer.annotate(cross=Count("keywords"))
.group_by("keywords")
.filter(cross__gte=cross_group_threshold)
.values_list("keywords", flat=True),
)
answer_same_group = await ChatAnswer.filter(context=context, count__gte=answer_count_threshold,
group_id=self.data.group_id)
answer_same_group = await ChatAnswer.filter(
context=context,
count__gte=answer_count_threshold,
group_id=self.data.group_id,
)
candidate_answers: List[Optional[ChatAnswer]] = []
# 检查候选回复是否在屏蔽列表中
@ -242,23 +297,32 @@ class LearningChat:
# answer.count -= answer_count_threshold - 1
candidate_answers.append(answer)
if not candidate_answers:
logger.debug('群聊学习', '➤➤没有符合条件的候选回复')
logger.debug("群聊学习", "➤➤没有符合条件的候选回复")
return None
# 从候选回复中进行选择
sum_count = sum(answer.count for answer in candidate_answers)
per_list = [answer.count / sum_count * (1 - 1 / answer.count) for answer in candidate_answers]
per_list = [
answer.count / sum_count * (1 - 1 / answer.count)
for answer in candidate_answers
]
per_list.append(1 - sum(per_list))
answer_dict = tuple(zip(candidate_answers, per_list))
logger.debug('群聊学习',
f'➤➤候选回复有<m>{"|".join([f"""{a.keywords}({round(p, 3)})""" for a, p in answer_dict])}|不回复({round(per_list[-1], 3)})</m>')
logger.debug(
"群聊学习",
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:
logger.debug('群聊学习', '➤➤但不进行回复')
if (
result := random.choices(candidate_answers + [None], weights=per_list)[
0
]
) is None:
logger.debug("群聊学习", "➤➤但不进行回复")
return None
result_message = random.choice(result.messages)
logger.debug('群聊学习', f'➤➤将回复<m>{result_message}</m>')
logger.debug("群聊学习", f"➤➤将回复<m>{result_message}</m>")
await asyncio.sleep(random.random() + 0.5)
return [result_message]
@ -266,21 +330,27 @@ class LearningChat:
"""屏蔽消息"""
bot = get_bot()
if message_id:
if not (message := await ChatMessage.filter(message_id=message_id).first()) or message.message in ALL_WORDS:
if (
not (message := await ChatMessage.filter(message_id=message_id).first())
or message.message in ALL_WORDS
):
return False
keywords = message.keywords
try:
await bot.delete_msg(message_id=message_id)
except ActionFailed:
logger.info('群聊学习', f'待禁用消息<m>{message_id}</m>尝试撤回<r>失败</r>')
elif (last_reply := await ChatMessage.filter(group_id=self.data.group_id, user_id=self.bot_id).first()) and (
last_reply.message not in ALL_WORDS):
logger.info("群聊学习", f"待禁用消息<m>{message_id}</m>尝试撤回<r>失败</r>")
elif (
last_reply := await ChatMessage.filter(
group_id=self.data.group_id, user_id=self.bot_id
).first()
) and (last_reply.message not in ALL_WORDS):
# 没有指定消息ID则屏蔽最后一条回复
keywords = last_reply.keywords
try:
await bot.delete_msg(message_id=last_reply.message_id)
except ActionFailed:
logger.info('群聊学习', f'待禁用消息<m>{last_reply.message_id}</m>尝试撤回<r>失败</r>')
logger.info("群聊学习", f"待禁用消息<m>{last_reply.message_id}</m>尝试撤回<r>失败</r>")
else:
return False
if ban_word := await ChatBlackList.filter(keywords=keywords).first():
@ -291,16 +361,24 @@ class LearningChat:
if len(ban_word.ban_group_id) >= 2:
# 如果有超过2个群都屏蔽了该条消息则全局屏蔽
ban_word.global_ban = True
logger.info('群聊学习', f'学习词<m>{keywords}</m>将被全局禁用')
logger.info("群聊学习", f"学习词<m>{keywords}</m>将被全局禁用")
await ChatAnswer.filter(keywords=keywords).delete()
else:
logger.info('群聊学习', f'群<m>{self.data.group_id}</m>禁用了学习词<m>{keywords}</m>')
await ChatAnswer.filter(keywords=keywords, group_id=self.data.group_id).delete()
logger.info(
"群聊学习", f"群<m>{self.data.group_id}</m>禁用了学习词<m>{keywords}</m>"
)
await ChatAnswer.filter(
keywords=keywords, group_id=self.data.group_id
).delete()
else:
# 没有屏蔽记录,则新建
logger.info('群聊学习', f'群<m>{self.data.group_id}</m>禁用了学习词<m>{keywords}</m>')
ban_word = ChatBlackList(keywords=keywords, ban_group_id=[self.data.group_id])
await ChatAnswer.filter(keywords=keywords, group_id=self.data.group_id).delete()
logger.info("群聊学习", f"群<m>{self.data.group_id}</m>禁用了学习词<m>{keywords}</m>")
ban_word = ChatBlackList(
keywords=keywords, ban_group_id=[self.data.group_id]
)
await ChatAnswer.filter(
keywords=keywords, group_id=self.data.group_id
).delete()
await ChatContext.filter(keywords=keywords).delete()
await ban_word.save()
return True
@ -316,49 +394,70 @@ class LearningChat:
if len(ban_word.ban_group_id) >= 2:
# 如果有超过2个群都屏蔽了该条消息则全局屏蔽
ban_word.global_ban = True
logger.info('群聊学习', f'学习词<m>{data.keywords}</m>将被全局禁用')
logger.info("群聊学习", f"学习词<m>{data.keywords}</m>将被全局禁用")
await ChatAnswer.filter(keywords=data.keywords).delete()
else:
logger.info('群聊学习', f'群<m>{data.group_id}</m>禁用了学习词<m>{data.keywords}</m>')
await ChatAnswer.filter(keywords=data.keywords, group_id=data.group_id).delete()
logger.info(
"群聊学习", f"群<m>{data.group_id}</m>禁用了学习词<m>{data.keywords}</m>"
)
await ChatAnswer.filter(
keywords=data.keywords, group_id=data.group_id
).delete()
else:
ban_word.global_ban = True
logger.info('群聊学习', f'学习词<m>{data.keywords}</m>将被全局禁用')
logger.info("群聊学习", f"学习词<m>{data.keywords}</m>将被全局禁用")
await ChatAnswer.filter(keywords=data.keywords).delete()
else:
# 没有屏蔽记录,则新建
if isinstance(data, ChatMessage):
logger.info('群聊学习', f'群<m>{data.group_id}</m>禁用了学习词<m>{data.keywords}</m>')
ban_word = ChatBlackList(keywords=data.keywords, ban_group_id=[data.group_id])
await ChatAnswer.filter(keywords=data.keywords, group_id=data.group_id).delete()
logger.info(
"群聊学习", f"群<m>{data.group_id}</m>禁用了学习词<m>{data.keywords}</m>"
)
ban_word = ChatBlackList(
keywords=data.keywords, ban_group_id=[data.group_id]
)
await ChatAnswer.filter(
keywords=data.keywords, group_id=data.group_id
).delete()
else:
logger.info('群聊学习', f'学习词<m>{data.keywords}</m>将被全局禁用')
logger.info("群聊学习", f"学习词<m>{data.keywords}</m>将被全局禁用")
ban_word = ChatBlackList(keywords=data.keywords, global_ban=True)
await ChatAnswer.filter(keywords=data.keywords).delete()
await ChatContext.filter(keywords=data.keywords).delete()
await ban_word.save()
@staticmethod
async def speak(self_id: int) -> Optional[Tuple[int, List[Union[str, MessageSegment]]]]:
async def speak(
self_id: int,
) -> Optional[Tuple[int, List[Union[str, MessageSegment]]]]:
# 主动发言
cur_time = int(time.time())
today_time = time.mktime(datetime.date.today().timetuple())
# 获取两小时内消息超过10条的群列表
groups = await ChatMessage.filter(time__gte=today_time).annotate(count=Count('id')).group_by('group_id'). \
filter(count__gte=10).values_list('group_id', flat=True)
groups = (
await ChatMessage.filter(time__gte=today_time)
.annotate(count=Count("id"))
.group_by("group_id")
.filter(count__gte=10)
.values_list("group_id", flat=True)
)
if not groups:
return None
total_messages = {}
# 获取这些群的两小时内的所有消息
for group_id in groups:
if messages := await ChatMessage.filter(group_id=group_id, time__gte=today_time):
if messages := await ChatMessage.filter(
group_id=group_id, time__gte=today_time
):
total_messages[group_id] = messages
if not total_messages:
return None
# 根据消息平均间隔来对群进行排序
def group_popularity_cmp(left_group: Tuple[int, List[ChatMessage]],
right_group: Tuple[int, List[ChatMessage]]):
def group_popularity_cmp(
left_group: Tuple[int, List[ChatMessage]],
right_group: Tuple[int, List[ChatMessage]],
):
def cmp(a, b):
return (a > b) - (a < b)
@ -366,34 +465,54 @@ class LearningChat:
right_group_id, right_messages = right_group
left_duration = left_messages[0].time - left_messages[-1].time
right_duration = right_messages[0].time - right_messages[-1].time
return cmp(len(left_messages) / left_duration, len(right_messages) / right_duration)
return cmp(
len(left_messages) / left_duration, len(right_messages) / right_duration
)
popularity: List[Tuple[int, List[ChatMessage]]] = sorted(total_messages.items(),
key=cmp_to_key(group_popularity_cmp), reverse=True)
logger.debug('群聊学习', f'主动发言:群热度排行<m>{">>".join([str(g[0]) for g in popularity])}</m>')
popularity: List[Tuple[int, List[ChatMessage]]] = sorted(
total_messages.items(), key=cmp_to_key(group_popularity_cmp), reverse=True
)
logger.debug(
"群聊学习", f'主动发言:群热度排行<m>{">>".join([str(g[0]) for g in popularity])}</m>'
)
for group_id, messages in popularity:
if len(messages) < 30:
logger.debug('群聊学习', f'主动发言:群<m>{group_id}</m>消息小于30条不发言')
logger.debug("群聊学习", f"主动发言:群<m>{group_id}</m>消息小于30条不发言")
continue
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'])
chat_config.ban_words
+ config.ban_words
+ [
"[CQ:xml",
"[CQ:json",
"[CQ:at",
"[CQ:video",
"[CQ:record",
"[CQ:share",
]
)
# 是否开启了主动发言
if not config.speak_enable or not config.enable:
logger.debug('群聊学习', f'主动发言:群<m>{group_id}</m>未开启,不发言')
logger.debug("群聊学习", f"主动发言:群<m>{group_id}</m>未开启,不发言")
continue
# 如果最后一条消息是自己发的,则不主动发言
if last_reply := await ChatMessage.filter(group_id=group_id, user_id=self_id).first():
if last_reply := await ChatMessage.filter(
group_id=group_id, user_id=self_id
).first():
if last_reply.time >= messages[0].time:
logger.debug('群聊学习',
f'主动发言:群<m>{group_id}</m>最后一条消息是{NICKNAME}发的{last_reply.message},不发言')
logger.debug(
"群聊学习",
f"主动发言:群<m>{group_id}</m>最后一条消息是{NICKNAME}发的{last_reply.message},不发言",
)
continue
elif cur_time - last_reply.time < config.speak_min_interval:
logger.debug('群聊学习', f'主动发言:群<m>{group_id}</m>上次主动发言时间小于主动发言最小间隔,不发言')
logger.debug(
"群聊学习", f"主动发言:群<m>{group_id}</m>上次主动发言时间小于主动发言最小间隔,不发言"
)
continue
# 该群每多少秒发一条消息
@ -402,50 +521,79 @@ class LearningChat:
silent_time = cur_time - messages[0].time
threshold = avg_interval * config.speak_threshold
if silent_time < threshold:
logger.debug('群聊学习',
f'主动发言:群<m>{group_id}</m>已沉默时间({silent_time})小于阈值({int(threshold)}),不发言')
logger.debug(
"群聊学习",
f"主动发言:群<m>{group_id}</m>已沉默时间({silent_time})小于阈值({int(threshold)}),不发言",
)
continue
if contexts := await ChatContext.filter(count__gte=config.answer_threshold).all():
if contexts := await ChatContext.filter(
count__gte=config.answer_threshold
).all():
speak_list = []
# context = random.choices(contexts, weights=[context.count for context in contexts])[0]
# contexts.sort(key=lambda x: x.count, reverse=True)
random.shuffle(contexts)
for context in contexts:
if (not speak_list or random.random() < config.speak_continuously_probability) and len(
speak_list) < config.speak_continuously_max_len:
if answers := await ChatAnswer.filter(context=context,
group_id=group_id,
count__gte=config.answer_threshold):
answer = random.choices(answers,
weights=[
answer.count + 1 if answer.time >= today_time else answer.count
for answer in answers])[0]
if (
not speak_list
or random.random() < config.speak_continuously_probability
) and len(speak_list) < config.speak_continuously_max_len:
if answers := await ChatAnswer.filter(
context=context,
group_id=group_id,
count__gte=config.answer_threshold,
):
answer = random.choices(
answers,
weights=[
answer.count + 1
if answer.time >= today_time
else answer.count
for answer in answers
],
)[0]
message = random.choice(answer.messages)
if len(message) < 2:
continue
if message.startswith('&#91;') and message.endswith('&#93;'):
if message.startswith("&#91;") and message.endswith(
"&#93;"
):
continue
if any(word in message for word in ban_words):
continue
speak_list.append(message)
follow_answer = answer
while random.random() < config.speak_continuously_probability and len(
speak_list) < config.speak_continuously_max_len:
if (follow_context := await ChatContext.filter(
keywords=follow_answer.keywords).first()) and (
follow_answers := await ChatAnswer.filter(
group_id=group_id,
context=follow_context,
count__gte=config.answer_threshold)):
follow_answer = random.choices(follow_answers,
weights=[
a.count + 1 if a.time >= today_time else a.count
for a in follow_answers])[0]
while (
random.random() < config.speak_continuously_probability
and len(speak_list) < config.speak_continuously_max_len
):
if (
follow_context := await ChatContext.filter(
keywords=follow_answer.keywords
).first()
) and (
follow_answers := await ChatAnswer.filter(
group_id=group_id,
context=follow_context,
count__gte=config.answer_threshold,
)
):
follow_answer = random.choices(
follow_answers,
weights=[
a.count + 1
if a.time >= today_time
else a.count
for a in follow_answers
],
)[0]
message = random.choice(follow_answer.messages)
if len(message) < 2:
continue
if message.startswith('&#91;') and message.endswith('&#93;'):
if message.startswith("&#91;") and message.endswith(
"&#93;"
):
continue
if all(word not in message for word in ban_words):
speak_list.append(message)
@ -455,13 +603,17 @@ class LearningChat:
break
if speak_list:
if random.random() < config.speak_poke_probability:
last_speak_users = {message.user_id for message in messages[:5] if message.user_id != self_id}
last_speak_users = {
message.user_id
for message in messages[:5]
if message.user_id != self_id
}
select_user = random.choice(list(last_speak_users))
speak_list.append(MessageSegment('poke', {'qq': select_user}))
speak_list.append(MessageSegment("poke", {"qq": select_user}))
return group_id, speak_list
else:
logger.debug('群聊学习', f'主动发言:群<m>{group_id}</m>没有找到符合条件的发言,不发言')
logger.debug('群聊学习', '主动发言:没有符合条件的群,不主动发言')
logger.debug("群聊学习", f"主动发言:群<m>{group_id}</m>没有找到符合条件的发言,不发言")
logger.debug("群聊学习", "主动发言:没有符合条件的群,不主动发言")
return None
async def _set_answer(self, message: ChatMessage):
@ -469,42 +621,62 @@ class LearningChat:
if context.count < chat_config.learn_max_count:
context.count += 1
context.time = self.data.time
if answer := await ChatAnswer.filter(keywords=self.data.keywords,
group_id=self.data.group_id,
context=context).first():
if answer := await ChatAnswer.filter(
keywords=self.data.keywords,
group_id=self.data.group_id,
context=context,
).first():
if answer.count < chat_config.learn_max_count:
answer.count += 1
answer.time = self.data.time
if self.data.message not in answer.messages:
answer.messages.append(self.data.message)
else:
answer = ChatAnswer(keywords=self.data.keywords,
group_id=self.data.group_id,
time=self.data.time,
context=context,
messages=[self.data.message])
answer = ChatAnswer(
keywords=self.data.keywords,
group_id=self.data.group_id,
time=self.data.time,
context=context,
messages=[self.data.message],
)
await answer.save()
await context.save()
else:
context = await ChatContext.create(keywords=message.keywords,
time=self.data.time)
answer = await ChatAnswer.create(keywords=self.data.keywords,
group_id=self.data.group_id,
time=self.data.time,
context=context,
messages=[self.data.message])
logger.debug('群聊学习', f'➤将被学习为<m>{message.message}</m>的回答,已学次数为<m>{answer.count}</m>')
context = await ChatContext.create(
keywords=message.keywords, time=self.data.time
)
answer = await ChatAnswer.create(
keywords=self.data.keywords,
group_id=self.data.group_id,
time=self.data.time,
context=context,
messages=[self.data.message],
)
logger.debug(
"群聊学习", f"➤将被学习为<m>{message.message}</m>的回答,已学次数为<m>{answer.count}</m>"
)
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]
)
# if len(raw_message) < 2:
# return False
if any(i in raw_message for i in
{'[CQ:xml', '[CQ:json', '[CQ:at', '[CQ:video', '[CQ:record', '[CQ:share'}):
if any(
i in raw_message
for i in {
"[CQ:xml",
"[CQ:json",
"[CQ:at",
"[CQ:video",
"[CQ:record",
"[CQ:share",
}
):
return False
if any(i in raw_message for i in self.ban_words):
return False
if raw_message.startswith('&#91;') and raw_message.endswith('&#93;'):
if raw_message.startswith("&#91;") and raw_message.endswith("&#93;"):
return False
if ban_word := await ChatBlackList.filter(keywords=message.keywords).first():
if ban_word.global_ban or message.group_id in ban_word.ban_group_id:

View File

@ -23,7 +23,7 @@ config = config_manager.config
JSON_DUMPS = functools.partial(json.dumps, ensure_ascii=False)
jieba.setLogLevel(jieba.logging.INFO)
jieba.load_userdict(str(Path(__file__).parent / 'genshin_word.txt')) # 加载原神词典
jieba.load_userdict(str(Path(__file__).parent / "genshin_word.txt")) # 加载原神词典
jieba.load_userdict(config.dictionary) # 加载用户自定义的词典
@ -46,14 +46,14 @@ class ChatMessage(Model):
"""时间戳"""
class Meta:
table = 'message'
indexes = ('group_id', 'time')
ordering = ['-time']
table = "message"
indexes = ("group_id", "time")
ordering = ["-time"]
@cached_property
def is_plain_text(self) -> bool:
"""是否纯文本"""
return '[CQ:' not in self.message
return "[CQ:" not in self.message
@cached_property
def keyword_list(self) -> List[str]:
@ -67,7 +67,9 @@ class ChatMessage(Model):
"""获取纯文本部分的关键词结果"""
if not self.is_plain_text and not len(self.plain_text):
return self.message
return self.message if len(self.keyword_list) < 2 else ' '.join(self.keyword_list)
return (
self.message if len(self.keyword_list) < 2 else " ".join(self.keyword_list)
)
class ChatContext(Model):
@ -79,13 +81,13 @@ class ChatContext(Model):
"""时间戳"""
count: int = fields.IntField(default=1)
"""次数"""
answers: fields.ReverseRelation['ChatAnswer']
answers: fields.ReverseRelation["ChatAnswer"]
"""答案"""
class Meta:
table = 'context'
indexes = ('keywords', 'time')
ordering = ['-time']
table = "context"
indexes = ("keywords", "time")
ordering = ["-time"]
class ChatAnswer(Model):
@ -103,12 +105,13 @@ class ChatAnswer(Model):
"""消息列表"""
context: fields.ForeignKeyNullableRelation[ChatContext] = fields.ForeignKeyField(
'LearningChat.ChatContext', related_name='answers', null=True)
"LearningChat.ChatContext", related_name="answers", null=True
)
class Meta:
table = 'answer'
indexes = ('keywords', 'time')
ordering = ['-time']
table = "answer"
indexes = ("keywords", "time")
ordering = ["-time"]
class ChatBlackList(Model):
@ -122,8 +125,10 @@ class ChatBlackList(Model):
"""禁用的群id"""
class Meta:
table = 'blacklist'
indexes = ('keywords',)
table = "blacklist"
indexes = ("keywords",)
register_database(db_name='LearningChat', models=__name__, db_path=DATABASE_PATH / 'LearningChat.db')
register_database(
db_name="LearningChat", models=__name__, db_path=DATABASE_PATH / "LearningChat.db"
)

View File

@ -4,7 +4,12 @@ from fastapi import APIRouter
from fastapi.responses import JSONResponse
from nonebot import get_bot
from LittlePaimon.plugins.Learning_Chat.models import ChatMessage, ChatContext, ChatAnswer, ChatBlackList
from LittlePaimon.plugins.Learning_Chat.models import (
ChatMessage,
ChatContext,
ChatAnswer,
ChatBlackList,
)
from LittlePaimon.web.api import BaseApiRouter
from LittlePaimon.web.api.utils import authentication
@ -19,232 +24,290 @@ from .config import config_manager
route = APIRouter()
@route.get('/chat_global_config', response_class=JSONResponse, dependencies=[authentication()])
@route.get(
"/chat_global_config", response_class=JSONResponse, dependencies=[authentication()]
)
async def get_chat_global_config():
try:
bot = get_bot()
groups = await bot.get_group_list()
member_list = []
for group in groups:
members = await bot.get_group_member_list(group_id=group['group_id'])
members = await bot.get_group_member_list(group_id=group["group_id"])
member_list.extend(
[{'label': f'{member["nickname"] or member["card"]}({member["user_id"]})', 'value': member['user_id']} for
member in members])
config = config_manager.config.dict(exclude={'group_config'})
config['member_list'] = member_list
[
{
"label": f'{member["nickname"] or member["card"]}({member["user_id"]})',
"value": member["user_id"],
}
for member in members
]
)
config = config_manager.config.dict(exclude={"group_config"})
config["member_list"] = member_list
return config
except ValueError:
return {
'status': -100,
'msg': '获取群和好友列表失败请确认已连接GOCQ'
}
return {"status": -100, "msg": "获取群和好友列表失败请确认已连接GOCQ"}
@route.post('/chat_global_config', response_class=JSONResponse, dependencies=[authentication()])
@route.post(
"/chat_global_config", response_class=JSONResponse, dependencies=[authentication()]
)
async def post_chat_global_config(data: dict):
config_manager.config.update(**data)
config_manager.save()
await ChatContext.filter(count__gt=config_manager.config.learn_max_count).update(
count=config_manager.config.learn_max_count)
count=config_manager.config.learn_max_count
)
await ChatAnswer.filter(count__gt=config_manager.config.learn_max_count).update(
count=config_manager.config.learn_max_count)
count=config_manager.config.learn_max_count
)
jieba.load_userdict(config_manager.config.dictionary)
return {
'status': 0,
'msg': '保存成功'
}
return {"status": 0, "msg": "保存成功"}
@route.get('/chat_group_config', response_class=JSONResponse, dependencies=[authentication()])
@route.get(
"/chat_group_config", response_class=JSONResponse, dependencies=[authentication()]
)
async def get_chat_global_config(group_id: int):
try:
members = await get_bot().get_group_member_list(group_id=group_id)
member_list = [{'label': f'{member["nickname"] or member["card"]}({member["user_id"]})', 'value': member['user_id']}
for member in members]
member_list = [
{
"label": f'{member["nickname"] or member["card"]}({member["user_id"]})',
"value": member["user_id"],
}
for member in members
]
config = config_manager.get_group_config(group_id).dict()
config['break_probability'] = config['break_probability'] * 100
config['speak_continuously_probability'] = config['speak_continuously_probability'] * 100
config['speak_poke_probability'] = config['speak_poke_probability'] * 100
config['member_list'] = member_list
config["break_probability"] = config["break_probability"] * 100
config["speak_continuously_probability"] = (
config["speak_continuously_probability"] * 100
)
config["speak_poke_probability"] = config["speak_poke_probability"] * 100
config["member_list"] = member_list
return config
except ValueError:
return {
'status': -100,
'msg': '获取群和好友列表失败请确认已连接GOCQ'
}
return {"status": -100, "msg": "获取群和好友列表失败请确认已连接GOCQ"}
@route.post('/chat_group_config', response_class=JSONResponse, dependencies=[authentication()])
@route.post(
"/chat_group_config", response_class=JSONResponse, dependencies=[authentication()]
)
async def post_chat_global_config(group_id: Union[int, str], data: dict):
if not data['answer_threshold_weights']:
return {
'status': 400,
'msg': '回复阈值权重不能为空,必须至少有一个数值'
}
if not data["answer_threshold_weights"]:
return {"status": 400, "msg": "回复阈值权重不能为空,必须至少有一个数值"}
else:
data['break_probability'] = data['break_probability'] / 100
data['speak_continuously_probability'] = data['speak_continuously_probability'] / 100
data['speak_poke_probability'] = data['speak_poke_probability'] / 100
if group_id != 'all':
groups = [{'group_id': group_id}]
data["break_probability"] = data["break_probability"] / 100
data["speak_continuously_probability"] = (
data["speak_continuously_probability"] / 100
)
data["speak_poke_probability"] = data["speak_poke_probability"] / 100
if group_id != "all":
groups = [{"group_id": group_id}]
else:
groups = await get_bot().get_group_list()
for group in groups:
config = config_manager.get_group_config(group['group_id'])
config = config_manager.get_group_config(group["group_id"])
config.update(**data)
config_manager.config.group_config[group['group_id']] = config
config_manager.config.group_config[group["group_id"]] = config
config_manager.save()
return {
'status': 0,
'msg': '保存成功'
}
return {"status": 0, "msg": "保存成功"}
@route.get('/get_chat_messages', response_class=JSONResponse, dependencies=[authentication()])
async def get_chat_messages(page: int = 1,
perPage: int = 10,
orderBy: str = 'time',
orderDir: str = 'desc',
group_id: Optional[str] = None,
user_id: Optional[str] = None,
message: Optional[str] = None):
orderBy = (orderBy or 'time') if (orderDir or 'desc') == 'asc' else f'-{orderBy or "time"}'
filter_args = {f'{k}__contains': v for k, v in
{'group_id': group_id, 'user_id': user_id, 'raw_message': message}.items() if v}
@route.get(
"/get_chat_messages", response_class=JSONResponse, dependencies=[authentication()]
)
async def get_chat_messages(
page: int = 1,
perPage: int = 10,
orderBy: str = "time",
orderDir: str = "desc",
group_id: Optional[str] = None,
user_id: Optional[str] = None,
message: Optional[str] = None,
):
orderBy = (
(orderBy or "time")
if (orderDir or "desc") == "asc"
else f'-{orderBy or "time"}'
)
filter_args = {
f"{k}__contains": v
for k, v in {
"group_id": group_id,
"user_id": user_id,
"raw_message": message,
}.items()
if v
}
return {
'status': 0,
'msg': 'ok',
'data': {
'items': await ChatMessage.filter(**filter_args).order_by(orderBy).offset((page - 1) * perPage).limit(
perPage).values(),
'total': await ChatMessage.filter(**filter_args).count()
}
"status": 0,
"msg": "ok",
"data": {
"items": await ChatMessage.filter(**filter_args)
.order_by(orderBy)
.offset((page - 1) * perPage)
.limit(perPage)
.values(),
"total": await ChatMessage.filter(**filter_args).count(),
},
}
@route.get('/get_chat_contexts', response_class=JSONResponse, dependencies=[authentication()])
async def get_chat_context(page: int = 1, perPage: int = 10, orderBy: str = 'time', orderDir: str = 'desc',
keywords: Optional[str] = None):
orderBy = (orderBy or 'time') if (orderDir or 'desc') == 'asc' else f'-{orderBy or "time"}'
filter_arg = {'keywords__contains': keywords} if keywords else {}
@route.get(
"/get_chat_contexts", response_class=JSONResponse, dependencies=[authentication()]
)
async def get_chat_context(
page: int = 1,
perPage: int = 10,
orderBy: str = "time",
orderDir: str = "desc",
keywords: Optional[str] = None,
):
orderBy = (
(orderBy or "time")
if (orderDir or "desc") == "asc"
else f'-{orderBy or "time"}'
)
filter_arg = {"keywords__contains": keywords} if keywords else {}
return {
'status': 0,
'msg': 'ok',
'data': {
'items': await ChatContext.filter(**filter_arg).order_by(orderBy).offset((page - 1) * perPage).limit(
perPage).values(),
'total': await ChatContext.filter(**filter_arg).count()
}
"status": 0,
"msg": "ok",
"data": {
"items": await ChatContext.filter(**filter_arg)
.order_by(orderBy)
.offset((page - 1) * perPage)
.limit(perPage)
.values(),
"total": await ChatContext.filter(**filter_arg).count(),
},
}
@route.get('/get_chat_answers', response_class=JSONResponse, dependencies=[authentication()])
async def get_chat_answers(context_id: Optional[int] = None, page: int = 1, perPage: int = 10, orderBy: str = 'count',
orderDir: str = 'desc', keywords: Optional[str] = None):
filter_arg = {'context_id': context_id} if context_id else {}
@route.get(
"/get_chat_answers", response_class=JSONResponse, dependencies=[authentication()]
)
async def get_chat_answers(
context_id: Optional[int] = None,
page: int = 1,
perPage: int = 10,
orderBy: str = "count",
orderDir: str = "desc",
keywords: Optional[str] = None,
):
filter_arg = {"context_id": context_id} if context_id else {}
if keywords:
filter_arg['keywords__contains'] = keywords # type: ignore
orderBy = (orderBy or 'count') if (orderDir or 'desc') == 'asc' else f'-{orderBy or "count"}'
filter_arg["keywords__contains"] = keywords # type: ignore
orderBy = (
(orderBy or "count")
if (orderDir or "desc") == "asc"
else f'-{orderBy or "count"}'
)
return {
'status': 0,
'msg': 'ok',
'data': {
'items': list(
map(lambda x: x.update({'messages': [{'msg': m} for m in x['messages']]}) or x,
await ChatAnswer.filter(**filter_arg).order_by(orderBy).offset((page - 1) * perPage).limit(
perPage).values())),
'total': await ChatAnswer.filter(**filter_arg).count()
}
"status": 0,
"msg": "ok",
"data": {
"items": list(
map(
lambda x: x.update(
{"messages": [{"msg": m} for m in x["messages"]]}
)
or x,
await ChatAnswer.filter(**filter_arg)
.order_by(orderBy)
.offset((page - 1) * perPage)
.limit(perPage)
.values(),
)
),
"total": await ChatAnswer.filter(**filter_arg).count(),
},
}
@route.get('/get_chat_blacklist', response_class=JSONResponse, dependencies=[authentication()])
async def get_chat_blacklist(page: int = 1, perPage: int = 10, keywords: Optional[str] = None,
bans: Optional[str] = None):
filter_arg = {'keywords__contains': keywords} if keywords else {}
items = await ChatBlackList.filter(**filter_arg).offset((page - 1) * perPage).limit(perPage).values()
@route.get(
"/get_chat_blacklist", response_class=JSONResponse, dependencies=[authentication()]
)
async def get_chat_blacklist(
page: int = 1,
perPage: int = 10,
keywords: Optional[str] = None,
bans: Optional[str] = None,
):
filter_arg = {"keywords__contains": keywords} if keywords else {}
items = (
await ChatBlackList.filter(**filter_arg)
.offset((page - 1) * perPage)
.limit(perPage)
.values()
)
for item in items:
item['bans'] = '全局禁用' if item['global_ban'] else str(item['ban_group_id'][0])
item["bans"] = "全局禁用" if item["global_ban"] else str(item["ban_group_id"][0])
if bans:
items = list(filter(lambda x: bans in x['bans'], items))
items = list(filter(lambda x: bans in x["bans"], items))
return {
'status': 0,
'msg': 'ok',
'data': {
'items': items,
'total': await ChatBlackList.filter(**filter_arg).count()
}
"status": 0,
"msg": "ok",
"data": {
"items": items,
"total": await ChatBlackList.filter(**filter_arg).count(),
},
}
@route.delete('/delete_chat', response_class=JSONResponse, dependencies=[authentication()])
@route.delete(
"/delete_chat", response_class=JSONResponse, dependencies=[authentication()]
)
async def delete_chat(id: int, type: str):
try:
if type == 'message':
if type == "message":
await ChatMessage.filter(id=id).delete()
elif type == 'context':
elif type == "context":
c = await ChatContext.get(id=id)
await ChatAnswer.filter(context=c).delete()
await c.delete()
elif type == 'answer':
elif type == "answer":
await ChatAnswer.filter(id=id).delete()
elif type == 'blacklist':
elif type == "blacklist":
await ChatBlackList.filter(id=id).delete()
return {
'status': 0,
'msg': '删除成功'
}
return {"status": 0, "msg": "删除成功"}
except Exception as e:
return {
'status': 500,
'msg': f'删除失败,{e}'
}
return {"status": 500, "msg": f"删除失败,{e}"}
@route.put('/ban_chat', response_class=JSONResponse, dependencies=[authentication()])
@route.put("/ban_chat", response_class=JSONResponse, dependencies=[authentication()])
async def ban_chat(id: int, type: str):
try:
if type == 'message':
if type == "message":
data = await ChatMessage.get(id=id)
elif type == 'context':
elif type == "context":
data = await ChatContext.get(id=id)
else:
data = await ChatAnswer.get(id=id)
await LearningChat.add_ban(data)
return {
'status': 0,
'msg': '禁用成功'
}
return {"status": 0, "msg": "禁用成功"}
except Exception as e:
return {
'status': 500,
'msg': f'禁用失败: {e}'
}
return {"status": 500, "msg": f"禁用失败: {e}"}
@route.put('/delete_all', response_class=JSONResponse, dependencies=[authentication()])
@route.put("/delete_all", response_class=JSONResponse, dependencies=[authentication()])
async def delete_all(type: str, id: Optional[int] = None):
try:
if type == 'message':
if type == "message":
await ChatMessage.all().delete()
elif type == 'context':
elif type == "context":
await ChatContext.all().delete()
elif type == 'answer':
elif type == "answer":
if id:
await ChatAnswer.filter(context_id=id).delete()
else:
await ChatAnswer.all().delete()
elif type == 'blacklist':
elif type == "blacklist":
await ChatBlackList.all().delete()
return {
'status': 0,
'msg': '操作成功'
}
return {"status": 0, "msg": "操作成功"}
except Exception as e:
return {
'status': 500,
'msg': f'操作失败,{e}'
}
return {"status": 500, "msg": f"操作失败,{e}"}
BaseApiRouter.include_router(route)

View File

@ -5,275 +5,626 @@ from LittlePaimon.utils import NICKNAME
from LittlePaimon.web.pages import admin_app
global_config_form = Form(
title='全局配置',
name='global_config',
initApi='/LittlePaimon/api/chat_global_config',
api='post:/LittlePaimon/api/chat_global_config',
title="全局配置",
name="global_config",
initApi="/LittlePaimon/api/chat_global_config",
api="post:/LittlePaimon/api/chat_global_config",
interval=None,
body=[
Switch(label='群聊学习总开关', name='total_enable', value='${total_enable}', onText='开启', offText='关闭',
labelRemark=Remark(shape='circle', content='关闭后,全局都将不会再学习和回复(但是仍会对收到的消息进行记录)。')),
InputNumber(label='单句关键词数量', name='KEYWORDS_SIZE', value='${KEYWORDS_SIZE}', visibleOn='${total_enable}',
min=2,
labelRemark=Remark(shape='circle', content='单句语句标签数量影响对一句话的主题词提取效果建议保持默认为3。')),
InputNumber(label='跨群回复阈值', name='cross_group_threshold', value='${cross_group_threshold}',
visibleOn='${total_enable}', min=1,
labelRemark=Remark(shape='circle', content='当学习到的一种回复在N个群都有那么这个回复就会变为全局回复。')),
InputNumber(label='最高学习次数', name='learn_max_count', value='${learn_max_count}',
visibleOn='${total_enable}', min=2, labelRemark=Remark(shape='circle',
content='学习的回复最高能累计到的次数,值越高,这个回复就会学习得越深,越容易进行回复,如果不想每次都大概率固定回复某一句话,可以将该值设低点。')),
InputTag(label='全局屏蔽词', name='ban_words', value='${ban_words}', enableBatchAdd=True,
placeholder='添加全局屏蔽词', visibleOn='${total_enable}', joinValues=False, extractValue=True,
labelRemark=Remark(shape='circle', content='全局屏蔽词含有这些词的消息不会学习和回复默认已屏蔽at、分享、语音、和视频等消息。(回车进行添加)')),
InputTag(label='全局屏蔽用户', source='${member_list}', name='ban_users', value='${ban_users}',
enableBatchAdd=True,
placeholder='添加全局屏蔽用户', visibleOn='${total_enable}', joinValues=False, extractValue=True,
labelRemark=Remark(shape='circle', content='全局屏蔽用户,和这些用户有关的消息不会学习和回复。(回车进行添加)')),
InputTag(label='自定义词典', name='dictionary', value='${dictionary}',
enableBatchAdd=True,
placeholder='添加自定义词语', visibleOn='${total_enable}', joinValues=False, extractValue=True,
labelRemark=Remark(shape='circle', content='添加自定义词语,让分词能够识别未收录的词汇,提高学习的准确性。你可以添加特殊名词,这样学习时就会将该词看作一个整体,目前词典中已默认添加部分原神相关词汇。(回车进行添加)')),
Switch(
label="群聊学习总开关",
name="total_enable",
value="${total_enable}",
onText="开启",
offText="关闭",
labelRemark=Remark(
shape="circle", content="关闭后,全局都将不会再学习和回复(但是仍会对收到的消息进行记录)。"
),
),
InputNumber(
label="单句关键词数量",
name="KEYWORDS_SIZE",
value="${KEYWORDS_SIZE}",
visibleOn="${total_enable}",
min=2,
labelRemark=Remark(
shape="circle", content="单句语句标签数量影响对一句话的主题词提取效果建议保持默认为3。"
),
),
InputNumber(
label="跨群回复阈值",
name="cross_group_threshold",
value="${cross_group_threshold}",
visibleOn="${total_enable}",
min=1,
labelRemark=Remark(
shape="circle", content="当学习到的一种回复在N个群都有那么这个回复就会变为全局回复。"
),
),
InputNumber(
label="最高学习次数",
name="learn_max_count",
value="${learn_max_count}",
visibleOn="${total_enable}",
min=2,
labelRemark=Remark(
shape="circle",
content="学习的回复最高能累计到的次数,值越高,这个回复就会学习得越深,越容易进行回复,如果不想每次都大概率固定回复某一句话,可以将该值设低点。",
),
),
InputTag(
label="全局屏蔽词",
name="ban_words",
value="${ban_words}",
enableBatchAdd=True,
placeholder="添加全局屏蔽词",
visibleOn="${total_enable}",
joinValues=False,
extractValue=True,
labelRemark=Remark(
shape="circle",
content="全局屏蔽词含有这些词的消息不会学习和回复默认已屏蔽at、分享、语音、和视频等消息。(回车进行添加)",
),
),
InputTag(
label="全局屏蔽用户",
source="${member_list}",
name="ban_users",
value="${ban_users}",
enableBatchAdd=True,
placeholder="添加全局屏蔽用户",
visibleOn="${total_enable}",
joinValues=False,
extractValue=True,
labelRemark=Remark(
shape="circle", content="全局屏蔽用户,和这些用户有关的消息不会学习和回复。(回车进行添加)"
),
),
InputTag(
label="自定义词典",
name="dictionary",
value="${dictionary}",
enableBatchAdd=True,
placeholder="添加自定义词语",
visibleOn="${total_enable}",
joinValues=False,
extractValue=True,
labelRemark=Remark(
shape="circle",
content="添加自定义词语,让分词能够识别未收录的词汇,提高学习的准确性。你可以添加特殊名词,这样学习时就会将该词看作一个整体,目前词典中已默认添加部分原神相关词汇。(回车进行添加)",
),
),
],
actions=[
Action(label="保存", level=LevelEnum.success, type="submit"),
Action(label="重置", level=LevelEnum.warning, type="reset"),
],
actions=[Action(label='保存', level=LevelEnum.success, type='submit'),
Action(label='重置', level=LevelEnum.warning, type='reset')]
)
group_select = Select(label='分群配置', name='group_id', source='${group_list}',
placeholder='选择群')
group_select = Select(
label="分群配置", name="group_id", source="${group_list}", placeholder="选择群"
)
group_config_form = Form(
title='分群配置',
visibleOn='group_id != null',
initApi='/LittlePaimon/api/chat_group_config?group_id=${group_id}',
api='post:/LittlePaimon/api/chat_group_config?group_id=${group_id}',
title="分群配置",
visibleOn="group_id != null",
initApi="/LittlePaimon/api/chat_group_config?group_id=${group_id}",
api="post:/LittlePaimon/api/chat_group_config?group_id=${group_id}",
interval=None,
body=[
Switch(label='群聊学习开关', name='enable', value='${enable}', onText='开启', offText='关闭',
labelRemark=Remark(shape='circle', content='针对该群的群聊学习开关,关闭后,仅该群不会学习和回复。')),
InputNumber(label='回复阈值', name='answer_threshold', value='${answer_threshold}', visibleOn='${enable}',
min=2, labelRemark=Remark(shape='circle', content='可以理解为学习成功所需要的次数,值越低学得越快。')),
InputArray(label='回复阈值权重', name='answer_threshold_weights', value='${answer_threshold_weights}',
items=InputNumber(min=1, max=100, value=25, suffix='%'), inline=True, visibleOn='${enable}',
labelRemark=Remark(shape='circle',
content='影响回复阈值的计算方式以默认的回复阈值4、权重[10, 30, 60]为例在计算阈值时60%概率为430%概率为310%概率为2。')),
InputNumber(label='复读阈值', name='repeat_threshold', value='${repeat_threshold}', visibleOn='${enable}',
min=2,
labelRemark=Remark(shape='circle', content=f'跟随复读所需要的阈值有N个人复读后{NICKNAME}就会跟着复读。')),
InputNumber(label='打断复读概率', name='break_probability', value='${break_probability}',
min=0, max=100, suffix='%', visibleOn='${AND(enable, speak_enable)}',
labelRemark=Remark(shape='circle', content='达到复读阈值时,打断复读而不是跟随复读的概率。')),
InputTag(label='屏蔽词', name='ban_words', value='${ban_words}', enableBatchAdd=True,
placeholder='添加屏蔽词', visibleOn='${enable}', joinValues=False, extractValue=True,
labelRemark=Remark(shape='circle', content='含有这些词的消息不会学习和回复。(回车进行添加)')),
InputTag(label='屏蔽用户', source='${member_list}', name='ban_users', value='${ban_users}', enableBatchAdd=True,
placeholder='添加屏蔽用户', visibleOn='${enable}', joinValues=False, extractValue=True,
labelRemark=Remark(shape='circle', content='和该群中这些用户有关的消息不会学习和回复。(回车进行添加)')),
Switch(label='主动发言开关', name='speak_enable', value='${speak_enable}', visibleOn='${enable}',
labelRemark=Remark(shape='circle', content=f'是否允许{NICKNAME}在该群主动发言,主动发言是指每隔一段时间挑选一个热度较高的群,主动发一些学习过的内容。')),
InputNumber(label='主动发言阈值', name='speak_threshold', value='${speak_threshold}',
visibleOn='${AND(enable, speak_enable)}', min=0,
labelRemark=Remark(shape='circle', content='值越低,主动发言的可能性越高。')),
InputNumber(label='主动发言最小间隔', name='speak_min_interval', value='${speak_min_interval}', min=0,
visibleOn='${AND(enable, speak_enable)}', suffix='',
labelRemark=Remark(shape='circle', content='进行主动发言的最小时间间隔。')),
InputNumber(label='连续主动发言概率', name='speak_continuously_probability',
value='${speak_continuously_probability}', min=0, max=100, suffix='%',
visibleOn='${AND(enable, speak_enable)}', labelRemark=Remark(shape='circle', content='触发主动发言时,连续进行发言的概率。')),
InputNumber(label='最大连续主动发言句数', name='speak_continuously_max_len',
value='${speak_continuously_max_len}', visibleOn='${AND(enable, speak_enable)}', min=1,
labelRemark=Remark(shape='circle', content='连续主动发言的最大句数。')),
InputNumber(label='主动发言附带戳一戳概率', name='speak_poke_probability', value='${speak_poke_probability}',
min=0, max=100, suffix='%', visibleOn='${AND(enable, speak_enable)}',
labelRemark=Remark(shape='circle', content='主动发言时附带戳一戳的概率会在最近5个发言者中随机选一个戳。')),
Switch(
label="群聊学习开关",
name="enable",
value="${enable}",
onText="开启",
offText="关闭",
labelRemark=Remark(shape="circle", content="针对该群的群聊学习开关,关闭后,仅该群不会学习和回复。"),
),
InputNumber(
label="回复阈值",
name="answer_threshold",
value="${answer_threshold}",
visibleOn="${enable}",
min=2,
labelRemark=Remark(shape="circle", content="可以理解为学习成功所需要的次数,值越低学得越快。"),
),
InputArray(
label="回复阈值权重",
name="answer_threshold_weights",
value="${answer_threshold_weights}",
items=InputNumber(min=1, max=100, value=25, suffix="%"),
inline=True,
visibleOn="${enable}",
labelRemark=Remark(
shape="circle",
content="影响回复阈值的计算方式以默认的回复阈值4、权重[10, 30, 60]为例在计算阈值时60%概率为430%概率为310%概率为2。",
),
),
InputNumber(
label="复读阈值",
name="repeat_threshold",
value="${repeat_threshold}",
visibleOn="${enable}",
min=2,
labelRemark=Remark(
shape="circle", content=f"跟随复读所需要的阈值有N个人复读后{NICKNAME}就会跟着复读。"
),
),
InputNumber(
label="打断复读概率",
name="break_probability",
value="${break_probability}",
min=0,
max=100,
suffix="%",
visibleOn="${AND(enable, speak_enable)}",
labelRemark=Remark(shape="circle", content="达到复读阈值时,打断复读而不是跟随复读的概率。"),
),
InputTag(
label="屏蔽词",
name="ban_words",
value="${ban_words}",
enableBatchAdd=True,
placeholder="添加屏蔽词",
visibleOn="${enable}",
joinValues=False,
extractValue=True,
labelRemark=Remark(shape="circle", content="含有这些词的消息不会学习和回复。(回车进行添加)"),
),
InputTag(
label="屏蔽用户",
source="${member_list}",
name="ban_users",
value="${ban_users}",
enableBatchAdd=True,
placeholder="添加屏蔽用户",
visibleOn="${enable}",
joinValues=False,
extractValue=True,
labelRemark=Remark(shape="circle", content="和该群中这些用户有关的消息不会学习和回复。(回车进行添加)"),
),
Switch(
label="主动发言开关",
name="speak_enable",
value="${speak_enable}",
visibleOn="${enable}",
labelRemark=Remark(
shape="circle",
content=f"是否允许{NICKNAME}在该群主动发言,主动发言是指每隔一段时间挑选一个热度较高的群,主动发一些学习过的内容。",
),
),
InputNumber(
label="主动发言阈值",
name="speak_threshold",
value="${speak_threshold}",
visibleOn="${AND(enable, speak_enable)}",
min=0,
labelRemark=Remark(shape="circle", content="值越低,主动发言的可能性越高。"),
),
InputNumber(
label="主动发言最小间隔",
name="speak_min_interval",
value="${speak_min_interval}",
min=0,
visibleOn="${AND(enable, speak_enable)}",
suffix="",
labelRemark=Remark(shape="circle", content="进行主动发言的最小时间间隔。"),
),
InputNumber(
label="连续主动发言概率",
name="speak_continuously_probability",
value="${speak_continuously_probability}",
min=0,
max=100,
suffix="%",
visibleOn="${AND(enable, speak_enable)}",
labelRemark=Remark(shape="circle", content="触发主动发言时,连续进行发言的概率。"),
),
InputNumber(
label="最大连续主动发言句数",
name="speak_continuously_max_len",
value="${speak_continuously_max_len}",
visibleOn="${AND(enable, speak_enable)}",
min=1,
labelRemark=Remark(shape="circle", content="连续主动发言的最大句数。"),
),
InputNumber(
label="主动发言附带戳一戳概率",
name="speak_poke_probability",
value="${speak_poke_probability}",
min=0,
max=100,
suffix="%",
visibleOn="${AND(enable, speak_enable)}",
labelRemark=Remark(
shape="circle", content="主动发言时附带戳一戳的概率会在最近5个发言者中随机选一个戳。"
),
),
],
actions=[
Action(label="保存", level=LevelEnum.success, type="submit"),
ActionType.Ajax(
label="保存至所有群",
level=LevelEnum.primary,
confirmText="确认将当前配置保存至所有群?",
api="post:/LittlePaimon/api/chat_group_config?group_id=all",
),
Action(label="重置", level=LevelEnum.warning, type="reset"),
],
actions=[Action(label='保存', level=LevelEnum.success, type='submit'),
ActionType.Ajax(
label='保存至所有群',
level=LevelEnum.primary,
confirmText='确认将当前配置保存至所有群?',
api='post:/LittlePaimon/api/chat_group_config?group_id=all'
),
Action(label='重置', level=LevelEnum.warning, type='reset')]
)
blacklist_table = TableCRUD(mode='table',
title='',
syncLocation=False,
api='/LittlePaimon/api/get_chat_blacklist',
interval=15000,
headerToolbar=[ActionType.Ajax(label='取消所有禁用',
level=LevelEnum.warning,
confirmText='确定要取消所有禁用吗?',
api='put:/LittlePaimon/api/delete_all?type=blacklist')],
itemActions=[ActionType.Ajax(tooltip='取消禁用',
icon='fa fa-check-circle-o text-info',
confirmText='取消该被禁用的内容/关键词,但是仍然需要重新学习哦!',
api='delete:/LittlePaimon/api/delete_chat?type=blacklist&id=${id}')
],
footable=True,
columns=[TableColumn(type='tpl', tpl='${keywords|truncate:20}', label='内容/关键词',
name='keywords',
searchable=True, popOver={'mode': 'dialog', 'title': '全文',
'className': 'break-all',
'body': {'type': 'tpl',
'tpl': '${keywords}'}}),
TableColumn(label='已禁用的群', name='bans', searchable=True),
])
message_table = TableCRUD(mode='table',
title='',
syncLocation=False,
api='/LittlePaimon/api/get_chat_messages',
interval=12000,
headerToolbar=[ActionType.Ajax(label='删除所有聊天记录',
level=LevelEnum.warning,
confirmText='确定要删除所有聊天记录吗?',
api='put:/LittlePaimon/api/delete_all?type=message')],
itemActions=[ActionType.Ajax(tooltip='禁用',
icon='fa fa-ban text-danger',
confirmText='禁用该聊天记录相关的学习内容和回复',
api='put:/LittlePaimon/api/ban_chat?type=message&id=${id}'),
ActionType.Ajax(tooltip='删除',
icon='fa fa-times text-danger',
confirmText='删除该条聊天记录',
api='delete:/LittlePaimon/api/delete_chat?type=message&id=${id}')
],
footable=True,
columns=[TableColumn(label='消息ID', name='message_id'),
TableColumn(label='群ID', name='group_id', searchable=True),
TableColumn(label='用户ID', name='user_id', searchable=True),
TableColumn(type='tpl', tpl='${raw_message|truncate:20}', label='消息', name='message',
searchable=True, popOver={'mode': 'dialog', 'title': '消息全文',
'className': 'break-all',
'body': {'type': 'tpl',
'tpl': '${raw_message}'}}),
TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}', label='时间',
name='time', sortable=True)
])
blacklist_table = TableCRUD(
mode="table",
title="",
syncLocation=False,
api="/LittlePaimon/api/get_chat_blacklist",
interval=15000,
headerToolbar=[
ActionType.Ajax(
label="取消所有禁用",
level=LevelEnum.warning,
confirmText="确定要取消所有禁用吗?",
api="put:/LittlePaimon/api/delete_all?type=blacklist",
)
],
itemActions=[
ActionType.Ajax(
tooltip="取消禁用",
icon="fa fa-check-circle-o text-info",
confirmText="取消该被禁用的内容/关键词,但是仍然需要重新学习哦!",
api="delete:/LittlePaimon/api/delete_chat?type=blacklist&id=${id}",
)
],
footable=True,
columns=[
TableColumn(
type="tpl",
tpl="${keywords|truncate:20}",
label="内容/关键词",
name="keywords",
searchable=True,
popOver={
"mode": "dialog",
"title": "全文",
"className": "break-all",
"body": {"type": "tpl", "tpl": "${keywords}"},
},
),
TableColumn(label="已禁用的群", name="bans", searchable=True),
],
)
message_table = TableCRUD(
mode="table",
title="",
syncLocation=False,
api="/LittlePaimon/api/get_chat_messages",
interval=12000,
headerToolbar=[
ActionType.Ajax(
label="删除所有聊天记录",
level=LevelEnum.warning,
confirmText="确定要删除所有聊天记录吗?",
api="put:/LittlePaimon/api/delete_all?type=message",
)
],
itemActions=[
ActionType.Ajax(
tooltip="禁用",
icon="fa fa-ban text-danger",
confirmText="禁用该聊天记录相关的学习内容和回复",
api="put:/LittlePaimon/api/ban_chat?type=message&id=${id}",
),
ActionType.Ajax(
tooltip="删除",
icon="fa fa-times text-danger",
confirmText="删除该条聊天记录",
api="delete:/LittlePaimon/api/delete_chat?type=message&id=${id}",
),
],
footable=True,
columns=[
TableColumn(label="消息ID", name="message_id"),
TableColumn(label="群ID", name="group_id", searchable=True),
TableColumn(label="用户ID", name="user_id", searchable=True),
TableColumn(
type="tpl",
tpl="${raw_message|truncate:20}",
label="消息",
name="message",
searchable=True,
popOver={
"mode": "dialog",
"title": "消息全文",
"className": "break-all",
"body": {"type": "tpl", "tpl": "${raw_message}"},
},
),
TableColumn(
type="tpl",
tpl="${time|date:YYYY-MM-DD HH\\:mm\\:ss}",
label="时间",
name="time",
sortable=True,
),
],
)
answer_table = TableCRUD(
mode='table',
mode="table",
syncLocation=False,
footable=True,
api='/LittlePaimon/api/get_chat_answers',
api="/LittlePaimon/api/get_chat_answers",
interval=12000,
headerToolbar=[ActionType.Ajax(label='删除所有已学习的回复',
level=LevelEnum.warning,
confirmText='确定要删除所有已学习的回复吗?',
api='put:/LittlePaimon/api/delete_all?type=answer')],
itemActions=[ActionType.Ajax(tooltip='禁用',
icon='fa fa-ban text-danger',
confirmText='禁用并删除该已学回复',
api='put:/LittlePaimon/api/ban_chat?type=answer&id=${id}'),
ActionType.Ajax(tooltip='删除',
icon='fa fa-times text-danger',
confirmText='仅删除该已学回复,不会禁用,所以依然能继续学',
api='delete:/LittlePaimon/api/delete_chat?type=answer&id=${id}')],
columns=[TableColumn(label='ID', name='id', visible=False),
TableColumn(label='群ID', name='group_id', searchable=True),
TableColumn(type='tpl', tpl='${keywords|truncate:20}', label='内容/关键词', name='keywords',
searchable=True, popOver={'mode': 'dialog', 'title': '内容全文', 'className': 'break-all',
'body': {'type': 'tpl', 'tpl': '${keywords}'}}),
TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}', label='最后学习时间', name='time',
sortable=True),
TableColumn(label='次数', name='count', sortable=True),
ColumnList(label='完整消息', name='messages', breakpoint='*', source='${messages}',
listItem=AmisList.Item(body={'name': 'msg'}))
])
headerToolbar=[
ActionType.Ajax(
label="删除所有已学习的回复",
level=LevelEnum.warning,
confirmText="确定要删除所有已学习的回复吗?",
api="put:/LittlePaimon/api/delete_all?type=answer",
)
],
itemActions=[
ActionType.Ajax(
tooltip="禁用",
icon="fa fa-ban text-danger",
confirmText="禁用并删除该已学回复",
api="put:/LittlePaimon/api/ban_chat?type=answer&id=${id}",
),
ActionType.Ajax(
tooltip="删除",
icon="fa fa-times text-danger",
confirmText="仅删除该已学回复,不会禁用,所以依然能继续学",
api="delete:/LittlePaimon/api/delete_chat?type=answer&id=${id}",
),
],
columns=[
TableColumn(label="ID", name="id", visible=False),
TableColumn(label="群ID", name="group_id", searchable=True),
TableColumn(
type="tpl",
tpl="${keywords|truncate:20}",
label="内容/关键词",
name="keywords",
searchable=True,
popOver={
"mode": "dialog",
"title": "内容全文",
"className": "break-all",
"body": {"type": "tpl", "tpl": "${keywords}"},
},
),
TableColumn(
type="tpl",
tpl="${time|date:YYYY-MM-DD HH\\:mm\\:ss}",
label="最后学习时间",
name="time",
sortable=True,
),
TableColumn(label="次数", name="count", sortable=True),
ColumnList(
label="完整消息",
name="messages",
breakpoint="*",
source="${messages}",
listItem=AmisList.Item(body=[AmisList.Item.ListBodyField(name="msg")]),
),
],
)
answer_table_on_context = TableCRUD(
mode='table',
mode="table",
syncLocation=False,
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=12000,
headerToolbar=[ActionType.Ajax(label='删除该内容所有回复',
level=LevelEnum.warning,
confirmText='确定要删除该条内容已学习的回复吗?',
api='put:/LittlePaimon/api/delete_all?type=answer&id=${id}')],
itemActions=[ActionType.Ajax(tooltip='禁用',
icon='fa fa-ban text-danger',
confirmText='禁用并删除该已学回复',
api='put:/LittlePaimon/api/ban_chat?type=answer&id=${id}'),
ActionType.Ajax(tooltip='删除',
icon='fa fa-times text-danger',
confirmText='仅删除该已学回复,但不禁用,依然能继续学',
api='delete:/LittlePaimon/api/delete_chat?type=answer&id=${id}')],
columns=[TableColumn(label='ID', name='id', visible=False),
TableColumn(label='群ID', name='group_id'),
TableColumn(type='tpl', tpl='${keywords|truncate:20}', label='内容/关键词', name='keywords',
searchable=True, popOver={'mode': 'dialog', 'title': '内容全文', 'className': 'break-all',
'body': {'type': 'tpl', 'tpl': '${keywords}'}}),
TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}', label='最后学习时间', name='time',
sortable=True),
TableColumn(label='次数', name='count', sortable=True),
ColumnList(label='完整消息', name='messages', breakpoint='*', source='${messages}',
listItem=AmisList.Item(body={'name': 'msg'}))
])
context_table = TableCRUD(mode='table',
title='',
syncLocation=False,
api='/LittlePaimon/api/get_chat_contexts',
interval=12000,
headerToolbar=[ActionType.Ajax(label='删除所有学习内容',
level=LevelEnum.warning,
confirmText='确定要删除所有已学习的内容吗?',
api='put:/LittlePaimon/api/delete_all?type=context')],
itemActions=[ActionType.Dialog(tooltip='回复列表',
icon='fa fa-book text-info',
dialog=Dialog(title='回复列表',
size='lg',
body=answer_table_on_context)),
ActionType.Ajax(tooltip='禁用',
icon='fa fa-ban text-danger',
confirmText='禁用并删除该学习的内容及其所有回复',
api='put:/LittlePaimon/api/ban_chat?type=context&id=${id}'),
ActionType.Ajax(tooltip='删除',
icon='fa fa-times text-danger',
confirmText='仅删除该学习的内容及其所有回复,但不禁用,依然能继续学',
api='delete:/LittlePaimon/api/delete_chat?type=context&id=${id}')
],
footable=True,
columns=[TableColumn(label='ID', name='id', visible=False),
TableColumn(type='tpl', tpl='${keywords|truncate:20}', label='内容/关键词',
name='keywords', searchable=True,
popOver={'mode': 'dialog', 'title': '内容全文', 'className': 'break-all',
'body': {'type': 'tpl', 'tpl': '${keywords}'}}),
TableColumn(type='tpl', tpl='${time|date:YYYY-MM-DD HH\\:mm\\:ss}',
label='最后学习时间', name='time', sortable=True),
TableColumn(label='已学次数', name='count', sortable=True),
])
headerToolbar=[
ActionType.Ajax(
label="删除该内容所有回复",
level=LevelEnum.warning,
confirmText="确定要删除该条内容已学习的回复吗?",
api="put:/LittlePaimon/api/delete_all?type=answer&id=${id}",
)
],
itemActions=[
ActionType.Ajax(
tooltip="禁用",
icon="fa fa-ban text-danger",
confirmText="禁用并删除该已学回复",
api="put:/LittlePaimon/api/ban_chat?type=answer&id=${id}",
),
ActionType.Ajax(
tooltip="删除",
icon="fa fa-times text-danger",
confirmText="仅删除该已学回复,但不禁用,依然能继续学",
api="delete:/LittlePaimon/api/delete_chat?type=answer&id=${id}",
),
],
columns=[
TableColumn(label="ID", name="id", visible=False),
TableColumn(label="群ID", name="group_id"),
TableColumn(
type="tpl",
tpl="${keywords|truncate:20}",
label="内容/关键词",
name="keywords",
searchable=True,
popOver={
"mode": "dialog",
"title": "内容全文",
"className": "break-all",
"body": {"type": "tpl", "tpl": "${keywords}"},
},
),
TableColumn(
type="tpl",
tpl="${time|date:YYYY-MM-DD HH\\:mm\\:ss}",
label="最后学习时间",
name="time",
sortable=True,
),
TableColumn(label="次数", name="count", sortable=True),
ColumnList(
label="完整消息",
name="messages",
breakpoint="*",
source="${messages}",
listItem=AmisList.Item(body=[AmisList.Item.ListBodyField(name="msg")]),
),
],
)
context_table = TableCRUD(
mode="table",
title="",
syncLocation=False,
api="/LittlePaimon/api/get_chat_contexts",
interval=12000,
headerToolbar=[
ActionType.Ajax(
label="删除所有学习内容",
level=LevelEnum.warning,
confirmText="确定要删除所有已学习的内容吗?",
api="put:/LittlePaimon/api/delete_all?type=context",
)
],
itemActions=[
ActionType.Dialog(
tooltip="回复列表",
icon="fa fa-book text-info",
dialog=Dialog(title="回复列表", size="lg", body=answer_table_on_context),
),
ActionType.Ajax(
tooltip="禁用",
icon="fa fa-ban text-danger",
confirmText="禁用并删除该学习的内容及其所有回复",
api="put:/LittlePaimon/api/ban_chat?type=context&id=${id}",
),
ActionType.Ajax(
tooltip="删除",
icon="fa fa-times text-danger",
confirmText="仅删除该学习的内容及其所有回复,但不禁用,依然能继续学",
api="delete:/LittlePaimon/api/delete_chat?type=context&id=${id}",
),
],
footable=True,
columns=[
TableColumn(label="ID", name="id", visible=False),
TableColumn(
type="tpl",
tpl="${keywords|truncate:20}",
label="内容/关键词",
name="keywords",
searchable=True,
popOver={
"mode": "dialog",
"title": "内容全文",
"className": "break-all",
"body": {"type": "tpl", "tpl": "${keywords}"},
},
),
TableColumn(
type="tpl",
tpl="${time|date:YYYY-MM-DD HH\\:mm\\:ss}",
label="最后学习时间",
name="time",
sortable=True,
),
TableColumn(label="已学次数", name="count", sortable=True),
],
)
message_page = PageSchema(url='/chat/messages', icon='fa fa-comments', label='群聊消息',
schema=Page(title='群聊消息', body=[
Alert(level=LevelEnum.info,
className='white-space-pre-wrap',
body=(f'此数据库记录了{NICKNAME}收到的除指令外的聊天记录。\n'
'· 点击"禁用"可以将某条聊天记录进行禁用,这样其相关的学习就会列入禁用列表。\n'
'· 点击"删除"可以删除某条记录,但不会影响它的学习。\n'
f'· 可以通过搜索{NICKNAME}的QQ号来查看它的回复记录。')),
message_table]))
context_page = PageSchema(url='/chat/contexts', icon='fa fa-comment', label='学习内容',
schema=Page(title='内容',
body=[Alert(level=LevelEnum.info,
className='white-space-pre-wrap',
body=(f'此数据库记录了{NICKNAME}所学习的内容。\n'
'· 点击"回复列表"可以查看该条内容已学习到的可能的回复。\n'
'· 点击"禁用"可以将该学习进行禁用,以后不会再学。\n'
'· 点击"删除"可以删除该学习,让它重新开始学习这句话。')),
context_table]))
answer_page = PageSchema(url='/chat/answers', icon='fa fa-commenting-o', label='内容回复',
schema=Page(title='回复',
body=[Alert(level=LevelEnum.info,
className='white-space-pre-wrap',
body=(f'此数据库记录了{NICKNAME}已学习到的所有回复,但看不到这些回复属于哪些内容,推荐到"学习内容"表进行操作。\n'
'· 点击"禁用"可以将该回复进行禁用,以后不会再学。\n'
'· 点击"删除"可以删除该回复,让它重新开始学习。')),
answer_table]))
blacklist_page = PageSchema(url='/chat/blacklist', icon='fa fa-ban', label='禁用列表',
schema=Page(title='禁用列表',
body=[Alert(level=LevelEnum.info,
className='white-space-pre-wrap',
body=f'此数据库记录了{NICKNAME}被禁用的内容/关键词。\n'
'· 可以取消禁用,使其能够重新继续学习。\n'
'· 不能在此添加禁用,只能在群中回复[不可以]或者在<配置>中添加屏蔽词来达到禁用效果。'),
blacklist_table]))
database_page = PageSchema(label='数据库', icon='fa fa-database',
children=[message_page, context_page, answer_page, blacklist_page])
config_page = PageSchema(url='/chat/configs', icon='fa fa-wrench', label='配置',
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])
message_page = PageSchema(
url="/chat/messages",
icon="fa fa-comments",
label="群聊消息",
schema=Page(
title="群聊消息",
body=[
Alert(
level=LevelEnum.info,
className="white-space-pre-wrap",
body=(
f"此数据库记录了{NICKNAME}收到的除指令外的聊天记录。\n"
'· 点击"禁用"可以将某条聊天记录进行禁用,这样其相关的学习就会列入禁用列表。\n'
'· 点击"删除"可以删除某条记录,但不会影响它的学习。\n'
f"· 可以通过搜索{NICKNAME}的QQ号来查看它的回复记录。"
),
),
message_table,
],
),
)
context_page = PageSchema(
url="/chat/contexts",
icon="fa fa-comment",
label="学习内容",
schema=Page(
title="内容",
body=[
Alert(
level=LevelEnum.info,
className="white-space-pre-wrap",
body=(
f"此数据库记录了{NICKNAME}所学习的内容。\n"
'· 点击"回复列表"可以查看该条内容已学习到的可能的回复。\n'
'· 点击"禁用"可以将该学习进行禁用,以后不会再学。\n'
'· 点击"删除"可以删除该学习,让它重新开始学习这句话。'
),
),
context_table,
],
),
)
answer_page = PageSchema(
url="/chat/answers",
icon="fa fa-commenting-o",
label="内容回复",
schema=Page(
title="回复",
body=[
Alert(
level=LevelEnum.info,
className="white-space-pre-wrap",
body=(
f'此数据库记录了{NICKNAME}已学习到的所有回复,但看不到这些回复属于哪些内容,推荐到"学习内容"表进行操作。\n'
'· 点击"禁用"可以将该回复进行禁用,以后不会再学。\n'
'· 点击"删除"可以删除该回复,让它重新开始学习。'
),
),
answer_table,
],
),
)
blacklist_page = PageSchema(
url="/chat/blacklist",
icon="fa fa-ban",
label="禁用列表",
schema=Page(
title="禁用列表",
interval=60000,
body=[
Alert(
level=LevelEnum.info,
className="white-space-pre-wrap",
body=f"此数据库记录了{NICKNAME}被禁用的内容/关键词。\n"
"· 可以取消禁用,使其能够重新继续学习。\n"
"· 不能在此添加禁用,只能在群中回复[不可以]或者在<配置>中添加屏蔽词来达到禁用效果。",
),
blacklist_table,
],
),
)
database_page = PageSchema(
label="数据库",
icon="fa fa-database",
children=[message_page, context_page, answer_page, blacklist_page],
)
config_page = PageSchema(
url="/chat/configs",
icon="fa fa-wrench",
label="配置",
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]
)
admin_app.pages[0].children.append(chat_page)