1
0
forked from bot/app

添加进程及生命周期管理器,添加轻雪框架支持

This commit is contained in:
远野千束 2024-07-24 02:36:46 +08:00
parent 6ef3b09ec9
commit c137f2f916
41 changed files with 988 additions and 206 deletions

12
liteyuki/__init__.py Normal file
View File

@ -0,0 +1,12 @@
from liteyuki.bot import (
LiteyukiBot,
get_bot
)
# def get_bot_instance() -> LiteyukiBot | None:
# """
# 获取轻雪实例
# Returns:
# LiteyukiBot: 当前的轻雪实例
# """
# return _BOT_INSTANCE

243
liteyuki/bot/__init__.py Normal file
View File

@ -0,0 +1,243 @@
import asyncio
import multiprocessing
from typing import Any, Coroutine, Optional
import nonebot
from liteyuki.plugin.load import load_plugin, load_plugins
from src.utils import (
adapter_manager,
driver_manager,
)
from src.utils.base.log import logger
from liteyuki.bot.lifespan import (
Lifespan,
LIFESPAN_FUNC,
)
from liteyuki.core.spawn_process import nb_run, ProcessingManager
__all__ = [
"LiteyukiBot",
"get_bot"
]
_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
class LiteyukiBot:
def __init__(self, *args, **kwargs):
global _BOT_INSTANCE
_BOT_INSTANCE = self # 引用
self.running = False
self.config: dict[str, Any] = kwargs
self.lifespan: Lifespan = Lifespan()
self.init(**self.config) # 初始化
if not _MAIN_PROCESS:
pass
else:
print("\033[34m" + r"""
__ ______ ________ ________ __ __ __ __ __ __ ______
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
""" + "\033[0m")
def run(self, *args, **kwargs):
if _MAIN_PROCESS:
load_plugins("liteyuki/plugins")
asyncio.run(self.lifespan.before_start())
self._run_nb_in_spawn_process(*args, **kwargs)
else:
# 子进程启动
driver_manager.init(config=self.config)
adapter_manager.init(self.config)
adapter_manager.register()
nonebot.load_plugin("src.liteyuki_main")
def _run_nb_in_spawn_process(self, *args, **kwargs):
"""
在新的进程中运行nonebot.run方法
Args:
*args:
**kwargs:
Returns:
"""
timeout_limit: int = 20
should_exit = False
while not should_exit:
ctx = multiprocessing.get_context("spawn")
event = ctx.Event()
ProcessingManager.event = event
process = ctx.Process(
target=nb_run,
args=(event,) + args,
kwargs=kwargs,
)
process.start() # 启动进程
asyncio.run(self.lifespan.after_start())
while not should_exit:
if ProcessingManager.event.wait(1):
logger.info("Receive reboot event")
process.terminate()
process.join(timeout_limit)
if process.is_alive():
logger.warning(
f"Process {process.pid} is still alive after {timeout_limit} seconds, force kill it."
)
process.kill()
break
elif process.is_alive():
continue
else:
should_exit = True
@property
def status(self) -> int:
"""
获取轻雪状态
Returns:
int: 0:未启动 1:运行中
"""
return 1 if self.running else 0
def restart(self):
"""
停止轻雪
Returns:
"""
logger.info("Stopping LiteyukiBot...")
logger.debug("Running before_restart functions...")
asyncio.run(self.lifespan.before_restart())
logger.debug("Running before_shutdown functions...")
asyncio.run(self.lifespan.before_shutdown())
ProcessingManager.restart()
self.running = False
def init(self, *args, **kwargs):
"""
初始化轻雪, 自动调用
Returns:
"""
self.init_config()
self.init_logger()
if not _MAIN_PROCESS:
nonebot.init(**kwargs)
asyncio.run(self.lifespan.after_nonebot_init())
def init_logger(self):
from src.utils.base.log import init_log
init_log()
def init_config(self):
pass
def register_adapters(self, *args):
pass
def on_before_start(self, func: LIFESPAN_FUNC):
"""
注册启动前的函数
Args:
func:
Returns:
"""
return self.lifespan.on_before_start(func)
def on_after_start(self, func: LIFESPAN_FUNC):
"""
注册启动后的函数
Args:
func:
Returns:
"""
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:
Returns:
"""
return self.lifespan.on_after_shutdown(func)
def on_before_restart(self, func: LIFESPAN_FUNC):
"""
注册重启前的函数
Args:
func:
Returns:
"""
return self.lifespan.on_before_restart(func)
def on_after_restart(self, func: LIFESPAN_FUNC):
"""
注册重启后的函数未实现
Args:
func:
Returns:
"""
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: Optional[LiteyukiBot] = None
def get_bot() -> Optional[LiteyukiBot]:
"""
获取轻雪实例
Returns:
LiteyukiBot: 当前的轻雪实例
"""
return _BOT_INSTANCE

181
liteyuki/bot/lifespan.py Normal file
View File

@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/7/23 下午8:24
@Author : snowykami
@Email : snowykami@outlook.com
@File : lifespan.py
@Software: PyCharm
"""
from typing import Any, Awaitable, Callable, TypeAlias
from liteyuki.utils import is_coroutine_callable
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
LIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC
class Lifespan:
def __init__(self) -> None:
"""
轻雪生命周期管理启动停止重启
"""
self.life_flag: int = 0 # 0: 启动前1: 启动后2: 停止前3: 停止后
self._before_start_funcs: list[LIFESPAN_FUNC] = []
self._after_start_funcs: list[LIFESPAN_FUNC] = []
self._before_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._before_restart_funcs: list[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:
"""
运行函数
Args:
funcs:
Returns:
"""
for func in funcs:
if is_coroutine_callable(func):
await func()
else:
func()
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_start_funcs.append(func)
return func
def on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_start_funcs.append(func)
return func
def on_before_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止前的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_shutdown_funcs.append(func)
return func
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_shutdown_funcs.append(func)
return func
def on_before_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_restart_funcs.append(func)
return func
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启后的函数
Args:
func:
Returns:
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:
"""
await self._run_funcs(self._before_start_funcs)
async def after_start(self) -> None:
"""
启动后
Returns:
"""
await self._run_funcs(self._after_start_funcs)
async def before_shutdown(self) -> None:
"""
停止前
Returns:
"""
await self._run_funcs(self._before_shutdown_funcs)
async def after_shutdown(self) -> None:
"""
停止后
Returns:
"""
await self._run_funcs(self._after_shutdown_funcs)
async def before_restart(self) -> None:
"""
重启前
Returns:
"""
await self._run_funcs(self._before_restart_funcs)
async def after_restart(self) -> None:
"""
重启后
Returns:
"""
await self._run_funcs(self._after_restart_funcs)
async def after_nonebot_init(self) -> None:
"""
NoneBot 初始化后
Returns:
"""
await self._run_funcs(self._after_nonebot_init_funcs)

View File

@ -0,0 +1,3 @@
from .spawn_process import *

View File

@ -0,0 +1,37 @@
import threading
from multiprocessing import get_context, Event
import nonebot
from nonebot import logger
from liteyuki.plugin.load import load_plugins
timeout_limit: int = 20
__all__ = [
"ProcessingManager",
"nb_run",
]
class ProcessingManager:
event: Event = None
@classmethod
def restart(cls, delay: int = 0):
"""
发送终止信号
Args:
delay: 延迟时间默认为0单位秒
Returns:
"""
if cls.event is None:
raise RuntimeError("ProcessingManager has not been initialized.")
if delay > 0:
threading.Timer(delay, function=cls.event.set).start()
return
cls.event.set()
def nb_run(event, *args, **kwargs):
ProcessingManager.event = event
nonebot.run(*args, **kwargs)

View File

@ -0,0 +1,17 @@
from liteyuki.plugin.model import Plugin, PluginMetadata
from liteyuki.plugin.load import load_plugin, _plugins
__all__ = [
"PluginMetadata",
"Plugin",
"load_plugin",
]
def get_loaded_plugins() -> dict[str, Plugin]:
"""
获取已加载的插件
Returns:
dict[str, Plugin]: 插件字典
"""
return _plugins

78
liteyuki/plugin/load.py Normal file
View File

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/7/23 下午11:59
@Author : snowykami
@Email : snowykami@outlook.com
@File : load.py
@Software: PyCharm
"""
import os
import traceback
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.utils import path_to_module_name
_plugins: dict[str, Plugin] = {}
def load_plugin(module_path: str | Path) -> Optional[Plugin]:
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
参数:
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
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)
)
logger.opt(colors=True).success(
f'Succeeded to load liteyuki plugin "<y>{module.__name__.split(".")[-1]}</y>"'
)
return _plugins[module.__name__]
except Exception as e:
logger.opt(colors=True).success(
f'Failed to load liteyuki plugin "<r>{module_path}</r>"'
)
traceback.print_exc()
return None
def load_plugins(*plugin_dir: str) -> set[Plugin]:
"""导入文件夹下多个插件
参数:
plugin_dir: 文件夹路径
"""
plugins = set()
for dir_path in plugin_dir:
# 遍历每一个文件夹下的py文件和包含__init__.py的文件夹不递归
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':
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')):
module_name = path_to_module_name(path)
if module_name:
load_plugin(module_name)
if _plugins.get(module_name):
plugins.add(_plugins[module_name])
return plugins

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/7/23 下午11:59
@Author : snowykami
@Email : snowykami@outlook.com
@File : manager.py
@Software: PyCharm
"""

45
liteyuki/plugin/model.py Normal file
View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/7/24 上午12:02
@Author : snowykami
@Email : snowykami@outlook.com
@File : model.py
@Software: PyCharm
"""
from types import ModuleType
from typing import Optional
from pydantic import BaseModel
class PluginMetadata(BaseModel):
"""
轻雪插件元数据由插件编写者提供
"""
name: str
description: str
usage: str = ""
type: str = ""
homepage: str = ""
running_in_main: bool = True # 是否在主进程运行
class Plugin(BaseModel):
"""
存储插件信息
"""
model_config = {
'arbitrary_types_allowed': True
}
name: str
"""插件名称 例如plugin_loader"""
module: ModuleType
"""插件模块对象"""
module_name: str
"""点分割模块路径 例如a.b.c"""
metadata: Optional[PluginMetadata] = None
def __hash__(self):
return hash(self.module_name)

