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格式不同应为小写) ### 轻雪配置项(Nonebot插件配置项也可以写在此与dotenv格式不同应为小写)
配置文件会在首次启动后生成,你可以在`config.yaml`中修改配置项后重启轻雪 配置文件会在首次启动后生成,你可以在`config.yaml`中修改配置项后重启轻雪
如果不确定字段的含义,请不要修改(部分在自动生成配置文件中未列出,需手动添加) 如果不确定字段的含义,请不要修改(部分在自动生成配置文件中未列出,需手动添加)
@ -26,6 +27,14 @@ default_language: "zh-CN" # 默认语言支持i18n部分语言和自行扩展
log_level: "INFO" # 日志等级 log_level: "INFO" # 日志等级
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用) log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
auto_report: true # 是否自动上报问题给轻雪服务器,仅包含硬件信息和运行软件版本 auto_report: true # 是否自动上报问题给轻雪服务器,仅包含硬件信息和运行软件版本
fake_device_info: # 统计卡片显示的虚假设备信息,用于保护隐私
cpu:
brand: AMD
cores: 16 # 物理核心数
logical_cores: 32 # 逻辑核心数
frequency: 3600 # CPU主频MHz
mem:
total: 32768000000 # 内存总数:字节
# 其他Nonebot插件的配置项 # 其他Nonebot插件的配置项
custom_config_1: "custom_value1" custom_config_1: "custom_value1"
@ -43,8 +52,8 @@ custom_config_2: "custom_value2"
| 地址 | ws://`address`/onebot/v11 | 地址取决于配置文件,本机默认为`127.0.0.1:20216` | | 地址 | ws://`address`/onebot/v11 | 地址取决于配置文件,本机默认为`127.0.0.1:20216` |
| AccessToken | `""` | 如果你给轻雪配置了`AccessToken`,请在此填写相同的值 | | AccessToken | `""` | 如果你给轻雪配置了`AccessToken`,请在此填写相同的值 |
### 其他通信方式 ### 其他通信方式
- 实现端与轻雪的通信方式不局限为反向WebSocket但是推荐使用反向WebSocket。 - 实现端与轻雪的通信方式不局限为反向WebSocket但是推荐使用反向WebSocket。
- 反向WebSocket的优点是轻雪作为服务端可以更好的控制连接适用于生产环境。 - 反向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.data_manager import StoredConfig, common_db
from liteyuki.utils.language import get_user_lang from liteyuki.utils.language import get_user_lang
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent 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 .reloader import Reloader
from liteyuki.utils import htmlrender from liteyuki.utils import htmlrender
@ -80,7 +80,7 @@ async def _(bot: T_Bot, event: T_MessageEvent):
reply += f"```\n{logs}\n```\n" reply += f"```\n{logs}\n```\n"
btn_restart = md.button(ulang.get("liteyuki.restart_now"), "restart-liteyuki") btn_restart = md.button(ulang.get("liteyuki.restart_now"), "restart-liteyuki")
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}" 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() @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(): for k, v in stored_config.config.items():
reply += f"\n{k}={v}" reply += f"\n{k}={v}"
reply += "\n```" reply += "\n```"
await send_markdown(reply, bot, event=event) await md.send_md(reply, bot, event=event)
@driver.on_startup @driver.on_startup

View File

