🔀手动Merge轻雪主仓库a77f97f

This commit is contained in:
EillesWan 2024-10-06 02:39:10 +08:00
parent 4cc2ae61db
commit f8b57bfe9a
108 changed files with 3131 additions and 3574 deletions

View File

@ -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`的输出,以及其他相关信息,以及你的建议

View File

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

View File

@ -1,44 +0,0 @@
# 问题反馈
## **请确保**
- 已认真阅读[文档]("https://bot.liteyuki.icu"),该问题不是文档提及的或你自己操作不当造成的
- 你的问题是在最新版本的代码上测试的
- 请勿重复提交相同或类似的issue
## **描述问题**
请在此简单描述问题
## **如何复现**
请阐述一下如何重现这个问题
### 预期
描述你期望发生的事情
### 实际
描述实际发生的事情
## **日志或截图**
```
日志内容
```
## **设备信息**
- **系统**: [例如 Ubuntu 22.04]
- **CPU**: [例如 Intel i7-7700K]
- **内存**: [例如 16GB]
- **Python**: [例如CPython 3.10.7]
**补充内容**
可选,推荐提供`pip freeze`的输出,以及其他相关信息,以及你的建议

View File

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

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

View File

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

View File

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

View File

@ -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. 优化了通道的性能

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
"""
该模块用于存放一些开发工具
"""

90
liteyuki/dev/observer.py Normal file
View 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
View 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()

View File

@ -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={})

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

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

View File

@ -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
View 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"### &emsp; ***@{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"### &emsp; ***{'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"&emsp;{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"### &emsp; ***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")

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""
此模块为内置插件文件夹用于存放内置插件
This module is the built-in plugin folder, used to store built-in plugins.
"""

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
# 说明
此目录为**轻雪插件**目录,非其他插件目录。

View 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("你好呀")

View 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("生命周期监控器:启动完成")

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
from liteyuki.plugin import PluginMetadata, PluginType
__plugin_meta__ = PluginMetadata(
name="进程管理器",
author="snowykami",
description="进程管理器,用于管理子进程",
type=PluginType.SERVICE
)

View File

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

View File

@ -0,0 +1,8 @@
from liteyuki.plugin import PluginMetadata, PluginType
__plugin_meta__ = PluginMetadata(
name="资源加载器",
author="snowykami",
description="进程管理器,用于管理子进程",
type=PluginType.SERVICE
)

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

View File

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

View File

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

View File

@ -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子进程已接收到数据")

View File

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

View File

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

View File

@ -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: 分辨率1reso
: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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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__}",

View File

@ -1,7 +1,6 @@
import datetime
import aiohttp
import httpx
import nonebot
from nonebot import require
from nonebot.exception import IgnoredException

View File

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

View 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"])

View File

@ -1,3 +0,0 @@
name: Sign Status
description: for Lagrange
version: 2024.4.26

View File

@ -1,4 +0,0 @@
.sign-chart {
height: 400px;
background-color: rgba(255, 255, 255, 0.7);
}

View File

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

View File

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

View File

@ -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=即复启

View File

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

View File

@ -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);
});
});
*/

View File

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

View File

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

View File

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

View File

@ -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:
"""
初始化配置文件确保配置文件中的必要字段存在且不会冲突

View File

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

View File

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

View File

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