Ⓜ️手动从旧梦 81a191f merge

This commit is contained in:
2024-08-12 11:44:30 +08:00
parent 068feaa591
commit 2d67a703bd
214 changed files with 6457 additions and 10418 deletions

View File

@ -3,10 +3,19 @@ from liteyuki.bot import (
get_bot
)
# def get_bot_instance() -> LiteyukiBot | None:
# """
# 获取轻雪实例
# Returns:
# LiteyukiBot: 当前的轻雪实例
# """
# return _BOT_INSTANCE
from liteyuki.comm import (
Channel,
chan,
Event
)
from liteyuki.plugin import (
load_plugin,
load_plugins
)
from liteyuki.log import (
logger,
init_log
)

View File

@ -1,147 +1,137 @@
import asyncio
import multiprocessing
from typing import Any, Coroutine, Optional
import os
import platform
import sys
import threading
import time
from typing import Any, Optional
import nonebot
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
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
from liteyuki.bot.lifespan import LIFESPAN_FUNC, Lifespan
from liteyuki.core import IS_MAIN_PROCESS
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
__all__ = [
"LiteyukiBot",
"get_bot"
]
_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
__all__ = ["LiteyukiBot", "get_bot"]
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) # 初始化
def run(self, *args, **kwargs):
self.lifespan: Lifespan = Lifespan()
if _MAIN_PROCESS:
load_plugins("liteyuki/plugins")
asyncio.run(self.lifespan.before_start())
self._run_nb_in_spawn_process(*args, **kwargs)
else:
# 子进程启动
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.call_restart_count = 0
driver_manager.init(config=self.config)
adapter_manager.init(self.config)
adapter_manager.register()
nonebot.load_plugin("src.liteyuki_main")
def run(self):
load_plugins("liteyuki/plugins") # 加载轻雪插件
def _run_nb_in_spawn_process(self, *args, **kwargs):
"""
在新的进程中运行nonebot.run方法
Args:
*args:
**kwargs:
self.loop_thread.start() # 启动事件循环
asyncio.run(self.lifespan.before_start()) # 启动前钩子
Returns:
"""
self.process_manager.add_target("nonebot", nb_run, **self.config)
self.process_manager.start("nonebot")
timeout_limit: int = 20
should_exit = False
self.process_manager.add_target("melobot", mb_run, **self.config)
self.process_manager.start("melobot")
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,
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",
)
process.start() # 启动进程
src_excludes_extensions = ("pyc",)
asyncio.run(self.lifespan.after_start())
logger.debug("轻雪重载 已启用,正在加载文件修改监测……")
restart = self.restart_process
while not should_exit:
if ProcessingManager.event.wait(1):
logger.info("接收到重启活动信息")
process.terminate()
process.join(timeout_limit)
if process.is_alive():
logger.warning(
f"进程 {process.pid}{timeout_limit} 秒后依旧存在,强制清灭。"
)
process.kill()
break
elif process.is_alive():
continue
else:
should_exit = True
class CodeModifiedHandler(FileSystemEventHandler):
"""
Handler for code file changes
"""
@staticmethod
def _run_coroutine(*coro: Coroutine):
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()
def restart(self, delay: int = 0):
"""
运行协程
Args:
coro:
重启轻雪本体
Returns:
"""
# 检测是否有现有的事件循环
new_loop = False
try:
loop = asyncio.get_event_loop()
except RuntimeError:
new_loop = True
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if new_loop:
for c in coro:
loop.run_until_complete(c)
loop.close()
if self.call_restart_count < 1:
executable = sys.executable
args = sys.argv
logger.info("正在重启 尹灵温...")
time.sleep(delay)
if platform.system() == "Windows":
cmd = "start"
elif platform.system() == "Linux":
cmd = "nohup"
elif platform.system() == "Darwin":
cmd = "open"
else:
cmd = "nohup"
self.process_manager.terminate_all()
# 进程退出后重启
threading.Thread(
target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",)
).start()
sys.exit(0)
self.call_restart_count += 1
else:
for c in coro:
loop.create_task(c)
@property
def status(self) -> int:
"""
获取轻雪状态
Returns:
int: 0:未启动 1:运行中
"""
return 1 if self.running else 0
def restart(self):
def restart_process(self, name: Optional[str] = None):
"""
停止轻雪
Args:
name: 进程名称, 默认为None, 所有进程
Returns:
"""
logger.info("正在停止灵温活动…")
logger.info("Stopping LiteyukiBot...")
logger.debug("正在启动 before_restart 的函数…")
self._run_coroutine(self.lifespan.before_restart())
logger.debug("正在启动 before_shutdown 的函数…")
self._run_coroutine(self.lifespan.before_shutdown())
self.loop.create_task(self.lifespan.before_shutdown()) # 重启前钩子
self.loop.create_task(self.lifespan.before_shutdown()) # 停止前钩子
ProcessingManager.restart()
self.running = False
if name:
self.process_manager.terminate(name)
else:
self.process_manager.terminate_all()
def init(self, *args, **kwargs):
"""
@ -151,20 +141,14 @@ class LiteyukiBot:
"""
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()
# 修改nonebot的日志配置
init_log(config=self.config)
def init_config(self):
pass
def register_adapters(self, *args):
pass
def on_before_start(self, func: LIFESPAN_FUNC):
"""
注册启动前的函数
@ -189,7 +173,7 @@ class LiteyukiBot:
def on_before_shutdown(self, func: LIFESPAN_FUNC):
"""
注册停止前的函数
注册停止前的函数,为子进程停止时调用
Args:
func:
@ -211,7 +195,7 @@ class LiteyukiBot:
def on_before_restart(self, func: LIFESPAN_FUNC):
"""
注册重启前的函数
注册重启前的函数,为子进程重启时调用
Args:
func:
@ -253,4 +237,8 @@ def get_bot() -> Optional[LiteyukiBot]:
Returns:
LiteyukiBot: 当前的轻雪实例
"""
return _BOT_INSTANCE
if IS_MAIN_PROCESS:
return _BOT_INSTANCE
else:
# 从多进程上下文中获取
pass

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 下午8:24
@Author : snowykami
@ -13,6 +10,7 @@ Copyright (C) 2020-2024 LiteyukiStudio. All rights reserved
"""
from typing import Any, Awaitable, Callable, TypeAlias
from liteyuki.log import logger
from liteyuki.utils import is_coroutine_callable
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
@ -138,6 +136,7 @@ class Lifespan:
启动前
Returns:
"""
logger.debug("正在运行 before_start 之函数")
await self._run_funcs(self._before_start_funcs)
async def after_start(self) -> None:
@ -145,6 +144,7 @@ class Lifespan:
启动后
Returns:
"""
logger.debug("正在运行 after_start 之函数")
await self._run_funcs(self._after_start_funcs)
async def before_shutdown(self) -> None:
@ -152,6 +152,7 @@ class Lifespan:
停止前
Returns:
"""
logger.debug("正在运行 before_shutdown 之函数")
await self._run_funcs(self._before_shutdown_funcs)
async def after_shutdown(self) -> None:
@ -159,6 +160,7 @@ class Lifespan:
停止后
Returns:
"""
logger.debug("正在运行 after_shutdown 之函数")
await self._run_funcs(self._after_shutdown_funcs)
async def before_restart(self) -> None:
@ -166,6 +168,7 @@ class Lifespan:
重启前
Returns:
"""
logger.debug("正在运行 before_restart 之函数")
await self._run_funcs(self._before_restart_funcs)
async def after_restart(self) -> None:
@ -174,6 +177,7 @@ class Lifespan:
Returns:
"""
logger.debug("正在运行 after_restart 之函数")
await self._run_funcs(self._after_restart_funcs)
async def after_nonebot_init(self) -> None:
@ -181,4 +185,5 @@ class Lifespan:
NoneBot 初始化后
Returns:
"""
logger.debug("正在运行 after_nonebot_init 之函数")
await self._run_funcs(self._after_nonebot_init_funcs)