View File

@ -0,0 +1,33 @@
import multiprocessing
import nonebot
from nonebot import get_driver
from liteyuki.plugin import PluginMetadata
from liteyuki import get_bot
__plugin_metadata__ = PluginMetadata(
name="plugin_loader",
description="轻雪插件加载器",
usage="",
type="",
homepage=""
)
liteyuki = get_bot()
@liteyuki.on_after_start
def _():
print("轻雪启动完成,运行在进程", multiprocessing.current_process().name)
@liteyuki.on_before_start
def _():
print("轻雪启动中")
@liteyuki.on_after_nonebot_init
async def _():
print("NoneBot初始化完成")
nonebot.load_plugin("src.liteyuki_main")

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/7/23 下午11:21
@Author : snowykami
@Email : snowykami@outlook.com
@File : data_source.py
@Software: PyCharm
"""

38
liteyuki/utils.py Normal file
View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""
一些常用的工具类部分来源于 nonebot 并遵循其许可进行修改
"""
import inspect
from pathlib import Path
from typing import Any, Callable
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
"""
判断是否为协程可调用对象
Args:
call: 可调用对象
Returns:
bool: 是否为协程可调用对象
"""
if inspect.isroutine(call):
return inspect.iscoroutinefunction(call)
if inspect.isclass(call):
return False
func_ = getattr(call, "__call__", None)
return inspect.iscoroutinefunction(func_)
def path_to_module_name(path: Path) -> str:
"""
转换路径为模块名
Args:
path: 路径a/b/c/d -> a.b.c.d
Returns:
str: 模块名
"""
rel_path = path.resolve().relative_to(Path.cwd().resolve())
if rel_path.stem == "__init__":
return ".".join(rel_path.parts[:-1])
else:
return ".".join(rel_path.parts[:-1] + (rel_path.stem,))

