118 Commits
beta ... v6.3.9

Author SHA1 Message Date
4bf8512a7d 新增on_keywords 2024-08-22 09:35:02 +08:00
a3a31a2c94 🔥 移除通道的部分特性 2024-08-22 07:23:44 +08:00
9ed4c1abb1 📝 文档新增源代码展示 2024-08-21 18:05:04 +08:00
a9c6ea0452 🐛 修复npm无法显示的问题 2024-08-21 17:59:21 +08:00
9e2bbe2e5c 🐛 修复npm无法显示的问题 2024-08-20 21:39:01 +08:00
598bff8c49 擴展event字段 2024-08-20 20:38:10 +08:00
eb7c8300fa 擴展event字段 2024-08-20 20:30:50 +08:00
287ab63091 移除测试插件 2024-08-20 06:24:00 +08:00
0c942d9806 添加对主流框架的消息io支持 2024-08-20 06:20:41 +08:00
237789e0d4 🚀 测试文档工作流 2024-08-19 23:50:15 +08:00
e656fa6a48 🚀 测试文档工作流 2024-08-19 23:49:21 +08:00
6dcb085b53 🚀 测试文档工作流 2024-08-19 23:47:39 +08:00
55a427e344 📝 修复生成文档中self多出类型注解的问题,修复__init__丢失的问题 2024-08-19 10:24:13 +08:00
43eef20b71 📝 修复生成文档中self多出类型注解的问题,修复__init__丢失的问题 2024-08-19 10:22:24 +08:00
b8fdb4146e 📝 修复生成文档中self多出类型注解的问题,修复__init__丢失的问题 2024-08-19 10:09:38 +08:00
cdbede7135 📝 修复生成文档中self多出类型注解的问题,修复__init__丢失的问题 2024-08-19 10:04:24 +08:00
85a3a9ad52 📝 生成了api文档 2024-08-19 09:55:47 +08:00
943e0c2665 修正工作流 2024-08-19 09:43:46 +08:00
fd4d680e87 修正工作流 2024-08-19 00:14:34 +08:00
0b763135c9 Merge remote-tracking branch 'origin/main' 2024-08-19 00:12:12 +08:00
832cc2ec44 修正工作流 2024-08-19 00:11:54 +08:00
3160b4be69 添加依赖版本检测 2024-08-19 00:06:24 +08:00
775596b5bf 🔥 remove melobot 2024-08-19 00:00:12 +08:00
84782a92d8 ⬆️ 更新nonebot依赖版本 2024-08-18 23:54:35 +08:00
6e817111cb 🐛 修复依赖pdm错误的问题 2024-08-18 23:52:59 +08:00
cd8d631348 📝 添加快速启动说明 2024-08-18 23:52:26 +08:00
af37e61d05 📝 修复中文文档 快速部署 路由错误 2024-08-18 23:43:48 +08:00
803b65e08e add color plugin name 2024-08-18 23:39:19 +08:00
aa9abde63a 🔖 liteyuki 6.3.6 2024-08-18 21:45:59 +08:00
9c35abc6e2 🔖 liteyuki 6.3.6 2024-08-18 21:45:11 +08:00
1af95a15aa 🔖 liteyuki 6.3.6 2024-08-18 21:29:19 +08:00
d2b693b1e0 Merge remote-tracking branch 'origin/main' 2024-08-18 21:26:29 +08:00
b05bbf2f19 🐛 Linux下创建子进程出错的问题 fork -> spawn 2024-08-18 21:26:19 +08:00
37ed3b0824 🐛 修复注册失败的问题 2024-08-18 19:58:36 +08:00
b56ec5ce38 Merge remote-tracking branch 'origin/main' 2024-08-18 11:51:18 +08:00
1fb3f6cd58 🐛 修复pdm依赖缺失 2024-08-18 11:51:07 +08:00
1dfe1a5819 🔖 发布NoneBot插件 liteyukibot-plugin-nonebot 2024-08-18 08:12:29 +08:00
5d194b8ebe 新增插件类型枚举 2024-08-18 08:06:25 +08:00
78810d2ca8 新增开发者模式,快速运行插件 2024-08-18 05:19:52 +08:00
87d4202ed3 新增开发者模式,快速运行插件 2024-08-18 05:11:10 +08:00
1d0b18291e 新增开发者模式,快速运行插件 2024-08-18 05:10:57 +08:00
16df5706ff 🚸 添加发布工作流 2024-08-18 04:40:02 +08:00
1b24157f08 🚸 添加发布工作流 2024-08-18 04:37:58 +08:00
8ba50b7bd6 🚸 添加发布工作流 2024-08-18 04:34:54 +08:00
502ccb46bb 🚸 添加发布工作流 2024-08-18 04:29:57 +08:00
fcbc410f1a 🚸 添加发布工作流 2024-08-18 04:26:50 +08:00
85a7c28a3a 🚸 添加发布工作流 2024-08-18 04:24:38 +08:00
7bd0da9316 🚸 添加发布工作流 2024-08-18 04:24:32 +08:00
212c338fcd 🚸 添加发布工作流 2024-08-18 04:22:55 +08:00
137270b886 🚸 添加发布工作流 2024-08-18 04:22:03 +08:00
079b940d8e 🚸 添加发布工作流 2024-08-18 04:18:20 +08:00
e396db67ce 🚸 添加发布工作流 2024-08-18 04:15:09 +08:00
f37b469ab9 🚸 添加发布工作流 2024-08-18 04:05:33 +08:00
0a3363ebce 🚸 添加发布工作流 2024-08-18 03:58:13 +08:00
53291822c0 🚸 添加发布工作流 2024-08-18 03:57:15 +08:00
5f5dcc7f99 🚸 添加发布工作流 2024-08-18 03:49:00 +08:00
9d27abfe04 🚸 添加发布工作流 2024-08-18 03:40:06 +08:00
4b4f030fe3 🚸 添加发布工作流 2024-08-18 03:31:29 +08:00
d5b0f947e0 🚸 添加发布工作流 2024-08-18 02:24:12 +08:00
9e6372185f Merge remote-tracking branch 'origin/main' 2024-08-18 02:20:21 +08:00
0e125f7c81 🚸 添加发布工作流 2024-08-18 02:20:09 +08:00
a3ea422ec3 支持通道通过通道在进程中传递 2024-08-18 01:25:11 +08:00
8d78e643e0 支持通道通过通道在进程中传递 2024-08-17 23:46:43 +08:00
aa9cae7008 Merge remote-tracking branch 'origin/main' 2024-08-17 19:12:22 +08:00
48085a946d 对通道类添加类型检查和泛型 2024-08-17 19:12:11 +08:00
8e27f6b9b0 对通道类添加类型检查和泛型 2024-08-17 19:10:03 +08:00
f980e77a4a 对通道类添加类型检查和泛型 2024-08-17 17:41:56 +08:00
01798f7b11 📝 fix docs router error 2024-08-17 04:50:27 +08:00
03057c8ef9 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	docs/.vuepress/public/js/zh/get_data.js
#	docs/.vuepress/theme.ts
#	docs/README.md
#	docs/en/README.md
2024-08-17 02:38:58 +08:00
ee851116d8 🐛 文档构建失败的问题 2024-08-17 02:38:24 +08:00
d367903946 📝 添加英文文档框架 2024-08-17 02:24:25 +08:00
66ade9efc6 Merge remote-tracking branch 'origin/main' 2024-08-17 01:28:08 +08:00
ff41e72378 📝 添加英文文档框架 2024-08-17 01:27:57 +08:00
169f1645a4 📝 Update dev_comm.md
📝
2024-08-17 00:39:49 +08:00
c3914b2b15 📝 添加shared_memory文档 2024-08-17 00:31:05 +08:00
b356524a9e 📝 添加shared_memory文档 2024-08-17 00:18:06 +08:00
85a13251a5 🐛 Ctrl+C 无法终止Channel接收线程的问题 2024-08-16 23:43:43 +08:00
0417805e46 🚚 移除通信测试插件 2024-08-16 22:53:54 +08:00
d3f1e35a12 📝 新增NoneBot插件 2024-08-16 21:53:12 +08:00
ff585ac7c2 新增线程安全共享内存储存器(注释) 2024-08-16 21:44:27 +08:00
1b692dd13f 新增线程安全共享内存储存器 2024-08-16 21:43:29 +08:00
dd00e6ecec Merge remote-tracking branch 'origin/main' 2024-08-16 21:38:34 +08:00
222250bc41 新增线程安全共享内存储存器 2024-08-16 21:38:22 +08:00
c36e706731 Merge pull request #64 from ElapsingDreams/main
🐛 继续修复liteyuki_status目录名过长布局问题, 💄 更新其显示布局
2024-08-16 03:00:01 +08:00
adc9b76688 🐛 修复无法重启进程的问题(未启动生命监视器) 2024-08-16 02:56:50 +08:00
2e3ea96972 🐛 继续修复liteyuki_status目录名过长布局问题, 💄 更新其显示布局 2024-08-15 23:22:25 +08:00
9b07d41f86 在结束进程时无法杀死进程的问题 2024-08-15 17:32:02 +08:00
65ad377099 Merge remote-tracking branch 'origin/main' 2024-08-15 16:40:58 +08:00
a61357f4e2 🐛 在结束进程时无法杀死进程的问题 2024-08-15 16:40:46 +08:00
60403b2a4f Merge pull request #63 from ElapsingDreams/main
🐛 尝试修复liteyuki_status插件目录过长的布局问题
2024-08-15 03:06:16 +08:00
624afa57ba 🐛 尝试修复liteyuki_status插件目录过长的布局问题 2024-08-15 02:59:03 +08:00
36a39e1ed7 Merge pull request #62 from ElapsingDreams/main
🌐 liteyuki_weather i18n (虽然仅支持三种语言)
2024-08-13 02:35:03 +08:00
4a872c3435 Merge branch 'LiteyukiStudio:main' into main 2024-08-12 22:14:25 +08:00
90b9d1af1e 🌐 liteyuki_weather i18n (虽然仅支持三种语言) 2024-08-12 22:13:59 +08:00
551ca06ea7 Merge pull request #61 from ElapsingDreams/main
🩹非关键问题的简单修复:插件服务声明
2024-08-12 20:40:22 +08:00
61c0cf2c2d Update weather_now.js 2024-08-12 20:01:29 +08:00
e801a99f67 🩹非关键问题的简单修复:插件服务声明 2024-08-12 19:13:03 +08:00
beebfe7deb Merge branch 'LiteyukiStudio:main' into main 2024-08-12 16:48:24 +08:00
32e1963d5a 🎨热修复前端显示 🙈更新.gitignore 2024-08-12 16:47:16 +08:00
facf5bedb1 Merge pull request #59 from ElapsingDreams/main
🎨🐛 修复了一个潜在的前端显示bug,优化代码结构
2024-08-12 16:37:38 +08:00
035d43fb18 🎨🐛 修复了一个潜在的前端显示bug,优化代码结构 2024-08-12 16:04:27 +08:00
0d5f9fee52 📝 尝试修复文档有些段落显示两次的问题 2024-08-12 15:47:59 +08:00
3cb03fa4dc 📝 添加轻雪插件编写的一些注释 2024-08-12 06:07:01 +08:00
47ef3f2a49 📝 添加轻雪插件编写简易文档和轻雪进程通信说明 2024-08-12 05:50:12 +08:00
8568c7bb99 新增observer类和开发调试器 2024-08-12 05:26:36 +08:00
02cf058552 Merge remote-tracking branch 'origin/main' 2024-08-12 04:46:33 +08:00
c9157f0e2c 新增observer类和开发调试器 2024-08-12 04:45:59 +08:00
83325e63ea 添加默认配置文件,支持多种及多个配置文件 2024-08-12 02:47:53 +08:00
37b8d969b1 🐛 fix 通道无法在进程内传递消息的问题 2024-08-12 02:41:53 +08:00
298bdc7b8c Merge pull request #58 from ElapsingDreams/main
Add New function and layout for liteyuki_weather
2024-08-11 21:45:30 +08:00
81a191f8ba Add New function and layout for liteyuki_weather 2024-08-11 01:40:26 +08:00
c3fc5d429b 🐛 fix 多线程占用数据库的问题 2024-08-10 23:35:32 +08:00
b08c934c78 🐛 fix 多线程占用数据库的问题 2024-08-10 23:34:34 +08:00
1b1ddbdd8d 🐛 fix 多线程占用数据库的问题 2024-08-10 22:54:00 +08:00
0d16d53cb7 🐛 fix 通道类回调函数在进程间传递时无法序列号的问题 2024-08-10 22:42:01 +08:00
6c39ed8ab5 🐛 fix 通道类回调函数在进程间传递时无法序列号的问题 2024-08-10 22:36:30 +08:00
2f8999b5ad 🐛 fix 通道类回调函数在进程间传递时无法序列号的问题 2024-08-10 22:27:42 +08:00
7107d03b72 🐛 fix 通道类回调函数在进程间传递时无法序列号的问题 2024-08-10 22:25:41 +08:00
247 changed files with 17035 additions and 6026 deletions

View File

@ -33,6 +33,18 @@ jobs:
cd docs
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: 构建文档
env:
NODE_OPTIONS: --max_old_space_size=8192

20
.github/workflows/pypi-publish.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Publish
on:
release:
types: [published]
jobs:
pypi-publish:
name: upload release to PyPI
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v3
- uses: pdm-project/setup-pdm@v3
- name: Publish package distributions to PyPI
run: pdm publish

27
.gitignore vendored
View File

@ -1,6 +1,10 @@
.venv/
.idea/
.vscode/
.cache/
venv/
node_modules/
data/
db/
@ -11,19 +15,23 @@ __pycache__/
*.pyd
*.pyw
/plugins/
#config
config/
!config/default.yml
_config.yml
config.yml
config.example.yml
compile.bat
src/resources/templates/latest-debug.html
# vuepress
.github
test.py
line_count.py
# mupy
mypy.ini
# nuitka
compile.bat
src/resources/templates/latest-debug.html
main.build/
main.dist/
main.exe
@ -36,3 +44,14 @@ prompt.txt
# js
**/echarts.js
.env
# pdm
.pdm-python
.pdm-build
dist
doc
mkdoc2.py
result.json

View File

@ -1,4 +1,4 @@
FROM python:3.11-bullseye
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.10-slim-bullseye
ENV TZ Asia/Shanghai

8
config/default.yml Normal file
View File

@ -0,0 +1,8 @@
nonebot:
host: 127.0.0.1
port: 20216
command_start: ["", "/"]
nickname: [ "liteyuki" ]
default_language: zh
driver: ~fastapi+~httpx+~websockets
alconna_use_command_start: true

View File

@ -17,4 +17,5 @@ export default defineClientConfig({
app.component("geoComp", geoComp);
// app.use(ElementPlus);
},
});
});

View File

