mirror of
https://github.com/LiteyukiStudio/LiteyukiBot.git
synced 2025-07-17 05:20:33 +00:00
Compare commits
111 Commits
Author | SHA1 | Date | |
---|---|---|---|
287ab63091 | |||
0c942d9806 | |||
237789e0d4 | |||
e656fa6a48 | |||
6dcb085b53 | |||
55a427e344 | |||
43eef20b71 | |||
b8fdb4146e | |||
cdbede7135 | |||
85a3a9ad52 | |||
943e0c2665 | |||
fd4d680e87 | |||
0b763135c9 | |||
832cc2ec44 | |||
3160b4be69 | |||
775596b5bf | |||
84782a92d8 | |||
6e817111cb | |||
cd8d631348 | |||
af37e61d05 | |||
803b65e08e | |||
aa9abde63a | |||
9c35abc6e2 | |||
1af95a15aa | |||
d2b693b1e0 | |||
b05bbf2f19 | |||
37ed3b0824 | |||
b56ec5ce38 | |||
1fb3f6cd58 | |||
1dfe1a5819 | |||
5d194b8ebe | |||
78810d2ca8 | |||
87d4202ed3 | |||
1d0b18291e | |||
16df5706ff | |||
1b24157f08 | |||
8ba50b7bd6 | |||
502ccb46bb | |||
fcbc410f1a | |||
85a7c28a3a | |||
7bd0da9316 | |||
212c338fcd | |||
137270b886 | |||
079b940d8e | |||
e396db67ce | |||
f37b469ab9 | |||
0a3363ebce | |||
53291822c0 | |||
5f5dcc7f99 | |||
9d27abfe04 | |||
4b4f030fe3 | |||
d5b0f947e0 | |||
9e6372185f | |||
0e125f7c81 | |||
a3ea422ec3 | |||
8d78e643e0 | |||
aa9cae7008 | |||
48085a946d | |||
8e27f6b9b0 | |||
f980e77a4a | |||
01798f7b11 | |||
03057c8ef9 | |||
ee851116d8 | |||
d367903946 | |||
66ade9efc6 | |||
ff41e72378 | |||
169f1645a4 | |||
c3914b2b15 | |||
b356524a9e | |||
85a13251a5 | |||
0417805e46 | |||
d3f1e35a12 | |||
ff585ac7c2 | |||
1b692dd13f | |||
dd00e6ecec | |||
222250bc41 | |||
c36e706731 | |||
adc9b76688 | |||
2e3ea96972 | |||
9b07d41f86 | |||
65ad377099 | |||
a61357f4e2 | |||
60403b2a4f | |||
624afa57ba | |||
36a39e1ed7 | |||
4a872c3435 | |||
90b9d1af1e | |||
551ca06ea7 | |||
61c0cf2c2d | |||
e801a99f67 | |||
beebfe7deb | |||
32e1963d5a | |||
facf5bedb1 | |||
035d43fb18 | |||
0d5f9fee52 | |||
3cb03fa4dc | |||
47ef3f2a49 | |||
8568c7bb99 | |||
02cf058552 | |||
c9157f0e2c | |||
83325e63ea | |||
37b8d969b1 | |||
298bdc7b8c | |||
81a191f8ba | |||
c3fc5d429b | |||
b08c934c78 | |||
1b1ddbdd8d | |||
0d16d53cb7 | |||
6c39ed8ab5 | |||
2f8999b5ad | |||
7107d03b72 |
12
.github/workflows/deploy-docs.yml
vendored
12
.github/workflows/deploy-docs.yml
vendored
@ -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
20
.github/workflows/pypi-publish.yml
vendored
Normal 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
|
26
.gitignore
vendored
26
.gitignore
vendored
@ -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,13 @@ prompt.txt
|
||||
# js
|
||||
**/echarts.js
|
||||
.env
|
||||
|
||||
|
||||
# pdm
|
||||
.pdm-python
|
||||
.pdm-build
|
||||
dist
|
||||
|
||||
doc
|
||||
|
||||
mkdoc2.py
|
@ -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
8
config/default.yml
Normal 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
|
@ -17,4 +17,5 @@ export default defineClientConfig({
|
||||
app.component("geoComp", geoComp);
|
||||
// app.use(ElementPlus);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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'}],
|
||||
|
@ -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/",
|
||||
}
|
||||
]);
|
25
docs/.vuepress/navbar/en.ts
Normal file
25
docs/.vuepress/navbar/en.ts
Normal 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/",
|
||||
},
|
||||
]);
|
2
docs/.vuepress/navbar/index.ts
Normal file
2
docs/.vuepress/navbar/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./zh.js"
|
||||
export * from "./en.js"
|
26
docs/.vuepress/navbar/zh.ts
Normal file
26
docs/.vuepress/navbar/zh.ts
Normal 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/",
|
||||
},
|
||||
]);
|
@ -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"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
73
docs/.vuepress/public/js/en/get_data.js
Normal file
73
docs/.vuepress/public/js/en/get_data.js
Normal 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> 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秒更新一次页面显示
|
@ -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> 当前在线:<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秒更新一次页面显示
|
@ -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",
|
||||
}
|
||||
],
|
||||
});
|
10
docs/.vuepress/sidebar/en.ts
Normal file
10
docs/.vuepress/sidebar/en.ts
Normal 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",
|
||||
}
|
||||
)
|
2
docs/.vuepress/sidebar/index.ts
Normal file
2
docs/.vuepress/sidebar/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./zh.js"
|
||||
export * from "./en.js"
|
11
docs/.vuepress/sidebar/zh.ts
Normal file
11
docs/.vuepress/sidebar/zh.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import {sidebar} from "vuepress-theme-hope";
|
||||
|
||||
|
||||
export const zhSidebarConfig = sidebar(
|
||||
{
|
||||
"/deploy/": "structure",
|
||||
"/usage/": "structure",
|
||||
"/store/": "structure",
|
||||
"/dev/": "structure",
|
||||
}
|
||||
)
|
@ -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,
|
@ -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: 兼容nonebot,melobot等,拥有良好的生态支持
|
||||
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
|
||||
|
||||
---
|
||||
---
|
@ -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
9
docs/deploy/cfg.txt
Normal 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
78
docs/deploy/config.md
Normal 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支持的任何适配器
|
@ -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构建镜像部署**
|
||||
|
@ -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
8
docs/dev/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: 开发及贡献
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category: 开发
|
||||
---
|
||||
|
||||
<Catalog />
|
7
docs/dev/api/README.md
Normal file
7
docs/dev/api/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: liteyuki
|
||||
index: true
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
227
docs/dev/api/bot/README.md
Normal file
227
docs/dev/api/bot/README.md
Normal file
@ -0,0 +1,227 @@
|
||||
---
|
||||
title: liteyuki.bot
|
||||
index: true
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `get_bot() -> LiteyukiBot`
|
||||
|
||||
获取轻雪实例
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
LiteyukiBot: 当前的轻雪实例
|
||||
|
||||
### ***def*** `get_config(key: str, default: Any) -> Any`
|
||||
|
||||
获取配置
|
||||
|
||||
Args:
|
||||
|
||||
key: 配置键
|
||||
|
||||
default: 默认值
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
Any: 配置值
|
||||
|
||||
### ***def*** `get_config_with_compat(key: str, compat_keys: tuple[str], default: Any) -> Any`
|
||||
|
||||
获取配置,兼容旧版本
|
||||
|
||||
Args:
|
||||
|
||||
key: 配置键
|
||||
|
||||
compat_keys: 兼容键
|
||||
|
||||
default: 默认值
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
Any: 配置值
|
||||
|
||||
### ***def*** `print_logo() -> None`
|
||||
|
||||
|
||||
|
||||
### ***class*** `LiteyukiBot`
|
||||
|
||||
|
||||
|
||||
###   ***def*** `__init__(self) -> None`
|
||||
|
||||
 初始化轻雪实例
|
||||
|
||||
Args:
|
||||
|
||||
*args:
|
||||
|
||||
**kwargs: 配置
|
||||
|
||||
###   ***def*** `run(self) -> None`
|
||||
|
||||
 启动逻辑
|
||||
|
||||
###   ***def*** `keep_alive(self) -> None`
|
||||
|
||||
 保持轻雪运行
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `restart(self, delay: int) -> None`
|
||||
|
||||
 重启轻雪本体
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `restart_process(self, name: Optional[str]) -> None`
|
||||
|
||||
 停止轻雪
|
||||
|
||||
Args:
|
||||
|
||||
name: 进程名称, 默认为None, 所有进程
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `init(self) -> None`
|
||||
|
||||
 初始化轻雪, 自动调用
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `init_logger(self) -> None`
|
||||
|
||||
 
|
||||
|
||||
###   ***def*** `stop(self) -> None`
|
||||
|
||||
 停止轻雪
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> None`
|
||||
|
||||
 注册启动前的函数
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> None`
|
||||
|
||||
 注册启动后的函数
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> None`
|
||||
|
||||
 注册停止后的函数:未实现
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> None`
|
||||
|
||||
 注册进程停止前的函数,为子进程停止时调用
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> None`
|
||||
|
||||
 注册进程重启前的函数,为子进程重启时调用
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> None`
|
||||
|
||||
 注册重启后的函数:未实现
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `on_after_nonebot_init(self, func: LIFESPAN_FUNC) -> None`
|
||||
|
||||
 注册nonebot初始化后的函数
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
### ***var*** `executable = sys.executable`
|
||||
|
||||
|
||||
|
||||
### ***var*** `args = sys.argv`
|
||||
|
||||
|
||||
|
||||
### ***var*** `chan_active = get_channel(f'{name}-active')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `cmd = 'start'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `chan_active = get_channel(f'{process_name}-active')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `cmd = 'nohup'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `cmd = 'open'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `cmd = 'nohup'`
|
||||
|
||||
|
||||
|
170
docs/dev/api/bot/lifespan.md
Normal file
170
docs/dev/api/bot/lifespan.md
Normal file
@ -0,0 +1,170 @@
|
||||
---
|
||||
title: liteyuki.bot.lifespan
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
|
||||
|
||||
运行函数
|
||||
|
||||
Args:
|
||||
|
||||
funcs:
|
||||
|
||||
Returns:
|
||||
|
||||
### ***class*** `Lifespan`
|
||||
|
||||
|
||||
|
||||
###   ***def*** `__init__(self) -> None`
|
||||
|
||||
 轻雪生命周期管理,启动、停止、重启
|
||||
|
||||
###   ***@staticmethod***
|
||||
###   ***def*** `run_funcs(funcs: list[LIFESPAN_FUNC | PROCESS_LIFESPAN_FUNC]) -> None`
|
||||
|
||||
 运行函数
|
||||
|
||||
Args:
|
||||
|
||||
funcs:
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
|
||||
|
||||
 注册启动时的函数
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
Returns:
|
||||
|
||||
LIFESPAN_FUNC:
|
||||
|
||||
###   ***def*** `on_after_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
|
||||
|
||||
 注册启动时的函数
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
Returns:
|
||||
|
||||
LIFESPAN_FUNC:
|
||||
|
||||
###   ***def*** `on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
|
||||
|
||||
 注册停止前的函数
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
Returns:
|
||||
|
||||
LIFESPAN_FUNC:
|
||||
|
||||
###   ***def*** `on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
|
||||
|
||||
 注册停止后的函数
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
LIFESPAN_FUNC:
|
||||
|
||||
###   ***def*** `on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
|
||||
|
||||
 注册重启时的函数
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
Returns:
|
||||
|
||||
LIFESPAN_FUNC:
|
||||
|
||||
###   ***def*** `on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC`
|
||||
|
||||
 注册重启后的函数
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
Returns:
|
||||
|
||||
LIFESPAN_FUNC:
|
||||
|
||||
###   ***def*** `on_after_nonebot_init(self, func: Any) -> None`
|
||||
|
||||
 注册 NoneBot 初始化后的函数
|
||||
|
||||
Args:
|
||||
|
||||
func:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `before_start(self) -> None`
|
||||
|
||||
 启动前
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `after_start(self) -> None`
|
||||
|
||||
 启动后
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `before_process_shutdown(self) -> None`
|
||||
|
||||
 停止前
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `after_shutdown(self) -> None`
|
||||
|
||||
 停止后
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `before_process_restart(self) -> None`
|
||||
|
||||
 重启前
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `after_restart(self) -> None`
|
||||
|
||||
 重启后
|
||||
|
||||
Returns:
|
||||
|
||||
### ***var*** `tasks = []`
|
||||
|
||||
|
||||
|
||||
### ***var*** `loop = asyncio.get_event_loop()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `loop = asyncio.new_event_loop()`
|
||||
|
||||
|
||||
|
7
docs/dev/api/comm/README.md
Normal file
7
docs/dev/api/comm/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: liteyuki.comm
|
||||
index: true
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
149
docs/dev/api/comm/channel.md
Normal file
149
docs/dev/api/comm/channel.md
Normal file
@ -0,0 +1,149 @@
|
||||
---
|
||||
title: liteyuki.comm.channel_
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `set_channel(name: str, channel: Channel) -> None`
|
||||
|
||||
设置通道实例
|
||||
|
||||
Args:
|
||||
|
||||
name: 通道名称
|
||||
|
||||
channel: 通道实例
|
||||
|
||||
### ***def*** `set_channels(channels: dict[str, Channel]) -> None`
|
||||
|
||||
设置通道实例
|
||||
|
||||
Args:
|
||||
|
||||
channels: 通道名称
|
||||
|
||||
### ***def*** `get_channel(name: str) -> Channel`
|
||||
|
||||
获取通道实例
|
||||
|
||||
Args:
|
||||
|
||||
name: 通道名称
|
||||
|
||||
Returns:
|
||||
|
||||
### ***def*** `get_channels() -> dict[str, Channel]`
|
||||
|
||||
获取通道实例
|
||||
|
||||
Returns:
|
||||
|
||||
### ***def*** `on_set_channel(data: tuple[str, dict[str, Any]]) -> None`
|
||||
|
||||
|
||||
|
||||
### ***def*** `on_get_channel(data: tuple[str, dict[str, Any]]) -> None`
|
||||
|
||||
|
||||
|
||||
### ***def*** `on_get_channels(data: tuple[str, dict[str, Any]]) -> None`
|
||||
|
||||
|
||||
|
||||
### ***def*** `decorator(func: Callable[[T], Any]) -> Callable[[T], Any]`
|
||||
|
||||
|
||||
|
||||
### ***async def*** `wrapper(data: T) -> Any`
|
||||
|
||||
|
||||
|
||||
### ***class*** `Channel(Generic[T])`
|
||||
|
||||
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
|
||||
|
||||
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
|
||||
|
||||
###   ***def*** `__init__(self, _id: str, type_check: bool) -> None`
|
||||
|
||||
 初始化通道
|
||||
|
||||
Args:
|
||||
|
||||
_id: 通道ID
|
||||
|
||||
###   ***def*** `send(self, data: T) -> None`
|
||||
|
||||
 发送数据
|
||||
|
||||
Args:
|
||||
|
||||
data: 数据
|
||||
|
||||
###   ***def*** `receive(self) -> T`
|
||||
|
||||
 接收数据
|
||||
|
||||
Args:
|
||||
|
||||
###   ***def*** `close(self) -> None`
|
||||
|
||||
 关闭通道
|
||||
|
||||
###   ***def*** `on_receive(self, filter_func: Optional[FILTER_FUNC]) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]`
|
||||
|
||||
 接收数据并执行函数
|
||||
|
||||
Args:
|
||||
|
||||
filter_func: 过滤函数,为None则不过滤
|
||||
|
||||
Returns:
|
||||
|
||||
装饰器,装饰一个函数在接收到数据后执行
|
||||
|
||||
### ***var*** `T = TypeVar('T')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `channel_deliver_active_channel = Channel(_id='channel_deliver_active_channel')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `channel_deliver_passive_channel = Channel(_id='channel_deliver_passive_channel')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `recv_chan = data[1]['recv_chan']`
|
||||
|
||||
|
||||
|
||||
### ***var*** `recv_chan = Channel[Channel[Any]]('recv_chan')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `recv_chan = Channel[dict[str, Channel[Any]]]('recv_chan')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `data = self.conn_recv.recv()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `func = _callback_funcs[func_id]`
|
||||
|
||||
|
||||
|
||||
### ***var*** `func = _callback_funcs[func_id]`
|
||||
|
||||
|
||||
|
||||
### ***var*** `data = self.conn_recv.recv()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `data = self.conn_recv.recv()`
|
||||
|
||||
|
||||
|
15
docs/dev/api/comm/event.md
Normal file
15
docs/dev/api/comm/event.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: liteyuki.comm.event
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***class*** `Event`
|
||||
|
||||
事件类
|
||||
|
||||
###   ***def*** `__init__(self, name: str, data: dict[str, Any]) -> None`
|
||||
|
||||
 
|
||||
|
140
docs/dev/api/comm/storage.md
Normal file
140
docs/dev/api/comm/storage.md
Normal file
@ -0,0 +1,140 @@
|
||||
---
|
||||
title: liteyuki.comm.storage
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `on_get(data: tuple[str, dict[str, Any]]) -> None`
|
||||
|
||||
|
||||
|
||||
### ***def*** `on_set(data: tuple[str, dict[str, Any]]) -> None`
|
||||
|
||||
|
||||
|
||||
### ***def*** `on_delete(data: tuple[str, dict[str, Any]]) -> None`
|
||||
|
||||
|
||||
|
||||
### ***def*** `on_get_all(data: tuple[str, dict[str, Any]]) -> None`
|
||||
|
||||
|
||||
|
||||
### ***class*** `KeyValueStore`
|
||||
|
||||
|
||||
|
||||
###   ***def*** `__init__(self) -> None`
|
||||
|
||||
 
|
||||
|
||||
###   ***def*** `set(self, key: str, value: Any) -> None`
|
||||
|
||||
 设置键值对
|
||||
|
||||
Args:
|
||||
|
||||
key: 键
|
||||
|
||||
value: 值
|
||||
|
||||
###   ***def*** `get(self, key: str, default: Optional[Any]) -> Optional[Any]`
|
||||
|
||||
 获取键值对