32
main.py
View File

@ -1,28 +1,6 @@
import nonebot from liteyuki import LiteyukiBot
from src.utils import adapter_manager, driver_manager, init from src.utils import load_from_yaml
from src.utils.base.config import load_from_yaml
from src.utils.base.data_manager import StoredConfig, common_db
from src.utils.base.ly_api import liteyuki_api
if __name__ == "__mp_main__": if __name__ in ("__main__", "__mp_main__"):
# Start as multiprocessing bot = LiteyukiBot(**load_from_yaml("config.yml"))
init() bot.run()
store_config: dict = common_db.where_one(StoredConfig(), default=StoredConfig()).config
static_config = load_from_yaml("config.yml")
store_config.update(static_config)
driver_manager.init(config=store_config)
adapter_manager.init(store_config)
nonebot.init(**store_config)
adapter_manager.register()
try:
nonebot.load_plugin("src.liteyuki_main")
nonebot.load_from_toml("pyproject.toml")
except BaseException as e:
if not isinstance(e, KeyboardInterrupt):
nonebot.logger.error(f"An error occurred: {e}, Bug will be reported automatically.")
liteyuki_api.bug_report(str(e.__repr__()))
if __name__ == "__main__":
# Start as __main__
from src.utils.base.reloader import Reloader
nonebot.run()

