mirror of
https://github.com/xuthus83/LittlePaimon.git
synced 2024-10-21 16:27:15 +08:00
✨ 群聊学习
重构**(旧数据将不进行迁移)**,Web UI
增加群聊学习配置
、日志查看
和主题切换
,优化部分提示(如出现pydantic
相关报错,请更新amis-python
库)
This commit is contained in:
parent
d9398c2fb2
commit
8ab63be8a1
@ -1,24 +1,10 @@
|
||||
from pathlib import Path
|
||||
|
||||
from nonebot import load_plugins, get_driver, logger, load_plugin
|
||||
from typing import List
|
||||
from LittlePaimon import database
|
||||
from nonebot import load_plugins, logger
|
||||
from LittlePaimon import database, web
|
||||
from LittlePaimon.utils import DRIVER, __version__, NICKNAME, SUPERUSERS
|
||||
from LittlePaimon.utils.tool import check_resource
|
||||
|
||||
DRIVER = get_driver()
|
||||
__version__ = '3.0.0rc2'
|
||||
|
||||
try:
|
||||
SUPERUSERS: List[int] = [int(s) for s in DRIVER.config.superusers]
|
||||
except Exception:
|
||||
SUPERUSERS = []
|
||||
logger.warning('请在.env.prod文件中中配置超级用户SUPERUSERS')
|
||||
|
||||
try:
|
||||
NICKNAME: str = list(DRIVER.config.nickname)[0]
|
||||
except Exception:
|
||||
NICKNAME = '派蒙'
|
||||
|
||||
logo = """<g>
|
||||
██╗ ██╗████████╗████████╗██╗ ███████╗ ██████╗ █████╗ ██╗███╗ ███╗ ██████╗ ███╗ ██╗
|
||||
██║ ██║╚══██╔══╝╚══██╔══╝██║ ██╔════╝ ██╔══██╗██╔══██╗██║████╗ ████║██╔═══██╗████╗ ██║
|
||||
@ -32,11 +18,9 @@ logo = """<g>
|
||||
async def startup():
|
||||
logger.opt(colors=True).info(logo)
|
||||
await database.connect()
|
||||
from LittlePaimon import web
|
||||
await check_resource()
|
||||
|
||||
|
||||
DRIVER.on_shutdown(database.disconnect)
|
||||
|
||||
|
||||
load_plugins(str(Path(__file__).parent / 'plugins'))
|
||||
|
@ -1,3 +1,5 @@
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
@ -41,6 +43,7 @@ class ConfigModel(BaseModel):
|
||||
admin_enable: bool = Field(True, alias='启用Web端')
|
||||
admin_password: str = Field('admin', alias='Web端管理员密码')
|
||||
secret_key: str = Field('49c294d32f69b732ef6447c18379451ce1738922a75cd1d4812ef150318a2ed0', alias='Web端token密钥')
|
||||
admin_theme: Literal['default', 'antd', 'ang', 'dark'] = Field('default', alias='Web端主题')
|
||||
|
||||
@property
|
||||
def alias_dict(self):
|
||||
|
@ -29,7 +29,7 @@
|
||||
"10000039": ["迪奥娜", "dio娜", "猫猫", "冰猫"],
|
||||
"10000041": ["莫娜", "穷b", "占星术士", "半部讨龙真君"],
|
||||
"10000042": ["刻晴", "刻师傅", "阿晴", "刻猫猫", "牛杂师傅", "玉衡星"],
|
||||
"10000043": ["砂糖", "眼镜娘", "雷莹", "风荧"],
|
||||
"10000043": ["砂糖", "眼镜娘", "雷莹"],
|
||||
"10000044": ["辛焱", "黑妹", "摇滚"],
|
||||
"10000045": ["罗莎莉亚", "修女", "罗莎"],
|
||||
"10000046": ["胡桃", "whotao", "堂主"],
|
||||
@ -121,6 +121,7 @@
|
||||
"乐团剑"
|
||||
],
|
||||
"渔获": [
|
||||
"渔获",
|
||||
"鱼叉"
|
||||
],
|
||||
"衔珠海皇": [
|
||||
|
@ -8,8 +8,7 @@ from nonebot.matcher import Matcher
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.message import run_preprocessor
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent, PrivateMessageEvent, GroupMessageEvent
|
||||
from LittlePaimon import DRIVER, SUPERUSERS
|
||||
from LittlePaimon.utils import logger
|
||||
from LittlePaimon.utils import logger, DRIVER, SUPERUSERS
|
||||
from LittlePaimon.utils.path import PLUGIN_CONFIG
|
||||
from LittlePaimon.utils.files import load_yaml, save_yaml
|
||||
from LittlePaimon.database.models import PluginPermission, PluginStatistics
|
||||
|
@ -3,9 +3,8 @@ from pathlib import Path
|
||||
|
||||
from tortoise import Tortoise
|
||||
from nonebot.log import logger
|
||||
from LittlePaimon.utils import scheduler
|
||||
from LittlePaimon.utils.path import GENSHIN_DB_PATH, SUB_DB_PATH, GENSHIN_VOICE_DB_PATH, MANAGER_DB_PATH, \
|
||||
LEARNING_CHAT_DB_PATH, YSC_TEMP_IMG_PATH
|
||||
from LittlePaimon.utils import scheduler, logger as my_logger
|
||||
from LittlePaimon.utils.path import GENSHIN_DB_PATH, SUB_DB_PATH, GENSHIN_VOICE_DB_PATH, MANAGER_DB_PATH, YSC_TEMP_IMG_PATH
|
||||
from .models import *
|
||||
|
||||
DATABASE = {
|
||||
@ -25,11 +24,7 @@ DATABASE = {
|
||||
'manager': {
|
||||
"engine": "tortoise.backends.sqlite",
|
||||
"credentials": {"file_path": MANAGER_DB_PATH},
|
||||
},
|
||||
'learning_chat': {
|
||||
"engine": "tortoise.backends.sqlite",
|
||||
"credentials": {"file_path": LEARNING_CHAT_DB_PATH},
|
||||
},
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"genshin": {
|
||||
@ -50,10 +45,6 @@ DATABASE = {
|
||||
"manager": {
|
||||
"models": ['LittlePaimon.database.models.manager'],
|
||||
"default_connection": "manager",
|
||||
},
|
||||
"learning_chat": {
|
||||
"models": ['LittlePaimon.database.models.learning_chat'],
|
||||
"default_connection": "learning_chat",
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -104,18 +95,18 @@ async def daily_reset():
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
|
||||
logger.info('原神实时便签', '重置每日提醒次数限制')
|
||||
my_logger.info('原神实时便签', '重置每日提醒次数限制')
|
||||
await DailyNoteSub.all().update(today_remind_num=0)
|
||||
|
||||
logger.info('原神Cookie', '清空每日Cookie缓存和限制')
|
||||
my_logger.info('原神Cookie', '清空每日Cookie缓存和限制')
|
||||
await CookieCache.all().delete()
|
||||
await PublicCookie.filter(status=2).update(status=1)
|
||||
|
||||
logger.info('功能调用统计', '清除超过一个月的统计数据')
|
||||
my_logger.info('功能调用统计', '清除超过一个月的统计数据')
|
||||
await PluginStatistics.filter(time__lt=now - datetime.timedelta(days=30)).delete()
|
||||
|
||||
if now.weekday() == 0:
|
||||
logger.info('原神猜语音', '清空每周排行榜')
|
||||
my_logger.info('原神猜语音', '清空每周排行榜')
|
||||
await GuessVoiceRank.all().delete()
|
||||
|
||||
if YSC_TEMP_IMG_PATH.exists():
|
||||
|
@ -2,9 +2,8 @@ from nonebot import get_bot, on_command
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent, MessageSegment
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from LittlePaimon import DRIVER, SUPERUSERS
|
||||
from LittlePaimon.database import GeneralSub
|
||||
from LittlePaimon.utils import scheduler, logger
|
||||
from LittlePaimon.utils import scheduler, logger, DRIVER, SUPERUSERS
|
||||
from LittlePaimon.utils.message import CommandObjectID, CommandSwitch, CommandTime
|
||||
from .generate import *
|
||||
|
||||
|
@ -1,21 +1,19 @@
|
||||
import asyncio
|
||||
import random
|
||||
import re
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
import time
|
||||
|
||||
from nonebot import on_keyword, on_message, get_bot
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, GROUP, Message, ActionFailed
|
||||
from nonebot import on_message, get_bot
|
||||
from nonebot.adapters.onebot.v11 import GroupMessageEvent, GROUP, Message, ActionFailed
|
||||
from nonebot.params import Arg
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me, Rule
|
||||
from nonebot.rule import Rule
|
||||
from nonebot.typing import T_State
|
||||
|
||||
from LittlePaimon import NICKNAME, SUPERUSERS
|
||||
from LittlePaimon.utils import scheduler, logger
|
||||
from .api import is_shutup
|
||||
from .models import LearningChat
|
||||
from LittlePaimon.utils import scheduler, logger, NICKNAME
|
||||
from .handler import LearningChat
|
||||
from .models import ChatMessage
|
||||
from .config import config_manager
|
||||
from . import web_api, web_page
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name='群聊学习',
|
||||
@ -27,186 +25,63 @@ __plugin_meta__ = PluginMetadata(
|
||||
}
|
||||
)
|
||||
|
||||
message_id_lock = threading.Lock()
|
||||
message_id_dict = defaultdict(list)
|
||||
|
||||
|
||||
async def chat_rule(event: GroupMessageEvent, state: T_State) -> bool:
|
||||
if not config_manager.config.total_enable:
|
||||
return False
|
||||
if event.group_id in config_manager.config.ban_groups:
|
||||
return False
|
||||
if event.user_id in config_manager.config.ban_users:
|
||||
return False
|
||||
if not event.raw_message or event.raw_message.startswith('['):
|
||||
return False
|
||||
if any(w in event.raw_message for w in config_manager.config.ban_words):
|
||||
return False
|
||||
to_learn = True
|
||||
with message_id_lock:
|
||||
"""多账号登陆,且在同一群中时;避免一条消息被处理多次"""
|
||||
message_id = event.message_id
|
||||
group_id = event.group_id
|
||||
if group_id in message_id_dict and message_id in message_id_dict[group_id]:
|
||||
to_learn = False
|
||||
|
||||
message_id_dict[group_id].append(message_id)
|
||||
if len(message_id_dict[group_id]) > 100:
|
||||
message_id_dict[group_id] = message_id_dict[group_id][:-10]
|
||||
|
||||
chat = LearningChat(event)
|
||||
answers = await chat.answer()
|
||||
if to_learn:
|
||||
await chat.learn()
|
||||
if answers:
|
||||
async def ChatRule(event: GroupMessageEvent, state: T_State) -> bool:
|
||||
if answers := await LearningChat(event).answer():
|
||||
state['answers'] = answers
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def is_reply(event: GroupMessageEvent) -> bool:
|
||||
return bool(event.reply)
|
||||
|
||||
|
||||
learning_chat = on_message(priority=99, block=False, rule=Rule(chat_rule), permission=GROUP, state={
|
||||
learning_chat = on_message(priority=99, block=False, rule=Rule(ChatRule), permission=GROUP, state={
|
||||
'pm_name': '群聊学习',
|
||||
'pm_description': '(被动技能)bot会学习群友们的发言',
|
||||
'pm_usage': '群聊学习',
|
||||
'pm_priority': 1
|
||||
})
|
||||
ban_chat = on_keyword({'不可以', '达咩', '不行'}, rule=to_me(), priority=1, block=True, state={
|
||||
'pm_name': '群聊学习禁用',
|
||||
'pm_description': '如果bot说了不好的话,回复这句话,告诉TA不能这么说,需管理权限',
|
||||
'pm_usage': '@bot 不可以',
|
||||
'pm_priority': 2
|
||||
})
|
||||
set_enable = on_keyword({'学说话', '快学', '开启学习', '闭嘴', '别学', '关闭学习'}, rule=to_me(), priority=1, block=True, state={
|
||||
'pm_name': '群聊学习开关',
|
||||
'pm_description': '开启或关闭当前群的学习能力,需管理权限',
|
||||
'pm_usage': '@bot 开启|关闭学习',
|
||||
'pm_priority': 3
|
||||
})
|
||||
# set_config = on_command('chat', aliases={'群聊学习设置'}, permission=SUPERUSER, priority=1, block=True, state={
|
||||
# 'pm_name': 'ysbc',
|
||||
# 'pm_description': '查询已绑定的原神cookie情况',
|
||||
# 'pm_usage': 'ysbc',
|
||||
# 'pm_priority': 2
|
||||
# })
|
||||
|
||||
|
||||
# ban_msg_latest = on_fullmatch(msg=('不可以发这个', '不能发这个', '达咩达咩'), rule=to_me(), priority=1, block=True, permission=GROUP_OWNER | GROUP_ADMIN | SUPERUSER)
|
||||
|
||||
|
||||
@learning_chat.handle()
|
||||
async def _(event: GroupMessageEvent, answers=Arg('answers')):
|
||||
for item in answers:
|
||||
logger.info('群聊学习', f'{NICKNAME}即将向群<m>{event.group_id}</m>发送<m>"{item}"</m>')
|
||||
await asyncio.sleep(random.randint(1, 3))
|
||||
for answer in answers:
|
||||
await asyncio.sleep(random.randint(1, 2))
|
||||
try:
|
||||
await learning_chat.send(Message(item))
|
||||
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())
|
||||
except ActionFailed:
|
||||
if not await is_shutup(event.self_id, event.group_id):
|
||||
# Bot没用在禁言中但发送失败,说明该条消息被风控,禁用调
|
||||
logger.info('群聊学习', f'{NICKNAME}将群<m>{event.group_id}</m>的发言<m>"{item}"</m>列入禁用列表')
|
||||
await LearningChat.ban(event.group_id, event.self_id,
|
||||
str(item), 'ActionFailed')
|
||||
break
|
||||
logger.info('群聊学习', f'{NICKNAME}向群<m>{event.group_id}</m>的回复<m>"{answer}"</m>发送<r>失败,可能处于风控中</r>')
|
||||
|
||||
|
||||
@ban_chat.handle()
|
||||
async def _(event: GroupMessageEvent):
|
||||
if event.sender.role not in ['admin', 'owner'] and event.user_id not in SUPERUSERS:
|
||||
await ban_chat.finish(random.choice([f'{NICKNAME}就喜欢说这个,哼!', f'你管得着{NICKNAME}吗!']))
|
||||
if event.reply:
|
||||
raw_message = ''
|
||||
for item in event.reply.message:
|
||||
raw_reply = str(item)
|
||||
# 去掉图片消息中的 url, subType 等字段
|
||||
raw_message += re.sub(r'(\[CQ:.+)(,url=*)(])',
|
||||
r'\1\2', raw_reply)
|
||||
logger.info('群聊学习', f'{NICKNAME}将群<m>{event.group_id}</m>的发言<m>"{raw_message}"</m>列入禁用列表')
|
||||
|
||||
if await LearningChat.ban(event.group_id, event.self_id, raw_message, str(event.user_id)):
|
||||
await ban_chat.finish(
|
||||
random.choice([f'{NICKNAME}知道错了...达咩!', f'{NICKNAME}不会再这么说了...', f'果面呐噻,{NICKNAME}说错话了...']))
|
||||
else:
|
||||
logger.info('群聊学习', f'{NICKNAME}将群<m>{event.group_id}</m>的最后一条发言列入禁用列表')
|
||||
|
||||
if await LearningChat.ban(event.group_id, event.self_id, '', str(event.user_id)):
|
||||
await ban_chat.finish(
|
||||
random.choice([f'{NICKNAME}知道错了...达咩!', f'{NICKNAME}不会再这么说了...', f'果面呐噻,{NICKNAME}说错话了...']))
|
||||
|
||||
|
||||
@set_enable.handle()
|
||||
async def _(event: MessageEvent):
|
||||
if event.user_id in SUPERUSERS:
|
||||
if any(w in event.raw_message for w in {'学说话', '快学', '开启学习'}):
|
||||
if config_manager.config.total_enable:
|
||||
msg = f'{NICKNAME}已经在努力尝试看懂你们说的话了!'
|
||||
else:
|
||||
config_manager.config.total_enable = True
|
||||
msg = f'{NICKNAME}会尝试学你们说怪话!'
|
||||
elif config_manager.config.total_enable:
|
||||
config_manager.config.total_enable = False
|
||||
msg = f'好好好,{NICKNAME}不学说话就是了!'
|
||||
else:
|
||||
msg = f'{NICKNAME}明明没有在学你们说话!'
|
||||
elif isinstance(event, GroupMessageEvent) and event.sender.role in {'admin', 'owner'}:
|
||||
if any(w in event.raw_message for w in {'学说话', '快学', '开启学习'}):
|
||||
if event.group_id in config_manager.config.ban_groups:
|
||||
config_manager.config.ban_groups.remove(event.group_id)
|
||||
msg = f'{NICKNAME}会尝试学你们说怪话!'
|
||||
else:
|
||||
msg = f'{NICKNAME}已经在努力尝试看懂你们说的话了!'
|
||||
elif event.group_id not in config_manager.config.ban_groups:
|
||||
config_manager.config.ban_groups.append(event.group_id)
|
||||
msg = f'好好好,{NICKNAME}不学说话就是了!'
|
||||
else:
|
||||
msg = f'{NICKNAME}明明没有在学你们说话!'
|
||||
else:
|
||||
msg = random.choice([f'你管得着{NICKNAME}吗!', f'你可没有权限要求{NICKNAME}!'])
|
||||
config_manager.save()
|
||||
await set_enable.finish(msg)
|
||||
|
||||
|
||||
# @set_config.handle()
|
||||
# async def _(event: MessageEvent, state: T_State, msg: Message = CommandArg()):
|
||||
# state['config_list'] = config_manager.config_list
|
||||
# configs_str = '\n'.join([f'{k}: {v}' for k, v in config_manager.config.dict(by_alias=True).items()])
|
||||
# if msg:
|
||||
# msg = msg.extract_plain_text().strip().split(' ')
|
||||
# if state['key'] in state['config_list']:
|
||||
# state['key'] = msg[0]
|
||||
# if len(msg) > 1:
|
||||
# state['value'] = msg[1]
|
||||
# else:
|
||||
# state['msg'] = '没有叫'
|
||||
|
||||
# @ban_msg_latest.handle()
|
||||
# async def _(event: GroupMessageEvent):
|
||||
# logger.info('群聊学习', f'{NICKNAME}将群<m>{event.group_id}</m>的最后一条发言列入禁用列表')
|
||||
#
|
||||
# if await LearningChat.ban(event.group_id, event.self_id, '', str(event.user_id)):
|
||||
# msg_send = ['派蒙知道错了...达咩!', '派蒙不会再这么说了...', '果面呐噻,派蒙说错话了...']
|
||||
# await ban_msg_latest.finish(random.choice(msg_send))
|
||||
|
||||
|
||||
@scheduler.scheduled_job('interval', seconds=5, misfire_grace_time=5)
|
||||
@scheduler.scheduled_job('interval', minutes=1, misfire_grace_time=5)
|
||||
async def speak_up():
|
||||
if not config_manager.config.total_enable:
|
||||
return
|
||||
if not (ret := await LearningChat.speak()):
|
||||
try:
|
||||
bot = get_bot()
|
||||
except ValueError:
|
||||
return
|
||||
bot_id, group_id, messages = ret
|
||||
if group_id in config_manager.config.ban_groups:
|
||||
if not (speak := await LearningChat.speak(int(bot.self_id))):
|
||||
return
|
||||
group_id, messages = speak
|
||||
for msg in messages:
|
||||
logger.info('群聊学习', f'{NICKNAME}即将向群<m>{group_id}</m>发送<m>"{msg}"</m>')
|
||||
await get_bot(str(bot_id)).send_group_msg(group_id=group_id, message=Message(msg))
|
||||
await asyncio.sleep(random.randint(2, 4))
|
||||
|
||||
|
||||
@scheduler.scheduled_job('cron', hour='4')
|
||||
async def update_data():
|
||||
if config_manager.config.total_enable:
|
||||
await LearningChat.clear_up_context()
|
||||
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())
|
||||
await asyncio.sleep(random.randint(2, 4))
|
||||
except ActionFailed:
|
||||
logger.info('群聊学习', f'{NICKNAME}向群<m>{group_id}</m>主动发言<m>"{msg}"</m><r>发送失败,可能处于风控中</r>')
|
||||
|
@ -1,16 +0,0 @@
|
||||
import time
|
||||
|
||||
from nonebot import get_bot
|
||||
from nonebot.adapters.onebot.v11 import Bot
|
||||
|
||||
|
||||
async def is_shutup(self_id: int, group_id: int) -> bool:
|
||||
"""
|
||||
判断账号是否在禁言
|
||||
:param self_id: 自身id
|
||||
:param group_id: 群id
|
||||
"""
|
||||
bot: Bot = get_bot(str(self_id))
|
||||
info = await bot.get_group_member_info(user_id=self_id, group_id=group_id)
|
||||
|
||||
return info['shut_up_timestamp'] > int(time.time())
|
@ -1,4 +1,4 @@
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@ -6,23 +6,41 @@ from LittlePaimon.utils.path import LEARNING_CHAT_CONFIG
|
||||
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='主动发言附带戳一戳概率')
|
||||
|
||||
def update(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
if key in self.__fields__:
|
||||
self.__setattr__(key, value)
|
||||
|
||||
|
||||
class ChatConfig(BaseModel):
|
||||
total_enable: bool = Field(True, alias='群聊学习总开关')
|
||||
ban_words: List[str] = Field([], alias='屏蔽词')
|
||||
ban_groups: List[int] = Field([], alias='屏蔽群')
|
||||
ban_users: List[int] = Field([], alias='屏蔽用户')
|
||||
ban_words: List[str] = Field([], alias='全局屏蔽词')
|
||||
ban_users: List[int] = Field([], alias='全局屏蔽用户')
|
||||
KEYWORDS_SIZE: int = Field(3, alias='单句关键词分词数量')
|
||||
answer_threshold: int = Field(4, alias='发言阈值')
|
||||
answer_threshold_weights: List[int] = Field([10, 30, 60], alias='发言阈值权重')
|
||||
cross_group_threshold: int = Field(2, alias='跨群回复阈值')
|
||||
repeat_threshold: int = Field(3, alias='复读阈值')
|
||||
speak_threshold: int = Field(5, alias='主动发言阈值')
|
||||
split_probability: float = Field(0.5, alias='按逗号分割回复概率')
|
||||
speak_continuously_probability: float = Field(0.5, alias='连续主动发言概率')
|
||||
speak_poke_probability: float = Field(0.5, alias='主动发言附带戳一戳概率')
|
||||
speak_continuously_max_len: int = Field(3, alias='最大连续说话句数')
|
||||
save_time_threshold: int = Field(3600, alias='持久化间隔秒数')
|
||||
save_count_threshold: int = Field(1000, 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='分群配置')
|
||||
|
||||
def update(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
if key in self.__fields__:
|
||||
self.__setattr__(key, value)
|
||||
|
||||
|
||||
class ChatConfigManager:
|
||||
@ -35,6 +53,12 @@ class ChatConfigManager:
|
||||
self.config = ChatConfig()
|
||||
self.save()
|
||||
|
||||
def get_group_config(self, group_id: int) -> ChatGroupConfig:
|
||||
if group_id not in self.config.group_config:
|
||||
self.config.group_config[group_id] = ChatGroupConfig()
|
||||
self.save()
|
||||
return self.config.group_config[group_id]
|
||||
|
||||
@property
|
||||
def config_list(self) -> List[str]:
|
||||
return list(self.config.dict(by_alias=True).keys())
|
||||
@ -42,8 +66,5 @@ class ChatConfigManager:
|
||||
def save(self):
|
||||
save_yaml(self.config.dict(by_alias=True), self.file_path)
|
||||
|
||||
# def set_config(self, config_name: str, value: any):
|
||||
# if config_name not in self.config.dict(by_alias=True).keys():
|
||||
|
||||
|
||||
config_manager = ChatConfigManager()
|
||||
|
656
LittlePaimon/plugins/Learning_Chat/genshin_word.txt
Normal file
656
LittlePaimon/plugins/Learning_Chat/genshin_word.txt
Normal file
@ -0,0 +1,656 @@
|
||||
角色
|
||||
神里绫华
|
||||
绫华
|
||||
白鹭公主
|
||||
0华
|
||||
凌华
|
||||
琴
|
||||
琴团长
|
||||
空
|
||||
空哥
|
||||
龙哥
|
||||
丽莎
|
||||
魔女
|
||||
荧
|
||||
荧妹
|
||||
爷
|
||||
妹妹
|
||||
主角
|
||||
芭芭拉
|
||||
内鬼
|
||||
凯亚
|
||||
凝冰渡海真君
|
||||
迪卢克
|
||||
卢姥爷
|
||||
姥爷
|
||||
卢锅巴
|
||||
正义人
|
||||
雷泽
|
||||
狼崽
|
||||
卢皮卡
|
||||
安柏
|
||||
打火姬
|
||||
温迪
|
||||
巴巴托斯
|
||||
风神
|
||||
芭芭脱丝
|
||||
卖唱
|
||||
香菱
|
||||
卯师傅
|
||||
锅巴
|
||||
北斗
|
||||
龙王
|
||||
行秋
|
||||
水神
|
||||
秋秋人
|
||||
魈
|
||||
金鹏
|
||||
夜叉
|
||||
三眼五显仙人
|
||||
降魔大圣
|
||||
靖妖傩舞
|
||||
凝光
|
||||
富婆
|
||||
天权星
|
||||
可莉
|
||||
哒哒哒
|
||||
炸弹人
|
||||
火花骑士
|
||||
嘟嘟可
|
||||
钟离
|
||||
帝君
|
||||
岩神
|
||||
摩拉克斯
|
||||
岩王爷
|
||||
岩王帝君
|
||||
菲谢尔
|
||||
皇女
|
||||
奥兹
|
||||
乌鸦
|
||||
中二少女
|
||||
小艾咪
|
||||
班尼特
|
||||
点赞哥
|
||||
达达利亚
|
||||
公子
|
||||
愚人众
|
||||
阿贾克斯
|
||||
达达鸭
|
||||
诺艾尔
|
||||
女仆
|
||||
高达
|
||||
岩王帝姬
|
||||
七七
|
||||
77
|
||||
肚饿真君
|
||||
重云
|
||||
甘雨
|
||||
王小美
|
||||
椰羊
|
||||
椰奶
|
||||
阿贝多
|
||||
炼金术士
|
||||
迪奥娜
|
||||
dio娜
|
||||
猫猫
|
||||
莫娜
|
||||
占星术士
|
||||
半部讨龙真君
|
||||
刻晴
|
||||
刻师傅
|
||||
阿晴
|
||||
刻猫猫
|
||||
牛杂师傅
|
||||
玉衡星
|
||||
砂糖
|
||||
辛焱
|
||||
黑妹
|
||||
罗莎莉亚
|
||||
修女
|
||||
罗莎
|
||||
胡桃
|
||||
whotao
|
||||
堂主
|
||||
枫原万叶
|
||||
万叶
|
||||
叶天帝
|
||||
烟绯
|
||||
罗翔
|
||||
宵宫
|
||||
霄宫
|
||||
托马
|
||||
优菈
|
||||
优拉
|
||||
浪花骑士
|
||||
尤拉
|
||||
雷电将军
|
||||
雷神
|
||||
巴尔
|
||||
巴尔泽布
|
||||
雷军
|
||||
雷电影
|
||||
影
|
||||
煮饭婆
|
||||
奶香一刀
|
||||
早柚
|
||||
忍者
|
||||
终末番
|
||||
珊瑚宫心海
|
||||
心海
|
||||
观赏鱼
|
||||
五郎
|
||||
修狗
|
||||
希娜小姐
|
||||
九条裟罗
|
||||
九条
|
||||
荒泷一斗
|
||||
一斗
|
||||
斗子哥
|
||||
八重神子
|
||||
神子
|
||||
八重
|
||||
屑狐狸
|
||||
埃洛伊
|
||||
申鹤
|
||||
云堇
|
||||
云先生
|
||||
云瑾
|
||||
神里绫人
|
||||
0人
|
||||
绫人
|
||||
凌人
|
||||
神里凌人
|
||||
夜兰
|
||||
久岐忍
|
||||
阿卡丽
|
||||
鹿野院平藏
|
||||
平藏
|
||||
小鹿
|
||||
柯莱
|
||||
多莉
|
||||
提纳里
|
||||
小提
|
||||
妮露
|
||||
赛诺
|
||||
风纪官
|
||||
坎蒂丝
|
||||
潘森
|
||||
纳西妲
|
||||
草神
|
||||
小吉祥草王
|
||||
草萝莉
|
||||
莱依拉
|
||||
散兵
|
||||
伞兵
|
||||
国崩
|
||||
卢本伟
|
||||
sb
|
||||
流浪者
|
||||
迪希雅
|
||||
艾尔海森
|
||||
白术
|
||||
|
||||
武器
|
||||
磐岩结绿
|
||||
绿箭
|
||||
绿剑
|
||||
斫峰之刃
|
||||
斫峰
|
||||
盾剑
|
||||
无工之剑
|
||||
无工
|
||||
贯虹之槊
|
||||
贯虹
|
||||
岩枪
|
||||
盾枪
|
||||
赤角石溃杵
|
||||
赤角
|
||||
石溃杵
|
||||
尘世之锁
|
||||
盾书
|
||||
终末嗟叹之诗
|
||||
终末
|
||||
终末弓
|
||||
乐团弓
|
||||
松籁响起之时
|
||||
松籁
|
||||
乐团大剑
|
||||
松剑
|
||||
苍古自由之誓
|
||||
苍古
|
||||
乐团剑
|
||||
渔获
|
||||
鱼叉
|
||||
衔珠海皇
|
||||
咸鱼大剑
|
||||
匣里日月
|
||||
日月
|
||||
匣里灭辰
|
||||
灭辰
|
||||
匣里龙吟
|
||||
龙吟
|
||||
天空之翼
|
||||
天空弓
|
||||
天空之刃
|
||||
天空剑
|
||||
天空之卷
|
||||
天空书
|
||||
天空之脊
|
||||
天空枪
|
||||
薄荷枪
|
||||
天空之傲
|
||||
天空大剑
|
||||
四风原典
|
||||
四风
|
||||
试作斩岩
|
||||
斩岩
|
||||
试作星镰
|
||||
星镰
|
||||
试作金珀
|
||||
金珀
|
||||
试作古华
|
||||
古华
|
||||
试作澹月
|
||||
澹月
|
||||
千岩长枪
|
||||
千岩枪
|
||||
千岩古剑
|
||||
千岩剑
|
||||
千岩大剑
|
||||
暗巷闪光
|
||||
暗巷剑
|
||||
暗巷猎手
|
||||
暗巷弓
|
||||
阿莫斯之弓
|
||||
阿莫斯
|
||||
痛苦弓
|
||||
雾切之回光
|
||||
雾切
|
||||
飞雷之弦振
|
||||
飞雷
|
||||
飞雷弓
|
||||
薙草之稻光
|
||||
薙草
|
||||
稻光
|
||||
薙草稻光
|
||||
马尾枪
|
||||
马尾
|
||||
薙刀
|
||||
神乐之真意
|
||||
神乐
|
||||
真意
|
||||
狼的末路
|
||||
狼末
|
||||
护摩之杖
|
||||
护摩
|
||||
和璞鸢
|
||||
鸟枪
|
||||
绿枪
|
||||
风鹰剑
|
||||
风鹰
|
||||
冬极白星
|
||||
冬极
|
||||
不灭月华
|
||||
月华
|
||||
波乱月白经津
|
||||
波乱
|
||||
波波津
|
||||
若水
|
||||
麒麟弓
|
||||
昭心
|
||||
糟心
|
||||
幽夜华尔兹
|
||||
幽夜
|
||||
雪葬的星银
|
||||
雪葬
|
||||
雪葬星银
|
||||
雪山大剑
|
||||
喜多院十文字
|
||||
喜多院
|
||||
十文字
|
||||
万国诸海图谱
|
||||
万国
|
||||
万国诸海
|
||||
天目影打刀
|
||||
天目刀
|
||||
天目
|
||||
破魔之弓
|
||||
破魔弓
|
||||
曚云之月
|
||||
曚云弓
|
||||
流月针
|
||||
流浪乐章
|
||||
赌狗书
|
||||
赌狗乐章
|
||||
赌狗
|
||||
桂木斩长正
|
||||
桂木
|
||||
斩长正
|
||||
腐殖之剑
|
||||
腐殖
|
||||
腐殖剑
|
||||
风花之颂
|
||||
风花弓
|
||||
证誓之明瞳
|
||||
证誓
|
||||
明瞳
|
||||
证誓明瞳
|
||||
嘟嘟可故事集
|
||||
嘟嘟可
|
||||
辰砂之纺锤
|
||||
辰砂
|
||||
纺锤
|
||||
白辰之环
|
||||
白辰
|
||||
决斗之枪
|
||||
决斗枪
|
||||
决斗
|
||||
月卡枪
|
||||
螭骨剑
|
||||
螭骨
|
||||
丈育剑
|
||||
离骨剑
|
||||
月卡大剑
|
||||
黑剑
|
||||
月卡剑
|
||||
苍翠猎弓
|
||||
绿弓
|
||||
月卡弓
|
||||
讨龙英杰谭
|
||||
讨龙
|
||||
神射手之誓
|
||||
神射手
|
||||
黑缨枪
|
||||
史莱姆枪
|
||||
「渔获」
|
||||
以理服人
|
||||
佣兵重剑
|
||||
信使
|
||||
冷刃
|
||||
历练的猎弓
|
||||
反曲弓
|
||||
口袋魔导书
|
||||
吃虎鱼刀
|
||||
学徒笔记
|
||||
宗室大剑
|
||||
宗室猎枪
|
||||
宗室秘法录
|
||||
宗室长剑
|
||||
宗室长弓
|
||||
异世界行记
|
||||
弓藏
|
||||
弹弓
|
||||
忍冬之果
|
||||
息灾
|
||||
恶王丸
|
||||
掠食者
|
||||
断浪长鳍
|
||||
新手长枪
|
||||
旅行剑
|
||||
无锋剑
|
||||
暗巷的酒与诗
|
||||
暗铁剑
|
||||
沐浴龙血的剑
|
||||
猎弓
|
||||
甲级宝珏
|
||||
白影剑
|
||||
白缨枪
|
||||
白铁大剑
|
||||
祭礼剑
|
||||
祭礼大剑
|
||||
祭礼弓
|
||||
祭礼残章
|
||||
笛剑
|
||||
绝弦
|
||||
翡玉法球
|
||||
落霞
|
||||
西风剑
|
||||
西风大剑
|
||||
西风猎弓
|
||||
西风秘典
|
||||
西风长枪
|
||||
训练大剑
|
||||
钟剑
|
||||
钢轮弓
|
||||
钺矛
|
||||
铁尖枪
|
||||
铁影阔剑
|
||||
铁蜂刺
|
||||
银剑
|
||||
降临之剑
|
||||
雨裁
|
||||
飞天大御剑
|
||||
飞天御剑
|
||||
魔导绪论
|
||||
鸦羽弓
|
||||
黎明神剑
|
||||
黑岩刺枪
|
||||
黑岩枪
|
||||
黑岩战弓
|
||||
黑岩弓
|
||||
黑岩斩刀
|
||||
黑岩绯玉
|
||||
黑岩长剑
|
||||
龙脊长枪
|
||||
笼钓瓶一心
|
||||
万叶刀
|
||||
一心传名刀
|
||||
猎人之径
|
||||
绿弓
|
||||
草弓
|
||||
提纳里专武
|
||||
竭泽
|
||||
鱼弓
|
||||
王下近侍
|
||||
须弥锻造弓
|
||||
贯月矢
|
||||
须弥锻造长枪
|
||||
盈满之实
|
||||
须弥锻造法器
|
||||
森林王器
|
||||
须弥锻造大剑
|
||||
原木刀
|
||||
须弥锻造单手剑
|
||||
圣显之钥
|
||||
圣显
|
||||
不灭剑华
|
||||
妮露武器
|
||||
西福斯的月光
|
||||
西福斯
|
||||
月光
|
||||
赤沙之杖
|
||||
赤沙
|
||||
风信之锋
|
||||
风信
|
||||
玛海菈的水色
|
||||
玛海菈
|
||||
水色
|
||||
千夜浮梦
|
||||
千夜
|
||||
神灯
|
||||
茶壶
|
||||
流浪的晚星
|
||||
晚星
|
||||
|
||||
圣遗物
|
||||
游医
|
||||
冒险家
|
||||
幸运儿
|
||||
学士
|
||||
战狂
|
||||
赌徒
|
||||
武人
|
||||
守护之心
|
||||
流放者
|
||||
行者之心
|
||||
奇迹
|
||||
勇士之心
|
||||
教官
|
||||
如雷的盛怒
|
||||
如雷
|
||||
追忆之注连
|
||||
追忆
|
||||
冰风迷途的勇士
|
||||
冰套
|
||||
染血的骑士道
|
||||
染血
|
||||
饰金之梦生
|
||||
饰金
|
||||
精通
|
||||
华馆梦醒形骸记
|
||||
华馆
|
||||
防御
|
||||
昔日宗室之仪
|
||||
宗室
|
||||
沉沦之心
|
||||
水套
|
||||
悠古的磐岩
|
||||
岩套
|
||||
海染砗磲
|
||||
海染
|
||||
毒奶
|
||||
翠绿之影
|
||||
风套
|
||||
苍白之火
|
||||
苍白
|
||||
物理
|
||||
流浪大地的乐团
|
||||
流浪
|
||||
逆飞的流星
|
||||
逆飞
|
||||
流星
|
||||
平息鸣雷的尊者
|
||||
平雷
|
||||
辰砂往生录
|
||||
辰砂
|
||||
掉血
|
||||
渡过烈火的贤人
|
||||
渡火
|
||||
千岩牢固
|
||||
千岩
|
||||
生命
|
||||
被怜爱的少女
|
||||
治疗
|
||||
少女
|
||||
来歆余响
|
||||
普攻
|
||||
余响
|
||||
炽烈的炎之魔女
|
||||
火套
|
||||
魔女
|
||||
绝缘之旗印
|
||||
充能
|
||||
绝缘
|
||||
角斗士的终幕礼
|
||||
角斗
|
||||
深林的记忆
|
||||
草套
|
||||
礼冠
|
||||
|
||||
敌人
|
||||
丘丘人
|
||||
盗宝团
|
||||
史莱姆
|
||||
飘浮灵
|
||||
骗骗花
|
||||
愚人众
|
||||
野伏众
|
||||
丘丘暴徒
|
||||
深渊法师
|
||||
债务处理人
|
||||
萤术士
|
||||
遗迹机兵
|
||||
遗迹重机
|
||||
遗迹猎者
|
||||
遗迹守卫
|
||||
幼岩龙蜥
|
||||
岩龙蜥
|
||||
丘丘王
|
||||
大雪猪王
|
||||
狂风之核
|
||||
藏镜侍女
|
||||
兽境之狼
|
||||
无相
|
||||
黄金王兽
|
||||
古岩龙蜥
|
||||
爆炎树
|
||||
急冻树
|
||||
纯水精灵
|
||||
雷音权现
|
||||
冰雾花
|
||||
烈焰花
|
||||
|
||||
物品
|
||||
原石
|
||||
摩拉
|
||||
相遇之缘
|
||||
蓝球
|
||||
纠缠之缘
|
||||
粉球
|
||||
创世结晶
|
||||
凝取结晶
|
||||
648
|
||||
328
|
||||
198
|
||||
月卡
|
||||
大月卡
|
||||
原粹树脂
|
||||
脆弱树脂
|
||||
浓缩树脂
|
||||
树脂
|
||||
秘境
|
||||
七天神像
|
||||
传送锚点
|
||||
尘歌壶
|
||||
七圣召唤
|
||||
|
||||
地名
|
||||
璃月
|
||||
蒙德
|
||||
龙脊雪山
|
||||
稻妻
|
||||
渊下宫
|
||||
须弥
|
||||
|
||||
战斗系统
|
||||
元素反应
|
||||
蒸发
|
||||
融化
|
||||
冻结
|
||||
感电
|
||||
超载
|
||||
结晶
|
||||
扩散
|
||||
燃烧
|
||||
绽放
|
||||
超绽放
|
||||
烈绽放
|
||||
激化
|
||||
超激化
|
||||
蔓激化
|
||||
|
||||
属性
|
||||
攻击力
|
||||
防御力
|
||||
生命值
|
||||
暴击率
|
||||
暴击伤害
|
||||
元素精通
|
||||
元素充能效率
|
||||
护盾强效
|
||||
|
||||
其他
|
||||
原神
|
||||
原魔
|
||||
原壶
|
||||
原牌
|
||||
崩坏
|
||||
氪金
|
||||
刷本
|
||||
周本
|
||||
狗托
|
||||
果面呐噻
|
||||
哒咩
|
||||
达咩
|
||||
啊这
|
||||
|
457
LittlePaimon/plugins/Learning_Chat/handler.py
Normal file
457
LittlePaimon/plugins/Learning_Chat/handler.py
Normal file
@ -0,0 +1,457 @@
|
||||
import datetime
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from functools import cmp_to_key
|
||||
|
||||
try:
|
||||
import jieba_fast.analyse as jieba_analyse
|
||||
except ImportError:
|
||||
import jieba.analyse as jieba_analyse
|
||||
from typing import List, Union, Optional, Tuple
|
||||
from enum import IntEnum, auto
|
||||
from nonebot import get_bot
|
||||
from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageSegment, ActionFailed
|
||||
from tortoise.functions import Count
|
||||
from LittlePaimon.utils import NICKNAME, SUPERUSERS, logger
|
||||
from .models import ChatBlackList, ChatContext, ChatAnswer, ChatMessage
|
||||
from .config import config_manager
|
||||
|
||||
chat_config = config_manager.config
|
||||
|
||||
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}有说什么奇怪的话吗?']
|
||||
ALL_WORDS = NO_PERMISSION_WORDS + SORRY_WORDS + DOUBT_WORDS + ENABLE_WORDS + DISABLE_WORDS
|
||||
|
||||
|
||||
class Result(IntEnum):
|
||||
Learn = auto()
|
||||
Pass = auto()
|
||||
Repeat = auto()
|
||||
Ban = auto()
|
||||
SetEnable = auto()
|
||||
|
||||
|
||||
class LearningChat:
|
||||
def __init__(self, event: GroupMessageEvent):
|
||||
if event.reply:
|
||||
self.reply = event.reply
|
||||
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=.+])|(\[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
|
||||
)
|
||||
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(),
|
||||
raw_message=event.raw_message,
|
||||
plain_text=event.get_plaintext(),
|
||||
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.config = config_manager.get_group_config(self.data.group_id)
|
||||
self.ban_users = set(chat_config.ban_users + self.config.ban_users)
|
||||
|
||||
async def _learn(self) -> Result:
|
||||
# logger.debug('群聊学习', f'收到来自群<m>{self.data.group_id}</m>的消息<m>{self.data.message}</m>')
|
||||
if not chat_config.total_enable or not self.config.enable or self.data.user_id in self.ban_users:
|
||||
# 如果未开启群聊学习或者发言人在屏蔽列表中,跳过
|
||||
return Result.Pass
|
||||
elif self.to_me and '不可以' in self.data.message:
|
||||
# 如果是对某句话进行禁言
|
||||
return Result.Ban
|
||||
elif self.to_me and any(w in self.data.message for w in {'学说话', '快学', '开启学习', '闭嘴', '别学', '关闭学习'}):
|
||||
return Result.SetEnable
|
||||
elif not await self._check_allow(self.data):
|
||||
# 本消息不合法,跳过
|
||||
return Result.Pass
|
||||
elif self.reply:
|
||||
# 如果是回复消息
|
||||
if not (message := await ChatMessage.get_or_none(message_id=self.reply.message_id)):
|
||||
# 回复的消息在数据库中有记录
|
||||
logger.debug('群聊学习', '➤是否学习:回复的消息不在数据库中,不学习')
|
||||
return Result.Pass
|
||||
if message.user_id in self.ban_users:
|
||||
# 且回复的人不在屏蔽列表中
|
||||
return Result.Pass
|
||||
if not await self._check_allow(message):
|
||||
# 且回复的内容通过校验
|
||||
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):
|
||||
# 获取本群一个小时内的最后5条消息
|
||||
if messages[0].message == self.data.message:
|
||||
# 判断是否为复读中
|
||||
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):
|
||||
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]):
|
||||
# 且最后一条消息的发送者不在屏蔽列表中并通过校验
|
||||
logger.debug('群聊学习', '➤是否学习:最后一条消息未通过校验,不学习')
|
||||
return Result.Pass
|
||||
# 则作为最后一条消息的答案
|
||||
await self._set_answer(messages[0])
|
||||
return Result.Learn
|
||||
else:
|
||||
# 不符合任何情况,跳过
|
||||
return Result.Pass
|
||||
|
||||
async def answer(self) -> Optional[List[Union[MessageSegment, str]]]:
|
||||
"""获取这句话的回复"""
|
||||
result = await self._learn()
|
||||
await self.data.save()
|
||||
if result == Result.Ban:
|
||||
# 禁用某句话
|
||||
if self.role not in {'superuser', 'admin', 'owner'}:
|
||||
# 检查权限
|
||||
return [random.choice(NO_PERMISSION_WORDS)]
|
||||
if self.reply:
|
||||
ban_result = await self._ban(message_id=self.reply.message_id)
|
||||
else:
|
||||
ban_result = await self._ban()
|
||||
if ban_result:
|
||||
return [random.choice(SORRY_WORDS)]
|
||||
else:
|
||||
return [random.choice(DOUBT_WORDS)]
|
||||
elif result == Result.SetEnable:
|
||||
# 开启/关闭学习
|
||||
if self.role in {'superuser', 'admin', 'owner'}:
|
||||
# 检查权限
|
||||
if any(w in self.data.message for w in {'学说话', '快学', '开启学习'}):
|
||||
self.config.update(enable=True)
|
||||
config_manager.config.group_config[self.data.group_id] = self.config
|
||||
config_manager.save()
|
||||
return [random.choice(ENABLE_WORDS)]
|
||||
else:
|
||||
self.config.update(enable=False)
|
||||
config_manager.config.group_config[self.data.group_id] = self.config
|
||||
config_manager.save()
|
||||
return [random.choice(DISABLE_WORDS)]
|
||||
else:
|
||||
return [random.choice(NO_PERMISSION_WORDS)]
|
||||
elif result == Result.Pass:
|
||||
# 跳过
|
||||
return None
|
||||
elif result == Result.Repeat and (messages := await ChatMessage.filter(group_id=self.data.group_id,
|
||||
time__gte=self.data.time - 3600).limit(
|
||||
self.config.repeat_threshold + 2)):
|
||||
# 如果达到阈值,进行复读
|
||||
if len(messages) >= self.config.repeat_threshold and all(
|
||||
message.message == self.data.message and message.user_id != self.bot_id for message in
|
||||
messages):
|
||||
if random.random() < self.config.break_probability:
|
||||
logger.debug('群聊学习', f'➤➤是否回复:达到复读阈值,打断复读!')
|
||||
return [random.choice(['打断复读', '打断!'])]
|
||||
else:
|
||||
logger.debug('群聊学习', f'➤➤是否回复:达到复读阈值,复读<m>{messages[0].message}</m>')
|
||||
return [self.data.message]
|
||||
else:
|
||||
# 回复
|
||||
if self.data.is_plain_text and len(self.data.plain_text) <= 1:
|
||||
logger.debug('群聊学习', '➤➤是否回复:消息过短,不回复')
|
||||
return None
|
||||
if not (context := await ChatContext.get_or_none(keywords=self.data.keywords)):
|
||||
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))
|
||||
|
||||
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
|
||||
cross_group_threshold = chat_config.cross_group_threshold
|
||||
else:
|
||||
answer_count_threshold = 1
|
||||
cross_group_threshold = 1
|
||||
|
||||
# 获取满足跨群条件的回复
|
||||
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)
|
||||
|
||||
candidate_answers: List[Optional[ChatAnswer]] = []
|
||||
# 检查候选回复是否在屏蔽列表中
|
||||
for answer in set(answers_cross) | set(answer_same_group):
|
||||
if not await self._check_allow(answer):
|
||||
continue
|
||||
# if answer_count_threshold > 0:
|
||||
# answer.count -= answer_count_threshold - 1
|
||||
candidate_answers.append(answer)
|
||||
if not candidate_answers:
|
||||
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.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>')
|
||||
|
||||
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>')
|
||||
return [result_message]
|
||||
|
||||
async def _ban(self, message_id: Optional[int] = None) -> bool:
|
||||
"""屏蔽消息"""
|
||||
bot = get_bot()
|
||||
if message_id:
|
||||
# 如果有指定消息ID,则屏蔽该消息
|
||||
if (message := await ChatMessage.get_or_none(message_id=message_id)) and message.message not in ALL_WORDS:
|
||||
keywords = message.keywords
|
||||
try:
|
||||
await bot.delete_msg(message_id=message_id)
|
||||
except ActionFailed:
|
||||
logger.info('群聊学习', f'待禁用消息<m>{message_id}</m>尝试撤回<r>失败</r>')
|
||||
else:
|
||||
return False
|
||||
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>')
|
||||
else:
|
||||
return False
|
||||
if ban_word := await ChatBlackList.get_or_none(keywords=keywords):
|
||||
# 如果已有屏蔽记录
|
||||
if self.data.group_id not in ban_word.ban_group_id:
|
||||
# 如果不在屏蔽群列表中,则添加
|
||||
ban_word.ban_group_id.append(self.data.group_id)
|
||||
if len(ban_word.ban_group_id) >= 2:
|
||||
# 如果有超过2个群都屏蔽了该条消息,则全局屏蔽
|
||||
ban_word.global_ban = True
|
||||
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()
|
||||
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()
|
||||
await ChatContext.filter(keywords=keywords).delete()
|
||||
await ban_word.save()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def add_ban(data: Union[ChatMessage, ChatContext, ChatAnswer]):
|
||||
if ban_word := await ChatBlackList.get_or_none(keywords=data.keywords):
|
||||
# 如果已有屏蔽记录
|
||||
if isinstance(data, ChatMessage):
|
||||
if data.group_id not in ban_word.ban_group_id:
|
||||
# 如果不在屏蔽群列表中,则添加
|
||||
ban_word.ban_group_id.append(data.group_id)
|
||||
if len(ban_word.ban_group_id) >= 2:
|
||||
# 如果有超过2个群都屏蔽了该条消息,则全局屏蔽
|
||||
ban_word.global_ban = True
|
||||
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()
|
||||
else:
|
||||
ban_word.global_ban = True
|
||||
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()
|
||||
else:
|
||||
|
||||
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]]]]:
|
||||
# 主动发言
|
||||
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)
|
||||
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):
|
||||
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 cmp(a, b):
|
||||
return (a > b) - (a < b)
|
||||
|
||||
left_group_id, left_messages = left_group
|
||||
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)
|
||||
|
||||
popularity: List[Tuple[int, List[ChatMessage]]] = sorted(total_messages.items(),
|
||||
key=cmp_to_key(group_popularity_cmp))
|
||||
logger.debug('群聊学习', f'主动发言:群热度排行<m>{">".join([str(g[0]) for g in popularity])}</m>')
|
||||
for group_id, messages in popularity:
|
||||
config = config_manager.get_group_config(group_id)
|
||||
|
||||
# 是否开启了主动发言
|
||||
if not config.speak_enable:
|
||||
continue
|
||||
|
||||
# 如果最后一条消息是自己发的,则不主动发言
|
||||
last_reply = await ChatMessage.filter(group_id=group_id, user_id=self_id).first()
|
||||
if last_reply and last_reply.time >= messages[0].time:
|
||||
continue
|
||||
|
||||
# 该群每多少秒发一条消息
|
||||
avg_interval = (messages[0].time - messages[-1].time) / len(messages)
|
||||
# 如果该群已沉默的时间小于阈值,则不主动发言
|
||||
if cur_time - messages[0].time < avg_interval * config.speak_threshold + config.speak_min_interval:
|
||||
continue
|
||||
|
||||
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)
|
||||
for context in contexts:
|
||||
if (random.random() < config.speak_continuously_probability or not len(speak_list)) and len(
|
||||
speak_list) < config.speak_continuously_max_len and (
|
||||
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)
|
||||
speak_list.append(message)
|
||||
while random.random() < config.speak_continuously_probability and len(
|
||||
speak_list) < config.speak_continuously_max_len:
|
||||
if (follow_context := await ChatContext.get_or_none(keywords=answer.keywords)) and (
|
||||
follow_answers := await ChatAnswer.filter(
|
||||
group_id=group_id,
|
||||
context=follow_context,
|
||||
count__gte=config.answer_threshold)):
|
||||
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(answer.messages)
|
||||
speak_list.append(message)
|
||||
else:
|
||||
break
|
||||
else:
|
||||
break
|
||||
if speak_list:
|
||||
# logger.debug('群聊学习', f'主动发言:将向群<m>{group_id}</m>主动发言<m>{" ".join(speak_list)}</m>')
|
||||
if random.random() < config.speak_poke_probability:
|
||||
last_speak_users = set(
|
||||
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}))
|
||||
return group_id, speak_list
|
||||
else:
|
||||
return None
|
||||
|
||||
async def _set_answer(self, message: ChatMessage):
|
||||
if context := await ChatContext.get_or_none(keywords=message.keywords):
|
||||
if context.count < chat_config.learn_max_count:
|
||||
context.count += 1
|
||||
context.time = self.data.time
|
||||
if answer := await ChatAnswer.get_or_none(keywords=self.data.keywords,
|
||||
group_id=self.data.group_id,
|
||||
context=context):
|
||||
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])
|
||||
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>')
|
||||
|
||||
async def _check_allow(self, message: Union[ChatMessage, ChatAnswer]) -> bool:
|
||||
raw_message = message.message if isinstance(message, ChatMessage) else message.messages[0]
|
||||
keywords = message.keywords
|
||||
if any(i in raw_message for i in
|
||||
{'[CQ:xml', '[CQ:json', '[CQ:at', '[CQ:video', '[CQ:record', '[CQ:share'}):
|
||||
# logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>')
|
||||
return False
|
||||
if any(i in raw_message for i in self.config.ban_words):
|
||||
# logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>')
|
||||
return False
|
||||
if raw_message.startswith('[') and raw_message.endswith(']'):
|
||||
# logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>')
|
||||
return False
|
||||
if ban_word := await ChatBlackList.get_or_none(keywords=keywords):
|
||||
if ban_word.global_ban or message.group_id in ban_word.ban_group_id:
|
||||
# logger.debug('群聊学习', f'➤检验<m>{keywords}</m><r>不通过</r>')
|
||||
return False
|
||||
# logger.debug('群聊学习', f'➤检验<m>{keywords}</m><g>通过</g>')
|
||||
return True
|
@ -1,541 +1,128 @@
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from functools import cached_property, cmp_to_key
|
||||
from typing import Generator, List, Optional, Union, Tuple, Dict, Any
|
||||
import functools
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
except ImportError:
|
||||
import json
|
||||
try:
|
||||
import jieba_fast as jieba
|
||||
import jieba_fast.analyse as jieba_analyse
|
||||
except ImportError:
|
||||
import jieba
|
||||
import jieba.analyse as jieba_analyse
|
||||
import pypinyin
|
||||
from dataclasses import dataclass
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
|
||||
from nonebot.adapters.onebot.v11 import Message, GroupMessageEvent
|
||||
|
||||
from LittlePaimon import NICKNAME
|
||||
from LittlePaimon.database import Message, Context, BlackList, Answers, Answer, BanWord
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
from LittlePaimon.database import register_database
|
||||
from LittlePaimon.utils.path import DATABASE_PATH
|
||||
from .config import config_manager
|
||||
|
||||
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(config.dictionary) # 加载用户自定义的词典
|
||||
|
||||
@dataclass
|
||||
class MessageData:
|
||||
group_id: int
|
||||
user_id: int
|
||||
raw_message: str
|
||||
plain_text: str
|
||||
time: int
|
||||
bot_id: int
|
||||
|
||||
class ChatMessage(Model):
|
||||
id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增主键"""
|
||||
group_id: int = fields.IntField()
|
||||
"""群id"""
|
||||
user_id: int = fields.IntField()
|
||||
"""用户id"""
|
||||
message_id: int = fields.IntField()
|
||||
"""消息id"""
|
||||
message: str = fields.TextField()
|
||||
"""消息"""
|
||||
raw_message: str = fields.TextField()
|
||||
"""原始消息"""
|
||||
plain_text: str = fields.TextField()
|
||||
"""纯文本消息"""
|
||||
time: int = fields.IntField()
|
||||
"""时间戳"""
|
||||
|
||||
class Meta:
|
||||
table = 'message'
|
||||
indexes = ('group_id', 'time')
|
||||
ordering = ['-time']
|
||||
|
||||
@cached_property
|
||||
def is_plain_text(self) -> bool:
|
||||
"""
|
||||
判断消息是否为纯文本
|
||||
"""
|
||||
return '[CQ:' not in self.raw_message and len(self.plain_text) != 0
|
||||
"""是否纯文本"""
|
||||
return '[CQ:' not in self.message
|
||||
|
||||
@cached_property
|
||||
def is_image(self) -> bool:
|
||||
"""
|
||||
判断消息是否为图片
|
||||
"""
|
||||
return '[CQ:image,' in self.raw_message or '[CQ:face,' in self.raw_message
|
||||
|
||||
@cached_property
|
||||
def _keywords_list(self):
|
||||
"""
|
||||
获取纯文本部分的关键词结果
|
||||
"""
|
||||
if not self.is_plain_text and len(self.plain_text) == 0:
|
||||
def keyword_list(self) -> List[str]:
|
||||
"""获取纯文本部分的关键词列表"""
|
||||
if not self.is_plain_text and not len(self.plain_text):
|
||||
return []
|
||||
|
||||
return jieba_analyse.extract_tags(
|
||||
self.plain_text, topK=config.KEYWORDS_SIZE)
|
||||
|
||||
@cached_property
|
||||
def keywords_len(self) -> int:
|
||||
"""
|
||||
获取关键词数量
|
||||
:return:
|
||||
"""
|
||||
return len(self._keywords_list)
|
||||
return jieba_analyse.extract_tags(self.plain_text, topK=config.KEYWORDS_SIZE)
|
||||
|
||||
@cached_property
|
||||
def keywords(self) -> str:
|
||||
"""将关键词列表字符串"""
|
||||
if not self.is_plain_text and len(self.plain_text) == 0:
|
||||
return self.raw_message
|
||||
|
||||
if self.keywords_len < 2:
|
||||
return self.plain_text
|
||||
else:
|
||||
# keywords_list.sort()
|
||||
return ' '.join(self._keywords_list)
|
||||
|
||||
@cached_property
|
||||
def keywords_pinyin(self) -> str:
|
||||
"""将关键词拼音列表字符串"""
|
||||
return ''.join([item[0] for item in pypinyin.pinyin(
|
||||
self.keywords, style=pypinyin.NORMAL, errors='default')]).lower()
|
||||
|
||||
@cached_property
|
||||
def to_me(self) -> bool:
|
||||
"""判断是否为艾特机器人"""
|
||||
return self.plain_text.startswith(NICKNAME)
|
||||
"""获取纯文本部分的关键词结果"""
|
||||
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)
|
||||
|
||||
|
||||
class LearningChat:
|
||||
reply_cache = defaultdict(lambda: defaultdict(list))
|
||||
"""回复的消息缓存"""
|
||||
message_cache = {}
|
||||
"""群消息缓存"""
|
||||
class ChatContext(Model):
|
||||
id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增主键"""
|
||||
keywords: str = fields.TextField()
|
||||
"""关键词"""
|
||||
time: int = fields.IntField()
|
||||
"""时间戳"""
|
||||
count: int = fields.IntField(default=1)
|
||||
"""次数"""
|
||||
answers: fields.ReverseRelation['ChatAnswer']
|
||||
"""答案"""
|
||||
|
||||
_reply_lock = threading.Lock()
|
||||
_message_lock = threading.Lock()
|
||||
_save_reserve_size = 100 # 保存时,给内存中保留的大小
|
||||
_late_save_time = 0 # 上次保存(消息数据持久化)的时刻 ( time.time(), 秒 )
|
||||
class Meta:
|
||||
table = 'context'
|
||||
indexes = ('keywords', 'time')
|
||||
ordering = ['-time']
|
||||
|
||||
def __init__(self, event: Union[GroupMessageEvent, MessageData]):
|
||||
if isinstance(event, GroupMessageEvent):
|
||||
self.message = MessageData(
|
||||
group_id=event.group_id,
|
||||
user_id=event.user_id,
|
||||
raw_message=re.sub(r',subType=\d+,url=.+]', r']', event.raw_message),
|
||||
plain_text=event.get_plaintext(),
|
||||
time=event.time,
|
||||
bot_id=event.self_id)
|
||||
else:
|
||||
self.message = event
|
||||
|
||||
async def learn(self) -> bool:
|
||||
"""学习这句话"""
|
||||
if not len(self.message.raw_message.strip()):
|
||||
return False
|
||||
class ChatAnswer(Model):
|
||||
id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增主键"""
|
||||
keywords: str = fields.TextField()
|
||||
"""关键词"""
|
||||
group_id: int = fields.IntField()
|
||||
"""群id"""
|
||||
count: int = fields.IntField(default=1)
|
||||
"""次数"""
|
||||
time: int = fields.IntField()
|
||||
"""时间戳"""
|
||||
messages: List[str] = fields.JSONField(encoder=JSON_DUMPS, default=list)
|
||||
"""消息列表"""
|
||||
|
||||
if self.message.group_id in LearningChat.message_cache:
|
||||
group_msgs = LearningChat.message_cache[self.message.group_id]
|
||||
# 将群里上一条发言插入数据库
|
||||
group_pre_msg = group_msgs[-1] if group_msgs else None
|
||||
await self._update_context(group_pre_msg)
|
||||
context: fields.ForeignKeyNullableRelation[ChatContext] = fields.ForeignKeyField(
|
||||
'LearningChat.ChatContext', related_name='answers', null=True)
|
||||
|
||||
if group_pre_msg and group_pre_msg['user_id'] != self.message.user_id:
|
||||
# 该用户在群里的上一条发言(倒序三句之内)
|
||||
for msg in group_msgs[:-3:-1]:
|
||||
if msg['user_id'] == self.message.user_id:
|
||||
await self._update_context(msg)
|
||||
break
|
||||
await self._update_message()
|
||||
return True
|
||||
class Meta:
|
||||
table = 'answer'
|
||||
indexes = ('keywords', 'time')
|
||||
ordering = ['-time']
|
||||
|
||||
async def answer(self) -> Optional[Generator[Union[Message, Message], None, None]]:
|
||||
"""获取这句话的回复"""
|
||||
if self.message.is_plain_text and len(self.message.plain_text) <= 1:
|
||||
"""不回复单个字的对话"""
|
||||
return None
|
||||
|
||||
if not (results := await self._get_context()):
|
||||
return None
|
||||
group_id = self.message.group_id
|
||||
raw_message = self.message.raw_message
|
||||
keywords = self.message.keywords
|
||||
bot_id = self.message.bot_id
|
||||
class ChatBlackList(Model):
|
||||
id: int = fields.IntField(pk=True, generated=True, auto_increment=True)
|
||||
"""自增主键"""
|
||||
keywords: str = fields.TextField()
|
||||
"""关键词"""
|
||||
global_ban: bool = fields.BooleanField(default=False)
|
||||
"""是否全局禁用"""
|
||||
ban_group_id: List[int] = fields.JSONField(default=list)
|
||||
"""禁用的群id"""
|
||||
|
||||
group_bot_replies = LearningChat.reply_cache[group_id][bot_id]
|
||||
with LearningChat._reply_lock:
|
||||
group_bot_replies.append({
|
||||
'time': int(time.time()),
|
||||
'pre_raw_message': raw_message,
|
||||
'pre_keywords': keywords,
|
||||
'reply': '[LearningChat: Reply]', # flag
|
||||
'reply_keywords': '[LearningChat: Reply]', # flag
|
||||
})
|
||||
class Meta:
|
||||
table = 'blacklist'
|
||||
indexes = ('keywords',)
|
||||
|
||||
def yield_results(results_: Tuple[List[str], str]) -> Generator[Message, None, None]:
|
||||
answer_list, answer_keywords = results_
|
||||
|
||||
for item in answer_list:
|
||||
with LearningChat._reply_lock:
|
||||
LearningChat.reply_cache[group_id][bot_id].append({
|
||||
'time': int(time.time()),
|
||||
'pre_raw_message': raw_message,
|
||||
'pre_keywords': keywords,
|
||||
'reply': item,
|
||||
'reply_keywords': answer_keywords,
|
||||
})
|
||||
yield item
|
||||
|
||||
with LearningChat._reply_lock:
|
||||
LearningChat.reply_cache[self.message.group_id][self.message.bot_id] = \
|
||||
LearningChat.reply_cache[self.message.group_id][
|
||||
self.message.bot_id][
|
||||
-self._save_reserve_size:]
|
||||
|
||||
return yield_results(results)
|
||||
|
||||
@staticmethod
|
||||
async def speak() -> Optional[Tuple[int, int, List[Message]]]:
|
||||
"""
|
||||
主动发言,返回当前最希望发言的 bot 账号、群号、发言消息 List,也有可能不发言
|
||||
"""
|
||||
basic_msgs_len = 10
|
||||
basic_delay = 600
|
||||
|
||||
def group_popularity_cmp(lhs: Tuple[int, List[Dict[str, Any]]],
|
||||
rhs: Tuple[int, List[Dict[str, Any]]]) -> int:
|
||||
def cmp(a: Any, b: Any):
|
||||
return (a > b) - (a < b)
|
||||
|
||||
lhs_group_id, lhs_msgs = lhs
|
||||
rhs_group_id, rhs_msgs = rhs
|
||||
lhs_len = len(lhs_msgs)
|
||||
rhs_len = len(rhs_msgs)
|
||||
if lhs_len < basic_msgs_len or rhs_len < basic_msgs_len:
|
||||
return cmp(lhs_len, rhs_len)
|
||||
|
||||
lhs_duration = lhs_msgs[-1]['time'] - lhs_msgs[0]['time']
|
||||
rhs_duration = rhs_msgs[-1]['time'] - rhs_msgs[0]['time']
|
||||
|
||||
if not lhs_duration or not rhs_duration:
|
||||
return cmp(lhs_len, rhs_len)
|
||||
|
||||
return cmp(lhs_len / lhs_duration,
|
||||
rhs_len / rhs_duration)
|
||||
|
||||
# 按群聊热度排序
|
||||
popularity = sorted(LearningChat.message_cache.items(),
|
||||
key=cmp_to_key(group_popularity_cmp))
|
||||
cur_time = time.time()
|
||||
for group_id, group_msgs in popularity:
|
||||
group_replies = LearningChat.reply_cache[group_id]
|
||||
if not len(group_replies) or len(group_msgs) < basic_msgs_len:
|
||||
continue
|
||||
|
||||
group_replies_front = list(group_replies.values())[0]
|
||||
if not len(group_replies_front) or group_replies_front[-1]['time'] > group_msgs[-1]['time']:
|
||||
continue
|
||||
|
||||
msgs_len = len(group_msgs)
|
||||
latest_time = group_msgs[-1]['time']
|
||||
duration = latest_time - group_msgs[0]['time']
|
||||
avg_interval = duration / msgs_len
|
||||
|
||||
if cur_time - latest_time < avg_interval * config.speak_threshold + basic_delay:
|
||||
continue
|
||||
# append 一个 flag, 防止这个群热度特别高,但压根就没有可用的 context 时,每次 speak 都查这个群,浪费时间
|
||||
with LearningChat._reply_lock:
|
||||
group_replies_front.append({
|
||||
'time': int(cur_time),
|
||||
'pre_raw_message': '[PallasBot: Speak]',
|
||||
'pre_keywords': '[PallasBot: Speak]',
|
||||
'reply': '[PallasBot: Speak]',
|
||||
'reply_keywords': '[PallasBot: Speak]',
|
||||
})
|
||||
|
||||
available_time = cur_time - 24 * 3600
|
||||
speak_context = await Context.filter(count__gt=config.answer_threshold,
|
||||
time__gt=available_time).all()
|
||||
speak_context_right = []
|
||||
for context in speak_context:
|
||||
for answer in context.answers:
|
||||
if answer.group_id == group_id and answer.time > available_time and answer.count > config.answer_threshold:
|
||||
speak_context_right.append(context)
|
||||
break
|
||||
if not speak_context_right:
|
||||
continue
|
||||
speak_context_right.sort(key=lambda x: len(x.ban))
|
||||
|
||||
ban_keywords = await LearningChat._get_ban_keywords(speak_context_right[0], group_id)
|
||||
messages = [answer.messages
|
||||
for answer in speak_context_right[0].answers
|
||||
if answer.count >= config.answer_threshold
|
||||
and answer.keywords not in ban_keywords
|
||||
and answer.group_id == group_id]
|
||||
if not messages:
|
||||
continue
|
||||
speak = random.choice(random.choice(messages))
|
||||
|
||||
bot_id = random.choice([bid for bid in group_replies.keys() if bid])
|
||||
with LearningChat._reply_lock:
|
||||
group_replies[bot_id].append({
|
||||
'time': int(cur_time),
|
||||
'pre_raw_message': '[PallasBot: Speak]',
|
||||
'pre_keywords': '[PallasBot: Speak]',
|
||||
'reply': speak,
|
||||
'reply_keywords': '[PallasBot: Speak]',
|
||||
})
|
||||
|
||||
speak_list = [speak]
|
||||
while random.random() < config.speak_continuously_probability and len(
|
||||
speak_list) < config.speak_continuously_max_len:
|
||||
pre_msg = str(speak_list[-1])
|
||||
answer = await LearningChat(MessageData(group_id=group_id,
|
||||
user_id=0,
|
||||
raw_message=pre_msg,
|
||||
plain_text=pre_msg,
|
||||
time=int(cur_time),
|
||||
bot_id=0)).answer()
|
||||
if not answer:
|
||||
break
|
||||
speak_list.extend(answer)
|
||||
|
||||
if random.random() < config.speak_poke_probability:
|
||||
target_id = random.choice(LearningChat.message_cache[group_id])['user_id']
|
||||
speak_list.append(f'[CQ:poke,qq={target_id}]')
|
||||
|
||||
return bot_id, group_id, speak_list
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def ban(group_id: int, bot_id: int, ban_raw_message: str, reason: str) -> bool:
|
||||
"""
|
||||
禁止以后回复这句话,仅对该群有效果
|
||||
"""
|
||||
|
||||
if group_id not in LearningChat.reply_cache:
|
||||
return False
|
||||
|
||||
ban_reply = None
|
||||
reply_data = LearningChat.reply_cache[group_id][bot_id][::-1]
|
||||
|
||||
for reply in reply_data:
|
||||
cur_reply = reply['reply']
|
||||
# 为空时就直接 ban 最后一条回复
|
||||
if not ban_raw_message or ban_raw_message in cur_reply:
|
||||
ban_reply = reply
|
||||
break
|
||||
|
||||
# 这种情况一般是有些 CQ 码,牛牛发送的时候,和被回复的时候,里面的内容不一样
|
||||
if not ban_reply:
|
||||
if search := re.search(r'(\[CQ:[a-zA-z0-9-_.]+)', ban_raw_message):
|
||||
type_keyword = search[1]
|
||||
for reply in reply_data:
|
||||
cur_reply = reply['reply']
|
||||
if type_keyword in cur_reply:
|
||||
ban_reply = reply
|
||||
break
|
||||
|
||||
if not ban_reply:
|
||||
return False
|
||||
|
||||
pre_keywords = reply['pre_keywords']
|
||||
keywords = reply['reply_keywords']
|
||||
|
||||
ban, _ = await Context.get_or_create(keywords=pre_keywords)
|
||||
ban.ban.append(BanWord(keywords=keywords,
|
||||
group_id=group_id,
|
||||
reason=reason,
|
||||
time=int(time.time())))
|
||||
await ban.save()
|
||||
blacklist, _ = await BlackList.get_or_create(group_id=group_id)
|
||||
if keywords in blacklist.answers_reserve:
|
||||
blacklist.answers.append(keywords)
|
||||
else:
|
||||
blacklist.answers_reserve.append(keywords)
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def persistence(cur_time: int = int(time.time())):
|
||||
"""
|
||||
持久化
|
||||
"""
|
||||
with LearningChat._message_lock:
|
||||
if save_list := [msg for group_msgs in LearningChat.message_cache.values() for msg in group_msgs if
|
||||
msg['time'] > LearningChat._late_save_time]:
|
||||
LearningChat.message_cache = {group_id: group_msgs[-LearningChat._save_reserve_size:] for
|
||||
group_id, group_msgs in LearningChat.message_cache.items()}
|
||||
LearningChat._late_save_time = cur_time
|
||||
else:
|
||||
return
|
||||
|
||||
await Message.bulk_create([Message(**msg) for msg in save_list])
|
||||
|
||||
async def _get_context(self):
|
||||
"""获取上下文消息"""
|
||||
if msgs := LearningChat.message_cache.get(self.message.group_id):
|
||||
# 是否在复读中
|
||||
if len(msgs) >= config.repeat_threshold and all(
|
||||
item['raw_message'] == self.message.raw_message for item in msgs[-config.repeat_threshold + 1:]):
|
||||
# 说明当前群里正在复读
|
||||
group_bot_replies = LearningChat.reply_cache[self.message.group_id][self.message.bot_id]
|
||||
if len(group_bot_replies) and group_bot_replies[-1]['reply'] != self.message.raw_message:
|
||||
return [self.message.raw_message, ], self.message.keywords
|
||||
else:
|
||||
# 已经复读过了,不回复
|
||||
return None
|
||||
if not (context := await Context.get_or_none(keywords=self.message.keywords)):
|
||||
return None
|
||||
|
||||
# 喝醉了的处理,先不做了
|
||||
answer_threshold_choice_list = list(
|
||||
range(config.answer_threshold - len(config.answer_threshold_weights) + 1, config.answer_threshold + 1))
|
||||
answer_count_threshold = random.choices(answer_threshold_choice_list, weights=config.answer_threshold_weights)[
|
||||
0]
|
||||
if self.message.keywords_len == config.KEYWORDS_SIZE:
|
||||
answer_count_threshold -= 1
|
||||
|
||||
cross_group_threshold = 1 if self.message.to_me else config.cross_group_threshold
|
||||
ban_keywords = await LearningChat._get_ban_keywords(context, self.message.group_id)
|
||||
|
||||
candidate_answers: Dict[str, Answer] = {}
|
||||
other_group_cache: Dict[str, Answer] = {}
|
||||
answers_count = defaultdict(int)
|
||||
|
||||
def candidate_append(dst: Dict[str, Answer], answer_: Answer):
|
||||
if answer_.keywords not in dst:
|
||||
dst[answer_.keywords] = answer_
|
||||
else:
|
||||
dst[answer_.keywords].count += answer_.count
|
||||
dst[answer_.keywords].messages += answer_.messages
|
||||
|
||||
for answer in context.answers:
|
||||
if answer.count < answer_count_threshold:
|
||||
continue
|
||||
if answer.keywords in ban_keywords:
|
||||
continue
|
||||
sample_msg = answer.messages[0]
|
||||
if self.message.is_image and '[CQ:' not in sample_msg:
|
||||
# 图片消息不回复纯文本
|
||||
continue
|
||||
if not self.message.to_me and sample_msg.startswith(NICKNAME):
|
||||
continue
|
||||
if any(i in sample_msg for i in{'[CQ:xml', '[CQ:json', '[CQ:at', '[CQ:video', '[CQ:record', '[CQ:share'}):
|
||||
# 不学xml、json和at
|
||||
continue
|
||||
|
||||
if answer.group_id == self.message.group_id:
|
||||
candidate_append(candidate_answers, answer)
|
||||
else:
|
||||
answers_count[answer.keywords] += 1
|
||||
cur_count = answers_count[answer.keywords]
|
||||
if cur_count < cross_group_threshold:
|
||||
candidate_append(other_group_cache, answer)
|
||||
elif cur_count == cross_group_threshold:
|
||||
if cur_count > 1:
|
||||
candidate_append(candidate_answers, other_group_cache[answer.keywords])
|
||||
candidate_append(candidate_answers, answer)
|
||||
else:
|
||||
candidate_append(candidate_answers, answer)
|
||||
if not candidate_answers:
|
||||
return None
|
||||
|
||||
final_answer = random.choices(list(candidate_answers.values()),
|
||||
weights=[min(answer.count, 10) for answer in candidate_answers.values()])[0]
|
||||
answer_str = random.choice(final_answer.messages)
|
||||
answer_keywords = final_answer.keywords
|
||||
|
||||
if 0 < answer_str.count(',') <= 3 and '[CQ:' not in answer_str and random.random() < config.split_probability:
|
||||
return answer_str.split(','), answer_keywords
|
||||
return [answer_str, ], answer_keywords
|
||||
|
||||
async def _update_context(self, pre_msg):
|
||||
if not pre_msg:
|
||||
return
|
||||
|
||||
# 在复读,不学
|
||||
if pre_msg['raw_message'] == self.message.raw_message:
|
||||
return
|
||||
# 回复别人的,不学
|
||||
if '[CQ:reply' in self.message.raw_message:
|
||||
return
|
||||
if context := await Context.filter(keywords=pre_msg['keywords']).first():
|
||||
context.count += 1
|
||||
context.time = self.message.time
|
||||
answer_index = next((idx for idx, answer in enumerate(context.answers)
|
||||
if answer.group_id == self.message.group_id
|
||||
and answer.keywords == self.message.keywords), -1)
|
||||
if answer_index == -1:
|
||||
context.answers.append(
|
||||
Answer(
|
||||
keywords=self.message.keywords,
|
||||
group_id=self.message.group_id,
|
||||
count=1,
|
||||
time=self.message.time,
|
||||
messages=[self.message.raw_message]
|
||||
)
|
||||
)
|
||||
else:
|
||||
context.answers[answer_index].count += 1
|
||||
context.answers[answer_index].time = self.message.time
|
||||
if self.message.is_plain_text:
|
||||
context.answers[answer_index].messages.append(self.message.raw_message)
|
||||
await context.save()
|
||||
else:
|
||||
answer = Answer(
|
||||
keywords=self.message.keywords,
|
||||
group_id=self.message.group_id,
|
||||
count=1,
|
||||
time=self.message.time,
|
||||
messages=[self.message.raw_message]
|
||||
)
|
||||
await Context.create(keywords=pre_msg['keywords'],
|
||||
time=self.message.time,
|
||||
count=1,
|
||||
answers=Answers(answers=[answer]))
|
||||
|
||||
async def _update_message(self):
|
||||
with LearningChat._message_lock:
|
||||
if self.message.group_id not in LearningChat.message_cache:
|
||||
LearningChat.message_cache[self.message.group_id] = []
|
||||
LearningChat.message_cache[self.message.group_id].append(
|
||||
{
|
||||
'group_id': self.message.group_id,
|
||||
'user_id': self.message.user_id,
|
||||
'raw_message': self.message.raw_message,
|
||||
'is_plain_text': self.message.is_plain_text,
|
||||
'plain_text': self.message.plain_text,
|
||||
'keywords': self.message.keywords,
|
||||
'time': self.message.time,
|
||||
}
|
||||
)
|
||||
|
||||
cur_time = self.message.time
|
||||
if LearningChat._late_save_time == 0:
|
||||
LearningChat._late_save_time = cur_time - 1
|
||||
return
|
||||
|
||||
if len(LearningChat.message_cache[self.message.group_id]) > config.save_count_threshold:
|
||||
await LearningChat.persistence(cur_time)
|
||||
elif cur_time - LearningChat._late_save_time > config.save_time_threshold:
|
||||
await LearningChat.persistence(cur_time)
|
||||
|
||||
@staticmethod
|
||||
async def _get_ban_keywords(context: Context, group_id: int) -> set:
|
||||
"""
|
||||
找到在 group_id 群中对应 context 不能回复的关键词
|
||||
"""
|
||||
ban_keywords, _ = await BlackList.get_or_create(group_id=group_id)
|
||||
if context.ban:
|
||||
ban_count = defaultdict(int)
|
||||
for ban in context.ban:
|
||||
ban_key = ban.keywords
|
||||
if ban.group_id == group_id:
|
||||
ban_keywords.answers.append(ban_key)
|
||||
else:
|
||||
ban_count[ban_key] += 1
|
||||
if ban_count[ban_key] == config.cross_group_threshold:
|
||||
ban_keywords.answers.append(ban_key)
|
||||
await ban_keywords.save()
|
||||
return set(ban_keywords.answers)
|
||||
|
||||
@staticmethod
|
||||
async def clear_up_context():
|
||||
"""
|
||||
清理所有超过 15 天没人说、且没有学会的话
|
||||
"""
|
||||
cur_time = int(time.time())
|
||||
expiration = cur_time - 15 * 24 * 3600 # 15 天前
|
||||
await Context.filter(time__lt=expiration, count__lt=config.answer_threshold).delete()
|
||||
contexts = await Context.filter(count__gt=100, clear_time__lt=expiration).all()
|
||||
for context in contexts:
|
||||
answers = [answer
|
||||
for answer in context.answers
|
||||
if answer.count > 1 or answer.time > expiration]
|
||||
context.answers = answers
|
||||
context.clear_time = cur_time
|
||||
await context.save()
|
||||
register_database(db_name='LearningChat', models=['LittlePaimon.plugins.Learning_Chat.models'], db_path=DATABASE_PATH / 'LearningChat.db')
|
||||
|
247
LittlePaimon/plugins/Learning_Chat/web_api.py
Normal file
247
LittlePaimon/plugins/Learning_Chat/web_api.py
Normal file
@ -0,0 +1,247 @@
|
||||
from typing import Optional, Union
|
||||
|
||||
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.web.api import BaseApiRouter
|
||||
from LittlePaimon.web.api.utils import authentication
|
||||
|
||||
from .handler import LearningChat
|
||||
from .config import config_manager
|
||||
|
||||
route = APIRouter()
|
||||
|
||||
|
||||
@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'])
|
||||
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
|
||||
return config
|
||||
except ValueError:
|
||||
return {
|
||||
'status': -100,
|
||||
'msg': '获取群和好友列表失败,请确认已连接GOCQ'
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
await ChatAnswer.filter(count__gt=config_manager.config.learn_max_count).update(
|
||||
count=config_manager.config.learn_max_count)
|
||||
return {
|
||||
'status': 0,
|
||||
'msg': '保存成功'
|
||||
}
|
||||
|
||||
|
||||
@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]
|
||||
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
|
||||
return config
|
||||
except ValueError:
|
||||
return {
|
||||
'status': -100,
|
||||
'msg': '获取群和好友列表失败,请确认已连接GOCQ'
|
||||
}
|
||||
|
||||
|
||||
@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': '回复阈值权重不能为空,必须至少有一个数值'
|
||||
}
|
||||
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}]
|
||||
else:
|
||||
groups = await get_bot().get_group_list()
|
||||
for group in groups:
|
||||
config = config_manager.get_group_config(group['group_id'])
|
||||
config.update(**data)
|
||||
config_manager.config.group_config[group['group_id']] = config
|
||||
config_manager.save()
|
||||
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}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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"}'
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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:
|
||||
if item['global_ban']:
|
||||
item['bans'] = '全局禁用'
|
||||
else:
|
||||
item['bans'] = str(item['ban_group_id'][0])
|
||||
if bans:
|
||||
items = list(filter(lambda x: bans in x['bans'], items))
|
||||
return {
|
||||
'status': 0,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'items': items,
|
||||
'total': len(items)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@route.delete('/delete_chat', response_class=JSONResponse, dependencies=[authentication()])
|
||||
async def delete_chat(id: int, type: str):
|
||||
try:
|
||||
if type == 'message':
|
||||
await ChatMessage.filter(id=id).delete()
|
||||
elif type == 'context':
|
||||
c = await ChatContext.get(id=id)
|
||||
await ChatAnswer.filter(context=c).delete()
|
||||
await c.delete()
|
||||
elif type == 'answer':
|
||||
await ChatAnswer.filter(id=id).delete()
|
||||
elif type == 'blacklist':
|
||||
await ChatBlackList.filter(id=id).delete()
|
||||
return {
|
||||
'status': 0,
|
||||
'msg': '删除成功'
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 500,
|
||||
'msg': f'删除失败,{e}'
|
||||
}
|
||||
|
||||
|
||||
@route.put('/ban_chat', response_class=JSONResponse, dependencies=[authentication()])
|
||||
async def ban_chat(id: int, type: str):
|
||||
try:
|
||||
if type == 'message':
|
||||
data = await ChatMessage.get(id=id)
|
||||
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': '禁用成功'
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 500,
|
||||
'msg': f'禁用失败: {e}'
|
||||
}
|
||||
|
||||
|
||||
@route.put('/delete_all', response_class=JSONResponse, dependencies=[authentication()])
|
||||
async def delete_all(type: str, id: Optional[int] = None):
|
||||
try:
|
||||
if type == 'message':
|
||||
await ChatMessage.all().delete()
|
||||
elif type == 'context':
|
||||
await ChatContext.all().delete()
|
||||
elif type == 'answer':
|
||||
if id:
|
||||
await ChatAnswer.filter(context_id=id).delete()
|
||||
else:
|
||||
await ChatAnswer.all().delete()
|
||||
elif type == 'blacklist':
|
||||
await ChatBlackList.all().delete()
|
||||
return {
|
||||
'status': 0,
|
||||
'msg': '操作成功'
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'status': 500,
|
||||
'msg': f'操作失败,{e}'
|
||||
}
|
||||
|
||||
|
||||
BaseApiRouter.include_router(route)
|
276
LittlePaimon/plugins/Learning_Chat/web_page.py
Normal file
276
LittlePaimon/plugins/Learning_Chat/web_page.py
Normal file
@ -0,0 +1,276 @@
|
||||
from amis import ColumnList, AmisList, ActionType, TableCRUD, TableColumn
|
||||
from amis import Dialog, PageSchema, Page, Switch, Remark, InputNumber, InputTag, Action
|
||||
from amis import Form, LevelEnum, Select, InputArray, Alert
|
||||
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',
|
||||
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='添加自定义词语,让分词能够识别未收录的词汇,提高学习的准确性。你可以添加特殊名词,这样学习时就会将该词看作一个整体,目前词典中已默认添加部分原神相关词汇。(回车进行添加)')),
|
||||
],
|
||||
actions=[Action(label='保存', level=LevelEnum.success, type='submit'),
|
||||
Action(label='重置', level=LevelEnum.warning, type='reset')]
|
||||
)
|
||||
group_select = Select(label='分群配置', name='group_id', source='/LittlePaimon/api/get_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}',
|
||||
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个发言者中随机选一个戳。')),
|
||||
],
|
||||
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',
|
||||
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:15}', label='内容/关键词',
|
||||
name='keywords',
|
||||
searchable=True, popOver={'mode': 'dialog', 'title': '全文',
|
||||
'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=6000,
|
||||
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='${message|truncate:15}', label='消息', name='message',
|
||||
searchable=True, popOver={'mode': 'dialog', 'title': '消息全文',
|
||||
'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',
|
||||
syncLocation=False,
|
||||
footable=True,
|
||||
api='/LittlePaimon/api/get_chat_answers',
|
||||
interval=6000,
|
||||
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:15}', label='内容/关键词', name='keywords',
|
||||
searchable=True, popOver={'mode': 'dialog', 'title': '内容全文',
|
||||
'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'}))
|
||||
])
|
||||
answer_table_on_context = TableCRUD(
|
||||
mode='table',
|
||||
syncLocation=False,
|
||||
footable=True,
|
||||
api='/LittlePaimon/api/get_chat_answers?context_id=${id}&page=${page}&perPage=${perPage}&orderBy=${orderBy}&orderDir=${orderDir}',
|
||||
interval=6000,
|
||||
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:15}', label='内容/关键词', name='keywords',
|
||||
searchable=True, popOver={'mode': 'dialog', 'title': '内容全文',
|
||||
'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=6000,
|
||||
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:15}', label='内容/关键词',
|
||||
name='keywords', searchable=True,
|
||||
popOver={'mode': 'dialog', 'title': '内容全文',
|
||||
'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',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
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='配置', 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)
|
@ -13,9 +13,8 @@ from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent, PrivateMessa
|
||||
GroupIncreaseNoticeEvent, FriendAddNoticeEvent, GroupMessageEvent
|
||||
from nonebot.typing import T_State
|
||||
|
||||
from LittlePaimon import NICKNAME, SUPERUSERS
|
||||
from LittlePaimon.config import config as bot_config
|
||||
from LittlePaimon.utils import scheduler, logger
|
||||
from LittlePaimon.utils import scheduler, logger, NICKNAME, SUPERUSERS
|
||||
from LittlePaimon.utils.message import format_message, replace_all
|
||||
from .config import config
|
||||
|
||||
|
@ -206,7 +206,7 @@ class MihoyoBBSCoin:
|
||||
if data['retcode'] != 1034:
|
||||
self.is_valid = False
|
||||
self.state = 'Cookie已失效' if data['retcode'] in [-100,
|
||||
10001] else f"出错了:{data['retcode']} {data['message']}" if data['retcode'] != 1034 else '疑似遇到验证码'
|
||||
10001] else f"出错了:{data['retcode']} {data['message']}" if data['retcode'] != 1034 else '遇验证码阻拦'
|
||||
logger.info('米游币自动获取', f'➤➤<r>{self.state}</r>')
|
||||
return f'讨论区签到:{self.state}'
|
||||
await asyncio.sleep(random.randint(15, 30))
|
||||
|
@ -8,10 +8,9 @@ from typing import Tuple, Dict, Any, Optional, Union
|
||||
|
||||
from nonebot import get_bot
|
||||
|
||||
from LittlePaimon import DRIVER
|
||||
from LittlePaimon.config import config
|
||||
from LittlePaimon.database import MihoyoBBSSub, LastQuery, PrivateCookie
|
||||
from LittlePaimon.utils import logger, scheduler
|
||||
from LittlePaimon.utils import logger, scheduler, DRIVER
|
||||
from LittlePaimon.utils.api import get_mihoyo_private_data, get_sign_reward_list, mihoyo_sign_headers, check_retcode
|
||||
from LittlePaimon.utils.requests import aiorequests
|
||||
from .draw import SignResult, draw_result
|
||||
|
@ -9,11 +9,10 @@ from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.typing import T_State
|
||||
|
||||
from LittlePaimon import NICKNAME
|
||||
from LittlePaimon.config import config
|
||||
from LittlePaimon.database import LastQuery, PrivateCookie, PublicCookie, Character, PlayerInfo, DailyNoteSub, \
|
||||
MihoyoBBSSub
|
||||
from LittlePaimon.utils import logger
|
||||
from LittlePaimon.utils import logger, NICKNAME
|
||||
from LittlePaimon.utils.api import get_bind_game_info, get_stoken_by_cookie
|
||||
from LittlePaimon.utils.message import recall_message
|
||||
|
||||
|
@ -54,7 +54,7 @@ async def handle_ssbq(player: Player):
|
||||
elif data['retcode'] == 1034:
|
||||
logger.info('原神实时便签', '➤', {'用户': player.user_id, 'UID': player.uid},
|
||||
'获取数据失败,状态码为1034,疑似验证码', False)
|
||||
return f'{player.uid}获取数据失败,疑似遇米游社验证码阻拦,请稍后再试\n'
|
||||
return f'{player.uid}遇验证码阻拦,需手动前往米游社进行验证后才能继续使用\n'
|
||||
elif data['retcode'] != 0:
|
||||
logger.info('原神实时便签', '➤', {'用户': player.user_id, 'UID': player.uid},
|
||||
f'获取数据失败,code为{data["retcode"]}, msg为{data["message"]}', False)
|
||||
|
@ -8,9 +8,8 @@ from nonebot import on_notice
|
||||
from nonebot.adapters.onebot.v11 import GroupUploadNoticeEvent, NoticeEvent
|
||||
from nonebot.rule import Rule
|
||||
|
||||
from LittlePaimon import __version__
|
||||
from LittlePaimon.database import PlayerInfo, LastQuery
|
||||
from LittlePaimon.utils import logger
|
||||
from LittlePaimon.utils import logger, __version__
|
||||
from LittlePaimon.utils.requests import aiorequests
|
||||
from LittlePaimon.utils.api import get_authkey_by_stoken
|
||||
from LittlePaimon.utils.files import load_json, save_json
|
||||
|
@ -9,8 +9,8 @@ from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.typing import T_State
|
||||
|
||||
from LittlePaimon import NICKNAME
|
||||
from LittlePaimon.database import PlayerAlias
|
||||
from LittlePaimon.utils import NICKNAME
|
||||
from LittlePaimon.utils.alias import get_match_alias
|
||||
from LittlePaimon.utils.message import MessageBuild
|
||||
from LittlePaimon.utils.path import RESOURCE_BASE_PATH
|
||||
|
@ -12,7 +12,7 @@ from nonebot.params import CommandArg, ArgPlainText, Arg
|
||||
from nonebot.typing import T_State
|
||||
from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent, GroupMessageEvent, ActionFailed
|
||||
from nonebot.adapters.onebot.v11.helpers import convert_chinese_to_bool
|
||||
from LittlePaimon import NICKNAME, DRIVER, __version__
|
||||
from LittlePaimon.utils import NICKNAME, DRIVER, __version__
|
||||
from LittlePaimon.utils.files import save_json, load_json
|
||||
from LittlePaimon.utils.update import check_update, update
|
||||
|
||||
|
@ -4,9 +4,8 @@ from nonebot import get_bot, on_command
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent, MessageSegment
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from LittlePaimon import DRIVER
|
||||
from LittlePaimon.database import GeneralSub
|
||||
from LittlePaimon.utils import scheduler, logger
|
||||
from LittlePaimon.utils import scheduler, logger, DRIVER
|
||||
from LittlePaimon.utils.message import CommandObjectID, CommandSwitch, CommandTime
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
|
@ -1,2 +1,28 @@
|
||||
from typing import List
|
||||
|
||||
from nonebot import get_driver
|
||||
from .logger import logger
|
||||
from .scheduler import scheduler
|
||||
|
||||
__version__ = '3.0.0rc3'
|
||||
|
||||
DRIVER = get_driver()
|
||||
try:
|
||||
SUPERUSERS: List[int] = [int(s) for s in DRIVER.config.superusers]
|
||||
except Exception:
|
||||
SUPERUSERS = []
|
||||
logger.warning('请在.env.prod文件中中配置超级用户SUPERUSERS')
|
||||
|
||||
try:
|
||||
NICKNAME: str = list(DRIVER.config.nickname)[0]
|
||||
except Exception:
|
||||
NICKNAME = '派蒙'
|
||||
|
||||
__all__ = [
|
||||
'logger',
|
||||
'scheduler',
|
||||
'DRIVER',
|
||||
'SUPERUSERS',
|
||||
'NICKNAME',
|
||||
'__version__'
|
||||
]
|
||||
|
@ -9,6 +9,7 @@ from typing import Optional, Literal, Union, Tuple
|
||||
from nonebot import logger as nb_logger
|
||||
from tortoise.queryset import Q
|
||||
|
||||
from LittlePaimon.config import config
|
||||
from LittlePaimon.database import PublicCookie, PrivateCookie, CookieCache
|
||||
from LittlePaimon.utils import logger
|
||||
from .requests import aiorequests
|
||||
@ -293,7 +294,8 @@ async def get_mihoyo_private_data(
|
||||
server_id = 'cn_qd01' if uid[0] == '5' else 'cn_gf01'
|
||||
cookie_info, _ = await get_cookie(user_id, uid, True, True)
|
||||
if not cookie_info:
|
||||
return '未绑定私人cookie,获取cookie的教程:\ndocs.qq.com/doc/DQ3JLWk1vQVllZ2Z1\n获取后,使用[ysb cookie]指令绑定'
|
||||
return '未绑定私人cookie,获取cookie的教程:\ndocs.qq.com/doc/DQ3JLWk1vQVllZ2Z1\n获取后,使用[ysb cookie]指令绑定' \
|
||||
+ (f'或前往{config.CookieWeb_url}网页添加绑定' if config.CookieWeb_enable else '')
|
||||
if mode == 'role_skill':
|
||||
data = await aiorequests.get(url=CHARACTER_SKILL_API,
|
||||
headers=mihoyo_headers(q=f'uid={uid}®ion={server_id}&avatar_id={role_id}',
|
||||
@ -394,7 +396,8 @@ async def get_authkey_by_stoken(user_id: str, uid: str) -> Tuple[Optional[str],
|
||||
server_id = 'cn_qd01' if uid[0] == '5' else 'cn_gf01'
|
||||
cookie_info, _ = await get_cookie(user_id, uid, True, True)
|
||||
if not cookie_info:
|
||||
return '未绑定私人cookie,获取cookie的教程:\ndocs.qq.com/doc/DQ3JLWk1vQVllZ2Z1\n获取后,使用[ysb cookie]指令绑定', False, cookie_info
|
||||
return '未绑定私人cookie,获取cookie的教程:\ndocs.qq.com/doc/DQ3JLWk1vQVllZ2Z1\n获取后,使用[ysb cookie]指令绑定' \
|
||||
+ (f'或前往{config.CookieWeb_url}网页添加绑定' if config.CookieWeb_enable else ''), False, cookie_info
|
||||
if not cookie_info.stoken:
|
||||
return 'cookie中没有stoken字段,请重新绑定', False, cookie_info
|
||||
headers = {
|
||||
|
@ -4,8 +4,8 @@ from typing import Optional, Literal, Tuple, Union, List, AsyncGenerator, AsyncI
|
||||
|
||||
from playwright.async_api import Page, Browser, Playwright, async_playwright, Error
|
||||
|
||||
from LittlePaimon import DRIVER
|
||||
from LittlePaimon.utils import logger
|
||||
from . import DRIVER
|
||||
from .logger import logger
|
||||
|
||||
_playwright: Optional[Playwright] = None
|
||||
_browser: Optional[Browser] = None
|
||||
|
@ -12,7 +12,7 @@ import tqdm.asyncio
|
||||
from PIL import Image
|
||||
from ruamel import yaml
|
||||
|
||||
from LittlePaimon.utils.path import RESOURCE_BASE_PATH
|
||||
from .path import RESOURCE_BASE_PATH
|
||||
from .requests import aiorequests
|
||||
|
||||
# 删除从安柏计划下载的问号图标
|
||||
|
@ -116,7 +116,7 @@ class GenshinInfoManager:
|
||||
return data
|
||||
elif data['retcode'] == 1034:
|
||||
logger.info('原神信息', f'更新<m>{self.uid}</m>的玩家数据时出错,状态码为1034,<r>疑似验证码</r>')
|
||||
return '疑似遇验证码阻拦,请稍后再试'
|
||||
return '遇验证码阻拦,需手动前往米游社进行验证后才能继续使用'
|
||||
elif data['retcode'] != 0:
|
||||
logger.info('原神信息', f'更新<m>{self.uid}</m>的玩家数据时出错,消息为<r>{data["message"]}</r>')
|
||||
return data['message']
|
||||
|
@ -12,8 +12,8 @@ from nonebot.matcher import Matcher
|
||||
from nonebot.params import CommandArg, Depends
|
||||
from nonebot.typing import T_State
|
||||
|
||||
from LittlePaimon import NICKNAME
|
||||
from LittlePaimon.database import LastQuery, PrivateCookie, Player, PlayerAlias
|
||||
from . import NICKNAME
|
||||
from .alias import get_match_alias
|
||||
from .files import load_image
|
||||
from .filter import filter_msg
|
||||
|
@ -25,7 +25,6 @@ GENSHIN_DB_PATH = DATABASE_PATH / 'genshin.db'
|
||||
SUB_DB_PATH = DATABASE_PATH / 'subscription.db'
|
||||
GENSHIN_VOICE_DB_PATH = DATABASE_PATH / 'genshin_voice.db'
|
||||
MANAGER_DB_PATH = DATABASE_PATH / 'manager.db'
|
||||
LEARNING_CHAT_DB_PATH = DATABASE_PATH / 'learning_chat.db'
|
||||
# enka制图资源路径
|
||||
ENKA_RES = RESOURCE_BASE_PATH / 'enka_card'
|
||||
# 原神表情路径
|
||||
|
@ -4,7 +4,7 @@ import datetime
|
||||
import psutil
|
||||
from nonebot import get_bot
|
||||
|
||||
from LittlePaimon import DRIVER, NICKNAME
|
||||
from . import DRIVER, NICKNAME
|
||||
|
||||
start_time: str
|
||||
|
||||
|
@ -5,7 +5,7 @@ import git
|
||||
from git.exc import GitCommandError, InvalidGitRepositoryError
|
||||
from nonebot.utils import run_sync
|
||||
|
||||
from LittlePaimon import __version__, NICKNAME
|
||||
from . import __version__, NICKNAME
|
||||
from .requests import aiorequests
|
||||
|
||||
|
||||
|
@ -2,16 +2,11 @@ import nonebot
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from LittlePaimon import DRIVER
|
||||
from LittlePaimon.config import config
|
||||
from LittlePaimon.utils import logger
|
||||
from LittlePaimon.utils import logger, DRIVER
|
||||
from .api import BaseApiRouter
|
||||
from .pages import admin_app, login_page, bind_cookie_page, blank_page
|
||||
|
||||
app: FastAPI = nonebot.get_app()
|
||||
app.include_router(BaseApiRouter)
|
||||
|
||||
logger.info('Web UI', f'<g>启用成功</g>,默认地址为<m>http://127.0.0.1:{DRIVER.config.port}/LittlePaimon/login</m>')
|
||||
|
||||
requestAdaptor = '''
|
||||
requestAdaptor(api) {
|
||||
@ -31,27 +26,43 @@ responseAdaptor(api, payload, query, request, response) {
|
||||
},
|
||||
'''
|
||||
|
||||
|
||||
@app.get('/LittlePaimon/admin', response_class=HTMLResponse)
|
||||
async def admin():
|
||||
if config.admin_enable:
|
||||
return admin_app.render(site_title='LittlePaimon 后台管理', theme='antd', requestAdaptor=requestAdaptor,
|
||||
responseAdaptor=responseAdaptor)
|
||||
else:
|
||||
return blank_page.render(site_title='LittlePaimon')
|
||||
icon_path = 'http://static.cherishmoon.fun/LittlePaimon/readme/logo.png'
|
||||
|
||||
|
||||
@app.get('/LittlePaimon/login', response_class=HTMLResponse)
|
||||
async def login():
|
||||
if config.admin_enable:
|
||||
return login_page.render(site_title='登录 | LittlePaimon 后台管理', theme='antd')
|
||||
else:
|
||||
return blank_page.render(site_title='LittlePaimon')
|
||||
@DRIVER.on_startup
|
||||
def init_web():
|
||||
app: FastAPI = nonebot.get_app()
|
||||
app.include_router(BaseApiRouter)
|
||||
logger.info('Web UI', f'<g>启用成功</g>,默认地址为<m>http://127.0.0.1:{DRIVER.config.port}/LittlePaimon/login</m>')
|
||||
|
||||
@app.get('/LittlePaimon/admin', response_class=HTMLResponse)
|
||||
async def admin():
|
||||
if config.admin_enable:
|
||||
return admin_app.render(site_title='LittlePaimon 后台管理',
|
||||
site_icon=icon_path,
|
||||
theme=config.admin_theme,
|
||||
requestAdaptor=requestAdaptor,
|
||||
responseAdaptor=responseAdaptor)
|
||||
else:
|
||||
return blank_page.render(site_title='LittlePaimon',
|
||||
site_icon=icon_path)
|
||||
|
||||
@app.get('/LittlePaimon/cookie', response_class=HTMLResponse)
|
||||
async def bind_cookie():
|
||||
if config.CookieWeb_enable:
|
||||
return bind_cookie_page.render(site_title='绑定Cookie | LittlePaimon')
|
||||
else:
|
||||
return blank_page.render(site_title='LittlePaimon')
|
||||
@app.get('/LittlePaimon/login', response_class=HTMLResponse)
|
||||
async def login():
|
||||
if config.admin_enable:
|
||||
return login_page.render(site_title='登录 | LittlePaimon 后台管理',
|
||||
site_icon=icon_path,
|
||||
theme=config.admin_theme)
|
||||
else:
|
||||
return blank_page.render(site_title='LittlePaimon',
|
||||
site_icon=icon_path)
|
||||
|
||||
@app.get('/LittlePaimon/cookie', response_class=HTMLResponse)
|
||||
async def bind_cookie():
|
||||
if config.CookieWeb_enable:
|
||||
return bind_cookie_page.render(site_title='绑定Cookie | LittlePaimon',
|
||||
site_icon=icon_path,
|
||||
theme=config.admin_theme)
|
||||
else:
|
||||
return blank_page.render(site_title='LittlePaimon',
|
||||
site_icon=icon_path)
|
||||
|
@ -9,7 +9,7 @@ from fastapi.responses import JSONResponse
|
||||
from nonebot import get_bot
|
||||
from nonebot.adapters.onebot.v11 import Bot
|
||||
|
||||
from LittlePaimon import SUPERUSERS
|
||||
from LittlePaimon.utils import SUPERUSERS
|
||||
from LittlePaimon.utils.files import save_json
|
||||
from LittlePaimon.utils.tool import cache
|
||||
from LittlePaimon.utils.update import update
|
||||
@ -20,14 +20,25 @@ route = APIRouter()
|
||||
|
||||
@route.get('/get_group_list', response_class=JSONResponse, dependencies=[authentication()])
|
||||
async def get_group_list():
|
||||
bot: Bot = get_bot()
|
||||
return await bot.get_group_list()
|
||||
try:
|
||||
group_list = await get_bot().get_group_list()
|
||||
return [{'label': f'{group["group_name"]}({group["group_id"]})', 'value': group['group_id']} for group in group_list]
|
||||
except ValueError:
|
||||
return {
|
||||
'status': -100,
|
||||
'msg': '获取群和好友列表失败,请确认已连接GOCQ'
|
||||
}
|
||||
|
||||
|
||||
@route.get('/get_group_members', response_class=JSONResponse, dependencies=[authentication()])
|
||||
async def get_group_members(group_id: int):
|
||||
bot: Bot = get_bot()
|
||||
return await bot.get_group_member_list(group_id=group_id)
|
||||
try:
|
||||
return await get_bot().get_group_member_list(group_id=group_id)
|
||||
except ValueError:
|
||||
return {
|
||||
'status': -100,
|
||||
'msg': '获取群和好友列表失败,请确认已连接GOCQ'
|
||||
}
|
||||
|
||||
|
||||
@route.get('/get_groups_and_members', response_class=JSONResponse, dependencies=[authentication()])
|
||||
@ -36,7 +47,7 @@ async def get_groups_and_members():
|
||||
result = []
|
||||
try:
|
||||
bot: Bot = get_bot()
|
||||
except Exception:
|
||||
except ValueError:
|
||||
return {
|
||||
'status': -100,
|
||||
'msg': '获取群和好友列表失败,请确认已连接GOCQ'
|
||||
@ -83,8 +94,11 @@ async def get_friend_list():
|
||||
friend_list = await bot.get_friend_list()
|
||||
return [{'label': f'{friend["nickname"]}({friend["user_id"]})', 'value': friend['user_id']} for friend in
|
||||
friend_list]
|
||||
except Exception:
|
||||
return {'status': 100, 'msg': '获取好友列表失败'}
|
||||
except ValueError:
|
||||
return {
|
||||
'status': -100,
|
||||
'msg': '获取群和好友列表失败,请确认已连接GOCQ'
|
||||
}
|
||||
|
||||
|
||||
@route.post('/bot_update', response_class=JSONResponse, dependencies=[authentication()])
|
||||
|
@ -2,7 +2,7 @@ from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from LittlePaimon import SUPERUSERS
|
||||
from LittlePaimon.utils import SUPERUSERS
|
||||
from LittlePaimon.config import config
|
||||
from .utils import create_token
|
||||
|
||||
|
@ -1,46 +1,45 @@
|
||||
# from nonebot import logger
|
||||
# from nonebot.log import default_filter, default_format
|
||||
# from LittlePaimon import DRIVER
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from nonebot.log import logger, default_filter, default_format
|
||||
from LittlePaimon.utils.status import get_status
|
||||
from .utils import authentication
|
||||
|
||||
show_logs = []
|
||||
info_logs = []
|
||||
debug_logs = []
|
||||
|
||||
|
||||
# @DRIVER.on_startup
|
||||
# async def start_up():
|
||||
#
|
||||
# def record_log(message: str):
|
||||
# show_logs.append(message)
|
||||
#
|
||||
# logger.opt(colors=True, ansi=True).add(record_log, colorize=True, filter=default_filter, format=default_format)
|
||||
def record_info_log(message: str):
|
||||
info_logs.append(message)
|
||||
if len(info_logs) > 500:
|
||||
info_logs.pop(0)
|
||||
|
||||
|
||||
def record_debug_log(message: str):
|
||||
# 过滤一些无用日志
|
||||
if not any(w in message for w in {'Checking for matchers', 'Running PreProcessors', 'OneBot V11 | Calling API'}):
|
||||
debug_logs.append(message)
|
||||
if len(debug_logs) > 300:
|
||||
debug_logs.pop(0)
|
||||
|
||||
|
||||
logger.add(record_info_log, level='INFO', colorize=True, filter=default_filter, format=default_format)
|
||||
logger.add(record_debug_log, level='DEBUG', colorize=True, filter=default_filter, format=default_format)
|
||||
|
||||
route = APIRouter()
|
||||
|
||||
|
||||
# @route.get('/log', response_class=StreamingResponse)
|
||||
# async def get_log():
|
||||
# async def streaming_logs():
|
||||
# count = 0
|
||||
# while True:
|
||||
# if show_logs:
|
||||
# yield show_logs.pop(0)
|
||||
# count = 0
|
||||
# else:
|
||||
# count += 1
|
||||
# if count > 600:
|
||||
# yield '超过一分钟没有新日志,日志已断开,请刷新页面重新连接\n'
|
||||
# await asyncio.sleep(2)
|
||||
# break
|
||||
# else:
|
||||
# yield '\n'
|
||||
# await asyncio.sleep(0.1)
|
||||
#
|
||||
# return StreamingResponse(streaming_logs())
|
||||
@route.get('/log', response_class=StreamingResponse)
|
||||
async def get_log(level: str = 'info'):
|
||||
show_logs = info_logs if level == 'info' else debug_logs
|
||||
|
||||
async def streaming_logs():
|
||||
for log in show_logs:
|
||||
yield log
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
return StreamingResponse(streaming_logs())
|
||||
|
||||
|
||||
@route.get('/status', response_class=JSONResponse, dependencies=[authentication()])
|
||||
|
@ -4,7 +4,7 @@ from typing import Optional
|
||||
from fastapi import Header, HTTPException, Depends
|
||||
from jose import jwt
|
||||
|
||||
from LittlePaimon import SUPERUSERS
|
||||
from LittlePaimon.utils import SUPERUSERS
|
||||
from LittlePaimon.config import config
|
||||
|
||||
SECRET_KEY = config.secret_key
|
||||
|
@ -1,4 +1,4 @@
|
||||
from LittlePaimon import __version__
|
||||
from LittlePaimon.utils import __version__
|
||||
from amis import AmisAPI, Collapse, Form, InputNumber, Textarea, Action, LevelEnum, Divider, Page, Html
|
||||
|
||||
collapse_text = "<h2>重要提醒:</h2>Cookie的作用相当于账号密码,非常重要,如是非可信任的机器人,请勿绑定!!<br><h2>获取方法:</h2>详见<a href='https://docs.qq.com/doc/DQ3JLWk1vQVllZ2Z1'>Cookie获取教程</a>"
|
||||
|
@ -13,7 +13,7 @@ cookie_web_form = Form(
|
||||
label='是否启用CookieWeb',
|
||||
name='启用CookieWeb',
|
||||
value='${启用CookieWeb}',
|
||||
labelRemark=Remark(content='是否启用为用户提供的绑定Cookie的网页'),
|
||||
labelRemark=Remark(shape='circle', content='是否启用为用户提供的绑定Cookie的网页'),
|
||||
onText='启用',
|
||||
offText='关闭'
|
||||
),
|
||||
@ -21,27 +21,51 @@ cookie_web_form = Form(
|
||||
label='CookieWeb地址',
|
||||
name='CookieWeb地址',
|
||||
value='${CookieWeb地址}',
|
||||
labelRemark=Remark(content='只是设置对用户显示的CookieWeb地址,要填写实际的地址')
|
||||
labelRemark=Remark(shape='circle', content='只是设置对用户显示的CookieWeb地址,要填写实际的地址')
|
||||
),
|
||||
Switch(
|
||||
label='是否启用Web端',
|
||||
name='启用Web端',
|
||||
value='${启用Web端}',
|
||||
labelRemark=Remark(content='即本Web管理页面,注意,关闭后刷新本页面会及时不能访问'),
|
||||
labelRemark=Remark(shape='circle', content='即本Web管理页面,注意,关闭后刷新本页面将不再能访问'),
|
||||
onText='启用',
|
||||
offText='关闭'
|
||||
),
|
||||
Select(
|
||||
label='Web端主题',
|
||||
name='Web端主题',
|
||||
value='${Web端主题}',
|
||||
labelRemark=Remark(shape='circle', content='Web端及CookieWeb的外观主题,刷新页面生效'),
|
||||
options=[
|
||||
{
|
||||
'label': '云舍',
|
||||
'value': 'default'
|
||||
},
|
||||
{
|
||||
'label': '仿AntD',
|
||||
'value': 'antd'
|
||||
},
|
||||
{
|
||||
'label': 'ang',
|
||||
'value': 'ang'
|
||||
},
|
||||
{
|
||||
'label': '暗黑',
|
||||
'value': 'dark'
|
||||
},
|
||||
]
|
||||
),
|
||||
InputText(
|
||||
label='Web端管理员密码',
|
||||
name='Web端管理员密码',
|
||||
value='${Web端管理员密码}',
|
||||
labelRemark=Remark(content='用于超级用户登录该Web端,修改后重启生效')
|
||||
labelRemark=Remark(shape='circle', content='用于超级用户登录该Web端,修改后重启生效')
|
||||
),
|
||||
InputText(
|
||||
label='Web端token密钥',
|
||||
name='Web端token密钥',
|
||||
value='${Web端token密钥}',
|
||||
labelRemark=Remark(content='用于对Web端身份认证的token进行加密,为32位字符串,请不要保持为默认密钥,务必进行修改,修改后重启生效')
|
||||
labelRemark=Remark(shape='circle', content='用于对Web端身份认证的token进行加密,为32位字符串,请不要保持为默认密钥,务必进行修改,修改后重启生效')
|
||||
),
|
||||
],
|
||||
actions=action_button
|
||||
@ -56,7 +80,7 @@ sim_gacha_form = Form(
|
||||
label='群冷却',
|
||||
name='模拟抽卡群冷却',
|
||||
value='${模拟抽卡群冷却}',
|
||||
labelRemark=Remark(content='每个群在多少秒内只能进行一次抽卡'),
|
||||
labelRemark=Remark(shape='circle', content='每个群在多少秒内只能进行一次抽卡'),
|
||||
displayMode='enhance',
|
||||
suffix='秒',
|
||||
min=0,
|
||||
@ -65,7 +89,7 @@ sim_gacha_form = Form(
|
||||
label='群员冷却',
|
||||
name='模拟抽卡群员冷却',
|
||||
value='${模拟抽卡群员冷却}',
|
||||
labelRemark=Remark(content='在上一个配置的基础上,每位群员在多少秒内只能进行一次抽卡'),
|
||||
labelRemark=Remark(shape='circle', content='在上一个配置的基础上,每位群员在多少秒内只能进行一次抽卡'),
|
||||
displayMode='enhance',
|
||||
suffix='秒',
|
||||
min=0,
|
||||
@ -74,7 +98,7 @@ sim_gacha_form = Form(
|
||||
label='单次最多十连数',
|
||||
name='模拟抽卡单次最多十连数',
|
||||
value='${模拟抽卡单次最多十连数}',
|
||||
labelRemark=Remark(content='单次模拟抽卡同时最多的十连数,推荐不超过6次'),
|
||||
labelRemark=Remark(shape='circle', content='单次模拟抽卡同时最多的十连数,推荐不超过6次'),
|
||||
displayMode='enhance',
|
||||
suffix='次',
|
||||
min=1
|
||||
@ -99,7 +123,7 @@ auto_mys_form = Form(
|
||||
label='米游社自动签到开始时间',
|
||||
name='米游社签到开始时间',
|
||||
value='${米游社签到开始时间}',
|
||||
labelRemark=Remark(content='会在每天这个时间点进行米游社自动签到任务,修改后重启生效'),
|
||||
labelRemark=Remark(shape='circle', content='会在每天这个时间点进行米游社自动签到任务,修改后重启生效'),
|
||||
inputFormat='HH时mm分',
|
||||
format='HH:mm'
|
||||
),
|
||||
@ -115,7 +139,7 @@ auto_mys_form = Form(
|
||||
label='米游币自动获取开始时间',
|
||||
name='米游币开始执行时间',
|
||||
value='${米游币开始执行时间}',
|
||||
labelRemark=Remark(content='会在每天这个时间点进行米游币自动获取任务,修改后重启生效'),
|
||||
labelRemark=Remark(shape='circle', content='会在每天这个时间点进行米游币自动获取任务,修改后重启生效'),
|
||||
inputFormat='HH时mm分',
|
||||
format='HH:mm'
|
||||
),
|
||||
@ -131,7 +155,7 @@ auto_mys_form = Form(
|
||||
label='云原神签到开始时间',
|
||||
name='云原神签到开始时间',
|
||||
value='${云原神签到开始时间}',
|
||||
labelRemark=Remark(content='会在每天这个时间点进行云原神自动签到,修改后重启生效'),
|
||||
labelRemark=Remark(shape='circle', content='会在每天这个时间点进行云原神自动签到,修改后重启生效'),
|
||||
inputFormat='HH时',
|
||||
timeFormat='HH',
|
||||
format='HH'
|
||||
@ -155,7 +179,7 @@ ssbq_form = Form(
|
||||
label='实时便签停止检查时间段',
|
||||
name='实时便签停止检查时间段',
|
||||
value='${实时便签停止检查时间段}',
|
||||
labelRemark=Remark(
|
||||
labelRemark=Remark(shape='circle',
|
||||
content='在这段时间(例如深夜)不进行实时便签检查,注意开始时间不要晚于结束时间,不然会有问题'),
|
||||
timeFormat='HH',
|
||||
format='HH',
|
||||
@ -165,7 +189,7 @@ ssbq_form = Form(
|
||||
label='实时便签检查间隔',
|
||||
name='实时便签检查间隔',
|
||||
value='${实时便签检查间隔}',
|
||||
labelRemark=Remark(content='每多少分钟检查进行一次实时便签,推荐不快于8分钟,修改后重启生效'),
|
||||
labelRemark=Remark(shape='circle', content='每多少分钟检查进行一次实时便签,推荐不快于8分钟,修改后重启生效'),
|
||||
displayMode='enhance',
|
||||
suffix='分钟',
|
||||
min=1,
|
||||
@ -217,7 +241,7 @@ notice_form = Form(
|
||||
label='启用好友和群请求通知',
|
||||
name='启用好友和群请求通知',
|
||||
value='${启用好友和群请求通知}',
|
||||
labelRemark=Remark(content='开启后,会在机器人收到好友或群添加、拉群等请求时向超管发消息'),
|
||||
labelRemark=Remark(shape='circle', content='开启后,会在机器人收到好友或群添加、拉群等请求时向超管发消息'),
|
||||
onText='开启',
|
||||
offText='关闭'
|
||||
),
|
||||
@ -225,7 +249,7 @@ notice_form = Form(
|
||||
label='自动接受好友请求',
|
||||
name='自动接受好友请求',
|
||||
value='${自动接受好友请求}',
|
||||
labelRemark=Remark(content='开启后,机器人会自动接受所有好友请求'),
|
||||
labelRemark=Remark(shape='circle', content='开启后,机器人会自动接受所有好友请求'),
|
||||
onText='开启',
|
||||
offText='关闭'
|
||||
),
|
||||
@ -233,7 +257,7 @@ notice_form = Form(
|
||||
label='自动接受群邀请',
|
||||
name='自动接受群邀请',
|
||||
value='${自动接受群邀请}',
|
||||
labelRemark=Remark(content='开启后,机器人会自动接受所有拉群请求'),
|
||||
labelRemark=Remark(shape='circle', content='开启后,机器人会自动接受所有拉群请求'),
|
||||
onText='开启',
|
||||
offText='关闭'
|
||||
),
|
||||
@ -241,7 +265,7 @@ notice_form = Form(
|
||||
label='启用好友和群欢迎消息',
|
||||
name='启用好友和群欢迎消息',
|
||||
value='${启用好友和群欢迎消息}',
|
||||
labelRemark=Remark(content='开启后,会向新添加的好友以及新进入的群发送欢迎消息'),
|
||||
labelRemark=Remark(shape='circle', content='开启后,会向新添加的好友以及新进入的群发送欢迎消息'),
|
||||
onText='开启',
|
||||
offText='关闭'
|
||||
),
|
||||
@ -257,7 +281,7 @@ other_form = Form(
|
||||
label='网页截图权限',
|
||||
name='启用网页截图权限',
|
||||
value='${启用网页截图权限}',
|
||||
labelRemark=Remark(content='开启后,任何人都能使用网页截图,关闭后则只有超管能使用'),
|
||||
labelRemark=Remark(shape='circle', content='开启后,任何人都能使用网页截图,关闭后则只有超管能使用'),
|
||||
onText='所有人',
|
||||
offText='仅超级用户'
|
||||
),
|
||||
@ -265,7 +289,7 @@ other_form = Form(
|
||||
label='原神猜语音时间',
|
||||
name='原神猜语音时间',
|
||||
value='${原神猜语音时间}',
|
||||
labelRemark=Remark(content='原神猜语音小游戏的持续时间'),
|
||||
labelRemark=Remark(shape='circle', content='原神猜语音小游戏的持续时间'),
|
||||
displayMode='enhance',
|
||||
suffix='秒',
|
||||
min=5,
|
||||
|
@ -1,5 +1,5 @@
|
||||
from LittlePaimon import __version__
|
||||
from amis import Page, PageSchema, Html, Property, Service, Flex, ActionType, LevelEnum, Divider
|
||||
from LittlePaimon.utils import __version__
|
||||
from amis import Page, PageSchema, Html, Property, Service, Flex, ActionType, LevelEnum, Divider, ButtonGroupSelect, Log, Alert, Form, Dialog
|
||||
|
||||
logo = Html(html=f'''
|
||||
<p align="center">
|
||||
@ -17,21 +17,59 @@ logo = Html(html=f'''
|
||||
</div>
|
||||
<br>
|
||||
''')
|
||||
select_log = ButtonGroupSelect(
|
||||
label='日志等级',
|
||||
name='log_level',
|
||||
btnLevel=LevelEnum.light,
|
||||
btnActiveLevel=LevelEnum.info,
|
||||
value='/LittlePaimon/api/log?level=info',
|
||||
options=[
|
||||
{
|
||||
'label': 'INFO',
|
||||
'value': '/LittlePaimon/api/log?level=info'
|
||||
},
|
||||
{
|
||||
'label': 'DEBUG',
|
||||
'value': '/LittlePaimon/api/log?level=debug'
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
log_page = Log(
|
||||
autoScroll=True,
|
||||
placeholder='暂无日志数据...',
|
||||
operation=['stop', 'showLineNumber', 'filter'],
|
||||
source='${log_level | raw}'
|
||||
)
|
||||
|
||||
operation_button = Flex(justify='center', items=[
|
||||
ActionType.Ajax(
|
||||
label='更新',
|
||||
api='/LittlePaimon/api/bot_update',
|
||||
confirmText='该操作将会对Bot进行检查并尝试更新,请在更新完成后重启Bot使更新生效',
|
||||
level=LevelEnum.info
|
||||
),
|
||||
ActionType.Ajax(
|
||||
label='重启',
|
||||
className='m-l',
|
||||
api='/LittlePaimon/api/bot_restart',
|
||||
confirmText='该操作将会使Bot重启,在完成重启之前,该页面也将无法访问(也可能会弹出报错,可无视),请耐心等待重启',
|
||||
level=LevelEnum.danger
|
||||
)
|
||||
ActionType.Ajax(
|
||||
label='更新',
|
||||
api='/LittlePaimon/api/bot_update',
|
||||
confirmText='该操作将会对Bot进行检查并尝试更新,请在更新完成后重启Bot使更新生效',
|
||||
level=LevelEnum.info
|
||||
),
|
||||
ActionType.Ajax(
|
||||
label='重启',
|
||||
className='m-l',
|
||||
api='/LittlePaimon/api/bot_restart',
|
||||
confirmText='该操作将会使Bot重启,在完成重启之前,该页面也将无法访问(也可能会弹出报错,可无视),请耐心等待重启',
|
||||
level=LevelEnum.danger
|
||||
),
|
||||
ActionType.Dialog(
|
||||
label='日志',
|
||||
className='m-l',
|
||||
level=LevelEnum.primary,
|
||||
dialog=Dialog(title='查看日志',
|
||||
size='xl',
|
||||
actions=[],
|
||||
body=[
|
||||
Alert(level=LevelEnum.info,
|
||||
body='查看最近最多500条日志,不会自动刷新,需要手动点击两次"暂停键"来进行刷新。'),
|
||||
Form(
|
||||
body=[select_log, log_page]
|
||||
)])
|
||||
)
|
||||
])
|
||||
|
||||
status = Service(
|
||||
@ -81,25 +119,5 @@ status = Service(
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# log_page = Log(
|
||||
# height=500,
|
||||
# autoScroll=True,
|
||||
# placeholder='日志加载中...',
|
||||
# operation=['stop', 'filter'],
|
||||
# source='/LittlePaimon/api/log'
|
||||
# )
|
||||
# log_button = ActionType.Dialog(
|
||||
# label='查看日志',
|
||||
# dialog=Dialog(
|
||||
# title='Nonebot日志',
|
||||
# body=log_page,
|
||||
# size='lg')
|
||||
# )
|
||||
# text = Tpl(tpl='接收消息数:${msg_received} | 发送消息数:${msg_sent}')
|
||||
|
||||
# page_detail = Page(title='主页',
|
||||
# initApi='/LittlePaimon/api/status',
|
||||
# body=[text, log_button])
|
||||
page_detail = Page(title='', body=[logo, operation_button, Divider(), status])
|
||||
page = PageSchema(url='/home', label='首页', icon='fa fa-home', isDefaultPage=True, schema=page_detail)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from LittlePaimon import __version__
|
||||
from LittlePaimon.utils import __version__
|
||||
from amis import Form, InputText, InputPassword, DisplayModeEnum, Horizontal, Remark, Html, Page, AmisAPI, Wrapper
|
||||
|
||||
logo = Html(html=f'''
|
||||
@ -30,9 +30,9 @@ login_api = AmisAPI(
|
||||
)
|
||||
|
||||
login_form = Form(api=login_api, title='', body=[
|
||||
InputText(name='user_id', label='用户ID', labelRemark=Remark(content='超级用户的QQ号,在.env.prod文件中配置')),
|
||||
InputPassword(name='password', label='密码', labelRemark=Remark(content='默认为admin,可以在paimon_config.json中修改')),
|
||||
# Switch(name='is_admin', label='身份组', onText='管理员', offText='用户', labelRemark=Remark(content='是否以管理员身份登录'))
|
||||
InputText(name='user_id', label='用户ID', labelRemark=Remark(shape='circle', content='超级用户的QQ号,在.env.prod文件中配置')),
|
||||
InputPassword(name='password', label='密码', labelRemark=Remark(shape='circle', content='默认为admin,可以在paimon_config.json中修改')),
|
||||
# Switch(name='is_admin', label='身份组', onText='管理员', offText='用户', labelRemark=Remark(shape='circle', content='是否以管理员身份登录'))
|
||||
], mode=DisplayModeEnum.horizontal, horizontal=Horizontal(left=3, right=9, offset=5), redirect='/LittlePaimon/admin')
|
||||
body = Wrapper(className='w-2/5 mx-auto my-0 m:w-full', body=login_form)
|
||||
login_page = Page(title='', body=[logo, body])
|
||||
|
@ -1,5 +1,5 @@
|
||||
from LittlePaimon import __version__
|
||||
from amis import App, PageSchema, Tpl, Page, Flex
|
||||
from amis import App, PageSchema, Tpl, Page, Flex, Log
|
||||
from LittlePaimon.utils import __version__
|
||||
from .config_manage import page as config_page
|
||||
from .home_page import page as home_page
|
||||
from .plugin_manage import page as plugin_manage_page
|
||||
@ -11,6 +11,12 @@ github_logo = Tpl(className='w-full',
|
||||
tpl='<div class="flex justify-between"><div></div><div><a href="https://github.com/CMHopeSunshine/LittlePaimon" target="_blank" title="Copyright"><i class="fa fa-github fa-2x"></i></a></div></div>')
|
||||
header = Flex(className='w-full', justify='flex-end', alignItems='flex-end', items=[github_logo])
|
||||
|
||||
log_page = Log(
|
||||
autoScroll=True,
|
||||
placeholder='日志加载中...',
|
||||
operation=['stop', 'filter'],
|
||||
source='/LittlePaimon/api/log'
|
||||
)
|
||||
|
||||
admin_app = App(brandName='LittlePaimon',
|
||||
logo='http://static.cherishmoon.fun/LittlePaimon/readme/logo.png',
|
||||
@ -21,8 +27,8 @@ admin_app = App(brandName='LittlePaimon',
|
||||
PageSchema(label='Cookie管理', icon='fa fa-key',
|
||||
children=[public_cookie_page, private_cookie_page]),
|
||||
PageSchema(label='机器人配置', icon='fa fa-wrench',
|
||||
children=[plugin_manage_page, config_page]),
|
||||
children=[plugin_manage_page, config_page])
|
||||
]}],
|
||||
footer=f'<div class="p-2 text-center bg-blue-100">Copyright © 2021 - 2022 <a href="https://github.com/CMHopeSunshine/LittlePaimon" target="_blank" class="link-secondary">LittlePaimon v{__version__}</a> X<a target="_blank" href="https://github.com/baidu/amis" class="link-secondary" rel="noopener"> amis v2.2.0</a></div>')
|
||||
|
||||
blank_page = Page(title='LittlePaimon', body='该页面未开启或不存在')
|
||||
blank_page = Page(title='LittlePaimon 404', body='该页面未开启或不存在')
|
||||
|
@ -7,7 +7,7 @@
|
||||
<p align="center">
|
||||
<a href="https://cdn.jsdelivr.net/gh/CMHopeSunshine/LittlePaimon@master/LICENSE"><img src="https://img.shields.io/github/license/CMHopeSunshine/LittlePaimon" alt="license"></a>
|
||||
<img src="https://img.shields.io/badge/Python-3.8+-yellow" alt="python">
|
||||
<img src="https://img.shields.io/badge/Version-3.0.0rc2-green" alt="version">
|
||||
<img src="https://img.shields.io/badge/Version-3.0.0rc3-green" alt="version">
|
||||
<a href="https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&inviteCode=MmWrI&from=246610&biz=ka"><img src="https://img.shields.io/badge/QQ频道交流-尘世闲游-blue?style=flat-square" alt="QQ guild"></a>
|
||||
</p>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user