群聊学习重构**(旧数据将不进行迁移)**,Web UI增加群聊学习配置日志查看主题切换,优化部分提示(如出现pydantic相关报错,请更新amis-python库)

This commit is contained in:
CMHopeSunshine 2022-11-13 19:19:36 +08:00
parent d9398c2fb2
commit 8ab63be8a1
43 changed files with 2083 additions and 908 deletions

View File

@ -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'))

View File

@ -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):

View File

@ -29,7 +29,7 @@
"10000039": ["迪奥娜", "dio娜", "猫猫", "冰猫"],
"10000041": ["莫娜", "穷b", "占星术士", "半部讨龙真君"],
"10000042": ["刻晴", "刻师傅", "阿晴", "刻猫猫", "牛杂师傅", "玉衡星"],
"10000043": ["砂糖", "眼镜娘", "雷莹", "风荧"],
"10000043": ["砂糖", "眼镜娘", "雷莹"],
"10000044": ["辛焱", "黑妹", "摇滚"],
"10000045": ["罗莎莉亚", "修女", "罗莎"],
"10000046": ["胡桃", "whotao", "堂主"],
@ -121,6 +121,7 @@
"乐团剑"
],
"渔获": [
"渔获",
"鱼叉"
],
"衔珠海皇": [

View File

@ -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

View File

@ -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():

View File

@ -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 *

View File

@ -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('&#91;'):
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>')

View File

@ -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())

View File

@ -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()

View File

