1
0
forked from bot/app

11 Commits

52 changed files with 2524 additions and 237 deletions

View File

@ -33,6 +33,18 @@ jobs:
cd docs cd docs
pnpm install pnpm install
- name: 设置Python
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: 生成API markdown
run: |-
python -m pip install pydantic
python liteyuki/mkdoc.py
- name: 构建文档 - name: 构建文档
env: env:
NODE_OPTIONS: --max_old_space_size=8192 NODE_OPTIONS: --max_old_space_size=8192

4
.gitignore vendored
View File

@ -50,3 +50,7 @@ prompt.txt
.pdm-python .pdm-python
.pdm-build .pdm-build
dist dist
doc
mkdoc2.py

View File

@ -1,31 +1,10 @@
import {sidebar} from "vuepress-theme-hope"; import {sidebar} from "vuepress-theme-hope";
export const enSidebarConfig = sidebar({ export const enSidebarConfig = sidebar(
"/en/": [
"",
{ {
text: "Install & Deploy", "/en/deploy/": "structure",
icon: "laptop-code", "/en/usage/": "structure",
prefix: "deploy/", "/en/store/": "structure",
children: "structure", "/en/dev/": "structure",
},
{
text: "Usage & Features",
icon: "book",
prefix: "usage/",
children: "structure",
},
{
text: "Resources & Plugins",
icon: "store",
prefix: "store/",
children: "structure",
},
{
text: "Development & Contribution",
icon: "pen-nib",
prefix: "dev/",
children: "structure",
} }
], )
});

View File

@ -1,31 +1,11 @@
import {sidebar} from "vuepress-theme-hope"; import {sidebar} from "vuepress-theme-hope";
export const zhSidebarConfig = sidebar({
"/": [ export const zhSidebarConfig = sidebar(
"",
{ {
text: "安装及部署", "/deploy/": "structure",
icon: "laptop-code", "/usage/": "structure",
prefix: "deploy/", "/store/": "structure",
children: "structure", "/dev/": "structure",
},
{
text: "使用及功能",
icon: "book",
prefix: "usage/",
children: "structure",
},
{
text: "资源及插件",
icon: "store",
prefix: "store/",
children: "structure",
},
{
text: "开发及贡献",
icon: "pen-nib",
prefix: "dev/",
children: "structure",
} }
], )
});

View File