30
liteyuki/comm/__init__.py Normal file
View File

@ -0,0 +1,30 @@
# -*- 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子进程之间的通信
"""
from liteyuki.comm.channel import (
Channel,
chan,
get_channel,
set_channel,
set_channels,
get_channels
)
from liteyuki.comm.event import Event
__all__ = [
"Channel",
"chan",
"Event",
"get_channel",
"set_channel",
"set_channels",
"get_channels"
]

219
liteyuki/comm/channel.py Normal file
View File

@ -0,0 +1,219 @@
# -*- 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
from multiprocessing import Pipe
from typing import Any, Optional, Callable, Awaitable, List, TypeAlias
from uuid import uuid4
from liteyuki.utils import is_coroutine_callable, run_coroutine
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
SYNC_FILTER_FUNC: TypeAlias = Callable[[Any], bool]
ASYNC_FILTER_FUNC: TypeAlias = Callable[[Any], Awaitable[bool]]
FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC
IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
_channel: dict[str, "Channel"] = {}
_callback_funcs: dict[str, ON_RECEIVE_FUNC] = {}
class Channel:
"""
通道类,用于进程间通信,进程内不可用,仅限主进程和子进程之间通信
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `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
self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False
def __str__(self):
return f"Channel({self.name})"
def send(self, data: Any):
"""
发送数据
Args:
data: 数据
"""
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)
def receive(self) -> Any:
"""
接收数据
Args:
"""
if self._closed:
raise RuntimeError("无法从已关闭的通道中接收")
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)
return data
def close(self):
"""
关闭通道
"""
self._closed = True
self.sub_receive_conn.close()
self.main_send_conn.close()
self.sub_send_conn.close()
self.main_receive_conn.close()
def on_receive(self, filter_func: Optional[FILTER_FUNC] = None) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
"""
接收数据并执行函数
Args:
filter_func: 过滤函数为None则不过滤
Returns:
装饰器,装饰一个函数在接收到数据后执行
"""
if (not self.is_sub_receive_loop_running) and not IS_MAIN_PROCESS:
threading.Thread(target=self._start_sub_receive_loop).start()
if (not self.is_main_receive_loop_running) and IS_MAIN_PROCESS:
threading.Thread(target=self._start_main_receive_loop).start()
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
else:
if not filter_func(data):
return
return await func(data)
function_id = str(uuid4())
_callback_funcs[function_id] = wrapper
if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(function_id)
else:
self._on_sub_receive_funcs.append(function_id)
return func
return decorator
def _run_on_main_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()
"""默认通道实例,可直接从模块导入使用"""
chan = Channel("default")
def set_channel(name: str, channel: Channel):
"""
设置通道实例
Args:
name: 通道名称
channel: 通道实例
"""
_channel[name] = channel
def set_channels(channels: dict[str, Channel]):
"""
设置通道实例
Args:
channels: 通道名称
"""
for name, channel in channels.items():
_channel[name] = channel
def get_channel(name: str) -> Optional[Channel]:
"""
获取通道实例
Args:
name: 通道名称
Returns:
"""
return _channel.get(name, None)
def get_channels() -> dict[str, Channel]:
"""
获取通道实例
Returns:
"""
return _channel

21
liteyuki/comm/event.py Normal file
View File

@ -0,0 +1,21 @@
# -*- 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
class Event:
"""
事件类
"""
def __init__(self, name: str, data: dict[str, Any]):
self.name = name
self.data = data

View File

@ -0,0 +1,49 @@
import os
from typing import List
import nonebot
import yaml
from pydantic import BaseModel
config = {} # 主进程全局配置,确保加载后读取
class SatoriNodeConfig(BaseModel):
host: str = ""
port: str = "5500"
path: str = ""
token: str = ""
class SatoriConfig(BaseModel):
comment: str = "此皆正处于开发之中,切勿在生产环境中启用。"
enable: bool = False
hosts: List[SatoriNodeConfig] = [SatoriNodeConfig()]
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_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)
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

View File

@ -1,3 +1,11 @@
import multiprocessing
from .spawn_process import *
from .manager import *
__all__ = [
"IS_MAIN_PROCESS"
]
IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"

120
liteyuki/core/manager.py Normal file
View File

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/7/27 上午11:12
@Author : snowykami
@Email : snowykami@outlook.com
@File : manager.py
@Software: PyCharm
"""
import asyncio
import threading
from multiprocessing import Process
from typing import TYPE_CHECKING
from liteyuki.comm import Channel, get_channel, set_channels
from liteyuki.log import logger
if TYPE_CHECKING:
from liteyuki.bot import LiteyukiBot
TIMEOUT = 10
__all__ = ["ProcessManager"]
class ProcessManager:
"""
在主进程中被调用
"""
def __init__(self, bot: "LiteyukiBot"):
self.bot = bot
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):
"""
开启后自动监控进程,并添加到进程字典中
Args:
name:
delay:
Returns:
"""
if name not in self.targets:
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
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))
if delay:
threading.Timer(delay, _start).start()
else:
threading.Thread(target=_start).start()
def add_target(self, name: str, target, *args, **kwargs):
self.targets[name] = (target, args, kwargs)
def join(self):
for name, process in self.targets:
process.join()
def terminate(self, name: str):
"""
终止进程并从进程字典中删除
Args:
name:
Returns:
"""
if name not in self.targets:
raise logger.warning(f"未有 Process {name} 之存在")
process = self.processes[name]
process.terminate()
process.join(TIMEOUT)
if process.is_alive():
process.kill()
def terminate_all(self):
for name in self.targets:
self.terminate(name)

View File

@ -0,0 +1,14 @@
from . import (
satori,
onebot
)
def init(config: dict):
onebot.init()
satori.init(config)
def register():
onebot.register()
satori.register()

View File

@ -0,0 +1,12 @@
import nonebot
from nonebot.adapters.onebot import v11, v12
def init():
pass
def register():
driver = nonebot.get_driver()
driver.register_adapter(v11.Adapter)
driver.register_adapter(v12.Adapter)

View File

@ -0,0 +1,26 @@
import json
import os
import nonebot
from nonebot.adapters import satori
def init(config: dict):
if config.get("satori", None) is None:
nonebot.logger.info("未查见 Satori 的配置文档,将跳过 Satori 初始化")
return None
satori_config = config.get("satori")
if not satori_config.get("enable", False):
nonebot.logger.info("未启用 Satori ,将跳过 Satori 初始化")
return None
if os.getenv("SATORI_CLIENTS", None) is not None:
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
def register():
if os.getenv("SATORI_CLIENTS", None) is not None:
driver = nonebot.get_driver()
driver.register_adapter(satori.Adapter)

View File

@ -0,0 +1,6 @@
from .auto_set_env import auto_set_env
def init(config: dict):
auto_set_env(config)
return

View File

@ -0,0 +1,20 @@
import os
import dotenv
import nonebot
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 已设入环境变量中,将跳过自动配置环节。")
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")
else:
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER)
nonebot.logger.info("已禁用 Satori将 driver 设为 ASGI")
return

View File

@ -0,0 +1,17 @@
ASGI_DRIVER = "~fastapi"
HTTPX_DRIVER = "~httpx"
WEBSOCKETS_DRIVER = "~websockets"
def get_driver_string(*argv):
output_string = ""
if ASGI_DRIVER in argv:
output_string += ASGI_DRIVER
for arg in argv:
if arg != ASGI_DRIVER:
output_string = f"{output_string}+{arg}"
return output_string
def get_driver_full_string(*argv):
return f"DRIVER={get_driver_string(argv)}"

View File

@ -1,37 +1,56 @@
import threading
from multiprocessing import get_context, Event
from typing import Optional, TYPE_CHECKING
import nonebot
from nonebot import logger
from liteyuki.plugin.load import load_plugins
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
__all__ = [
"ProcessingManager",
"nb_run",
]
"""导出对象用于主进程与nonebot通信"""
_channels = {}
class ProcessingManager:
event: Event = None
def nb_run(chan_active: "Channel", chan_passive: "Channel", *args, **kwargs):
"""
初始化NoneBot并运行在子进程
Args:
@classmethod
def restart(cls, delay: int = 0):
"""
发送终止信号
Args:
delay: 延迟时间默认为0单位秒
Returns:
"""
if cls.event is None:
raise RuntimeError("ProcessingManager 未初始化。")
if delay > 0:
threading.Timer(delay, function=cls.event.set).start()
return
cls.event.set()
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 nb_run(event, *args, **kwargs):
ProcessingManager.event = event
nonebot.run(*args, **kwargs)
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()

84
liteyuki/log.py Normal file
View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/7/27 上午9:12
@Author : snowykami
@Email : snowykami@outlook.com
@File : log.py
@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 = (
"<c>{time:YYYY-MM-DD HH:mm:ss}</c> "
"<lvl>[{level.icon}]</lvl> "
"<c><{name}.{module}.{function}:{line}></c> "
"{message}"
)
# 默认日志格式
default_format: str = (
"<c>{time:MM-DD HH:mm:ss}</c> "
"<lvl>[{level.icon}]</lvl> "
"<c><{name}></c> "
"{message}"
)
def get_format(level: str) -> str:
if level == "DEBUG":
return debug_format
else:
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)
# 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}")

View File

@ -1,10 +1,11 @@
from liteyuki.plugin.model import Plugin, PluginMetadata
from liteyuki.plugin.load import load_plugin, _plugins
from liteyuki.plugin.load import load_plugin, load_plugins, _plugins
__all__ = [
"PluginMetadata",
"Plugin",
"load_plugin",
"load_plugins",
]

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
@ -25,6 +22,11 @@ from liteyuki.utils import path_to_module_name
_plugins: dict[str, Plugin] = {}
__all__ = [
"load_plugin",
"load_plugins",
]
def load_plugin(module_path: str | Path) -> Optional[Plugin]:
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。

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/24 上午12:02
@Author : snowykami
@ -19,10 +16,10 @@ from pydantic import BaseModel
class PluginMetadata(BaseModel):
"""
轻雪插件元数据,由插件编写者提供
轻雪插件元数据,由插件编写者提供name为必填项
"""
name: str
description: str
description: str = ""
usage: str = ""
type: str = ""
homepage: str = ""

View File

@ -0,0 +1,55 @@
# -*- 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

@ -1,41 +0,0 @@
import multiprocessing
import time
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=""
)
from src.utils import TempConfig, common_db
liteyuki = get_bot()
@liteyuki.on_after_start
def _():
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
# 储存重启计时信息
if temp_data.data.get("reload", False):
delta_time = time.time() - temp_data.data.get("reload_time", 0)
temp_data.data["delta_time"] = delta_time
common_db.save(temp_data) # 更新数据
@liteyuki.on_before_start
def _():
print("灵温正在启动")
@liteyuki.on_after_nonebot_init
async def _():
print("NoneBot初始化完成")
nonebot.load_plugin("src.liteyuki_main")

View File

@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All rights reserved
版权所有 © 2020-2024 神羽SnowyKami & 金羿Eilles with LiteyukiStudio & TriM Org.
保留所有权利
@Time : 2024/7/23 下午11:21
@Author : snowykami
@Email : snowykami@outlook.com
@File : data_source.py
@Software: PyCharm
"""

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/10 下午11:25
@Author : snowykami
@Email : snowykami@outlook.com
@File : register_service.py
@Software: PyCharm
"""
import json
import os.path
import platform
import requests
from git import Repo
from liteyuki.plugin import PluginMetadata
from liteyuki import get_bot, logger
__plugin_meta__ = PluginMetadata(
name="注册服务",
)
liteyuki = get_bot()
commit_hash = Repo(".").head.commit.hexsha
def register_bot():
url = "https://api.liteyuki.icu/register"
data = {
"name" : "尹灵温|轻雪-睿乐",
"version" : "即时更新",
"hash" : commit_hash,
"version_i": 99,
"python" : f"{platform.python_implementation()} {platform.python_version()}",
"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}")
except Exception as e:
logger.warning(f"虽然向 Liteyuki 服务器注册失败,但无关紧要:{e}")
@liteyuki.on_before_start
async def _():
if not os.path.exists("data/liteyuki/liteyuki.json"):
register_bot()

View File

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

View File

@ -2,9 +2,12 @@
"""
一些常用的工具类,部分来源于 nonebot 并遵循其许可进行修改
"""
import asyncio
import inspect
from pathlib import Path
from typing import Any, Callable
from typing import Any, Callable, Coroutine
from liteyuki.log import logger
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
@ -23,6 +26,39 @@ def is_coroutine_callable(call: Callable[..., Any]) -> bool:
return inspect.iscoroutinefunction(func_)
def run_coroutine(*coro: Coroutine):
"""
运行协程
Args:
coro:
Returns:
"""
# 检测是否有现有的事件循环
try:
loop = asyncio.get_event_loop()
if loop.is_running():
# 如果事件循环正在运行,创建任务
for c in coro:
asyncio.ensure_future(c)
else:
# 如果事件循环未运行,运行直到完成
for c in coro:
loop.run_until_complete(c)
except RuntimeError:
# 如果没有找到事件循环,创建一个新的
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(asyncio.gather(*coro))
loop.close()
except Exception as e:
# 捕获其他异常,防止协程被重复等待
logger.error(f"协程异常:{e}")
def path_to_module_name(path: Path) -> str:
"""
转换路径为模块名