mirror of
https://github.com/TriM-Organization/LiteyukiBot-TriM.git
synced 2024-11-13 10:57:38 +08:00
🔀手动Merge轻雪主仓库a77f97f
This commit is contained in:
parent
4cc2ae61db
commit
f8b57bfe9a
53
.github/ISSUE_TEMPLATE/问题反馈.md
vendored
53
.github/ISSUE_TEMPLATE/问题反馈.md
vendored
@ -1,53 +0,0 @@
|
||||
---
|
||||
name: 问题反馈
|
||||
about: 反馈你在使用轻雪中遇到的问题
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# 问题反馈
|
||||
|
||||
## **请确保**
|
||||
|
||||
- 已认真阅读[文档]("https://bot.liteyuki.icu"),该问题不是文档提及的或你自己操作不当造成的
|
||||
- 你的问题是在最新版本的代码上测试的
|
||||
- 请勿重复提交相同或类似的issue
|
||||
|
||||
|
||||
## **描述问题**
|
||||
|
||||
请在此简单描述问题
|
||||
|
||||
|
||||
|
||||
## **如何复现**
|
||||
|
||||
请阐述一下如何重现这个问题
|
||||
### 预期
|
||||
|
||||
描述你期望发生的事情
|
||||
|
||||
### 实际
|
||||
|
||||
描述实际发生的事情
|
||||
|
||||
|
||||
|
||||
## **日志或截图**
|
||||
```
|
||||
日志内容
|
||||
```
|
||||
|
||||
|
||||
## **设备信息**
|
||||
- **系统**: [例如 Ubuntu 22.04]
|
||||
- **CPU**: [例如 Intel i7-7700K]
|
||||
- **内存**: [例如 16GB]
|
||||
- **Python**: [例如CPython 3.10.7]
|
||||
|
||||
|
||||
**补充内容**
|
||||
|
||||
可选,推荐提供`pip freeze`的输出,以及其他相关信息,以及你的建议
|
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@ -1,11 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
44
.github/workflows/ISSUE_TEMPLATE.md
vendored
44
.github/workflows/ISSUE_TEMPLATE.md
vendored
@ -1,44 +0,0 @@
|
||||
# 问题反馈
|
||||
|
||||
## **请确保**
|
||||
|
||||
- 已认真阅读[文档]("https://bot.liteyuki.icu"),该问题不是文档提及的或你自己操作不当造成的
|
||||
- 你的问题是在最新版本的代码上测试的
|
||||
- 请勿重复提交相同或类似的issue
|
||||
|
||||
|
||||
## **描述问题**
|
||||
|
||||
请在此简单描述问题
|
||||
|
||||
|
||||
|
||||
## **如何复现**
|
||||
|
||||
请阐述一下如何重现这个问题
|
||||
### 预期
|
||||
|
||||
描述你期望发生的事情
|
||||
|
||||
### 实际
|
||||
|
||||
描述实际发生的事情
|
||||
|
||||
|
||||
|
||||
## **日志或截图**
|
||||
```
|
||||
日志内容
|
||||
```
|
||||
|
||||
|
||||
## **设备信息**
|
||||
- **系统**: [例如 Ubuntu 22.04]
|
||||
- **CPU**: [例如 Intel i7-7700K]
|
||||
- **内存**: [例如 16GB]
|
||||
- **Python**: [例如CPython 3.10.7]
|
||||
|
||||
|
||||
**补充内容**
|
||||
|
||||
可选,推荐提供`pip freeze`的输出,以及其他相关信息,以及你的建议
|
49
.github/workflows/deploy-docs.yml
vendored
49
.github/workflows/deploy-docs.yml
vendored
@ -1,49 +0,0 @@
|
||||
|
||||
name: 部署文档
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# 确保这是你正在使用的分支名称
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-gh-pages:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# 如果你文档需要 Git 子模块,取消注释下一行
|
||||
# submodules: true
|
||||
|
||||
- name: 安装 pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
run_install: true
|
||||
version: 8
|
||||
|
||||
|
||||
- name: 设置 Node.js
|
||||
run: |-
|
||||
cd docs
|
||||
pnpm install
|
||||
|
||||
- name: 构建文档
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=8192
|
||||
run: |-
|
||||
cd docs
|
||||
pnpm run docs:build
|
||||
> .vuepress/dist/.nojekyll
|
||||
|
||||
- name: 部署文档
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
# 这是文档部署到的分支名称
|
||||
branch: gh-pages
|
||||
folder: docs/.vuepress/dist
|
27
.gitignore
vendored
27
.gitignore
vendored
@ -14,6 +14,9 @@ __pycache__/
|
||||
*.pyd
|
||||
*.pyw
|
||||
/plugins/
|
||||
|
||||
#config
|
||||
/config/
|
||||
_config.yml
|
||||
config.yml
|
||||
config.example.yml
|
||||
@ -29,11 +32,11 @@ src/nonebot_plugins/dislink_plugin_ccnd
|
||||
.github
|
||||
# pyproject.toml
|
||||
|
||||
test.py
|
||||
line_count.py
|
||||
# mypy
|
||||
mypy.ini
|
||||
|
||||
# nuitka
|
||||
compile.bat
|
||||
main.build/
|
||||
main.dist/
|
||||
main.exe
|
||||
@ -46,3 +49,23 @@ prompt.txt
|
||||
# js
|
||||
**/echarts.js
|
||||
.env
|
||||
|
||||
# pdm
|
||||
.pdm-python
|
||||
.pdm-build
|
||||
dist
|
||||
|
||||
doc
|
||||
|
||||
mkdoc2.py
|
||||
result.json
|
||||
|
||||
# litedoc
|
||||
docs/zh/dev/api
|
||||
docs/en/dev/api
|
||||
mkdoc.bat
|
||||
|
||||
# vitepress
|
||||
docs/.vitepress/dist/
|
||||
docs/.vitepress/cache
|
||||
docs/.vitepress/.temp
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.11-bullseye
|
||||
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.10-slim-bullseye
|
||||
|
||||
ENV TZ Asia/Shanghai
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
[![][Python3.10+]][python-link]
|
||||
[![][Usage]][usage-link]
|
||||
|
||||
- 基于[Nonebot2](https://github.com/nonebot/nonebot2),有良好的生态支持
|
||||
- 原生支持与任意 `Python`Bot 框架互联,有良好的生态支持
|
||||
- 开箱即用,无需复杂配置
|
||||
- 集成包管理器,支持一键安装插件
|
||||
- 支持 OneBot 标准通信但不限于此
|
||||
@ -27,10 +27,7 @@
|
||||
|
||||
### 感谢
|
||||
|
||||
- 感谢[NoneBot2](https://nonebot.dev)提供的框架支持
|
||||
- 感谢[nonebot-plugin-htmlrender](https://github.com/kexue-z/nonebot-plugin-htmlrender)提供的渲染功能
|
||||
- 讨厌上述项目提供的**基于 Chromium 浏览器**的 HTML 渲染功能,就功能本身而言很好,但我讨厌一声不响在我电脑里装浏览器这个行为,虽然这并不妨碍我感谢之 ——金羿
|
||||
- 感谢[nonebot-plugin-alconna](https://github.com/ArcletProject/nonebot-plugin-alconna)提供的命令解析功能
|
||||
- 感谢所有贡献者们
|
||||
- 十分感谢[神羽 SnowyKami](https://github.com/snowykami)提供的技术指导和服务器资源
|
||||
- 特别感谢[云裳工作室](https://doc.ysmcc.cn/doc/1/)提供的服务器挂载
|
||||
- 由衷感谢我在学习生活中遇到的所有朋友们,你们身为我生命中的一处景色,不断地推进我此生的进程。
|
||||
|
31
clean_pycache.py
Normal file
31
clean_pycache.py
Normal file
@ -0,0 +1,31 @@
|
||||
import shutil
|
||||
import os
|
||||
from rich.console import Console
|
||||
from rich.progress import track
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def main():
|
||||
with console.status("正在搜寻可清理之文件"):
|
||||
egg_info: list = []
|
||||
for file in os.listdir():
|
||||
if file.endswith(".egg-info"):
|
||||
egg_info.append(file)
|
||||
console.print(file)
|
||||
|
||||
pycache: list = []
|
||||
for dirpath, dirnames, filenames in os.walk("./"):
|
||||
for dirname in dirnames:
|
||||
if dirname == "__pycache__":
|
||||
pycache.append(fn := os.path.join(dirpath, dirname))
|
||||
console.print(fn)
|
||||
for file in track(
|
||||
["build", "dist", "logs", "log", *egg_info, *pycache], description="正在清理"
|
||||
):
|
||||
if os.path.isdir(file) and os.access(file, os.W_OK):
|
||||
shutil.rmtree(file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,11 +1,12 @@
|
||||
from liteyuki.bot import (
|
||||
LiteyukiBot,
|
||||
get_bot
|
||||
get_bot,
|
||||
get_config,
|
||||
get_config_with_compat
|
||||
)
|
||||
|
||||
from liteyuki.comm import (
|
||||
Channel,
|
||||
chan,
|
||||
Event
|
||||
)
|
||||
|
||||
@ -15,7 +16,27 @@ from liteyuki.plugin import (
|
||||
)
|
||||
|
||||
from liteyuki.log import (
|
||||
logger,
|
||||
init_log
|
||||
|
||||
init_log,
|
||||
logger
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LiteyukiBot",
|
||||
"get_bot",
|
||||
"get_config",
|
||||
"get_config_with_compat",
|
||||
"Channel",
|
||||
"Event",
|
||||
"load_plugin",
|
||||
"load_plugins",
|
||||
"init_log",
|
||||
"logger",
|
||||
]
|
||||
|
||||
__version__ = "6.3.9" # 测试版本号
|
||||
# 6.3.9
|
||||
# 更改了on语法
|
||||
|
||||
# 6.3.8
|
||||
# 1. 初步添加对聊天的支持
|
||||
# 2. 优化了通道的性能
|
||||
|
@ -1,103 +1,102 @@
|
||||
import asyncio
|
||||
import atexit
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from liteyuki.bot.lifespan import LIFESPAN_FUNC, Lifespan
|
||||
from liteyuki.core import IS_MAIN_PROCESS
|
||||
from liteyuki.bot.lifespan import LIFESPAN_FUNC, Lifespan, PROCESS_LIFESPAN_FUNC
|
||||
from liteyuki.comm.channel import get_channel
|
||||
from liteyuki.core.manager import ProcessManager
|
||||
from liteyuki.core.spawn_process import mb_run, nb_run
|
||||
from liteyuki.log import init_log, logger
|
||||
from liteyuki.plugin import load_plugins
|
||||
from liteyuki.plugin import load_plugin
|
||||
from liteyuki.utils import IS_MAIN_PROCESS
|
||||
|
||||
__all__ = ["LiteyukiBot", "get_bot"]
|
||||
__all__ = [
|
||||
"LiteyukiBot",
|
||||
"get_bot",
|
||||
"get_config",
|
||||
"get_config_with_compat",
|
||||
]
|
||||
|
||||
|
||||
class LiteyukiBot:
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, **kwargs) -> None:
|
||||
"""
|
||||
初始化轻雪实例
|
||||
Args:
|
||||
**kwargs: 配置
|
||||
"""
|
||||
"""常规操作"""
|
||||
print_logo()
|
||||
global _BOT_INSTANCE
|
||||
_BOT_INSTANCE = self # 引用
|
||||
|
||||
"""配置"""
|
||||
self.config: dict[str, Any] = kwargs
|
||||
|
||||
"""初始化"""
|
||||
self.init(**self.config) # 初始化
|
||||
logger.info("尹灵温 正在初始化…")
|
||||
|
||||
self.lifespan: Lifespan = Lifespan()
|
||||
"""生命周期管理"""
|
||||
self.lifespan = Lifespan()
|
||||
self.process_manager: ProcessManager = ProcessManager(lifespan=self.lifespan)
|
||||
|
||||
self.process_manager: ProcessManager = ProcessManager(bot=self)
|
||||
"""事件循环"""
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True)
|
||||
self.stop_event = threading.Event()
|
||||
self.call_restart_count = 0
|
||||
|
||||
"""加载插件加载器"""
|
||||
load_plugin("liteyuki.plugins.plugin_loader") # 加载轻雪插件
|
||||
|
||||
async def _run(self):
|
||||
"""
|
||||
启动逻辑
|
||||
"""
|
||||
await self.lifespan.before_start() # 启动前钩子
|
||||
await self.lifespan.after_start() # 启动后钩子
|
||||
await self.keep_alive()
|
||||
|
||||
def run(self):
|
||||
load_plugins("liteyuki/plugins") # 加载轻雪插件
|
||||
"""
|
||||
外部启动接口
|
||||
"""
|
||||
self.process_manager.start_all()
|
||||
try:
|
||||
asyncio.run(self._run())
|
||||
except KeyboardInterrupt:
|
||||
logger.opt(colors=True).info("<y>尹灵温 关闭中…</y>")
|
||||
self.stop()
|
||||
logger.opt(colors=True).info("<y>尹灵温 已关停</y>")
|
||||
|
||||
self.loop_thread.start() # 启动事件循环
|
||||
asyncio.run(self.lifespan.before_start()) # 启动前钩子
|
||||
|
||||
self.process_manager.add_target("nonebot", nb_run, **self.config)
|
||||
self.process_manager.start("nonebot")
|
||||
|
||||
self.process_manager.add_target("melobot", mb_run, **self.config)
|
||||
self.process_manager.start("melobot")
|
||||
|
||||
asyncio.run(self.lifespan.after_start()) # 启动后钩子
|
||||
|
||||
self.start_watcher() # 启动文件监视器
|
||||
|
||||
def start_watcher(self):
|
||||
if self.config.get("debug", False):
|
||||
|
||||
src_directories = (
|
||||
"liteyuki",
|
||||
"src/liteyuki_main",
|
||||
"src/liteyuki_plugins",
|
||||
"src/nonebot_plugins",
|
||||
"src/utils",
|
||||
)
|
||||
src_excludes_extensions = ("pyc",)
|
||||
|
||||
logger.debug("轻雪重载 已启用,正在加载文件修改监测……")
|
||||
restart = self.restart_process
|
||||
|
||||
class CodeModifiedHandler(FileSystemEventHandler):
|
||||
"""
|
||||
Handler for code file changes
|
||||
"""
|
||||
|
||||
def on_modified(self, event):
|
||||
if (
|
||||
event.src_path.endswith(src_excludes_extensions)
|
||||
or event.is_directory
|
||||
or "__pycache__" in event.src_path
|
||||
):
|
||||
return
|
||||
logger.info(f"文件 {event.src_path} 已修改,机器人自动重启……")
|
||||
restart()
|
||||
|
||||
code_modified_handler = CodeModifiedHandler()
|
||||
|
||||
observer = Observer()
|
||||
for directory in src_directories:
|
||||
observer.schedule(code_modified_handler, directory, recursive=True)
|
||||
observer.start()
|
||||
async def keep_alive(self):
|
||||
"""
|
||||
保持轻雪运行
|
||||
"""
|
||||
logger.info("尹灵温 持续运行中…")
|
||||
try:
|
||||
while not self.stop_event.is_set():
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception:
|
||||
logger.info("尹灵温 现退停…")
|
||||
self.stop()
|
||||
|
||||
def restart(self, delay: int = 0):
|
||||
"""
|
||||
重启轻雪本体
|
||||
Returns:
|
||||
|
||||
Args:
|
||||
delay ([`int`](https%3A//docs.python.org/3/library/functions.html#int), optional): 延迟重启时间. Defaults to 0.
|
||||
"""
|
||||
|
||||
if self.call_restart_count < 1:
|
||||
executable = sys.executable
|
||||
args = sys.argv
|
||||
logger.info("正在重启 尹灵温...")
|
||||
logger.info("正在重启 尹灵温机器人框架")
|
||||
time.sleep(delay)
|
||||
if platform.system() == "Windows":
|
||||
cmd = "start"
|
||||
@ -110,7 +109,9 @@ class LiteyukiBot:
|
||||
self.process_manager.terminate_all()
|
||||
# 进程退出后重启
|
||||
threading.Thread(
|
||||
target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",)
|
||||
target=os.system,
|
||||
args=(f"{cmd} {executable} {' '.join(args)}",),
|
||||
daemon=True,
|
||||
).start()
|
||||
sys.exit(0)
|
||||
self.call_restart_count += 1
|
||||
@ -119,44 +120,46 @@ class LiteyukiBot:
|
||||
"""
|
||||
停止轻雪
|
||||
Args:
|
||||
name: 进程名称, 默认为None, 所有进程
|
||||
name ([`Optional`](https%3A//docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)]): 进程名. Defaults to None.
|
||||
Returns:
|
||||
|
||||
"""
|
||||
logger.info("Stopping LiteyukiBot...")
|
||||
|
||||
self.loop.create_task(self.lifespan.before_shutdown()) # 重启前钩子
|
||||
self.loop.create_task(self.lifespan.before_shutdown()) # 停止前钩子
|
||||
|
||||
if name:
|
||||
self.process_manager.terminate(name)
|
||||
if name is not None:
|
||||
chan_active = get_channel(f"{name}-active")
|
||||
chan_active.send(1)
|
||||
else:
|
||||
self.process_manager.terminate_all()
|
||||
for process_name in self.process_manager.processes:
|
||||
chan_active = get_channel(f"{process_name}-active")
|
||||
chan_active.send(1)
|
||||
|
||||
def init(self, *args, **kwargs):
|
||||
"""
|
||||
初始化轻雪, 自动调用
|
||||
Returns:
|
||||
|
||||
Args:
|
||||
*args: 参数
|
||||
**kwargs: 关键字参数
|
||||
"""
|
||||
self.init_config()
|
||||
self.init_logger()
|
||||
|
||||
def init_logger(self):
|
||||
# 修改nonebot的日志配置
|
||||
"""
|
||||
初始化日志
|
||||
"""
|
||||
init_log(config=self.config)
|
||||
|
||||
def init_config(self):
|
||||
pass
|
||||
def stop(self):
|
||||
"""
|
||||
停止轻雪
|
||||
"""
|
||||
self.process_manager.terminate_all()
|
||||
self.stop_event.set()
|
||||
|
||||
def on_before_start(self, func: LIFESPAN_FUNC):
|
||||
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
注册启动前的函数
|
||||
Args:
|
||||
func:
|
||||
|
||||
func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
|
||||
Returns:
|
||||
|
||||
[`LIFESPAN_FUNC`](./lifespan#var-lifespan-func): 生命周期函数
|
||||
"""
|
||||
return self.lifespan.on_before_start(func)
|
||||
|
||||
@ -164,81 +167,128 @@ class LiteyukiBot:
|
||||
"""
|
||||
注册启动后的函数
|
||||
Args:
|
||||
func:
|
||||
|
||||
func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
|
||||
Returns:
|
||||
|
||||
[`LIFESPAN_FUNC`](./lifespan#var-lifespan-func): 生命周期函数
|
||||
"""
|
||||
return self.lifespan.on_after_start(func)
|
||||
|
||||
def on_before_shutdown(self, func: LIFESPAN_FUNC):
|
||||
"""
|
||||
注册停止前的函数,为子进程停止时调用
|
||||
Args:
|
||||
func:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return self.lifespan.on_before_shutdown(func)
|
||||
|
||||
def on_after_shutdown(self, func: LIFESPAN_FUNC):
|
||||
"""
|
||||
注册停止后的函数:未实现
|
||||
Args:
|
||||
func:
|
||||
|
||||
func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
|
||||
Returns:
|
||||
|
||||
[`LIFESPAN_FUNC`](./lifespan#var-lifespan-func): 生命周期函数
|
||||
"""
|
||||
return self.lifespan.on_after_shutdown(func)
|
||||
|
||||
def on_before_restart(self, func: LIFESPAN_FUNC):
|
||||
def on_before_process_shutdown(self, func: PROCESS_LIFESPAN_FUNC):
|
||||
"""
|
||||
注册重启前的函数,为子进程重启时调用
|
||||
注册进程停止前的函数,为子进程停止时调用
|
||||
Args:
|
||||
func:
|
||||
|
||||
func ([`PROCESS_LIFESPAN_FUNC`](./lifespan#var-process-lifespan-func)): 生命周期函数
|
||||
Returns:
|
||||
[`PROCESS_LIFESPAN_FUNC`](./lifespan#var-process-lifespan-func): 生命周期函数
|
||||
"""
|
||||
return self.lifespan.on_before_process_shutdown(func)
|
||||
|
||||
def on_before_process_restart(
|
||||
self, func: PROCESS_LIFESPAN_FUNC
|
||||
) -> PROCESS_LIFESPAN_FUNC:
|
||||
"""
|
||||
注册进程重启前的函数,为子进程重启时调用
|
||||
Args:
|
||||
func ([`PROCESS_LIFESPAN_FUNC`](./lifespan#var-process-lifespan-func)): 生命周期函数
|
||||
Returns:
|
||||
[`PROCESS_LIFESPAN_FUNC`](./lifespan#var-process-lifespan-func): 生命周期函数
|
||||
"""
|
||||
|
||||
return self.lifespan.on_before_restart(func)
|
||||
return self.lifespan.on_before_process_restart(func)
|
||||
|
||||
def on_after_restart(self, func: LIFESPAN_FUNC):
|
||||
"""
|
||||
注册重启后的函数:未实现
|
||||
Args:
|
||||
func:
|
||||
|
||||
func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
|
||||
Returns:
|
||||
|
||||
[`LIFESPAN_FUNC`](./lifespan#var-lifespan-func): 生命周期函数
|
||||
"""
|
||||
return self.lifespan.on_after_restart(func)
|
||||
|
||||
def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
|
||||
"""
|
||||
注册nonebot初始化后的函数
|
||||
Args:
|
||||
func:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return self.lifespan.on_after_nonebot_init(func)
|
||||
_BOT_INSTANCE: LiteyukiBot
|
||||
|
||||
|
||||
_BOT_INSTANCE: Optional[LiteyukiBot] = None
|
||||
|
||||
|
||||
def get_bot() -> Optional[LiteyukiBot]:
|
||||
def get_bot() -> LiteyukiBot:
|
||||
"""
|
||||
获取轻雪实例
|
||||
Returns:
|
||||
LiteyukiBot: 当前的轻雪实例
|
||||
[`LiteyukiBot`](#class-liteyukibot): 轻雪实例
|
||||
"""
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
if _BOT_INSTANCE is None:
|
||||
raise RuntimeError("尹灵温 实例未初始化")
|
||||
return _BOT_INSTANCE
|
||||
else:
|
||||
# 从多进程上下文中获取
|
||||
pass
|
||||
raise RuntimeError("无法在子进程中获取机器人实例")
|
||||
|
||||
|
||||
def get_config(key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
获取配置
|
||||
Args:
|
||||
key ([`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 配置键
|
||||
default ([`Any`](https%3A//docs.python.org/3/library/functions.html#any), optional): 默认值. Defaults to None.
|
||||
Returns:
|
||||
[`Any`](https%3A//docs.python.org/3/library/functions.html#any): 配置值
|
||||
"""
|
||||
return get_bot().config.get(key, default)
|
||||
|
||||
|
||||
def get_config_with_compat(
|
||||
key: str, compat_keys: tuple[str], default: Any = None
|
||||
) -> Any:
|
||||
"""
|
||||
获取配置,兼容旧版本
|
||||
Args:
|
||||
key ([`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 配置键
|
||||
compat_keys ([`tuple`](https%3A//docs.python.org/3/library/stdtypes.html#tuple)[`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 兼容键
|
||||
default ([`Any`](https%3A//docs.python.org/3/library/functions.html#any), optional): 默认值. Defaults to None.
|
||||
|
||||
Returns:
|
||||
[`Any`](https%3A//docs.python.org/3/library/functions.html#any): 配置值
|
||||
"""
|
||||
if key in get_bot().config:
|
||||
return get_bot().config[key]
|
||||
for compat_key in compat_keys:
|
||||
if compat_key in get_bot().config:
|
||||
logger.warning(f'配置键 "{compat_key}" 即将被 "{key}" 取代,请及时更新')
|
||||
return get_bot().config[compat_key]
|
||||
return default
|
||||
|
||||
|
||||
def print_logo():
|
||||
"""@litedoc-hide"""
|
||||
print(
|
||||
"\033[34m"
|
||||
+ r"""
|
||||
▅▅▅▅▅▅▅▅▅▅▅▅▅▅██ ▅▅▅▅▅▅▅▅▅▅▅▅▅▅██ ██ ▅▅▅▅▅▅▅▅▅▅█™
|
||||
▛ ██ ██ ▛ ██ ███ ██ ██
|
||||
██ ██ ███████████████ ██ ████████▅ ██
|
||||
███████████████ ██ ███ ██ ██
|
||||
██ ██ ▅██████████████▛ ██ ████████████
|
||||
██ ██ ███ ███
|
||||
████████████████ ██▅ ███ ██ ▅▅▅▅▅▅▅▅▅▅▅██
|
||||
███ █ ▜███████ ██ ███ ██ ██ ██ ██
|
||||
███ ███ █████▛ ██ ██ ██ ██ ██
|
||||
███ ██ ███ █ ██ ██ ██ ██ ██
|
||||
███ █████ ██████ ███ ██████████████
|
||||
商业标记 版权所有 © 2024 金羿Eilles
|
||||
机器软件 版权所有 © 2020-2024 神羽SnowyKami & 金羿Eilles\\
|
||||
会同 LiteyukiStudio & 睿乐组织
|
||||
保留所有权利
|
||||
"""
|
||||
+ "\033[0m"
|
||||
)
|
||||
|
@ -8,14 +8,19 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
@File : lifespan.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from typing import Any, Awaitable, Callable, TypeAlias
|
||||
import asyncio
|
||||
from typing import Any, Awaitable, Callable, TypeAlias, Sequence
|
||||
|
||||
from liteyuki.log import logger
|
||||
from liteyuki.utils import is_coroutine_callable
|
||||
from liteyuki.utils import is_coroutine_callable, async_wrapper
|
||||
|
||||
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
||||
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
||||
LIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC
|
||||
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any] # 同步生命周期函数
|
||||
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]] # 异步生命周期函数
|
||||
LIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC # 生命周期函数
|
||||
|
||||
SYNC_PROCESS_LIFESPAN_FUNC: TypeAlias = Callable[[str], Any] # 同步进程生命周期函数
|
||||
ASYNC_PROCESS_LIFESPAN_FUNC: TypeAlias = Callable[[str], Awaitable[Any]] # 异步进程生命周期函数
|
||||
PROCESS_LIFESPAN_FUNC: TypeAlias = SYNC_PROCESS_LIFESPAN_FUNC | ASYNC_PROCESS_LIFESPAN_FUNC # 进程函数
|
||||
|
||||
|
||||
class Lifespan:
|
||||
@ -23,41 +28,35 @@ class Lifespan:
|
||||
"""
|
||||
轻雪生命周期管理,启动、停止、重启
|
||||
"""
|
||||
|
||||
self.life_flag: int = 0 # 0: 启动前,1: 启动后,2: 停止前,3: 停止后
|
||||
self.life_flag: int = 0
|
||||
|
||||
self._before_start_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._after_start_funcs: list[LIFESPAN_FUNC] = []
|
||||
|
||||
self._before_shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._before_process_shutdown_funcs: list[PROCESS_LIFESPAN_FUNC] = []
|
||||
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||
|
||||
self._before_restart_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._before_process_restart_funcs: list[PROCESS_LIFESPAN_FUNC] = []
|
||||
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
|
||||
|
||||
self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
|
||||
|
||||
@staticmethod
|
||||
async def _run_funcs(funcs: list[LIFESPAN_FUNC]) -> None:
|
||||
async def run_funcs(funcs: Sequence[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
|
||||
"""
|
||||
运行函数
|
||||
并发运行异步函数
|
||||
Args:
|
||||
funcs:
|
||||
funcs ([`Sequence`](https%3A//docs.python.org/3/library/typing.html#typing.Sequence)[[`ASYNC_LIFESPAN_FUNC`](#var-lifespan-func) | [`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func)]): 函数列表
|
||||
Returns:
|
||||
"""
|
||||
for func in funcs:
|
||||
if is_coroutine_callable(func):
|
||||
await func()
|
||||
else:
|
||||
func()
|
||||
tasks = [func(*args, **kwargs) if is_coroutine_callable(func) else async_wrapper(func)(*args, **kwargs) for func in funcs]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
注册启动时的函数
|
||||
Args:
|
||||
func:
|
||||
func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
|
||||
Returns:
|
||||
LIFESPAN_FUNC:
|
||||
[`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
|
||||
"""
|
||||
self._before_start_funcs.append(func)
|
||||
return func
|
||||
@ -66,124 +65,95 @@ class Lifespan:
|
||||
"""
|
||||
注册启动时的函数
|
||||
Args:
|
||||
func:
|
||||
func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
|
||||
Returns:
|
||||
LIFESPAN_FUNC:
|
||||
[`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
|
||||
"""
|
||||
self._after_start_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_before_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
def on_before_process_shutdown(self, func: PROCESS_LIFESPAN_FUNC) -> PROCESS_LIFESPAN_FUNC:
|
||||
"""
|
||||
注册停止前的函数
|
||||
注册进程停止前的函数
|
||||
Args:
|
||||
func:
|
||||
func ([`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func)): 进程生命周期函数
|
||||
Returns:
|
||||
LIFESPAN_FUNC:
|
||||
[`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func): 进程生命周期函数
|
||||
"""
|
||||
self._before_shutdown_funcs.append(func)
|
||||
self._before_process_shutdown_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
注册停止后的函数
|
||||
Args:
|
||||
func:
|
||||
|
||||
func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
|
||||
Returns:
|
||||
LIFESPAN_FUNC:
|
||||
|
||||
[`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
|
||||
"""
|
||||
self._after_shutdown_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_before_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
def on_before_process_restart(self, func: PROCESS_LIFESPAN_FUNC) -> PROCESS_LIFESPAN_FUNC:
|
||||
"""
|
||||
注册重启时的函数
|
||||
注册进程重启前的函数
|
||||
Args:
|
||||
func:
|
||||
func ([`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func)): 进程生命周期函数
|
||||
Returns:
|
||||
LIFESPAN_FUNC:
|
||||
[`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func): 进程生命周期函数
|
||||
"""
|
||||
self._before_restart_funcs.append(func)
|
||||
self._before_process_restart_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
注册重启后的函数
|
||||
Args:
|
||||
func:
|
||||
func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
|
||||
Returns:
|
||||
LIFESPAN_FUNC:
|
||||
[`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
|
||||
"""
|
||||
self._after_restart_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_after_nonebot_init(self, func):
|
||||
"""
|
||||
注册 NoneBot 初始化后的函数
|
||||
Args:
|
||||
func:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self._after_nonebot_init_funcs.append(func)
|
||||
return func
|
||||
|
||||
async def before_start(self) -> None:
|
||||
"""
|
||||
启动前
|
||||
Returns:
|
||||
启动前钩子
|
||||
"""
|
||||
logger.debug("正在运行 before_start 之函数")
|
||||
await self._run_funcs(self._before_start_funcs)
|
||||
logger.debug("运行 before_start 函数")
|
||||
await self.run_funcs(self._before_start_funcs)
|
||||
|
||||
async def after_start(self) -> None:
|
||||
"""
|
||||
启动后
|
||||
Returns:
|
||||
启动后钩子
|
||||
"""
|
||||
logger.debug("正在运行 after_start 之函数")
|
||||
await self._run_funcs(self._after_start_funcs)
|
||||
logger.debug("运行 after_start 函数")
|
||||
await self.run_funcs(self._after_start_funcs)
|
||||
|
||||
async def before_shutdown(self) -> None:
|
||||
async def before_process_shutdown(self, *args, **kwargs) -> None:
|
||||
"""
|
||||
停止前
|
||||
Returns:
|
||||
停止前钩子
|
||||
"""
|
||||
logger.debug("正在运行 before_shutdown 之函数")
|
||||
await self._run_funcs(self._before_shutdown_funcs)
|
||||
logger.debug("运行 before_shutdown 函数")
|
||||
await self.run_funcs(self._before_process_shutdown_funcs, *args, **kwargs)
|
||||
|
||||
async def after_shutdown(self) -> None:
|
||||
"""
|
||||
停止后
|
||||
Returns:
|
||||
停止后钩子 未实现
|
||||
"""
|
||||
logger.debug("正在运行 after_shutdown 之函数")
|
||||
await self._run_funcs(self._after_shutdown_funcs)
|
||||
logger.debug("运行 after_shutdown 函数")
|
||||
await self.run_funcs(self._after_shutdown_funcs)
|
||||
|
||||
async def before_restart(self) -> None:
|
||||
async def before_process_restart(self, *args, **kwargs) -> None:
|
||||
"""
|
||||
重启前
|
||||
Returns:
|
||||
重启前钩子
|
||||
"""
|
||||
logger.debug("正在运行 before_restart 之函数")
|
||||
await self._run_funcs(self._before_restart_funcs)
|
||||
logger.debug("运行 before_restart 函数")
|
||||
await self.run_funcs(self._before_process_restart_funcs, *args, **kwargs)
|
||||
|
||||
async def after_restart(self) -> None:
|
||||
"""
|
||||
重启后
|
||||
Returns:
|
||||
|
||||
重启后钩子 未实现
|
||||
"""
|
||||
logger.debug("正在运行 after_restart 之函数")
|
||||
await self._run_funcs(self._after_restart_funcs)
|
||||
|
||||
async def after_nonebot_init(self) -> None:
|
||||
"""
|
||||
NoneBot 初始化后
|
||||
Returns:
|
||||
"""
|
||||
logger.debug("正在运行 after_nonebot_init 之函数")
|
||||
await self._run_funcs(self._after_nonebot_init_funcs)
|
||||
logger.debug("运行 after_restart 函数")
|
||||
await self.run_funcs(self._after_restart_funcs)
|
||||
|
@ -1,30 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/7/26 下午10:36
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : __init__.py
|
||||
@Software: PyCharm
|
||||
该模块用于轻雪主进程和Nonebot子进程之间的通信
|
||||
依赖关系
|
||||
event -> _
|
||||
storage -> channel_
|
||||
rpc -> channel_, storage
|
||||
"""
|
||||
from liteyuki.comm.channel import (
|
||||
Channel,
|
||||
chan,
|
||||
get_channel,
|
||||
set_channel,
|
||||
set_channels,
|
||||
get_channels
|
||||
get_channels,
|
||||
active_channel,
|
||||
passive_channel
|
||||
)
|
||||
from liteyuki.comm.event import Event
|
||||
|
||||
__all__ = [
|
||||
"Channel",
|
||||
"chan",
|
||||
"Event",
|
||||
"get_channel",
|
||||
"set_channel",
|
||||
"set_channels",
|
||||
"get_channels"
|
||||
"get_channels",
|
||||
"active_channel",
|
||||
"passive_channel"
|
||||
]
|
||||
|
||||
from liteyuki.utils import IS_MAIN_PROCESS
|
||||
|
||||
# 第一次引用必定为赋值
|
||||
_ref_count = 0
|
||||
if not IS_MAIN_PROCESS:
|
||||
if (active_channel is None or passive_channel is None) and _ref_count > 0:
|
||||
raise RuntimeError("无法在子进程中初始化 Channel")
|
||||
_ref_count += 1
|
||||
|
@ -1,219 +1,305 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/7/26 下午11:21
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : channel.py
|
||||
@Software: PyCharm
|
||||
|
||||
本模块定义了一个通用的通道类,用于进程间通信
|
||||
"""
|
||||
import functools
|
||||
import multiprocessing
|
||||
import threading
|
||||
import asyncio
|
||||
from multiprocessing import Pipe
|
||||
from typing import Any, Optional, Callable, Awaitable, List, TypeAlias
|
||||
from uuid import uuid4
|
||||
from typing import Any, Callable, Coroutine, Generic, Optional, TypeAlias, TypeVar, get_args
|
||||
|
||||
from liteyuki.utils import is_coroutine_callable, run_coroutine
|
||||
from liteyuki.log import logger
|
||||
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable
|
||||
|
||||
SYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[Any], Any]
|
||||
ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[Any], Awaitable[Any]]
|
||||
ON_RECEIVE_FUNC: TypeAlias = SYNC_ON_RECEIVE_FUNC | ASYNC_ON_RECEIVE_FUNC
|
||||
T = TypeVar("T")
|
||||
|
||||
SYNC_FILTER_FUNC: TypeAlias = Callable[[Any], bool]
|
||||
ASYNC_FILTER_FUNC: TypeAlias = Callable[[Any], Awaitable[bool]]
|
||||
FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC
|
||||
SYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Any] # 同步接收函数
|
||||
ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Coroutine[Any, Any, Any]] # 异步接收函数
|
||||
ON_RECEIVE_FUNC: TypeAlias = SYNC_ON_RECEIVE_FUNC | ASYNC_ON_RECEIVE_FUNC # 接收函数
|
||||
|
||||
IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
|
||||
SYNC_FILTER_FUNC: TypeAlias = Callable[[T], bool] # 同步过滤函数
|
||||
ASYNC_FILTER_FUNC: TypeAlias = Callable[[T], Coroutine[Any, Any, bool]] # 异步过滤函数
|
||||
FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC # 过滤函数
|
||||
|
||||
_func_id: int = 0
|
||||
_channel: dict[str, "Channel"] = {}
|
||||
_callback_funcs: dict[str, ON_RECEIVE_FUNC] = {}
|
||||
_callback_funcs: dict[int, ON_RECEIVE_FUNC] = {}
|
||||
|
||||
|
||||
class Channel:
|
||||
class Channel(Generic[T]):
|
||||
"""
|
||||
通道类,用于进程间通信,进程内不可用,仅限主进程和子进程之间通信
|
||||
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
|
||||
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
|
||||
"""
|
||||
|
||||
def __init__(self, _id: str):
|
||||
self.main_send_conn, self.sub_receive_conn = Pipe()
|
||||
self.sub_send_conn, self.main_receive_conn = Pipe()
|
||||
self._closed = False
|
||||
self._on_main_receive_funcs: list[str] = []
|
||||
self._on_sub_receive_funcs: list[str] = []
|
||||
self.name: str = _id
|
||||
def __init__(self, name: str, type_check: Optional[bool] = None):
|
||||
"""
|
||||
初始化通道
|
||||
Args:
|
||||
name: 通道ID
|
||||
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
|
||||
"""
|
||||
|
||||
self.is_main_receive_loop_running = False
|
||||
self.is_sub_receive_loop_running = False
|
||||
self.conn_send, self.conn_recv = Pipe()
|
||||
self._conn_send_inner, self._conn_recv_inner = Pipe() # 内部通道,用于子进程通信
|
||||
self._closed = False
|
||||
self._on_main_receive_func_ids: list[int] = []
|
||||
self._on_sub_receive_func_ids: list[int] = []
|
||||
self.name: str = name
|
||||
|
||||
self.is_receive_loop_running = False
|
||||
|
||||
if type_check is None:
|
||||
# 若传入泛型则默认开启类型检查
|
||||
type_check = self._get_generic_type() is not None
|
||||
|
||||
elif type_check:
|
||||
if self._get_generic_type() is None:
|
||||
raise TypeError("Type hint 是强制类型检查之所必须")
|
||||
self.type_check = type_check
|
||||
if name in _channel:
|
||||
raise ValueError(f"Channel {name} 已存在")
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
if name in _channel:
|
||||
raise ValueError(f"Channel {name} 已存在")
|
||||
_channel[name] = self
|
||||
logger.debug(f"Channel {name} 已在主进程中初始化")
|
||||
else:
|
||||
logger.debug(f"Channel {name} 已初始化于子进程中,之后应于主进程中手动设置为妙")
|
||||
|
||||
def _get_generic_type(self) -> Optional[type]:
|
||||
"""
|
||||
获取通道传递泛型类型
|
||||
Returns:
|
||||
Optional[type]: 泛型类型
|
||||
"""
|
||||
if hasattr(self, '__orig_class__'):
|
||||
return get_args(self.__orig_class__)[0]
|
||||
return None
|
||||
|
||||
def _validate_structure(self, data: Any, structure: type) -> bool:
|
||||
"""
|
||||
验证数据结构
|
||||
Args:
|
||||
data: 数据
|
||||
structure: 结构
|
||||
Returns:
|
||||
bool: 是否通过验证
|
||||
"""
|
||||
if isinstance(structure, type):
|
||||
return isinstance(data, structure)
|
||||
elif isinstance(structure, tuple):
|
||||
if not isinstance(data, tuple) or len(data) != len(structure):
|
||||
return False
|
||||
return all(self._validate_structure(d, s) for d, s in zip(data, structure))
|
||||
elif isinstance(structure, list):
|
||||
if not isinstance(data, list):
|
||||
return False
|
||||
return all(self._validate_structure(d, structure[0]) for d in data)
|
||||
elif isinstance(structure, dict):
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
return all(k in data and self._validate_structure(data[k], structure[k]) for k in structure)
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"Channel({self.name})"
|
||||
|
||||
def send(self, data: Any):
|
||||
def send(self, data: T):
|
||||
"""
|
||||
发送数据
|
||||
发送数据,发送函数为同步函数,没有异步的必要
|
||||
Args:
|
||||
data: 数据
|
||||
data (T): 数据
|
||||
"""
|
||||
if self._closed:
|
||||
raise RuntimeError("无法发送至已关闭的通道中")
|
||||
if IS_MAIN_PROCESS:
|
||||
print("主进程发送数据:", data)
|
||||
self.main_send_conn.send(data)
|
||||
else:
|
||||
print("子进程发送数据:", data)
|
||||
self.sub_send_conn.send(data)
|
||||
if self.type_check:
|
||||
_type = self._get_generic_type()
|
||||
if _type is not None and not self._validate_structure(data, _type):
|
||||
raise TypeError(f"该数据必须为 {_type} 实例,而非 {type(data)}")
|
||||
|
||||
def receive(self) -> Any:
|
||||
if self._closed:
|
||||
raise RuntimeError("数据无法向已关闭的 Channel 中发送")
|
||||
self.conn_send.send(data)
|
||||
|
||||
def receive(self) -> T:
|
||||
"""
|
||||
接收数据
|
||||
Args:
|
||||
同步接收数据,会阻塞线程
|
||||
Returns:
|
||||
T: 数据
|
||||
"""
|
||||
if self._closed:
|
||||
raise RuntimeError("无法从已关闭的通道中接收")
|
||||
raise RuntimeError("无法在已关闭的 Channel 中接取数据")
|
||||
|
||||
while True:
|
||||
# 判断receiver是否为None或者receiver是否等于接收者,是则接收数据,否则不动数据
|
||||
if IS_MAIN_PROCESS:
|
||||
data = self.main_receive_conn.recv()
|
||||
print("主进程接收数据:", data)
|
||||
else:
|
||||
data = self.sub_receive_conn.recv()
|
||||
print("子进程接收数据:", data)
|
||||
|
||||
data = self.conn_recv.recv()
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
async def async_receive(self) -> T:
|
||||
"""
|
||||
关闭通道
|
||||
异步接收数据,会挂起等待
|
||||
Returns:
|
||||
T: 数据
|
||||
"""
|
||||
self._closed = True
|
||||
self.sub_receive_conn.close()
|
||||
self.main_send_conn.close()
|
||||
self.sub_send_conn.close()
|
||||
self.main_receive_conn.close()
|
||||
loop = asyncio.get_running_loop()
|
||||
data = await loop.run_in_executor(None, self.receive)
|
||||
return data
|
||||
|
||||
def on_receive(self, filter_func: Optional[FILTER_FUNC] = None) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
|
||||
def on_receive(self, filter_func: Optional[FILTER_FUNC] = None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
|
||||
"""
|
||||
接收数据并执行函数
|
||||
Args:
|
||||
filter_func: 过滤函数,为None则不过滤
|
||||
filter_func ([`Optional`](https%3A//docs.python.org/3/library/typing.html#typing.Optional)[[`FILTER_FUNC`](#var-FILTER_FUNC)], optional): 过滤函数. Defaults to None.
|
||||
Returns:
|
||||
装饰器,装饰一个函数在接收到数据后执行
|
||||
Callable[[Callable[[T], Any]], Callable[[T], Any]]: 装饰器
|
||||
"""
|
||||
if (not self.is_sub_receive_loop_running) and not IS_MAIN_PROCESS:
|
||||
threading.Thread(target=self._start_sub_receive_loop).start()
|
||||
if not IS_MAIN_PROCESS:
|
||||
raise RuntimeError("on_receive 仅可用于主进程内")
|
||||
|
||||
if (not self.is_main_receive_loop_running) and IS_MAIN_PROCESS:
|
||||
threading.Thread(target=self._start_main_receive_loop).start()
|
||||
def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
|
||||
global _func_id
|
||||
|
||||
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
|
||||
async def wrapper(data: Any) -> Any:
|
||||
async def wrapper(data: T) -> Any:
|
||||
if filter_func is not None:
|
||||
if is_coroutine_callable(filter_func):
|
||||
if not await filter_func(data):
|
||||
if not (await filter_func(data)): # type: ignore
|
||||
return
|
||||
else:
|
||||
if not filter_func(data):
|
||||
return
|
||||
return await func(data)
|
||||
|
||||
function_id = str(uuid4())
|
||||
_callback_funcs[function_id] = wrapper
|
||||
if is_coroutine_callable(func):
|
||||
return await func(data)
|
||||
else:
|
||||
return func(data)
|
||||
|
||||
_callback_funcs[_func_id] = wrapper
|
||||
if IS_MAIN_PROCESS:
|
||||
self._on_main_receive_funcs.append(function_id)
|
||||
self._on_main_receive_func_ids.append(_func_id)
|
||||
else:
|
||||
self._on_sub_receive_funcs.append(function_id)
|
||||
self._on_sub_receive_func_ids.append(_func_id)
|
||||
_func_id += 1
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def _run_on_main_receive_funcs(self, data: Any):
|
||||
async def _run_on_receive_funcs(self, data: Any):
|
||||
"""
|
||||
运行接收函数
|
||||
Args:
|
||||
data: 数据
|
||||
"""
|
||||
for func_id in self._on_main_receive_funcs:
|
||||
func = _callback_funcs[func_id]
|
||||
run_coroutine(func(data))
|
||||
|
||||
def _run_on_sub_receive_funcs(self, data: Any):
|
||||
"""
|
||||
运行接收函数
|
||||
Args:
|
||||
data: 数据
|
||||
"""
|
||||
for func_id in self._on_sub_receive_funcs:
|
||||
func = _callback_funcs[func_id]
|
||||
run_coroutine(func(data))
|
||||
|
||||
def _start_main_receive_loop(self):
|
||||
"""
|
||||
开始接收数据
|
||||
"""
|
||||
self.is_main_receive_loop_running = True
|
||||
while not self._closed:
|
||||
data = self.main_receive_conn.recv()
|
||||
self._run_on_main_receive_funcs(data)
|
||||
|
||||
def _start_sub_receive_loop(self):
|
||||
"""
|
||||
开始接收数据
|
||||
"""
|
||||
self.is_sub_receive_loop_running = True
|
||||
while not self._closed:
|
||||
data = self.sub_receive_conn.recv()
|
||||
self._run_on_sub_receive_funcs(data)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self) -> Any:
|
||||
return self.receive()
|
||||
if IS_MAIN_PROCESS:
|
||||
[asyncio.create_task(_callback_funcs[func_id](data)) for func_id in self._on_main_receive_func_ids]
|
||||
else:
|
||||
[asyncio.create_task(_callback_funcs[func_id](data)) for func_id in self._on_sub_receive_func_ids]
|
||||
|
||||
|
||||
"""默认通道实例,可直接从模块导入使用"""
|
||||
chan = Channel("default")
|
||||
"""子进程可用的主动和被动通道"""
|
||||
active_channel: Channel = Channel(name="active_channel") # 主动通道
|
||||
passive_channel: Channel = Channel(name="passive_channel") # 被动通道
|
||||
publish_channel: Channel[tuple[str, dict[str, Any]]] = Channel(name="publish_channel") # 发布通道
|
||||
"""通道传递通道,主进程创建单例,子进程初始化时实例化"""
|
||||
channel_deliver_active_channel: Channel[Channel[Any]] # 主动通道传递通道
|
||||
channel_deliver_passive_channel: Channel[tuple[str, dict[str, Any]]] # 被动通道传递通道
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
channel_deliver_active_channel = Channel(name="channel_deliver_active_channel") # 主动通道传递通道
|
||||
channel_deliver_passive_channel = Channel(name="channel_deliver_passive_channel") # 被动通道传递通道
|
||||
|
||||
|
||||
def set_channel(name: str, channel: Channel):
|
||||
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "set_channel")
|
||||
def on_set_channel(data: tuple[str, dict[str, Any]]):
|
||||
name, channel = data[1]["name"], data[1]["channel_"]
|
||||
set_channel(name, channel)
|
||||
|
||||
|
||||
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "get_channel")
|
||||
def on_get_channel(data: tuple[str, dict[str, Any]]):
|
||||
name, recv_chan = data[1]["name"], data[1]["recv_chan"]
|
||||
recv_chan.send(get_channel(name))
|
||||
|
||||
|
||||
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "get_channels")
|
||||
def on_get_channels(data: tuple[str, dict[str, Any]]):
|
||||
recv_chan = data[1]["recv_chan"]
|
||||
recv_chan.send(get_channels())
|
||||
|
||||
|
||||
def set_channel(name: str, channel: "Channel"):
|
||||
"""
|
||||
设置通道实例
|
||||
Args:
|
||||
name: 通道名称
|
||||
channel: 通道实例
|
||||
name ([`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 通道名称
|
||||
channel ([`Channel`](#class-channel-generic-t)): 通道实例
|
||||
"""
|
||||
_channel[name] = channel
|
||||
if not isinstance(channel, Channel):
|
||||
raise TypeError(f"channel_ 必须为 Channel 实例,而非 {type(channel)}")
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
if name in _channel:
|
||||
raise ValueError(f"Channel {name} 已存在")
|
||||
_channel[name] = channel
|
||||
else:
|
||||
# 请求主进程设置通道
|
||||
channel_deliver_passive_channel.send(
|
||||
(
|
||||
"set_channel", {
|
||||
"name" : name,
|
||||
"channel_": channel,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def set_channels(channels: dict[str, Channel]):
|
||||
def set_channels(channels: dict[str, "Channel"]):
|
||||
"""
|
||||
设置通道实例
|
||||
Args:
|
||||
channels: 通道名称
|
||||
channels ([`dict`](https%3A//docs.python.org/3/library/stdtypes.html#dict)[[`str`](https%3A//docs.python.org/3/library/stdtypes.html#str), [`Channel`](#class-channel-generic-t)]): 通道实例
|
||||
"""
|
||||
for name, channel in channels.items():
|
||||
_channel[name] = channel
|
||||
set_channel(name, channel)
|
||||
|
||||
|
||||
def get_channel(name: str) -> Optional[Channel]:
|
||||
def get_channel(name: str) -> "Channel":
|
||||
"""
|
||||
获取通道实例
|
||||
Args:
|
||||
name: 通道名称
|
||||
name ([`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 通道名称
|
||||
Returns:
|
||||
[`Channel`](#class-channel-generic-t): 通道实例
|
||||
"""
|
||||
return _channel.get(name, None)
|
||||
if IS_MAIN_PROCESS:
|
||||
return _channel[name]
|
||||
|
||||
else:
|
||||
recv_chan = Channel[Channel[Any]]("recv_chan")
|
||||
channel_deliver_passive_channel.send(
|
||||
(
|
||||
"get_channel",
|
||||
{
|
||||
"name" : name,
|
||||
"recv_chan": recv_chan
|
||||
}
|
||||
)
|
||||
)
|
||||
return recv_chan.receive()
|
||||
|
||||
|
||||
def get_channels() -> dict[str, Channel]:
|
||||
def get_channels() -> dict[str, "Channel"]:
|
||||
"""
|
||||
获取通道实例
|
||||
获取通道实例们
|
||||
Returns:
|
||||
[`dict`](https%3A//docs.python.org/3/library/stdtypes.html#dict)[[`str`](https%3A//docs.python.org/3/library/stdtypes.html#str), [`Channel`](#class-channel-generic-t)]: 通道实例
|
||||
"""
|
||||
return _channel
|
||||
if IS_MAIN_PROCESS:
|
||||
return _channel
|
||||
else:
|
||||
recv_chan = Channel[dict[str, Channel[Any]]]("recv_chan")
|
||||
channel_deliver_passive_channel.send(
|
||||
(
|
||||
"get_channels",
|
||||
{
|
||||
"recv_chan": recv_chan
|
||||
}
|
||||
)
|
||||
)
|
||||
return recv_chan.receive()
|
||||
|
@ -1,12 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/7/26 下午10:47
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : event.py
|
||||
@Software: PyCharm
|
||||
本模块用于轻雪主进程和子进程之间的通信的事件类
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
|
26
liteyuki/comm/rpc.py
Normal file
26
liteyuki/comm/rpc.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
本模块用于实现RPC(基于IPC)通信
|
||||
"""
|
||||
|
||||
from typing import TypeAlias, Callable, Any
|
||||
|
||||
from liteyuki.comm.channel import Channel
|
||||
|
||||
ON_CALLING_FUNC: TypeAlias = Callable[[tuple, dict], Any]
|
||||
|
||||
|
||||
class RPC:
|
||||
"""
|
||||
RPC类
|
||||
"""
|
||||
|
||||
def __init__(self, on_calling: ON_CALLING_FUNC) -> None:
|
||||
self.on_calling = on_calling
|
||||
|
||||
def call(self, args: tuple, kwargs: dict) -> Any:
|
||||
"""
|
||||
调用
|
||||
"""
|
||||
# 获取self.calling函数名
|
||||
return self.on_calling(args, kwargs)
|
48
liteyuki/comm/socks_channel.py
Normal file
48
liteyuki/comm/socks_channel.py
Normal file
@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
基于socket的通道
|
||||
"""
|
||||
|
||||
|
||||
class SocksChannel:
|
||||
"""
|
||||
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
|
||||
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
"""
|
||||
初始化通道
|
||||
Args:
|
||||
name: 通道ID
|
||||
"""
|
||||
|
||||
self._name = name
|
||||
self._conn_send = None
|
||||
self._conn_recv = None
|
||||
self._closed = False
|
||||
|
||||
def send(self, data):
|
||||
"""
|
||||
发送数据
|
||||
Args:
|
||||
data: 数据
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def receive(self):
|
||||
"""
|
||||
接收数据
|
||||
Returns:
|
||||
data: 数据
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
关闭通道
|
||||
"""
|
||||
|
||||
pass
|
247
liteyuki/comm/storage.py
Normal file
247
liteyuki/comm/storage.py
Normal file
@ -0,0 +1,247 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
共享内存模块。类似于redis,但是更加轻量级并且线程安全
|
||||
"""
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from liteyuki.comm import channel
|
||||
from liteyuki.comm.channel import ASYNC_ON_RECEIVE_FUNC, Channel, ON_RECEIVE_FUNC
|
||||
from liteyuki.utils import (
|
||||
IS_MAIN_PROCESS,
|
||||
is_coroutine_callable,
|
||||
run_coroutine_in_thread,
|
||||
)
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
_locks = {}
|
||||
|
||||
_on_main_subscriber_receive_funcs: dict[str, list[ASYNC_ON_RECEIVE_FUNC]] = {} # type: ignore
|
||||
"""主进程订阅者接收函数"""
|
||||
_on_sub_subscriber_receive_funcs: dict[str, list[ASYNC_ON_RECEIVE_FUNC]] = {} # type: ignore
|
||||
"""子进程订阅者接收函数"""
|
||||
|
||||
|
||||
def _get_lock(key) -> threading.Lock:
|
||||
"""
|
||||
获取锁
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
if key not in _locks:
|
||||
_locks[key] = threading.Lock()
|
||||
return _locks[key]
|
||||
else:
|
||||
raise RuntimeError("无法在子进程中获取线程锁")
|
||||
|
||||
|
||||
class KeyValueStore:
|
||||
def __init__(self):
|
||||
self._store = {}
|
||||
self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](
|
||||
name="shared_memory-active"
|
||||
)
|
||||
self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](
|
||||
name="shared_memory-passive"
|
||||
)
|
||||
|
||||
self.publish_channel = Channel[tuple[str, Any]](name="shared_memory-publish")
|
||||
|
||||
self.is_main_receive_loop_running = False
|
||||
self.is_sub_receive_loop_running = False
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
设置键值对
|
||||
Args:
|
||||
key: 键
|
||||
value: 值
|
||||
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
lock = _get_lock(key)
|
||||
with lock:
|
||||
self._store[key] = value
|
||||
else:
|
||||
# 向主进程发送请求拿取
|
||||
self.passive_chan.send(("set", {"key": key, "value": value}))
|
||||
|
||||
def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
|
||||
"""
|
||||
获取键值对
|
||||
Args:
|
||||
key: 键
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
Any: 值
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
lock = _get_lock(key)
|
||||
with lock:
|
||||
return self._store.get(key, default)
|
||||
else:
|
||||
recv_chan = Channel[Optional[Any]]("recv_chan")
|
||||
self.passive_chan.send(
|
||||
("get", {"key": key, "default": default, "recv_chan": recv_chan})
|
||||
)
|
||||
return recv_chan.receive()
|
||||
|
||||
def delete(self, key: str, ignore_key_error: bool = True) -> None:
|
||||
"""
|
||||
删除键值对
|
||||
Args:
|
||||
key: 键
|
||||
ignore_key_error: 是否忽略键不存在的错误
|
||||
|
||||
Returns:
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
lock = _get_lock(key)
|
||||
with lock:
|
||||
if key in self._store:
|
||||
try:
|
||||
del self._store[key]
|
||||
del _locks[key]
|
||||
except KeyError as e:
|
||||
if not ignore_key_error:
|
||||
raise e
|
||||
else:
|
||||
# 向主进程发送请求删除
|
||||
self.passive_chan.send(("delete", {"key": key}))
|
||||
|
||||
def get_all(self) -> dict[str, Any]:
|
||||
"""
|
||||
获取所有键值对
|
||||
Returns:
|
||||
dict[str, Any]: 键值对
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
return self._store
|
||||
else:
|
||||
recv_chan = Channel[dict[str, Any]]("recv_chan")
|
||||
self.passive_chan.send(("get_all", {"recv_chan": recv_chan}))
|
||||
return recv_chan.receive()
|
||||
|
||||
def publish(self, channel_: str, data: Any) -> None:
|
||||
"""
|
||||
发布消息
|
||||
Args:
|
||||
channel_: 频道
|
||||
data: 数据
|
||||
|
||||
Returns:
|
||||
"""
|
||||
self.active_chan.send(("publish", {"channel": channel_, "data": data}))
|
||||
|
||||
def on_subscriber_receive(
|
||||
self, channel_: str
|
||||
) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
|
||||
"""
|
||||
订阅者接收消息时的回调
|
||||
Args:
|
||||
channel_: 频道
|
||||
|
||||
Returns:
|
||||
装饰器
|
||||
"""
|
||||
if not IS_MAIN_PROCESS:
|
||||
raise RuntimeError("无法订阅一子线程消息")
|
||||
|
||||
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
|
||||
async def wrapper(data: Any):
|
||||
if is_coroutine_callable(func):
|
||||
await func(data)
|
||||
else:
|
||||
func(data)
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
if channel_ not in _on_main_subscriber_receive_funcs:
|
||||
_on_main_subscriber_receive_funcs[channel_] = []
|
||||
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
|
||||
else:
|
||||
if channel_ not in _on_sub_subscriber_receive_funcs:
|
||||
_on_sub_subscriber_receive_funcs[channel_] = []
|
||||
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@staticmethod
|
||||
async def run_subscriber_receive_funcs(channel_: str, data: Any):
|
||||
"""
|
||||
运行订阅者接收函数
|
||||
Args:
|
||||
channel_: 频道
|
||||
data: 数据
|
||||
"""
|
||||
[
|
||||
asyncio.create_task(func(data))
|
||||
for func in _on_main_subscriber_receive_funcs[channel_]
|
||||
]
|
||||
|
||||
async def start_receive_loop(self):
|
||||
"""
|
||||
启动发布订阅接收器循环,在主进程中运行,若有子进程订阅则推送给子进程
|
||||
"""
|
||||
|
||||
if not IS_MAIN_PROCESS:
|
||||
raise RuntimeError("无法在子进程中启用订阅接收器循环")
|
||||
while True:
|
||||
data = await self.active_chan.async_receive()
|
||||
if data[0] == "publish":
|
||||
# 运行主进程订阅函数
|
||||
await self.run_subscriber_receive_funcs(
|
||||
data[1]["channel"], data[1]["data"]
|
||||
)
|
||||
# 推送给子进程
|
||||
self.publish_channel.send(data)
|
||||
|
||||
|
||||
class GlobalKeyValueStore:
|
||||
_instance = None
|
||||
_lock = threading.Lock()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
with cls._lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = KeyValueStore()
|
||||
return cls._instance
|
||||
|
||||
|
||||
shared_memory: KeyValueStore = GlobalKeyValueStore.get_instance() # 共享内存对象
|
||||
|
||||
# 全局单例访问点
|
||||
if IS_MAIN_PROCESS:
|
||||
|
||||
@shared_memory.passive_chan.on_receive(lambda d: d[0] == "get")
|
||||
def on_get(data: tuple[str, dict[str, Any]]):
|
||||
key = data[1]["key"]
|
||||
default = data[1]["default"]
|
||||
recv_chan = data[1]["recv_chan"]
|
||||
recv_chan.send(shared_memory.get(key, default))
|
||||
|
||||
@shared_memory.passive_chan.on_receive(lambda d: d[0] == "set")
|
||||
def on_set(data: tuple[str, dict[str, Any]]):
|
||||
key = data[1]["key"]
|
||||
value = data[1]["value"]
|
||||
shared_memory.set(key, value)
|
||||
|
||||
@shared_memory.passive_chan.on_receive(lambda d: d[0] == "delete")
|
||||
def on_delete(data: tuple[str, dict[str, Any]]):
|
||||
key = data[1]["key"]
|
||||
shared_memory.delete(key)
|
||||
|
||||
@shared_memory.passive_chan.on_receive(lambda d: d[0] == "get_all")
|
||||
def on_get_all(data: tuple[str, dict[str, Any]]):
|
||||
recv_chan = data[1]["recv_chan"]
|
||||
recv_chan.send(shared_memory.get_all())
|
||||
|
||||
|
||||
_ref_count = 0 # import 引用计数, 防止获取空指针
|
||||
if not IS_MAIN_PROCESS:
|
||||
if (shared_memory is None) and _ref_count > 1:
|
||||
raise RuntimeError("共享内存未初始化")
|
||||
_ref_count += 1
|
@ -1,49 +1,132 @@
|
||||
"""
|
||||
该模块用于常用配置文件的加载
|
||||
多配置文件编写原则:
|
||||
1. 尽量不要冲突: 一个键不要多次出现
|
||||
2. 分工明确: 每个配置文件给一个或一类服务提供配置
|
||||
3. 扁平化编写: 配置文件尽量扁平化,不要出现过多的嵌套
|
||||
4. 注意冲突时的优先级: 项目目录下的配置文件优先级高于config目录下的配置文件
|
||||
5. 请不要将需要动态加载的内容写入配置文件,你应该使用其他储存方式
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
import nonebot
|
||||
import json
|
||||
import copy
|
||||
import toml
|
||||
import yaml
|
||||
from pydantic import BaseModel
|
||||
|
||||
from typing import Any
|
||||
|
||||
from liteyuki.log import logger
|
||||
|
||||
_SUPPORTED_CONFIG_FORMATS = (".yaml", ".yml", ".json", ".toml")
|
||||
|
||||
|
||||
config = {} # 主进程全局配置,确保加载后读取
|
||||
def flat_config(config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
扁平化配置文件
|
||||
|
||||
{a:{b:{c:1}}} -> {"a.b.c": 1}
|
||||
Args:
|
||||
config: 配置项目
|
||||
|
||||
Returns:
|
||||
扁平化后的配置文件,但也包含原有的键值对
|
||||
"""
|
||||
new_config = copy.deepcopy(config)
|
||||
for key, value in config.items():
|
||||
if isinstance(value, dict):
|
||||
for k, v in flat_config(value).items():
|
||||
new_config[f"{key}.{k}"] = v
|
||||
return new_config
|
||||
|
||||
|
||||
class SatoriNodeConfig(BaseModel):
|
||||
host: str = ""
|
||||
port: str = "5500"
|
||||
path: str = ""
|
||||
token: str = ""
|
||||
def load_from_yaml(file_: str) -> dict[str, Any]:
|
||||
"""
|
||||
Load config from yaml file
|
||||
|
||||
"""
|
||||
logger.debug("正在从 {} 中加载 YAML 配置".format(file_))
|
||||
config = yaml.safe_load(open(file_, "r", encoding="utf-8"))
|
||||
return flat_config(config if config is not None else {})
|
||||
|
||||
|
||||
class SatoriConfig(BaseModel):
|
||||
comment: str = "此皆正处于开发之中,切勿在生产环境中启用。"
|
||||
enable: bool = False
|
||||
hosts: List[SatoriNodeConfig] = [SatoriNodeConfig()]
|
||||
def load_from_json(file_: str) -> dict[str, Any]:
|
||||
"""
|
||||
Load config from json file
|
||||
"""
|
||||
logger.debug("正在从 {} 中加载 JSON 配置".format(file_))
|
||||
config = json.load(open(file_, "r", encoding="utf-8"))
|
||||
return flat_config(config if config is not None else {})
|
||||
|
||||
|
||||
class BasicConfig(BaseModel):
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 20247
|
||||
superusers: list[str] = []
|
||||
command_start: list[str] = ["/", ""]
|
||||
nickname: list[str] = [f"灵温"]
|
||||
satori: SatoriConfig = SatoriConfig()
|
||||
data_path: str = "data/liteyuki"
|
||||
def load_from_toml(file_: str) -> dict[str, Any]:
|
||||
"""
|
||||
Load config from toml file
|
||||
"""
|
||||
logger.debug("正在从 {} 中加载 TOML 配置".format(file_))
|
||||
config = toml.load(open(file_, "r", encoding="utf-8"))
|
||||
return flat_config(config if config is not None else {})
|
||||
|
||||
|
||||
def load_from_yaml(file: str) -> dict:
|
||||
global config
|
||||
nonebot.logger.debug("Loading config from %s" % file)
|
||||
if not os.path.exists(file):
|
||||
nonebot.logger.warning(f"未找到配置文件 {file} ,已创建默认配置,请修改后重启。")
|
||||
with open(file, "w", encoding="utf-8") as f:
|
||||
yaml.dump(BasicConfig().dict(), f, default_flow_style=False)
|
||||
def load_from_files(*files: str, no_warning: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
从指定文件加载配置项,会自动识别文件格式
|
||||
默认执行扁平化选项
|
||||
"""
|
||||
config = {}
|
||||
for file in files:
|
||||
if os.path.exists(file):
|
||||
if file.endswith((".yaml", "yml")):
|
||||
config.update(load_from_yaml(file))
|
||||
elif file.endswith(".json"):
|
||||
config.update(load_from_json(file))
|
||||
elif file.endswith(".toml"):
|
||||
config.update(load_from_toml(file))
|
||||
else:
|
||||
if not no_warning:
|
||||
logger.warning(f"不支持配置文件 {file} 的类型")
|
||||
else:
|
||||
if not no_warning:
|
||||
logger.warning(f"配置文件 {file} 未寻得")
|
||||
return config
|
||||
|
||||
with open(file, "r", encoding="utf-8") as f:
|
||||
conf = yaml.load(f, Loader=yaml.FullLoader)
|
||||
config = conf
|
||||
if conf is None:
|
||||
nonebot.logger.warning(f"配置文件 {file} 为空,已创建默认配置,请修改后重启。")
|
||||
conf = BasicConfig().dict()
|
||||
return conf
|
||||
|
||||
def load_configs_from_dirs(
|
||||
*directories: str, no_waring: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
从目录下加载配置文件,不递归
|
||||
按照读取文件的优先级反向覆盖
|
||||
默认执行扁平化选项
|
||||
"""
|
||||
config = {}
|
||||
for directory in directories:
|
||||
if not os.path.exists(directory):
|
||||
if not no_waring:
|
||||
logger.warning(f"目录 {directory} 未寻得")
|
||||
continue
|
||||
for file in os.listdir(directory):
|
||||
if file.endswith(_SUPPORTED_CONFIG_FORMATS):
|
||||
config.update(
|
||||
load_from_files(os.path.join(directory, file), no_warning=no_waring)
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def load_config_in_default(no_waring: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
从一个标准的轻雪项目加载配置文件
|
||||
项目目录下的config.*和config目录下的所有配置文件
|
||||
项目目录下的配置文件优先
|
||||
"""
|
||||
config = load_configs_from_dirs("config", no_waring=no_waring)
|
||||
config.update(
|
||||
load_from_files(
|
||||
"config.yaml",
|
||||
"config.toml",
|
||||
"config.json",
|
||||
"config.yml",
|
||||
no_warning=no_waring,
|
||||
)
|
||||
)
|
||||
return config
|
||||
|
@ -1,11 +1,2 @@
|
||||
import multiprocessing
|
||||
|
||||
from .spawn_process import *
|
||||
from .manager import *
|
||||
|
||||
__all__ = [
|
||||
"IS_MAIN_PROCESS"
|
||||
]
|
||||
|
||||
IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
|
||||
|
||||
|
@ -9,92 +9,175 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import asyncio
|
||||
import multiprocessing
|
||||
import threading
|
||||
from multiprocessing import Process
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any, Callable, TYPE_CHECKING, TypeAlias
|
||||
|
||||
from liteyuki.comm import Channel, get_channel, set_channels
|
||||
from liteyuki.log import logger
|
||||
from liteyuki.utils import IS_MAIN_PROCESS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from liteyuki.bot import LiteyukiBot
|
||||
from liteyuki.bot.lifespan import Lifespan
|
||||
from liteyuki.comm.storage import KeyValueStore
|
||||
|
||||
from liteyuki.comm import Channel
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
from liteyuki.comm.channel import get_channel, publish_channel, get_channels
|
||||
from liteyuki.comm.storage import shared_memory
|
||||
from liteyuki.comm.channel import (
|
||||
channel_deliver_active_channel,
|
||||
channel_deliver_passive_channel,
|
||||
)
|
||||
else:
|
||||
from liteyuki.comm import channel
|
||||
from liteyuki.comm import storage
|
||||
|
||||
TARGET_FUNC: TypeAlias = Callable[..., Any]
|
||||
TIMEOUT = 10
|
||||
|
||||
__all__ = ["ProcessManager"]
|
||||
multiprocessing.set_start_method("spawn", force=True)
|
||||
|
||||
|
||||
class ChannelDeliver:
|
||||
def __init__(
|
||||
self,
|
||||
active: Channel[Any],
|
||||
passive: Channel[Any],
|
||||
channel_deliver_active: Channel[Channel[Any]],
|
||||
channel_deliver_passive: Channel[tuple[str, dict]],
|
||||
publish: Channel[tuple[str, Any]],
|
||||
):
|
||||
self.active = active
|
||||
self.passive = passive
|
||||
self.channel_deliver_active = channel_deliver_active
|
||||
self.channel_deliver_passive = channel_deliver_passive
|
||||
self.publish = publish
|
||||
|
||||
|
||||
# 函数处理一些跨进程通道的
|
||||
def _delivery_channel_wrapper(
|
||||
func: TARGET_FUNC, cd: ChannelDeliver, sm: "KeyValueStore", *args, **kwargs
|
||||
):
|
||||
"""
|
||||
子进程入口函数
|
||||
处理一些操作
|
||||
"""
|
||||
# 给子进程设置通道
|
||||
if IS_MAIN_PROCESS:
|
||||
raise RuntimeError("函数仅可在子进程中被调用")
|
||||
|
||||
channel.active_channel = cd.active # 子进程主动通道
|
||||
channel.passive_channel = cd.passive # 子进程被动通道
|
||||
channel.channel_deliver_active_channel = (
|
||||
cd.channel_deliver_active
|
||||
) # 子进程通道传递主动通道
|
||||
channel.channel_deliver_passive_channel = (
|
||||
cd.channel_deliver_passive
|
||||
) # 子进程通道传递被动通道
|
||||
channel.publish_channel = cd.publish # 子进程发布通道
|
||||
|
||||
# 给子进程创建共享内存实例
|
||||
|
||||
storage.shared_memory = sm
|
||||
|
||||
func(*args, **kwargs)
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""
|
||||
在主进程中被调用
|
||||
进程管理器
|
||||
"""
|
||||
|
||||
def __init__(self, bot: "LiteyukiBot"):
|
||||
self.bot = bot
|
||||
self.targets: dict[str, tuple[callable, tuple, dict]] = {}
|
||||
def __init__(self, lifespan: "Lifespan"):
|
||||
self.lifespan = lifespan
|
||||
self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
|
||||
self.processes: dict[str, Process] = {}
|
||||
|
||||
set_channels(
|
||||
{
|
||||
"nonebot-active": Channel(_id="nonebot-active"),
|
||||
"melobot-active": Channel(_id="melobot-active"),
|
||||
"nonebot-passive": Channel(_id="nonebot-passive"),
|
||||
"melobot-passive": Channel(_id="melobot-passive"),
|
||||
}
|
||||
)
|
||||
|
||||
def start(self, name: str, delay: int = 0):
|
||||
def _run_process(self, name: str):
|
||||
"""
|
||||
开启后自动监控进程,并添加到进程字典中
|
||||
开启后自动监控进程,并添加到进程字典中,会阻塞,请创建task
|
||||
Args:
|
||||
name:
|
||||
delay:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if name not in self.targets:
|
||||
raise KeyError(f"未有 Process {name} 之存在")
|
||||
raise KeyError(f"Process {name} 未寻得")
|
||||
|
||||
def _start():
|
||||
should_exit = False
|
||||
while not should_exit:
|
||||
chan_active = get_channel(f"{name}-active")
|
||||
chan_passive = get_channel(f"{name}-passive")
|
||||
process = Process(
|
||||
target=self.targets[name][0],
|
||||
args=(chan_active, chan_passive, *self.targets[name][1]),
|
||||
kwargs=self.targets[name][2],
|
||||
)
|
||||
self.processes[name] = process
|
||||
process.start()
|
||||
while not should_exit:
|
||||
# 0退出 1重启
|
||||
data = chan_active.receive()
|
||||
if data == 1:
|
||||
logger.info(f"重启 {name} 进程")
|
||||
asyncio.run(self.bot.lifespan.before_shutdown())
|
||||
asyncio.run(self.bot.lifespan.before_restart())
|
||||
self.terminate(name)
|
||||
break
|
||||
chan_active = get_channel(f"{name}-active")
|
||||
|
||||
elif data == 0:
|
||||
logger.info(f"关停 {name} 进程")
|
||||
asyncio.run(self.bot.lifespan.before_shutdown())
|
||||
should_exit = True
|
||||
self.terminate(name)
|
||||
else:
|
||||
logger.warning("数据未知,省略:{}".format(data))
|
||||
def _start_process():
|
||||
process = Process(
|
||||
target=self.targets[name][0],
|
||||
args=self.targets[name][1],
|
||||
kwargs=self.targets[name][2],
|
||||
daemon=True,
|
||||
)
|
||||
self.processes[name] = process
|
||||
process.start()
|
||||
|
||||
if delay:
|
||||
threading.Timer(delay, _start).start()
|
||||
else:
|
||||
threading.Thread(target=_start).start()
|
||||
# 启动进程并监听信号
|
||||
_start_process()
|
||||
while True:
|
||||
data = chan_active.receive()
|
||||
if data == 0:
|
||||
# 停止
|
||||
logger.info(f"正在关停 Process {name}")
|
||||
self.terminate(name)
|
||||
break
|
||||
elif data == 1:
|
||||
# 重启
|
||||
logger.info(f"正在重启 Process {name}")
|
||||
self.terminate(name)
|
||||
_start_process()
|
||||
continue
|
||||
else:
|
||||
logger.warning("接收到未知信号数据 {} ,已忽略".format(data))
|
||||
|
||||
def add_target(self, name: str, target, *args, **kwargs):
|
||||
self.targets[name] = (target, args, kwargs)
|
||||
def start_all(self):
|
||||
"""
|
||||
对外启动方法,启动所有进程,创建asyncio task
|
||||
"""
|
||||
# [asyncio.create_task(self._run_process(name)) for name in self.targets]
|
||||
|
||||
def join(self):
|
||||
for name in self.targets:
|
||||
logger.debug(f"正在启动 Process {name}")
|
||||
threading.Thread(
|
||||
target=self._run_process, args=(name,), daemon=True
|
||||
).start()
|
||||
|
||||
def add_target(self, name: str, target: TARGET_FUNC, args: tuple = (), kwargs=None):
|
||||
"""
|
||||
添加进程
|
||||
Args:
|
||||
name: 进程名,用于获取和唯一标识
|
||||
target: 进程函数
|
||||
args: 进程函数参数
|
||||
kwargs: 进程函数关键字参数,通常会默认传入chan_active和chan_passive
|
||||
"""
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
chan_active: Channel = Channel(name=f"{name}-active")
|
||||
chan_passive: Channel = Channel(name=f"{name}-passive")
|
||||
|
||||
channel_deliver = ChannelDeliver(
|
||||
active=chan_active,
|
||||
passive=chan_passive,
|
||||
channel_deliver_active=channel_deliver_active_channel,
|
||||
channel_deliver_passive=channel_deliver_passive_channel,
|
||||
publish=publish_channel,
|
||||
)
|
||||
|
||||
self.targets[name] = (
|
||||
_delivery_channel_wrapper,
|
||||
(target, channel_deliver, shared_memory, *args),
|
||||
kwargs,
|
||||
)
|
||||
# 主进程通道
|
||||
|
||||
def join_all(self):
|
||||
for name, process in self.targets:
|
||||
process.join()
|
||||
|
||||
@ -107,14 +190,29 @@ class ProcessManager:
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if name not in self.targets:
|
||||
raise logger.warning(f"未有 Process {name} 之存在")
|
||||
if name not in self.processes:
|
||||
logger.warning(f"Process {name} 未寻得")
|
||||
return
|
||||
process = self.processes[name]
|
||||
process.terminate()
|
||||
process.join(TIMEOUT)
|
||||
if process.is_alive():
|
||||
process.kill()
|
||||
logger.success(f"Process {name} 已迫令终止")
|
||||
|
||||
def terminate_all(self):
|
||||
for name in self.targets:
|
||||
self.terminate(name)
|
||||
|
||||
def is_process_alive(self, name: str) -> bool:
|
||||
"""
|
||||
检查进程是否存活
|
||||
Args:
|
||||
name:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if name not in self.targets:
|
||||
logger.warning(f"Process {name} 未寻得")
|
||||
return self.processes[name].is_alive()
|
||||
|
@ -1,56 +0,0 @@
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
import nonebot
|
||||
|
||||
from liteyuki.core.nb import adapter_manager, driver_manager
|
||||
from liteyuki.comm.channel import set_channel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from liteyuki.comm.channel import Channel
|
||||
|
||||
timeout_limit: int = 20
|
||||
|
||||
"""导出对象,用于主进程与nonebot通信"""
|
||||
_channels = {}
|
||||
|
||||
|
||||
def nb_run(chan_active: "Channel", chan_passive: "Channel", *args, **kwargs):
|
||||
"""
|
||||
初始化NoneBot并运行在子进程
|
||||
Args:
|
||||
|
||||
chan_active:
|
||||
chan_passive:
|
||||
**kwargs:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
set_channel("nonebot-active", chan_active)
|
||||
set_channel("nonebot-passive", chan_passive)
|
||||
nonebot.init(**kwargs)
|
||||
driver_manager.init(config=kwargs)
|
||||
adapter_manager.init(kwargs)
|
||||
adapter_manager.register()
|
||||
nonebot.load_plugin("src.liteyuki_main")
|
||||
nonebot.run()
|
||||
|
||||
|
||||
def mb_run(chan_active: "Channel", chan_passive: "Channel", *args, **kwargs):
|
||||
"""
|
||||
初始化MeloBot并运行在子进程
|
||||
Args:
|
||||
chan_active
|
||||
chan_passive
|
||||
*args:
|
||||
**kwargs:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
set_channel("melobot-active", chan_active)
|
||||
set_channel("melobot-passive", chan_passive)
|
||||
|
||||
# bot = MeloBot(__name__)
|
||||
# bot.init(AbstractConnector(cd_time=0))
|
||||
# bot.run()
|
4
liteyuki/dev/__init__.py
Normal file
4
liteyuki/dev/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
该模块用于存放一些开发工具
|
||||
"""
|
90
liteyuki/dev/observer.py
Normal file
90
liteyuki/dev/observer.py
Normal file
@ -0,0 +1,90 @@
|
||||
"""
|
||||
此模块用于注册观察者函数,使用watchdog监控文件变化并重启bot
|
||||
启用该模块需要在配置文件中设置`dev_mode`为True
|
||||
"""
|
||||
import time
|
||||
from typing import Callable, TypeAlias
|
||||
|
||||
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from liteyuki import get_bot, get_config_with_compat, logger
|
||||
|
||||
liteyuki_bot = get_bot()
|
||||
|
||||
CALLBACK_FUNC: TypeAlias = Callable[[FileSystemEvent], None] # 位置1为FileSystemEvent
|
||||
FILTER_FUNC: TypeAlias = Callable[[FileSystemEvent], bool] # 位置1为FileSystemEvent
|
||||
observer = Observer()
|
||||
|
||||
|
||||
def debounce(wait):
|
||||
"""
|
||||
防抖函数
|
||||
"""
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
nonlocal last_call_time
|
||||
current_time = time.time()
|
||||
if (current_time - last_call_time) > wait:
|
||||
last_call_time = current_time
|
||||
return func(*args, **kwargs)
|
||||
|
||||
last_call_time = None
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
if get_config_with_compat("liteyuki.reload", ("dev_mode",), False):
|
||||
logger.debug("Liteyuki Reload 已启用,正在监视文件更新")
|
||||
observer.start()
|
||||
|
||||
|
||||
class CodeModifiedHandler(FileSystemEventHandler):
|
||||
"""
|
||||
Handler for code file changes
|
||||
"""
|
||||
|
||||
@debounce(1)
|
||||
def on_modified(self, event):
|
||||
raise NotImplementedError("on_modified 函数在继承后必须实现")
|
||||
|
||||
def on_created(self, event):
|
||||
self.on_modified(event)
|
||||
|
||||
def on_deleted(self, event):
|
||||
self.on_modified(event)
|
||||
|
||||
def on_moved(self, event):
|
||||
self.on_modified(event)
|
||||
|
||||
def on_any_event(self, event):
|
||||
self.on_modified(event)
|
||||
|
||||
|
||||
def on_file_system_event(directories: tuple[str], recursive: bool = True, event_filter: FILTER_FUNC = None) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]:
|
||||
"""
|
||||
注册文件系统变化监听器
|
||||
Args:
|
||||
directories: 监听目录们
|
||||
recursive: 是否递归监听子目录
|
||||
event_filter: 事件过滤器, 返回True则执行回调函数
|
||||
Returns:
|
||||
装饰器,装饰一个函数在接收到数据后执行
|
||||
"""
|
||||
|
||||
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
|
||||
def wrapper(event: FileSystemEvent):
|
||||
|
||||
if event_filter is not None and not event_filter(event):
|
||||
return
|
||||
func(event)
|
||||
|
||||
code_modified_handler = CodeModifiedHandler()
|
||||
code_modified_handler.on_modified = wrapper
|
||||
for directory in directories:
|
||||
observer.schedule(code_modified_handler, directory, recursive=recursive)
|
||||
|
||||
return func
|
||||
|
||||
return decorator
|
28
liteyuki/dev/plugin.py
Normal file
28
liteyuki/dev/plugin.py
Normal file
@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/18 上午5:04
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : plugin.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from liteyuki.bot import LiteyukiBot
|
||||
from liteyuki.config import load_config_in_default
|
||||
|
||||
|
||||
def run_plugins(*module_path: str | Path):
|
||||
"""
|
||||
运行插件,无需手动初始化bot
|
||||
Args:
|
||||
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
|
||||
"""
|
||||
cfg = load_config_in_default()
|
||||
plugins = cfg.get("liteyuki.plugins", [])
|
||||
plugins.extend(module_path)
|
||||
cfg["liteyuki.plugins"] = plugins
|
||||
bot = LiteyukiBot(**cfg)
|
||||
bot.run()
|
@ -9,22 +9,10 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import sys
|
||||
|
||||
import loguru
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
logger = loguru.logger
|
||||
if TYPE_CHECKING:
|
||||
# avoid sphinx autodoc resolve annotation failed
|
||||
# because loguru module do not have `Logger` class actually
|
||||
from loguru import Record
|
||||
|
||||
|
||||
def default_filter(record: "Record"):
|
||||
"""默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。"""
|
||||
log_level = record["extra"].get("nonebot_log_level", "INFO")
|
||||
levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level
|
||||
return record["level"].no >= levelno
|
||||
|
||||
|
||||
# DEBUG日志格式
|
||||
debug_format: str = (
|
||||
@ -50,35 +38,26 @@ def get_format(level: str) -> str:
|
||||
return default_format
|
||||
|
||||
|
||||
logger = loguru.logger.bind()
|
||||
|
||||
|
||||
def init_log(config: dict):
|
||||
"""
|
||||
在语言加载完成后执行
|
||||
Returns:
|
||||
|
||||
"""
|
||||
global logger
|
||||
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level=0,
|
||||
diagnose=False,
|
||||
filter=default_filter,
|
||||
format=get_format(config.get("log_level", "INFO")),
|
||||
)
|
||||
show_icon = config.get("log_icon", True)
|
||||
logger.level("DEBUG", color="<blue>", icon=f"{'🐛' if show_icon else ''}试")
|
||||
logger.level("INFO", color="<normal>", icon=f"{'ℹ️' if show_icon else ''}讯")
|
||||
logger.level("SUCCESS", color="<green>", icon=f"{'✅' if show_icon else ''}警")
|
||||
logger.level("WARNING", color="<yellow>", icon=f"{'⚠️' if show_icon else ''}误")
|
||||
logger.level("ERROR", color="<red>", icon=f"{'⭕' if show_icon else ''}成")
|
||||
|
||||
# debug = lang.get("log.debug", default="==DEBUG")
|
||||
# info = lang.get("log.info", default="===INFO")
|
||||
# success = lang.get("log.success", default="SUCCESS")
|
||||
# warning = lang.get("log.warning", default="WARNING")
|
||||
# error = lang.get("log.error", default="==ERROR")
|
||||
#
|
||||
# logger.level("DEBUG", color="<blue>", icon=f"{'🐛' if show_icon else ''}{debug}")
|
||||
# logger.level("INFO", color="<normal>", icon=f"{'ℹ️' if show_icon else ''}{info}")
|
||||
# logger.level("SUCCESS", color="<green>", icon=f"{'✅' if show_icon else ''}{success}")
|
||||
# logger.level("WARNING", color="<yellow>", icon=f"{'⚠️' if show_icon else ''}{warning}")
|
||||
# logger.level("ERROR", color="<red>", icon=f"{'⭕' if show_icon else ''}{error}")
|
||||
|
||||
init_log(config={})
|
||||
|
10
liteyuki/message/__init__.py
Normal file
10
liteyuki/message/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:44
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : __init__.py.py
|
||||
@Software: PyCharm
|
||||
"""
|
88
liteyuki/message/event.py
Normal file
88
liteyuki/message/event.py
Normal file
@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:47
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : event.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from typing import Any, Optional
|
||||
|
||||
from liteyuki import Channel
|
||||
from liteyuki.comm.storage import shared_memory
|
||||
|
||||
|
||||
class MessageEvent:
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
bot_id: str,
|
||||
message: list[dict[str, Any]] | str,
|
||||
message_type: str,
|
||||
raw_message: str,
|
||||
session_id: str,
|
||||
user_id: str,
|
||||
session_type: str,
|
||||
receive_channel: Optional[Channel["MessageEvent"]] = None,
|
||||
data: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
轻雪抽象消息事件
|
||||
Args:
|
||||
|
||||
bot_id: 机器人ID
|
||||
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
|
||||
raw_message: 原始消息(通常为纯文本的格式)
|
||||
message_type: 消息类型(private, group, other)
|
||||
|
||||
session_id: 会话ID(私聊通常为用户ID,群聊通常为群ID)
|
||||
session_type: 会话类型(private, group)
|
||||
receive_channel: 接收频道(用于回复消息)
|
||||
|
||||
data: 附加数据
|
||||
"""
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
self.message_type = message_type
|
||||
self.data = data
|
||||
self.bot_id = bot_id
|
||||
|
||||
self.message = message
|
||||
self.raw_message = raw_message
|
||||
|
||||
self.session_id = session_id
|
||||
self.session_type = session_type
|
||||
self.user_id = user_id
|
||||
|
||||
self.receive_channel = receive_channel
|
||||
|
||||
def __str__(self):
|
||||
return (f"Event(message_type={self.message_type}, data={self.data}, bot_id={self.bot_id}, "
|
||||
f"session_id={self.session_id}, session_type={self.session_type})")
|
||||
|
||||
def reply(self, message: str | dict[str, Any]):
|
||||
"""
|
||||
回复消息
|
||||
Args:
|
||||
message:
|
||||
Returns:
|
||||
"""
|
||||
reply_event = MessageEvent(
|
||||
message_type=self.session_type,
|
||||
message=message,
|
||||
raw_message="",
|
||||
data={
|
||||
"message": message
|
||||
},
|
||||
bot_id=self.bot_id,
|
||||
session_id=self.session_id,
|
||||
user_id=self.user_id,
|
||||
session_type=self.session_type,
|
||||
receive_channel=None
|
||||
)
|
||||
# shared_memory.publish(self.receive_channel, reply_event)
|
||||
if self.receive_channel:
|
||||
self.receive_channel.send(reply_event)
|
63
liteyuki/message/matcher.py
Normal file
63
liteyuki/message/matcher.py
Normal file
@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:51
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : matcher.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import traceback
|
||||
from typing import Any, TypeAlias, Callable, Coroutine
|
||||
|
||||
from liteyuki.message.event import MessageEvent
|
||||
from liteyuki.message.rule import Rule
|
||||
|
||||
EventHandler: TypeAlias = Callable[[MessageEvent], Coroutine[None, None, Any]]
|
||||
|
||||
|
||||
class Matcher:
|
||||
def __init__(self, rule: Rule, priority: int, block: bool):
|
||||
"""
|
||||
匹配器
|
||||
Args:
|
||||
rule: 规则
|
||||
priority: 优先级 >= 0
|
||||
block: 是否阻断后续优先级更低的匹配器
|
||||
"""
|
||||
self.rule = rule
|
||||
self.priority = priority
|
||||
self.block = block
|
||||
self.handlers: list[EventHandler] = []
|
||||
|
||||
def __str__(self):
|
||||
return f"Matcher(rule={self.rule}, priority={self.priority}, block={self.block})"
|
||||
|
||||
def handle(self) -> Callable[[EventHandler], EventHandler]:
|
||||
"""
|
||||
添加处理函数,装饰器
|
||||
Returns:
|
||||
装饰器 handler
|
||||
"""
|
||||
def decorator(handler: EventHandler) -> EventHandler:
|
||||
self.handlers.append(handler)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
async def run(self, event: MessageEvent) -> None:
|
||||
"""
|
||||
运行处理函数
|
||||
Args:
|
||||
event:
|
||||
Returns:
|
||||
"""
|
||||
if not await self.rule(event):
|
||||
return
|
||||
|
||||
for handler in self.handlers:
|
||||
try:
|
||||
await handler(event)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
62
liteyuki/message/on.py
Normal file
62
liteyuki/message/on.py
Normal file
@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:52
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : on.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
|
||||
from queue import Queue
|
||||
|
||||
from liteyuki.comm.storage import shared_memory
|
||||
from liteyuki.log import logger
|
||||
from liteyuki.message.event import MessageEvent
|
||||
from liteyuki.message.matcher import Matcher
|
||||
from liteyuki.message.rule import Rule, empty_rule
|
||||
|
||||
_matcher_list: list[Matcher] = []
|
||||
_queue: Queue = Queue()
|
||||
|
||||
|
||||
@shared_memory.on_subscriber_receive("event_to_liteyuki")
|
||||
async def _(event: MessageEvent):
|
||||
print("AA")
|
||||
current_priority = -1
|
||||
for i, matcher in enumerate(_matcher_list):
|
||||
logger.info(f"为 Event {event} 运行 Matcher {matcher}")
|
||||
await matcher.run(event)
|
||||
# 同优先级不阻断,不同优先级阻断
|
||||
if current_priority != matcher.priority:
|
||||
current_priority = matcher.priority
|
||||
if matcher.block:
|
||||
break
|
||||
else:
|
||||
logger.info(f"无 Matcher 适配于 Event {event}")
|
||||
print("BB")
|
||||
|
||||
|
||||
def add_matcher(matcher: Matcher):
|
||||
for i, m in enumerate(_matcher_list):
|
||||
if m.priority < matcher.priority:
|
||||
_matcher_list.insert(i, matcher)
|
||||
break
|
||||
else:
|
||||
_matcher_list.append(matcher)
|
||||
|
||||
|
||||
def on_message(rule: Rule = empty_rule, priority: int = 0, block: bool = False) -> Matcher:
|
||||
matcher = Matcher(rule, priority, block)
|
||||
# 按照优先级插入
|
||||
add_matcher(matcher)
|
||||
return matcher
|
||||
|
||||
|
||||
def on_keywords(keywords: list[str], rule=empty_rule, priority: int = 0, block: bool = False) -> Matcher:
|
||||
@Rule
|
||||
async def on_keywords_rule(event: MessageEvent):
|
||||
return any(keyword in event.raw_message for keyword in keywords)
|
||||
|
||||
return on_message(on_keywords_rule & rule, priority, block)
|
51
liteyuki/message/rule.py
Normal file
51
liteyuki/message/rule.py
Normal file
@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:55
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : rule.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import inspect
|
||||
from typing import Optional, TypeAlias, Callable, Coroutine
|
||||
|
||||
from liteyuki.message.event import MessageEvent
|
||||
from liteyuki import get_config
|
||||
|
||||
_superusers: list[str] = get_config("liteyuki.superusers", [])
|
||||
|
||||
RuleHandlerFunc: TypeAlias = Callable[[MessageEvent], Coroutine[None, None, bool]]
|
||||
"""规则函数签名"""
|
||||
|
||||
|
||||
class Rule:
|
||||
def __init__(self, handler: RuleHandlerFunc):
|
||||
self.handler = handler
|
||||
|
||||
def __or__(self, other: "Rule") -> "Rule":
|
||||
async def combined_handler(event: MessageEvent) -> bool:
|
||||
return await self.handler(event) or await other.handler(event)
|
||||
|
||||
return Rule(combined_handler)
|
||||
|
||||
def __and__(self, other: "Rule") -> "Rule":
|
||||
async def combined_handler(event: MessageEvent) -> bool:
|
||||
return await self.handler(event) and await other.handler(event)
|
||||
|
||||
return Rule(combined_handler)
|
||||
|
||||
async def __call__(self, event: MessageEvent) -> bool:
|
||||
if self.handler is None:
|
||||
return True
|
||||
return await self.handler(event)
|
||||
|
||||
|
||||
@Rule
|
||||
async def empty_rule(event: MessageEvent) -> bool:
|
||||
return True
|
||||
|
||||
@Rule
|
||||
async def is_su_rule(event: MessageEvent) -> bool:
|
||||
return str(event.user_id) in _superusers
|
@ -2,9 +2,9 @@
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/10 下午5:18
|
||||
@Time : 2024/8/19 下午10:47
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : reloader_monitor.py
|
||||
@File : session.py
|
||||
@Software: PyCharm
|
||||
"""
|
357
liteyuki/mkdoc.py
Normal file
357
liteyuki/mkdoc.py
Normal file
@ -0,0 +1,357 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 上午6:23
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : mkdoc.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel
|
||||
|
||||
NO_TYPE_ANY = "Any"
|
||||
NO_TYPE_HINT = "NoTypeHint"
|
||||
|
||||
|
||||
class DefType(Enum):
|
||||
FUNCTION = "function"
|
||||
METHOD = "method"
|
||||
STATIC_METHOD = "staticmethod"
|
||||
CLASS_METHOD = "classmethod"
|
||||
PROPERTY = "property"
|
||||
|
||||
|
||||
class FunctionInfo(BaseModel):
|
||||
name: str
|
||||
args: list[tuple[str, str]]
|
||||
return_type: str
|
||||
docstring: str
|
||||
source_code: str = ""
|
||||
|
||||
type: DefType
|
||||
"""若为类中def,则有"""
|
||||
is_async: bool
|
||||
|
||||
|
||||
class AttributeInfo(BaseModel):
|
||||
name: str
|
||||
type: str
|
||||
value: Any = None
|
||||
docstring: str = ""
|
||||
|
||||
|
||||
class ClassInfo(BaseModel):
|
||||
name: str
|
||||
docstring: str
|
||||
methods: list[FunctionInfo]
|
||||
attributes: list[AttributeInfo]
|
||||
inherit: list[str]
|
||||
|
||||
|
||||
class ModuleInfo(BaseModel):
|
||||
module_path: str
|
||||
"""点分割模块路径 例如 liteyuki.bot"""
|
||||
|
||||
functions: list[FunctionInfo]
|
||||
classes: list[ClassInfo]
|
||||
attributes: list[AttributeInfo]
|
||||
docstring: str
|
||||
|
||||
|
||||
def get_relative_path(base_path: str, target_path: str) -> str:
|
||||
"""
|
||||
获取相对路径
|
||||
Args:
|
||||
base_path: 基础路径
|
||||
target_path: 目标路径
|
||||
"""
|
||||
return os.path.relpath(target_path, base_path)
|
||||
|
||||
|
||||
def write_to_files(file_data: dict[str, str]):
|
||||
"""
|
||||
输出文件
|
||||
Args:
|
||||
file_data: 文件数据 相对路径
|
||||
"""
|
||||
|
||||
for rp, data in file_data.items():
|
||||
|
||||
if not os.path.exists(os.path.dirname(rp)):
|
||||
os.makedirs(os.path.dirname(rp))
|
||||
with open(rp, 'w', encoding='utf-8') as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
def get_file_list(module_folder: str):
|
||||
file_list = []
|
||||
for root, dirs, files in os.walk(module_folder):
|
||||
for file in files:
|
||||
if file.endswith((".py", ".pyi")):
|
||||
file_list.append(os.path.join(root, file))
|
||||
return file_list
|
||||
|
||||
|
||||
def get_module_info_normal(file_path: str, ignore_private: bool = True) -> ModuleInfo:
|
||||
"""
|
||||
获取函数和类
|
||||
Args:
|
||||
file_path: Python 文件路径
|
||||
ignore_private: 忽略私有函数和类
|
||||
Returns:
|
||||
模块信息
|
||||
"""
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
file_content = file.read()
|
||||
tree = ast.parse(file_content)
|
||||
|
||||
dot_sep_module_path = file_path.replace(os.sep, '.').replace(".py", "").replace(".pyi", "")
|
||||
module_docstring = ast.get_docstring(tree)
|
||||
|
||||
module_info = ModuleInfo(
|
||||
module_path=dot_sep_module_path,
|
||||
functions=[],
|
||||
classes=[],
|
||||
attributes=[],
|
||||
docstring=module_docstring if module_docstring else ""
|
||||
)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
# 模块函数 且不在类中 若ignore_private=True则忽略私有函数
|
||||
if not any(isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(node)) and (not ignore_private or not node.name.startswith('_')):
|
||||
|
||||
# 判断第一个参数是否为self或cls,后期用其他办法优化
|
||||
if node.args.args:
|
||||
first_arg = node.args.args[0]
|
||||
if first_arg.arg in ("self", "cls"):
|
||||
continue
|
||||
|
||||
function_docstring = ast.get_docstring(node)
|
||||
|
||||
func_info = FunctionInfo(
|
||||
name=node.name,
|
||||
args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args],
|
||||
return_type=ast.unparse(node.returns) if node.returns else "None",
|
||||
docstring=function_docstring if function_docstring else "",
|
||||
type=DefType.FUNCTION,
|
||||
is_async=isinstance(node, ast.AsyncFunctionDef),
|
||||
source_code=ast.unparse(node)
|
||||
)
|
||||
module_info.functions.append(func_info)
|
||||
|
||||
elif isinstance(node, ast.ClassDef):
|
||||
# 模块类
|
||||
class_docstring = ast.get_docstring(node)
|
||||
|
||||
class_info = ClassInfo(
|
||||
name=node.name,
|
||||
docstring=class_docstring if class_docstring else "",
|
||||
methods=[],
|
||||
attributes=[],
|
||||
inherit=[ast.unparse(base) for base in node.bases]
|
||||
)
|
||||
|
||||
for class_node in node.body:
|
||||
# methods [instance, static, class, property],保留__init__方法
|
||||
if isinstance(class_node, ast.FunctionDef) and (not ignore_private or not class_node.name.startswith('_') or class_node.name == "__init__"):
|
||||
method_docstring = ast.get_docstring(class_node)
|
||||
def_type = DefType.METHOD
|
||||
if class_node.decorator_list:
|
||||
if any(isinstance(decorator, ast.Name) and decorator.id == "staticmethod" for decorator in class_node.decorator_list):
|
||||
def_type = DefType.STATIC_METHOD
|
||||
elif any(isinstance(decorator, ast.Name) and decorator.id == "classmethod" for decorator in class_node.decorator_list):
|
||||
def_type = DefType.CLASS_METHOD
|
||||
elif any(isinstance(decorator, ast.Name) and decorator.id == "property" for decorator in class_node.decorator_list):
|
||||
def_type = DefType.PROPERTY
|
||||
class_info.methods.append(FunctionInfo(
|
||||
name=class_node.name,
|
||||
args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in class_node.args.args],
|
||||
return_type=ast.unparse(class_node.returns) if class_node.returns else "None",
|
||||
docstring=method_docstring if method_docstring else "",
|
||||
type=def_type,
|
||||
is_async=isinstance(class_node, ast.AsyncFunctionDef),
|
||||
source_code=ast.unparse(class_node)
|
||||
))
|
||||
# attributes
|
||||
elif isinstance(class_node, ast.Assign):
|
||||
for target in class_node.targets:
|
||||
if isinstance(target, ast.Name):
|
||||
class_info.attributes.append(AttributeInfo(
|
||||
name=target.id,
|
||||
type=ast.unparse(class_node.value)
|
||||
))
|
||||
module_info.classes.append(class_info)
|
||||
|
||||
elif isinstance(node, ast.Assign):
|
||||
# 检查是否在类或函数中
|
||||
if not any(isinstance(parent, (ast.ClassDef, ast.FunctionDef)) for parent in ast.iter_child_nodes(node)):
|
||||
# 模块属性变量
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and (not ignore_private or not target.id.startswith('_')):
|
||||
attr_type = NO_TYPE_HINT
|
||||
if isinstance(node.value, ast.AnnAssign) and node.value.annotation:
|
||||
attr_type = ast.unparse(node.value.annotation)
|
||||
module_info.attributes.append(AttributeInfo(
|
||||
name=target.id,
|
||||
type=attr_type,
|
||||
value=ast.unparse(node.value) if node.value else None
|
||||
))
|
||||
|
||||
return module_info
|
||||
|
||||
|
||||
def generate_markdown(module_info: ModuleInfo, front_matter=None, lang: str = "zh-CN") -> str:
|
||||
"""
|
||||
生成模块的Markdown
|
||||
你可在此自定义生成的Markdown格式
|
||||
Args:
|
||||
module_info: 模块信息
|
||||
front_matter: 自定义选项title, index, icon, category
|
||||
lang: 语言
|
||||
Returns:
|
||||
Markdown 字符串
|
||||
"""
|
||||
|
||||
content = ""
|
||||
|
||||
front_matter = "---\n" + "\n".join([f"{k}: {v}" for k, v in front_matter.items()]) + "\n---\n\n"
|
||||
|
||||
content += front_matter
|
||||
|
||||
# 模块函数
|
||||
for func in module_info.functions:
|
||||
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] else arg[0] for arg in func.args]
|
||||
content += f"### ***{'async ' if func.is_async else ''}def*** `{func.name}({', '.join(args_with_type)}) -> {func.return_type}`\n\n"
|
||||
|
||||
func.docstring = func.docstring.replace("\n", "\n\n")
|
||||
content += f"{func.docstring}\n\n"
|
||||
|
||||
# 函数源代码可展开区域
|
||||
content += f"<details>\n<summary>源代码</summary>\n\n```python\n{func.source_code}\n```\n</details>\n\n"
|
||||
|
||||
# 类
|
||||
for cls in module_info.classes:
|
||||
if cls.inherit:
|
||||
inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ""
|
||||
content += f"### ***class*** `{cls.name}{inherit}`\n\n"
|
||||
else:
|
||||
content += f"### ***class*** `{cls.name}`\n\n"
|
||||
|
||||
cls.docstring = cls.docstring.replace("\n", "\n\n")
|
||||
content += f"{cls.docstring}\n\n"
|
||||
for method in cls.methods:
|
||||
# 类函数
|
||||
|
||||
if method.type != DefType.METHOD:
|
||||
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] else arg[0] for arg in method.args]
|
||||
content += f"###   ***@{method.type.value}***\n"
|
||||
else:
|
||||
# self不加类型提示
|
||||
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] and arg[0] != "self" else arg[0] for arg in method.args]
|
||||
content += f"###   ***{'async ' if method.is_async else ''}def*** `{method.name}({', '.join(args_with_type)}) -> {method.return_type}`\n\n"
|
||||
|
||||
method.docstring = method.docstring.replace("\n", "\n\n")
|
||||
content += f" {method.docstring}\n\n"
|
||||
# 函数源代码可展开区域
|
||||
|
||||
if lang == "zh-CN":
|
||||
TEXT_SOURCE_CODE = "源代码"
|
||||
else:
|
||||
TEXT_SOURCE_CODE = "Source Code"
|
||||
|
||||
content += f"<details>\n<summary>{TEXT_SOURCE_CODE}</summary>\n\n```python\n{method.source_code}\n```\n</details>\n\n"
|
||||
for attr in cls.attributes:
|
||||
content += f"###   ***attr*** `{attr.name}: {attr.type}`\n\n"
|
||||
|
||||
# 模块属性
|
||||
for attr in module_info.attributes:
|
||||
if attr.type == NO_TYPE_HINT:
|
||||
content += f"### ***var*** `{attr.name} = {attr.value}`\n\n"
|
||||
else:
|
||||
content += f"### ***var*** `{attr.name}: {attr.type} = {attr.value}`\n\n"
|
||||
|
||||
attr.docstring = attr.docstring.replace("\n", "\n\n")
|
||||
content += f"{attr.docstring}\n\n"
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, lang: str = "zh-CN", ignored_paths=None):
|
||||
"""
|
||||
生成文档
|
||||
Args:
|
||||
module_folder: 模块文件夹
|
||||
output_dir: 输出文件夹
|
||||
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b, True时例如docs/api/module/module_a.md, docs/api/module/module_b.md
|
||||
ignored_paths: 忽略的路径
|
||||
lang: 语言
|
||||
"""
|
||||
if ignored_paths is None:
|
||||
ignored_paths = []
|
||||
file_data: dict[str, str] = {} # 路径 -> 字串
|
||||
|
||||
file_list = get_file_list(module_folder)
|
||||
|
||||
# 清理输出目录
|
||||
shutil.rmtree(output_dir, ignore_errors=True)
|
||||
os.mkdir(output_dir)
|
||||
|
||||
replace_data = {
|
||||
"__init__": "README",
|
||||
".py" : ".md",
|
||||
}
|
||||
|
||||
for pyfile_path in file_list:
|
||||
if any(ignored_path.replace("\\", "/") in pyfile_path.replace("\\", "/") for ignored_path in ignored_paths):
|
||||
continue
|
||||
|
||||
no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path) # 去头路径
|
||||
|
||||
# markdown相对路径
|
||||
rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path
|
||||
for rk, rv in replace_data.items():
|
||||
rel_md_path = rel_md_path.replace(rk, rv)
|
||||
|
||||
abs_md_path = os.path.join(output_dir, rel_md_path)
|
||||
|
||||
# 获取模块信息
|
||||
module_info = get_module_info_normal(pyfile_path)
|
||||
|
||||
# 生成markdown
|
||||
|
||||
if "README" in abs_md_path:
|
||||
front_matter = {
|
||||
"title" : module_info.module_path.replace(".__init__", "").replace("_", "\\n"),
|
||||
"index" : "true",
|
||||
"icon" : "laptop-code",
|
||||
"category": "API"
|
||||
}
|
||||
else:
|
||||
front_matter = {
|
||||
"title" : module_info.module_path.replace("_", "\\n"),
|
||||
"order" : "1",
|
||||
"icon" : "laptop-code",
|
||||
"category": "API"
|
||||
}
|
||||
|
||||
md_content = generate_markdown(module_info, front_matter)
|
||||
print(f"Generate {pyfile_path} -> {abs_md_path}")
|
||||
file_data[abs_md_path] = md_content
|
||||
|
||||
write_to_files(file_data)
|
||||
|
||||
|
||||
# 入口脚本
|
||||
if __name__ == '__main__':
|
||||
# 这里填入你的模块路径
|
||||
generate_docs('liteyuki', 'docs/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"], lang="zh-CN")
|
||||
generate_docs('liteyuki', 'docs/en/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"], lang="en")
|
@ -1,11 +1,12 @@
|
||||
from liteyuki.plugin.model import Plugin, PluginMetadata
|
||||
from liteyuki.plugin.model import Plugin, PluginMetadata, PluginType
|
||||
from liteyuki.plugin.load import load_plugin, load_plugins, _plugins
|
||||
|
||||
__all__ = [
|
||||
"PluginMetadata",
|
||||
"Plugin",
|
||||
"load_plugin",
|
||||
"load_plugins",
|
||||
"PluginMetadata",
|
||||
"Plugin",
|
||||
"PluginType",
|
||||
"load_plugin",
|
||||
"load_plugins",
|
||||
]
|
||||
|
||||
|
||||
|
@ -10,14 +10,12 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
"""
|
||||
import os
|
||||
import traceback
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from nonebot import logger
|
||||
|
||||
from liteyuki.plugin.model import Plugin, PluginMetadata
|
||||
from importlib import import_module
|
||||
|
||||
from liteyuki.log import logger
|
||||
from liteyuki.plugin.model import Plugin, PluginMetadata, PluginType
|
||||
from liteyuki.utils import path_to_module_name
|
||||
|
||||
_plugins: dict[str, Plugin] = {}
|
||||
@ -25,6 +23,7 @@ _plugins: dict[str, Plugin] = {}
|
||||
__all__ = [
|
||||
"load_plugin",
|
||||
"load_plugins",
|
||||
"_plugins",
|
||||
]
|
||||
|
||||
|
||||
@ -35,45 +34,92 @@ def load_plugin(module_path: str | Path) -> Optional[Plugin]:
|
||||
module_path: 插件名称 `path.to.your.plugin`
|
||||
或插件路径 `pathlib.Path(path/to/your/plugin)`
|
||||
"""
|
||||
module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path
|
||||
module_path = (
|
||||
path_to_module_name(Path(module_path))
|
||||
if isinstance(module_path, Path)
|
||||
else module_path
|
||||
)
|
||||
try:
|
||||
module = import_module(module_path)
|
||||
_plugins[module.__name__] = Plugin(
|
||||
name=module.__name__,
|
||||
module=module,
|
||||
module_name=module_path,
|
||||
metadata=module.__dict__.get("__plugin_metadata__", None)
|
||||
)
|
||||
if module.__dict__.get("__plugin_metadata__", None):
|
||||
metadata: "PluginMetadata" = module.__dict__["__plugin_metadata__"]
|
||||
display_name = module.__name__.split(".")[-1]
|
||||
elif module.__dict__.get("__liteyuki_plugin_meta__", None):
|
||||
metadata: "PluginMetadata" = module.__dict__["__liteyuki_plugin_meta__"]
|
||||
display_name = format_display_name(
|
||||
f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type
|
||||
)
|
||||
elif module.__dict__.get("__plugin_meta__", None):
|
||||
metadata: "PluginMetadata" = module.__dict__["__plugin_meta__"]
|
||||
display_name = format_display_name(
|
||||
f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type
|
||||
)
|
||||
else:
|
||||
|
||||
logger.opt(colors=True).warning(
|
||||
f'轻雪插件 "{module.__name__}" 的元信息未指定,将采用空的元信息'
|
||||
)
|
||||
|
||||
metadata = PluginMetadata(
|
||||
name=module.__name__,
|
||||
)
|
||||
display_name = module.__name__.split(".")[-1]
|
||||
|
||||
_plugins[module.__name__].metadata = metadata
|
||||
|
||||
logger.opt(colors=True).success(
|
||||
f'成功加载 轻雪插件 "<y>{module.__name__.split(".")[-1]}</y>"'
|
||||
f'成功加载轻雪插件 "{display_name}"'
|
||||
)
|
||||
return _plugins[module.__name__]
|
||||
|
||||
except Exception as e:
|
||||
logger.opt(colors=True).success(
|
||||
f'未能加载 轻雪插件 "<r>{module_path}</r>"'
|
||||
f'无法加载轻雪插件 "<r>{module_path}</r>"'
|
||||
)
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
|
||||
def load_plugins(*plugin_dir: str) -> set[Plugin]:
|
||||
def load_plugins(*plugin_dir: str, ignore_warning: bool = True) -> set[Plugin]:
|
||||
"""导入文件夹下多个插件
|
||||
|
||||
参数:
|
||||
plugin_dir: 文件夹路径
|
||||
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
|
||||
"""
|
||||
plugins = set()
|
||||
for dir_path in plugin_dir:
|
||||
# 遍历每一个文件夹下的py文件和包含__init__.py的文件夹,不递归
|
||||
if not os.path.exists(dir_path):
|
||||
if not ignore_warning:
|
||||
logger.warning(f"插件目录 '{dir_path}' 不存在")
|
||||
continue
|
||||
|
||||
if not os.listdir(dir_path):
|
||||
if not ignore_warning:
|
||||
logger.warning(f"插件目录 '{dir_path}' 为空")
|
||||
continue
|
||||
|
||||
if not os.path.isdir(dir_path):
|
||||
if not ignore_warning:
|
||||
logger.warning(f"本应是插件目录的路径 '{dir_path}' 并非如此")
|
||||
continue
|
||||
|
||||
for f in os.listdir(dir_path):
|
||||
path = Path(os.path.join(dir_path, f))
|
||||
|
||||
module_name = None
|
||||
if os.path.isfile(path) and f.endswith('.py') and f != '__init__.py':
|
||||
if os.path.isfile(path) and f.endswith(".py") and f != "__init__.py":
|
||||
module_name = f"{path_to_module_name(Path(dir_path))}.{f[:-3]}"
|
||||
|
||||
elif os.path.isdir(path) and os.path.exists(os.path.join(path, '__init__.py')):
|
||||
elif os.path.isdir(path) and os.path.exists(
|
||||
os.path.join(path, "__init__.py")
|
||||
):
|
||||
module_name = path_to_module_name(path)
|
||||
|
||||
if module_name:
|
||||
@ -81,3 +127,27 @@ def load_plugins(*plugin_dir: str) -> set[Plugin]:
|
||||
if _plugins.get(module_name):
|
||||
plugins.add(_plugins[module_name])
|
||||
return plugins
|
||||
|
||||
|
||||
def format_display_name(display_name: str, plugin_type: PluginType) -> str:
|
||||
"""
|
||||
设置插件名称颜色,根据不同类型插件设置颜色
|
||||
Args:
|
||||
display_name: 插件名称
|
||||
plugin_type: 插件类型
|
||||
|
||||
Returns:
|
||||
str: 设置后的插件名称 <y>name</y>
|
||||
"""
|
||||
color = "y"
|
||||
match plugin_type:
|
||||
case PluginType.APPLICATION:
|
||||
color = "m"
|
||||
case PluginType.TEST:
|
||||
color = "g"
|
||||
case PluginType.MODULE:
|
||||
color = "e"
|
||||
case PluginType.SERVICE:
|
||||
color = "c"
|
||||
|
||||
return f"<{color}>{display_name} [{plugin_type.name}]</{color}>"
|
||||
|
@ -1,9 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All rights reserved
|
||||
|
||||
版权所有 © 2020-2024 神羽SnowyKami & 金羿Eilles with LiteyukiStudio & TriM Org.
|
||||
保留所有权利
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/7/23 下午11:59
|
||||
@Author : snowykami
|
||||
|
@ -8,22 +8,61 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
@File : model.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from enum import Enum
|
||||
from types import ModuleType
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PluginType(Enum):
|
||||
"""
|
||||
插件类型枚举值
|
||||
"""
|
||||
APPLICATION = "application"
|
||||
"""应用端:例如NoneBot"""
|
||||
|
||||
SERVICE = "service"
|
||||
"""服务端:例如AI绘画后端"""
|
||||
|
||||
MODULE = "module"
|
||||
"""模块:导出对象给其他插件使用"""
|
||||
|
||||
UNCLASSIFIED = "unclassified"
|
||||
"""未分类:默认值"""
|
||||
|
||||
TEST = "test"
|
||||
"""测试:测试插件"""
|
||||
|
||||
|
||||
class PluginMetadata(BaseModel):
|
||||
"""
|
||||
轻雪插件元数据,由插件编写者提供,name为必填项
|
||||
Attributes:
|
||||
----------
|
||||
|
||||
name: str
|
||||
插件名称
|
||||
description: str
|
||||
插件描述
|
||||
usage: str
|
||||
插件使用方法
|
||||
type: str
|
||||
插件类型
|
||||
author: str
|
||||
插件作者
|
||||
homepage: str
|
||||
插件主页
|
||||
extra: dict[str, Any]
|
||||
额外信息
|
||||
"""
|
||||
name: str
|
||||
description: str = ""
|
||||
usage: str = ""
|
||||
type: str = ""
|
||||
type: PluginType = PluginType.UNCLASSIFIED
|
||||
author: str = ""
|
||||
homepage: str = ""
|
||||
running_in_main: bool = True # 是否在主进程运行
|
||||
extra: dict[str, Any] = {}
|
||||
|
||||
|
||||
class Plugin(BaseModel):
|
||||
|
5
liteyuki/plugins/__init__.py
Normal file
5
liteyuki/plugins/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
此模块为内置插件文件夹,用于存放内置插件。
|
||||
This module is the built-in plugin folder, used to store built-in plugins.
|
||||
"""
|
@ -1,55 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
#
|
||||
# @Time : 2024/7/22 上午11:25
|
||||
# @Author : snowykami
|
||||
# @Email : snowykami@outlook.com
|
||||
# @File : asa.py
|
||||
# @Software: PyCharm
|
||||
import asyncio
|
||||
|
||||
from liteyuki.plugin import PluginMetadata
|
||||
from liteyuki import get_bot, logger
|
||||
from liteyuki.comm.channel import get_channel
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="lifespan_monitor",
|
||||
)
|
||||
|
||||
bot = get_bot()
|
||||
nbp_chan = get_channel("nonebot-passive")
|
||||
mbp_chan = get_channel("melobot-passive")
|
||||
|
||||
|
||||
@bot.on_before_start
|
||||
def _():
|
||||
logger.info("生命周期监控器:启动前")
|
||||
|
||||
|
||||
@bot.on_before_shutdown
|
||||
def _():
|
||||
print(get_channel("main"))
|
||||
logger.info("生命周期监控器:停止前")
|
||||
|
||||
|
||||
@bot.on_before_restart
|
||||
def _():
|
||||
logger.info("生命周期监控器:重启前")
|
||||
|
||||
|
||||
@bot.on_after_start
|
||||
def _():
|
||||
logger.info("生命周期监控器:启动后")
|
||||
|
||||
|
||||
@bot.on_after_start
|
||||
async def _():
|
||||
logger.info("生命周期监控器:启动后")
|
||||
|
||||
|
||||
|
||||
# @mbp_chan.on_receive()
|
||||
# @nbp_chan.on_receive()
|
||||
# async def _(data):
|
||||
# print("主进程收到数据", data)
|
19
liteyuki/plugins/liteecho.py
Normal file
19
liteyuki/plugins/liteecho.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/22 下午12:31
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : liteecho.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
|
||||
from liteyuki.message.on import on_startswith
|
||||
from liteyuki.message.event import MessageEvent
|
||||
from liteyuki.message.rule import is_su_rule
|
||||
|
||||
|
||||
@on_startswith(["ryounecho", "ryeco"], rule=is_su_rule).handle()
|
||||
async def liteecho(event: MessageEvent):
|
||||
event.reply(event.raw_message.strip()[8:].strip())
|
32
liteyuki/plugins/plugin_loader/__init__.py
Normal file
32
liteyuki/plugins/plugin_loader/__init__.py
Normal file
@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/11 下午10:02
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : __init__.py.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from liteyuki import get_config, load_plugin
|
||||
from liteyuki.plugin import PluginMetadata, load_plugins, PluginType
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="外部轻雪插件加载器",
|
||||
description="插件加载器,用于加载轻雪原生插件",
|
||||
type=PluginType.SERVICE
|
||||
)
|
||||
|
||||
|
||||
def default_plugins_loader():
|
||||
"""
|
||||
默认插件加载器,应在初始化时调用
|
||||
"""
|
||||
for plugin in get_config("liteyuki.plugins", []):
|
||||
load_plugin(plugin)
|
||||
|
||||
for plugin_dir in get_config("liteyuki.plugin_dirs", ["src/liteyuki_plugins"]):
|
||||
load_plugins(plugin_dir)
|
||||
|
||||
|
||||
default_plugins_loader()
|
@ -4,11 +4,15 @@
|
||||
"""
|
||||
import asyncio
|
||||
import inspect
|
||||
import multiprocessing
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Coroutine
|
||||
|
||||
from liteyuki.log import logger
|
||||
|
||||
IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
|
||||
|
||||
|
||||
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
|
||||
"""
|
||||
@ -39,7 +43,7 @@ def run_coroutine(*coro: Coroutine):
|
||||
# 检测是否有现有的事件循环
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
if loop.is_running():
|
||||
# 如果事件循环正在运行,创建任务
|
||||
for c in coro:
|
||||
@ -59,6 +63,18 @@ def run_coroutine(*coro: Coroutine):
|
||||
logger.error(f"协程异常:{e}")
|
||||
|
||||
|
||||
def run_coroutine_in_thread(*coro: Coroutine):
|
||||
"""
|
||||
在新线程中运行协程
|
||||
Args:
|
||||
coro:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
threading.Thread(target=run_coroutine, args=coro, daemon=True).start()
|
||||
|
||||
|
||||
def path_to_module_name(path: Path) -> str:
|
||||
"""
|
||||
转换路径为模块名
|
||||
@ -72,3 +88,19 @@ def path_to_module_name(path: Path) -> str:
|
||||
return ".".join(rel_path.parts[:-1])
|
||||
else:
|
||||
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))
|
||||
|
||||
|
||||
def async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]:
|
||||
"""
|
||||
异步包装器
|
||||
Args:
|
||||
func: Sync Callable
|
||||
Returns:
|
||||
Coroutine: Asynchronous Callable
|
||||
"""
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
wrapper.__signature__ = inspect.signature(func)
|
||||
return wrapper
|
||||
|
8
main.py
8
main.py
@ -1,6 +1,10 @@
|
||||
"""
|
||||
启动脚本,会执行一些启动的操作,比如加载配置文件,初始化 bot 实例等。
|
||||
"""
|
||||
from liteyuki import LiteyukiBot
|
||||
from liteyuki.config import load_from_yaml
|
||||
from liteyuki.config import load_config_in_default
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot = LiteyukiBot(**load_from_yaml("config.yml"))
|
||||
bot = LiteyukiBot(**load_config_in_default(no_waring=True))
|
||||
bot.run()
|
||||
|
@ -1,58 +1,44 @@
|
||||
# PEP 621 project metadata
|
||||
# See https://www.python.org/dev/peps/pep-0621/
|
||||
# This file is for project use, but don`t use with nb-cli
|
||||
# 此文件为项目所用,请不要和nb-cli一起使用以防被修改
|
||||
[tool.poetry]
|
||||
# This file is liteyuki framework use only, don`t use it with applications or nb-cli.
|
||||
# 此文件仅供 liteyuki 框架使用,请勿用于应用程序及nb-cli,请使用pip进行安装。
|
||||
[project]
|
||||
name = "ryoun-trim"
|
||||
version = "0"
|
||||
description = "based on liteyuki6"
|
||||
authors = ["金羿Eilles"]
|
||||
license = "MIT & LSO"
|
||||
package-mode = false
|
||||
dynamic = ["version"]
|
||||
description = "A lightweight bot process management framework and application."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "Eilles", email = "EillesWan@outlook.com" },
|
||||
{ name = "TriM-Org.", email = "TriM-Organization@hotmail.com" },
|
||||
{ name = "snowykami", email = "snowykami@outlook.com" },
|
||||
{ name = "LiteyukiStudio", email = "studio@liteyuki.icu" },
|
||||
]
|
||||
license = { text = "汉钰律许可协议 第一版" }
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
aiofiles = "~23.2.1"
|
||||
aiohttp = "~3.9.3"
|
||||
aiosqlite3 = "~0.3.0"
|
||||
colored = "~2.2.4"
|
||||
fastapi = "~0.110.0"
|
||||
GitPython = "~3.1.42"
|
||||
httpx = "~0.27.0"
|
||||
importlib_metadata = "~7.0.2"
|
||||
jieba = "~0.42.1"
|
||||
loguru = "~0.7.2"
|
||||
nb-cli = "~1.4.1"
|
||||
nonebot-adapter-onebot = "~2.4.3"
|
||||
nonebot-adapter-satori = "~0.11.5"
|
||||
nonebot-plugin-alconna = "~0.46.3"
|
||||
nonebot-plugin-apscheduler = "~0.4.0"
|
||||
nonebot-plugin-htmlrender = "~0.3.1"
|
||||
nonebot2 = { version = "~2.3.0", extras = ["fastapi", "httpx", "websockets"] }
|
||||
numpy = "<2.0.0"
|
||||
packaging = "~23.1"
|
||||
psutil = "~5.9.8"
|
||||
py-cpuinfo = "~9.0.0"
|
||||
pydantic = "~2.7.0"
|
||||
Pygments = "~2.17.2"
|
||||
python-dotenv = "~1.0.1"
|
||||
pytest = "~8.3.1"
|
||||
pytz = "~2024.1"
|
||||
PyYAML = "~6.0.1"
|
||||
requests = "~2.31.0"
|
||||
starlette = "~0.36.3"
|
||||
watchdog = "~4.0.0"
|
||||
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "tuna"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
|
||||
[tool.nonebot]
|
||||
dependencies = [
|
||||
"loguru~=0.7.2",
|
||||
"pydantic==2.8.2",
|
||||
"PyYAML==6.0.2",
|
||||
"toml==0.10.2",
|
||||
"watchdog==4.0.1",
|
||||
"pdm-backend==2.3.3"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://bot.liteyuki.icu"
|
||||
repository = "https://gitee.com/TriM-Organization/LiteyukiBot-TriM"
|
||||
documentation = "https://bot.liteyuki.icu"
|
||||
Homepage = "https://bot.liteyuki.icu"
|
||||
Repository = "https://gitee.com/TriM-Organization/LiteyukiBot-TriM"
|
||||
"Issue Tracker" = "https://github.com/LiteyukiStudio/LiteyukiBot/issues/new?assignees=&labels=&projects=&template=%E9%97%AE%E9%A2%98%E5%8F%8D%E9%A6%88.md&title="
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["pdm-backend"]
|
||||
build-backend = "pdm.backend"
|
||||
|
||||
[tool.pdm.build]
|
||||
includes = ["liteyuki/", "LICENSE", "README.md", "LICENSE.MD"]
|
||||
excludes = ["tests/", "docs/", "src/"]
|
||||
|
||||
[tool.pdm.version]
|
||||
source = "file"
|
||||
path = "liteyuki/__init__.py"
|
@ -1,35 +1,34 @@
|
||||
aiohttp~=3.9.3
|
||||
aiofiles~=23.2.1
|
||||
colored~=2.2.4
|
||||
GitPython~=3.1.42
|
||||
httpx~=0.27.0
|
||||
nb-cli~=1.4.1
|
||||
nonebot2[fastapi,httpx,websockets]~=2.3.0
|
||||
nonebot-plugin-htmlrender~=0.3.1
|
||||
nonebot-adapter-onebot~=2.4.3
|
||||
nonebot-plugin-alconna~=0.46.3
|
||||
nonebot_plugin_apscheduler~=0.4.0
|
||||
nonebot-adapter-satori~=0.11.5
|
||||
aiohttp>=3.9.3
|
||||
aiofiles>=23.2.1
|
||||
colored>=2.2.4
|
||||
GitPython>=3.1.43
|
||||
httpx>=0.27.0
|
||||
nonebot-plugin-htmlrender>=0.1.0
|
||||
nonebot2[fastapi,httpx,websockets]>=2.3.3
|
||||
nonebot-adapter-onebot>=2.4.3
|
||||
nonebot-plugin-alconna>=0.46.3
|
||||
nonebot_plugin_apscheduler>=0.4.0
|
||||
nonebot-adapter-satori>=0.11.5
|
||||
# pyppeteer>=2.0.0
|
||||
markdown>=3.3.6
|
||||
zhDateTime>=1.1.1
|
||||
numpy<2.0.0
|
||||
packaging~=23.1
|
||||
psutil~=5.9.8
|
||||
py-cpuinfo~=9.0.0
|
||||
pydantic~=2.7.0
|
||||
Pygments~=2.17.2
|
||||
pytz~=2024.1
|
||||
PyYAML~=6.0.1
|
||||
starlette~=0.36.3
|
||||
loguru~=0.7.2
|
||||
importlib_metadata~=7.0.2
|
||||
requests~=2.31.0
|
||||
watchdog~=4.0.0
|
||||
pillow~=10.0.0
|
||||
jieba~=0.42.1
|
||||
aiosqlite3~=0.3.0
|
||||
fastapi~=0.110.0
|
||||
python-dotenv~=1.0.1
|
||||
packaging>=23.1
|
||||
psutil>=5.9.8
|
||||
py-cpuinfo>=9.0.0
|
||||
Pygments>=2.17.2
|
||||
pyppeteer>=2.0.0
|
||||
pytz>=2024.1
|
||||
PyYAML>=6.0.1
|
||||
pillow>=10.0.0
|
||||
toml>=0.10.2
|
||||
importlib_metadata>=7.0.2
|
||||
watchdog>=4.0.0
|
||||
jieba>=0.42.1
|
||||
python-dotenv>=1.0.1
|
||||
nonebot_plugin_session
|
||||
pypinyin
|
||||
zhDateTime>=1.0.3
|
||||
Musicreater>=2.2.0
|
||||
librosa==0.10.1
|
||||
librosa==0.10.1
|
||||
TrimMCStruct
|
||||
brotli
|
@ -2,7 +2,6 @@ from nonebot.plugin import PluginMetadata
|
||||
|
||||
from .core import *
|
||||
from .loader import *
|
||||
from .dev import *
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
@ -18,29 +17,6 @@ __plugin_meta__ = PluginMetadata(
|
||||
|
||||
from ..utils.base.language import Language, get_default_lang_code
|
||||
|
||||
print(
|
||||
"\033[34m"
|
||||
+ r"""
|
||||
▅▅▅▅▅▅▅▅▅▅▅▅▅▅██ ▅▅▅▅▅▅▅▅▅▅▅▅▅▅██ ██ ▅▅▅▅▅▅▅▅▅▅█™
|
||||
▛ ██ ██ ▛ ██ ███ ██ ██
|
||||
██ ██ ███████████████ ██ ████████▅ ██
|
||||
███████████████ ██ ███ ██ ██
|
||||
██ ██ ▅██████████████▛ ██ ████████████
|
||||
██ ██ ███ ███
|
||||
████████████████ ██▅ ███ ██ ▅▅▅▅▅▅▅▅▅▅▅██
|
||||
███ █ ▜███████ ██ ███ ██ ██ ██ ██
|
||||
███ ███ █████▛ ██ ██ ██ ██ ██
|
||||
███ ██ ███ █ ██ ██ ██ ██ ██
|
||||
███ █████ ██████ ███ ██████████████
|
||||
商标标记 © 2024 金羿Eilles
|
||||
版权所有 © 2020-2024 神羽SnowyKami & 金羿Eilles\\
|
||||
with LiteyukiStudio & TriM Org.
|
||||
保留所有权利
|
||||
"""
|
||||
+ "\033[0m"
|
||||
)
|
||||
|
||||
|
||||
sys_lang = Language(get_default_lang_code())
|
||||
nonebot.logger.info(
|
||||
sys_lang.get("main.current_language", LANG=sys_lang.get("language.name"))
|
||||
|
@ -1,44 +1,43 @@
|
||||
import base64
|
||||
import time
|
||||
from typing import Any, AnyStr
|
||||
from typing import AnyStr
|
||||
|
||||
import time
|
||||
from typing import AnyStr
|
||||
|
||||
import nonebot
|
||||
import pip
|
||||
from nonebot import Bot, get_driver, require # type: ignore
|
||||
from nonebot import get_driver, require
|
||||
from nonebot.adapters import onebot, satori
|
||||
from nonebot.adapters.onebot.v11 import Message, escape, unescape
|
||||
from nonebot.exception import MockApiException
|
||||
from nonebot.adapters.onebot.v11 import Message, unescape
|
||||
from nonebot.internal.matcher import Matcher
|
||||
from nonebot.permission import SUPERUSER
|
||||
|
||||
# from src.liteyuki.core import Reloader
|
||||
from src.utils import event as event_utils, satori_utils
|
||||
from src.utils.base.config import get_config, load_from_yaml
|
||||
from src.utils.base.data_manager import StoredConfig, TempConfig, common_db
|
||||
from src.utils.base.config import get_config
|
||||
from src.utils.base.data_manager import TempConfig, common_db
|
||||
from src.utils.base.language import get_user_lang
|
||||
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
|
||||
|
||||
from .api import update_liteyuki
|
||||
from ..utils.base import reload
|
||||
from ..utils.base.ly_function import get_function
|
||||
from .api import update_liteyuki # type: ignore
|
||||
from ..utils.base import reload # type: ignore
|
||||
from ..utils.base.ly_function import get_function # type: ignore
|
||||
from ..utils.message.html_tool import md_to_pic
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
require("nonebot_plugin_apscheduler")
|
||||
from nonebot_plugin_alconna import (
|
||||
UniMessage,
|
||||
on_alconna,
|
||||
Alconna,
|
||||
Args,
|
||||
Subcommand,
|
||||
Arparma,
|
||||
MultiVar,
|
||||
)
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).config.get(
|
||||
"markdown_image", False
|
||||
)
|
||||
driver = get_driver()
|
||||
|
||||
|
||||
@on_alconna(
|
||||
@ -50,8 +49,8 @@ markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).con
|
||||
).handle()
|
||||
# Satori OK
|
||||
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
|
||||
if result.main_args.get("text"):
|
||||
await matcher.finish(Message(unescape(result.main_args.get("text")))) # type: ignore
|
||||
if text := result.main_args.get("text"):
|
||||
await matcher.finish(Message(unescape(text)))
|
||||
else:
|
||||
await matcher.finish(f"君安!灵温向你问好~\n此机 {bot.self_id}")
|
||||
|
||||
@ -72,7 +71,7 @@ async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
|
||||
aliases={"更新灵温"}, command=Alconna("update-ryoun"), permission=SUPERUSER
|
||||
).handle()
|
||||
# Satori OK
|
||||
async def _(bot: T_Bot, event: T_MessageEvent):
|
||||
async def _(bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
|
||||
# 使用git pull更新
|
||||
|
||||
ulang = get_user_lang(
|
||||
@ -84,7 +83,9 @@ async def _(bot: T_Bot, event: T_MessageEvent):
|
||||
btn_restart = md.btn_cmd(ulang.get("liteyuki.restart_now"), "reload-liteyuki")
|
||||
pip.main(["install", "-r", "requirements.txt"])
|
||||
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}"
|
||||
await md.send_md(reply, bot, event=event, at_sender=False)
|
||||
# await md.send_md(reply, bot)
|
||||
img_bytes = await md_to_pic(reply)
|
||||
await UniMessage.send(UniMessage.image(raw=img_bytes))
|
||||
|
||||
|
||||
@on_alconna(
|
||||
@ -115,108 +116,9 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
|
||||
)
|
||||
|
||||
common_db.save(temp_data)
|
||||
|
||||
reload()
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"配置"},
|
||||
command=Alconna(
|
||||
"config",
|
||||
Subcommand(
|
||||
"set",
|
||||
Args["key", str]["value", Any],
|
||||
alias=["设置"],
|
||||
),
|
||||
Subcommand("get", Args["key", str, None], alias=["查询", "获取"]),
|
||||
Subcommand("remove", Args["key", str], alias=["删除"]),
|
||||
),
|
||||
permission=SUPERUSER,
|
||||
).handle()
|
||||
# Satori OK
|
||||
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, matcher: Matcher):
|
||||
ulang = get_user_lang(str(event_utils.get_user_id(event)))
|
||||
stored_config: StoredConfig = common_db.where_one(
|
||||
StoredConfig(), default=StoredConfig()
|
||||
)
|
||||
if result.subcommands.get("set"):
|
||||
key, value = result.subcommands.get("set").args.get(
|
||||
"key"
|
||||
), result.subcommands.get("set").args.get("value")
|
||||
try:
|
||||
value = eval(value)
|
||||
except:
|
||||
pass
|
||||
stored_config.config[key] = value
|
||||
common_db.save(stored_config)
|
||||
await matcher.finish(
|
||||
f"{ulang.get('liteyuki.config_set_success', KEY=key, VAL=value)}"
|
||||
)
|
||||
elif result.subcommands.get("get"):
|
||||
key = result.subcommands.get("get").args.get("key")
|
||||
file_config = load_from_yaml("config.yml")
|
||||
reply = f"{ulang.get('liteyuki.current_config')}"
|
||||
if key:
|
||||
reply += f"```dotenv\n{key}={file_config.get(key, stored_config.config.get(key))}\n```"
|
||||
else:
|
||||
reply = f"{ulang.get('liteyuki.current_config')}"
|
||||
reply += f"\n{ulang.get('liteyuki.static_config')}\n```dotenv"
|
||||
for k, v in file_config.items():
|
||||
reply += f"\n{k}={v}"
|
||||
reply += "\n```"
|
||||
if len(stored_config.config) > 0:
|
||||
reply += f"\n{ulang.get('liteyuki.stored_config')}\n```dotenv"
|
||||
for k, v in stored_config.config.items():
|
||||
reply += f"\n{k}={v} {type(v)}"
|
||||
reply += "\n```"
|
||||
await md.send_md(reply, bot, event=event)
|
||||
elif result.subcommands.get("remove"):
|
||||
key = result.subcommands.get("remove").args.get("key")
|
||||
if key in stored_config.config:
|
||||
stored_config.config.pop(key)
|
||||
common_db.save(stored_config)
|
||||
await matcher.finish(
|
||||
f"{ulang.get('liteyuki.config_remove_success', KEY=key)}"
|
||||
)
|
||||
else:
|
||||
await matcher.finish(f"{ulang.get('liteyuki.invalid_command', TEXT=key)}")
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"切换图片模式"}, command=Alconna("switch-image-mode"), permission=SUPERUSER
|
||||
).handle()
|
||||
# Satori OK
|
||||
async def _(event: T_MessageEvent, matcher: Matcher):
|
||||
global markdown_image
|
||||
# 切换图片模式,False以图片形式发送,True以markdown形式发送
|
||||
ulang = get_user_lang(str(event_utils.get_user_id(event)))
|
||||
stored_config: StoredConfig = common_db.where_one(
|
||||
StoredConfig(), default=StoredConfig()
|
||||
)
|
||||
stored_config.config["markdown_image"] = not stored_config.config.get(
|
||||
"markdown_image", False
|
||||
)
|
||||
markdown_image = stored_config.config["markdown_image"]
|
||||
common_db.save(stored_config)
|
||||
await matcher.finish(
|
||||
ulang.get(
|
||||
"liteyuki.image_mode_on"
|
||||
if stored_config.config["markdown_image"]
|
||||
else "liteyuki.image_mode_off"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# @on_alconna(
|
||||
# command=Alconna(
|
||||
# "liteyuki-docs",
|
||||
# ),
|
||||
# aliases={"轻雪文档"},
|
||||
# ).handle()
|
||||
# # Satori OK
|
||||
# async def _(matcher: Matcher):
|
||||
# await matcher.finish("https://bot.liteyuki.icu/usage")
|
||||
|
||||
|
||||
@on_alconna(
|
||||
command=Alconna(
|
||||
@ -318,62 +220,6 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
|
||||
await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
|
||||
|
||||
|
||||
# system hook
|
||||
@Bot.on_calling_api # 图片模式检测
|
||||
async def test_for_md_image(bot: T_Bot, api: str, data: dict):
|
||||
# 截获大图发送,转换为markdown发送
|
||||
if (
|
||||
api in ["send_msg", "send_private_msg", "send_group_msg"]
|
||||
and markdown_image
|
||||
and data.get("user_id") != bot.self_id
|
||||
):
|
||||
if (
|
||||
api == "send_msg"
|
||||
and data.get("message_type") == "private"
|
||||
or api == "send_private_msg"
|
||||
):
|
||||
session_type = "private"
|
||||
session_id = data.get("user_id")
|
||||
elif (
|
||||
api == "send_msg"
|
||||
and data.get("message_type") == "group"
|
||||
or api == "send_group_msg"
|
||||
):
|
||||
session_type = "group"
|
||||
session_id = data.get("group_id")
|
||||
else:
|
||||
return
|
||||
if (
|
||||
len(data.get("message", [])) == 1
|
||||
and data["message"][0].get("type") == "image"
|
||||
):
|
||||
file: str = data["message"][0].data.get("file")
|
||||
# file:// http:// base64://
|
||||
if file.startswith("http"):
|
||||
result = await md.send_md(
|
||||
await md.image_async(file),
|
||||
bot,
|
||||
message_type=session_type,
|
||||
session_id=session_id,
|
||||
)
|
||||
elif file.startswith("file"):
|
||||
file = file.replace("file://", "")
|
||||
result = await md.send_image(
|
||||
open(file, "rb").read(),
|
||||
bot,
|
||||
message_type=session_type,
|
||||
session_id=session_id,
|
||||
)
|
||||
elif file.startswith("base64"):
|
||||
file_bytes = base64.b64decode(file.replace("base64://", ""))
|
||||
result = await md.send_image(
|
||||
file_bytes, bot, message_type=session_type, session_id=session_id
|
||||
)
|
||||
else:
|
||||
return
|
||||
raise MockApiException(result=result)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def on_startup():
|
||||
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
|
||||
@ -426,6 +272,14 @@ async def _(bot: T_Bot):
|
||||
group_id=reload_session_id,
|
||||
message=return_msg,
|
||||
)
|
||||
elif isinstance(bot, onebot.v12.Bot):
|
||||
await bot.send_msg(
|
||||
message_type=reload_session_type,
|
||||
user_id=reload_session_id,
|
||||
group_id=reload_session_id,
|
||||
message=return_msg,
|
||||
detail_type="group",
|
||||
)
|
||||
else:
|
||||
await bot.call_api(
|
||||
"send_msg",
|
||||
@ -445,7 +299,6 @@ async def every_day_update():
|
||||
if result:
|
||||
await broadcast_to_superusers(f"灵温已更新:```\n{logs}\n```")
|
||||
nonebot.logger.info(f"灵温已更新:{logs}")
|
||||
# ProcessingManager.restart(3)
|
||||
reload()
|
||||
else:
|
||||
nonebot.logger.info(logs)
|
||||
|
@ -1,33 +0,0 @@
|
||||
import nonebot
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
|
||||
from liteyuki.bot import get_bot
|
||||
from src.utils.base import reload
|
||||
from src.utils.base.config import get_config
|
||||
from src.utils.base.resource import load_resources
|
||||
|
||||
if get_config("debug", False):
|
||||
|
||||
liteyuki_bot = get_bot()
|
||||
|
||||
res_directories = (
|
||||
"src/resources",
|
||||
"resources",
|
||||
)
|
||||
|
||||
class ResourceModifiedHandler(FileSystemEventHandler):
|
||||
"""
|
||||
Handler for resource file changes
|
||||
"""
|
||||
|
||||
def on_modified(self, event):
|
||||
nonebot.logger.info(f"资源 {event.src_path} 变更,重载资源包……")
|
||||
load_resources()
|
||||
|
||||
resource_modified_handle = ResourceModifiedHandler()
|
||||
|
||||
observer = Observer()
|
||||
for directory in res_directories:
|
||||
observer.schedule(resource_modified_handle, directory, recursive=True)
|
||||
observer.start()
|
@ -8,15 +8,10 @@ from src.utils.base.data_manager import InstalledPlugin, plugin_db
|
||||
from src.utils.base.resource import load_resources
|
||||
from src.utils.message.tools import check_for_package
|
||||
|
||||
from liteyuki import get_bot, chan
|
||||
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
load_resources()
|
||||
init_log()
|
||||
|
||||
driver = get_driver()
|
||||
liteyuki_bot = get_bot()
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@ -32,7 +27,7 @@ async def load_plugins():
|
||||
for installed_plugin in installed_plugins:
|
||||
if not check_for_package(installed_plugin.module_name):
|
||||
nonebot.logger.error(
|
||||
f"插件 {installed_plugin.module_name} 在加载列表中但未安装。请使用超管账户对机器人发送 `npm fixup` 以重新安装。"
|
||||
f"插件 {installed_plugin.module_name} 仍在加载列表中但未安装。"
|
||||
)
|
||||
else:
|
||||
nonebot.load_plugin(installed_plugin.module_name)
|
||||
|
@ -1,3 +0,0 @@
|
||||
# 说明
|
||||
|
||||
此目录为**轻雪插件**目录,非其他插件目录。
|
24
src/liteyuki_plugins/hello_liteyuki.py
Normal file
24
src/liteyuki_plugins/hello_liteyuki.py
Normal file
@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/20 上午5:12
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : liteyuki_reply.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from liteyuki.plugin import PluginMetadata, PluginType
|
||||
from liteyuki.message.on import on_message
|
||||
from liteyuki.message.event import MessageEvent
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="你好轻雪",
|
||||
type=PluginType.APPLICATION
|
||||
)
|
||||
|
||||
|
||||
@on_message().handle()
|
||||
async def _(event: MessageEvent):
|
||||
if str(event.raw_message) == "你好轻雪":
|
||||
event.reply("你好呀")
|
42
src/liteyuki_plugins/lifespan_monitor.py
Normal file
42
src/liteyuki_plugins/lifespan_monitor.py
Normal file
@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
#
|
||||
# @Time : 2024/7/22 上午11:25
|
||||
# @Author : snowykami
|
||||
# @Email : snowykami@outlook.com
|
||||
# @File : asa.py
|
||||
# @Software: PyCharm
|
||||
import asyncio
|
||||
import multiprocessing
|
||||
|
||||
from liteyuki.plugin import PluginMetadata, PluginType
|
||||
from liteyuki import get_bot, logger
|
||||
from liteyuki.comm.channel import get_channel
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="生命周期日志",
|
||||
type=PluginType.SERVICE,
|
||||
)
|
||||
|
||||
bot = get_bot()
|
||||
|
||||
|
||||
@bot.on_before_start
|
||||
def _():
|
||||
logger.info("生命周期监控器:准备启动")
|
||||
|
||||
|
||||
@bot.on_before_process_shutdown
|
||||
def _(name="name"):
|
||||
logger.info("生命周期监控器:准备停止")
|
||||
|
||||
|
||||
@bot.on_before_process_restart
|
||||
def _(name="name"):
|
||||
logger.info("生命周期监控器:准备重启")
|
||||
|
||||
|
||||
@bot.on_after_start
|
||||
async def _():
|
||||
logger.info("生命周期监控器:启动完成")
|
53
src/liteyuki_plugins/liteyukibot_plugin_nonebot/__init__.py
Normal file
53
src/liteyuki_plugins/liteyukibot_plugin_nonebot/__init__.py
Normal file
@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/11 下午5:24
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : __init__.py.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
|
||||
import nonebot
|
||||
from liteyuki.utils import IS_MAIN_PROCESS
|
||||
from liteyuki.plugin import PluginMetadata, PluginType
|
||||
from .nb_utils import adapter_manager, driver_manager # type: ignore
|
||||
from liteyuki.log import logger
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="NoneBot2启动器",
|
||||
type=PluginType.APPLICATION,
|
||||
)
|
||||
|
||||
|
||||
def nb_run(*args, **kwargs):
|
||||
"""
|
||||
初始化NoneBot并运行在子进程
|
||||
Args:
|
||||
**kwargs:
|
||||
|
||||
Returns:
|
||||
"""
|
||||
# 给子进程传递通道对象
|
||||
kwargs.update(kwargs.get("nonebot", {})) # nonebot配置优先
|
||||
nonebot.init(**kwargs)
|
||||
|
||||
driver_manager.init(config=kwargs)
|
||||
adapter_manager.init(kwargs)
|
||||
adapter_manager.register()
|
||||
|
||||
try:
|
||||
# nonebot.load_plugin("nonebot-plugin-lnpm") # 尝试加载轻雪NoneBot插件加载器(Nonebot插件)
|
||||
nonebot.load_plugin("src.liteyuki_main") # 尝试加载轻雪主插件(Nonebot插件)
|
||||
except Exception as e:
|
||||
pass
|
||||
nonebot.run()
|
||||
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
from liteyuki import get_bot
|
||||
from .dev_reloader import *
|
||||
|
||||
liteyuki = get_bot()
|
||||
liteyuki.process_manager.add_target(name="nonebot", target=nb_run, args=(), kwargs=liteyuki.config)
|
@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
NoneBot 开发环境重载监视器
|
||||
"""
|
||||
import os.path
|
||||
|
||||
from liteyuki.dev import observer
|
||||
from liteyuki import get_bot, logger
|
||||
from liteyuki.utils import IS_MAIN_PROCESS
|
||||
from watchdog.events import FileSystemEvent
|
||||
|
||||
|
||||
liteyuki = get_bot()
|
||||
|
||||
exclude_extensions = (".pyc", ".pyo")
|
||||
|
||||
|
||||
@observer.on_file_system_event(
|
||||
directories=("src/nonebot_plugins",),
|
||||
event_filter=lambda event: not event.src_path.endswith(exclude_extensions) and ("__pycache__" not in event.src_path ) and os.path.isfile(event.src_path)
|
||||
)
|
||||
def restart_nonebot_process(event: FileSystemEvent):
|
||||
logger.debug(f"文件 {event.src_path} 已更新,正在重载 nonebot")
|
||||
liteyuki.restart_process("nonebot")
|
@ -7,14 +7,14 @@ from nonebot.adapters import satori
|
||||
|
||||
def init(config: dict):
|
||||
if config.get("satori", None) is None:
|
||||
nonebot.logger.info("未查见 Satori 的配置文档,将跳过 Satori 初始化")
|
||||
nonebot.logger.info("未寻得 Satori 设定信息,跳过初始化")
|
||||
return None
|
||||
satori_config = config.get("satori")
|
||||
if not satori_config.get("enable", False):
|
||||
nonebot.logger.info("未启用 Satori ,将跳过 Satori 初始化")
|
||||
nonebot.logger.info("Satori 未启用,跳过初始化")
|
||||
return None
|
||||
if os.getenv("SATORI_CLIENTS", None) is not None:
|
||||
nonebot.logger.info("Satori 客户端已设入环境变量,跳过此步。")
|
||||
nonebot.logger.info("Satori 客户端已在环境变量中配置,跳过初始化")
|
||||
os.environ["SATORI_CLIENTS"] = json.dumps(satori_config.get("hosts", []), ensure_ascii=False)
|
||||
config['satori_clients'] = satori_config.get("hosts", [])
|
||||
return
|
@ -9,12 +9,12 @@ from .defines import *
|
||||
def auto_set_env(config: dict):
|
||||
dotenv.load_dotenv(".env")
|
||||
if os.getenv("DRIVER", None) is not None:
|
||||
nonebot.logger.info("Driver 已设入环境变量中,将跳过自动配置环节。")
|
||||
nonebot.logger.info("Driver 已在环境变量中配置,跳过自动设定")
|
||||
return
|
||||
if config.get("satori", {'enable': False}).get("enable", False):
|
||||
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER, HTTPX_DRIVER, WEBSOCKETS_DRIVER)
|
||||
nonebot.logger.info("已启用 Satori,将 driver 设为 ASGI+HTTPX+WEBSOCKETS")
|
||||
nonebot.logger.info("启用 Satori,已设定 Driver 为 ASGI+HTTPX+WEBSOCKETS")
|
||||
else:
|
||||
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER)
|
||||
nonebot.logger.info("已禁用 Satori,将 driver 设为 ASGI")
|
||||
nonebot.logger.info("禁用 Satori,已设定 Driver 为 ASGI")
|
||||
return
|
8
src/liteyuki_plugins/process_manager/__init__.py
Normal file
8
src/liteyuki_plugins/process_manager/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
from liteyuki.plugin import PluginMetadata, PluginType
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="进程管理器",
|
||||
author="snowykami",
|
||||
description="进程管理器,用于管理子进程",
|
||||
type=PluginType.SERVICE
|
||||
)
|
@ -11,21 +11,22 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
import json
|
||||
import os.path
|
||||
import platform
|
||||
|
||||
import requests
|
||||
from aiohttp import ClientSession
|
||||
from git import Repo
|
||||
from liteyuki.plugin import PluginMetadata
|
||||
|
||||
from liteyuki.plugin import PluginMetadata, PluginType
|
||||
from liteyuki import get_bot, logger
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="注册服务",
|
||||
type=PluginType.SERVICE
|
||||
)
|
||||
|
||||
liteyuki = get_bot()
|
||||
commit_hash = Repo(".").head.commit.hexsha
|
||||
|
||||
|
||||
def register_bot():
|
||||
async def register_bot():
|
||||
url = "https://api.liteyuki.icu/register"
|
||||
data = {
|
||||
"name" : "尹灵温|轻雪-睿乐",
|
||||
@ -36,17 +37,19 @@ def register_bot():
|
||||
"os" : f"{platform.system()} {platform.version()} {platform.machine()}"
|
||||
}
|
||||
try:
|
||||
logger.info("正在等待 Liteyuki 注册服务器……")
|
||||
resp = requests.post(url, json=data, timeout=(10, 15))
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if liteyuki_id := data.get("liteyuki_id"):
|
||||
with open("data/liteyuki/liteyuki.json", "wb") as f:
|
||||
f.write(json.dumps(data).encode("utf-8"))
|
||||
logger.success("成功将 {} 注册到 Liteyuki 服务器".format(liteyuki_id))
|
||||
else:
|
||||
raise ValueError(f"无法向 Liteyuki 服务器注册:{data}")
|
||||
|
||||
logger.info("正在等待 Liteyuki 注册服务器…")
|
||||
async with ClientSession() as session:
|
||||
async with session.post(url, json=data, timeout=15) as resp:
|
||||
if resp.status == 200:
|
||||
data = await resp.json()
|
||||
if liteyuki_id := data.get("liteyuki_id"):
|
||||
with open("data/liteyuki/liteyuki.json", "wb") as f:
|
||||
f.write(json.dumps(data).encode("utf-8"))
|
||||
logger.success("成功将 {} 注册到 Liteyuki 服务器".format(liteyuki_id))
|
||||
else:
|
||||
raise ValueError(f"无法向 Liteyuki 服务器注册:{data}")
|
||||
else:
|
||||
raise ValueError(f"无法向 Liteyuki 服务器注册:{resp.status}")
|
||||
except Exception as e:
|
||||
logger.warning(f"虽然向 Liteyuki 服务器注册失败,但无关紧要:{e}")
|
||||
|
||||
@ -54,4 +57,6 @@ def register_bot():
|
||||
@liteyuki.on_before_start
|
||||
async def _():
|
||||
if not os.path.exists("data/liteyuki/liteyuki.json"):
|
||||
register_bot()
|
||||
if not os.path.exists("data/liteyuki"):
|
||||
os.makedirs("data/liteyuki")
|
||||
await register_bot()
|
8
src/liteyuki_plugins/resource_loader/__init__.py
Normal file
8
src/liteyuki_plugins/resource_loader/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
from liteyuki.plugin import PluginMetadata, PluginType
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="资源加载器",
|
||||
author="snowykami",
|
||||
description="进程管理器,用于管理子进程",
|
||||
type=PluginType.SERVICE
|
||||
)
|
19
src/liteyuki_plugins/scheduled_tasks/__init__.py
Normal file
19
src/liteyuki_plugins/scheduled_tasks/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/15 下午11:29
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : __init__.py.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from liteyuki.plugin import PluginMetadata, PluginType
|
||||
|
||||
from .divided_by_lifespan import *
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="计划任务",
|
||||
description="计划任务插件,一些杂项任务的计划执行。",
|
||||
type=PluginType.SERVICE
|
||||
)
|
@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/15 下午11:32
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : __init__.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from .after_start import *
|
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/15 下午11:32
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : after_start.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import time
|
||||
|
||||
from liteyuki import get_bot
|
||||
from liteyuki.comm.storage import shared_memory
|
||||
|
||||
liteyuki = get_bot()
|
||||
|
||||
|
||||
@liteyuki.on_before_start
|
||||
def save_startup_timestamp():
|
||||
"""
|
||||
储存启动的时间戳
|
||||
"""
|
||||
startup_timestamp = time.time()
|
||||
shared_memory.set("startup_timestamp", startup_timestamp)
|
@ -1,27 +0,0 @@
|
||||
import multiprocessing
|
||||
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from liteyuki.comm import get_channel
|
||||
from .rt_guide import *
|
||||
from .crt_matchers import *
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="CRT生成工具",
|
||||
description="一些CRT牌子生成器",
|
||||
usage="我觉得你应该会用",
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki" : True,
|
||||
"toggleable" : True,
|
||||
"default_enable": True,
|
||||
}
|
||||
)
|
||||
|
||||
# chan = get_channel("nonebot-passive")
|
||||
#
|
||||
#
|
||||
# @chan.on_receive()
|
||||
# async def _(d):
|
||||
# print("CRT子进程接收到数据:", d)
|
||||
# chan.send("CRT子进程已接收到数据")
|
@ -1,575 +0,0 @@
|
||||
import os
|
||||
import uuid
|
||||
from typing import Tuple, Union, List
|
||||
|
||||
import nonebot
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
|
||||
default_color = (255, 255, 255, 255)
|
||||
default_font = "resources/fonts/MiSans-Semibold.ttf"
|
||||
|
||||
|
||||
def render_canvas_from_json(file: str, background: Image) -> "Canvas":
|
||||
pass
|
||||
|
||||
|
||||
class BasePanel:
|
||||
def __init__(self,
|
||||
uv_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0),
|
||||
box_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0),
|
||||
parent_point: Tuple[float, float] = (0.5, 0.5),
|
||||
point: Tuple[float, float] = (0.5, 0.5)):
|
||||
"""
|
||||
:param uv_size: 底面板大小
|
||||
:param box_size: 子(自身)面板大小
|
||||
:param parent_point: 底面板锚点
|
||||
:param point: 子(自身)面板锚点
|
||||
"""
|
||||
self.canvas: Canvas | None = None
|
||||
self.uv_size = uv_size
|
||||
self.box_size = box_size
|
||||
self.parent_point = parent_point
|
||||
self.point = point
|
||||
self.parent: BasePanel | None = None
|
||||
self.canvas_box: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0)
|
||||
# 此节点在父节点上的盒子
|
||||
self.box = (
|
||||
self.parent_point[0] - self.point[0] * self.box_size[0] / self.uv_size[0],
|
||||
self.parent_point[1] - self.point[1] * self.box_size[1] / self.uv_size[1],
|
||||
self.parent_point[0] + (1 - self.point[0]) * self.box_size[0] / self.uv_size[0],
|
||||
self.parent_point[1] + (1 - self.point[1]) * self.box_size[1] / self.uv_size[1]
|
||||
)
|
||||
|
||||
def load(self, only_calculate=False):
|
||||
"""
|
||||
将对象写入画布
|
||||
此处仅作声明
|
||||
由各子类重写
|
||||
|
||||
:return:
|
||||
"""
|
||||
self.actual_pos = self.canvas_box
|
||||
|
||||
def save_as(self, canvas_box, only_calculate=False):
|
||||
"""
|
||||
此函数执行时间较长,建议异步运行
|
||||
:param only_calculate:
|
||||
:param canvas_box 此节点在画布上的盒子,并不是在父节点上的盒子
|
||||
:return:
|
||||
"""
|
||||
for name, child in self.__dict__.items():
|
||||
# 此节点在画布上的盒子
|
||||
if isinstance(child, BasePanel) and name not in ["canvas", "parent"]:
|
||||
child.parent = self
|
||||
if isinstance(self, Canvas):
|
||||
child.canvas = self
|
||||
else:
|
||||
child.canvas = self.canvas
|
||||
dxc = canvas_box[2] - canvas_box[0]
|
||||
dyc = canvas_box[3] - canvas_box[1]
|
||||
child.canvas_box = (
|
||||
canvas_box[0] + dxc * child.box[0],
|
||||
canvas_box[1] + dyc * child.box[1],
|
||||
canvas_box[0] + dxc * child.box[2],
|
||||
canvas_box[1] + dyc * child.box[3]
|
||||
)
|
||||
child.load(only_calculate)
|
||||
child.save_as(child.canvas_box, only_calculate)
|
||||
|
||||
|
||||
class Canvas(BasePanel):
|
||||
def __init__(self, base_img: Image.Image):
|
||||
self.base_img = base_img
|
||||
self.canvas = self
|
||||
super(Canvas, self).__init__()
|
||||
self.draw_line_list = []
|
||||
|
||||
def export(self, file, alpha=False):
|
||||
self.base_img = self.base_img.convert("RGBA")
|
||||
self.save_as((0, 0, 1, 1))
|
||||
draw = ImageDraw.Draw(self.base_img)
|
||||
for line in self.draw_line_list:
|
||||
draw.line(*line)
|
||||
if not alpha:
|
||||
self.base_img = self.base_img.convert("RGB")
|
||||
self.base_img.save(file)
|
||||
|
||||
def delete(self):
|
||||
os.remove(self.file)
|
||||
|
||||
def get_actual_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]:
|
||||
"""
|
||||
获取控件实际相对大小
|
||||
函数执行时间较长
|
||||
|
||||
:param path: 控件路径
|
||||
:return:
|
||||
"""
|
||||
sub_obj = self
|
||||
self.save_as((0, 0, 1, 1), True)
|
||||
control_path = ""
|
||||
for i, seq in enumerate(path.split(".")):
|
||||
if seq not in sub_obj.__dict__:
|
||||
raise KeyError(f"在{control_path}中找不到控件:{seq}")
|
||||
control_path += f".{seq}"
|
||||
sub_obj = sub_obj.__dict__[seq]
|
||||
return sub_obj.actual_pos
|
||||
|
||||
def get_actual_pixel_size(self, path: str) -> Union[None, Tuple[int, int]]:
|
||||
"""
|
||||
获取控件实际像素长宽
|
||||
函数执行时间较长
|
||||
:param path: 控件路径
|
||||
:return:
|
||||
"""
|
||||
sub_obj = self
|
||||
self.save_as((0, 0, 1, 1), True)
|
||||
control_path = ""
|
||||
for i, seq in enumerate(path.split(".")):
|
||||
if seq not in sub_obj.__dict__:
|
||||
raise KeyError(f"在{control_path}中找不到控件:{seq}")
|
||||
control_path += f".{seq}"
|
||||
sub_obj = sub_obj.__dict__[seq]
|
||||
dx = int(sub_obj.canvas.base_img.size[0] * (sub_obj.actual_pos[2] - sub_obj.actual_pos[0]))
|
||||
dy = int(sub_obj.canvas.base_img.size[1] * (sub_obj.actual_pos[3] - sub_obj.actual_pos[1]))
|
||||
return dx, dy
|
||||
|
||||
def get_actual_pixel_box(self, path: str) -> Union[None, Tuple[int, int, int, int]]:
|
||||
"""
|
||||
获取控件实际像素大小盒子
|
||||
函数执行时间较长
|
||||
:param path: 控件路径
|
||||
:return:
|
||||
"""
|
||||
sub_obj = self
|
||||
self.save_as((0, 0, 1, 1), True)
|
||||
control_path = ""
|
||||
for i, seq in enumerate(path.split(".")):
|
||||
if seq not in sub_obj.__dict__:
|
||||
raise KeyError(f"在{control_path}中找不到控件:{seq}")
|
||||
control_path += f".{seq}"
|
||||
sub_obj = sub_obj.__dict__[seq]
|
||||
x1 = int(sub_obj.canvas.base_img.size[0] * sub_obj.actual_pos[0])
|
||||
y1 = int(sub_obj.canvas.base_img.size[1] * sub_obj.actual_pos[1])
|
||||
x2 = int(sub_obj.canvas.base_img.size[2] * sub_obj.actual_pos[2])
|
||||
y2 = int(sub_obj.canvas.base_img.size[3] * sub_obj.actual_pos[3])
|
||||
return x1, y1, x2, y2
|
||||
|
||||
def get_parent_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]:
|
||||
"""
|
||||
获取控件在父节点的大小
|
||||
函数执行时间较长
|
||||
|
||||
:param path: 控件路径
|
||||
:return:
|
||||
"""
|
||||
sub_obj = self.get_control_by_path(path)
|
||||
on_parent_pos = (
|
||||
(sub_obj.actual_pos[0] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]),
|
||||
(sub_obj.actual_pos[1] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1]),
|
||||
(sub_obj.actual_pos[2] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]),
|
||||
(sub_obj.actual_pos[3] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1])
|
||||
)
|
||||
return on_parent_pos
|
||||
|
||||
def get_control_by_path(self, path: str) -> Union[BasePanel, "Img", "Rectangle", "Text"]:
|
||||
sub_obj = self
|
||||
self.save_as((0, 0, 1, 1), True)
|
||||
control_path = ""
|
||||
for i, seq in enumerate(path.split(".")):
|
||||
if seq not in sub_obj.__dict__:
|
||||
raise KeyError(f"在{control_path}中找不到控件:{seq}")
|
||||
control_path += f".{seq}"
|
||||
sub_obj = sub_obj.__dict__[seq]
|
||||
return sub_obj
|
||||
|
||||
def draw_line(self, path: str, p1: Tuple[float, float], p2: Tuple[float, float], color, width):
|
||||
"""
|
||||
画线
|
||||
|
||||
:param color:
|
||||
:param width:
|
||||
:param path:
|
||||
:param p1:
|
||||
:param p2:
|
||||
:return:
|
||||
"""
|
||||
ac_pos = self.get_actual_box(path)
|
||||
control = self.get_control_by_path(path)
|
||||
dx = ac_pos[2] - ac_pos[0]
|
||||
dy = ac_pos[3] - ac_pos[1]
|
||||
xy_box = int((ac_pos[0] + dx * p1[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p1[1]) * control.canvas.base_img.size[1]), int(
|
||||
(ac_pos[0] + dx * p2[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p2[1]) * control.canvas.base_img.size[1])
|
||||
self.draw_line_list.append((xy_box, color, width))
|
||||
|
||||
|
||||
class Panel(BasePanel):
|
||||
def __init__(self, uv_size, box_size, parent_point, point):
|
||||
super(Panel, self).__init__(uv_size, box_size, parent_point, point)
|
||||
|
||||
|
||||
class TextSegment:
|
||||
def __init__(self, text, **kwargs):
|
||||
if not isinstance(text, str):
|
||||
raise TypeError("请输入字符串")
|
||||
self.text = text
|
||||
self.color = kwargs.get("color", None)
|
||||
self.font = kwargs.get("font", None)
|
||||
|
||||
@staticmethod
|
||||
def text2text_segment_list(text: str):
|
||||
"""
|
||||
暂时没写好
|
||||
|
||||
:param text: %FFFFFFFF%1123%FFFFFFFF%21323
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Text(BasePanel):
|
||||
def __init__(self, uv_size, box_size, parent_point, point, text: Union[str, list], font=default_font, color=(255, 255, 255, 255), vertical=False,
|
||||
line_feed=False, force_size=False, fill=(0, 0, 0, 0), fillet=0, outline=(0, 0, 0, 0), outline_width=0, rectangle_side=0, font_size=None, dp: int = 5,
|
||||
anchor: str = "la"):
|
||||
"""
|
||||
:param uv_size:
|
||||
:param box_size:
|
||||
:param parent_point:
|
||||
:param point:
|
||||
:param text: list[TextSegment] | str
|
||||
:param font:
|
||||
:param color:
|
||||
:param vertical: 是否竖直
|
||||
:param line_feed: 是否换行
|
||||
:param force_size: 强制大小
|
||||
:param dp: 字体大小递减精度
|
||||
:param anchor : https://www.zhihu.com/question/474216280
|
||||
:param fill: 底部填充颜色
|
||||
:param fillet: 填充圆角
|
||||
:param rectangle_side: 边框宽度
|
||||
:param outline: 填充矩形边框颜色
|
||||
:param outline_width: 填充矩形边框宽度
|
||||
"""
|
||||
self.actual_pos = None
|
||||
self.outline_width = outline_width
|
||||
self.outline = outline
|
||||
self.fill = fill
|
||||
self.fillet = fillet
|
||||
self.font = font
|
||||
self.text = text
|
||||
self.color = color
|
||||
self.force_size = force_size
|
||||
self.vertical = vertical
|
||||
self.line_feed = line_feed
|
||||
self.dp = dp
|
||||
self.font_size = font_size
|
||||
self.rectangle_side = rectangle_side
|
||||
self.anchor = anchor
|
||||
super(Text, self).__init__(uv_size, box_size, parent_point, point)
|
||||
|
||||
def load(self, only_calculate=False):
|
||||
"""限制区域像素大小"""
|
||||
if isinstance(self.text, str):
|
||||
self.text = [
|
||||
TextSegment(text=self.text, color=self.color, font=self.font)
|
||||
]
|
||||
all_text = str()
|
||||
for text in self.text:
|
||||
all_text += text.text
|
||||
limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1])
|
||||
font_size = limited_size[1] if self.font_size is None else self.font_size
|
||||
image_font = ImageFont.truetype(self.font, font_size)
|
||||
actual_size = image_font.getsize(all_text)
|
||||
while (actual_size[0] > limited_size[0] or actual_size[1] > limited_size[1]) and not self.force_size:
|
||||
font_size -= self.dp
|
||||
image_font = ImageFont.truetype(self.font, font_size)
|
||||
actual_size = image_font.getsize(all_text)
|
||||
draw = ImageDraw.Draw(self.canvas.base_img)
|
||||
if isinstance(self.parent, Img) or isinstance(self.parent, Text):
|
||||
self.parent.canvas_box = self.parent.actual_pos
|
||||
dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0]
|
||||
dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1]
|
||||
dx1 = actual_size[0] / self.canvas.base_img.size[0]
|
||||
dy1 = actual_size[1] / self.canvas.base_img.size[1]
|
||||
start_point = [
|
||||
int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]),
|
||||
int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1])
|
||||
]
|
||||
self.actual_pos = (
|
||||
start_point[0] / self.canvas.base_img.size[0],
|
||||
start_point[1] / self.canvas.base_img.size[1],
|
||||
(start_point[0] + actual_size[0]) / self.canvas.base_img.size[0],
|
||||
(start_point[1] + actual_size[1]) / self.canvas.base_img.size[1],
|
||||
)
|
||||
self.font_size = font_size
|
||||
if not only_calculate:
|
||||
for text_segment in self.text:
|
||||
if text_segment.color is None:
|
||||
text_segment.color = self.color
|
||||
if text_segment.font is None:
|
||||
text_segment.font = self.font
|
||||
image_font = ImageFont.truetype(font=text_segment.font, size=font_size)
|
||||
if self.fill[-1] > 0:
|
||||
rectangle = Shape.rectangle(size=(actual_size[0] + 2 * self.rectangle_side, actual_size[1] + 2 * self.rectangle_side), fillet=self.fillet, fill=self.fill,
|
||||
width=self.outline_width, outline=self.outline)
|
||||
self.canvas.base_img.paste(im=rectangle, box=(start_point[0] - self.rectangle_side,
|
||||
start_point[1] - self.rectangle_side,
|
||||
start_point[0] + actual_size[0] + self.rectangle_side,
|
||||
start_point[1] + actual_size[1] + self.rectangle_side),
|
||||
mask=rectangle.split()[-1])
|
||||
draw.text((start_point[0] - self.rectangle_side, start_point[1] - self.rectangle_side),
|
||||
text_segment.text, text_segment.color, font=image_font, anchor=self.anchor)
|
||||
text_width = image_font.getsize(text_segment.text)
|
||||
start_point[0] += text_width[0]
|
||||
|
||||
|
||||
class Img(BasePanel):
|
||||
def __init__(self, uv_size, box_size, parent_point, point, img: Image.Image, keep_ratio=True):
|
||||
self.img_base_img = img
|
||||
self.keep_ratio = keep_ratio
|
||||
super(Img, self).__init__(uv_size, box_size, parent_point, point)
|
||||
|
||||
def load(self, only_calculate=False):
|
||||
self.preprocess()
|
||||
self.img_base_img = self.img_base_img.convert("RGBA")
|
||||
limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), \
|
||||
int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1])
|
||||
|
||||
if self.keep_ratio:
|
||||
"""保持比例"""
|
||||
actual_ratio = self.img_base_img.size[0] / self.img_base_img.size[1]
|
||||
limited_ratio = limited_size[0] / limited_size[1]
|
||||
if actual_ratio >= limited_ratio:
|
||||
# 图片过长
|
||||
self.img_base_img = self.img_base_img.resize(
|
||||
(int(self.img_base_img.size[0] * limited_size[0] / self.img_base_img.size[0]),
|
||||
int(self.img_base_img.size[1] * limited_size[0] / self.img_base_img.size[0]))
|
||||
)
|
||||
else:
|
||||
self.img_base_img = self.img_base_img.resize(
|
||||
(int(self.img_base_img.size[0] * limited_size[1] / self.img_base_img.size[1]),
|
||||
int(self.img_base_img.size[1] * limited_size[1] / self.img_base_img.size[1]))
|
||||
)
|
||||
|
||||
else:
|
||||
"""不保持比例"""
|
||||
self.img_base_img = self.img_base_img.resize(limited_size)
|
||||
|
||||
# 占比长度
|
||||
if isinstance(self.parent, Img) or isinstance(self.parent, Text):
|
||||
self.parent.canvas_box = self.parent.actual_pos
|
||||
|
||||
dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0]
|
||||
dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1]
|
||||
|
||||
dx1 = self.img_base_img.size[0] / self.canvas.base_img.size[0]
|
||||
dy1 = self.img_base_img.size[1] / self.canvas.base_img.size[1]
|
||||
start_point = (
|
||||
int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]),
|
||||
int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1])
|
||||
)
|
||||
alpha = self.img_base_img.split()[3]
|
||||
self.actual_pos = (
|
||||
start_point[0] / self.canvas.base_img.size[0],
|
||||
start_point[1] / self.canvas.base_img.size[1],
|
||||
(start_point[0] + self.img_base_img.size[0]) / self.canvas.base_img.size[0],
|
||||
(start_point[1] + self.img_base_img.size[1]) / self.canvas.base_img.size[1],
|
||||
)
|
||||
if not only_calculate:
|
||||
self.canvas.base_img.paste(self.img_base_img, start_point, alpha)
|
||||
|
||||
def preprocess(self):
|
||||
pass
|
||||
|
||||
|
||||
class Rectangle(Img):
|
||||
def __init__(self, uv_size, box_size, parent_point, point, fillet: Union[int, float] = 0, img: Union[Image.Image] = None, keep_ratio=True,
|
||||
color=default_color, outline_width=0, outline_color=default_color):
|
||||
"""
|
||||
圆角图
|
||||
:param uv_size:
|
||||
:param box_size:
|
||||
:param parent_point:
|
||||
:param point:
|
||||
:param fillet: 圆角半径浮点或整数
|
||||
:param img:
|
||||
:param keep_ratio:
|
||||
"""
|
||||
self.fillet = fillet
|
||||
self.color = color
|
||||
self.outline_width = outline_width
|
||||
self.outline_color = outline_color
|
||||
super(Rectangle, self).__init__(uv_size, box_size, parent_point, point, img, keep_ratio)
|
||||
|
||||
def preprocess(self):
|
||||
limited_size = (int(self.canvas.base_img.size[0] * (self.canvas_box[2] - self.canvas_box[0])),
|
||||
int(self.canvas.base_img.size[1] * (self.canvas_box[3] - self.canvas_box[1])))
|
||||
if not self.keep_ratio and self.img_base_img is not None and self.img_base_img.size[0] / self.img_base_img.size[1] != limited_size[0] / limited_size[1]:
|
||||
self.img_base_img = self.img_base_img.resize(limited_size)
|
||||
self.img_base_img = Shape.rectangle(size=limited_size, fillet=self.fillet, fill=self.color, width=self.outline_width, outline=self.outline_color)
|
||||
|
||||
|
||||
class Color:
|
||||
GREY = (128, 128, 128, 255)
|
||||
RED = (255, 0, 0, 255)
|
||||
GREEN = (0, 255, 0, 255)
|
||||
BLUE = (0, 0, 255, 255)
|
||||
YELLOW = (255, 255, 0, 255)
|
||||
PURPLE = (255, 0, 255, 255)
|
||||
CYAN = (0, 255, 255, 255)
|
||||
WHITE = (255, 255, 255, 255)
|
||||
BLACK = (0, 0, 0, 255)
|
||||
|
||||
@staticmethod
|
||||
def hex2dec(colorHex: str) -> Tuple[int, int, int, int]:
|
||||
"""
|
||||
:param colorHex: FFFFFFFF (ARGB)-> (R, G, B, A)
|
||||
:return:
|
||||
"""
|
||||
return int(colorHex[2:4], 16), int(colorHex[4:6], 16), int(colorHex[6:8], 16), int(colorHex[0:2], 16)
|
||||
|
||||
|
||||
class Shape:
|
||||
@staticmethod
|
||||
def circular(radius: int, fill: tuple, width: int = 0, outline: tuple = Color.BLACK) -> Image.Image:
|
||||
"""
|
||||
:param radius: 半径(像素)
|
||||
:param fill: 填充颜色
|
||||
:param width: 轮廓粗细(像素)
|
||||
:param outline: 轮廓颜色
|
||||
:return: 圆形Image对象
|
||||
"""
|
||||
img = Image.new("RGBA", (radius * 2, radius * 2), color=radius)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.ellipse(xy=(0, 0, radius * 2, radius * 2), fill=fill, outline=outline, width=width)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def rectangle(size: Tuple[int, int], fill: tuple, width: int = 0, outline: tuple = Color.BLACK, fillet: int = 0) -> Image.Image:
|
||||
"""
|
||||
:param fillet: 圆角半径(像素)
|
||||
:param size: 长宽(像素)
|
||||
:param fill: 填充颜色
|
||||
:param width: 轮廓粗细(像素)
|
||||
:param outline: 轮廓颜色
|
||||
:return: 矩形Image对象
|
||||
"""
|
||||
img = Image.new("RGBA", size, color=fill)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.rounded_rectangle(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline, width=width, radius=fillet)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def ellipse(size: Tuple[int, int], fill: tuple, outline: int = 0, outline_color: tuple = Color.BLACK) -> Image.Image:
|
||||
"""
|
||||
:param size: 长宽(像素)
|
||||
:param fill: 填充颜色
|
||||
:param outline: 轮廓粗细(像素)
|
||||
:param outline_color: 轮廓颜色
|
||||
:return: 椭圆Image对象
|
||||
"""
|
||||
img = Image.new("RGBA", size, color=fill)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.ellipse(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline_color, width=outline)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def polygon(points: List[Tuple[int, int]], fill: tuple, outline: int, outline_color: tuple) -> Image.Image:
|
||||
"""
|
||||
:param points: 多边形顶点列表
|
||||
:param fill: 填充颜色
|
||||
:param outline: 轮廓粗细(像素)
|
||||
:param outline_color: 轮廓颜色
|
||||
:return: 多边形Image对象
|
||||
"""
|
||||
img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.polygon(xy=points, fill=fill, outline=outline_color, width=outline)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def line(points: List[Tuple[int, int]], fill: tuple, width: int) -> Image:
|
||||
"""
|
||||
:param points: 线段顶点列表
|
||||
:param fill: 填充颜色
|
||||
:param width: 线段粗细(像素)
|
||||
:return: 线段Image对象
|
||||
"""
|
||||
img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.line(xy=points, fill=fill, width=width)
|
||||
return img
|
||||
|
||||
|
||||
class Utils:
|
||||
|
||||
@staticmethod
|
||||
def central_clip_by_ratio(img: Image.Image, size: Tuple, use_cache=True):
|
||||
"""
|
||||
:param use_cache: 是否使用缓存,剪切过一次后默认生成缓存
|
||||
:param img:
|
||||
:param size: 仅为比例,满填充裁剪
|
||||
:return:
|
||||
"""
|
||||
cache_file_path = str()
|
||||
if use_cache:
|
||||
filename_without_end = ".".join(os.path.basename(img.fp.name).split(".")[0:-1]) + f"_{size[0]}x{size[1]}" + ".png"
|
||||
cache_file_path = os.path.join(".cache", filename_without_end)
|
||||
if os.path.exists(cache_file_path):
|
||||
nonebot.logger.info("本次使用缓存加载图片,不裁剪")
|
||||
return Image.open(os.path.join(".cache", filename_without_end))
|
||||
img_ratio = img.size[0] / img.size[1]
|
||||
limited_ratio = size[0] / size[1]
|
||||
if limited_ratio > img_ratio:
|
||||
actual_size = (
|
||||
img.size[0],
|
||||
img.size[0] / size[0] * size[1]
|
||||
)
|
||||
box = (
|
||||
0, (img.size[1] - actual_size[1]) // 2,
|
||||
img.size[0], img.size[1] - (img.size[1] - actual_size[1]) // 2
|
||||
)
|
||||
else:
|
||||
actual_size = (
|
||||
img.size[1] / size[1] * size[0],
|
||||
img.size[1],
|
||||
)
|
||||
box = (
|
||||
(img.size[0] - actual_size[0]) // 2, 0,
|
||||
img.size[0] - (img.size[0] - actual_size[0]) // 2, img.size[1]
|
||||
)
|
||||
img = img.crop(box).resize(size)
|
||||
if use_cache:
|
||||
img.save(cache_file_path)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def circular_clip(img: Image.Image):
|
||||
"""
|
||||
裁剪为alpha圆形
|
||||
|
||||
:param img:
|
||||
:return:
|
||||
"""
|
||||
length = min(img.size)
|
||||
alpha_cover = Image.new("RGBA", (length, length), color=(0, 0, 0, 0))
|
||||
if img.size[0] > img.size[1]:
|
||||
box = (
|
||||
(img.size[0] - img[1]) // 2, 0,
|
||||
(img.size[0] - img[1]) // 2 + img.size[1], img.size[1]
|
||||
)
|
||||
else:
|
||||
box = (
|
||||
0, (img.size[1] - img.size[0]) // 2,
|
||||
img.size[0], (img.size[1] - img.size[0]) // 2 + img.size[0]
|
||||
)
|
||||
img = img.crop(box).resize((length, length))
|
||||
draw = ImageDraw.Draw(alpha_cover)
|
||||
draw.ellipse(xy=(0, 0, length, length), fill=(255, 255, 255, 255))
|
||||
alpha = alpha_cover.split()[-1]
|
||||
img.putalpha(alpha)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def open_img(path) -> Image.Image:
|
||||
return Image.open(path, "RGBA")
|
@ -1,78 +0,0 @@
|
||||
from urllib.parse import quote
|
||||
|
||||
import aiohttp
|
||||
from nonebot import require
|
||||
|
||||
from src.utils.event import get_user_id
|
||||
from src.utils.base.language import Language
|
||||
from src.utils.base.ly_typing import T_MessageEvent
|
||||
from src.utils.base.resource import get_path
|
||||
from src.utils.message.html_tool import template2image
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
|
||||
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, Option
|
||||
|
||||
crt_cmd = on_alconna(
|
||||
Alconna(
|
||||
"crt",
|
||||
Subcommand(
|
||||
"route",
|
||||
Args["start", str, "沙坪坝"]["end", str, "上新街"],
|
||||
alias=("r",),
|
||||
help_text="查询两地之间的地铁路线"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@crt_cmd.assign("route")
|
||||
async def _(result: Arparma, event: T_MessageEvent):
|
||||
# 获取语言
|
||||
ulang = Language(get_user_id(event))
|
||||
|
||||
# 获取参数
|
||||
# 你也别问我为什么要quote两次,问就是CRT官网的锅,只有这样才可以运行
|
||||
start = quote(quote(result.other_args.get("start")))
|
||||
end = quote(quote(result.other_args.get("end")))
|
||||
|
||||
# 判断参数语言
|
||||
query_lang_code = ""
|
||||
if start.isalpha() and end.isalpha():
|
||||
query_lang_code = "Eng"
|
||||
|
||||
# 构造请求 URL
|
||||
url = f"https://www.cqmetro.cn/Front/html/TakeLine!queryYs{query_lang_code}TakeLine.action?entity.startStaName={start}&entity.endStaName={end}"
|
||||
|
||||
# 请求数据
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
result = await resp.json()
|
||||
|
||||
# 检查结果/无则终止
|
||||
if not result.get("result"):
|
||||
await crt_cmd.send(ulang.get("crt.no_result"))
|
||||
return
|
||||
|
||||
# 模板传参定义
|
||||
templates = {
|
||||
"data" : {
|
||||
"result": result["result"],
|
||||
},
|
||||
"localization": ulang.get_many(
|
||||
"crt.station",
|
||||
"crt.hour",
|
||||
"crt.minute",
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
# 生成图片
|
||||
image = await template2image(
|
||||
template=get_path("templates/crt_route.html"),
|
||||
templates=templates,
|
||||
debug=True
|
||||
)
|
||||
|
||||
# 发送图片
|
||||
await crt_cmd.send(UniMessage.image(raw=image))
|
@ -1,419 +0,0 @@
|
||||
import json
|
||||
from typing import List, Any
|
||||
|
||||
from PIL import Image
|
||||
from arclet.alconna import Alconna
|
||||
from nb_cli import run_sync
|
||||
from nonebot import on_command
|
||||
from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, Args, MultiVar, Arparma, UniMessage
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .canvas import *
|
||||
from ...utils.base.resource import get_path
|
||||
|
||||
resolution = 256
|
||||
|
||||
|
||||
class Entrance(BaseModel):
|
||||
identifier: str
|
||||
size: tuple[int, int]
|
||||
dest: List[str]
|
||||
|
||||
|
||||
class Station(BaseModel):
|
||||
identifier: str
|
||||
chineseName: str
|
||||
englishName: str
|
||||
position: tuple[int, int]
|
||||
|
||||
|
||||
class Line(BaseModel):
|
||||
identifier: str
|
||||
chineseName: str
|
||||
englishName: str
|
||||
color: Any
|
||||
stations: List["Station"]
|
||||
|
||||
|
||||
font_light = get_path("templates/fonts/MiSans/MiSans-Light.woff2")
|
||||
font_bold = get_path("templates/fonts/MiSans/MiSans-Bold.woff2")
|
||||
|
||||
@run_sync
|
||||
def generate_entrance_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
|
||||
reso: int = resolution):
|
||||
"""
|
||||
Generates an entrance sign for the ride.
|
||||
"""
|
||||
width, height = ratio[0] * reso, ratio[1] * reso
|
||||
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.WHITE))
|
||||
# 加黑色图框
|
||||
baseCanvas.outline = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0, 0),
|
||||
point=(0, 0),
|
||||
img=Shape.rectangle(
|
||||
size=(width, height),
|
||||
fillet=0,
|
||||
fill=(0, 0, 0, 0),
|
||||
width=15,
|
||||
outline=Color.BLACK
|
||||
)
|
||||
)
|
||||
|
||||
baseCanvas.contentPanel = Panel(
|
||||
uv_size=(width, height),
|
||||
box_size=(width - 28, height - 28),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
)
|
||||
|
||||
linePanelHeight = 0.7 * ratio[1]
|
||||
linePanelWidth = linePanelHeight * 1.3
|
||||
|
||||
# 画线路面板部分
|
||||
|
||||
for i, line in enumerate(lineInfo):
|
||||
linePanel = baseCanvas.contentPanel.__dict__[f"Line_{i}_Panel"] = Panel(
|
||||
uv_size=ratio,
|
||||
box_size=(linePanelWidth, linePanelHeight),
|
||||
parent_point=(i * linePanelWidth / ratio[0], 1),
|
||||
point=(0, 1),
|
||||
)
|
||||
|
||||
linePanel.colorCube = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.15, 1),
|
||||
parent_point=(0.125, 1),
|
||||
point=(0, 1),
|
||||
img=Shape.rectangle(
|
||||
size=(100, 100),
|
||||
fillet=0,
|
||||
fill=line.color,
|
||||
),
|
||||
keep_ratio=False
|
||||
)
|
||||
|
||||
textPanel = linePanel.TextPanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.625, 1),
|
||||
parent_point=(1, 1),
|
||||
point=(1, 1)
|
||||
)
|
||||
|
||||
# 中文线路名
|
||||
textPanel.namePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 2 / 3),
|
||||
parent_point=(0, 0),
|
||||
point=(0, 0),
|
||||
)
|
||||
nameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.namePanel".format(i))
|
||||
textPanel.namePanel.text = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
text=line.chineseName,
|
||||
color=Color.BLACK,
|
||||
font_size=int(nameSize[1] * 0.5),
|
||||
force_size=True,
|
||||
font=font_bold
|
||||
|
||||
)
|
||||
|
||||
# 英文线路名
|
||||
textPanel.englishNamePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1 / 3),
|
||||
parent_point=(0, 1),
|
||||
point=(0, 1),
|
||||
)
|
||||
englishNameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.englishNamePanel".format(i))
|
||||
textPanel.englishNamePanel.text = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
text=line.englishName,
|
||||
color=Color.BLACK,
|
||||
font_size=int(englishNameSize[1] * 0.6),
|
||||
force_size=True,
|
||||
font=font_light
|
||||
)
|
||||
|
||||
# 画名称部分
|
||||
namePanel = baseCanvas.contentPanel.namePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 0.4),
|
||||
parent_point=(0.5, 0),
|
||||
point=(0.5, 0),
|
||||
)
|
||||
|
||||
namePanel.text = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
text=name,
|
||||
color=Color.BLACK,
|
||||
font_size=int(height * 0.3),
|
||||
force_size=True,
|
||||
font=font_bold
|
||||
)
|
||||
|
||||
aliasesPanel = baseCanvas.contentPanel.aliasesPanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 0.5),
|
||||
parent_point=(0.5, 1),
|
||||
point=(0.5, 1),
|
||||
|
||||
)
|
||||
for j, alias in enumerate(aliases):
|
||||
aliasesPanel.__dict__[alias] = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.35, 0.5),
|
||||
parent_point=(0.5, 0.5 * j),
|
||||
point=(0.5, 0),
|
||||
text=alias,
|
||||
color=Color.BLACK,
|
||||
font_size=int(height * 0.15),
|
||||
font=font_light
|
||||
)
|
||||
|
||||
# 画入口标识
|
||||
entrancePanel = baseCanvas.contentPanel.entrancePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.2, 1),
|
||||
parent_point=(1, 0.5),
|
||||
point=(1, 0.5),
|
||||
)
|
||||
# 中文文本
|
||||
entrancePanel.namePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 0.5),
|
||||
parent_point=(1, 0),
|
||||
point=(1, 0),
|
||||
)
|
||||
entrancePanel.namePanel.text = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0, 0.5),
|
||||
point=(0, 0.5),
|
||||
text=f"{entranceIdentifier}出入口",
|
||||
color=Color.BLACK,
|
||||
font_size=int(height * 0.2),
|
||||
force_size=True,
|
||||
font=font_bold
|
||||
)
|
||||
# 英文文本
|
||||
entrancePanel.englishNamePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 0.5),
|
||||
parent_point=(1, 1),
|
||||
point=(1, 1),
|
||||
)
|
||||
entrancePanel.englishNamePanel.text = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0, 0.5),
|
||||
point=(0, 0.5),
|
||||
text=f"Entrance {entranceIdentifier}",
|
||||
color=Color.BLACK,
|
||||
font_size=int(height * 0.15),
|
||||
force_size=True,
|
||||
font=font_light
|
||||
)
|
||||
|
||||
return baseCanvas.base_img.tobytes()
|
||||
|
||||
|
||||
crt_alc = on_alconna(
|
||||
Alconna(
|
||||
"crt",
|
||||
Subcommand(
|
||||
"entrance",
|
||||
Args["name", str]["lines", str, ""]["entrance", int, 1], # /crt entrance 璧山&Bishan 1号线&Line1&#ff0000,27号线&Line1&#ff0000 1A
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@crt_alc.assign("entrance")
|
||||
async def _(result: Arparma):
|
||||
args = result.subcommands.get("entrance").args
|
||||
name = args["name"]
|
||||
lines = args["lines"]
|
||||
entrance = args["entrance"]
|
||||
line_info = []
|
||||
for line in lines.split(","):
|
||||
line_args = line.split("&")
|
||||
line_info.append(Line(
|
||||
identifier=1,
|
||||
chineseName=line_args[0],
|
||||
englishName=line_args[1],
|
||||
color=line_args[2],
|
||||
stations=[]
|
||||
))
|
||||
img_bytes = await generate_entrance_sign(
|
||||
name=name,
|
||||
aliases=name.split("&"),
|
||||
lineInfo=line_info,
|
||||
entranceIdentifier=entrance,
|
||||
ratio=(8, 1),
|
||||
reso=256,
|
||||
)
|
||||
await crt_alc.finish(
|
||||
UniMessage.image(raw=img_bytes)
|
||||
)
|
||||
|
||||
|
||||
def generate_platform_line_pic(line: Line, station: Station, ratio=None, reso: int = resolution):
|
||||
"""
|
||||
生成站台线路图
|
||||
:param line: 线路对象
|
||||
:param station: 本站点对象
|
||||
:param ratio: 比例
|
||||
:param reso: 分辨率,1:reso
|
||||
:return: 两个方向的站牌
|
||||
"""
|
||||
if ratio is None:
|
||||
ratio = [4, 1]
|
||||
width, height = ratio[0] * reso, ratio[1] * reso
|
||||
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.YELLOW))
|
||||
# 加黑色图框
|
||||
baseCanvas.linePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.8, 0.15),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
)
|
||||
|
||||
# 直线块
|
||||
baseCanvas.linePanel.recLine = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=Shape.rectangle(
|
||||
size=(10, 10),
|
||||
fill=line.color,
|
||||
),
|
||||
keep_ratio=False
|
||||
)
|
||||
# 灰色直线块
|
||||
baseCanvas.linePanel.recLineGrey = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=Shape.rectangle(
|
||||
size=(10, 10),
|
||||
fill=Color.GREY,
|
||||
),
|
||||
keep_ratio=False
|
||||
)
|
||||
# 生成各站圆点
|
||||
outline_width = 40
|
||||
circleForward = Shape.circular(
|
||||
radius=200,
|
||||
fill=Color.WHITE,
|
||||
width=outline_width,
|
||||
outline=line.color,
|
||||
)
|
||||
|
||||
circleThisPanel = Canvas(Image.new("RGBA", (200, 200), (0, 0, 0, 0)))
|
||||
circleThisPanel.circleOuter = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=Shape.circular(
|
||||
radius=200,
|
||||
fill=Color.WHITE,
|
||||
width=outline_width,
|
||||
outline=line.color,
|
||||
),
|
||||
)
|
||||
circleThisPanel.circleOuter.circleInner = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.7, 0.7),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=Shape.circular(
|
||||
radius=200,
|
||||
fill=line.color,
|
||||
width=0,
|
||||
outline=line.color,
|
||||
),
|
||||
)
|
||||
|
||||
circleThisPanel.export("a.png", alpha=True)
|
||||
circleThis = circleThisPanel.base_img
|
||||
|
||||
circlePassed = Shape.circular(
|
||||
radius=200,
|
||||
fill=Color.WHITE,
|
||||
width=outline_width,
|
||||
outline=Color.GREY,
|
||||
)
|
||||
|
||||
arrival = False
|
||||
distance = 1 / (len(line.stations) - 1)
|
||||
for i, sta in enumerate(line.stations):
|
||||
box_size = (1.618, 1.618)
|
||||
if sta.identifier == station.identifier:
|
||||
arrival = True
|
||||
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1.8, 1.8),
|
||||
parent_point=(distance * i, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=circleThis,
|
||||
keep_ratio=True
|
||||
)
|
||||
continue
|
||||
if arrival:
|
||||
# 后方站绘制
|
||||
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=box_size,
|
||||
parent_point=(distance * i, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=circleForward,
|
||||
keep_ratio=True
|
||||
)
|
||||
else:
|
||||
# 前方站绘制
|
||||
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=box_size,
|
||||
parent_point=(distance * i, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=circlePassed,
|
||||
keep_ratio=True
|
||||
)
|
||||
return baseCanvas
|
||||
|
||||
|
||||
def generate_platform_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
|
||||
reso: int = resolution
|
||||
):
|
||||
pass
|
||||
|
||||
# def main():
|
||||
# generate_entrance_sign(
|
||||
# "璧山",
|
||||
# aliases=["Bishan"],
|
||||
# lineInfo=[
|
||||
#
|
||||
# Line(identifier="2", chineseName="1号线", englishName="Line 1", color=Color.RED, stations=[]),
|
||||
# Line(identifier="3", chineseName="27号线", englishName="Line 27", color="#685bc7", stations=[]),
|
||||
# Line(identifier="1", chineseName="璧铜线", englishName="BT Line", color="#685BC7", stations=[]),
|
||||
# ],
|
||||
# entranceIdentifier="1",
|
||||
# ratio=(8, 1)
|
||||
# )
|
||||
#
|
||||
#
|
||||
# main()
|
@ -1,15 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from .minesweeper import *
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="轻雪小游戏",
|
||||
description="内置了一些小游戏",
|
||||
usage="",
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki": True,
|
||||
"toggleable" : True,
|
||||
"default_enable" : True,
|
||||
}
|
||||
)
|
@ -1,168 +0,0 @@
|
||||
import random
|
||||
from pydantic import BaseModel
|
||||
from src.utils.message.message import MarkdownMessage as md
|
||||
|
||||
class Dot(BaseModel):
|
||||
row: int
|
||||
col: int
|
||||
mask: bool = True
|
||||
value: int = 0
|
||||
flagged: bool = False
|
||||
|
||||
|
||||
class Minesweeper:
|
||||
# 0-8: number of mines around, 9: mine, -1: undefined
|
||||
NUMS = "⓪①②③④⑤⑥⑦⑧🅑⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
|
||||
MASK = "🅜"
|
||||
FLAG = "🅕"
|
||||
MINE = "🅑"
|
||||
|
||||
def __init__(self, rows, cols, num_mines, session_type, session_id):
|
||||
assert rows > 0 and cols > 0 and 0 < num_mines < rows * cols
|
||||
self.session_type = session_type
|
||||
self.session_id = session_id
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.num_mines = num_mines
|
||||
self.board: list[list[Dot]] = [[Dot(row=i, col=j) for j in range(cols)] for i in range(rows)]
|
||||
self.is_first = True
|
||||
|
||||
def reveal(self, row, col) -> bool:
|
||||
"""
|
||||
展开
|
||||
Args:
|
||||
row:
|
||||
col:
|
||||
|
||||
Returns:
|
||||
游戏是否继续
|
||||
|
||||
"""
|
||||
|
||||
if self.is_first:
|
||||
# 第一次展开,生成地雷
|
||||
self.generate_board(self.board[row][col])
|
||||
self.is_first = False
|
||||
|
||||
if self.board[row][col].value == 9:
|
||||
self.board[row][col].mask = False
|
||||
return False
|
||||
|
||||
if not self.board[row][col].mask:
|
||||
return True
|
||||
|
||||
self.board[row][col].mask = False
|
||||
|
||||
if self.board[row][col].value == 0:
|
||||
self.reveal_neighbors(row, col)
|
||||
return True
|
||||
|
||||
def is_win(self) -> bool:
|
||||
"""
|
||||
是否胜利
|
||||
Returns:
|
||||
"""
|
||||
for row in range(self.rows):
|
||||
for col in range(self.cols):
|
||||
if self.board[row][col].mask and self.board[row][col].value != 9:
|
||||
return False
|
||||
return True
|
||||
|
||||
def generate_board(self, first_dot: Dot):
|
||||
"""
|
||||
避开第一个点,生成地雷
|
||||
Args:
|
||||
first_dot: 第一个点
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
generate_count = 0
|
||||
while generate_count < self.num_mines:
|
||||
row = random.randint(0, self.rows - 1)
|
||||
col = random.randint(0, self.cols - 1)
|
||||
if self.board[row][col].value == 9 or (row, col) == (first_dot.row, first_dot.col):
|
||||
continue
|
||||
self.board[row][col] = Dot(row=row, col=col, mask=True, value=9)
|
||||
generate_count += 1
|
||||
|
||||
for row in range(self.rows):
|
||||
for col in range(self.cols):
|
||||
if self.board[row][col].value != 9:
|
||||
self.board[row][col].value = self.count_adjacent_mines(row, col)
|
||||
|
||||
def count_adjacent_mines(self, row, col):
|
||||
"""
|
||||
计算周围地雷数量
|
||||
Args:
|
||||
row:
|
||||
col:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
count = 0
|
||||
for r in range(max(0, row - 1), min(self.rows, row + 2)):
|
||||
for c in range(max(0, col - 1), min(self.cols, col + 2)):
|
||||
if self.board[r][c].value == 9:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def reveal_neighbors(self, row, col):
|
||||
"""
|
||||
递归展开,使用深度优先搜索
|
||||
Args:
|
||||
row:
|
||||
col:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
for r in range(max(0, row - 1), min(self.rows, row + 2)):
|
||||
for c in range(max(0, col - 1), min(self.cols, col + 2)):
|
||||
if self.board[r][c].mask:
|
||||
self.board[r][c].mask = False
|
||||
if self.board[r][c].value == 0:
|
||||
self.reveal_neighbors(r, c)
|
||||
|
||||
def mark(self, row, col) -> bool:
|
||||
"""
|
||||
标记
|
||||
Args:
|
||||
row:
|
||||
col:
|
||||
Returns:
|
||||
是否标记成功,如果已经展开则无法标记
|
||||
"""
|
||||
if self.board[row][col].mask:
|
||||
self.board[row][col].flagged = not self.board[row][col].flagged
|
||||
return self.board[row][col].flagged
|
||||
|
||||
def board_markdown(self) -> str:
|
||||
"""
|
||||
打印地雷板
|
||||
Returns:
|
||||
"""
|
||||
dis = " "
|
||||
start = "> " if self.cols >= 10 else ""
|
||||
text = start + self.NUMS[0] + dis*2
|
||||
# 横向两个雷之间的间隔字符
|
||||
# 生成横向索引
|
||||
for i in range(self.cols):
|
||||
text += f"{self.NUMS[i]}" + dis
|
||||
text += "\n\n"
|
||||
for i, row in enumerate(self.board):
|
||||
text += start + f"{self.NUMS[i]}" + dis*2
|
||||
for dot in row:
|
||||
if dot.mask and not dot.flagged:
|
||||
text += md.btn_cmd(self.MASK, f"minesweeper reveal {dot.row} {dot.col}")
|
||||
elif dot.flagged:
|
||||
text += md.btn_cmd(self.FLAG, f"minesweeper mark {dot.row} {dot.col}")
|
||||
else:
|
||||
text += self.NUMS[dot.value]
|
||||
text += dis
|
||||
text += "\n"
|
||||
btn_mark = md.btn_cmd("标记", f"minesweeper mark ", enter=False)
|
||||
btn_end = md.btn_cmd("结束", "minesweeper end", enter=True)
|
||||
text += f" {btn_mark} {btn_end}"
|
||||
return text
|
@ -1,103 +0,0 @@
|
||||
from nonebot import require
|
||||
|
||||
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||
from src.utils.message.message import MarkdownMessage as md
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from .game import Minesweeper
|
||||
|
||||
from nonebot_plugin_alconna import Alconna, on_alconna, Subcommand, Args, Arparma
|
||||
|
||||
minesweeper = on_alconna(
|
||||
aliases={"扫雷"},
|
||||
command=Alconna(
|
||||
"minesweeper",
|
||||
Subcommand(
|
||||
"start",
|
||||
Args["row", int, 8]["col", int, 8]["mines", int, 10],
|
||||
alias=["开始"],
|
||||
|
||||
),
|
||||
Subcommand(
|
||||
"end",
|
||||
alias=["结束"]
|
||||
),
|
||||
Subcommand(
|
||||
"reveal",
|
||||
Args["row", int]["col", int],
|
||||
alias=["展开"]
|
||||
|
||||
),
|
||||
Subcommand(
|
||||
"mark",
|
||||
Args["row", int]["col", int],
|
||||
alias=["标记"]
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
minesweeper_cache: list[Minesweeper] = []
|
||||
|
||||
|
||||
def get_minesweeper_cache(event: T_MessageEvent) -> Minesweeper | None:
|
||||
for i in minesweeper_cache:
|
||||
if i.session_type == event.message_type:
|
||||
if i.session_id == event.user_id or i.session_id == event.group_id:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
@minesweeper.handle()
|
||||
async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
|
||||
game = get_minesweeper_cache(event)
|
||||
if result.subcommands.get("start"):
|
||||
if game:
|
||||
await minesweeper.finish("当前会话不能同时进行多个扫雷游戏")
|
||||
else:
|
||||
try:
|
||||
new_game = Minesweeper(
|
||||
rows=result.subcommands["start"].args["row"],
|
||||
cols=result.subcommands["start"].args["col"],
|
||||
num_mines=result.subcommands["start"].args["mines"],
|
||||
session_type=event.message_type,
|
||||
session_id=event.user_id if event.message_type == "private" else event.group_id,
|
||||
)
|
||||
minesweeper_cache.append(new_game)
|
||||
await minesweeper.send("游戏开始")
|
||||
await md.send_md(new_game.board_markdown(), bot, event=event)
|
||||
except AssertionError:
|
||||
await minesweeper.finish("参数错误")
|
||||
elif result.subcommands.get("end"):
|
||||
if game:
|
||||
minesweeper_cache.remove(game)
|
||||
await minesweeper.finish("游戏结束")
|
||||
else:
|
||||
await minesweeper.finish("当前没有扫雷游戏")
|
||||
elif result.subcommands.get("reveal"):
|
||||
if not game:
|
||||
await minesweeper.finish("当前没有扫雷游戏")
|
||||
else:
|
||||
row = result.subcommands["reveal"].args["row"]
|
||||
col = result.subcommands["reveal"].args["col"]
|
||||
if not (0 <= row < game.rows and 0 <= col < game.cols):
|
||||
await minesweeper.finish("参数错误")
|
||||
if not game.reveal(row, col):
|
||||
minesweeper_cache.remove(game)
|
||||
await md.send_md(game.board_markdown(), bot, event=event)
|
||||
await minesweeper.finish("游戏结束")
|
||||
await md.send_md(game.board_markdown(), bot, event=event)
|
||||
if game.is_win():
|
||||
minesweeper_cache.remove(game)
|
||||
await minesweeper.finish("游戏胜利")
|
||||
elif result.subcommands.get("mark"):
|
||||
if not game:
|
||||
await minesweeper.finish("当前没有扫雷游戏")
|
||||
else:
|
||||
row = result.subcommands["mark"].args["row"]
|
||||
col = result.subcommands["mark"].args["col"]
|
||||
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 md.send_md(game.board_markdown(), bot, event=event)
|
||||
else:
|
||||
await minesweeper.finish("参数错误")
|
@ -14,17 +14,21 @@ from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import Plugin, PluginMetadata
|
||||
from nonebot.utils import run_sync
|
||||
|
||||
|
||||
from src.utils.base.data_manager import InstalledPlugin
|
||||
from src.utils.base.language import get_user_lang
|
||||
from src.utils.base.ly_typing import T_Bot
|
||||
from src.utils.message.message import MarkdownMessage as md
|
||||
from src.utils.message.markdown import MarkdownComponent as mdc, compile_md, escape_md
|
||||
from src.utils.base.permission import GROUP_ADMIN, GROUP_OWNER
|
||||
from src.utils.message.tools import clamp
|
||||
from src.utils.message.message import MarkdownMessage as md
|
||||
from src.utils.message.markdown import MarkdownComponent as mdc, compile_md, escape_md
|
||||
from src.utils.message.html_tool import md_to_pic
|
||||
from .common import *
|
||||
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from nonebot_plugin_alconna import (
|
||||
UniMessage,
|
||||
on_alconna,
|
||||
Alconna,
|
||||
Args,
|
||||
@ -147,7 +151,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
||||
session_id = event.group_id
|
||||
new_event = event
|
||||
else:
|
||||
raise FinishedException(ulang.get("Permission Denied"))
|
||||
raise FinishedException(ulang.get("liteyuki.permission_denied"))
|
||||
|
||||
session_enable = get_plugin_session_enable(
|
||||
new_event, plugin_name
|
||||
@ -292,7 +296,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
||||
reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}"
|
||||
else:
|
||||
reply = ulang.get("npm.search_no_result")
|
||||
await md.send_md(reply, bot, event=event)
|
||||
img_bytes = await md_to_pic(reply)
|
||||
await UniMessage.send(UniMessage.image(raw=img_bytes))
|
||||
|
||||
elif sc.get("install") and perm_s:
|
||||
plugin_name: str = result.subcommands["install"].args.get("plugin_name")
|
||||
@ -320,7 +325,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
||||
info = md.escape(
|
||||
ulang.get("npm.install_success", NAME=store_plugin.name)
|
||||
) # markdown转义
|
||||
await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event)
|
||||
await npm.send(f"{info}\n\n" + f"\n{log}\n")
|
||||
else:
|
||||
await npm.finish(
|
||||
ulang.get(
|
||||
@ -331,12 +336,12 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
||||
info = ulang.get(
|
||||
"npm.load_failed", NAME=plugin_name, HOMEPAGE=homepage_btn
|
||||
).replace("_", r"\\_")
|
||||
await md.send_md(f"{info}\n\n" f"```\n{log}\n```\n", bot, event=event)
|
||||
await npm.finish(f"{info}\n\n" f"```\n{log}\n```\n")
|
||||
else:
|
||||
info = ulang.get(
|
||||
"npm.install_failed", NAME=plugin_name, HOMEPAGE=homepage_btn
|
||||
).replace("_", r"\\_")
|
||||
await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event)
|
||||
await npm.send(f"{info}\n\n" f"```\n{log}\n```")
|
||||
|
||||
elif sc.get("uninstall") and perm_s:
|
||||
plugin_name: str = result.subcommands["uninstall"].args.get("plugin_name") # type: ignore
|
||||
@ -464,7 +469,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
||||
else ulang.get("npm.next_page")
|
||||
)
|
||||
reply += f"\n{btn_prev} {page}/{total} {btn_next}"
|
||||
await md.send_md(reply, bot, event=event)
|
||||
img_bytes = await md_to_pic(reply)
|
||||
await UniMessage.send(UniMessage.image(raw=img_bytes))
|
||||
|
||||
else:
|
||||
if await SUPERUSER(bot, event):
|
||||
@ -517,7 +523,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
||||
f"\n\n>page为页数,num为每页显示数量"
|
||||
f"\n\n>*{md.escape('npm list [page] [num]')}*"
|
||||
)
|
||||
await md.send_md(reply, bot, event=event)
|
||||
img_bytes = await md_to_pic(reply)
|
||||
await UniMessage.send(UniMessage.image(raw=img_bytes))
|
||||
else:
|
||||
|
||||
btn_list = md.btn_cmd(
|
||||
@ -539,7 +546,8 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
||||
f"\n\n>page为页数,num为每页显示数量"
|
||||
f"\n\n>*{md.escape('npm list [page] [num]')}*"
|
||||
)
|
||||
await md.send_md(reply, bot, event=event)
|
||||
img_bytes = await md_to_pic(reply)
|
||||
await UniMessage.send(UniMessage.image(raw=img_bytes))
|
||||
|
||||
|
||||
@on_alconna(
|
||||
@ -554,7 +562,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
||||
Subcommand(
|
||||
disable,
|
||||
Args["group_id", str, None],
|
||||
alias=["d", "停用", "禁用"],
|
||||
alias=["d", "停用"],
|
||||
),
|
||||
),
|
||||
permission=SUPERUSER | GROUP_OWNER | GROUP_ADMIN,
|
||||
@ -679,7 +687,7 @@ async def _(result: Arparma, matcher: Matcher, event: T_MessageEvent, bot: T_Bot
|
||||
else mdc.paragraph(ulang.get("npm.homepage"))
|
||||
),
|
||||
]
|
||||
await md.send_md(compile_md(reply), bot, event=event)
|
||||
await matcher.finish(compile_md(reply))
|
||||
else:
|
||||
await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
|
||||
else:
|
||||
|
@ -198,7 +198,7 @@ async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma, matcher: Matcher
|
||||
else:
|
||||
pass
|
||||
if send_as_md:
|
||||
await md.send_md(reply, bot, event=event)
|
||||
await matcher.send(reply)
|
||||
else:
|
||||
if reply:
|
||||
await matcher.finish(reply)
|
||||
|
@ -2,7 +2,6 @@ import nonebot
|
||||
|
||||
from nonebot.message import event_preprocessor
|
||||
|
||||
# from nonebot_plugin_alconna.typings import Event
|
||||
from src.utils.base.ly_typing import T_MessageEvent
|
||||
from src.utils import satori_utils
|
||||
from nonebot.adapters import satori
|
||||
|
@ -1,163 +0,0 @@
|
||||
import datetime
|
||||
import time
|
||||
|
||||
import aiohttp
|
||||
from nonebot import require
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from src.utils.base.config import get_config
|
||||
from src.utils.base.data import Database, LiteModel
|
||||
from src.utils.base.resource import get_path
|
||||
from src.utils.message.html_tool import template2image
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
require("nonebot_plugin_apscheduler")
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot_plugin_alconna import Alconna, AlconnaResult, CommandResult, Subcommand, UniMessage, on_alconna, Args
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="签名服务器状态",
|
||||
description="适用于ntqq的签名状态查看",
|
||||
usage=(
|
||||
"sign count 查看当前签名数\n"
|
||||
"sign data 查看签名数变化\n"
|
||||
"sign chart [limit] 查看签名数变化图表\n"
|
||||
),
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki" : True,
|
||||
"toggleable" : True,
|
||||
"default_enable": True,
|
||||
}
|
||||
)
|
||||
|
||||
SIGN_COUNT_URLS: dict[str, str] = get_config("sign_count_urls", None)
|
||||
SIGN_COUNT_DURATION = get_config("sign_count_duration", 10)
|
||||
|
||||
|
||||
class SignCount(LiteModel):
|
||||
TABLE_NAME: str = "sign_count"
|
||||
time: float = 0.0
|
||||
count: int = 0
|
||||
sid: str = ""
|
||||
|
||||
|
||||
sign_db = Database("data/liteyuki/ntqq_sign.ldb")
|
||||
sign_db.auto_migrate(SignCount())
|
||||
|
||||
sign_status = on_alconna(Alconna(
|
||||
"sign",
|
||||
Subcommand(
|
||||
"chart",
|
||||
Args["limit", int, 10000]
|
||||
),
|
||||
Subcommand(
|
||||
"count"
|
||||
),
|
||||
Subcommand(
|
||||
"data"
|
||||
)
|
||||
))
|
||||
|
||||
cache_img: bytes = None
|
||||
|
||||
|
||||
@sign_status.assign("count")
|
||||
async def _():
|
||||
reply = "Current sign count:"
|
||||
for name, count in (await get_now_sign()).items():
|
||||
reply += f"\n{name}: {count[1]}"
|
||||
await sign_status.send(reply)
|
||||
|
||||
|
||||
@sign_status.assign("data")
|
||||
async def _():
|
||||
query_stamp = [1, 5, 10, 15]
|
||||
|
||||
reply = "QPS from last " + ", ".join([str(i) for i in query_stamp]) + "mins"
|
||||
for name, url in SIGN_COUNT_URLS.items():
|
||||
count_data = []
|
||||
for stamp in query_stamp:
|
||||
count_rows = sign_db.where_all(SignCount(), "sid = ? and time > ?", url, time.time() - 60 * stamp)
|
||||
if len(count_rows) < 2:
|
||||
count_data.append(-1)
|
||||
else:
|
||||
count_data.append((count_rows[-1].count - count_rows[0].count)/(stamp*60))
|
||||
reply += f"\n{name}: " + ", ".join([f"{i:.1f}" for i in count_data])
|
||||
await sign_status.send(reply)
|
||||
|
||||
|
||||
@sign_status.assign("chart")
|
||||
async def _(arp: CommandResult = AlconnaResult()):
|
||||
limit = arp.result.subcommands.get("chart").args.get("limit")
|
||||
if limit == 10000:
|
||||
if cache_img:
|
||||
await sign_status.send(UniMessage.image(raw=cache_img))
|
||||
return
|
||||
img = await generate_chart(limit)
|
||||
await sign_status.send(UniMessage.image(raw=img))
|
||||
|
||||
|
||||
@scheduler.scheduled_job("interval", seconds=SIGN_COUNT_DURATION, next_run_time=datetime.datetime.now())
|
||||
async def update_sign_count():
|
||||
global cache_img
|
||||
if not SIGN_COUNT_URLS:
|
||||
return
|
||||
data = await get_now_sign()
|
||||
for name, count in data.items():
|
||||
await save_sign_count(count[0], count[1], SIGN_COUNT_URLS[name])
|
||||
|
||||
cache_img = await generate_chart(10000)
|
||||
|
||||
|
||||
async def get_now_sign() -> dict[str, tuple[float, int]]:
|
||||
"""
|
||||
Get the sign count and the time of the latest sign
|
||||
Returns:
|
||||
tuple[float, int] | None: (time, count)
|
||||
"""
|
||||
data = {}
|
||||
now = time.time()
|
||||
async with aiohttp.ClientSession() as client:
|
||||
for name, url in SIGN_COUNT_URLS.items():
|
||||
async with client.get(url) as resp:
|
||||
count = (await resp.json())["count"]
|
||||
data[name] = (now, count)
|
||||
return data
|
||||
|
||||
|
||||
async def save_sign_count(timestamp: float, count: int, sid: str):
|
||||
"""
|
||||
Save the sign count to the database
|
||||
Args:
|
||||
sid: the sign id, use url as the id
|
||||
count:
|
||||
timestamp (float): the time of the sign count (int): the count of the sign
|
||||
"""
|
||||
sign_db.save(SignCount(time=timestamp, count=count, sid=sid))
|
||||
|
||||
|
||||
async def generate_chart(limit):
|
||||
data = []
|
||||
for name, url in SIGN_COUNT_URLS.items():
|
||||
count_rows = sign_db.where_all(SignCount(), "sid = ? ORDER BY id DESC LIMIT ?", url, limit)
|
||||
count_rows.reverse()
|
||||
data.append(
|
||||
{
|
||||
"name" : name,
|
||||
# "data": [[row.time, row.count] for row in count_rows]
|
||||
"times" : [row.time for row in count_rows],
|
||||
"counts": [row.count for row in count_rows]
|
||||
}
|
||||
)
|
||||
|
||||
img = await template2image(
|
||||
template=get_path("templates/sign_status.html"),
|
||||
templates={
|
||||
"data": data
|
||||
},
|
||||
)
|
||||
|
||||
return img
|
@ -61,6 +61,8 @@ async def get_stat_msg_image(
|
||||
condition_args.append(user_id)
|
||||
|
||||
msg_rows = msg_db.where_all(MessageEventModel(), condition, *condition_args)
|
||||
if not msg_rows:
|
||||
msg_rows = []
|
||||
timestamps = []
|
||||
msg_count = []
|
||||
msg_rows.sort(key=lambda x: x.time)
|
||||
@ -157,8 +159,8 @@ async def get_stat_rank_image(
|
||||
templates = {
|
||||
"data": {
|
||||
"name": ulang.get("stat.rank")
|
||||
+ f" 类别:{rank_type}"
|
||||
+ f" 制约:{limit}",
|
||||
+ f" Type {rank_type}"
|
||||
+ f" Limit {limit}",
|
||||
"ranking": ranking,
|
||||
}
|
||||
}
|
||||
|
@ -96,8 +96,10 @@ async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
|
||||
bot_id = result.other_args.get("bot_id")
|
||||
user_id = result.other_args.get("user_id")
|
||||
|
||||
if group_id in ["current", "c"]:
|
||||
if group_id in ["current", "c"] and hasattr(event, "group_id"):
|
||||
group_id = str(event_utils.get_group_id(event))
|
||||
else:
|
||||
group_id = "all"
|
||||
|
||||
if group_id in ["all", "a"]:
|
||||
group_id = "all"
|
||||
|
@ -7,7 +7,8 @@ from cpuinfo import cpuinfo
|
||||
from nonebot import require
|
||||
from nonebot.adapters import satori
|
||||
|
||||
from src.utils import __NAME__, __VERSION__
|
||||
from src.utils import __NAME__
|
||||
from liteyuki import __version__
|
||||
from src.utils.base.config import get_config
|
||||
from src.utils.base.data_manager import TempConfig, common_db
|
||||
from src.utils.base.language import Language
|
||||
@ -227,11 +228,19 @@ async def get_hardware_data() -> dict:
|
||||
pass
|
||||
swap = psutil.swap_memory()
|
||||
cpu_brand_raw = cpuinfo.get_cpu_info().get("brand_raw", "未知处理器")
|
||||
if "AMD" in cpu_brand_raw:
|
||||
if "amd" in cpu_brand_raw.lower():
|
||||
brand = "AMD"
|
||||
elif "Intel" in cpu_brand_raw:
|
||||
elif "intel" in cpu_brand_raw:
|
||||
brand = "英特尔"
|
||||
elif "Nvidia" in cpu_brand_raw:
|
||||
elif "apple" in cpu_brand_raw.lower():
|
||||
brand = "苹果"
|
||||
elif "qualcomm" in cpu_brand_raw.lower():
|
||||
brand = "高通"
|
||||
elif "mediatek" in cpu_brand_raw.lower():
|
||||
brand = "联发科"
|
||||
elif "samsung" in cpu_brand_raw.lower():
|
||||
brand = "三星"
|
||||
elif "nvidia" in cpu_brand_raw.lower():
|
||||
brand = "英伟达"
|
||||
else:
|
||||
brand = "未知处理器"
|
||||
@ -262,7 +271,9 @@ async def get_hardware_data() -> dict:
|
||||
for disk in psutil.disk_partitions(all=True):
|
||||
try:
|
||||
disk_usage = psutil.disk_usage(disk.mountpoint)
|
||||
if disk_usage.total == 0:
|
||||
if disk_usage.total == 0 or disk.mountpoint.startswith(
|
||||
("/var", "/boot", "/run", "/proc", "/sys", "/dev", "/tmp", "/snap")
|
||||
):
|
||||
continue # 虚拟磁盘
|
||||
result["disk"].append(
|
||||
{
|
||||
@ -283,7 +294,7 @@ async def get_liteyuki_data() -> dict:
|
||||
temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
|
||||
result = {
|
||||
"name": list(get_config("nickname", [__NAME__]))[0],
|
||||
"version": f"{__VERSION__}{'-' + commit_hash[:7] if (commit_hash and len(commit_hash) > 8) else ''}",
|
||||
"version": f"{__version__}{'-' + commit_hash[:7] if (commit_hash and len(commit_hash) > 8) else ''}",
|
||||
"plugins": len(nonebot.get_loaded_plugins()),
|
||||
"resources": len(get_loaded_resource_packs()),
|
||||
"nonebot": f"{nonebot.__version__}",
|
||||
|
@ -1,7 +1,6 @@
|
||||
import datetime
|
||||
|
||||
import aiohttp
|
||||
import httpx
|
||||
import nonebot
|
||||
from nonebot import require
|
||||
from nonebot.exception import IgnoredException
|
||||
|
@ -5,15 +5,24 @@ from nonebot import require
|
||||
|
||||
from src.utils.base.data import LiteModel, Database
|
||||
from src.utils.base.data_manager import User, user_db, group_db
|
||||
from src.utils.base.language import Language, change_user_lang, get_all_lang, get_user_lang
|
||||
from src.utils.base.language import (
|
||||
Language,
|
||||
change_user_lang,
|
||||
get_all_lang,
|
||||
get_user_lang,
|
||||
)
|
||||
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||
from src.utils.message.message import MarkdownMessage as md
|
||||
|
||||
# from src.utils.message.html_tool import md_to_pic
|
||||
from .const import representative_timezones_list
|
||||
from src.utils import event as event_utils
|
||||
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
|
||||
|
||||
|
||||
profile_alc = on_alconna(
|
||||
Alconna(
|
||||
"profile",
|
||||
@ -28,7 +37,7 @@ profile_alc = on_alconna(
|
||||
alias=["g", "查询"],
|
||||
),
|
||||
),
|
||||
aliases={"用户信息"}
|
||||
aliases={"用户信息"},
|
||||
)
|
||||
|
||||
|
||||
@ -42,13 +51,21 @@ class Profile(LiteModel):
|
||||
|
||||
@profile_alc.handle()
|
||||
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
user: User = user_db.where_one(User(), "user_id = ?", event_utils.get_user_id(event),
|
||||
default=User(user_id=str(event_utils.get_user_id(event))))
|
||||
user: User = user_db.where_one(
|
||||
User(),
|
||||
"user_id = ?",
|
||||
event_utils.get_user_id(event),
|
||||
default=User(user_id=str(event_utils.get_user_id(event))),
|
||||
)
|
||||
ulang = get_user_lang(str(event_utils.get_user_id(event)))
|
||||
if result.subcommands.get("set"):
|
||||
if result.subcommands["set"].args.get("value"):
|
||||
# 对合法性进行校验后设置
|
||||
r = set_profile(result.args["key"], result.args["value"], str(event_utils.get_user_id(event)))
|
||||
r = set_profile(
|
||||
result.args["key"],
|
||||
result.args["value"],
|
||||
str(event_utils.get_user_id(event)),
|
||||
)
|
||||
if r:
|
||||
user.profile[result.args["key"]] = result.args["value"]
|
||||
user_db.save(user) # 数据库保存
|
||||
@ -56,18 +73,28 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
ulang.get(
|
||||
"user.profile.set_success",
|
||||
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
|
||||
VALUE=result.args["value"]
|
||||
VALUE=result.args["value"],
|
||||
)
|
||||
)
|
||||
else:
|
||||
await profile_alc.finish(ulang.get("user.profile.set_failed", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
|
||||
await profile_alc.finish(
|
||||
ulang.get(
|
||||
"user.profile.set_failed",
|
||||
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
# 未输入值,尝试呼出菜单
|
||||
menu = get_profile_menu(result.args["key"], ulang)
|
||||
if menu:
|
||||
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']}")))
|
||||
await profile_alc.finish(
|
||||
ulang.get(
|
||||
"user.profile.input_value",
|
||||
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
|
||||
)
|
||||
)
|
||||
|
||||
user.profile[result.args["key"]] = result.args["value"]
|
||||
|
||||
@ -92,11 +119,16 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
continue
|
||||
val = profile.dict()[key]
|
||||
key_text = ulang.get(f"user.profile.{key}")
|
||||
btn_set = md.btn_cmd(ulang.get("user.profile.edit"), f"profile set {key}",
|
||||
enter=True if key in enter_attr else False)
|
||||
reply += (f"\n**{key_text}** **{val}**\n"
|
||||
f"\n> {ulang.get(f'user.profile.{key}.desc')}"
|
||||
f"\n> {btn_set} \n\n***\n")
|
||||
btn_set = md.btn_cmd(
|
||||
ulang.get("user.profile.edit"),
|
||||
f"profile set {key}",
|
||||
enter=True if key in enter_attr else False,
|
||||
)
|
||||
reply += (
|
||||
f"\n**{key_text}** **{val}**\n"
|
||||
f"\n> {ulang.get(f'user.profile.{key}.desc')}"
|
||||
f"\n> {btn_set} \n\n***\n"
|
||||
)
|
||||
await md.send_md(reply, bot, event=event)
|
||||
|
||||
|
||||
@ -119,7 +151,9 @@ def get_profile_menu(key: str, ulang: Language) -> Optional[str]:
|
||||
reply = f"**{setting_name} {ulang.get('user.profile.settings')}**\n***\n"
|
||||
if key == "lang":
|
||||
for lang_code, lang_name in get_all_lang().items():
|
||||
btn_set_lang = md.btn_cmd(f"{lang_name}({lang_code})", f"profile set {key} {lang_code}")
|
||||
btn_set_lang = md.btn_cmd(
|
||||
f"{lang_name}({lang_code})", f"profile set {key} {lang_code}"
|
||||
)
|
||||
reply += f"\n{btn_set_lang}\n***\n"
|
||||
elif key == "timezone":
|
||||
for tz in representative_timezones_list:
|
||||
|
55
src/nonebot_plugins/to_liteyuki.py
Normal file
55
src/nonebot_plugins/to_liteyuki.py
Normal file
@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/20 上午5:10
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : to_liteyuki.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from nonebot import Bot, get_bot, on_message, get_driver
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent, Bot
|
||||
|
||||
from liteyuki import Channel
|
||||
from liteyuki.comm import get_channel
|
||||
from liteyuki.comm.storage import shared_memory
|
||||
from liteyuki.message.event import MessageEvent as LiteyukiMessageEvent
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="轻雪push",
|
||||
description="把消息事件传递给轻雪框架进行处理",
|
||||
usage="用户无需使用",
|
||||
)
|
||||
|
||||
recv_channel = Channel[LiteyukiMessageEvent](name="event_to_nonebot")
|
||||
|
||||
|
||||
# @on_message().handle()
|
||||
# async def _(bot: Bot, event: MessageEvent):
|
||||
# liteyuki_event = LiteyukiMessageEvent(
|
||||
# message_type=event.message_type,
|
||||
# message=event.dict()["message"],
|
||||
# raw_message=event.raw_message,
|
||||
# data=event.dict(),
|
||||
# bot_id=bot.self_id,
|
||||
# user_id=str(event.user_id),
|
||||
# session_id=str(event.user_id if event.message_type == "private" else event.group_id),
|
||||
# session_type=event.message_type,
|
||||
# receive_channel=recv_channel,
|
||||
# )
|
||||
# shared_memory.publish("event_to_liteyuki", liteyuki_event)
|
||||
|
||||
|
||||
# @get_driver().on_bot_connect
|
||||
# async def _():
|
||||
# while True:
|
||||
# event = await recv_channel.async_receive()
|
||||
# bot: Bot = get_bot(event.bot_id) # type: ignore
|
||||
# if event.message_type == "private":
|
||||
# await bot.send_private_msg(user_id=int(event.session_id), message=event.data["message"])
|
||||
# elif event.message_type == "group":
|
||||
# await bot.send_group_msg(group_id=int(event.session_id), message=event.data["message"])
|
@ -1,3 +0,0 @@
|
||||
name: Sign Status
|
||||
description: for Lagrange
|
||||
version: 2024.4.26
|
@ -1,4 +0,0 @@
|
||||
.sign-chart {
|
||||
height: 400px;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
// 数据类型声明
|
||||
// import * as echarts from 'echarts';
|
||||
|
||||
let data = JSON.parse(document.getElementById("data").innerText) // object
|
||||
const signChartDivTemplate = document.importNode(document.getElementById("sign-chart-template").content, true)
|
||||
data.forEach((item) => {
|
||||
let signChartDiv = signChartDivTemplate.cloneNode(true)
|
||||
let chartID = item["name"]
|
||||
// 初始化ECharts实例
|
||||
// 设置id
|
||||
signChartDiv.querySelector(".sign-chart").id = chartID
|
||||
document.body.appendChild(signChartDiv)
|
||||
|
||||
let signChart = echarts.init(document.getElementById(chartID))
|
||||
let timeCount = []
|
||||
|
||||
item["counts"].forEach((count, index) => {
|
||||
// 计算平均值,index - 1的count + index的count + index + 1的count /3
|
||||
if (index > 0) {
|
||||
timeCount.push((item["counts"][index] - item["counts"][index - 1]) / (60*(item["times"][index] - item["times"][index - 1])))
|
||||
}
|
||||
})
|
||||
|
||||
console.log(timeCount)
|
||||
|
||||
signChart.setOption(
|
||||
{
|
||||
animation: false,
|
||||
title: {
|
||||
text: item["name"],
|
||||
textStyle: {
|
||||
color: '#000000' // 设置标题文本颜色为红色
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: item["times"].map(timestampToTime),
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
min: Math.min(...item["counts"]),
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
min: Math.min(...timeCount),
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
data: item["counts"],
|
||||
type: 'line',
|
||||
yAxisIndex: 0
|
||||
},
|
||||
{
|
||||
data: timeCount,
|
||||
type: 'line',
|
||||
yAxisIndex: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
function timestampToTime(timestamp) {
|
||||
let date = new Date(timestamp * 1000)
|
||||
let Y = date.getFullYear() + '-'
|
||||
let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
|
||||
let D = date.getDate() + ' '
|
||||
let h = date.getHours() + ':'
|
||||
let m = date.getMinutes() + ':'
|
||||
let s = date.getSeconds()
|
||||
return M + D + h + m + s
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh" xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Liteyuki Status</title>
|
||||
<link rel="stylesheet" href="./css/card.css">
|
||||
<link rel="stylesheet" href="./css/fonts.css">
|
||||
<link rel="stylesheet" href="./css/sign_status.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<template id="sign-chart-template">
|
||||
<div class="info-box sign-chart">
|
||||
</div>
|
||||
</template>
|
||||
<div class="data-storage" id="data">{{ data | tojson }}</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.5.0/echarts.min.js"></script>
|
||||
<script src="./js/sign_status.js"></script>
|
||||
<script src="./js/card.js"></script>
|
||||
</body>
|
@ -1,10 +1,10 @@
|
||||
language.name=文言
|
||||
|
||||
log.debug=试言
|
||||
log.info=讯文
|
||||
log.warning=警示
|
||||
log.error=查误
|
||||
log.success=名成
|
||||
log.debug=试
|
||||
log.info=讯
|
||||
log.warning=警
|
||||
log.error=误
|
||||
log.success=成
|
||||
|
||||
liteyuki.restart=复启
|
||||
liteyuki.restart_now=即复启
|
||||
|
@ -113,6 +113,7 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
.disk-title {
|
||||
position: absolute;
|
||||
color: var(--main-text-color);
|
||||
@ -126,6 +127,31 @@
|
||||
max-width: calc(100% - 40px);
|
||||
z-index: 2;
|
||||
}
|
||||
*/
|
||||
|
||||
.disk-name {
|
||||
position: absolute;
|
||||
color: var(--main-text-color);
|
||||
font-size: 26px;
|
||||
margin-left: 20px;
|
||||
text-align: left;
|
||||
white-space: normal; /* 允许换行 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all; /* 允许在单词内换行 */
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.disk-details {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
/* 调整位置以确保有足够的空间显示 */
|
||||
color: var(--main-text-color);
|
||||
font-size: 24px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#motto-text {
|
||||
font-size: 42px;
|
||||
|
@ -1,9 +1,9 @@
|
||||
const data = JSON.parse(document.getElementById('data').innerText);
|
||||
const bot_data = data['bot']; // 机器人数据
|
||||
const hardwareData = data['hardware']; // 硬件数据
|
||||
const liteyukiData = data['liteyuki']; // LiteYuki数据
|
||||
const localData = data['localization']; // 本地化语言数据
|
||||
const motto_ = data['motto']; // 言论数据
|
||||
const data = JSON.parse(document.getElementById("data").innerText);
|
||||
const bot_data = data["bot"]; // 机器人数据
|
||||
const hardwareData = data["hardware"]; // 硬件数据
|
||||
const liteyukiData = data["liteyuki"]; // LiteYuki数据
|
||||
const localData = data["localization"]; // 本地化语言数据
|
||||
const motto_ = data["motto"]; // 言论数据
|
||||
|
||||
/**
|
||||
* 创建CPU/内存/交换饼图
|
||||
@ -16,52 +16,54 @@ function createPieChartOption(title, data) {
|
||||
animation: false,
|
||||
title: {
|
||||
text: title,
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
left: "center",
|
||||
top: "center",
|
||||
textStyle: {
|
||||
color: '#000',
|
||||
color: "#000",
|
||||
fontSize: 30,
|
||||
lineHeight: 36
|
||||
}
|
||||
lineHeight: 36,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'item',
|
||||
backgroundColor: '#000',
|
||||
trigger: "item",
|
||||
backgroundColor: "#000",
|
||||
},
|
||||
color: data.length === 3 ? ['#053349', '#007ebd', "#00000044"] : ['#007ebd', '#00000044'],
|
||||
color:
|
||||
data.length === 3
|
||||
? ["#053349", "#007ebd", "#00000044"]
|
||||
: ["#007ebd", "#00000044"],
|
||||
series: [
|
||||
{
|
||||
name: 'info',
|
||||
type: 'pie',
|
||||
radius: ['80%', '100%'],
|
||||
center: ['50%', '50%'],
|
||||
name: "info",
|
||||
type: "pie",
|
||||
radius: ["80%", "100%"],
|
||||
center: ["50%", "50%"],
|
||||
itemStyle: {
|
||||
normal: {
|
||||
label: {
|
||||
show: false
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
}
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
textStyle: {
|
||||
fontSize: '50',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}
|
||||
}
|
||||
fontSize: "50",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
}
|
||||
data: data,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function convertSize(size, precision = 2, addUnit = true, suffix = " X字节") {
|
||||
let isNegative = size < 0;
|
||||
size = Math.abs(size);
|
||||
@ -81,7 +83,7 @@ function convertSize(size, precision = 2, addUnit = true, suffix = " X字节") {
|
||||
}
|
||||
|
||||
if (addUnit) {
|
||||
return size.toFixed(precision) + suffix.replace('X', unit);
|
||||
return size.toFixed(precision) + suffix.replace("X", unit);
|
||||
} else {
|
||||
return size;
|
||||
}
|
||||
@ -92,206 +94,260 @@ function convertSize(size, precision = 2, addUnit = true, suffix = " X字节") {
|
||||
* @param title
|
||||
* @param percent 数据
|
||||
*/
|
||||
function createBarChart(title, percent) {
|
||||
function createBarChart(title, percent, name) {
|
||||
// percent为百分比,最大值为100
|
||||
let diskDiv = document.createElement('div')
|
||||
diskDiv.setAttribute('class', 'disk-info')
|
||||
diskDiv.style.marginBottom = '20px'
|
||||
let diskDiv = document.createElement("div");
|
||||
diskDiv.setAttribute("class", "disk-info");
|
||||
diskDiv.style.marginBottom = "20px";
|
||||
diskDiv.innerHTML = `
|
||||
<div class="disk-title">${title}</div>
|
||||
<div class="disk-usage" style="width: ${percent}%"></div>
|
||||
`
|
||||
`;
|
||||
updateDiskNameWidth(diskDiv);
|
||||
|
||||
return diskDiv
|
||||
return diskDiv;
|
||||
}
|
||||
|
||||
// 更新 .disk-name 宽度
|
||||
function updateDiskNameWidth(diskInfoElement) {
|
||||
let diskDetails = diskInfoElement.querySelector(".disk-details");
|
||||
let diskName = diskInfoElement.querySelector(".disk-name");
|
||||
let detailsWidth = diskDetails.offsetWidth;
|
||||
let parentWidth = diskInfoElement.offsetWidth;
|
||||
|
||||
let nameMaxWidth = parentWidth - detailsWidth - 20 - 40;
|
||||
diskName.style.maxWidth = `${nameMaxWidth}px`;
|
||||
}
|
||||
|
||||
function secondsToTextTime(seconds) {
|
||||
let days = Math.floor(seconds / 86400)
|
||||
let hours = Math.floor((seconds % 86400) / 3600)
|
||||
let minutes = Math.floor((seconds % 3600) / 60)
|
||||
let seconds_ = Math.floor(seconds % 60)
|
||||
return `${days}${localData['days']} ${hours}${localData['hours']} ${minutes}${localData['minutes']} ${seconds_}${localData['seconds']}`
|
||||
let days = Math.floor(seconds / 86400);
|
||||
let hours = Math.floor((seconds % 86400) / 3600);
|
||||
let minutes = Math.floor((seconds % 3600) / 60);
|
||||
let seconds_ = Math.floor(seconds % 60);
|
||||
return `${days}${localData["days"]} ${hours}${localData["hours"]} ${minutes}${localData["minutes"]} ${seconds_}${localData["seconds"]}`;
|
||||
}
|
||||
|
||||
// 主函数
|
||||
function main() {
|
||||
// 添加机器人信息
|
||||
bot_data['bots'].forEach(
|
||||
(bot) => {
|
||||
let botInfoDiv = document.importNode(document.getElementById('bot-template').content, true) // 复制模板
|
||||
bot_data["bots"].forEach((bot) => {
|
||||
let botInfoDiv = document.importNode(
|
||||
document.getElementById("bot-template").content,
|
||||
true
|
||||
); // 复制模板
|
||||
|
||||
// 设置机器人信息
|
||||
botInfoDiv.className = "info-box bot-info";
|
||||
|
||||
// 设置机器人信息
|
||||
botInfoDiv.className = 'info-box bot-info'
|
||||
|
||||
botInfoDiv.querySelector('.bot-icon-img').setAttribute('src', bot['icon'])
|
||||
botInfoDiv.querySelector('.bot-name').innerText = bot['name']
|
||||
let tagArray = [
|
||||
bot['protocol_name'],
|
||||
`${bot['app_name']}`,
|
||||
`${localData['groups']}${bot['groups']}`,
|
||||
`${localData['friends']}${bot['friends']}`,
|
||||
`${localData['message_sent']}${bot['message_sent']}`,
|
||||
`${localData['message_received']}${bot['message_received']}`,
|
||||
]
|
||||
// 添加一些标签
|
||||
tagArray.forEach(
|
||||
(tag, index) => {
|
||||
let tagSpan = document.createElement('span')
|
||||
tagSpan.className = 'bot-tag'
|
||||
tagSpan.innerText = tag
|
||||
// 给最后一个标签不添加后缀
|
||||
tagSpan.setAttribute('suffix', (index === 0) || (tag[0] == '\n') ? '0' : '1')
|
||||
botInfoDiv.querySelector('.bot-tags').appendChild(tagSpan)
|
||||
}
|
||||
)
|
||||
document.body.insertBefore(botInfoDiv, document.getElementById('hardware-info')) // 插入对象
|
||||
|
||||
}
|
||||
)
|
||||
botInfoDiv.querySelector(".bot-icon-img").setAttribute("src", bot["icon"]);
|
||||
botInfoDiv.querySelector(".bot-name").innerText = bot["name"];
|
||||
let tagArray = [
|
||||
bot["protocol_name"],
|
||||
`${bot["app_name"]}`,
|
||||
`${localData["groups"]}${bot["groups"]}`,
|
||||
`${localData["friends"]}${bot["friends"]}`,
|
||||
`${localData["message_sent"]}${bot["message_sent"]}`,
|
||||
`${localData["message_received"]}${bot["message_received"]}`,
|
||||
];
|
||||
// 添加一些标签
|
||||
tagArray.forEach((tag, index) => {
|
||||
let tagSpan = document.createElement("span");
|
||||
tagSpan.className = "bot-tag";
|
||||
tagSpan.innerText = tag;
|
||||
// 给最后一个标签不添加后缀
|
||||
tagSpan.setAttribute("suffix", index === 0 || tag[0] == "\n" ? "0" : "1");
|
||||
botInfoDiv.querySelector(".bot-tags").appendChild(tagSpan);
|
||||
});
|
||||
document.body.insertBefore(
|
||||
botInfoDiv,
|
||||
document.getElementById("hardware-info")
|
||||
); // 插入对象
|
||||
});
|
||||
|
||||
// 添加轻雪信息
|
||||
let liteyukiInfoDiv = document.importNode(document.getElementById('bot-template').content, true) // 复制模板
|
||||
liteyukiInfoDiv.className = 'info-box bot-info'
|
||||
liteyukiInfoDiv.querySelector('.bot-icon-img').setAttribute('src', './img/litetrimo.png')
|
||||
liteyukiInfoDiv.querySelector('.bot-name').innerText = `${liteyukiData['name']} - 睿乐`
|
||||
let liteyukiInfoDiv = document.importNode(
|
||||
document.getElementById("bot-template").content,
|
||||
true
|
||||
); // 复制模板
|
||||
liteyukiInfoDiv.className = "info-box bot-info";
|
||||
liteyukiInfoDiv
|
||||
.querySelector(".bot-icon-img")
|
||||
.setAttribute("src", "./img/litetrimo.png");
|
||||
liteyukiInfoDiv.querySelector(
|
||||
".bot-name"
|
||||
).innerText = `${liteyukiData["name"]} - 睿乐`;
|
||||
|
||||
let tagArray = [
|
||||
`灵温 ${liteyukiData['version']}`,
|
||||
`Nonebot ${liteyukiData['nonebot']}`,
|
||||
`${liteyukiData['python']}`,
|
||||
liteyukiData['system'],
|
||||
`${localData['plugins']}${liteyukiData['plugins']}`,
|
||||
`${localData['resources']}${liteyukiData['resources']}`,
|
||||
`${localData['bots']}${liteyukiData['bots']}`,
|
||||
`${localData['runtime']} ${secondsToTextTime(liteyukiData['runtime'])}`,
|
||||
]
|
||||
tagArray.forEach(
|
||||
(tag, index) => {
|
||||
let tagSpan = document.createElement('span')
|
||||
tagSpan.className = 'bot-tag'
|
||||
tagSpan.innerText = tag
|
||||
// 给最后一个标签不添加后缀
|
||||
tagSpan.setAttribute('suffix', (index === 0) || (tag[0] == '\n') ? '0' : '1')
|
||||
liteyukiInfoDiv.querySelector('.bot-tags').appendChild(tagSpan)
|
||||
}
|
||||
)
|
||||
document.body.insertBefore(liteyukiInfoDiv, document.getElementById('hardware-info')) // 插入对象
|
||||
`灵温 ${liteyukiData["version"]}`,
|
||||
`Nonebot ${liteyukiData["nonebot"]}`,
|
||||
`${liteyukiData["python"]}`,
|
||||
liteyukiData["system"],
|
||||
`${localData["plugins"]}${liteyukiData["plugins"]}`,
|
||||
`${localData["resources"]}${liteyukiData["resources"]}`,
|
||||
`${localData["bots"]}${liteyukiData["bots"]}`,
|
||||
`${localData["runtime"]} ${secondsToTextTime(liteyukiData["runtime"])}`,
|
||||
];
|
||||
tagArray.forEach((tag, index) => {
|
||||
let tagSpan = document.createElement("span");
|
||||
tagSpan.className = "bot-tag";
|
||||
tagSpan.innerText = tag;
|
||||
// 给最后一个标签不添加后缀
|
||||
tagSpan.setAttribute("suffix", index === 0 || tag[0] == "\n" ? "0" : "1");
|
||||
liteyukiInfoDiv.querySelector(".bot-tags").appendChild(tagSpan);
|
||||
});
|
||||
document.body.insertBefore(
|
||||
liteyukiInfoDiv,
|
||||
document.getElementById("hardware-info")
|
||||
); // 插入对象
|
||||
|
||||
// 添加硬件信息
|
||||
const cpuData = hardwareData['cpu']
|
||||
const memData = hardwareData['memory']
|
||||
const swapData = hardwareData['swap']
|
||||
const cpuData = hardwareData["cpu"];
|
||||
const memData = hardwareData["memory"];
|
||||
const swapData = hardwareData["swap"];
|
||||
|
||||
const cpuTagArray = [
|
||||
cpuData['name'],
|
||||
`${cpuData['cores']}${localData['cores']} ${cpuData['threads']}${localData['threads']}`,
|
||||
`${(cpuData['freq'] / 1000).toFixed(2)}吉赫兹`
|
||||
]
|
||||
cpuData["name"],
|
||||
`${cpuData["cores"]}${localData["cores"]} ${cpuData["threads"]}${localData["threads"]}`,
|
||||
`${(cpuData["freq"] / 1000).toFixed(2)}吉赫兹`,
|
||||
];
|
||||
|
||||
const memTagArray = [
|
||||
`${localData['process']} ${convertSize(memData['usedProcess'])}`,
|
||||
`${localData['used']} ${convertSize(memData['used'])}`,
|
||||
`${localData['free']} ${convertSize(memData['free'])}`,
|
||||
`${localData['total']} ${convertSize(memData['total'])}`
|
||||
]
|
||||
`${localData["process"]} ${convertSize(memData["usedProcess"])}`,
|
||||
`${localData["used"]} ${convertSize(memData["used"])}`,
|
||||
`${localData["free"]} ${convertSize(memData["free"])}`,
|
||||
`${localData["total"]} ${convertSize(memData["total"])}`,
|
||||
];
|
||||
|
||||
const swapTagArray = [
|
||||
`${localData['used']} ${convertSize(swapData['used'])}`,
|
||||
`${localData['free']} ${convertSize(swapData['free'])}`,
|
||||
`${localData['total']} ${convertSize(swapData['total'])}`
|
||||
]
|
||||
let cpuDeviceInfoDiv = document.importNode(document.getElementById('device-info').content, true)
|
||||
let memDeviceInfoDiv = document.importNode(document.getElementById('device-info').content, true)
|
||||
let swapDeviceInfoDiv = document.importNode(document.getElementById('device-info').content, true)
|
||||
`${localData["used"]} ${convertSize(swapData["used"])}`,
|
||||
`${localData["free"]} ${convertSize(swapData["free"])}`,
|
||||
`${localData["total"]} ${convertSize(swapData["total"])}`,
|
||||
];
|
||||
let cpuDeviceInfoDiv = document.importNode(
|
||||
document.getElementById("device-info").content,
|
||||
true
|
||||
);
|
||||
let memDeviceInfoDiv = document.importNode(
|
||||
document.getElementById("device-info").content,
|
||||
true
|
||||
);
|
||||
let swapDeviceInfoDiv = document.importNode(
|
||||
document.getElementById("device-info").content,
|
||||
true
|
||||
);
|
||||
|
||||
cpuDeviceInfoDiv.querySelector('.device-info').setAttribute('id', 'cpu-info')
|
||||
memDeviceInfoDiv.querySelector('.device-info').setAttribute('id', 'mem-info')
|
||||
swapDeviceInfoDiv.querySelector('.device-info').setAttribute('id', 'swap-info')
|
||||
cpuDeviceInfoDiv.querySelector('.device-chart').setAttribute('id', 'cpu-chart')
|
||||
memDeviceInfoDiv.querySelector('.device-chart').setAttribute('id', 'mem-chart')
|
||||
swapDeviceInfoDiv.querySelector('.device-chart').setAttribute('id', 'swap-chart')
|
||||
cpuDeviceInfoDiv.querySelector(".device-info").setAttribute("id", "cpu-info");
|
||||
memDeviceInfoDiv.querySelector(".device-info").setAttribute("id", "mem-info");
|
||||
swapDeviceInfoDiv
|
||||
.querySelector(".device-info")
|
||||
.setAttribute("id", "swap-info");
|
||||
cpuDeviceInfoDiv
|
||||
.querySelector(".device-chart")
|
||||
.setAttribute("id", "cpu-chart");
|
||||
memDeviceInfoDiv
|
||||
.querySelector(".device-chart")
|
||||
.setAttribute("id", "mem-chart");
|
||||
swapDeviceInfoDiv
|
||||
.querySelector(".device-chart")
|
||||
.setAttribute("id", "swap-chart");
|
||||
|
||||
let devices = {
|
||||
'cpu': cpuDeviceInfoDiv,
|
||||
'mem': memDeviceInfoDiv,
|
||||
'swap': swapDeviceInfoDiv
|
||||
}
|
||||
cpu: cpuDeviceInfoDiv,
|
||||
mem: memDeviceInfoDiv,
|
||||
swap: swapDeviceInfoDiv,
|
||||
};
|
||||
// 遍历添加标签
|
||||
for (let device in devices) {
|
||||
let tagArray = []
|
||||
let tagArray = [];
|
||||
switch (device) {
|
||||
case 'cpu':
|
||||
tagArray = cpuTagArray
|
||||
break
|
||||
case 'mem':
|
||||
tagArray = memTagArray
|
||||
break
|
||||
case 'swap':
|
||||
tagArray = swapTagArray
|
||||
break
|
||||
case "cpu":
|
||||
tagArray = cpuTagArray;
|
||||
break;
|
||||
case "mem":
|
||||
tagArray = memTagArray;
|
||||
break;
|
||||
case "swap":
|
||||
tagArray = swapTagArray;
|
||||
break;
|
||||
}
|
||||
tagArray.forEach(
|
||||
(tag, index) => {
|
||||
let tagDiv = document.createElement('div')
|
||||
tagDiv.className = 'device-tag'
|
||||
tagDiv.innerText = tag
|
||||
// 给最后一个标签不添加后缀
|
||||
tagDiv.setAttribute('suffix', index === tagArray.length - 1 ? '0' : '1')
|
||||
devices[device].querySelector('.device-tags').appendChild(tagDiv)
|
||||
}
|
||||
)
|
||||
tagArray.forEach((tag, index) => {
|
||||
let tagDiv = document.createElement("div");
|
||||
tagDiv.className = "device-tag";
|
||||
tagDiv.innerText = tag;
|
||||
// 给最后一个标签不添加后缀
|
||||
tagDiv.setAttribute("suffix", index === tagArray.length - 1 ? "0" : "1");
|
||||
devices[device].querySelector(".device-tags").appendChild(tagDiv);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 插入
|
||||
document.getElementById('hardware-info').appendChild(cpuDeviceInfoDiv)
|
||||
document.getElementById('hardware-info').appendChild(memDeviceInfoDiv)
|
||||
document.getElementById('hardware-info').appendChild(swapDeviceInfoDiv)
|
||||
document.getElementById("hardware-info").appendChild(cpuDeviceInfoDiv);
|
||||
document.getElementById("hardware-info").appendChild(memDeviceInfoDiv);
|
||||
document.getElementById("hardware-info").appendChild(swapDeviceInfoDiv);
|
||||
|
||||
let cpuChart = echarts.init(document.getElementById('cpu-chart'))
|
||||
let memChart = echarts.init(document.getElementById('mem-chart'))
|
||||
let swapChart = echarts.init(document.getElementById('swap-chart'))
|
||||
let cpuChart = echarts.init(document.getElementById("cpu-chart"));
|
||||
let memChart = echarts.init(document.getElementById("mem-chart"));
|
||||
let swapChart = echarts.init(document.getElementById("swap-chart"));
|
||||
|
||||
cpuChart.setOption(
|
||||
createPieChartOption(
|
||||
`${localData["cpu"]}\n${cpuData["percent"].toFixed(1)}%`,
|
||||
[
|
||||
{ name: "used", value: cpuData["percent"] },
|
||||
{ name: "free", value: 100 - cpuData["percent"] },
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
cpuChart.setOption(createPieChartOption(`${localData['cpu']}\n${cpuData['percent'].toFixed(1)}%`, [
|
||||
{ name: 'used', value: cpuData['percent'] },
|
||||
{ name: 'free', value: 100 - cpuData['percent'] }
|
||||
]))
|
||||
|
||||
memChart.setOption(createPieChartOption(`${localData['memory']}\n${memData['percent'].toFixed(1)}%`, [
|
||||
{ name: 'process', value: memData['usedProcess'] },
|
||||
{ name: 'used', value: memData['used'] - memData['usedProcess'] },
|
||||
{ name: 'free', value: memData['free'] }
|
||||
]))
|
||||
|
||||
|
||||
swapChart.setOption(createPieChartOption(`${localData['swap']}\n${swapData['percent'].toFixed(1)}%`, [
|
||||
{ name: 'used', value: swapData['used'] },
|
||||
{ name: 'free', value: swapData['free'] }
|
||||
]))
|
||||
memChart.setOption(
|
||||
createPieChartOption(
|
||||
`${localData["memory"]}\n${memData["percent"].toFixed(1)}%`,
|
||||
[
|
||||
{ name: "process", value: memData["usedProcess"] },
|
||||
{ name: "used", value: memData["used"] - memData["usedProcess"] },
|
||||
{ name: "free", value: memData["free"] },
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
swapChart.setOption(
|
||||
createPieChartOption(
|
||||
`${localData["swap"]}\n${swapData["percent"].toFixed(1)}%`,
|
||||
[
|
||||
{ name: "used", value: swapData["used"] },
|
||||
{ name: "free", value: swapData["free"] },
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
// 磁盘信息
|
||||
const diskData = hardwareData['disk']
|
||||
diskData.forEach(
|
||||
(disk) => {
|
||||
let diskTitle = `${disk['name']} ${localData['free']} ${convertSize(disk['free'])} ${localData['total']} ${convertSize(disk['total'])}`
|
||||
// 最后一个把margin-bottom去掉
|
||||
let diskDiv = createBarChart(diskTitle, disk['percent'])
|
||||
if (disk === diskData[diskData.length - 1]) {
|
||||
diskDiv.style.marginBottom = '0'
|
||||
}
|
||||
document.getElementById('disk-info').appendChild(createBarChart(diskTitle, disk['percent']))
|
||||
})
|
||||
const diskData = hardwareData["disk"];
|
||||
diskData.forEach((disk) => {
|
||||
let diskTitle = `${localData['free']} ${convertSize(disk['free'])} ${localData['total']} ${convertSize(disk['total'])}`;
|
||||
let diskDiv = createBarChart(diskTitle, disk['percent'], disk['name']);
|
||||
// 最后一个把margin-bottom去掉
|
||||
if (disk === diskData[diskData.length - 1]) {
|
||||
diskDiv.style.marginBottom = "0";
|
||||
}
|
||||
document.getElementById('disk-info').appendChild(diskDiv);
|
||||
});
|
||||
// 随机一言
|
||||
let mottoText = motto_['text']
|
||||
let mottoFrom = motto_['source']
|
||||
document.getElementById('motto-text').innerText = mottoText
|
||||
document.getElementById('motto-from').innerText = mottoFrom
|
||||
let mottoText = motto_["text"];
|
||||
let mottoFrom = motto_["source"];
|
||||
document.getElementById("motto-text").innerText = mottoText;
|
||||
document.getElementById("motto-from").innerText = mottoFrom;
|
||||
// 致谢
|
||||
document.getElementById('addition-info').innerText = '感谢 锅炉 云裳工作室 提供服务器支持'
|
||||
|
||||
document.getElementById("addition-info").innerText =
|
||||
"感谢 锅炉 云裳工作室 提供服务器支持";
|
||||
}
|
||||
|
||||
main()
|
||||
main();
|
||||
/*
|
||||
// 窗口大小改变监听器 -- Debug
|
||||
window.addEventListener('resize', () => {
|
||||
const diskInfos = document.querySelectorAll('.disk-info');
|
||||
diskInfos.forEach(diskInfo => {
|
||||
updateDiskNameWidth(diskInfo);
|
||||
});
|
||||
});
|
||||
*/
|
@ -837,22 +837,6 @@
|
||||
"time": "2023-06-20T16:04:40.706727Z",
|
||||
"skip_test": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_htmlrender",
|
||||
"project_link": "nonebot-plugin-htmlrender",
|
||||
"name": "nonebot-plugin-htmlrender",
|
||||
"desc": "通过浏览器渲染图片",
|
||||
"author": "kexue-z",
|
||||
"homepage": "https://github.com/kexue-z/nonebot-plugin-htmlrender",
|
||||
"tags": [],
|
||||
"is_official": false,
|
||||
"type": "library",
|
||||
"supported_adapters": null,
|
||||
"valid": true,
|
||||
"version": "0.3.1",
|
||||
"time": "2024-03-14T08:47:15.010445Z",
|
||||
"skip_test": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_admin",
|
||||
"project_link": "nonebot-plugin-admin",
|
||||
@ -8718,31 +8702,6 @@
|
||||
"time": "2023-07-14T10:32:08.006009Z",
|
||||
"skip_test": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_templates",
|
||||
"project_link": "nonebot-plugin-templates",
|
||||
"name": "templates_render",
|
||||
"desc": "使用htmlrender和jinja2渲染,使用构建的menu,card或dict进行模板渲染",
|
||||
"author": "canxin121",
|
||||
"homepage": "https://github.com/canxin121/nonebot_plugin_templates",
|
||||
"tags": [
|
||||
{
|
||||
"label": "模板渲染",
|
||||
"color": "#eacd52"
|
||||
},
|
||||
{
|
||||
"label": "图片生成",
|
||||
"color": "#adea52"
|
||||
}
|
||||
],
|
||||
"is_official": false,
|
||||
"type": "library",
|
||||
"supported_adapters": null,
|
||||
"valid": true,
|
||||
"version": "0.1.6",
|
||||
"time": "2023-08-24T09:56:33.184091Z",
|
||||
"skip_test": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot_plugin_pokesomeone",
|
||||
"project_link": "nonebot-plugin-pokesomeone",
|
||||
|
@ -1,13 +1,9 @@
|
||||
import json
|
||||
import os.path
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
|
||||
import nonebot
|
||||
|
||||
__NAME__ = "尹灵温|轻雪-睿乐"
|
||||
__VERSION__ = "6.3.4" # 60201
|
||||
__VERSION__ = "6.3.9" # 60201
|
||||
|
||||
import requests
|
||||
|
||||
@ -15,36 +11,10 @@ from src.utils.base.config import load_from_yaml, config
|
||||
from src.utils.base.log import init_log
|
||||
from git import Repo
|
||||
|
||||
|
||||
major, minor, patch = map(int, __VERSION__.split("."))
|
||||
__VERSION_I__ = 99000000 + major * 10000 + minor * 100 + patch
|
||||
|
||||
|
||||
def register_bot():
|
||||
url = "https://api.liteyuki.icu/register"
|
||||
data = {
|
||||
"name": __NAME__,
|
||||
"version": __VERSION__,
|
||||
"version_i": __VERSION_I__,
|
||||
"python": f"{platform.python_implementation()} {platform.python_version()}",
|
||||
"os": f"{platform.system()} {platform.version()} {platform.machine()}",
|
||||
}
|
||||
try:
|
||||
nonebot.logger.info("正在等待 Liteyuki 注册服务器…")
|
||||
resp = requests.post(url, json=data, timeout=(10, 15))
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if liteyuki_id := data.get("liteyuki_id"):
|
||||
with open("data/liteyuki/liteyuki.json", "wb") as f:
|
||||
f.write(json.dumps(data).encode("utf-8"))
|
||||
nonebot.logger.success(f"成功将 {liteyuki_id} 注册到 Liteyuki 服务器")
|
||||
else:
|
||||
raise ValueError(f"无法向 Liteyuki 服务器注册:{data}")
|
||||
|
||||
except Exception as e:
|
||||
nonebot.logger.warning(f"向 Liteyuki 服务器注册失败,但无所谓:{e}")
|
||||
|
||||
|
||||
def init():
|
||||
"""
|
||||
初始化
|
||||
@ -64,25 +34,15 @@ def init():
|
||||
repo = Repo(".")
|
||||
except Exception as e:
|
||||
nonebot.logger.error(
|
||||
f"无法读取 Git 仓库 {e},你是否是从仓库内下载的Zip文件?请使用git clone。"
|
||||
f"无法读取 Git 仓库 `{e}`,你是否是从仓库直接下载的Zip文件?请使用git clone。"
|
||||
)
|
||||
|
||||
# temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
|
||||
# temp_data.data["start_time"] = time.time()
|
||||
# common_db.save(temp_data)
|
||||
|
||||
# 在加载完成语言后再初始化日志
|
||||
nonebot.logger.info("尹灵温 正在初始化…")
|
||||
|
||||
if not os.path.exists("data/liteyuki/liteyuki.json"):
|
||||
register_bot()
|
||||
|
||||
if not os.path.exists("pyproject.toml"):
|
||||
with open("pyproject.toml", "w", encoding="utf-8") as f:
|
||||
f.write("[tool.nonebot]\n")
|
||||
|
||||
nonebot.logger.info(
|
||||
"正在 {} Python{}.{}.{} 上运行 尹灵温".format(
|
||||
"正在 {} Python{}.{}.{} 上运行 尹灵温-NoneBot".format(
|
||||
sys.executable,
|
||||
sys.version_info.major,
|
||||
sys.version_info.minor,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import threading
|
||||
|
||||
from nonebot import logger
|
||||
from liteyuki.comm.channel import get_channel
|
||||
from liteyuki.comm.channel import active_channel
|
||||
|
||||
|
||||
def reload(delay: float = 0.0, receiver: str = "nonebot"):
|
||||
@ -14,13 +13,9 @@ def reload(delay: float = 0.0, receiver: str = "nonebot"):
|
||||
Returns:
|
||||
|
||||
"""
|
||||
chan = get_channel(receiver + "-active")
|
||||
if chan is None:
|
||||
logger.error(f"未见 Channel {receiver}-active 以至无法重载")
|
||||
return
|
||||
|
||||
if delay > 0:
|
||||
threading.Timer(delay, chan.send, args=(1,)).start()
|
||||
threading.Timer(delay, active_channel.send, args=(1,)).start()
|
||||
return
|
||||
else:
|
||||
chan.send(1)
|
||||
active_channel.send(1)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import os
|
||||
import platform
|
||||
from typing import List
|
||||
|
||||
import nonebot
|
||||
@ -7,6 +8,7 @@ from pydantic import BaseModel
|
||||
|
||||
from ..message.tools import random_hex_string
|
||||
|
||||
|
||||
config = {} # 全局配置,确保加载后读取
|
||||
|
||||
|
||||
@ -29,23 +31,37 @@ class BasicConfig(BaseModel):
|
||||
superusers: list[str] = []
|
||||
command_start: list[str] = ["/", ""]
|
||||
nickname: list[str] = [f"灵温-{random_hex_string(6)}"]
|
||||
default_language: str = "zh-WY"
|
||||
satori: SatoriConfig = SatoriConfig()
|
||||
data_path: str = "data/liteyuki"
|
||||
chromium_path: str = (
|
||||
"/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" # type: ignore
|
||||
if platform.system() == "Darwin"
|
||||
else (
|
||||
"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
|
||||
if platform.system() == "Windows"
|
||||
else "/usr/bin/chromium-browser"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def load_from_yaml(file: str) -> dict:
|
||||
def load_from_yaml(file_: str) -> dict:
|
||||
global config
|
||||
nonebot.logger.debug("Loading config from %s" % file)
|
||||
if not os.path.exists(file):
|
||||
nonebot.logger.warning(f"未找到配置文件 {file} ,已创建默认配置,请修改后重启。")
|
||||
with open(file, "w", encoding="utf-8") as f:
|
||||
nonebot.logger.debug("正在从 {} 中加载配置项".format(file_))
|
||||
if not os.path.exists(file_):
|
||||
nonebot.logger.warning(
|
||||
f"未寻得配置文件 {file_} ,已以默认配置创建,请在重启后更改为你所需的内容。"
|
||||
)
|
||||
with open(file_, "w", encoding="utf-8") as f:
|
||||
yaml.dump(BasicConfig().dict(), f, default_flow_style=False)
|
||||
|
||||
with open(file, "r", encoding="utf-8") as f:
|
||||
with open(file_, "r", encoding="utf-8") as f:
|
||||
conf = init_conf(yaml.load(f, Loader=yaml.FullLoader))
|
||||
config = conf
|
||||
if conf is None:
|
||||
nonebot.logger.warning(f"配置文件 {file} 为空,已创建默认配置,请修改后重启。")
|
||||
nonebot.logger.warning(
|
||||
f"配置文件 {file_} 为空,已以默认配置创建,请在重启后更改为你所需的内容。"
|
||||
)
|
||||
conf = BasicConfig().dict()
|
||||
return conf
|
||||
|
||||
@ -75,7 +91,6 @@ def get_config(key: str, default=None):
|
||||
return default
|
||||
|
||||
|
||||
|
||||
def init_conf(conf: dict) -> dict:
|
||||
"""
|
||||
初始化配置文件,确保配置文件中的必要字段存在,且不会冲突
|
||||
|
@ -30,7 +30,7 @@ class Database:
|
||||
os.makedirs(os.path.dirname(db_name))
|
||||
|
||||
self.db_name = db_name
|
||||
self.conn = sqlite3.connect(db_name)
|
||||
self.conn = sqlite3.connect(db_name, check_same_thread=False)
|
||||
self.cursor = self.conn.cursor()
|
||||
|
||||
self._on_save_callbacks = []
|
||||
@ -94,7 +94,7 @@ class Database:
|
||||
f"数据库 Selecting {model.TABLE_NAME} WHERE {condition.replace('?', '%s') % args}"
|
||||
)
|
||||
if not table_name:
|
||||
raise ValueError(f"数据模型{model_type.__name__}未提供表名")
|
||||
raise ValueError(f"数据模型 {model_type.__name__} 未提供表名")
|
||||
|
||||
# condition = f"WHERE {condition}"
|
||||
# print(f"SELECT * FROM {table_name} {condition}", args)
|
||||
@ -118,7 +118,7 @@ class Database:
|
||||
]
|
||||
|
||||
def save(self, *args: LiteModel):
|
||||
"""增/改操作
|
||||
self.returns_ = """增/改操作
|
||||
Args:
|
||||
*args:
|
||||
Returns:
|
||||
@ -126,7 +126,7 @@ class Database:
|
||||
table_list = [
|
||||
item[0]
|
||||
for item in self.cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
"SELECT name FROM sqlite_master WHERE type ='table'"
|
||||
).fetchall()
|
||||
]
|
||||
for model in args:
|
||||
@ -158,7 +158,7 @@ class Database:
|
||||
new_obj[field] = value
|
||||
else:
|
||||
raise ValueError(
|
||||
f"数据模型{table_name}包含不支持的数据类型,字段:{field} 值:{value} 值类型:{type(value)}"
|
||||
f"数据模型 {table_name} 包含不支持的数据类型,字段:{field} 值:{value} 值类型:{type(value)}"
|
||||
)
|
||||
if table_name:
|
||||
fields, values = [], []
|
||||
@ -273,9 +273,9 @@ class Database:
|
||||
|
||||
"""
|
||||
table_name = model.TABLE_NAME
|
||||
logger.debug(f"数据库 Deleting {model} WHERE {condition} {args}")
|
||||
logger.debug(f"Deleting {model} WHERE {condition} {args}")
|
||||
if not table_name:
|
||||
raise ValueError(f"数据模型{model.__class__.__name__}未提供表名")
|
||||
raise ValueError(f"数据模型 {model.__class__.__name__} 未提供表名")
|
||||
if model.id is not None:
|
||||
condition = f"id = {model.id}"
|
||||
if not condition and not allow_empty:
|
||||
@ -297,7 +297,7 @@ class Database:
|
||||
"""
|
||||
for model in args:
|
||||
if not model.TABLE_NAME:
|
||||
raise ValueError(f"数据模型{type(model).__name__}未提供表名")
|
||||
raise ValueError(f"数据模型 {type(model).__name__} 未提供表名")
|
||||
|
||||
# 若无则创建表
|
||||
self.cursor.execute(
|
||||
|
@ -2,9 +2,8 @@ import os
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from .data import Database, LiteModel, Database
|
||||
from .data import Database, LiteModel
|
||||
|
||||
print("导入数据库模块")
|
||||
DATA_PATH = "data/liteyuki"
|
||||
user_db: Database = Database(os.path.join(DATA_PATH, "users.ldb"))
|
||||
group_db: Database = Database(os.path.join(DATA_PATH, "groups.ldb"))
|
||||
@ -64,7 +63,7 @@ def auto_migrate():
|
||||
user_db.auto_migrate(User())
|
||||
group_db.auto_migrate(Group())
|
||||
plugin_db.auto_migrate(InstalledPlugin(), GlobalPlugin())
|
||||
common_db.auto_migrate(GlobalPlugin(), StoredConfig(), TempConfig())
|
||||
common_db.auto_migrate(GlobalPlugin(), TempConfig())
|
||||
|
||||
|
||||
auto_migrate()
|
||||
|
@ -1,92 +0,0 @@
|
||||
import json
|
||||
import os.path
|
||||
import platform
|
||||
|
||||
import aiohttp
|
||||
import nonebot
|
||||
import psutil
|
||||
import requests
|
||||
|
||||
from .config import load_from_yaml
|
||||
from .. import __NAME__, __VERSION_I__, __VERSION__
|
||||
|
||||
|
||||
class LiteyukiAPI:
|
||||
def __init__(self):
|
||||
self.liteyuki_id = None
|
||||
if os.path.exists("data/liteyuki/liteyuki.json"):
|
||||
with open("data/liteyuki/liteyuki.json", "rb") as f:
|
||||
self.data = json.loads(f.read())
|
||||
self.liteyuki_id = self.data.get("liteyuki_id")
|
||||
self.report = load_from_yaml("config.yml").get("auto_report", True)
|
||||
|
||||
if self.report:
|
||||
nonebot.logger.info("已启用自动上报")
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict:
|
||||
"""
|
||||
获取设备信息
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return {
|
||||
"name": __NAME__,
|
||||
"version": __VERSION__,
|
||||
"version_i": __VERSION_I__,
|
||||
"python": f"{platform.python_implementation()} {platform.python_version()}",
|
||||
"os": f"{platform.system()} {platform.version()} {platform.machine()}",
|
||||
"cpu": f"{psutil.cpu_count(logical=False)}c{psutil.cpu_count()}t{psutil.cpu_freq().current}MHz",
|
||||
"memory_total": f"{psutil.virtual_memory().total / 1024 ** 3:.2f}吉字节",
|
||||
"memory_used": f"{psutil.virtual_memory().used / 1024 ** 3:.2f}吉字节",
|
||||
"memory_bot": f"{psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2:.2f}兆字节",
|
||||
"disk": f"{psutil.disk_usage('/').total / 1024 ** 3:.2f}吉字节",
|
||||
}
|
||||
|
||||
def bug_report(self, content: str):
|
||||
"""
|
||||
提交bug报告
|
||||
Args:
|
||||
content:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if self.report:
|
||||
nonebot.logger.warning(f"正在上报查误:{content}")
|
||||
url = "https://api.liteyuki.icu/bug_report"
|
||||
data = {
|
||||
"liteyuki_id": self.liteyuki_id,
|
||||
"content": content,
|
||||
"device_info": self.device_info,
|
||||
}
|
||||
resp = requests.post(url, json=data)
|
||||
if resp.status_code == 200:
|
||||
nonebot.logger.success(
|
||||
f"成功上报差误信息,报文ID为:{resp.json().get('report_id')}"
|
||||
)
|
||||
else:
|
||||
nonebot.logger.error(f"差误上报错误:{resp.text}")
|
||||
else:
|
||||
nonebot.logger.warning(f"已禁用自动上报:{content}")
|
||||
|
||||
def register(self):
|
||||
pass
|
||||
|
||||
async def heartbeat_report(self):
|
||||
"""
|
||||
提交心跳,预留接口
|
||||
Returns:
|
||||
|
||||
"""
|
||||
url = "https://api.liteyuki.icu/heartbeat"
|
||||
data = {
|
||||
"liteyuki_id": self.liteyuki_id,
|
||||
"version": __VERSION__,
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, json=data) as resp:
|
||||
if resp.status == 200:
|
||||
nonebot.logger.success("心跳成功送达。")
|
||||
else:
|
||||
nonebot.logger.error("休克:{}".format(await resp.text()))
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user