@ -5,6 +5,7 @@ import {enNavbarConfig, zhNavbarConfig} from "./navbar/index.js";
export default hopeTheme({ export default hopeTheme({
hostname: "https://vuepress-theme-hope-docs-demo.netlify.app", hostname: "https://vuepress-theme-hope-docs-demo.netlify.app",
hotReload: true,
locales: { locales: {
"/": { "/": {

7
docs/dev/api/README.md Normal file
View File

@ -0,0 +1,7 @@
---
title: liteyuki
index: true
icon: laptop-code
category: API
---

227
docs/dev/api/bot/README.md Normal file
View File

@ -0,0 +1,227 @@
---
title: liteyuki.bot
index: true
icon: laptop-code
category: API
---
### ***def*** `get_bot() -> LiteyukiBot`
获取轻雪实例
Returns:
LiteyukiBot: 当前的轻雪实例
### ***def*** `get_config(key: str, default: Any) -> Any`
获取配置
Args:
key: 配置键
default: 默认值
Returns:
Any: 配置值
### ***def*** `get_config_with_compat(key: str, compat_keys: tuple[str], default: Any) -> Any`
获取配置,兼容旧版本
Args:
key: 配置键
compat_keys: 兼容键
default: 默认值
Returns:
Any: 配置值
### ***def*** `print_logo() -> None`
### ***class*** `LiteyukiBot`
###   ***def*** `__init__(self) -> None`
 初始化轻雪实例
Args:
*args:
**kwargs: 配置
###   ***def*** `run(self) -> None`
 启动逻辑
###   ***def*** `keep_alive(self) -> None`
 保持轻雪运行
Returns:
###   ***def*** `restart(self, delay: int) -> None`
 重启轻雪本体
Returns:
###   ***def*** `restart_process(self, name: Optional[str]) -> None`
 停止轻雪
Args:
name: 进程名称, 默认为None, 所有进程
Returns:
###   ***def*** `init(self) -> None`
 初始化轻雪, 自动调用
Returns:
###   ***def*** `init_logger(self) -> None`
 
###   ***def*** `stop(self) -> None`
 停止轻雪
Returns:
###   ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> None`
 注册启动前的函数
Args:
func:
Returns:
###   ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> None`
 注册启动后的函数
Args:
func:
Returns:
###   ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> None`
 注册停止后的函数:未实现
Args:
func:
Returns:
###   ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> None`
 注册进程停止前的函数,为子进程停止时调用
Args:
func:
Returns:
###   ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> None`
 注册进程重启前的函数,为子进程重启时调用
Args:
func:
Returns:
###   ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> None`
 注册重启后的函数:未实现
Args:
func:
Returns:
###   ***def*** `on_after_nonebot_init(self, func: LIFESPAN_FUNC) -> None`
 注册nonebot初始化后的函数
Args:
func:
Returns:
### ***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'`

View File

@ -0,0 +1,170 @@
---
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:
### ***class*** `Lifespan`
###   ***def*** `__init__(self) -> None`
 轻雪生命周期管理,启动、停止、重启
###   ***@staticmethod***
###   ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
 运行函数
Args:
funcs:
Returns:
###   ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
 注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
###   ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
 注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
###   ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
 注册停止前的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
###   ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
 注册停止后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
###   ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
 注册重启时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
###   ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
 注册重启后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
###   ***def*** `on_after_nonebot_init(self, func: Any) -> None`
 注册 NoneBot 初始化后的函数
Args:
func:
Returns:
###   ***def*** `before_start(self) -> None`
 启动前
Returns:
###   ***def*** `after_start(self) -> None`
 启动后
Returns:
###   ***def*** `before_process_shutdown(self) -> None`
 停止前
Returns:
###   ***def*** `after_shutdown(self) -> None`
 停止后
Returns:
###   ***def*** `before_process_restart(self) -> None`
 重启前
Returns:
###   ***def*** `after_restart(self) -> None`
 重启后
Returns:
### ***var*** `tasks = []`
### ***var*** `loop = asyncio.get_event_loop()`
### ***var*** `loop = asyncio.new_event_loop()`

View File

@ -0,0 +1,7 @@
---
title: liteyuki.comm
index: true
icon: laptop-code
category: API
---

View File

@ -0,0 +1,149 @@
---
title: liteyuki.comm.channel_
order: 1
icon: laptop-code
category: API
---
### ***def*** `set_channel(name: str, channel: Channel) -> None`
设置通道实例
Args:
name: 通道名称
channel: 通道实例
### ***def*** `set_channels(channels: dict[str, Channel]) -> None`
设置通道实例
Args:
channels: 通道名称
### ***def*** `get_channel(name: str) -> Channel`
获取通道实例
Args:
name: 通道名称
Returns:
### ***def*** `get_channels() -> dict[str, Channel]`
获取通道实例
Returns:
### ***def*** `on_set_channel(data: tuple[str, dict[str, Any]]) -> None`
### ***def*** `on_get_channel(data: tuple[str, dict[str, Any]]) -> None`
### ***def*** `on_get_channels(data: tuple[str, dict[str, Any]]) -> None`
### ***def*** `decorator(func: Callable[[T], Any]) -> Callable[[T], Any]`
### ***async def*** `wrapper(data: T) -> Any`
### ***class*** `Channel(Generic[T])`
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
###   ***def*** `__init__(self, _id: str, type_check: bool) -> None`
 初始化通道
Args:
_id: 通道ID
###   ***def*** `send(self, data: T) -> None`
 发送数据
Args:
data: 数据
###   ***def*** `receive(self) -> T`
 接收数据
Args:
###   ***def*** `close(self) -> None`
 关闭通道
###   ***def*** `on_receive(self, filter_func: Optional[FILTER_FUNC]) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]`
 接收数据并执行函数
Args:
filter_func: 过滤函数为None则不过滤
Returns:
装饰器,装饰一个函数在接收到数据后执行
### ***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*** `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()`

View File

@ -0,0 +1,15 @@
---
title: liteyuki.comm.event
order: 1
icon: laptop-code
category: API
---
### ***class*** `Event`
事件类
###   ***def*** `__init__(self, name: str, data: dict[str, Any]) -> None`
 

View File

@ -0,0 +1,140 @@
---
title: liteyuki.comm.storage
order: 1
icon: laptop-code
category: API
---
### ***def*** `on_get(data: tuple[str, dict[str, Any]]) -> None`
### ***def*** `on_set(data: tuple[str, dict[str, Any]]) -> None`
### ***def*** `on_delete(data: tuple[str, dict[str, Any]]) -> None`
### ***def*** `on_get_all(data: tuple[str, dict[str, Any]]) -> None`
### ***class*** `KeyValueStore`
###   ***def*** `__init__(self) -> None`
 
###   ***def*** `set(self, key: str, value: Any) -> None`
 设置键值对
Args:
key: 键
value: 值
###   ***def*** `get(self, key: str, default: Optional[Any]) -> Optional[Any]`
 获取键值对
Args:
key: 键
default: 默认值
Returns:
Any: 值
###   ***def*** `delete(self, key: str, ignore_key_error: bool) -> None`
 删除键值对
Args:
key: 键
ignore_key_error: 是否忽略键不存在的错误
Returns:
###   ***def*** `get_all(self) -> dict[str, Any]`
 获取所有键值对
Returns:
dict[str, Any]: 键值对
### ***class*** `GlobalKeyValueStore`
###   ***@classmethod***
###   ***def*** `get_instance(cls: Any) -> None`
 
###   ***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')`

99
docs/dev/api/config.md Normal file
View File

@ -0,0 +1,99 @@
---
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:
扁平化后的配置文件,但也包含原有的键值对
### ***def*** `load_from_yaml(file: str) -> dict[str, Any]`
Load config from yaml file
### ***def*** `load_from_json(file: str) -> dict[str, Any]`
Load config from json file
### ***def*** `load_from_toml(file: str) -> dict[str, Any]`
Load config from toml file
### ***def*** `load_from_files() -> dict[str, Any]`
从指定文件加载配置项,会自动识别文件格式
默认执行扁平化选项
### ***def*** `load_configs_from_dirs() -> dict[str, Any]`
从目录下加载配置文件,不递归
按照读取文件的优先级反向覆盖
默认执行扁平化选项
### ***def*** `load_config_in_default(no_waring: bool) -> dict[str, Any]`
从一个标准的轻雪项目加载配置文件
项目目录下的config.*和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)`

View File

@ -0,0 +1,7 @@
---
title: liteyuki.core
index: true
icon: laptop-code
category: API
---

View File

@ -0,0 +1,111 @@
---
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]]) -> None`
 
### ***class*** `ProcessManager`
进程管理器
###   ***def*** `__init__(self, lifespan: 'Lifespan') -> None`
 
###   ***def*** `start(self, name: str) -> None`
 开启后自动监控进程,并添加到进程字典中
Args:
name:
Returns:
###   ***def*** `start_all(self) -> None`
 启动所有进程
###   ***def*** `add_target(self, name: str, target: TARGET_FUNC, args: tuple, kwargs: Any) -> None`
 添加进程
Args:
name: 进程名,用于获取和唯一标识
target: 进程函数
args: 进程函数参数
kwargs: 进程函数关键字参数通常会默认传入chan_active和chan_passive
###   ***def*** `join_all(self) -> None`
 
###   ***def*** `terminate(self, name: str) -> None`
 终止进程并从进程字典中删除
Args:
name:
Returns:
###   ***def*** `terminate_all(self) -> None`
 
###   ***def*** `is_process_alive(self, name: str) -> bool`
 检查进程是否存活
Args:
name:
Returns:
### ***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)`
### ***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 = {}`

View File

@ -0,0 +1,7 @@
---
title: liteyuki.dev
index: true
icon: laptop-code
category: API
---

View File

@ -0,0 +1,91 @@
---
title: liteyuki.dev.observer
order: 1
icon: laptop-code
category: API
---
### ***def*** `debounce(wait: Any) -> None`
防抖函数
### ***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:
装饰器,装饰一个函数在接收到数据后执行
### ***def*** `decorator(func: Any) -> None`
### ***def*** `decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC`
### ***def*** `wrapper() -> None`
### ***def*** `wrapper(event: FileSystemEvent) -> None`
### ***class*** `CodeModifiedHandler(FileSystemEventHandler)`
Handler for code file changes
###   ***def*** `on_modified(self, event: Any) -> None`
 
###   ***def*** `on_created(self, event: Any) -> None`
 
###   ***def*** `on_deleted(self, event: Any) -> None`
 
###   ***def*** `on_moved(self, event: Any) -> None`
 
###   ***def*** `on_any_event(self, event: Any) -> None`
 
### ***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`

View File

@ -0,0 +1,27 @@
---
title: liteyuki.dev.plugin
order: 1
icon: laptop-code
category: API
---
### ***def*** `run_plugins() -> None`
运行插件无需手动初始化bot
Args:
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
### ***var*** `cfg = load_config_in_default()`
### ***var*** `plugins = cfg.get('liteyuki.plugins', [])`
### ***var*** `bot = LiteyukiBot(**cfg)`

11
docs/dev/api/exception.md Normal file
View File

@ -0,0 +1,11 @@
---
title: liteyuki.exception
order: 1
icon: laptop-code
category: API
---
### ***class*** `LiteyukiException(BaseException)`
Liteyuki的异常基类。

21
docs/dev/api/log.md Normal file
View File

@ -0,0 +1,21 @@
---
title: liteyuki.log
order: 1
icon: laptop-code
category: API
---
### ***def*** `get_format(level: str) -> str`
### ***def*** `init_log(config: dict) -> None`
在语言加载完成后执行
Returns:
### ***var*** `show_icon = config.get('log_icon', True)`

271
docs/dev/api/mkdoc.md Normal file
View File

@ -0,0 +1,271 @@
---
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: 目标路径
### ***def*** `write_to_files(file_data: dict[str, str]) -> None`
输出文件
Args:
file_data: 文件数据 相对路径
### ***def*** `get_file_list(module_folder: str) -> None`
### ***def*** `get_module_info_normal(file_path: str, ignore_private: bool) -> ModuleInfo`
获取函数和类
Args:
file_path: Python 文件路径
ignore_private: 忽略私有函数和类
Returns:
模块信息
### ***def*** `generate_markdown(module_info: ModuleInfo, front_matter: Any) -> str`
生成模块的Markdown
你可在此自定义生成的Markdown格式
Args:
module_info: 模块信息
front_matter: 自定义选项title, index, icon, category
Returns:
Markdown 字符串
### ***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: 忽略的路径
### ***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))`
### ***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`

View File

@ -0,0 +1,15 @@
---
title: liteyuki.plugin
index: true
icon: laptop-code
category: API
---
### ***def*** `get_loaded_plugins() -> dict[str, Plugin]`
获取已加载的插件
Returns:
dict[str, Plugin]: 插件字典

103
docs/dev/api/plugin/load.md Normal file
View File

@ -0,0 +1,103 @@
---
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)`
### ***def*** `load_plugins() -> set[Plugin]`
导入文件夹下多个插件
参数:
plugin_dir: 文件夹路径
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
### ***def*** `format_display_name(display_name: str, plugin_type: PluginType) -> str`
设置插件名称颜色,根据不同类型插件设置颜色
Args:
display_name: 插件名称
plugin_type: 插件类型
Returns:
str: 设置后的插件名称 <y>name</y>
### ***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)`

View File

@ -0,0 +1,7 @@
---
title: liteyuki.plugin.manager
order: 1
icon: laptop-code
category: API
---

View File

@ -0,0 +1,89 @@
---
title: liteyuki.plugin.model
order: 1
icon: laptop-code
category: API
---
### ***class*** `PluginType(Enum)`
插件类型枚举值
### &emsp; ***attr*** `APPLICATION: 'application'`
### &emsp; ***attr*** `SERVICE: 'service'`
### &emsp; ***attr*** `IMPLEMENTATION: 'implementation'`
### &emsp; ***attr*** `MODULE: 'module'`
### &emsp; ***attr*** `UNCLASSIFIED: 'unclassified'`
### ***class*** `PluginMetadata(BaseModel)`
轻雪插件元数据由插件编写者提供name为必填项
Attributes:
----------
name: str
插件名称
description: str
插件描述
usage: str
插件使用方法
type: str
插件类型
author: str
插件作者
homepage: str
插件主页
extra: dict[str, Any]
额外信息
### ***class*** `Plugin(BaseModel)`
存储插件信息
### &emsp; ***attr*** `model_config: {'arbitrary_types_allowed': True}`
### ***var*** `APPLICATION = 'application'`
### ***var*** `SERVICE = 'service'`
### ***var*** `IMPLEMENTATION = 'implementation'`
### ***var*** `MODULE = 'module'`
### ***var*** `UNCLASSIFIED = 'unclassified'`
### ***var*** `model_config = {'arbitrary_types_allowed': True}`

79
docs/dev/api/utils.md Normal file
View File

@ -0,0 +1,79 @@
---
title: liteyuki.utils
order: 1
icon: laptop-code
category: API
---
### ***def*** `is_coroutine_callable(call: Callable[..., Any]) -> bool`
判断是否为协程可调用对象
Args:
call: 可调用对象
Returns:
bool: 是否为协程可调用对象
### ***def*** `run_coroutine() -> None`
运行协程
Args:
coro:
Returns:
### ***def*** `path_to_module_name(path: Path) -> str`
转换路径为模块名
Args:
path: 路径a/b/c/d -> a.b.c.d
Returns:
str: 模块名
### ***def*** `async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]`
异步包装器
Args:
func: Sync Callable
Returns:
Coroutine: Asynchronous Callable
### ***async def*** `wrapper() -> None`
### ***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()`

View File

@ -20,7 +20,6 @@ from liteyuki.log import (
logger logger
) )
__all__ = [ __all__ = [
"LiteyukiBot", "LiteyukiBot",
"get_bot", "get_bot",
@ -34,6 +33,8 @@ __all__ = [
"logger", "logger",
] ]
__version__ = "6.3.7" # 测试版本号 __version__ = "6.3.8" # 测试版本号
# 6.3.8
# 1. 初步添加对聊天的支持
# 2. 优化了通道的性能

View File

@ -10,6 +10,7 @@ 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

View File

@ -42,7 +42,7 @@ class Lifespan:
self._after_nonebot_init_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: def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
""" """
运行函数 运行函数
Args: Args:
@ -149,7 +149,7 @@ class Lifespan:
Returns: Returns:
""" """
logger.debug("Running before_start functions") logger.debug("Running before_start functions")
self._run_funcs(self._before_start_funcs) self.run_funcs(self._before_start_funcs)
def after_start(self) -> None: def after_start(self) -> None:
""" """
@ -157,7 +157,7 @@ class Lifespan:
Returns: Returns:
""" """
logger.debug("Running after_start functions") logger.debug("Running after_start functions")
self._run_funcs(self._after_start_funcs) self.run_funcs(self._after_start_funcs)
def before_process_shutdown(self) -> None: def before_process_shutdown(self) -> None:
""" """
@ -165,7 +165,7 @@ class Lifespan:
Returns: Returns:
""" """
logger.debug("Running before_shutdown functions") logger.debug("Running before_shutdown functions")
self._run_funcs(self._before_process_shutdown_funcs) self.run_funcs(self._before_process_shutdown_funcs)
def after_shutdown(self) -> None: def after_shutdown(self) -> None:
""" """
@ -173,7 +173,7 @@ class Lifespan:
Returns: Returns:
""" """
logger.debug("Running after_shutdown functions") logger.debug("Running after_shutdown functions")
self._run_funcs(self._after_shutdown_funcs) self.run_funcs(self._after_shutdown_funcs)
def before_process_restart(self) -> None: def before_process_restart(self) -> None:
""" """
@ -181,7 +181,7 @@ class Lifespan:
Returns: Returns:
""" """
logger.debug("Running before_restart functions") logger.debug("Running before_restart functions")
self._run_funcs(self._before_process_restart_funcs) self.run_funcs(self._before_process_restart_funcs)
def after_restart(self) -> None: def after_restart(self) -> None:
""" """
@ -190,4 +190,4 @@ class Lifespan:
""" """
logger.debug("Running after_restart functions") logger.debug("Running after_restart functions")
self._run_funcs(self._after_restart_funcs) self.run_funcs(self._after_restart_funcs)

View File

@ -3,8 +3,8 @@
该模块用于轻雪主进程和Nonebot子进程之间的通信 该模块用于轻雪主进程和Nonebot子进程之间的通信
依赖关系 依赖关系
event -> _ event -> _
storage -> channel storage -> channel_
rpc -> channel, storage rpc -> channel_, storage
""" """
from liteyuki.comm.channel import ( from liteyuki.comm.channel import (
Channel, Channel,

View File

@ -5,7 +5,7 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/7/26 下午11:21 @Time : 2024/7/26 下午11:21
@Author : snowykami @Author : snowykami
@Email : snowykami@outlook.com @Email : snowykami@outlook.com
@File : channel.py @File : channel_.py
@Software: PyCharm @Software: PyCharm
本模块定义了一个通用的通道类,用于进程间通信 本模块定义了一个通用的通道类,用于进程间通信
@ -38,11 +38,12 @@ class Channel(Generic[T]):
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器 有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
""" """
def __init__(self, _id: str, type_check: bool = False): def __init__(self, _id: str, type_check: Optional[bool] = None):
""" """
初始化通道 初始化通道
Args: Args:
_id: 通道ID _id: 通道ID
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
""" """
self.conn_send, self.conn_recv = Pipe() self.conn_send, self.conn_recv = Pipe()
self._closed = False self._closed = False
@ -53,7 +54,11 @@ class Channel(Generic[T]):
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
if type_check: if type_check is None:
# 若传入泛型则默认开启类型检查
type_check = self._get_generic_type() is not None
elif type_check:
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
@ -110,7 +115,7 @@ class Channel(Generic[T]):
raise TypeError(f"Data must be an instance of {_type}, {type(data)} found") raise TypeError(f"Data must be an instance of {_type}, {type(data)} found")
if self._closed: if self._closed:
raise RuntimeError("Cannot send to a closed channel") raise RuntimeError("Cannot send to a closed channel_")
self.conn_send.send(data) self.conn_send.send(data)
def receive(self) -> T: def receive(self) -> T:
@ -119,7 +124,7 @@ class Channel(Generic[T]):
Args: Args:
""" """
if self._closed: if self._closed:
raise RuntimeError("Cannot receive from a closed channel") raise RuntimeError("Cannot receive from a closed channel_")
while True: while True:
data = self.conn_recv.recv() data = self.conn_recv.recv()
@ -226,10 +231,12 @@ class Channel(Generic[T]):
"""子进程可用的主动和被动通道""" """子进程可用的主动和被动通道"""
active_channel: Optional["Channel"] = None active_channel: Optional["Channel"] = None
passive_channel: Optional["Channel"] = None passive_channel: Optional["Channel"] = None
publish_channel: Channel[tuple[str, dict[str, Any]]] = Channel(_id="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(_id="channel_deliver_active_channel")
channel_deliver_passive_channel = Channel(_id="channel_deliver_passive_channel") channel_deliver_passive_channel = Channel(_id="channel_deliver_passive_channel")
@ -237,7 +244,7 @@ if IS_MAIN_PROCESS:
@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")
def on_set_channel(data: tuple[str, dict[str, Any]]): def on_set_channel(data: tuple[str, dict[str, Any]]):
name, channel = data[1]["name"], data[1]["channel"] name, channel = data[1]["name"], data[1]["channel_"]
set_channel(name, channel) set_channel(name, channel)
@ -261,7 +268,7 @@ def set_channel(name: str, channel: Channel):
channel: 通道实例 channel: 通道实例
""" """
if not isinstance(channel, Channel): if not isinstance(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:
_channel[name] = channel _channel[name] = channel
@ -271,7 +278,7 @@ def set_channel(name: str, channel: Channel):
( (
"set_channel", { "set_channel", {
"name" : name, "name" : name,
"channel": channel, "channel_": channel,
} }
) )
) )

View File

@ -4,14 +4,20 @@
""" """
import threading import threading
from typing import Any, Optional from typing import Any, Coroutine, Optional, TypeAlias, Callable
from liteyuki.comm.channel import Channel from liteyuki.comm import channel
from liteyuki.utils import IS_MAIN_PROCESS from liteyuki.comm.channel import Channel, ON_RECEIVE_FUNC, ASYNC_ON_RECEIVE_FUNC
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable, run_coroutine
if IS_MAIN_PROCESS: if IS_MAIN_PROCESS:
_locks = {} _locks = {}
_on_main_subscriber_receive_funcs: dict[str, list[ASYNC_ON_RECEIVE_FUNC]] = {} # type: ignore
"""主进程订阅者接收函数"""
_on_sub_subscriber_receive_funcs: dict[str, list[ASYNC_ON_RECEIVE_FUNC]] = {} # type: ignore
"""子进程订阅者接收函数"""
def _get_lock(key) -> threading.Lock: def _get_lock(key) -> threading.Lock:
""" """
@ -25,12 +31,28 @@ 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]]]](_id="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]]]](_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: def set(self, key: str, value: Any) -> None:
""" """
设置键值对 设置键值对
@ -134,6 +156,94 @@ class KeyValueStore:
) )
return recv_chan.receive() return recv_chan.receive()
def publish(self, channel_: str, data: Any) -> None:
"""
发布消息
Args:
channel_: 频道
data: 数据
Returns:
"""
self.active_chan.send(
(
"publish",
{
"channel": channel_,
"data" : data
}
)
)
def on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
"""
订阅者接收消息时的回调
Args:
channel_: 频道
Returns:
装饰器
"""
if 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):
"""
运行订阅者接收函数
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_]])
else:
if 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 _start_receive_loop(self):
"""
启动发布订阅接收器循环,在主进程中运行,若有子进程订阅则推送给子进程
"""
if IS_MAIN_PROCESS:
while True:
data = self.active_chan.receive()
if data[0] == "publish":
# 运行主进程订阅函数
self.run_subscriber_receive_funcs(data[1]["channel"], data[1]["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:
_instance = None _instance = None
@ -141,20 +251,17 @@ class GlobalKeyValueStore:
@classmethod @classmethod
def get_instance(cls): def get_instance(cls):
if IS_MAIN_PROCESS:
if cls._instance is None: if cls._instance is None:
with cls._lock: with cls._lock:
if cls._instance is None: if cls._instance is None:
cls._instance = KeyValueStore() cls._instance = KeyValueStore()
return cls._instance return cls._instance
else:
raise RuntimeError("Cannot get instance in sub process.")
shared_memory: KeyValueStore = GlobalKeyValueStore.get_instance()
# 全局单例访问点 # 全局单例访问点
if IS_MAIN_PROCESS: if IS_MAIN_PROCESS:
shared_memory: KeyValueStore = GlobalKeyValueStore.get_instance()
@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]]):
@ -182,9 +289,13 @@ 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: else:
# 子进程在入口函数中对shared_memory进行初始化 # 子进程在入口函数中对shared_memory进行初始化
shared_memory: Optional[KeyValueStore] = None # type: ignore @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:

View File

@ -13,7 +13,7 @@ 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 from liteyuki.comm.channel import Channel, get_channel, set_channels, publish_channel
from liteyuki.comm.storage import shared_memory 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
@ -42,12 +42,14 @@ class ChannelDeliver:
active: Channel[Any], active: Channel[Any],
passive: Channel[Any], passive: Channel[Any],
channel_deliver_active: Channel[Channel[Any]], channel_deliver_active: Channel[Channel[Any]],
channel_deliver_passive: Channel[tuple[str, dict]] channel_deliver_passive: Channel[tuple[str, dict]],
publish: Channel[tuple[str, Any]],
): ):
self.active = active self.active = active
self.passive = passive self.passive = passive
self.channel_deliver_active = channel_deliver_active self.channel_deliver_active = channel_deliver_active
self.channel_deliver_passive = channel_deliver_passive self.channel_deliver_passive = channel_deliver_passive
self.publish = publish
# 函数处理一些跨进程通道的 # 函数处理一些跨进程通道的
@ -64,6 +66,7 @@ def _delivery_channel_wrapper(func: TARGET_FUNC, cd: ChannelDeliver, sm: "KeyVal
channel.passive_channel = cd.passive # 子进程被动通道 channel.passive_channel = cd.passive # 子进程被动通道
channel.channel_deliver_active_channel = cd.channel_deliver_active # 子进程通道传递主动通道 channel.channel_deliver_active_channel = cd.channel_deliver_active # 子进程通道传递主动通道
channel.channel_deliver_passive_channel = cd.channel_deliver_passive # 子进程通道传递被动通道 channel.channel_deliver_passive_channel = cd.channel_deliver_passive # 子进程通道传递被动通道
channel.publish_channel = cd.publish # 子进程发布通道
# 给子进程创建共享内存实例 # 给子进程创建共享内存实例
from liteyuki.comm import storage from liteyuki.comm import storage
@ -148,7 +151,8 @@ class ProcessManager:
active=chan_active, active=chan_active,
passive=chan_passive, passive=chan_passive,
channel_deliver_active=channel_deliver_active_channel, channel_deliver_active=channel_deliver_active_channel,
channel_deliver_passive=channel_deliver_passive_channel channel_deliver_passive=channel_deliver_passive_channel,
publish=publish_channel
) )
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)

View File

@ -10,7 +10,9 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
""" """
import sys import sys
from loguru import logger import loguru
logger = loguru.logger
# DEBUG日志格式 # DEBUG日志格式
debug_format: str = ( debug_format: str = (

View File

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

56
liteyuki/message/event.py Normal file
View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/19 下午10:47
@Author : snowykami
@Email : snowykami@outlook.com
@File : event.py
@Software: PyCharm
"""
from typing import Any
from liteyuki.comm.storage import shared_memory
class Event:
def __init__(self, type: str, data: dict[str, Any], bot_id: str, session_id: str, session_type: str, receive_channel: str = "event_to_nonebot"):
"""
事件
Args:
type: 类型
data: 数据
bot_id: 机器人ID
session_id: 会话ID
session_type: 会话类型
receive_channel: 接收频道
"""
self.type = type
self.data = data
self.bot_id = bot_id
self.session_id = session_id
self.session_type = session_type
self.receive_channel = receive_channel
def __str__(self):
return f"Event(type={self.type}, data={self.data}, bot_id={self.bot_id}, session_id={self.session_id}, session_type={self.session_type})"
def reply(self, message: str | dict[str, Any]):
"""
回复消息
Args:
message:
Returns:
"""
to_nonebot_event = Event(
type=self.session_type,
data={
"message": message
},
bot_id=self.bot_id,
session_id=self.session_id,
session_type=self.session_type,
receive_channel="_"
)
print(to_nonebot_event)
shared_memory.publish(self.receive_channel, to_nonebot_event)

View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/19 下午10:51
@Author : snowykami
@Email : snowykami@outlook.com
@File : matcher.py
@Software: PyCharm
"""
import traceback
from typing import Any, TypeAlias, Callable, Coroutine
from liteyuki import Event
from liteyuki.message.rule import Rule
EventHandler: TypeAlias = Callable[[Event], Coroutine[None, None, Any]]
class Matcher:
def __init__(self, rule: Rule, priority: int, block: bool):
"""
匹配器
Args:
rule: 规则
priority: 优先级 >= 0
block: 是否阻断后续优先级更低的匹配器
"""
self.rule = rule
self.priority = priority
self.block = block
self.handlers: list[EventHandler] = []
def __str__(self):
return f"Matcher(rule={self.rule}, priority={self.priority}, block={self.block})"
def handle(self, handler: EventHandler) -> EventHandler:
"""
添加处理函数,装饰器
Args:
handler:
Returns:
EventHandler
"""
self.handlers.append(handler)
return handler
async def run(self, event: Event) -> None:
"""
运行处理函数
Args:
event:
Returns:
"""
if not await self.rule(event):
return
for handler in self.handlers:
try:
await handler(event)
except Exception:
traceback.print_exc()

47
liteyuki/message/on.py Normal file
View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/19 下午10:52
@Author : snowykami
@Email : snowykami@outlook.com
@File : on.py
@Software: PyCharm
"""
import threading
from queue import Queue
from liteyuki.comm.storage import shared_memory
from liteyuki.log import logger
from liteyuki.message.event import Event
from liteyuki.message.matcher import Matcher
from liteyuki.message.rule import Rule
_matcher_list: list[Matcher] = []
_queue: Queue = Queue()
@shared_memory.on_subscriber_receive("event_to_liteyuki")
async def _(event: Event):
current_priority = -1
for i, matcher in enumerate(_matcher_list):
logger.info(f"Running matcher {matcher} for event: {event}")
await matcher.run(event)
# 同优先级不阻断,不同优先级阻断
if current_priority != matcher.priority:
current_priority = matcher.priority
if matcher.block:
break
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

33
liteyuki/message/rule.py Normal file
View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/19 下午10:55
@Author : snowykami
@Email : snowykami@outlook.com
@File : rule.py
@Software: PyCharm
"""
from typing import Optional, TypeAlias, Callable, Coroutine
from liteyuki.message.event import Event
RuleHandler: TypeAlias = Callable[[Event], Coroutine[None, None, bool]]
"""规则函数签名"""
class Rule:
def __init__(self, handler: Optional[RuleHandler] = None):
self.handler = handler
def __or__(self, other: "Rule") -> "Rule":
return Rule(lambda event: self.handler(event) or other.handler(event))
def __and__(self, other: "Rule") -> "Rule":
return Rule(lambda event: self.handler(event) and other.handler(event))
async def __call__(self, event: Event) -> bool:
if self.handler is None:
return True
return await self.handler(event)

View File

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

343
liteyuki/mkdoc.py Normal file
View File

@ -0,0 +1,343 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/19 上午6:23
@Author : snowykami
@Email : snowykami@outlook.com
@File : mkdoc.py
@Software: PyCharm
"""
import ast
import os
import shutil
from typing import Any
from enum import Enum
from pydantic import BaseModel
NO_TYPE_ANY = "Any"
NO_TYPE_HINT = "NoTypeHint"
class DefType(Enum):
FUNCTION = "function"
METHOD = "method"
STATIC_METHOD = "staticmethod"
CLASS_METHOD = "classmethod"
PROPERTY = "property"
class FunctionInfo(BaseModel):
name: str
args: list[tuple[str, str]]
return_type: str
docstring: str
type: DefType
"""若为类中def则有"""
is_async: bool
class AttributeInfo(BaseModel):
name: str
type: str
value: Any = None
docstring: str = ""
class ClassInfo(BaseModel):
name: str
docstring: str
methods: list[FunctionInfo]
attributes: list[AttributeInfo]
inherit: list[str]
class ModuleInfo(BaseModel):
module_path: str
"""点分割模块路径 例如 liteyuki.bot"""
functions: list[FunctionInfo]
classes: list[ClassInfo]
attributes: list[AttributeInfo]
docstring: str
def get_relative_path(base_path: str, target_path: str) -> str:
"""
获取相对路径
Args:
base_path: 基础路径
target_path: 目标路径
"""
return os.path.relpath(target_path, base_path)
def write_to_files(file_data: dict[str, str]):
"""
输出文件
Args:
file_data: 文件数据 相对路径
"""
for rp, data in file_data.items():
if not os.path.exists(os.path.dirname(rp)):
os.makedirs(os.path.dirname(rp))
with open(rp, 'w', encoding='utf-8') as f:
f.write(data)
def get_file_list(module_folder: str):
file_list = []
for root, dirs, files in os.walk(module_folder):
for file in files:
if file.endswith((".py", ".pyi")):
file_list.append(os.path.join(root, file))
return file_list
def get_module_info_normal(file_path: str, ignore_private: bool = True) -> ModuleInfo:
"""
获取函数和类
Args:
file_path: Python 文件路径
ignore_private: 忽略私有函数和类
Returns:
模块信息
"""
with open(file_path, 'r', encoding='utf-8') as file:
file_content = file.read()
tree = ast.parse(file_content)
dot_sep_module_path = file_path.replace(os.sep, '.').replace(".py", "").replace(".pyi", "")
module_docstring = ast.get_docstring(tree)
module_info = ModuleInfo(
module_path=dot_sep_module_path,
functions=[],
classes=[],
attributes=[],
docstring=module_docstring if module_docstring else ""
)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
# 模块函数 且不在类中 若ignore_private=True则忽略私有函数
if not any(isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(node)) and (not ignore_private or not node.name.startswith('_')):
# 判断第一个参数是否为self或cls后期用其他办法优化
if node.args.args:
first_arg = node.args.args[0]
if first_arg.arg in ("self", "cls"):
continue
function_docstring = ast.get_docstring(node)
func_info = FunctionInfo(
name=node.name,
args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args],
return_type=ast.unparse(node.returns) if node.returns else "None",
docstring=function_docstring if function_docstring else "",
type=DefType.FUNCTION,
is_async=isinstance(node, ast.AsyncFunctionDef)
)
module_info.functions.append(func_info)
elif isinstance(node, ast.ClassDef):
# 模块类
class_docstring = ast.get_docstring(node)
class_info = ClassInfo(
name=node.name,
docstring=class_docstring if class_docstring else "",
methods=[],
attributes=[],
inherit=[ast.unparse(base) for base in node.bases]
)
for class_node in node.body:
# methods [instance, static, class property]保留__init__方法
if isinstance(class_node, ast.FunctionDef) and (not ignore_private or not class_node.name.startswith('_') or class_node.name == "__init__"):
method_docstring = ast.get_docstring(class_node)
def_type = DefType.METHOD
if class_node.decorator_list:
if any(isinstance(decorator, ast.Name) and decorator.id == "staticmethod" for decorator in class_node.decorator_list):
def_type = DefType.STATIC_METHOD
elif any(isinstance(decorator, ast.Name) and decorator.id == "classmethod" for decorator in class_node.decorator_list):
def_type = DefType.CLASS_METHOD
elif any(isinstance(decorator, ast.Name) and decorator.id == "property" for decorator in class_node.decorator_list):
def_type = DefType.PROPERTY
class_info.methods.append(FunctionInfo(
name=class_node.name,
args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in class_node.args.args],
return_type=ast.unparse(class_node.returns) if class_node.returns else "None",
docstring=method_docstring if method_docstring else "",
type=def_type,
is_async=isinstance(class_node, ast.AsyncFunctionDef)
))
# attributes
elif isinstance(class_node, ast.Assign):
for target in class_node.targets:
if isinstance(target, ast.Name):
class_info.attributes.append(AttributeInfo(
name=target.id,
type=ast.unparse(class_node.value)
))
module_info.classes.append(class_info)
elif isinstance(node, ast.Assign):
# 检查是否在类或函数中
if not any(isinstance(parent, (ast.ClassDef, ast.FunctionDef)) for parent in ast.iter_child_nodes(node)):
# 模块属性变量
for target in node.targets:
if isinstance(target, ast.Name) and (not ignore_private or not target.id.startswith('_')):
attr_type = NO_TYPE_HINT
if isinstance(node.value, ast.AnnAssign) and node.value.annotation:
attr_type = ast.unparse(node.value.annotation)
module_info.attributes.append(AttributeInfo(
name=target.id,
type=attr_type,
value=ast.unparse(node.value) if node.value else None
))
return module_info
def generate_markdown(module_info: ModuleInfo, front_matter=None) -> 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"
# 类
for cls in module_info.classes:
if cls.inherit:
inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ""
content += f"### ***class*** `{cls.name}{inherit}`\n\n"
else:
content += f"### ***class*** `{cls.name}`\n\n"
cls.docstring = cls.docstring.replace("\n", "\n\n")
content += f"{cls.docstring}\n\n"
for method in cls.methods:
# 类函数
if method.type != DefType.METHOD:
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] else arg[0] for arg in method.args]
content += f"### &emsp; ***@{method.type.value}***\n"
else:
# self不加类型提示
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] and arg[0] != "self" else arg[0] for arg in method.args]
content += f"### &emsp; ***{'async ' if method.is_async else ''}def*** `{method.name}({', '.join(args_with_type)}) -> {method.return_type}`\n\n"
method.docstring = method.docstring.replace("\n", "\n\n")
content += f"&emsp;{method.docstring}\n\n"
for attr in cls.attributes:
content += f"### &emsp; ***attr*** `{attr.name}: {attr.type}`\n\n"
# 模块属性
for attr in module_info.attributes:
if attr.type == NO_TYPE_HINT:
content += f"### ***var*** `{attr.name} = {attr.value}`\n\n"
else:
content += f"### ***var*** `{attr.name}: {attr.type} = {attr.value}`\n\n"
attr.docstring = attr.docstring.replace("\n", "\n\n")
content += f"{attr.docstring}\n\n"
return content
def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, 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) # 去头路径
# markdown相对路径
rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path
for rk, rv in replace_data.items():
rel_md_path = rel_md_path.replace(rk, rv)
abs_md_path = os.path.join(output_dir, rel_md_path)
# 获取模块信息
module_info = get_module_info_normal(pyfile_path)
# 生成markdown
if "README" in abs_md_path:
front_matter = {
"title" : module_info.module_path.replace(".__init__", "").replace("_", "\\n"),
"index" : "true",
"icon" : "laptop-code",
"category": "API"
}
else:
front_matter = {
"title" : module_info.module_path.replace("_", "\\n"),
"order" : "1",
"icon" : "laptop-code",
"category": "API"
}
md_content = generate_markdown(module_info, front_matter)
print(f"Generate {pyfile_path} -> {abs_md_path}")
file_data[abs_md_path] = md_content
write_to_files(file_data)
# 入口脚本
if __name__ == '__main__':
# 这里填入你的模块路径
generate_docs('liteyuki', 'docs/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"])
generate_docs('liteyuki', 'docs/en/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"])

View File

@ -117,7 +117,7 @@ def format_display_name(display_name: str, plugin_type: PluginType) -> str:
match plugin_type: match plugin_type:
case PluginType.APPLICATION: case PluginType.APPLICATION:
color = "m" color = "m"
case PluginType.IMPLEMENTATION: case PluginType.TEST:
color = "g" color = "g"
case PluginType.MODULE: case PluginType.MODULE:
color = "e" color = "e"

View File

@ -25,15 +25,15 @@ class PluginType(Enum):
SERVICE = "service" SERVICE = "service"
"""服务端例如AI绘画后端""" """服务端例如AI绘画后端"""
IMPLEMENTATION = "implementation"
"""实现端:例如与聊天平台的协议实现"""
MODULE = "module" MODULE = "module"
"""模块:导出对象给其他插件使用""" """模块:导出对象给其他插件使用"""
UNCLASSIFIED = "unclassified" UNCLASSIFIED = "unclassified"
"""未分类:默认值""" """未分类:默认值"""
TEST = "test"
"""测试:测试插件"""
class PluginMetadata(BaseModel): class PluginMetadata(BaseModel):
""" """

View File

@ -1,20 +1,21 @@
import base64
import time import time
from typing import Any, AnyStr from typing import AnyStr
import time
from typing import AnyStr
import nonebot import nonebot
import pip import pip
from nonebot import Bot, get_driver, require from nonebot import get_driver, require
from nonebot.adapters import onebot, satori from nonebot.adapters import onebot, satori
from nonebot.adapters.onebot.v11 import Message, unescape from nonebot.adapters.onebot.v11 import Message, unescape
from nonebot.exception import MockApiException
from nonebot.internal.matcher import Matcher from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
# from src.liteyuki.core import Reloader # from src.liteyuki.core import Reloader
from src.utils import event as event_utils, satori_utils from src.utils import event as event_utils, satori_utils
from src.utils.base.config import get_config, load_from_yaml from src.utils.base.config import get_config
from src.utils.base.data_manager import StoredConfig, TempConfig, common_db from src.utils.base.data_manager import TempConfig, common_db
from src.utils.base.language import get_user_lang from src.utils.base.language import get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
@ -24,13 +25,11 @@ from ..utils.base.ly_function import get_function
require("nonebot_plugin_alconna") require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler") require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand, Arparma, MultiVar from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma, MultiVar
from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_apscheduler import scheduler
driver = get_driver() driver = get_driver()
markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).config.get("markdown_image", False)
@on_alconna( @on_alconna(
command=Alconna( command=Alconna(
@ -96,90 +95,6 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
reload() reload()
@on_alconna(
aliases={"配置"},
command=Alconna(
"config",
Subcommand(
"set",
Args["key", str]["value", Any],
alias=["设置"],
),
Subcommand(
"get",
Args["key", str, None],
alias=["查询", "获取"]
),
Subcommand(
"remove",
Args["key", str],
alias=["删除"]
)
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, matcher: Matcher):
ulang = get_user_lang(str(event_utils.get_user_id(event)))
stored_config: StoredConfig = common_db.where_one(StoredConfig(), default=StoredConfig())
if result.subcommands.get("set"):
key, value = result.subcommands.get("set").args.get("key"), result.subcommands.get("set").args.get("value")
try:
value = eval(value)
except:
pass
stored_config.config[key] = value
common_db.save(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_set_success', KEY=key, VAL=value)}")
elif result.subcommands.get("get"):
key = result.subcommands.get("get").args.get("key")
file_config = load_from_yaml("config.yml")
reply = f"{ulang.get('liteyuki.current_config')}"
if key:
reply += f"```dotenv\n{key}={file_config.get(key, stored_config.config.get(key))}\n```"
else:
reply = f"{ulang.get('liteyuki.current_config')}"
reply += f"\n{ulang.get('liteyuki.static_config')}\n```dotenv"
for k, v in file_config.items():
reply += f"\n{k}={v}"
reply += "\n```"
if len(stored_config.config) > 0:
reply += f"\n{ulang.get('liteyuki.stored_config')}\n```dotenv"
for k, v in stored_config.config.items():
reply += f"\n{k}={v} {type(v)}"
reply += "\n```"
await md.send_md(reply, bot, event=event)
elif result.subcommands.get("remove"):
key = result.subcommands.get("remove").args.get("key")
if key in stored_config.config:
stored_config.config.pop(key)
common_db.save(stored_config)
await matcher.finish(f"{ulang.get('liteyuki.config_remove_success', KEY=key)}")
else:
await matcher.finish(f"{ulang.get('liteyuki.invalid_command', TEXT=key)}")
@on_alconna(
aliases={"切换图片模式"},
command=Alconna(
"switch-image-mode"
),
permission=SUPERUSER
).handle()
# Satori OK
async def _(event: T_MessageEvent, matcher: Matcher):
global markdown_image
# 切换图片模式False以图片形式发送True以markdown形式发送
ulang = get_user_lang(str(event_utils.get_user_id(event)))
stored_config: StoredConfig = common_db.where_one(StoredConfig(), default=StoredConfig())
stored_config.config["markdown_image"] = not stored_config.config.get("markdown_image", False)
markdown_image = stored_config.config["markdown_image"]
common_db.save(stored_config)
await matcher.finish(
ulang.get("liteyuki.image_mode_on" if stored_config.config["markdown_image"] else "liteyuki.image_mode_off"))
@on_alconna( @on_alconna(
command=Alconna( command=Alconna(
"liteyuki-docs", "liteyuki-docs",
@ -285,38 +200,6 @@ async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher
await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}") await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
# system hook
@Bot.on_calling_api # 图片模式检测
async def test_for_md_image(bot: T_Bot, api: str, data: dict):
# 截获大图发送转换为markdown发送
if api in ["send_msg", "send_private_msg", "send_group_msg"] and markdown_image and data.get(
"user_id") != bot.self_id:
if api == "send_msg" and data.get("message_type") == "private" or api == "send_private_msg":
session_type = "private"
session_id = data.get("user_id")
elif api == "send_msg" and data.get("message_type") == "group" or api == "send_group_msg":
session_type = "group"
session_id = data.get("group_id")
else:
return
if len(data.get("message", [])) == 1 and data["message"][0].get("type") == "image":
file: str = data["message"][0].data.get("file")
# file:// http:// base64://
if file.startswith("http"):
result = await md.send_md(await md.image_async(file), bot, message_type=session_type,
session_id=session_id)
elif file.startswith("file"):
file = file.replace("file://", "")
result = await md.send_image(open(file, "rb").read(), bot, message_type=session_type,
session_id=session_id)
elif file.startswith("base64"):
file_bytes = base64.b64decode(file.replace("base64://", ""))
result = await md.send_image(file_bytes, bot, message_type=session_type, session_id=session_id)
else:
return
raise MockApiException(result=result)
@driver.on_startup @driver.on_startup
async def on_startup(): async def on_startup():
temp_data = common_db.where_one(TempConfig(), default=TempConfig()) temp_data = common_db.where_one(TempConfig(), default=TempConfig())

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/20 上午5:12
@Author : snowykami
@Email : snowykami@outlook.com
@File : liteyuki_reply.py
@Software: PyCharm
"""
from liteyuki.plugin import PluginMetadata, PluginType
from liteyuki.message.on import on_message
from liteyuki.message.event import Event
__plugin_meta__ = PluginMetadata(
name="你好轻雪",
type=PluginType.TEST
)
@on_message().handle
async def _(event: Event):
if str(event.data["raw_message"]) == "你好轻雪":
event.reply("你好呀")

View File

@ -69,6 +69,8 @@ async def get_stat_msg_image(
condition, condition,
*condition_args *condition_args
) )
if not msg_rows:
msg_rows = []
timestamps = [] timestamps = []
msg_count = [] msg_count = []
msg_rows.sort(key=lambda x: x.time) msg_rows.sort(key=lambda x: x.time)

View File

@ -98,8 +98,10 @@ async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
bot_id = result.other_args.get("bot_id") bot_id = result.other_args.get("bot_id")
user_id = result.other_args.get("user_id") user_id = result.other_args.get("user_id")
if group_id in ["current", "c"]: if group_id in ["current", "c"] and hasattr(event, "group_id"):
group_id = str(event_utils.get_group_id(event)) group_id = str(event_utils.get_group_id(event))
else:
group_id = "all"
if group_id in ["all", "a"]: if group_id in ["all", "a"]:
group_id = "all" group_id = "all"

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/19 下午10:30
@Author : snowykami
@Email : snowykami@outlook.com
@File : __init__.py.py
@Software: PyCharm
"""
from nonebot import require
from liteyuki.comm.storage import shared_memory
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import UniMessage, Command, on_alconna

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/20 上午5:10
@Author : snowykami
@Email : snowykami@outlook.com
@File : to_liteyuki.py
@Software: PyCharm
"""
from nonebot import Bot, get_bot, on_message
from nonebot.plugin import PluginMetadata
from nonebot.adapters.onebot.v11 import MessageEvent, Bot
from liteyuki.comm.storage import shared_memory
from liteyuki.message.event import Event
__plugin_meta__ = PluginMetadata(
name="轻雪物流",
description="把消息事件传递给轻雪框架进行处理",
usage="用户无需使用",
)
@on_message().handle()
async def _(bot: Bot, event: MessageEvent):
liteyuki_event = Event(
type=event.message_type,
data=event.dict(),
bot_id=bot.self_id,
session_id=str(event.user_id if event.message_type == "private" else event.group_id),
session_type=event.message_type,
receive_channel="event_to_nonebot"
)
shared_memory.publish("event_to_liteyuki", liteyuki_event)
@shared_memory.on_subscriber_receive("event_to_nonebot")
async def _(event: Event):
bot: Bot = get_bot(event.bot_id)
await bot.send_msg(message_type=event.type, user_id=int(event.session_id), group_id=int(event.session_id), message=event.data["message"])

View File

@ -28,7 +28,7 @@ class Database:
os.makedirs(os.path.dirname(db_name)) os.makedirs(os.path.dirname(db_name))
self.db_name = db_name self.db_name = db_name
self.conn = sqlite3.connect(db_name) self.conn = sqlite3.connect(db_name, check_same_thread=False)
self.cursor = self.conn.cursor() self.cursor = self.conn.cursor()
self._on_save_callbacks = [] self._on_save_callbacks = []
@ -105,7 +105,7 @@ class Database:
return [model_type(**self._load(dict(zip(fields, result)))) for result in results] return [model_type(**self._load(dict(zip(fields, result)))) for result in results]
def save(self, *args: LiteModel): def save(self, *args: LiteModel):
"""增/改操作 self.returns_ = """增/改操作
Args: Args:
*args: *args:
Returns: Returns:

View File

@ -65,7 +65,7 @@ def auto_migrate():
user_db.auto_migrate(User()) user_db.auto_migrate(User())
group_db.auto_migrate(Group()) group_db.auto_migrate(Group())
plugin_db.auto_migrate(InstalledPlugin(), GlobalPlugin()) plugin_db.auto_migrate(InstalledPlugin(), GlobalPlugin())
common_db.auto_migrate(GlobalPlugin(), StoredConfig(), TempConfig()) common_db.auto_migrate(GlobalPlugin(), TempConfig())
auto_migrate() auto_migrate()

View File

@ -1,6 +1,6 @@
from nonebot.adapters import satori from nonebot.adapters import satori
from nonebot.adapters import onebot
from src.utils.base.ly_typing import T_MessageEvent from src.utils.base.ly_typing import T_MessageEvent, T_GroupMessageEvent
def get_user_id(event: T_MessageEvent): def get_user_id(event: T_MessageEvent):
@ -10,11 +10,13 @@ def get_user_id(event: T_MessageEvent):
return event.user_id return event.user_id
def get_group_id(event: T_MessageEvent): def get_group_id(event: T_GroupMessageEvent):
if isinstance(event, satori.event.Event): if isinstance(event, satori.event.Event):
return event.guild.id return event.guild.id
else: elif isinstance(event, onebot.v11.GroupMessageEvent):
return event.group_id return event.group_id
else:
return None
def get_message_type(event: T_MessageEvent) -> str: def get_message_type(event: T_MessageEvent) -> str: