🎉 Web UI管理页面

This commit is contained in:
CMHopeSunshine 2022-10-25 12:13:44 +08:00
parent 46ed8f893c
commit c95b0df400
29 changed files with 2487 additions and 885 deletions

View File

@ -7,7 +7,7 @@ from LittlePaimon.utils.migration import migrate_database
from LittlePaimon.utils.tool import check_resource
DRIVER = get_driver()
__version__ = '3.0.0beta9'
__version__ = '3.0.0rc1'
try:
SUPERUSERS: List[int] = [int(s) for s in DRIVER.config.superusers]
@ -33,7 +33,7 @@ logo = """<g>
async def startup():
logger.opt(colors=True).info(logo)
await database.connect()
from LittlePaimon import admin
from LittlePaimon import web
# await migrate_database()
await check_resource()

View File

@ -1,12 +0,0 @@
import nonebot
from fastapi import FastAPI
from LittlePaimon.utils import logger
from LittlePaimon.manager.plugin_manager import plugin_manager
app: FastAPI = nonebot.get_app()
if plugin_manager.config.CookieWeb_enable:
from pywebio.platform.fastapi import webio_routes
from .bind_cookie import bind_cookie_page
logger.info('原神Cookie', f'<g>启用CookieWeb成功地址{plugin_manager.config.CookieWeb_url}</g>')
app.mount('/LittlePaimon/cookie', FastAPI(routes=webio_routes(bind_cookie_page)))

View File

@ -1,62 +0,0 @@
import datetime
from pywebio.input import *
from pywebio.output import *
from pywebio.platform import config
from pywebio.session import run_asyncio_coroutine
from LittlePaimon.utils import logger
from LittlePaimon.database.models import LastQuery, PrivateCookie
from LittlePaimon.utils.api import get_bind_game_info, get_stoken_by_cookie
css_style = 'body {background: #000000 url(https://static.cherishmoon.fun/blog/h-wallpaper/zhounianqing.png);} #input-container {background: rgba(0,0,0,0);} summary {background-color: rgba(255,255,255,1);} .markdown-body {background-color: rgba(255,255,255,1);}'
@config(title='小派蒙绑定原神Cookie', description='绑定Cookie', css_style=css_style)
async def bind_cookie_page():
with put_collapse('获取Cookie的方法'):
put_markdown('**重要提醒**Cookie的作用相当于账号密码非常重要如是非可信任的机器人请勿绑定')
put_markdown('**方法**:详见[Cookie获取教程](https://docs.qq.com/doc/DQ3JLWk1vQVllZ2Z1)')
data = await input_group('绑定Cookie', [
input('QQ号', name='qq', required=True, validate=is_qq_number),
textarea('米游社Cookie', name='cookie', required=True),
checkbox(name='confirm', options=['我已知晓Cookie的重要性确认绑定'], validate=is_confirm)
])
result = await run_asyncio_coroutine(bind_cookie(data))
popup('绑定结果', put_markdown(result))
def is_qq_number(qq: str):
if not qq.isdigit():
return '必须是合法的QQ号'
def is_cookie(cookie: str):
if 'cookie_token' not in cookie or all(i not in cookie for i in ['account_id', 'login_uid', 'ltuid', 'stuid']):
return 'Cookie必须包含cookie_token以及account_id、login_uid、ltuid、stuid其中之一'
def is_confirm(confirm: list):
if not confirm:
return '请先勾选确认'
async def bind_cookie(data: dict):
if result := await get_bind_game_info(data['cookie']):
game_name = result['nickname']
game_uid = result['game_role_id']
mys_id = result['mys_id']
await LastQuery.update_or_create(user_id=str(data['qq']),
defaults={'uid': game_uid, 'last_time': datetime.datetime.now()})
logger.info('原神Cookie', '', {'用户': str(data['qq']), 'uid': game_uid}, '成功绑定cookie', True)
if 'login_ticket' in data['cookie'] and (stoken := await get_stoken_by_cookie(data['cookie'])):
await PrivateCookie.update_or_create(user_id=str(data['qq']), uid=game_uid, mys_id=mys_id,
defaults={'cookie': data['cookie'],
'stoken': f'stuid={mys_id};stoken={stoken};'})
return f'QQ`{data["qq"]}`成功绑定原神玩家`{game_name}`-UID`{game_uid}`'
else:
await PrivateCookie.update_or_create(user_id=str(data['qq']), uid=game_uid, mys_id=mys_id,
defaults={'cookie': data['cookie']})
return f'QQ`{data["qq"]}`成功绑定原神玩家`{game_name}`-UID`{game_uid}`\n但cookie中没有`login_ticket`或`login_ticket`无效,`米游币相关功能`无法使用哦'
else:
return '这个cookie**无效**,请确认是否正确\n请重新获取cookie后**刷新**本页面再次绑定'

View File

@ -83,9 +83,8 @@ class PluginManager:
await asyncio.gather(*[PluginPermission.update_or_create(name=plugin.name, session_id=user['user_id'],
session_type='user') for user in user_list])
if plugin.name not in hidden_plugins:
metadata = plugin.metadata
if plugin.name not in self.data:
if metadata:
if metadata := plugin.metadata:
self.data[plugin.name] = PluginInfo.parse_obj({
'name': metadata.name,
'module_name': plugin.name,
@ -122,3 +121,14 @@ class PluginManager:
plugin.matchers.sort(key=lambda x: x.pm_priority)
plugin.matchers = [m for m in plugin.matchers if m.pm_show and m.pm_usage]
return plugin_list
async def get_plugin_list_for_admin(self) -> List[dict]:
load_plugins = nb_plugin.get_loaded_plugins()
load_plugins = [p.name for p in load_plugins]
plugin_list = [p.dict() for p in self.data.values()]
for plugin in plugin_list:
plugin['matchers'].sort(key=lambda x: x['pm_priority'])
plugin['isLoad'] = plugin['module_name'] in load_plugins
plugin['status'] = await PluginPermission.filter(name=plugin['module_name'], status=True).exists()
plugin_list.sort(key=lambda x: (x['isLoad'], x['status'], -x['priority']), reverse=True)
return plugin_list

View File

@ -71,3 +71,7 @@ class Config(BaseModel):
screenshot_enable: bool = Field(True, alias='启用网页截图权限')
guess_voice_time: int = Field(30, alias='原神猜语音时间')
admin_enable: bool = Field(True, alias='启用Web端')
admin_password: str = Field('admin', alias='Web端管理员密码')
secret_key: str = Field('49c294d32f69b732ef6447c18379451ce1738922a75cd1d4812ef150318a2ed0', alias='Web端token密钥')

View File

@ -11,7 +11,7 @@ from LittlePaimon.utils.brower import screenshot
async def permission_check(event: MessageEvent) -> bool:
if pm.config.screenshot_enable:
return True
return event.user_id not in SUPERUSERS and event.sender.role not in ['admin', 'owner']
return event.user_id not in SUPERUSERS
__plugin_meta__ = PluginMetadata(

View File

@ -0,0 +1,53 @@
import asyncio
import datetime
import psutil
from nonebot import get_bot
from LittlePaimon import DRIVER, NICKNAME
start_time: str
async def get_status():
status_result = {
'nickname': NICKNAME
}
try:
status_result['start_time'] = start_time
except Exception:
status_result['start_time'] = '未知'
try:
bot = get_bot()
bot_status = await bot.get_status()
status_result['bot_id'] = bot.self_id
if bot_status := bot_status.get('stat'):
print(bot_status)
status_result['msg_received'] = bot_status.get('message_received', '未知')
status_result['msg_sent'] = bot_status.get('message_sent', '未知')
except Exception:
status_result['bot_id'] = '未知'
status_result['msg_received'] = '未知'
status_result['msg_sent'] = '未知'
status_result['system_start_time'] = datetime.datetime.fromtimestamp(psutil.boot_time()).strftime(
"%Y-%m-%d %H:%M:%S")
psutil.cpu_percent()
await asyncio.sleep(0.1)
cpu_percent = psutil.cpu_percent()
# cpu_count = psutil.cpu_count(logical=False)
# cpu_count_logical = psutil.cpu_count()
# cpu_freq = psutil.cpu_freq()
ram_stat = psutil.virtual_memory()
swap_stat = psutil.swap_memory()
status_result['cpu_percent'] = f'{cpu_percent}%'
status_result['ram_percent'] = f'{ram_stat.percent}%'
status_result['swap_percent'] = f'{swap_stat.percent}%'
return status_result
@DRIVER.on_startup
async def start_up():
global start_time
start_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')

View File

@ -1,5 +1,8 @@
import asyncio
import datetime
import functools
import hashlib
import inspect
import time
from collections import defaultdict
from LittlePaimon.utils import aiorequests, logger
@ -45,6 +48,37 @@ class FreqLimiter:
freq_limiter = FreqLimiter()
def cache(ttl=datetime.timedelta(hours=1)):
"""
缓存装饰器
:param ttl: 过期时间
"""
def wrap(func):
cache_data = {}
@functools.wraps(func)
async def wrapped(*args, **kw):
nonlocal cache_data
bound = inspect.signature(func).bind(*args, **kw)
bound.apply_defaults()
ins_key = '|'.join([f'{k}_{v}' for k, v in bound.arguments.items()])
default_data = {"time": None, "value": None}
data = cache_data.get(ins_key, default_data)
now = datetime.datetime.now()
if not data['time'] or now - data['time'] > ttl:
try:
data['value'] = await func(*args, **kw)
data['time'] = now
cache_data[ins_key] = data
except Exception as e:
raise e
return data['value']
return wrapped
return wrap
async def check_resource():
logger.info('资源检查', '开始检查资源')
resource_list = await aiorequests.get('http://img.genshin.cherishmoon.fun/resources/resources_list')
@ -59,7 +93,8 @@ async def check_resource():
file_path.unlink()
flag = True
try:
await aiorequests.download(url=f'http://img.genshin.cherishmoon.fun/resources/{resource["path"]}', save_path=file_path)
await aiorequests.download(url=f'http://img.genshin.cherishmoon.fun/resources/{resource["path"]}',
save_path=file_path)
await asyncio.sleep(0.5)
except Exception as e:
logger.warning('资源检查', f'下载<m>{resource.split("/")[-1]}</m>时<r>出错: {e}</r>')
@ -67,5 +102,3 @@ async def check_resource():
logger.info('资源检查', '<g>资源下载完成</g>')
else:
logger.info('资源检查', '<g>资源完好,无需下载</g>')

View File

@ -0,0 +1,56 @@
import nonebot
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from LittlePaimon.utils import logger
from .pages import admin_app, login_page, bind_cookie_page, blank_page
from .api import BaseApiRouter
from LittlePaimon.manager.plugin_manager import plugin_manager
app: FastAPI = nonebot.get_app()
app.include_router(BaseApiRouter)
logger.info('Web UI', '<g>启用成功</g>默认地址为https://127.0.0.1:13579/LittlePaimon/login')
requestAdaptor = '''
requestAdaptor(api) {
api.headers["token"] = localStorage.getItem("token");
return api;
},
'''
responseAdaptor = '''
responseAdaptor(api, payload, query, request, response) {
if (response.data.detail == '登录验证失败或已失效,请重新登录') {
window.location.href = '/LittlePaimon/login'
window.localStorage.clear()
window.sessionStorage.clear()
window.alert('登录验证失败或已失效,请重新登录')
}
return payload
},
'''
@app.get('/LittlePaimon/admin', response_class=HTMLResponse)
async def admin():
if plugin_manager.config.admin_enable:
return admin_app.render(site_title='LittlePaimon 后台管理', theme='antd', requestAdaptor=requestAdaptor,
responseAdaptor=responseAdaptor)
else:
return blank_page.render(site_title='LittlePaimon')
@app.get('/LittlePaimon/login', response_class=HTMLResponse)
async def login():
if plugin_manager.config.admin_enable:
return login_page.render(site_title='登录 | LittlePaimon 后台管理', theme='antd')
else:
return blank_page.render(site_title='LittlePaimon')
@app.get('/LittlePaimon/cookie', response_class=HTMLResponse)
async def bind_cookie():
if plugin_manager.config.CookieWeb_enable:
return bind_cookie_page.render(site_title='绑定Cookie | LittlePaimon')
else:
return blank_page.render(site_title='LittlePaimon')

View File

@ -0,0 +1,17 @@
from fastapi import APIRouter
from .cookie import route as cookie_route
from .plugin import route as plugin_route
from .bot_info import route as bot_info_route
from .status import route as status_route
from .login import route as login_route
from .utils import authentication
# from .learning_chat import route as chat_route
BaseApiRouter = APIRouter(prefix='/LittlePaimon/api')
BaseApiRouter.include_router(cookie_route)
BaseApiRouter.include_router(plugin_route)
BaseApiRouter.include_router(bot_info_route)
BaseApiRouter.include_router(status_route)
BaseApiRouter.include_router(login_route)
# BaseApiRouter.include_router(chat_route)

View File

@ -0,0 +1,105 @@
import asyncio
import datetime
import os
import sys
from pathlib import Path
from fastapi.responses import JSONResponse
from fastapi import APIRouter
from nonebot import get_bot
from nonebot.adapters.onebot.v11 import Bot
from LittlePaimon import SUPERUSERS
from LittlePaimon.manager.bot_manager.handler import update
from LittlePaimon.utils.files import save_json
from LittlePaimon.utils.tool import cache
from .utils import authentication
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()
@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)
@route.get('/get_groups_and_members', response_class=JSONResponse, dependencies=[authentication()])
@cache(datetime.timedelta(minutes=10))
async def get_groups_and_members():
result = []
try:
bot: Bot = get_bot()
except Exception:
return {
'status': -100,
'msg': '获取群和好友列表失败请确认已连接GOCQ'
}
group_list = await bot.get_group_list()
friend_list = await bot.get_friend_list()
for group in group_list:
group_members = await bot.get_group_member_list(group_id=group['group_id'])
result.append({
'left_label': f'{group["group_name"]}({group["group_id"]})',
'right_label': f'{group["group_name"]}(群{group["group_id"]})',
'value': f'{group["group_id"]}',
'children': [{'left_label': f'{m["card"] or m["nickname"]}({m["user_id"]})',
'right_label': f'群({group["group_name"]}) - {m["card"] or m["nickname"]}({m["user_id"]})',
'value': f'{group["group_id"]}.{m["user_id"]}'} for m in group_members if
str(m['user_id']) != bot.self_id]
})
await asyncio.sleep(0.2)
result = [
{
'label': '群组',
'selectMode': 'tree',
'searchable': True,
'children': result
},
{
'label': '好友',
'selectMode': 'list',
'searchable': True,
'children': [{
'left_label': f'{f["nickname"]}({f["user_id"]})',
'right_label': f'{f["nickname"]}({f["user_id"]})',
'value': f'{f["user_id"]}'
} for f in friend_list if str(f['user_id']) != bot.self_id]
}]
return result
@route.get('/get_friend_list', response_class=JSONResponse, dependencies=[authentication()])
@cache(datetime.timedelta(minutes=10))
async def get_friend_list():
try:
bot: Bot = get_bot()
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': '获取好友列表失败'}
@route.post('/bot_update', response_class=JSONResponse, dependencies=[authentication()])
async def bot_update():
result = await update()
return {
'status': 0,
'msg': result
}
@route.post('/bot_restart', response_class=JSONResponse, dependencies=[authentication()])
async def bot_restart():
save_json({'session_type': 'private',
'session_id': SUPERUSERS[0]},
Path() / 'rebooting.json')
if sys.argv[0].endswith('nb'):
sys.argv[0] = 'bot.py'
os.execv(sys.executable, ['python'] + sys.argv)

View File

@ -0,0 +1,111 @@
import datetime
from typing import Optional
from LittlePaimon.database.models import PublicCookie, PrivateCookie, LastQuery
from fastapi.responses import JSONResponse
from fastapi import APIRouter
from pydantic import BaseModel
from LittlePaimon.utils.api import get_bind_game_info, get_stoken_by_cookie
from .utils import authentication
route = APIRouter()
class BindCookie(BaseModel):
user_id: int
cookie: str
@route.post('/bind_cookie', response_class=JSONResponse)
async def bind_cookie(data: BindCookie):
if game_info := await get_bind_game_info(data.cookie):
game_uid = game_info['game_role_id']
mys_id = game_info['mys_id']
await LastQuery.update_or_create(user_id=data.user_id,
defaults={'uid': game_uid, 'last_time': datetime.datetime.now()})
if 'login_ticket' in data.cookie and (stoken := await get_stoken_by_cookie(data.cookie)):
await PrivateCookie.update_or_create(user_id=data.user_id, uid=game_uid, mys_id=mys_id,
defaults={'cookie': data.cookie,
'stoken': f'stuid={mys_id};stoken={stoken};'})
return {'status': 0, 'msg': f'QQ{data.user_id}的UID{game_uid}的Cookie以及Stoken绑定成功。'}
else:
await PrivateCookie.update_or_create(user_id=data.user_id, uid=game_uid, mys_id=mys_id,
defaults={'cookie': data.cookie})
return {'status': 0, 'msg': f'QQ{data.user_id}的UID{game_uid}绑定Cookie成功但未绑定stoken。'}
else:
return {'status': 200, 'msg': '该Cookie无效请根据教程重新获取'}
@route.get('/get_public_cookies', response_class=JSONResponse, dependencies=[authentication()])
async def get_public_cookies(status: Optional[int] = None):
if status is None:
return await PublicCookie.all().values()
else:
return await PublicCookie.filter(status=status).values()
@route.get('/get_private_cookies', response_class=JSONResponse, dependencies=[authentication()])
async def get_private_cookies(page: int = 1, perPage: int = 10, user_id: Optional[str] = None,
uid: Optional[str] = None, mys_id: Optional[str] = None, status: Optional[int] = None):
query = {'user_id__contains': user_id} if user_id else {'uid__contains': uid} if uid else {
'mys_id__contains': mys_id} if mys_id else {}
if status is not None:
query['status'] = status
data = await PrivateCookie.filter(**query).offset((page - 1) * perPage).limit(perPage).values()
return {
'status': 0,
'msg': 'ok',
'data': {
'items': data,
'total': await PrivateCookie.filter(**query).count()
}
}
@route.get('/get_private_cookie', response_class=JSONResponse, dependencies=[authentication()])
async def get_private_cookie(id: int):
return await PrivateCookie.get_or_none(id=id).values()
@route.delete('/delete_public_cookie', response_class=JSONResponse, dependencies=[authentication()])
async def delete_public_cookie(id: int):
await PublicCookie.filter(id=id).delete()
return {'status': 0, 'msg': f'{id}号公共Cookie删除成功'}
@route.delete('/delete_private_cookie', response_class=JSONResponse, dependencies=[authentication()])
async def delete_private_cookie(id: int):
await PrivateCookie.filter(id=id).delete()
return {'status': 0, 'msg': f'{id}号私人Cookie删除成功'}
@route.post('/add_public_cookie', response_class=JSONResponse, dependencies=[authentication()])
async def add_public_cookie(data: dict):
cookie = data.get('cookie')
if not cookie:
return {'status': 100, 'msg': '参数错误'}
if await get_bind_game_info(cookie, True):
new_cookie = await PublicCookie.create(cookie=cookie)
return {'status': 0, 'msg': f'{new_cookie.id}号公共Cookie添加成功'}
else:
return {'status': 200, 'msg': '该Cookie无效'}
@route.post('/save_private_cookie', response_class=JSONResponse, dependencies=[authentication()])
async def save_private_cookie(data: BindCookie):
if game_info := await get_bind_game_info(data.cookie):
game_uid = game_info['game_role_id']
mys_id = game_info['mys_id']
await LastQuery.update_or_create(user_id=data.user_id,
defaults={'uid': game_uid, 'last_time': datetime.datetime.now()})
if 'login_ticket' in data.cookie and (stoken := await get_stoken_by_cookie(data.cookie)):
await PrivateCookie.update_or_create(user_id=data.user_id, uid=game_uid, mys_id=mys_id,
defaults={'cookie': data.cookie,
'stoken': f'stuid={mys_id};stoken={stoken};'})
return {'status': 0, 'msg': f'QQ{data.user_id}的UID{game_uid}的Cookie以及Stoken添加/保存成功。'}
else:
await PrivateCookie.update_or_create(user_id=data.user_id, uid=game_uid, mys_id=mys_id,
defaults={'cookie': data.cookie})
return {'status': 0, 'msg': f'QQ{data.user_id}的UID{game_uid}的Cookie添加/保存成功但未绑定stoken。'}
else:
return {'status': 200, 'msg': '该Cookie无效请根据教程重新获取'}

View File

@ -0,0 +1,33 @@
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from LittlePaimon import SUPERUSERS
from LittlePaimon.manager.plugin_manager import plugin_manager
from .utils import create_token
PASSWORD = plugin_manager.config.admin_password
class UserModel(BaseModel):
user_id: int
password: str
route = APIRouter()
@route.post('/login', response_class=JSONResponse)
async def login(user: UserModel):
if user.user_id not in SUPERUSERS or user.password != PASSWORD:
return {
'status': -100,
'msg': '登录失败请确认用户ID和密码无误'
}
token = create_token(user.user_id)
return {
'status': 0,
'msg': '登录成功',
'data': {
'token': token
}
}

View File

@ -0,0 +1,151 @@
import datetime
from pathlib import Path
from fastapi.responses import JSONResponse
from fastapi import APIRouter
from LittlePaimon.database.models import PluginPermission
from LittlePaimon.manager.plugin_manager import plugin_manager
from LittlePaimon.manager.plugin_manager.model import PluginInfo, Config
from .utils import authentication
route = APIRouter()
@route.get('/get_plugins', response_class=JSONResponse, dependencies=[authentication()])
async def get_plugins():
plugins = await plugin_manager.get_plugin_list_for_admin()
return {
'status': 0,
'msg': 'ok',
'data': {
'rows': plugins,
'total': len(plugins)
}
}
@route.post('/set_plugin_status', response_class=JSONResponse, dependencies=[authentication()])
async def set_plugin_status(data: dict):
module_name = data.get('plugin')
status = data.get('status')
await PluginPermission.filter(name=module_name).update(status=status)
return {'status': 0, 'msg': f'成功设置{module_name}插件状态为{status}'}
@route.get('/get_plugin_bans', response_class=JSONResponse, dependencies=[authentication()])
async def get_plugin_status(module_name: str):
result = []
bans = await PluginPermission.filter(name=module_name).all()
for ban in bans:
if ban.session_type == 'group':
result.extend(f'{ban.session_id}.{b}' for b in ban.ban)
if not ban.status:
result.append(f'{ban.session_id}')
elif ban.session_type == 'user' and not ban.status:
result.append(f'{ban.session_id}')
return {
'status': 0,
'msg': 'ok',
'data': {
'module_name': module_name,
'bans': result
}
}
@route.post('/set_plugin_bans', response_class=JSONResponse, dependencies=[authentication()])
async def set_plugin_bans(data: dict):
bans = data['bans']
name = data['module_name']
await PluginPermission.filter(name=name).update(status=True, ban=[])
for ban in bans:
if ban.startswith(''):
if '.' in ban:
group_id = int(ban.split('.')[0][1:])
user_id = int(ban.split('.')[1])
plugin = await PluginPermission.filter(name=name, session_type='group', session_id=group_id).first()
plugin.ban.append(user_id)
await plugin.save()
else:
await PluginPermission.filter(name=name, session_type='group', session_id=int(ban[1:])).update(
status=False)
else:
await PluginPermission.filter(name=name, session_type='user', session_id=int(ban)).update(status=False)
return {
'status': 0,
'msg': '插件权限设置成功'
}
@route.post('/set_plugin_detail', response_class=JSONResponse, dependencies=[authentication()])
async def set_plugin_detail(plugin_info: PluginInfo):
plugin_manager.data[plugin_info.module_name] = plugin_info
plugin_manager.save()
return {
'status': 0,
'msg': '插件信息设置成功'
}
@route.get('/get_config', response_class=JSONResponse, dependencies=[authentication()])
async def get_config():
config = plugin_manager.config.dict(by_alias=True)
config['米游社签到开始时间'] = datetime.datetime(1970, 1, 1, hour=config['米游社签到开始时间(小时)'], minute=config['米游社签到开始时间(分钟)']).strftime('%H:%M')
config['米游币开始执行时间'] = datetime.datetime(1970, 1, 1, hour=config['米游币开始执行时间(小时)'], minute=config['米游币开始执行时间(分钟)']).strftime('%H:%M')
config['实时便签停止检查时间段'] = (f'0{config["实时便签停止检查开始时间"]}' if config['实时便签停止检查开始时间'] < 10 else str(config['实时便签停止检查开始时间'])) + \
':00,' + (f'0{config["实时便签停止检查结束时间"]}' if config['实时便签停止检查结束时间'] < 10 else str(config['实时便签停止检查结束时间'])) + ':00'
config['云原神签到开始时间'] = f'0{config["云原神签到时间(小时)"]}' if config['云原神签到时间(小时)'] < 10 else str(config['云原神签到时间(小时)'])
return {
'status': 0,
'msg': 'ok',
'data': config
}
@route.post('/set_config', response_class=JSONResponse, dependencies=[authentication()])
async def set_config(data: dict):
if '米游社签到开始时间' in data:
temp_time = datetime.datetime.strptime(data['米游社签到开始时间'], '%H:%M')
data['米游社签到开始时间(小时)'] = temp_time.hour
data['米游社签到开始时间(分钟)'] = temp_time.minute
if '米游币开始执行时间' in data:
temp_time = datetime.datetime.strptime(data['米游币开始执行时间'], '%H:%M')
data['米游币开始执行时间(小时)'] = temp_time.hour
data['米游币开始执行时间(分钟)'] = temp_time.minute
if '实时便签停止检查时间段' in data:
temp_time_split = data['实时便签停止检查时间段'].split(',')
data['实时便签停止检查开始时间'] = int(temp_time_split[0][:2])
data['实时便签停止检查结束时间'] = int(temp_time_split[1][:2])
if '云原神签到开始时间' in data:
data['云原神签到时间(小时)'] = int(data['云原神签到开始时间'])
config = plugin_manager.config.dict(by_alias=True)
config.update(**data)
plugin_manager.config = Config.parse_obj(config)
plugin_manager.save()
return {
'status': 0,
'msg': '保存成功'
}
@route.get('/env_config', response_class=JSONResponse, dependencies=[authentication()])
async def env_config(file_name: str):
return {
'status': 0,
'msg': 'ok',
'data': {
'data': (Path() / file_name).read_text(encoding='utf-8')
}
}
@route.post('/env_config', response_class=JSONResponse, dependencies=[authentication()])
async def env_config(file_name: str, data: dict):
with open(Path() / file_name, 'w', encoding='utf-8') as f:
f.write(data['editor'])
return {
'status': 0,
'msg': f'{file_name}文件保存成功'
}

View File

@ -0,0 +1,51 @@
import asyncio
# from nonebot import logger
# from nonebot.log import default_filter, default_format
# from LittlePaimon import DRIVER
from LittlePaimon.utils.status import get_status
from fastapi.responses import JSONResponse
from fastapi import APIRouter
from .utils import authentication
show_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)
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('/status', response_class=JSONResponse, dependencies=[authentication()])
async def status():
return await get_status()

View File

@ -0,0 +1,28 @@
import datetime
from typing import Optional
from fastapi import Header, HTTPException, Depends
from jose import jwt
from LittlePaimon import SUPERUSERS
from LittlePaimon.manager.plugin_manager import plugin_manager
SECRET_KEY = plugin_manager.config.secret_key
ALGORITHM = 'HS256'
TOKEN_EXPIRE_MINUTES = 30
def authentication():
def inner(token: Optional[str] = Header(...)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=ALGORITHM)
if not (user_id := payload.get('user_id')) or int(user_id) not in SUPERUSERS:
raise HTTPException(status_code=400, detail='登录验证失败或已失效,请重新登录')
except (jwt.JWTError, jwt.ExpiredSignatureError, AttributeError):
raise HTTPException(status_code=400, detail='登录验证失败或已失效,请重新登录')
return Depends(inner)
def create_token(user_id: int):
data = {'user_id': user_id, 'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=TOKEN_EXPIRE_MINUTES)}
return jwt.encode(data, SECRET_KEY, algorithm=ALGORITHM)

View File

@ -0,0 +1,3 @@
from .main import admin_app, blank_page
from .login import login_page
from .bind_cookie import bind_cookie_page

View File

@ -0,0 +1,23 @@
from amis import AmisAPI, Collapse, Form, InputNumber, Textarea, Action, LevelEnum, Divider, Page, Html
from LittlePaimon import __version__
mk = {
"type": "markdown",
"value": "**重要提醒**"
}
collapse_text = "<h2>重要提醒:</h2>Cookie的作用相当于账号密码非常重要如是非可信任的机器人请勿绑定<br><h2>获取方法:</h2>详见<a href='https://docs.qq.com/doc/DQ3JLWk1vQVllZ2Z1'>Cookie获取教程</a>"
api = AmisAPI(method='post', url='/LittlePaimon/api/bind_cookie')
collapse = Collapse(header='Cookie说明及获取方法', body=Html(html=collapse_text))
form = Form(title='绑定Cookie', api=api, body=[
InputNumber(name='user_id', label='QQ号', description='在此处输入你要绑定的QQ号', required=True),
Textarea(name='cookie', label='Cookie', description='在此处粘贴你的Cookie', required=True, clearable=True),
# Checkboxes(name='function', label='同时开启以下功能', options=[
# {'label': '米游社自动签到', 'value': 'sign'},
# {'label': '米游币自动获取', 'value': 'coin'}
# ], joinValues=False, extractValue=True)
], actions=[Action(label='绑定', level=LevelEnum.success, confirmText='我已知晓Cookie的重要性确认绑定', type='submit'),
Action(label='重置', level=LevelEnum.warning, type='reset')])
footer = Html(html=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>')
bind_cookie_page = Page(title='绑定Cookie', body=[collapse, Divider(), form, footer])

View File

@ -0,0 +1,348 @@
from amis import Action, Divider, Form, InputText, LevelEnum, Page, PageSchema, Switch, Remark, InputNumber, InputTime, InputTimeRange, Alert, Editor, Tabs, TabsModeEnum, Select
action_button = [Action(label='保存', level=LevelEnum.success, type='submit'),
Action(label='重置', level=LevelEnum.warning, type='reset')]
cookie_web_form = Form(
title='Web端配置',
api='/LittlePaimon/api/set_config',
visibleOn='${select == 1}',
body=[
Switch(
label='是否启用CookieWeb',
name='启用CookieWeb',
value='${启用CookieWeb}',
labelRemark=Remark(content='是否启用为用户提供的绑定Cookie的网页'),
onText='启用',
offText='关闭'
),
InputText(
label='CookieWeb地址',
name='CookieWeb地址',
value='${CookieWeb地址}',
labelRemark=Remark(content='只是设置对用户显示的CookieWeb地址要填写实际的地址')
),
Switch(
label='是否启用Web端',
name='启用Web端',
value='${启用Web端}',
labelRemark=Remark(content='即本Web管理页面注意关闭后刷新本页面会及时不能访问'),
onText='启用',
offText='关闭'
),
InputText(
label='Web端管理员密码',
name='Web端管理员密码',
value='${Web端管理员密码}',
labelRemark=Remark(content='用于超级用户登录该Web端修改后重启生效')
),
InputText(
label='Web端token密钥',
name='Web端token密钥',
value='${Web端token密钥}',
labelRemark=Remark(content='用于对Web端身份认证的token进行加密为32位字符串请不要保持为默认密钥务必进行修改修改后重启生效')
),
],
actions=action_button
)
sim_gacha_form = Form(
title='模拟抽卡',
api='/LittlePaimon/api/set_config',
visibleOn='${select == 2}',
body=[
InputNumber(
label='群冷却',
name='模拟抽卡群冷却',
value='${模拟抽卡群冷却}',
labelRemark=Remark(content='每个群在多少秒内只能进行一次抽卡'),
displayMode='enhance',
suffix='',
min=0,
),
InputNumber(
label='群员冷却',
name='模拟抽卡群员冷却',
value='${模拟抽卡群员冷却}',
labelRemark=Remark(content='在上一个配置的基础上,每位群员在多少秒内只能进行一次抽卡'),
displayMode='enhance',
suffix='',
min=0,
),
InputNumber(
label='单次最多十连数',
name='模拟抽卡单次最多十连数',
value='${模拟抽卡单次最多十连数}',
labelRemark=Remark(content='单次模拟抽卡同时最多的十连数推荐不超过6次'),
displayMode='enhance',
suffix='',
min=1
)
],
actions=action_button
)
auto_mys_form = Form(
title='自动任务',
api='/LittlePaimon/api/set_config',
visibleOn='${select == 3}',
body=[
Switch(
label='米游社自动签到开关',
name='米游社自动签到开关',
value='${米游社自动签到开关}',
onText='开启',
offText='关闭'
),
InputTime(
label='米游社自动签到开始时间',
name='米游社签到开始时间',
value='${米游社签到开始时间}',
labelRemark=Remark(content='会在每天这个时间点进行米游社自动签到任务,修改后重启生效'),
inputFormat='HH时mm分',
format='HH:mm'
),
Divider(),
Switch(
label='米游币自动获取开关',
name='米游币自动获取开关',
value='${米游币自动获取开关}',
onText='开启',
offText='关闭'
),
InputTime(
label='米游币自动获取开始时间',
name='米游币开始执行时间',
value='${米游币开始执行时间}',
labelRemark=Remark(content='会在每天这个时间点进行米游币自动获取任务,修改后重启生效'),
inputFormat='HH时mm分',
format='HH:mm'
),
Divider(),
Switch(
label='云原神自动签到开关',
name='云原神自动签到开关',
value='${云原神自动签到开关}',
onText='开启',
offText='关闭'
),
InputTime(
label='云原神签到开始时间',
name='云原神签到开始时间',
value='${云原神签到开始时间}',
labelRemark=Remark(content='会在每天这个时间点进行云原神自动签到,修改后重启生效'),
inputFormat='HH时',
timeFormat='HH',
format='HH'
),
],
actions=action_button)
ssbq_form = Form(
title='实时便签',
api='/LittlePaimon/api/set_config',
visibleOn='${select == 4}',
body=[
Switch(
label='实时便签检查开关',
name='实时便签检查开关',
value='${实时便签检查开关}',
onText='开启',
offText='关闭'
),
InputTimeRange(
label='实时便签停止检查时间段',
name='实时便签停止检查时间段',
value='${实时便签停止检查时间段}',
labelRemark=Remark(
content='在这段时间(例如深夜)不进行实时便签检查,注意开始时间不要晚于结束时间,不然会有问题'),
timeFormat='HH',
format='HH',
inputFormat='HH时'
),
InputNumber(
label='实时便签检查间隔',
name='实时便签检查间隔',
value='${实时便签检查间隔}',
labelRemark=Remark(content='每多少分钟检查进行一次实时便签推荐不快于8分钟修改后重启生效'),
displayMode='enhance',
suffix='分钟',
min=1,
)
],
actions=action_button
)
ys_form = Form(
title='原神信息',
api='/LittlePaimon/api/set_config',
visibleOn='${select == 5}',
body=[
Alert(level='info',
body='当这些指令原神信息数据最后更新时间距今已超过对应小时数时,就自动进行一次更新后再返回信息'),
InputNumber(
label='ys自动更新小时数',
name='ys自动更新小时',
value='${ys自动更新小时}',
displayMode='enhance',
suffix='小时',
min=1,
),
InputNumber(
label='ysa自动更新小时数',
name='ysa自动更新小时',
value='${ysa自动更新小时}',
displayMode='enhance',
suffix='小时',
min=1,
),
InputNumber(
label='ysd自动更新小时数',
name='ysd自动更新小时',
value='${ysd自动更新小时}',
displayMode='enhance',
suffix='小时',
min=1,
)
],
actions=action_button
)
notice_form = Form(
title='好友和群事件',
api='/LittlePaimon/api/set_config',
visibleOn='${select == 6}',
body=[
Switch(
label='启用好友和群请求通知',
name='启用好友和群请求通知',
value='${启用好友和群请求通知}',
labelRemark=Remark(content='开启后,会在机器人收到好友或群添加、拉群等请求时向超管发消息'),
onText='开启',
offText='关闭'
),
Switch(
label='自动接受好友请求',
name='自动接受好友请求',
value='${自动接受好友请求}',
labelRemark=Remark(content='开启后,机器人会自动接受所有好友请求'),
onText='开启',
offText='关闭'
),
Switch(
label='自动接受群邀请',
name='自动接受群邀请',
value='${自动接受群邀请}',
labelRemark=Remark(content='开启后,机器人会自动接受所有拉群请求'),
onText='开启',
offText='关闭'
),
Switch(
label='启用好友和群欢迎消息',
name='启用好友和群欢迎消息',
value='${启用好友和群欢迎消息}',
labelRemark=Remark(content='开启后,会向新添加的好友以及新进入的群发送欢迎消息'),
onText='开启',
offText='关闭'
),
],
actions=action_button
)
other_form = Form(
title='其他配置',
api='/LittlePaimon/api/set_config',
visibleOn='${select == 7}',
body=[
Switch(
label='网页截图权限',
name='启用网页截图权限',
value='${启用网页截图权限}',
labelRemark=Remark(content='开启后,任何人都能使用网页截图,关闭后则只有超管能使用'),
onText='所有人',
offText='仅超级用户'
),
InputNumber(
label='原神猜语音时间',
name='原神猜语音时间',
value='${原神猜语音时间}',
labelRemark=Remark(content='原神猜语音小游戏的持续时间'),
displayMode='enhance',
suffix='',
min=5,
)
],
actions=action_button
)
nonebot_form = Form(
title='Nonebot配置',
initApi='get:/LittlePaimon/api/env_config?file_name=${file_name}',
api='post:/LittlePaimon/api/env_config?file_name=${file_name}',
visibleOn='${select == 8}',
body=[
Select(
name='file_name',
label='选择文件',
value='.env.prod',
options=[
{
'label': '.env',
'value': '.env'
},
{
'label': '.env.prod',
'value': '.env.prod'
},
{
'label': '.env.dev',
'value': '.env.dev'
}
]
),
Editor(
name='editor',
label='编辑',
value='${data}',
placeholder='暂无内容'
)
],
actions=[action_button[0]]
)
select = Select(label='选择配置类',
size='sm',
name='select',
value=1,
options=[
{
'label': 'Web端配置',
'value': 1},
{
'label': '模拟抽卡',
'value': 2},
{
'label': '自动任务',
'value': 3
},
{
'label': '实时便签',
'value': 4
},
{
'label': '原神信息',
'value': 5
},
{
'label': '好友和群事件',
'value': 6
},
{
'label': '其他配置',
'value': 7
},
{
'label': 'Nonebot配置',
'value': 8
}
])
page = PageSchema(url='/configs', label='配置项管理',
schema=Page(title='配置项管理', initApi='/LittlePaimon/api/get_config', body=[select, cookie_web_form, sim_gacha_form, auto_mys_form, ssbq_form, ys_form, notice_form, other_form, nonebot_form]))

View File

@ -0,0 +1,9 @@
status_map = {
0: '疑似失效',
1: '可用',
2: '达到每日限制'
}
status_filter = {
'options': [{'label': '疑似失效', 'value': 0},
{'label': '可用', 'value': 1},
{'label': '达到每日限制', 'value': 2}]}

View File

@ -0,0 +1,90 @@
from amis import Page, PageSchema, Html, Property, Service
from LittlePaimon import __version__
logo = Html(html=f'''
<p align="center">
<a href="https://github.com/CMHopeSunshine/LittlePaimon/">
<img src="http://static.cherishmoon.fun/LittlePaimon/readme/logo.png"
width="256" height="256" alt="LittlePaimon">
</a>
</p>
<h1 align="center">LittlePaimon 控制台 V{__version__}</h1>
<div align="center">
<a href="https://github.com/CMHopeSunshine/LittlePaimon" target="_blank">
Github仓库</a> &nbsp; · &nbsp;
<a href="https://docs.paimon.cherishmoon.fun/"
target="_blank">文档</a>
</div>
<br>
<br>
''')
status = Service(
api='/LittlePaimon/api/status',
body=Property(
title='机器人信息',
column=2,
items=[
Property.Item(
label='Bot昵称',
content='${nickname}'
),
Property.Item(
label='Bot qq号',
content='${bot_id}'
),
Property.Item(
label='Bot启动时间',
content='${start_time}'
),
Property.Item(
label='系统启动时间',
content='${system_start_time}'
),
Property.Item(
label='已接收信息',
content='${msg_received}'
),
Property.Item(
label='已发送信息',
content='${msg_sent}'
),
Property.Item(
label='CPU占用率',
content='${cpu_percent}'
),
Property.Item(
label='RAM占用率',
content='${ram_percent}'
),
Property.Item(
label='SWAP占用率',
content='${swap_percent}',
span=2
),
]
)
)
# 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, status])
page = PageSchema(url='/home', label='首页', icon='fa fa-home', isDefaultPage=True, schema=page_detail)

View File

@ -0,0 +1,38 @@
from LittlePaimon import __version__
from amis import Form, InputText, InputPassword, DisplayModeEnum, Horizontal, Remark, Html, Page, Container, AmisAPI
logo = Html(html=f'''
<p align="center">
<a href="https://github.com/CMHopeSunshine/LittlePaimon/">
<img src="http://static.cherishmoon.fun/LittlePaimon/readme/logo.png"
width="256" height="256" alt="LittlePaimon">
</a>
</p>
<h1 align="center">LittlePaimon 控制台 V{__version__}</h1>
<div align="center">
<a href="https://github.com/CMHopeSunshine/LittlePaimon" target="_blank">
Github仓库</a> &nbsp; · &nbsp;
<a href="https://docs.paimon.cherishmoon.fun/"
target="_blank">文档</a>
</div>
<br>
<br>
''')
login_api = AmisAPI(
url='/LittlePaimon/api/login',
method='post',
adaptor='''
if (payload.status == 0) {
localStorage.setItem("token", payload.data.token);
}
return payload;
'''
)
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='是否以管理员身份登录'))
], mode=DisplayModeEnum.horizontal, horizontal=Horizontal(left=3, right=9, offset=5), redirect='/LittlePaimon/admin')
body = Container(style={'width': '400px', 'margin': '0 auto'}, body=login_form)
login_page = Page(title='', body=[logo, body])

View File

@ -0,0 +1,55 @@
from LittlePaimon import __version__
from amis import App, PageSchema, Tpl, Page, DropDownButton, ActionType, LevelEnum, Flex
from .public_cookie import page as public_cookie_page
from .private_cookie import page as private_cookie_page
from .plugin_manage import page as plugin_manage_page
from .home_page import page as home_page
from .config_manage import page as config_page
# from .learning_chat_manage import page as learning_chat_page
# dropdown = DropDownButton(
# buttons=[
# ActionType.Dialog(label='用户信息',
# dialog=Dialog(title='用户信息',
# body='待定')),
# ActionType.Url(label='退出登录',
# url='/LittlePaimon/api/logout')
# ]
# )
action_button = DropDownButton(
label='操作',
trigger='hover',
buttons=[
ActionType.Ajax(
label='更新',
api='/LittlePaimon/api/bot_update',
confirmText='该操作将会对Bot进行检查并尝试更新请在更新完成后重启Bot使更新生效',
),
ActionType.Ajax(
label='重启',
api='/LittlePaimon/api/bot_restart',
confirmText='该操作将会使Bot重启在完成重启之前该页面也将无法访问请耐心等待重启',
)
]
)
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=[action_button, github_logo])
admin_app = App(brandName='LittlePaimon',
logo='http://static.cherishmoon.fun/LittlePaimon/readme/logo.png',
header=header,
pages=[{
'children': [
home_page,
PageSchema(label='Cookie管理', url='/cookie', icon='fa fa-key',
children=[public_cookie_page, private_cookie_page]),
PageSchema(label='机器人配置', url='/config', icon='fa fa-wrench',
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='该页面未开启或不存在')

View File

@ -0,0 +1,136 @@
from amis import Form, Transfer, ActionType, Dialog, InputSubForm, DisplayModeEnum, InputText, Textarea, Switch, InputNumber, Alert, Card, Tpl, CardsCRUD, Static, PageSchema, Page
# -------------插件使用权限设置------------------
ban_form = Form(title='',
api='post:/LittlePaimon/api/set_plugin_bans',
initApi='get:/LittlePaimon/api/get_plugin_bans?module_name=${module_name}',
body=[
Transfer(
type='tabs-transfer',
name='bans',
value='${bans}',
label='',
resultTitle='已被禁用列表',
selectMode='tree',
joinValues=False,
extractValue=True,
statistics=True,
multiple=True,
menuTpl='${left_label}',
valueTpl='${right_label}',
source='get:/LittlePaimon/api/get_groups_and_members',
)])
permission_button = ActionType.Dialog(label='使用权限',
icon='fa fa-pencil',
# visibleOn='${isLoad}',
dialog=Dialog(title='${name}使用权限设置', size='lg', body=ban_form))
# -------------插件使用权限设置------------------
# -------------插件帮助图设置------------------
command_form = InputSubForm(name='matchers',
label='命令列表',
multiple=True,
btnLabel='${pm_name}',
value='${matchers}',
description='该插件下具体的命令使用方法设置',
addButtonText='添加命令',
form=Form(
title='命令信息设置',
mode=DisplayModeEnum.horizontal,
labelAlign='right',
body=[
InputText(label='命令标识名称', name='pm_name', value='${pm_name}', required=True,
description='仅用于标识命令,不会显示在帮助图中,所以若是本身已有的命令,请勿修改!'),
InputText(label='命令用法', name='pm_usage', value='${pm_usage}',
description='命令的使用方法,建议不要太长'),
Textarea(label='命令详细描述', name='pm_description', value='${pm_description}',
description='命令的详细描述,可以用\\n强制换行'),
Switch(label='是否展示', name='pm_show', value='${pm_show}',
description='是否在帮助图中展示该命令'),
InputNumber(label='展示优先级', name='pm_priority', value='${pm_priority}',
description='展示优先级,数字越大越靠前', min=0, max=99,
displayMode='enhance'),
]
))
detail_form = Form(title='',
api='post:/LittlePaimon/api/set_plugin_detail',
submitText='保存修改',
mode=DisplayModeEnum.horizontal,
labelAlign='right',
body=[
InputText(label='插件名称', name='name', value='${name}', required=True,
description='插件显示的名称,建议不要过长'),
Static(label='插件模块名', name='module_name', value='${module_name}'),
Textarea(label='插件描述', name='description', value='${description}', clearable=True,
description='仅用于在本管理页面中显示,不会在帮助图中显示'),
Textarea(label='插件使用说明', name='usage', value='${detail}', clearable=True,
description='会在该插件没有具体命令的使用说明时,显示在帮助图中'),
Switch(label='是否展示', name='show', value='${show}',
description='是否在帮助图中展示该插件'),
InputNumber(label='展示优先级', name='priority', value='${priority}',
description='在帮助图及本管理页面中展示的顺序,数字越小越靠前',
min=0, max=99, displayMode='enhance'),
command_form
])
tips_alert = Alert(level='info',
body='以下设置用于在本管理页面以及help帮助图中显示插件的信息不会影响插件的实际使用你可以修改这些信息来自定义帮助图效果。')
detail_button = ActionType.Dialog(label='信息',
size='lg',
icon='fa fa-pencil',
dialog=Dialog(title='${name}信息设置', size='lg', body=[tips_alert, detail_form]))
card = Card(
header=Card.Header(title='$name',
subTitle='$module_name',
description='$description',
avatarText='$name'),
actions=[detail_button, permission_button],
toolbar=[
Tpl(tpl='未加载', className='label label-warning', hiddenOn='${isLoad}'),
Switch(name='enable',
value='${status}',
onText='启用',
offText='禁用',
visibleOn='${isLoad}',
onEvent={
'change': {
'actions': {
'actionType': 'ajax',
'args': {
'api': {
'url': '/LittlePaimon/api/set_plugin_status',
'method': 'post'
},
'messages': {
'success': '${name}插件全局开关已设置为${event.data.value}',
'failed': '插件设置失败'
},
'status': '${event.data.value}',
'plugin': '${module_name}'
}
}
}
})
])
cards_curd = CardsCRUD(mode='cards',
title='',
syncLocation=False,
api='/LittlePaimon/api/get_plugins',
loadDataOnce=True,
source='${rows | filter:name:match:keywords_name | filter:description:match:keywords_description | filter:status:match:status}',
filter={
'body': [
InputText(name='keywords_name', label='插件名'),
InputText(name='keywords_description', label='插件描述'),
Switch(name='status', label='插件状态', onText='启用', offText='禁用')
]
},
perPage=12,
autoJumpToTopOnPagerChange=True,
placeholder='暂无插件信息',
footerToolbar=['switch-per-page', 'pagination'],
columnsCount=3,
card=card)
page = PageSchema(url='/plugins', label='插件管理', schema=Page(title='插件管理', body=cards_curd))

View File

@ -0,0 +1,41 @@
from amis import Page, PageSchema, ActionType, LevelEnum, Dialog, Form, InputNumber, Textarea, Action, Static, TableCRUD, TableColumn, ColumnOperation
from .constants import status_map, status_filter
add_button = ActionType.Dialog(label='添加私人Cookie',
level=LevelEnum.primary,
dialog=Dialog(title='添加私人Cookie',
body=Form(api='post:/LittlePaimon/api/save_private_cookie',
body=[InputNumber(name='user_id', label='QQ号', required=True),
Textarea(name='cookie', label='Cookie', required=True)])))
delete_button = ActionType.Ajax(label='删除', level=LevelEnum.danger,
confirmText='确认删除该私人Cookie',
api='delete:/LittlePaimon/api/delete_private_cookie?id=${id}')
save_action_button = Action(label='保存修改', level=LevelEnum.warning, type='submit')
cancel_action_button = Action(label='关闭', level=LevelEnum.default, actionType='close')
detail_button = ActionType.Dialog(label='详情',
level=LevelEnum.info,
dialog=Dialog(title='Cookie详情',
body=Form(
api='post:/LittlePaimon/api/save_private_cookie',
body=[Static(name='id', label='ID'),
InputNumber(name='user_id', label='QQ号', required=True),
Static(name='uid', label='UID'),
Static(name='mys_id', label='米游社ID'),
Textarea(name='cookie', label='Cookie', required=True),
Static(name='stoken', label='Stoken')]),
actions=[cancel_action_button, save_action_button]))
table = TableCRUD(mode='table',
title='',
syncLocation=False,
api='/LittlePaimon/api/get_private_cookies',
columns=[TableColumn(label='ID', name='id'),
TableColumn(label='QQ号', name='user_id', searchable=True),
TableColumn(label='UID', name='uid', searchable=True),
TableColumn(label='米游社ID', name='mys_id', searchable=True),
TableColumn(type='mapping', label='状态', name='status', filterable=status_filter,
map=status_map),
ColumnOperation(label='操作', buttons=[detail_button, delete_button])],
headerToolbar=[add_button],
footerToolbar=['switch-per-page', 'pagination'])
page = PageSchema(url='/private_cookie', label='私人Cookie', schema=Page(title='私人Cookie', body=table))

View File

@ -0,0 +1,21 @@
from amis import Page, PageSchema, ActionType, LevelEnum, Dialog, Form, Textarea, TableCRUD, TableColumn, ColumnOperation
from .constants import status_map, status_filter
add_button = ActionType.Dialog(label='添加公共Cookie',
level=LevelEnum.primary,
dialog=Dialog(title='添加公共Cookie',
body=Form(api='post:/LittlePaimon/api/add_public_cookie',
body=[Textarea(name='cookie', label='Cookie', required=True)])))
delete_button = ActionType.Ajax(label='删除', level=LevelEnum.danger,
confirmText='确认删除该公共Cookie',
api='delete:/LittlePaimon/api/delete_public_cookie?id=${id}')
table = TableCRUD(mode='table',
title='',
syncLocation=False,
api='/LittlePaimon/api/get_public_cookies',
columns=[TableColumn(label='ID', name='id', width='8%'),
TableColumn(label='Cookie', name='cookie', width='64%'),
TableColumn(type='mapping', label='状态', name='status', filterable=status_filter, map=status_map, width='12%'),
ColumnOperation(label='操作', buttons=[delete_button], width='16%')],
headerToolbar=[add_button])
page = PageSchema(label='公共Cookie', url='public_cookie', schema=Page(title='公共Cookie', body=table))

1741
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,15 +6,15 @@ authors = ["惜月 <277073121@qq.com>"]
license = "AGPL"
[[tool.poetry.source]]
name = "ali"
name = "tsinghua"
default = true
url = "https://mirrors.aliyun.com/pypi/simple/"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
[tool.poetry.dependencies]
python = "^3.8"
nonebot2 = "^2.0.0-beta.5"
nonebot2 = ">=2.0.0-beta.5"
nonebot-adapter-onebot = "^2.1"
nonebot-plugin-apscheduler = "^0.1.2"
nonebot-plugin-apscheduler = "^0.2.0"
nonebot-plugin-htmlrender = "^0.1.1"
beautifulsoup4 = "^4.10.0"
httpx = "^0.23.0"
@ -26,13 +26,14 @@ tqdm = "^4.64.0"
"ruamel.yaml" = "^0.17.21"
ujson = "^5.4.0"
expandvars = "^0.9.0"
pywebio = "^1.6.2"
jieba = "^0.42.1"
scipy = "^1.9.1"
scikit-learn = "^1.1.2"
shapely = "^1.8.4"
gitpython = "^3.1.27"
pypinyin = "^0.47.1"
python-jose = "^3.3.0"
amis-python = "^1.0.5"
[tool.poetry.dev-dependencies]
nb-cli = "^0.6.7"

View File

@ -1,24 +1,92 @@
nonebot2>=2.0.0-beta.5
nonebot-adapter-onebot>=2.1
nonebot-plugin-apscheduler>=0.1.2
nonebot-plugin-htmlrender>=0.1.1
beautifulsoup4>=4.10.0
httpx>=0.23.0
lxml>=4.8.0
Pillow>=9.1.0
matplotlib>=3.5.1
aiofiles>=0.8.0
tortoise-orm>=0.19.2
tqdm>=4.64.0
ruamel.yaml>=0.17.21
ujson>=5.4.0
expandvars>=0.9.0
pywebio>=1.6.2
jieba>=0.42.1
scipy>=1.9.1
scikit-learn>=1.1.2
shapely>=1.8.4
gitpython>=3.1.27
pypinyin>=0.47.1
--index-url https://pypi.tuna.tsinghua.edu.cn/simple
aiofiles==0.8.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.3" and python_full_version < "4.0.0"
aiosqlite==0.17.0; python_version >= "3.7" and python_version < "4.0"
amis-python==1.0.5; python_version >= "3.7" and python_version < "4.0"
anyio==3.6.2; python_full_version >= "3.6.2" and python_version >= "3.8" and python_version < "4.0"
apscheduler==3.9.1; python_version >= "3.8" and python_full_version < "3.0.0" and python_version < "4.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.8"
backports.zoneinfo==0.2.1; python_version >= "3.8" and python_full_version < "3.0.0" and python_version < "3.9" and (python_version >= "3.8" and python_full_version < "3.0.0" and python_version < "4.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.8") or python_full_version >= "3.5.0" and python_version < "3.9" and python_version >= "3.8" and (python_version >= "3.8" and python_full_version < "3.0.0" and python_version < "4.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.8")
beautifulsoup4==4.11.1; python_full_version >= "3.6.0"
certifi==2022.9.24; python_version >= "3.7"
click==8.1.3; python_version >= "3.8" and python_version < "4.0"
colorama==0.4.6; python_version >= "3.8" and python_full_version < "3.0.0" and platform_system == "Windows" and python_version < "4.0" and sys_platform == "win32" or python_full_version >= "3.7.0" and platform_system == "Windows" and python_version >= "3.8" and python_version < "4.0" and sys_platform == "win32"
contourpy==1.0.5; python_version >= "3.8"
cycler==0.11.0; python_version >= "3.8"
ecdsa==0.18.0; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0"
expandvars==0.9.0; python_version >= "3.4"
fastapi==0.79.1; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.3" and python_full_version < "4.0.0"
fonttools==4.38.0; python_version >= "3.8"
gitdb==4.0.9; python_version >= "3.7"
gitpython==3.1.29; python_version >= "3.7"
greenlet==1.1.3; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.7"
h11==0.12.0; python_version >= "3.8" and python_version < "4.0"
httpcore==0.15.0; python_version >= "3.7"
httptools==0.5.0; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.5.0"
httpx==0.23.0; python_version >= "3.7"
idna==3.4
importlib-metadata==5.0.0; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.7" and python_version < "3.10"
iso8601==1.1.0; python_full_version >= "3.6.2" and python_version < "4.0" and python_version >= "3.7"
jieba==0.42.1
jinja2==3.1.2; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.7" and python_version < "4.0"
joblib==1.2.0; python_version >= "3.8"
kiwisolver==1.4.4; python_version >= "3.8"
loguru==0.6.0; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.3" and python_full_version < "4.0.0"
lxml==4.9.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
markdown==3.4.1; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.7"
markupsafe==2.1.1; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.7" and python_version < "4.0"
matplotlib==3.6.1; python_version >= "3.8"
msgpack==1.0.4; python_version >= "3.8" and python_version < "4.0"
multidict==6.0.2; python_version >= "3.8" and python_version < "4.0"
nonebot-adapter-onebot==2.1.5; python_version >= "3.8" and python_version < "4.0"
nonebot-plugin-apscheduler==0.2.0; python_version >= "3.8" and python_version < "4.0"
nonebot-plugin-htmlrender==0.1.1; python_full_version >= "3.7.3" and python_full_version < "4.0.0"
nonebot2==2.0.0rc1; python_version >= "3.8" and python_version < "4.0"
numpy==1.23.4; python_version >= "3.8"
packaging==21.3; python_version >= "3.8"
pillow==9.2.0; python_version >= "3.7"
playwright==1.27.1; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.7"
pyasn1==0.4.8; python_version >= "3.6" and python_version < "4"
pydantic==1.9.2; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.3" and python_full_version < "4.0.0"
pyee==8.1.0; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.7"
pygments==2.13.0; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.6"
pygtrie==2.5.0; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.3" and python_full_version < "4.0.0"
pymdown-extensions==9.7; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.7"
pyparsing==3.0.9; python_full_version >= "3.6.8" and python_version >= "3.8"
pypika-tortoise==0.1.6; python_version >= "3.7" and python_version < "4.0"
pypinyin==0.47.1; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0" and python_version < "4")
python-dateutil==2.8.2; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.8"
python-dotenv==0.21.0
python-jose==3.3.0
python-markdown-math==0.8; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.6"
pytz-deprecation-shim==0.1.0.post0; python_version >= "3.8" and python_full_version < "3.0.0" and python_version < "4.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.8"
pytz==2022.5; python_version >= "3.7" and python_version < "4.0" and (python_version >= "3.8" and python_full_version < "3.0.0" and python_version < "4.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.8")
pyyaml==6.0; python_version >= "3.8" and python_version < "4.0"
rfc3986==1.5.0; python_version >= "3.7"
rsa==4.9; python_version >= "3.6" and python_version < "4"
ruamel.yaml.clib==0.2.7; platform_python_implementation == "CPython" and python_version < "3.11" and python_version >= "3.5"
ruamel.yaml==0.17.21; python_version >= "3"
scikit-learn==1.1.2; python_version >= "3.8"
scipy==1.9.3; python_version >= "3.8"
setuptools-scm==7.0.5; python_version >= "3.8"
shapely==1.8.5.post1; python_version >= "3.6"
six==1.16.0; python_version >= "3.8" and python_full_version < "3.0.0" and python_version < "4.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.8"
smmap==5.0.0; python_version >= "3.7"
sniffio==1.3.0; python_full_version >= "3.6.2" and python_version >= "3.8" and python_version < "4.0"
soupsieve==2.3.2.post1; python_version >= "3.6" and python_full_version >= "3.6.0"
starlette==0.19.1; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.6.1"
threadpoolctl==3.1.0; python_version >= "3.8"
tomli==2.0.1; python_version >= "3.8"
tomlkit==0.10.2; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.3" and python_full_version < "4.0.0"
tortoise-orm==0.19.2; python_version >= "3.7" and python_version < "4.0"
tqdm==4.64.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
typing-extensions==4.4.0; python_version >= "3.8" and python_version <= "3.8" and python_full_version >= "3.7.3" and python_full_version < "4.0.0"
tzdata==2022.5; python_version >= "3.8" and python_full_version < "3.0.0" and python_version < "4.0" and platform_system == "Windows" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.8" and platform_system == "Windows"
tzlocal==4.2; python_version >= "3.8" and python_full_version < "3.0.0" and python_version < "4.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.8"
ujson==5.5.0; python_version >= "3.7"
uvicorn==0.18.3; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.3" and python_full_version < "4.0.0"
uvloop==0.17.0; sys_platform != "win32" and sys_platform != "cygwin" and platform_python_implementation != "PyPy" and python_version >= "3.8" and python_version < "4.0"
watchfiles==0.18.0; python_version >= "3.8" and python_version < "4.0"
websockets==10.3; python_version >= "3.8" and python_version < "4.0"
win32-setctime==1.1.0; python_version >= "3.8" and python_version < "4.0" and sys_platform == "win32"
yarl==1.8.1; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.3" and python_full_version < "4.0.0"
zipp==3.10.0; python_full_version >= "3.7.3" and python_full_version < "4.0.0" and python_version >= "3.7" and python_version < "3.10"