@ -5,13 +5,29 @@ import viteBundler from "@vuepress/bundler-vite";
export default defineUserConfig({
base: "/",
lang: "zh-CN",
title: "LiteyukiBot 轻雪机器人",
description: "LiteyukiBot | 轻雪机器人 | An OneBot Standard ChatBot | 一个OneBot标准的聊天机器人",
locales: {
"/": {
// 设置正在使用的语言
lang: "zh-CN",
title: "LiteyukiBot 轻雪机器人",
description: "LiteyukiBot | 轻雪机器人 | An OneBot Standard ChatBot | 一个OneBot标准的聊天机器人",
head: [
["script", {"src": "/js/zh/get_data.js", "type": "module"}],
]
},
"/en/": {
// 设置正在使用的语言
lang: "en-US",
title: "LiteyukiBot",
description: "LiteyukiBot | An OneBot Standard ChatBot ",
head: [
["script", {"src": "/js/en/get_data.js", "type": "module"}],
]
},
},
head: [
// 设置 favor.ico.vuepress/public 下
["script", {src: "/js/style.js", "type": "module"}],
["script", {src: "/js/get_data.js", "type": "module"}],
['link', {rel: 'icon', href: 'https://cdn.liteyuki.icu/favicon.ico'},],
['link', {rel: 'stylesheet', href: 'https://cdn.bootcdn.net/ajax/libs/firacode/6.2.0/fira_code.min.css'}],

View File

@ -1,20 +0,0 @@
import {navbar} from "vuepress-theme-hope";
export default navbar([
"/",
{
text: "安装及部署",
link: "/deployment/",
prefix: "deployment/",
},
{
text: "使用及开发",
link: "/usage/",
prefix: "usage/",
},
{
text: "资源及插件",
link: "/store/resource",
prefix: "store/",
}
]);

View File

@ -0,0 +1,25 @@
import {navbar} from "vuepress-theme-hope";
export const enNavbarConfig = navbar([
"/en/",
{
text: "Deploy",
link: "/en/deploy/",
prefix: "deploy/",
},
{
text: "Usage",
link: "/en/usage/",
prefix: "usage/",
},
{
text: "Extensions",
link: "/en/store/",
prefix: "store/",
},
{
text: "Contribute",
link: "/en/dev/",
prefix: "dev/",
},
]);

View File

@ -0,0 +1,2 @@
export * from "./zh.js"
export * from "./en.js"

View File

@ -0,0 +1,26 @@
import {navbar} from "vuepress-theme-hope";
export const zhNavbarConfig = navbar([
"/",
{
text: "安装及部署",
link: "/deploy/",
prefix: "deploy/",
},
{
text: "使用及功能",
link: "/usage/",
prefix: "usage/",
},
{
text: "资源及插件",
link: "/store/",
prefix: "store/",
},
{
text: "开发及贡献",
link: "/dev/",
prefix: "dev/",
},
]);

View File

@ -1,11 +1,11 @@
[
{
"module_name": "nonebot_plugin_status",
"project_link": "nonebot-plugin-status",
"name": "测试",
"desc": "测试",
"module_name": "liteyukibot-plugin-nonebot",
"project_link": "liteyukibot-plugin-nonebot",
"name": "NoneBot插件",
"desc": "在轻雪中使用NoneBot为NoneBot开发者提供了更多便捷功能(已内置)",
"author": "snowykami",
"homepage": "https://github.com/nonebot/plugin-status",
"homepage": "https://github.com/LiteyukiStudio/liteyukibot-plugin-nonebot",
"tags": [
{
"label": "server",
@ -14,10 +14,7 @@
],
"is_official": true,
"type": "application",
"supported_adapters": null,
"valid": true,
"version": "0.8.1",
"time": "2024-03-04T06:57:10.250823Z",
"skip_test": false
"version": "rolling"
}
]
]

View File

@ -0,0 +1,73 @@
// 定义全局变量来存储数据
let globalTotal = 0;
let globalOnline = 0;
// 从API获取数据并更新全局变量
function fetchAndUpdateData() {
Promise.all([
fetch("https://api.liteyuki.icu/count").then(res => res.json()),
fetch("https://api.liteyuki.icu/online").then(res => res.json())
])
.then(([countRes, onlineRes]) => {
globalTotal = countRes.register;
globalOnline = onlineRes.online;
})
.catch(err => {
console.error("Error fetching data:", err);
});
}
// 更新页面显示,使用全局变量中的数据
function updatePageDisplay() {
let countInfo = document.getElementById("count-info");
if (!countInfo) {
let info = `<div id="count-info" style="text-align: center; font-size: 20px; font-weight: 500">
Instances:<span id="total">${globalTotal}</span>&nbsp;&nbsp;&nbsp;&nbsp;Online:<span id="online">${globalOnline}</span></div>`;
let mainDescription = document.querySelector("#main-description");
if (mainDescription) {
mainDescription.insertAdjacentHTML('afterend', info);
}
}
}
// 初始调用更新数据
fetchAndUpdateData();
updatePageDisplay();
// 设置定时器,分别以不同频率调用更新数据和更新页面的函数
setInterval(fetchAndUpdateData, 10000); // 每10秒更新一次数据
setInterval(updatePageDisplay, 1000); // 每1秒更新一次页面显示

View File

@ -1,39 +1,73 @@
// 定义全局变量来存储数据
let globalTotal = 0;
let globalOnline = 0;
// 从API获取数据并更新全局变量
function fetchAndUpdateData() {
Promise.all([
fetch("https://api.liteyuki.icu/count").then(res => res.json()),
fetch("https://api.liteyuki.icu/online").then(res => res.json())
])
.then(([countRes, onlineRes]) => {
globalTotal = countRes.register;
globalOnline = onlineRes.online;
})
.catch(err => {
console.error("Error fetching data:", err);
});
}
// 更新页面显示,使用全局变量中的数据
function updatePageDisplay() {
let countInfo = document.getElementById("count-info");
if (!countInfo) {
let info = `<div id="count-info" style="text-align: center; font-size: 20px; font-weight: 500">
全球实例:<span id="total">${globalTotal}</span>&nbsp;&nbsp;&nbsp;&nbsp;线:<span id="online">${globalOnline}</span></div>`;
let mainDescription = document.querySelector("#main-description");
if (mainDescription) {
mainDescription.insertAdjacentHTML('afterend', info);
}
}
}
// 初始调用更新数据
fetchAndUpdateData();
updatePageDisplay();
// 设置定时器,分别以不同频率调用更新数据和更新页面的函数
setInterval(fetchAndUpdateData, 10000); // 每10秒更新一次数据
setInterval(updatePageDisplay, 1000); // 每1秒更新一次页面显示

View File

@ -1,25 +0,0 @@
import {sidebar} from "vuepress-theme-hope";
export default sidebar({
"/": [
"",
{
text: "安装及部署",
icon: "laptop-code",
prefix: "deployment/",
children: "structure",
},
{
text: "使用及开发",
icon: "book",
prefix: "usage/",
children: "structure",
},
{
text: "资源及插件",
icon: "store",
prefix: "store/",
children: "structure",
}
],
});

View File

@ -0,0 +1,10 @@
import {sidebar} from "vuepress-theme-hope";
export const enSidebarConfig = sidebar(
{
"/en/deploy/": "structure",
"/en/usage/": "structure",
"/en/store/": "structure",
"/en/dev/": "structure",
}
)

View File

@ -0,0 +1,2 @@
export * from "./zh.js"
export * from "./en.js"

View File

@ -0,0 +1,11 @@
import {sidebar} from "vuepress-theme-hope";
export const zhSidebarConfig = sidebar(
{
"/deploy/": "structure",
"/usage/": "structure",
"/store/": "structure",
"/dev/": "structure",
}
)

View File

@ -1,14 +1,30 @@
import {hopeTheme} from "vuepress-theme-hope";
import navbar from "./navbar.js";
import sidebar from "./sidebar.js";
import {enSidebarConfig, zhSidebarConfig} from "./sidebar/index.js";
import {enNavbarConfig, zhNavbarConfig} from "./navbar/index.js";
export default hopeTheme({
hostname: "https://vuepress-theme-hope-docs-demo.netlify.app",
hotReload: true,
locales: {
"/": {
navbar: zhNavbarConfig,
sidebar: zhSidebarConfig,
author: {
name: "远野千束",
url: "https://sfkm.me",
}
},
"/en/": {
navbar: enNavbarConfig,
sidebar: enSidebarConfig,
author: {
name: "SnowyKami",
url: "https://sfkm.me",
}
}
author: {
name: "远野千束",
url: "https://sfkm.me",
},
iconAssets: "fontawesome-with-brands",
@ -19,12 +35,6 @@ export default hopeTheme({
docsDir: "docs",
// 导航栏
navbar,
// 侧边栏
sidebar,
// 页脚
footer: "LiteyukiBot",
displayFooter: true,

View File

@ -9,12 +9,12 @@ bgImageDark:
bgImageStyle:
background-attachment: fixed
heroText: LiteyukiBot
tagline: LiteyukiBot 轻雪机器人,基于NoneBot2构建的综合应用型聊天机器人
tagline: LiteyukiBot 轻雪机器人,综合性的机器人应用及管理框架
actions:
- text: 快速部署
icon: rocket
link: ./deployment/install.html
link: ./deploy/install.html
type: primary
- text: 使用手册
@ -30,9 +30,9 @@ highlights:
background-repeat: repeat
background-size: initial
features:
- title: 基于NoneBot2
- title: 支持多种框架
icon: robot
details: 拥有良好的生态支持
details: 兼容nonebotmelobot等拥有良好的生态支持
link: https://nonebot.dev/
- title: 便捷管理
@ -79,5 +79,4 @@ highlights:
details: 如果你有多个 Python 环境,请使用 <code>pythonx -m pip install -r requirements.txt</code>
- title: 使用 <code>python main.py</code> 启动项目。
copyright: © 2021-2024 SnowyKami All Rights Reserved
---
---

View File

@ -1,86 +0,0 @@
---
home: true
icon: home
title: 首页
heroImage: https://cdn.liteyuki.icu/static/img/logo.png
bgImage:
bgImageDark:
bgImageStyle:
background-attachment: fixed
heroText: LiteyukiBot
tagline: 轻雪机器人一个以轻量和简洁为设计理念基于Nonebot2的OneBot标准聊天机器人
actions:
- text: 快速部署
icon: lightbulb
link: ./deployment/install.html
type: primary
- text: 使用手册
icon: book
link: ./usage/basic_command.html
highlights:
- header: 简洁至上
image: /assets/image/layout.svg
bgImage: https://theme-hope-assets.vuejs.press/bg/2-light.svg
bgImageDark: https://theme-hope-assets.vuejs.press/bg/2-dark.svg
bgImageStyle:
background-repeat: repeat
background-size: initial
features:
- title: 基于Nonebot2
icon: robot
details: 拥有良好的生态支持
link: https://nonebot.dev/
- title: 便捷插件管理
icon: plug
details: 使用<code>包管理器</code>,无需命令行操作即可安装/卸载插件
- title: 人性化交互
icon: mouse-pointer
details: 新的点击交互模式,拒绝手打指令
- title: 主题支持
icon: paint-brush
details: 使用资源包对外观进行完全自定义
link: https://bot.liteyuki.icu/usage/resource_pack.html
- title: 国际化
icon: globe
details: 通过资源包支持多种语言
link: https://baike.baidu.com/item/i18n/6771940
- title: 简易配置
icon: cog
details: 无需繁琐前期过程,开箱即用
link: https://bot.liteyuki.icu/deployment/config.html
- title: 高性能
icon: tachometer-alt
details: 500个插件3s内启动
- title: 滚动更新
icon: cloud-download
details: 让你的机器人保持最新提交
- title: 开源
icon: code
details: 项目遵循MIT协议开源欢迎各位的贡献
- header: 快速部署
image: /assets/image/box.svg
bgImage: https://theme-hope-assets.vuejs.press/bg/3-light.svg
bgImageDark: https://theme-hope-assets.vuejs.press/bg/3-dark.svg
highlights:
- title: 安装 Git 和 Python3.10+
- title: 使用 <code>git clone https://github.com/snowykami/LiteyukiBot</code> 以克隆项目至本地。
details: 如果无法连接到GitHub可以使用 <code>git clone https://gitee.com/snowykami/LiteyukiBot</code>
- title: 使用 <code>cd LiteyukiBot</code> 切换到项目目录。
- title: 使用 <code>pip install -r requirements.txt</code> 安装项目依赖。
details: 如果你有多个 Python 环境,请使用 <code>pythonx -m pip install -r requirements.txt</code>
- title: 使用 <code>python main.py</code> 启动项目。
copyright: © 2021-2024 SnowyKami All Rights Reserved
---

9
docs/deploy/cfg.txt Normal file
View File

@ -0,0 +1,9 @@
# note
开发者选项
allow_update: true # 是否允许更新
log_level: "INFO" # 日志等级
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
auto_report: true # 是否自动上报问题给轻雪服务器
auto_update: true # 是否自动更新轻雪每天4点检查更新
safe_mode: false # 安全模式,开启后将不会加载任何第三方插件
dev_mode: false # 开发者模式,开启后将会启动看门狗

78
docs/deploy/config.md Normal file
View File

@ -0,0 +1,78 @@
---
title: 配置
icon: cog
order: 2
category: 使用指南
tag:
- 配置
- 部署
---
轻雪支持`yaml``json``toml`作为配置文件,取决于你个人的喜好
首次运行后生成`config.yml``config`目录,你可修改配置项后重启轻雪,绝大多数情况下,你只需要修改`superusers``nickname`字段即可
启动时会加载项目目录下`config.yml/yaml/json/toml``config`目录下的所有配置文件,你可在`config`目录下创建多个配置文件,轻雪会自动合并这些配置文件
## **基础配置项**
```yaml
nonebot:
# Nonebot机器人的配置以前的最外层配置项仍可为Nonebot服务但是部分内容会被覆盖请尽快迁移
command_start: [ "/", "" ] # 指令前缀,若没有""空命令头请开启alconna_use_command_start保证alconna解析正常
host: 127.0.0.1 # 监听地址默认为本机若要接收外部请求请填写0.0.0.0
port: 20216 # 绑定端口
nickname: [ "liteyuki" ] # 机器人昵称列表
superusers: [ "1919810" ] # 超级用户列表
liteyuki:
# 写在外层的配置项将会被覆盖建议迁移到liteyuki下
log_level: "INFO" # 日志等级
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
auto_report: true # 是否自动上报问题给轻雪服务器
auto_update: true # 是否自动更新轻雪每天4点检查更新
plugins: [ ] # 轻雪插件列表
plugin_dirs: [ ] # 轻雪插件目录列表
```
## **其他配置**
以下为默认值,如需自定义请手动添加
```yaml
# 高级NoneBot配置
nonebot:
onebot_access_token: "" # 访问令牌,对公开放时建议设置
default_language: "zh-CN" # 默认语言
alconna_auto_completion: false # alconna是否自动补全指令默认false建议开启
safe_mode: false # 安全模式开启后将不会加载任何第三方NoneBot插件
# 其他Nonebot插件的配置项
custom_config_1: "custom_value1"
custom_config_2: "custom_value2"
# 开发者选项
liteyuki:
allow_update: true # 是否允许更新
debug: false # 轻雪调试开启会自动重载Bot或者资源其他插件自带的调试功能也将开启
dev_mode: false # 开发者模式,开启后将会启动监视者,监视文件变化并自动重载
...
```
> [!tip]
> 如果要使用NoneBot和dotenv配置文件请自行创建`.env.{ENVIRONMENT}`,并在`config.yml`中添加`nonebot.environment:{ENVIRONMENT}`字段
## **与NoneBot对接的OneBot实现端配置**
生产环境中推荐反向WebSocket
不同的实现端给出的字段可能不同,但是基本上都是一样的,这里给出一个参考值
| 字段 | 参考值 | 说明 |
|-------------|------------------------------------|----------------------------------|
| 协议 | 反向WebSocket | 推荐使用反向ws协议进行通信即轻雪作为服务端 |
| 地址 | ws://127.0.0.1:20216/onebot/v11/ws | 地址取决于配置文件,本机默认为`127.0.0.1:20216` |
| AccessToken | `""` | 如果你给轻雪配置了`AccessToken`,请在此填写相同的值 |
## **其他**
- 要使用其他通信方式请访问[OneBot Adapter](https://onebot.adapters.nonebot.dev/)获取详细信息
- 轻雪不局限于OneBot适配器你可以使用NoneBot2支持的任何适配器

View File

@ -25,7 +25,7 @@ python main.py
```
> [!tip]
> 推荐使用虚拟环境来运行轻雪,以避免依赖冲突,你可以使用`python -m venv venv`来创建虚拟环境,然后使用`venv\Scripts\activate`来激活虚拟环境
> 推荐使用虚拟环境来运行轻雪,以避免依赖冲突,你可以使用`python -m venv .venv`来创建虚拟环境,然后使用`.venv\Scripts\activate`来激活虚拟环境Linux下使用`source .venv/bin/activate`激活)
### **使用Docker构建镜像部署**

View File

@ -1,62 +0,0 @@
---
title: 配置
icon: cog
order: 2
category: 使用指南
tag:
- 配置
- 部署
---
首次运行后生成`config.yml`,你可以修改配置项后重启轻雪,绝大多数情况下,你只需要修改`superusers``nickname`字段即可
## **基础配置项**
```yaml
command_start: [ "/", "" ] # 指令前缀,若没有""空命令头请开启alconna_use_command_start保证alconna解析正常
host: 127.0.0.1 # 监听地址默认为本机若要接收外部请求请填写0.0.0.0
port: 20216 # 绑定端口
nickname: [ "liteyuki" ] # 机器人昵称列表
superusers: [ "1919810" ] # 超级用户列表
```
## **其他配置**
以下为默认值,如需自定义请手动添加
```yaml
onebot_access_token: "" # 访问令牌,对公开放时建议设置
default_language: "zh-CN" # 默认语言
alconna_auto_completion: false # alconna是否自动补全指令默认false建议开启
# 开发者选项
allow_update: true # 是否允许更新
log_level: "INFO" # 日志等级
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
auto_report: true # 是否自动上报问题给轻雪服务器
auto_update: true # 是否自动更新轻雪每天4点检查更新
debug: false # 轻雪调试开启会自动重载Bot或者资源其他插件自带的调试功能也将开启
safe_mode: false # 安全模式,开启后将不会加载任何第三方插件
# 其他Nonebot插件的配置项
custom_config_1: "custom_value1"
custom_config_2: "custom_value2"
...
```
> [!tip]
> 如果要使用dotenv配置文件请自行创建`.env.{ENVIRONMENT}`,并在`config.yml`中添加`environment:{ENVIRONMENT}`字段
## **OneBot实现端配置**
生产环境中推荐反向WebSocket
不同的实现端给出的字段可能不同,但是基本上都是一样的,这里给出一个参考值
| 字段 | 参考值 | 说明 |
|-------------|------------------------------------|----------------------------------|
| 协议 | 反向WebSocket | 推荐使用反向ws协议进行通信即轻雪作为服务端 |
| 地址 | ws://127.0.0.1:20216/onebot/v11/ws | 地址取决于配置文件,本机默认为`127.0.0.1:20216` |
| AccessToken | `""` | 如果你给轻雪配置了`AccessToken`,请在此填写相同的值 |
## **其他**
- 要使用其他通信方式请访问[OneBot Adapter](https://onebot.adapters.nonebot.dev/)获取详细信息
- 轻雪不局限于OneBot适配器你可以使用NoneBot2支持的任何适配器

8
docs/dev/README.md Normal file
View File

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

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

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

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

@ -0,0 +1,581 @@
---
title: liteyuki.bot
index: true
icon: laptop-code
category: API
---
### ***def*** `get_bot() -> LiteyukiBot`
获取轻雪实例
Returns:
LiteyukiBot: 当前的轻雪实例
<details>
<summary>源代码</summary>
```python
def get_bot() -> LiteyukiBot:
"""
获取轻雪实例
Returns:
LiteyukiBot: 当前的轻雪实例
"""
if IS_MAIN_PROCESS:
if _BOT_INSTANCE is None:
raise RuntimeError('Liteyuki instance not initialized.')
return _BOT_INSTANCE
else:
raise RuntimeError("Can't get bot instance in sub process.")
```
</details>
### ***def*** `get_config(key: str, default: Any) -> Any`
获取配置
Args:
key: 配置键
default: 默认值
Returns:
Any: 配置值
<details>
<summary>源代码</summary>
```python
def get_config(key: str, default: Any=None) -> Any:
"""
获取配置
Args:
key: 配置键
default: 默认值
Returns:
Any: 配置值
"""
return get_bot().config.get(key, default)
```
</details>
### ***def*** `get_config_with_compat(key: str, compat_keys: tuple[str], default: Any) -> Any`
获取配置,兼容旧版本
Args:
key: 配置键
compat_keys: 兼容键
default: 默认值
Returns:
Any: 配置值
<details>
<summary>源代码</summary>
```python
def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any=None) -> Any:
"""
获取配置,兼容旧版本
Args:
key: 配置键
compat_keys: 兼容键
default: 默认值
Returns:
Any: 配置值
"""
if key in get_bot().config:
return get_bot().config[key]
for compat_key in compat_keys:
if compat_key in get_bot().config:
logger.warning(f'Config key "{compat_key}" will be deprecated, use "{key}" instead.')
return get_bot().config[compat_key]
return default
```
</details>
### ***def*** `print_logo() -> None`
<details>
<summary>源代码</summary>
```python
def print_logo():
print('\x1b[34m' + '\n __ ______ ________ ________ __ __ __ __ __ __ ______ \n / | / |/ |/ |/ \\ / |/ | / |/ | / |/ |\n $$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \\ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/ \n $$ | $$ | $$ | $$ |__ $$ \\/$$/ $$ | $$ |$$ |/$$/ $$ | \n $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ | \n $$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \\ $$ | \n $$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \\__$$ |$$ |$$ \\ _$$ |_ \n $$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |\n $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ \n ' + '\x1b[0m')
```
</details>
### ***class*** `LiteyukiBot`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;初始化轻雪实例
Args:
*args:
**kwargs: 配置
<details>
<summary>源代码</summary>
```python
def __init__(self, *args, **kwargs) -> None:
"""
初始化轻雪实例
Args:
*args:
**kwargs: 配置
"""
'常规操作'
print_logo()
global _BOT_INSTANCE
_BOT_INSTANCE = self
'配置'
self.config: dict[str, Any] = kwargs
'初始化'
self.init(**self.config)
logger.info('Liteyuki is initializing...')
'生命周期管理'
self.lifespan = Lifespan()
self.process_manager: ProcessManager = ProcessManager(lifespan=self.lifespan)
'事件循环'
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.stop_event = threading.Event()
self.call_restart_count = 0
'加载插件加载器'
load_plugin('liteyuki.plugins.plugin_loader')
'信号处理'
signal.signal(signal.SIGINT, self._handle_exit)
signal.signal(signal.SIGTERM, self._handle_exit)
atexit.register(self.process_manager.terminate_all)
```
</details>
### &emsp; ***def*** `run(self) -> None`
&emsp;启动逻辑
<details>
<summary>源代码</summary>
```python
def run(self):
"""
启动逻辑
"""
self.lifespan.before_start()
self.process_manager.start_all()
self.lifespan.after_start()
self.keep_alive()
```
</details>
### &emsp; ***def*** `keep_alive(self) -> None`
&emsp;保持轻雪运行
Returns:
<details>
<summary>源代码</summary>
```python
def keep_alive(self):
"""
保持轻雪运行
Returns:
"""
try:
while not self.stop_event.is_set():
time.sleep(0.5)
except KeyboardInterrupt:
logger.info('Liteyuki is stopping...')
self.stop()
```
</details>
### &emsp; ***def*** `restart(self, delay: int) -> None`
&emsp;重启轻雪本体
Returns:
<details>
<summary>源代码</summary>
```python
def restart(self, delay: int=0):
"""
重启轻雪本体
Returns:
"""
if self.call_restart_count < 1:
executable = sys.executable
args = sys.argv
logger.info('Restarting LiteyukiBot...')
time.sleep(delay)
if platform.system() == 'Windows':
cmd = 'start'
elif platform.system() == 'Linux':
cmd = 'nohup'
elif platform.system() == 'Darwin':
cmd = 'open'
else:
cmd = 'nohup'
self.process_manager.terminate_all()
threading.Thread(target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",)).start()
sys.exit(0)
self.call_restart_count += 1
```
</details>
### &emsp; ***def*** `restart_process(self, name: Optional[str]) -> None`
&emsp;停止轻雪
Args:
name: 进程名称, 默认为None, 所有进程
Returns:
<details>
<summary>源代码</summary>
```python
def restart_process(self, name: Optional[str]=None):
"""
停止轻雪
Args:
name: 进程名称, 默认为None, 所有进程
Returns:
"""
self.lifespan.before_process_shutdown()
self.lifespan.before_process_shutdown()
if name is not None:
chan_active = get_channel(f'{name}-active')
chan_active.send(1)
else:
for process_name in self.process_manager.processes:
chan_active = get_channel(f'{process_name}-active')
chan_active.send(1)
```
</details>
### &emsp; ***def*** `init(self) -> None`
&emsp;初始化轻雪, 自动调用
Returns:
<details>
<summary>源代码</summary>
```python
def init(self, *args, **kwargs):
"""
初始化轻雪, 自动调用
Returns:
"""
self.init_logger()
```
</details>
### &emsp; ***def*** `init_logger(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def init_logger(self):
init_log(config=self.config)
```
</details>
### &emsp; ***def*** `stop(self) -> None`
&emsp;停止轻雪
Returns:
<details>
<summary>源代码</summary>
```python
def stop(self):
"""
停止轻雪
Returns:
"""
self.stop_event.set()
self.loop.stop()
```
</details>
### &emsp; ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册启动前的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_start(self, func: LIFESPAN_FUNC):
"""
注册启动前的函数
Args:
func:
Returns:
"""
return self.lifespan.on_before_start(func)
```
</details>
### &emsp; ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册启动后的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_start(self, func: LIFESPAN_FUNC):
"""
注册启动后的函数
Args:
func:
Returns:
"""
return self.lifespan.on_after_start(func)
```
</details>
### &emsp; ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册停止后的函数:未实现
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_shutdown(self, func: LIFESPAN_FUNC):
"""
注册停止后的函数:未实现
Args:
func:
Returns:
"""
return self.lifespan.on_after_shutdown(func)
```
</details>
### &emsp; ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册进程停止前的函数,为子进程停止时调用
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_process_shutdown(self, func: LIFESPAN_FUNC):
"""
注册进程停止前的函数,为子进程停止时调用
Args:
func:
Returns:
"""
return self.lifespan.on_before_process_shutdown(func)
```
</details>
### &emsp; ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册进程重启前的函数,为子进程重启时调用
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_process_restart(self, func: LIFESPAN_FUNC):
"""
注册进程重启前的函数,为子进程重启时调用
Args:
func:
Returns:
"""
return self.lifespan.on_before_process_restart(func)
```
</details>
### &emsp; ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册重启后的函数:未实现
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_restart(self, func: LIFESPAN_FUNC):
"""
注册重启后的函数:未实现
Args:
func:
Returns:
"""
return self.lifespan.on_after_restart(func)
```
</details>
### &emsp; ***def*** `on_after_nonebot_init(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册nonebot初始化后的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
"""
注册nonebot初始化后的函数
Args:
func:
Returns:
"""
return self.lifespan.on_after_nonebot_init(func)
```
</details>
### ***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,450 @@
---
title: liteyuki.bot.lifespan
order: 1
icon: laptop-code
category: API
---
### ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
运行函数
Args:
funcs:
Returns:
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
"""
运行函数
Args:
funcs:
Returns:
"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = []
for func in funcs:
if is_coroutine_callable(func):
tasks.append(func(*args, **kwargs))
else:
tasks.append(async_wrapper(func)(*args, **kwargs))
loop.run_until_complete(asyncio.gather(*tasks))
```
</details>
### ***class*** `Lifespan`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;轻雪生命周期管理,启动、停止、重启
<details>
<summary>源代码</summary>
```python
def __init__(self) -> None:
"""
轻雪生命周期管理,启动、停止、重启
"""
self.life_flag: int = 0
self._before_start_funcs: list[LIFESPAN_FUNC] = []
self._after_start_funcs: list[LIFESPAN_FUNC] = []
self._before_process_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._before_process_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
```
</details>
### &emsp; ***@staticmethod***
### &emsp; ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
&emsp;运行函数
Args:
funcs:
Returns:
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
"""
运行函数
Args:
funcs:
Returns:
"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = []
for func in funcs:
if is_coroutine_callable(func):
tasks.append(func(*args, **kwargs))
else:
tasks.append(async_wrapper(func)(*args, **kwargs))
loop.run_until_complete(asyncio.gather(*tasks))
```
</details>
### &emsp; ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_start_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_start_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册停止前的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止前的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_process_shutdown_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册停止后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_shutdown_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册重启时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_process_restart_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册重启后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_restart_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_nonebot_init(self, func: Any) -> None`
&emsp;注册 NoneBot 初始化后的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_nonebot_init(self, func):
"""
注册 NoneBot 初始化后的函数
Args:
func:
Returns:
"""
self._after_nonebot_init_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `before_start(self) -> None`
&emsp;启动前
Returns:
<details>
<summary>源代码</summary>
```python
def before_start(self) -> None:
"""
启动前
Returns:
"""
logger.debug('Running before_start functions')
self.run_funcs(self._before_start_funcs)
```
</details>
### &emsp; ***def*** `after_start(self) -> None`
&emsp;启动后
Returns:
<details>
<summary>源代码</summary>
```python
def after_start(self) -> None:
"""
启动后
Returns:
"""
logger.debug('Running after_start functions')
self.run_funcs(self._after_start_funcs)
```
</details>
### &emsp; ***def*** `before_process_shutdown(self) -> None`
&emsp;停止前
Returns:
<details>
<summary>源代码</summary>
```python
def before_process_shutdown(self) -> None:
"""
停止前
Returns:
"""
logger.debug('Running before_shutdown functions')
self.run_funcs(self._before_process_shutdown_funcs)
```
</details>
### &emsp; ***def*** `after_shutdown(self) -> None`
&emsp;停止后
Returns:
<details>
<summary>源代码</summary>
```python
def after_shutdown(self) -> None:
"""
停止后
Returns:
"""
logger.debug('Running after_shutdown functions')
self.run_funcs(self._after_shutdown_funcs)
```
</details>
### &emsp; ***def*** `before_process_restart(self) -> None`
&emsp;重启前
Returns:
<details>
<summary>源代码</summary>
```python
def before_process_restart(self) -> None:
"""
重启前
Returns:
"""
logger.debug('Running before_restart functions')
self.run_funcs(self._before_process_restart_funcs)
```
</details>
### &emsp; ***def*** `after_restart(self) -> None`
&emsp;重启后
Returns:
<details>
<summary>源代码</summary>
```python
def after_restart(self) -> None:
"""
重启后
Returns:
"""
logger.debug('Running after_restart functions')
self.run_funcs(self._after_restart_funcs)
```
</details>
### ***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,427 @@
---
title: liteyuki.comm.channel
order: 1
icon: laptop-code
category: API
---
### ***def*** `set_channel(name: str, channel: Channel) -> None`
设置通道实例
Args:
name: 通道名称
channel: 通道实例
<details>
<summary>源代码</summary>
```python
def set_channel(name: str, channel: Channel):
"""
设置通道实例
Args:
name: 通道名称
channel: 通道实例
"""
if not isinstance(channel, Channel):
raise TypeError(f'channel_ must be an instance of Channel, {type(channel)} found')
if IS_MAIN_PROCESS:
_channel[name] = channel
else:
channel_deliver_passive_channel.send(('set_channel', {'name': name, 'channel_': channel}))
```
</details>
### ***def*** `set_channels(channels: dict[str, Channel]) -> None`
设置通道实例
Args:
channels: 通道名称
<details>
<summary>源代码</summary>
```python
def set_channels(channels: dict[str, Channel]):
"""
设置通道实例
Args:
channels: 通道名称
"""
for name, channel in channels.items():
set_channel(name, channel)
```
</details>
### ***def*** `get_channel(name: str) -> Channel`
获取通道实例
Args:
name: 通道名称
Returns:
<details>
<summary>源代码</summary>
```python
def get_channel(name: str) -> Channel:
"""
获取通道实例
Args:
name: 通道名称
Returns:
"""
if IS_MAIN_PROCESS:
return _channel[name]
else:
recv_chan = Channel[Channel[Any]]('recv_chan')
channel_deliver_passive_channel.send(('get_channel', {'name': name, 'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### ***def*** `get_channels() -> dict[str, Channel]`
获取通道实例
Returns:
<details>
<summary>源代码</summary>
```python
def get_channels() -> dict[str, Channel]:
"""
获取通道实例
Returns:
"""
if IS_MAIN_PROCESS:
return _channel
else:
recv_chan = Channel[dict[str, Channel[Any]]]('recv_chan')
channel_deliver_passive_channel.send(('get_channels', {'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### ***def*** `on_set_channel(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'set_channel')
def on_set_channel(data: tuple[str, dict[str, Any]]):
name, channel = (data[1]['name'], data[1]['channel_'])
set_channel(name, channel)
```
</details>
### ***def*** `on_get_channel(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'get_channel')
def on_get_channel(data: tuple[str, dict[str, Any]]):
name, recv_chan = (data[1]['name'], data[1]['recv_chan'])
recv_chan.send(get_channel(name))
```
</details>
### ***def*** `on_get_channels(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'get_channels')
def on_get_channels(data: tuple[str, dict[str, Any]]):
recv_chan = data[1]['recv_chan']
recv_chan.send(get_channels())
```
</details>
### ***def*** `decorator(func: Callable[[T], Any]) -> Callable[[T], Any]`
<details>
<summary>源代码</summary>
```python
def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
global _func_id
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
_callback_funcs[_func_id] = wrapper
if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(_func_id)
else:
self._on_sub_receive_funcs.append(_func_id)
_func_id += 1
return func
```
</details>
### ***async def*** `wrapper(data: T) -> Any`
<details>
<summary>源代码</summary>
```python
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
```
</details>
### ***class*** `Channel(Generic[T])`
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
### &emsp; ***def*** `__init__(self, _id: str, type_check: Optional[bool]) -> None`
&emsp;初始化通道
Args:
_id: 通道ID
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
<details>
<summary>源代码</summary>
```python
def __init__(self, _id: str, type_check: Optional[bool]=None):
"""
初始化通道
Args:
_id: 通道ID
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
"""
self.conn_send, self.conn_recv = Pipe()
self._closed = False
self._on_main_receive_funcs: list[int] = []
self._on_sub_receive_funcs: list[int] = []
self.name: str = _id
self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False
if type_check is None:
type_check = self._get_generic_type() is not None
elif type_check:
if self._get_generic_type() is None:
raise TypeError('Type hint is required for enforcing type check.')
self.type_check = type_check
```
</details>
### &emsp; ***def*** `send(self, data: T) -> None`
&emsp;发送数据
Args:
data: 数据
<details>
<summary>源代码</summary>
```python
def send(self, data: T):
"""
发送数据
Args:
data: 数据
"""
if self.type_check:
_type = self._get_generic_type()
if _type is not None and (not self._validate_structure(data, _type)):
raise TypeError(f'Data must be an instance of {_type}, {type(data)} found')
if self._closed:
raise RuntimeError('Cannot send to a closed channel_')
self.conn_send.send(data)
```
</details>
### &emsp; ***def*** `receive(self) -> T`
&emsp;接收数据
Args:
<details>
<summary>源代码</summary>
```python
def receive(self) -> T:
"""
接收数据
Args:
"""
if self._closed:
raise RuntimeError('Cannot receive from a closed channel_')
while True:
data = self.conn_recv.recv()
return data
```
</details>
### &emsp; ***def*** `close(self) -> None`
&emsp;关闭通道
<details>
<summary>源代码</summary>
```python
def close(self):
"""
关闭通道
"""
self._closed = True
self.conn_send.close()
self.conn_recv.close()
```
</details>
### &emsp; ***def*** `on_receive(self, filter_func: Optional[FILTER_FUNC]) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]`
&emsp;接收数据并执行函数
Args:
filter_func: 过滤函数为None则不过滤
Returns:
装饰器,装饰一个函数在接收到数据后执行
<details>
<summary>源代码</summary>
```python
def on_receive(self, filter_func: Optional[FILTER_FUNC]=None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
"""
接收数据并执行函数
Args:
filter_func: 过滤函数为None则不过滤
Returns:
装饰器,装饰一个函数在接收到数据后执行
"""
if not self.is_sub_receive_loop_running and (not IS_MAIN_PROCESS):
threading.Thread(target=self._start_sub_receive_loop, daemon=True).start()
if not self.is_main_receive_loop_running and IS_MAIN_PROCESS:
threading.Thread(target=self._start_main_receive_loop, daemon=True).start()
def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
global _func_id
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
_callback_funcs[_func_id] = wrapper
if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(_func_id)
else:
self._on_sub_receive_funcs.append(_func_id)
_func_id += 1
return func
return decorator
```
</details>
### ***var*** `T = TypeVar('T')`
### ***var*** `channel_deliver_active_channel = Channel(_id='channel_deliver_active_channel')`
### ***var*** `channel_deliver_passive_channel = Channel(_id='channel_deliver_passive_channel')`
### ***var*** `recv_chan = data[1]['recv_chan']`
### ***var*** `recv_chan = Channel[Channel[Any]]('recv_chan')`
### ***var*** `recv_chan = Channel[dict[str, Channel[Any]]]('recv_chan')`
### ***var*** `type_check = self._get_generic_type() is not None`
### ***var*** `data = self.conn_recv.recv()`
### ***var*** `func = _callback_funcs[func_id]`
### ***var*** `func = _callback_funcs[func_id]`
### ***var*** `data = self.conn_recv.recv()`
### ***var*** `data = self.conn_recv.recv()`

View File

@ -0,0 +1,25 @@
---
title: liteyuki.comm.event
order: 1
icon: laptop-code
category: API
---
### ***class*** `Event`
事件类
### &emsp; ***def*** `__init__(self, name: str, data: dict[str, Any]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, name: str, data: dict[str, Any]):
self.name = name
self.data = data
```
</details>

View File

@ -0,0 +1,563 @@
---
title: liteyuki.comm.storage
order: 1
icon: laptop-code
category: API
---
### ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_subscriber_receive_funcs(channel_: str, data: Any):
"""
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
"""
if IS_MAIN_PROCESS:
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
```
</details>
### ***def*** `on_get(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'get')
def on_get(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
default = data[1]['default']
recv_chan = data[1]['recv_chan']
recv_chan.send(shared_memory.get(key, default))
```
</details>
### ***def*** `on_set(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'set')
def on_set(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
value = data[1]['value']
shared_memory.set(key, value)
```
</details>
### ***def*** `on_delete(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'delete')
def on_delete(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
shared_memory.delete(key)
```
</details>
### ***def*** `on_get_all(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'get_all')
def on_get_all(data: tuple[str, dict[str, Any]]):
recv_chan = data[1]['recv_chan']
recv_chan.send(shared_memory.get_all())
```
</details>
### ***def*** `on_publish(data: tuple[str, Any]) -> None`
<details>
<summary>源代码</summary>
```python
@channel.publish_channel.on_receive()
def on_publish(data: tuple[str, Any]):
channel_, data = data
shared_memory.run_subscriber_receive_funcs(channel_, data)
```
</details>
### ***def*** `decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC`
<details>
<summary>源代码</summary>
```python
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
if IS_MAIN_PROCESS:
if channel_ not in _on_main_subscriber_receive_funcs:
_on_main_subscriber_receive_funcs[channel_] = []
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
else:
if channel_ not in _on_sub_subscriber_receive_funcs:
_on_sub_subscriber_receive_funcs[channel_] = []
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
return wrapper
```
</details>
### ***async def*** `wrapper(data: Any) -> None`
<details>
<summary>源代码</summary>
```python
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
```
</details>
### ***class*** `Subscriber`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self):
self._subscribers = {}
```
</details>
### &emsp; ***def*** `receive(self) -> Any`
&emsp;
<details>
<summary>源代码</summary>
```python
def receive(self) -> Any:
pass
```
</details>
### &emsp; ***def*** `unsubscribe(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def unsubscribe(self) -> None:
pass
```
</details>
### ***class*** `KeyValueStore`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self):
self._store = {}
self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-active')
self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-passive')
self.publish_channel = Channel[tuple[str, Any]](_id='shared_memory-publish')
self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False
```
</details>
### &emsp; ***def*** `set(self, key: str, value: Any) -> None`
&emsp;设置键值对
Args:
key: 键
value: 值
<details>
<summary>源代码</summary>
```python
def set(self, key: str, value: Any) -> None:
"""
设置键值对
Args:
key: 键
value: 值
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
self._store[key] = value
else:
self.passive_chan.send(('set', {'key': key, 'value': value}))
```
</details>
### &emsp; ***def*** `get(self, key: str, default: Optional[Any]) -> Optional[Any]`
&emsp;获取键值对
Args:
key: 键
default: 默认值
Returns:
Any: 值
<details>
<summary>源代码</summary>
```python
def get(self, key: str, default: Optional[Any]=None) -> Optional[Any]:
"""
获取键值对
Args:
key: 键
default: 默认值
Returns:
Any: 值
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
return self._store.get(key, default)
else:
recv_chan = Channel[Optional[Any]]('recv_chan')
self.passive_chan.send(('get', {'key': key, 'default': default, 'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### &emsp; ***def*** `delete(self, key: str, ignore_key_error: bool) -> None`
&emsp;删除键值对
Args:
key: 键
ignore_key_error: 是否忽略键不存在的错误
Returns:
<details>
<summary>源代码</summary>
```python
def delete(self, key: str, ignore_key_error: bool=True) -> None:
"""
删除键值对
Args:
key: 键
ignore_key_error: 是否忽略键不存在的错误
Returns:
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
if key in self._store:
try:
del self._store[key]
del _locks[key]
except KeyError as e:
if not ignore_key_error:
raise e
else:
self.passive_chan.send(('delete', {'key': key}))
```
</details>
### &emsp; ***def*** `get_all(self) -> dict[str, Any]`
&emsp;获取所有键值对
Returns:
dict[str, Any]: 键值对
<details>
<summary>源代码</summary>
```python
def get_all(self) -> dict[str, Any]:
"""
获取所有键值对
Returns:
dict[str, Any]: 键值对
"""
if IS_MAIN_PROCESS:
return self._store
else:
recv_chan = Channel[dict[str, Any]]('recv_chan')
self.passive_chan.send(('get_all', {'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### &emsp; ***def*** `publish(self, channel_: str, data: Any) -> None`
&emsp;发布消息
Args:
channel_: 频道
data: 数据
Returns:
<details>
<summary>源代码</summary>
```python
def publish(self, channel_: str, data: Any) -> None:
"""
发布消息
Args:
channel_: 频道
data: 数据
Returns:
"""
self.active_chan.send(('publish', {'channel': channel_, 'data': data}))
```
</details>
### &emsp; ***def*** `on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]`
&emsp;订阅者接收消息时的回调
Args:
channel_: 频道
Returns:
装饰器
<details>
<summary>源代码</summary>
```python
def on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
"""
订阅者接收消息时的回调
Args:
channel_: 频道
Returns:
装饰器
"""
if IS_MAIN_PROCESS and (not self.is_main_receive_loop_running):
threading.Thread(target=self._start_receive_loop, daemon=True).start()
shared_memory.is_main_receive_loop_running = True
elif not IS_MAIN_PROCESS and (not self.is_sub_receive_loop_running):
threading.Thread(target=self._start_receive_loop, daemon=True).start()
shared_memory.is_sub_receive_loop_running = True
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
if IS_MAIN_PROCESS:
if channel_ not in _on_main_subscriber_receive_funcs:
_on_main_subscriber_receive_funcs[channel_] = []
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
else:
if channel_ not in _on_sub_subscriber_receive_funcs:
_on_sub_subscriber_receive_funcs[channel_] = []
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
return wrapper
return decorator
```
</details>
### &emsp; ***@staticmethod***
### &emsp; ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
&emsp;运行订阅者接收函数
Args:
channel_: 频道
data: 数据
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_subscriber_receive_funcs(channel_: str, data: Any):
"""
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
"""
if IS_MAIN_PROCESS:
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
```
</details>
### ***class*** `GlobalKeyValueStore`
### &emsp; ***@classmethod***
### &emsp; ***def*** `get_instance(cls: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
@classmethod
def get_instance(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = KeyValueStore()
return cls._instance
```
</details>
### &emsp; ***attr*** `_instance: None`
### &emsp; ***attr*** `_lock: threading.Lock()`
### ***var*** `key = data[1]['key']`
### ***var*** `default = data[1]['default']`
### ***var*** `recv_chan = data[1]['recv_chan']`
### ***var*** `key = data[1]['key']`
### ***var*** `value = data[1]['value']`
### ***var*** `key = data[1]['key']`
### ***var*** `recv_chan = data[1]['recv_chan']`
### ***var*** `lock = _get_lock(key)`
### ***var*** `lock = _get_lock(key)`
### ***var*** `recv_chan = Channel[Optional[Any]]('recv_chan')`
### ***var*** `lock = _get_lock(key)`
### ***var*** `recv_chan = Channel[dict[str, Any]]('recv_chan')`
### ***var*** `data = self.active_chan.receive()`
### ***var*** `data = self.publish_channel.receive()`

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

@ -0,0 +1,231 @@
---
title: liteyuki.config
order: 1
icon: laptop-code
category: API
---
### ***def*** `flat_config(config: dict[str, Any]) -> dict[str, Any]`
扁平化配置文件
{a:{b:{c:1}}} -> {"a.b.c": 1}
Args:
config: 配置项目
Returns:
扁平化后的配置文件,但也包含原有的键值对
<details>
<summary>源代码</summary>
```python
def flat_config(config: dict[str, Any]) -> dict[str, Any]:
"""
扁平化配置文件
{a:{b:{c:1}}} -> {"a.b.c": 1}
Args:
config: 配置项目
Returns:
扁平化后的配置文件,但也包含原有的键值对
"""
new_config = copy.deepcopy(config)
for key, value in config.items():
if isinstance(value, dict):
for k, v in flat_config(value).items():
new_config[f'{key}.{k}'] = v
return new_config
```
</details>
### ***def*** `load_from_yaml(file: str) -> dict[str, Any]`
Load config from yaml file
<details>
<summary>源代码</summary>
```python
def load_from_yaml(file: str) -> dict[str, Any]:
"""
Load config from yaml file
"""
logger.debug(f'Loading YAML config from {file}')
config = yaml.safe_load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_json(file: str) -> dict[str, Any]`
Load config from json file
<details>
<summary>源代码</summary>
```python
def load_from_json(file: str) -> dict[str, Any]:
"""
Load config from json file
"""
logger.debug(f'Loading JSON config from {file}')
config = json.load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_toml(file: str) -> dict[str, Any]`
Load config from toml file
<details>
<summary>源代码</summary>
```python
def load_from_toml(file: str) -> dict[str, Any]:
"""
Load config from toml file
"""
logger.debug(f'Loading TOML config from {file}')
config = toml.load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_files() -> dict[str, Any]`
从指定文件加载配置项,会自动识别文件格式
默认执行扁平化选项
<details>
<summary>源代码</summary>
```python
def load_from_files(*files: str, no_warning: bool=False) -> dict[str, Any]:
"""
从指定文件加载配置项,会自动识别文件格式
默认执行扁平化选项
"""
config = {}
for file in files:
if os.path.exists(file):
if file.endswith(('.yaml', 'yml')):
config.update(load_from_yaml(file))
elif file.endswith('.json'):
config.update(load_from_json(file))
elif file.endswith('.toml'):
config.update(load_from_toml(file))
elif not no_warning:
logger.warning(f'Unsupported config file format: {file}')
elif not no_warning:
logger.warning(f'Config file not found: {file}')
return config
```
</details>
### ***def*** `load_configs_from_dirs() -> dict[str, Any]`
从目录下加载配置文件,不递归
按照读取文件的优先级反向覆盖
默认执行扁平化选项
<details>
<summary>源代码</summary>
```python
def load_configs_from_dirs(*directories: str, no_waring: bool=False) -> dict[str, Any]:
"""
从目录下加载配置文件,不递归
按照读取文件的优先级反向覆盖
默认执行扁平化选项
"""
config = {}
for directory in directories:
if not os.path.exists(directory):
if not no_waring:
logger.warning(f'Directory not found: {directory}')
continue
for file in os.listdir(directory):
if file.endswith(_SUPPORTED_CONFIG_FORMATS):
config.update(load_from_files(os.path.join(directory, file), no_warning=no_waring))
return config
```
</details>
### ***def*** `load_config_in_default(no_waring: bool) -> dict[str, Any]`
从一个标准的轻雪项目加载配置文件
项目目录下的config.*和config目录下的所有配置文件
项目目录下的配置文件优先
<details>
<summary>源代码</summary>
```python
def load_config_in_default(no_waring: bool=False) -> dict[str, Any]:
"""
从一个标准的轻雪项目加载配置文件
项目目录下的config.*和config目录下的所有配置文件
项目目录下的配置文件优先
"""
config = load_configs_from_dirs('config', no_waring=no_waring)
config.update(load_from_files('config.yaml', 'config.toml', 'config.json', 'config.yml', no_warning=no_waring))
return config
```
</details>
### ***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,275 @@
---
title: liteyuki.core.manager
order: 1
icon: laptop-code
category: API
---
### ***class*** `ChannelDeliver`
### &emsp; ***def*** `__init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]], publish: Channel[tuple[str, Any]]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]], publish: Channel[tuple[str, Any]]):
self.active = active
self.passive = passive
self.channel_deliver_active = channel_deliver_active
self.channel_deliver_passive = channel_deliver_passive
self.publish = publish
```
</details>
### ***class*** `ProcessManager`
进程管理器
### &emsp; ***def*** `__init__(self, lifespan: 'Lifespan') -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, lifespan: 'Lifespan'):
self.lifespan = lifespan
self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
self.processes: dict[str, Process] = {}
```
</details>
### &emsp; ***def*** `start(self, name: str) -> None`
&emsp;开启后自动监控进程,并添加到进程字典中
Args:
name:
Returns:
<details>
<summary>源代码</summary>
```python
def start(self, name: str):
"""
开启后自动监控进程,并添加到进程字典中
Args:
name:
Returns:
"""
if name not in self.targets:
raise KeyError(f'Process {name} not found.')
chan_active = get_channel(f'{name}-active')
def _start_process():
process = Process(target=self.targets[name][0], args=self.targets[name][1], kwargs=self.targets[name][2], daemon=True)
self.processes[name] = process
process.start()
_start_process()
while True:
data = chan_active.receive()
if data == 0:
logger.info(f'Stopping process {name}')
self.lifespan.before_process_shutdown()
self.terminate(name)
break
elif data == 1:
logger.info(f'Restarting process {name}')
self.lifespan.before_process_shutdown()
self.lifespan.before_process_restart()
self.terminate(name)
_start_process()
continue
else:
logger.warning('Unknown data received, ignored.')
```
</details>
### &emsp; ***def*** `start_all(self) -> None`
&emsp;启动所有进程
<details>
<summary>源代码</summary>
```python
def start_all(self):
"""
启动所有进程
"""
for name in self.targets:
threading.Thread(target=self.start, args=(name,), daemon=True).start()
```
</details>
### &emsp; ***def*** `add_target(self, name: str, target: TARGET_FUNC, args: tuple, kwargs: Any) -> None`
&emsp;添加进程
Args:
name: 进程名,用于获取和唯一标识
target: 进程函数
args: 进程函数参数
kwargs: 进程函数关键字参数通常会默认传入chan_active和chan_passive
<details>
<summary>源代码</summary>
```python
def add_target(self, name: str, target: TARGET_FUNC, args: tuple=(), kwargs=None):
"""
添加进程
Args:
name: 进程名,用于获取和唯一标识
target: 进程函数
args: 进程函数参数
kwargs: 进程函数关键字参数通常会默认传入chan_active和chan_passive
"""
if kwargs is None:
kwargs = {}
chan_active: Channel = Channel(_id=f'{name}-active')
chan_passive: Channel = Channel(_id=f'{name}-passive')
channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel, publish=publish_channel)
self.targets[name] = (_delivery_channel_wrapper, (target, channel_deliver, shared_memory, *args), kwargs)
set_channels({f'{name}-active': chan_active, f'{name}-passive': chan_passive})
```
</details>
### &emsp; ***def*** `join_all(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def join_all(self):
for name, process in self.targets:
process.join()
```
</details>
### &emsp; ***def*** `terminate(self, name: str) -> None`
&emsp;终止进程并从进程字典中删除
Args:
name:
Returns:
<details>
<summary>源代码</summary>
```python
def terminate(self, name: str):
"""
终止进程并从进程字典中删除
Args:
name:
Returns:
"""
if name not in self.processes:
logger.warning(f'Process {name} not found.')
return
process = self.processes[name]
process.terminate()
process.join(TIMEOUT)
if process.is_alive():
process.kill()
logger.success(f'Process {name} terminated.')
```
</details>
### &emsp; ***def*** `terminate_all(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def terminate_all(self):
for name in self.targets:
self.terminate(name)
```
</details>
### &emsp; ***def*** `is_process_alive(self, name: str) -> bool`
&emsp;检查进程是否存活
Args:
name:
Returns:
<details>
<summary>源代码</summary>
```python
def is_process_alive(self, name: str) -> bool:
"""
检查进程是否存活
Args:
name:
Returns:
"""
if name not in self.targets:
logger.warning(f'Process {name} not found.')
return self.processes[name].is_alive()
```
</details>
### ***var*** `TIMEOUT = 10`
### ***var*** `chan_active = get_channel(f'{name}-active')`
### ***var*** `channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel, publish=publish_channel)`
### ***var*** `process = self.processes[name]`
### ***var*** `process = Process(target=self.targets[name][0], args=self.targets[name][1], kwargs=self.targets[name][2], daemon=True)`
### ***var*** `data = chan_active.receive()`
### ***var*** `kwargs = {}`

View File

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

View File

@ -0,0 +1,249 @@
---
title: liteyuki.dev.observer
order: 1
icon: laptop-code
category: API
---
### ***def*** `debounce(wait: Any) -> None`
防抖函数
<details>
<summary>源代码</summary>
```python
def debounce(wait):
"""
防抖函数
"""
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
last_call_time = None
return wrapper
return decorator
```
</details>
### ***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:
装饰器,装饰一个函数在接收到数据后执行
<details>
<summary>源代码</summary>
```python
def on_file_system_event(directories: tuple[str], recursive: bool=True, event_filter: FILTER_FUNC=None) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]:
"""
注册文件系统变化监听器
Args:
directories: 监听目录们
recursive: 是否递归监听子目录
event_filter: 事件过滤器, 返回True则执行回调函数
Returns:
装饰器,装饰一个函数在接收到数据后执行
"""
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
code_modified_handler = CodeModifiedHandler()
code_modified_handler.on_modified = wrapper
for directory in directories:
observer.schedule(code_modified_handler, directory, recursive=recursive)
return func
return decorator
```
</details>
### ***def*** `decorator(func: Any) -> None`
<details>
<summary>源代码</summary>
```python
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
last_call_time = None
return wrapper
```
</details>
### ***def*** `decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC`
<details>
<summary>源代码</summary>
```python
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
code_modified_handler = CodeModifiedHandler()
code_modified_handler.on_modified = wrapper
for directory in directories:
observer.schedule(code_modified_handler, directory, recursive=recursive)
return func
```
</details>
### ***def*** `wrapper() -> None`
<details>
<summary>源代码</summary>
```python
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
```
</details>
### ***def*** `wrapper(event: FileSystemEvent) -> None`
<details>
<summary>源代码</summary>
```python
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
```
</details>
### ***class*** `CodeModifiedHandler(FileSystemEventHandler)`
Handler for code file changes
### &emsp; ***def*** `on_modified(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
@debounce(1)
def on_modified(self, event):
raise NotImplementedError('on_modified must be implemented')
```
</details>
### &emsp; ***def*** `on_created(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_created(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_deleted(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_deleted(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_moved(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_moved(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_any_event(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_any_event(self, event):
self.on_modified(event)
```
</details>
### ***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,46 @@
---
title: liteyuki.dev.plugin
order: 1
icon: laptop-code
category: API
---
### ***def*** `run_plugins() -> None`
运行插件无需手动初始化bot
Args:
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
<details>
<summary>源代码</summary>
```python
def run_plugins(*module_path: str | Path):
"""
运行插件无需手动初始化bot
Args:
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
"""
cfg = load_config_in_default()
plugins = cfg.get('liteyuki.plugins', [])
plugins.extend(module_path)
cfg['liteyuki.plugins'] = plugins
bot = LiteyukiBot(**cfg)
bot.run()
```
</details>
### ***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的异常基类。

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

@ -0,0 +1,58 @@
---
title: liteyuki.log
order: 1
icon: laptop-code
category: API
---
### ***def*** `get_format(level: str) -> str`
<details>
<summary>源代码</summary>
```python
def get_format(level: str) -> str:
if level == 'DEBUG':
return debug_format
else:
return default_format
```
</details>
### ***def*** `init_log(config: dict) -> None`
在语言加载完成后执行
Returns:
<details>
<summary>源代码</summary>
```python
def init_log(config: dict):
"""
在语言加载完成后执行
Returns:
"""
logger.remove()
logger.add(sys.stdout, level=0, diagnose=False, format=get_format(config.get('log_level', 'INFO')))
show_icon = config.get('log_icon', True)
logger.level('DEBUG', color='<blue>', icon=f"{('🐛' if show_icon else '')}DEBUG")
logger.level('INFO', color='<normal>', icon=f"{('' if show_icon else '')}INFO")
logger.level('SUCCESS', color='<green>', icon=f"{('✅' if show_icon else '')}SUCCESS")
logger.level('WARNING', color='<yellow>', icon=f"{('⚠️' if show_icon else '')}WARNING")
logger.level('ERROR', color='<red>', icon=f"{('⭕' if show_icon else '')}ERROR")
```
</details>
### ***var*** `logger = loguru.logger`
### ***var*** `show_icon = config.get('log_icon', True)`

View File

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

View File

@ -0,0 +1,106 @@
---
title: liteyuki.message.event
order: 1
icon: laptop-code
category: API
---
### ***class*** `MessageEvent`
### &emsp; ***def*** `__init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, data: Optional[dict[str, Any]]) -> None`
&emsp;轻雪抽象消息事件
Args:
bot_id: 机器人ID
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
raw_message: 原始消息(通常为纯文本的格式)
message_type: 消息类型(private, group, other)
session_id: 会话ID(私聊通常为用户ID群聊通常为群ID)
session_type: 会话类型(private, group)
receive_channel: 接收频道(用于回复消息)
data: 附加数据
<details>
<summary>源代码</summary>
```python
def __init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, data: Optional[dict[str, Any]]=None):
"""
轻雪抽象消息事件
Args:
bot_id: 机器人ID
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
raw_message: 原始消息(通常为纯文本的格式)
message_type: 消息类型(private, group, other)
session_id: 会话ID(私聊通常为用户ID群聊通常为群ID)
session_type: 会话类型(private, group)
receive_channel: 接收频道(用于回复消息)
data: 附加数据
"""
if data is None:
data = {}
self.message_type = message_type
self.data = data
self.bot_id = bot_id
self.message = message
self.raw_message = raw_message
self.session_id = session_id
self.session_type = session_type
self.receive_channel = receive_channel
```
</details>
### &emsp; ***def*** `reply(self, message: str | dict[str, Any]) -> None`
&emsp;回复消息
Args:
message:
Returns:
<details>
<summary>源代码</summary>
```python
def reply(self, message: str | dict[str, Any]):
"""
回复消息
Args:
message:
Returns:
"""
reply_event = MessageEvent(message_type=self.session_type, message=message, raw_message='', data={'message': message}, bot_id=self.bot_id, session_id=self.session_id, session_type=self.session_type, receive_channel='_')
shared_memory.publish(self.receive_channel, reply_event)
```
</details>
### ***var*** `reply_event = MessageEvent(message_type=self.session_type, message=message, raw_message='', data={'message': message}, bot_id=self.bot_id, session_id=self.session_id, session_type=self.session_type, receive_channel='_')`
### ***var*** `data = {}`

View File

@ -0,0 +1,71 @@
---
title: liteyuki.message.matcher
order: 1
icon: laptop-code
category: API
---
### ***class*** `Matcher`
### &emsp; ***def*** `__init__(self, rule: Rule, priority: int, block: bool) -> None`
&emsp;匹配器
Args:
rule: 规则
priority: 优先级 >= 0
block: 是否阻断后续优先级更低的匹配器
<details>
<summary>源代码</summary>
```python
def __init__(self, rule: Rule, priority: int, block: bool):
"""
匹配器
Args:
rule: 规则
priority: 优先级 >= 0
block: 是否阻断后续优先级更低的匹配器
"""
self.rule = rule
self.priority = priority
self.block = block
self.handlers: list[EventHandler] = []
```
</details>
### &emsp; ***def*** `handle(self, handler: EventHandler) -> EventHandler`
&emsp;添加处理函数,装饰器
Args:
handler:
Returns:
EventHandler
<details>
<summary>源代码</summary>
```python
def handle(self, handler: EventHandler) -> EventHandler:
"""
添加处理函数,装饰器
Args:
handler:
Returns:
EventHandler
"""
self.handlers.append(handler)
return handler
```
</details>

View File

@ -0,0 +1,39 @@
---
title: liteyuki.message.on
order: 1
icon: laptop-code
category: API
---
### ***def*** `on_message(rule: Rule, priority: int, block: bool) -> Matcher`
<details>
<summary>源代码</summary>
```python
def on_message(rule: Rule=Rule(), priority: int=0, block: bool=True) -> Matcher:
matcher = Matcher(rule, priority, block)
for i, m in enumerate(_matcher_list):
if m.priority < matcher.priority:
_matcher_list.insert(i, matcher)
break
else:
_matcher_list.append(matcher)
return matcher
```
</details>
### ***var*** `current_priority = -1`
### ***var*** `matcher = Matcher(rule, priority, block)`
### ***var*** `current_priority = matcher.priority`

View File

@ -0,0 +1,24 @@
---
title: liteyuki.message.rule
order: 1
icon: laptop-code
category: API
---
### ***class*** `Rule`
### &emsp; ***def*** `__init__(self, handler: Optional[RuleHandler]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, handler: Optional[RuleHandler]=None):
self.handler = handler
```
</details>

View File

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

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

@ -0,0 +1,473 @@
---
title: liteyuki.mkdoc
order: 1
icon: laptop-code
category: API
---
### ***def*** `get_relative_path(base_path: str, target_path: str) -> str`
获取相对路径
Args:
base_path: 基础路径
target_path: 目标路径
<details>
<summary>源代码</summary>
```python
def get_relative_path(base_path: str, target_path: str) -> str:
"""
获取相对路径
Args:
base_path: 基础路径
target_path: 目标路径
"""
return os.path.relpath(target_path, base_path)
```
</details>
### ***def*** `write_to_files(file_data: dict[str, str]) -> None`
输出文件
Args:
file_data: 文件数据 相对路径
<details>
<summary>源代码</summary>
```python
def write_to_files(file_data: dict[str, str]):
"""
输出文件
Args:
file_data: 文件数据 相对路径
"""
for rp, data in file_data.items():
if not os.path.exists(os.path.dirname(rp)):
os.makedirs(os.path.dirname(rp))
with open(rp, 'w', encoding='utf-8') as f:
f.write(data)
```
</details>
### ***def*** `get_file_list(module_folder: str) -> None`
<details>
<summary>源代码</summary>
```python
def get_file_list(module_folder: str):
file_list = []
for root, dirs, files in os.walk(module_folder):
for file in files:
if file.endswith(('.py', '.pyi')):
file_list.append(os.path.join(root, file))
return file_list
```
</details>
### ***def*** `get_module_info_normal(file_path: str, ignore_private: bool) -> ModuleInfo`
获取函数和类
Args:
file_path: Python 文件路径
ignore_private: 忽略私有函数和类
Returns:
模块信息
<details>
<summary>源代码</summary>
```python
def get_module_info_normal(file_path: str, ignore_private: bool=True) -> ModuleInfo:
"""
获取函数和类
Args:
file_path: Python 文件路径
ignore_private: 忽略私有函数和类
Returns:
模块信息
"""
with open(file_path, 'r', encoding='utf-8') as file:
file_content = file.read()
tree = ast.parse(file_content)
dot_sep_module_path = file_path.replace(os.sep, '.').replace('.py', '').replace('.pyi', '')
module_docstring = ast.get_docstring(tree)
module_info = ModuleInfo(module_path=dot_sep_module_path, functions=[], classes=[], attributes=[], docstring=module_docstring if module_docstring else '')
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if not any((isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(node))) and (not ignore_private or not node.name.startswith('_')):
if node.args.args:
first_arg = node.args.args[0]
if first_arg.arg in ('self', 'cls'):
continue
function_docstring = ast.get_docstring(node)
func_info = FunctionInfo(name=node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args], return_type=ast.unparse(node.returns) if node.returns else 'None', docstring=function_docstring if function_docstring else '', type=DefType.FUNCTION, is_async=isinstance(node, ast.AsyncFunctionDef), source_code=ast.unparse(node))
module_info.functions.append(func_info)
elif isinstance(node, ast.ClassDef):
class_docstring = ast.get_docstring(node)
class_info = ClassInfo(name=node.name, docstring=class_docstring if class_docstring else '', methods=[], attributes=[], inherit=[ast.unparse(base) for base in node.bases])
for class_node in node.body:
if isinstance(class_node, ast.FunctionDef) and (not ignore_private or not class_node.name.startswith('_') or class_node.name == '__init__'):
method_docstring = ast.get_docstring(class_node)
def_type = DefType.METHOD
if class_node.decorator_list:
if any((isinstance(decorator, ast.Name) and decorator.id == 'staticmethod' for decorator in class_node.decorator_list)):
def_type = DefType.STATIC_METHOD
elif any((isinstance(decorator, ast.Name) and decorator.id == 'classmethod' for decorator in class_node.decorator_list)):
def_type = DefType.CLASS_METHOD
elif any((isinstance(decorator, ast.Name) and decorator.id == 'property' for decorator in class_node.decorator_list)):
def_type = DefType.PROPERTY
class_info.methods.append(FunctionInfo(name=class_node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in class_node.args.args], return_type=ast.unparse(class_node.returns) if class_node.returns else 'None', docstring=method_docstring if method_docstring else '', type=def_type, is_async=isinstance(class_node, ast.AsyncFunctionDef), source_code=ast.unparse(class_node)))
elif isinstance(class_node, ast.Assign):
for target in class_node.targets:
if isinstance(target, ast.Name):
class_info.attributes.append(AttributeInfo(name=target.id, type=ast.unparse(class_node.value)))
module_info.classes.append(class_info)
elif isinstance(node, ast.Assign):
if not any((isinstance(parent, (ast.ClassDef, ast.FunctionDef)) for parent in ast.iter_child_nodes(node))):
for target in node.targets:
if isinstance(target, ast.Name) and (not ignore_private or not target.id.startswith('_')):
attr_type = NO_TYPE_HINT
if isinstance(node.value, ast.AnnAssign) and node.value.annotation:
attr_type = ast.unparse(node.value.annotation)
module_info.attributes.append(AttributeInfo(name=target.id, type=attr_type, value=ast.unparse(node.value) if node.value else None))
return module_info
```
</details>
### ***def*** `generate_markdown(module_info: ModuleInfo, front_matter: Any) -> str`
生成模块的Markdown
你可在此自定义生成的Markdown格式
Args:
module_info: 模块信息
front_matter: 自定义选项title, index, icon, category
Returns:
Markdown 字符串
<details>
<summary>源代码</summary>
```python
def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
"""
生成模块的Markdown
你可在此自定义生成的Markdown格式
Args:
module_info: 模块信息
front_matter: 自定义选项title, index, icon, category
Returns:
Markdown 字符串
"""
content = ''
front_matter = '---\n' + '\n'.join([f'{k}: {v}' for k, v in front_matter.items()]) + '\n---\n\n'
content += front_matter
for func in module_info.functions:
args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in func.args]
content += f"### ***{('async ' if func.is_async else '')}def*** `{func.name}({', '.join(args_with_type)}) -> {func.return_type}`\n\n"
func.docstring = func.docstring.replace('\n', '\n\n')
content += f'{func.docstring}\n\n'
content += f'<details>\n<summary>源代码</summary>\n\n```python\n{func.source_code}\n```\n</details>\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:
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'
content += f'<details>\n<summary>源代码</summary>\n\n```python\n{method.source_code}\n```\n</details>\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
```
</details>
### ***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: 忽略的路径
<details>
<summary>源代码</summary>
```python
def generate_docs(module_folder: str, output_dir: str, with_top: bool=False, ignored_paths=None):
"""
生成文档
Args:
module_folder: 模块文件夹
output_dir: 输出文件夹
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b True时例如docs/api/module/module_a.md docs/api/module/module_b.md
ignored_paths: 忽略的路径
"""
if ignored_paths is None:
ignored_paths = []
file_data: dict[str, str] = {}
file_list = get_file_list(module_folder)
shutil.rmtree(output_dir, ignore_errors=True)
os.mkdir(output_dir)
replace_data = {'__init__': 'README', '.py': '.md'}
for pyfile_path in file_list:
if any((ignored_path.replace('\\', '/') in pyfile_path.replace('\\', '/') for ignored_path in ignored_paths)):
continue
no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path)
rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path
for rk, rv in replace_data.items():
rel_md_path = rel_md_path.replace(rk, rv)
abs_md_path = os.path.join(output_dir, rel_md_path)
module_info = get_module_info_normal(pyfile_path)
if 'README' in abs_md_path:
front_matter = {'title': module_info.module_path.replace('.__init__', '').replace('_', '\\n'), 'index': 'true', 'icon': 'laptop-code', 'category': 'API'}
else:
front_matter = {'title': module_info.module_path.replace('_', '\\n'), 'order': '1', 'icon': 'laptop-code', 'category': 'API'}
md_content = generate_markdown(module_info, front_matter)
print(f'Generate {pyfile_path} -> {abs_md_path}')
file_data[abs_md_path] = md_content
write_to_files(file_data)
```
</details>
### ***class*** `DefType(Enum)`
### &emsp; ***attr*** `FUNCTION: 'function'`
### &emsp; ***attr*** `METHOD: 'method'`
### &emsp; ***attr*** `STATIC_METHOD: 'staticmethod'`
### &emsp; ***attr*** `CLASS_METHOD: 'classmethod'`
### &emsp; ***attr*** `PROPERTY: 'property'`
### ***class*** `FunctionInfo(BaseModel)`
### ***class*** `AttributeInfo(BaseModel)`
### ***class*** `ClassInfo(BaseModel)`
### ***class*** `ModuleInfo(BaseModel)`
### ***var*** `NO_TYPE_ANY = 'Any'`
### ***var*** `NO_TYPE_HINT = 'NoTypeHint'`
### ***var*** `FUNCTION = 'function'`
### ***var*** `METHOD = 'method'`
### ***var*** `STATIC_METHOD = 'staticmethod'`
### ***var*** `CLASS_METHOD = 'classmethod'`
### ***var*** `PROPERTY = 'property'`
### ***var*** `file_list = []`
### ***var*** `dot_sep_module_path = file_path.replace(os.sep, '.').replace('.py', '').replace('.pyi', '')`
### ***var*** `module_docstring = ast.get_docstring(tree)`
### ***var*** `module_info = ModuleInfo(module_path=dot_sep_module_path, functions=[], classes=[], attributes=[], docstring=module_docstring if module_docstring else '')`
### ***var*** `content = ''`
### ***var*** `front_matter = '---\n' + '\n'.join([f'{k}: {v}' for k, v in front_matter.items()]) + '\n---\n\n'`
### ***var*** `file_list = get_file_list(module_folder)`
### ***var*** `replace_data = {'__init__': 'README', '.py': '.md'}`
### ***var*** `file_content = file.read()`
### ***var*** `tree = ast.parse(file_content)`
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in func.args]`
### ***var*** `ignored_paths = []`
### ***var*** `no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path)`
### ***var*** `rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path`
### ***var*** `abs_md_path = os.path.join(output_dir, rel_md_path)`
### ***var*** `module_info = get_module_info_normal(pyfile_path)`
### ***var*** `md_content = generate_markdown(module_info, front_matter)`
### ***var*** `inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ''`
### ***var*** `rel_md_path = rel_md_path.replace(rk, rv)`
### ***var*** `front_matter = {'title': module_info.module_path.replace('.__init__', '').replace('_', '\\n'), 'index': 'true', 'icon': 'laptop-code', 'category': 'API'}`
### ***var*** `front_matter = {'title': module_info.module_path.replace('_', '\\n'), 'order': '1', 'icon': 'laptop-code', 'category': 'API'}`
### ***var*** `function_docstring = ast.get_docstring(node)`
### ***var*** `func_info = FunctionInfo(name=node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args], return_type=ast.unparse(node.returns) if node.returns else 'None', docstring=function_docstring if function_docstring else '', type=DefType.FUNCTION, is_async=isinstance(node, ast.AsyncFunctionDef), source_code=ast.unparse(node))`
### ***var*** `class_docstring = ast.get_docstring(node)`
### ***var*** `class_info = ClassInfo(name=node.name, docstring=class_docstring if class_docstring else '', methods=[], attributes=[], inherit=[ast.unparse(base) for base in node.bases])`
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in method.args]`
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] and arg[0] != 'self' else arg[0] for arg in method.args]`
### ***var*** `first_arg = node.args.args[0]`
### ***var*** `method_docstring = ast.get_docstring(class_node)`
### ***var*** `def_type = DefType.METHOD`
### ***var*** `def_type = DefType.STATIC_METHOD`
### ***var*** `attr_type = NO_TYPE_HINT`
### ***var*** `def_type = DefType.CLASS_METHOD`
### ***var*** `attr_type = ast.unparse(node.value.annotation)`
### ***var*** `def_type = DefType.PROPERTY`

View File

@ -0,0 +1,29 @@
---
title: liteyuki.plugin
index: true
icon: laptop-code
category: API
---
### ***def*** `get_loaded_plugins() -> dict[str, Plugin]`
获取已加载的插件
Returns:
dict[str, Plugin]: 插件字典
<details>
<summary>源代码</summary>
```python
def get_loaded_plugins() -> dict[str, Plugin]:
"""
获取已加载的插件
Returns:
dict[str, Plugin]: 插件字典
"""
return _plugins
```
</details>

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

@ -0,0 +1,199 @@
---
title: liteyuki.plugin.load
order: 1
icon: laptop-code
category: API
---
### ***def*** `load_plugin(module_path: str | Path) -> Optional[Plugin]`
加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
参数:
module_path: 插件名称 `path.to.your.plugin`
或插件路径 `pathlib.Path(path/to/your/plugin)`
<details>
<summary>源代码</summary>
```python
def load_plugin(module_path: str | Path) -> Optional[Plugin]:
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
参数:
module_path: 插件名称 `path.to.your.plugin`
或插件路径 `pathlib.Path(path/to/your/plugin)`
"""
module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path
try:
module = import_module(module_path)
_plugins[module.__name__] = Plugin(name=module.__name__, module=module, module_name=module_path, metadata=module.__dict__.get('__plugin_metadata__', None))
display_name = module.__name__.split('.')[-1]
if module.__dict__.get('__plugin_meta__'):
metadata: 'PluginMetadata' = module.__dict__['__plugin_meta__']
display_name = format_display_name(f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type)
logger.opt(colors=True).success(f'Succeeded to load liteyuki plugin "{display_name}"')
return _plugins[module.__name__]
except Exception as e:
logger.opt(colors=True).success(f'Failed to load liteyuki plugin "<r>{module_path}</r>"')
traceback.print_exc()
return None
```
</details>
### ***def*** `load_plugins() -> set[Plugin]`
导入文件夹下多个插件
参数:
plugin_dir: 文件夹路径
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
<details>
<summary>源代码</summary>
```python
def load_plugins(*plugin_dir: str, ignore_warning: bool=True) -> set[Plugin]:
"""导入文件夹下多个插件
参数:
plugin_dir: 文件夹路径
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
"""
plugins = set()
for dir_path in plugin_dir:
if not os.path.exists(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' does not exist.")
continue
if not os.listdir(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' is empty.")
continue
if not os.path.isdir(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' is not a directory.")
continue
for f in os.listdir(dir_path):
path = Path(os.path.join(dir_path, f))
module_name = None
if os.path.isfile(path) and f.endswith('.py') and (f != '__init__.py'):
module_name = f'{path_to_module_name(Path(dir_path))}.{f[:-3]}'
elif os.path.isdir(path) and os.path.exists(os.path.join(path, '__init__.py')):
module_name = path_to_module_name(path)
if module_name:
load_plugin(module_name)
if _plugins.get(module_name):
plugins.add(_plugins[module_name])
return plugins
```
</details>
### ***def*** `format_display_name(display_name: str, plugin_type: PluginType) -> str`
设置插件名称颜色,根据不同类型插件设置颜色
Args:
display_name: 插件名称
plugin_type: 插件类型
Returns:
str: 设置后的插件名称 <y>name</y>
<details>
<summary>源代码</summary>
```python
def format_display_name(display_name: str, plugin_type: PluginType) -> str:
"""
设置插件名称颜色,根据不同类型插件设置颜色
Args:
display_name: 插件名称
plugin_type: 插件类型
Returns:
str: 设置后的插件名称 <y>name</y>
"""
color = 'y'
match plugin_type:
case PluginType.APPLICATION:
color = 'm'
case PluginType.TEST:
color = 'g'
case PluginType.MODULE:
color = 'e'
case PluginType.SERVICE:
color = 'c'
return f'<{color}>{display_name} [{plugin_type.name}]</{color}>'
```
</details>
### ***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*** `MODULE: 'module'`
### &emsp; ***attr*** `UNCLASSIFIED: 'unclassified'`
### &emsp; ***attr*** `TEST: 'test'`
### ***class*** `PluginMetadata(BaseModel)`
轻雪插件元数据由插件编写者提供name为必填项
Attributes:
----------
name: str
插件名称
description: str
插件描述
usage: str
插件使用方法
type: str
插件类型
author: str
插件作者
homepage: str
插件主页
extra: dict[str, Any]
额外信息
### ***class*** `Plugin(BaseModel)`
存储插件信息
### &emsp; ***attr*** `model_config: {'arbitrary_types_allowed': True}`
### ***var*** `APPLICATION = 'application'`
### ***var*** `SERVICE = 'service'`
### ***var*** `MODULE = 'module'`
### ***var*** `UNCLASSIFIED = 'unclassified'`
### ***var*** `TEST = 'test'`
### ***var*** `model_config = {'arbitrary_types_allowed': True}`

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

@ -0,0 +1,180 @@
---
title: liteyuki.utils
order: 1
icon: laptop-code
category: API
---
### ***def*** `is_coroutine_callable(call: Callable[..., Any]) -> bool`
判断是否为协程可调用对象
Args:
call: 可调用对象
Returns:
bool: 是否为协程可调用对象
<details>
<summary>源代码</summary>
```python
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
"""
判断是否为协程可调用对象
Args:
call: 可调用对象
Returns:
bool: 是否为协程可调用对象
"""
if inspect.isroutine(call):
return inspect.iscoroutinefunction(call)
if inspect.isclass(call):
return False
func_ = getattr(call, '__call__', None)
return inspect.iscoroutinefunction(func_)
```
</details>
### ***def*** `run_coroutine() -> None`
运行协程
Args:
coro:
Returns:
<details>
<summary>源代码</summary>
```python
def run_coroutine(*coro: Coroutine):
"""
运行协程
Args:
coro:
Returns:
"""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
for c in coro:
asyncio.ensure_future(c)
else:
for c in coro:
loop.run_until_complete(c)
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(asyncio.gather(*coro))
loop.close()
except Exception as e:
logger.error(f'Exception occurred: {e}')
```
</details>
### ***def*** `path_to_module_name(path: Path) -> str`
转换路径为模块名
Args:
path: 路径a/b/c/d -> a.b.c.d
Returns:
str: 模块名
<details>
<summary>源代码</summary>
```python
def path_to_module_name(path: Path) -> str:
"""
转换路径为模块名
Args:
path: 路径a/b/c/d -> a.b.c.d
Returns:
str: 模块名
"""
rel_path = path.resolve().relative_to(Path.cwd().resolve())
if rel_path.stem == '__init__':
return '.'.join(rel_path.parts[:-1])
else:
return '.'.join(rel_path.parts[:-1] + (rel_path.stem,))
```
</details>
### ***def*** `async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]`
异步包装器
Args:
func: Sync Callable
Returns:
Coroutine: Asynchronous Callable
<details>
<summary>源代码</summary>
```python
def async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]:
"""
异步包装器
Args:
func: Sync Callable
Returns:
Coroutine: Asynchronous Callable
"""
async def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.__signature__ = inspect.signature(func)
return wrapper
```
</details>
### ***async def*** `wrapper() -> None`
<details>
<summary>源代码</summary>
```python
async def wrapper(*args, **kwargs):
return func(*args, **kwargs)
```
</details>
### ***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()`

99
docs/dev/dev_comm.md Normal file
View File

@ -0,0 +1,99 @@
---
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,15 +1,13 @@
---
title: 轻雪函数
icon: code
order: 4
category: 使用指南
tag:
- 配置
order: 2
category: 开发
---
## **轻雪函数**
轻雪函数 Liteyuki Function 是轻雪的一个功能它允许你在轻雪中运行一些自定义的由数据驱动的命令类似于Minecraft的mcfunction.
轻雪函数 Liteyuki Function 是轻雪的一个功能它允许你在轻雪中运行一些自定义的由数据驱动的命令类似于Minecraft的mcfunction,属于资源包的一部分,但需单独起篇幅.
### **函数文件**
@ -70,3 +68,7 @@ await
> [!warning]
> 但若出现非单function的情况有一个task任务没有完成而await被执行了那么当前所有函数包的task都会被截停销毁
> [!tip]
> 编写轻雪函数推荐你使用VS Code插件[Liteyuki Function](https://github.com/LiteyukiStudio/lyfunctionTextmate)实现语法高亮

82
docs/dev/dev_lyplugin.md Normal file
View File

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

82
docs/en/README.md Normal file
View File

@ -0,0 +1,82 @@
---
home: true
icon: home
title: Home
heroImage: https://cdn.liteyuki.icu/static/svg/lylogo-full.svg
heroImageDark: https://cdn.liteyuki.icu/static/svg/lylogo-full-dark.svg
bgImage:
bgImageDark:
bgImageStyle:
background-attachment: fixed
heroText: LiteyukiBot
tagline: LiteyukiBot A high-performance, easy-to-use chatbot framework and application
actions:
- text: Get Started
icon: rocket
link: ./deploy/install.html
type: primary
- text: Usage
icon: book
link: ./usage/basic_command.html
highlights:
- header: Simple and Efficient
image: /assets/image/layout.svg
bgImage: https://theme-hope-assets.vuejs.press/bg/2-light.svg
bgImageDark: https://theme-hope-assets.vuejs.press/bg/2-dark.svg
bgImageStyle:
background-repeat: repeat
background-size: initial
features:
- title: Multi-Framework Support
icon: robot
details: Compatible with nonebot, melobot, etc., with good ecological support
link: https://nonebot.dev/
- title: Convenient Management
icon: plug
details: Use package manager to manage plugins and resource packs
- title: Custom Themes Support
icon: paint-brush
details: Fully customize the appearance with resource packs
link: https://bot.liteyuki.icu/usage/resource_pack.html
- title: i18n
icon: globe
details: Support multiple languages through resource packs
link: https://baike.baidu.com/item/i18n/6771940
- title: Easy to Use
icon: cog
details: No need for cumbersome pre-processes, ready to use
link: https://bot.liteyuki.icu/deployment/config.html
- title: High Performance
icon: tachometer-alt
details: 500 plugins, start within 2s
- title: Rolling Update
icon: cloud-download
details: Keep your bot up to date
- title: OpenSource
icon: code
details: MIT LICENCE open source project, welcome your contribution
- header: Quick Start
image: /assets/image/box.svg
bgImage: https://theme-hope-assets.vuejs.press/bg/3-light.svg
bgImageDark: https://theme-hope-assets.vuejs.press/bg/3-dark.svg
highlights:
- title: Install Git and Python3.10+ environment
- title: Use <code>git clone https://github.com/LiteyukiStudio/LiteyukiBot --depth=1</code> to clone the project locally
- title: Use <code>cd LiteyukiBot</code> to change the directory to the project root
- title: Use <code>pip install -r requirements.txt</code> install the project dependencies
details: If you have multiple Python environments, please use <code>pythonx -m pip install -r requirements.txt</code>.
- title: Start bot with <code>python main.py</code>.
copyright: © 2021-2024 SnowyKami All Rights Reserved
---

8
docs/en/deploy/README.md Normal file
View File

@ -0,0 +1,8 @@
---
title: Deploy
index: false
icon: laptop-code
category: deploy
---
<Catalog />

9
docs/en/deploy/cfg.txt Normal file
View File

@ -0,0 +1,9 @@
# note
开发者选项
allow_update: true # 是否允许更新
log_level: "INFO" # 日志等级
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
auto_report: true # 是否自动上报问题给轻雪服务器
auto_update: true # 是否自动更新轻雪每天4点检查更新
safe_mode: false # 安全模式,开启后将不会加载任何第三方插件
dev_mode: false # 开发者模式,开启后将会启动看门狗

77
docs/en/deploy/config.md Normal file
View File

@ -0,0 +1,77 @@
---
title: Configuration
icon: cog
order: 2
category: deployment
tag:
- Configuration
---
轻雪支持`yaml``json``toml`作为配置文件,取决于你个人的喜好
首次运行后生成`config.yml``config`目录,你可修改配置项后重启轻雪,绝大多数情况下,你只需要修改`superusers``nickname`字段即可
启动时会加载项目目录下`config.yml/yaml/json/toml``config`目录下的所有配置文件,你可在`config`目录下创建多个配置文件,轻雪会自动合并这些配置文件
## **基础配置项**
```yaml
nonebot:
# Nonebot机器人的配置以前的最外层配置项仍可为Nonebot服务但是部分内容会被覆盖请尽快迁移
command_start: [ "/", "" ] # 指令前缀,若没有""空命令头请开启alconna_use_command_start保证alconna解析正常
host: 127.0.0.1 # 监听地址默认为本机若要接收外部请求请填写0.0.0.0
port: 20216 # 绑定端口
nickname: [ "liteyuki" ] # 机器人昵称列表
superusers: [ "1919810" ] # 超级用户列表
liteyuki:
# 写在外层的配置项将会被覆盖建议迁移到liteyuki下
log_level: "INFO" # 日志等级
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
auto_report: true # 是否自动上报问题给轻雪服务器
auto_update: true # 是否自动更新轻雪每天4点检查更新
plugins: [ ] # 轻雪插件列表
plugin_dirs: [ ] # 轻雪插件目录列表
```
## **其他配置**
以下为默认值,如需自定义请手动添加
```yaml
# 高级NoneBot配置
nonebot:
onebot_access_token: "" # 访问令牌,对公开放时建议设置
default_language: "zh-CN" # 默认语言
alconna_auto_completion: false # alconna是否自动补全指令默认false建议开启
safe_mode: false # 安全模式开启后将不会加载任何第三方NoneBot插件
# 其他Nonebot插件的配置项
custom_config_1: "custom_value1"
custom_config_2: "custom_value2"
# 开发者选项
liteyuki:
allow_update: true # 是否允许更新
debug: false # 轻雪调试开启会自动重载Bot或者资源其他插件自带的调试功能也将开启
dev_mode: false # 开发者模式,开启后将会启动监视者,监视文件变化并自动重载
...
```
> [!tip]
> 如果要使用NoneBot和dotenv配置文件请自行创建`.env.{ENVIRONMENT}`,并在`config.yml`中添加`nonebot.environment:{ENVIRONMENT}`字段
## **与NoneBot对接的OneBot实现端配置**
生产环境中推荐反向WebSocket
不同的实现端给出的字段可能不同,但是基本上都是一样的,这里给出一个参考值
| 字段 | 参考值 | 说明 |
|-------------|------------------------------------|----------------------------------|
| 协议 | 反向WebSocket | 推荐使用反向ws协议进行通信即轻雪作为服务端 |
| 地址 | ws://127.0.0.1:20216/onebot/v11/ws | 地址取决于配置文件,本机默认为`127.0.0.1:20216` |
| AccessToken | `""` | 如果你给轻雪配置了`AccessToken`,请在此填写相同的值 |
## **其他**
- 要使用其他通信方式请访问[OneBot Adapter](https://onebot.adapters.nonebot.dev/)获取详细信息
- 轻雪不局限于OneBot适配器你可以使用NoneBot2支持的任何适配器

57
docs/en/deploy/fandq.md Normal file
View File

@ -0,0 +1,57 @@
---
title: FAQ
icon: question
order: 3
category: deployment
tag:
- FAQ
---
## **常见问题**
- 设备上Python环境太乱了pip和python不对应怎么办
- 请使用`/path/to/python -m pip install -r requirements.txt`来安装依赖,
然后用`/path/to/python main.py`来启动Bot
其中`/path/to/python`是你要用来运行Bot的可执行文件
- 为什么我启动后机器人没有反应?
- 请检查配置文件的`command_start``superusers`,确认你有权限使用命令并按照正确的命令发送
- 确认命令头没有和`nickname{}`冲突,例如一个命令是`help`,但是`Bot`昵称有一个`help`那么将会被解析为nickname而不是命令
- 更新轻雪失败,报错`InvalidGitRepositoryError`
- 请正确安装`Git`,并使用克隆而非直接下载的方式部署轻雪
- 怎么登录聊天平台例如QQ
- 你有这个问题说明你不是很了解这个项目,本项目不负责实现登录功能,只负责处理和回应消息,登录功能由实现端(协议端)提供,
实现端本身不负责处理响应逻辑将消息按照OneBot标准处理好上报给轻雪
你需要使用Onebot标准的实现端来连接到轻雪并将消息上报给轻雪下面已经列出一些推荐的实现端
- `Playwright`安装失败
- 输入`playwright install`安装浏览器
- 有的插件安装后报错无法启动
- 请先查阅插件文档,确认插件必要配置项完好后,仍然出现问题,请联系插件作者或在安全模式`safe_mode: true`下启动轻雪,在安全模式下你可以使用`npm uninstall`卸载问题插件
- 其他问题
-
加入QQ群[775840726](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=SzmDYbfR6jY94o9KFNon7AwelRyI6M_u&authKey=ygeBdEmdFNyCWuNR4w0M1M8%2B5oDg7k%2FDfN0tzBkYcnbB%2FGHNnlVEnCIGbdftsnn7&noverify=0&group_code=775840726)
## **推荐方案(QQ)**
1. [Lagrange.OneBot](https://github.com/KonataDev/Lagrange.Core)基于NTQQ的OneBot实现目前Markdown消息支持Lagrange
2. [LLOneBot](https://github.com/LLOneBot/LLOneBot)NTQQ的OneBot插件需要安装NTQQ
3. [OpenShamrock](https://github.com/whitechi73/OpenShamrock)基于Lsposed的OneBot11实现
4. [TRSS-Yunzai](https://github.com/TimeRainStarSky/Yunzai),基于`node.js`,可使用`ws-plugin`进行通信
5. [go-cqhttp](https://github.com/Mrs4s/go-cqhttp)`go`语言实现的OneBot11实现端目前可用性较低
6. [Gensokyo](https://github.com/Hoshinonyaruko/Gensokyo),基于 OneBot QQ官方机器人Api Golang 原生实现,需要官方机器人权限
7. 人工实现的`Onebot`协议自己整一个WebSocket客户端看着QQ的消息然后给轻雪传输数据
## **推荐方案(Minecraft)**
1. [MinecraftOneBot](https://github.com/snowykami/MinecraftOnebot)我们专门为Minecraft开发的服务器Bot支持OneBotV11标准
使用其他项目连接请先自行查阅文档若有困难请联系对应开发者而不是Liteyuki的开发者
## **鸣谢**
- [Nonebot2](https://nonebot.dev)提供的框架支持
- [nonebot-plugin-htmlrender](https://github.com/kexue-z/nonebot-plugin-htmlrender/tree/master)提供的渲染功能
- [nonebot-plugin-alconna](https://github.com/ArcletProject/nonebot-plugin-alconna)提供的命令解析功能
- [MiSans](https://hyperos.mi.com/font/zh/)[MapleMono](https://gitee.com/mirrors/Maple-Mono)提供的字体,且遵守了相关字体开源协议

61
docs/en/deploy/install.md Normal file
View File

@ -0,0 +1,61 @@
---
title: Installation
icon: download
order: 1
category: deployment
tag:
- 安装
---
## **Installation**
### **Conventional deployment**
1. Install [`Git`](https://git-scm.com/download/) and [`Python3.10+`](https://www.python.org/downloads/release/python-31010/) environment
```bash
# Clone the project locally, --depth=1 to reduce the size of the cloned repository, this project updates depend on Git
git clone https://github.com/LiteyukiStudio/LiteyukiBot --depth=1
# change the directory to the project root
cd LiteyukiBot
# install the project dependencies
pip install -r requirements.txt
# start bot
python main.py
```
> [!tip]
> Recommended to use `venv` to run Liteyuki to avoid dependency conflicts, you can use `python -m venv .venv` to create a virtual environment, and then use `.venv\Scripts\activate` to activate the virtual environment (use `source .venv/bin/activate` to activate on Linux)
### **Use docker**
1. Install [`Docker`](https://docs.docker.com/get-docker/)
2. Clone project repo `git clone https://github.com/LiteyukiStudio/LiteyukiBot --depth=1`
3. change directory `cd LiteyukiBot`
4. build image with `docker build -t liteyukibot .`
5. start container `docker run -p 20216:20216 -v $(pwd):/liteyukibot -v $(pwd)/.cache:/root/.cache liteyukibot`
> [!tip]
> For Windows, please use the absolute project directory `/path/to/LiteyukiBot` instead of $(pwd)
>
> If you have changed the port number, replace `20216` in `20216:20216` with your port number
### **Use TRSS Scripts**
[TRSS_Liteyuki LiteyukiBot manage script](https://timerainstarsky.github.io/TRSS_Liteyuki/), This feature is supported by TRSS and is not an official feature of LiteyukiBot. It is recommended to use Arch Linux.
## **Device requirements**
- Minimum Windows system version: `Windows 10+` / `Windows Server 2019+`
- Linux systems need to support Python 3.10+, with `Ubuntu 20.04+` recommended
- CPU: `1 vCPU` and more(Bot is multi processes, the more cores, the better the performance)
- Memory: Without other plugins, the Bot will occupy `300~500MB`, including processes like `chromium` and `node`. The memory occupied by other plugins depends on the specific plugins, and it is recommended to have more than `1GB`.
- Storage: At least `1GB` of space is required.
> [!warning]
> If there are multiple environments on the device, please use `path/to/python -m pip install -r requirements.txt` to install dependencies, where `path/to/python` is the path to your Python executable.
> [!warning]
> The update feature of Liteyuki depends on Git. If you have not installed Git and directly download the source code to run, you will not be able to use the update feature.
#### For other issues, please go to [Q&A](/deployment/fandq)

8
docs/en/dev/README.md Normal file
View File

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

View File

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

View File

@ -0,0 +1,581 @@
---
title: liteyuki.bot
index: true
icon: laptop-code
category: API
---
### ***def*** `get_bot() -> LiteyukiBot`
获取轻雪实例
Returns:
LiteyukiBot: 当前的轻雪实例
<details>
<summary>源代码</summary>
```python
def get_bot() -> LiteyukiBot:
"""
获取轻雪实例
Returns:
LiteyukiBot: 当前的轻雪实例
"""
if IS_MAIN_PROCESS:
if _BOT_INSTANCE is None:
raise RuntimeError('Liteyuki instance not initialized.')
return _BOT_INSTANCE
else:
raise RuntimeError("Can't get bot instance in sub process.")
```
</details>
### ***def*** `get_config(key: str, default: Any) -> Any`
获取配置
Args:
key: 配置键
default: 默认值
Returns:
Any: 配置值
<details>
<summary>源代码</summary>
```python
def get_config(key: str, default: Any=None) -> Any:
"""
获取配置
Args:
key: 配置键
default: 默认值
Returns:
Any: 配置值
"""
return get_bot().config.get(key, default)
```
</details>
### ***def*** `get_config_with_compat(key: str, compat_keys: tuple[str], default: Any) -> Any`
获取配置,兼容旧版本
Args:
key: 配置键
compat_keys: 兼容键
default: 默认值
Returns:
Any: 配置值
<details>
<summary>源代码</summary>
```python
def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any=None) -> Any:
"""
获取配置,兼容旧版本
Args:
key: 配置键
compat_keys: 兼容键
default: 默认值
Returns:
Any: 配置值
"""
if key in get_bot().config:
return get_bot().config[key]
for compat_key in compat_keys:
if compat_key in get_bot().config:
logger.warning(f'Config key "{compat_key}" will be deprecated, use "{key}" instead.')
return get_bot().config[compat_key]
return default
```
</details>
### ***def*** `print_logo() -> None`
<details>
<summary>源代码</summary>
```python
def print_logo():
print('\x1b[34m' + '\n __ ______ ________ ________ __ __ __ __ __ __ ______ \n / | / |/ |/ |/ \\ / |/ | / |/ | / |/ |\n $$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \\ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/ \n $$ | $$ | $$ | $$ |__ $$ \\/$$/ $$ | $$ |$$ |/$$/ $$ | \n $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ | \n $$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \\ $$ | \n $$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \\__$$ |$$ |$$ \\ _$$ |_ \n $$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |\n $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/ \n ' + '\x1b[0m')
```
</details>
### ***class*** `LiteyukiBot`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;初始化轻雪实例
Args:
*args:
**kwargs: 配置
<details>
<summary>源代码</summary>
```python
def __init__(self, *args, **kwargs) -> None:
"""
初始化轻雪实例
Args:
*args:
**kwargs: 配置
"""
'常规操作'
print_logo()
global _BOT_INSTANCE
_BOT_INSTANCE = self
'配置'
self.config: dict[str, Any] = kwargs
'初始化'
self.init(**self.config)
logger.info('Liteyuki is initializing...')
'生命周期管理'
self.lifespan = Lifespan()
self.process_manager: ProcessManager = ProcessManager(lifespan=self.lifespan)
'事件循环'
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.stop_event = threading.Event()
self.call_restart_count = 0
'加载插件加载器'
load_plugin('liteyuki.plugins.plugin_loader')
'信号处理'
signal.signal(signal.SIGINT, self._handle_exit)
signal.signal(signal.SIGTERM, self._handle_exit)
atexit.register(self.process_manager.terminate_all)
```
</details>
### &emsp; ***def*** `run(self) -> None`
&emsp;启动逻辑
<details>
<summary>源代码</summary>
```python
def run(self):
"""
启动逻辑
"""
self.lifespan.before_start()
self.process_manager.start_all()
self.lifespan.after_start()
self.keep_alive()
```
</details>
### &emsp; ***def*** `keep_alive(self) -> None`
&emsp;保持轻雪运行
Returns:
<details>
<summary>源代码</summary>
```python
def keep_alive(self):
"""
保持轻雪运行
Returns:
"""
try:
while not self.stop_event.is_set():
time.sleep(0.5)
except KeyboardInterrupt:
logger.info('Liteyuki is stopping...')
self.stop()
```
</details>
### &emsp; ***def*** `restart(self, delay: int) -> None`
&emsp;重启轻雪本体
Returns:
<details>
<summary>源代码</summary>
```python
def restart(self, delay: int=0):
"""
重启轻雪本体
Returns:
"""
if self.call_restart_count < 1:
executable = sys.executable
args = sys.argv
logger.info('Restarting LiteyukiBot...')
time.sleep(delay)
if platform.system() == 'Windows':
cmd = 'start'
elif platform.system() == 'Linux':
cmd = 'nohup'
elif platform.system() == 'Darwin':
cmd = 'open'
else:
cmd = 'nohup'
self.process_manager.terminate_all()
threading.Thread(target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",)).start()
sys.exit(0)
self.call_restart_count += 1
```
</details>
### &emsp; ***def*** `restart_process(self, name: Optional[str]) -> None`
&emsp;停止轻雪
Args:
name: 进程名称, 默认为None, 所有进程
Returns:
<details>
<summary>源代码</summary>
```python
def restart_process(self, name: Optional[str]=None):
"""
停止轻雪
Args:
name: 进程名称, 默认为None, 所有进程
Returns:
"""
self.lifespan.before_process_shutdown()
self.lifespan.before_process_shutdown()
if name is not None:
chan_active = get_channel(f'{name}-active')
chan_active.send(1)
else:
for process_name in self.process_manager.processes:
chan_active = get_channel(f'{process_name}-active')
chan_active.send(1)
```
</details>
### &emsp; ***def*** `init(self) -> None`
&emsp;初始化轻雪, 自动调用
Returns:
<details>
<summary>源代码</summary>
```python
def init(self, *args, **kwargs):
"""
初始化轻雪, 自动调用
Returns:
"""
self.init_logger()
```
</details>
### &emsp; ***def*** `init_logger(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def init_logger(self):
init_log(config=self.config)
```
</details>
### &emsp; ***def*** `stop(self) -> None`
&emsp;停止轻雪
Returns:
<details>
<summary>源代码</summary>
```python
def stop(self):
"""
停止轻雪
Returns:
"""
self.stop_event.set()
self.loop.stop()
```
</details>
### &emsp; ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册启动前的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_start(self, func: LIFESPAN_FUNC):
"""
注册启动前的函数
Args:
func:
Returns:
"""
return self.lifespan.on_before_start(func)
```
</details>
### &emsp; ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册启动后的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_start(self, func: LIFESPAN_FUNC):
"""
注册启动后的函数
Args:
func:
Returns:
"""
return self.lifespan.on_after_start(func)
```
</details>
### &emsp; ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册停止后的函数:未实现
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_shutdown(self, func: LIFESPAN_FUNC):
"""
注册停止后的函数:未实现
Args:
func:
Returns:
"""
return self.lifespan.on_after_shutdown(func)
```
</details>
### &emsp; ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册进程停止前的函数,为子进程停止时调用
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_process_shutdown(self, func: LIFESPAN_FUNC):
"""
注册进程停止前的函数,为子进程停止时调用
Args:
func:
Returns:
"""
return self.lifespan.on_before_process_shutdown(func)
```
</details>
### &emsp; ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册进程重启前的函数,为子进程重启时调用
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_before_process_restart(self, func: LIFESPAN_FUNC):
"""
注册进程重启前的函数,为子进程重启时调用
Args:
func:
Returns:
"""
return self.lifespan.on_before_process_restart(func)
```
</details>
### &emsp; ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册重启后的函数:未实现
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_restart(self, func: LIFESPAN_FUNC):
"""
注册重启后的函数:未实现
Args:
func:
Returns:
"""
return self.lifespan.on_after_restart(func)
```
</details>
### &emsp; ***def*** `on_after_nonebot_init(self, func: LIFESPAN_FUNC) -> None`
&emsp;注册nonebot初始化后的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
"""
注册nonebot初始化后的函数
Args:
func:
Returns:
"""
return self.lifespan.on_after_nonebot_init(func)
```
</details>
### ***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,450 @@
---
title: liteyuki.bot.lifespan
order: 1
icon: laptop-code
category: API
---
### ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
运行函数
Args:
funcs:
Returns:
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
"""
运行函数
Args:
funcs:
Returns:
"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = []
for func in funcs:
if is_coroutine_callable(func):
tasks.append(func(*args, **kwargs))
else:
tasks.append(async_wrapper(func)(*args, **kwargs))
loop.run_until_complete(asyncio.gather(*tasks))
```
</details>
### ***class*** `Lifespan`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;轻雪生命周期管理,启动、停止、重启
<details>
<summary>源代码</summary>
```python
def __init__(self) -> None:
"""
轻雪生命周期管理,启动、停止、重启
"""
self.life_flag: int = 0
self._before_start_funcs: list[LIFESPAN_FUNC] = []
self._after_start_funcs: list[LIFESPAN_FUNC] = []
self._before_process_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._before_process_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
```
</details>
### &emsp; ***@staticmethod***
### &emsp; ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
&emsp;运行函数
Args:
funcs:
Returns:
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC], *args, **kwargs) -> None:
"""
运行函数
Args:
funcs:
Returns:
"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = []
for func in funcs:
if is_coroutine_callable(func):
tasks.append(func(*args, **kwargs))
else:
tasks.append(async_wrapper(func)(*args, **kwargs))
loop.run_until_complete(asyncio.gather(*tasks))
```
</details>
### &emsp; ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_start_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_start_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册停止前的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止前的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_process_shutdown_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册停止后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_shutdown_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册重启时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启时的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._before_process_restart_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
&emsp;注册重启后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
<details>
<summary>源代码</summary>
```python
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启后的函数
Args:
func:
Returns:
LIFESPAN_FUNC:
"""
self._after_restart_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `on_after_nonebot_init(self, func: Any) -> None`
&emsp;注册 NoneBot 初始化后的函数
Args:
func:
Returns:
<details>
<summary>源代码</summary>
```python
def on_after_nonebot_init(self, func):
"""
注册 NoneBot 初始化后的函数
Args:
func:
Returns:
"""
self._after_nonebot_init_funcs.append(func)
return func
```
</details>
### &emsp; ***def*** `before_start(self) -> None`
&emsp;启动前
Returns:
<details>
<summary>源代码</summary>
```python
def before_start(self) -> None:
"""
启动前
Returns:
"""
logger.debug('Running before_start functions')
self.run_funcs(self._before_start_funcs)
```
</details>
### &emsp; ***def*** `after_start(self) -> None`
&emsp;启动后
Returns:
<details>
<summary>源代码</summary>
```python
def after_start(self) -> None:
"""
启动后
Returns:
"""
logger.debug('Running after_start functions')
self.run_funcs(self._after_start_funcs)
```
</details>
### &emsp; ***def*** `before_process_shutdown(self) -> None`
&emsp;停止前
Returns:
<details>
<summary>源代码</summary>
```python
def before_process_shutdown(self) -> None:
"""
停止前
Returns:
"""
logger.debug('Running before_shutdown functions')
self.run_funcs(self._before_process_shutdown_funcs)
```
</details>
### &emsp; ***def*** `after_shutdown(self) -> None`
&emsp;停止后
Returns:
<details>
<summary>源代码</summary>
```python
def after_shutdown(self) -> None:
"""
停止后
Returns:
"""
logger.debug('Running after_shutdown functions')
self.run_funcs(self._after_shutdown_funcs)
```
</details>
### &emsp; ***def*** `before_process_restart(self) -> None`
&emsp;重启前
Returns:
<details>
<summary>源代码</summary>
```python
def before_process_restart(self) -> None:
"""
重启前
Returns:
"""
logger.debug('Running before_restart functions')
self.run_funcs(self._before_process_restart_funcs)
```
</details>
### &emsp; ***def*** `after_restart(self) -> None`
&emsp;重启后
Returns:
<details>
<summary>源代码</summary>
```python
def after_restart(self) -> None:
"""
重启后
Returns:
"""
logger.debug('Running after_restart functions')
self.run_funcs(self._after_restart_funcs)
```
</details>
### ***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,427 @@
---
title: liteyuki.comm.channel
order: 1
icon: laptop-code
category: API
---
### ***def*** `set_channel(name: str, channel: Channel) -> None`
设置通道实例
Args:
name: 通道名称
channel: 通道实例
<details>
<summary>源代码</summary>
```python
def set_channel(name: str, channel: Channel):
"""
设置通道实例
Args:
name: 通道名称
channel: 通道实例
"""
if not isinstance(channel, Channel):
raise TypeError(f'channel_ must be an instance of Channel, {type(channel)} found')
if IS_MAIN_PROCESS:
_channel[name] = channel
else:
channel_deliver_passive_channel.send(('set_channel', {'name': name, 'channel_': channel}))
```
</details>
### ***def*** `set_channels(channels: dict[str, Channel]) -> None`
设置通道实例
Args:
channels: 通道名称
<details>
<summary>源代码</summary>
```python
def set_channels(channels: dict[str, Channel]):
"""
设置通道实例
Args:
channels: 通道名称
"""
for name, channel in channels.items():
set_channel(name, channel)
```
</details>
### ***def*** `get_channel(name: str) -> Channel`
获取通道实例
Args:
name: 通道名称
Returns:
<details>
<summary>源代码</summary>
```python
def get_channel(name: str) -> Channel:
"""
获取通道实例
Args:
name: 通道名称
Returns:
"""
if IS_MAIN_PROCESS:
return _channel[name]
else:
recv_chan = Channel[Channel[Any]]('recv_chan')
channel_deliver_passive_channel.send(('get_channel', {'name': name, 'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### ***def*** `get_channels() -> dict[str, Channel]`
获取通道实例
Returns:
<details>
<summary>源代码</summary>
```python
def get_channels() -> dict[str, Channel]:
"""
获取通道实例
Returns:
"""
if IS_MAIN_PROCESS:
return _channel
else:
recv_chan = Channel[dict[str, Channel[Any]]]('recv_chan')
channel_deliver_passive_channel.send(('get_channels', {'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### ***def*** `on_set_channel(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'set_channel')
def on_set_channel(data: tuple[str, dict[str, Any]]):
name, channel = (data[1]['name'], data[1]['channel_'])
set_channel(name, channel)
```
</details>
### ***def*** `on_get_channel(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'get_channel')
def on_get_channel(data: tuple[str, dict[str, Any]]):
name, recv_chan = (data[1]['name'], data[1]['recv_chan'])
recv_chan.send(get_channel(name))
```
</details>
### ***def*** `on_get_channels(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == 'get_channels')
def on_get_channels(data: tuple[str, dict[str, Any]]):
recv_chan = data[1]['recv_chan']
recv_chan.send(get_channels())
```
</details>
### ***def*** `decorator(func: Callable[[T], Any]) -> Callable[[T], Any]`
<details>
<summary>源代码</summary>
```python
def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
global _func_id
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
_callback_funcs[_func_id] = wrapper
if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(_func_id)
else:
self._on_sub_receive_funcs.append(_func_id)
_func_id += 1
return func
```
</details>
### ***async def*** `wrapper(data: T) -> Any`
<details>
<summary>源代码</summary>
```python
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
```
</details>
### ***class*** `Channel(Generic[T])`
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
### &emsp; ***def*** `__init__(self, _id: str, type_check: Optional[bool]) -> None`
&emsp;初始化通道
Args:
_id: 通道ID
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
<details>
<summary>源代码</summary>
```python
def __init__(self, _id: str, type_check: Optional[bool]=None):
"""
初始化通道
Args:
_id: 通道ID
type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
"""
self.conn_send, self.conn_recv = Pipe()
self._closed = False
self._on_main_receive_funcs: list[int] = []
self._on_sub_receive_funcs: list[int] = []
self.name: str = _id
self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False
if type_check is None:
type_check = self._get_generic_type() is not None
elif type_check:
if self._get_generic_type() is None:
raise TypeError('Type hint is required for enforcing type check.')
self.type_check = type_check
```
</details>
### &emsp; ***def*** `send(self, data: T) -> None`
&emsp;发送数据
Args:
data: 数据
<details>
<summary>源代码</summary>
```python
def send(self, data: T):
"""
发送数据
Args:
data: 数据
"""
if self.type_check:
_type = self._get_generic_type()
if _type is not None and (not self._validate_structure(data, _type)):
raise TypeError(f'Data must be an instance of {_type}, {type(data)} found')
if self._closed:
raise RuntimeError('Cannot send to a closed channel_')
self.conn_send.send(data)
```
</details>
### &emsp; ***def*** `receive(self) -> T`
&emsp;接收数据
Args:
<details>
<summary>源代码</summary>
```python
def receive(self) -> T:
"""
接收数据
Args:
"""
if self._closed:
raise RuntimeError('Cannot receive from a closed channel_')
while True:
data = self.conn_recv.recv()
return data
```
</details>
### &emsp; ***def*** `close(self) -> None`
&emsp;关闭通道
<details>
<summary>源代码</summary>
```python
def close(self):
"""
关闭通道
"""
self._closed = True
self.conn_send.close()
self.conn_recv.close()
```
</details>
### &emsp; ***def*** `on_receive(self, filter_func: Optional[FILTER_FUNC]) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]`
&emsp;接收数据并执行函数
Args:
filter_func: 过滤函数为None则不过滤
Returns:
装饰器,装饰一个函数在接收到数据后执行
<details>
<summary>源代码</summary>
```python
def on_receive(self, filter_func: Optional[FILTER_FUNC]=None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
"""
接收数据并执行函数
Args:
filter_func: 过滤函数为None则不过滤
Returns:
装饰器,装饰一个函数在接收到数据后执行
"""
if not self.is_sub_receive_loop_running and (not IS_MAIN_PROCESS):
threading.Thread(target=self._start_sub_receive_loop, daemon=True).start()
if not self.is_main_receive_loop_running and IS_MAIN_PROCESS:
threading.Thread(target=self._start_main_receive_loop, daemon=True).start()
def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
global _func_id
async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
if not await filter_func(data):
return
elif not filter_func(data):
return
if is_coroutine_callable(func):
return await func(data)
else:
return func(data)
_callback_funcs[_func_id] = wrapper
if IS_MAIN_PROCESS:
self._on_main_receive_funcs.append(_func_id)
else:
self._on_sub_receive_funcs.append(_func_id)
_func_id += 1
return func
return decorator
```
</details>
### ***var*** `T = TypeVar('T')`
### ***var*** `channel_deliver_active_channel = Channel(_id='channel_deliver_active_channel')`
### ***var*** `channel_deliver_passive_channel = Channel(_id='channel_deliver_passive_channel')`
### ***var*** `recv_chan = data[1]['recv_chan']`
### ***var*** `recv_chan = Channel[Channel[Any]]('recv_chan')`
### ***var*** `recv_chan = Channel[dict[str, Channel[Any]]]('recv_chan')`
### ***var*** `type_check = self._get_generic_type() is not None`
### ***var*** `data = self.conn_recv.recv()`
### ***var*** `func = _callback_funcs[func_id]`
### ***var*** `func = _callback_funcs[func_id]`
### ***var*** `data = self.conn_recv.recv()`
### ***var*** `data = self.conn_recv.recv()`

View File

@ -0,0 +1,25 @@
---
title: liteyuki.comm.event
order: 1
icon: laptop-code
category: API
---
### ***class*** `Event`
事件类
### &emsp; ***def*** `__init__(self, name: str, data: dict[str, Any]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, name: str, data: dict[str, Any]):
self.name = name
self.data = data
```
</details>

View File

@ -0,0 +1,563 @@
---
title: liteyuki.comm.storage
order: 1
icon: laptop-code
category: API
---
### ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_subscriber_receive_funcs(channel_: str, data: Any):
"""
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
"""
if IS_MAIN_PROCESS:
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
```
</details>
### ***def*** `on_get(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'get')
def on_get(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
default = data[1]['default']
recv_chan = data[1]['recv_chan']
recv_chan.send(shared_memory.get(key, default))
```
</details>
### ***def*** `on_set(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'set')
def on_set(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
value = data[1]['value']
shared_memory.set(key, value)
```
</details>
### ***def*** `on_delete(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'delete')
def on_delete(data: tuple[str, dict[str, Any]]):
key = data[1]['key']
shared_memory.delete(key)
```
</details>
### ***def*** `on_get_all(data: tuple[str, dict[str, Any]]) -> None`
<details>
<summary>源代码</summary>
```python
@shared_memory.passive_chan.on_receive(lambda d: d[0] == 'get_all')
def on_get_all(data: tuple[str, dict[str, Any]]):
recv_chan = data[1]['recv_chan']
recv_chan.send(shared_memory.get_all())
```
</details>
### ***def*** `on_publish(data: tuple[str, Any]) -> None`
<details>
<summary>源代码</summary>
```python
@channel.publish_channel.on_receive()
def on_publish(data: tuple[str, Any]):
channel_, data = data
shared_memory.run_subscriber_receive_funcs(channel_, data)
```
</details>
### ***def*** `decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC`
<details>
<summary>源代码</summary>
```python
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
if IS_MAIN_PROCESS:
if channel_ not in _on_main_subscriber_receive_funcs:
_on_main_subscriber_receive_funcs[channel_] = []
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
else:
if channel_ not in _on_sub_subscriber_receive_funcs:
_on_sub_subscriber_receive_funcs[channel_] = []
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
return wrapper
```
</details>
### ***async def*** `wrapper(data: Any) -> None`
<details>
<summary>源代码</summary>
```python
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
```
</details>
### ***class*** `Subscriber`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self):
self._subscribers = {}
```
</details>
### &emsp; ***def*** `receive(self) -> Any`
&emsp;
<details>
<summary>源代码</summary>
```python
def receive(self) -> Any:
pass
```
</details>
### &emsp; ***def*** `unsubscribe(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def unsubscribe(self) -> None:
pass
```
</details>
### ***class*** `KeyValueStore`
### &emsp; ***def*** `__init__(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self):
self._store = {}
self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-active')
self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id='shared_memory-passive')
self.publish_channel = Channel[tuple[str, Any]](_id='shared_memory-publish')
self.is_main_receive_loop_running = False
self.is_sub_receive_loop_running = False
```
</details>
### &emsp; ***def*** `set(self, key: str, value: Any) -> None`
&emsp;设置键值对
Args:
key: 键
value: 值
<details>
<summary>源代码</summary>
```python
def set(self, key: str, value: Any) -> None:
"""
设置键值对
Args:
key: 键
value: 值
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
self._store[key] = value
else:
self.passive_chan.send(('set', {'key': key, 'value': value}))
```
</details>
### &emsp; ***def*** `get(self, key: str, default: Optional[Any]) -> Optional[Any]`
&emsp;获取键值对
Args:
key: 键
default: 默认值
Returns:
Any: 值
<details>
<summary>源代码</summary>
```python
def get(self, key: str, default: Optional[Any]=None) -> Optional[Any]:
"""
获取键值对
Args:
key: 键
default: 默认值
Returns:
Any: 值
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
return self._store.get(key, default)
else:
recv_chan = Channel[Optional[Any]]('recv_chan')
self.passive_chan.send(('get', {'key': key, 'default': default, 'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### &emsp; ***def*** `delete(self, key: str, ignore_key_error: bool) -> None`
&emsp;删除键值对
Args:
key: 键
ignore_key_error: 是否忽略键不存在的错误
Returns:
<details>
<summary>源代码</summary>
```python
def delete(self, key: str, ignore_key_error: bool=True) -> None:
"""
删除键值对
Args:
key: 键
ignore_key_error: 是否忽略键不存在的错误
Returns:
"""
if IS_MAIN_PROCESS:
lock = _get_lock(key)
with lock:
if key in self._store:
try:
del self._store[key]
del _locks[key]
except KeyError as e:
if not ignore_key_error:
raise e
else:
self.passive_chan.send(('delete', {'key': key}))
```
</details>
### &emsp; ***def*** `get_all(self) -> dict[str, Any]`
&emsp;获取所有键值对
Returns:
dict[str, Any]: 键值对
<details>
<summary>源代码</summary>
```python
def get_all(self) -> dict[str, Any]:
"""
获取所有键值对
Returns:
dict[str, Any]: 键值对
"""
if IS_MAIN_PROCESS:
return self._store
else:
recv_chan = Channel[dict[str, Any]]('recv_chan')
self.passive_chan.send(('get_all', {'recv_chan': recv_chan}))
return recv_chan.receive()
```
</details>
### &emsp; ***def*** `publish(self, channel_: str, data: Any) -> None`
&emsp;发布消息
Args:
channel_: 频道
data: 数据
Returns:
<details>
<summary>源代码</summary>
```python
def publish(self, channel_: str, data: Any) -> None:
"""
发布消息
Args:
channel_: 频道
data: 数据
Returns:
"""
self.active_chan.send(('publish', {'channel': channel_, 'data': data}))
```
</details>
### &emsp; ***def*** `on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]`
&emsp;订阅者接收消息时的回调
Args:
channel_: 频道
Returns:
装饰器
<details>
<summary>源代码</summary>
```python
def on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
"""
订阅者接收消息时的回调
Args:
channel_: 频道
Returns:
装饰器
"""
if IS_MAIN_PROCESS and (not self.is_main_receive_loop_running):
threading.Thread(target=self._start_receive_loop, daemon=True).start()
shared_memory.is_main_receive_loop_running = True
elif not IS_MAIN_PROCESS and (not self.is_sub_receive_loop_running):
threading.Thread(target=self._start_receive_loop, daemon=True).start()
shared_memory.is_sub_receive_loop_running = True
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
async def wrapper(data: Any):
if is_coroutine_callable(func):
await func(data)
else:
func(data)
if IS_MAIN_PROCESS:
if channel_ not in _on_main_subscriber_receive_funcs:
_on_main_subscriber_receive_funcs[channel_] = []
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
else:
if channel_ not in _on_sub_subscriber_receive_funcs:
_on_sub_subscriber_receive_funcs[channel_] = []
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
return wrapper
return decorator
```
</details>
### &emsp; ***@staticmethod***
### &emsp; ***def*** `run_subscriber_receive_funcs(channel_: str, data: Any) -> None`
&emsp;运行订阅者接收函数
Args:
channel_: 频道
data: 数据
<details>
<summary>源代码</summary>
```python
@staticmethod
def run_subscriber_receive_funcs(channel_: str, data: Any):
"""
运行订阅者接收函数
Args:
channel_: 频道
data: 数据
"""
if IS_MAIN_PROCESS:
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
elif channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
```
</details>
### ***class*** `GlobalKeyValueStore`
### &emsp; ***@classmethod***
### &emsp; ***def*** `get_instance(cls: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
@classmethod
def get_instance(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = KeyValueStore()
return cls._instance
```
</details>
### &emsp; ***attr*** `_instance: None`
### &emsp; ***attr*** `_lock: threading.Lock()`
### ***var*** `key = data[1]['key']`
### ***var*** `default = data[1]['default']`
### ***var*** `recv_chan = data[1]['recv_chan']`
### ***var*** `key = data[1]['key']`
### ***var*** `value = data[1]['value']`
### ***var*** `key = data[1]['key']`
### ***var*** `recv_chan = data[1]['recv_chan']`
### ***var*** `lock = _get_lock(key)`
### ***var*** `lock = _get_lock(key)`
### ***var*** `recv_chan = Channel[Optional[Any]]('recv_chan')`
### ***var*** `lock = _get_lock(key)`
### ***var*** `recv_chan = Channel[dict[str, Any]]('recv_chan')`
### ***var*** `data = self.active_chan.receive()`
### ***var*** `data = self.publish_channel.receive()`

231
docs/en/dev/api/config.md Normal file
View File

@ -0,0 +1,231 @@
---
title: liteyuki.config
order: 1
icon: laptop-code
category: API
---
### ***def*** `flat_config(config: dict[str, Any]) -> dict[str, Any]`
扁平化配置文件
{a:{b:{c:1}}} -> {"a.b.c": 1}
Args:
config: 配置项目
Returns:
扁平化后的配置文件,但也包含原有的键值对
<details>
<summary>源代码</summary>
```python
def flat_config(config: dict[str, Any]) -> dict[str, Any]:
"""
扁平化配置文件
{a:{b:{c:1}}} -> {"a.b.c": 1}
Args:
config: 配置项目
Returns:
扁平化后的配置文件,但也包含原有的键值对
"""
new_config = copy.deepcopy(config)
for key, value in config.items():
if isinstance(value, dict):
for k, v in flat_config(value).items():
new_config[f'{key}.{k}'] = v
return new_config
```
</details>
### ***def*** `load_from_yaml(file: str) -> dict[str, Any]`
Load config from yaml file
<details>
<summary>源代码</summary>
```python
def load_from_yaml(file: str) -> dict[str, Any]:
"""
Load config from yaml file
"""
logger.debug(f'Loading YAML config from {file}')
config = yaml.safe_load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_json(file: str) -> dict[str, Any]`
Load config from json file
<details>
<summary>源代码</summary>
```python
def load_from_json(file: str) -> dict[str, Any]:
"""
Load config from json file
"""
logger.debug(f'Loading JSON config from {file}')
config = json.load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_toml(file: str) -> dict[str, Any]`
Load config from toml file
<details>
<summary>源代码</summary>
```python
def load_from_toml(file: str) -> dict[str, Any]:
"""
Load config from toml file
"""
logger.debug(f'Loading TOML config from {file}')
config = toml.load(open(file, 'r', encoding='utf-8'))
return flat_config(config if config is not None else {})
```
</details>
### ***def*** `load_from_files() -> dict[str, Any]`
从指定文件加载配置项,会自动识别文件格式
默认执行扁平化选项
<details>
<summary>源代码</summary>
```python
def load_from_files(*files: str, no_warning: bool=False) -> dict[str, Any]:
"""
从指定文件加载配置项,会自动识别文件格式
默认执行扁平化选项
"""
config = {}
for file in files:
if os.path.exists(file):
if file.endswith(('.yaml', 'yml')):
config.update(load_from_yaml(file))
elif file.endswith('.json'):
config.update(load_from_json(file))
elif file.endswith('.toml'):
config.update(load_from_toml(file))
elif not no_warning:
logger.warning(f'Unsupported config file format: {file}')
elif not no_warning:
logger.warning(f'Config file not found: {file}')
return config
```
</details>
### ***def*** `load_configs_from_dirs() -> dict[str, Any]`
从目录下加载配置文件,不递归
按照读取文件的优先级反向覆盖
默认执行扁平化选项
<details>
<summary>源代码</summary>
```python
def load_configs_from_dirs(*directories: str, no_waring: bool=False) -> dict[str, Any]:
"""
从目录下加载配置文件,不递归
按照读取文件的优先级反向覆盖
默认执行扁平化选项
"""
config = {}
for directory in directories:
if not os.path.exists(directory):
if not no_waring:
logger.warning(f'Directory not found: {directory}')
continue
for file in os.listdir(directory):
if file.endswith(_SUPPORTED_CONFIG_FORMATS):
config.update(load_from_files(os.path.join(directory, file), no_warning=no_waring))
return config
```
</details>
### ***def*** `load_config_in_default(no_waring: bool) -> dict[str, Any]`
从一个标准的轻雪项目加载配置文件
项目目录下的config.*和config目录下的所有配置文件
项目目录下的配置文件优先
<details>
<summary>源代码</summary>
```python
def load_config_in_default(no_waring: bool=False) -> dict[str, Any]:
"""
从一个标准的轻雪项目加载配置文件
项目目录下的config.*和config目录下的所有配置文件
项目目录下的配置文件优先
"""
config = load_configs_from_dirs('config', no_waring=no_waring)
config.update(load_from_files('config.yaml', 'config.toml', 'config.json', 'config.yml', no_warning=no_waring))
return config
```
</details>
### ***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,275 @@
---
title: liteyuki.core.manager
order: 1
icon: laptop-code
category: API
---
### ***class*** `ChannelDeliver`
### &emsp; ***def*** `__init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]], publish: Channel[tuple[str, Any]]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]], publish: Channel[tuple[str, Any]]):
self.active = active
self.passive = passive
self.channel_deliver_active = channel_deliver_active
self.channel_deliver_passive = channel_deliver_passive
self.publish = publish
```
</details>
### ***class*** `ProcessManager`
进程管理器
### &emsp; ***def*** `__init__(self, lifespan: 'Lifespan') -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, lifespan: 'Lifespan'):
self.lifespan = lifespan
self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
self.processes: dict[str, Process] = {}
```
</details>
### &emsp; ***def*** `start(self, name: str) -> None`
&emsp;开启后自动监控进程,并添加到进程字典中
Args:
name:
Returns:
<details>
<summary>源代码</summary>
```python
def start(self, name: str):
"""
开启后自动监控进程,并添加到进程字典中
Args:
name:
Returns:
"""
if name not in self.targets:
raise KeyError(f'Process {name} not found.')
chan_active = get_channel(f'{name}-active')
def _start_process():
process = Process(target=self.targets[name][0], args=self.targets[name][1], kwargs=self.targets[name][2], daemon=True)
self.processes[name] = process
process.start()
_start_process()
while True:
data = chan_active.receive()
if data == 0:
logger.info(f'Stopping process {name}')
self.lifespan.before_process_shutdown()
self.terminate(name)
break
elif data == 1:
logger.info(f'Restarting process {name}')
self.lifespan.before_process_shutdown()
self.lifespan.before_process_restart()
self.terminate(name)
_start_process()
continue
else:
logger.warning('Unknown data received, ignored.')
```
</details>
### &emsp; ***def*** `start_all(self) -> None`
&emsp;启动所有进程
<details>
<summary>源代码</summary>
```python
def start_all(self):
"""
启动所有进程
"""
for name in self.targets:
threading.Thread(target=self.start, args=(name,), daemon=True).start()
```
</details>
### &emsp; ***def*** `add_target(self, name: str, target: TARGET_FUNC, args: tuple, kwargs: Any) -> None`
&emsp;添加进程
Args:
name: 进程名,用于获取和唯一标识
target: 进程函数
args: 进程函数参数
kwargs: 进程函数关键字参数通常会默认传入chan_active和chan_passive
<details>
<summary>源代码</summary>
```python
def add_target(self, name: str, target: TARGET_FUNC, args: tuple=(), kwargs=None):
"""
添加进程
Args:
name: 进程名,用于获取和唯一标识
target: 进程函数
args: 进程函数参数
kwargs: 进程函数关键字参数通常会默认传入chan_active和chan_passive
"""
if kwargs is None:
kwargs = {}
chan_active: Channel = Channel(_id=f'{name}-active')
chan_passive: Channel = Channel(_id=f'{name}-passive')
channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel, publish=publish_channel)
self.targets[name] = (_delivery_channel_wrapper, (target, channel_deliver, shared_memory, *args), kwargs)
set_channels({f'{name}-active': chan_active, f'{name}-passive': chan_passive})
```
</details>
### &emsp; ***def*** `join_all(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def join_all(self):
for name, process in self.targets:
process.join()
```
</details>
### &emsp; ***def*** `terminate(self, name: str) -> None`
&emsp;终止进程并从进程字典中删除
Args:
name:
Returns:
<details>
<summary>源代码</summary>
```python
def terminate(self, name: str):
"""
终止进程并从进程字典中删除
Args:
name:
Returns:
"""
if name not in self.processes:
logger.warning(f'Process {name} not found.')
return
process = self.processes[name]
process.terminate()
process.join(TIMEOUT)
if process.is_alive():
process.kill()
logger.success(f'Process {name} terminated.')
```
</details>
### &emsp; ***def*** `terminate_all(self) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def terminate_all(self):
for name in self.targets:
self.terminate(name)
```
</details>
### &emsp; ***def*** `is_process_alive(self, name: str) -> bool`
&emsp;检查进程是否存活
Args:
name:
Returns:
<details>
<summary>源代码</summary>
```python
def is_process_alive(self, name: str) -> bool:
"""
检查进程是否存活
Args:
name:
Returns:
"""
if name not in self.targets:
logger.warning(f'Process {name} not found.')
return self.processes[name].is_alive()
```
</details>
### ***var*** `TIMEOUT = 10`
### ***var*** `chan_active = get_channel(f'{name}-active')`
### ***var*** `channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel, publish=publish_channel)`
### ***var*** `process = self.processes[name]`
### ***var*** `process = Process(target=self.targets[name][0], args=self.targets[name][1], kwargs=self.targets[name][2], daemon=True)`
### ***var*** `data = chan_active.receive()`
### ***var*** `kwargs = {}`

View File

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

View File

@ -0,0 +1,249 @@
---
title: liteyuki.dev.observer
order: 1
icon: laptop-code
category: API
---
### ***def*** `debounce(wait: Any) -> None`
防抖函数
<details>
<summary>源代码</summary>
```python
def debounce(wait):
"""
防抖函数
"""
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
last_call_time = None
return wrapper
return decorator
```
</details>
### ***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:
装饰器,装饰一个函数在接收到数据后执行
<details>
<summary>源代码</summary>
```python
def on_file_system_event(directories: tuple[str], recursive: bool=True, event_filter: FILTER_FUNC=None) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]:
"""
注册文件系统变化监听器
Args:
directories: 监听目录们
recursive: 是否递归监听子目录
event_filter: 事件过滤器, 返回True则执行回调函数
Returns:
装饰器,装饰一个函数在接收到数据后执行
"""
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
code_modified_handler = CodeModifiedHandler()
code_modified_handler.on_modified = wrapper
for directory in directories:
observer.schedule(code_modified_handler, directory, recursive=recursive)
return func
return decorator
```
</details>
### ***def*** `decorator(func: Any) -> None`
<details>
<summary>源代码</summary>
```python
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
last_call_time = None
return wrapper
```
</details>
### ***def*** `decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC`
<details>
<summary>源代码</summary>
```python
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
code_modified_handler = CodeModifiedHandler()
code_modified_handler.on_modified = wrapper
for directory in directories:
observer.schedule(code_modified_handler, directory, recursive=recursive)
return func
```
</details>
### ***def*** `wrapper() -> None`
<details>
<summary>源代码</summary>
```python
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if current_time - last_call_time > wait:
last_call_time = current_time
return func(*args, **kwargs)
```
</details>
### ***def*** `wrapper(event: FileSystemEvent) -> None`
<details>
<summary>源代码</summary>
```python
def wrapper(event: FileSystemEvent):
if event_filter is not None and (not event_filter(event)):
return
func(event)
```
</details>
### ***class*** `CodeModifiedHandler(FileSystemEventHandler)`
Handler for code file changes
### &emsp; ***def*** `on_modified(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
@debounce(1)
def on_modified(self, event):
raise NotImplementedError('on_modified must be implemented')
```
</details>
### &emsp; ***def*** `on_created(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_created(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_deleted(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_deleted(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_moved(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_moved(self, event):
self.on_modified(event)
```
</details>
### &emsp; ***def*** `on_any_event(self, event: Any) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def on_any_event(self, event):
self.on_modified(event)
```
</details>
### ***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,46 @@
---
title: liteyuki.dev.plugin
order: 1
icon: laptop-code
category: API
---
### ***def*** `run_plugins() -> None`
运行插件无需手动初始化bot
Args:
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
<details>
<summary>源代码</summary>
```python
def run_plugins(*module_path: str | Path):
"""
运行插件无需手动初始化bot
Args:
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
"""
cfg = load_config_in_default()
plugins = cfg.get('liteyuki.plugins', [])
plugins.extend(module_path)
cfg['liteyuki.plugins'] = plugins
bot = LiteyukiBot(**cfg)
bot.run()
```
</details>
### ***var*** `cfg = load_config_in_default()`
### ***var*** `plugins = cfg.get('liteyuki.plugins', [])`
### ***var*** `bot = LiteyukiBot(**cfg)`

View File

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

58
docs/en/dev/api/log.md Normal file
View File

@ -0,0 +1,58 @@
---
title: liteyuki.log
order: 1
icon: laptop-code
category: API
---
### ***def*** `get_format(level: str) -> str`
<details>
<summary>源代码</summary>
```python
def get_format(level: str) -> str:
if level == 'DEBUG':
return debug_format
else:
return default_format
```
</details>
### ***def*** `init_log(config: dict) -> None`
在语言加载完成后执行
Returns:
<details>
<summary>源代码</summary>
```python
def init_log(config: dict):
"""
在语言加载完成后执行
Returns:
"""
logger.remove()
logger.add(sys.stdout, level=0, diagnose=False, format=get_format(config.get('log_level', 'INFO')))
show_icon = config.get('log_icon', True)
logger.level('DEBUG', color='<blue>', icon=f"{('🐛' if show_icon else '')}DEBUG")
logger.level('INFO', color='<normal>', icon=f"{('' if show_icon else '')}INFO")
logger.level('SUCCESS', color='<green>', icon=f"{('✅' if show_icon else '')}SUCCESS")
logger.level('WARNING', color='<yellow>', icon=f"{('⚠️' if show_icon else '')}WARNING")
logger.level('ERROR', color='<red>', icon=f"{('⭕' if show_icon else '')}ERROR")
```
</details>
### ***var*** `logger = loguru.logger`
### ***var*** `show_icon = config.get('log_icon', True)`

View File

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

View File

@ -0,0 +1,106 @@
---
title: liteyuki.message.event
order: 1
icon: laptop-code
category: API
---
### ***class*** `MessageEvent`
### &emsp; ***def*** `__init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, data: Optional[dict[str, Any]]) -> None`
&emsp;轻雪抽象消息事件
Args:
bot_id: 机器人ID
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
raw_message: 原始消息(通常为纯文本的格式)
message_type: 消息类型(private, group, other)
session_id: 会话ID(私聊通常为用户ID群聊通常为群ID)
session_type: 会话类型(private, group)
receive_channel: 接收频道(用于回复消息)
data: 附加数据
<details>
<summary>源代码</summary>
```python
def __init__(self, bot_id: str, message: list[dict[str, Any]] | str, message_type: str, raw_message: str, session_id: str, session_type: str, receive_channel: str, data: Optional[dict[str, Any]]=None):
"""
轻雪抽象消息事件
Args:
bot_id: 机器人ID
message: 消息,消息段数组[{type: str, data: dict[str, Any]}]
raw_message: 原始消息(通常为纯文本的格式)
message_type: 消息类型(private, group, other)
session_id: 会话ID(私聊通常为用户ID群聊通常为群ID)
session_type: 会话类型(private, group)
receive_channel: 接收频道(用于回复消息)
data: 附加数据
"""
if data is None:
data = {}
self.message_type = message_type
self.data = data
self.bot_id = bot_id
self.message = message
self.raw_message = raw_message
self.session_id = session_id
self.session_type = session_type
self.receive_channel = receive_channel
```
</details>
### &emsp; ***def*** `reply(self, message: str | dict[str, Any]) -> None`
&emsp;回复消息
Args:
message:
Returns:
<details>
<summary>源代码</summary>
```python
def reply(self, message: str | dict[str, Any]):
"""
回复消息
Args:
message:
Returns:
"""
reply_event = MessageEvent(message_type=self.session_type, message=message, raw_message='', data={'message': message}, bot_id=self.bot_id, session_id=self.session_id, session_type=self.session_type, receive_channel='_')
shared_memory.publish(self.receive_channel, reply_event)
```
</details>
### ***var*** `reply_event = MessageEvent(message_type=self.session_type, message=message, raw_message='', data={'message': message}, bot_id=self.bot_id, session_id=self.session_id, session_type=self.session_type, receive_channel='_')`
### ***var*** `data = {}`

View File

@ -0,0 +1,71 @@
---
title: liteyuki.message.matcher
order: 1
icon: laptop-code
category: API
---
### ***class*** `Matcher`
### &emsp; ***def*** `__init__(self, rule: Rule, priority: int, block: bool) -> None`
&emsp;匹配器
Args:
rule: 规则
priority: 优先级 >= 0
block: 是否阻断后续优先级更低的匹配器
<details>
<summary>源代码</summary>
```python
def __init__(self, rule: Rule, priority: int, block: bool):
"""
匹配器
Args:
rule: 规则
priority: 优先级 >= 0
block: 是否阻断后续优先级更低的匹配器
"""
self.rule = rule
self.priority = priority
self.block = block
self.handlers: list[EventHandler] = []
```
</details>
### &emsp; ***def*** `handle(self, handler: EventHandler) -> EventHandler`
&emsp;添加处理函数,装饰器
Args:
handler:
Returns:
EventHandler
<details>
<summary>源代码</summary>
```python
def handle(self, handler: EventHandler) -> EventHandler:
"""
添加处理函数,装饰器
Args:
handler:
Returns:
EventHandler
"""
self.handlers.append(handler)
return handler
```
</details>

View File

@ -0,0 +1,39 @@
---
title: liteyuki.message.on
order: 1
icon: laptop-code
category: API
---
### ***def*** `on_message(rule: Rule, priority: int, block: bool) -> Matcher`
<details>
<summary>源代码</summary>
```python
def on_message(rule: Rule=Rule(), priority: int=0, block: bool=True) -> Matcher:
matcher = Matcher(rule, priority, block)
for i, m in enumerate(_matcher_list):
if m.priority < matcher.priority:
_matcher_list.insert(i, matcher)
break
else:
_matcher_list.append(matcher)
return matcher
```
</details>
### ***var*** `current_priority = -1`
### ***var*** `matcher = Matcher(rule, priority, block)`
### ***var*** `current_priority = matcher.priority`

View File

@ -0,0 +1,24 @@
---
title: liteyuki.message.rule
order: 1
icon: laptop-code
category: API
---
### ***class*** `Rule`
### &emsp; ***def*** `__init__(self, handler: Optional[RuleHandler]) -> None`
&emsp;
<details>
<summary>源代码</summary>
```python
def __init__(self, handler: Optional[RuleHandler]=None):
self.handler = handler
```
</details>

View File

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

473
docs/en/dev/api/mkdoc.md Normal file
View File

@ -0,0 +1,473 @@
---
title: liteyuki.mkdoc
order: 1
icon: laptop-code
category: API
---
### ***def*** `get_relative_path(base_path: str, target_path: str) -> str`
获取相对路径
Args:
base_path: 基础路径
target_path: 目标路径
<details>
<summary>源代码</summary>
```python
def get_relative_path(base_path: str, target_path: str) -> str:
"""
获取相对路径
Args:
base_path: 基础路径
target_path: 目标路径
"""
return os.path.relpath(target_path, base_path)
```
</details>
### ***def*** `write_to_files(file_data: dict[str, str]) -> None`
输出文件
Args:
file_data: 文件数据 相对路径
<details>
<summary>源代码</summary>
```python
def write_to_files(file_data: dict[str, str]):
"""
输出文件
Args:
file_data: 文件数据 相对路径
"""
for rp, data in file_data.items():
if not os.path.exists(os.path.dirname(rp)):
os.makedirs(os.path.dirname(rp))
with open(rp, 'w', encoding='utf-8') as f:
f.write(data)
```
</details>
### ***def*** `get_file_list(module_folder: str) -> None`
<details>
<summary>源代码</summary>
```python
def get_file_list(module_folder: str):
file_list = []
for root, dirs, files in os.walk(module_folder):
for file in files:
if file.endswith(('.py', '.pyi')):
file_list.append(os.path.join(root, file))
return file_list
```
</details>
### ***def*** `get_module_info_normal(file_path: str, ignore_private: bool) -> ModuleInfo`
获取函数和类
Args:
file_path: Python 文件路径
ignore_private: 忽略私有函数和类
Returns:
模块信息
<details>
<summary>源代码</summary>
```python
def get_module_info_normal(file_path: str, ignore_private: bool=True) -> ModuleInfo:
"""
获取函数和类
Args:
file_path: Python 文件路径
ignore_private: 忽略私有函数和类
Returns:
模块信息
"""
with open(file_path, 'r', encoding='utf-8') as file:
file_content = file.read()
tree = ast.parse(file_content)
dot_sep_module_path = file_path.replace(os.sep, '.').replace('.py', '').replace('.pyi', '')
module_docstring = ast.get_docstring(tree)
module_info = ModuleInfo(module_path=dot_sep_module_path, functions=[], classes=[], attributes=[], docstring=module_docstring if module_docstring else '')
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if not any((isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(node))) and (not ignore_private or not node.name.startswith('_')):
if node.args.args:
first_arg = node.args.args[0]
if first_arg.arg in ('self', 'cls'):
continue
function_docstring = ast.get_docstring(node)
func_info = FunctionInfo(name=node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args], return_type=ast.unparse(node.returns) if node.returns else 'None', docstring=function_docstring if function_docstring else '', type=DefType.FUNCTION, is_async=isinstance(node, ast.AsyncFunctionDef), source_code=ast.unparse(node))
module_info.functions.append(func_info)
elif isinstance(node, ast.ClassDef):
class_docstring = ast.get_docstring(node)
class_info = ClassInfo(name=node.name, docstring=class_docstring if class_docstring else '', methods=[], attributes=[], inherit=[ast.unparse(base) for base in node.bases])
for class_node in node.body:
if isinstance(class_node, ast.FunctionDef) and (not ignore_private or not class_node.name.startswith('_') or class_node.name == '__init__'):
method_docstring = ast.get_docstring(class_node)
def_type = DefType.METHOD
if class_node.decorator_list:
if any((isinstance(decorator, ast.Name) and decorator.id == 'staticmethod' for decorator in class_node.decorator_list)):
def_type = DefType.STATIC_METHOD
elif any((isinstance(decorator, ast.Name) and decorator.id == 'classmethod' for decorator in class_node.decorator_list)):
def_type = DefType.CLASS_METHOD
elif any((isinstance(decorator, ast.Name) and decorator.id == 'property' for decorator in class_node.decorator_list)):
def_type = DefType.PROPERTY
class_info.methods.append(FunctionInfo(name=class_node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in class_node.args.args], return_type=ast.unparse(class_node.returns) if class_node.returns else 'None', docstring=method_docstring if method_docstring else '', type=def_type, is_async=isinstance(class_node, ast.AsyncFunctionDef), source_code=ast.unparse(class_node)))
elif isinstance(class_node, ast.Assign):
for target in class_node.targets:
if isinstance(target, ast.Name):
class_info.attributes.append(AttributeInfo(name=target.id, type=ast.unparse(class_node.value)))
module_info.classes.append(class_info)
elif isinstance(node, ast.Assign):
if not any((isinstance(parent, (ast.ClassDef, ast.FunctionDef)) for parent in ast.iter_child_nodes(node))):
for target in node.targets:
if isinstance(target, ast.Name) and (not ignore_private or not target.id.startswith('_')):
attr_type = NO_TYPE_HINT
if isinstance(node.value, ast.AnnAssign) and node.value.annotation:
attr_type = ast.unparse(node.value.annotation)
module_info.attributes.append(AttributeInfo(name=target.id, type=attr_type, value=ast.unparse(node.value) if node.value else None))
return module_info
```
</details>
### ***def*** `generate_markdown(module_info: ModuleInfo, front_matter: Any) -> str`
生成模块的Markdown
你可在此自定义生成的Markdown格式
Args:
module_info: 模块信息
front_matter: 自定义选项title, index, icon, category
Returns:
Markdown 字符串
<details>
<summary>源代码</summary>
```python
def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
"""
生成模块的Markdown
你可在此自定义生成的Markdown格式
Args:
module_info: 模块信息
front_matter: 自定义选项title, index, icon, category
Returns:
Markdown 字符串
"""
content = ''
front_matter = '---\n' + '\n'.join([f'{k}: {v}' for k, v in front_matter.items()]) + '\n---\n\n'
content += front_matter
for func in module_info.functions:
args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in func.args]
content += f"### ***{('async ' if func.is_async else '')}def*** `{func.name}({', '.join(args_with_type)}) -> {func.return_type}`\n\n"
func.docstring = func.docstring.replace('\n', '\n\n')
content += f'{func.docstring}\n\n'
content += f'<details>\n<summary>源代码</summary>\n\n```python\n{func.source_code}\n```\n</details>\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:
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'
content += f'<details>\n<summary>源代码</summary>\n\n```python\n{method.source_code}\n```\n</details>\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
```
</details>
### ***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: 忽略的路径
<details>
<summary>源代码</summary>
```python
def generate_docs(module_folder: str, output_dir: str, with_top: bool=False, ignored_paths=None):
"""
生成文档
Args:
module_folder: 模块文件夹
output_dir: 输出文件夹
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b True时例如docs/api/module/module_a.md docs/api/module/module_b.md
ignored_paths: 忽略的路径
"""
if ignored_paths is None:
ignored_paths = []
file_data: dict[str, str] = {}
file_list = get_file_list(module_folder)
shutil.rmtree(output_dir, ignore_errors=True)
os.mkdir(output_dir)
replace_data = {'__init__': 'README', '.py': '.md'}
for pyfile_path in file_list:
if any((ignored_path.replace('\\', '/') in pyfile_path.replace('\\', '/') for ignored_path in ignored_paths)):
continue
no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path)
rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path
for rk, rv in replace_data.items():
rel_md_path = rel_md_path.replace(rk, rv)
abs_md_path = os.path.join(output_dir, rel_md_path)
module_info = get_module_info_normal(pyfile_path)
if 'README' in abs_md_path:
front_matter = {'title': module_info.module_path.replace('.__init__', '').replace('_', '\\n'), 'index': 'true', 'icon': 'laptop-code', 'category': 'API'}
else:
front_matter = {'title': module_info.module_path.replace('_', '\\n'), 'order': '1', 'icon': 'laptop-code', 'category': 'API'}
md_content = generate_markdown(module_info, front_matter)
print(f'Generate {pyfile_path} -> {abs_md_path}')
file_data[abs_md_path] = md_content
write_to_files(file_data)
```
</details>
### ***class*** `DefType(Enum)`
### &emsp; ***attr*** `FUNCTION: 'function'`
### &emsp; ***attr*** `METHOD: 'method'`
### &emsp; ***attr*** `STATIC_METHOD: 'staticmethod'`
### &emsp; ***attr*** `CLASS_METHOD: 'classmethod'`
### &emsp; ***attr*** `PROPERTY: 'property'`
### ***class*** `FunctionInfo(BaseModel)`
### ***class*** `AttributeInfo(BaseModel)`
### ***class*** `ClassInfo(BaseModel)`
### ***class*** `ModuleInfo(BaseModel)`
### ***var*** `NO_TYPE_ANY = 'Any'`
### ***var*** `NO_TYPE_HINT = 'NoTypeHint'`
### ***var*** `FUNCTION = 'function'`
### ***var*** `METHOD = 'method'`
### ***var*** `STATIC_METHOD = 'staticmethod'`
### ***var*** `CLASS_METHOD = 'classmethod'`
### ***var*** `PROPERTY = 'property'`
### ***var*** `file_list = []`
### ***var*** `dot_sep_module_path = file_path.replace(os.sep, '.').replace('.py', '').replace('.pyi', '')`
### ***var*** `module_docstring = ast.get_docstring(tree)`
### ***var*** `module_info = ModuleInfo(module_path=dot_sep_module_path, functions=[], classes=[], attributes=[], docstring=module_docstring if module_docstring else '')`
### ***var*** `content = ''`
### ***var*** `front_matter = '---\n' + '\n'.join([f'{k}: {v}' for k, v in front_matter.items()]) + '\n---\n\n'`
### ***var*** `file_list = get_file_list(module_folder)`
### ***var*** `replace_data = {'__init__': 'README', '.py': '.md'}`
### ***var*** `file_content = file.read()`
### ***var*** `tree = ast.parse(file_content)`
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in func.args]`
### ***var*** `ignored_paths = []`
### ***var*** `no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path)`
### ***var*** `rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path`
### ***var*** `abs_md_path = os.path.join(output_dir, rel_md_path)`
### ***var*** `module_info = get_module_info_normal(pyfile_path)`
### ***var*** `md_content = generate_markdown(module_info, front_matter)`
### ***var*** `inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ''`
### ***var*** `rel_md_path = rel_md_path.replace(rk, rv)`
### ***var*** `front_matter = {'title': module_info.module_path.replace('.__init__', '').replace('_', '\\n'), 'index': 'true', 'icon': 'laptop-code', 'category': 'API'}`
### ***var*** `front_matter = {'title': module_info.module_path.replace('_', '\\n'), 'order': '1', 'icon': 'laptop-code', 'category': 'API'}`
### ***var*** `function_docstring = ast.get_docstring(node)`
### ***var*** `func_info = FunctionInfo(name=node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args], return_type=ast.unparse(node.returns) if node.returns else 'None', docstring=function_docstring if function_docstring else '', type=DefType.FUNCTION, is_async=isinstance(node, ast.AsyncFunctionDef), source_code=ast.unparse(node))`
### ***var*** `class_docstring = ast.get_docstring(node)`
### ***var*** `class_info = ClassInfo(name=node.name, docstring=class_docstring if class_docstring else '', methods=[], attributes=[], inherit=[ast.unparse(base) for base in node.bases])`
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in method.args]`
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] and arg[0] != 'self' else arg[0] for arg in method.args]`
### ***var*** `first_arg = node.args.args[0]`
### ***var*** `method_docstring = ast.get_docstring(class_node)`
### ***var*** `def_type = DefType.METHOD`
### ***var*** `def_type = DefType.STATIC_METHOD`
### ***var*** `attr_type = NO_TYPE_HINT`
### ***var*** `def_type = DefType.CLASS_METHOD`
### ***var*** `attr_type = ast.unparse(node.value.annotation)`
### ***var*** `def_type = DefType.PROPERTY`

View File

@ -0,0 +1,29 @@
---
title: liteyuki.plugin
index: true
icon: laptop-code
category: API
---
### ***def*** `get_loaded_plugins() -> dict[str, Plugin]`
获取已加载的插件
Returns:
dict[str, Plugin]: 插件字典
<details>
<summary>源代码</summary>
```python
def get_loaded_plugins() -> dict[str, Plugin]:
"""
获取已加载的插件
Returns:
dict[str, Plugin]: 插件字典
"""
return _plugins
```
</details>

View File

@ -0,0 +1,199 @@
---
title: liteyuki.plugin.load
order: 1
icon: laptop-code
category: API
---
### ***def*** `load_plugin(module_path: str | Path) -> Optional[Plugin]`
加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
参数:
module_path: 插件名称 `path.to.your.plugin`
或插件路径 `pathlib.Path(path/to/your/plugin)`
<details>
<summary>源代码</summary>
```python
def load_plugin(module_path: str | Path) -> Optional[Plugin]:
"""加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
参数:
module_path: 插件名称 `path.to.your.plugin`
或插件路径 `pathlib.Path(path/to/your/plugin)`
"""
module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path
try:
module = import_module(module_path)
_plugins[module.__name__] = Plugin(name=module.__name__, module=module, module_name=module_path, metadata=module.__dict__.get('__plugin_metadata__', None))
display_name = module.__name__.split('.')[-1]
if module.__dict__.get('__plugin_meta__'):
metadata: 'PluginMetadata' = module.__dict__['__plugin_meta__']
display_name = format_display_name(f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type)
logger.opt(colors=True).success(f'Succeeded to load liteyuki plugin "{display_name}"')
return _plugins[module.__name__]
except Exception as e:
logger.opt(colors=True).success(f'Failed to load liteyuki plugin "<r>{module_path}</r>"')
traceback.print_exc()
return None
```
</details>
### ***def*** `load_plugins() -> set[Plugin]`
导入文件夹下多个插件
参数:
plugin_dir: 文件夹路径
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
<details>
<summary>源代码</summary>
```python
def load_plugins(*plugin_dir: str, ignore_warning: bool=True) -> set[Plugin]:
"""导入文件夹下多个插件
参数:
plugin_dir: 文件夹路径
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
"""
plugins = set()
for dir_path in plugin_dir:
if not os.path.exists(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' does not exist.")
continue
if not os.listdir(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' is empty.")
continue
if not os.path.isdir(dir_path):
if not ignore_warning:
logger.warning(f"Plugins dir '{dir_path}' is not a directory.")
continue
for f in os.listdir(dir_path):
path = Path(os.path.join(dir_path, f))
module_name = None
if os.path.isfile(path) and f.endswith('.py') and (f != '__init__.py'):
module_name = f'{path_to_module_name(Path(dir_path))}.{f[:-3]}'
elif os.path.isdir(path) and os.path.exists(os.path.join(path, '__init__.py')):
module_name = path_to_module_name(path)
if module_name:
load_plugin(module_name)
if _plugins.get(module_name):
plugins.add(_plugins[module_name])
return plugins
```
</details>
### ***def*** `format_display_name(display_name: str, plugin_type: PluginType) -> str`
设置插件名称颜色,根据不同类型插件设置颜色
Args:
display_name: 插件名称
plugin_type: 插件类型
Returns:
str: 设置后的插件名称 <y>name</y>
<details>
<summary>源代码</summary>
```python
def format_display_name(display_name: str, plugin_type: PluginType) -> str:
"""
设置插件名称颜色,根据不同类型插件设置颜色
Args:
display_name: 插件名称
plugin_type: 插件类型
Returns:
str: 设置后的插件名称 <y>name</y>
"""
color = 'y'
match plugin_type:
case PluginType.APPLICATION:
color = 'm'
case PluginType.TEST:
color = 'g'
case PluginType.MODULE:
color = 'e'
case PluginType.SERVICE:
color = 'c'
return f'<{color}>{display_name} [{plugin_type.name}]</{color}>'
```
</details>
### ***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*** `MODULE: 'module'`
### &emsp; ***attr*** `UNCLASSIFIED: 'unclassified'`
### &emsp; ***attr*** `TEST: 'test'`
### ***class*** `PluginMetadata(BaseModel)`
轻雪插件元数据由插件编写者提供name为必填项
Attributes:
----------
name: str
插件名称
description: str
插件描述
usage: str
插件使用方法
type: str
插件类型
author: str
插件作者
homepage: str
插件主页
extra: dict[str, Any]
额外信息
### ***class*** `Plugin(BaseModel)`
存储插件信息
### &emsp; ***attr*** `model_config: {'arbitrary_types_allowed': True}`
### ***var*** `APPLICATION = 'application'`
### ***var*** `SERVICE = 'service'`
### ***var*** `MODULE = 'module'`
### ***var*** `UNCLASSIFIED = 'unclassified'`
### ***var*** `TEST = 'test'`
### ***var*** `model_config = {'arbitrary_types_allowed': True}`

180
docs/en/dev/api/utils.md Normal file
View File

@ -0,0 +1,180 @@
---
title: liteyuki.utils
order: 1
icon: laptop-code
category: API
---
### ***def*** `is_coroutine_callable(call: Callable[..., Any]) -> bool`
判断是否为协程可调用对象
Args:
call: 可调用对象
Returns:
bool: 是否为协程可调用对象
<details>
<summary>源代码</summary>
```python
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
"""
判断是否为协程可调用对象
Args:
call: 可调用对象
Returns:
bool: 是否为协程可调用对象
"""
if inspect.isroutine(call):
return inspect.iscoroutinefunction(call)
if inspect.isclass(call):
return False
func_ = getattr(call, '__call__', None)
return inspect.iscoroutinefunction(func_)
```
</details>
### ***def*** `run_coroutine() -> None`
运行协程
Args:
coro:
Returns:
<details>
<summary>源代码</summary>
```python
def run_coroutine(*coro: Coroutine):
"""
运行协程
Args:
coro:
Returns:
"""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
for c in coro:
asyncio.ensure_future(c)
else:
for c in coro:
loop.run_until_complete(c)
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(asyncio.gather(*coro))
loop.close()
except Exception as e:
logger.error(f'Exception occurred: {e}')
```
</details>
### ***def*** `path_to_module_name(path: Path) -> str`
转换路径为模块名
Args:
path: 路径a/b/c/d -> a.b.c.d
Returns:
str: 模块名
<details>
<summary>源代码</summary>
```python
def path_to_module_name(path: Path) -> str:
"""
转换路径为模块名
Args:
path: 路径a/b/c/d -> a.b.c.d
Returns:
str: 模块名
"""
rel_path = path.resolve().relative_to(Path.cwd().resolve())
if rel_path.stem == '__init__':
return '.'.join(rel_path.parts[:-1])
else:
return '.'.join(rel_path.parts[:-1] + (rel_path.stem,))
```
</details>
### ***def*** `async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]`
异步包装器
Args:
func: Sync Callable
Returns:
Coroutine: Asynchronous Callable
<details>
<summary>源代码</summary>
```python
def async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]:
"""
异步包装器
Args:
func: Sync Callable
Returns:
Coroutine: Asynchronous Callable
"""
async def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.__signature__ = inspect.signature(func)
return wrapper
```
</details>
### ***async def*** `wrapper() -> None`
<details>
<summary>源代码</summary>
```python
async def wrapper(*args, **kwargs):
return func(*args, **kwargs)
```
</details>
### ***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()`

99
docs/en/dev/dev_comm.md Normal file
View File

@ -0,0 +1,99 @@
---
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)

74
docs/en/dev/dev_lyfunc.md Normal file
View File

@ -0,0 +1,74 @@
---
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

@ -0,0 +1,63 @@
---
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

@ -0,0 +1,53 @@
---
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]
> 资源包的结构会随着轻雪的更新而有变动,第三方资源包开发者需要注意版本兼容性,同时用户也应该自行选择可用的资源包

8
docs/en/store/README.md Normal file
View File

@ -0,0 +1,8 @@
---
title: Extensions Store
index: false
icon: store
category: store
---
<Catalog />

Some files were not shown because too many files have changed in this diff Show More