mirror of
https://github.com/TriM-Organization/LiteyukiBot-TriM.git
synced 2024-11-28 16:24:51 +08:00
feat: 配置项目的热修改
This commit is contained in:
parent
c8851bd696
commit
f9e61fd184
@ -9,6 +9,7 @@ tag:
|
||||
---
|
||||
|
||||
### 轻雪配置项(Nonebot插件配置项也可以写在此,与dotenv格式不同,应为小写)
|
||||
|
||||
配置文件会在首次启动后生成,你可以在`config.yaml`中修改配置项后重启轻雪
|
||||
如果不确定字段的含义,请不要修改(部分在自动生成配置文件中未列出,需手动添加)
|
||||
|
||||
@ -26,6 +27,14 @@ default_language: "zh-CN" # 默认语言,支持i18n部分语言和自行扩展
|
||||
log_level: "INFO" # 日志等级
|
||||
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
|
||||
auto_report: true # 是否自动上报问题给轻雪服务器,仅包含硬件信息和运行软件版本
|
||||
fake_device_info: # 统计卡片显示的虚假设备信息,用于保护隐私
|
||||
cpu:
|
||||
brand: AMD
|
||||
cores: 16 # 物理核心数
|
||||
logical_cores: 32 # 逻辑核心数
|
||||
frequency: 3600 # CPU主频:MHz
|
||||
mem:
|
||||
total: 32768000000 # 内存总数:字节
|
||||
|
||||
# 其他Nonebot插件的配置项
|
||||
custom_config_1: "custom_value1"
|
||||
@ -43,8 +52,8 @@ custom_config_2: "custom_value2"
|
||||
| 地址 | ws://`address`/onebot/v11 | 地址取决于配置文件,本机默认为`127.0.0.1:20216` |
|
||||
| AccessToken | `""` | 如果你给轻雪配置了`AccessToken`,请在此填写相同的值 |
|
||||
|
||||
|
||||
### 其他通信方式
|
||||
|
||||
- 实现端与轻雪的通信方式不局限为反向WebSocket,但是推荐使用反向WebSocket。
|
||||
- 反向WebSocket的优点是轻雪作为服务端,可以更好的控制连接,适用于生产环境。
|
||||
- 在某些情况下,你也可以使用正向WebSocket,比如你在开发轻雪插件时,可以使用正向WebSocket主动连接实现端
|
@ -9,7 +9,7 @@ from liteyuki.utils.config import config, load_from_yaml
|
||||
from liteyuki.utils.data_manager import StoredConfig, common_db
|
||||
from liteyuki.utils.language import get_user_lang
|
||||
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent
|
||||
from liteyuki.utils.message import Markdown as md, send_markdown
|
||||
from liteyuki.utils.message import Markdown as md
|
||||
from .reloader import Reloader
|
||||
from liteyuki.utils import htmlrender
|
||||
|
||||
@ -80,7 +80,7 @@ async def _(bot: T_Bot, event: T_MessageEvent):
|
||||
reply += f"```\n{logs}\n```\n"
|
||||
btn_restart = md.button(ulang.get("liteyuki.restart_now"), "restart-liteyuki")
|
||||
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}"
|
||||
await send_markdown(reply, bot, event=event, at_sender=False)
|
||||
await md.send_md(reply, bot, event=event, at_sender=False)
|
||||
|
||||
|
||||
@reload_liteyuki.handle()
|
||||
@ -119,7 +119,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
for k, v in stored_config.config.items():
|
||||
reply += f"\n{k}={v}"
|
||||
reply += "\n```"
|
||||
await send_markdown(reply, bot, event=event)
|
||||
await md.send_md(reply, bot, event=event)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
|
@ -1,19 +1,131 @@
|
||||
import json
|
||||
import random
|
||||
|
||||
import psutil
|
||||
import requests
|
||||
from PIL import Image
|
||||
from nonebot.adapters.onebot.v11 import MessageSegment
|
||||
from nonebot.permission import SUPERUSER
|
||||
from liteyuki.utils.htmlrender import template_to_pic, html_to_pic
|
||||
|
||||
from liteyuki.utils.language import get_user_lang
|
||||
from liteyuki.utils.liteyuki_api import liteyuki_api
|
||||
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent
|
||||
from liteyuki.utils.message import Markdown as md
|
||||
from liteyuki.utils.resource import get_path
|
||||
from nonebot import on_command
|
||||
from cpuinfo import get_cpu_info
|
||||
|
||||
stats = on_command("stats", priority=5, permission=SUPERUSER)
|
||||
from liteyuki.utils.tools import convert_size
|
||||
|
||||
stats = on_command("stats", aliases={"状态"}, priority=5, permission=SUPERUSER)
|
||||
|
||||
protocol_names = {
|
||||
0: "iPad",
|
||||
1: "Android Phone",
|
||||
2: "Android Watch",
|
||||
3: "Mac",
|
||||
5: "iPad",
|
||||
6: "Android Pad",
|
||||
}
|
||||
|
||||
|
||||
@stats.handle()
|
||||
async def _():
|
||||
image_bytes = await template_to_pic(
|
||||
template_path=get_path("templates/index.html", abs_path=True),
|
||||
templates={}
|
||||
async def _(bot: T_Bot, event: T_MessageEvent):
|
||||
ulang = get_user_lang(str(event.user_id))
|
||||
fake_device_info: dict = bot.config.dict().get("fake_device_info", {})
|
||||
mem_total = fake_device_info.get('mem', {}).get('total', psutil.virtual_memory().total)
|
||||
|
||||
mem_used_bot = psutil.Process().memory_info().rss
|
||||
mem_used_other = psutil.virtual_memory().used - mem_used_bot
|
||||
mem_free = mem_total - mem_used_other - mem_used_bot
|
||||
|
||||
groups = len(await bot.get_group_list())
|
||||
friends = len(await bot.get_friend_list())
|
||||
|
||||
status = await bot.get_status()
|
||||
statistics = status.get("stat", {})
|
||||
version_info = await bot.get_version_info()
|
||||
|
||||
cpu_info = get_cpu_info()
|
||||
if "AMD" in cpu_info.get("brand_raw", ""):
|
||||
brand = "AMD"
|
||||
elif "Intel" in cpu_info.get("brand_raw", ""):
|
||||
brand = "Intel"
|
||||
else:
|
||||
brand = "Unknown"
|
||||
|
||||
if fake_device_info.get("cpu", {}).get("brand"):
|
||||
brand = fake_device_info.get("cpu", {}).get("brand")
|
||||
|
||||
cpu_info = get_cpu_info()
|
||||
templ = {
|
||||
"CPUDATA" : [
|
||||
{
|
||||
"name" : "USED",
|
||||
"value": psutil.cpu_percent(interval=1)
|
||||
},
|
||||
{
|
||||
"name" : "FREE",
|
||||
"value": 100 - psutil.cpu_percent(interval=1)
|
||||
}
|
||||
],
|
||||
"MEMDATA" : [
|
||||
|
||||
{
|
||||
"name" : "OTHER",
|
||||
"value": mem_used_other
|
||||
},
|
||||
{
|
||||
"name" : "FREE",
|
||||
"value": mem_free
|
||||
},
|
||||
{
|
||||
"name" : "BOT",
|
||||
"value": mem_used_bot
|
||||
},
|
||||
],
|
||||
"SWAPDATA" : [
|
||||
{
|
||||
"name" : "USED",
|
||||
"value": psutil.swap_memory().used
|
||||
},
|
||||
{
|
||||
"name" : "FREE",
|
||||
"value": psutil.swap_memory().free
|
||||
}
|
||||
],
|
||||
"BOT_ID" : bot.self_id,
|
||||
"BOT_NAME" : (await bot.get_login_info())["nickname"],
|
||||
"BOT_TAGS" : [
|
||||
protocol_names.get(version_info.get("protocol_name"), "Linux"), version_info.get("app_name"), version_info.get("app_version"),
|
||||
f"{ulang.get('liteyuki.stats.groups')} {groups}", f"{ulang.get('liteyuki.stats.friends')} {friends}",
|
||||
f"{ulang.get('liteyuki.stats.sent')} {statistics.get('message_sent', 0)}",
|
||||
f"{ulang.get('liteyuki.stats.received')} {statistics.get('message_received', 0)}" \
|
||||
],
|
||||
"CPU_TAGS" : [
|
||||
f"{brand} {cpu_info.get('arch', 'Unknown')}",
|
||||
f"{fake_device_info.get('cpu', {}).get('cores', psutil.cpu_count(logical=False))}C "
|
||||
f"{fake_device_info.get('cpu', {}).get('logical_cores', psutil.cpu_count(logical=True))}T",
|
||||
f"{fake_device_info.get('cpu', {}).get('frequency', psutil.cpu_freq().current) / 1000}GHz"
|
||||
],
|
||||
"MEM_TAGS" : [
|
||||
f"Bot {convert_size(mem_used_bot, 1)}",
|
||||
f"{ulang.get('main.monitor.used')} {convert_size(mem_used_other + mem_used_bot, 1)}",
|
||||
f"{ulang.get('main.monitor.total')} {convert_size(mem_total, 1)}",
|
||||
],
|
||||
"SWAP_TAGS": [
|
||||
f"{ulang.get('main.monitor.used')} {convert_size(psutil.swap_memory().used, 1)}",
|
||||
f"{ulang.get('main.monitor.total')} {convert_size(psutil.swap_memory().total, 1)}",
|
||||
],
|
||||
"CPU" : ulang.get("main.monitor.cpu"),
|
||||
"MEM" : ulang.get("main.monitor.memory"),
|
||||
"SWAP" : ulang.get("main.monitor.swap"),
|
||||
}
|
||||
image_bytes = await template_to_pic(
|
||||
template_path=get_path("templates/stats.html", abs_path=True),
|
||||
templates=templ,
|
||||
wait=1,
|
||||
device_scale_factor=4,
|
||||
)
|
||||
# await md.send_image(image_bytes, bot, event=event)
|
||||
await stats.finish(MessageSegment.image(image_bytes))
|
||||
|
@ -4,7 +4,7 @@ from nonebot.plugin import PluginMetadata
|
||||
|
||||
from liteyuki.utils.data import Database, LiteModel
|
||||
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent
|
||||
from liteyuki.utils.message import send_markdown
|
||||
from liteyuki.utils.message import Markdown as md
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from nonebot_plugin_alconna import on_alconna
|
||||
@ -108,7 +108,7 @@ async def _(event: T_MessageEvent, bot: T_Bot):
|
||||
push_message = (
|
||||
f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n"
|
||||
f"{msg_formatted}")
|
||||
await send_markdown(push_message, bot2, message_type=push.target.session_type, session_id=push.target.session_id)
|
||||
await md.send_md(push_message, bot2, message_type=push.target.session_type, session_id=push.target.session_id)
|
||||
return
|
||||
|
||||
|
||||
|
@ -5,7 +5,7 @@ from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent, v11
|
||||
from liteyuki.utils.message import send_markdown
|
||||
from liteyuki.utils.message import Markdown as md
|
||||
|
||||
md_test = on_command("mdts", aliases={"会话md"}, permission=SUPERUSER)
|
||||
md_group = on_command("mdg", aliases={"群md"}, permission=SUPERUSER)
|
||||
@ -23,7 +23,7 @@ placeholder = {
|
||||
|
||||
@md_test.handle()
|
||||
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
|
||||
await send_markdown(
|
||||
await md.send_md(
|
||||
str(arg),
|
||||
bot,
|
||||
message_type=event.message_type,
|
||||
@ -40,8 +40,8 @@ async def _(bot: v11.Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()
|
||||
if str(event.user_id) == str(bot.self_id) and str(bot.self_id) in ["2751454815"]:
|
||||
nonebot.logger.info("开始处理:%s" % str(event.message_id))
|
||||
|
||||
data = await send_markdown(str(arg), bot, message_type=event.message_type,
|
||||
session_id=event.user_id if event.message_type == "private" else event.group_id)
|
||||
data = await md.send_md(str(arg), bot, message_type=event.message_type,
|
||||
session_id=event.user_id if event.message_type == "private" else event.group_id)
|
||||
await bot.delete_msg(message_id=event.message_id)
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
from nonebot import require
|
||||
|
||||
from ...utils.ly_typing import T_Bot, T_MessageEvent
|
||||
from ...utils.message import send_markdown
|
||||
from ...utils.message import Markdown as md
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from .game import Minesweeper
|
||||
@ -63,7 +63,7 @@ async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
|
||||
)
|
||||
minesweeper_cache.append(new_game)
|
||||
await minesweeper.send("游戏开始")
|
||||
await send_markdown(new_game.board_markdown(), bot, event=event)
|
||||
await md.send_md(new_game.board_markdown(), bot, event=event)
|
||||
except AssertionError:
|
||||
await minesweeper.finish("参数错误")
|
||||
elif result.subcommands.get("end"):
|
||||
@ -82,9 +82,9 @@ async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
|
||||
await minesweeper.finish("参数错误")
|
||||
if not game.reveal(row, col):
|
||||
minesweeper_cache.remove(game)
|
||||
await send_markdown(game.board_markdown(), bot, event=event)
|
||||
await md.send_md(game.board_markdown(), bot, event=event)
|
||||
await minesweeper.finish("游戏结束")
|
||||
await send_markdown(game.board_markdown(), bot, event=event)
|
||||
await md.send_md(game.board_markdown(), bot, event=event)
|
||||
if game.is_win():
|
||||
minesweeper_cache.remove(game)
|
||||
await minesweeper.finish("游戏胜利")
|
||||
@ -97,6 +97,6 @@ async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
|
||||
if not (0 <= row < game.rows and 0 <= col < game.cols):
|
||||
await minesweeper.finish("参数错误")
|
||||
game.board[row][col].flagged = not game.board[row][col].flagged
|
||||
await send_markdown(game.board_markdown(), bot, event=event)
|
||||
await md.send_md(game.board_markdown(), bot, event=event)
|
||||
else:
|
||||
await minesweeper.finish("参数错误")
|
||||
|
@ -10,7 +10,7 @@ from nonebot import require
|
||||
from nonebot.permission import SUPERUSER
|
||||
from liteyuki.utils.language import get_user_lang
|
||||
from liteyuki.utils.ly_typing import T_Bot
|
||||
from liteyuki.utils.message import Markdown as md, send_markdown
|
||||
from liteyuki.utils.message import Markdown as md
|
||||
from .common import *
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
@ -81,7 +81,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}"
|
||||
else:
|
||||
reply = ulang.get("npm.search_no_result")
|
||||
await send_markdown(reply, bot, event=event)
|
||||
await md.send_md(reply, bot, event=event)
|
||||
|
||||
elif result.subcommands.get("install"):
|
||||
plugin_module_name: str = result.subcommands["install"].args.get("plugin_name")
|
||||
@ -104,7 +104,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
if found_in_db_plugin is None:
|
||||
plugin_db.upsert(installed_plugin)
|
||||
info = md.escape(ulang.get("npm.install_success", NAME=store_plugin.name)) # markdown转义
|
||||
await send_markdown(
|
||||
await md.send_md(
|
||||
f"{info}\n\n"
|
||||
f"```\n{log}\n```",
|
||||
bot,
|
||||
@ -114,7 +114,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
await npm_alc.finish(ulang.get("npm.plugin_already_installed", NAME=store_plugin.name))
|
||||
else:
|
||||
info = ulang.get("npm.load_failed", NAME=plugin_module_name, HOMEPAGE=homepage_btn).replace("_", r"\\_")
|
||||
await send_markdown(
|
||||
await md.send_md(
|
||||
f"{info}\n\n"
|
||||
f"```\n{log}\n```\n",
|
||||
bot,
|
||||
@ -122,7 +122,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
)
|
||||
else:
|
||||
info = ulang.get("npm.install_failed", NAME=plugin_module_name, HOMEPAGE=homepage_btn).replace("_", r"\\_")
|
||||
await send_markdown(
|
||||
await md.send_md(
|
||||
f"{info}\n\n"
|
||||
f"```\n{log}\n```",
|
||||
bot,
|
||||
|
@ -13,7 +13,7 @@ from nonebot.plugin import Plugin
|
||||
from liteyuki.utils.data_manager import GlobalPlugin, Group, InstalledPlugin, User, group_db, plugin_db, user_db
|
||||
from liteyuki.utils.language import get_user_lang
|
||||
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent
|
||||
from liteyuki.utils.message import Markdown as md, send_markdown
|
||||
from liteyuki.utils.message import Markdown as md
|
||||
from liteyuki.utils.permission import GROUP_ADMIN, GROUP_OWNER
|
||||
from .common import get_plugin_can_be_toggle, get_plugin_default_enable, get_plugin_global_enable, get_plugin_session_enable
|
||||
from .installer import get_store_plugin, npm_update
|
||||
@ -107,7 +107,7 @@ async def _(event: T_MessageEvent, bot: T_Bot):
|
||||
reply += f" {btn_uninstall} {btn_toggle_global}"
|
||||
|
||||
reply += "\n\n***\n"
|
||||
await send_markdown(reply, bot, event=event)
|
||||
await md.send_md(reply, bot, event=event)
|
||||
|
||||
|
||||
@toggle_plugin.handle()
|
||||
@ -228,6 +228,6 @@ async def pre_handle(event: Event, matcher: Matcher):
|
||||
raise IgnoredException("Plugin disabled in session")
|
||||
|
||||
|
||||
@Bot.on_calling_api
|
||||
async def _(bot: Bot, api: str, data: dict[str, any]):
|
||||
nonebot.logger.info(f"Plugin Callapi: {api}: {data}")
|
||||
# @Bot.on_calling_api
|
||||
# async def _(bot: Bot, api: str, data: dict[str, any]):
|
||||
# nonebot.logger.info(f"Plugin Callapi: {api}: {data}")
|
||||
|
@ -7,7 +7,7 @@ from liteyuki.utils.data import LiteModel
|
||||
from liteyuki.utils.data_manager import User, user_db
|
||||
from liteyuki.utils.language import Language, get_all_lang, get_user_lang
|
||||
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent
|
||||
from liteyuki.utils.message import Markdown as md, send_markdown
|
||||
from liteyuki.utils.message import Markdown as md
|
||||
from .const import representative_timezones_list
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
@ -62,7 +62,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
# 未输入值,尝试呼出菜单
|
||||
menu = get_profile_menu(result.args["key"], ulang)
|
||||
if menu:
|
||||
await send_markdown(menu, bot, event=event)
|
||||
await md.send_md(menu, bot, event=event)
|
||||
else:
|
||||
await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
|
||||
|
||||
@ -94,7 +94,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
reply += (f"\n**{key_text}** **{val}**\n"
|
||||
f"\n> {ulang.get(f'user.profile.{key}.desc')}"
|
||||
f"\n> {btn_set} \n\n***\n")
|
||||
await send_markdown(reply, bot, event=event)
|
||||
await md.send_md(reply, bot, event=event)
|
||||
|
||||
|
||||
def get_profile_menu(key: str, ulang: Language) -> Optional[str]:
|
||||
|
@ -13,6 +13,14 @@ liteyuki.current_config=当前配置项如下
|
||||
liteyuki.static_config=静态文件配置项
|
||||
liteyuki.stored_config=储存的配置项
|
||||
liteyuki.config_set_success=配置项 {KEY}={VAL} 设置成功
|
||||
liteyuki.stats.group=群
|
||||
liteyuki.stats.user=好友
|
||||
liteyuki.stats.plugin=插件
|
||||
liteyuki.stats.sent=发送
|
||||
liteyuki.stats.received=接收
|
||||
liteyuki.stats.run_time=运行时间
|
||||
liteyuki.stats.groups=群
|
||||
liteyuki.stats.friends=好友
|
||||
|
||||
main.current_language=当前配置语言为: {LANG}
|
||||
main.enable_webdash=已启用网页监控面板: {URL}
|
||||
@ -23,6 +31,8 @@ main.monitor.memory=内存
|
||||
main.monitor.swap=交换空间
|
||||
main.monitor.disk=磁盘
|
||||
main.monitor.usage=使用率
|
||||
main.monitor.total=总计
|
||||
main.monitor.used=已用
|
||||
|
||||
data_manager.migrate_success=数据模型{NAME}迁移成功
|
||||
|
||||
|
13
liteyuki/resources/templates/css/fonts.css
Normal file
13
liteyuki/resources/templates/css/fonts.css
Normal file
@ -0,0 +1,13 @@
|
||||
@font-face {
|
||||
font-family: 'MiSans';
|
||||
src: url('../../fonts/normal.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'MiSans';
|
||||
src: url('../../fonts/bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
BIN
liteyuki/resources/templates/img/bg1.jpg
Normal file
BIN
liteyuki/resources/templates/img/bg1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 900 KiB |
85683
liteyuki/resources/templates/js/echarts.js
Normal file
85683
liteyuki/resources/templates/js/echarts.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,236 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<link rel="stylesheet" href="css/fonts.css">
|
||||
<style>
|
||||
|
||||
body {
|
||||
font-family: 'MiSans', serif;
|
||||
/* 使背景图不重复 */
|
||||
background-repeat: repeat-y;
|
||||
/* 设置背景图居中裁剪 */
|
||||
background-size: cover;
|
||||
/* 使背景图相对于视窗居中 */
|
||||
background-position: center;
|
||||
/* 设置背景图 */
|
||||
background-image: url('img/bg1.jpg');
|
||||
color: white;
|
||||
// 上10px,左右10px,下0px
|
||||
margin: 10px 10px 0;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.info-box {
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
backdrop-filter: blur(30px);
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#cpu-chart, #mem-chart, #swap-chart {
|
||||
height: 150px;
|
||||
width: 100px;
|
||||
margin: -10px 15px;
|
||||
}
|
||||
|
||||
#bot-info {
|
||||
// 垂直方向居中
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#hardware-info {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#bot-icon {
|
||||
border-radius: 50%;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
#bot-name, #bot-tag {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#bot-name {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#bot-id {
|
||||
margin-left: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#bot-tag {
|
||||
/* 这将使标签在容器宽度满时自动换行 */
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chart-label {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.tag::after {
|
||||
content: "|";
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
height: 50%; /* 调整这个值来改变竖线的高度 */
|
||||
line-height: 50%; /* 使竖线垂直居中 */
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="text/javascript" src="js/echarts.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<!-- 横向放置三个饼图,分别表示CPU/内存/SWAP占用-->
|
||||
|
||||
<div class="info-box" id="bot-info">
|
||||
<span>
|
||||
<img id="bot-icon" src="https://q.qlogo.cn/g?b=qq&nk={{BOT_ID}}}&s=640" alt="BotIcon">
|
||||
</span>
|
||||
<span>
|
||||
<span id="bot-name">
|
||||
{{ BOT_NAME }}
|
||||
</span>
|
||||
<span id="bot-id">
|
||||
{{ BOT_ID }}
|
||||
</span>
|
||||
<div id="bot-tag"></div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="info-box" id="hardware-info">
|
||||
<div id="cpu-info">
|
||||
<div id="cpu-chart"></div>
|
||||
</div>
|
||||
<div id="mem-info">
|
||||
<div id="mem-chart"></div>
|
||||
</div>
|
||||
<div id="swap-info">
|
||||
<div id="swap-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cpuData" style="display: none;">{{ CPUDATA | tojson }}</div>
|
||||
<div id="memData" style="display: none;">{{ MEMDATA | tojson }}</div>
|
||||
<div id="swapData" style="display: none;">{{ SWAPDATA | tojson }}</div>
|
||||
<div id="botTag" style="display: none;">{{ BOT_TAGS | tojson }}</div>
|
||||
<div id="cpuTag" style="display: none;">{{ CPU_TAGS | tojson }}</div>
|
||||
<div id="memTag" style="display: none;">{{ MEM_TAGS | tojson }}</div>
|
||||
<div id="swapTag" style="display: none;">{{ SWAP_TAGS | tojson }}</div>
|
||||
|
||||
<script>
|
||||
// 环形图
|
||||
{
|
||||
let bgs = ["bg1.jpg"]
|
||||
// 随机选择背景图片
|
||||
document.body.style.backgroundImage = `url(./img/${bgs[Math.floor(Math.random() * bgs.length)]})`;
|
||||
|
||||
let botTags = JSON.parse(document.getElementById('botTag').innerText);
|
||||
// 获取tag,是字符串数组,将其处理后变成一个一个的span标签,并且class为tag
|
||||
botTags.forEach(tag => {
|
||||
let tagSpan = document.createElement('span');
|
||||
tagSpan.innerText = tag;
|
||||
tagSpan.className = 'tag';
|
||||
document.getElementById('bot-tag').appendChild(tagSpan);
|
||||
});
|
||||
|
||||
|
||||
let cpuInfo = echarts.init(document.getElementById('cpu-chart'));
|
||||
let memInfo = echarts.init(document.getElementById('mem-chart'));
|
||||
let swapInfo = echarts.init(document.getElementById('swap-chart'));
|
||||
let cpuData = JSON.parse(document.getElementById('cpuData').innerText);
|
||||
let memData = JSON.parse(document.getElementById('memData').innerText);
|
||||
let swapData = JSON.parse(document.getElementById('swapData').innerText);
|
||||
|
||||
sub_tag_data = {
|
||||
cpu: JSON.parse(document.getElementById('cpuTag').innerText),
|
||||
mem: JSON.parse(document.getElementById('memTag').innerText),
|
||||
swap: JSON.parse(document.getElementById('swapTag').innerText)
|
||||
}
|
||||
// 遍历key和value,key是cpu,mem,swap,value是对应的tag数组,添加div标签class为chart-label
|
||||
for (let key in sub_tag_data) {
|
||||
let infoDiv = document.getElementById(key + '-info');
|
||||
sub_tag_data[key].forEach(tag => {
|
||||
let tagSpan = document.createElement('div');
|
||||
tagSpan.innerText = tag;
|
||||
tagSpan.className = 'chart-label';
|
||||
infoDiv.appendChild(tagSpan);
|
||||
});
|
||||
}
|
||||
|
||||
function getOption(title, data) {
|
||||
return {
|
||||
animation: false,
|
||||
title: {
|
||||
text: title,
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
//文字颜色
|
||||
color: '#fff',
|
||||
fontSize: 15
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: "item",
|
||||
backgroundColor: "#ffffff00",
|
||||
// {a}(系列名称),{b}(数据项名称),{c}(数值), {d}(百分比)
|
||||
},
|
||||
color: ['#a2d8f4', "#ffffff44", '#00a6ff'],
|
||||
series: [
|
||||
{
|
||||
name: 'info',
|
||||
type: 'pie',
|
||||
radius: ['80%', '100%'],
|
||||
center: ['50%', '50%'],
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
textStyle: {
|
||||
fontSize: '25',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
cpuInfo.setOption(getOption("{{ CPU }}", cpuData));
|
||||
memInfo.setOption(getOption('{{ MEM }}', memData));
|
||||
swapInfo.setOption(getOption('{{ SWAP }}', swapData));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,29 +0,0 @@
|
||||
{
|
||||
"type": "canvas",
|
||||
"children": [
|
||||
{
|
||||
"type": "rect",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 100,
|
||||
"height": 100,
|
||||
"fill": "red"
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 100,
|
||||
"height": 100,
|
||||
"fill": "green"
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"x": 200,
|
||||
"y": 200,
|
||||
"width": 100,
|
||||
"height": 100,
|
||||
"fill": "blue"
|
||||
}
|
||||
]
|
||||
}
|
93
liteyuki/resources/templates/test.html
Normal file
93
liteyuki/resources/templates/test.html
Normal file
@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #fff; // 设置背景色为灰色
|
||||
background-repeat: no-repeat; // 设置背景图片不重复
|
||||
background-size: 100% auto;
|
||||
}
|
||||
#pieHuan {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
margin-top: 50px;
|
||||
// 圆角矩形,使背景高斯模糊
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="text/javascript" src="js/echarts.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="pieHuan"></div>
|
||||
<script>{
|
||||
// 设置背景图为img/bg1.jpg
|
||||
let bgs = ["bg1.jpg"]
|
||||
// 随机选择背景图片
|
||||
document.body.style.backgroundImage = `url(./img/${bgs[Math.floor(Math.random() * bgs.length)]})`;
|
||||
// 环形图
|
||||
let pieHuan = echarts.init(document.getElementById('pieHuan'));
|
||||
pieHuanOption = {
|
||||
// 标题
|
||||
title: {
|
||||
text: 'echarts实现环形图'
|
||||
},
|
||||
// 图例
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: "item",
|
||||
backgroundColor: "#1677FF",
|
||||
// {a}(系列名称),{b}(数据项名称),{c}(数值), {d}(百分比)
|
||||
formatter: "{a}:{b}<br/>{c}条({d}%)"
|
||||
},
|
||||
// 不同区域的颜色
|
||||
color: ['#65a5ff', '#dcebff'],
|
||||
series: [
|
||||
{
|
||||
name: '访问来源',
|
||||
type: 'pie',
|
||||
// 数组的第一项是内半径,第二项是外半径;可以设置不同的内外半径显示成圆环图
|
||||
radius: ['30%', '50%'],
|
||||
// 饼图的中心(圆心)坐标,数组的第一项是横坐标,第二项是纵坐标;设置成百分比时第一项是相对于容器宽度,第二项是相对于容器高度
|
||||
center: ['50%', '50%'],
|
||||
itemStyle: {
|
||||
// 显示图例
|
||||
normal: {
|
||||
label: {
|
||||
show: true
|
||||
},
|
||||
labelLine: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
// 标签内容是否高亮
|
||||
show: true,
|
||||
textStyle: {
|
||||
fontSize: '30',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{value: 335, name: '百度'},
|
||||
{value: 335, name: '搜狐'}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
pieHuan.setOption(pieHuanOption);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -20,7 +20,7 @@ from playwright.async_api import Browser, Error, Page, Playwright, async_playwri
|
||||
from .config import Config
|
||||
import asyncio
|
||||
|
||||
config = get_plugin_config(Config)
|
||||
config = Config()
|
||||
|
||||
_browser: Optional[Browser] = None
|
||||
_playwright: Optional[Playwright] = None
|
||||
|
@ -130,6 +130,20 @@ class Language:
|
||||
nonebot.logger.error(f"Failed to get language text or format: {e}")
|
||||
return default or item
|
||||
|
||||
def get_many(self, *args) -> dict[str, str]:
|
||||
"""
|
||||
获取多个文本
|
||||
Args:
|
||||
*args: 文本键
|
||||
|
||||
Returns:
|
||||
dict: 文本字典
|
||||
"""
|
||||
d = {}
|
||||
for item in args:
|
||||
d[item] = self.get(item)
|
||||
return d
|
||||
|
||||
|
||||
def get_user_lang(user_id: str) -> Language:
|
||||
"""
|
||||
|
@ -2,9 +2,11 @@ import json
|
||||
import os.path
|
||||
import platform
|
||||
|
||||
import aiohttp
|
||||
import nonebot
|
||||
import psutil
|
||||
import requests
|
||||
from aiohttp import FormData
|
||||
|
||||
from . import __VERSION_I__, __VERSION__, __NAME__
|
||||
from .config import config, load_from_yaml
|
||||
@ -66,5 +68,31 @@ class LiteyukiAPI:
|
||||
else:
|
||||
nonebot.logger.warning(f"Bug report is disabled: {content}")
|
||||
|
||||
async def upload_image(self, image: bytes) -> str | None:
|
||||
"""
|
||||
上传图片到图床
|
||||
Args:
|
||||
image:
|
||||
|
||||
Returns:
|
||||
图片url
|
||||
"""
|
||||
assert self.liteyuki_id, "Liteyuki ID is not set"
|
||||
assert isinstance(image, bytes), "Image must be bytes"
|
||||
url = "https://api.liteyuki.icu/upload_image"
|
||||
data = FormData()
|
||||
data.add_field("liteyuki_id", self.liteyuki_id)
|
||||
data.add_field('image', image, filename='image', content_type='application/octet-stream')
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
url,
|
||||
data=data
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
return (await resp.json()).get("url")
|
||||
else:
|
||||
nonebot.logger.error(f"Upload image failed: {await resp.text()}")
|
||||
return None
|
||||
|
||||
|
||||
liteyuki_api = LiteyukiAPI()
|
||||
|
@ -1,81 +1,135 @@
|
||||
import asyncio
|
||||
import io
|
||||
from urllib.parse import quote
|
||||
|
||||
import aiofiles
|
||||
from PIL import Image
|
||||
import aiohttp
|
||||
import nonebot
|
||||
from nonebot.adapters.onebot import v11, v12
|
||||
from typing import Any
|
||||
|
||||
from .liteyuki_api import liteyuki_api
|
||||
from .ly_typing import T_Bot, T_MessageEvent
|
||||
|
||||
|
||||
async def send_markdown(markdown: str, bot: T_Bot, *, message_type: str = None, session_id: str | int = None, event: T_MessageEvent = None, **kwargs) -> dict[
|
||||
str, Any]:
|
||||
formatted_md = v11.unescape(markdown).replace("\n", r"\n").replace('"', r'\\\"')
|
||||
if event is not None and message_type is None:
|
||||
message_type = event.message_type
|
||||
session_id = event.user_id if event.message_type == "private" else event.group_id
|
||||
try:
|
||||
forward_id = await bot.call_api(
|
||||
api="send_forward_msg",
|
||||
messages=[
|
||||
v11.MessageSegment(
|
||||
type="node",
|
||||
data={
|
||||
"name" : "Liteyuki.OneBot",
|
||||
"uin" : bot.self_id,
|
||||
"content": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"data": {
|
||||
"content": '{"content":"%s"}' % formatted_md
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
data = await bot.send_msg(
|
||||
user_id=session_id,
|
||||
group_id=session_id,
|
||||
message_type=message_type,
|
||||
message=[
|
||||
v11.MessageSegment(
|
||||
type="longmsg",
|
||||
data={
|
||||
"id": forward_id
|
||||
}
|
||||
),
|
||||
],
|
||||
**kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
nonebot.logger.warning("send_markdown error, send as plain text: %s" % e.__repr__())
|
||||
if isinstance(bot, v11.Bot):
|
||||
data = await bot.send_msg(
|
||||
message_type=message_type,
|
||||
message=markdown,
|
||||
user_id=int(session_id),
|
||||
group_id=int(session_id),
|
||||
**kwargs
|
||||
)
|
||||
elif isinstance(bot, v12.Bot):
|
||||
data = await bot.send_message(
|
||||
detail_type=message_type,
|
||||
message=v12.Message(
|
||||
v12.MessageSegment.text(
|
||||
text=markdown
|
||||
)
|
||||
),
|
||||
user_id=str(session_id),
|
||||
group_id=str(session_id),
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
nonebot.logger.error("send_markdown: bot type not supported")
|
||||
data = {}
|
||||
return data
|
||||
|
||||
|
||||
class Markdown:
|
||||
@staticmethod
|
||||
async def send_md(
|
||||
markdown: str,
|
||||
bot: T_Bot, *,
|
||||
message_type: str = None,
|
||||
session_id: str | int = None,
|
||||
event: T_MessageEvent = None,
|
||||
**kwargs
|
||||
) -> dict[str, Any]:
|
||||
formatted_md = v11.unescape(markdown).replace("\n", r"\n").replace('"', r'\\\"')
|
||||
if event is not None and message_type is None:
|
||||
message_type = event.message_type
|
||||
session_id = event.user_id if event.message_type == "private" else event.group_id
|
||||
try:
|
||||
forward_id = await bot.call_api(
|
||||
api="send_forward_msg",
|
||||
messages=[
|
||||
v11.MessageSegment(
|
||||
type="node",
|
||||
data={
|
||||
"name" : "Liteyuki.OneBot",
|
||||
"uin" : bot.self_id,
|
||||
"content": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"data": {
|
||||
"content": '{"content":"%s"}' % formatted_md
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
data = await bot.send_msg(
|
||||
user_id=session_id,
|
||||
group_id=session_id,
|
||||
message_type=message_type,
|
||||
message=[
|
||||
v11.MessageSegment(
|
||||
type="longmsg",
|
||||
data={
|
||||
"id": forward_id
|
||||
}
|
||||
),
|
||||
],
|
||||
**kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
nonebot.logger.warning("send_markdown error, send as plain text: %s" % e.__repr__())
|
||||
if isinstance(bot, v11.Bot):
|
||||
data = await bot.send_msg(
|
||||
message_type=message_type,
|
||||
message=markdown,
|
||||
user_id=int(session_id),
|
||||
group_id=int(session_id),
|
||||
**kwargs
|
||||
)
|
||||
elif isinstance(bot, v12.Bot):
|
||||
data = await bot.send_message(
|
||||
detail_type=message_type,
|
||||
message=v12.Message(
|
||||
v12.MessageSegment.text(
|
||||
text=markdown
|
||||
)
|
||||
),
|
||||
user_id=str(session_id),
|
||||
group_id=str(session_id),
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
nonebot.logger.error("send_markdown: bot type not supported")
|
||||
data = {}
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
async def send_image(
|
||||
image: bytes | str,
|
||||
bot: T_Bot, *,
|
||||
message_type: str = None,
|
||||
session_id: str | int = None,
|
||||
event: T_MessageEvent = None,
|
||||
**kwargs
|
||||
) -> dict:
|
||||
"""
|
||||
发送单张装逼大图
|
||||
Args:
|
||||
image: 图片字节流或图片本地路径,链接请使用Markdown.image_async方法获取后通过send_md发送
|
||||
bot: bot instance
|
||||
message_type: message type
|
||||
session_id: session id
|
||||
event: event
|
||||
kwargs: other arguments
|
||||
Returns:
|
||||
dict: response data
|
||||
|
||||
"""
|
||||
if isinstance(image, str):
|
||||
async with aiofiles.open(image, "rb") as f:
|
||||
image = await f.read()
|
||||
|
||||
image_url = await liteyuki_api.upload_image(image)
|
||||
image_size = Image.open(io.BytesIO(image)).size
|
||||
image_md = Markdown.image(image_url, image_size)
|
||||
return await Markdown.send_md(image_md, bot, message_type=message_type, session_id=session_id, event=event, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
async def get_image_url(image: bytes | str, bot: T_Bot) -> str:
|
||||
"""把图片上传到图床,返回链接
|
||||
Args:
|
||||
bot: 发送的bot
|
||||
image: 图片字节流或图片本地路径
|
||||
Returns:
|
||||
"""
|
||||
# 等林文轩修好Lagrange.OneBot再说
|
||||
|
||||
@staticmethod
|
||||
def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str:
|
||||
"""生成点击回调按钮
|
||||
@ -104,6 +158,38 @@ class Markdown:
|
||||
"""
|
||||
return f"[🔗{name}]({url})"
|
||||
|
||||
@staticmethod
|
||||
def image(url: str, size: tuple[int, int]) -> str:
|
||||
"""生成图片
|
||||
Args:
|
||||
size:
|
||||
url: 图片链接
|
||||
|
||||
Returns:
|
||||
markdown格式的图片
|
||||
|
||||
"""
|
||||
return f"![image #{size[0]}px #{size[1]}px]({url})"
|
||||
|
||||
@staticmethod
|
||||
async def image_async(url: str) -> str:
|
||||
"""获取图片,自动获取大小
|
||||
Args:
|
||||
url: 图片链接
|
||||
|
||||
Returns:
|
||||
图片bytes
|
||||
|
||||
"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
image = Image.open(io.BytesIO(await resp.read()))
|
||||
return Markdown.image(url, image.size)
|
||||
except Exception as e:
|
||||
nonebot.logger.error(f"get image error: {e}")
|
||||
return "[Image Error]"
|
||||
|
||||
@staticmethod
|
||||
def escape(text: str) -> str:
|
||||
"""转义特殊字符
|
||||
|
@ -47,9 +47,21 @@ def get_path(path: str, abs_path: bool = False, default: Any = None) -> str | An
|
||||
"""
|
||||
获取资源包中的文件
|
||||
Args:
|
||||
abs_path:
|
||||
abs_path: 是否返回绝对路径
|
||||
default: 默认
|
||||
path: 文件相对路径
|
||||
Returns: 文件绝对路径
|
||||
"""
|
||||
return _resource_data.get(path, default) if not abs_path else os.path.abspath(_resource_data.get(path, default))
|
||||
|
||||
|
||||
def get_files(path: str, abs_path: bool = False) -> list[str]:
|
||||
"""
|
||||
获取资源包中一个文件夹的所有文件
|
||||
Args:
|
||||
abs_path:
|
||||
path: 文件夹相对路径
|
||||
Returns: 文件绝对路径
|
||||
"""
|
||||
return [os.path.abspath(file) for file in _resource_data if file.startswith(path)] if abs_path else [
|
||||
file for file in _resource_data if file.startswith(path)]
|
||||
|
@ -3,6 +3,7 @@ aiofiles==23.2.1
|
||||
arclet-alconna==1.8.5
|
||||
arclet-alconna-tools==0.7.0
|
||||
colored==2.2.4
|
||||
py-cpuinfo==9.0.0
|
||||
dash==2.16.1
|
||||
GitPython==3.1.42
|
||||
nonebot2[fastapi]==2.2.1
|
||||
|
Loading…
Reference in New Issue
Block a user