|
||||
|
||||
Args:
|
||||
|
||||
key: 键
|
||||
|
||||
default: 默认值
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
Any: 值
|
||||
|
||||
###   ***def*** `delete(self, key: str, ignore_key_error: bool) -> None`
|
||||
|
||||
 删除键值对
|
||||
|
||||
Args:
|
||||
|
||||
key: 键
|
||||
|
||||
ignore_key_error: 是否忽略键不存在的错误
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `get_all(self) -> dict[str, Any]`
|
||||
|
||||
 获取所有键值对
|
||||
|
||||
Returns:
|
||||
|
||||
dict[str, Any]: 键值对
|
||||
|
||||
### ***class*** `GlobalKeyValueStore`
|
||||
|
||||
|
||||
|
||||
###   ***@classmethod***
|
||||
###   ***def*** `get_instance(cls: Any) -> None`
|
||||
|
||||
 
|
||||
|
||||
###   ***attr*** `_instance: None`
|
||||
|
||||
###   ***attr*** `_lock: threading.Lock()`
|
||||
|
||||
### ***var*** `key = data[1]['key']`
|
||||
|
||||
|
||||
|
||||
### ***var*** `default = data[1]['default']`
|
||||
|
||||
|
||||
|
||||
### ***var*** `recv_chan = data[1]['recv_chan']`
|
||||
|
||||
|
||||
|
||||
### ***var*** `key = data[1]['key']`
|
||||
|
||||
|
||||
|
||||
### ***var*** `value = data[1]['value']`
|
||||
|
||||
|
||||
|
||||
### ***var*** `key = data[1]['key']`
|
||||
|
||||
|
||||
|
||||
### ***var*** `recv_chan = data[1]['recv_chan']`
|
||||
|
||||
|
||||
|
||||
### ***var*** `lock = _get_lock(key)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `lock = _get_lock(key)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `recv_chan = Channel[Optional[Any]]('recv_chan')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `lock = _get_lock(key)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `recv_chan = Channel[dict[str, Any]]('recv_chan')`
|
||||
|
||||
|
||||
|
99
docs/dev/api/config.md
Normal file
99
docs/dev/api/config.md
Normal file
@ -0,0 +1,99 @@
|
||||
---
|
||||
title: liteyuki.config
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `flat_config(config: dict[str, Any]) -> dict[str, Any]`
|
||||
|
||||
扁平化配置文件
|
||||
|
||||
|
||||
|
||||
{a:{b:{c:1}}} -> {"a.b.c": 1}
|
||||
|
||||
Args:
|
||||
|
||||
config: 配置项目
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
扁平化后的配置文件,但也包含原有的键值对
|
||||
|
||||
### ***def*** `load_from_yaml(file: str) -> dict[str, Any]`
|
||||
|
||||
Load config from yaml file
|
||||
|
||||
### ***def*** `load_from_json(file: str) -> dict[str, Any]`
|
||||
|
||||
Load config from json file
|
||||
|
||||
### ***def*** `load_from_toml(file: str) -> dict[str, Any]`
|
||||
|
||||
Load config from toml file
|
||||
|
||||
### ***def*** `load_from_files() -> dict[str, Any]`
|
||||
|
||||
从指定文件加载配置项,会自动识别文件格式
|
||||
|
||||
默认执行扁平化选项
|
||||
|
||||
### ***def*** `load_configs_from_dirs() -> dict[str, Any]`
|
||||
|
||||
从目录下加载配置文件,不递归
|
||||
|
||||
按照读取文件的优先级反向覆盖
|
||||
|
||||
默认执行扁平化选项
|
||||
|
||||
### ***def*** `load_config_in_default(no_waring: bool) -> dict[str, Any]`
|
||||
|
||||
从一个标准的轻雪项目加载配置文件
|
||||
|
||||
项目目录下的config.*和config目录下的所有配置文件
|
||||
|
||||
项目目录下的配置文件优先
|
||||
|
||||
### ***class*** `SatoriNodeConfig(BaseModel)`
|
||||
|
||||
|
||||
|
||||
### ***class*** `SatoriConfig(BaseModel)`
|
||||
|
||||
|
||||
|
||||
### ***class*** `BasicConfig(BaseModel)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `new_config = copy.deepcopy(config)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `config = yaml.safe_load(open(file, 'r', encoding='utf-8'))`
|
||||
|
||||
|
||||
|
||||
### ***var*** `config = json.load(open(file, 'r', encoding='utf-8'))`
|
||||
|
||||
|
||||
|
||||
### ***var*** `config = toml.load(open(file, 'r', encoding='utf-8'))`
|
||||
|
||||
|
||||
|
||||
### ***var*** `config = {}`
|
||||
|
||||
|
||||
|
||||
### ***var*** `config = {}`
|
||||
|
||||
|
||||
|
||||
### ***var*** `config = load_configs_from_dirs('config', no_waring=no_waring)`
|
||||
|
||||
|
||||
|
7
docs/dev/api/core/README.md
Normal file
7
docs/dev/api/core/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: liteyuki.core
|
||||
index: true
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
111
docs/dev/api/core/manager.md
Normal file
111
docs/dev/api/core/manager.md
Normal file
@ -0,0 +1,111 @@
|
||||
---
|
||||
title: liteyuki.core.manager
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***class*** `ChannelDeliver`
|
||||
|
||||
|
||||
|
||||
###   ***def*** `__init__(self, active: Channel[Any], passive: Channel[Any], channel_deliver_active: Channel[Channel[Any]], channel_deliver_passive: Channel[tuple[str, dict]]) -> None`
|
||||
|
||||
 
|
||||
|
||||
### ***class*** `ProcessManager`
|
||||
|
||||
进程管理器
|
||||
|
||||
###   ***def*** `__init__(self, lifespan: 'Lifespan') -> None`
|
||||
|
||||
 
|
||||
|
||||
###   ***def*** `start(self, name: str) -> None`
|
||||
|
||||
 开启后自动监控进程,并添加到进程字典中
|
||||
|
||||
Args:
|
||||
|
||||
name:
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `start_all(self) -> None`
|
||||
|
||||
 启动所有进程
|
||||
|
||||
###   ***def*** `add_target(self, name: str, target: TARGET_FUNC, args: tuple, kwargs: Any) -> None`
|
||||
|
||||
 添加进程
|
||||
|
||||
Args:
|
||||
|
||||
name: 进程名,用于获取和唯一标识
|
||||
|
||||
target: 进程函数
|
||||
|
||||
args: 进程函数参数
|
||||
|
||||
kwargs: 进程函数关键字参数,通常会默认传入chan_active和chan_passive
|
||||
|
||||
###   ***def*** `join_all(self) -> None`
|
||||
|
||||
 
|
||||
|
||||
###   ***def*** `terminate(self, name: str) -> None`
|
||||
|
||||
 终止进程并从进程字典中删除
|
||||
|
||||
Args:
|
||||
|
||||
name:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
###   ***def*** `terminate_all(self) -> None`
|
||||
|
||||
 
|
||||
|
||||
###   ***def*** `is_process_alive(self, name: str) -> bool`
|
||||
|
||||
 检查进程是否存活
|
||||
|
||||
Args:
|
||||
|
||||
name:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
### ***var*** `TIMEOUT = 10`
|
||||
|
||||
|
||||
|
||||
### ***var*** `chan_active = get_channel(f'{name}-active')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `channel_deliver = ChannelDeliver(active=chan_active, passive=chan_passive, channel_deliver_active=channel_deliver_active_channel, channel_deliver_passive=channel_deliver_passive_channel)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `process = self.processes[name]`
|
||||
|
||||
|
||||
|
||||
### ***var*** `process = Process(target=self.targets[name][0], args=self.targets[name][1], kwargs=self.targets[name][2], daemon=True)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `data = chan_active.receive()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `kwargs = {}`
|
||||
|
||||
|
||||
|
7
docs/dev/api/dev/README.md
Normal file
7
docs/dev/api/dev/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: liteyuki.dev
|
||||
index: true
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
91
docs/dev/api/dev/observer.md
Normal file
91
docs/dev/api/dev/observer.md
Normal file
@ -0,0 +1,91 @@
|
||||
---
|
||||
title: liteyuki.dev.observer
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `debounce(wait: Any) -> None`
|
||||
|
||||
防抖函数
|
||||
|
||||
### ***def*** `on_file_system_event(directories: tuple[str], recursive: bool, event_filter: FILTER_FUNC) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]`
|
||||
|
||||
注册文件系统变化监听器
|
||||
|
||||
Args:
|
||||
|
||||
directories: 监听目录们
|
||||
|
||||
recursive: 是否递归监听子目录
|
||||
|
||||
event_filter: 事件过滤器, 返回True则执行回调函数
|
||||
|
||||
Returns:
|
||||
|
||||
装饰器,装饰一个函数在接收到数据后执行
|
||||
|
||||
### ***def*** `decorator(func: Any) -> None`
|
||||
|
||||
|
||||
|
||||
### ***def*** `decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC`
|
||||
|
||||
|
||||
|
||||
### ***def*** `wrapper() -> None`
|
||||
|
||||
|
||||
|
||||
### ***def*** `wrapper(event: FileSystemEvent) -> None`
|
||||
|
||||
|
||||
|
||||
### ***class*** `CodeModifiedHandler(FileSystemEventHandler)`
|
||||
|
||||
Handler for code file changes
|
||||
|
||||
###   ***def*** `on_modified(self, event: Any) -> None`
|
||||
|
||||
 
|
||||
|
||||
###   ***def*** `on_created(self, event: Any) -> None`
|
||||
|
||||
 
|
||||
|
||||
###   ***def*** `on_deleted(self, event: Any) -> None`
|
||||
|
||||
 
|
||||
|
||||
###   ***def*** `on_moved(self, event: Any) -> None`
|
||||
|
||||
 
|
||||
|
||||
###   ***def*** `on_any_event(self, event: Any) -> None`
|
||||
|
||||
 
