Merge remote-tracking branch 'origin/main'

# Conflicts:
#	.github/workflows/deploy-docs.yml
#	docs/dev/api/README.md
#	docs/dev/api/bot/README.md
#	docs/dev/api/bot/lifespan.md
#	docs/dev/api/comm/README.md
#	docs/dev/api/comm/channel.md
#	docs/dev/api/comm/event.md
#	docs/dev/api/comm/storage.md
#	docs/dev/api/config.md
#	docs/dev/api/core/README.md
#	docs/dev/api/core/manager.md
#	docs/dev/api/dev/README.md
#	docs/dev/api/dev/observer.md
#	docs/dev/api/dev/plugin.md
#	docs/dev/api/exception.md
#	docs/dev/api/log.md
#	docs/dev/api/message/README.md
#	docs/dev/api/message/event.md
#	docs/dev/api/message/matcher.md
#	docs/dev/api/message/on.md
#	docs/dev/api/message/rule.md
#	docs/dev/api/message/session.md
#	docs/dev/api/mkdoc.md
#	docs/dev/api/plugin/README.md
#	docs/dev/api/plugin/load.md
#	docs/dev/api/plugin/manager.md
#	docs/dev/api/plugin/model.md
#	docs/dev/api/utils.md
#	docs/en/dev/api/README.md
#	docs/en/dev/api/bot/README.md
#	docs/en/dev/api/bot/lifespan.md
#	docs/en/dev/api/comm/README.md
#	docs/en/dev/api/comm/channel.md
#	docs/en/dev/api/comm/event.md
#	docs/en/dev/api/comm/storage.md
#	docs/en/dev/api/config.md
#	docs/en/dev/api/core/README.md
#	docs/en/dev/api/core/manager.md
#	docs/en/dev/api/dev/README.md
#	docs/en/dev/api/dev/observer.md
#	docs/en/dev/api/dev/plugin.md
#	docs/en/dev/api/exception.md
#	docs/en/dev/api/log.md
#	docs/en/dev/api/message/README.md
#	docs/en/dev/api/message/event.md
#	docs/en/dev/api/message/matcher.md
#	docs/en/dev/api/message/on.md
#	docs/en/dev/api/message/rule.md
#	docs/en/dev/api/message/session.md
#	docs/en/dev/api/mkdoc.md
#	docs/en/dev/api/plugin/README.md
#	docs/en/dev/api/plugin/load.md
#	docs/en/dev/api/plugin/manager.md
#	docs/en/dev/api/plugin/model.md
#	docs/en/dev/api/utils.md
#	litedoc/__main__.py
#	litedoc/docstring/docstring.py
#	litedoc/output.py
#	litedoc/style/markdown.py
#	litedoc/syntax/astparser.py
This commit is contained in:
snowy 2024-08-29 14:20:19 +08:00
commit 4910de74fd
27 changed files with 303 additions and 920 deletions

View File

@ -1,8 +0,0 @@
---
title: 开发及贡献
index: false
icon: laptop-code
category: 开发
---
<Catalog />

View File