@ -1,19 +1,131 @@
import json
import random
import psutil
import requests import requests
from PIL import Image
from nonebot.adapters.onebot.v11 import MessageSegment from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from liteyuki.utils.htmlrender import template_to_pic, html_to_pic 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 liteyuki.utils.resource import get_path
from nonebot import on_command 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() @stats.handle()
async def _(): async def _(bot: T_Bot, event: T_MessageEvent):
image_bytes = await template_to_pic( ulang = get_user_lang(str(event.user_id))
template_path=get_path("templates/index.html", abs_path=True), fake_device_info: dict = bot.config.dict().get("fake_device_info", {})
templates={} 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)) 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.data import Database, LiteModel
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent 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") require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna from nonebot_plugin_alconna import on_alconna
@ -108,7 +108,7 @@ async def _(event: T_MessageEvent, bot: T_Bot):
push_message = ( push_message = (
f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n" f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n"
f"{msg_formatted}") 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 return

View File

@ -5,7 +5,7 @@ from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent, v11 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_test = on_command("mdts", aliases={"会话md"}, permission=SUPERUSER)
md_group = on_command("mdg", aliases={"群md"}, permission=SUPERUSER) md_group = on_command("mdg", aliases={"群md"}, permission=SUPERUSER)
@ -23,7 +23,7 @@ placeholder = {
@md_test.handle() @md_test.handle()
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()): async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
await send_markdown( await md.send_md(
str(arg), str(arg),
bot, bot,
message_type=event.message_type, 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"]: if str(event.user_id) == str(bot.self_id) and str(bot.self_id) in ["2751454815"]:
nonebot.logger.info("开始处理:%s" % str(event.message_id)) 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) session_id=event.user_id if event.message_type == "private" else event.group_id)
await bot.delete_msg(message_id=event.message_id) await bot.delete_msg(message_id=event.message_id)

View File

@ -1,7 +1,7 @@
from nonebot import require from nonebot import require
from ...utils.ly_typing import T_Bot, T_MessageEvent 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") require("nonebot_plugin_alconna")
from .game import Minesweeper from .game import Minesweeper
@ -63,7 +63,7 @@ async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
) )
minesweeper_cache.append(new_game) minesweeper_cache.append(new_game)
await minesweeper.send("游戏开始") 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: except AssertionError:
await minesweeper.finish("参数错误") await minesweeper.finish("参数错误")
elif result.subcommands.get("end"): elif result.subcommands.get("end"):
@ -82,9 +82,9 @@ async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
await minesweeper.finish("参数错误") await minesweeper.finish("参数错误")
if not game.reveal(row, col): if not game.reveal(row, col):
minesweeper_cache.remove(game) 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 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(): if game.is_win():
minesweeper_cache.remove(game) minesweeper_cache.remove(game)
await minesweeper.finish("游戏胜利") 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): if not (0 <= row < game.rows and 0 <= col < game.cols):
await minesweeper.finish("参数错误") await minesweeper.finish("参数错误")
game.board[row][col].flagged = not game.board[row][col].flagged 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: else:
await minesweeper.finish("参数错误") await minesweeper.finish("参数错误")

View File

@ -10,7 +10,7 @@ from nonebot import require
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from liteyuki.utils.language import get_user_lang from liteyuki.utils.language import get_user_lang
from liteyuki.utils.ly_typing import T_Bot 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 * from .common import *
require("nonebot_plugin_alconna") 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)}" reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}"
else: else:
reply = ulang.get("npm.search_no_result") 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"): elif result.subcommands.get("install"):
plugin_module_name: str = result.subcommands["install"].args.get("plugin_name") 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: if found_in_db_plugin is None:
plugin_db.upsert(installed_plugin) plugin_db.upsert(installed_plugin)
info = md.escape(ulang.get("npm.install_success", NAME=store_plugin.name)) # markdown转义 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"{info}\n\n"
f"```\n{log}\n```", f"```\n{log}\n```",
bot, 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)) await npm_alc.finish(ulang.get("npm.plugin_already_installed", NAME=store_plugin.name))
else: else:
info = ulang.get("npm.load_failed", NAME=plugin_module_name, HOMEPAGE=homepage_btn).replace("_", r"\\_") 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"{info}\n\n"
f"```\n{log}\n```\n", f"```\n{log}\n```\n",
bot, bot,
@ -122,7 +122,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
) )
else: else:
info = ulang.get("npm.install_failed", NAME=plugin_module_name, HOMEPAGE=homepage_btn).replace("_", r"\\_") 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"{info}\n\n"
f"```\n{log}\n```", f"```\n{log}\n```",
bot, 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.data_manager import GlobalPlugin, Group, InstalledPlugin, User, group_db, plugin_db, user_db
from liteyuki.utils.language import get_user_lang from liteyuki.utils.language import get_user_lang
from liteyuki.utils.ly_typing import T_Bot, T_MessageEvent 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 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 .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 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 += f" {btn_uninstall} {btn_toggle_global}"
reply += "\n\n***\n" reply += "\n\n***\n"
await send_markdown(reply, bot, event=event) await md.send_md(reply, bot, event=event)
@toggle_plugin.handle() @toggle_plugin.handle()
@ -228,6 +228,6 @@ async def pre_handle(event: Event, matcher: Matcher):
raise IgnoredException("Plugin disabled in session") raise IgnoredException("Plugin disabled in session")
@Bot.on_calling_api # @Bot.on_calling_api
async def _(bot: Bot, api: str, data: dict[str, any]): # async def _(bot: Bot, api: str, data: dict[str, any]):
nonebot.logger.info(f"Plugin Callapi: {api}: {data}") # 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.data_manager import User, user_db
from liteyuki.utils.language import Language, get_all_lang, get_user_lang 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.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 from .const import representative_timezones_list
require("nonebot_plugin_alconna") 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) menu = get_profile_menu(result.args["key"], ulang)
if menu: if menu:
await send_markdown(menu, bot, event=event) await md.send_md(menu, bot, event=event)
else: else:
await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}"))) 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" reply += (f"\n**{key_text}** **{val}**\n"
f"\n> {ulang.get(f'user.profile.{key}.desc')}" f"\n> {ulang.get(f'user.profile.{key}.desc')}"
f"\n> {btn_set} \n\n***\n") 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]: def get_profile_menu(key: str, ulang: Language) -> Optional[str]:

