diff --git a/docs/dev/api/bot/README.md b/docs/dev/api/bot/README.md
index 8f027bf5..9f019444 100644
--- a/docs/dev/api/bot/README.md
+++ b/docs/dev/api/bot/README.md
@@ -15,6 +15,26 @@ Returns:
LiteyukiBot: 当前的轻雪实例
+
+源代码
+
+```python
+def get_bot() -> LiteyukiBot:
+ """
+ 获取轻雪实例
+
+ Returns:
+ LiteyukiBot: 当前的轻雪实例
+ """
+ if IS_MAIN_PROCESS:
+ if _BOT_INSTANCE is None:
+ raise RuntimeError('Liteyuki instance not initialized.')
+ return _BOT_INSTANCE
+ else:
+ raise RuntimeError("Can't get bot instance in sub process.")
+```
+
+
### ***def*** `get_config(key: str, default: Any) -> Any`
获取配置
@@ -31,6 +51,24 @@ Returns:
Any: 配置值
+
+源代码
+
+```python
+def get_config(key: str, default: Any=None) -> Any:
+ """
+ 获取配置
+ Args:
+ key: 配置键
+ default: 默认值
+
+ Returns:
+ Any: 配置值
+ """
+ return get_bot().config.get(key, default)
+```
+
+
### ***def*** `get_config_with_compat(key: str, compat_keys: tuple[str], default: Any) -> Any`
获取配置,兼容旧版本
@@ -49,10 +87,44 @@ Returns:
Any: 配置值
+
+源代码
+
+```python
+def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any=None) -> Any:
+ """
+ 获取配置,兼容旧版本
+ Args:
+ key: 配置键
+ compat_keys: 兼容键
+ default: 默认值
+
+ Returns:
+ 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'Config key "{compat_key}" will be deprecated, use "{key}" instead.')
+ return get_bot().config[compat_key]
+ return default
+```
+
+
### ***def*** `print_logo() -> None`
+
+源代码
+
+```python
+def print_logo():
+ print('\x1b[34m' + '\n __ ______ ________ ________ __ __ __ __ __ __ ______ \n / | / |/ |/ |/ \\ / |/ | / |/ | / |/ |\n $$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \\ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/ \n $$ | $$ | $$ | $$ |__ $$ \\/$$/ $$ | $$ |$$ |/$$/ $$ | \n $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ | \n $$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \\ $$ | \n $$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \\__$$ |$$ |$$ \\ _$$ |_ \n $$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |\n $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ \n ' + '\x1b[0m')
+```
+
+
### ***class*** `LiteyukiBot`
@@ -67,22 +139,124 @@ Args:
**kwargs: 配置
+
+源代码
+
+```python
+def __init__(self, *args, **kwargs) -> None:
+ """
+ 初始化轻雪实例
+ Args:
+ *args:
+ **kwargs: 配置
+
+ """
+ '常规操作'
+ print_logo()
+ global _BOT_INSTANCE
+ _BOT_INSTANCE = self
+ '配置'
+ self.config: dict[str, Any] = kwargs
+ '初始化'
+ self.init(**self.config)
+ logger.info('Liteyuki is initializing...')
+ '生命周期管理'
+ self.lifespan = Lifespan()
+ self.process_manager: ProcessManager = ProcessManager(lifespan=self.lifespan)
+ '事件循环'
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
+ self.stop_event = threading.Event()
+ self.call_restart_count = 0
+ '加载插件加载器'
+ load_plugin('liteyuki.plugins.plugin_loader')
+ '信号处理'
+ signal.signal(signal.SIGINT, self._handle_exit)
+ signal.signal(signal.SIGTERM, self._handle_exit)
+ atexit.register(self.process_manager.terminate_all)
+```
+
+
### ***def*** `run(self) -> None`
启动逻辑
+
+源代码
+
+```python
+def run(self):
+ """
+ 启动逻辑
+ """
+ self.lifespan.before_start()
+ self.process_manager.start_all()
+ self.lifespan.after_start()
+ self.keep_alive()
+```
+
+
### ***def*** `keep_alive(self) -> None`
保持轻雪运行
Returns:
+
+源代码
+
+```python
+def keep_alive(self):
+ """
+ 保持轻雪运行
+ Returns:
+
+ """
+ try:
+ while not self.stop_event.is_set():
+ time.sleep(0.5)
+ except KeyboardInterrupt:
+ logger.info('Liteyuki is stopping...')
+ self.stop()
+```
+
+
### ***def*** `restart(self, delay: int) -> None`
重启轻雪本体
Returns:
+
+源代码
+
+```python
+def restart(self, delay: int=0):
+ """
+ 重启轻雪本体
+ Returns:
+
+ """
+ if self.call_restart_count < 1:
+ executable = sys.executable
+ args = sys.argv
+ logger.info('Restarting LiteyukiBot...')
+ 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
+```
+
+
### ***def*** `restart_process(self, name: Optional[str]) -> None`
停止轻雪
@@ -93,22 +267,83 @@ Args:
Returns:
+
+源代码
+
+```python
+def restart_process(self, name: Optional[str]=None):
+ """
+ 停止轻雪
+ Args:
+ name: 进程名称, 默认为None, 所有进程
+ Returns:
+ """
+ self.lifespan.before_process_shutdown()
+ self.lifespan.before_process_shutdown()
+ if name is not None:
+ chan_active = get_channel(f'{name}-active')
+ chan_active.send(1)
+ else:
+ for process_name in self.process_manager.processes:
+ chan_active = get_channel(f'{process_name}-active')
+ chan_active.send(1)
+```
+
+
### ***def*** `init(self) -> None`
初始化轻雪, 自动调用
Returns:
+
+源代码
+
+```python
+def init(self, *args, **kwargs):
+ """
+ 初始化轻雪, 自动调用
+ Returns:
+
+ """
+ self.init_logger()
+```
+
+
### ***def*** `init_logger(self) -> None`
+
+源代码
+
+```python
+def init_logger(self):
+ init_log(config=self.config)
+```
+
+
### ***def*** `stop(self) -> None`
停止轻雪
Returns:
+
+源代码
+
+```python
+def stop(self):
+ """
+ 停止轻雪
+ Returns:
+
+ """
+ self.stop_event.set()
+ self.loop.stop()
+```
+
+
### ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> None`
注册启动前的函数
@@ -121,6 +356,23 @@ Args:
Returns:
+
+源代码
+
+```python
+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) -> None`
注册启动后的函数
@@ -133,6 +385,23 @@ Args:
Returns:
+
+源代码
+
+```python
+def on_after_start(self, func: LIFESPAN_FUNC):
+ """
+ 注册启动后的函数
+ Args:
+ func:
+
+ Returns:
+
+ """
+ return self.lifespan.on_after_start(func)
+```
+
+
### ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> None`
注册停止后的函数:未实现
@@ -145,6 +414,23 @@ Args:
Returns:
+
+源代码
+
+```python
+def on_after_shutdown(self, func: LIFESPAN_FUNC):
+ """
+ 注册停止后的函数:未实现
+ Args:
+ func:
+
+ Returns:
+
+ """
+ return self.lifespan.on_after_shutdown(func)
+```
+
+
### ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> None`
注册进程停止前的函数,为子进程停止时调用
@@ -157,6 +443,23 @@ Args:
Returns:
+
+源代码
+
+```python
+def on_before_process_shutdown(self, func: LIFESPAN_FUNC):
+ """
+ 注册进程停止前的函数,为子进程停止时调用
+ Args:
+ func:
+
+ Returns:
+
+ """
+ return self.lifespan.on_before_process_shutdown(func)
+```
+
+
### ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> None`
注册进程重启前的函数,为子进程重启时调用
@@ -169,6 +472,23 @@ Args:
Returns:
+
+源代码
+
+```python
+def on_before_process_restart(self, func: LIFESPAN_FUNC):
+ """
+ 注册进程重启前的函数,为子进程重启时调用
+ Args:
+ func:
+
+ Returns:
+
+ """
+ return self.lifespan.on_before_process_restart(func)
+```
+
+
### ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> None`
注册重启后的函数:未实现
@@ -181,6 +501,23 @@ Args:
Returns:
+
+源代码
+
+```python
+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) -> None`
注册nonebot初始化后的函数
@@ -193,6 +530,23 @@ Args:
Returns:
+
+源代码
+
+```python
+def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
+ """
+ 注册nonebot初始化后的函数
+ Args:
+ func:
+
+ Returns:
+
+ """
+ return self.lifespan.on_after_nonebot_init(func)
+```
+
+
### ***var*** `executable = sys.executable`
diff --git a/docs/dev/api/bot/lifespan.md b/docs/dev/api/bot/lifespan.md
index 396403b4..6466bd52 100644
--- a/docs/dev/api/bot/lifespan.md
+++ b/docs/dev/api/bot/lifespan.md
@@ -15,6 +15,33 @@ Args:
Returns:
+
+源代码
+
+```python
+@staticmethod
+def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
+ """
+ 运行函数
+ Args:
+ funcs:
+ Returns:
+ """
+ try:
+ loop = asyncio.get_event_loop()
+ except RuntimeError:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ tasks = []
+ for func in funcs:
+ if is_coroutine_callable(func):
+ tasks.append(func(*args, **kwargs))
+ else:
+ tasks.append(async_wrapper(func)(*args, **kwargs))
+ loop.run_until_complete(asyncio.gather(*tasks))
+```
+
+
### ***class*** `Lifespan`
@@ -23,6 +50,25 @@ Returns:
轻雪生命周期管理,启动、停止、重启
+
+源代码
+
+```python
+def __init__(self) -> None:
+ """
+ 轻雪生命周期管理,启动、停止、重启
+ """
+ self.life_flag: int = 0
+ self._before_start_funcs: list[LIFESPAN_FUNC] = []
+ self._after_start_funcs: list[LIFESPAN_FUNC] = []
+ self._before_process_shutdown_funcs: list[LIFESPAN_FUNC] = []
+ self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
+ self._before_process_restart_funcs: list[LIFESPAN_FUNC] = []
+ self._after_restart_funcs: list[LIFESPAN_FUNC] = []
+ self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
+```
+
+
### ***@staticmethod***
### ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
@@ -34,6 +80,33 @@ Args:
Returns:
+
+源代码
+
+```python
+@staticmethod
+def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
+ """
+ 运行函数
+ Args:
+ funcs:
+ Returns:
+ """
+ try:
+ loop = asyncio.get_event_loop()
+ except RuntimeError:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ tasks = []
+ for func in funcs:
+ if is_coroutine_callable(func):
+ tasks.append(func(*args, **kwargs))
+ else:
+ tasks.append(async_wrapper(func)(*args, **kwargs))
+ loop.run_until_complete(asyncio.gather(*tasks))
+```
+
+
### ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
注册启动时的函数
@@ -46,6 +119,23 @@ Returns:
LIFESPAN_FUNC:
+
+源代码
+
+```python
+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`
注册启动时的函数
@@ -58,6 +148,23 @@ Returns:
LIFESPAN_FUNC:
+
+源代码
+
+```python
+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_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
注册停止前的函数
@@ -70,6 +177,23 @@ Returns:
LIFESPAN_FUNC:
+
+源代码
+
+```python
+def on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
+ """
+ 注册停止前的函数
+ Args:
+ func:
+ Returns:
+ LIFESPAN_FUNC:
+ """
+ self._before_process_shutdown_funcs.append(func)
+ return func
+```
+
+
### ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
注册停止后的函数
@@ -84,6 +208,25 @@ Returns:
LIFESPAN_FUNC:
+
+源代码
+
+```python
+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_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
注册重启时的函数
@@ -96,6 +239,23 @@ Returns:
LIFESPAN_FUNC:
+
+源代码
+
+```python
+def on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
+ """
+ 注册重启时的函数
+ Args:
+ func:
+ Returns:
+ LIFESPAN_FUNC:
+ """
+ self._before_process_restart_funcs.append(func)
+ return func
+```
+
+
### ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
注册重启后的函数
@@ -108,6 +268,23 @@ Returns:
LIFESPAN_FUNC:
+
+源代码
+
+```python
+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: Any) -> None`
注册 NoneBot 初始化后的函数
@@ -120,42 +297,145 @@ Args:
Returns:
+
+源代码
+
+```python
+def on_after_nonebot_init(self, func):
+ """
+ 注册 NoneBot 初始化后的函数
+ Args:
+ func:
+
+ Returns:
+
+ """
+ self._after_nonebot_init_funcs.append(func)
+ return func
+```
+
+
### ***def*** `before_start(self) -> None`
启动前
Returns:
+
+源代码
+
+```python
+def before_start(self) -> None:
+ """
+ 启动前
+ Returns:
+ """
+ logger.debug('Running before_start functions')
+ self.run_funcs(self._before_start_funcs)
+```
+
+
### ***def*** `after_start(self) -> None`
启动后
Returns:
+
+源代码
+
+```python
+def after_start(self) -> None:
+ """
+ 启动后
+ Returns:
+ """
+ logger.debug('Running after_start functions')
+ self.run_funcs(self._after_start_funcs)
+```
+
+
### ***def*** `before_process_shutdown(self) -> None`
停止前
Returns:
+
+源代码
+
+```python
+def before_process_shutdown(self) -> None:
+ """
+ 停止前
+ Returns:
+ """
+ logger.debug('Running before_shutdown functions')
+ self.run_funcs(self._before_process_shutdown_funcs)
+```
+
+
### ***def*** `after_shutdown(self) -> None`
停止后
Returns:
+
+源代码
+
+```python
+def after_shutdown(self) -> None:
+ """
+ 停止后
+ Returns:
+ """
+ logger.debug('Running after_shutdown functions')
+ self.run_funcs(self._after_shutdown_funcs)
+```
+
+
### ***def*** `before_process_restart(self) -> None`
重启前
Returns:
+
+源代码
+
+```python
+def before_process_restart(self) -> None:
+ """
+ 重启前
+ Returns:
+ """
+ logger.debug('Running before_restart functions')
+ self.run_funcs(self._before_process_restart_funcs)
+```
+
+
### ***def*** `after_restart(self) -> None`
重启后
Returns:
+
+源代码
+
+```python
+def after_restart(self) -> None:
+ """
+ 重启后
+ Returns:
+
+ """
+ logger.debug('Running after_restart functions')
+ self.run_funcs(self._after_restart_funcs)
+```
+
+
### ***var*** `tasks = []`
diff --git a/docs/dev/api/comm/channel.md b/docs/dev/api/comm/channel.md
index f34d7054..8e5e170f 100644
--- a/docs/dev/api/comm/channel.md
+++ b/docs/dev/api/comm/channel.md
@@ -1,5 +1,5 @@
---
-title: liteyuki.comm.channel_
+title: liteyuki.comm.channel
order: 1
icon: laptop-code
category: API
@@ -15,6 +15,26 @@ Args:
channel: 通道实例
+
+源代码
+
+```python
+def set_channel(name: str, channel: Channel):
+ """
+ 设置通道实例
+ Args:
+ name: 通道名称
+ channel: 通道实例
+ """
+ if not isinstance(channel, Channel):
+ raise TypeError(f'channel_ must be an instance of Channel, {type(channel)} found')
+ if IS_MAIN_PROCESS:
+ _channel[name] = channel
+ else:
+ channel_deliver_passive_channel.send(('set_channel', {'name': name, 'channel_': channel}))
+```
+
+
### ***def*** `set_channels(channels: dict[str, Channel]) -> None`
设置通道实例
@@ -23,6 +43,21 @@ Args:
channels: 通道名称
+
+源代码
+
+```python
+def set_channels(channels: dict[str, Channel]):
+ """
+ 设置通道实例
+ Args:
+ channels: 通道名称
+ """
+ for name, channel in channels.items():
+ set_channel(name, channel)
+```
+
+
### ***def*** `get_channel(name: str) -> Channel`
获取通道实例
@@ -33,39 +68,156 @@ Args:
Returns:
+
+源代码
+
+```python
+def get_channel(name: str) -> Channel:
+ """
+ 获取通道实例
+ Args:
+ name: 通道名称
+ Returns:
+ """
+ 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]`
获取通道实例
Returns:
+
+源代码
+
+```python
+def get_channels() -> dict[str, Channel]:
+ """
+ 获取通道实例
+ Returns:
+ """
+ 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()
+```
+
+
### ***def*** `on_set_channel(data: tuple[str, dict[str, Any]]) -> None`
+
+源代码
+
+```python
+@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)
+```
+
+
### ***def*** `on_get_channel(data: tuple[str, dict[str, Any]]) -> None`
+
+源代码
+
+```python
+@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))
+```
+
+
### ***def*** `on_get_channels(data: tuple[str, dict[str, Any]]) -> None`
+
+源代码
+
+```python
+@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*** `decorator(func: Callable[[T], Any]) -> Callable[[T], Any]`
+
+源代码
+
+```python
+def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
+ global _func_id
+
+ async def wrapper(data: T) -> Any:
+ if filter_func is not None:
+ if is_coroutine_callable(filter_func):
+ if not await filter_func(data):
+ return
+ elif not filter_func(data):
+ return
+ 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(_func_id)
+ else:
+ self._on_sub_receive_funcs.append(_func_id)
+ _func_id += 1
+ return func
+```
+
+
### ***async def*** `wrapper(data: T) -> Any`
+
+源代码
+
+```python
+async def wrapper(data: T) -> Any:
+ if filter_func is not None:
+ if is_coroutine_callable(filter_func):
+ if not await filter_func(data):
+ return
+ elif not filter_func(data):
+ return
+ if is_coroutine_callable(func):
+ return await func(data)
+ else:
+ return func(data)
+```
+
+
### ***class*** `Channel(Generic[T])`
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
-### ***def*** `__init__(self, _id: str, type_check: bool) -> None`
+### ***def*** `__init__(self, _id: str, type_check: Optional[bool]) -> None`
初始化通道
@@ -73,6 +225,35 @@ Args:
_id: 通道ID
+ type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
+
+
+源代码
+
+```python
+def __init__(self, _id: str, type_check: Optional[bool]=None):
+ """
+ 初始化通道
+ Args:
+ _id: 通道ID
+ type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
+ """
+ self.conn_send, self.conn_recv = Pipe()
+ self._closed = False
+ self._on_main_receive_funcs: list[int] = []
+ self._on_sub_receive_funcs: list[int] = []
+ self.name: str = _id
+ self.is_main_receive_loop_running = False
+ self.is_sub_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 is required for enforcing type check.')
+ self.type_check = type_check
+```
+
+
### ***def*** `send(self, data: T) -> None`
发送数据
@@ -81,16 +262,67 @@ Args:
data: 数据
+
+源代码
+
+```python
+def send(self, data: T):
+ """
+ 发送数据
+ Args:
+ 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'Data must be an instance of {_type}, {type(data)} found')
+ if self._closed:
+ raise RuntimeError('Cannot send to a closed channel_')
+ self.conn_send.send(data)
+```
+
+
### ***def*** `receive(self) -> T`
接收数据
Args:
+
+源代码
+
+```python
+def receive(self) -> T:
+ """
+ 接收数据
+ Args:
+ """
+ if self._closed:
+ raise RuntimeError('Cannot receive from a closed channel_')
+ while True:
+ data = self.conn_recv.recv()
+ return data
+```
+
+
### ***def*** `close(self) -> None`
关闭通道
+
+源代码
+
+```python
+def close(self):
+ """
+ 关闭通道
+ """
+ self._closed = True
+ self.conn_send.close()
+ self.conn_recv.close()
+```
+
+
### ***def*** `on_receive(self, filter_func: Optional[FILTER_FUNC]) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]`
接收数据并执行函数
@@ -103,6 +335,48 @@ Returns:
装饰器,装饰一个函数在接收到数据后执行
+
+源代码
+
+```python
+def on_receive(self, filter_func: Optional[FILTER_FUNC]=None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
+ """
+ 接收数据并执行函数
+ 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, daemon=True).start()
+ if not self.is_main_receive_loop_running and IS_MAIN_PROCESS:
+ threading.Thread(target=self._start_main_receive_loop, daemon=True).start()
+
+ def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
+ global _func_id
+
+ async def wrapper(data: T) -> Any:
+ if filter_func is not None:
+ if is_coroutine_callable(filter_func):
+ if not await filter_func(data):
+ return
+ elif not filter_func(data):
+ return
+ 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(_func_id)
+ else:
+ self._on_sub_receive_funcs.append(_func_id)
+ _func_id += 1
+ return func
+ return decorator
+```
+
+
### ***var*** `T = TypeVar('T')`
@@ -127,6 +401,10 @@ Returns:
+### ***var*** `type_check = self._get_generic_type() is not None`
+
+
+
### ***var*** `data = self.conn_recv.recv()`
diff --git a/docs/dev/api/comm/event.md b/docs/dev/api/comm/event.md
index a5dbf619..a2a15f55 100644
--- a/docs/dev/api/comm/event.md
+++ b/docs/dev/api/comm/event.md
@@ -13,3 +13,13 @@ category: API
+
+源代码
+
+```python
+def __init__(self, name: str, data: dict[str, Any]):
+ self.name = name
+ self.data = data
+```
+
+
diff --git a/docs/dev/api/comm/storage.md b/docs/dev/api/comm/storage.md
index 9042d3a1..cd19dbb7 100644
--- a/docs/dev/api/comm/storage.md
+++ b/docs/dev/api/comm/storage.md
@@ -5,22 +5,200 @@ icon: laptop-code
category: API
---
+### ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
+
+运行订阅者接收函数
+
+Args:
+
+ channel_: 频道
+
+ data: 数据
+
+
+源代码
+
+```python
+@staticmethod
+def run_subscriber_receive_funcs(channel_: str, data: Any):
+ """
+ 运行订阅者接收函数
+ Args:
+ channel_: 频道
+ data: 数据
+ """
+ if IS_MAIN_PROCESS:
+ if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
+ run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
+ elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
+ run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
+```
+
+
### ***def*** `on_get(data: tuple[str, dict[str, Any]]) -> None`
+
+源代码
+
+```python
+@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))
+```
+
+
### ***def*** `on_set(data: tuple[str, dict[str, Any]]) -> None`
+
+源代码
+
+```python
+@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)
+```
+
+
### ***def*** `on_delete(data: tuple[str, dict[str, Any]]) -> None`
+
+源代码
+
+```python
+@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)
+```
+
+
### ***def*** `on_get_all(data: tuple[str, dict[str, Any]]) -> None`
+
+源代码
+
+```python
+@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())
+```
+
+
+### ***def*** `on_publish(data: tuple[str, Any]) -> None`
+
+
+
+
+源代码
+
+```python
+@channel.publish_channel.on_receive()
+def on_publish(data: tuple[str, Any]):
+ channel_, data = data
+ shared_memory.run_subscriber_receive_funcs(channel_, data)
+```
+
+
+### ***def*** `decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC`
+
+
+
+
+源代码
+
+```python
+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
+```
+
+
+### ***async def*** `wrapper(data: Any) -> None`
+
+
+
+
+源代码
+
+```python
+async def wrapper(data: Any):
+ if is_coroutine_callable(func):
+ await func(data)
+ else:
+ func(data)
+```
+
+
+### ***class*** `Subscriber`
+
+
+
+### ***def*** `__init__(self) -> None`
+
+
+
+
+源代码
+
+```python
+def __init__(self):
+ self._subscribers = {}
+```
+
+
+### ***def*** `receive(self) -> Any`
+
+
+
+
+源代码
+
+```python
+def receive(self) -> Any:
+ pass
+```
+
+
+### ***def*** `unsubscribe(self) -> None`
+
+
+
+
+源代码
+
+```python
+def unsubscribe(self) -> None:
+ pass
+```
+
+
### ***class*** `KeyValueStore`
@@ -29,6 +207,20 @@ category: API
+
+源代码
+
+```python
+def __init__(self):
+ self._store = {}
+ self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-active')
+ self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-passive')
+ self.publish_channel = Channel[tuple[str, Any]](_id='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`
设置键值对
@@ -39,6 +231,27 @@ Args:
value: 值
+
+源代码
+
+```python
+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]) -> Optional[Any]`
获取键值对
@@ -55,6 +268,31 @@ Returns:
Any: 值
+
+源代码
+
+```python
+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) -> None`
删除键值对
@@ -69,6 +307,34 @@ Args:
Returns:
+
+源代码
+
+```python
+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]`
获取所有键值对
@@ -77,6 +343,141 @@ Returns:
dict[str, Any]: 键值对
+
+源代码
+
+```python
+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:
+
+
+源代码
+
+```python
+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:
+
+ 装饰器
+
+
+源代码
+
+```python
+def on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
+ """
+ 订阅者接收消息时的回调
+ Args:
+ channel_: 频道
+
+ Returns:
+ 装饰器
+ """
+ if IS_MAIN_PROCESS and (not self.is_main_receive_loop_running):
+ threading.Thread(target=self._start_receive_loop, daemon=True).start()
+ shared_memory.is_main_receive_loop_running = True
+ elif not IS_MAIN_PROCESS and (not self.is_sub_receive_loop_running):
+ threading.Thread(target=self._start_receive_loop, daemon=True).start()
+ shared_memory.is_sub_receive_loop_running = True
+
+ 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***
+### ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
+
+ 运行订阅者接收函数
+
+Args:
+
+ channel_: 频道
+
+ data: 数据
+
+
+源代码
+
+```python
+@staticmethod
+def run_subscriber_receive_funcs(channel_: str, data: Any):
+ """
+ 运行订阅者接收函数
+ Args:
+ channel_: 频道
+ data: 数据
+ """
+ if IS_MAIN_PROCESS:
+ if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
+ run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
+ elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
+ run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
+```
+
+
### ***class*** `GlobalKeyValueStore`
@@ -86,6 +487,20 @@ Returns:
+
+源代码
+
+```python
+@classmethod
+def get_instance(cls):
+ if cls._instance is None:
+ with cls._lock:
+ if cls._instance is None:
+ cls._instance = KeyValueStore()
+ return cls._instance
+```
+
+
### ***attr*** `_instance: None`
### ***attr*** `_lock: threading.Lock()`
@@ -138,3 +553,11 @@ Returns:
+### ***var*** `data = self.active_chan.receive()`
+
+
+
+### ***var*** `data = self.publish_channel.receive()`
+
+
+
diff --git a/docs/dev/api/config.md b/docs/dev/api/config.md
index 30066528..24afd6c9 100644
--- a/docs/dev/api/config.md
+++ b/docs/dev/api/config.md
@@ -23,24 +23,117 @@ Returns:
扁平化后的配置文件,但也包含原有的键值对
+
+源代码
+
+```python
+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
+```
+
+
### ***def*** `load_from_yaml(file: str) -> dict[str, Any]`
Load config from yaml file
+
+源代码
+
+```python
+def load_from_yaml(file: str) -> dict[str, Any]:
+ """
+ Load config from yaml file
+
+ """
+ logger.debug(f'Loading YAML config from {file}')
+ config = yaml.safe_load(open(file, 'r', encoding='utf-8'))
+ return flat_config(config if config is not None else {})
+```
+
+
### ***def*** `load_from_json(file: str) -> dict[str, Any]`
Load config from json file
+
+源代码
+
+```python
+def load_from_json(file: str) -> dict[str, Any]:
+ """
+ Load config from json file
+ """
+ logger.debug(f'Loading JSON config from {file}')
+ config = json.load(open(file, 'r', encoding='utf-8'))
+ return flat_config(config if config is not None else {})
+```
+
+
### ***def*** `load_from_toml(file: str) -> dict[str, Any]`
Load config from toml file
+
+源代码
+
+```python
+def load_from_toml(file: str) -> dict[str, Any]:
+ """
+ Load config from toml file
+ """
+ logger.debug(f'Loading TOML config from {file}')
+ config = toml.load(open(file, 'r', encoding='utf-8'))
+ return flat_config(config if config is not None else {})
+```
+
+
### ***def*** `load_from_files() -> dict[str, Any]`
从指定文件加载配置项,会自动识别文件格式
默认执行扁平化选项
+
+源代码
+
+```python
+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))
+ elif not no_warning:
+ logger.warning(f'Unsupported config file format: {file}')
+ elif not no_warning:
+ logger.warning(f'Config file not found: {file}')
+ return config
+```
+
+
### ***def*** `load_configs_from_dirs() -> dict[str, Any]`
从目录下加载配置文件,不递归
@@ -49,6 +142,29 @@ Load config from toml file
默认执行扁平化选项
+
+源代码
+
+```python
+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 not found: {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) -> dict[str, Any]`
从一个标准的轻雪项目加载配置文件
@@ -57,6 +173,22 @@ Load config from toml file
项目目录下的配置文件优先
+
+源代码
+
+```python
+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
+```
+
+
### ***class*** `SatoriNodeConfig(BaseModel)`
diff --git a/docs/dev/api/core/manager.md b/docs/dev/api/core/manager.md
index ed8f2f18..52397375 100644
--- a/docs/dev/api/core/manager.md
+++ b/docs/dev/api/core/manager.md
@@ -9,10 +9,23 @@ category: API
-### ***def*** `__init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]]) -> None`
+### ***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]]) -> None`
+
+源代码
+
+```python
+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
+```
+
+
### ***class*** `ProcessManager`
进程管理器
@@ -21,6 +34,17 @@ category: API
+
+源代码
+
+```python
+def __init__(self, lifespan: 'Lifespan'):
+ self.lifespan = lifespan
+ self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
+ self.processes: dict[str, Process] = {}
+```
+
+
### ***def*** `start(self, name: str) -> None`
开启后自动监控进程,并添加到进程字典中
@@ -31,10 +55,63 @@ Args:
Returns:
+
+源代码
+
+```python
+def start(self, name: str):
+ """
+ 开启后自动监控进程,并添加到进程字典中
+ Args:
+ name:
+ Returns:
+
+ """
+ if name not in self.targets:
+ raise KeyError(f'Process {name} not found.')
+ chan_active = get_channel(f'{name}-active')
+
+ 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()
+ _start_process()
+ while True:
+ data = chan_active.receive()
+ if data == 0:
+ logger.info(f'Stopping process {name}')
+ self.lifespan.before_process_shutdown()
+ self.terminate(name)
+ break
+ elif data == 1:
+ logger.info(f'Restarting process {name}')
+ self.lifespan.before_process_shutdown()
+ self.lifespan.before_process_restart()
+ self.terminate(name)
+ _start_process()
+ continue
+ else:
+ logger.warning('Unknown data received, ignored.')
+```
+
+
### ***def*** `start_all(self) -> None`
启动所有进程
+
+源代码
+
+```python
+def start_all(self):
+ """
+ 启动所有进程
+ """
+ for name in self.targets:
+ threading.Thread(target=self.start, args=(name,), daemon=True).start()
+```
+
+
### ***def*** `add_target(self, name: str, target: TARGET_FUNC, args: tuple, kwargs: Any) -> None`
添加进程
@@ -49,10 +126,43 @@ Args:
kwargs: 进程函数关键字参数,通常会默认传入chan_active和chan_passive
+
+源代码
+
+```python
+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(_id=f'{name}-active')
+ chan_passive: Channel = Channel(_id=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)
+ set_channels({f'{name}-active': chan_active, f'{name}-passive': chan_passive})
+```
+
+
### ***def*** `join_all(self) -> None`
+
+源代码
+
+```python
+def join_all(self):
+ for name, process in self.targets:
+ process.join()
+```
+
+
### ***def*** `terminate(self, name: str) -> None`
终止进程并从进程字典中删除
@@ -65,10 +175,45 @@ Args:
Returns:
+
+源代码
+
+```python
+def terminate(self, name: str):
+ """
+ 终止进程并从进程字典中删除
+ Args:
+ name:
+
+ Returns:
+
+ """
+ if name not in self.processes:
+ logger.warning(f'Process {name} not found.')
+ return
+ process = self.processes[name]
+ process.terminate()
+ process.join(TIMEOUT)
+ if process.is_alive():
+ process.kill()
+ logger.success(f'Process {name} terminated.')
+```
+
+
### ***def*** `terminate_all(self) -> None`
+
+源代码
+
+```python
+def terminate_all(self):
+ for name in self.targets:
+ self.terminate(name)
+```
+
+
### ***def*** `is_process_alive(self, name: str) -> bool`
检查进程是否存活
@@ -81,6 +226,25 @@ Args:
Returns:
+
+源代码
+
+```python
+def is_process_alive(self, name: str) -> bool:
+ """
+ 检查进程是否存活
+ Args:
+ name:
+
+ Returns:
+
+ """
+ if name not in self.targets:
+ logger.warning(f'Process {name} not found.')
+ return self.processes[name].is_alive()
+```
+
+
### ***var*** `TIMEOUT = 10`
@@ -89,7 +253,7 @@ Returns:
-### ***var*** `channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel)`
+### ***var*** `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)`
diff --git a/docs/dev/api/dev/observer.md b/docs/dev/api/dev/observer.md
index f0f5643b..2005bbf2 100644
--- a/docs/dev/api/dev/observer.md
+++ b/docs/dev/api/dev/observer.md
@@ -9,6 +9,29 @@ category: API
防抖函数
+
+源代码
+
+```python
+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
+```
+
+
### ***def*** `on_file_system_event(directories: tuple[str], recursive: bool, event_filter: FILTER_FUNC) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]`
注册文件系统变化监听器
@@ -25,22 +48,111 @@ Returns:
装饰器,装饰一个函数在接收到数据后执行
+
+源代码
+
+```python
+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
+```
+
+
### ***def*** `decorator(func: Any) -> None`
+
+源代码
+
+```python
+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
+```
+
+
### ***def*** `decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC`
+
+源代码
+
+```python
+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
+```
+
+
### ***def*** `wrapper() -> None`
+
+源代码
+
+```python
+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)
+```
+
+
### ***def*** `wrapper(event: FileSystemEvent) -> None`
+
+源代码
+
+```python
+def wrapper(event: FileSystemEvent):
+ if event_filter is not None and (not event_filter(event)):
+ return
+ func(event)
+```
+
+
### ***class*** `CodeModifiedHandler(FileSystemEventHandler)`
Handler for code file changes
@@ -49,22 +161,68 @@ Handler for code file changes
+
+源代码
+
+```python
+@debounce(1)
+def on_modified(self, event):
+ raise NotImplementedError('on_modified must be implemented')
+```
+
+
### ***def*** `on_created(self, event: Any) -> None`
+
+源代码
+
+```python
+def on_created(self, event):
+ self.on_modified(event)
+```
+
+
### ***def*** `on_deleted(self, event: Any) -> None`
+
+源代码
+
+```python
+def on_deleted(self, event):
+ self.on_modified(event)
+```
+
+
### ***def*** `on_moved(self, event: Any) -> None`
+
+源代码
+
+```python
+def on_moved(self, event):
+ self.on_modified(event)
+```
+
+
### ***def*** `on_any_event(self, event: Any) -> None`
+
+源代码
+
+```python
+def on_any_event(self, event):
+ self.on_modified(event)
+```
+
+
### ***var*** `liteyuki_bot = get_bot()`
diff --git a/docs/dev/api/dev/plugin.md b/docs/dev/api/dev/plugin.md
index a93c922b..caafa5ea 100644
--- a/docs/dev/api/dev/plugin.md
+++ b/docs/dev/api/dev/plugin.md
@@ -13,6 +13,25 @@ Args:
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
+
+源代码
+
+```python
+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()
+```
+
+
### ***var*** `cfg = load_config_in_default()`
diff --git a/docs/dev/api/log.md b/docs/dev/api/log.md
index c698f3ba..af382275 100644
--- a/docs/dev/api/log.md
+++ b/docs/dev/api/log.md
@@ -9,12 +9,49 @@ category: API
+
+源代码
+
+```python
+def get_format(level: str) -> str:
+ if level == 'DEBUG':
+ return debug_format
+ else:
+ return default_format
+```
+
+
### ***def*** `init_log(config: dict) -> None`
在语言加载完成后执行
Returns:
+
+源代码
+
+```python
+def init_log(config: dict):
+ """
+ 在语言加载完成后执行
+ Returns:
+
+ """
+ logger.remove()
+ logger.add(sys.stdout, level=0, diagnose=False, format=get_format(config.get('log_level', 'INFO')))
+ show_icon = config.get('log_icon', True)
+ logger.level('DEBUG', color='', icon=f"{('🐛' if show_icon else '')}DEBUG")
+ logger.level('INFO', color='', icon=f"{('ℹ️' if show_icon else '')}INFO")
+ logger.level('SUCCESS', color='', icon=f"{('✅' if show_icon else '')}SUCCESS")
+ logger.level('WARNING', color='', icon=f"{('⚠️' if show_icon else '')}WARNING")
+ logger.level('ERROR', color='', icon=f"{('⭕' if show_icon else '')}ERROR")
+```
+
+
+### ***var*** `logger = loguru.logger`
+
+
+
### ***var*** `show_icon = config.get('log_icon', True)`
diff --git a/docs/dev/api/message/README.md b/docs/dev/api/message/README.md
new file mode 100644
index 00000000..0d664851
--- /dev/null
+++ b/docs/dev/api/message/README.md
@@ -0,0 +1,7 @@
+---
+title: liteyuki.message
+index: true
+icon: laptop-code
+category: API
+---
+
diff --git a/docs/dev/api/message/event.md b/docs/dev/api/message/event.md
new file mode 100644
index 00000000..740ff8bd
--- /dev/null
+++ b/docs/dev/api/message/event.md
@@ -0,0 +1,106 @@
+---
+title: liteyuki.message.event
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***class*** `MessageEvent`
+
+
+
+### ***def*** `__init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, 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: 附加数据
+
+
+源代码
+
+```python
+def __init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, 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.receive_channel = receive_channel
+```
+
+
+### ***def*** `reply(self, message: str | dict[str, Any]) -> None`
+
+ 回复消息
+
+Args:
+
+ message:
+
+Returns:
+
+
+源代码
+
+```python
+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, session_type=self.session_type, receive_channel='_')
+ shared_memory.publish(self.receive_channel, reply_event)
+```
+
+
+### ***var*** `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, session_type=self.session_type, receive_channel='_')`
+
+
+
+### ***var*** `data = {}`
+
+
+
diff --git a/docs/dev/api/message/matcher.md b/docs/dev/api/message/matcher.md
new file mode 100644
index 00000000..4a6341b6
--- /dev/null
+++ b/docs/dev/api/message/matcher.md
@@ -0,0 +1,71 @@
+---
+title: liteyuki.message.matcher
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***class*** `Matcher`
+
+
+
+### ***def*** `__init__(self, rule: Rule, priority: int, block: bool) -> None`
+
+ 匹配器
+
+Args:
+
+ rule: 规则
+
+ priority: 优先级 >= 0
+
+ block: 是否阻断后续优先级更低的匹配器
+
+
+源代码
+
+```python
+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*** `handle(self, handler: EventHandler) -> EventHandler`
+
+ 添加处理函数,装饰器
+
+Args:
+
+ handler:
+
+Returns:
+
+ EventHandler
+
+
+源代码
+
+```python
+def handle(self, handler: EventHandler) -> EventHandler:
+ """
+ 添加处理函数,装饰器
+ Args:
+ handler:
+ Returns:
+ EventHandler
+ """
+ self.handlers.append(handler)
+ return handler
+```
+
+
diff --git a/docs/dev/api/message/on.md b/docs/dev/api/message/on.md
new file mode 100644
index 00000000..4f3c7230
--- /dev/null
+++ b/docs/dev/api/message/on.md
@@ -0,0 +1,39 @@
+---
+title: liteyuki.message.on
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `on_message(rule: Rule, priority: int, block: bool) -> Matcher`
+
+
+
+
+源代码
+
+```python
+def on_message(rule: Rule=Rule(), priority: int=0, block: bool=True) -> Matcher:
+ matcher = Matcher(rule, priority, block)
+ for i, m in enumerate(_matcher_list):
+ if m.priority < matcher.priority:
+ _matcher_list.insert(i, matcher)
+ break
+ else:
+ _matcher_list.append(matcher)
+ return matcher
+```
+
+
+### ***var*** `current_priority = -1`
+
+
+
+### ***var*** `matcher = Matcher(rule, priority, block)`
+
+
+
+### ***var*** `current_priority = matcher.priority`
+
+
+
diff --git a/docs/dev/api/message/rule.md b/docs/dev/api/message/rule.md
new file mode 100644
index 00000000..a135f740
--- /dev/null
+++ b/docs/dev/api/message/rule.md
@@ -0,0 +1,24 @@
+---
+title: liteyuki.message.rule
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***class*** `Rule`
+
+
+
+### ***def*** `__init__(self, handler: Optional[RuleHandler]) -> None`
+
+
+
+
+源代码
+
+```python
+def __init__(self, handler: Optional[RuleHandler]=None):
+ self.handler = handler
+```
+
+
diff --git a/docs/dev/api/message/session.md b/docs/dev/api/message/session.md
new file mode 100644
index 00000000..f8fcad6f
--- /dev/null
+++ b/docs/dev/api/message/session.md
@@ -0,0 +1,7 @@
+---
+title: liteyuki.message.session
+order: 1
+icon: laptop-code
+category: API
+---
+
diff --git a/docs/dev/api/mkdoc.md b/docs/dev/api/mkdoc.md
index 765d9a81..802aff3c 100644
--- a/docs/dev/api/mkdoc.md
+++ b/docs/dev/api/mkdoc.md
@@ -15,6 +15,21 @@ Args:
target_path: 目标路径
+
+源代码
+
+```python
+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]) -> None`
输出文件
@@ -23,10 +38,42 @@ Args:
file_data: 文件数据 相对路径
+
+源代码
+
+```python
+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) -> None`
+
+源代码
+
+```python
+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) -> ModuleInfo`
获取函数和类
@@ -41,6 +88,67 @@ Returns:
模块信息
+
+源代码
+
+```python
+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)):
+ if not any((isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(node))) and (not ignore_private or not node.name.startswith('_')):
+ 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:
+ 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)))
+ 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: Any) -> str`
生成模块的Markdown
@@ -57,6 +165,60 @@ Returns:
Markdown 字符串
+
+源代码
+
+```python
+def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
+ """
+ 生成模块的Markdown
+ 你可在此自定义生成的Markdown格式
+ Args:
+ module_info: 模块信息
+ front_matter: 自定义选项title, index, icon, category
+ 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'\n源代码
\n\n```python\n{func.source_code}\n```\n \n\n'
+ for cls in module_info.classes:
+ if cls.inherit:
+ inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ''
+ content += f'### ***class*** `{cls.name}{inherit}`\n\n'
+ else:
+ content += f'### ***class*** `{cls.name}`\n\n'
+ cls.docstring = cls.docstring.replace('\n', '\n\n')
+ content += f'{cls.docstring}\n\n'
+ for method in cls.methods:
+ if method.type != DefType.METHOD:
+ args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in method.args]
+ content += f'### ***@{method.type.value}***\n'
+ else:
+ args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] and arg[0] != 'self' else arg[0] for arg in method.args]
+ content += f"### ***{('async ' if method.is_async else '')}def*** `{method.name}({', '.join(args_with_type)}) -> {method.return_type}`\n\n"
+ method.docstring = method.docstring.replace('\n', '\n\n')
+ content += f' {method.docstring}\n\n'
+ content += f'\n源代码
\n\n```python\n{method.source_code}\n```\n \n\n'
+ for attr in cls.attributes:
+ content += f'### ***attr*** `{attr.name}: {attr.type}`\n\n'
+ for attr in module_info.attributes:
+ if attr.type == NO_TYPE_HINT:
+ content += f'### ***var*** `{attr.name} = {attr.value}`\n\n'
+ else:
+ content += f'### ***var*** `{attr.name}: {attr.type} = {attr.value}`\n\n'
+ attr.docstring = attr.docstring.replace('\n', '\n\n')
+ content += f'{attr.docstring}\n\n'
+ return content
+```
+
+
### ***def*** `generate_docs(module_folder: str, output_dir: str, with_top: bool, ignored_paths: Any) -> None`
生成文档
@@ -71,6 +233,46 @@ Args:
ignored_paths: 忽略的路径
+
+源代码
+
+```python
+def generate_docs(module_folder: str, output_dir: str, with_top: bool=False, 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: 忽略的路径
+ """
+ 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)
+ 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)
+ 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)
+```
+
+
### ***class*** `DefType(Enum)`
@@ -217,7 +419,7 @@ Args:
-### ***var*** `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))`
+### ***var*** `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))`
diff --git a/docs/dev/api/plugin/README.md b/docs/dev/api/plugin/README.md
index 2e7020f9..6c67f485 100644
--- a/docs/dev/api/plugin/README.md
+++ b/docs/dev/api/plugin/README.md
@@ -13,3 +13,17 @@ Returns:
dict[str, Plugin]: 插件字典
+
+源代码
+
+```python
+def get_loaded_plugins() -> dict[str, Plugin]:
+ """
+ 获取已加载的插件
+ Returns:
+ dict[str, Plugin]: 插件字典
+ """
+ return _plugins
+```
+
+
diff --git a/docs/dev/api/plugin/load.md b/docs/dev/api/plugin/load.md
index 9669642e..b999f5e1 100644
--- a/docs/dev/api/plugin/load.md
+++ b/docs/dev/api/plugin/load.md
@@ -17,6 +17,34 @@ category: API
或插件路径 `pathlib.Path(path/to/your/plugin)`
+
+源代码
+
+```python
+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))
+ display_name = module.__name__.split('.')[-1]
+ if module.__dict__.get('__plugin_meta__'):
+ metadata: 'PluginMetadata' = module.__dict__['__plugin_meta__']
+ display_name = format_display_name(f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type)
+ logger.opt(colors=True).success(f'Succeeded to load liteyuki plugin "{display_name}"')
+ return _plugins[module.__name__]
+ except Exception as e:
+ logger.opt(colors=True).success(f'Failed to load liteyuki plugin "{module_path}"')
+ traceback.print_exc()
+ return None
+```
+
+
### ***def*** `load_plugins() -> set[Plugin]`
导入文件夹下多个插件
@@ -29,6 +57,46 @@ category: API
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
+
+源代码
+
+```python
+def load_plugins(*plugin_dir: str, ignore_warning: bool=True) -> set[Plugin]:
+ """导入文件夹下多个插件
+
+ 参数:
+ plugin_dir: 文件夹路径
+ ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
+ """
+ plugins = set()
+ for dir_path in plugin_dir:
+ if not os.path.exists(dir_path):
+ if not ignore_warning:
+ logger.warning(f"Plugins dir '{dir_path}' does not exist.")
+ continue
+ if not os.listdir(dir_path):
+ if not ignore_warning:
+ logger.warning(f"Plugins dir '{dir_path}' is empty.")
+ continue
+ if not os.path.isdir(dir_path):
+ if not ignore_warning:
+ logger.warning(f"Plugins dir '{dir_path}' is not a directory.")
+ 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'):
+ 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
+```
+
+
### ***def*** `format_display_name(display_name: str, plugin_type: PluginType) -> str`
设置插件名称颜色,根据不同类型插件设置颜色
@@ -45,6 +113,34 @@ Returns:
str: 设置后的插件名称 name
+
+源代码
+
+```python
+def format_display_name(display_name: str, plugin_type: PluginType) -> str:
+ """
+ 设置插件名称颜色,根据不同类型插件设置颜色
+ Args:
+ display_name: 插件名称
+ plugin_type: 插件类型
+
+ Returns:
+ str: 设置后的插件名称 name
+ """
+ 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}>'
+```
+
+
### ***var*** `module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path`
diff --git a/docs/dev/api/plugin/model.md b/docs/dev/api/plugin/model.md
index fd8fec0c..6f986049 100644
--- a/docs/dev/api/plugin/model.md
+++ b/docs/dev/api/plugin/model.md
@@ -13,12 +13,12 @@ category: API
### ***attr*** `SERVICE: 'service'`
-### ***attr*** `IMPLEMENTATION: 'implementation'`
-
### ***attr*** `MODULE: 'module'`
### ***attr*** `UNCLASSIFIED: 'unclassified'`
+### ***attr*** `TEST: 'test'`
+
### ***class*** `PluginMetadata(BaseModel)`
轻雪插件元数据,由插件编写者提供,name为必填项
@@ -71,10 +71,6 @@ extra: dict[str, Any]
-### ***var*** `IMPLEMENTATION = 'implementation'`
-
-
-
### ***var*** `MODULE = 'module'`
@@ -83,6 +79,10 @@ extra: dict[str, Any]
+### ***var*** `TEST = 'test'`
+
+
+
### ***var*** `model_config = {'arbitrary_types_allowed': True}`
diff --git a/docs/dev/api/utils.md b/docs/dev/api/utils.md
index b215a53d..84532f46 100644
--- a/docs/dev/api/utils.md
+++ b/docs/dev/api/utils.md
@@ -17,6 +17,27 @@ Returns:
bool: 是否为协程可调用对象
+
+源代码
+
+```python
+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*** `run_coroutine() -> None`
运行协程
@@ -29,6 +50,37 @@ Args:
Returns:
+
+源代码
+
+```python
+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'Exception occurred: {e}')
+```
+
+
### ***def*** `path_to_module_name(path: Path) -> str`
转换路径为模块名
@@ -41,6 +93,26 @@ Returns:
str: 模块名
+
+源代码
+
+```python
+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,))
+```
+
+
### ***def*** `async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]`
异步包装器
@@ -53,10 +125,39 @@ Returns:
Coroutine: Asynchronous Callable
+
+源代码
+
+```python
+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
+```
+
+
### ***async def*** `wrapper() -> None`
+
+源代码
+
+```python
+async def wrapper(*args, **kwargs):
+ return func(*args, **kwargs)
+```
+
+
### ***var*** `IS_MAIN_PROCESS = multiprocessing.current_process().name == 'MainProcess'`
diff --git a/docs/en/dev/api/README.md b/docs/en/dev/api/README.md
new file mode 100644
index 00000000..181ec73d
--- /dev/null
+++ b/docs/en/dev/api/README.md
@@ -0,0 +1,7 @@
+---
+title: liteyuki
+index: true
+icon: laptop-code
+category: API
+---
+
diff --git a/docs/en/dev/api/bot/README.md b/docs/en/dev/api/bot/README.md
new file mode 100644
index 00000000..9f019444
--- /dev/null
+++ b/docs/en/dev/api/bot/README.md
@@ -0,0 +1,581 @@
+---
+title: liteyuki.bot
+index: true
+icon: laptop-code
+category: API
+---
+
+### ***def*** `get_bot() -> LiteyukiBot`
+
+获取轻雪实例
+
+
+
+Returns:
+
+ LiteyukiBot: 当前的轻雪实例
+
+
+源代码
+
+```python
+def get_bot() -> LiteyukiBot:
+ """
+ 获取轻雪实例
+
+ Returns:
+ LiteyukiBot: 当前的轻雪实例
+ """
+ if IS_MAIN_PROCESS:
+ if _BOT_INSTANCE is None:
+ raise RuntimeError('Liteyuki instance not initialized.')
+ return _BOT_INSTANCE
+ else:
+ raise RuntimeError("Can't get bot instance in sub process.")
+```
+
+
+### ***def*** `get_config(key: str, default: Any) -> Any`
+
+获取配置
+
+Args:
+
+ key: 配置键
+
+ default: 默认值
+
+
+
+Returns:
+
+ Any: 配置值
+
+
+源代码
+
+```python
+def get_config(key: str, default: Any=None) -> Any:
+ """
+ 获取配置
+ Args:
+ key: 配置键
+ default: 默认值
+
+ Returns:
+ Any: 配置值
+ """
+ return get_bot().config.get(key, default)
+```
+
+
+### ***def*** `get_config_with_compat(key: str, compat_keys: tuple[str], default: Any) -> Any`
+
+获取配置,兼容旧版本
+
+Args:
+
+ key: 配置键
+
+ compat_keys: 兼容键
+
+ default: 默认值
+
+
+
+Returns:
+
+ Any: 配置值
+
+
+源代码
+
+```python
+def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any=None) -> Any:
+ """
+ 获取配置,兼容旧版本
+ Args:
+ key: 配置键
+ compat_keys: 兼容键
+ default: 默认值
+
+ Returns:
+ 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'Config key "{compat_key}" will be deprecated, use "{key}" instead.')
+ return get_bot().config[compat_key]
+ return default
+```
+
+
+### ***def*** `print_logo() -> None`
+
+
+
+
+源代码
+
+```python
+def print_logo():
+ print('\x1b[34m' + '\n __ ______ ________ ________ __ __ __ __ __ __ ______ \n / | / |/ |/ |/ \\ / |/ | / |/ | / |/ |\n $$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \\ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/ \n $$ | $$ | $$ | $$ |__ $$ \\/$$/ $$ | $$ |$$ |/$$/ $$ | \n $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ | \n $$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \\ $$ | \n $$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \\__$$ |$$ |$$ \\ _$$ |_ \n $$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |\n $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ \n ' + '\x1b[0m')
+```
+
+
+### ***class*** `LiteyukiBot`
+
+
+
+### ***def*** `__init__(self) -> None`
+
+ 初始化轻雪实例
+
+Args:
+
+ *args:
+
+ **kwargs: 配置
+
+
+源代码
+
+```python
+def __init__(self, *args, **kwargs) -> None:
+ """
+ 初始化轻雪实例
+ Args:
+ *args:
+ **kwargs: 配置
+
+ """
+ '常规操作'
+ print_logo()
+ global _BOT_INSTANCE
+ _BOT_INSTANCE = self
+ '配置'
+ self.config: dict[str, Any] = kwargs
+ '初始化'
+ self.init(**self.config)
+ logger.info('Liteyuki is initializing...')
+ '生命周期管理'
+ self.lifespan = Lifespan()
+ self.process_manager: ProcessManager = ProcessManager(lifespan=self.lifespan)
+ '事件循环'
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
+ self.stop_event = threading.Event()
+ self.call_restart_count = 0
+ '加载插件加载器'
+ load_plugin('liteyuki.plugins.plugin_loader')
+ '信号处理'
+ signal.signal(signal.SIGINT, self._handle_exit)
+ signal.signal(signal.SIGTERM, self._handle_exit)
+ atexit.register(self.process_manager.terminate_all)
+```
+
+
+### ***def*** `run(self) -> None`
+
+ 启动逻辑
+
+
+源代码
+
+```python
+def run(self):
+ """
+ 启动逻辑
+ """
+ self.lifespan.before_start()
+ self.process_manager.start_all()
+ self.lifespan.after_start()
+ self.keep_alive()
+```
+
+
+### ***def*** `keep_alive(self) -> None`
+
+ 保持轻雪运行
+
+Returns:
+
+
+源代码
+
+```python
+def keep_alive(self):
+ """
+ 保持轻雪运行
+ Returns:
+
+ """
+ try:
+ while not self.stop_event.is_set():
+ time.sleep(0.5)
+ except KeyboardInterrupt:
+ logger.info('Liteyuki is stopping...')
+ self.stop()
+```
+
+
+### ***def*** `restart(self, delay: int) -> None`
+
+ 重启轻雪本体
+
+Returns:
+
+
+源代码
+
+```python
+def restart(self, delay: int=0):
+ """
+ 重启轻雪本体
+ Returns:
+
+ """
+ if self.call_restart_count < 1:
+ executable = sys.executable
+ args = sys.argv
+ logger.info('Restarting LiteyukiBot...')
+ 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
+```
+
+
+### ***def*** `restart_process(self, name: Optional[str]) -> None`
+
+ 停止轻雪
+
+Args:
+
+ name: 进程名称, 默认为None, 所有进程
+
+Returns:
+
+
+源代码
+
+```python
+def restart_process(self, name: Optional[str]=None):
+ """
+ 停止轻雪
+ Args:
+ name: 进程名称, 默认为None, 所有进程
+ Returns:
+ """
+ self.lifespan.before_process_shutdown()
+ self.lifespan.before_process_shutdown()
+ if name is not None:
+ chan_active = get_channel(f'{name}-active')
+ chan_active.send(1)
+ else:
+ for process_name in self.process_manager.processes:
+ chan_active = get_channel(f'{process_name}-active')
+ chan_active.send(1)
+```
+
+
+### ***def*** `init(self) -> None`
+
+ 初始化轻雪, 自动调用
+
+Returns:
+
+
+源代码
+
+```python
+def init(self, *args, **kwargs):
+ """
+ 初始化轻雪, 自动调用
+ Returns:
+
+ """
+ self.init_logger()
+```
+
+
+### ***def*** `init_logger(self) -> None`
+
+
+
+
+源代码
+
+```python
+def init_logger(self):
+ init_log(config=self.config)
+```
+
+
+### ***def*** `stop(self) -> None`
+
+ 停止轻雪
+
+Returns:
+
+
+源代码
+
+```python
+def stop(self):
+ """
+ 停止轻雪
+ Returns:
+
+ """
+ self.stop_event.set()
+ self.loop.stop()
+```
+
+
+### ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> None`
+
+ 注册启动前的函数
+
+Args:
+
+ func:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+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) -> None`
+
+ 注册启动后的函数
+
+Args:
+
+ func:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+def on_after_start(self, func: LIFESPAN_FUNC):
+ """
+ 注册启动后的函数
+ Args:
+ func:
+
+ Returns:
+
+ """
+ return self.lifespan.on_after_start(func)
+```
+
+
+### ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> None`
+
+ 注册停止后的函数:未实现
+
+Args:
+
+ func:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+def on_after_shutdown(self, func: LIFESPAN_FUNC):
+ """
+ 注册停止后的函数:未实现
+ Args:
+ func:
+
+ Returns:
+
+ """
+ return self.lifespan.on_after_shutdown(func)
+```
+
+
+### ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> None`
+
+ 注册进程停止前的函数,为子进程停止时调用
+
+Args:
+
+ func:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+def on_before_process_shutdown(self, func: LIFESPAN_FUNC):
+ """
+ 注册进程停止前的函数,为子进程停止时调用
+ Args:
+ func:
+
+ Returns:
+
+ """
+ return self.lifespan.on_before_process_shutdown(func)
+```
+
+
+### ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> None`
+
+ 注册进程重启前的函数,为子进程重启时调用
+
+Args:
+
+ func:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+def on_before_process_restart(self, func: LIFESPAN_FUNC):
+ """
+ 注册进程重启前的函数,为子进程重启时调用
+ Args:
+ func:
+
+ Returns:
+
+ """
+ return self.lifespan.on_before_process_restart(func)
+```
+
+
+### ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> None`
+
+ 注册重启后的函数:未实现
+
+Args:
+
+ func:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+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) -> None`
+
+ 注册nonebot初始化后的函数
+
+Args:
+
+ func:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
+ """
+ 注册nonebot初始化后的函数
+ Args:
+ func:
+
+ Returns:
+
+ """
+ return self.lifespan.on_after_nonebot_init(func)
+```
+
+
+### ***var*** `executable = sys.executable`
+
+
+
+### ***var*** `args = sys.argv`
+
+
+
+### ***var*** `chan_active = get_channel(f'{name}-active')`
+
+
+
+### ***var*** `cmd = 'start'`
+
+
+
+### ***var*** `chan_active = get_channel(f'{process_name}-active')`
+
+
+
+### ***var*** `cmd = 'nohup'`
+
+
+
+### ***var*** `cmd = 'open'`
+
+
+
+### ***var*** `cmd = 'nohup'`
+
+
+
diff --git a/docs/en/dev/api/bot/lifespan.md b/docs/en/dev/api/bot/lifespan.md
new file mode 100644
index 00000000..6466bd52
--- /dev/null
+++ b/docs/en/dev/api/bot/lifespan.md
@@ -0,0 +1,450 @@
+---
+title: liteyuki.bot.lifespan
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
+
+运行函数
+
+Args:
+
+ funcs:
+
+Returns:
+
+
+源代码
+
+```python
+@staticmethod
+def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
+ """
+ 运行函数
+ Args:
+ funcs:
+ Returns:
+ """
+ try:
+ loop = asyncio.get_event_loop()
+ except RuntimeError:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ tasks = []
+ for func in funcs:
+ if is_coroutine_callable(func):
+ tasks.append(func(*args, **kwargs))
+ else:
+ tasks.append(async_wrapper(func)(*args, **kwargs))
+ loop.run_until_complete(asyncio.gather(*tasks))
+```
+
+
+### ***class*** `Lifespan`
+
+
+
+### ***def*** `__init__(self) -> None`
+
+ 轻雪生命周期管理,启动、停止、重启
+
+
+源代码
+
+```python
+def __init__(self) -> None:
+ """
+ 轻雪生命周期管理,启动、停止、重启
+ """
+ self.life_flag: int = 0
+ self._before_start_funcs: list[LIFESPAN_FUNC] = []
+ self._after_start_funcs: list[LIFESPAN_FUNC] = []
+ self._before_process_shutdown_funcs: list[LIFESPAN_FUNC] = []
+ self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
+ self._before_process_restart_funcs: list[LIFESPAN_FUNC] = []
+ self._after_restart_funcs: list[LIFESPAN_FUNC] = []
+ self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
+```
+
+
+### ***@staticmethod***
+### ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
+
+ 运行函数
+
+Args:
+
+ funcs:
+
+Returns:
+
+
+源代码
+
+```python
+@staticmethod
+def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
+ """
+ 运行函数
+ Args:
+ funcs:
+ Returns:
+ """
+ try:
+ loop = asyncio.get_event_loop()
+ except RuntimeError:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ tasks = []
+ for func in funcs:
+ if is_coroutine_callable(func):
+ tasks.append(func(*args, **kwargs))
+ else:
+ tasks.append(async_wrapper(func)(*args, **kwargs))
+ loop.run_until_complete(asyncio.gather(*tasks))
+```
+
+
+### ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
+
+ 注册启动时的函数
+
+Args:
+
+ func:
+
+Returns:
+
+ LIFESPAN_FUNC:
+
+
+源代码
+
+```python
+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:
+
+
+源代码
+
+```python
+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_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
+
+ 注册停止前的函数
+
+Args:
+
+ func:
+
+Returns:
+
+ LIFESPAN_FUNC:
+
+
+源代码
+
+```python
+def on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
+ """
+ 注册停止前的函数
+ Args:
+ func:
+ Returns:
+ LIFESPAN_FUNC:
+ """
+ self._before_process_shutdown_funcs.append(func)
+ return func
+```
+
+
+### ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
+
+ 注册停止后的函数
+
+Args:
+
+ func:
+
+
+
+Returns:
+
+ LIFESPAN_FUNC:
+
+
+源代码
+
+```python
+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_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
+
+ 注册重启时的函数
+
+Args:
+
+ func:
+
+Returns:
+
+ LIFESPAN_FUNC:
+
+
+源代码
+
+```python
+def on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
+ """
+ 注册重启时的函数
+ Args:
+ func:
+ Returns:
+ LIFESPAN_FUNC:
+ """
+ self._before_process_restart_funcs.append(func)
+ return func
+```
+
+
+### ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
+
+ 注册重启后的函数
+
+Args:
+
+ func:
+
+Returns:
+
+ LIFESPAN_FUNC:
+
+
+源代码
+
+```python
+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: Any) -> None`
+
+ 注册 NoneBot 初始化后的函数
+
+Args:
+
+ func:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+def on_after_nonebot_init(self, func):
+ """
+ 注册 NoneBot 初始化后的函数
+ Args:
+ func:
+
+ Returns:
+
+ """
+ self._after_nonebot_init_funcs.append(func)
+ return func
+```
+
+
+### ***def*** `before_start(self) -> None`
+
+ 启动前
+
+Returns:
+
+
+源代码
+
+```python
+def before_start(self) -> None:
+ """
+ 启动前
+ Returns:
+ """
+ logger.debug('Running before_start functions')
+ self.run_funcs(self._before_start_funcs)
+```
+
+
+### ***def*** `after_start(self) -> None`
+
+ 启动后
+
+Returns:
+
+
+源代码
+
+```python
+def after_start(self) -> None:
+ """
+ 启动后
+ Returns:
+ """
+ logger.debug('Running after_start functions')
+ self.run_funcs(self._after_start_funcs)
+```
+
+
+### ***def*** `before_process_shutdown(self) -> None`
+
+ 停止前
+
+Returns:
+
+
+源代码
+
+```python
+def before_process_shutdown(self) -> None:
+ """
+ 停止前
+ Returns:
+ """
+ logger.debug('Running before_shutdown functions')
+ self.run_funcs(self._before_process_shutdown_funcs)
+```
+
+
+### ***def*** `after_shutdown(self) -> None`
+
+ 停止后
+
+Returns:
+
+
+源代码
+
+```python
+def after_shutdown(self) -> None:
+ """
+ 停止后
+ Returns:
+ """
+ logger.debug('Running after_shutdown functions')
+ self.run_funcs(self._after_shutdown_funcs)
+```
+
+
+### ***def*** `before_process_restart(self) -> None`
+
+ 重启前
+
+Returns:
+
+
+源代码
+
+```python
+def before_process_restart(self) -> None:
+ """
+ 重启前
+ Returns:
+ """
+ logger.debug('Running before_restart functions')
+ self.run_funcs(self._before_process_restart_funcs)
+```
+
+
+### ***def*** `after_restart(self) -> None`
+
+ 重启后
+
+Returns:
+
+
+源代码
+
+```python
+def after_restart(self) -> None:
+ """
+ 重启后
+ Returns:
+
+ """
+ logger.debug('Running after_restart functions')
+ self.run_funcs(self._after_restart_funcs)
+```
+
+
+### ***var*** `tasks = []`
+
+
+
+### ***var*** `loop = asyncio.get_event_loop()`
+
+
+
+### ***var*** `loop = asyncio.new_event_loop()`
+
+
+
diff --git a/docs/en/dev/api/comm/README.md b/docs/en/dev/api/comm/README.md
new file mode 100644
index 00000000..09bccc34
--- /dev/null
+++ b/docs/en/dev/api/comm/README.md
@@ -0,0 +1,7 @@
+---
+title: liteyuki.comm
+index: true
+icon: laptop-code
+category: API
+---
+
diff --git a/docs/en/dev/api/comm/channel.md b/docs/en/dev/api/comm/channel.md
new file mode 100644
index 00000000..8e5e170f
--- /dev/null
+++ b/docs/en/dev/api/comm/channel.md
@@ -0,0 +1,427 @@
+---
+title: liteyuki.comm.channel
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `set_channel(name: str, channel: Channel) -> None`
+
+设置通道实例
+
+Args:
+
+ name: 通道名称
+
+ channel: 通道实例
+
+
+源代码
+
+```python
+def set_channel(name: str, channel: Channel):
+ """
+ 设置通道实例
+ Args:
+ name: 通道名称
+ channel: 通道实例
+ """
+ if not isinstance(channel, Channel):
+ raise TypeError(f'channel_ must be an instance of Channel, {type(channel)} found')
+ if IS_MAIN_PROCESS:
+ _channel[name] = channel
+ else:
+ channel_deliver_passive_channel.send(('set_channel', {'name': name, 'channel_': channel}))
+```
+
+
+### ***def*** `set_channels(channels: dict[str, Channel]) -> None`
+
+设置通道实例
+
+Args:
+
+ channels: 通道名称
+
+
+源代码
+
+```python
+def set_channels(channels: dict[str, Channel]):
+ """
+ 设置通道实例
+ Args:
+ channels: 通道名称
+ """
+ for name, channel in channels.items():
+ set_channel(name, channel)
+```
+
+
+### ***def*** `get_channel(name: str) -> Channel`
+
+获取通道实例
+
+Args:
+
+ name: 通道名称
+
+Returns:
+
+
+源代码
+
+```python
+def get_channel(name: str) -> Channel:
+ """
+ 获取通道实例
+ Args:
+ name: 通道名称
+ Returns:
+ """
+ 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]`
+
+获取通道实例
+
+Returns:
+
+
+源代码
+
+```python
+def get_channels() -> dict[str, Channel]:
+ """
+ 获取通道实例
+ Returns:
+ """
+ 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()
+```
+
+
+### ***def*** `on_set_channel(data: tuple[str, dict[str, Any]]) -> None`
+
+
+
+
+源代码
+
+```python
+@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)
+```
+
+
+### ***def*** `on_get_channel(data: tuple[str, dict[str, Any]]) -> None`
+
+
+
+
+源代码
+
+```python
+@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))
+```
+
+
+### ***def*** `on_get_channels(data: tuple[str, dict[str, Any]]) -> None`
+
+
+
+
+源代码
+
+```python
+@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*** `decorator(func: Callable[[T], Any]) -> Callable[[T], Any]`
+
+
+
+
+源代码
+
+```python
+def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
+ global _func_id
+
+ async def wrapper(data: T) -> Any:
+ if filter_func is not None:
+ if is_coroutine_callable(filter_func):
+ if not await filter_func(data):
+ return
+ elif not filter_func(data):
+ return
+ 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(_func_id)
+ else:
+ self._on_sub_receive_funcs.append(_func_id)
+ _func_id += 1
+ return func
+```
+
+
+### ***async def*** `wrapper(data: T) -> Any`
+
+
+
+
+源代码
+
+```python
+async def wrapper(data: T) -> Any:
+ if filter_func is not None:
+ if is_coroutine_callable(filter_func):
+ if not await filter_func(data):
+ return
+ elif not filter_func(data):
+ return
+ if is_coroutine_callable(func):
+ return await func(data)
+ else:
+ return func(data)
+```
+
+
+### ***class*** `Channel(Generic[T])`
+
+通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
+
+有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
+
+### ***def*** `__init__(self, _id: str, type_check: Optional[bool]) -> None`
+
+ 初始化通道
+
+Args:
+
+ _id: 通道ID
+
+ type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
+
+
+源代码
+
+```python
+def __init__(self, _id: str, type_check: Optional[bool]=None):
+ """
+ 初始化通道
+ Args:
+ _id: 通道ID
+ type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
+ """
+ self.conn_send, self.conn_recv = Pipe()
+ self._closed = False
+ self._on_main_receive_funcs: list[int] = []
+ self._on_sub_receive_funcs: list[int] = []
+ self.name: str = _id
+ self.is_main_receive_loop_running = False
+ self.is_sub_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 is required for enforcing type check.')
+ self.type_check = type_check
+```
+
+
+### ***def*** `send(self, data: T) -> None`
+
+ 发送数据
+
+Args:
+
+ data: 数据
+
+
+源代码
+
+```python
+def send(self, data: T):
+ """
+ 发送数据
+ Args:
+ 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'Data must be an instance of {_type}, {type(data)} found')
+ if self._closed:
+ raise RuntimeError('Cannot send to a closed channel_')
+ self.conn_send.send(data)
+```
+
+
+### ***def*** `receive(self) -> T`
+
+ 接收数据
+
+Args:
+
+
+源代码
+
+```python
+def receive(self) -> T:
+ """
+ 接收数据
+ Args:
+ """
+ if self._closed:
+ raise RuntimeError('Cannot receive from a closed channel_')
+ while True:
+ data = self.conn_recv.recv()
+ return data
+```
+
+
+### ***def*** `close(self) -> None`
+
+ 关闭通道
+
+
+源代码
+
+```python
+def close(self):
+ """
+ 关闭通道
+ """
+ self._closed = True
+ self.conn_send.close()
+ self.conn_recv.close()
+```
+
+
+### ***def*** `on_receive(self, filter_func: Optional[FILTER_FUNC]) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]`
+
+ 接收数据并执行函数
+
+Args:
+
+ filter_func: 过滤函数,为None则不过滤
+
+Returns:
+
+ 装饰器,装饰一个函数在接收到数据后执行
+
+
+源代码
+
+```python
+def on_receive(self, filter_func: Optional[FILTER_FUNC]=None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
+ """
+ 接收数据并执行函数
+ 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, daemon=True).start()
+ if not self.is_main_receive_loop_running and IS_MAIN_PROCESS:
+ threading.Thread(target=self._start_main_receive_loop, daemon=True).start()
+
+ def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
+ global _func_id
+
+ async def wrapper(data: T) -> Any:
+ if filter_func is not None:
+ if is_coroutine_callable(filter_func):
+ if not await filter_func(data):
+ return
+ elif not filter_func(data):
+ return
+ 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(_func_id)
+ else:
+ self._on_sub_receive_funcs.append(_func_id)
+ _func_id += 1
+ return func
+ return decorator
+```
+
+
+### ***var*** `T = TypeVar('T')`
+
+
+
+### ***var*** `channel_deliver_active_channel = Channel(_id='channel_deliver_active_channel')`
+
+
+
+### ***var*** `channel_deliver_passive_channel = Channel(_id='channel_deliver_passive_channel')`
+
+
+
+### ***var*** `recv_chan = data[1]['recv_chan']`
+
+
+
+### ***var*** `recv_chan = Channel[Channel[Any]]('recv_chan')`
+
+
+
+### ***var*** `recv_chan = Channel[dict[str, Channel[Any]]]('recv_chan')`
+
+
+
+### ***var*** `type_check = self._get_generic_type() is not None`
+
+
+
+### ***var*** `data = self.conn_recv.recv()`
+
+
+
+### ***var*** `func = _callback_funcs[func_id]`
+
+
+
+### ***var*** `func = _callback_funcs[func_id]`
+
+
+
+### ***var*** `data = self.conn_recv.recv()`
+
+
+
+### ***var*** `data = self.conn_recv.recv()`
+
+
+
diff --git a/docs/en/dev/api/comm/event.md b/docs/en/dev/api/comm/event.md
new file mode 100644
index 00000000..a2a15f55
--- /dev/null
+++ b/docs/en/dev/api/comm/event.md
@@ -0,0 +1,25 @@
+---
+title: liteyuki.comm.event
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***class*** `Event`
+
+事件类
+
+### ***def*** `__init__(self, name: str, data: dict[str, Any]) -> None`
+
+
+
+
+源代码
+
+```python
+def __init__(self, name: str, data: dict[str, Any]):
+ self.name = name
+ self.data = data
+```
+
+
diff --git a/docs/en/dev/api/comm/storage.md b/docs/en/dev/api/comm/storage.md
new file mode 100644
index 00000000..cd19dbb7
--- /dev/null
+++ b/docs/en/dev/api/comm/storage.md
@@ -0,0 +1,563 @@
+---
+title: liteyuki.comm.storage
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
+
+运行订阅者接收函数
+
+Args:
+
+ channel_: 频道
+
+ data: 数据
+
+
+源代码
+
+```python
+@staticmethod
+def run_subscriber_receive_funcs(channel_: str, data: Any):
+ """
+ 运行订阅者接收函数
+ Args:
+ channel_: 频道
+ data: 数据
+ """
+ if IS_MAIN_PROCESS:
+ if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
+ run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
+ elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
+ run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
+```
+
+
+### ***def*** `on_get(data: tuple[str, dict[str, Any]]) -> None`
+
+
+
+
+源代码
+
+```python
+@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))
+```
+
+
+### ***def*** `on_set(data: tuple[str, dict[str, Any]]) -> None`
+
+
+
+
+源代码
+
+```python
+@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)
+```
+
+
+### ***def*** `on_delete(data: tuple[str, dict[str, Any]]) -> None`
+
+
+
+
+源代码
+
+```python
+@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)
+```
+
+
+### ***def*** `on_get_all(data: tuple[str, dict[str, Any]]) -> None`
+
+
+
+
+源代码
+
+```python
+@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())
+```
+
+
+### ***def*** `on_publish(data: tuple[str, Any]) -> None`
+
+
+
+
+源代码
+
+```python
+@channel.publish_channel.on_receive()
+def on_publish(data: tuple[str, Any]):
+ channel_, data = data
+ shared_memory.run_subscriber_receive_funcs(channel_, data)
+```
+
+
+### ***def*** `decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC`
+
+
+
+
+源代码
+
+```python
+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
+```
+
+
+### ***async def*** `wrapper(data: Any) -> None`
+
+
+
+
+源代码
+
+```python
+async def wrapper(data: Any):
+ if is_coroutine_callable(func):
+ await func(data)
+ else:
+ func(data)
+```
+
+
+### ***class*** `Subscriber`
+
+
+
+### ***def*** `__init__(self) -> None`
+
+
+
+
+源代码
+
+```python
+def __init__(self):
+ self._subscribers = {}
+```
+
+
+### ***def*** `receive(self) -> Any`
+
+
+
+
+源代码
+
+```python
+def receive(self) -> Any:
+ pass
+```
+
+
+### ***def*** `unsubscribe(self) -> None`
+
+
+
+
+源代码
+
+```python
+def unsubscribe(self) -> None:
+ pass
+```
+
+
+### ***class*** `KeyValueStore`
+
+
+
+### ***def*** `__init__(self) -> None`
+
+
+
+
+源代码
+
+```python
+def __init__(self):
+ self._store = {}
+ self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-active')
+ self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-passive')
+ self.publish_channel = Channel[tuple[str, Any]](_id='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: 值
+
+
+源代码
+
+```python
+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]) -> Optional[Any]`
+
+ 获取键值对
+
+Args:
+
+ key: 键
+
+ default: 默认值
+
+
+
+Returns:
+
+ Any: 值
+
+
+源代码
+
+```python
+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) -> None`
+
+ 删除键值对
+
+Args:
+
+ key: 键
+
+ ignore_key_error: 是否忽略键不存在的错误
+
+
+
+Returns:
+
+
+源代码
+
+```python
+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]: 键值对
+
+
+源代码
+
+```python
+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:
+
+
+源代码
+
+```python
+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:
+
+ 装饰器
+
+
+源代码
+
+```python
+def on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
+ """
+ 订阅者接收消息时的回调
+ Args:
+ channel_: 频道
+
+ Returns:
+ 装饰器
+ """
+ if IS_MAIN_PROCESS and (not self.is_main_receive_loop_running):
+ threading.Thread(target=self._start_receive_loop, daemon=True).start()
+ shared_memory.is_main_receive_loop_running = True
+ elif not IS_MAIN_PROCESS and (not self.is_sub_receive_loop_running):
+ threading.Thread(target=self._start_receive_loop, daemon=True).start()
+ shared_memory.is_sub_receive_loop_running = True
+
+ 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***
+### ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
+
+ 运行订阅者接收函数
+
+Args:
+
+ channel_: 频道
+
+ data: 数据
+
+
+源代码
+
+```python
+@staticmethod
+def run_subscriber_receive_funcs(channel_: str, data: Any):
+ """
+ 运行订阅者接收函数
+ Args:
+ channel_: 频道
+ data: 数据
+ """
+ if IS_MAIN_PROCESS:
+ if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
+ run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
+ elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
+ run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
+```
+
+
+### ***class*** `GlobalKeyValueStore`
+
+
+
+### ***@classmethod***
+### ***def*** `get_instance(cls: Any) -> None`
+
+
+
+
+源代码
+
+```python
+@classmethod
+def get_instance(cls):
+ if cls._instance is None:
+ with cls._lock:
+ if cls._instance is None:
+ cls._instance = KeyValueStore()
+ return cls._instance
+```
+
+
+### ***attr*** `_instance: None`
+
+### ***attr*** `_lock: threading.Lock()`
+
+### ***var*** `key = data[1]['key']`
+
+
+
+### ***var*** `default = data[1]['default']`
+
+
+
+### ***var*** `recv_chan = data[1]['recv_chan']`
+
+
+
+### ***var*** `key = data[1]['key']`
+
+
+
+### ***var*** `value = data[1]['value']`
+
+
+
+### ***var*** `key = data[1]['key']`
+
+
+
+### ***var*** `recv_chan = data[1]['recv_chan']`
+
+
+
+### ***var*** `lock = _get_lock(key)`
+
+
+
+### ***var*** `lock = _get_lock(key)`
+
+
+
+### ***var*** `recv_chan = Channel[Optional[Any]]('recv_chan')`
+
+
+
+### ***var*** `lock = _get_lock(key)`
+
+
+
+### ***var*** `recv_chan = Channel[dict[str, Any]]('recv_chan')`
+
+
+
+### ***var*** `data = self.active_chan.receive()`
+
+
+
+### ***var*** `data = self.publish_channel.receive()`
+
+
+
diff --git a/docs/en/dev/api/config.md b/docs/en/dev/api/config.md
new file mode 100644
index 00000000..24afd6c9
--- /dev/null
+++ b/docs/en/dev/api/config.md
@@ -0,0 +1,231 @@
+---
+title: liteyuki.config
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `flat_config(config: dict[str, Any]) -> dict[str, Any]`
+
+扁平化配置文件
+
+
+
+{a:{b:{c:1}}} -> {"a.b.c": 1}
+
+Args:
+
+ config: 配置项目
+
+
+
+Returns:
+
+ 扁平化后的配置文件,但也包含原有的键值对
+
+
+源代码
+
+```python
+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
+```
+
+
+### ***def*** `load_from_yaml(file: str) -> dict[str, Any]`
+
+Load config from yaml file
+
+
+源代码
+
+```python
+def load_from_yaml(file: str) -> dict[str, Any]:
+ """
+ Load config from yaml file
+
+ """
+ logger.debug(f'Loading YAML config from {file}')
+ config = yaml.safe_load(open(file, 'r', encoding='utf-8'))
+ return flat_config(config if config is not None else {})
+```
+
+
+### ***def*** `load_from_json(file: str) -> dict[str, Any]`
+
+Load config from json file
+
+
+源代码
+
+```python
+def load_from_json(file: str) -> dict[str, Any]:
+ """
+ Load config from json file
+ """
+ logger.debug(f'Loading JSON config from {file}')
+ config = json.load(open(file, 'r', encoding='utf-8'))
+ return flat_config(config if config is not None else {})
+```
+
+
+### ***def*** `load_from_toml(file: str) -> dict[str, Any]`
+
+Load config from toml file
+
+
+源代码
+
+```python
+def load_from_toml(file: str) -> dict[str, Any]:
+ """
+ Load config from toml file
+ """
+ logger.debug(f'Loading TOML config from {file}')
+ config = toml.load(open(file, 'r', encoding='utf-8'))
+ return flat_config(config if config is not None else {})
+```
+
+
+### ***def*** `load_from_files() -> dict[str, Any]`
+
+从指定文件加载配置项,会自动识别文件格式
+
+默认执行扁平化选项
+
+
+源代码
+
+```python
+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))
+ elif not no_warning:
+ logger.warning(f'Unsupported config file format: {file}')
+ elif not no_warning:
+ logger.warning(f'Config file not found: {file}')
+ return config
+```
+
+
+### ***def*** `load_configs_from_dirs() -> dict[str, Any]`
+
+从目录下加载配置文件,不递归
+
+按照读取文件的优先级反向覆盖
+
+默认执行扁平化选项
+
+
+源代码
+
+```python
+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 not found: {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) -> dict[str, Any]`
+
+从一个标准的轻雪项目加载配置文件
+
+项目目录下的config.*和config目录下的所有配置文件
+
+项目目录下的配置文件优先
+
+
+源代码
+
+```python
+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
+```
+
+
+### ***class*** `SatoriNodeConfig(BaseModel)`
+
+
+
+### ***class*** `SatoriConfig(BaseModel)`
+
+
+
+### ***class*** `BasicConfig(BaseModel)`
+
+
+
+### ***var*** `new_config = copy.deepcopy(config)`
+
+
+
+### ***var*** `config = yaml.safe_load(open(file, 'r', encoding='utf-8'))`
+
+
+
+### ***var*** `config = json.load(open(file, 'r', encoding='utf-8'))`
+
+
+
+### ***var*** `config = toml.load(open(file, 'r', encoding='utf-8'))`
+
+
+
+### ***var*** `config = {}`
+
+
+
+### ***var*** `config = {}`
+
+
+
+### ***var*** `config = load_configs_from_dirs('config', no_waring=no_waring)`
+
+
+
diff --git a/docs/en/dev/api/core/README.md b/docs/en/dev/api/core/README.md
new file mode 100644
index 00000000..26c3500b
--- /dev/null
+++ b/docs/en/dev/api/core/README.md
@@ -0,0 +1,7 @@
+---
+title: liteyuki.core
+index: true
+icon: laptop-code
+category: API
+---
+
diff --git a/docs/en/dev/api/core/manager.md b/docs/en/dev/api/core/manager.md
new file mode 100644
index 00000000..52397375
--- /dev/null
+++ b/docs/en/dev/api/core/manager.md
@@ -0,0 +1,275 @@
+---
+title: liteyuki.core.manager
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***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]]) -> None`
+
+
+
+
+源代码
+
+```python
+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
+```
+
+
+### ***class*** `ProcessManager`
+
+进程管理器
+
+### ***def*** `__init__(self, lifespan: 'Lifespan') -> None`
+
+
+
+
+源代码
+
+```python
+def __init__(self, lifespan: 'Lifespan'):
+ self.lifespan = lifespan
+ self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
+ self.processes: dict[str, Process] = {}
+```
+
+
+### ***def*** `start(self, name: str) -> None`
+
+ 开启后自动监控进程,并添加到进程字典中
+
+Args:
+
+ name:
+
+Returns:
+
+
+源代码
+
+```python
+def start(self, name: str):
+ """
+ 开启后自动监控进程,并添加到进程字典中
+ Args:
+ name:
+ Returns:
+
+ """
+ if name not in self.targets:
+ raise KeyError(f'Process {name} not found.')
+ chan_active = get_channel(f'{name}-active')
+
+ 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()
+ _start_process()
+ while True:
+ data = chan_active.receive()
+ if data == 0:
+ logger.info(f'Stopping process {name}')
+ self.lifespan.before_process_shutdown()
+ self.terminate(name)
+ break
+ elif data == 1:
+ logger.info(f'Restarting process {name}')
+ self.lifespan.before_process_shutdown()
+ self.lifespan.before_process_restart()
+ self.terminate(name)
+ _start_process()
+ continue
+ else:
+ logger.warning('Unknown data received, ignored.')
+```
+
+
+### ***def*** `start_all(self) -> None`
+
+ 启动所有进程
+
+
+源代码
+
+```python
+def start_all(self):
+ """
+ 启动所有进程
+ """
+ for name in self.targets:
+ threading.Thread(target=self.start, args=(name,), daemon=True).start()
+```
+
+
+### ***def*** `add_target(self, name: str, target: TARGET_FUNC, args: tuple, kwargs: Any) -> None`
+
+ 添加进程
+
+Args:
+
+ name: 进程名,用于获取和唯一标识
+
+ target: 进程函数
+
+ args: 进程函数参数
+
+ kwargs: 进程函数关键字参数,通常会默认传入chan_active和chan_passive
+
+
+源代码
+
+```python
+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(_id=f'{name}-active')
+ chan_passive: Channel = Channel(_id=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)
+ set_channels({f'{name}-active': chan_active, f'{name}-passive': chan_passive})
+```
+
+
+### ***def*** `join_all(self) -> None`
+
+
+
+
+源代码
+
+```python
+def join_all(self):
+ for name, process in self.targets:
+ process.join()
+```
+
+
+### ***def*** `terminate(self, name: str) -> None`
+
+ 终止进程并从进程字典中删除
+
+Args:
+
+ name:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+def terminate(self, name: str):
+ """
+ 终止进程并从进程字典中删除
+ Args:
+ name:
+
+ Returns:
+
+ """
+ if name not in self.processes:
+ logger.warning(f'Process {name} not found.')
+ return
+ process = self.processes[name]
+ process.terminate()
+ process.join(TIMEOUT)
+ if process.is_alive():
+ process.kill()
+ logger.success(f'Process {name} terminated.')
+```
+
+
+### ***def*** `terminate_all(self) -> None`
+
+
+
+
+源代码
+
+```python
+def terminate_all(self):
+ for name in self.targets:
+ self.terminate(name)
+```
+
+
+### ***def*** `is_process_alive(self, name: str) -> bool`
+
+ 检查进程是否存活
+
+Args:
+
+ name:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+def is_process_alive(self, name: str) -> bool:
+ """
+ 检查进程是否存活
+ Args:
+ name:
+
+ Returns:
+
+ """
+ if name not in self.targets:
+ logger.warning(f'Process {name} not found.')
+ return self.processes[name].is_alive()
+```
+
+
+### ***var*** `TIMEOUT = 10`
+
+
+
+### ***var*** `chan_active = get_channel(f'{name}-active')`
+
+
+
+### ***var*** `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)`
+
+
+
+### ***var*** `process = self.processes[name]`
+
+
+
+### ***var*** `process = Process(target=self.targets[name][0], args=self.targets[name][1], kwargs=self.targets[name][2], daemon=True)`
+
+
+
+### ***var*** `data = chan_active.receive()`
+
+
+
+### ***var*** `kwargs = {}`
+
+
+
diff --git a/docs/en/dev/api/dev/README.md b/docs/en/dev/api/dev/README.md
new file mode 100644
index 00000000..6d883442
--- /dev/null
+++ b/docs/en/dev/api/dev/README.md
@@ -0,0 +1,7 @@
+---
+title: liteyuki.dev
+index: true
+icon: laptop-code
+category: API
+---
+
diff --git a/docs/en/dev/api/dev/observer.md b/docs/en/dev/api/dev/observer.md
new file mode 100644
index 00000000..2005bbf2
--- /dev/null
+++ b/docs/en/dev/api/dev/observer.md
@@ -0,0 +1,249 @@
+---
+title: liteyuki.dev.observer
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `debounce(wait: Any) -> None`
+
+防抖函数
+
+
+源代码
+
+```python
+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
+```
+
+
+### ***def*** `on_file_system_event(directories: tuple[str], recursive: bool, event_filter: FILTER_FUNC) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]`
+
+注册文件系统变化监听器
+
+Args:
+
+ directories: 监听目录们
+
+ recursive: 是否递归监听子目录
+
+ event_filter: 事件过滤器, 返回True则执行回调函数
+
+Returns:
+
+ 装饰器,装饰一个函数在接收到数据后执行
+
+
+源代码
+
+```python
+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
+```
+
+
+### ***def*** `decorator(func: Any) -> None`
+
+
+
+
+源代码
+
+```python
+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
+```
+
+
+### ***def*** `decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC`
+
+
+
+
+源代码
+
+```python
+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
+```
+
+
+### ***def*** `wrapper() -> None`
+
+
+
+
+源代码
+
+```python
+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)
+```
+
+
+### ***def*** `wrapper(event: FileSystemEvent) -> None`
+
+
+
+
+源代码
+
+```python
+def wrapper(event: FileSystemEvent):
+ if event_filter is not None and (not event_filter(event)):
+ return
+ func(event)
+```
+
+
+### ***class*** `CodeModifiedHandler(FileSystemEventHandler)`
+
+Handler for code file changes
+
+### ***def*** `on_modified(self, event: Any) -> None`
+
+
+
+
+源代码
+
+```python
+@debounce(1)
+def on_modified(self, event):
+ raise NotImplementedError('on_modified must be implemented')
+```
+
+
+### ***def*** `on_created(self, event: Any) -> None`
+
+
+
+
+源代码
+
+```python
+def on_created(self, event):
+ self.on_modified(event)
+```
+
+
+### ***def*** `on_deleted(self, event: Any) -> None`
+
+
+
+
+源代码
+
+```python
+def on_deleted(self, event):
+ self.on_modified(event)
+```
+
+
+### ***def*** `on_moved(self, event: Any) -> None`
+
+
+
+
+源代码
+
+```python
+def on_moved(self, event):
+ self.on_modified(event)
+```
+
+
+### ***def*** `on_any_event(self, event: Any) -> None`
+
+
+
+
+源代码
+
+```python
+def on_any_event(self, event):
+ self.on_modified(event)
+```
+
+
+### ***var*** `liteyuki_bot = get_bot()`
+
+
+
+### ***var*** `observer = Observer()`
+
+
+
+### ***var*** `last_call_time = None`
+
+
+
+### ***var*** `code_modified_handler = CodeModifiedHandler()`
+
+
+
+### ***var*** `current_time = time.time()`
+
+
+
+### ***var*** `last_call_time = current_time`
+
+
+
diff --git a/docs/en/dev/api/dev/plugin.md b/docs/en/dev/api/dev/plugin.md
new file mode 100644
index 00000000..caafa5ea
--- /dev/null
+++ b/docs/en/dev/api/dev/plugin.md
@@ -0,0 +1,46 @@
+---
+title: liteyuki.dev.plugin
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `run_plugins() -> None`
+
+运行插件,无需手动初始化bot
+
+Args:
+
+ module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
+
+
+源代码
+
+```python
+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()
+```
+
+
+### ***var*** `cfg = load_config_in_default()`
+
+
+
+### ***var*** `plugins = cfg.get('liteyuki.plugins', [])`
+
+
+
+### ***var*** `bot = LiteyukiBot(**cfg)`
+
+
+
diff --git a/docs/en/dev/api/exception.md b/docs/en/dev/api/exception.md
new file mode 100644
index 00000000..469c00d6
--- /dev/null
+++ b/docs/en/dev/api/exception.md
@@ -0,0 +1,11 @@
+---
+title: liteyuki.exception
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***class*** `LiteyukiException(BaseException)`
+
+Liteyuki的异常基类。
+
diff --git a/docs/en/dev/api/log.md b/docs/en/dev/api/log.md
new file mode 100644
index 00000000..af382275
--- /dev/null
+++ b/docs/en/dev/api/log.md
@@ -0,0 +1,58 @@
+---
+title: liteyuki.log
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `get_format(level: str) -> str`
+
+
+
+
+源代码
+
+```python
+def get_format(level: str) -> str:
+ if level == 'DEBUG':
+ return debug_format
+ else:
+ return default_format
+```
+
+
+### ***def*** `init_log(config: dict) -> None`
+
+在语言加载完成后执行
+
+Returns:
+
+
+源代码
+
+```python
+def init_log(config: dict):
+ """
+ 在语言加载完成后执行
+ Returns:
+
+ """
+ logger.remove()
+ logger.add(sys.stdout, level=0, diagnose=False, format=get_format(config.get('log_level', 'INFO')))
+ show_icon = config.get('log_icon', True)
+ logger.level('DEBUG', color='', icon=f"{('🐛' if show_icon else '')}DEBUG")
+ logger.level('INFO', color='', icon=f"{('ℹ️' if show_icon else '')}INFO")
+ logger.level('SUCCESS', color='', icon=f"{('✅' if show_icon else '')}SUCCESS")
+ logger.level('WARNING', color='', icon=f"{('⚠️' if show_icon else '')}WARNING")
+ logger.level('ERROR', color='', icon=f"{('⭕' if show_icon else '')}ERROR")
+```
+
+
+### ***var*** `logger = loguru.logger`
+
+
+
+### ***var*** `show_icon = config.get('log_icon', True)`
+
+
+
diff --git a/docs/en/dev/api/message/README.md b/docs/en/dev/api/message/README.md
new file mode 100644
index 00000000..0d664851
--- /dev/null
+++ b/docs/en/dev/api/message/README.md
@@ -0,0 +1,7 @@
+---
+title: liteyuki.message
+index: true
+icon: laptop-code
+category: API
+---
+
diff --git a/docs/en/dev/api/message/event.md b/docs/en/dev/api/message/event.md
new file mode 100644
index 00000000..740ff8bd
--- /dev/null
+++ b/docs/en/dev/api/message/event.md
@@ -0,0 +1,106 @@
+---
+title: liteyuki.message.event
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***class*** `MessageEvent`
+
+
+
+### ***def*** `__init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, 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: 附加数据
+
+
+源代码
+
+```python
+def __init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, 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.receive_channel = receive_channel
+```
+
+
+### ***def*** `reply(self, message: str | dict[str, Any]) -> None`
+
+ 回复消息
+
+Args:
+
+ message:
+
+Returns:
+
+
+源代码
+
+```python
+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, session_type=self.session_type, receive_channel='_')
+ shared_memory.publish(self.receive_channel, reply_event)
+```
+
+
+### ***var*** `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, session_type=self.session_type, receive_channel='_')`
+
+
+
+### ***var*** `data = {}`
+
+
+
diff --git a/docs/en/dev/api/message/matcher.md b/docs/en/dev/api/message/matcher.md
new file mode 100644
index 00000000..4a6341b6
--- /dev/null
+++ b/docs/en/dev/api/message/matcher.md
@@ -0,0 +1,71 @@
+---
+title: liteyuki.message.matcher
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***class*** `Matcher`
+
+
+
+### ***def*** `__init__(self, rule: Rule, priority: int, block: bool) -> None`
+
+ 匹配器
+
+Args:
+
+ rule: 规则
+
+ priority: 优先级 >= 0
+
+ block: 是否阻断后续优先级更低的匹配器
+
+
+源代码
+
+```python
+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*** `handle(self, handler: EventHandler) -> EventHandler`
+
+ 添加处理函数,装饰器
+
+Args:
+
+ handler:
+
+Returns:
+
+ EventHandler
+
+
+源代码
+
+```python
+def handle(self, handler: EventHandler) -> EventHandler:
+ """
+ 添加处理函数,装饰器
+ Args:
+ handler:
+ Returns:
+ EventHandler
+ """
+ self.handlers.append(handler)
+ return handler
+```
+
+
diff --git a/docs/en/dev/api/message/on.md b/docs/en/dev/api/message/on.md
new file mode 100644
index 00000000..4f3c7230
--- /dev/null
+++ b/docs/en/dev/api/message/on.md
@@ -0,0 +1,39 @@
+---
+title: liteyuki.message.on
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `on_message(rule: Rule, priority: int, block: bool) -> Matcher`
+
+
+
+
+源代码
+
+```python
+def on_message(rule: Rule=Rule(), priority: int=0, block: bool=True) -> Matcher:
+ matcher = Matcher(rule, priority, block)
+ for i, m in enumerate(_matcher_list):
+ if m.priority < matcher.priority:
+ _matcher_list.insert(i, matcher)
+ break
+ else:
+ _matcher_list.append(matcher)
+ return matcher
+```
+
+
+### ***var*** `current_priority = -1`
+
+
+
+### ***var*** `matcher = Matcher(rule, priority, block)`
+
+
+
+### ***var*** `current_priority = matcher.priority`
+
+
+
diff --git a/docs/en/dev/api/message/rule.md b/docs/en/dev/api/message/rule.md
new file mode 100644
index 00000000..a135f740
--- /dev/null
+++ b/docs/en/dev/api/message/rule.md
@@ -0,0 +1,24 @@
+---
+title: liteyuki.message.rule
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***class*** `Rule`
+
+
+
+### ***def*** `__init__(self, handler: Optional[RuleHandler]) -> None`
+
+
+
+
+源代码
+
+```python
+def __init__(self, handler: Optional[RuleHandler]=None):
+ self.handler = handler
+```
+
+
diff --git a/docs/en/dev/api/message/session.md b/docs/en/dev/api/message/session.md
new file mode 100644
index 00000000..f8fcad6f
--- /dev/null
+++ b/docs/en/dev/api/message/session.md
@@ -0,0 +1,7 @@
+---
+title: liteyuki.message.session
+order: 1
+icon: laptop-code
+category: API
+---
+
diff --git a/docs/en/dev/api/mkdoc.md b/docs/en/dev/api/mkdoc.md
new file mode 100644
index 00000000..802aff3c
--- /dev/null
+++ b/docs/en/dev/api/mkdoc.md
@@ -0,0 +1,473 @@
+---
+title: liteyuki.mkdoc
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `get_relative_path(base_path: str, target_path: str) -> str`
+
+获取相对路径
+
+Args:
+
+ base_path: 基础路径
+
+ target_path: 目标路径
+
+
+源代码
+
+```python
+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]) -> None`
+
+输出文件
+
+Args:
+
+ file_data: 文件数据 相对路径
+
+
+源代码
+
+```python
+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) -> None`
+
+
+
+
+源代码
+
+```python
+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) -> ModuleInfo`
+
+获取函数和类
+
+Args:
+
+ file_path: Python 文件路径
+
+ ignore_private: 忽略私有函数和类
+
+Returns:
+
+ 模块信息
+
+
+源代码
+
+```python
+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)):
+ if not any((isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(node))) and (not ignore_private or not node.name.startswith('_')):
+ 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:
+ 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)))
+ 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: Any) -> str`
+
+生成模块的Markdown
+
+你可在此自定义生成的Markdown格式
+
+Args:
+
+ module_info: 模块信息
+
+ front_matter: 自定义选项title, index, icon, category
+
+Returns:
+
+ Markdown 字符串
+
+
+源代码
+
+```python
+def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
+ """
+ 生成模块的Markdown
+ 你可在此自定义生成的Markdown格式
+ Args:
+ module_info: 模块信息
+ front_matter: 自定义选项title, index, icon, category
+ 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'\n源代码
\n\n```python\n{func.source_code}\n```\n \n\n'
+ for cls in module_info.classes:
+ if cls.inherit:
+ inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ''
+ content += f'### ***class*** `{cls.name}{inherit}`\n\n'
+ else:
+ content += f'### ***class*** `{cls.name}`\n\n'
+ cls.docstring = cls.docstring.replace('\n', '\n\n')
+ content += f'{cls.docstring}\n\n'
+ for method in cls.methods:
+ if method.type != DefType.METHOD:
+ args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in method.args]
+ content += f'### ***@{method.type.value}***\n'
+ else:
+ args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] and arg[0] != 'self' else arg[0] for arg in method.args]
+ content += f"### ***{('async ' if method.is_async else '')}def*** `{method.name}({', '.join(args_with_type)}) -> {method.return_type}`\n\n"
+ method.docstring = method.docstring.replace('\n', '\n\n')
+ content += f' {method.docstring}\n\n'
+ content += f'\n源代码
\n\n```python\n{method.source_code}\n```\n \n\n'
+ for attr in cls.attributes:
+ content += f'### ***attr*** `{attr.name}: {attr.type}`\n\n'
+ for attr in module_info.attributes:
+ if attr.type == NO_TYPE_HINT:
+ content += f'### ***var*** `{attr.name} = {attr.value}`\n\n'
+ else:
+ content += f'### ***var*** `{attr.name}: {attr.type} = {attr.value}`\n\n'
+ attr.docstring = attr.docstring.replace('\n', '\n\n')
+ content += f'{attr.docstring}\n\n'
+ return content
+```
+
+
+### ***def*** `generate_docs(module_folder: str, output_dir: str, with_top: bool, ignored_paths: Any) -> 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: 忽略的路径
+
+
+源代码
+
+```python
+def generate_docs(module_folder: str, output_dir: str, with_top: bool=False, 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: 忽略的路径
+ """
+ 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)
+ 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)
+ 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)
+```
+
+
+### ***class*** `DefType(Enum)`
+
+
+
+### ***attr*** `FUNCTION: 'function'`
+
+### ***attr*** `METHOD: 'method'`
+
+### ***attr*** `STATIC_METHOD: 'staticmethod'`
+
+### ***attr*** `CLASS_METHOD: 'classmethod'`
+
+### ***attr*** `PROPERTY: 'property'`
+
+### ***class*** `FunctionInfo(BaseModel)`
+
+
+
+### ***class*** `AttributeInfo(BaseModel)`
+
+
+
+### ***class*** `ClassInfo(BaseModel)`
+
+
+
+### ***class*** `ModuleInfo(BaseModel)`
+
+
+
+### ***var*** `NO_TYPE_ANY = 'Any'`
+
+
+
+### ***var*** `NO_TYPE_HINT = 'NoTypeHint'`
+
+
+
+### ***var*** `FUNCTION = 'function'`
+
+
+
+### ***var*** `METHOD = 'method'`
+
+
+
+### ***var*** `STATIC_METHOD = 'staticmethod'`
+
+
+
+### ***var*** `CLASS_METHOD = 'classmethod'`
+
+
+
+### ***var*** `PROPERTY = 'property'`
+
+
+
+### ***var*** `file_list = []`
+
+
+
+### ***var*** `dot_sep_module_path = file_path.replace(os.sep, '.').replace('.py', '').replace('.pyi', '')`
+
+
+
+### ***var*** `module_docstring = ast.get_docstring(tree)`
+
+
+
+### ***var*** `module_info = ModuleInfo(module_path=dot_sep_module_path, functions=[], classes=[], attributes=[], docstring=module_docstring if module_docstring else '')`
+
+
+
+### ***var*** `content = ''`
+
+
+
+### ***var*** `front_matter = '---\n' + '\n'.join([f'{k}: {v}' for k, v in front_matter.items()]) + '\n---\n\n'`
+
+
+
+### ***var*** `file_list = get_file_list(module_folder)`
+
+
+
+### ***var*** `replace_data = {'__init__': 'README', '.py': '.md'}`
+
+
+
+### ***var*** `file_content = file.read()`
+
+
+
+### ***var*** `tree = ast.parse(file_content)`
+
+
+
+### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in func.args]`
+
+
+
+### ***var*** `ignored_paths = []`
+
+
+
+### ***var*** `no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path)`
+
+
+
+### ***var*** `rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path`
+
+
+
+### ***var*** `abs_md_path = os.path.join(output_dir, rel_md_path)`
+
+
+
+### ***var*** `module_info = get_module_info_normal(pyfile_path)`
+
+
+
+### ***var*** `md_content = generate_markdown(module_info, front_matter)`
+
+
+
+### ***var*** `inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ''`
+
+
+
+### ***var*** `rel_md_path = rel_md_path.replace(rk, rv)`
+
+
+
+### ***var*** `front_matter = {'title': module_info.module_path.replace('.__init__', '').replace('_', '\\n'), 'index': 'true', 'icon': 'laptop-code', 'category': 'API'}`
+
+
+
+### ***var*** `front_matter = {'title': module_info.module_path.replace('_', '\\n'), 'order': '1', 'icon': 'laptop-code', 'category': 'API'}`
+
+
+
+### ***var*** `function_docstring = ast.get_docstring(node)`
+
+
+
+### ***var*** `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))`
+
+
+
+### ***var*** `class_docstring = ast.get_docstring(node)`
+
+
+
+### ***var*** `class_info = ClassInfo(name=node.name, docstring=class_docstring if class_docstring else '', methods=[], attributes=[], inherit=[ast.unparse(base) for base in node.bases])`
+
+
+
+### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in method.args]`
+
+
+
+### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] and arg[0] != 'self' else arg[0] for arg in method.args]`
+
+
+
+### ***var*** `first_arg = node.args.args[0]`
+
+
+
+### ***var*** `method_docstring = ast.get_docstring(class_node)`
+
+
+
+### ***var*** `def_type = DefType.METHOD`
+
+
+
+### ***var*** `def_type = DefType.STATIC_METHOD`
+
+
+
+### ***var*** `attr_type = NO_TYPE_HINT`
+
+
+
+### ***var*** `def_type = DefType.CLASS_METHOD`
+
+
+
+### ***var*** `attr_type = ast.unparse(node.value.annotation)`
+
+
+
+### ***var*** `def_type = DefType.PROPERTY`
+
+
+
diff --git a/docs/en/dev/api/plugin/README.md b/docs/en/dev/api/plugin/README.md
new file mode 100644
index 00000000..6c67f485
--- /dev/null
+++ b/docs/en/dev/api/plugin/README.md
@@ -0,0 +1,29 @@
+---
+title: liteyuki.plugin
+index: true
+icon: laptop-code
+category: API
+---
+
+### ***def*** `get_loaded_plugins() -> dict[str, Plugin]`
+
+获取已加载的插件
+
+Returns:
+
+ dict[str, Plugin]: 插件字典
+
+
+源代码
+
+```python
+def get_loaded_plugins() -> dict[str, Plugin]:
+ """
+ 获取已加载的插件
+ Returns:
+ dict[str, Plugin]: 插件字典
+ """
+ return _plugins
+```
+
+
diff --git a/docs/en/dev/api/plugin/load.md b/docs/en/dev/api/plugin/load.md
new file mode 100644
index 00000000..b999f5e1
--- /dev/null
+++ b/docs/en/dev/api/plugin/load.md
@@ -0,0 +1,199 @@
+---
+title: liteyuki.plugin.load
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `load_plugin(module_path: str | Path) -> Optional[Plugin]`
+
+加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
+
+
+
+参数:
+
+ module_path: 插件名称 `path.to.your.plugin`
+
+ 或插件路径 `pathlib.Path(path/to/your/plugin)`
+
+
+源代码
+
+```python
+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))
+ display_name = module.__name__.split('.')[-1]
+ if module.__dict__.get('__plugin_meta__'):
+ metadata: 'PluginMetadata' = module.__dict__['__plugin_meta__']
+ display_name = format_display_name(f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type)
+ logger.opt(colors=True).success(f'Succeeded to load liteyuki plugin "{display_name}"')
+ return _plugins[module.__name__]
+ except Exception as e:
+ logger.opt(colors=True).success(f'Failed to load liteyuki plugin "{module_path}"')
+ traceback.print_exc()
+ return None
+```
+
+
+### ***def*** `load_plugins() -> set[Plugin]`
+
+导入文件夹下多个插件
+
+
+
+参数:
+
+ plugin_dir: 文件夹路径
+
+ ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
+
+
+源代码
+
+```python
+def load_plugins(*plugin_dir: str, ignore_warning: bool=True) -> set[Plugin]:
+ """导入文件夹下多个插件
+
+ 参数:
+ plugin_dir: 文件夹路径
+ ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
+ """
+ plugins = set()
+ for dir_path in plugin_dir:
+ if not os.path.exists(dir_path):
+ if not ignore_warning:
+ logger.warning(f"Plugins dir '{dir_path}' does not exist.")
+ continue
+ if not os.listdir(dir_path):
+ if not ignore_warning:
+ logger.warning(f"Plugins dir '{dir_path}' is empty.")
+ continue
+ if not os.path.isdir(dir_path):
+ if not ignore_warning:
+ logger.warning(f"Plugins dir '{dir_path}' is not a directory.")
+ 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'):
+ 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
+```
+
+
+### ***def*** `format_display_name(display_name: str, plugin_type: PluginType) -> str`
+
+设置插件名称颜色,根据不同类型插件设置颜色
+
+Args:
+
+ display_name: 插件名称
+
+ plugin_type: 插件类型
+
+
+
+Returns:
+
+ str: 设置后的插件名称 name
+
+
+源代码
+
+```python
+def format_display_name(display_name: str, plugin_type: PluginType) -> str:
+ """
+ 设置插件名称颜色,根据不同类型插件设置颜色
+ Args:
+ display_name: 插件名称
+ plugin_type: 插件类型
+
+ Returns:
+ str: 设置后的插件名称 name
+ """
+ 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}>'
+```
+
+
+### ***var*** `module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path`
+
+
+
+### ***var*** `plugins = set()`
+
+
+
+### ***var*** `color = 'y'`
+
+
+
+### ***var*** `module = import_module(module_path)`
+
+
+
+### ***var*** `display_name = module.__name__.split('.')[-1]`
+
+
+
+### ***var*** `display_name = format_display_name(f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type)`
+
+
+
+### ***var*** `path = Path(os.path.join(dir_path, f))`
+
+
+
+### ***var*** `module_name = None`
+
+
+
+### ***var*** `color = 'm'`
+
+
+
+### ***var*** `color = 'g'`
+
+
+
+### ***var*** `color = 'e'`
+
+
+
+### ***var*** `color = 'c'`
+
+
+
+### ***var*** `module_name = f'{path_to_module_name(Path(dir_path))}.{f[:-3]}'`
+
+
+
+### ***var*** `module_name = path_to_module_name(path)`
+
+
+
diff --git a/docs/en/dev/api/plugin/manager.md b/docs/en/dev/api/plugin/manager.md
new file mode 100644
index 00000000..7d4d951c
--- /dev/null
+++ b/docs/en/dev/api/plugin/manager.md
@@ -0,0 +1,7 @@
+---
+title: liteyuki.plugin.manager
+order: 1
+icon: laptop-code
+category: API
+---
+
diff --git a/docs/en/dev/api/plugin/model.md b/docs/en/dev/api/plugin/model.md
new file mode 100644
index 00000000..6f986049
--- /dev/null
+++ b/docs/en/dev/api/plugin/model.md
@@ -0,0 +1,89 @@
+---
+title: liteyuki.plugin.model
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***class*** `PluginType(Enum)`
+
+插件类型枚举值
+
+### ***attr*** `APPLICATION: 'application'`
+
+### ***attr*** `SERVICE: 'service'`
+
+### ***attr*** `MODULE: 'module'`
+
+### ***attr*** `UNCLASSIFIED: 'unclassified'`
+
+### ***attr*** `TEST: 'test'`
+
+### ***class*** `PluginMetadata(BaseModel)`
+
+轻雪插件元数据,由插件编写者提供,name为必填项
+
+Attributes:
+
+----------
+
+
+
+name: str
+
+ 插件名称
+
+description: str
+
+ 插件描述
+
+usage: str
+
+ 插件使用方法
+
+type: str
+
+ 插件类型
+
+author: str
+
+ 插件作者
+
+homepage: str
+
+ 插件主页
+
+extra: dict[str, Any]
+
+ 额外信息
+
+### ***class*** `Plugin(BaseModel)`
+
+存储插件信息
+
+### ***attr*** `model_config: {'arbitrary_types_allowed': True}`
+
+### ***var*** `APPLICATION = 'application'`
+
+
+
+### ***var*** `SERVICE = 'service'`
+
+
+
+### ***var*** `MODULE = 'module'`
+
+
+
+### ***var*** `UNCLASSIFIED = 'unclassified'`
+
+
+
+### ***var*** `TEST = 'test'`
+
+
+
+### ***var*** `model_config = {'arbitrary_types_allowed': True}`
+
+
+
diff --git a/docs/en/dev/api/utils.md b/docs/en/dev/api/utils.md
new file mode 100644
index 00000000..84532f46
--- /dev/null
+++ b/docs/en/dev/api/utils.md
@@ -0,0 +1,180 @@
+---
+title: liteyuki.utils
+order: 1
+icon: laptop-code
+category: API
+---
+
+### ***def*** `is_coroutine_callable(call: Callable[..., Any]) -> bool`
+
+判断是否为协程可调用对象
+
+Args:
+
+ call: 可调用对象
+
+Returns:
+
+ bool: 是否为协程可调用对象
+
+
+源代码
+
+```python
+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*** `run_coroutine() -> None`
+
+运行协程
+
+Args:
+
+ coro:
+
+
+
+Returns:
+
+
+源代码
+
+```python
+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'Exception occurred: {e}')
+```
+
+
+### ***def*** `path_to_module_name(path: Path) -> str`
+
+转换路径为模块名
+
+Args:
+
+ path: 路径a/b/c/d -> a.b.c.d
+
+Returns:
+
+ str: 模块名
+
+
+源代码
+
+```python
+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,))
+```
+
+
+### ***def*** `async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]`
+
+异步包装器
+
+Args:
+
+ func: Sync Callable
+
+Returns:
+
+ Coroutine: Asynchronous Callable
+
+
+源代码
+
+```python
+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
+```
+
+
+### ***async def*** `wrapper() -> None`
+
+
+
+
+源代码
+
+```python
+async def wrapper(*args, **kwargs):
+ return func(*args, **kwargs)
+```
+
+
+### ***var*** `IS_MAIN_PROCESS = multiprocessing.current_process().name == 'MainProcess'`
+
+
+
+### ***var*** `func_ = getattr(call, '__call__', None)`
+
+
+
+### ***var*** `rel_path = path.resolve().relative_to(Path.cwd().resolve())`
+
+
+
+### ***var*** `loop = asyncio.get_event_loop()`
+
+
+
+### ***var*** `loop = asyncio.new_event_loop()`
+
+
+
diff --git a/liteyuki/mkdoc.py b/liteyuki/mkdoc.py
index b62ba5b6..fb179473 100644
--- a/liteyuki/mkdoc.py
+++ b/liteyuki/mkdoc.py
@@ -33,6 +33,7 @@ class FunctionInfo(BaseModel):
args: list[tuple[str, str]]
return_type: str
docstring: str
+ source_code: str = ""
type: DefType
"""若为类中def,则有"""
@@ -142,7 +143,8 @@ def get_module_info_normal(file_path: str, ignore_private: bool = True) -> Modul
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)
+ is_async=isinstance(node, ast.AsyncFunctionDef),
+ source_code=ast.unparse(node)
)
module_info.functions.append(func_info)
@@ -176,7 +178,8 @@ def get_module_info_normal(file_path: str, ignore_private: bool = True) -> Modul
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)
+ is_async=isinstance(class_node, ast.AsyncFunctionDef),
+ source_code=ast.unparse(class_node)
))
# attributes
elif isinstance(class_node, ast.Assign):
@@ -223,8 +226,6 @@ def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
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]
@@ -233,6 +234,9 @@ def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
func.docstring = func.docstring.replace("\n", "\n\n")
content += f"{func.docstring}\n\n"
+ # 函数源代码可展开区域
+ content += f"\n源代码
\n\n```python\n{func.source_code}\n```\n \n\n"
+
# 类
for cls in module_info.classes:
if cls.inherit:
@@ -256,6 +260,8 @@ def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
method.docstring = method.docstring.replace("\n", "\n\n")
content += f" {method.docstring}\n\n"
+ # 函数源代码可展开区域
+ content += f"\n源代码
\n\n```python\n{method.source_code}\n```\n \n\n"
for attr in cls.attributes:
content += f"### ***attr*** `{attr.name}: {attr.type}`\n\n"