diff --git a/LittlePaimon/plugins/Learning_Chat/__init__.py b/LittlePaimon/plugins/Learning_Chat/__init__.py index 2ff2059..97fde20 100644 --- a/LittlePaimon/plugins/Learning_Chat/__init__.py +++ b/LittlePaimon/plugins/Learning_Chat/__init__.py @@ -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}将向群{event.group_id}回复"{answer}"') + logger.info( + "群聊学习", f'{NICKNAME}将向群{event.group_id}回复"{answer}"' + ) 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}向群{event.group_id}的回复"{answer}"发送失败,可能处于风控中') + logger.info( + "群聊学习", + f'{NICKNAME}向群{event.group_id}的回复"{answer}"发送失败,可能处于风控中', + ) -@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}向群{group_id}主动发言"{msg}"') - 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}向群{group_id}主动发言"{msg}"') + 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}向群{group_id}主动发言"{msg}"发送失败,可能处于风控中') + logger.info( + "群聊学习", + f'{NICKNAME}向群{group_id}主动发言"{msg}"发送失败,可能处于风控中', + ) diff --git a/LittlePaimon/plugins/Learning_Chat/config.py b/LittlePaimon/plugins/Learning_Chat/config.py index 8f4fd68..fa7b752 100644 --- a/LittlePaimon/plugins/Learning_Chat/config.py +++ b/LittlePaimon/plugins/Learning_Chat/config.py @@ -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(): diff --git a/LittlePaimon/plugins/Learning_Chat/handler.py b/LittlePaimon/plugins/Learning_Chat/handler.py index 676f59b..9d66c8d 100644 --- a/LittlePaimon/plugins/Learning_Chat/handler.py +++ b/LittlePaimon/plugins/Learning_Chat/handler.py @@ -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'➤该群{self.data.group_id}未开启群聊学习,跳过') + logger.debug("群聊学习", f"➤该群{self.data.group_id}未开启群聊学习,跳过") # 如果未开启群聊学习,跳过 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'➤发言人{self.data.user_id}在屏蔽列表中,跳过') + logger.debug("群聊学习", f"➤发言人{self.data.user_id}在屏蔽列表中,跳过") 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'群{self.data.group_id}{"开启" if result == Result.SetEnable else "关闭"}学习功能') - return [random.choice(ENABLE_WORDS if result == Result.SetEnable else DISABLE_WORDS)] + logger.info( + "群聊学习", + f'群{self.data.group_id}{"开启" 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'➤➤达到复读阈值,复读{messages[0].message}') + logger.debug("群聊学习", f"➤➤达到复读阈值,复读{messages[0].message}") 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'➤➤本次回复阈值为{answer_count_threshold},跨群阈值为{cross_group_threshold}') + logger.debug( + "群聊学习", + f"➤➤本次回复阈值为{answer_count_threshold},跨群阈值为{cross_group_threshold}", + ) # 获取满足跨群条件的回复 - 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'➤➤候选回复有{"|".join([f"""{a.keywords}({round(p, 3)})""" for a, p in answer_dict])}|不回复({round(per_list[-1], 3)})') + logger.debug( + "群聊学习", + f'➤➤候选回复有{"|".join([f"""{a.keywords}({round(p, 3)})""" for a, p in answer_dict])}|不回复({round(per_list[-1], 3)})', + ) - 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'➤➤将回复{result_message}') + logger.debug("群聊学习", f"➤➤将回复{result_message}") 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'待禁用消息{message_id}尝试撤回失败') - 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"待禁用消息{message_id}尝试撤回失败") + 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'待禁用消息{last_reply.message_id}尝试撤回失败') + logger.info("群聊学习", f"待禁用消息{last_reply.message_id}尝试撤回失败") 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'学习词{keywords}将被全局禁用') + logger.info("群聊学习", f"学习词{keywords}将被全局禁用") await ChatAnswer.filter(keywords=keywords).delete() else: - logger.info('群聊学习', f'群{self.data.group_id}禁用了学习词{keywords}') - await ChatAnswer.filter(keywords=keywords, group_id=self.data.group_id).delete() + logger.info( + "群聊学习", f"群{self.data.group_id}禁用了学习词{keywords}" + ) + await ChatAnswer.filter( + keywords=keywords, group_id=self.data.group_id + ).delete() else: # 没有屏蔽记录,则新建 - logger.info('群聊学习', f'群{self.data.group_id}禁用了学习词{keywords}') - 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"群{self.data.group_id}禁用了学习词{keywords}") + 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'学习词{data.keywords}将被全局禁用') + logger.info("群聊学习", f"学习词{data.keywords}将被全局禁用") await ChatAnswer.filter(keywords=data.keywords).delete() else: - logger.info('群聊学习', f'群{data.group_id}禁用了学习词{data.keywords}') - await ChatAnswer.filter(keywords=data.keywords, group_id=data.group_id).delete() + logger.info( + "群聊学习", f"群{data.group_id}禁用了学习词{data.keywords}" + ) + await ChatAnswer.filter( + keywords=data.keywords, group_id=data.group_id + ).delete() else: ban_word.global_ban = True - logger.info('群聊学习', f'学习词{data.keywords}将被全局禁用') + logger.info("群聊学习", f"学习词{data.keywords}将被全局禁用") await ChatAnswer.filter(keywords=data.keywords).delete() else: # 没有屏蔽记录,则新建 if isinstance(data, ChatMessage): - logger.info('群聊学习', f'群{data.group_id}禁用了学习词{data.keywords}') - 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"群{data.group_id}禁用了学习词{data.keywords}" + ) + 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'学习词{data.keywords}将被全局禁用') + logger.info("群聊学习", f"学习词{data.keywords}将被全局禁用") 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'主动发言:群热度排行{">>".join([str(g[0]) for g in popularity])}') + popularity: List[Tuple[int, List[ChatMessage]]] = sorted( + total_messages.items(), key=cmp_to_key(group_popularity_cmp), reverse=True + ) + logger.debug( + "群聊学习", f'主动发言:群热度排行{">>".join([str(g[0]) for g in popularity])}' + ) for group_id, messages in popularity: if len(messages) < 30: - logger.debug('群聊学习', f'主动发言:群{group_id}消息小于30条,不发言') + logger.debug("群聊学习", f"主动发言:群{group_id}消息小于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'主动发言:群{group_id}未开启,不发言') + logger.debug("群聊学习", f"主动发言:群{group_id}未开启,不发言") 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'主动发言:群{group_id}最后一条消息是{NICKNAME}发的{last_reply.message},不发言') + logger.debug( + "群聊学习", + f"主动发言:群{group_id}最后一条消息是{NICKNAME}发的{last_reply.message},不发言", + ) continue elif cur_time - last_reply.time < config.speak_min_interval: - logger.debug('群聊学习', f'主动发言:群{group_id}上次主动发言时间小于主动发言最小间隔,不发言') + logger.debug( + "群聊学习", f"主动发言:群{group_id}上次主动发言时间小于主动发言最小间隔,不发言" + ) 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'主动发言:群{group_id}已沉默时间({silent_time})小于阈值({int(threshold)}),不发言') + logger.debug( + "群聊学习", + f"主动发言:群{group_id}已沉默时间({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('[') and message.endswith(']'): + if message.startswith("[") and message.endswith( + "]" + ): 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('[') and message.endswith(']'): + if message.startswith("[") and message.endswith( + "]" + ): 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'主动发言:群{group_id}没有找到符合条件的发言,不发言') - logger.debug('群聊学习', '主动发言:没有符合条件的群,不主动发言') + logger.debug("群聊学习", f"主动发言:群{group_id}没有找到符合条件的发言,不发言") + 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'➤将被学习为{message.message}的回答,已学次数为{answer.count}') + 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"➤将被学习为{message.message}的回答,已学次数为{answer.count}" + ) 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('[') and raw_message.endswith(']'): + if raw_message.startswith("[") and raw_message.endswith("]"): 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: diff --git a/LittlePaimon/plugins/Learning_Chat/models.py b/LittlePaimon/plugins/Learning_Chat/models.py index 302b8fe..69adb74 100644 --- a/LittlePaimon/plugins/Learning_Chat/models.py +++ b/LittlePaimon/plugins/Learning_Chat/models.py @@ -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" +) diff --git a/LittlePaimon/plugins/Learning_Chat/web_api.py b/LittlePaimon/plugins/Learning_Chat/web_api.py index deac058..6b4503d 100644 --- a/LittlePaimon/plugins/Learning_Chat/web_api.py +++ b/LittlePaimon/plugins/Learning_Chat/web_api.py @@ -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) diff --git a/LittlePaimon/plugins/Learning_Chat/web_page.py b/LittlePaimon/plugins/Learning_Chat/web_page.py index 4f4e61e..4a4d112 100644 --- a/LittlePaimon/plugins/Learning_Chat/web_page.py +++ b/LittlePaimon/plugins/Learning_Chat/web_page.py @@ -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%概率为4,30%概率为3,10%概率为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%概率为4,30%概率为3,10%概率为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)