|
||||
|
||||
### ***var*** `liteyuki_bot = get_bot()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `observer = Observer()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `last_call_time = None`
|
||||
|
||||
|
||||
|
||||
### ***var*** `code_modified_handler = CodeModifiedHandler()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `current_time = time.time()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `last_call_time = current_time`
|
||||
|
||||
|
||||
|
27
docs/dev/api/dev/plugin.md
Normal file
27
docs/dev/api/dev/plugin.md
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
title: liteyuki.dev.plugin
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `run_plugins() -> None`
|
||||
|
||||
运行插件,无需手动初始化bot
|
||||
|
||||
Args:
|
||||
|
||||
module_path: 插件路径,参考`liteyuki.load_plugin`的函数签名
|
||||
|
||||
### ***var*** `cfg = load_config_in_default()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `plugins = cfg.get('liteyuki.plugins', [])`
|
||||
|
||||
|
||||
|
||||
### ***var*** `bot = LiteyukiBot(**cfg)`
|
||||
|
||||
|
||||
|
11
docs/dev/api/exception.md
Normal file
11
docs/dev/api/exception.md
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
title: liteyuki.exception
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***class*** `LiteyukiException(BaseException)`
|
||||
|
||||
Liteyuki的异常基类。
|
||||
|
21
docs/dev/api/log.md
Normal file
21
docs/dev/api/log.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
title: liteyuki.log
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `get_format(level: str) -> str`
|
||||
|
||||
|
||||
|
||||
### ***def*** `init_log(config: dict) -> None`
|
||||
|
||||
在语言加载完成后执行
|
||||
|
||||
Returns:
|
||||
|
||||
### ***var*** `show_icon = config.get('log_icon', True)`
|
||||
|
||||
|
||||
|
271
docs/dev/api/mkdoc.md
Normal file
271
docs/dev/api/mkdoc.md
Normal file
@ -0,0 +1,271 @@
|
||||
---
|
||||
title: liteyuki.mkdoc
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `get_relative_path(base_path: str, target_path: str) -> str`
|
||||
|
||||
获取相对路径
|
||||
|
||||
Args:
|
||||
|
||||
base_path: 基础路径
|
||||
|
||||
target_path: 目标路径
|
||||
|
||||
### ***def*** `write_to_files(file_data: dict[str, str]) -> None`
|
||||
|
||||
输出文件
|
||||
|
||||
Args:
|
||||
|
||||
file_data: 文件数据 相对路径
|
||||
|
||||
### ***def*** `get_file_list(module_folder: str) -> None`
|
||||
|
||||
|
||||
|
||||
### ***def*** `get_module_info_normal(file_path: str, ignore_private: bool) -> ModuleInfo`
|
||||
|
||||
获取函数和类
|
||||
|
||||
Args:
|
||||
|
||||
file_path: Python 文件路径
|
||||
|
||||
ignore_private: 忽略私有函数和类
|
||||
|
||||
Returns:
|
||||
|
||||
模块信息
|
||||
|
||||
### ***def*** `generate_markdown(module_info: ModuleInfo, front_matter: Any) -> str`
|
||||
|
||||
生成模块的Markdown
|
||||
|
||||
你可在此自定义生成的Markdown格式
|
||||
|
||||
Args:
|
||||
|
||||
module_info: 模块信息
|
||||
|
||||
front_matter: 自定义选项title, index, icon, category
|
||||
|
||||
Returns:
|
||||
|
||||
Markdown 字符串
|
||||
|
||||
### ***def*** `generate_docs(module_folder: str, output_dir: str, with_top: bool, ignored_paths: Any) -> None`
|
||||
|
||||
生成文档
|
||||
|
||||
Args:
|
||||
|
||||
module_folder: 模块文件夹
|
||||
|
||||
output_dir: 输出文件夹
|
||||
|
||||
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b, True时例如docs/api/module/module_a.md, docs/api/module/module_b.md
|
||||
|
||||
ignored_paths: 忽略的路径
|
||||
|
||||
### ***class*** `DefType(Enum)`
|
||||
|
||||
|
||||
|
||||
###   ***attr*** `FUNCTION: 'function'`
|
||||
|
||||
###   ***attr*** `METHOD: 'method'`
|
||||
|
||||
###   ***attr*** `STATIC_METHOD: 'staticmethod'`
|
||||
|
||||
###   ***attr*** `CLASS_METHOD: 'classmethod'`
|
||||
|
||||
###   ***attr*** `PROPERTY: 'property'`
|
||||
|
||||
### ***class*** `FunctionInfo(BaseModel)`
|
||||
|
||||
|
||||
|
||||
### ***class*** `AttributeInfo(BaseModel)`
|
||||
|
||||
|
||||
|
||||
### ***class*** `ClassInfo(BaseModel)`
|
||||
|
||||
|
||||
|
||||
### ***class*** `ModuleInfo(BaseModel)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `NO_TYPE_ANY = 'Any'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `NO_TYPE_HINT = 'NoTypeHint'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `FUNCTION = 'function'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `METHOD = 'method'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `STATIC_METHOD = 'staticmethod'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `CLASS_METHOD = 'classmethod'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `PROPERTY = 'property'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `file_list = []`
|
||||
|
||||
|
||||
|
||||
### ***var*** `dot_sep_module_path = file_path.replace(os.sep, '.').replace('.py', '').replace('.pyi', '')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `module_docstring = ast.get_docstring(tree)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `module_info = ModuleInfo(module_path=dot_sep_module_path, functions=[], classes=[], attributes=[], docstring=module_docstring if module_docstring else '')`
|
||||
|
||||
|
||||
|
||||
### ***var*** `content = ''`
|
||||
|
||||
|
||||
|
||||
### ***var*** `front_matter = '---\n' + '\n'.join([f'{k}: {v}' for k, v in front_matter.items()]) + '\n---\n\n'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `file_list = get_file_list(module_folder)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `replace_data = {'__init__': 'README', '.py': '.md'}`
|
||||
|
||||
|
||||
|
||||
### ***var*** `file_content = file.read()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `tree = ast.parse(file_content)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in func.args]`
|
||||
|
||||
|
||||
|
||||
### ***var*** `ignored_paths = []`
|
||||
|
||||
|
||||
|
||||
### ***var*** `no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path`
|
||||
|
||||
|
||||
|
||||
### ***var*** `abs_md_path = os.path.join(output_dir, rel_md_path)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `module_info = get_module_info_normal(pyfile_path)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `md_content = generate_markdown(module_info, front_matter)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ''`
|
||||
|
||||
|
||||
|
||||
### ***var*** `rel_md_path = rel_md_path.replace(rk, rv)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `front_matter = {'title': module_info.module_path.replace('.__init__', '').replace('_', '\\n'), 'index': 'true', 'icon': 'laptop-code', 'category': 'API'}`
|
||||
|
||||
|
||||
|
||||
### ***var*** `front_matter = {'title': module_info.module_path.replace('_', '\\n'), 'order': '1', 'icon': 'laptop-code', 'category': 'API'}`
|
||||
|
||||
|
||||
|
||||
### ***var*** `function_docstring = ast.get_docstring(node)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `func_info = FunctionInfo(name=node.name, args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args], return_type=ast.unparse(node.returns) if node.returns else 'None', docstring=function_docstring if function_docstring else '', type=DefType.FUNCTION, is_async=isinstance(node, ast.AsyncFunctionDef))`
|
||||
|
||||
|
||||
|
||||
### ***var*** `class_docstring = ast.get_docstring(node)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `class_info = ClassInfo(name=node.name, docstring=class_docstring if class_docstring else '', methods=[], attributes=[], inherit=[ast.unparse(base) for base in node.bases])`
|
||||
|
||||
|
||||
|
||||
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] else arg[0] for arg in method.args]`
|
||||
|
||||
|
||||
|
||||
### ***var*** `args_with_type = [f'{arg[0]}: {arg[1]}' if arg[1] and arg[0] != 'self' else arg[0] for arg in method.args]`
|
||||
|
||||
|
||||
|
||||
### ***var*** `first_arg = node.args.args[0]`
|
||||
|
||||
|
||||
|
||||
### ***var*** `method_docstring = ast.get_docstring(class_node)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `def_type = DefType.METHOD`
|
||||
|
||||
|
||||
|
||||
### ***var*** `def_type = DefType.STATIC_METHOD`
|
||||
|
||||
|
||||
|
||||
### ***var*** `attr_type = NO_TYPE_HINT`
|
||||
|
||||
|
||||
|
||||
### ***var*** `def_type = DefType.CLASS_METHOD`
|
||||
|
||||
|
||||
|
||||
### ***var*** `attr_type = ast.unparse(node.value.annotation)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `def_type = DefType.PROPERTY`
|
||||
|
||||
|
||||
|
15
docs/dev/api/plugin/README.md
Normal file
15
docs/dev/api/plugin/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: liteyuki.plugin
|
||||
index: true
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `get_loaded_plugins() -> dict[str, Plugin]`
|
||||
|
||||
获取已加载的插件
|
||||
|
||||
Returns:
|
||||
|
||||
dict[str, Plugin]: 插件字典
|
||||
|
103
docs/dev/api/plugin/load.md
Normal file
103
docs/dev/api/plugin/load.md
Normal file
@ -0,0 +1,103 @@
|
||||
---
|
||||
title: liteyuki.plugin.load
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `load_plugin(module_path: str | Path) -> Optional[Plugin]`
|
||||
|
||||
加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。
|
||||
|
||||
|
||||
|
||||
参数:
|
||||
|
||||
module_path: 插件名称 `path.to.your.plugin`
|
||||
|
||||
或插件路径 `pathlib.Path(path/to/your/plugin)`
|
||||
|
||||
### ***def*** `load_plugins() -> set[Plugin]`
|
||||
|
||||
导入文件夹下多个插件
|
||||
|
||||
|
||||
|
||||
参数:
|
||||
|
||||
plugin_dir: 文件夹路径
|
||||
|
||||
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
|
||||
|
||||
### ***def*** `format_display_name(display_name: str, plugin_type: PluginType) -> str`
|
||||
|
||||
设置插件名称颜色,根据不同类型插件设置颜色
|
||||
|
||||
Args:
|
||||
|
||||
display_name: 插件名称
|
||||
|
||||
plugin_type: 插件类型
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
str: 设置后的插件名称 <y>name</y>
|
||||
|
||||
### ***var*** `module_path = path_to_module_name(Path(module_path)) if isinstance(module_path, Path) else module_path`
|
||||
|
||||
|
||||
|
||||
### ***var*** `plugins = set()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `color = 'y'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `module = import_module(module_path)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `display_name = module.__name__.split('.')[-1]`
|
||||
|
||||
|
||||
|
||||
### ***var*** `display_name = format_display_name(f"{metadata.name}({module.__name__.split('.')[-1]})", metadata.type)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `path = Path(os.path.join(dir_path, f))`
|
||||
|
||||
|
||||
|
||||
### ***var*** `module_name = None`
|
||||
|
||||
|
||||
|
||||
### ***var*** `color = 'm'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `color = 'g'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `color = 'e'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `color = 'c'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `module_name = f'{path_to_module_name(Path(dir_path))}.{f[:-3]}'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `module_name = path_to_module_name(path)`
|
||||
|
||||
|
||||
|
7
docs/dev/api/plugin/manager.md
Normal file
7
docs/dev/api/plugin/manager.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: liteyuki.plugin.manager
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
89
docs/dev/api/plugin/model.md
Normal file
89
docs/dev/api/plugin/model.md
Normal file
@ -0,0 +1,89 @@
|
||||
---
|
||||
title: liteyuki.plugin.model
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***class*** `PluginType(Enum)`
|
||||
|
||||
插件类型枚举值
|
||||
|
||||
###   ***attr*** `APPLICATION: 'application'`
|
||||
|
||||
###   ***attr*** `SERVICE: 'service'`
|
||||
|
||||
###   ***attr*** `IMPLEMENTATION: 'implementation'`
|
||||
|
||||
###   ***attr*** `MODULE: 'module'`
|
||||
|
||||
###   ***attr*** `UNCLASSIFIED: 'unclassified'`
|
||||
|
||||
### ***class*** `PluginMetadata(BaseModel)`
|
||||
|
||||
轻雪插件元数据,由插件编写者提供,name为必填项
|
||||
|
||||
Attributes:
|
||||
|
||||
----------
|
||||
|
||||
|
||||
|
||||
name: str
|
||||
|
||||
插件名称
|
||||
|
||||
description: str
|
||||
|
||||
插件描述
|
||||
|
||||
usage: str
|
||||
|
||||
插件使用方法
|
||||
|
||||
type: str
|
||||
|
||||
插件类型
|
||||
|
||||
author: str
|
||||
|
||||
插件作者
|
||||
|
||||
homepage: str
|
||||
|
||||
插件主页
|
||||
|
||||
extra: dict[str, Any]
|
||||
|
||||
额外信息
|
||||
|
||||
### ***class*** `Plugin(BaseModel)`
|
||||
|
||||
存储插件信息
|
||||
|
||||
###   ***attr*** `model_config: {'arbitrary_types_allowed': True}`
|
||||
|
||||
### ***var*** `APPLICATION = 'application'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `SERVICE = 'service'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `IMPLEMENTATION = 'implementation'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `MODULE = 'module'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `UNCLASSIFIED = 'unclassified'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `model_config = {'arbitrary_types_allowed': True}`
|
||||
|
||||
|
||||
|
79
docs/dev/api/utils.md
Normal file
79
docs/dev/api/utils.md
Normal file
@ -0,0 +1,79 @@
|
||||
---
|
||||
title: liteyuki.utils
|
||||
order: 1
|
||||
icon: laptop-code
|
||||
category: API
|
||||
---
|
||||
|
||||
### ***def*** `is_coroutine_callable(call: Callable[..., Any]) -> bool`
|
||||
|
||||
判断是否为协程可调用对象
|
||||
|
||||
Args:
|
||||
|
||||
call: 可调用对象
|
||||
|
||||
Returns:
|
||||
|
||||
bool: 是否为协程可调用对象
|
||||
|
||||
### ***def*** `run_coroutine() -> None`
|
||||
|
||||
运行协程
|
||||
|
||||
Args:
|
||||
|
||||
coro:
|
||||
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
### ***def*** `path_to_module_name(path: Path) -> str`
|
||||
|
||||
转换路径为模块名
|
||||
|
||||
Args:
|
||||
|
||||
path: 路径a/b/c/d -> a.b.c.d
|
||||
|
||||
Returns:
|
||||
|
||||
str: 模块名
|
||||
|
||||
### ***def*** `async_wrapper(func: Callable[..., Any]) -> Callable[..., Coroutine]`
|
||||
|
||||
异步包装器
|
||||
|
||||
Args:
|
||||
|
||||
func: Sync Callable
|
||||
|
||||
Returns:
|
||||
|
||||
Coroutine: Asynchronous Callable
|
||||
|
||||
### ***async def*** `wrapper() -> None`
|
||||
|
||||
|
||||
|
||||
### ***var*** `IS_MAIN_PROCESS = multiprocessing.current_process().name == 'MainProcess'`
|
||||
|
||||
|
||||
|
||||
### ***var*** `func_ = getattr(call, '__call__', None)`
|
||||
|
||||
|
||||
|
||||
### ***var*** `rel_path = path.resolve().relative_to(Path.cwd().resolve())`
|
||||
|
||||
|
||||
|
||||
### ***var*** `loop = asyncio.get_event_loop()`
|
||||
|
||||
|
||||
|
||||
### ***var*** `loop = asyncio.new_event_loop()`
|
||||
|
||||
|
||||
|
99
docs/dev/dev_comm.md
Normal file
99
docs/dev/dev_comm.md
Normal 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)
|
@ -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
82
docs/dev/dev_lyplugin.md
Normal 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进程
|
@ -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
82
docs/en/README.md
Normal 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
8
docs/en/deploy/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Deploy
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category: deploy
|
||||
---
|
||||
|
||||
<Catalog />
|
9
docs/en/deploy/cfg.txt
Normal file
9
docs/en/deploy/cfg.txt
Normal 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
77
docs/en/deploy/config.md
Normal 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
57
docs/en/deploy/fandq.md
Normal 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
61
docs/en/deploy/install.md
Normal 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
8
docs/en/dev/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Contribute
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category: 开发
|
||||
---
|
||||
|
||||
<Catalog />
|
99
docs/en/dev/dev_comm.md
Normal file
99
docs/en/dev/dev_comm.md
Normal 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
74
docs/en/dev/dev_lyfunc.md
Normal 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)实现语法高亮
|
63
docs/en/dev/dev_lyplugin.md
Normal file
63
docs/en/dev/dev_lyplugin.md
Normal 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进程
|
53
docs/en/dev/dev_resource_pack.md
Normal file
53
docs/en/dev/dev_resource_pack.md
Normal 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
8
docs/en/store/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Extensions Store
|
||||
index: false
|
||||
icon: store
|
||||
category: store
|
||||
---
|
||||
|
||||
<Catalog />
|
8
docs/en/store/plugin.md
Normal file
8
docs/en/store/plugin.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Plugin Store
|
||||
icon: plug
|
||||
order: 2
|
||||
category: extension
|
||||
---
|
||||
|
||||
<pluginStoreComp />
|
8
docs/en/store/resource.md
Normal file
8
docs/en/store/resource.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Resource Store
|
||||
icon: box
|
||||
order: 1
|
||||
category: extension
|
||||
---
|
||||
|
||||
<resourceStoreComp />
|
8
docs/en/usage/README.md
Normal file
8
docs/en/usage/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Usage
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category: usage
|
||||
---
|
||||
|
||||
<Catalog />
|
16
docs/en/usage/agreement.md
Normal file
16
docs/en/usage/agreement.md
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
title: User Agreement
|
||||
icon: user-secret
|
||||
order: 3
|
||||
category: usage
|
||||
---
|
||||
|
||||
1. 本项目遵循`MIT`协议,你可以自由使用,修改,分发,但是请保留原作者信息
|
||||
2. 你可以选择开启`auto_report`(默认开启),轻雪会收集以下内容
|
||||
- 运行环境的设备信息:CPU,内存,系统信息及Python信息
|
||||
- 插件信息(不含插件数据)
|
||||
- 部分异常信息,
|
||||
- 会话负载信息(不含隐私部分)
|
||||
以上内容仅用于项目的优化,不包含任何隐私信息,且通过安全的方式传输到轻雪的服务器,若你不希望提供这些信息,可以在配置文件中把`auto_report`设定为`false`
|
||||
3. 本项目不会收集用户的任何隐私信息,但请注意甄别第三方插件的安全性
|
||||
4. 使用此项目代表你已经同意以上协议
|
124
docs/en/usage/basic_command.md
Normal file
124
docs/en/usage/basic_command.md
Normal file
@ -0,0 +1,124 @@
|
||||
---
|
||||
title: Basic Commands
|
||||
icon: comment
|
||||
order: 1
|
||||
category: usage
|
||||
---
|
||||
|
||||
# 基础插件
|
||||
---
|
||||
|
||||
> [!tip]
|
||||
> **参数**:`<param>`为必填参数,`[option]`为可选参数。
|
||||
>
|
||||
> **命令别名**:配置了命令别名的命令可以使用别名代替原命令,例如`npm install ~`可以使用`插件 安装 ~`代替。
|
||||
|
||||
## **轻雪命令`liteyuki_command`**
|
||||
|
||||
|
||||
| 命令 | 说明 | 权限 | 举例 | 可用参数 |
|
||||
| :----------------------------------------: | :---------------------------------------------------------------------------------------------: | :----------------------------------------: | :---------------------------------------------------------: | :----------------------------------------------------------------------------------: |
|
||||
| `reload-liteyuki` | 重载轻雪 | 超级用户 | ❌ | ❌ |
|
||||
| `update-liteyuki` | 更新轻雪 | 超级用户 | ❌ | ❌ |
|
||||
| `liteecho` | 查看当前bot 版本 | 超级用户 | ❌ | ❌ |
|
||||
| `status` | 查看统计信息和状态 | 超级用户 | ❌ | ❌ |
|
||||
| `config set <key> value` | 添加配置项,若存在则会覆盖,输入值会被执行以转换为正确的类型,"10"和10是不一样的 | 超级用户 | `config set name 'liteyuki-bot'` | `<key>`: 若存在则覆盖, 若不存在则创建于`config.yml` ; `value`: yml格式的所有合法内容 |
|
||||
| `config get [key] ` | 查询配置项,不带key返回配置项列表,推荐私聊使用 | 超级用户 | `config get name` | `<key>`: 若存在则返回, 若不存在则返回空 |
|
||||
| `switch-image-mode ` | 在普通图片和Markdown大图之间切换,该功能需要commit:505468b及以后的Lagrange.OneBot,默认普通图片 | 超级用户 | `switch-image-mode` | ❌ |
|
||||
| `/api api_name [args] ` | 调用机器人API | 超级用户 | `/api get_group_member_list group_id=1234567` | `<args>`: 参数列表, 格式为onebot v11协议api, 可用%20代替空格 |
|
||||
| `/function function_name [args] [kwargs] ` | 调用机器人函数(`.lyfunction`语法) | 超级用户 | `/function send_group_msg group_id=1234567 message='hello'` | `<args>`和`<kwargs>`: 参数列表, api格式为onebot v11协议api |
|
||||
| group enable/disable [group_id] | 在群聊启用/停用机器人,group_id仅超级用户可用 | 超级用户,群聊仅群主、管理员、超级用户可用 | `group enable 1145141919810` | `<group_id>`: 群号 |
|
||||
| liteyuki-docs | 查看轻雪文档 | 所有人 | ❌ | ❌ |
|
||||
|
||||
|
||||
---
|
||||
### **命令别名**
|
||||
|
||||
|
||||
| 命令 | 别名 |
|
||||
| :---------------: | :----------------------------------: |
|
||||
| status | 状态 |
|
||||
| reload-liteyuki | 重启轻雪 |
|
||||
| update-liteyuki | 更新轻雪 |
|
||||
| reload-resources | 重载资源 |
|
||||
| config | 配置, `set` 设置 / `get` 查询 |
|
||||
| switch-image-mode | 切换图片模式 |
|
||||
| liteyuki-docs | 轻雪文档 |
|
||||
| group | 群聊, `enable` 启用 / `disable` 停用 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **插件/包管理器 `liteyuki_pacman`**
|
||||
|
||||
- 插件管理
|
||||
|
||||
| 命令 | 说明 | 权限 |
|
||||
| :-----------------------------------------------------: | :----------------------------------------: | :----------------------------------------------: |
|
||||
| `npm update` | 更新插件商店索引 | 超级用户 |
|
||||
| `npm install <plugin_name>` | 安装插件 | 超级用户 |
|
||||
| `npm uninstall <plugin_name>` | 卸载插件 | 超级用户 |
|
||||
| `npm search <keywords...>` | 通过关键词搜索插件 | 超级用户 |
|
||||
| `npm enable-global/disable-global <plugin_name>` | 全局启用/停用插件 | 超级用户 |
|
||||
| `npm enable/disable <plugin_name> [--group <group_id>]` | 当前会话启用/停用插件 | 群聊仅群主、管理员、超级用户可用,私聊所有人可用 |
|
||||
| `npm list [page] [num]` | 列出所有插件 page为页数,num为每页显示数量 | 群聊仅群主、管理员、超级用户可用,私聊所有人可用 |
|
||||
| `help <plugin_name>` | 查看插件帮助 | 所有人 |
|
||||
|
||||
|
||||
- 资源包管理
|
||||
|
||||
| 命令 | 说明 | 权限 |
|
||||
| :----------------------: | :------------------------------------------: | :------: |
|
||||
| `rpm list [page] [num]` | 列出所有资源包 page为页数,num为每页显示数量 | 超级用户 |
|
||||
| `rpm load <pack_name>` | 加载资源包 | 超级用户 |
|
||||
| `rpm unload <pack_name>` | 卸载资源包 | 超级用户 |
|
||||
| `rpm change <pack_name>` | 修改优先级 | 超级用户 |
|
||||
| `rpm reload` | 重载所有资源包 | 超级用户 |
|
||||
|
||||
|
||||
### 命令别名
|
||||
|
||||
| 命令 | 别名 |
|
||||
| :--------------: | :------: |
|
||||
| `npm` | 插件管理 |
|
||||
| `update` | 更新 |
|
||||
| `install` | 安装 |
|
||||
| `uninstall` | 卸载 |
|
||||
| `search` | 搜索 |
|
||||
| `enable` | 启用 |
|
||||
| `disable` | 停用 |
|
||||
| `enable-global` | 全局启用 |
|
||||
| `disable-global` | 全局停用 |
|
||||
| `rpm` | 资源包 |
|
||||
| `load` | 加载 |
|
||||
| `unload` | 卸载 |
|
||||
| `change` | 更改 |
|
||||
| `reload` | 重载 |
|
||||
| `list` | 列表 |
|
||||
| `help` | 帮助 |
|
||||
|
||||
> [!warning]
|
||||
> 受限于NoneBot2钩子函数的依赖注入参数,插件停用只能阻断传入响应,对于主动推送的插件不生效,请阅读插件主页的说明。
|
||||
>
|
||||
|
||||
---
|
||||
|
||||
|
||||
## **用户管理`liteyuki_user`**
|
||||
|
||||
| 命令 | 说明 | 权限 |
|
||||
| :-------------------------: | :----------------------------: | :----: |
|
||||
| `profile` | 查看用户信息菜单 | 所有人 |
|
||||
| `profile set <key> [value]` | 设置用户信息或打开属性设置菜单 | 所有人 |
|
||||
| `profile get <key>` | 获取用户信息 | 所有人 |
|
||||
|
||||
|
||||
###命令别名
|
||||
|
||||
| 命令 | 别名 |
|
||||
| :-------: | :------: |
|
||||
| `profile` | 个人信息 |
|
||||
| `set` | 设置 |
|
||||
| `get` | 查询 |
|
||||
|
||||
|
70
docs/en/usage/extra_command.md
Normal file
70
docs/en/usage/extra_command.md
Normal file
@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Extra Commands
|
||||
icon: comment
|
||||
order: 2
|
||||
category: usage
|
||||
---
|
||||
|
||||
## 功能插件命令
|
||||
|
||||
### **轻雪天气`liteyuki_weather`**
|
||||
|
||||
查询实时天气,支持绑定城市,支持中英文城市名,支持多个关键词查询。
|
||||
|
||||
配置项
|
||||
|
||||
```yaml
|
||||
weather_key: "" # 和风天气的天气key,会自动判断key版本
|
||||
```
|
||||
|
||||
命令
|
||||
|
||||
```shell
|
||||
weather <keywords...> # Keywords为城市名,支持中英文
|
||||
```
|
||||
查询目标地实时天气,例如:"天气 北京 海淀", "weather Tokyo Shinjuku"
|
||||
|
||||
```shell
|
||||
bind-city <keywords...> # Keywords为城市名,支持中英文
|
||||
```
|
||||
|
||||
绑定查询城市,个人全局生效
|
||||
|
||||
#### 命令别名
|
||||
|
||||
| 命令 | 别名 |
|
||||
| :-------: | :------- |
|
||||
| weather | 天气 |
|
||||
| bind-city | 绑定城市 |
|
||||
|
||||
---
|
||||
|
||||
### **统计信息`liteyuki_statistics`**
|
||||
|
||||
统计信息
|
||||
命令
|
||||
|
||||
```shell
|
||||
statistic message --duration <duration> --period <period> --group [current|group_id] --bot [current|bot_id]
|
||||
```
|
||||
|
||||
功能: 用于统计Bot接收到的消息, 统计周期为`period`, 统计时间范围为`duration`
|
||||
|
||||
| 参数 | 格式 |
|
||||
| :------: | :------------------------------------------------------------: |
|
||||
| duration | 使用通用日期简写: `1d`(天), `1h`(小时), `45m`(分钟), `14s`(秒) |
|
||||
| period | 使用通用日期简写: `1d`(天), `1h`(小时), `45m`(分钟), `14s`(秒) |
|
||||
| group | `current` (当前群聊) 或 `group_id` (QQ群号) |
|
||||
| bot | `current` (当前Bot) 或 `bot_id` |
|
||||
|
||||
#### 命令别名
|
||||
|
||||
| 命令 | 别名 |
|
||||
| :----------: | :---: |
|
||||
| `statistic` | stat |
|
||||
| `message` | m |
|
||||
| `--duration` | -d |
|
||||
| --period` | -p |
|
||||
| `--group` | -g |
|
||||
| `--bot` | -b |
|
||||
| `current` | c |
|
107
docs/foolsday.md
107
docs/foolsday.md
@ -1,107 +0,0 @@
|
||||
---
|
||||
home: true
|
||||
icon: home
|
||||
title: 首页
|
||||
heroImage: https://cdn.liteyuki.icu/static/img/lykwi.png
|
||||
bgImage:
|
||||
bgImageDark:
|
||||
bgImageStyle:
|
||||
background-attachment: fixed
|
||||
heroText: HeavylavaBot666 # LiteyukiBot 6
|
||||
tagline: 重浆机器人,一个以笨重和复杂为设计理念基于Koishi114514的TwoBotv1919810标准聊天机器人,可用于雪地清扫,使用Typethon编写
|
||||
#tagline: 轻雪机器人,一个以轻量和简洁为设计理念基于Nonebot2的OneBot标准聊天机器人
|
||||
# 泰普森(X
|
||||
|
||||
actions:
|
||||
- text: 快速结束 # 快速开始
|
||||
icon: lightbulb
|
||||
link: ./deployment/install.html
|
||||
type: primary
|
||||
|
||||
- text: 奇怪的册子 # 使用手册
|
||||
icon: book
|
||||
link: ./usage/basic_command.html
|
||||
|
||||
#1. 安装 `Git` 和 `Python3.10+` 环境
|
||||
#2. 克隆项目 `git clone https://github.com/snowykami/LiteyukiBot` (无法连接可以用`https://gitee.com/snowykami/LiteyukiBot`)
|
||||
#3. 切换目录`cd LiteyukiBot`
|
||||
#4. 安装依赖`pip install -r requirements.txt`(如果多个Python环境请指定后安装`pythonx -m pip install -r requirements.txt`)
|
||||
#5. 启动`python main.py`
|
||||
|
||||
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: 基于Koishi.js233
|
||||
icon: robot
|
||||
details: 拥有辣鸡的生态支持
|
||||
link: https://nonebot.dev/
|
||||
|
||||
- title: 盲目插件管理
|
||||
icon: plug
|
||||
details: 基于nbshi使用<code>xmpn和bib</code>,让你无法安装/卸载插件
|
||||
|
||||
- title: 纯命令行
|
||||
icon: mouse-pointer
|
||||
details: 老的掉牙的交互模式,必须手打指令
|
||||
|
||||
- title: 猪蹄支持
|
||||
icon: paint-brush
|
||||
details: 支持多种烤猪蹄样式,丢弃烤箱,拥抱烧烤架,满足你的干饭需求
|
||||
|
||||
- title: 去国际化
|
||||
icon: globe
|
||||
details: 支持多种语言,包括i18n部分语言和自行扩展的语言代码
|
||||
link: https://baike.baidu.com/item/i18n/6771940
|
||||
|
||||
- title: 超难配置
|
||||
icon: cog
|
||||
details: 无需过多配置,开箱即用
|
||||
link: https://bot.liteyuki.icu/deployment/config.html
|
||||
|
||||
- title: 高占用
|
||||
icon: memory
|
||||
details: 使用更多的意义不明的依赖和资源
|
||||
|
||||
- title: 一个Bot标准
|
||||
icon: link
|
||||
details: 支持OneBotv11/12标准的四种通信协议
|
||||
link: https://onebot.dev/
|
||||
|
||||
- title: Alconna
|
||||
icon: link
|
||||
details: 使用Alconna实现低效命令解析
|
||||
link: https://github.com/nonebot/plugin-alconna
|
||||
|
||||
- title: 不准更新
|
||||
icon: cloud-download
|
||||
details: 要更新自己写新版本去
|
||||
|
||||
- title: 服务支持
|
||||
icon: server
|
||||
details: 内置重浆API,但随时可能提桶跑路
|
||||
|
||||
- title: 闭源
|
||||
icon: code
|
||||
details: 要源代码自己逆向去
|
||||
|
||||
- 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: 安装 winget 和 nothing.js # git & node.js+
|
||||
- title: 使用 <code>git clone https://github.com/snowykami/LiteyukiBot</code> 以克隆项目至FTP。 # 本地
|
||||
details: 如果无法连接到PoonHub,可以使用 <code>git clone https://gitee.com/snowykami/LiteyukiBot</code>。
|
||||
- title: 使用 <code>cd LiteyukiBot</code> 切换到项目目录。
|
||||
- title: 使用 <code>npm install -r requirements.txt</code> 安装项目依赖。
|
||||
details: 如果你有多个 nothing.js 环境,请使用 <code>pythonx -m npm install -r requirements.txt</code>。
|
||||
- title: 使用 <code>node main.py</code> 启动项目。
|
||||
copyright: © 2021-2024 SnowyKami All Rights Reserved
|
||||
---
|
@ -1,5 +1,8 @@
|
||||
---
|
||||
title: 资源商店
|
||||
icon: store
|
||||
title: 资源及插件商店
|
||||
index: false
|
||||
icon: store
|
||||
category: 商店
|
||||
---
|
||||
|
||||
<Catalog />
|
@ -1,13 +0,0 @@
|
||||
---
|
||||
title: 轻雪API
|
||||
icon: code
|
||||
order: 5
|
||||
category: 使用指南
|
||||
tag:
|
||||
- 配置
|
||||
- 部署
|
||||
---
|
||||
|
||||
## **轻雪API**
|
||||
|
||||
轻雪API是轻雪运行中部分服务的支持,由`go`语言编写,例如错误反馈,图床链接等,目前服务由轻雪服务器提供,用户无需额外部署
|
@ -1,11 +1,12 @@
|
||||
from liteyuki.bot import (
|
||||
LiteyukiBot,
|
||||
get_bot
|
||||
get_bot,
|
||||
get_config,
|
||||
get_config_with_compat
|
||||
)
|
||||
|
||||
from liteyuki.comm import (
|
||||
Channel,
|
||||
chan,
|
||||
Event
|
||||
)
|
||||
|
||||
@ -15,7 +16,25 @@ from liteyuki.plugin import (
|
||||
)
|
||||
|
||||
from liteyuki.log import (
|
||||
logger,
|
||||
init_log
|
||||
|
||||
init_log,
|
||||
logger
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LiteyukiBot",
|
||||
"get_bot",
|
||||
"get_config",
|
||||
"get_config_with_compat",
|
||||
"Channel",
|
||||
"Event",
|
||||
"load_plugin",
|
||||
"load_plugins",
|
||||
"init_log",
|
||||
"logger",
|
||||
]
|
||||
|
||||
__version__ = "6.3.8" # 测试版本号
|
||||
|
||||
# 6.3.8
|
||||
# 1. 初步添加对聊天的支持
|
||||
# 2. 优化了通道的性能
|
||||
|
@ -1,84 +1,146 @@
|
||||
import asyncio
|
||||
import atexit
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import asyncio
|
||||
from typing import Any, Optional
|
||||
from multiprocessing import freeze_support
|
||||
|
||||
from liteyuki.bot.lifespan import (LIFESPAN_FUNC, Lifespan)
|
||||
from liteyuki.comm.channel import Channel
|
||||
from liteyuki.core import IS_MAIN_PROCESS
|
||||
from liteyuki.comm.channel import get_channel
|
||||
from liteyuki.comm.storage import shared_memory
|
||||
from liteyuki.core.manager import ProcessManager
|
||||
from liteyuki.core.spawn_process import mb_run, nb_run
|
||||
from liteyuki.log import init_log, logger
|
||||
from liteyuki.plugin import load_plugins
|
||||
from liteyuki.utils import run_coroutine
|
||||
from liteyuki.plugin import load_plugin
|
||||
from liteyuki.utils import IS_MAIN_PROCESS
|
||||
|
||||
__all__ = [
|
||||
"LiteyukiBot",
|
||||
"get_bot"
|
||||
"get_bot",
|
||||
"get_config",
|
||||
"get_config_with_compat",
|
||||
]
|
||||
|
||||
"""是否为主进程"""
|
||||
|
||||
|
||||
class LiteyukiBot:
|
||||
def __init__(self, *args, **kwargs):
|
||||
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) # 初始化
|
||||
|
||||
self.lifespan: Lifespan = Lifespan()
|
||||
self.chan = Channel() # 进程通信通道
|
||||
self.pm: ProcessManager = ProcessManager(bot=self, chan=self.chan)
|
||||
"""配置"""
|
||||
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.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True)
|
||||
self.stop_event = threading.Event()
|
||||
self.call_restart_count = 0
|
||||
|
||||
print("\033[34m" + r"""
|
||||
__ ______ ________ ________ __ __ __ __ __ __ ______
|
||||
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
|
||||
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
|
||||
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
|
||||
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
|
||||
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
|
||||
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
|
||||
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
|
||||
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
|
||||
""" + "\033[0m")
|
||||
"""加载插件加载器"""
|
||||
load_plugin("liteyuki.plugins.plugin_loader") # 加载轻雪插件
|
||||
|
||||
"""信号处理"""
|
||||
signal.signal(signal.SIGINT, self._handle_exit)
|
||||
signal.signal(signal.SIGTERM, self._handle_exit)
|
||||
atexit.register(self.process_manager.terminate_all) # 注册退出时的函数
|
||||
|
||||
def run(self):
|
||||
load_plugins("liteyuki/plugins") # 加载轻雪插件
|
||||
"""
|
||||
启动逻辑
|
||||
"""
|
||||
self.lifespan.before_start() # 启动前钩子
|
||||
self.process_manager.start_all()
|
||||
self.lifespan.after_start() # 启动后钩子
|
||||
self.keep_alive()
|
||||
|
||||
self.loop_thread.start() # 启动事件循环
|
||||
asyncio.run(self.lifespan.before_start()) # 启动前钩子
|
||||
def keep_alive(self):
|
||||
"""
|
||||
保持轻雪运行
|
||||
Returns:
|
||||
|
||||
self.pm.add_target("nonebot", nb_run, **self.config)
|
||||
self.pm.start("nonebot")
|
||||
"""
|
||||
try:
|
||||
while not self.stop_event.is_set():
|
||||
time.sleep(0.5)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Liteyuki is stopping...")
|
||||
self.stop()
|
||||
|
||||
self.pm.add_target("melobot", mb_run, **self.config)
|
||||
self.pm.start("melobot")
|
||||
def _handle_exit(self, signum, frame):
|
||||
"""
|
||||
信号处理
|
||||
Args:
|
||||
signum:
|
||||
frame:
|
||||
|
||||
asyncio.run(self.lifespan.after_start()) # 启动后钩子
|
||||
Returns:
|
||||
|
||||
def restart(self, name: Optional[str] = None):
|
||||
"""
|
||||
logger.info("Received signal, stopping all processes.")
|
||||
self.stop()
|
||||
sys.exit(0)
|
||||
|
||||
def restart(self, delay: int = 0):
|
||||
"""
|
||||
重启轻雪本体
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if self.call_restart_count < 1:
|
||||
executable = sys.executable
|
||||
args = sys.argv
|
||||
logger.info("Restarting LiteyukiBot...")
|
||||
time.sleep(delay)
|
||||
if platform.system() == "Windows":
|
||||
cmd = "start"
|
||||
elif platform.system() == "Linux":
|
||||
cmd = "nohup"
|
||||
elif platform.system() == "Darwin":
|
||||
cmd = "open"
|
||||
else:
|
||||
cmd = "nohup"
|
||||
self.process_manager.terminate_all()
|
||||
# 进程退出后重启
|
||||
threading.Thread(target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",)).start()
|
||||
sys.exit(0)
|
||||
self.call_restart_count += 1
|
||||
|
||||
def restart_process(self, name: Optional[str] = None):
|
||||
"""
|
||||
停止轻雪
|
||||
Args:
|
||||
name: 进程名称, 默认为None, 所有进程
|
||||
Returns:
|
||||
|
||||
"""
|
||||
logger.info("Stopping LiteyukiBot...")
|
||||
self.lifespan.before_process_shutdown() # 重启前钩子
|
||||
self.lifespan.before_process_shutdown() # 停止前钩子
|
||||
|
||||
self.loop.create_task(self.lifespan.before_shutdown()) # 重启前钩子
|
||||
self.loop.create_task(self.lifespan.before_shutdown()) # 停止前钩子
|
||||
|
||||
if name:
|
||||
self.chan.send(1, name)
|
||||
if name is not None:
|
||||
chan_active = get_channel(f"{name}-active")
|
||||
chan_active.send(1)
|
||||
else:
|
||||
for name in self.pm.targets:
|
||||
self.chan.send(1, name)
|
||||
for process_name in self.process_manager.processes:
|
||||
chan_active = get_channel(f"{process_name}-active")
|
||||
chan_active.send(1)
|
||||
|
||||
def init(self, *args, **kwargs):
|
||||
"""
|
||||
@ -86,15 +148,20 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.init_config()
|
||||
self.init_logger()
|
||||
|
||||
def init_logger(self):
|
||||
# 修改nonebot的日志配置
|
||||
init_log(config=self.config)
|
||||
|
||||
def init_config(self):
|
||||
pass
|
||||
def stop(self):
|
||||
"""
|
||||
停止轻雪
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.stop_event.set()
|
||||
self.loop.stop()
|
||||
|
||||
def on_before_start(self, func: LIFESPAN_FUNC):
|
||||
"""
|
||||
@ -118,17 +185,6 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
|
||||
"""
|
||||
return self.lifespan.on_after_start(func)
|
||||
|
||||
def on_before_shutdown(self, func: LIFESPAN_FUNC):
|
||||
"""
|
||||
注册停止前的函数,为子进程停止时调用
|
||||
Args:
|
||||
func:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return self.lifespan.on_before_shutdown(func)
|
||||
|
||||
def on_after_shutdown(self, func: LIFESPAN_FUNC):
|
||||
"""
|
||||
注册停止后的函数:未实现
|
||||
@ -140,9 +196,20 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
|
||||
"""
|
||||
return self.lifespan.on_after_shutdown(func)
|
||||
|
||||
def on_before_restart(self, func: LIFESPAN_FUNC):
|
||||
def on_before_process_shutdown(self, func: LIFESPAN_FUNC):
|
||||
"""
|
||||
注册重启前的函数,为子进程重启时调用
|
||||
注册进程停止前的函数,为子进程停止时调用
|
||||
Args:
|
||||
func:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return self.lifespan.on_before_process_shutdown(func)
|
||||
|
||||
def on_before_process_restart(self, func: LIFESPAN_FUNC):
|
||||
"""
|
||||
注册进程重启前的函数,为子进程重启时调用
|
||||
Args:
|
||||
func:
|
||||
|
||||
@ -150,7 +217,7 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
|
||||
|
||||
"""
|
||||
|
||||
return self.lifespan.on_before_restart(func)
|
||||
return self.lifespan.on_before_process_restart(func)
|
||||
|
||||
def on_after_restart(self, func: LIFESPAN_FUNC):
|
||||
"""
|
||||
@ -175,17 +242,67 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
|
||||
return self.lifespan.on_after_nonebot_init(func)
|
||||
|
||||
|
||||
_BOT_INSTANCE: Optional[LiteyukiBot] = None
|
||||
_BOT_INSTANCE: LiteyukiBot
|
||||
|
||||
|
||||
def get_bot() -> Optional[LiteyukiBot]:
|
||||
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:
|
||||
# 从多进程上下文中获取
|
||||
pass
|
||||
raise RuntimeError("Can't get bot instance in sub process.")
|
||||
|
||||
|
||||
def get_config(key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
获取配置
|
||||
Args:
|
||||
key: 配置键
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
Any: 配置值
|
||||
"""
|
||||
return get_bot().config.get(key, default)
|
||||
|
||||
|
||||
def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any = None) -> Any:
|
||||
"""
|
||||
获取配置,兼容旧版本
|
||||
Args:
|
||||
key: 配置键
|
||||
compat_keys: 兼容键
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
Any: 配置值
|
||||
"""
|
||||
if key in get_bot().config:
|
||||
return get_bot().config[key]
|
||||
for compat_key in compat_keys:
|
||||
if compat_key in get_bot().config:
|
||||
logger.warning(f"Config key \"{compat_key}\" will be deprecated, use \"{key}\" instead.")
|
||||
return get_bot().config[compat_key]
|
||||
return default
|
||||
|
||||
|
||||
def print_logo():
|
||||
print("\033[34m" + r"""
|
||||
__ ______ ________ ________ __ __ __ __ __ __ ______
|
||||
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
|
||||
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
|
||||
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
|
||||
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
|
||||
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
|
||||
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
|
||||
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
|
||||
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
|
||||
""" + "\033[0m")
|
||||
|
@ -8,48 +8,60 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
@File : lifespan.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Any, Awaitable, Callable, TypeAlias
|
||||
|
||||
from liteyuki.log import logger
|
||||
from liteyuki.utils import is_coroutine_callable
|
||||
from liteyuki.utils import is_coroutine_callable, async_wrapper
|
||||
|
||||
SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
|
||||
ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
|
||||
LIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC
|
||||
|
||||
SYNC_PROCESS_LIFESPAN_FUNC: TypeAlias = Callable[[str], Any]
|
||||
ASYNC_PROCESS_LIFESPAN_FUNC: TypeAlias = Callable[[str], Awaitable[Any]]
|
||||
PROCESS_LIFESPAN_FUNC: TypeAlias = SYNC_PROCESS_LIFESPAN_FUNC | ASYNC_PROCESS_LIFESPAN_FUNC
|
||||
|
||||
|
||||
class Lifespan:
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
轻雪生命周期管理,启动、停止、重启
|
||||
"""
|
||||
|
||||
self.life_flag: int = 0 # 0: 启动前,1: 启动后,2: 停止前,3: 停止后
|
||||
self.life_flag: int = 0
|
||||
|
||||
self._before_start_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._after_start_funcs: list[LIFESPAN_FUNC] = []
|
||||
|
||||
self._before_shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._before_process_shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||
|
||||
self._before_restart_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._before_process_restart_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
|
||||
|
||||
self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
|
||||
|
||||
@staticmethod
|
||||
async def _run_funcs(funcs: list[LIFESPAN_FUNC]) -> None:
|
||||
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):
|
||||
await func()
|
||||
tasks.append(func(*args, **kwargs))
|
||||
else:
|
||||
func()
|
||||
tasks.append(async_wrapper(func)(*args, **kwargs))
|
||||
loop.run_until_complete(asyncio.gather(*tasks))
|
||||
|
||||
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
@ -73,7 +85,7 @@ class Lifespan:
|
||||
self._after_start_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_before_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
def on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
注册停止前的函数
|
||||
Args:
|
||||
@ -81,7 +93,7 @@ class Lifespan:
|
||||
Returns:
|
||||
LIFESPAN_FUNC:
|
||||
"""
|
||||
self._before_shutdown_funcs.append(func)
|
||||
self._before_process_shutdown_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
@ -97,7 +109,7 @@ class Lifespan:
|
||||
self._after_shutdown_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_before_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
def on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""
|
||||
注册重启时的函数
|
||||
Args:
|
||||
@ -105,7 +117,7 @@ class Lifespan:
|
||||
Returns:
|
||||
LIFESPAN_FUNC:
|
||||
"""
|
||||
self._before_restart_funcs.append(func)
|
||||
self._before_process_restart_funcs.append(func)
|
||||
return func
|
||||
|
||||
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
@ -131,59 +143,51 @@ class Lifespan:
|
||||
self._after_nonebot_init_funcs.append(func)
|
||||
return func
|
||||
|
||||
async def before_start(self) -> None:
|
||||
def before_start(self) -> None:
|
||||
"""
|
||||
启动前
|
||||
Returns:
|
||||
"""
|
||||
logger.debug("Running before_start functions")
|
||||
await self._run_funcs(self._before_start_funcs)
|
||||
self.run_funcs(self._before_start_funcs)
|
||||
|
||||
async def after_start(self) -> None:
|
||||
def after_start(self) -> None:
|
||||
"""
|
||||
启动后
|
||||
Returns:
|
||||
"""
|
||||
logger.debug("Running after_start functions")
|
||||
await self._run_funcs(self._after_start_funcs)
|
||||
self.run_funcs(self._after_start_funcs)
|
||||
|
||||
async def before_shutdown(self) -> None:
|
||||
def before_process_shutdown(self) -> None:
|
||||
"""
|
||||
停止前
|
||||
Returns:
|
||||
"""
|
||||
logger.debug("Running before_shutdown functions")
|
||||
await self._run_funcs(self._before_shutdown_funcs)
|
||||
self.run_funcs(self._before_process_shutdown_funcs)
|
||||
|
||||
async def after_shutdown(self) -> None:
|
||||
def after_shutdown(self) -> None:
|
||||
"""
|
||||
停止后
|
||||
Returns:
|
||||
"""
|
||||
logger.debug("Running after_shutdown functions")
|
||||
await self._run_funcs(self._after_shutdown_funcs)
|
||||
self.run_funcs(self._after_shutdown_funcs)
|
||||
|
||||
async def before_restart(self) -> None:
|
||||
def before_process_restart(self) -> None:
|
||||
"""
|
||||
重启前
|
||||
Returns:
|
||||
"""
|
||||
logger.debug("Running before_restart functions")
|
||||
await self._run_funcs(self._before_restart_funcs)
|
||||
self.run_funcs(self._before_process_restart_funcs)
|
||||
|
||||
async def after_restart(self) -> None:
|
||||
def after_restart(self) -> None:
|
||||
"""
|
||||
重启后
|
||||
Returns:
|
||||
|
||||
"""
|
||||
logger.debug("Running after_restart functions")
|
||||
await self._run_funcs(self._after_restart_funcs)
|
||||
|
||||
async def after_nonebot_init(self) -> None:
|
||||
"""
|
||||
NoneBot 初始化后
|
||||
Returns:
|
||||
"""
|
||||
logger.debug("Running after_nonebot_init functions")
|
||||
await self._run_funcs(self._after_nonebot_init_funcs)
|
||||
self.run_funcs(self._after_restart_funcs)
|
||||
|
@ -1,19 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/7/26 下午10:36
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : __init__.py
|
||||
@Software: PyCharm
|
||||
该模块用于轻雪主进程和Nonebot子进程之间的通信
|
||||
依赖关系
|
||||
event -> _
|
||||
storage -> channel_
|
||||
rpc -> channel_, storage
|
||||
"""
|
||||
from liteyuki.comm.channel import Channel, chan
|
||||
from liteyuki.comm.channel import (
|
||||
Channel,
|
||||
get_channel,
|
||||
set_channel,
|
||||
set_channels,
|
||||
get_channels,
|
||||
active_channel,
|
||||
passive_channel
|
||||
)
|
||||
from liteyuki.comm.event import Event
|
||||
|
||||
__all__ = [
|
||||
"Channel",
|
||||
"chan",
|
||||
"Event",
|
||||
"get_channel",
|
||||
"set_channel",
|
||||
"set_channels",
|
||||
"get_channels",
|
||||
"active_channel",
|
||||
"passive_channel"
|
||||
]
|
||||
|
||||
from liteyuki.utils import IS_MAIN_PROCESS
|
||||
|
||||
# 第一次引用必定为赋值
|
||||
_ref_count = 0
|
||||
if not IS_MAIN_PROCESS:
|
||||
if (active_channel is None or passive_channel is None) and _ref_count > 0:
|
||||
raise RuntimeError("Error: Channel not initialized in sub process")
|
||||
_ref_count += 1
|
||||
|
@ -5,128 +5,217 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
@Time : 2024/7/26 下午11:21
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : channel.py
|
||||
@File : channel_.py
|
||||
@Software: PyCharm
|
||||
|
||||
本模块定义了一个通用的通道类,用于进程间通信
|
||||
"""
|
||||
import threading
|
||||
from multiprocessing import Pipe
|
||||
from typing import Any, Optional, Callable, Awaitable, List, TypeAlias
|
||||
from typing import Any, Callable, Coroutine, Generic, Optional, TypeAlias, TypeVar, get_args
|
||||
|
||||
from liteyuki.utils import is_coroutine_callable, run_coroutine
|
||||
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable, run_coroutine
|
||||
from liteyuki.log import logger
|
||||
|
||||
SYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[Any], Any]
|
||||
ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[Any], Awaitable[Any]]
|
||||
T = TypeVar("T")
|
||||
|
||||
SYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Any]
|
||||
ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Coroutine[Any, Any, Any]]
|
||||
ON_RECEIVE_FUNC: TypeAlias = SYNC_ON_RECEIVE_FUNC | ASYNC_ON_RECEIVE_FUNC
|
||||
|
||||
SYNC_FILTER_FUNC: TypeAlias = Callable[[Any], bool]
|
||||
ASYNC_FILTER_FUNC: TypeAlias = Callable[[Any], Awaitable[bool]]
|
||||
SYNC_FILTER_FUNC: TypeAlias = Callable[[T], bool]
|
||||
ASYNC_FILTER_FUNC: TypeAlias = Callable[[T], Coroutine[Any, Any, bool]]
|
||||
FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC
|
||||
|
||||
_func_id: int = 0
|
||||
_channel: dict[str, "Channel"] = {}
|
||||
_callback_funcs: dict[int, ON_RECEIVE_FUNC] = {}
|
||||
|
||||
class Channel:
|
||||
|
||||
class Channel(Generic[T]):
|
||||
"""
|
||||
通道类,用于进程间通信
|
||||
通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
|
||||
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.receive_conn, self.send_conn = Pipe()
|
||||
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_receive_funcs: List[ON_RECEIVE_FUNC] = []
|
||||
self._on_receive_funcs_with_receiver: dict[str, List[ON_RECEIVE_FUNC]] = {}
|
||||
self._on_main_receive_funcs: list[int] = []
|
||||
self._on_sub_receive_funcs: list[int] = []
|
||||
self.name: str = _id
|
||||
|
||||
def send(self, data: Any, receiver: Optional[str] = None):
|
||||
self.is_main_receive_loop_running = False
|
||||
self.is_sub_receive_loop_running = False
|
||||
|
||||
if type_check is None:
|
||||
# 若传入泛型则默认开启类型检查
|
||||
type_check = self._get_generic_type() is not None
|
||||
|
||||
elif type_check:
|
||||
if self._get_generic_type() is None:
|
||||
raise TypeError("Type hint is required for enforcing type check.")
|
||||
self.type_check = type_check
|
||||
|
||||
def _get_generic_type(self) -> Optional[type]:
|
||||
"""
|
||||
获取通道传递泛型类型
|
||||
|
||||
Returns:
|
||||
Optional[type]: 泛型类型
|
||||
"""
|
||||
if hasattr(self, '__orig_class__'):
|
||||
return get_args(self.__orig_class__)[0]
|
||||
return None
|
||||
|
||||
def _validate_structure(self, data: Any, structure: type) -> bool:
|
||||
"""
|
||||
验证数据结构
|
||||
Args:
|
||||
data: 数据
|
||||
structure: 结构
|
||||
|
||||
Returns:
|
||||
bool: 是否通过验证
|
||||
"""
|
||||
if isinstance(structure, type):
|
||||
return isinstance(data, structure)
|
||||
elif isinstance(structure, tuple):
|
||||
if not isinstance(data, tuple) or len(data) != len(structure):
|
||||
return False
|
||||
return all(self._validate_structure(d, s) for d, s in zip(data, structure))
|
||||
elif isinstance(structure, list):
|
||||
if not isinstance(data, list):
|
||||
return False
|
||||
return all(self._validate_structure(d, structure[0]) for d in data)
|
||||
elif isinstance(structure, dict):
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
return all(k in data and self._validate_structure(data[k], structure[k]) for k in structure)
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return f"Channel({self.name})"
|
||||
|
||||
def send(self, data: T):
|
||||
"""
|
||||
发送数据
|
||||
Args:
|
||||
data: 数据
|
||||
receiver: 接收者,如果为None则广播
|
||||
"""
|
||||
if self._closed:
|
||||
raise RuntimeError("Cannot send to a closed channel")
|
||||
self.send_conn.send((data, receiver))
|
||||
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")
|
||||
|
||||
def receive(self, receiver: str = None) -> Any:
|
||||
if self._closed:
|
||||
raise RuntimeError("Cannot send to a closed channel_")
|
||||
self.conn_send.send(data)
|
||||
|
||||
def receive(self) -> T:
|
||||
"""
|
||||
接收数据
|
||||
Args:
|
||||
receiver: 接收者,如果为None则接收任意数据
|
||||
"""
|
||||
if self._closed:
|
||||
raise RuntimeError("Cannot receive from a closed channel")
|
||||
while True:
|
||||
# 判断receiver是否为None或者receiver是否等于接收者,是则接收数据,否则不动数据
|
||||
data, receiver_ = self.receive_conn.recv()
|
||||
if receiver is None or receiver == receiver_:
|
||||
self._run_on_receive_funcs(data, receiver_)
|
||||
return data
|
||||
self.send_conn.send((data, receiver_))
|
||||
raise RuntimeError("Cannot receive from a closed channel_")
|
||||
|
||||
def peek(self) -> Optional[Any]:
|
||||
"""
|
||||
查看管道中的数据,不移除
|
||||
Returns:
|
||||
"""
|
||||
if self._closed:
|
||||
raise RuntimeError("Cannot peek from a closed channel")
|
||||
if self.receive_conn.poll():
|
||||
data, receiver = self.receive_conn.recv()
|
||||
self.receive_conn.send((data, receiver))
|
||||
while True:
|
||||
data = self.conn_recv.recv()
|
||||
return data
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
关闭通道
|
||||
"""
|
||||
self._closed = True
|
||||
self.receive_conn.close()
|
||||
self.send_conn.close()
|
||||
self.conn_send.close()
|
||||
self.conn_recv.close()
|
||||
|
||||
def on_receive(self, filter_func: Optional[FILTER_FUNC] = None, receiver: Optional[str] = None) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
|
||||
def on_receive(self, filter_func: Optional[FILTER_FUNC] = None) -> Callable[[Callable[[T], Any]], Callable[[T], Any]]:
|
||||
"""
|
||||
接收数据并执行函数
|
||||
Args:
|
||||
filter_func: 过滤函数,为None则不过滤
|
||||
receiver: 接收者, 为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()
|
||||
|
||||
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
|
||||
async def wrapper(data: Any) -> Any:
|
||||
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):
|
||||
if not (await filter_func(data)): # type: ignore
|
||||
return
|
||||
else:
|
||||
if not filter_func(data):
|
||||
return
|
||||
return await func(data)
|
||||
|
||||
if receiver is None:
|
||||
self._on_receive_funcs.append(wrapper)
|
||||
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:
|
||||
if receiver not in self._on_receive_funcs_with_receiver:
|
||||
self._on_receive_funcs_with_receiver[receiver] = []
|
||||
self._on_receive_funcs_with_receiver[receiver].append(wrapper)
|
||||
self._on_sub_receive_funcs.append(_func_id)
|
||||
_func_id += 1
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def _run_on_receive_funcs(self, data: Any, receiver: Optional[str] = None):
|
||||
def _run_on_main_receive_funcs(self, data: Any):
|
||||
"""
|
||||
运行接收函数
|
||||
Args:
|
||||
data: 数据
|
||||
"""
|
||||
if receiver is None:
|
||||
for func in self._on_receive_funcs:
|
||||
run_coroutine(func(data))
|
||||
else:
|
||||
for func in self._on_receive_funcs_with_receiver.get(receiver, []):
|
||||
run_coroutine(func(data))
|
||||
for func_id in self._on_main_receive_funcs:
|
||||
func = _callback_funcs[func_id]
|
||||
run_coroutine(func(data))
|
||||
|
||||
def _run_on_sub_receive_funcs(self, data: Any):
|
||||
"""
|
||||
运行接收函数
|
||||
Args:
|
||||
data: 数据
|
||||
"""
|
||||
for func_id in self._on_sub_receive_funcs:
|
||||
func = _callback_funcs[func_id]
|
||||
run_coroutine(func(data))
|
||||
|
||||
def _start_main_receive_loop(self):
|
||||
"""
|
||||
开始接收数据
|
||||
"""
|
||||
self.is_main_receive_loop_running = True
|
||||
while not self._closed:
|
||||
data = self.conn_recv.recv()
|
||||
self._run_on_main_receive_funcs(data)
|
||||
|
||||
def _start_sub_receive_loop(self):
|
||||
"""
|
||||
开始接收数据
|
||||
"""
|
||||
self.is_sub_receive_loop_running = True
|
||||
while not self._closed:
|
||||
data = self.conn_recv.recv()
|
||||
self._run_on_sub_receive_funcs(data)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
@ -134,6 +223,116 @@ class Channel:
|
||||
def __next__(self) -> Any:
|
||||
return self.receive()
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
logger.debug(f"Channel {self.name} deleted.")
|
||||
|
||||
"""默认通道实例,可直接从模块导入使用"""
|
||||
chan = Channel()
|
||||
|
||||
"""子进程可用的主动和被动通道"""
|
||||
active_channel: Optional["Channel"] = None
|
||||
passive_channel: Optional["Channel"] = None
|
||||
publish_channel: Channel[tuple[str, dict[str, Any]]] = Channel(_id="publish_channel")
|
||||
|
||||
"""通道传递通道,主进程创建单例,子进程初始化时实例化"""
|
||||
channel_deliver_active_channel: Channel[Channel[Any]]
|
||||
channel_deliver_passive_channel: Channel[tuple[str, dict[str, Any]]]
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
channel_deliver_active_channel = Channel(_id="channel_deliver_active_channel")
|
||||
channel_deliver_passive_channel = Channel(_id="channel_deliver_passive_channel")
|
||||
|
||||
|
||||
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "set_channel")
|
||||
def on_set_channel(data: tuple[str, dict[str, Any]]):
|
||||
name, channel = data[1]["name"], data[1]["channel_"]
|
||||
set_channel(name, channel)
|
||||
|
||||
|
||||
@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))
|
||||
|
||||
|
||||
@channel_deliver_passive_channel.on_receive(filter_func=lambda data: data[0] == "get_channels")
|
||||
def on_get_channels(data: tuple[str, dict[str, Any]]):
|
||||
recv_chan = data[1]["recv_chan"]
|
||||
recv_chan.send(get_channels())
|
||||
|
||||
|
||||
def set_channel(name: str, channel: Channel):
|
||||
"""
|
||||
设置通道实例
|
||||
Args:
|
||||
name: 通道名称
|
||||
channel: 通道实例
|
||||
"""
|
||||
if not isinstance(channel, Channel):
|
||||
raise TypeError(f"channel_ must be an instance of Channel, {type(channel)} found")
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
_channel[name] = channel
|
||||
else:
|
||||
# 请求主进程设置通道
|
||||
channel_deliver_passive_channel.send(
|
||||
(
|
||||
"set_channel", {
|
||||
"name" : name,
|
||||
"channel_": channel,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def set_channels(channels: dict[str, Channel]):
|
||||
"""
|
||||
设置通道实例
|
||||
Args:
|
||||
channels: 通道名称
|
||||
"""
|
||||
for name, channel in channels.items():
|
||||
set_channel(name, channel)
|
||||
|
||||
|
||||
def get_channel(name: str) -> Channel:
|
||||
"""
|
||||
获取通道实例
|
||||
Args:
|
||||
name: 通道名称
|
||||
Returns:
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
return _channel[name]
|
||||
|
||||
else:
|
||||
recv_chan = Channel[Channel[Any]]("recv_chan")
|
||||
channel_deliver_passive_channel.send(
|
||||
(
|
||||
"get_channel",
|
||||
{
|
||||
"name" : name,
|
||||
"recv_chan": recv_chan
|
||||
}
|
||||
)
|
||||
)
|
||||
return recv_chan.receive()
|
||||
|
||||
|
||||
def get_channels() -> dict[str, Channel]:
|
||||
"""
|
||||
获取通道实例
|
||||
Returns:
|
||||
"""
|
||||
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()
|
||||
|
304
liteyuki/comm/storage.py
Normal file
304
liteyuki/comm/storage.py
Normal file
@ -0,0 +1,304 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
共享内存模块。类似于redis,但是更加轻量级并且线程安全
|
||||
"""
|
||||
|
||||
import threading
|
||||
from typing import Any, Coroutine, Optional, TypeAlias, Callable
|
||||
|
||||
from liteyuki.comm import channel
|
||||
from liteyuki.comm.channel import Channel, ON_RECEIVE_FUNC, ASYNC_ON_RECEIVE_FUNC
|
||||
from liteyuki.utils import IS_MAIN_PROCESS, is_coroutine_callable, run_coroutine
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
_locks = {}
|
||||
|
||||
_on_main_subscriber_receive_funcs: dict[str, list[ASYNC_ON_RECEIVE_FUNC]] = {} # type: ignore
|
||||
"""主进程订阅者接收函数"""
|
||||
_on_sub_subscriber_receive_funcs: dict[str, list[ASYNC_ON_RECEIVE_FUNC]] = {} # type: ignore
|
||||
"""子进程订阅者接收函数"""
|
||||
|
||||
|
||||
def _get_lock(key) -> threading.Lock:
|
||||
"""
|
||||
获取锁
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
if key not in _locks:
|
||||
_locks[key] = threading.Lock()
|
||||
return _locks[key]
|
||||
else:
|
||||
raise RuntimeError("Cannot get lock in sub process.")
|
||||
|
||||
|
||||
class Subscriber:
|
||||
def __init__(self):
|
||||
self._subscribers = {}
|
||||
|
||||
def receive(self) -> Any:
|
||||
pass
|
||||
|
||||
def unsubscribe(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class KeyValueStore:
|
||||
def __init__(self):
|
||||
self._store = {}
|
||||
self.active_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id="shared_memory-active")
|
||||
self.passive_chan = Channel[tuple[str, Optional[dict[str, Any]]]](_id="shared_memory-passive")
|
||||
|
||||
self.publish_channel = Channel[tuple[str, Any]](_id="shared_memory-publish")
|
||||
|
||||
self.is_main_receive_loop_running = False
|
||||
self.is_sub_receive_loop_running = False
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
设置键值对
|
||||
Args:
|
||||
key: 键
|
||||
value: 值
|
||||
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
lock = _get_lock(key)
|
||||
with lock:
|
||||
self._store[key] = value
|
||||
else:
|
||||
# 向主进程发送请求拿取
|
||||
self.passive_chan.send(
|
||||
(
|
||||
"set",
|
||||
{
|
||||
"key" : key,
|
||||
"value": value
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
|
||||
"""
|
||||
获取键值对
|
||||
Args:
|
||||
key: 键
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
Any: 值
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
lock = _get_lock(key)
|
||||
with lock:
|
||||
return self._store.get(key, default)
|
||||
else:
|
||||
recv_chan = Channel[Optional[Any]]("recv_chan")
|
||||
self.passive_chan.send(
|
||||
(
|
||||
"get",
|
||||
{
|
||||
"key" : key,
|
||||
"default" : default,
|
||||
"recv_chan": recv_chan
|
||||
}
|
||||
|
||||
)
|
||||
)
|
||||
return recv_chan.receive()
|
||||
|
||||
def delete(self, key: str, ignore_key_error: bool = True) -> None:
|
||||
"""
|
||||
删除键值对
|
||||
Args:
|
||||
key: 键
|
||||
ignore_key_error: 是否忽略键不存在的错误
|
||||
|
||||
Returns:
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
lock = _get_lock(key)
|
||||
with lock:
|
||||
if key in self._store:
|
||||
try:
|
||||
del self._store[key]
|
||||
del _locks[key]
|
||||
except KeyError as e:
|
||||
if not ignore_key_error:
|
||||
raise e
|
||||
else:
|
||||
# 向主进程发送请求删除
|
||||
self.passive_chan.send(
|
||||
(
|
||||
"delete",
|
||||
{
|
||||
"key": key
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def get_all(self) -> dict[str, Any]:
|
||||
"""
|
||||
获取所有键值对
|
||||
Returns:
|
||||
dict[str, Any]: 键值对
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
return self._store
|
||||
else:
|
||||
recv_chan = Channel[dict[str, Any]]("recv_chan")
|
||||
self.passive_chan.send(
|
||||
(
|
||||
"get_all",
|
||||
{
|
||||
"recv_chan": recv_chan
|
||||
}
|
||||
)
|
||||
)
|
||||
return recv_chan.receive()
|
||||
|
||||
def publish(self, channel_: str, data: Any) -> None:
|
||||
"""
|
||||
发布消息
|
||||
Args:
|
||||
channel_: 频道
|
||||
data: 数据
|
||||
|
||||
Returns:
|
||||
"""
|
||||
self.active_chan.send(
|
||||
(
|
||||
"publish",
|
||||
{
|
||||
"channel": channel_,
|
||||
"data" : data
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def on_subscriber_receive(self, channel_: str) -> Callable[[ON_RECEIVE_FUNC], ON_RECEIVE_FUNC]:
|
||||
"""
|
||||
订阅者接收消息时的回调
|
||||
Args:
|
||||
channel_: 频道
|
||||
|
||||
Returns:
|
||||
装饰器
|
||||
"""
|
||||
if IS_MAIN_PROCESS and not self.is_main_receive_loop_running:
|
||||
threading.Thread(target=self._start_receive_loop, daemon=True).start()
|
||||
shared_memory.is_main_receive_loop_running = True
|
||||
elif not IS_MAIN_PROCESS and not self.is_sub_receive_loop_running:
|
||||
threading.Thread(target=self._start_receive_loop, daemon=True).start()
|
||||
shared_memory.is_sub_receive_loop_running = True
|
||||
|
||||
def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
|
||||
async def wrapper(data: Any):
|
||||
if is_coroutine_callable(func):
|
||||
await func(data)
|
||||
else:
|
||||
func(data)
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
if channel_ not in _on_main_subscriber_receive_funcs:
|
||||
_on_main_subscriber_receive_funcs[channel_] = []
|
||||
_on_main_subscriber_receive_funcs[channel_].append(wrapper)
|
||||
else:
|
||||
if channel_ not in _on_sub_subscriber_receive_funcs:
|
||||
_on_sub_subscriber_receive_funcs[channel_] = []
|
||||
_on_sub_subscriber_receive_funcs[channel_].append(wrapper)
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@staticmethod
|
||||
def run_subscriber_receive_funcs(channel_: str, data: Any):
|
||||
"""
|
||||
运行订阅者接收函数
|
||||
Args:
|
||||
channel_: 频道
|
||||
data: 数据
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
if channel_ in _on_main_subscriber_receive_funcs and _on_main_subscriber_receive_funcs[channel_]:
|
||||
run_coroutine(*[func(data) for func in _on_main_subscriber_receive_funcs[channel_]])
|
||||
else:
|
||||
if channel_ in _on_sub_subscriber_receive_funcs and _on_sub_subscriber_receive_funcs[channel_]:
|
||||
run_coroutine(*[func(data) for func in _on_sub_subscriber_receive_funcs[channel_]])
|
||||
|
||||
def _start_receive_loop(self):
|
||||
"""
|
||||
启动发布订阅接收器循环,在主进程中运行,若有子进程订阅则推送给子进程
|
||||
"""
|
||||
if IS_MAIN_PROCESS:
|
||||
while True:
|
||||
data = self.active_chan.receive()
|
||||
if data[0] == "publish":
|
||||
# 运行主进程订阅函数
|
||||
self.run_subscriber_receive_funcs(data[1]["channel"], data[1]["data"])
|
||||
# 推送给子进程
|
||||
self.publish_channel.send(data)
|
||||
else:
|
||||
while True:
|
||||
data = self.publish_channel.receive()
|
||||
if data[0] == "publish":
|
||||
# 运行子进程订阅函数
|
||||
self.run_subscriber_receive_funcs(data[1]["channel"], data[1]["data"])
|
||||
|
||||
|
||||
class GlobalKeyValueStore:
|
||||
_instance = None
|
||||
_lock = threading.Lock()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
with cls._lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = KeyValueStore()
|
||||
return cls._instance
|
||||
|
||||
|
||||
shared_memory: KeyValueStore = GlobalKeyValueStore.get_instance()
|
||||
|
||||
# 全局单例访问点
|
||||
if IS_MAIN_PROCESS:
|
||||
|
||||
@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))
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@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())
|
||||
|
||||
|
||||
else:
|
||||
# 子进程在入口函数中对shared_memory进行初始化
|
||||
@channel.publish_channel.on_receive()
|
||||
def on_publish(data: tuple[str, Any]):
|
||||
channel_, data = data
|
||||
shared_memory.run_subscriber_receive_funcs(channel_, data)
|
||||
|
||||
_ref_count = 0 # import 引用计数, 防止获取空指针
|
||||
if not IS_MAIN_PROCESS:
|
||||
if (shared_memory is None) and _ref_count > 1:
|
||||
raise RuntimeError("Shared memory not initialized.")
|
||||
_ref_count += 1
|
@ -0,0 +1,143 @@
|
||||
"""
|
||||
该模块用于常用配置文件的加载
|
||||
多配置文件编写原则:
|
||||
1.尽量不要冲突: 一个键不要多次出现
|
||||
2.分工明确: 每个配置文件给一个或一类服务提供配置
|
||||
3.扁平化编写: 配置文件尽量扁平化,不要出现过多的嵌套
|
||||
4.注意冲突时的优先级: 项目目录下的配置文件优先级高于config目录下的配置文件
|
||||
5.请不要将需要动态加载的内容写入配置文件,你应该使用其他储存方式
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
from typing import Any, List
|
||||
|
||||
import toml
|
||||
import yaml
|
||||
from pydantic import BaseModel
|
||||
|
||||
from liteyuki.log import logger
|
||||
|
||||
_SUPPORTED_CONFIG_FORMATS = (".yaml", ".yml", ".json", ".toml")
|
||||
|
||||
|
||||
class SatoriNodeConfig(BaseModel):
|
||||
host: str = ""
|
||||
port: str = "5500"
|
||||
path: str = ""
|
||||
token: str = ""
|
||||
|
||||
|
||||
class SatoriConfig(BaseModel):
|
||||
comment: str = "These features are still in development. Do not enable in production environment."
|
||||
enable: bool = False
|
||||
hosts: List[SatoriNodeConfig] = [SatoriNodeConfig()]
|
||||
|
||||
|
||||
class BasicConfig(BaseModel):
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 20216
|
||||
superusers: list[str] = []
|
||||
command_start: list[str] = ["/", ""]
|
||||
nickname: list[str] = [f"LiteyukiBot"]
|
||||
satori: SatoriConfig = SatoriConfig()
|
||||
data_path: str = "data/liteyuki"
|
||||
|
||||
|
||||
def flat_config(config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
扁平化配置文件
|
||||
|
||||
{a:{b:{c:1}}} -> {"a.b.c": 1}
|
||||
Args:
|
||||
config: 配置项目
|
||||
|
||||
Returns:
|
||||
扁平化后的配置文件,但也包含原有的键值对
|
||||
"""
|
||||
new_config = copy.deepcopy(config)
|
||||
for key, value in config.items():
|
||||
if isinstance(value, dict):
|
||||
for k, v in flat_config(value).items():
|
||||
new_config[f"{key}.{k}"] = v
|
||||
return new_config
|
||||
|
||||
|
||||
def load_from_yaml(file: str) -> dict[str, Any]:
|
||||
"""
|
||||
Load config from yaml file
|
||||
|
||||
"""
|
||||
logger.debug(f"Loading YAML config from {file}")
|
||||
config = yaml.safe_load(open(file, "r", encoding="utf-8"))
|
||||
return flat_config(config if config is not None else {})
|
||||
|
||||
|
||||
def load_from_json(file: str) -> dict[str, Any]:
|
||||
"""
|
||||
Load config from json file
|
||||
"""
|
||||
logger.debug(f"Loading JSON config from {file}")
|
||||
config = json.load(open(file, "r", encoding="utf-8"))
|
||||
return flat_config(config if config is not None else {})
|
||||
|
||||
|
||||
def load_from_toml(file: str) -> dict[str, Any]:
|
||||
"""
|
||||
Load config from toml file
|
||||
"""
|
||||
logger.debug(f"Loading TOML config from {file}")
|
||||
config = toml.load(open(file, "r", encoding="utf-8"))
|
||||
return flat_config(config if config is not None else {})
|
||||
|
||||
|
||||
def load_from_files(*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))
|
||||
else:
|
||||
if not no_warning:
|
||||
logger.warning(f"Unsupported config file format: {file}")
|
||||
else:
|
||||
if not no_warning:
|
||||
logger.warning(f"Config file not found: {file}")
|
||||
return config
|
||||
|
||||
|
||||
def load_configs_from_dirs(*directories: str, no_waring: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
从目录下加载配置文件,不递归
|
||||
按照读取文件的优先级反向覆盖
|
||||
默认执行扁平化选项
|
||||
"""
|
||||
config = {}
|
||||
for directory in directories:
|
||||
if not os.path.exists(directory):
|
||||
if not no_waring:
|
||||
logger.warning(f"Directory not found: {directory}")
|
||||
continue
|
||||
for file in os.listdir(directory):
|
||||
if file.endswith(_SUPPORTED_CONFIG_FORMATS):
|
||||
config.update(load_from_files(os.path.join(directory, file), no_warning=no_waring))
|
||||
return config
|
||||
|
||||
|
||||
def load_config_in_default(no_waring: bool = 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
|
||||
|
@ -1,11 +1,4 @@
|
||||
import multiprocessing
|
||||
|
||||
from .spawn_process import *
|
||||
from .manager import *
|
||||
|
||||
__all__ = [
|
||||
"IS_MAIN_PROCESS"
|
||||
]
|
||||
|
||||
IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
|
||||
|
||||
|
@ -8,82 +8,163 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
@File : manager.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import asyncio
|
||||
import multiprocessing
|
||||
import threading
|
||||
from multiprocessing import Process
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any, Callable, TYPE_CHECKING, TypeAlias
|
||||
|
||||
from liteyuki.comm import Channel
|
||||
from liteyuki.comm.channel import Channel, get_channel, set_channels, publish_channel
|
||||
from liteyuki.comm.storage import shared_memory
|
||||
from liteyuki.log import logger
|
||||
from liteyuki.utils import IS_MAIN_PROCESS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from liteyuki.bot import LiteyukiBot
|
||||
from liteyuki.bot.lifespan import Lifespan
|
||||
from liteyuki.comm.storage import KeyValueStore
|
||||
|
||||
if IS_MAIN_PROCESS:
|
||||
from liteyuki.comm.channel import channel_deliver_active_channel, channel_deliver_passive_channel
|
||||
else:
|
||||
from liteyuki.comm import channel
|
||||
|
||||
TARGET_FUNC: TypeAlias = Callable[..., Any]
|
||||
TIMEOUT = 10
|
||||
|
||||
__all__ = [
|
||||
"ProcessManager"
|
||||
]
|
||||
multiprocessing.set_start_method("spawn", force=True)
|
||||
|
||||
|
||||
class ChannelDeliver:
|
||||
def __init__(
|
||||
self,
|
||||
active: Channel[Any],
|
||||
passive: Channel[Any],
|
||||
channel_deliver_active: Channel[Channel[Any]],
|
||||
channel_deliver_passive: Channel[tuple[str, dict]],
|
||||
publish: Channel[tuple[str, Any]],
|
||||
):
|
||||
self.active = active
|
||||
self.passive = passive
|
||||
self.channel_deliver_active = channel_deliver_active
|
||||
self.channel_deliver_passive = channel_deliver_passive
|
||||
self.publish = publish
|
||||
|
||||
|
||||
# 函数处理一些跨进程通道的
|
||||
def _delivery_channel_wrapper(func: TARGET_FUNC, cd: ChannelDeliver, sm: "KeyValueStore", *args, **kwargs):
|
||||
"""
|
||||
子进程入口函数
|
||||
处理一些操作
|
||||
"""
|
||||
# 给子进程设置通道
|
||||
if IS_MAIN_PROCESS:
|
||||
raise RuntimeError("Function should only be called in a sub process.")
|
||||
|
||||
channel.active_channel = cd.active # 子进程主动通道
|
||||
channel.passive_channel = cd.passive # 子进程被动通道
|
||||
channel.channel_deliver_active_channel = cd.channel_deliver_active # 子进程通道传递主动通道
|
||||
channel.channel_deliver_passive_channel = cd.channel_deliver_passive # 子进程通道传递被动通道
|
||||
channel.publish_channel = cd.publish # 子进程发布通道
|
||||
|
||||
# 给子进程创建共享内存实例
|
||||
from liteyuki.comm import storage
|
||||
storage.shared_memory = sm
|
||||
|
||||
func(*args, **kwargs)
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""
|
||||
在主进程中被调用
|
||||
进程管理器
|
||||
"""
|
||||
|
||||
def __init__(self, bot: "LiteyukiBot", chan: Channel):
|
||||
self.bot = bot
|
||||
self.chan = chan
|
||||
self.targets: dict[str, tuple[callable, tuple, dict]] = {}
|
||||
def __init__(self, lifespan: "Lifespan"):
|
||||
self.lifespan = lifespan
|
||||
self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
|
||||
self.processes: dict[str, Process] = {}
|
||||
|
||||
def start(self, name: str, delay: int = 0):
|
||||
def start(self, name: str):
|
||||
"""
|
||||
开启后自动监控进程,并添加到进程字典中
|
||||
Args:
|
||||
name:
|
||||
delay:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
|
||||
if name not in self.targets:
|
||||
raise KeyError(f"Process {name} not found.")
|
||||
|
||||
def _start():
|
||||
should_exit = False
|
||||
while not should_exit:
|
||||
process = Process(target=self.targets[name][0], args=(self.chan, *self.targets[name][1]), kwargs=self.targets[name][2])
|
||||
self.processes[name] = process
|
||||
process.start()
|
||||
while not should_exit:
|
||||
# 0退出 1重启
|
||||
data = self.chan.receive(name)
|
||||
if data == 1:
|
||||
logger.info(f"Restarting process {name}")
|
||||
asyncio.run(self.bot.lifespan.before_shutdown())
|
||||
asyncio.run(self.bot.lifespan.before_restart())
|
||||
self.terminate(name)
|
||||
break
|
||||
chan_active = get_channel(f"{name}-active")
|
||||
|
||||
elif data == 0:
|
||||
logger.info(f"Stopping process {name}")
|
||||
asyncio.run(self.bot.lifespan.before_shutdown())
|
||||
should_exit = True
|
||||
self.terminate(name)
|
||||
else:
|
||||
logger.warning("Unknown data received, ignored.")
|
||||
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()
|
||||
|
||||
if delay:
|
||||
threading.Timer(delay, _start).start()
|
||||
else:
|
||||
threading.Thread(target=_start).start()
|
||||
# 启动进程并监听信号
|
||||
_start_process()
|
||||
|
||||
def add_target(self, name: str, target, *args, **kwargs):
|
||||
self.targets[name] = (target, args, kwargs)
|
||||
while True:
|
||||
data = chan_active.receive()
|
||||
if data == 0:
|
||||
# 停止
|
||||
logger.info(f"Stopping process {name}")
|
||||
self.lifespan.before_process_shutdown()
|
||||
self.terminate(name)
|
||||
break
|
||||
elif data == 1:
|
||||
# 重启
|
||||
logger.info(f"Restarting process {name}")
|
||||
self.lifespan.before_process_shutdown()
|
||||
self.lifespan.before_process_restart()
|
||||
self.terminate(name)
|
||||
_start_process()
|
||||
continue
|
||||
else:
|
||||
logger.warning("Unknown data received, ignored.")
|
||||
|
||||
def join(self):
|
||||
def start_all(self):
|
||||
"""
|
||||
启动所有进程
|
||||
"""
|
||||
for name in self.targets:
|
||||
threading.Thread(target=self.start, args=(name,), daemon=True).start()
|
||||
|
||||
def add_target(self, name: str, target: TARGET_FUNC, args: tuple = (), kwargs=None):
|
||||
"""
|
||||
添加进程
|
||||
Args:
|
||||
name: 进程名,用于获取和唯一标识
|
||||
target: 进程函数
|
||||
args: 进程函数参数
|
||||
kwargs: 进程函数关键字参数,通常会默认传入chan_active和chan_passive
|
||||
"""
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
chan_active: Channel = Channel(_id=f"{name}-active")
|
||||
chan_passive: Channel = Channel(_id=f"{name}-passive")
|
||||
|
||||
channel_deliver = ChannelDeliver(
|
||||
active=chan_active,
|
||||
passive=chan_passive,
|
||||
channel_deliver_active=channel_deliver_active_channel,
|
||||
channel_deliver_passive=channel_deliver_passive_channel,
|
||||
publish=publish_channel
|
||||
)
|
||||
|
||||
self.targets[name] = (_delivery_channel_wrapper, (target, channel_deliver, shared_memory, *args), kwargs)
|
||||
# 主进程通道
|
||||
set_channels(
|
||||
{
|
||||
f"{name}-active" : chan_active,
|
||||
f"{name}-passive": chan_passive
|
||||
}
|
||||
)
|
||||
|
||||
def join_all(self):
|
||||
for name, process in self.targets:
|
||||
process.join()
|
||||
|
||||
@ -96,10 +177,29 @@ class ProcessManager:
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if name not in self.targets:
|
||||
raise logger.warning(f"Process {name} not found.")
|
||||
if name not in self.processes:
|
||||
logger.warning(f"Process {name} not found.")
|
||||
return
|
||||
process = self.processes[name]
|
||||
process.terminate()
|
||||
process.join(TIMEOUT)
|
||||
if process.is_alive():
|
||||
process.kill()
|
||||
logger.success(f"Process {name} terminated.")
|
||||
|
||||
def terminate_all(self):
|
||||
for name in self.targets:
|
||||
self.terminate(name)
|
||||
|
||||
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()
|
||||
|
@ -1,51 +0,0 @@
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
import nonebot
|
||||
|
||||
from liteyuki.core.nb import adapter_manager, driver_manager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from liteyuki.comm.channel import Channel
|
||||
|
||||
timeout_limit: int = 20
|
||||
|
||||
"""导出对象,用于主进程与nonebot通信"""
|
||||
chan_in_spawn_nb: Optional["Channel"] = None
|
||||
|
||||
|
||||
def nb_run(chan, *args, **kwargs):
|
||||
"""
|
||||
初始化NoneBot并运行在子进程
|
||||
Args:
|
||||
|
||||
chan:
|
||||
*args:
|
||||
**kwargs:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
global chan_in_spawn_nb
|
||||
chan_in_spawn_nb = chan
|
||||
nonebot.init(**kwargs)
|
||||
driver_manager.init(config=kwargs)
|
||||
adapter_manager.init(kwargs)
|
||||
adapter_manager.register()
|
||||
nonebot.load_plugin("src.liteyuki_main")
|
||||
nonebot.run()
|
||||
|
||||
|
||||
def mb_run(chan, *args, **kwargs):
|
||||
"""
|
||||
初始化MeloBot并运行在子进程
|
||||
Args:
|
||||
chan
|
||||
*args:
|
||||
**kwargs:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# bot = MeloBot(__name__)
|
||||
# bot.init(AbstractConnector(cd_time=0))
|
||||
# bot.run()
|
4
liteyuki/dev/__init__.py
Normal file
4
liteyuki/dev/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
该模块用于存放一些开发工具
|
||||
"""
|
91
liteyuki/dev/observer.py
Normal file
91
liteyuki/dev/observer.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""
|
||||
此模块用于注册观察者函数,使用watchdog监控文件变化并重启bot
|
||||
启用该模块需要在配置文件中设置`dev_mode`为True
|
||||
"""
|
||||
import time
|
||||
from typing import Callable, TypeAlias
|
||||
|
||||
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from liteyuki import get_bot, get_config_with_compat, logger
|
||||
|
||||
liteyuki_bot = get_bot()
|
||||
|
||||
CALLBACK_FUNC: TypeAlias = Callable[[FileSystemEvent], None] # 位置1为FileSystemEvent
|
||||
FILTER_FUNC: TypeAlias = Callable[[FileSystemEvent], bool] # 位置1为FileSystemEvent
|
||||
observer = Observer()
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
if get_config_with_compat("liteyuki.reload", ("dev_mode",), False):
|
||||
logger.debug("Liteyuki Reload enabled, watching for file changes...")
|
||||
observer.start()
|
||||
|
||||
|
||||
class CodeModifiedHandler(FileSystemEventHandler):
|
||||
"""
|
||||
Handler for code file changes
|
||||
"""
|
||||
|
||||
@debounce(1)
|
||||
def on_modified(self, event):
|
||||
raise NotImplementedError("on_modified must be implemented")
|
||||
|
||||
def on_created(self, event):
|
||||
self.on_modified(event)
|
||||
|
||||
def on_deleted(self, event):
|
||||
self.on_modified(event)
|
||||
|
||||
def on_moved(self, event):
|
||||
self.on_modified(event)
|
||||
|
||||
def on_any_event(self, event):
|
||||
self.on_modified(event)
|
||||
|
||||
|
||||
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
|
28
liteyuki/dev/plugin.py
Normal file
28
liteyuki/dev/plugin.py
Normal file
@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/18 上午5:04
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : plugin.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from liteyuki.bot import LiteyukiBot
|
||||
from liteyuki.config import load_config_in_default
|
||||
|
||||
|
||||
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()
|
@ -9,22 +9,10 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import sys
|
||||
|
||||
import loguru
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
logger = loguru.logger
|
||||
if TYPE_CHECKING:
|
||||
# avoid sphinx autodoc resolve annotation failed
|
||||
# because loguru module do not have `Logger` class actually
|
||||
from loguru import Record
|
||||
|
||||
|
||||
def default_filter(record: "Record"):
|
||||
"""默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。"""
|
||||
log_level = record["extra"].get("nonebot_log_level", "INFO")
|
||||
levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level
|
||||
return record["level"].no >= levelno
|
||||
|
||||
|
||||
# DEBUG日志格式
|
||||
debug_format: str = (
|
||||
@ -50,35 +38,26 @@ def get_format(level: str) -> str:
|
||||
return default_format
|
||||
|
||||
|
||||
logger = loguru.logger.bind()
|
||||
|
||||
|
||||
def init_log(config: dict):
|
||||
"""
|
||||
在语言加载完成后执行
|
||||
Returns:
|
||||
|
||||
"""
|
||||
global logger
|
||||
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level=0,
|
||||
diagnose=False,
|
||||
filter=default_filter,
|
||||
format=get_format(config.get("log_level", "INFO")),
|
||||
)
|
||||
show_icon = config.get("log_icon", True)
|
||||
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")
|
||||
|
||||
# debug = lang.get("log.debug", default="==DEBUG")
|
||||
# info = lang.get("log.info", default="===INFO")
|
||||
# success = lang.get("log.success", default="SUCCESS")
|
||||
# warning = lang.get("log.warning", default="WARNING")
|
||||
# error = lang.get("log.error", default="==ERROR")
|
||||
#
|
||||
# logger.level("DEBUG", color="<blue>", icon=f"{'🐛' if show_icon else ''}{debug}")
|
||||
# logger.level("INFO", color="<normal>", icon=f"{'ℹ️' if show_icon else ''}{info}")
|
||||
# logger.level("SUCCESS", color="<green>", icon=f"{'✅' if show_icon else ''}{success}")
|
||||
# logger.level("WARNING", color="<yellow>", icon=f"{'⚠️' if show_icon else ''}{warning}")
|
||||
# logger.level("ERROR", color="<red>", icon=f"{'⭕' if show_icon else ''}{error}")
|
||||
|
||||
init_log(config={})
|
||||
|
10
liteyuki/message/__init__.py
Normal file
10
liteyuki/message/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:44
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : __init__.py.py
|
||||
@Software: PyCharm
|
||||
"""
|
56
liteyuki/message/event.py
Normal file
56
liteyuki/message/event.py
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:47
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : event.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from liteyuki.comm.storage import shared_memory
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self, type: str, data: dict[str, Any], bot_id: str, session_id: str, session_type: str, receive_channel: str = "event_to_nonebot"):
|
||||
"""
|
||||
事件
|
||||
Args:
|
||||
type: 类型
|
||||
data: 数据
|
||||
bot_id: 机器人ID
|
||||
session_id: 会话ID
|
||||
session_type: 会话类型
|
||||
receive_channel: 接收频道
|
||||
"""
|
||||
self.type = type
|
||||
self.data = data
|
||||
self.bot_id = bot_id
|
||||
self.session_id = session_id
|
||||
self.session_type = session_type
|
||||
self.receive_channel = receive_channel
|
||||
|
||||
def __str__(self):
|
||||
return f"Event(type={self.type}, data={self.data}, bot_id={self.bot_id}, session_id={self.session_id}, session_type={self.session_type})"
|
||||
|
||||
def reply(self, message: str | dict[str, Any]):
|
||||
"""
|
||||
回复消息
|
||||
Args:
|
||||
message:
|
||||
Returns:
|
||||
"""
|
||||
to_nonebot_event = Event(
|
||||
type=self.session_type,
|
||||
data={
|
||||
"message": message
|
||||
},
|
||||
bot_id=self.bot_id,
|
||||
session_id=self.session_id,
|
||||
session_type=self.session_type,
|
||||
receive_channel="_"
|
||||
)
|
||||
print(to_nonebot_event)
|
||||
shared_memory.publish(self.receive_channel, to_nonebot_event)
|
62
liteyuki/message/matcher.py
Normal file
62
liteyuki/message/matcher.py
Normal file
@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:51
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : matcher.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import traceback
|
||||
from typing import Any, TypeAlias, Callable, Coroutine
|
||||
|
||||
from liteyuki import Event
|
||||
from liteyuki.message.rule import Rule
|
||||
|
||||
EventHandler: TypeAlias = Callable[[Event], Coroutine[None, None, Any]]
|
||||
|
||||
|
||||
class Matcher:
|
||||
def __init__(self, rule: Rule, priority: int, block: bool):
|
||||
"""
|
||||
匹配器
|
||||
Args:
|
||||
rule: 规则
|
||||
priority: 优先级 >= 0
|
||||
block: 是否阻断后续优先级更低的匹配器
|
||||
"""
|
||||
self.rule = rule
|
||||
self.priority = priority
|
||||
self.block = block
|
||||
self.handlers: list[EventHandler] = []
|
||||
|
||||
def __str__(self):
|
||||
return f"Matcher(rule={self.rule}, priority={self.priority}, block={self.block})"
|
||||
|
||||
def handle(self, handler: EventHandler) -> EventHandler:
|
||||
"""
|
||||
添加处理函数,装饰器
|
||||
Args:
|
||||
handler:
|
||||
Returns:
|
||||
EventHandler
|
||||
"""
|
||||
self.handlers.append(handler)
|
||||
return handler
|
||||
|
||||
async def run(self, event: Event) -> None:
|
||||
"""
|
||||
运行处理函数
|
||||
Args:
|
||||
event:
|
||||
Returns:
|
||||
"""
|
||||
if not await self.rule(event):
|
||||
return
|
||||
|
||||
for handler in self.handlers:
|
||||
try:
|
||||
await handler(event)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
47
liteyuki/message/on.py
Normal file
47
liteyuki/message/on.py
Normal file
@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:52
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : on.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
import threading
|
||||
|
||||
from queue import Queue
|
||||
|
||||
from liteyuki.comm.storage import shared_memory
|
||||
from liteyuki.log import logger
|
||||
from liteyuki.message.event import Event
|
||||
from liteyuki.message.matcher import Matcher
|
||||
from liteyuki.message.rule import Rule
|
||||
|
||||
_matcher_list: list[Matcher] = []
|
||||
_queue: Queue = Queue()
|
||||
|
||||
|
||||
@shared_memory.on_subscriber_receive("event_to_liteyuki")
|
||||
async def _(event: Event):
|
||||
current_priority = -1
|
||||
for i, matcher in enumerate(_matcher_list):
|
||||
logger.info(f"Running matcher {matcher} for event: {event}")
|
||||
await matcher.run(event)
|
||||
# 同优先级不阻断,不同优先级阻断
|
||||
if current_priority != matcher.priority:
|
||||
current_priority = matcher.priority
|
||||
if matcher.block:
|
||||
break
|
||||
|
||||
|
||||
def on_message(rule: Rule = Rule(), priority: int = 0, block: bool = True) -> Matcher:
|
||||
matcher = Matcher(rule, priority, block)
|
||||
# 按照优先级插入
|
||||
for i, m in enumerate(_matcher_list):
|
||||
if m.priority < matcher.priority:
|
||||
_matcher_list.insert(i, matcher)
|
||||
break
|
||||
else:
|
||||
_matcher_list.append(matcher)
|
||||
return matcher
|
33
liteyuki/message/rule.py
Normal file
33
liteyuki/message/rule.py
Normal file
@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:55
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : rule.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
|
||||
from typing import Optional, TypeAlias, Callable, Coroutine
|
||||
|
||||
from liteyuki.message.event import Event
|
||||
|
||||
RuleHandler: TypeAlias = Callable[[Event], Coroutine[None, None, bool]]
|
||||
"""规则函数签名"""
|
||||
|
||||
|
||||
class Rule:
|
||||
def __init__(self, handler: Optional[RuleHandler] = None):
|
||||
self.handler = handler
|
||||
|
||||
def __or__(self, other: "Rule") -> "Rule":
|
||||
return Rule(lambda event: self.handler(event) or other.handler(event))
|
||||
|
||||
def __and__(self, other: "Rule") -> "Rule":
|
||||
return Rule(lambda event: self.handler(event) and other.handler(event))
|
||||
|
||||
async def __call__(self, event: Event) -> bool:
|
||||
if self.handler is None:
|
||||
return True
|
||||
return await self.handler(event)
|
10
liteyuki/message/session.py
Normal file
10
liteyuki/message/session.py
Normal file
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 下午10:47
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : session.py
|
||||
@Software: PyCharm
|
||||
"""
|
343
liteyuki/mkdoc.py
Normal file
343
liteyuki/mkdoc.py
Normal file
@ -0,0 +1,343 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
|
||||
@Time : 2024/8/19 上午6:23
|
||||
@Author : snowykami
|
||||
@Email : snowykami@outlook.com
|
||||
@File : mkdoc.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel
|
||||
|
||||
NO_TYPE_ANY = "Any"
|
||||
NO_TYPE_HINT = "NoTypeHint"
|
||||
|
||||
|
||||
class DefType(Enum):
|
||||
FUNCTION = "function"
|
||||
METHOD = "method"
|
||||
STATIC_METHOD = "staticmethod"
|
||||
CLASS_METHOD = "classmethod"
|
||||
PROPERTY = "property"
|
||||
|
||||
|
||||
class FunctionInfo(BaseModel):
|
||||
name: str
|
||||
args: list[tuple[str, str]]
|
||||
return_type: str
|
||||
docstring: str
|
||||
|
||||
type: DefType
|
||||
"""若为类中def,则有"""
|
||||
is_async: bool
|
||||
|
||||
|
||||
class AttributeInfo(BaseModel):
|
||||
name: str
|
||||
type: str
|
||||
value: Any = None
|
||||
docstring: str = ""
|
||||
|
||||
|
||||
class ClassInfo(BaseModel):
|
||||
name: str
|
||||
docstring: str
|
||||
methods: list[FunctionInfo]
|
||||
attributes: list[AttributeInfo]
|
||||
inherit: list[str]
|
||||
|
||||
|
||||
class ModuleInfo(BaseModel):
|
||||
module_path: str
|
||||
"""点分割模块路径 例如 liteyuki.bot"""
|
||||
|
||||
functions: list[FunctionInfo]
|
||||
classes: list[ClassInfo]
|
||||
attributes: list[AttributeInfo]
|
||||
docstring: str
|
||||
|
||||
|
||||
def get_relative_path(base_path: str, target_path: str) -> str:
|
||||
"""
|
||||
获取相对路径
|
||||
Args:
|
||||
base_path: 基础路径
|
||||
target_path: 目标路径
|
||||
"""
|
||||
return os.path.relpath(target_path, base_path)
|
||||
|
||||
|
||||
def write_to_files(file_data: dict[str, str]):
|
||||
"""
|
||||
输出文件
|
||||
Args:
|
||||
file_data: 文件数据 相对路径
|
||||
"""
|
||||
|
||||
for rp, data in file_data.items():
|
||||
|
||||
if not os.path.exists(os.path.dirname(rp)):
|
||||
os.makedirs(os.path.dirname(rp))
|
||||
with open(rp, 'w', encoding='utf-8') as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
def get_file_list(module_folder: str):
|
||||
file_list = []
|
||||
for root, dirs, files in os.walk(module_folder):
|
||||
for file in files:
|
||||
if file.endswith((".py", ".pyi")):
|
||||
file_list.append(os.path.join(root, file))
|
||||
return file_list
|
||||
|
||||
|
||||
def get_module_info_normal(file_path: str, ignore_private: bool = True) -> ModuleInfo:
|
||||
"""
|
||||
获取函数和类
|
||||
Args:
|
||||
file_path: Python 文件路径
|
||||
ignore_private: 忽略私有函数和类
|
||||
Returns:
|
||||
模块信息
|
||||
"""
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
file_content = file.read()
|
||||
tree = ast.parse(file_content)
|
||||
|
||||
dot_sep_module_path = file_path.replace(os.sep, '.').replace(".py", "").replace(".pyi", "")
|
||||
module_docstring = ast.get_docstring(tree)
|
||||
|
||||
module_info = ModuleInfo(
|
||||
module_path=dot_sep_module_path,
|
||||
functions=[],
|
||||
classes=[],
|
||||
attributes=[],
|
||||
docstring=module_docstring if module_docstring else ""
|
||||
)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
# 模块函数 且不在类中 若ignore_private=True则忽略私有函数
|
||||
if not any(isinstance(parent, ast.ClassDef) for parent in ast.iter_child_nodes(node)) and (not ignore_private or not node.name.startswith('_')):
|
||||
|
||||
# 判断第一个参数是否为self或cls,后期用其他办法优化
|
||||
if node.args.args:
|
||||
first_arg = node.args.args[0]
|
||||
if first_arg.arg in ("self", "cls"):
|
||||
continue
|
||||
|
||||
function_docstring = ast.get_docstring(node)
|
||||
|
||||
func_info = FunctionInfo(
|
||||
name=node.name,
|
||||
args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in node.args.args],
|
||||
return_type=ast.unparse(node.returns) if node.returns else "None",
|
||||
docstring=function_docstring if function_docstring else "",
|
||||
type=DefType.FUNCTION,
|
||||
is_async=isinstance(node, ast.AsyncFunctionDef)
|
||||
)
|
||||
module_info.functions.append(func_info)
|
||||
|
||||
elif isinstance(node, ast.ClassDef):
|
||||
# 模块类
|
||||
class_docstring = ast.get_docstring(node)
|
||||
|
||||
class_info = ClassInfo(
|
||||
name=node.name,
|
||||
docstring=class_docstring if class_docstring else "",
|
||||
methods=[],
|
||||
attributes=[],
|
||||
inherit=[ast.unparse(base) for base in node.bases]
|
||||
)
|
||||
|
||||
for class_node in node.body:
|
||||
# methods [instance, static, class, property],保留__init__方法
|
||||
if isinstance(class_node, ast.FunctionDef) and (not ignore_private or not class_node.name.startswith('_') or class_node.name == "__init__"):
|
||||
method_docstring = ast.get_docstring(class_node)
|
||||
def_type = DefType.METHOD
|
||||
if class_node.decorator_list:
|
||||
if any(isinstance(decorator, ast.Name) and decorator.id == "staticmethod" for decorator in class_node.decorator_list):
|
||||
def_type = DefType.STATIC_METHOD
|
||||
elif any(isinstance(decorator, ast.Name) and decorator.id == "classmethod" for decorator in class_node.decorator_list):
|
||||
def_type = DefType.CLASS_METHOD
|
||||
elif any(isinstance(decorator, ast.Name) and decorator.id == "property" for decorator in class_node.decorator_list):
|
||||
def_type = DefType.PROPERTY
|
||||
class_info.methods.append(FunctionInfo(
|
||||
name=class_node.name,
|
||||
args=[(arg.arg, ast.unparse(arg.annotation) if arg.annotation else NO_TYPE_ANY) for arg in class_node.args.args],
|
||||
return_type=ast.unparse(class_node.returns) if class_node.returns else "None",
|
||||
docstring=method_docstring if method_docstring else "",
|
||||
type=def_type,
|
||||
is_async=isinstance(class_node, ast.AsyncFunctionDef)
|
||||
))
|
||||
# attributes
|
||||
elif isinstance(class_node, ast.Assign):
|
||||
for target in class_node.targets:
|
||||
if isinstance(target, ast.Name):
|
||||
class_info.attributes.append(AttributeInfo(
|
||||
name=target.id,
|
||||
type=ast.unparse(class_node.value)
|
||||
))
|
||||
module_info.classes.append(class_info)
|
||||
|
||||
elif isinstance(node, ast.Assign):
|
||||
# 检查是否在类或函数中
|
||||
if not any(isinstance(parent, (ast.ClassDef, ast.FunctionDef)) for parent in ast.iter_child_nodes(node)):
|
||||
# 模块属性变量
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and (not ignore_private or not target.id.startswith('_')):
|
||||
attr_type = NO_TYPE_HINT
|
||||
if isinstance(node.value, ast.AnnAssign) and node.value.annotation:
|
||||
attr_type = ast.unparse(node.value.annotation)
|
||||
module_info.attributes.append(AttributeInfo(
|
||||
name=target.id,
|
||||
type=attr_type,
|
||||
value=ast.unparse(node.value) if node.value else None
|
||||
))
|
||||
|
||||
return module_info
|
||||
|
||||
|
||||
def generate_markdown(module_info: ModuleInfo, front_matter=None) -> str:
|
||||
"""
|
||||
生成模块的Markdown
|
||||
你可在此自定义生成的Markdown格式
|
||||
Args:
|
||||
module_info: 模块信息
|
||||
front_matter: 自定义选项title, index, icon, category
|
||||
Returns:
|
||||
Markdown 字符串
|
||||
"""
|
||||
|
||||
content = ""
|
||||
|
||||
front_matter = "---\n" + "\n".join([f"{k}: {v}" for k, v in front_matter.items()]) + "\n---\n\n"
|
||||
|
||||
content += front_matter
|
||||
|
||||
|
||||
|
||||
# 模块函数
|
||||
for func in module_info.functions:
|
||||
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] else arg[0] for arg in func.args]
|
||||
content += f"### ***{'async ' if func.is_async else ''}def*** `{func.name}({', '.join(args_with_type)}) -> {func.return_type}`\n\n"
|
||||
|
||||
func.docstring = func.docstring.replace("\n", "\n\n")
|
||||
content += f"{func.docstring}\n\n"
|
||||
|
||||
# 类
|
||||
for cls in module_info.classes:
|
||||
if cls.inherit:
|
||||
inherit = f"({', '.join(cls.inherit)})" if cls.inherit else ""
|
||||
content += f"### ***class*** `{cls.name}{inherit}`\n\n"
|
||||
else:
|
||||
content += f"### ***class*** `{cls.name}`\n\n"
|
||||
|
||||
cls.docstring = cls.docstring.replace("\n", "\n\n")
|
||||
content += f"{cls.docstring}\n\n"
|
||||
for method in cls.methods:
|
||||
# 类函数
|
||||
|
||||
if method.type != DefType.METHOD:
|
||||
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] else arg[0] for arg in method.args]
|
||||
content += f"###   ***@{method.type.value}***\n"
|
||||
else:
|
||||
# self不加类型提示
|
||||
args_with_type = [f"{arg[0]}: {arg[1]}" if arg[1] and arg[0] != "self" else arg[0] for arg in method.args]
|
||||
content += f"###   ***{'async ' if method.is_async else ''}def*** `{method.name}({', '.join(args_with_type)}) -> {method.return_type}`\n\n"
|
||||
|
||||
method.docstring = method.docstring.replace("\n", "\n\n")
|
||||
content += f" {method.docstring}\n\n"
|
||||
for attr in cls.attributes:
|
||||
content += f"###   ***attr*** `{attr.name}: {attr.type}`\n\n"
|
||||
|
||||
# 模块属性
|
||||
for attr in module_info.attributes:
|
||||
if attr.type == NO_TYPE_HINT:
|
||||
content += f"### ***var*** `{attr.name} = {attr.value}`\n\n"
|
||||
else:
|
||||
content += f"### ***var*** `{attr.name}: {attr.type} = {attr.value}`\n\n"
|
||||
|
||||
attr.docstring = attr.docstring.replace("\n", "\n\n")
|
||||
content += f"{attr.docstring}\n\n"
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def generate_docs(module_folder: str, output_dir: str, with_top: bool = False, ignored_paths=None):
|
||||
"""
|
||||
生成文档
|
||||
Args:
|
||||
module_folder: 模块文件夹
|
||||
output_dir: 输出文件夹
|
||||
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b, True时例如docs/api/module/module_a.md, docs/api/module/module_b.md
|
||||
ignored_paths: 忽略的路径
|
||||
"""
|
||||
if ignored_paths is None:
|
||||
ignored_paths = []
|
||||
file_data: dict[str, str] = {} # 路径 -> 字串
|
||||
|
||||
file_list = get_file_list(module_folder)
|
||||
|
||||
# 清理输出目录
|
||||
shutil.rmtree(output_dir, ignore_errors=True)
|
||||
os.mkdir(output_dir)
|
||||
|
||||
replace_data = {
|
||||
"__init__": "README",
|
||||
".py" : ".md",
|
||||
}
|
||||
|
||||
for pyfile_path in file_list:
|
||||
if any(ignored_path.replace("\\", "/") in pyfile_path.replace("\\", "/") for ignored_path in ignored_paths):
|
||||
continue
|
||||
|
||||
no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path) # 去头路径
|
||||
|
||||
# markdown相对路径
|
||||
rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path
|
||||
for rk, rv in replace_data.items():
|
||||
rel_md_path = rel_md_path.replace(rk, rv)
|
||||
|
||||
abs_md_path = os.path.join(output_dir, rel_md_path)
|
||||
|
||||
# 获取模块信息
|
||||
module_info = get_module_info_normal(pyfile_path)
|
||||
|
||||
# 生成markdown
|
||||
|
||||
if "README" in abs_md_path:
|
||||
front_matter = {
|
||||
"title" : module_info.module_path.replace(".__init__", "").replace("_", "\\n"),
|
||||
"index" : "true",
|
||||
"icon" : "laptop-code",
|
||||
"category": "API"
|
||||
}
|
||||
else:
|
||||
front_matter = {
|
||||
"title" : module_info.module_path.replace("_", "\\n"),
|
||||
"order" : "1",
|
||||
"icon" : "laptop-code",
|
||||
"category": "API"
|
||||
}
|
||||
|
||||
md_content = generate_markdown(module_info, front_matter)
|
||||
print(f"Generate {pyfile_path} -> {abs_md_path}")
|
||||
file_data[abs_md_path] = md_content
|
||||
|
||||
write_to_files(file_data)
|
||||
|
||||
|
||||
# 入口脚本
|
||||
if __name__ == '__main__':
|
||||
# 这里填入你的模块路径
|
||||
generate_docs('liteyuki', 'docs/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"])
|
||||
generate_docs('liteyuki', 'docs/en/dev/api', with_top=False, ignored_paths=["liteyuki/plugins"])
|
@ -1,9 +1,10 @@
|
||||
from liteyuki.plugin.model import Plugin, PluginMetadata
|
||||
from liteyuki.plugin.model import Plugin, PluginMetadata, PluginType
|
||||
from liteyuki.plugin.load import load_plugin, load_plugins, _plugins
|
||||
|
||||
__all__ = [
|
||||
"PluginMetadata",
|
||||
"Plugin",
|
||||
"PluginType",
|
||||
"load_plugin",
|
||||
"load_plugins",
|
||||
]
|
||||
|
@ -10,21 +10,20 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
"""
|
||||
import os
|
||||
import traceback
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from nonebot import logger
|
||||
|
||||
from liteyuki.plugin.model import Plugin, PluginMetadata
|
||||
from importlib import import_module
|
||||
|
||||
from liteyuki.log import logger
|
||||
from liteyuki.plugin.model import Plugin, PluginMetadata, PluginType
|
||||
from liteyuki.utils import path_to_module_name
|
||||
|
||||
_plugins: dict[str, Plugin] = {}
|
||||
|
||||
__all__ = [
|
||||
"load_plugin",
|
||||
"load_plugins",
|
||||
"load_plugin",
|
||||
"load_plugins",
|
||||
"_plugins",
|
||||
]
|
||||
|
||||
|
||||
@ -44,8 +43,13 @@ def load_plugin(module_path: str | Path) -> Optional[Plugin]:
|
||||
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 "<y>{module.__name__.split(".")[-1]}</y>"'
|
||||
f'Succeeded to load liteyuki plugin "{display_name}"'
|
||||
)
|
||||
return _plugins[module.__name__]
|
||||
|
||||
@ -57,15 +61,31 @@ def load_plugin(module_path: str | Path) -> Optional[Plugin]:
|
||||
return None
|
||||
|
||||
|
||||
def load_plugins(*plugin_dir: str) -> set[Plugin]:
|
||||
def load_plugins(*plugin_dir: str, ignore_warning: bool = True) -> set[Plugin]:
|
||||
"""导入文件夹下多个插件
|
||||
|
||||
参数:
|
||||
plugin_dir: 文件夹路径
|
||||
ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
|
||||
"""
|
||||
plugins = set()
|
||||
for dir_path in plugin_dir:
|
||||
# 遍历每一个文件夹下的py文件和包含__init__.py的文件夹,不递归
|
||||
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))
|
||||
|
||||
@ -81,3 +101,27 @@ def load_plugins(*plugin_dir: str) -> set[Plugin]:
|
||||
if _plugins.get(module_name):
|
||||
plugins.add(_plugins[module_name])
|
||||
return plugins
|
||||
|
||||
|
||||
def format_display_name(display_name: str, plugin_type: PluginType) -> str:
|
||||
"""
|
||||
设置插件名称颜色,根据不同类型插件设置颜色
|
||||
Args:
|
||||
display_name: 插件名称
|
||||
plugin_type: 插件类型
|
||||
|
||||
Returns:
|
||||
str: 设置后的插件名称 <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}>"
|
||||
|
@ -8,22 +8,61 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
|
||||
@File : model.py
|
||||
@Software: PyCharm
|
||||
"""
|
||||
from enum import Enum
|
||||
from types import ModuleType
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PluginType(Enum):
|
||||
"""
|
||||
插件类型枚举值
|
||||
"""
|
||||
APPLICATION = "application"
|
||||
"""应用端:例如NoneBot"""
|
||||
|
||||
SERVICE = "service"
|
||||
"""服务端:例如AI绘画后端"""
|
||||
|
||||
MODULE = "module"
|
||||
"""模块:导出对象给其他插件使用"""
|
||||
|
||||
UNCLASSIFIED = "unclassified"
|
||||
"""未分类:默认值"""
|
||||
|
||||
TEST = "test"
|
||||
"""测试:测试插件"""
|
||||
|
||||
|
||||
class PluginMetadata(BaseModel):
|
||||
"""
|
||||
轻雪插件元数据,由插件编写者提供,name为必填项
|
||||
Attributes:
|
||||
----------
|
||||
|
||||
name: str
|
||||
插件名称
|
||||
description: str
|
||||
插件描述
|
||||
usage: str
|
||||
插件使用方法
|
||||
type: str
|
||||
插件类型
|
||||
author: str
|
||||
插件作者
|
||||
homepage: str
|
||||
插件主页
|
||||
extra: dict[str, Any]
|
||||
额外信息
|
||||
"""
|
||||
name: str
|
||||
description: str = ""
|
||||
usage: str = ""
|
||||
type: str = ""
|
||||
type: PluginType = PluginType.UNCLASSIFIED
|
||||
author: str = ""
|
||||
homepage: str = ""
|
||||
running_in_main: bool = True # 是否在主进程运行
|
||||
extra: dict[str, Any] = {}
|
||||
|
||||
|
||||
class Plugin(BaseModel):
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user