View File

@ -13,6 +13,14 @@ liteyuki.current_config=当前配置项如下
liteyuki.static_config=静态文件配置项 liteyuki.static_config=静态文件配置项
liteyuki.stored_config=储存的配置项 liteyuki.stored_config=储存的配置项
liteyuki.config_set_success=配置项 {KEY}={VAL} 设置成功 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.current_language=当前配置语言为: {LANG}
main.enable_webdash=已启用网页监控面板: {URL} main.enable_webdash=已启用网页监控面板: {URL}
@ -23,6 +31,8 @@ main.monitor.memory=内存
main.monitor.swap=交换空间 main.monitor.swap=交换空间
main.monitor.disk=磁盘 main.monitor.disk=磁盘
main.monitor.usage=使用率 main.monitor.usage=使用率
main.monitor.total=总计
main.monitor.used=已用
data_manager.migrate_success=数据模型{NAME}迁移成功 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> <!DOCTYPE html>
<html lang="en">
<head> <head>
<meta charset="UTF-8"> <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> </head>
<body> <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>
<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> </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 from .config import Config
import asyncio import asyncio
config = get_plugin_config(Config) config = Config()
_browser: Optional[Browser] = None _browser: Optional[Browser] = None
_playwright: Optional[Playwright] = 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}") nonebot.logger.error(f"Failed to get language text or format: {e}")
return default or item 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: def get_user_lang(user_id: str) -> Language:
""" """

View File

@ -2,9 +2,11 @@ import json
import os.path import os.path
import platform import platform
import aiohttp
import nonebot import nonebot
import psutil import psutil
import requests import requests
from aiohttp import FormData
from . import __VERSION_I__, __VERSION__, __NAME__ from . import __VERSION_I__, __VERSION__, __NAME__
from .config import config, load_from_yaml from .config import config, load_from_yaml
@ -66,5 +68,31 @@ class LiteyukiAPI:
else: else:
nonebot.logger.warning(f"Bug report is disabled: {content}") 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() liteyuki_api = LiteyukiAPI()

View File

@ -1,13 +1,28 @@
import asyncio
import io
from urllib.parse import quote from urllib.parse import quote
import aiofiles
from PIL import Image
import aiohttp
import nonebot import nonebot
from nonebot.adapters.onebot import v11, v12 from nonebot.adapters.onebot import v11, v12
from typing import Any from typing import Any
from .liteyuki_api import liteyuki_api
from .ly_typing import T_Bot, T_MessageEvent 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[ class Markdown:
str, Any]: @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'\\\"') formatted_md = v11.unescape(markdown).replace("\n", r"\n").replace('"', r'\\\"')
if event is not None and message_type is None: if event is not None and message_type is None:
message_type = event.message_type message_type = event.message_type
@ -74,8 +89,47 @@ async def send_markdown(markdown: str, bot: T_Bot, *, message_type: str = None,
data = {} data = {}
return 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 @staticmethod
def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str: def button(name: str, cmd: str, reply: bool = False, enter: bool = True) -> str:
"""生成点击回调按钮 """生成点击回调按钮
@ -104,6 +158,38 @@ class Markdown:
""" """
return f"[🔗{name}]({url})" 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 @staticmethod
def escape(text: str) -> str: 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: Args:
abs_path: abs_path: 是否返回绝对路径
default: 默认 default: 默认
path: 文件相对路径 path: 文件相对路径
Returns: 文件绝对路径 Returns: 文件绝对路径
""" """
return _resource_data.get(path, default) if not abs_path else os.path.abspath(_resource_data.get(path, default)) 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==1.8.5
arclet-alconna-tools==0.7.0 arclet-alconna-tools==0.7.0
colored==2.2.4 colored==2.2.4
py-cpuinfo==9.0.0
dash==2.16.1 dash==2.16.1
GitPython==3.1.42 GitPython==3.1.42
nonebot2[fastapi]==2.2.1 nonebot2[fastapi]==2.2.1