View File

@ -1,38 +1,58 @@
[tool.nonebot]
[project]
# PEP 621 project metadata # PEP 621 project metadata
# See https://www.python.org/dev/peps/pep-0621/ # See https://www.python.org/dev/peps/pep-0621/
authors = [ # This file is for project use, but don`t use with nb-cli
{name = "SnowyKami", email = "snowykami@outlook.com"}, # 此文件为项目所用请不要和nb-cli一起使用以防被修改
] [tool.poetry]
license = {text = "MIT & LSO"}
requires-python = ">=3.10,<4.0"
dependencies = [
]
dynamic = ["version"]
name = "liteyuki-bot" name = "liteyuki-bot"
description = "Push dynamics and live informations from bilibili to QQ. Based on nonebot2." version = "6"
readme = "README.md" description = "based on nonebot2"
keywords = ["nonebot", "nonebot2", "qqbot", "liteyuki", "bot"] authors = ["Snowykami"]
license = "MIT & LSO"
package-mode = false
[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]
[project.urls] [project.urls]
homepage = "https://bot.liteyuki.icu" homepage = "https://bot.liteyuki.icu"
repository = "https://github.com/LiteyukiStudio/LiteyukiBot" repository = "https://github.com/LiteyukiStudio/LiteyukiBot"
documentation = "https://bot.liteyuki.icu" documentation = "https://bot.liteyuki.icu"
[tool.pdm.dev-dependencies]
dev = []
[tool.nonebot]
adapters = [
{ name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11" }
]
plugins = ["haruka_bot", "nonebot_plugin_gocqhttp", "nonebot_plugin_guild_patch"]
plugin_dirs = []
builtin_plugins = []
[project.scripts]
ly = "main.py"

View File

@ -18,14 +18,13 @@ pydantic~=2.7.0
Pygments~=2.17.2 Pygments~=2.17.2
pytz~=2024.1 pytz~=2024.1
PyYAML~=6.0.1 PyYAML~=6.0.1
pillow~=10.0.0
starlette~=0.36.3 starlette~=0.36.3
loguru~=0.7.2 loguru~=0.7.2
importlib_metadata~=7.0.2 importlib_metadata~=7.0.2
requests~=2.31.0 requests~=2.31.0
watchdog~=4.0.0 watchdog~=4.0.0
pillow~=10.2.0
jieba~=0.42.1 jieba~=0.42.1
pip~=23.2.1
aiosqlite3~=0.3.0 aiosqlite3~=0.3.0
fastapi~=0.110.0 fastapi~=0.110.0
python-dotenv~=1.0.1 python-dotenv~=1.0.1

View File

@ -1,6 +0,0 @@
import abc
class Bot(abc.ABC):
def __init__(self):
pass

View File

@ -18,17 +18,6 @@ __plugin_meta__ = PluginMetadata(
from ..utils.base.language import Language, get_default_lang_code from ..utils.base.language import Language, get_default_lang_code
print("\033[34m" + r"""
__ ______ ________ ________ __ __ __ __ __ __ ______
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
""" + "\033[0m")
sys_lang = Language(get_default_lang_code()) sys_lang = Language(get_default_lang_code())
nonebot.logger.info(sys_lang.get("main.current_language", LANG=sys_lang.get("language.name"))) nonebot.logger.info(sys_lang.get("main.current_language", LANG=sys_lang.get("language.name")))

View File

@ -16,9 +16,11 @@ from src.utils.base.data_manager import StoredConfig, TempConfig, common_db
from src.utils.base.language import get_user_lang from src.utils.base.language import get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from src.utils.base.reloader import Reloader # from src.liteyuki.core import Reloader
from src.utils import event as event_utils, satori_utils from src.utils import event as event_utils, satori_utils
from liteyuki.core import ProcessingManager
from .api import update_liteyuki from .api import update_liteyuki
from liteyuki.bot import get_bot
from ..utils.base.ly_function import get_function from ..utils.base.ly_function import get_function
require("nonebot_plugin_alconna") require("nonebot_plugin_alconna")
@ -92,7 +94,9 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
) )
common_db.save(temp_data) common_db.save(temp_data)
Reloader.reload(0) # Reloader.reload(0)
bot = get_bot()
bot.restart()
@on_alconna( @on_alconna(
@ -281,7 +285,6 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
result = str(e) result = str(e)
args_show = "\n".join("- %s: %s" % (k, v) for k, v in args_dict.items()) args_show = "\n".join("- %s: %s" % (k, v) for k, v in args_dict.items())
print(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}") await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
@ -371,7 +374,7 @@ async def every_day_update():
if result: if result:
await broadcast_to_superusers(f"Liteyuki updated: ```\n{logs}\n```") await broadcast_to_superusers(f"Liteyuki updated: ```\n{logs}\n```")
nonebot.logger.info(f"Liteyuki updated: {logs}") nonebot.logger.info(f"Liteyuki updated: {logs}")
Reloader.reload(5) ProcessingManager.restart()
else: else:
nonebot.logger.info(logs) nonebot.logger.info(logs)

View File

@ -2,24 +2,27 @@ import nonebot
from watchdog.observers import Observer from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler from watchdog.events import FileSystemEventHandler
from liteyuki.bot import get_bot
from src.utils.base.config import get_config from src.utils.base.config import get_config
from src.utils.base.reloader import Reloader from liteyuki.core import ProcessingManager
from src.utils.base.resource import load_resources from src.utils.base.resource import load_resources
if get_config("debug", False): if get_config("debug", False):
liteyuki_bot = get_bot()
src_directories = ( src_directories = (
"src/liteyuki_main", "src/liteyuki_main",
"src/plugins", "src/plugins",
"src/utils", "src/utils",
) )
src_excludes_extensions = ( src_excludes_extensions = (
"pyc", "pyc",
) )
res_directories = ( res_directories = (
"src/resources", "src/resources",
"resources", "resources",
) )
nonebot.logger.info("Liteyuki Reload enabled, watching for file changes...") nonebot.logger.info("Liteyuki Reload enabled, watching for file changes...")
@ -35,7 +38,7 @@ if get_config("debug", False):
src_excludes_extensions) or event.is_directory or "__pycache__" in event.src_path: src_excludes_extensions) or event.is_directory or "__pycache__" in event.src_path:
return return
nonebot.logger.info(f"{event.src_path} modified, reloading bot...") nonebot.logger.info(f"{event.src_path} modified, reloading bot...")
Reloader.reload() liteyuki_bot.restart()
class ResourceModifiedHandler(FileSystemEventHandler): class ResourceModifiedHandler(FileSystemEventHandler):

View File

@ -6,10 +6,13 @@ from src.utils.base.data_manager import InstalledPlugin, plugin_db
from src.utils.base.resource import load_resources from src.utils.base.resource import load_resources
from src.utils.message.tools import check_for_package from src.utils.message.tools import check_for_package
from liteyuki import get_bot
load_resources() load_resources()
init_log() init_log()
driver = get_driver() driver = get_driver()
liteyuki_bot = get_bot()
@driver.on_startup @driver.on_startup
@ -29,3 +32,33 @@ async def load_plugins():
nonebot.plugin.load_plugins("plugins") nonebot.plugin.load_plugins("plugins")
else: else:
nonebot.logger.info("Safe mode is on, no plugin loaded.") nonebot.logger.info("Safe mode is on, no plugin loaded.")
@liteyuki_bot.on_before_start
async def _():
print("启动前")
@liteyuki_bot.on_after_start
async def _():
print("启动后")
@liteyuki_bot.on_before_shutdown
async def _():
print("停止前")
@liteyuki_bot.on_after_shutdown
async def _():
print("停止后")
@liteyuki_bot.on_before_restart
async def _():
print("重启前")
@liteyuki_bot.on_after_restart
async def _():
print("重启后")

View File

@ -1,4 +1,7 @@
import multiprocessing
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from liteyuki.plugin import get_loaded_plugins
from .rt_guide import * from .rt_guide import *
from .crt_matchers import * from .crt_matchers import *
@ -14,3 +17,5 @@ __plugin_meta__ = PluginMetadata(
"default_enable": True, "default_enable": True,
} }
) )
print("Loaded plugins:", len(get_loaded_plugins()))