@ -1,99 +0,0 @@
---
title: 进程通信
icon: exchange-alt
order: 4
category: 开发
---
## **通道通信**
### 简介
轻雪运行在主进程 MainProcess 里,其他插件框架进程是伴随的子进程,因此无法通过内存共享和直接对象传递的方式进行通信,轻雪提供了一个通道`Channel`用于跨进程通信,你可以通过`Channel`发送消息给其他进程,也可以监听其他进程的消息。
例如子进程接收到用户信息需要重启机器人,这时可以通过通道对主进程发送消息,主进程接收到消息后重启对应子进程。
### 示例
通道是全双工的,有两种接收模式,但一个通道只能使用一种,即被动模式和主动模式,被动模式由`chan.on_receive()`装饰回调函数实现,主动模式需调用`chan.receive()`实现
- 创建子进程的同时会初始化一个被动通道和一个主动通道,且通道标识为`{process_name}-active`和`{process_name}-passive`
- 主进程中通过`get_channel`函数获取通道对象
- 子进程中导入单例`active_channel`及`passive_channel`即可
> 在轻雪插件中(主进程中)
```python
import asyncio
from liteyuki.comm import get_channel, Channel
from liteyuki import get_bot
# get_channel函数获取通道对象参数为调用set_channel时的通道标识
channel_passive = get_channel("nonebot-passive") # 获取被动通道
channel_active = get_channel("nonebot-active") # 获取主动通道
liteyuki_bot = get_bot()
# 注册一个函数在轻雪启动后运行
@liteyuki_bot.on_after_start
async def send_data():
while True:
channel_passive.send("I am liteyuki main process passive")
channel_active.send("I am liteyuki main process active")
await asyncio.sleep(3) # 每3秒发送一次消息
```
> 在子进程中例如NoneBot插件中
```python
from nonebot import get_driver
from liteyuki.comm import active_channel, passive_channel # 子进程中获取通道直接导入进程全局单例即可
from liteyuki.log import logger
driver = get_driver()
# 被动模式,通过装饰器注册一个函数在接收到消息时运行,每次接收到字符串数据时都会运行
@passive_channel.on_receive(filter_func=lambda data: isinstance(data, str))
async def on_passive_receive(data):
logger.info(f"Passive receive: {data}")
# 注册一个函数在NoneBot启动后运行
@driver.on_startup
def on_startup():
while True:
data = active_channel.receive()
logger.info(f"Active receive: {data}")
```
> 启动后控制台输出
```log
0000-00-00 00:00:00 [ℹ️信息] Passive receive: I am liteyuki main process passive
0000-00-00 00:00:00 [ℹ️信息] Active receive: I am liteyuki main process active
0000-00-00 00:00:03 [ℹ️信息] Passive receive: I am liteyuki main process passive
0000-00-00 00:00:03 [ℹ️信息] Active receive: I am liteyuki main process active
...
```
## **共享内存通信**
### 简介
- 相比于普通进程通信,内存共享使得代码编写更加简洁,轻雪框架提供了一个内存共享通信的接口,你可以通过`storage`模块实现内存共享通信,该模块封装通道实现
- 内存共享是线程安全的,你可以在多个线程中读写共享内存,线程锁会自动保护共享内存的读写操作
### 示例
> 在任意进程中均可使用
```python
from liteyuki.comm.storage import shared_memory
shared_memory.set("key", "value") # 设置共享内存
value = shared_memory.get("key") # 获取共享内存
```
源代码:[liteyuki/comm/storage.py](https://github.com/LiteyukiStudio/LiteyukiBot/blob/main/liteyuki/comm/storage.py)

View File

@ -1,74 +0,0 @@
---
title: 轻雪函数
icon: code
order: 2
category: 开发
---
## **轻雪函数**
轻雪函数 Liteyuki Function 是轻雪的一个功能它允许你在轻雪中运行一些自定义的由数据驱动的命令类似于Minecraft的mcfunction属于资源包的一部分但需单独起篇幅.
### **函数文件**
函数文件放在资源包的`functions`目录下,文件名以`.mcfunction` `.lyfunction` `.lyf`结尾,例如`test.mcfunction`,文件内容为一系列的命令,每行一个命令,支持单行注释`#`(编辑时的语法高亮可采取`shell`格式),例如:
```shell
# 在发信器输出"hello world"
cmd echo hello world
# 如果你想同时输出多行内容可以尝试换行符(Python格式)
cmd echo hello world\nLiteyuki bot
```
也支持句末注释,例如:
```shell
cmd echo hello world # 输出"hello world"
```
### **命令文档**
```shell
var <var1=value1> [var2=value2] ... # 定义变量
cmd <command> # 在设备上执行命令
api <api_name> [var=value...] # 调用Bot API
function <func_name> # 调用函数,可递归
sleep <time> # 异步等待单位s
nohup <command> # 使用新的task执行命令即不等待
end # 结束函数关键字包括子task
await # 等待所有异步任务结束若函数中启动了其他task需要在最后调用否则task对象会被销毁
```
#### **示例**
```shell
# 疯狂戳好友
# 使用 /function poke user_id=123456 执行
# 每隔0.2s戳两次,无限戳,会触发最大递归深度限制
# 若要戳20s后停止则需要删除await添加sleep 20和end
api friend_poke user_id=user_id
api friend_poke user_id=user_id
sleep 0.2
nohup function poke
await
```
### **API**
理论上所有基于onebotv11的api都可调用不同Adapter api也有差别.
[Onebot v11 API文档](https://283375.github.io/onebot_v11_vitepress/api/index.html)
### **结束关键字**
由于LiteyukiBot基于异步运行, 所以在编写lyfunction时也要注意异步的调用避免出现"单线程走到底"的情况是效率提升的关键.
`await` 异步任务结束关键字用于结束当前已完成function的执行
> [!warning]
> 但若出现非单function的情况有一个task任务没有完成而await被执行了那么当前所有函数包的task都会被截停销毁
> [!tip]
> 编写轻雪函数推荐你使用VS Code插件[Liteyuki Function](https://github.com/LiteyukiStudio/lyfunctionTextmate)实现语法高亮

View File

@ -1,82 +0,0 @@
---
title: 轻雪插件开发
icon: laptop-code
order: 3
category: 开发
---
## 简介
轻雪插件是轻雪内置的一部分功能,运行在主进程中,可以很高程度地扩展轻雪的功能
## 开始
### 创建插件
一个`.py`文件或一个包含`__init__.py`的文件夹即可被识别为插件
首先创建一个文件夹,例如`watchdog_plugin`,并在其中创建一个`__init__.py`文件,即可创建一个插件
`__init__.py`
```python
from liteyuki.plugin import PluginMetadata, PluginType
from .watch_dog import * # 导入逻辑部分
# 定义插件元数据
__plugin_meta__ = PluginMetadata(
name="NoneDog", # 插件名称
version="1.0.0", # 插件版本
description="A simple plugin for nonebot developer", # 插件描述
type=PluginType.SERVICE # 插件类型
)
# 你的插件代码
...
```
### 编写逻辑部分
轻雪主进程不涉及聊天部分,因此插件主要是一些后台任务或者与聊天机器人的通信
以下我们会编写一个简单的插件用于开发NoneBot时进行文件系统变更重载
`watch_dog.py`
```python
import os
from liteyuki.dev import observer # 导入文件系统观察器
from liteyuki import get_bot, logger # 导入轻雪Bot和日志
from watchdog.events import FileSystemEvent # 导入文件系统事件
liteyuki = get_bot() # 获取唯一的轻雪Bot实例
exclude_extensions = (".pyc", ".pyo") # 排除的文件扩展名
# 用observer的on_file_system_event装饰器监听文件系统事件
@observer.on_file_system_event(
directories=("src/nonebot_plugins",),
event_filter=lambda event: not event.src_path.endswith(exclude_extensions) and ("__pycache__" not in event.src_path) and os.path.isfile(event.src_path)
)
def restart_nonebot_process(event: FileSystemEvent):
logger.debug(f"File {event.src_path} changed, reloading nonebot...")
liteyuki.restart_process("nonebot") # 调用重启进程方法
```
### 加载插件
#### 方法1
- 在配置文件中的`liteyuki.plugins`中添加你的插件路径,例如`watchdog_plugin`,重启轻雪即可加载插件。
#### 方法2
- 使用开发工具快速运行插件,无需手动创建实例
- 创建入口文件,例如`main.py`,并在其中写入以下代码
```python
from liteyuki.dev.plugin import run_plugins
run_plugins("watchdog_plugin")
```
然后运行`python main.py`即可启动插件
启用插件后我们在src/nonebot_plugins下创建一个文件例如`test.py`并在其中写入一些代码保存后轻雪会自动重载NoneBot进程

View File

@ -1,53 +0,0 @@
---
title: 资源包开发
icon: box
order: 1
category: 开发
---
## 简介
资源包,亦可根据用途称为主题包、字体包、语言包等,它允许你一定程度上自定义轻雪的外观,并且不用修改源代码
- [资源/主题商店](/store/)提供了一些资源包供你选择,你也可以自己制作资源包
- 资源包的制作很简单,如果你接触过`Minecraft`的资源包,那么你能够很快就上手,仅需按照原有路径进行文件替换即可,讲起打包成一个新的资源包。
- 部分内容制作需要一点点前端基础,例如`html``css`
- 轻雪原版资源包请查看`LiteyukiBot/liteyuki/resources`,可以在此基础上进行修改
- 欢迎各位投稿资源包到轻雪资源商店
请注意主题包中的html渲染使用Js来规定数据的渲染位置请确保您所编写的html代码能被Bot解析否则会导致渲染失败或渲染结果不理想/异常/错位等无法预料的事情发生。推荐在编写html时同时更改对应Js代码以避免出现无法预料的问题。
---
## 加载资源包
- 资源包通常是以`.zip`格式压缩的,只需要将其解压到根目录`resources`目录下即可,注意不要嵌套文件夹,正常的路径应该是这样的
```shell
main.py
resources
└─resource_pack_1
├─metadata.yml
├─templates
└───...
└─resource_pack_2
├─metadata.yml
└─...
```
- 你自己制作的资源包也应该遵循这个规则,并且应该在`metadata.yml`中填写一些信息
- 若没有`metadata.yml`文件,则该文件夹不会被识别为资源包
```yaml
name: "资源包名称"
version: "1.0.0"
description: "资源包描述"
# 你可以自定义一些信息,但请保证以上三个字段
...
```
- 资源包加载遵循一个优先级即后加载的资源包会覆盖前面的资源包例如你在A包中定义了一个`index.html`文件B包也定义了一个`index.html`文件那么加载B包后A包中的`index.html`文件会被覆盖
- 对于不同资源包的不同文件是可以相对引用的例如你在A中定义了`templates/index.html`在B中定义了`templates/style.css`可以在A的`index.html`中用`./style.css`相对路径引用B中的css
> [!tip]
> 资源包的结构会随着轻雪的更新而有变动,第三方资源包开发者需要注意版本兼容性,同时用户也应该自行选择可用的资源包

View File

@ -1,8 +0,0 @@
---
title: Contribute
index: false
icon: laptop-code
category: 开发
---
<Catalog />

View File

@ -1,99 +0,0 @@
---
title: Communication
icon: exchange-alt
order: 4
category: development
---
## **通道通信**
### 简介
轻雪运行在主进程 MainProcess 里,其他插件框架进程是伴随的子进程,因此无法通过内存共享和直接对象传递的方式进行通信,轻雪提供了一个通道`Channel`用于跨进程通信,你可以通过`Channel`发送消息给其他进程,也可以监听其他进程的消息。
例如子进程接收到用户信息需要重启机器人,这时可以通过通道对主进程发送消息,主进程接收到消息后重启对应子进程。
### 示例
通道是全双工的,有两种接收模式,但一个通道只能使用一种,即被动模式和主动模式,被动模式由`chan.on_receive()`装饰回调函数实现,主动模式需调用`chan.receive()`实现
- 创建子进程的同时会初始化一个被动通道和一个主动通道,且通道标识为`{process_name}-active`和`{process_name}-passive`
- 主进程中通过`get_channel`函数获取通道对象
- 子进程中导入单例`active_channel`及`passive_channel`即可
> 在轻雪插件中(主进程中)
```python
import asyncio
from liteyuki.comm import get_channel, Channel
from liteyuki import get_bot
# get_channel函数获取通道对象参数为调用set_channel时的通道标识
channel_passive = get_channel("nonebot-passive") # 获取被动通道
channel_active = get_channel("nonebot-active") # 获取主动通道
liteyuki_bot = get_bot()
# 注册一个函数在轻雪启动后运行
@liteyuki_bot.on_after_start
async def send_data():
while True:
channel_passive.send("I am liteyuki main process passive")
channel_active.send("I am liteyuki main process active")
await asyncio.sleep(3) # 每3秒发送一次消息
```
> 在子进程中例如NoneBot插件中
```python
from nonebot import get_driver
from liteyuki.comm import active_channel, passive_channel # 子进程中获取通道直接导入进程全局单例即可
from liteyuki.log import logger
driver = get_driver()
# 被动模式,通过装饰器注册一个函数在接收到消息时运行,每次接收到字符串数据时都会运行
@passive_channel.on_receive(filter_func=lambda data: isinstance(data, str))
async def on_passive_receive(data):
logger.info(f"Passive receive: {data}")
# 注册一个函数在NoneBot启动后运行
@driver.on_startup
def on_startup():
while True:
data = active_channel.receive()
logger.info(f"Active receive: {data}")
```
> 启动后控制台输出
```log
0000-00-00 00:00:00 [ℹ️信息] Passive receive: I am liteyuki main process passive
0000-00-00 00:00:00 [ℹ️信息] Active receive: I am liteyuki main process active
0000-00-00 00:00:03 [ℹ️信息] Passive receive: I am liteyuki main process passive
0000-00-00 00:00:03 [ℹ️信息] Active receive: I am liteyuki main process active
...
```
## **共享内存通信**
### 简介
- 相比于普通进程通信,内存共享使得代码编写更加简洁,轻雪框架提供了一个内存共享通信的接口,你可以通过`storage`模块实现内存共享通信,该模块封装通道实现
- 内存共享是线程安全的,你可以在多个线程中读写共享内存,线程锁会自动保护共享内存的读写操作
### 示例
> 在任意进程中均可使用
```python
from liteyuki.comm.storage import shared_memory
shared_memory.set("key", "value") # 设置共享内存
value = shared_memory.get("key") # 获取共享内存
```
- 源代码:[liteyuki/comm/storage.py](https://github.com/LiteyukiStudio/LiteyukiBot/blob/main/liteyuki/comm/storage.py)

View File

@ -1,74 +0,0 @@
---
title: Liteyuki Function
icon: code
order: 2
category: development
---
## **轻雪函数**
轻雪函数 Liteyuki Function 是轻雪的一个功能它允许你在轻雪中运行一些自定义的由数据驱动的命令类似于Minecraft的mcfunction属于资源包的一部分但需单独起篇幅.
### **函数文件**
函数文件放在资源包的`functions`目录下,文件名以`.mcfunction` `.lyfunction` `.lyf`结尾,例如`test.mcfunction`,文件内容为一系列的命令,每行一个命令,支持单行注释`#`(编辑时的语法高亮可采取`shell`格式),例如:
```shell
# 在发信器输出"hello world"
cmd echo hello world
# 如果你想同时输出多行内容可以尝试换行符(Python格式)
cmd echo hello world\nLiteyuki bot
```
也支持句末注释,例如:
```shell
cmd echo hello world # 输出"hello world"
```
### **命令文档**
```shell
var <var1=value1> [var2=value2] ... # 定义变量
cmd <command> # 在设备上执行命令
api <api_name> [var=value...] # 调用Bot API
function <func_name> # 调用函数,可递归
sleep <time> # 异步等待单位s
nohup <command> # 使用新的task执行命令即不等待
end # 结束函数关键字包括子task
await # 等待所有异步任务结束若函数中启动了其他task需要在最后调用否则task对象会被销毁
```
#### **示例**
```shell
# 疯狂戳好友
# 使用 /function poke user_id=123456 执行
# 每隔0.2s戳两次,无限戳,会触发最大递归深度限制
# 若要戳20s后停止则需要删除await添加sleep 20和end
api friend_poke user_id=user_id
api friend_poke user_id=user_id
sleep 0.2
nohup function poke
await
```
### **API**
理论上所有基于onebotv11的api都可调用不同Adapter api也有差别.
[Onebot v11 API文档](https://283375.github.io/onebot_v11_vitepress/api/index.html)
### **结束关键字**
由于LiteyukiBot基于异步运行, 所以在编写lyfunction时也要注意异步的调用避免出现"单线程走到底"的情况是效率提升的关键.
`await` 异步任务结束关键字用于结束当前已完成function的执行
> [!warning]
> 但若出现非单function的情况有一个task任务没有完成而await被执行了那么当前所有函数包的task都会被截停销毁
> [!tip]
> 编写轻雪函数推荐你使用VS Code插件[Liteyuki Function](https://github.com/LiteyukiStudio/lyfunctionTextmate)实现语法高亮

View File

@ -1,63 +0,0 @@
---
title: Liteyuki Plugin
icon: laptop-code
order: 3
category: development
---
## 简介
轻雪插件是轻雪内置的一部分功能,运行在主进程中,可以很高程度地扩展轻雪的功能
## 开始
### 创建插件
在标准项目中位于liteyuki/plugins和src/liteyuki_plugins下的Python modules均会被当作插件加载你可自行添加配置文件以指定插件的加载路径
一个`.py`文件或一个包含`__init__.py`的文件夹即可被识别为插件
创建一个文件夹,例如`watchdog_plugin`,并在其中创建一个`__init__.py`文件,即可创建一个插件
```python
from liteyuki.plugin import PluginMetadata
# 定义插件元数据,推荐填写
__plugin_meta__ = PluginMetadata(
name="NoneDog", # 插件名称
version="1.0.0", # 插件版本
description="A simple plugin for nonebot developer" # 插件描述
)
# 你的插件代码
...
```
### 编写逻辑部分
轻雪主进程不涉及聊天部分,因此插件主要是一些后台任务或者与聊天机器人的通信
以下我们会编写一个简单的插件用于开发NoneBot时进行文件系统变更重载
```python
import os
from liteyuki.dev import observer # 导入文件系统观察器
from liteyuki import get_bot, logger # 导入轻雪Bot和日志
from watchdog.events import FileSystemEvent # 导入文件系统事件
liteyuki = get_bot() # 获取唯一的轻雪Bot实例
exclude_extensions = (".pyc", ".pyo") # 排除的文件扩展名
# 用observer的on_file_system_event装饰器监听文件系统事件
@observer.on_file_system_event(
directories=("src/nonebot_plugins",),
event_filter=lambda event: not event.src_path.endswith(exclude_extensions) and ("__pycache__" not in event.src_path) and os.path.isfile(event.src_path)
)
def restart_nonebot_process(event: FileSystemEvent):
logger.debug(f"File {event.src_path} changed, reloading nonebot...")
liteyuki.restart_process("nonebot") # 调用重启进程方法
```
### 加载插件
在配置文件中的`liteyuki.plugins`中添加你的插件路径,例如`watchdog_plugin`重启轻雪即可加载插件。然后我们在src/nonebot_plugins下创建一个文件例如`test.py`并在其中写入一些代码保存后轻雪会自动重载NoneBot进程

View File

@ -1,53 +0,0 @@
---
title: Resource Pack
icon: box
order: 1
category: development
---
## 简介
资源包,亦可根据用途称为主题包、字体包、语言包等,它允许你一定程度上自定义轻雪的外观,并且不用修改源代码
- [资源/主题商店](/store/)提供了一些资源包供你选择,你也可以自己制作资源包
- 资源包的制作很简单,如果你接触过`Minecraft`的资源包,那么你能够很快就上手,仅需按照原有路径进行文件替换即可,讲起打包成一个新的资源包。
- 部分内容制作需要一点点前端基础,例如`html``css`
- 轻雪原版资源包请查看`LiteyukiBot/liteyuki/resources`,可以在此基础上进行修改
- 欢迎各位投稿资源包到轻雪资源商店
请注意主题包中的html渲染使用Js来规定数据的渲染位置请确保您所编写的html代码能被Bot解析否则会导致渲染失败或渲染结果不理想/异常/错位等无法预料的事情发生。推荐在编写html时同时更改对应Js代码以避免出现无法预料的问题。
---
## 加载资源包
- 资源包通常是以`.zip`格式压缩的,只需要将其解压到根目录`resources`目录下即可,注意不要嵌套文件夹,正常的路径应该是这样的
```shell
main.py
resources
└─resource_pack_1
├─metadata.yml
├─templates
└───...
└─resource_pack_2
├─metadata.yml
└─...
```
- 你自己制作的资源包也应该遵循这个规则,并且应该在`metadata.yml`中填写一些信息
- 若没有`metadata.yml`文件,则该文件夹不会被识别为资源包
```yaml
name: "资源包名称"
version: "1.0.0"
description: "资源包描述"
# 你可以自定义一些信息,但请保证以上三个字段
...
```
- 资源包加载遵循一个优先级即后加载的资源包会覆盖前面的资源包例如你在A包中定义了一个`index.html`文件B包也定义了一个`index.html`文件那么加载B包后A包中的`index.html`文件会被覆盖
- 对于不同资源包的不同文件是可以相对引用的例如你在A中定义了`templates/index.html`在B中定义了`templates/style.css`可以在A的`index.html`中用`./style.css`相对路径引用B中的css
> [!tip]
> 资源包的结构会随着轻雪的更新而有变动,第三方资源包开发者需要注意版本兼容性,同时用户也应该自行选择可用的资源包

47
litedoc/translator.py Normal file
View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/29 下午12:02
@Author : snowykami
@Email : snowykami@outlook.com
@File : translator.py
@Software: PyCharm
"""
from typing import Optional
from translate import Translator # type: ignore
# 特殊映射语言
i18n_lang2googletrans_lang = {
"zh-Hans": "zh-cn",
"zh-Hant": "zh-tw",
"en" : "en",
}
def get_google_lang(lang: str) -> str:
"""
Get google translate language
Args:
lang: language
Returns:
google translate language
"""
return i18n_lang2googletrans_lang.get(lang, lang)
def translate(text: str, lang: str, source_lang: str) -> str:
"""
Translate text to target language
Args:
source_lang:
text: text
lang: target language
Returns:
translated text
"""
if lang == source_lang:
return text
google_lang = get_google_lang(lang)
return Translator(to_lang=google_lang, from_lang=source_lang).translate(text)

View File

@ -10,7 +10,6 @@ from typing import Any, Optional
from liteyuki.bot.lifespan import (LIFESPAN_FUNC, Lifespan) from liteyuki.bot.lifespan import (LIFESPAN_FUNC, Lifespan)
from liteyuki.comm.channel import get_channel from liteyuki.comm.channel import get_channel
from liteyuki.comm.storage import shared_memory
from liteyuki.core.manager import ProcessManager from liteyuki.core.manager import ProcessManager
from liteyuki.log import init_log, logger from liteyuki.log import init_log, logger
from liteyuki.plugin import load_plugin from liteyuki.plugin import load_plugin
@ -63,16 +62,25 @@ class LiteyukiBot:
signal.signal(signal.SIGTERM, self._handle_exit) signal.signal(signal.SIGTERM, self._handle_exit)
atexit.register(self.process_manager.terminate_all) # 注册退出时的函数 atexit.register(self.process_manager.terminate_all) # 注册退出时的函数
def run(self): async def _run(self):
""" """
启动逻辑 启动逻辑
""" """
self.lifespan.before_start() # 启动前钩子 await self.lifespan.before_start() # 启动前钩子
self.process_manager.start_all() await self.process_manager.start_all()
self.lifespan.after_start() # 启动后钩子 await self.lifespan.after_start() # 启动后钩子
self.keep_alive() await self.keep_alive()
def keep_alive(self): def run(self):
"""
外部启动接口
"""
try:
asyncio.run(self._run())
except KeyboardInterrupt:
logger.info("Liteyuki is stopping...")
async def keep_alive(self):
""" """
保持轻雪运行 保持轻雪运行
Returns: Returns:
@ -131,9 +139,6 @@ class LiteyukiBot:
name: 进程名称, 默认为None, 所有进程 name: 进程名称, 默认为None, 所有进程
Returns: Returns:
""" """
self.lifespan.before_process_shutdown() # 重启前钩子
self.lifespan.before_process_shutdown() # 停止前钩子
if name is not None: if name is not None:
chan_active = get_channel(f"{name}-active") chan_active = get_channel(f"{name}-active")
chan_active.send(1) chan_active.send(1)
@ -230,17 +235,6 @@ class LiteyukiBot:
""" """
return self.lifespan.on_after_restart(func) return self.lifespan.on_after_restart(func)
def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
"""
注册nonebot初始化后的函数
Args:
func:
Returns:
"""
return self.lifespan.on_after_nonebot_init(func)
_BOT_INSTANCE: LiteyukiBot _BOT_INSTANCE: LiteyukiBot

View File

@ -39,29 +39,17 @@ class Lifespan:
self._before_process_restart_funcs: list[LIFESPAN_FUNC] = [] self._before_process_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_restart_funcs: list[LIFESPAN_FUNC] = [] self._after_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
@staticmethod @staticmethod
def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None: async def run_funcs(funcs: list[ASYNC_LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
""" """
运行函数 并发运行异步函数
Args: Args:
funcs: funcs:
Returns: Returns:
""" """
try: loop = asyncio.get_running_loop()
loop = asyncio.get_event_loop() tasks = [func(*args, **kwargs) if is_coroutine_callable(func) else async_wrapper(func)(*args, **kwargs) for func in funcs]
except RuntimeError: await asyncio.gather(*tasks)
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: def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
""" """
@ -131,63 +119,51 @@ class Lifespan:
self._after_restart_funcs.append(func) self._after_restart_funcs.append(func)
return func return func
def on_after_nonebot_init(self, func): async def before_start(self) -> None:
"""
注册 NoneBot 初始化后的函数
Args:
func:
Returns:
"""
self._after_nonebot_init_funcs.append(func)
return func
def before_start(self) -> None:
""" """
启动前 启动前
Returns: Returns:
""" """
logger.debug("Running before_start functions") logger.debug("Running before_start functions")
self.run_funcs(self._before_start_funcs) await self.run_funcs(self._before_start_funcs)
def after_start(self) -> None: async def after_start(self) -> None:
""" """
启动后 启动后
Returns: Returns:
""" """
logger.debug("Running after_start functions") logger.debug("Running after_start functions")
self.run_funcs(self._after_start_funcs) await self.run_funcs(self._after_start_funcs)
def before_process_shutdown(self) -> None: async def before_process_shutdown(self) -> None:
""" """
停止前 停止前
Returns: Returns:
""" """
logger.debug("Running before_shutdown functions") logger.debug("Running before_shutdown functions")
self.run_funcs(self._before_process_shutdown_funcs) await self.run_funcs(self._before_process_shutdown_funcs)
def after_shutdown(self) -> None: async def after_shutdown(self) -> None:
""" """
停止后 停止后
Returns: Returns:
""" """
logger.debug("Running after_shutdown functions") logger.debug("Running after_shutdown functions")
self.run_funcs(self._after_shutdown_funcs) await self.run_funcs(self._after_shutdown_funcs)
def before_process_restart(self) -> None: async def before_process_restart(self) -> None:
""" """
重启前 重启前
Returns: Returns:
""" """
logger.debug("Running before_restart functions") logger.debug("Running before_restart functions")
self.run_funcs(self._before_process_restart_funcs) await self.run_funcs(self._before_process_restart_funcs)
def after_restart(self) -> None: async def after_restart(self) -> None:
""" """
重启后 重启后
Returns: Returns:
""" """
logger.debug("Running after_restart functions") logger.debug("Running after_restart functions")
self.run_funcs(self._after_restart_funcs) await self.run_funcs(self._after_restart_funcs)

View File

@ -10,12 +10,12 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
本模块定义了一个通用的通道类用于进程间通信 本模块定义了一个通用的通道类用于进程间通信
""" """
import threading import asyncio
from multiprocessing import Pipe from multiprocessing import Pipe
from typing import Any, Callable, Coroutine, Generic, Optional, TypeAlias, TypeVar, get_args from typing import Any, Callable, Coroutine, Generic, Optional, TypeAlias, TypeVar, get_args
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable, run_coroutine
from liteyuki.log import logger from liteyuki.log import logger
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable
T = TypeVar("T") T = TypeVar("T")
@ -38,21 +38,22 @@ class Channel(Generic[T]):
有两种接收工作方式但是只能选择一种主动接收和被动接收主动接收使用 `receive` 方法被动接收使用 `on_receive` 装饰器 有两种接收工作方式但是只能选择一种主动接收和被动接收主动接收使用 `receive` 方法被动接收使用 `on_receive` 装饰器
""" """
def __init__(self, _id: str = "", type_check: Optional[bool] = None): def __init__(self, name: str, type_check: Optional[bool] = None):
""" """
初始化通道 初始化通道
Args: Args:
_id: 通道ID name: 通道ID
type_check: 是否开启类型检查, 若为空则传入泛型默认开启否则默认关闭 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.conn_send, self.conn_recv = Pipe()
self.is_sub_receive_loop_running = False self._conn_send_inner, self._conn_recv_inner = Pipe() # 内部通道,用于子进程通信
self._closed = False
self._on_main_receive_func_ids: list[int] = []
self._on_sub_receive_func_ids: list[int] = []
self.name: str = name
self.is_receive_loop_running = False
if type_check is None: if type_check is None:
# 若传入泛型则默认开启类型检查 # 若传入泛型则默认开启类型检查
@ -62,6 +63,16 @@ class Channel(Generic[T]):
if self._get_generic_type() is None: if self._get_generic_type() is None:
raise TypeError("Type hint is required for enforcing type check.") raise TypeError("Type hint is required for enforcing type check.")
self.type_check = type_check self.type_check = type_check
if name in _channel:
raise ValueError(f"Channel {name} already exists")
if IS_MAIN_PROCESS:
if name in _channel:
raise ValueError(f"Channel {name} already exists")
_channel[name] = self
logger.debug(f"Channel {name} initialized in main process")
else:
logger.debug(f"Channel {name} initialized in sub process, should manually set in main process")
def _get_generic_type(self) -> Optional[type]: def _get_generic_type(self) -> Optional[type]:
""" """
@ -105,7 +116,7 @@ class Channel(Generic[T]):
def send(self, data: T): def send(self, data: T):
""" """
发送数据 发送数据发送函数为同步函数没有异步的必要
Args: Args:
data: 数据 data: 数据
""" """
@ -120,7 +131,7 @@ class Channel(Generic[T]):
def receive(self) -> T: def receive(self) -> T:
""" """
接收数据 同步接收数据会阻塞线程
Args: Args:
""" """
if self._closed: if self._closed:
@ -130,13 +141,15 @@ class Channel(Generic[T]):
data = self.conn_recv.recv() data = self.conn_recv.recv()
return data return data
def close(self): async def async_receive(self) -> T:
""" """
关闭通道 异步接收数据会挂起等待
""" """
self._closed = True print("等待接收数据")
self.conn_send.close() loop = asyncio.get_running_loop()
self.conn_recv.close() data = await loop.run_in_executor(None, self.receive)
print("接收到数据")
return data
def on_receive(self, filter_func: Optional[FILTER_FUNC] = None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]: def on_receive(self, filter_func: Optional[FILTER_FUNC] = None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
""" """
@ -146,11 +159,8 @@ class Channel(Generic[T]):
Returns: Returns:
装饰器装饰一个函数在接收到数据后执行 装饰器装饰一个函数在接收到数据后执行
""" """
if (not self.is_sub_receive_loop_running) and not IS_MAIN_PROCESS: if not IS_MAIN_PROCESS:
threading.Thread(target=self._start_sub_receive_loop, daemon=True).start() raise RuntimeError("on_receive can only be used in main process")
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]: def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
global _func_id global _func_id
@ -171,65 +181,52 @@ class Channel(Generic[T]):
_callback_funcs[_func_id] = wrapper _callback_funcs[_func_id] = wrapper
if IS_MAIN_PROCESS: if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(_func_id) self._on_main_receive_func_ids.append(_func_id)
else: else:
self._on_sub_receive_funcs.append(_func_id) self._on_sub_receive_func_ids.append(_func_id)
_func_id += 1 _func_id += 1
return func return func
return decorator return decorator
def _run_on_main_receive_funcs(self, data: Any): async def _run_on_receive_funcs(self, data: Any):
""" """
运行接收函数 运行接收函数
Args: Args:
data: 数据 data: 数据
""" """
for func_id in self._on_main_receive_funcs: if IS_MAIN_PROCESS:
func = _callback_funcs[func_id] [asyncio.create_task(_callback_funcs[func_id](data)) for func_id in self._on_main_receive_func_ids]
run_coroutine(func(data)) else:
[asyncio.create_task(_callback_funcs[func_id](data)) for func_id in self._on_sub_receive_func_ids]
def _run_on_sub_receive_funcs(self, data: Any): async def start_receive_loop(self):
"""
运行接收函数
Args:
data: 数据
"""
for func_id in self._on_sub_receive_funcs:
func = _callback_funcs[func_id]
run_coroutine(func(data))
def _start_main_receive_loop(self):
""" """
开始接收数据 开始接收数据
会自动判断主进程和子进程需要在对应进程都调度一次
""" """
self.is_main_receive_loop_running = True if len(self._on_main_receive_func_ids) == 0:
while not self._closed: logger.warning(f"No on_receive function registered for {self.name}")
data = self.conn_recv.recv() return
self._run_on_main_receive_funcs(data)
def _start_sub_receive_loop(self): self.is_receive_loop_running = True
""" logger.debug(f"Starting receive loop for {self.name}")
开始接收数据
"""
self.is_sub_receive_loop_running = True
while not self._closed: while not self._closed:
data = self.conn_recv.recv() data = await self.async_receive()
self._run_on_sub_receive_funcs(data) await self._run_on_receive_funcs(data)
"""子进程可用的主动和被动通道""" """子进程可用的主动和被动通道"""
active_channel: Optional["Channel"] = None active_channel: Channel = Channel(name="active_channel")
passive_channel: Optional["Channel"] = None passive_channel: Channel = Channel(name="passive_channel")
publish_channel: Channel[tuple[str, dict[str, Any]]] = Channel(_id="publish_channel") publish_channel: Channel[tuple[str, dict[str, Any]]] = Channel(name="publish_channel")
"""通道传递通道,主进程创建单例,子进程初始化时实例化""" """通道传递通道,主进程创建单例,子进程初始化时实例化"""
channel_deliver_active_channel: Channel[Channel[Any]] channel_deliver_active_channel: Channel[Channel[Any]]
channel_deliver_passive_channel: Channel[tuple[str, dict[str, Any]]] channel_deliver_passive_channel: Channel[tuple[str, dict[str, Any]]]
if IS_MAIN_PROCESS: if IS_MAIN_PROCESS:
channel_deliver_active_channel = Channel(_id="channel_deliver_active_channel") channel_deliver_active_channel = Channel(name="channel_deliver_active_channel")
channel_deliver_passive_channel = Channel(_id="channel_deliver_passive_channel") channel_deliver_passive_channel = Channel(name="channel_deliver_passive_channel")
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "set_channel") @channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "set_channel")
@ -250,7 +247,7 @@ if IS_MAIN_PROCESS:
recv_chan.send(get_channels()) recv_chan.send(get_channels())
def set_channel(name: str, channel: Channel): def set_channel(name: str, channel: "Channel"):
""" """
设置通道实例 设置通道实例
Args: Args:
@ -261,6 +258,8 @@ def set_channel(name: str, channel: Channel):
raise TypeError(f"channel_ must be an instance of Channel, {type(channel)} found") raise TypeError(f"channel_ must be an instance of Channel, {type(channel)} found")
if IS_MAIN_PROCESS: if IS_MAIN_PROCESS:
if name in _channel:
raise ValueError(f"Channel {name} already exists")
_channel[name] = channel _channel[name] = channel
else: else:
# 请求主进程设置通道 # 请求主进程设置通道
@ -274,7 +273,7 @@ def set_channel(name: str, channel: Channel):
) )
def set_channels(channels: dict[str, Channel]): def set_channels(channels: dict[str, "Channel"]):
""" """
设置通道实例 设置通道实例
Args: Args:
@ -284,7 +283,7 @@ def set_channels(channels: dict[str, Channel]):
set_channel(name, channel) set_channel(name, channel)
def get_channel(name: str) -> Channel: def get_channel(name: str) -> "Channel":
""" """
获取通道实例 获取通道实例
Args: Args:
@ -308,7 +307,7 @@ def get_channel(name: str) -> Channel:
return recv_chan.receive() return recv_chan.receive()
def get_channels() -> dict[str, Channel]: def get_channels() -> dict[str, "Channel"]:
""" """
获取通道实例 获取通道实例
Returns: Returns:

26
liteyuki/comm/rpc.py Normal file
View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""
本模块用于实现RPC(基于IPC)通信
"""
from typing import TypeAlias, Callable, Any
from liteyuki.comm.channel import Channel
ON_CALLING_FUNC: TypeAlias = Callable[[tuple, dict], Any]
class RPC:
"""
RPC类
"""
def __init__(self, on_calling: ON_CALLING_FUNC) -> None:
self.on_calling = on_calling
def call(self, args: tuple, kwargs: dict) -> Any:
"""
调用
"""
# 获取self.calling函数名
return self.on_calling(args, kwargs)

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/25 下午3:54
@Author : snowykami
@Email : snowykami@outlook.com
@File : channelv2.py
@Software: PyCharm
"""
class SocksChannel:
"""
通道类可以在进程间和进程内通信双向但同时只能有一个发送者和一个接收者
有两种接收工作方式但是只能选择一种主动接收和被动接收主动接收使用 `receive` 方法被动接收使用 `on_receive` 装饰器
"""
def __init__(self, name: str):
"""
初始化通道
Args:
name: 通道ID
"""
self._name = name
self._conn_send = None
self._conn_recv = None
self._closed = False
def send(self, data):
"""
发送数据
Args:
data: 数据
"""
pass
def receive(self):
"""
接收数据
Returns:
data: 数据
"""
pass
def close(self):
"""
关闭通道
"""
pass

View File

@ -2,13 +2,13 @@
""" """
共享内存模块类似于redis但是更加轻量级并且线程安全 共享内存模块类似于redis但是更加轻量级并且线程安全
""" """
import asyncio
import threading import threading
from typing import Any, Coroutine, Optional, TypeAlias, Callable from typing import Any, Callable, Optional
from liteyuki.comm import channel from liteyuki.comm import channel
from liteyuki.comm.channel import Channel, ON_RECEIVE_FUNC, ASYNC_ON_RECEIVE_FUNC from liteyuki.comm.channel import ASYNC_ON_RECEIVE_FUNC, Channel, ON_RECEIVE_FUNC
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable, run_coroutine, run_coroutine_in_thread from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable, run_coroutine_in_thread
if IS_MAIN_PROCESS: if IS_MAIN_PROCESS:
_locks = {} _locks = {}
@ -31,24 +31,13 @@ def _get_lock(key) -> threading.Lock:
raise RuntimeError("Cannot get lock in sub process.") raise RuntimeError("Cannot get lock in sub process.")
class Subscriber:
def __init__(self):
self._subscribers = {}
def receive(self) -> Any:
pass
def unsubscribe(self) -> None:
pass
class KeyValueStore: class KeyValueStore:
def __init__(self): def __init__(self):
self._store = {} self._store = {}
self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id="shared_memory-active") self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](name="shared_memory-active")
self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id="shared_memory-passive") self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](name="shared_memory-passive")
self.publish_channel = Channel[tuple[str, Any]](_id="shared_memory-publish") self.publish_channel = Channel[tuple[str, Any]](name="shared_memory-publish")
self.is_main_receive_loop_running = False self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False self.is_sub_receive_loop_running = False
@ -184,12 +173,8 @@ class KeyValueStore:
Returns: Returns:
装饰器 装饰器
""" """
if IS_MAIN_PROCESS and not self.is_main_receive_loop_running: if not IS_MAIN_PROCESS:
threading.Thread(target=self._start_receive_loop, daemon=True).start() raise RuntimeError("Cannot subscribe in sub process.")
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: def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any): async def wrapper(data: Any):
@ -211,38 +196,29 @@ class KeyValueStore:
return decorator return decorator
@staticmethod @staticmethod
def run_subscriber_receive_funcs(channel_: str, data: Any): async def run_subscriber_receive_funcs(channel_: str, data: Any):
""" """
运行订阅者接收函数 运行订阅者接收函数
Args: Args:
channel_: 频道 channel_: 频道
data: 数据 data: 数据
""" """
if IS_MAIN_PROCESS: [asyncio.create_task(func(data)) for func in _on_main_subscriber_receive_funcs[channel_]]
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
run_coroutine_in_thread(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
else:
if channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
run_coroutine_in_thread(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
def _start_receive_loop(self): async def start_receive_loop(self):
""" """
启动发布订阅接收器循环在主进程中运行若有子进程订阅则推送给子进程 启动发布订阅接收器循环在主进程中运行若有子进程订阅则推送给子进程
""" """
if IS_MAIN_PROCESS:
if not IS_MAIN_PROCESS:
raise RuntimeError("Cannot start receive loop in sub process.")
while True: while True:
data = self.active_chan.receive() data = await self.active_chan.async_receive()
if data[0] == "publish": if data[0] == "publish":
# 运行主进程订阅函数 # 运行主进程订阅函数
self.run_subscriber_receive_funcs(data[1]["channel"], data[1]["data"]) await self.run_subscriber_receive_funcs(data[1]["channel"], data[1]["data"])
# 推送给子进程 # 推送给子进程
self.publish_channel.send(data) self.publish_channel.send(data)
else:
while True:
data = self.publish_channel.receive()
if data[0] == "publish":
# 运行子进程订阅函数
self.run_subscriber_receive_funcs(data[1]["channel"], data[1]["data"])
class GlobalKeyValueStore: class GlobalKeyValueStore:
@ -262,7 +238,6 @@ shared_memory: KeyValueStore = GlobalKeyValueStore.get_instance()
# 全局单例访问点 # 全局单例访问点
if IS_MAIN_PROCESS: if IS_MAIN_PROCESS:
@shared_memory.passive_chan.on_receive(lambda d: d[0] == "get") @shared_memory.passive_chan.on_receive(lambda d: d[0] == "get")
def on_get(data: tuple[str, dict[str, Any]]): def on_get(data: tuple[str, dict[str, Any]]):
key = data[1]["key"] key = data[1]["key"]
@ -289,14 +264,6 @@ if IS_MAIN_PROCESS:
recv_chan = data[1]["recv_chan"] recv_chan = data[1]["recv_chan"]
recv_chan.send(shared_memory.get_all()) recv_chan.send(shared_memory.get_all())
else:
# 子进程在入口函数中对shared_memory进行初始化
@channel.publish_channel.on_receive()
def on_publish(data: tuple[str, Any]):
channel_, data = data
shared_memory.run_subscriber_receive_funcs(channel_, data)
_ref_count = 0 # import 引用计数, 防止获取空指针 _ref_count = 0 # import 引用计数, 防止获取空指针
if not IS_MAIN_PROCESS: if not IS_MAIN_PROCESS:
if (shared_memory is None) and _ref_count > 1: if (shared_memory is None) and _ref_count > 1:

View File

@ -1,4 +1,2 @@
import multiprocessing
from .manager import * from .manager import *

View File

@ -8,13 +8,11 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@File : manager.py @File : manager.py
@Software: PyCharm @Software: PyCharm
""" """
import asyncio
import multiprocessing import multiprocessing
import threading
from multiprocessing import Process from multiprocessing import Process
from typing import Any, Callable, TYPE_CHECKING, TypeAlias from typing import Any, Callable, TYPE_CHECKING, TypeAlias
from liteyuki.comm.channel import Channel, get_channel, set_channels, publish_channel
from liteyuki.comm.storage import shared_memory
from liteyuki.log import logger from liteyuki.log import logger
from liteyuki.utils import IS_MAIN_PROCESS from liteyuki.utils import IS_MAIN_PROCESS
@ -22,10 +20,15 @@ if TYPE_CHECKING:
from liteyuki.bot.lifespan import Lifespan from liteyuki.bot.lifespan import Lifespan
from liteyuki.comm.storage import KeyValueStore from liteyuki.comm.storage import KeyValueStore
from liteyuki.comm import Channel
if IS_MAIN_PROCESS: if IS_MAIN_PROCESS:
from liteyuki.comm.channel import get_channel, publish_channel, get_channels
from liteyuki.comm.storage import shared_memory
from liteyuki.comm.channel import channel_deliver_active_channel, channel_deliver_passive_channel from liteyuki.comm.channel import channel_deliver_active_channel, channel_deliver_passive_channel
else: else:
from liteyuki.comm import channel from liteyuki.comm import channel
from liteyuki.comm import storage
TARGET_FUNC: TypeAlias = Callable[..., Any] TARGET_FUNC: TypeAlias = Callable[..., Any]
TIMEOUT = 10 TIMEOUT = 10
@ -69,7 +72,7 @@ def _delivery_channel_wrapper(func: TARGET_FUNC, cd: ChannelDeliver, sm: "KeyVal
channel.publish_channel = cd.publish # 子进程发布通道 channel.publish_channel = cd.publish # 子进程发布通道
# 给子进程创建共享内存实例 # 给子进程创建共享内存实例
from liteyuki.comm import storage
storage.shared_memory = sm storage.shared_memory = sm
func(*args, **kwargs) func(*args, **kwargs)
@ -85,13 +88,12 @@ class ProcessManager:
self.targets: dict[str, tuple[Callable, tuple, dict]] = {} self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
self.processes: dict[str, Process] = {} self.processes: dict[str, Process] = {}
def start(self, name: str): async def _run_process(self, name: str):
""" """
开启后自动监控进程并添加到进程字典中 开启后自动监控进程并添加到进程字典中会阻塞请创建task
Args: Args:
name: name:
Returns: Returns:
""" """
if name not in self.targets: if name not in self.targets:
raise KeyError(f"Process {name} not found.") raise KeyError(f"Process {name} not found.")
@ -108,30 +110,31 @@ class ProcessManager:
_start_process() _start_process()
while True: while True:
data = chan_active.receive() data = await chan_active.async_receive()
if data == 0: if data == 0:
# 停止 # 停止
logger.info(f"Stopping process {name}") logger.info(f"Stopping process {name}")
self.lifespan.before_process_shutdown() await self.lifespan.before_process_shutdown()
self.terminate(name) self.terminate(name)
break break
elif data == 1: elif data == 1:
# 重启 # 重启
logger.info(f"Restarting process {name}") logger.info(f"Restarting process {name}")
self.lifespan.before_process_shutdown() await self.lifespan.before_process_shutdown()
self.lifespan.before_process_restart() await self.lifespan.before_process_restart()
self.terminate(name) self.terminate(name)
_start_process() _start_process()
continue continue
else: else:
logger.warning("Unknown data received, ignored.") logger.warning("Unknown data received, ignored.")
def start_all(self): async def start_all(self):
""" """
启动所有进程 对外启动方法启动所有进程创建asyncio task
""" """
for name in self.targets: [asyncio.create_task(chan.start_receive_loop()) for chan in get_channels().values()]
threading.Thread(target=self.start, args=(name,), daemon=True).start() [asyncio.create_task(sm.start_receive_loop()) for sm in [shared_memory]]
[asyncio.create_task(self._run_process(name)) for name in self.targets]
def add_target(self, name: str, target: TARGET_FUNC, args: tuple = (), kwargs=None): def add_target(self, name: str, target: TARGET_FUNC, args: tuple = (), kwargs=None):
""" """
@ -144,8 +147,8 @@ class ProcessManager:
""" """
if kwargs is None: if kwargs is None:
kwargs = {} kwargs = {}
chan_active: Channel = Channel(_id=f"{name}-active") chan_active: Channel = Channel(name=f"{name}-active")
chan_passive: Channel = Channel(_id=f"{name}-passive") chan_passive: Channel = Channel(name=f"{name}-passive")
channel_deliver = ChannelDeliver( channel_deliver = ChannelDeliver(
active=chan_active, active=chan_active,
@ -157,12 +160,6 @@ class ProcessManager:
self.targets[name] = (_delivery_channel_wrapper, (target, channel_deliver, shared_memory, *args), kwargs) 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): def join_all(self):
for name, process in self.targets: for name, process in self.targets:

View File

@ -10,6 +10,7 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
""" """
from typing import Any, Optional from typing import Any, Optional
from liteyuki import Channel
from liteyuki.comm.storage import shared_memory from liteyuki.comm.storage import shared_memory
@ -24,7 +25,7 @@ class MessageEvent:
session_id: str, session_id: str,
user_id: str, user_id: str,
session_type: str, session_type: str,
receive_channel: str, receive_channel: Optional[Channel["MessageEvent"]] = None,
data: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None,
): ):
""" """
@ -78,7 +79,10 @@ class MessageEvent:
}, },
bot_id=self.bot_id, bot_id=self.bot_id,
session_id=self.session_id, session_id=self.session_id,
user_id=self.user_id,
session_type=self.session_type, session_type=self.session_type,
receive_channel="_" receive_channel=None
) )
shared_memory.publish(self.receive_channel, reply_event) # shared_memory.publish(self.receive_channel, reply_event)
if self.receive_channel:
self.receive_channel.send(reply_event)

View File

@ -23,6 +23,7 @@ _queue: Queue = Queue()
@shared_memory.on_subscriber_receive("event_to_liteyuki") @shared_memory.on_subscriber_receive("event_to_liteyuki")
async def _(event: MessageEvent): async def _(event: MessageEvent):
print("AA")
current_priority = -1 current_priority = -1
for i, matcher in enumerate(_matcher_list): for i, matcher in enumerate(_matcher_list):
logger.info(f"Running matcher {matcher} for event: {event}") logger.info(f"Running matcher {matcher} for event: {event}")
@ -32,17 +33,24 @@ async def _(event: MessageEvent):
current_priority = matcher.priority current_priority = matcher.priority
if matcher.block: if matcher.block:
break break
else:
logger.info(f"No matcher matched for event: {event}")
print("BB")
def on_message(rule: Rule = empty_rule, priority: int = 0, block: bool = False) -> Matcher: def add_matcher(matcher: Matcher):
matcher = Matcher(rule, priority, block)
# 按照优先级插入
for i, m in enumerate(_matcher_list): for i, m in enumerate(_matcher_list):
if m.priority < matcher.priority: if m.priority < matcher.priority:
_matcher_list.insert(i, matcher) _matcher_list.insert(i, matcher)
break break
else: else:
_matcher_list.append(matcher) _matcher_list.append(matcher)
def on_message(rule: Rule = empty_rule, priority: int = 0, block: bool = False) -> Matcher:
matcher = Matcher(rule, priority, block)
# 按照优先级插入
add_matcher(matcher)
return matcher return matcher
@ -50,4 +58,5 @@ def on_keywords(keywords: list[str], rule=empty_rule, priority: int = 0, block:
@Rule @Rule
async def on_keywords_rule(event: MessageEvent): async def on_keywords_rule(event: MessageEvent):
return any(keyword in event.raw_message for keyword in keywords) return any(keyword in event.raw_message for keyword in keywords)
return on_message(on_keywords_rule & rule, priority, block) return on_message(on_keywords_rule & rule, priority, block)

View File

@ -43,7 +43,7 @@ def run_coroutine(*coro: Coroutine):
# 检测是否有现有的事件循环 # 检测是否有现有的事件循环
try: try:
loop = asyncio.get_event_loop() loop = asyncio.get_running_loop()
if loop.is_running(): if loop.is_running():
# 如果事件循环正在运行,创建任务 # 如果事件循环正在运行,创建任务
for c in coro: for c in coro:
@ -62,6 +62,7 @@ def run_coroutine(*coro: Coroutine):
# 捕获其他异常,防止协程被重复等待 # 捕获其他异常,防止协程被重复等待
logger.error(f"Exception occurred: {e}") logger.error(f"Exception occurred: {e}")
def run_coroutine_in_thread(*coro: Coroutine): def run_coroutine_in_thread(*coro: Coroutine):
""" """
在新线程中运行协程 在新线程中运行协程
@ -73,6 +74,7 @@ def run_coroutine_in_thread(*coro: Coroutine):
""" """
threading.Thread(target=run_coroutine, args=coro, daemon=True).start() threading.Thread(target=run_coroutine, args=coro, daemon=True).start()
def path_to_module_name(path: Path) -> str: def path_to_module_name(path: Path) -> str:
""" """
转换路径为模块名 转换路径为模块名

View File

@ -4,6 +4,7 @@
from liteyuki import LiteyukiBot from liteyuki import LiteyukiBot
from liteyuki.config import load_config_in_default from liteyuki.config import load_config_in_default
if __name__ == "__main__": if __name__ == "__main__":
bot = LiteyukiBot(**load_config_in_default(no_waring=True)) bot = LiteyukiBot(**load_config_in_default(no_waring=True))
bot.run() bot.run()

View File

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/22 上午9:06
@Author : snowykami
@Email : snowykami@outlook.com
@File : anti_dislink.py
@Software: PyCharm
"""
import random
from liteyuki.plugin import PluginMetadata, PluginType
from liteyuki.message.on import on_keywords
__plugin_meta__ = PluginMetadata(
name="严禁断联化",
type=PluginType.APPLICATION
)
@on_keywords(["看看你的", "看看j", "给我看看"]).handle()
async def _(event):
event.reply(random.choice(["No dislink", "严禁断联化"]))

View File

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/22 上午8:37
@Author : snowykami
@Email : snowykami@outlook.com
@File : ts_chan_main.py
@Software: PyCharm
"""
import asyncio
from liteyuki.comm import Channel, set_channel, get_channel
from liteyuki import get_bot
set_channel("chan-main", Channel("chan-main"))
set_channel("chan-sub", Channel("chan-sub"))
chan_main = get_channel("chan-main")
# @get_bot().on_after_start
# async def _():
# while True:
# chan_main.send("Hello, World!")
# await asyncio.sleep(5)

View File

@ -10,18 +10,23 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
""" """
import asyncio import asyncio
from nonebot import Bot, get_bot, on_message from nonebot import Bot, get_bot, on_message, get_driver
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from nonebot.adapters.onebot.v11 import MessageEvent, Bot from nonebot.adapters.onebot.v11 import MessageEvent, Bot
from liteyuki import Channel
from liteyuki.comm import get_channel
from liteyuki.comm.storage import shared_memory from liteyuki.comm.storage import shared_memory
from liteyuki.message.event import MessageEvent as LiteyukiMessageEvent from liteyuki.message.event import MessageEvent as LiteyukiMessageEvent
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="轻雪物流", name="轻雪push",
description="把消息事件传递给轻雪框架进行处理", description="把消息事件传递给轻雪框架进行处理",
usage="用户无需使用", usage="用户无需使用",
) )
recv_channel = Channel[LiteyukiMessageEvent](name="event_to_nonebot")
@on_message().handle() @on_message().handle()
async def _(bot: Bot, event: MessageEvent): async def _(bot: Bot, event: MessageEvent):
@ -34,14 +39,16 @@ async def _(bot: Bot, event: MessageEvent):
user_id=str(event.user_id), user_id=str(event.user_id),
session_id=str(event.user_id if event.message_type == "private" else event.group_id), session_id=str(event.user_id if event.message_type == "private" else event.group_id),
session_type=event.message_type, session_type=event.message_type,
receive_channel="event_to_nonebot" receive_channel=recv_channel,
) )
shared_memory.publish("event_to_liteyuki", liteyuki_event) shared_memory.publish("event_to_liteyuki", liteyuki_event)
@shared_memory.on_subscriber_receive("event_to_nonebot") @get_driver().on_bot_connect
async def _(event: LiteyukiMessageEvent): async def _():
bot: Bot = get_bot(event.bot_id) while True:
event = await recv_channel.async_receive()
bot: Bot = get_bot(event.bot_id) # type: ignore
if event.message_type == "private": if event.message_type == "private":
await bot.send_private_msg(user_id=int(event.session_id), message=event.data["message"]) await bot.send_private_msg(user_id=int(event.session_id), message=event.data["message"])
elif event.message_type == "group": elif event.message_type == "group":

View File

@ -1,35 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/22 上午8:39
@Author : snowykami
@Email : snowykami@outlook.com
@File : ts_chan_sub.py
@Software: PyCharm
"""
import asyncio
from liteyuki.comm import Channel, get_channel
from nonebot import get_bot
from nonebot.adapters.onebot.v11 import Bot
chan_main = get_channel("chan-main")
# @chan_main.on_receive()
# async def _(data: str):
# print("Received data from chan-main:", data)
# try:
# bot: Bot = get_bot("2443429204") # type: ignore
#
# def send_msg():
#
# bot.send_msg(message_type="private", user_id=2443429204, message=data)
#
# print("tsA")
# print("tsA1")
# await asyncio.ensure_future(c)
# print("tsB")
# except Exception as e:
# print(e)
# pass