@ -0,0 +1,656 @@
角色
神里绫华
绫华
白鹭公主
0华
凌华
琴团长
空哥
龙哥
丽莎
魔女
荧妹
妹妹
主角
芭芭拉
内鬼
凯亚
凝冰渡海真君
迪卢克
卢姥爷
姥爷
卢锅巴
正义人
雷泽
狼崽
卢皮卡
安柏
打火姬
温迪
巴巴托斯
风神
芭芭脱丝
卖唱
香菱
卯师傅
锅巴
北斗
龙王
行秋
水神
秋秋人
金鹏
夜叉
三眼五显仙人
降魔大圣
靖妖傩舞
凝光
富婆
天权星
可莉
哒哒哒
炸弹人
火花骑士
嘟嘟可
钟离
帝君
岩神
摩拉克斯
岩王爷
岩王帝君
菲谢尔
皇女
奥兹
乌鸦
中二少女
小艾咪
班尼特
点赞哥
达达利亚
公子
愚人众
阿贾克斯
达达鸭
诺艾尔
女仆
高达
岩王帝姬
七七
77
肚饿真君
重云
甘雨
王小美
椰羊
椰奶
阿贝多
炼金术士
迪奥娜
dio娜
猫猫
莫娜
占星术士
半部讨龙真君
刻晴
刻师傅
阿晴
刻猫猫
牛杂师傅
玉衡星
砂糖
辛焱
黑妹
罗莎莉亚
修女
罗莎
胡桃
whotao
堂主
枫原万叶
万叶
叶天帝
烟绯
罗翔
宵宫
霄宫
托马
优菈
优拉
浪花骑士
尤拉
雷电将军
雷神
巴尔
巴尔泽布
雷军
雷电影
煮饭婆
奶香一刀
早柚
忍者
终末番
珊瑚宫心海
心海
观赏鱼
五郎
修狗
希娜小姐
九条裟罗
九条
荒泷一斗
一斗
斗子哥
八重神子
神子
八重
屑狐狸
埃洛伊
申鹤
云堇
云先生
云瑾
神里绫人
0人
绫人
凌人
神里凌人
夜兰
久岐忍
阿卡丽
鹿野院平藏
平藏
小鹿
柯莱
多莉
提纳里
小提
妮露
赛诺
风纪官
坎蒂丝
潘森
纳西妲
草神
小吉祥草王
草萝莉
莱依拉
散兵
伞兵
国崩
卢本伟
sb
流浪者
迪希雅
艾尔海森
白术
武器
磐岩结绿
绿箭
绿剑
斫峰之刃
斫峰
盾剑
无工之剑
无工
贯虹之槊
贯虹
岩枪
盾枪
赤角石溃杵
赤角
石溃杵
尘世之锁
盾书
终末嗟叹之诗
终末
终末弓
乐团弓
松籁响起之时
松籁
乐团大剑
松剑
苍古自由之誓
苍古
乐团剑
渔获
鱼叉
衔珠海皇
咸鱼大剑
匣里日月
日月
匣里灭辰
灭辰
匣里龙吟
龙吟
天空之翼
天空弓
天空之刃
天空剑
天空之卷
天空书
天空之脊
天空枪
薄荷枪
天空之傲
天空大剑
四风原典
四风
试作斩岩
斩岩
试作星镰
星镰
试作金珀
金珀
试作古华
古华
试作澹月
澹月
千岩长枪
千岩枪
千岩古剑
千岩剑
千岩大剑
暗巷闪光
暗巷剑
暗巷猎手
暗巷弓
阿莫斯之弓
阿莫斯
痛苦弓
雾切之回光
雾切
飞雷之弦振
飞雷
飞雷弓
薙草之稻光
薙草
稻光
薙草稻光
马尾枪
马尾
薙刀
神乐之真意
神乐
真意
狼的末路
狼末
护摩之杖
护摩
和璞鸢
鸟枪
绿枪
风鹰剑
风鹰
冬极白星
冬极
不灭月华
月华
波乱月白经津
波乱
波波津
若水
麒麟弓
昭心
糟心
幽夜华尔兹
幽夜
雪葬的星银
雪葬
雪葬星银
雪山大剑
喜多院十文字
喜多院
十文字
万国诸海图谱
万国
万国诸海
天目影打刀
天目刀
天目
破魔之弓
破魔弓
曚云之月
曚云弓
流月针
流浪乐章
赌狗书
赌狗乐章
赌狗
桂木斩长正
桂木
斩长正
腐殖之剑
腐殖
腐殖剑
风花之颂
风花弓
证誓之明瞳
证誓
明瞳
证誓明瞳
嘟嘟可故事集
嘟嘟可
辰砂之纺锤
辰砂
纺锤
白辰之环
白辰
决斗之枪
决斗枪
决斗
月卡枪
螭骨剑
螭骨
丈育剑
离骨剑
月卡大剑
黑剑
月卡剑
苍翠猎弓
绿弓
月卡弓
讨龙英杰谭
讨龙
神射手之誓
神射手
黑缨枪
史莱姆枪
「渔获」
以理服人
佣兵重剑
信使
冷刃
历练的猎弓
反曲弓
口袋魔导书
吃虎鱼刀
学徒笔记
宗室大剑
宗室猎枪
宗室秘法录
宗室长剑
宗室长弓
异世界行记
弓藏
弹弓
忍冬之果
息灾
恶王丸
掠食者
断浪长鳍
新手长枪
旅行剑
无锋剑
暗巷的酒与诗
暗铁剑
沐浴龙血的剑
猎弓
甲级宝珏
白影剑
白缨枪
白铁大剑
祭礼剑
祭礼大剑
祭礼弓
祭礼残章
笛剑
绝弦
翡玉法球
落霞
西风剑
西风大剑
西风猎弓
西风秘典
西风长枪
训练大剑
钟剑
钢轮弓
钺矛
铁尖枪
铁影阔剑
铁蜂刺
银剑
降临之剑
雨裁
飞天大御剑
飞天御剑
魔导绪论
鸦羽弓
黎明神剑
黑岩刺枪
黑岩枪
黑岩战弓
黑岩弓
黑岩斩刀
黑岩绯玉
黑岩长剑
龙脊长枪
笼钓瓶一心
万叶刀
一心传名刀
猎人之径
绿弓
草弓
提纳里专武
竭泽
鱼弓
王下近侍
须弥锻造弓
贯月矢
须弥锻造长枪
盈满之实
须弥锻造法器
森林王器
须弥锻造大剑
原木刀
须弥锻造单手剑
圣显之钥
圣显
不灭剑华
妮露武器
西福斯的月光
西福斯
月光
赤沙之杖
赤沙
风信之锋
风信
玛海菈的水色
玛海菈
水色
千夜浮梦
千夜
神灯
茶壶
流浪的晚星
晚星
圣遗物
游医
冒险家
幸运儿
学士
战狂
赌徒
武人
守护之心
流放者
行者之心
奇迹
勇士之心
教官
如雷的盛怒
如雷
追忆之注连
追忆
冰风迷途的勇士
冰套
染血的骑士道
染血
饰金之梦生
饰金
精通
华馆梦醒形骸记
华馆
防御
昔日宗室之仪
宗室
沉沦之心
水套
悠古的磐岩
岩套
海染砗磲
海染
毒奶
翠绿之影
风套
苍白之火
苍白
物理
流浪大地的乐团
流浪
逆飞的流星
逆飞
流星
平息鸣雷的尊者
平雷
辰砂往生录
辰砂
掉血
渡过烈火的贤人
渡火
千岩牢固
千岩
生命
被怜爱的少女
治疗
少女
来歆余响
普攻
余响
炽烈的炎之魔女
火套
魔女
绝缘之旗印
充能
绝缘
角斗士的终幕礼
角斗
深林的记忆
草套
礼冠
敌人
丘丘人
盗宝团
史莱姆
飘浮灵
骗骗花
愚人众
野伏众
丘丘暴徒
深渊法师
债务处理人
萤术士
遗迹机兵
遗迹重机
遗迹猎者
遗迹守卫
幼岩龙蜥
岩龙蜥
丘丘王
大雪猪王
狂风之核
藏镜侍女
兽境之狼
无相
黄金王兽
古岩龙蜥
爆炎树
急冻树
纯水精灵
雷音权现
冰雾花
烈焰花
物品
原石
摩拉
相遇之缘
蓝球
纠缠之缘
粉球
创世结晶
凝取结晶
648
328
198
月卡
大月卡
原粹树脂
脆弱树脂
浓缩树脂
树脂
秘境
七天神像
传送锚点
尘歌壶
七圣召唤
地名
璃月
蒙德
龙脊雪山
稻妻
渊下宫
须弥
战斗系统
元素反应
蒸发
融化
冻结
感电
超载
结晶
扩散
燃烧
绽放
超绽放
烈绽放
激化
超激化
蔓激化
属性
攻击力
防御力
生命值
暴击率
暴击伤害
元素精通
元素充能效率
护盾强效
其他
原神
原魔
原壶
原牌
崩坏
氪金
刷本
周本
狗托
果面呐噻
哒咩
达咩
啊这