View File

@ -1,4 +1,6 @@
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from .npm import *
from .rpm import *
__author__ = "snowykami" __author__ = "snowykami"
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(

View File

@ -0,0 +1,69 @@
# npm update/upgrade
# npm search
# npm install/uninstall
# npm list
from nonebot import require
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import (
on_alconna,
Alconna,
Args,
MultiVar,
Subcommand,
Option
)
"""包管理器alc"""
npm_alc = on_alconna(
aliases={"插件", "nonebot-plugin-manager"},
command=Alconna(
"npm",
Subcommand(
"list",
Args["page", int, 1]["num", int, 10],
alias={"ls", "列表", "列出"},
dest="list installed plugins",
help_text="列出已安装插件",
),
Subcommand(
"search",
Args["keywords", MultiVar(str)],
alias=["s", "搜索"],
dest="search plugins",
help_text="搜索本地商店插件,需自行更新",
),
Subcommand(
"install",
Args["package_name", str],
alias=["i", "安装"],
dest="install plugin",
help_text="安装插件",
),
Subcommand(
"uninstall",
Args["package_name", str],
alias=["u", "卸载"],
dest="uninstall plugin",
help_text="卸载插件",
),
Subcommand(
"update",
alias={"更新"},
dest="update local store index",
help_text="更新本地索引库",
),
Subcommand(
"upgrade",
Args["package_name", str],
Option(
"package_name",
Args["package_name", str, None], # Optional
),
alias={"升级"},
dest="upgrade all plugins without package name",
help_text="升级插件",
),
),
)

View File

@ -1,6 +1,28 @@
import json
from pathlib import Path
import aiofiles
from pydantic import BaseModel
from src.utils.base.config import get_config
from src.utils.io import fetch
class Session: class Session:
def __init__(self, session_type: str, session_id: int | str): def __init__(self, session_type: str, session_id: int | str):
self.session_type = session_type self.session_type = session_type
self.session_id = session_id self.session_id = session_id
async def update_local_store_index() -> list[str]:
"""
更新本地插件索引库
Returns:
新增插件包名列表list[str]
"""
url = "https://registry.nonebot.dev/plugins.json"
save_file = Path(get_config("data_path"), "data/liteyuki") / "pacman/plugins.json"
raw_text = await fetch(url)
data = json.loads(raw_text)
with aiofiles.open(save_file, "w") as f:
await f.write(raw_text)

View File

@ -1,29 +0,0 @@
import nonebot
from src.utils import adapter_manager, driver_manager, init
from src.utils.base.config import load_from_yaml
from src.utils.base.data_manager import StoredConfig, common_db
from src.utils.base.ly_api import liteyuki_api
if __name__ == "__mp_main__":
# Start as multiprocessing
init()
store_config: dict = common_db.where_one(StoredConfig(), default=StoredConfig()).config
static_config = load_from_yaml("config.yml")
store_config.update(static_config)
driver_manager.init(config=store_config)
adapter_manager.init(store_config)
nonebot.init(**store_config)
adapter_manager.register()
try:
nonebot.load_plugin("liteyuki.liteyuki_main")
nonebot.load_from_toml("pyproject.toml")
except BaseException as e:
if not isinstance(e, KeyboardInterrupt):
nonebot.logger.error(f"An error occurred: {e}, Bug will be reported automatically.")
liteyuki_api.bug_report(str(e.__repr__()))
if __name__ == "__main__":
# Start as __main__
from src.utils.base.reloader import Reloader
nonebot.run()

View File

@ -32,6 +32,7 @@ class BasicConfig(BaseModel):
command_start: list[str] = ["/", ""] command_start: list[str] = ["/", ""]
nickname: list[str] = [f"LiteyukiBot-{random_hex_string(6)}"] nickname: list[str] = [f"LiteyukiBot-{random_hex_string(6)}"]
satori: SatoriConfig = SatoriConfig() satori: SatoriConfig = SatoriConfig()
data_path: str = "data/liteyuki"
def load_from_yaml(file: str) -> dict: def load_from_yaml(file: str) -> dict:
@ -95,6 +96,8 @@ def init_conf(conf: dict) -> dict:
""" """
# 若command_start中无""则添加必要命令头开启alconna_use_command_start防止冲突 # 若command_start中无""则添加必要命令头开启alconna_use_command_start防止冲突
if "" not in conf.get("command_start", []): # 以下内容由于issue #53 被注释
conf["alconna_use_command_start"] = True # if "" not in conf.get("command_start", []):
# conf["alconna_use_command_start"] = True
return conf return conf
pass

View File

@ -20,8 +20,9 @@ class LiteyukiAPI:
self.data = json.loads(f.read()) self.data = json.loads(f.read())
self.liteyuki_id = self.data.get("liteyuki_id") self.liteyuki_id = self.data.get("liteyuki_id")
self.report = load_from_yaml("config.yml").get("auto_report", True) self.report = load_from_yaml("config.yml").get("auto_report", True)
if self.report: if self.report:
nonebot.logger.info("Auto bug report is enabled") nonebot.logger.info("Auto report enabled")
@property @property
def device_info(self) -> dict: def device_info(self) -> dict:
@ -37,10 +38,10 @@ class LiteyukiAPI:
"python" : f"{platform.python_implementation()} {platform.python_version()}", "python" : f"{platform.python_implementation()} {platform.python_version()}",
"os" : f"{platform.system()} {platform.version()} {platform.machine()}", "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", "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 / 1024 / 1024:.2f}GB", "memory_total": f"{psutil.virtual_memory().total / 1024 ** 3:.2f}GB",
"memory_used" : f"{psutil.virtual_memory().used / 1024 / 1024 / 1024:.2f}GB", "memory_used" : f"{psutil.virtual_memory().used / 1024 ** 3:.2f}GB",
"memory_bot" : f"{psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024:.2f}MB", "memory_bot" : f"{psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2:.2f}MB",
"disk" : f"{psutil.disk_usage('/').total / 1024 / 1024 / 1024:.2f}GB" "disk" : f"{psutil.disk_usage('/').total / 1024 ** 3:.2f}GB"
} }
def bug_report(self, content: str): def bug_report(self, content: str):
@ -77,14 +78,11 @@ class LiteyukiAPI:
url = "https://api.liteyuki.icu/heartbeat" url = "https://api.liteyuki.icu/heartbeat"
data = { data = {
"liteyuki_id": self.liteyuki_id, "liteyuki_id": self.liteyuki_id,
"version": __VERSION__, "version" : __VERSION__,
} }
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(url, json=data) as resp: async with session.post(url, json=data) as resp:
if resp.status == 200: if resp.status == 200:
nonebot.logger.success("Heartbeat sent successfully") nonebot.logger.success("Heartbeat sent successfully")
else: else:
nonebot.logger.error(f"Heartbeat failed: {await resp.text()}") nonebot.logger.error(f"Heartbeat failed: {await resp.text()}")
liteyuki_api = LiteyukiAPI()

