feat: 配置项目的热修改

This commit is contained in:
snowy 2024-03-31 06:22:53 +08:00
parent c8851bd696
commit f9e61fd184
22 changed files with 86394 additions and 138 deletions

View File

@ -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主动连接实现端

View File

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

View File

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

View File

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

View File

@ -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,7 +40,7 @@ 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,
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)

View File

@ -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("参数错误")

View File

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

View File

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

View File

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

View File

@ -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}迁移成功

View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 KiB

File diff suppressed because it is too large Load Diff

View File

@ -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和valuekey是cpumemswapvalue是对应的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>

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,28 @@
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]:
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
@ -74,8 +89,47 @@ async def send_markdown(markdown: str, bot: T_Bot, *, message_type: str = None,
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再说
class Markdown:
@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:
"""转义特殊字符

View File

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

View File

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