View 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('&#91;') and raw_message.endswith('&#93;'):
# 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

View File

@ -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')

View 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)

View 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%概率为430%概率为310%概率为2。')),
InputNumber(label='复读阈值', name='repeat_threshold', value='${repeat_threshold}', visibleOn='${enable}',
min=2,
labelRemark=Remark(shape='circle', content=f'跟随复读所需要的阈值有N个人复读后{NICKNAME}就会跟着复读。')),
InputNumber(label='打断复读概率', name='break_probability', value='${break_probability}',
min=0, max=100, suffix='%', visibleOn='${AND(enable, speak_enable)}',
labelRemark=Remark(shape='circle', content='达到复读阈值时,打断复读而不是跟随复读的概率。')),
InputTag(label='屏蔽词', name='ban_words', value='${ban_words}', enableBatchAdd=True,
placeholder='添加屏蔽词', visibleOn='${enable}', joinValues=False, extractValue=True,
labelRemark=Remark(shape='circle', content='含有这些词的消息不会学习和回复。(回车进行添加)')),
InputTag(label='屏蔽用户', source='${member_list}', name='ban_users', value='${ban_users}', enableBatchAdd=True,
placeholder='添加屏蔽用户', visibleOn='${enable}', joinValues=False, extractValue=True,
labelRemark=Remark(shape='circle', content='和该群中这些用户有关的消息不会学习和回复。(回车进行添加)')),
Switch(label='主动发言开关', name='speak_enable', value='${speak_enable}', visibleOn='${enable}',
labelRemark=Remark(shape='circle', content=f'是否允许{NICKNAME}在该群主动发言,主动发言是指每隔一段时间挑选一个热度较高的群,主动发一些学习过的内容。')),
InputNumber(label='主动发言阈值', name='speak_threshold', value='${speak_threshold}',
visibleOn='${AND(enable, speak_enable)}', min=0,
labelRemark=Remark(shape='circle', content='值越低,主动发言的可能性越高。')),
InputNumber(label='主动发言最小间隔', name='speak_min_interval', value='${speak_min_interval}', min=0,
visibleOn='${AND(enable, speak_enable)}', suffix='',
labelRemark=Remark(shape='circle', content='进行主动发言的最小时间间隔。')),
InputNumber(label='连续主动发言概率', name='speak_continuously_probability',
value='${speak_continuously_probability}', min=0, max=100, suffix='%',
visibleOn='${AND(enable, speak_enable)}', labelRemark=Remark(shape='circle', content='触发主动发言时,连续进行发言的概率。')),
InputNumber(label='最大连续主动发言句数', name='speak_continuously_max_len',
value='${speak_continuously_max_len}', visibleOn='${AND(enable, speak_enable)}', min=1,
labelRemark=Remark(shape='circle', content='连续主动发言的最大句数。')),
InputNumber(label='主动发言附带戳一戳概率', name='speak_poke_probability', value='${speak_poke_probability}',
min=0, max=100, suffix='%', visibleOn='${AND(enable, speak_enable)}',
labelRemark=Remark(shape='circle', content='主动发言时附带戳一戳的概率会在最近5个发言者中随机选一个戳。')),
],
actions=[Action(label='保存', level=LevelEnum.success, type='submit'),
ActionType.Ajax(
label='保存至所有群',
level=LevelEnum.primary,
confirmText='确认将当前配置保存至所有群?',
api='post:/LittlePaimon/api/chat_group_config?group_id=all'
),
Action(label='重置', level=LevelEnum.warning, type='reset')]
)
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)

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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__'
]

View File

@ -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}&region={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 = {

View File

@ -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

View File

@ -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
# 删除从安柏计划下载的问号图标

View File

@ -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']

View File

@ -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

View File

@ -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'
# 原神表情路径

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()])

View File

@ -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

View File

@ -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()])

View File

@ -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

View File

@ -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>"

View File

@ -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,

View File

@ -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)

View File

@ -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])

View File

@ -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='该页面未开启或不存在')

View File

@ -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>