View File

@ -1,61 +0,0 @@
import threading
from multiprocessing import get_context
import nonebot
from nonebot import logger
reboot_grace_time_limit: int = 20
_nb_run = nonebot.run
class Reloader:
event: threading.Event = None
@classmethod
def reload(cls, delay: int = 0):
if cls.event is None:
raise RuntimeError()
if delay > 0:
threading.Timer(delay, function=cls.event.set).start()
return
cls.event.set()
def _run(ev: threading.Event, *args, **kwargs):
Reloader.event = ev
_nb_run(*args, **kwargs)
def run(*args, **kwargs):
should_exit = False
ctx = get_context("spawn")
while not should_exit:
event = ctx.Event()
process = ctx.Process(
target=_run,
args=(
event,
*args,
),
kwargs=kwargs,
)
process.start()
while not should_exit:
if event.wait(1):
logger.info("Receive reboot event")
process.terminate()
process.join(reboot_grace_time_limit)
if process.is_alive():
logger.warning(
f"Cannot shutdown gracefully in {reboot_grace_time_limit} second, force kill process."
)
process.kill()
break
elif process.is_alive():
continue
else:
should_exit = True
nonebot.run = run

View File

@ -9,7 +9,6 @@ from .defines import *
def auto_set_env(config: dict): def auto_set_env(config: dict):
dotenv.load_dotenv(".env") dotenv.load_dotenv(".env")
if os.getenv("DRIVER", None) is not None: if os.getenv("DRIVER", None) is not None:
print(os.getenv("DRIVER"))
nonebot.logger.info("Driver already set in environment variable, skip auto configure.") nonebot.logger.info("Driver already set in environment variable, skip auto configure.")
return return
if config.get("satori", {'enable': False}).get("enable", False): if config.get("satori", {'enable': False}).get("enable", False):

View File

@ -1,15 +1,17 @@
from aiohttp import ClientSession from aiohttp import ClientSession
from .net import *
async def simple_get(url: str) -> str: from .file import *
"""
简单异步get请求
Args: async def simple_get(url: str) -> str:
url: """
简单异步get请求
Returns: Args:
url:
"""
async with ClientSession() as session: Returns:
async with session.get(url) as resp: """
return await resp.text() async with ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()

29
src/utils/io/file.py Normal file
View File

@ -0,0 +1,29 @@
import aiofiles
async def write_file(
file_path: str,
content: str | bytes,
mode: str = "w"
):
"""
写入文件
Args:
mode: 写入模式
file_path: 文件路径
content: 内容
"""
async with aiofiles.open(file_path, mode) as f:
await f.write(content)
async def read_file(file_path: str, mode: str = "r") -> str:
"""
读取文件
Args:
file_path: 文件路径
mode: 读取模式
Returns:
"""
async with aiofiles.open(file_path, mode) as f:
return await f.read()

12
src/utils/io/net.py Normal file
View File

@ -0,0 +1,12 @@
async def fetch(url: str) -> str:
"""
异步get请求
Args:
url:
Returns:
"""
async with ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()

5
test/test_core.py Normal file
View File

@ -0,0 +1,5 @@
from src.liteyuki import LiteyukiBot
if __name__ == "__main__":
lyb = LiteyukiBot()
lyb.run()