Compare commits
560 Commits
Author | SHA1 | Date | |
---|---|---|---|
5566e9294f | |||
ae464ed3ea | |||
add5174e93 | |||
|
fe85f1e612 | ||
efb13340f0 | |||
56996ef082 | |||
fec96e694d | |||
a4e423168c | |||
20eb1809f1 | |||
66c88d8a30 | |||
7aaa589a51 | |||
84133b2f27 | |||
1de7d81693 | |||
4cbf043547 | |||
7d755581cb | |||
b634352c95 | |||
|
1d35ab21f8 | ||
7a006bc6fd | |||
26c9cb7e35 | |||
|
fc017c8255 | ||
|
908812a3d9 | ||
6c7d073cb1 | |||
8677286bd4 | |||
232ffcf714 | |||
6871bc0d22 | |||
641c16ee7a | |||
75c67bdffc | |||
3a60450358 | |||
7b97210a31 | |||
d97145ee5e | |||
bc3a4355c3 | |||
d835a32683 | |||
aedea1acb9 | |||
3bce8325c1 | |||
d1c1320963 | |||
317e07eb71 | |||
37749ae15e | |||
f94c10de61 | |||
5ccef735be | |||
262002b49a | |||
40c6ba6d9e | |||
60093b562b | |||
30880ec13b | |||
cc1d82312a | |||
efca13d397 | |||
3a8c09d6db | |||
cc1bb8e5e4 | |||
93c17b6026 | |||
fd3f6272f1 | |||
4d87a3c0b7 | |||
86f47ee411 | |||
1d6b8d60f3 | |||
3890704045 | |||
b0761e9873 | |||
291314de93 | |||
fd835e9406 | |||
d681c5645a | |||
d0619f1fe8 | |||
b022a364e3 | |||
df00c61dd8 | |||
94a021bab0 | |||
6b20e9eae0 | |||
0a35a3c6f8 | |||
2e75c7bc65 | |||
3341505715 | |||
bdde9c45fd | |||
7bf94a15c8 | |||
4510477026 | |||
86e50e369b | |||
796fc6f233 | |||
|
80c6875567 | ||
ab89cd1c72 | |||
5e454bc971 | |||
70bfb0fcee | |||
13b95c2732 | |||
ef5866343d | |||
d5ccd105a2 | |||
20ad8dc53f | |||
de9c91d8bd | |||
6b64a0c379 | |||
f117da7ff3 | |||
f548a07230 | |||
e2e53c21fa | |||
3eaf23a56b | |||
4a5dd1f727 | |||
c2cb416b4e | |||
5cd528d5e9 | |||
980fca650b | |||
9c525141f6 | |||
3d218a0e8d | |||
db385f597b | |||
98a9d6413a | |||
a77f97fd4b | |||
e6ea1b700f | |||
596f4d06ea | |||
8e3d3b3b5d | |||
a34ad87e01 | |||
6c4c7f34cd | |||
0c859957b4 | |||
fbb9ed82ee | |||
b469c9420e | |||
aa4d930cc4 | |||
76be748160 | |||
a9dd37b8a5 | |||
5900d621f2 | |||
7442a3651b | |||
413f438689 | |||
1fc4999b09 | |||
975446a096 | |||
98cdd2f4b8 | |||
c0beec0429 | |||
614d78b3fa | |||
24b0f345e4 | |||
0ae10aa1b2 | |||
9fe7478840 | |||
|
efca0bc7b3 | ||
50c5e99b98 | |||
7415efcc90 | |||
5bb4584e6a | |||
795a6f3f76 | |||
|
fa74e08514 | ||
e6cf6e0c68 | |||
6789c16773 | |||
cdea0f8563 | |||
9df55671ac | |||
d96c6f13c1 | |||
bce1bf8704 | |||
8eb626b8da | |||
|
e6505d335b | ||
c8cb341afb | |||
e99cb88b13 | |||
|
78c3e299d0 | ||
23338437e9 | |||
f95899aebd | |||
5df10c66b6 | |||
811d1594cd | |||
c162208638 | |||
679d6597d8 | |||
f402799f28 | |||
|
60542d7426 | ||
db1fb58717 | |||
7d5675ec97 | |||
d8c50752f7 | |||
|
c674b837bb | ||
d867996072 | |||
7ef36c6933 | |||
982aae4dbf | |||
b5d3c6aaa8 | |||
5537bc32df | |||
5c0c723c5d | |||
0ed3b307d9 | |||
53a603d4ee | |||
fbf906bea7 | |||
a87e8bc3e8 | |||
a16a67dbc9 | |||
4c2231feb5 | |||
3932dd60da | |||
3c6380cb82 | |||
2612f99f35 | |||
0b4b9a6241 | |||
2d100885ee | |||
cb335720b7 | |||
dc8ad30b84 | |||
09e00652c3 | |||
b5b15c82f8 | |||
72e71124b8 | |||
d2be2acc95 | |||
d95614e960 | |||
dad9482d7a | |||
fff5d09ad9 | |||
e6ffd1fcc0 | |||
a97747b7c4 | |||
1921dcd023 | |||
18af1d00bd | |||
30cdc1da23 | |||
|
bb84958ce4 | ||
|
44de3fd00a | ||
39b1920532 | |||
aa591ec29e | |||
310c3f065d | |||
2311ef82c3 | |||
b4b931fc95 | |||
d1b887fcaa | |||
5a2990770c | |||
1d0f0a2539 | |||
dbc4d83b08 | |||
da905d21bd | |||
7d91079500 | |||
81a006a308 | |||
be59e241c6 | |||
e493139d85 | |||
1f59ec2ef9 | |||
20d05f609d | |||
0f9683de89 | |||
c805db3371 | |||
58d0d12c1f | |||
359683dbae | |||
94cab8b743 | |||
837447b6e4 | |||
ad52eade07 | |||
9cae3edb6b | |||
0860b61ccd | |||
d125c52b50 | |||
d485e095ae | |||
499caca7e3 | |||
83a2d36209 | |||
8b77ced05e | |||
49a9617f08 | |||
06aa919d9b | |||
77b77c285b | |||
bdc32b26fe | |||
736125f4ee | |||
89cb75f105 | |||
34a6261f27 | |||
ae18bfaee1 | |||
8510b0ed3f | |||
967f1a0e5b | |||
6c1fc62ef1 | |||
433c6b3b85 | |||
ee1ae5a071 | |||
49a15d512e | |||
fd1d73cc32 | |||
29c2aa9404 | |||
0ec1195930 | |||
d7a625bedb | |||
3854376210 | |||
5253d0e515 | |||
|
3bc7fa82b1 | ||
7d98d5819d | |||
8316c0ff63 | |||
315b8c91e5 | |||
|
405eb10a8a | ||
c3072e93c7 | |||
ae34ff622d | |||
d2704818d9 | |||
2ab4184314 | |||
9aade6599c | |||
2f87b06c83 | |||
8bb3f15bd9 | |||
7f198c83b5 | |||
f70c75e9c4 | |||
be5a4b270d | |||
50c0216435 | |||
4910de74fd | |||
f12b6854b7 | |||
b0b61fbaf7 | |||
7c0b0df6ed | |||
cb3ee4b72f | |||
3a3ef4d6ae | |||
|
9b3dea840e | ||
ca89fa7efd | |||
4705eac79c | |||
ebe0c5bcbb | |||
93c287bbd9 | |||
|
0defb00ede | ||
|
81c1d0286d | ||
|
fb25005bd5 | ||
2eb5aae23f | |||
a7d0560932 | |||
|
391f112bb3 | ||
|
bc8d13ba8a | ||
|
e8ec2ee28a | ||
|
b6c8fcbccd | ||
71476560e4 | |||
aa2d182840 | |||
4bf8512a7d | |||
a3a31a2c94 | |||
9ed4c1abb1 | |||
a9c6ea0452 | |||
9e2bbe2e5c | |||
598bff8c49 | |||
eb7c8300fa | |||
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 | |||
3bd40e7271 | |||
8ace3e68f4 | |||
f69feb1def | |||
9fb423d5e0 | |||
|
e9df67a661 | ||
b6871ea13a | |||
|
cb84a7d0d9 | ||
|
25f7540f86 | ||
c29a3fd6d4 | |||
ab48396db9 | |||
51982b63c3 | |||
2b537d27ec | |||
16930e96aa | |||
d63ba4943a | |||
5d22f20ce3 | |||
2451849fd6 | |||
61680d9e87 | |||
850dd75822 | |||
6ba983fae3 | |||
ca34f9c2a1 | |||
0fb5b84392 | |||
39a9c39924 | |||
13692228c6 | |||
f22f4d229d | |||
97dbf42a4d | |||
041a219151 | |||
c137f2f916 | |||
6ef3b09ec9 | |||
263b78e995 | |||
0d87848a7e | |||
44ad0832ba | |||
93ced26e07 | |||
|
363daf6251 | ||
a6b1d1c9e0 | |||
|
576d8c23b3 | ||
c232c6e5f6 | |||
5d6ae52157 | |||
01e6256ed4 | |||
dbc114a529 | |||
4d77af8f0c | |||
c36a925bb5 | |||
605dd035d4 | |||
|
7526ae13d7 | ||
|
0eb41f70d2 | ||
|
a4c7ee738c | ||
8d4602c40d | |||
bb20d9623d | |||
73cc28d1cf | |||
ae54cd923c | |||
9d3c9a7d70 | |||
92a4274be7 | |||
|
4800b3f46c | ||
|
cce593b2f4 | ||
|
c491642713 | ||
|
5b4dd638a4 | ||
8440952167 | |||
4e4227e204 | |||
effb01d43c | |||
28d730a2ca | |||
|
0bd135a5c9 | ||
|
5522391942 | ||
7576355e95 | |||
c9e518f2ed | |||
|
593cf2407b | ||
|
bdb4c76d70 | ||
|
7cce805d39 | ||
b9d3ecc15d | |||
|
ec5eae08f7 | ||
c1edf31577 | |||
5262c04e46 | |||
8b01943d14 | |||
35823be13e | |||
4162ea33ff | |||
1787ef4db7 | |||
38b13611c9 | |||
52fa143e75 | |||
89047a0c8a | |||
9fbded7d6a | |||
c657781599 | |||
ecbe1ff79e | |||
d6811ab9b3 | |||
d45170db3e | |||
6e63768c71 | |||
1424bc2cf6 | |||
051fe3d15d | |||
fcae485071 | |||
2e9a7fdf94 | |||
|
570d7e18a4 | ||
8ac53970a3 | |||
dd3e108e10 | |||
|
c29bd81ffb | ||
|
a1173e4d84 | ||
|
d877e30a05 | ||
33ad54090d | |||
83ee6cfdbd | |||
f48971a0c4 | |||
a4b71aa73c | |||
4b7df662e8 | |||
4cd7b6718b | |||
|
ac2a94dda0 | ||
|
de7e65b32a | ||
1b283261c3 | |||
39cbfc1baa | |||
e563f18d31 | |||
1d03b3f28f | |||
ae0025a203 | |||
b5bd7acb7f | |||
657e7e52ac | |||
d6341c88cd | |||
bdb1191f9e | |||
3b29b67c0b | |||
cc43e53c4b | |||
a25c900d49 | |||
206651da94 | |||
be28116a98 | |||
4cdf29557c | |||
62928e47eb | |||
9b50b719d9 | |||
def60bf298 | |||
6496b6e463 | |||
6ce4c972a0 | |||
70e3c9968a | |||
074882f092 | |||
c2b3018908 | |||
96c85d9dca | |||
e15aafd781 | |||
|
9e17b84a5d | ||
c66d470166 | |||
4deb7d11a1 | |||
6f069f83d4 | |||
fa53df1e8a | |||
ba17f9d159 | |||
b558b51601 | |||
3ea0acd48b | |||
c171873fa6 | |||
1ccf94883a | |||
b26f8e0d24 | |||
5bc2725d1b | |||
8e06244311 | |||
0f35613e50 | |||
4cfad0b5ca | |||
c2593e71c0 | |||
bb331232ca | |||
|
96e8293bf4 | ||
|
e13464cb7c | ||
|
c5f8fbe86d | ||
|
8667706377 | ||
|
86e47ab226 | ||
|
10c383d66a | ||
|
4c65a308d6 | ||
|
246e43317f | ||
|
974b97b744 | ||
|
c914ddc0ee | ||
|
6509b293db | ||
|
a72eeb4c3f | ||
|
309397b72c | ||
|
077658c68d | ||
|
322ad19889 | ||
ab9d3d3d3e | |||
|
06a109d2b5 | ||
|
351743068a | ||
|
4e6532ff0d | ||
|
eaf57f2c33 | ||
|
7abdac7c9c | ||
|
002df66878 | ||
|
251bfaf410 | ||
|
24722447da | ||
|
90e7a90bcf | ||
6d3d3fc52c | |||
e843d790b1 | |||
c90ac1d21a | |||
041ceb81d8 | |||
c6f2a29320 | |||
0532d7592e | |||
f9fe1922d4 | |||
|
88b5b55062 | ||
afe501a06d |
33
.github/workflows/ISSUE_TEMPLATE.md
vendored
@@ -1,33 +0,0 @@
|
||||
---
|
||||
name: BUG 反馈
|
||||
about: 使用轻雪时遇到了问题?
|
||||
---
|
||||
## 问题反馈
|
||||
|
||||
### **描述**
|
||||
请详细描述一下你所遇到的bug
|
||||
|
||||
### **确保**
|
||||
|
||||
- [ ] 我已查阅所有issues,没有相似或已被证实的内容
|
||||
- [ ] 我已按照文档指引进行正确的操作,仍会复现该问题
|
||||
|
||||
### **预期效果**
|
||||
你想要什么样的预期效果
|
||||
|
||||
### **实际效果**
|
||||
实际上是怎么样的
|
||||
|
||||
### **运行环境**
|
||||
- 系统及版本:
|
||||
- Python环境:
|
||||
- commit哈希或版本:
|
||||
- 硬件信息(可选):
|
||||
|
||||
### **运行日志**
|
||||
```
|
||||
将相关日志粘贴到此处
|
||||
```
|
||||
|
||||
### **严重等级**
|
||||
致命|严重|警告|轻微
|
49
.github/workflows/deploy-docs.yml
vendored
@@ -1,49 +0,0 @@
|
||||
|
||||
name: 部署文档
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
# 确保这是你正在使用的分支名称
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-gh-pages:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# 如果你文档需要 Git 子模块,取消注释下一行
|
||||
# submodules: true
|
||||
|
||||
- name: 安装 pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
run_install: true
|
||||
version: 8
|
||||
|
||||
|
||||
- name: 设置 Node.js
|
||||
run: |-
|
||||
cd docs
|
||||
pnpm install
|
||||
|
||||
- name: 构建文档
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=8192
|
||||
run: |-
|
||||
cd docs
|
||||
pnpm run docs:build
|
||||
> .vuepress/dist/.nojekyll
|
||||
|
||||
- name: 部署文档
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
# 这是文档部署到的分支名称
|
||||
branch: gh-pages
|
||||
folder: docs/.vuepress/dist
|
21
.github/workflows/liteyuki-pypi-publish.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Liteyuki PyPI Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
liteyuki-pypi-publish:
|
||||
name: upload release to PyPI (Nightly)
|
||||
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
|
38
.github/workflows/pytest.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Pytest API Testing
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "v7" ]
|
||||
pull_request:
|
||||
branches: [ "v7" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
Pytes-API-Testing:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
uv run pytest --junitxml=report/report.xml
|
||||
|
||||
- name: Archive Pytest test report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SuperTest-test-report
|
||||
path: report
|
||||
|
||||
- name: Upload Pytest report to GitHub
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Pytest-test-report
|
||||
path: report
|
56
.gitignore
vendored
@@ -1,37 +1,27 @@
|
||||
.venv/
|
||||
.idea/
|
||||
.cache/
|
||||
node_modules/
|
||||
data/
|
||||
db/
|
||||
/resources/
|
||||
# python and toolchains
|
||||
.mypy_cache/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.pyw
|
||||
/plugins/
|
||||
_config.yml
|
||||
config.yml
|
||||
config.example.yml
|
||||
compile.bat
|
||||
liteyuki/resources/templates/latest-debug.html
|
||||
# vuepress
|
||||
.github
|
||||
pyproject.toml
|
||||
.pytest_cache/
|
||||
|
||||
test.py
|
||||
line_count.py
|
||||
# idea
|
||||
.idea/
|
||||
.vscode/
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# nuitka
|
||||
main.build/
|
||||
main.dist/
|
||||
main.exe
|
||||
main.cmd
|
||||
docs/.vuepress/.cache/
|
||||
docs/.vuepress/.temp/
|
||||
docs/.vuepress/dist/
|
||||
prompt.txt
|
||||
# platform
|
||||
# macOS
|
||||
**/.DS_Store
|
||||
# windows
|
||||
Thumbs.db
|
||||
# linux
|
||||
|
||||
# js
|
||||
**/echarts.js
|
||||
# development
|
||||
.env
|
||||
.env.*
|
||||
plugins/
|
||||
data/
|
||||
configs/
|
||||
config.yaml
|
||||
config-dev.yaml
|
||||
config-prod.yaml
|
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
21
Dockerfile
@@ -1,19 +1,16 @@
|
||||
FROM python:3.11-bullseye
|
||||
|
||||
ENV TZ Asia/Shanghai
|
||||
|
||||
COPY docker/sources.list /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y git
|
||||
FROM python:3.12-alpine
|
||||
|
||||
WORKDIR /liteyukibot
|
||||
|
||||
COPY . /liteyukibot
|
||||
COPY main.py .
|
||||
COPY pyproject.toml .
|
||||
COPY liteyukibot/ .
|
||||
COPY uv.lock .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
RUN pip install uv
|
||||
|
||||
RUN apt-get install -y libnss3 libnspr4 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libxkbcommon0 libasound2
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
|
||||
EXPOSE 20216
|
||||
RUN uv venv --python 3.12 && uv sync
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
CMD [".venv/bin/python3", "main.py"]
|
21
LICENSE
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Snowykami
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
42
LISENCE
Normal file
@@ -0,0 +1,42 @@
|
||||
LSO License
|
||||
LiteyukiStudio Open Source License
|
||||
|
||||
---
|
||||
|
||||
Copyright © 2025 Liteyuki Studio & Snowykami
|
||||
|
||||
---
|
||||
|
||||
Any individual or organization that obtains a copy of this software is hereby granted, free of charge, the relevant rights under this license agreement.
|
||||
These rights include, but are not limited to, the right to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software.
|
||||
|
||||
This software and its related documentation files (hereinafter referred to as "this software". The "software" includes user manuals, technical documents, API documentation, sample code, etc.) are released in an open - source form on the Internet or other media platforms under this license agreement.
|
||||
Anyone has the right to obtain a copy of this software through proper channels and distribute and/or use it in accordance with this license agreement.
|
||||
|
||||
In case of a conflict with other open - source or non - open - source licenses,
|
||||
unless otherwise specifically stated, all conflicting parts shall be subject to this open - source license agreement.
|
||||
The conflicting parts mainly include:
|
||||
1. Principles of commerciality or profitability
|
||||
2. Legal liability
|
||||
3. Licensed ways of publication and distribution
|
||||
|
||||
During the process of software distribution and dissemination through media or their media exchanges,
|
||||
this license agreement shall be retained by default and distributed and redistributed in the same way. If the distributed project does not include this license agreement, the project can still continue to use this license agreement without additional addition.
|
||||
|
||||
When processing or re - processing the software and its copies for profit purposes,
|
||||
if this license agreement is used, the individual or organization to which the re - processed software belongs can decide on its own to change, add, or delete non - essential license terms.
|
||||
The essential license terms include:
|
||||
1. Distribution of rights and their scope of application
|
||||
2. Disclaimer clause and its final interpretation
|
||||
3. Copyright statement and its legal handling
|
||||
|
||||
However, when obtaining a copy of the software, the following points should still be noted:
|
||||
- The above copyright notice and this license notice must be included in the software copy, and the software and its copies must be used in the same form as the original.
|
||||
- When using the software, the copy must be presented publicly under the same license agreement. The software copy shall not be used for external profit under a non - original license agreement without the permission of the original author.
|
||||
|
||||
---
|
||||
|
||||
The software is provided "as is" without any warranty of any kind, either express or implied,
|
||||
including but not limited to the warranty of merchantability and non - infringement for specific purposes.
|
||||
|
||||
In any case, the author or copyright owner shall not be liable for any claims, damages, or other liabilities arising from the use of the software, whether in contract litigation, infringement litigation, or other forms of litigation. The author and its copyright owner have the right to refuse compensation for any losses caused by the user for personal reasons.
|
90
README.md
@@ -1,52 +1,80 @@
|
||||
<div align="center">
|
||||
|
||||
[//]: # (<img src="https://cdn.liteyuki.icu/static/img/logo.png" style="align-content: center; width: 50%; margin-top:10%;" alt="a">)
|
||||
[![][banner]][lightyuki-link]
|
||||
<h2><a href="https://bot.liteyuki.icu"> <span style="color: #a2d8f4">轻雪</span> <span style="color: #d0e9ff">6</span></a></h2>
|
||||
[//]: # (<img src="https://cdn.liteyuki.org/logos/bot.svg" style="align-content: center; width: 50%; margin-top:10%;" alt="a">)
|
||||
[![][banner]][liteyuki-link]
|
||||
<h2><a href="https://bot.liteyuki.org"> <span style="color: #a2d8f4">轻雪</span> <span style="color: #d0e9ff">7</span></a></h2>
|
||||
<h4> <span style="color: #a2d8f4">✨ 轻量,高效,易于扩展✨</span></h4>
|
||||
|
||||
[![][OneBot]][onebot-link]
|
||||
[![][Nonebot2]][nonebot-link]
|
||||
[![][Liteyuki6.0]][lightyuki-link]
|
||||
[![][Python3.10+]][python-link]
|
||||
[![][Liteyuki7.0]][liteyuki-link]
|
||||
[![][Python3.12+]][python-link]
|
||||
[![][Usage]][usage-link]
|
||||
[![][Repo]][repo-link]
|
||||
[![][Github]][github-link]
|
||||
[![][LiteyukiLab]][liteyukilab-link]
|
||||

|
||||
|
||||
- 基于[Nonebot2](https://github.com/nonebot/nonebot2),有良好的生态支持
|
||||
- 开箱即用,无需复杂配置
|
||||
- 新的点击交互模式,拒绝手打指令
|
||||
- 可视化插件管理包管理,支持一键安装插件
|
||||
- 支持OneBot标准通信但不限于此
|
||||
- 自定义主题支持,满足审美需求
|
||||
- 国际化支持,支持多种语言
|
||||
|
||||
<h3>👇所有内容已迁移至👇</h3>
|
||||
<h2><a href="https://bot.liteyuki.icu">轻雪主页</a></h2>
|
||||
**👇所有内容请访问👇**
|
||||
[bot.liteyuki.org](https://bot.liteyuki.org)
|
||||
</div>
|
||||
|
||||
### 感谢
|
||||
- [Nonebot2](https://nonebot.dev)提供的框架支持
|
||||
- [nonebot-plugin-htmlrender](https://github.com/kexue-z/nonebot-plugin-htmlrender)提供的渲染功能
|
||||
- [nonebot-plugin-alconna](https://github.com/ArcletProject/nonebot-plugin-alconna)提供的命令解析功能
|
||||
> 受限的自由才是真正的自由
|
||||
|
||||
## 关于
|
||||
开发中
|
||||
访问[轻雪7.0](https://bot.liteyuki.org)主页获取更多信息
|
||||
|
||||
## 特点及优势
|
||||
|
||||
- 化繁为简, 加速开发
|
||||
- 轻量级,快速启动
|
||||
- 模块化设计,易于扩展
|
||||
|
||||
## 服务及支持(敬请期待)
|
||||
- 提供Liteyuki Cloud官方的容器化托管服务(SaaS),无需担心服务器问题
|
||||
|
||||
|
||||
[OneBot]: https://img.shields.io/badge/OneBot-11/12-blue?style=for-the-badge
|
||||
[Liteyuki7.0]: https://img.shields.io/badge/Liteyuki-7.0-blue?style=for-the-badge
|
||||
|
||||
[Nonebot2]: https://img.shields.io/badge/Nonebot-2-red?style=for-the-badge
|
||||
[Python3.12+]: https://img.shields.io/badge/Python-3.12+-blue?style=for-the-badge
|
||||
|
||||
[Liteyuki6.0]: https://img.shields.io/badge/Liteyuki-6.0-blue?style=for-the-badge
|
||||
[Usage]: https://img.shields.io/badge/主页-文档-blue?style=for-the-badge
|
||||
|
||||
[Python3.10+]: https://img.shields.io/badge/Python-3.10+-blue?style=for-the-badge
|
||||
[Repo]: https://img.shields.io/badge/官方托管-仓库-blue?style=for-the-badge
|
||||
|
||||
[Usage]: https://img.shields.io/badge/文档-页面-blue?style=for-the-badge
|
||||
[Github]: https://img.shields.io/badge/Github-仓库-blue?style=for-the-badge
|
||||
|
||||
[onebot-link]:https://onebot.dev/
|
||||
[LiteyukiLab]: https://img.shields.io/badge/轻雪社区-官方-blue?style=for-the-badge
|
||||
|
||||
[nonebot-link]:https://nonebot.dev/
|
||||
|
||||
[lightyuki-link]:/
|
||||
|
||||
[python-link]:https://www.python.org/
|
||||
|
||||
[usage-link]:https://bot.liteyuki.icu/
|
||||
[usage-link]:https://bot.liteyuki.org/
|
||||
|
||||
[banner]: https://socialify.git.ci/snowykami/LiteyukiBot/image?description=1&forks=1&issues=1&Plus&pulls=1&stargazers=1&theme=Auto&logo=https%3A%2F%2Fcdn.liteyuki.icu%2Fstatic%2Fimg%2Flogo.png
|
||||
[liteyuki-link]:https://bot.liteyuki.org/
|
||||
|
||||
[repo-link]:https://git.liteyuki.org/bot/app
|
||||
|
||||
[github-link]:https://github.com/LiteyukiStudio/LiteyukiBot
|
||||
|
||||
[liteyukilab-link]:https://lab.liteyuki.org/@LiteyukiBot
|
||||
|
||||
[banner]: https://socialify.git.ci/LiteyukiStudio/LiteyukiBot/image?description=1&forks=1&issues=1&Plus&pulls=1&stargazers=1&theme=Auto&logo=https%3a%2f%2fcdn.liteyuki.org%2flogos%2fbot.svg
|
||||
|
||||
## 开发环境配置
|
||||
|
||||
1. 项目使用uv进行包管理,你也可以使用uv进行环境管理,[安装uv](https://docs.astral.sh/uv/#installation)
|
||||
|
||||
2. 进入项目目录使用uv同步环境和依赖
|
||||
|
||||
```bash
|
||||
uv sync --all # 安装包括dev和prod的所有依赖
|
||||
```
|
||||
|
||||
3. VSCode扩展
|
||||
|
||||
- Python
|
||||
- Mypy
|
||||
- Ruff
|
||||
|
||||
4. 环境变量指定ENVIRONMENT=dev或prod或其他,然后加载.env.{}文件,环境变量
|
12
docker-compose-dev.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
server:
|
||||
container_name: liteyukibot
|
||||
image: liteyukibot:v7
|
||||
restart: always
|
||||
volumes:
|
||||
- ./plugins:/liteyukibot/plugins
|
||||
- ./data:/liteyukibot/data
|
||||
- ./configs:/liteyukibot/configs
|
||||
- ./config.yaml:/liteyukibot/config.yaml
|
||||
ports:
|
||||
- "8090:8080"
|
16
docker-compose-example.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
liteyukibot:
|
||||
container_name: bot
|
||||
# Liteyuki latest: reg.liteyuki.org/bot/app:latest
|
||||
# Liteyuki nightly: reg.liteyuki.org/bot/app:nightly
|
||||
# GHCR latest: ghcr.io/liteyukistudio/bot-app:latest 暂未发布
|
||||
# Docker Hub latest: docker.io/liteyukistudio/bot-app:latest 暂未发布
|
||||
image: reg.liteyuki.org/bot/app:latest
|
||||
restart: always
|
||||
volumes:
|
||||
- ./configs:/liteyukibot/configs # 配置目录,包含配置文件
|
||||
- ./data:/liteyukibot/data # 数据目录,包含下载器安装的插件目录
|
||||
- ./plugins:/liteyukibot/plugins # 外部插件目录,插件开发者可用
|
||||
- ./config.yaml:/liteyukibot/config.yaml # 配置文件,包含所有配置项
|
||||
ports:
|
||||
- "8090:8080"
|
@@ -1,10 +0,0 @@
|
||||
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free
|
||||
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free
|
||||
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free
|
||||
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free
|
||||
|
||||
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free
|
||||
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free
|
||||
|
||||
deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free
|
||||
# deb-src https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free
|
5
docs/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
|
||||
node_modules/
|
||||
./.vuepress/.cache/
|
||||
./.vuepress/.temp/
|
||||
./.vuepress/dist/
|
@@ -1,14 +0,0 @@
|
||||
import {defineClientConfig} from "vuepress/client";
|
||||
import resourceStoreComp from "./components/res_store.vue";
|
||||
import pluginStoreComp from "./components/plugin_store.vue";
|
||||
//导入element-plus
|
||||
import ElementPlus from 'element-plus';
|
||||
|
||||
export default defineClientConfig({
|
||||
enhance: ({app, router, siteData}) => {
|
||||
app.component("resourceStoreComp", resourceStoreComp);
|
||||
app.component("pluginStoreComp", pluginStoreComp);
|
||||
app.use(ElementPlus);
|
||||
|
||||
},
|
||||
});
|
@@ -1,126 +0,0 @@
|
||||
<template>
|
||||
<div class="item-card">
|
||||
<div class="item-name">{{ props.item.name }}</div>
|
||||
<div class="item-description">{{ props.item.desc }}</div>
|
||||
<div class="item-bar">
|
||||
<!-- 三个可点击svg,一个github,一个下载,一个可点击"https://github.com/{{ username }}.png?size=80"个人头像配上id-->
|
||||
<a :href=props.item.homepage class="btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
|
||||
<path fill="currentColor"
|
||||
d="m7.775 3.275l1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0a.751.751 0 0 1 .018-1.042a.751.751 0 0 1 1.042-.018a1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018a.751.751 0 0 1-.018-1.042m-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018a.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0a.751.751 0 0 1-.018 1.042a.751.751 0 0 1-1.042.018a1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- <button class="copy-btn btn"><div @click="copyToClipboard">安装</div></button> 点击后把安装命令写入剪贴板-->
|
||||
<button class="btn copy-btn" @click="copyToClipboard">复制安装命令</button>
|
||||
|
||||
<div class="btn">
|
||||
<a class="author-info" :href="`https://github.com/${props.item.author }`">
|
||||
<img class="icon avatar" :src="`https://github.com/${ props.item.author }.png?size=80`" alt="">
|
||||
<div class="author-name">{{ props.item.author }}</div>
|
||||
</a>
|
||||
</div>
|
||||
<!-- 复制键,复制安装命令,npm install props.item.module_name-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {defineProps, onMounted} from 'vue'
|
||||
import Clipboard from 'clipboard'
|
||||
// 复制安装命令按钮
|
||||
|
||||
// 构建复制成功和失败的提示
|
||||
const props = defineProps({
|
||||
item: Object
|
||||
})
|
||||
|
||||
const copyToClipboard = () => {
|
||||
const clipboard = new Clipboard('.copy-btn', {
|
||||
text: () => `npm install ${props.item.module_name}`
|
||||
})
|
||||
clipboard.on('success', () => {
|
||||
})
|
||||
clipboard.on('error', () => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 复制到剪贴板的函数
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item-card {
|
||||
position: relative;
|
||||
border-radius: 15px;
|
||||
background-color: #00000011;
|
||||
height: 160px;
|
||||
padding: 16px;
|
||||
margin: 10px;
|
||||
box-sizing: border-box;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #00000000;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
color: #111;
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.item-description {
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: $themeColor;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.item-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
color: #00000055;
|
||||
}
|
||||
</style>
|
@@ -1,39 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import ItemCard from './plugin_item_card.vue'
|
||||
|
||||
// 插件商店Nonebot
|
||||
let items = ref([])
|
||||
fetch('https://registry.nonebot.dev/plugins.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
items.value = data
|
||||
})
|
||||
.catch(error => console.error(error))
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>插件商店</h1>
|
||||
<p>所有内容来自<a href="https://nonebot.dev/store/plugins">NoneBot插件商店</a>,在此仅作引用,具体请访问NoneBot插件商店</p>
|
||||
<div class="market">
|
||||
<!-- 布局商品-->
|
||||
<ItemCard v-for="item in items" :key="item.id" :item="item"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
color: #00a6ff;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.market {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<div class="item-card">
|
||||
<div class="item-name">{{ props.item.name }}</div>
|
||||
<div class="item-description">{{ props.item.description }}</div>
|
||||
<div class="item-bar">
|
||||
<!-- 三个可点击svg,一个github,一个下载,一个可点击"https://github.com/{{ username }}.png?size=80"个人头像配上id-->
|
||||
<a :href=props.item.link class="">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16">
|
||||
<path fill="currentColor"
|
||||
d="m7.775 3.275l1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0a.751.751 0 0 1 .018-1.042a.751.751 0 0 1 1.042-.018a1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018a.751.751 0 0 1-.018-1.042m-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018a.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0a.751.751 0 0 1-.018 1.042a.751.751 0 0 1-1.042.018a1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83"/>
|
||||
</svg>
|
||||
</a>
|
||||
<div><a class="author-info" :href="`https://github.com/${props.item.author }`">
|
||||
<img class="icon avatar" :src="`https://github.com/${ props.item.author }.png?size=80`" alt="">
|
||||
<div class="author-name">{{ props.item.author }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {defineProps} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
item: Object
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item-card {
|
||||
position: relative;
|
||||
border-radius: 15px;
|
||||
background-color: #00000011;
|
||||
height: 160px;
|
||||
padding: 16px;
|
||||
margin: 10px;
|
||||
box-sizing: border-box;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
border: 2px solid $themeColor;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
color: $themeColor;
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.item-description {
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: $themeColor;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.item-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
color: #00000055;
|
||||
}
|
||||
</style>
|
@@ -1,38 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import ItemCard from './res_item_card.vue'
|
||||
|
||||
// 从public/assets/resources.json加载插件
|
||||
let items = ref([])
|
||||
fetch('https://bot.liteyuki.icu/assets/resources.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
items.value = data
|
||||
})
|
||||
.catch(error => console.error(error))
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>主题/资源商店</h1>
|
||||
<div class="market">
|
||||
<!-- 布局商品-->
|
||||
<ItemCard v-for="item in items" :key="item.id" :item="item" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
color: #00a6ff;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.market {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
@@ -1,30 +0,0 @@
|
||||
import {defineUserConfig} from "vuepress";
|
||||
import theme from "./theme.js";
|
||||
import viteBundler from "@vuepress/bundler-vite";
|
||||
|
||||
export default defineUserConfig({
|
||||
base: "/",
|
||||
|
||||
lang: "zh-CN",
|
||||
title: "LiteyukiBot 轻雪机器人",
|
||||
description: "LiteyukiBot | 轻雪机器人 | An OneBot Standard ChatBot | 一个OneBot标准的聊天机器人",
|
||||
head: [
|
||||
// 设置 favor.ico,.vuepress/public 下
|
||||
['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'}],
|
||||
|
||||
[
|
||||
"meta",
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no",
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
theme,
|
||||
// 和 PWA 一起启用
|
||||
// shouldPrefetch: false,
|
||||
});
|
@@ -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/",
|
||||
}
|
||||
]);
|
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 9.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path fill="#FDD7AD" d="M512 0 335.448 88.272l-70.616 35.312-70.624 35.312-176.552 88.28v529.648L512 1024l494.344-247.176V247.176z"/><path fill="#CBB292" d="m759.176 370.76-70.624 35.304-494.344-247.168 70.624-35.312zM512 494.344V1024L17.656 776.824V247.176z"/><path fill="#7F6E5D" d="M1006.344 247.168v529.656L512 1024V494.344l176.552-88.28v70.624l141.24-70.624v-70.616z"/><path fill="#7F5B53" d="M829.792 335.448v70.624L688.56 476.68v-70.624z"/><path fill="#CBB292" d="m829.792 335.448-70.624 35.312-494.344-247.176 70.624-35.312z"/><path fill="#2C3E50" d="m682.52 550.32 157.032-78.512a17.656 17.656 0 0 1 25.552 15.792v9.32a52.96 52.96 0 0 1-29.28 47.376L678.8 622.8a17.656 17.656 0 0 1-25.552-15.792v-9.312a52.96 52.96 0 0 1 29.28-47.376z"/></svg>
|
Before Width: | Height: | Size: 854 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024"><defs><linearGradient id="a" x1="522.593" x2="522.593" y1="-70.302" y2="-335.937" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#fe5d5a" stop-opacity=".1"/><stop offset=".908" stop-color="#ef1220" stop-opacity=".5"/></linearGradient><linearGradient id="b" x1="107.12" x2="935.038" y1="-373.67" y2="-373.67" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ff5e59"/><stop offset="1" stop-color="#f01422"/></linearGradient><linearGradient id="c" x1="519.405" x2="519.405" y1="-195.547" y2="-726.816" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ffe2e2"/><stop offset=".888" stop-color="#ff8e8e"/></linearGradient><linearGradient id="d" x1="191.5" x2="483.9" y1="-564.9" y2="-564.9" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#e92700" stop-opacity=".3"/><stop offset=".013" stop-color="#ef1220" stop-opacity=".2"/></linearGradient><linearGradient id="e" x1="403.502" x2="253.121" y1="-847.32" y2="-586.853" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ff5e59"/><stop offset=".201" stop-color="#f01422"/></linearGradient><linearGradient id="f" x1="330.485" x2="330.485" y1="-801.787" y2="-625.789" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ff5e59"/><stop offset=".201" stop-color="#f01422"/></linearGradient><linearGradient id="g" x1="397.351" x2="256.845" y1="-647.231" y2="-890.596" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#ffa6a6"/><stop offset=".908" stop-color="#ff6b5d"/></linearGradient></defs><path fill="url(#a)" d="M501.2 662.3 327.6 763.8c-13.9 8.1-14.2 28.1-.5 36.7l179.1 97.7c10.9 5.9 24.1 5.9 34.9-.1l177-97.9c13.6-8.5 13.4-28.3-.3-36.5l-168.4-101c-14.8-9-33.3-9.1-48.2-.4Z"/><path fill="#f63037" d="m110.2 525.7-3.1 77.6 57.5 18.5L184 519.4Z"/><path fill="url(#b)" d="m476.6 363.5-328 154.6c-21 42.7-55.4 65.4-35.5 103.5 4.2 8 9.4 14.4 15.4 18.1l358.2 195.5c21.8 11.9 48.1 11.8 69.8-.2l354-195.8c27.2-16.9 34.8-90.3 7.3-106.8L573 364.1c-29.7-17.8-66.6-18-96.4-.6Z"/><path fill="url(#c)" d="M476.6 298.7 129.4 501.6c-27.8 16.3-28.4 56.3-1 73.3l358.2 195.5c21.8 11.9 48.1 11.8 69.8-.2l354-195.8c27.2-16.9 26.9-56.6-.6-73.1L573 299.3c-29.7-17.8-66.6-18-96.4-.6Z"/><path fill="#ff8989" fill-opacity=".31" d="m481.2 387.8 39.4 123.4c1.1 3.4 4 6 7.6 6.6l173.4 30.4-33-118.3c-.9-3.3-3.6-5.8-7-6.5l-180.4-35.6ZM327 499.2l40.4 101.1L496.7 525c2.5-1.5 3.7-4.5 2.7-7.3l-36-106.8-127.6 65c-8.6 4.3-12.4 14.4-8.8 23.3ZM523.8 540.5l-140.3 77.2L567.2 659c3.2.7 6.6.1 9.3-1.6l134.6-85-174.7-33.8c-4.3-1-8.7-.3-12.6 1.9Z"/><path fill="url(#d)" d="M483.9 406.1c0 35.46-65.46 64.2-146.2 64.2s-146.2-28.74-146.2-64.2c0-35.46 65.46-64.2 146.2-64.2s146.2 28.74 146.2 64.2Z"/><path fill="url(#e)" d="m254.2 188.4-123 83.1c-1.8 1.3-2.6 3.6-1.8 5.7l39.1 110.6c.6 1.7 2 2.9 3.8 3.2l221.8 40.5c1.3.3 2.7-.1 3.7-.8l131.7-93.6c1.9-1.4 2.6-3.9 1.7-6.1l-49.4-107c-.6-1.5-2.1-2.6-3.7-2.8l-220.3-33.5c-1.3-.2-2.6.1-3.6.7Z"/><path fill="url(#f)" d="m528.6 274.5 3 59.1-205 65.6-177.2-72.7-20-49.2 1.9-54.1Z"/><path fill="url(#g)" d="m250.6 138-112.3 76c-6 4.1-8.5 11.7-6.1 18.5l34.2 96.6c1.9 5.4 6.6 9.3 12.1 10.4l211 38.5c4.3.7 8.6-.2 12.1-2.7l120.5-85.5c6.3-4.4 8.4-12.7 5.3-19.7l-43.1-93.5c-2.2-4.9-6.8-8.3-12.1-9.1L262 135.6c-4-.7-8 .2-11.4 2.4Z"/><path fill="#fff" d="m419.8 252.8-79-11-29-57.7c-3.8-7.6-13.2-10.7-20.8-6.9-7.6 3.8-10.7 13.2-6.9 20.8l26.6 52.9-61.8 42.2c-7.1 4.8-8.9 14.5-4.1 21.5 3 4.4 7.9 6.8 12.8 6.8 3 0 6-.9 8.7-2.7l68-46.4 81.1 11.2c.7.1 1.4.1 2.1.1 7.6 0 14.3-5.6 15.3-13.4 1.4-8.4-4.5-16.2-13-17.4Z"/></svg>
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>
|
Before Width: | Height: | Size: 963 B |
@@ -1 +0,0 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 960 B |
Before Width: | Height: | Size: 9.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1536 1024"><path fill="#1296db" d="M1425.067.256H110.933A110.933 110.933 0 0 0 0 110.848v723.627a110.933 110.933 0 0 0 110.933 110.933h1314.39c61.269 0 110.933-49.75 110.677-110.677V110.848A110.933 110.933 0 0 0 1425.067.256z" class="selected" data-spm-anchor-id="a313x.7781069.0.i4"/><path fill="#FFF" d="M664.747 723.797V435.883L517.12 620.373l-147.456-184.49v288l-148.053-67.158V221.781h147.626l147.627 184.576 147.541-184.576h147.627v565.76z"/><path d="M1024 0h426.667A85.333 85.333 0 0 1 1536 85.333v768a85.333 85.333 0 0 1-85.333 85.334H1024V0z" opacity=".1"/><path fill="#FFF" d="m1256.96 731.307-170.667-216.491h113.75V304.64h113.749v210.176h113.835z" opacity=".5"/></svg>
|
Before Width: | Height: | Size: 771 B |
@@ -1,39 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "KawaiiStatus",
|
||||
"author": "SnowyKami",
|
||||
"description": "可爱的状态卡片,仿照koishi的制作",
|
||||
"link": "https://cdn.liteyuki.icu/static/lrp/KawaiiStatus.zip",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/SnowyKami/CDN/img/KawaiiStatus.png"
|
||||
},
|
||||
{
|
||||
"name": "MiSans字体包",
|
||||
"author": "SnowyKami",
|
||||
"description": "小米官方字体MiSans",
|
||||
"link": "https://cdn.liteyuki.icu/static/lrp/MiSansFonts.zip"
|
||||
},
|
||||
{
|
||||
"name": "MapleMono字体包",
|
||||
"author": "SnowyKami",
|
||||
"description": "适用于字母的字体包",
|
||||
"link": "https://cdn.liteyuki.icu/static/lrp/MapleMonoFonts.zip"
|
||||
},
|
||||
{
|
||||
"name": "野兽先辈主题HomoTheme",
|
||||
"author": "SnowyKami",
|
||||
"description": "野兽先辈主题包,114514!",
|
||||
"link": "https://cdn.liteyuki.icu/static/lrp/HomoTheme.zip"
|
||||
},
|
||||
{
|
||||
"name": "示例包2",
|
||||
"author": "SnowyKami",
|
||||
"description": "A simple bot that shows the status of the bot and the server.",
|
||||
"link": ""
|
||||
},
|
||||
{
|
||||
"name": "示例包3",
|
||||
"author": "SnowyKami",
|
||||
"description": "A simple bot that shows the status of the bot and the server.",
|
||||
"link": ""
|
||||
}
|
||||
]
|
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 92 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" class="icon" viewBox="0 0 3280.944 2800"><path fill="#41b883" d="M1645.332 601.004h375.675L1081.82 2238.478 142.636 601.004h718.477l220.708 379.704 216.013-379.704z"/><path fill="#41b883" d="M142.636 601.004l939.185 1637.474 939.186-1637.474h-375.675l-563.51 982.484-568.208-982.484z"/><path fill="#35495e" d="M513.188 601.004l568.207 987.23 563.511-987.23h-347.498l-216.013 379.704-220.708-379.704zM1607.792 1311.83l594.678 2.293 187.353-316.325-598.662 2.292zM2198.506 1909.57C2867.436 732.7 2939.502 605.426 2937.874 603.78c-.715-.723 45.303-1.314 102.262-1.314s103.562.428 103.562.951c0 .523-208.57 367.978-463.491 816.567L2216.715 2235.6l-102.1.596-102.102.596z"/><path fill="#41b883" d="M1680.563 2233.328c0-1.34 168.208-298.145 440.375-777.048a4135645.775 4135645.775 0 00337.619-594.19l146.13-257.25 170.746-.04 170.747-.04-5.536 9.741c-3.044 5.358-43.727 77.302-90.407 159.875-85.356 150.992-337.562 595.163-656.602 1156.373l-172 302.559-170.536.588c-93.795.322-170.536.069-170.536-.567z"/><path fill="#35495e" d="M1429.783 1625.351l594.679 2.292 187.353-316.324-598.662 2.292z"/><path fill="#41b883" d="M1524.207 1464.903l608.285 6.877 173.746-320.909h-619.072z"/></svg>
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -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",
|
||||
}
|
||||
],
|
||||
});
|
@@ -1,7 +0,0 @@
|
||||
// you can change config here
|
||||
$colors: #c0392b, #d35400, #f39c12, #27ae60, #16a085, #2980b9, #8e44ad, #2c3e50,
|
||||
#7f8c8d !default;
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
// place your custom styles here
|
||||
|
||||
#main-title {
|
||||
font-family: ColorTube, "Fira Code", serif;
|
||||
color: #ff0000 !important; /* 你想要的颜色 */
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ColorTube;
|
||||
src: url("/assets/fonts/ColorTube.woff") format("woff")
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Fira Code", monospace !important;
|
||||
}
|
@@ -1,2 +0,0 @@
|
||||
// you can change colors here
|
||||
$theme-color: #00a6ff;
|
@@ -1,192 +0,0 @@
|
||||
import {hopeTheme} from "vuepress-theme-hope";
|
||||
import navbar from "./navbar.js";
|
||||
import sidebar from "./sidebar.js";
|
||||
|
||||
export default hopeTheme({
|
||||
|
||||
hostname: "https://vuepress-theme-hope-docs-demo.netlify.app",
|
||||
|
||||
author: {
|
||||
name: "远野千束",
|
||||
url: "https://snowykami.me",
|
||||
},
|
||||
|
||||
iconAssets: "fontawesome-with-brands",
|
||||
|
||||
logo: "https://cdn.liteyuki.icu/static/img/liteyuki_icon_640.png",
|
||||
|
||||
repo: "https://github.com/snowykami/LiteyukiBot",
|
||||
|
||||
docsDir: "docs",
|
||||
|
||||
// 导航栏
|
||||
navbar,
|
||||
|
||||
// 侧边栏
|
||||
sidebar,
|
||||
|
||||
// 页脚
|
||||
footer: "LiteyukiBot",
|
||||
displayFooter: true,
|
||||
|
||||
// 加密配置
|
||||
encrypt: {
|
||||
config: {
|
||||
"/demo/encrypt.html": ["1234"],
|
||||
},
|
||||
},
|
||||
|
||||
// 多语言配置
|
||||
metaLocales: {
|
||||
editLink: "在 GitHub 上编辑此页",
|
||||
},
|
||||
|
||||
// 如果想要实时查看任何改变,启用它。注: 这对更新性能有很大负面影响
|
||||
// hotReload: true,
|
||||
|
||||
// 在这里配置主题提供的插件
|
||||
plugins: {
|
||||
search: true,
|
||||
// search: true,
|
||||
comment: {
|
||||
provider: "Giscus",
|
||||
repo: "snowykami/LiteyukiBot",
|
||||
repoId: "R_kgDOHVNKpQ",
|
||||
category: "Announcements",
|
||||
categoryId: "DIC_kwDOHVNKpc4CeWxj",
|
||||
},
|
||||
|
||||
components: {
|
||||
components: ["Badge", "VPCard"],
|
||||
},
|
||||
|
||||
// 此处开启了很多功能用于演示,你应仅保留用到的功能。
|
||||
mdEnhance: {
|
||||
alert: true,
|
||||
align: true,
|
||||
attrs: true,
|
||||
codetabs: true,
|
||||
footnote: true,
|
||||
component: true,
|
||||
demo: true,
|
||||
figure: true,
|
||||
imgLazyload: true,
|
||||
imgSize: true,
|
||||
include: true,
|
||||
mark: true,
|
||||
stylize: [
|
||||
{
|
||||
matcher: "Recommended",
|
||||
replacer: ({tag}) => {
|
||||
if (tag === "em")
|
||||
return {
|
||||
tag: "Badge",
|
||||
attrs: {type: "tip"},
|
||||
content: "Recommended",
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
sub: true,
|
||||
sup: true,
|
||||
tabs: true,
|
||||
vPre: true,
|
||||
|
||||
|
||||
// 在启用之前安装 chart.js
|
||||
// chart: true,
|
||||
|
||||
// insert component easily
|
||||
|
||||
// 在启用之前安装 echarts
|
||||
// echarts: true,
|
||||
|
||||
// 在启用之前安装 flowchart.ts
|
||||
// flowchart: true,
|
||||
|
||||
// gfm requires mathjax-full to provide tex support
|
||||
// gfm: true,
|
||||
|
||||
// 在启用之前安装 katex
|
||||
// katex: true,
|
||||
|
||||
// 在启用之前安装 mathjax-full
|
||||
// mathjax: true,
|
||||
|
||||
// 在启用之前安装 mermaid
|
||||
// mermaid: true,
|
||||
|
||||
// playground: {
|
||||
// presets: ["ts", "vue"],
|
||||
// },
|
||||
|
||||
// 在启用之前安装 reveal.js
|
||||
// revealJs: {
|
||||
// plugins: ["highlight", "math", "search", "notes", "zoom"],
|
||||
// },
|
||||
|
||||
// 在启用之前安装 @vue/repl
|
||||
// vuePlayground: true,
|
||||
|
||||
// install sandpack-vue3 before enabling it
|
||||
// sandpack: true,
|
||||
},
|
||||
|
||||
// 如果你需要 PWA。安装 @vuepress/plugin-pwa 并取消下方注释
|
||||
// pwa: {
|
||||
// favicon: "/favicon.ico",
|
||||
// cacheHTML: true,
|
||||
// cachePic: true,
|
||||
// appendBase: true,
|
||||
// apple: {
|
||||
// icon: "/assets/icon/apple-icon-152.png",
|
||||
// statusBarColor: "black",
|
||||
// },
|
||||
// msTile: {
|
||||
// image: "/assets/icon/ms-icon-144.png",
|
||||
// color: "#ffffff",
|
||||
// },
|
||||
// manifest: {
|
||||
// icons: [
|
||||
// {
|
||||
// src: "/assets/icon/chrome-mask-512.png",
|
||||
// sizes: "512x512",
|
||||
// purpose: "maskable",
|
||||
// type: "image/png",
|
||||
// },
|
||||
// {
|
||||
// src: "/assets/icon/chrome-mask-192.png",
|
||||
// sizes: "192x192",
|
||||
// purpose: "maskable",
|
||||
// type: "image/png",
|
||||
// },
|
||||
// {
|
||||
// src: "/assets/icon/chrome-512.png",
|
||||
// sizes: "512x512",
|
||||
// type: "image/png",
|
||||
// },
|
||||
// {
|
||||
// src: "/assets/icon/chrome-192.png",
|
||||
// sizes: "192x192",
|
||||
// type: "image/png",
|
||||
// },
|
||||
// ],
|
||||
// shortcuts: [
|
||||
// {
|
||||
// name: "Demo",
|
||||
// short_name: "Demo",
|
||||
// url: "/demo/",
|
||||
// icons: [
|
||||
// {
|
||||
// src: "/assets/icon/guide-maskable.png",
|
||||
// sizes: "192x192",
|
||||
// purpose: "maskable",
|
||||
// type: "image/png",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
},
|
||||
});
|
100
docs/README.md
@@ -1,100 +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>npm</code>,无需命令行操作即可安装/卸载插件
|
||||
|
||||
- title: 点击交互
|
||||
icon: mouse-pointer
|
||||
details: 新的点击交互模式,拒绝手打指令
|
||||
|
||||
- title: 主题支持
|
||||
icon: paint-brush
|
||||
details: 支持多种主题,可自定义资源包,满足你的审美需求
|
||||
link: https://bot.liteyuki.icu/usage/resource_pack.html
|
||||
|
||||
- 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: OneBot标准
|
||||
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: 项目遵循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
|
||||
---
|
@@ -1,8 +0,0 @@
|
||||
---
|
||||
title: 项目部署
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category: 部署
|
||||
---
|
||||
|
||||
<Catalog />
|
@@ -1,61 +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,建议开启
|
||||
# 开发者选项
|
||||
log_level: "INFO" # 日志等级
|
||||
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
|
||||
auto_report: true # 是否自动上报问题给轻雪服务器
|
||||
auto_update: true # 是否自动更新轻雪,每天4点检查更新
|
||||
debug: false # 轻雪调试,开启后在调试时修改代码或资源会自动重载相应内容
|
||||
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支持的任何适配器
|
@@ -1,58 +0,0 @@
|
||||
---
|
||||
title: 答疑
|
||||
icon: question
|
||||
order: 3
|
||||
category: 使用指南
|
||||
tag:
|
||||
- 配置
|
||||
- 部署
|
||||
---
|
||||
|
||||
## **常见问题**
|
||||
|
||||
- 设备上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)提供的字体,且遵守了相关字体开源协议
|
@@ -1,50 +0,0 @@
|
||||
---
|
||||
title: 安装
|
||||
icon: download
|
||||
order: 1
|
||||
category: 使用指南
|
||||
tag:
|
||||
- 安装
|
||||
---
|
||||
|
||||
|
||||
|
||||
## **开始安装**
|
||||
### **常规方法**
|
||||
1. 安装 [`Git`](https://git-scm.com/download/) 和 [`Python3.10+`](https://www.python.org/downloads/release/python-31010/) 环境
|
||||
2. 克隆项目 `git clone https://github.com/snowykami/LiteyukiBot`
|
||||
3. 进入轻雪目录 `cd LiteyukiBot`
|
||||
4. 安装依赖 `pip install -r requirements.txt`
|
||||
5. 启动 `python main.py`
|
||||
|
||||
### **使用Docker(测试中)**
|
||||
1. 安装 [`Docker`](https://docs.docker.com/get-docker/)
|
||||
2. 克隆项目 `git clone https://github.com/snowykami/LiteyukiBot`
|
||||
3. 进入轻雪目录 `cd LiteyukiBot`
|
||||
4. 构建镜像 `docker build -t liteyukibot .`
|
||||
5. 启动容器 `docker run -p 20216:20216 -v $(pwd):/liteyukibot -v $(pwd)/.cache:/root/.cache liteyukibot`
|
||||
|
||||
|
||||
> [!tip]
|
||||
> Windows请使用项目绝对目录`/path/to/LiteyukiBot`代替`$(pwd)` <br>
|
||||
> 若你修改了端口号请将`20216:20216`中的`20216`替换为你的端口号
|
||||
|
||||
## **设备要求**
|
||||
- Windows系统版本最低`Windows10+`/`Windows Server 2019+`
|
||||
- Linux系统要支持Python3.10+,推荐`Ubuntu 20.04+`(~~别用你那b CentOS~~)
|
||||
- CPU: 至少`1vCPU`
|
||||
- 内存: Bot无其他插件会占用`200~300MB`,其他插件占用视具体插件而定,建议`1GB`以上
|
||||
- 硬盘: 至少`1GB`空间
|
||||
|
||||
> [!warning]
|
||||
> 如果设备上有多个环境,请使用`path/to/python -m pip install -r requirements.txt`来安装依赖,`path/to/python`为你的Python可执行文件路径
|
||||
|
||||
> [!tip]
|
||||
> 推荐使用虚拟环境来运行轻雪,以避免依赖冲突,你可以使用`python -m venv venv`来创建虚拟环境,然后使用`venv\Scripts\activate`来激活虚拟环境
|
||||
|
||||
> [!warning]
|
||||
> 轻雪的更新功能依赖Git,如果你没有安装Git,你将无法使用更新功能
|
||||
|
||||
#### 其他问题请移步至[答疑](/deployment/fandq)
|
||||
|
||||
[//]: # (#### 想在Linux命令行中拥有更好的体验?试试[TRSS_Liteyuki轻雪机器人管理脚本](https://timerainstarsky.github.io/TRSS_Liteyuki/),该功能仅供参考,不是LiteyukiBot官方提供的功能)
|
106
docs/foolsday.md
@@ -1,106 +0,0 @@
|
||||
---
|
||||
home: true
|
||||
icon: home
|
||||
title: 首页
|
||||
heroImage: https://cdn.liteyuki.icu/static/img/logo.png
|
||||
bgImage:
|
||||
bgImageDark:
|
||||
bgImageStyle:
|
||||
background-attachment: fixed
|
||||
heroText: HeavyyukiBot666 # LiteyukiBot 6
|
||||
tagline: 重雪机器人,一个以笨重和复杂为设计理念基于Koishi114514的OneBotv1919810标准聊天机器人,可用于雪地清扫,使用Typethon编写
|
||||
#tagline: 轻雪机器人,一个以轻量和简洁为设计理念基于Nonebot2的OneBot标准聊天机器人
|
||||
|
||||
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>npmx和pip</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: 安装 Git 和 node.js+
|
||||
- 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>npm install -r requirements.txt</code> 安装项目依赖。
|
||||
details: 如果你有多个 node.js 环境,请使用 <code>pythonx -m npm install -r requirements.txt</code>。
|
||||
- title: 使用 <code>node main.py</code> 启动项目。
|
||||
copyright: © 2021-2024 SnowyKami All Rights Reserved
|
||||
---
|
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "liteyuki",
|
||||
"version": "2.0.0",
|
||||
"description": "A project of vuepress-theme-hope",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"docs:build": "vuepress-vite build .",
|
||||
"docs:clean-dev": "vuepress-vite dev . --clean-cache",
|
||||
"docs:dev": "vuepress-vite dev .",
|
||||
"docs:update-package": "pnpm dlx vp-update"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vuepress/bundler-vite": "2.0.0-rc.9",
|
||||
"@vuepress/plugin-search": "2.0.0-rc.24",
|
||||
"vue": "^3.4.21",
|
||||
"vuepress": "2.0.0-rc.9",
|
||||
"vuepress-plugin-search-pro": "2.0.0-rc.34",
|
||||
"vuepress-theme-hope": "2.0.0-rc.32"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.11",
|
||||
"element-plus": "^2.7.0",
|
||||
"element-ui": "^2.15.14"
|
||||
}
|
||||
}
|
3180
docs/pnpm-lock.yaml
generated
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: 资源商店
|
||||
icon: store
|
||||
index: false
|
||||
---
|
@@ -1,8 +0,0 @@
|
||||
---
|
||||
title: 插件商店
|
||||
icon: plug
|
||||
order: 2
|
||||
category: 使用手册
|
||||
---
|
||||
|
||||
<pluginStoreComp />
|
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: 资源商店
|
||||
icon: box
|
||||
order: 1
|
||||
category: 使用手册
|
||||
---
|
||||
<resourceStoreComp />
|
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "ES2022",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"./.vuepress/**/*.ts",
|
||||
"./.vuepress/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
---
|
||||
title: 使用手册
|
||||
index: false
|
||||
icon: laptop-code
|
||||
category: 使用手册
|
||||
---
|
||||
|
||||
<Catalog />
|
@@ -1,16 +0,0 @@
|
||||
---
|
||||
title: 用户协议
|
||||
icon: user-secret
|
||||
order: 3
|
||||
category: 使用手册
|
||||
---
|
||||
|
||||
1. 本项目遵循`MIT`协议,你可以自由使用,修改,分发,但是请保留原作者信息
|
||||
2. 你可以选择开启`auto_report`(默认开启),轻雪会收集以下内容
|
||||
- 运行环境的设备信息:CPU,内存,系统信息,Python信息
|
||||
- 插件信息(不含插件数据),
|
||||
- 部分异常信息,
|
||||
- 会话负载信息(不含隐私部分)
|
||||
以上内容仅用于项目的优化,不包含任何隐私信息,且通过安全的方式传输到轻雪的服务器,若你不希望提供这些信息,可以在配置文件中把`auto_report`设定为`false`
|
||||
3. 本项目不会收集用户的任何隐私信息,但请注意甄别第三方插件的安全性
|
||||
4. 使用此项目代表你已经同意以上协议
|
@@ -1,102 +0,0 @@
|
||||
---
|
||||
title: 基础命令
|
||||
icon: comment
|
||||
order: 1
|
||||
category: 使用手册
|
||||
---
|
||||
|
||||
## 基础插件
|
||||
|
||||
### **轻雪 `liteyuki`**
|
||||
|
||||
```shell
|
||||
# 仅超级用户
|
||||
reload-liteyuki # 重载轻雪
|
||||
update-liteyuki # 更新轻雪
|
||||
liteecho # 查看当前bot
|
||||
status # 查看统计信息和状态
|
||||
config set <key> value # 添加配置项,若存在则会覆盖,输入值会被执行以转换为正确的类型,"10"和10是不一样的
|
||||
config get [key] # 查询配置项,不带key返回配置项列表,推荐私聊使用
|
||||
switch-image-mode # 在普通图片和Markdown大图之间切换,该功能需要commit:505468b及以后的Lagrange.OneBot,
|
||||
/api api_name [args] # 调用机器人API,例如/api get_group_member_list group_id=1234567,空格用%20
|
||||
# 仅超级用户,群聊仅群主、管理员、超级用户可用
|
||||
group enable/disable [group_id] # 在群聊启用/停用机器人,group_id仅超级用户可用
|
||||
# 所有人可用
|
||||
liteyuki-docs # 查看轻雪文档
|
||||
```
|
||||
|
||||
命令别名
|
||||
|
||||
```shell
|
||||
status 状态,
|
||||
reload-liteyuki 重启轻雪,
|
||||
update-liteyuki 更新轻雪,
|
||||
reload-resources 重载资源,
|
||||
config 配置 | set 设置 | get 查询,
|
||||
switch-image-mode 切换图片模式,
|
||||
liteyuki-docs 轻雪文档
|
||||
group 群聊 | enable 启用 | disable 停用
|
||||
```
|
||||
|
||||
### **插件/包管理器 `liteyuki_pacman`**
|
||||
|
||||
- 插件管理
|
||||
|
||||
```shell
|
||||
# 仅超级用户
|
||||
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> # 当前会话启用/停用插件
|
||||
npm list [page] [num] # 列出所有插件 page为页数,num为每页显示数量
|
||||
|
||||
# 所有人
|
||||
help <plugin_name> # 查看插件帮助
|
||||
```
|
||||
|
||||
- 资源包管理
|
||||
|
||||
```shell
|
||||
# 仅超级用户
|
||||
rpm list [page] [num] # 列出所有资源包 page为页数,num为每页显示数量
|
||||
rpm load <pack_name> # 加载资源包
|
||||
rpm unload <pack_name> # 卸载资源包
|
||||
rpm change <pack_name> # 修改优先级
|
||||
rpm reload # 重载所有资源包
|
||||
```
|
||||
|
||||
命令别名
|
||||
|
||||
```shell
|
||||
npm 插件管理 | update 更新 | install 安装 | uninstall 卸载 | search 搜索
|
||||
enable 启用 | disable 停用 | enable-global 全局启用 | disable-global 全局停用
|
||||
rpm 资源包 | load 加载 | unload 卸载 | change 更改 | reload 重载 | list 列表
|
||||
help 帮助
|
||||
```
|
||||
|
||||
> [!warning]
|
||||
> 受限于NoneBot2钩子函数的依赖注入参数,插件停用只能阻断传入响应,对于主动推送的插件不生效,请阅读插件主页的说明。
|
||||
|
||||
### **用户管理`liteyuki_user`**
|
||||
|
||||
```shell
|
||||
# 所有人可用
|
||||
profile # 查看用户信息菜单
|
||||
profile set <key> [value] # 设置用户信息或打开属性设置菜单
|
||||
profile get <key> # 获取用户信息
|
||||
```
|
||||
|
||||
命令别名
|
||||
|
||||
```shell
|
||||
profile 个人信息 | set 设置 | get 查询
|
||||
```
|
||||
|
||||
> [!tip]
|
||||
> **参数**:`<param>`为必填参数,`[option]`为可选参数。
|
||||
>
|
||||
> **命令别名**:配置了命令别名的命令可以使用别名代替原命令,例如`npm install ~`可以使用`插件 安装 ~`代替。
|
@@ -1,29 +0,0 @@
|
||||
---
|
||||
title: 功能命令
|
||||
icon: comment
|
||||
order: 2
|
||||
category: 使用手册
|
||||
---
|
||||
|
||||
## 功能插件命令
|
||||
|
||||
### **轻雪天气`liteyuki_weather`**
|
||||
|
||||
配置项
|
||||
|
||||
```yaml
|
||||
weather_key: "" # 和风天气的天气key,会自动判断key版本
|
||||
```
|
||||
|
||||
命令
|
||||
|
||||
```shell
|
||||
weather <keywords...> # 查询目标地实时天气,例如:"天气 北京 海淀", "weather Tokyo Shinjuku"
|
||||
bind-city <keywords...> # 绑定查询城市,个人全局生效
|
||||
```
|
||||
|
||||
命令别名
|
||||
|
||||
```shell
|
||||
weather 天气, bind-city 绑定城市
|
||||
```
|
@@ -1,45 +0,0 @@
|
||||
---
|
||||
title: 轻雪API
|
||||
icon: code
|
||||
order: 4
|
||||
category: 使用指南
|
||||
tag:
|
||||
- 配置
|
||||
- 部署
|
||||
---
|
||||
|
||||
## **轻雪API**
|
||||
|
||||
轻雪API是轻雪运行中部分服务的支持,由`go`语言编写,例如错误反馈,图床链接等,目前服务由轻雪服务器提供,用户无需额外部署
|
||||
|
||||
接口
|
||||
|
||||
- `url` `https://api.liteyuki.icu`
|
||||
|
||||
- `POST` `/register` 注册一个Bot
|
||||
- 参数
|
||||
- `name` `string` Bot名称
|
||||
- `version` `string` Bot版本
|
||||
- `version_id` `int` Bot版本ID
|
||||
- `python` `string` Python版本
|
||||
- `os` `string` 操作系统
|
||||
- 返回
|
||||
- `code` `int` 状态码
|
||||
- `liteyuki_id` `string` 轻雪ID
|
||||
|
||||
- `POST` `/bug_report` 上报错误
|
||||
- 参数
|
||||
- `liteyuki_id` `string` 轻雪ID
|
||||
- `content` `string` 错误信息
|
||||
- `device_info` `string` 设备信息
|
||||
- 返回
|
||||
- `code` `int` 状态码
|
||||
- `report_id` `string` 错误ID
|
||||
|
||||
- `POST` `/upload_image` 图床上传
|
||||
- 参数
|
||||
- `image` `file` 图片文件
|
||||
- `liteyuki_id` `string` 轻雪ID,用于鉴权
|
||||
- 返回
|
||||
- `code` `int` 状态码
|
||||
- `url` `string` 图床链接
|
@@ -1,39 +0,0 @@
|
||||
---
|
||||
title: 资源包
|
||||
icon: paint-brush
|
||||
order: 3
|
||||
category: 使用手册
|
||||
---
|
||||
|
||||
## 简介
|
||||
资源包,亦可根据用途称为主题包、字体包、语言包等,它允许你一定程度上自定义轻雪的外观,并且不用修改源代码
|
||||
- [资源/主题商店](/store/)提供了一些资源包供你选择,你也可以自己制作资源包
|
||||
- 资源包的制作很简单,如果你接触过`Minecraft`的资源包,那么你能够很快就上手,仅需按照原有路径进行文件替换即可,讲起打包成一个新的资源包。
|
||||
- 部分内容制作需要一点点前端基础,例如`html`,`css`
|
||||
- 轻雪原版资源包请查看`LiteyukiBot/liteyuki/resources`,可以在此基础上进行修改
|
||||
- 欢迎各位投稿资源包到轻雪资源商店
|
||||
|
||||
## 加载资源包
|
||||
- 资源包通常是以`.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
|
@@ -1,35 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from .core import *
|
||||
from .loader import *
|
||||
from .dev_tools import *
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="轻雪核心插件",
|
||||
description="轻雪主程序插件,包含了许多初始化的功能",
|
||||
usage="",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki" : True,
|
||||
"toggleable": False,
|
||||
}
|
||||
)
|
||||
|
||||
from ..utils.base.language import Language, get_default_lang_code
|
||||
|
||||
print("\033[34m" + r"""
|
||||
__ ______ ________ ________ __ __ __ __ __ __ ______
|
||||
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
|
||||
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
|
||||
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
|
||||
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
|
||||
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
|
||||
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
|
||||
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
|
||||
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
|
||||
""" + "\033[0m")
|
||||
|
||||
sys_lang = Language(get_default_lang_code())
|
||||
nonebot.logger.info(sys_lang.get("main.current_language", LANG=sys_lang.get("language.name")))
|
||||
# nonebot.logger.info(sys_lang.get("main.enable_webdash", URL=f"http://127.0.0.1:{config.get('port', 20216)}"))
|
@@ -1,41 +0,0 @@
|
||||
import nonebot
|
||||
from git import Repo
|
||||
|
||||
remote_urls = [
|
||||
"https://github.com/snowykami/LiteyukiBot.git",
|
||||
"https://gitee.com/snowykami/LiteyukiBot.git"
|
||||
]
|
||||
|
||||
|
||||
def detect_update() -> bool:
|
||||
# 对每个远程仓库进行检查,只要有一个仓库有更新,就返回True
|
||||
for remote_url in remote_urls:
|
||||
repo = Repo(".")
|
||||
repo.remotes.origin.set_url(remote_url)
|
||||
repo.remotes.origin.fetch()
|
||||
if repo.head.commit != repo.commit('origin/main'):
|
||||
return True
|
||||
|
||||
|
||||
def update_liteyuki() -> tuple[bool, str]:
|
||||
"""更新轻雪
|
||||
:return: 是否更新成功,更新变动"""
|
||||
|
||||
new_commit_detected = detect_update()
|
||||
if new_commit_detected:
|
||||
repo = Repo(".")
|
||||
logs = ""
|
||||
# 对每个远程仓库进行更新
|
||||
for remote_url in remote_urls:
|
||||
try:
|
||||
logs += f"\nremote: {remote_url}"
|
||||
repo.remotes.origin.set_url(remote_url)
|
||||
repo.remotes.origin.pull()
|
||||
diffs = repo.head.commit.diff("origin/main")
|
||||
for diff in diffs.iter_change_type('M'):
|
||||
logs += f"\n{diff.a_path}"
|
||||
return True, logs
|
||||
except:
|
||||
continue
|
||||
else:
|
||||
return False, "Nothing Changed"
|
@@ -1,324 +0,0 @@
|
||||
import base64
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import nonebot
|
||||
import pip
|
||||
from nonebot import Bot, get_driver, require
|
||||
from nonebot.adapters.onebot.v11 import Message, escape, unescape
|
||||
from nonebot.exception import MockApiException
|
||||
from nonebot.internal.matcher import Matcher
|
||||
from nonebot.permission import SUPERUSER
|
||||
|
||||
from liteyuki.utils.base.config import get_config, load_from_yaml
|
||||
from liteyuki.utils.base.data_manager import StoredConfig, TempConfig, common_db
|
||||
from liteyuki.utils.base.language import get_user_lang
|
||||
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||
from liteyuki.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
|
||||
from liteyuki.utils.base.reloader import Reloader
|
||||
from .api import update_liteyuki
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
require("nonebot_plugin_apscheduler")
|
||||
from nonebot_plugin_alconna import UniMessage, on_alconna, Alconna, Args, Subcommand, Arparma, MultiVar
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
markdown_image = common_db.first(StoredConfig(), default=StoredConfig()).config.get("markdown_image", False)
|
||||
|
||||
|
||||
@on_alconna(
|
||||
command=Alconna(
|
||||
"liteecho",
|
||||
Args["text", str, ""],
|
||||
),
|
||||
permission=SUPERUSER
|
||||
).handle()
|
||||
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
|
||||
if result.main_args.get("text"):
|
||||
await matcher.finish(Message(unescape(result.main_args.get("text"))))
|
||||
else:
|
||||
await matcher.finish(f"Hello, Liteyuki!\nBot {bot.self_id}")
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"更新轻雪"},
|
||||
command=Alconna(
|
||||
"update-liteyuki"
|
||||
),
|
||||
permission=SUPERUSER
|
||||
).handle()
|
||||
async def _(bot: T_Bot, event: T_MessageEvent):
|
||||
# 使用git pull更新
|
||||
ulang = get_user_lang(str(event.user_id))
|
||||
success, logs = update_liteyuki()
|
||||
reply = "Liteyuki updated!\n"
|
||||
reply += f"```\n{logs}\n```\n"
|
||||
btn_restart = md.btn_cmd(ulang.get("liteyuki.restart_now"), "reload-liteyuki")
|
||||
pip.main(["install", "-r", "requirements.txt"])
|
||||
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}"
|
||||
await md.send_md(reply, bot, event=event, at_sender=False)
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"重启轻雪"},
|
||||
command=Alconna(
|
||||
"reload-liteyuki"
|
||||
),
|
||||
permission=SUPERUSER
|
||||
).handle()
|
||||
async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
|
||||
await matcher.send("Liteyuki reloading")
|
||||
temp_data = common_db.first(TempConfig(), default=TempConfig())
|
||||
|
||||
temp_data.data.update(
|
||||
{
|
||||
"reload" : True,
|
||||
"reload_time" : time.time(),
|
||||
"reload_bot_id" : bot.self_id,
|
||||
"reload_session_type": event.message_type,
|
||||
"reload_session_id" : event.group_id if event.message_type == "group" else event.user_id,
|
||||
"delta_time" : 0
|
||||
}
|
||||
)
|
||||
|
||||
common_db.save(temp_data)
|
||||
Reloader.reload(0)
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"配置"},
|
||||
command=Alconna(
|
||||
"config",
|
||||
Subcommand(
|
||||
"set",
|
||||
Args["key", str]["value", Any],
|
||||
alias=["设置"],
|
||||
|
||||
),
|
||||
Subcommand(
|
||||
"get",
|
||||
Args["key", str, None],
|
||||
alias=["查询", "获取"]
|
||||
),
|
||||
Subcommand(
|
||||
"remove",
|
||||
Args["key", str],
|
||||
alias=["删除"]
|
||||
)
|
||||
),
|
||||
permission=SUPERUSER
|
||||
).handle()
|
||||
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, matcher: Matcher):
|
||||
ulang = get_user_lang(str(event.user_id))
|
||||
stored_config: StoredConfig = common_db.first(StoredConfig(), default=StoredConfig())
|
||||
if result.subcommands.get("set"):
|
||||
key, value = result.subcommands.get("set").args.get("key"), result.subcommands.get("set").args.get("value")
|
||||
try:
|
||||
value = eval(value)
|
||||
except:
|
||||
pass
|
||||
stored_config.config[key] = value
|
||||
common_db.save(stored_config)
|
||||
await matcher.finish(f"{ulang.get('liteyuki.config_set_success', KEY=key, VAL=value)}")
|
||||
elif result.subcommands.get("get"):
|
||||
key = result.subcommands.get("get").args.get("key")
|
||||
file_config = load_from_yaml("config.yml")
|
||||
reply = f"{ulang.get('liteyuki.current_config')}"
|
||||
if key:
|
||||
reply += f"```dotenv\n{key}={file_config.get(key, stored_config.config.get(key))}\n```"
|
||||
else:
|
||||
reply = f"{ulang.get('liteyuki.current_config')}"
|
||||
reply += f"\n{ulang.get('liteyuki.static_config')}\n```dotenv"
|
||||
for k, v in file_config.items():
|
||||
reply += f"\n{k}={v}"
|
||||
reply += "\n```"
|
||||
if len(stored_config.config) > 0:
|
||||
reply += f"\n{ulang.get('liteyuki.stored_config')}\n```dotenv"
|
||||
for k, v in stored_config.config.items():
|
||||
reply += f"\n{k}={v} {type(v)}"
|
||||
reply += "\n```"
|
||||
await md.send_md(reply, bot, event=event)
|
||||
elif result.subcommands.get("remove"):
|
||||
key = result.subcommands.get("remove").args.get("key")
|
||||
if key in stored_config.config:
|
||||
stored_config.config.pop(key)
|
||||
common_db.save(stored_config)
|
||||
await matcher.finish(f"{ulang.get('liteyuki.config_remove_success', KEY=key)}")
|
||||
else:
|
||||
await matcher.finish(f"{ulang.get('liteyuki.invalid_command', TEXT=key)}")
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"切换图片模式"},
|
||||
command=Alconna(
|
||||
"switch-image-mode"
|
||||
),
|
||||
permission=SUPERUSER
|
||||
).handle()
|
||||
async def _(event: T_MessageEvent, matcher: Matcher):
|
||||
global markdown_image
|
||||
# 切换图片模式,False以图片形式发送,True以markdown形式发送
|
||||
ulang = get_user_lang(str(event.user_id))
|
||||
stored_config: StoredConfig = common_db.first(StoredConfig(), default=StoredConfig())
|
||||
stored_config.config["markdown_image"] = not stored_config.config.get("markdown_image", False)
|
||||
markdown_image = stored_config.config["markdown_image"]
|
||||
common_db.save(stored_config)
|
||||
await matcher.finish(ulang.get("liteyuki.image_mode_on" if stored_config.config["markdown_image"] else "liteyuki.image_mode_off"))
|
||||
|
||||
|
||||
@on_alconna(
|
||||
command=Alconna(
|
||||
"liteyuki-docs",
|
||||
),
|
||||
aliases={"轻雪文档"},
|
||||
).handle()
|
||||
async def _(matcher: Matcher):
|
||||
await matcher.finish("https://bot.liteyuki.icu/usage")
|
||||
|
||||
|
||||
@on_alconna(
|
||||
command=Alconna(
|
||||
"/api",
|
||||
Args["api", str]["args", MultiVar(str), ()],
|
||||
),
|
||||
permission=SUPERUSER
|
||||
).handle()
|
||||
async def _(result: Arparma, bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
|
||||
"""
|
||||
调用API
|
||||
Args:
|
||||
result:
|
||||
bot:
|
||||
event:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
api_name = result.main_args.get("api")
|
||||
args: tuple[str] = result.main_args.get("args", ()) # 类似于url参数,但每个参数间用空格分隔,空格是%20
|
||||
args_dict = {}
|
||||
|
||||
for arg in args:
|
||||
key, value = arg.split("=", 1)
|
||||
args_dict[key] = unescape(value.replace("%20", " "))
|
||||
|
||||
if api_name in need_user_id and "user_id" not in args_dict:
|
||||
args_dict["user_id"] = str(event.user_id)
|
||||
if api_name in need_group_id and "group_id" not in args_dict and event.message_type == "group":
|
||||
args_dict["group_id"] = str(event.group_id)
|
||||
|
||||
if "message" in args_dict:
|
||||
args_dict["message"] = Message(args_dict["message"])
|
||||
|
||||
try:
|
||||
result = await bot.call_api(api_name, **args_dict)
|
||||
except Exception as e:
|
||||
result = str(e)
|
||||
|
||||
args_show = "\n".join("- %s: %s" % (k, v) for k, v in args_dict.items())
|
||||
print(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
|
||||
await matcher.finish(f"API: {api_name}\n\nArgs: \n{args_show}\n\nResult: {result}")
|
||||
|
||||
|
||||
# system hook
|
||||
@Bot.on_calling_api # 图片模式检测
|
||||
async def test_for_md_image(bot: T_Bot, api: str, data: dict):
|
||||
# 截获大图发送,转换为markdown发送
|
||||
if api in ["send_msg", "send_private_msg", "send_group_msg"] and markdown_image and data.get("user_id") != bot.self_id:
|
||||
if api == "send_msg" and data.get("message_type") == "private" or api == "send_private_msg":
|
||||
session_type = "private"
|
||||
session_id = data.get("user_id")
|
||||
elif api == "send_msg" and data.get("message_type") == "group" or api == "send_group_msg":
|
||||
session_type = "group"
|
||||
session_id = data.get("group_id")
|
||||
else:
|
||||
return
|
||||
if len(data.get("message", [])) == 1 and data["message"][0].get("type") == "image":
|
||||
file: str = data["message"][0].data.get("file")
|
||||
# file:// http:// base64://
|
||||
if file.startswith("http"):
|
||||
result = await md.send_md(await md.image_async(file), bot, message_type=session_type, session_id=session_id)
|
||||
elif file.startswith("file"):
|
||||
file = file.replace("file://", "")
|
||||
result = await md.send_image(open(file, "rb").read(), bot, message_type=session_type, session_id=session_id)
|
||||
elif file.startswith("base64"):
|
||||
file_bytes = base64.b64decode(file.replace("base64://", ""))
|
||||
result = await md.send_image(file_bytes, bot, message_type=session_type, session_id=session_id)
|
||||
else:
|
||||
return
|
||||
raise MockApiException(result=result)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def on_startup():
|
||||
temp_data = common_db.first(TempConfig(), default=TempConfig())
|
||||
# 储存重启信息
|
||||
if temp_data.data.get("reload", False):
|
||||
delta_time = time.time() - temp_data.data.get("reload_time", 0)
|
||||
temp_data.data["delta_time"] = delta_time
|
||||
common_db.save(temp_data) # 更新数据
|
||||
|
||||
|
||||
@driver.on_shutdown
|
||||
async def on_shutdown():
|
||||
pass
|
||||
|
||||
|
||||
@driver.on_bot_connect
|
||||
async def _(bot: T_Bot):
|
||||
temp_data = common_db.first(TempConfig(), default=TempConfig())
|
||||
# 用于重启计时
|
||||
if temp_data.data.get("reload", False):
|
||||
temp_data.data["reload"] = False
|
||||
reload_bot_id = temp_data.data.get("reload_bot_id", 0)
|
||||
if reload_bot_id != bot.self_id:
|
||||
return
|
||||
reload_session_type = temp_data.data.get("reload_session_type", "private")
|
||||
reload_session_id = temp_data.data.get("reload_session_id", 0)
|
||||
delta_time = temp_data.data.get("delta_time", 0)
|
||||
common_db.save(temp_data) # 更新数据
|
||||
await bot.call_api(
|
||||
"send_msg",
|
||||
message_type=reload_session_type,
|
||||
user_id=reload_session_id,
|
||||
group_id=reload_session_id,
|
||||
message="Liteyuki reloaded in %.2f s" % delta_time
|
||||
)
|
||||
|
||||
|
||||
# 每天4点更新
|
||||
@scheduler.scheduled_job("cron", hour=4)
|
||||
async def every_day_update():
|
||||
if get_config("auto_update", default=True):
|
||||
result, logs = update_liteyuki()
|
||||
pip.main(["install", "-r", "requirements.txt"])
|
||||
if result:
|
||||
await broadcast_to_superusers(f"Liteyuki updated: ```\n{logs}\n```")
|
||||
nonebot.logger.info(f"Liteyuki updated: {logs}")
|
||||
Reloader.reload(5)
|
||||
else:
|
||||
nonebot.logger.info(logs)
|
||||
|
||||
|
||||
# 安全的需要用户id的api
|
||||
need_user_id = (
|
||||
"send_private_msg",
|
||||
"send_msg",
|
||||
"set_group_card",
|
||||
"set_group_special_title",
|
||||
"get_stranger_info",
|
||||
"get_group_member_info"
|
||||
)
|
||||
|
||||
need_group_id = (
|
||||
"send_group_msg",
|
||||
"send_msg",
|
||||
"set_group_card",
|
||||
"set_group_name",
|
||||
"set_group_special_title",
|
||||
"get_group_member_info",
|
||||
"get_group_member_list",
|
||||
"get_group_honor_info"
|
||||
)
|
@@ -1,43 +0,0 @@
|
||||
import time
|
||||
|
||||
import nonebot
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
|
||||
from liteyuki.utils.base.config import get_config
|
||||
from liteyuki.utils.base.reloader import Reloader
|
||||
from liteyuki.utils.base.resource import load_resources
|
||||
|
||||
if get_config("debug", False):
|
||||
nonebot.logger.info("Liteyuki Reload is enable, watching for file changes...")
|
||||
|
||||
|
||||
class CodeModifiedHandler(FileSystemEventHandler):
|
||||
def on_modified(self, event):
|
||||
if "liteyuki/resources" not in event.src_path.replace("\\", "/"):
|
||||
nonebot.logger.debug(f"{event.src_path} has been modified, reloading bot...")
|
||||
Reloader.reload()
|
||||
|
||||
|
||||
class ResourceModifiedHandler(FileSystemEventHandler):
|
||||
def on_modified(self, event):
|
||||
if not event.is_directory:
|
||||
nonebot.logger.debug(f"{event.src_path} has been modified, reloading resource...")
|
||||
load_resources()
|
||||
|
||||
|
||||
code_modified_handler = CodeModifiedHandler()
|
||||
resource_modified_handle = ResourceModifiedHandler()
|
||||
|
||||
observer = Observer()
|
||||
observer.schedule(resource_modified_handle, path="liteyuki/resources", recursive=True)
|
||||
observer.schedule(resource_modified_handle, path="resources", recursive=True)
|
||||
observer.schedule(code_modified_handler, path="liteyuki", recursive=True)
|
||||
observer.start()
|
||||
|
||||
# try:
|
||||
# while True:
|
||||
# time.sleep(1)
|
||||
# except KeyboardInterrupt:
|
||||
# observer.stop()
|
||||
# observer.join()
|
@@ -1,27 +0,0 @@
|
||||
import nonebot.plugin
|
||||
|
||||
from liteyuki.utils import init_log
|
||||
from liteyuki.utils.base.config import get_config
|
||||
from liteyuki.utils.base.data_manager import InstalledPlugin, plugin_db
|
||||
from liteyuki.utils.base.resource import load_resources
|
||||
from liteyuki.utils.message.tools import check_for_package
|
||||
|
||||
load_resources()
|
||||
init_log()
|
||||
|
||||
nonebot.plugin.load_plugins("liteyuki/plugins")
|
||||
# 从数据库读取已安装的插件
|
||||
if not get_config("safe_mode", False):
|
||||
# 安全模式下,不加载插件
|
||||
|
||||
installed_plugins: list[InstalledPlugin] = plugin_db.all(InstalledPlugin())
|
||||
if installed_plugins:
|
||||
for installed_plugin in installed_plugins:
|
||||
if not check_for_package(installed_plugin.module_name):
|
||||
nonebot.logger.error(f"{installed_plugin.module_name} not installed, but in loading database. please run `npm fixup` in chat to reinstall it.")
|
||||
else:
|
||||
nonebot.load_plugin(installed_plugin.module_name)
|
||||
|
||||
nonebot.plugin.load_plugins("plugins")
|
||||
else:
|
||||
nonebot.logger.info("Safe mode is on, no plugin loaded.")
|
@@ -1,15 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from .rt_guide import *
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="CRT生成工具",
|
||||
description="一些CRT牌子生成器",
|
||||
usage="我觉得你应该会用",
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki" : True,
|
||||
"toggleable" : True,
|
||||
"default_enable": True,
|
||||
}
|
||||
)
|
@@ -1,575 +0,0 @@
|
||||
import os
|
||||
import uuid
|
||||
from typing import Tuple, Union, List
|
||||
|
||||
import nonebot
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
|
||||
default_color = (255, 255, 255, 255)
|
||||
default_font = "resources/fonts/MiSans-Semibold.ttf"
|
||||
|
||||
|
||||
def render_canvas_from_json(file: str, background: Image) -> "Canvas":
|
||||
pass
|
||||
|
||||
|
||||
class BasePanel:
|
||||
def __init__(self,
|
||||
uv_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0),
|
||||
box_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0),
|
||||
parent_point: Tuple[float, float] = (0.5, 0.5),
|
||||
point: Tuple[float, float] = (0.5, 0.5)):
|
||||
"""
|
||||
:param uv_size: 底面板大小
|
||||
:param box_size: 子(自身)面板大小
|
||||
:param parent_point: 底面板锚点
|
||||
:param point: 子(自身)面板锚点
|
||||
"""
|
||||
self.canvas: Canvas | None = None
|
||||
self.uv_size = uv_size
|
||||
self.box_size = box_size
|
||||
self.parent_point = parent_point
|
||||
self.point = point
|
||||
self.parent: BasePanel | None = None
|
||||
self.canvas_box: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0)
|
||||
# 此节点在父节点上的盒子
|
||||
self.box = (
|
||||
self.parent_point[0] - self.point[0] * self.box_size[0] / self.uv_size[0],
|
||||
self.parent_point[1] - self.point[1] * self.box_size[1] / self.uv_size[1],
|
||||
self.parent_point[0] + (1 - self.point[0]) * self.box_size[0] / self.uv_size[0],
|
||||
self.parent_point[1] + (1 - self.point[1]) * self.box_size[1] / self.uv_size[1]
|
||||
)
|
||||
|
||||
def load(self, only_calculate=False):
|
||||
"""
|
||||
将对象写入画布
|
||||
此处仅作声明
|
||||
由各子类重写
|
||||
|
||||
:return:
|
||||
"""
|
||||
self.actual_pos = self.canvas_box
|
||||
|
||||
def save_as(self, canvas_box, only_calculate=False):
|
||||
"""
|
||||
此函数执行时间较长,建议异步运行
|
||||
:param only_calculate:
|
||||
:param canvas_box 此节点在画布上的盒子,并不是在父节点上的盒子
|
||||
:return:
|
||||
"""
|
||||
for name, child in self.__dict__.items():
|
||||
# 此节点在画布上的盒子
|
||||
if isinstance(child, BasePanel) and name not in ["canvas", "parent"]:
|
||||
child.parent = self
|
||||
if isinstance(self, Canvas):
|
||||
child.canvas = self
|
||||
else:
|
||||
child.canvas = self.canvas
|
||||
dxc = canvas_box[2] - canvas_box[0]
|
||||
dyc = canvas_box[3] - canvas_box[1]
|
||||
child.canvas_box = (
|
||||
canvas_box[0] + dxc * child.box[0],
|
||||
canvas_box[1] + dyc * child.box[1],
|
||||
canvas_box[0] + dxc * child.box[2],
|
||||
canvas_box[1] + dyc * child.box[3]
|
||||
)
|
||||
child.load(only_calculate)
|
||||
child.save_as(child.canvas_box, only_calculate)
|
||||
|
||||
|
||||
class Canvas(BasePanel):
|
||||
def __init__(self, base_img: Image.Image):
|
||||
self.base_img = base_img
|
||||
self.canvas = self
|
||||
super(Canvas, self).__init__()
|
||||
self.draw_line_list = []
|
||||
|
||||
def export(self, file, alpha=False):
|
||||
self.base_img = self.base_img.convert("RGBA")
|
||||
self.save_as((0, 0, 1, 1))
|
||||
draw = ImageDraw.Draw(self.base_img)
|
||||
for line in self.draw_line_list:
|
||||
draw.line(*line)
|
||||
if not alpha:
|
||||
self.base_img = self.base_img.convert("RGB")
|
||||
self.base_img.save(file)
|
||||
|
||||
def delete(self):
|
||||
os.remove(self.file)
|
||||
|
||||
def get_actual_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]:
|
||||
"""
|
||||
获取控件实际相对大小
|
||||
函数执行时间较长
|
||||
|
||||
:param path: 控件路径
|
||||
:return:
|
||||
"""
|
||||
sub_obj = self
|
||||
self.save_as((0, 0, 1, 1), True)
|
||||
control_path = ""
|
||||
for i, seq in enumerate(path.split(".")):
|
||||
if seq not in sub_obj.__dict__:
|
||||
raise KeyError(f"在{control_path}中找不到控件:{seq}")
|
||||
control_path += f".{seq}"
|
||||
sub_obj = sub_obj.__dict__[seq]
|
||||
return sub_obj.actual_pos
|
||||
|
||||
def get_actual_pixel_size(self, path: str) -> Union[None, Tuple[int, int]]:
|
||||
"""
|
||||
获取控件实际像素长宽
|
||||
函数执行时间较长
|
||||
:param path: 控件路径
|
||||
:return:
|
||||
"""
|
||||
sub_obj = self
|
||||
self.save_as((0, 0, 1, 1), True)
|
||||
control_path = ""
|
||||
for i, seq in enumerate(path.split(".")):
|
||||
if seq not in sub_obj.__dict__:
|
||||
raise KeyError(f"在{control_path}中找不到控件:{seq}")
|
||||
control_path += f".{seq}"
|
||||
sub_obj = sub_obj.__dict__[seq]
|
||||
dx = int(sub_obj.canvas.base_img.size[0] * (sub_obj.actual_pos[2] - sub_obj.actual_pos[0]))
|
||||
dy = int(sub_obj.canvas.base_img.size[1] * (sub_obj.actual_pos[3] - sub_obj.actual_pos[1]))
|
||||
return dx, dy
|
||||
|
||||
def get_actual_pixel_box(self, path: str) -> Union[None, Tuple[int, int, int, int]]:
|
||||
"""
|
||||
获取控件实际像素大小盒子
|
||||
函数执行时间较长
|
||||
:param path: 控件路径
|
||||
:return:
|
||||
"""
|
||||
sub_obj = self
|
||||
self.save_as((0, 0, 1, 1), True)
|
||||
control_path = ""
|
||||
for i, seq in enumerate(path.split(".")):
|
||||
if seq not in sub_obj.__dict__:
|
||||
raise KeyError(f"在{control_path}中找不到控件:{seq}")
|
||||
control_path += f".{seq}"
|
||||
sub_obj = sub_obj.__dict__[seq]
|
||||
x1 = int(sub_obj.canvas.base_img.size[0] * sub_obj.actual_pos[0])
|
||||
y1 = int(sub_obj.canvas.base_img.size[1] * sub_obj.actual_pos[1])
|
||||
x2 = int(sub_obj.canvas.base_img.size[2] * sub_obj.actual_pos[2])
|
||||
y2 = int(sub_obj.canvas.base_img.size[3] * sub_obj.actual_pos[3])
|
||||
return x1, y1, x2, y2
|
||||
|
||||
def get_parent_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]:
|
||||
"""
|
||||
获取控件在父节点的大小
|
||||
函数执行时间较长
|
||||
|
||||
:param path: 控件路径
|
||||
:return:
|
||||
"""
|
||||
sub_obj = self.get_control_by_path(path)
|
||||
on_parent_pos = (
|
||||
(sub_obj.actual_pos[0] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]),
|
||||
(sub_obj.actual_pos[1] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1]),
|
||||
(sub_obj.actual_pos[2] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]),
|
||||
(sub_obj.actual_pos[3] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1])
|
||||
)
|
||||
return on_parent_pos
|
||||
|
||||
def get_control_by_path(self, path: str) -> Union[BasePanel, "Img", "Rectangle", "Text"]:
|
||||
sub_obj = self
|
||||
self.save_as((0, 0, 1, 1), True)
|
||||
control_path = ""
|
||||
for i, seq in enumerate(path.split(".")):
|
||||
if seq not in sub_obj.__dict__:
|
||||
raise KeyError(f"在{control_path}中找不到控件:{seq}")
|
||||
control_path += f".{seq}"
|
||||
sub_obj = sub_obj.__dict__[seq]
|
||||
return sub_obj
|
||||
|
||||
def draw_line(self, path: str, p1: Tuple[float, float], p2: Tuple[float, float], color, width):
|
||||
"""
|
||||
画线
|
||||
|
||||
:param color:
|
||||
:param width:
|
||||
:param path:
|
||||
:param p1:
|
||||
:param p2:
|
||||
:return:
|
||||
"""
|
||||
ac_pos = self.get_actual_box(path)
|
||||
control = self.get_control_by_path(path)
|
||||
dx = ac_pos[2] - ac_pos[0]
|
||||
dy = ac_pos[3] - ac_pos[1]
|
||||
xy_box = int((ac_pos[0] + dx * p1[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p1[1]) * control.canvas.base_img.size[1]), int(
|
||||
(ac_pos[0] + dx * p2[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p2[1]) * control.canvas.base_img.size[1])
|
||||
self.draw_line_list.append((xy_box, color, width))
|
||||
|
||||
|
||||
class Panel(BasePanel):
|
||||
def __init__(self, uv_size, box_size, parent_point, point):
|
||||
super(Panel, self).__init__(uv_size, box_size, parent_point, point)
|
||||
|
||||
|
||||
class TextSegment:
|
||||
def __init__(self, text, **kwargs):
|
||||
if not isinstance(text, str):
|
||||
raise TypeError("请输入字符串")
|
||||
self.text = text
|
||||
self.color = kwargs.get("color", None)
|
||||
self.font = kwargs.get("font", None)
|
||||
|
||||
@staticmethod
|
||||
def text2text_segment_list(text: str):
|
||||
"""
|
||||
暂时没写好
|
||||
|
||||
:param text: %FFFFFFFF%1123%FFFFFFFF%21323
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Text(BasePanel):
|
||||
def __init__(self, uv_size, box_size, parent_point, point, text: Union[str, list], font=default_font, color=(255, 255, 255, 255), vertical=False,
|
||||
line_feed=False, force_size=False, fill=(0, 0, 0, 0), fillet=0, outline=(0, 0, 0, 0), outline_width=0, rectangle_side=0, font_size=None, dp: int = 5,
|
||||
anchor: str = "la"):
|
||||
"""
|
||||
:param uv_size:
|
||||
:param box_size:
|
||||
:param parent_point:
|
||||
:param point:
|
||||
:param text: list[TextSegment] | str
|
||||
:param font:
|
||||
:param color:
|
||||
:param vertical: 是否竖直
|
||||
:param line_feed: 是否换行
|
||||
:param force_size: 强制大小
|
||||
:param dp: 字体大小递减精度
|
||||
:param anchor : https://www.zhihu.com/question/474216280
|
||||
:param fill: 底部填充颜色
|
||||
:param fillet: 填充圆角
|
||||
:param rectangle_side: 边框宽度
|
||||
:param outline: 填充矩形边框颜色
|
||||
:param outline_width: 填充矩形边框宽度
|
||||
"""
|
||||
self.actual_pos = None
|
||||
self.outline_width = outline_width
|
||||
self.outline = outline
|
||||
self.fill = fill
|
||||
self.fillet = fillet
|
||||
self.font = font
|
||||
self.text = text
|
||||
self.color = color
|
||||
self.force_size = force_size
|
||||
self.vertical = vertical
|
||||
self.line_feed = line_feed
|
||||
self.dp = dp
|
||||
self.font_size = font_size
|
||||
self.rectangle_side = rectangle_side
|
||||
self.anchor = anchor
|
||||
super(Text, self).__init__(uv_size, box_size, parent_point, point)
|
||||
|
||||
def load(self, only_calculate=False):
|
||||
"""限制区域像素大小"""
|
||||
if isinstance(self.text, str):
|
||||
self.text = [
|
||||
TextSegment(text=self.text, color=self.color, font=self.font)
|
||||
]
|
||||
all_text = str()
|
||||
for text in self.text:
|
||||
all_text += text.text
|
||||
limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1])
|
||||
font_size = limited_size[1] if self.font_size is None else self.font_size
|
||||
image_font = ImageFont.truetype(self.font, font_size)
|
||||
actual_size = image_font.getsize(all_text)
|
||||
while (actual_size[0] > limited_size[0] or actual_size[1] > limited_size[1]) and not self.force_size:
|
||||
font_size -= self.dp
|
||||
image_font = ImageFont.truetype(self.font, font_size)
|
||||
actual_size = image_font.getsize(all_text)
|
||||
draw = ImageDraw.Draw(self.canvas.base_img)
|
||||
if isinstance(self.parent, Img) or isinstance(self.parent, Text):
|
||||
self.parent.canvas_box = self.parent.actual_pos
|
||||
dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0]
|
||||
dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1]
|
||||
dx1 = actual_size[0] / self.canvas.base_img.size[0]
|
||||
dy1 = actual_size[1] / self.canvas.base_img.size[1]
|
||||
start_point = [
|
||||
int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]),
|
||||
int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1])
|
||||
]
|
||||
self.actual_pos = (
|
||||
start_point[0] / self.canvas.base_img.size[0],
|
||||
start_point[1] / self.canvas.base_img.size[1],
|
||||
(start_point[0] + actual_size[0]) / self.canvas.base_img.size[0],
|
||||
(start_point[1] + actual_size[1]) / self.canvas.base_img.size[1],
|
||||
)
|
||||
self.font_size = font_size
|
||||
if not only_calculate:
|
||||
for text_segment in self.text:
|
||||
if text_segment.color is None:
|
||||
text_segment.color = self.color
|
||||
if text_segment.font is None:
|
||||
text_segment.font = self.font
|
||||
image_font = ImageFont.truetype(font=text_segment.font, size=font_size)
|
||||
if self.fill[-1] > 0:
|
||||
rectangle = Shape.rectangle(size=(actual_size[0] + 2 * self.rectangle_side, actual_size[1] + 2 * self.rectangle_side), fillet=self.fillet, fill=self.fill,
|
||||
width=self.outline_width, outline=self.outline)
|
||||
self.canvas.base_img.paste(im=rectangle, box=(start_point[0] - self.rectangle_side,
|
||||
start_point[1] - self.rectangle_side,
|
||||
start_point[0] + actual_size[0] + self.rectangle_side,
|
||||
start_point[1] + actual_size[1] + self.rectangle_side),
|
||||
mask=rectangle.split()[-1])
|
||||
draw.text((start_point[0] - self.rectangle_side, start_point[1] - self.rectangle_side),
|
||||
text_segment.text, text_segment.color, font=image_font, anchor=self.anchor)
|
||||
text_width = image_font.getsize(text_segment.text)
|
||||
start_point[0] += text_width[0]
|
||||
|
||||
|
||||
class Img(BasePanel):
|
||||
def __init__(self, uv_size, box_size, parent_point, point, img: Image.Image, keep_ratio=True):
|
||||
self.img_base_img = img
|
||||
self.keep_ratio = keep_ratio
|
||||
super(Img, self).__init__(uv_size, box_size, parent_point, point)
|
||||
|
||||
def load(self, only_calculate=False):
|
||||
self.preprocess()
|
||||
self.img_base_img = self.img_base_img.convert("RGBA")
|
||||
limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), \
|
||||
int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1])
|
||||
|
||||
if self.keep_ratio:
|
||||
"""保持比例"""
|
||||
actual_ratio = self.img_base_img.size[0] / self.img_base_img.size[1]
|
||||
limited_ratio = limited_size[0] / limited_size[1]
|
||||
if actual_ratio >= limited_ratio:
|
||||
# 图片过长
|
||||
self.img_base_img = self.img_base_img.resize(
|
||||
(int(self.img_base_img.size[0] * limited_size[0] / self.img_base_img.size[0]),
|
||||
int(self.img_base_img.size[1] * limited_size[0] / self.img_base_img.size[0]))
|
||||
)
|
||||
else:
|
||||
self.img_base_img = self.img_base_img.resize(
|
||||
(int(self.img_base_img.size[0] * limited_size[1] / self.img_base_img.size[1]),
|
||||
int(self.img_base_img.size[1] * limited_size[1] / self.img_base_img.size[1]))
|
||||
)
|
||||
|
||||
else:
|
||||
"""不保持比例"""
|
||||
self.img_base_img = self.img_base_img.resize(limited_size)
|
||||
|
||||
# 占比长度
|
||||
if isinstance(self.parent, Img) or isinstance(self.parent, Text):
|
||||
self.parent.canvas_box = self.parent.actual_pos
|
||||
|
||||
dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0]
|
||||
dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1]
|
||||
|
||||
dx1 = self.img_base_img.size[0] / self.canvas.base_img.size[0]
|
||||
dy1 = self.img_base_img.size[1] / self.canvas.base_img.size[1]
|
||||
start_point = (
|
||||
int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]),
|
||||
int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1])
|
||||
)
|
||||
alpha = self.img_base_img.split()[3]
|
||||
self.actual_pos = (
|
||||
start_point[0] / self.canvas.base_img.size[0],
|
||||
start_point[1] / self.canvas.base_img.size[1],
|
||||
(start_point[0] + self.img_base_img.size[0]) / self.canvas.base_img.size[0],
|
||||
(start_point[1] + self.img_base_img.size[1]) / self.canvas.base_img.size[1],
|
||||
)
|
||||
if not only_calculate:
|
||||
self.canvas.base_img.paste(self.img_base_img, start_point, alpha)
|
||||
|
||||
def preprocess(self):
|
||||
pass
|
||||
|
||||
|
||||
class Rectangle(Img):
|
||||
def __init__(self, uv_size, box_size, parent_point, point, fillet: Union[int, float] = 0, img: Union[Image.Image] = None, keep_ratio=True,
|
||||
color=default_color, outline_width=0, outline_color=default_color):
|
||||
"""
|
||||
圆角图
|
||||
:param uv_size:
|
||||
:param box_size:
|
||||
:param parent_point:
|
||||
:param point:
|
||||
:param fillet: 圆角半径浮点或整数
|
||||
:param img:
|
||||
:param keep_ratio:
|
||||
"""
|
||||
self.fillet = fillet
|
||||
self.color = color
|
||||
self.outline_width = outline_width
|
||||
self.outline_color = outline_color
|
||||
super(Rectangle, self).__init__(uv_size, box_size, parent_point, point, img, keep_ratio)
|
||||
|
||||
def preprocess(self):
|
||||
limited_size = (int(self.canvas.base_img.size[0] * (self.canvas_box[2] - self.canvas_box[0])),
|
||||
int(self.canvas.base_img.size[1] * (self.canvas_box[3] - self.canvas_box[1])))
|
||||
if not self.keep_ratio and self.img_base_img is not None and self.img_base_img.size[0] / self.img_base_img.size[1] != limited_size[0] / limited_size[1]:
|
||||
self.img_base_img = self.img_base_img.resize(limited_size)
|
||||
self.img_base_img = Shape.rectangle(size=limited_size, fillet=self.fillet, fill=self.color, width=self.outline_width, outline=self.outline_color)
|
||||
|
||||
|
||||
class Color:
|
||||
GREY = (128, 128, 128, 255)
|
||||
RED = (255, 0, 0, 255)
|
||||
GREEN = (0, 255, 0, 255)
|
||||
BLUE = (0, 0, 255, 255)
|
||||
YELLOW = (255, 255, 0, 255)
|
||||
PURPLE = (255, 0, 255, 255)
|
||||
CYAN = (0, 255, 255, 255)
|
||||
WHITE = (255, 255, 255, 255)
|
||||
BLACK = (0, 0, 0, 255)
|
||||
|
||||
@staticmethod
|
||||
def hex2dec(colorHex: str) -> Tuple[int, int, int, int]:
|
||||
"""
|
||||
:param colorHex: FFFFFFFF (ARGB)-> (R, G, B, A)
|
||||
:return:
|
||||
"""
|
||||
return int(colorHex[2:4], 16), int(colorHex[4:6], 16), int(colorHex[6:8], 16), int(colorHex[0:2], 16)
|
||||
|
||||
|
||||
class Shape:
|
||||
@staticmethod
|
||||
def circular(radius: int, fill: tuple, width: int = 0, outline: tuple = Color.BLACK) -> Image.Image:
|
||||
"""
|
||||
:param radius: 半径(像素)
|
||||
:param fill: 填充颜色
|
||||
:param width: 轮廓粗细(像素)
|
||||
:param outline: 轮廓颜色
|
||||
:return: 圆形Image对象
|
||||
"""
|
||||
img = Image.new("RGBA", (radius * 2, radius * 2), color=radius)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.ellipse(xy=(0, 0, radius * 2, radius * 2), fill=fill, outline=outline, width=width)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def rectangle(size: Tuple[int, int], fill: tuple, width: int = 0, outline: tuple = Color.BLACK, fillet: int = 0) -> Image.Image:
|
||||
"""
|
||||
:param fillet: 圆角半径(像素)
|
||||
:param size: 长宽(像素)
|
||||
:param fill: 填充颜色
|
||||
:param width: 轮廓粗细(像素)
|
||||
:param outline: 轮廓颜色
|
||||
:return: 矩形Image对象
|
||||
"""
|
||||
img = Image.new("RGBA", size, color=fill)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.rounded_rectangle(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline, width=width, radius=fillet)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def ellipse(size: Tuple[int, int], fill: tuple, outline: int = 0, outline_color: tuple = Color.BLACK) -> Image.Image:
|
||||
"""
|
||||
:param size: 长宽(像素)
|
||||
:param fill: 填充颜色
|
||||
:param outline: 轮廓粗细(像素)
|
||||
:param outline_color: 轮廓颜色
|
||||
:return: 椭圆Image对象
|
||||
"""
|
||||
img = Image.new("RGBA", size, color=fill)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.ellipse(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline_color, width=outline)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def polygon(points: List[Tuple[int, int]], fill: tuple, outline: int, outline_color: tuple) -> Image.Image:
|
||||
"""
|
||||
:param points: 多边形顶点列表
|
||||
:param fill: 填充颜色
|
||||
:param outline: 轮廓粗细(像素)
|
||||
:param outline_color: 轮廓颜色
|
||||
:return: 多边形Image对象
|
||||
"""
|
||||
img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.polygon(xy=points, fill=fill, outline=outline_color, width=outline)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def line(points: List[Tuple[int, int]], fill: tuple, width: int) -> Image:
|
||||
"""
|
||||
:param points: 线段顶点列表
|
||||
:param fill: 填充颜色
|
||||
:param width: 线段粗细(像素)
|
||||
:return: 线段Image对象
|
||||
"""
|
||||
img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.line(xy=points, fill=fill, width=width)
|
||||
return img
|
||||
|
||||
|
||||
class Utils:
|
||||
|
||||
@staticmethod
|
||||
def central_clip_by_ratio(img: Image.Image, size: Tuple, use_cache=True):
|
||||
"""
|
||||
:param use_cache: 是否使用缓存,剪切过一次后默认生成缓存
|
||||
:param img:
|
||||
:param size: 仅为比例,满填充裁剪
|
||||
:return:
|
||||
"""
|
||||
cache_file_path = str()
|
||||
if use_cache:
|
||||
filename_without_end = ".".join(os.path.basename(img.fp.name).split(".")[0:-1]) + f"_{size[0]}x{size[1]}" + ".png"
|
||||
cache_file_path = os.path.join(".cache", filename_without_end)
|
||||
if os.path.exists(cache_file_path):
|
||||
nonebot.logger.info("本次使用缓存加载图片,不裁剪")
|
||||
return Image.open(os.path.join(".cache", filename_without_end))
|
||||
img_ratio = img.size[0] / img.size[1]
|
||||
limited_ratio = size[0] / size[1]
|
||||
if limited_ratio > img_ratio:
|
||||
actual_size = (
|
||||
img.size[0],
|
||||
img.size[0] / size[0] * size[1]
|
||||
)
|
||||
box = (
|
||||
0, (img.size[1] - actual_size[1]) // 2,
|
||||
img.size[0], img.size[1] - (img.size[1] - actual_size[1]) // 2
|
||||
)
|
||||
else:
|
||||
actual_size = (
|
||||
img.size[1] / size[1] * size[0],
|
||||
img.size[1],
|
||||
)
|
||||
box = (
|
||||
(img.size[0] - actual_size[0]) // 2, 0,
|
||||
img.size[0] - (img.size[0] - actual_size[0]) // 2, img.size[1]
|
||||
)
|
||||
img = img.crop(box).resize(size)
|
||||
if use_cache:
|
||||
img.save(cache_file_path)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def circular_clip(img: Image.Image):
|
||||
"""
|
||||
裁剪为alpha圆形
|
||||
|
||||
:param img:
|
||||
:return:
|
||||
"""
|
||||
length = min(img.size)
|
||||
alpha_cover = Image.new("RGBA", (length, length), color=(0, 0, 0, 0))
|
||||
if img.size[0] > img.size[1]:
|
||||
box = (
|
||||
(img.size[0] - img[1]) // 2, 0,
|
||||
(img.size[0] - img[1]) // 2 + img.size[1], img.size[1]
|
||||
)
|
||||
else:
|
||||
box = (
|
||||
0, (img.size[1] - img.size[0]) // 2,
|
||||
img.size[0], (img.size[1] - img.size[0]) // 2 + img.size[0]
|
||||
)
|
||||
img = img.crop(box).resize((length, length))
|
||||
draw = ImageDraw.Draw(alpha_cover)
|
||||
draw.ellipse(xy=(0, 0, length, length), fill=(255, 255, 255, 255))
|
||||
alpha = alpha_cover.split()[-1]
|
||||
img.putalpha(alpha)
|
||||
return img
|
||||
|
||||
@staticmethod
|
||||
def open_img(path) -> Image.Image:
|
||||
return Image.open(path, "RGBA")
|
@@ -1,419 +0,0 @@
|
||||
import json
|
||||
from typing import List, Any
|
||||
|
||||
from PIL import Image
|
||||
from arclet.alconna import Alconna
|
||||
from nb_cli import run_sync
|
||||
from nonebot import on_command
|
||||
from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, Args, MultiVar, Arparma, UniMessage
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .canvas import *
|
||||
from ...utils.base.resource import get_path
|
||||
|
||||
resolution = 256
|
||||
|
||||
|
||||
class Entrance(BaseModel):
|
||||
identifier: str
|
||||
size: tuple[int, int]
|
||||
dest: List[str]
|
||||
|
||||
|
||||
class Station(BaseModel):
|
||||
identifier: str
|
||||
chineseName: str
|
||||
englishName: str
|
||||
position: tuple[int, int]
|
||||
|
||||
|
||||
class Line(BaseModel):
|
||||
identifier: str
|
||||
chineseName: str
|
||||
englishName: str
|
||||
color: Any
|
||||
stations: List["Station"]
|
||||
|
||||
|
||||
font_light = get_path("templates/fonts/MiSans/MiSans-Light.woff2")
|
||||
font_bold = get_path("templates/fonts/MiSans/MiSans-Bold.woff2")
|
||||
|
||||
@run_sync
|
||||
def generate_entrance_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
|
||||
reso: int = resolution):
|
||||
"""
|
||||
Generates an entrance sign for the ride.
|
||||
"""
|
||||
width, height = ratio[0] * reso, ratio[1] * reso
|
||||
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.WHITE))
|
||||
# 加黑色图框
|
||||
baseCanvas.outline = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0, 0),
|
||||
point=(0, 0),
|
||||
img=Shape.rectangle(
|
||||
size=(width, height),
|
||||
fillet=0,
|
||||
fill=(0, 0, 0, 0),
|
||||
width=15,
|
||||
outline=Color.BLACK
|
||||
)
|
||||
)
|
||||
|
||||
baseCanvas.contentPanel = Panel(
|
||||
uv_size=(width, height),
|
||||
box_size=(width - 28, height - 28),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
)
|
||||
|
||||
linePanelHeight = 0.7 * ratio[1]
|
||||
linePanelWidth = linePanelHeight * 1.3
|
||||
|
||||
# 画线路面板部分
|
||||
|
||||
for i, line in enumerate(lineInfo):
|
||||
linePanel = baseCanvas.contentPanel.__dict__[f"Line_{i}_Panel"] = Panel(
|
||||
uv_size=ratio,
|
||||
box_size=(linePanelWidth, linePanelHeight),
|
||||
parent_point=(i * linePanelWidth / ratio[0], 1),
|
||||
point=(0, 1),
|
||||
)
|
||||
|
||||
linePanel.colorCube = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.15, 1),
|
||||
parent_point=(0.125, 1),
|
||||
point=(0, 1),
|
||||
img=Shape.rectangle(
|
||||
size=(100, 100),
|
||||
fillet=0,
|
||||
fill=line.color,
|
||||
),
|
||||
keep_ratio=False
|
||||
)
|
||||
|
||||
textPanel = linePanel.TextPanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.625, 1),
|
||||
parent_point=(1, 1),
|
||||
point=(1, 1)
|
||||
)
|
||||
|
||||
# 中文线路名
|
||||
textPanel.namePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 2 / 3),
|
||||
parent_point=(0, 0),
|
||||
point=(0, 0),
|
||||
)
|
||||
nameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.namePanel".format(i))
|
||||
textPanel.namePanel.text = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
text=line.chineseName,
|
||||
color=Color.BLACK,
|
||||
font_size=int(nameSize[1] * 0.5),
|
||||
force_size=True,
|
||||
font=font_bold
|
||||
|
||||
)
|
||||
|
||||
# 英文线路名
|
||||
textPanel.englishNamePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1 / 3),
|
||||
parent_point=(0, 1),
|
||||
point=(0, 1),
|
||||
)
|
||||
englishNameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.englishNamePanel".format(i))
|
||||
textPanel.englishNamePanel.text = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
text=line.englishName,
|
||||
color=Color.BLACK,
|
||||
font_size=int(englishNameSize[1] * 0.6),
|
||||
force_size=True,
|
||||
font=font_light
|
||||
)
|
||||
|
||||
# 画名称部分
|
||||
namePanel = baseCanvas.contentPanel.namePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 0.4),
|
||||
parent_point=(0.5, 0),
|
||||
point=(0.5, 0),
|
||||
)
|
||||
|
||||
namePanel.text = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
text=name,
|
||||
color=Color.BLACK,
|
||||
font_size=int(height * 0.3),
|
||||
force_size=True,
|
||||
font=font_bold
|
||||
)
|
||||
|
||||
aliasesPanel = baseCanvas.contentPanel.aliasesPanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 0.5),
|
||||
parent_point=(0.5, 1),
|
||||
point=(0.5, 1),
|
||||
|
||||
)
|
||||
for j, alias in enumerate(aliases):
|
||||
aliasesPanel.__dict__[alias] = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.35, 0.5),
|
||||
parent_point=(0.5, 0.5 * j),
|
||||
point=(0.5, 0),
|
||||
text=alias,
|
||||
color=Color.BLACK,
|
||||
font_size=int(height * 0.15),
|
||||
font=font_light
|
||||
)
|
||||
|
||||
# 画入口标识
|
||||
entrancePanel = baseCanvas.contentPanel.entrancePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.2, 1),
|
||||
parent_point=(1, 0.5),
|
||||
point=(1, 0.5),
|
||||
)
|
||||
# 中文文本
|
||||
entrancePanel.namePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 0.5),
|
||||
parent_point=(1, 0),
|
||||
point=(1, 0),
|
||||
)
|
||||
entrancePanel.namePanel.text = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0, 0.5),
|
||||
point=(0, 0.5),
|
||||
text=f"{entranceIdentifier}出入口",
|
||||
color=Color.BLACK,
|
||||
font_size=int(height * 0.2),
|
||||
force_size=True,
|
||||
font=font_bold
|
||||
)
|
||||
# 英文文本
|
||||
entrancePanel.englishNamePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 0.5),
|
||||
parent_point=(1, 1),
|
||||
point=(1, 1),
|
||||
)
|
||||
entrancePanel.englishNamePanel.text = Text(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0, 0.5),
|
||||
point=(0, 0.5),
|
||||
text=f"Entrance {entranceIdentifier}",
|
||||
color=Color.BLACK,
|
||||
font_size=int(height * 0.15),
|
||||
force_size=True,
|
||||
font=font_light
|
||||
)
|
||||
|
||||
return baseCanvas.base_img.tobytes()
|
||||
|
||||
|
||||
crt_alc = on_alconna(
|
||||
Alconna(
|
||||
"crt",
|
||||
Subcommand(
|
||||
"entrance",
|
||||
Args["name", str]["lines", str, ""]["entrance", int, 1], # /crt entrance 璧山&Bishan 1号线&Line1&#ff0000,27号线&Line1&#ff0000 1A
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@crt_alc.assign("entrance")
|
||||
async def _(result: Arparma):
|
||||
args = result.subcommands.get("entrance").args
|
||||
name = args["name"]
|
||||
lines = args["lines"]
|
||||
entrance = args["entrance"]
|
||||
line_info = []
|
||||
for line in lines.split(","):
|
||||
line_args = line.split("&")
|
||||
line_info.append(Line(
|
||||
identifier=1,
|
||||
chineseName=line_args[0],
|
||||
englishName=line_args[1],
|
||||
color=line_args[2],
|
||||
stations=[]
|
||||
))
|
||||
img_bytes = await generate_entrance_sign(
|
||||
name=name,
|
||||
aliases=name.split("&"),
|
||||
lineInfo=line_info,
|
||||
entranceIdentifier=entrance,
|
||||
ratio=(8, 1),
|
||||
reso=256,
|
||||
)
|
||||
await crt_alc.finish(
|
||||
UniMessage.image(raw=img_bytes)
|
||||
)
|
||||
|
||||
|
||||
def generate_platform_line_pic(line: Line, station: Station, ratio=None, reso: int = resolution):
|
||||
"""
|
||||
生成站台线路图
|
||||
:param line: 线路对象
|
||||
:param station: 本站点对象
|
||||
:param ratio: 比例
|
||||
:param reso: 分辨率,1:reso
|
||||
:return: 两个方向的站牌
|
||||
"""
|
||||
if ratio is None:
|
||||
ratio = [4, 1]
|
||||
width, height = ratio[0] * reso, ratio[1] * reso
|
||||
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.YELLOW))
|
||||
# 加黑色图框
|
||||
baseCanvas.linePanel = Panel(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.8, 0.15),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
)
|
||||
|
||||
# 直线块
|
||||
baseCanvas.linePanel.recLine = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=Shape.rectangle(
|
||||
size=(10, 10),
|
||||
fill=line.color,
|
||||
),
|
||||
keep_ratio=False
|
||||
)
|
||||
# 灰色直线块
|
||||
baseCanvas.linePanel.recLineGrey = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=Shape.rectangle(
|
||||
size=(10, 10),
|
||||
fill=Color.GREY,
|
||||
),
|
||||
keep_ratio=False
|
||||
)
|
||||
# 生成各站圆点
|
||||
outline_width = 40
|
||||
circleForward = Shape.circular(
|
||||
radius=200,
|
||||
fill=Color.WHITE,
|
||||
width=outline_width,
|
||||
outline=line.color,
|
||||
)
|
||||
|
||||
circleThisPanel = Canvas(Image.new("RGBA", (200, 200), (0, 0, 0, 0)))
|
||||
circleThisPanel.circleOuter = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1, 1),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=Shape.circular(
|
||||
radius=200,
|
||||
fill=Color.WHITE,
|
||||
width=outline_width,
|
||||
outline=line.color,
|
||||
),
|
||||
)
|
||||
circleThisPanel.circleOuter.circleInner = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(0.7, 0.7),
|
||||
parent_point=(0.5, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=Shape.circular(
|
||||
radius=200,
|
||||
fill=line.color,
|
||||
width=0,
|
||||
outline=line.color,
|
||||
),
|
||||
)
|
||||
|
||||
circleThisPanel.export("a.png", alpha=True)
|
||||
circleThis = circleThisPanel.base_img
|
||||
|
||||
circlePassed = Shape.circular(
|
||||
radius=200,
|
||||
fill=Color.WHITE,
|
||||
width=outline_width,
|
||||
outline=Color.GREY,
|
||||
)
|
||||
|
||||
arrival = False
|
||||
distance = 1 / (len(line.stations) - 1)
|
||||
for i, sta in enumerate(line.stations):
|
||||
box_size = (1.618, 1.618)
|
||||
if sta.identifier == station.identifier:
|
||||
arrival = True
|
||||
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=(1.8, 1.8),
|
||||
parent_point=(distance * i, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=circleThis,
|
||||
keep_ratio=True
|
||||
)
|
||||
continue
|
||||
if arrival:
|
||||
# 后方站绘制
|
||||
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=box_size,
|
||||
parent_point=(distance * i, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=circleForward,
|
||||
keep_ratio=True
|
||||
)
|
||||
else:
|
||||
# 前方站绘制
|
||||
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
|
||||
uv_size=(1, 1),
|
||||
box_size=box_size,
|
||||
parent_point=(distance * i, 0.5),
|
||||
point=(0.5, 0.5),
|
||||
img=circlePassed,
|
||||
keep_ratio=True
|
||||
)
|
||||
return baseCanvas
|
||||
|
||||
|
||||
def generate_platform_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
|
||||
reso: int = resolution
|
||||
):
|
||||
pass
|
||||
|
||||
# def main():
|
||||
# generate_entrance_sign(
|
||||
# "璧山",
|
||||
# aliases=["Bishan"],
|
||||
# lineInfo=[
|
||||
#
|
||||
# Line(identifier="2", chineseName="1号线", englishName="Line 1", color=Color.RED, stations=[]),
|
||||
# Line(identifier="3", chineseName="27号线", englishName="Line 27", color="#685bc7", stations=[]),
|
||||
# Line(identifier="1", chineseName="璧铜线", englishName="BT Line", color="#685BC7", stations=[]),
|
||||
# ],
|
||||
# entranceIdentifier="1",
|
||||
# ratio=(8, 1)
|
||||
# )
|
||||
#
|
||||
#
|
||||
# main()
|
@@ -1,125 +0,0 @@
|
||||
import nonebot
|
||||
from nonebot import on_message, require
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from liteyuki.utils.base.data import Database, LiteModel
|
||||
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||
from liteyuki.utils.message.message import MarkdownMessage as md
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from nonebot_plugin_alconna import on_alconna
|
||||
from arclet.alconna import Arparma, Alconna, Args, Option, Subcommand
|
||||
|
||||
|
||||
class Node(LiteModel):
|
||||
TABLE_NAME: str = "node"
|
||||
bot_id: str = ""
|
||||
session_type: str = ""
|
||||
session_id: str = ""
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.bot_id}.{self.session_type}.{self.session_id}"
|
||||
|
||||
|
||||
class Push(LiteModel):
|
||||
TABLE_NAME: str = "push"
|
||||
source: Node = Node()
|
||||
target: Node = Node()
|
||||
inde: int = 0
|
||||
|
||||
|
||||
pushes_db = Database("data/pushes.ldb")
|
||||
pushes_db.auto_migrate(Push(), Node())
|
||||
|
||||
alc = Alconna(
|
||||
"lep",
|
||||
Subcommand(
|
||||
"add",
|
||||
Args["source", str],
|
||||
Args["target", str],
|
||||
Option("bidirectional", Args["bidirectional", bool])
|
||||
),
|
||||
Subcommand(
|
||||
"rm",
|
||||
Args["index", int],
|
||||
|
||||
),
|
||||
Subcommand(
|
||||
"list",
|
||||
)
|
||||
)
|
||||
|
||||
add_push = on_alconna(alc)
|
||||
|
||||
|
||||
@add_push.handle()
|
||||
async def _(result: Arparma):
|
||||
"""bot_id.session_type.session_id"""
|
||||
if result.subcommands.get("add"):
|
||||
source = result.subcommands["add"].args.get("source")
|
||||
target = result.subcommands["add"].args.get("target")
|
||||
if source and target:
|
||||
source = source.split(".")
|
||||
target = target.split(".")
|
||||
push1 = Push(
|
||||
source=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
|
||||
target=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
|
||||
inde=len(pushes_db.all(Push(), default=[]))
|
||||
)
|
||||
pushes_db.save(push1)
|
||||
|
||||
if result.subcommands["add"].args.get("bidirectional"):
|
||||
push2 = Push(
|
||||
source=Node(bot_id=target[0], session_type=target[1], session_id=target[2]),
|
||||
target=Node(bot_id=source[0], session_type=source[1], session_id=source[2]),
|
||||
inde=len(pushes_db.all(Push(), default=[]))
|
||||
)
|
||||
pushes_db.save(push2)
|
||||
await add_push.finish("添加成功")
|
||||
else:
|
||||
await add_push.finish("参数缺失")
|
||||
elif result.subcommands.get("rm"):
|
||||
index = result.subcommands["rm"].args.get("index")
|
||||
if index is not None:
|
||||
try:
|
||||
pushes_db.delete(Push(), "inde = ?", index)
|
||||
await add_push.finish("删除成功")
|
||||
except IndexError:
|
||||
await add_push.finish("索引错误")
|
||||
else:
|
||||
await add_push.finish("参数缺失")
|
||||
elif result.subcommands.get("list"):
|
||||
await add_push.finish(
|
||||
"\n".join([f"{push.inde} {push.source.bot_id}.{push.source.session_type}.{push.source.session_id} -> "
|
||||
f"{push.target.bot_id}.{push.target.session_type}.{push.target.session_id}" for i, push in
|
||||
enumerate(pushes_db.all(Push(), default=[]))]))
|
||||
else:
|
||||
await add_push.finish("参数错误")
|
||||
|
||||
|
||||
@on_message(block=False).handle()
|
||||
async def _(event: T_MessageEvent, bot: T_Bot):
|
||||
for push in pushes_db.all(Push(), default=[]):
|
||||
if str(push.source) == f"{bot.self_id}.{event.message_type}.{event.user_id if event.message_type == 'private' else event.group_id}":
|
||||
bot2 = nonebot.get_bot(push.target.bot_id)
|
||||
msg_formatted = ""
|
||||
for line in str(event.message).split("\n"):
|
||||
msg_formatted += f"**{line.strip()}**\n"
|
||||
push_message = (
|
||||
f"> From {event.sender.nickname}@{push.source.session_type}.{push.source.session_id}\n> Bot {bot.self_id}\n\n"
|
||||
f"{msg_formatted}")
|
||||
await md.send_md(push_message, bot2, message_type=push.target.session_type,
|
||||
session_id=push.target.session_id)
|
||||
return
|
||||
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="轻雪事件推送",
|
||||
description="事件推送插件,支持单向和双向推送,支持跨Bot推送",
|
||||
usage="",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki": True,
|
||||
}
|
||||
)
|
@@ -1,61 +0,0 @@
|
||||
from nonebot import on_command, require
|
||||
from nonebot.adapters.onebot.v11 import MessageSegment
|
||||
from nonebot.params import CommandArg
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent, v11
|
||||
from liteyuki.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
|
||||
from liteyuki.utils.message.html_tool import *
|
||||
|
||||
md_test = on_command("mdts", permission=SUPERUSER)
|
||||
btn_test = on_command("btnts", permission=SUPERUSER)
|
||||
latex_test = on_command("latex", permission=SUPERUSER)
|
||||
|
||||
placeholder = {
|
||||
"[": "[",
|
||||
"]": "]",
|
||||
"&": "&",
|
||||
",": ",",
|
||||
"\n" : r"\n",
|
||||
"\"" : r'\\\"'
|
||||
}
|
||||
|
||||
|
||||
@md_test.handle()
|
||||
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
|
||||
await md.send_md(
|
||||
str(arg),
|
||||
bot,
|
||||
message_type=event.message_type,
|
||||
session_id=event.user_id if event.message_type == "private" else event.group_id
|
||||
)
|
||||
|
||||
|
||||
@btn_test.handle()
|
||||
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
|
||||
await md.send_btn(
|
||||
str(arg),
|
||||
bot,
|
||||
message_type=event.message_type,
|
||||
session_id=event.user_id if event.message_type == "private" else event.group_id
|
||||
)
|
||||
|
||||
|
||||
@latex_test.handle()
|
||||
async def _(bot: T_Bot, event: T_MessageEvent, arg: v11.Message = CommandArg()):
|
||||
latex_text = f"$${str(arg)}$$"
|
||||
img = await md_to_pic(latex_text)
|
||||
await bot.send(event=event, message=MessageSegment.image(img))
|
||||
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="轻雪Markdown测试",
|
||||
description="用于测试Markdown的插件",
|
||||
usage="",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki": True,
|
||||
}
|
||||
)
|
@@ -1,14 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="Minecraft工具箱",
|
||||
description="一些Minecraft相关工具箱",
|
||||
usage="我觉得你应该会用",
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki" : True,
|
||||
"toggleable" : True,
|
||||
"default_enable": True,
|
||||
}
|
||||
)
|
@@ -1,15 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from .minesweeper import *
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="轻雪小游戏",
|
||||
description="内置了一些小游戏",
|
||||
usage="",
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki": True,
|
||||
"toggleable" : True,
|
||||
"default_enable" : True,
|
||||
}
|
||||
)
|
@@ -1,169 +0,0 @@
|
||||
import random
|
||||
from pydantic import BaseModel
|
||||
from liteyuki.utils.message.message import MarkdownMessage as md
|
||||
|
||||
class Dot(BaseModel):
|
||||
row: int
|
||||
col: int
|
||||
mask: bool = True
|
||||
value: int = 0
|
||||
flagged: bool = False
|
||||
|
||||
|
||||
class Minesweeper:
|
||||
# 0-8: number of mines around, 9: mine, -1: undefined
|
||||
NUMS = "⓪①②③④⑤⑥⑦⑧🅑⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
|
||||
MASK = "🅜"
|
||||
FLAG = "🅕"
|
||||
MINE = "🅑"
|
||||
|
||||
def __init__(self, rows, cols, num_mines, session_type, session_id):
|
||||
assert rows > 0 and cols > 0 and 0 < num_mines < rows * cols
|
||||
self.session_type = session_type
|
||||
self.session_id = session_id
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.num_mines = num_mines
|
||||
self.board: list[list[Dot]] = [[Dot(row=i, col=j) for j in range(cols)] for i in range(rows)]
|
||||
self.is_first = True
|
||||
|
||||
def reveal(self, row, col) -> bool:
|
||||
"""
|
||||
展开
|
||||
Args:
|
||||
row:
|
||||
col:
|
||||
|
||||
Returns:
|
||||
游戏是否继续
|
||||
|
||||
"""
|
||||
|
||||
if self.is_first:
|
||||
# 第一次展开,生成地雷
|
||||
self.generate_board(self.board[row][col])
|
||||
self.is_first = False
|
||||
|
||||
if self.board[row][col].value == 9:
|
||||
self.board[row][col].mask = False
|
||||
return False
|
||||
|
||||
if not self.board[row][col].mask:
|
||||
return True
|
||||
|
||||
self.board[row][col].mask = False
|
||||
|
||||
if self.board[row][col].value == 0:
|
||||
self.reveal_neighbors(row, col)
|
||||
return True
|
||||
|
||||
def is_win(self) -> bool:
|
||||
"""
|
||||
是否胜利
|
||||
Returns:
|
||||
"""
|
||||
for row in range(self.rows):
|
||||
for col in range(self.cols):
|
||||
if self.board[row][col].mask and self.board[row][col].value != 9:
|
||||
return False
|
||||
return True
|
||||
|
||||
def generate_board(self, first_dot: Dot):
|
||||
"""
|
||||
避开第一个点,生成地雷
|
||||
Args:
|
||||
first_dot: 第一个点
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
generate_count = 0
|
||||
while generate_count < self.num_mines:
|
||||
row = random.randint(0, self.rows - 1)
|
||||
col = random.randint(0, self.cols - 1)
|
||||
if self.board[row][col].value == 9 or (row, col) == (first_dot.row, first_dot.col):
|
||||
continue
|
||||
self.board[row][col] = Dot(row=row, col=col, mask=True, value=9)
|
||||
generate_count += 1
|
||||
|
||||
for row in range(self.rows):
|
||||
for col in range(self.cols):
|
||||
if self.board[row][col].value != 9:
|
||||
self.board[row][col].value = self.count_adjacent_mines(row, col)
|
||||
|
||||
def count_adjacent_mines(self, row, col):
|
||||
"""
|
||||
计算周围地雷数量
|
||||
Args:
|
||||
row:
|
||||
col:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
count = 0
|
||||
for r in range(max(0, row - 1), min(self.rows, row + 2)):
|
||||
for c in range(max(0, col - 1), min(self.cols, col + 2)):
|
||||
if self.board[r][c].value == 9:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def reveal_neighbors(self, row, col):
|
||||
"""
|
||||
递归展开,使用深度优先搜索
|
||||
Args:
|
||||
row:
|
||||
col:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
for r in range(max(0, row - 1), min(self.rows, row + 2)):
|
||||
for c in range(max(0, col - 1), min(self.cols, col + 2)):
|
||||
if self.board[r][c].mask:
|
||||
self.board[r][c].mask = False
|
||||
if self.board[r][c].value == 0:
|
||||
self.reveal_neighbors(r, c)
|
||||
|
||||
def mark(self, row, col) -> bool:
|
||||
"""
|
||||
标记
|
||||
Args:
|
||||
row:
|
||||
col:
|
||||
Returns:
|
||||
是否标记成功,如果已经展开则无法标记
|
||||
"""
|
||||
if self.board[row][col].mask:
|
||||
self.board[row][col].flagged = not self.board[row][col].flagged
|
||||
return self.board[row][col].flagged
|
||||
|
||||
def board_markdown(self) -> str:
|
||||
"""
|
||||
打印地雷板
|
||||
Returns:
|
||||
"""
|
||||
dis = " "
|
||||
start = "> " if self.cols >= 10 else ""
|
||||
text = start + self.NUMS[0] + dis*2
|
||||
# 横向两个雷之间的间隔字符
|
||||
# 生成横向索引
|
||||
for i in range(self.cols):
|
||||
text += f"{self.NUMS[i]}" + dis
|
||||
text += "\n\n"
|
||||
for i, row in enumerate(self.board):
|
||||
text += start + f"{self.NUMS[i]}" + dis*2
|
||||
print([d.value for d in row])
|
||||
for dot in row:
|
||||
if dot.mask and not dot.flagged:
|
||||
text += md.btn_cmd(self.MASK, f"minesweeper reveal {dot.row} {dot.col}")
|
||||
elif dot.flagged:
|
||||
text += md.btn_cmd(self.FLAG, f"minesweeper mark {dot.row} {dot.col}")
|
||||
else:
|
||||
text += self.NUMS[dot.value]
|
||||
text += dis
|
||||
text += "\n"
|
||||
btn_mark = md.btn_cmd("标记", f"minesweeper mark ", enter=False)
|
||||
btn_end = md.btn_cmd("结束", "minesweeper end", enter=True)
|
||||
text += f" {btn_mark} {btn_end}"
|
||||
return text
|
@@ -1,103 +0,0 @@
|
||||
from nonebot import require
|
||||
|
||||
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||
from liteyuki.utils.message.message import MarkdownMessage as md
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from .game import Minesweeper
|
||||
|
||||
from nonebot_plugin_alconna import Alconna, on_alconna, Subcommand, Args, Arparma
|
||||
|
||||
minesweeper = on_alconna(
|
||||
aliases={"扫雷"},
|
||||
command=Alconna(
|
||||
"minesweeper",
|
||||
Subcommand(
|
||||
"start",
|
||||
Args["row", int, 8]["col", int, 8]["mines", int, 10],
|
||||
alias=["开始"],
|
||||
|
||||
),
|
||||
Subcommand(
|
||||
"end",
|
||||
alias=["结束"]
|
||||
),
|
||||
Subcommand(
|
||||
"reveal",
|
||||
Args["row", int]["col", int],
|
||||
alias=["展开"]
|
||||
|
||||
),
|
||||
Subcommand(
|
||||
"mark",
|
||||
Args["row", int]["col", int],
|
||||
alias=["标记"]
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
minesweeper_cache: list[Minesweeper] = []
|
||||
|
||||
|
||||
def get_minesweeper_cache(event: T_MessageEvent) -> Minesweeper | None:
|
||||
for i in minesweeper_cache:
|
||||
if i.session_type == event.message_type:
|
||||
if i.session_id == event.user_id or i.session_id == event.group_id:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
@minesweeper.handle()
|
||||
async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot):
|
||||
game = get_minesweeper_cache(event)
|
||||
if result.subcommands.get("start"):
|
||||
if game:
|
||||
await minesweeper.finish("当前会话不能同时进行多个扫雷游戏")
|
||||
else:
|
||||
try:
|
||||
new_game = Minesweeper(
|
||||
rows=result.subcommands["start"].args["row"],
|
||||
cols=result.subcommands["start"].args["col"],
|
||||
num_mines=result.subcommands["start"].args["mines"],
|
||||
session_type=event.message_type,
|
||||
session_id=event.user_id if event.message_type == "private" else event.group_id,
|
||||
)
|
||||
minesweeper_cache.append(new_game)
|
||||
await minesweeper.send("游戏开始")
|
||||
await md.send_md(new_game.board_markdown(), bot, event=event)
|
||||
except AssertionError:
|
||||
await minesweeper.finish("参数错误")
|
||||
elif result.subcommands.get("end"):
|
||||
if game:
|
||||
minesweeper_cache.remove(game)
|
||||
await minesweeper.finish("游戏结束")
|
||||
else:
|
||||
await minesweeper.finish("当前没有扫雷游戏")
|
||||
elif result.subcommands.get("reveal"):
|
||||
if not game:
|
||||
await minesweeper.finish("当前没有扫雷游戏")
|
||||
else:
|
||||
row = result.subcommands["reveal"].args["row"]
|
||||
col = result.subcommands["reveal"].args["col"]
|
||||
if not (0 <= row < game.rows and 0 <= col < game.cols):
|
||||
await minesweeper.finish("参数错误")
|
||||
if not game.reveal(row, col):
|
||||
minesweeper_cache.remove(game)
|
||||
await md.send_md(game.board_markdown(), bot, event=event)
|
||||
await minesweeper.finish("游戏结束")
|
||||
await md.send_md(game.board_markdown(), bot, event=event)
|
||||
if game.is_win():
|
||||
minesweeper_cache.remove(game)
|
||||
await minesweeper.finish("游戏胜利")
|
||||
elif result.subcommands.get("mark"):
|
||||
if not game:
|
||||
await minesweeper.finish("当前没有扫雷游戏")
|
||||
else:
|
||||
row = result.subcommands["mark"].args["row"]
|
||||
col = result.subcommands["mark"].args["col"]
|
||||
if not (0 <= row < game.rows and 0 <= col < game.cols):
|
||||
await minesweeper.finish("参数错误")
|
||||
game.board[row][col].flagged = not game.board[row][col].flagged
|
||||
await md.send_md(game.board_markdown(), bot, event=event)
|
||||
else:
|
||||
await minesweeper.finish("参数错误")
|
@@ -1,22 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from .npm import *
|
||||
from .rpm import *
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="轻雪包管理器",
|
||||
description="本地插件管理和插件商店支持,资源包管理,支持启用/停用,安装/卸载插件",
|
||||
usage=(
|
||||
"npm list\n"
|
||||
"npm enable/disable <plugin_name>\n"
|
||||
"npm search <keywords...>\n"
|
||||
"npm install/uninstall <plugin_name>\n"
|
||||
),
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki": True,
|
||||
"toggleable" : False,
|
||||
"default_enable" : True,
|
||||
}
|
||||
)
|
@@ -1,245 +0,0 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
import aiofiles
|
||||
import nonebot.plugin
|
||||
|
||||
from liteyuki.utils.base.data import LiteModel
|
||||
from liteyuki.utils.base.data_manager import GlobalPlugin, Group, User, group_db, plugin_db, user_db
|
||||
from liteyuki.utils.base.ly_typing import T_MessageEvent
|
||||
|
||||
__group_data = {} # 群数据缓存, {group_id: Group}
|
||||
__user_data = {} # 用户数据缓存, {user_id: User}
|
||||
__default_enable = {} # 插件默认启用状态缓存, {plugin_name: bool} static
|
||||
__global_enable = {} # 插件全局启用状态缓存, {plugin_name: bool} dynamic
|
||||
|
||||
|
||||
class PluginTag(LiteModel):
|
||||
label: str
|
||||
color: str = '#000000'
|
||||
|
||||
|
||||
class StorePlugin(LiteModel):
|
||||
name: str
|
||||
desc: str
|
||||
module_name: str # 插件商店中的模块名不等于本地的模块名,前者是文件夹名,后者是点分割模块名
|
||||
project_link: str = ""
|
||||
homepage: str = ""
|
||||
author: str = ""
|
||||
type: str | None = None
|
||||
version: str | None = ""
|
||||
time: str = ""
|
||||
tags: list[PluginTag] = []
|
||||
is_official: bool = False
|
||||
|
||||
|
||||
def get_plugin_exist(plugin_name: str) -> bool:
|
||||
"""
|
||||
获取插件是否存在于加载列表
|
||||
Args:
|
||||
plugin_name:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
for plugin in nonebot.plugin.get_loaded_plugins():
|
||||
if plugin.name == plugin_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def get_store_plugin(plugin_name: str) -> Optional[StorePlugin]:
|
||||
"""
|
||||
获取插件信息
|
||||
|
||||
Args:
|
||||
plugin_name (str): 插件模块名
|
||||
|
||||
Returns:
|
||||
Optional[StorePlugin]: 插件信息
|
||||
"""
|
||||
async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f:
|
||||
plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())]
|
||||
for plugin in plugins:
|
||||
if plugin.module_name == plugin_name:
|
||||
return plugin
|
||||
return None
|
||||
|
||||
|
||||
def get_plugin_default_enable(plugin_name: str) -> bool:
|
||||
"""
|
||||
获取插件默认启用状态,由插件定义,不存在则默认为启用,优先从缓存中获取
|
||||
|
||||
Args:
|
||||
plugin_name (str): 插件模块名
|
||||
|
||||
Returns:
|
||||
bool: 插件默认状态
|
||||
"""
|
||||
if plugin_name not in __default_enable:
|
||||
plug = nonebot.plugin.get_plugin(plugin_name)
|
||||
default_enable = (plug.metadata.extra.get("default_enable", True) if plug.metadata else True) if plug else True
|
||||
__default_enable[plugin_name] = default_enable
|
||||
|
||||
return __default_enable[plugin_name]
|
||||
|
||||
|
||||
def get_plugin_session_enable(event: T_MessageEvent, plugin_name: str) -> bool:
|
||||
"""
|
||||
获取插件当前会话启用状态
|
||||
|
||||
Args:
|
||||
event: 会话事件
|
||||
plugin_name (str): 插件模块名
|
||||
|
||||
Returns:
|
||||
bool: 插件当前状态
|
||||
"""
|
||||
if event.message_type == "group":
|
||||
group_id = str(event.group_id)
|
||||
if group_id not in __group_data:
|
||||
group: Group = group_db.first(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
|
||||
__group_data[str(event.group_id)] = group
|
||||
|
||||
session = __group_data[group_id]
|
||||
else:
|
||||
# session: User = user_db.first(User(), "user_id = ?", event.user_id, default=User(user_id=str(event.user_id)))
|
||||
user_id = str(event.user_id)
|
||||
if user_id not in __user_data:
|
||||
user: User = user_db.first(User(), "user_id = ?", user_id, default=User(user_id=user_id))
|
||||
__user_data[user_id] = user
|
||||
session = __user_data[user_id]
|
||||
# 默认停用插件在启用列表内表示启用
|
||||
# 默认停用插件不在启用列表内表示停用
|
||||
# 默认启用插件在停用列表内表示停用
|
||||
# 默认启用插件不在停用列表内表示启用
|
||||
default_enable = get_plugin_default_enable(plugin_name)
|
||||
if default_enable:
|
||||
return plugin_name not in session.disabled_plugins
|
||||
else:
|
||||
return plugin_name in session.enabled_plugins
|
||||
|
||||
|
||||
def set_plugin_session_enable(event: T_MessageEvent, plugin_name: str, enable: bool):
|
||||
"""
|
||||
设置插件会话启用状态,同时更新数据库和缓存
|
||||
Args:
|
||||
event:
|
||||
plugin_name:
|
||||
enable:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if event.message_type == "group":
|
||||
session = group_db.first(Group(), "group_id = ?", str(event.group_id), default=Group(group_id=str(event.group_id)))
|
||||
else:
|
||||
session = user_db.first(User(), "user_id = ?", str(event.user_id), default=User(user_id=str(event.user_id)))
|
||||
default_enable = get_plugin_default_enable(plugin_name)
|
||||
if default_enable:
|
||||
if enable:
|
||||
session.disabled_plugins.remove(plugin_name)
|
||||
else:
|
||||
session.disabled_plugins.append(plugin_name)
|
||||
else:
|
||||
if enable:
|
||||
session.enabled_plugins.append(plugin_name)
|
||||
else:
|
||||
session.enabled_plugins.remove(plugin_name)
|
||||
|
||||
if event.message_type == "group":
|
||||
__group_data[str(event.group_id)] = session
|
||||
print(session)
|
||||
group_db.save(session)
|
||||
else:
|
||||
__user_data[str(event.user_id)] = session
|
||||
user_db.save(session)
|
||||
|
||||
|
||||
def get_plugin_global_enable(plugin_name: str) -> bool:
|
||||
"""
|
||||
获取插件全局启用状态, 优先从缓存中获取
|
||||
Args:
|
||||
plugin_name:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if plugin_name not in __global_enable:
|
||||
plugin = plugin_db.first(
|
||||
GlobalPlugin(),
|
||||
"module_name = ?",
|
||||
plugin_name,
|
||||
default=GlobalPlugin(module_name=plugin_name, enabled=True))
|
||||
__global_enable[plugin_name] = plugin.enabled
|
||||
|
||||
return __global_enable[plugin_name]
|
||||
|
||||
|
||||
def set_plugin_global_enable(plugin_name: str, enable: bool):
|
||||
"""
|
||||
设置插件全局启用状态,同时更新数据库和缓存
|
||||
Args:
|
||||
plugin_name:
|
||||
enable:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
plugin = plugin_db.first(
|
||||
GlobalPlugin(),
|
||||
"module_name = ?",
|
||||
plugin_name,
|
||||
default=GlobalPlugin(module_name=plugin_name, enabled=True))
|
||||
plugin.enabled = enable
|
||||
|
||||
plugin_db.save(plugin)
|
||||
__global_enable[plugin_name] = enable
|
||||
|
||||
|
||||
def get_plugin_can_be_toggle(plugin_name: str) -> bool:
|
||||
"""
|
||||
获取插件是否可以被启用/停用
|
||||
|
||||
Args:
|
||||
plugin_name (str): 插件模块名
|
||||
|
||||
Returns:
|
||||
bool: 插件是否可以被启用/停用
|
||||
"""
|
||||
plug = nonebot.plugin.get_plugin(plugin_name)
|
||||
return plug.metadata.extra.get("toggleable", True) if plug and plug.metadata else True
|
||||
|
||||
|
||||
def get_group_enable(group_id: str) -> bool:
|
||||
"""
|
||||
获取群组是否启用插机器人
|
||||
|
||||
Args:
|
||||
group_id (str): 群组ID
|
||||
|
||||
Returns:
|
||||
bool: 群组是否启用插件
|
||||
"""
|
||||
group_id = str(group_id)
|
||||
if group_id not in __group_data:
|
||||
group: Group = group_db.first(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
|
||||
__group_data[group_id] = group
|
||||
|
||||
return __group_data[group_id].enable
|
||||
|
||||
|
||||
def set_group_enable(group_id: str, enable: bool):
|
||||
"""
|
||||
设置群组是否启用插机器人
|
||||
|
||||
Args:
|
||||
group_id (str): 群组ID
|
||||
enable (bool): 是否启用
|
||||
"""
|
||||
group_id = str(group_id)
|
||||
group: Group = group_db.first(Group(), "group_id = ?", group_id, default=Group(group_id=group_id))
|
||||
group.enable = enable
|
||||
|
||||
__group_data[group_id] = group
|
||||
group_db.save(group)
|
@@ -1,641 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import aiohttp
|
||||
import nonebot.plugin
|
||||
import pip
|
||||
from io import StringIO
|
||||
from arclet.alconna import MultiVar
|
||||
from nonebot import Bot, require
|
||||
from nonebot.exception import FinishedException, IgnoredException, MockApiException
|
||||
from nonebot.internal.adapter import Event
|
||||
from nonebot.internal.matcher import Matcher
|
||||
from nonebot.message import run_preprocessor
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import Plugin, PluginMetadata
|
||||
from nonebot.utils import run_sync
|
||||
|
||||
from liteyuki.utils.base.data_manager import InstalledPlugin
|
||||
from liteyuki.utils.base.language import get_user_lang
|
||||
from liteyuki.utils.base.ly_typing import T_Bot
|
||||
from liteyuki.utils.message.message import MarkdownMessage as md
|
||||
from liteyuki.utils.message.markdown import MarkdownComponent as mdc, compile_md, escape_md
|
||||
from liteyuki.utils.base.permission import GROUP_ADMIN, GROUP_OWNER
|
||||
from liteyuki.utils.message.tools import clamp
|
||||
from .common import *
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Arparma, Subcommand
|
||||
|
||||
# const
|
||||
enable_global = "enable-global"
|
||||
disable_global = "disable-global"
|
||||
enable = "enable"
|
||||
disable = "disable"
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"插件"},
|
||||
command=Alconna(
|
||||
"npm",
|
||||
Subcommand(
|
||||
"enable",
|
||||
Args["plugin_name", str],
|
||||
alias=["e", "启用"],
|
||||
),
|
||||
Subcommand(
|
||||
"disable",
|
||||
Args["plugin_name", str],
|
||||
alias=["d", "停用"],
|
||||
),
|
||||
Subcommand(
|
||||
enable_global,
|
||||
Args["plugin_name", str],
|
||||
alias=["eg", "全局启用"],
|
||||
),
|
||||
Subcommand(
|
||||
disable_global,
|
||||
Args["plugin_name", str],
|
||||
alias=["dg", "全局停用"],
|
||||
),
|
||||
# 安装部分
|
||||
Subcommand(
|
||||
"update",
|
||||
alias=["u", "更新"],
|
||||
),
|
||||
Subcommand(
|
||||
"search",
|
||||
Args["keywords", MultiVar(str)],
|
||||
alias=["s", "搜索"],
|
||||
),
|
||||
Subcommand(
|
||||
"install",
|
||||
Args["plugin_name", str],
|
||||
alias=["i", "安装"],
|
||||
),
|
||||
Subcommand(
|
||||
"uninstall",
|
||||
Args["plugin_name", str],
|
||||
alias=["r", "rm", "卸载"],
|
||||
),
|
||||
Subcommand(
|
||||
"list",
|
||||
Args["page", int, 1]["num", int, 10],
|
||||
alias=["ls", "列表"],
|
||||
)
|
||||
)
|
||||
).handle()
|
||||
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
|
||||
if not os.path.exists("data/liteyuki/plugins.json"):
|
||||
await npm_update()
|
||||
# 判断会话类型
|
||||
ulang = get_user_lang(str(event.user_id))
|
||||
plugin_name = result.args.get("plugin_name")
|
||||
sc = result.subcommands # 获取子命令
|
||||
perm_s = await SUPERUSER(bot, event) # 判断是否为超级用户
|
||||
# 支持对自定义command_start的判断
|
||||
if sc.get("enable") or result.subcommands.get("disable"):
|
||||
|
||||
toggle = result.subcommands.get("enable") is not None
|
||||
|
||||
plugin_exist = get_plugin_exist(plugin_name)
|
||||
|
||||
session_enable = get_plugin_session_enable(event, plugin_name) # 获取插件当前状态
|
||||
|
||||
can_be_toggled = get_plugin_can_be_toggle(plugin_name) # 获取插件是否可以被启用/停用
|
||||
|
||||
if not plugin_exist:
|
||||
await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
|
||||
|
||||
if not can_be_toggled:
|
||||
await npm.finish(ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_name))
|
||||
|
||||
if session_enable == toggle:
|
||||
await npm.finish(
|
||||
ulang.get("npm.plugin_already", NAME=plugin_name, STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable")))
|
||||
|
||||
if event.message_type == "private":
|
||||
session = user_db.first(User(), "user_id = ?", event.user_id, default=User(user_id=event.user_id))
|
||||
else:
|
||||
if await GROUP_ADMIN(bot, event) or await GROUP_OWNER(bot, event) or await SUPERUSER(bot, event):
|
||||
session = group_db.first(Group(), "group_id = ?", event.group_id, default=Group(group_id=str(event.group_id)))
|
||||
else:
|
||||
raise FinishedException(ulang.get("Permission Denied"))
|
||||
try:
|
||||
set_plugin_session_enable(event, plugin_name, toggle)
|
||||
except Exception as e:
|
||||
nonebot.logger.error(e)
|
||||
await npm.finish(
|
||||
ulang.get(
|
||||
"npm.toggle_failed",
|
||||
NAME=plugin_name,
|
||||
STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"),
|
||||
ERROR=str(e))
|
||||
)
|
||||
|
||||
await npm.finish(
|
||||
ulang.get(
|
||||
"npm.toggle_success",
|
||||
NAME=plugin_name,
|
||||
STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"))
|
||||
)
|
||||
|
||||
elif sc.get(enable_global) or result.subcommands.get(disable_global) and await SUPERUSER(bot, event):
|
||||
plugin_exist = get_plugin_exist(plugin_name)
|
||||
|
||||
toggle = result.subcommands.get(enable_global) is not None
|
||||
|
||||
can_be_toggled = get_plugin_can_be_toggle(plugin_name)
|
||||
|
||||
if not plugin_exist:
|
||||
await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
|
||||
|
||||
if not can_be_toggled:
|
||||
await npm.finish(ulang.get("npm.plugin_cannot_be_toggled", NAME=plugin_name))
|
||||
|
||||
global_enable = get_plugin_global_enable(plugin_name)
|
||||
if global_enable == toggle:
|
||||
await npm.finish(
|
||||
ulang.get("npm.plugin_already", NAME=plugin_name, STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable")))
|
||||
|
||||
try:
|
||||
set_plugin_global_enable(plugin_name, toggle)
|
||||
except Exception as e:
|
||||
await npm.finish(
|
||||
ulang.get(
|
||||
"npm.toggle_failed",
|
||||
NAME=plugin_name,
|
||||
STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"),
|
||||
ERROR=str(e))
|
||||
)
|
||||
|
||||
await npm.finish(
|
||||
ulang.get(
|
||||
"npm.toggle_success",
|
||||
NAME=plugin_name,
|
||||
STATUS=ulang.get("npm.enable") if toggle else ulang.get("npm.disable"))
|
||||
)
|
||||
|
||||
elif sc.get("update") and perm_s:
|
||||
r = await npm_update()
|
||||
if r:
|
||||
await npm.finish(ulang.get("npm.store_update_success"))
|
||||
else:
|
||||
await npm.finish(ulang.get("npm.store_update_failed"))
|
||||
|
||||
elif sc.get("search"):
|
||||
keywords: list[str] = result.subcommands["search"].args.get("keywords")
|
||||
rs = await npm_search(keywords)
|
||||
max_show = 10
|
||||
if len(rs):
|
||||
reply = f"{ulang.get('npm.search_result')} | {ulang.get('npm.total', TOTAL=len(rs))}\n***"
|
||||
for storePlugin in rs[:min(max_show, len(rs))]:
|
||||
btn_install_or_update = md.btn_cmd(
|
||||
ulang.get("npm.update") if get_plugin_exist(storePlugin.module_name) else ulang.get("npm.install"),
|
||||
"npm install %s" % storePlugin.module_name
|
||||
)
|
||||
link_page = md.btn_link(ulang.get("npm.homepage"), storePlugin.homepage)
|
||||
link_pypi = md.btn_link(ulang.get("npm.pypi"), storePlugin.homepage)
|
||||
|
||||
reply += (f"\n# **{storePlugin.name}**\n"
|
||||
f"\n> **{storePlugin.desc}**\n"
|
||||
f"\n> {ulang.get('npm.author')}: {storePlugin.author}"
|
||||
f"\n> *{md.escape(storePlugin.module_name)}*"
|
||||
f"\n> {btn_install_or_update} {link_page} {link_pypi}\n\n***\n")
|
||||
if len(rs) > max_show:
|
||||
reply += f"\n{ulang.get('npm.too_many_results', HIDE_NUM=len(rs) - max_show)}"
|
||||
else:
|
||||
reply = ulang.get("npm.search_no_result")
|
||||
await md.send_md(reply, bot, event=event)
|
||||
|
||||
elif sc.get("install") and perm_s:
|
||||
plugin_name: str = result.subcommands["install"].args.get("plugin_name")
|
||||
store_plugin = await get_store_plugin(plugin_name)
|
||||
await npm.send(ulang.get("npm.installing", NAME=plugin_name))
|
||||
|
||||
r, log = await npm_install(plugin_name)
|
||||
log = log.replace("\\", "/")
|
||||
|
||||
if not store_plugin:
|
||||
await npm.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
|
||||
|
||||
homepage_btn = md.btn_cmd(ulang.get("npm.homepage"), store_plugin.homepage)
|
||||
if r:
|
||||
r_load = nonebot.load_plugin(plugin_name) # 加载插件
|
||||
installed_plugin = InstalledPlugin(module_name=plugin_name) # 构造插件信息模型
|
||||
found_in_db_plugin = plugin_db.first(InstalledPlugin(), "module_name = ?", plugin_name) # 查询数据库中是否已经安装
|
||||
if r_load:
|
||||
if found_in_db_plugin is None:
|
||||
plugin_db.save(installed_plugin)
|
||||
info = md.escape(ulang.get("npm.install_success", NAME=store_plugin.name)) # markdown转义
|
||||
await md.send_md(
|
||||
f"{info}\n\n"
|
||||
f"```\n{log}\n```",
|
||||
bot,
|
||||
event=event
|
||||
)
|
||||
else:
|
||||
await npm.finish(ulang.get("npm.plugin_already_installed", NAME=store_plugin.name))
|
||||
else:
|
||||
info = ulang.get("npm.load_failed", NAME=plugin_name, HOMEPAGE=homepage_btn).replace("_", r"\\_")
|
||||
await md.send_md(
|
||||
f"{info}\n\n"
|
||||
f"```\n{log}\n```\n",
|
||||
bot,
|
||||
event=event
|
||||
)
|
||||
else:
|
||||
info = ulang.get("npm.install_failed", NAME=plugin_name, HOMEPAGE=homepage_btn).replace("_", r"\\_")
|
||||
await md.send_md(
|
||||
f"{info}\n\n"
|
||||
f"```\n{log}\n```",
|
||||
bot,
|
||||
event=event
|
||||
)
|
||||
|
||||
elif sc.get("uninstall") and perm_s:
|
||||
plugin_name: str = result.subcommands["uninstall"].args.get("plugin_name") # type: ignore
|
||||
found_installed_plugin: InstalledPlugin = plugin_db.first(InstalledPlugin(), "module_name = ?", plugin_name)
|
||||
if found_installed_plugin:
|
||||
plugin_db.delete(InstalledPlugin(), "module_name = ?", plugin_name)
|
||||
reply = f"{ulang.get('npm.uninstall_success', NAME=found_installed_plugin.module_name)}"
|
||||
await npm.finish(reply)
|
||||
else:
|
||||
await npm.finish(ulang.get("npm.plugin_not_installed", NAME=plugin_name))
|
||||
|
||||
elif sc.get("list"):
|
||||
loaded_plugin_list = sorted(nonebot.get_loaded_plugins(), key=lambda x: x.name)
|
||||
num_per_page = result.subcommands.get("list").args.get("num")
|
||||
total = len(loaded_plugin_list) // num_per_page + (1 if len(loaded_plugin_list) % num_per_page else 0)
|
||||
|
||||
page = clamp(result.subcommands.get("list").args.get("page"), 1, total)
|
||||
|
||||
# 已加载插件 | 总计10 | 第1/3页
|
||||
reply = (f"# {ulang.get('npm.loaded_plugins')} | "
|
||||
f"{ulang.get('npm.total', TOTAL=len(nonebot.get_loaded_plugins()))} | "
|
||||
f"{ulang.get('npm.page', PAGE=page, TOTAL=total)} \n***\n")
|
||||
|
||||
permission_oas = await GROUP_ADMIN(bot, event) or await GROUP_OWNER(bot, event) or await SUPERUSER(bot, event)
|
||||
permission_s = await SUPERUSER(bot, event)
|
||||
|
||||
for storePlugin in loaded_plugin_list[(page - 1) * num_per_page: min(page * num_per_page, len(loaded_plugin_list))]:
|
||||
# 检查是否有 metadata 属性
|
||||
# 添加帮助按钮
|
||||
|
||||
btn_usage = md.btn_cmd(ulang.get("npm.usage"), f"help {storePlugin.name}", False)
|
||||
store_plugin = await get_store_plugin(storePlugin.name)
|
||||
session_enable = get_plugin_session_enable(event, storePlugin.name)
|
||||
if store_plugin:
|
||||
# btn_homepage = md.btn_link(ulang.get("npm.homepage"), store_plugin.homepage)
|
||||
show_name = store_plugin.name
|
||||
elif storePlugin.metadata:
|
||||
# if storePlugin.metadata.extra.get("liteyuki"):
|
||||
# btn_homepage = md.btn_link(ulang.get("npm.homepage"), "https://github.com/snowykami/LiteyukiBot")
|
||||
# else:
|
||||
# btn_homepage = ulang.get("npm.homepage")
|
||||
show_name = storePlugin.metadata.name
|
||||
else:
|
||||
# btn_homepage = ulang.get("npm.homepage")
|
||||
show_name = storePlugin.name
|
||||
ulang.get("npm.no_description")
|
||||
|
||||
if storePlugin.metadata:
|
||||
reply += f"\n**{md.escape(show_name)}**\n"
|
||||
else:
|
||||
reply += f"**{md.escape(show_name)}**\n"
|
||||
|
||||
reply += f"\n > {btn_usage}"
|
||||
|
||||
if permission_oas:
|
||||
# 添加启用/停用插件按钮
|
||||
cmd_toggle = f"npm {'disable' if session_enable else 'enable'} {storePlugin.name}"
|
||||
text_toggle = ulang.get("npm.disable" if session_enable else "npm.enable")
|
||||
can_be_toggle = get_plugin_can_be_toggle(storePlugin.name)
|
||||
btn_toggle = text_toggle if not can_be_toggle else md.btn_cmd(text_toggle, cmd_toggle)
|
||||
reply += f" {btn_toggle}"
|
||||
|
||||
if permission_s:
|
||||
plugin_in_database = plugin_db.first(InstalledPlugin(), "module_name = ?", storePlugin.name)
|
||||
# 添加移除插件和全局切换按钮
|
||||
global_enable = get_plugin_global_enable(storePlugin.name)
|
||||
btn_uninstall = (
|
||||
md.btn_cmd(ulang.get("npm.uninstall"), f'npm uninstall {storePlugin.name}')) if plugin_in_database else ulang.get(
|
||||
'npm.uninstall')
|
||||
btn_toggle_global_text = ulang.get("npm.disable_global" if global_enable else "npm.enable_global")
|
||||
cmd_toggle_global = f"npm {'disable' if global_enable else 'enable'}-global {storePlugin.name}"
|
||||
btn_toggle_global = btn_toggle_global_text if not can_be_toggle else md.btn_cmd(btn_toggle_global_text, cmd_toggle_global)
|
||||
|
||||
reply += f" {btn_uninstall} {btn_toggle_global}"
|
||||
reply += "\n\n***\n"
|
||||
# 根据页数添加翻页按钮。第一页显示上一页文本而不是按钮,最后一页显示下一页文本而不是按钮
|
||||
btn_prev = md.btn_cmd(ulang.get("npm.prev_page"), f"npm list {page - 1} {num_per_page}") if page > 1 else ulang.get("npm.prev_page")
|
||||
btn_next = md.btn_cmd(ulang.get("npm.next_page"), f"npm list {page + 1} {num_per_page}") if page < total else ulang.get("npm.next_page")
|
||||
reply += f"\n{btn_prev} {page}/{total} {btn_next}"
|
||||
await md.send_md(reply, bot, event=event)
|
||||
|
||||
else:
|
||||
if await SUPERUSER(bot, event):
|
||||
btn_enable_global = md.btn_cmd(ulang.get("npm.enable_global"), "npm enable-global", False, False)
|
||||
btn_disable_global = md.btn_cmd(ulang.get("npm.disable_global"), "npm disable-global", False, False)
|
||||
btn_search = md.btn_cmd(ulang.get("npm.search"), "npm search ", False, False)
|
||||
btn_uninstall_ = md.btn_cmd(ulang.get("npm.uninstall"), "npm uninstall ", False, False)
|
||||
btn_install_ = md.btn_cmd(ulang.get("npm.install"), "npm install ", False, False)
|
||||
btn_update = md.btn_cmd(ulang.get("npm.update_index"), "npm update", False, True)
|
||||
btn_list = md.btn_cmd(ulang.get("npm.list_plugins"), "npm list ", False, False)
|
||||
btn_disable = md.btn_cmd(ulang.get("npm.disable_session"), "npm disable ", False, False)
|
||||
btn_enable = md.btn_cmd(ulang.get("npm.enable_session"), "npm enable ", False, False)
|
||||
reply = (
|
||||
f"\n# **{ulang.get('npm.help')}**"
|
||||
f"\n{btn_update}"
|
||||
f"\n\n>*{md.escape('npm update')}*\n"
|
||||
f"\n{btn_install_}"
|
||||
f"\n\n>*{md.escape('npm install <plugin_name')}*>\n"
|
||||
f"\n{btn_uninstall_}"
|
||||
f"\n\n>*{md.escape('npm uninstall <plugin_name')}*>\n"
|
||||
f"\n{btn_search}"
|
||||
f"\n\n>*{md.escape('npm search <keywords...')}*>\n"
|
||||
f"\n{btn_disable_global}"
|
||||
f"\n\n>*{md.escape('npm disable-global <plugin_name')}*>\n"
|
||||
f"\n{btn_enable_global}"
|
||||
f"\n\n>*{md.escape('npm enable-global <plugin_name')}*>\n"
|
||||
f"\n{btn_disable}"
|
||||
f"\n\n>*{md.escape('npm disable <plugin_name')}*>\n"
|
||||
f"\n{btn_enable}"
|
||||
f"\n\n>*{md.escape('npm enable <plugin_name')}*>\n"
|
||||
f"\n{btn_list}"
|
||||
f"\n\n>page为页数,num为每页显示数量"
|
||||
f"\n\n>*{md.escape('npm list [page] [num]')}*"
|
||||
)
|
||||
await md.send_md(reply, bot, event=event)
|
||||
else:
|
||||
|
||||
btn_list = md.btn_cmd(ulang.get("npm.list_plugins"), "npm list ", False, False)
|
||||
btn_disable = md.btn_cmd(ulang.get("npm.disable_session"), "npm disable ", False, False)
|
||||
btn_enable = md.btn_cmd(ulang.get("npm.enable_session"), "npm enable ", False, False)
|
||||
reply = (
|
||||
f"\n# **{ulang.get('npm.help')}**"
|
||||
f"\n{btn_disable}"
|
||||
f"\n\n>*{md.escape('npm disable <plugin_name')}*>\n"
|
||||
f"\n{btn_enable}"
|
||||
f"\n\n>*{md.escape('npm enable <plugin_name')}*>\n"
|
||||
f"\n{btn_list}"
|
||||
f"\n\n>page为页数,num为每页显示数量"
|
||||
f"\n\n>*{md.escape('npm list [page] [num]')}*"
|
||||
)
|
||||
await md.send_md(reply, bot, event=event)
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"群聊"},
|
||||
command=Alconna(
|
||||
"gm",
|
||||
Subcommand(
|
||||
enable,
|
||||
Args["group_id", str, None],
|
||||
alias=["e", "启用"],
|
||||
),
|
||||
Subcommand(
|
||||
disable,
|
||||
Args["group_id", str, None],
|
||||
alias=["d", "停用"],
|
||||
),
|
||||
),
|
||||
permission=SUPERUSER | GROUP_OWNER | GROUP_ADMIN
|
||||
).handle()
|
||||
async def _(bot: T_Bot, event: T_MessageEvent, gm: Matcher, result: Arparma):
|
||||
ulang = get_user_lang(str(event.user_id))
|
||||
to_enable = result.subcommands.get(enable) is not None
|
||||
|
||||
group_id = None
|
||||
if await SUPERUSER(bot, event):
|
||||
# 仅超级用户可以自定义群号
|
||||
group_id = result.subcommands.get(enable, result.subcommands.get(disable)).args.get("group_id")
|
||||
if group_id is None and event.message_type == "group":
|
||||
group_id = str(event.group_id)
|
||||
|
||||
if group_id is None:
|
||||
await gm.finish(ulang.get("liteyuki.invalid_command"), liteyuki_pass=True)
|
||||
|
||||
enabled = get_group_enable(group_id)
|
||||
if enabled == to_enable:
|
||||
await gm.finish(ulang.get("liteyuki.group_already", STATUS=ulang.get("npm.enable") if to_enable else ulang.get("npm.disable"), GROUP=group_id),
|
||||
liteyuki_pass=True)
|
||||
else:
|
||||
set_group_enable(group_id, to_enable)
|
||||
await gm.finish(
|
||||
ulang.get("liteyuki.group_success", STATUS=ulang.get("npm.enable") if to_enable else ulang.get("npm.disable"), GROUP=group_id),
|
||||
liteyuki_pass=True
|
||||
)
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"帮助"},
|
||||
command=Alconna(
|
||||
"help",
|
||||
Args["plugin_name", str, None],
|
||||
)
|
||||
).handle()
|
||||
async def _(result: Arparma, matcher: Matcher, event: T_MessageEvent, bot: T_Bot):
|
||||
ulang = get_user_lang(str(event.user_id))
|
||||
plugin_name = result.main_args.get("plugin_name")
|
||||
if plugin_name:
|
||||
searched_plugins = search_loaded_plugin(plugin_name)
|
||||
if searched_plugins:
|
||||
loaded_plugin = searched_plugins[0]
|
||||
else:
|
||||
await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
|
||||
|
||||
if loaded_plugin:
|
||||
if loaded_plugin.metadata is None:
|
||||
loaded_plugin.metadata = PluginMetadata(name=plugin_name, description="", usage="")
|
||||
# 从商店获取详细信息
|
||||
store_plugin = await get_store_plugin(plugin_name)
|
||||
if loaded_plugin.metadata.extra.get("liteyuki"):
|
||||
store_plugin = StorePlugin(
|
||||
name=loaded_plugin.metadata.name,
|
||||
desc=loaded_plugin.metadata.description,
|
||||
author="SnowyKami",
|
||||
module_name=plugin_name,
|
||||
homepage="https://github.com/snowykami/LiteyukiBot"
|
||||
)
|
||||
elif store_plugin is None:
|
||||
store_plugin = StorePlugin(
|
||||
name=loaded_plugin.metadata.name,
|
||||
desc=loaded_plugin.metadata.description,
|
||||
author="",
|
||||
module_name=plugin_name,
|
||||
homepage=""
|
||||
)
|
||||
|
||||
if store_plugin:
|
||||
link = store_plugin.homepage
|
||||
elif loaded_plugin.metadata.extra.get("liteyuki"):
|
||||
link = "https://github.com/snowykami/LiteyukiBot"
|
||||
else:
|
||||
link = None
|
||||
|
||||
reply = [
|
||||
mdc.heading(escape_md(store_plugin.name)),
|
||||
mdc.quote(store_plugin.module_name),
|
||||
mdc.quote(mdc.bold(ulang.get("npm.author")) + " " +
|
||||
(mdc.link(store_plugin.author, f"https://github.com/{store_plugin.author}") if store_plugin.author else "Unknown")),
|
||||
mdc.quote(mdc.bold(ulang.get("npm.description")) + " " + mdc.paragraph(max(loaded_plugin.metadata.description, store_plugin.desc))),
|
||||
mdc.heading(ulang.get("npm.usage"), 2),
|
||||
mdc.quote(escape_md(loaded_plugin.metadata.usage)),
|
||||
mdc.link(ulang.get("npm.homepage"), link) if link else mdc.paragraph(ulang.get("npm.no_homepage"))
|
||||
]
|
||||
await md.send_md(compile_md(reply), bot, event=event)
|
||||
else:
|
||||
await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
# 传入事件阻断hook
|
||||
@run_preprocessor
|
||||
async def pre_handle(event: Event, matcher: Matcher):
|
||||
plugin: Plugin = matcher.plugin
|
||||
plugin_global_enable = get_plugin_global_enable(plugin.name)
|
||||
if not plugin_global_enable:
|
||||
raise IgnoredException("Plugin disabled globally")
|
||||
if event.get_type() == "message":
|
||||
plugin_session_enable = get_plugin_session_enable(event, plugin.name)
|
||||
if not plugin_session_enable:
|
||||
raise IgnoredException("Plugin disabled in session")
|
||||
|
||||
|
||||
# 群聊开关阻断hook
|
||||
@Bot.on_calling_api
|
||||
async def block_disable_session(bot: Bot, api: str, args: dict):
|
||||
if "group_id" in args and not args.get("liteyuki_pass", False):
|
||||
group_id = args["group_id"]
|
||||
if not get_group_enable(group_id):
|
||||
nonebot.logger.debug(f"Group {group_id} disabled")
|
||||
raise MockApiException(f"Group {group_id} disabled")
|
||||
|
||||
|
||||
async def npm_update() -> bool:
|
||||
"""
|
||||
更新本地插件json缓存
|
||||
|
||||
Returns:
|
||||
bool: 是否成功更新
|
||||
"""
|
||||
url_list = [
|
||||
"https://registry.nonebot.dev/plugins.json",
|
||||
]
|
||||
for url in url_list:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
async with aiofiles.open("data/liteyuki/plugins.json", "wb") as f:
|
||||
data = await resp.read()
|
||||
await f.write(data)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def npm_search(keywords: list[str]) -> list[StorePlugin]:
|
||||
"""
|
||||
在本地缓存商店数据中搜索插件
|
||||
|
||||
Args:
|
||||
keywords (list[str]): 关键词列表
|
||||
|
||||
Returns:
|
||||
list[StorePlugin]: 插件列表
|
||||
"""
|
||||
plugin_blacklist = [
|
||||
"nonebot_plugin_xiuxian_2",
|
||||
"nonebot_plugin_htmlrender",
|
||||
"nonebot_plugin_alconna",
|
||||
]
|
||||
|
||||
results = []
|
||||
async with aiofiles.open("data/liteyuki/plugins.json", "r", encoding="utf-8") as f:
|
||||
plugins: list[StorePlugin] = [StorePlugin(**pobj) for pobj in json.loads(await f.read())]
|
||||
for plugin in plugins:
|
||||
if plugin.module_name in plugin_blacklist:
|
||||
continue
|
||||
plugin_text = ' '.join(
|
||||
[
|
||||
plugin.name,
|
||||
plugin.desc,
|
||||
plugin.author,
|
||||
plugin.module_name,
|
||||
' '.join([tag.label for tag in plugin.tags])
|
||||
]
|
||||
)
|
||||
if all([keyword in plugin_text for keyword in keywords]):
|
||||
results.append(plugin)
|
||||
return results
|
||||
|
||||
|
||||
@run_sync
|
||||
def npm_install(plugin_package_name) -> tuple[bool, str]:
|
||||
"""
|
||||
异步安装插件,使用pip安装
|
||||
Args:
|
||||
plugin_package_name:
|
||||
|
||||
Returns:
|
||||
tuple[bool, str]: 是否成功,输出信息
|
||||
|
||||
"""
|
||||
# 重定向标准输出
|
||||
buffer = StringIO()
|
||||
sys.stdout = buffer
|
||||
sys.stderr = buffer
|
||||
|
||||
update = False
|
||||
if get_plugin_exist(plugin_package_name):
|
||||
update = True
|
||||
|
||||
mirrors = [
|
||||
"https://pypi.tuna.tsinghua.edu.cn/simple", # 清华大学
|
||||
"https://pypi.org/simple", # 官方源
|
||||
]
|
||||
|
||||
# 使用pip安装包,对每个镜像尝试一次,成功后返回值
|
||||
success = False
|
||||
for mirror in mirrors:
|
||||
try:
|
||||
nonebot.logger.info(f"pip install try mirror: {mirror}")
|
||||
if update:
|
||||
result = pip.main(["install", "--upgrade", plugin_package_name, "-i", mirror])
|
||||
else:
|
||||
result = pip.main(["install", plugin_package_name, "-i", mirror])
|
||||
success = result == 0
|
||||
if success:
|
||||
break
|
||||
else:
|
||||
nonebot.logger.warning(f"pip install failed, try next mirror.")
|
||||
except Exception as e:
|
||||
success = False
|
||||
continue
|
||||
|
||||
sys.stdout = sys.__stdout__
|
||||
sys.stderr = sys.__stderr__
|
||||
|
||||
return success, buffer.getvalue()
|
||||
|
||||
|
||||
def search_loaded_plugin(keyword: str) -> list[Plugin]:
|
||||
"""
|
||||
搜索已加载插件
|
||||
|
||||
Args:
|
||||
keyword (str): 关键词
|
||||
|
||||
Returns:
|
||||
list[Plugin]: 插件列表
|
||||
"""
|
||||
if nonebot.get_plugin(keyword) is not None:
|
||||
return [nonebot.get_plugin(keyword)]
|
||||
else:
|
||||
results = []
|
||||
for plugin in nonebot.get_loaded_plugins():
|
||||
if plugin.metadata is None:
|
||||
plugin.metadata = PluginMetadata(name=plugin.name, description="", usage="")
|
||||
if keyword in plugin.name + plugin.metadata.name + plugin.metadata.description:
|
||||
results.append(plugin)
|
||||
return results
|
@@ -1,171 +0,0 @@
|
||||
# 轻雪资源包管理器
|
||||
import os
|
||||
|
||||
import yaml
|
||||
from nonebot import require
|
||||
from nonebot.permission import SUPERUSER
|
||||
|
||||
from liteyuki.utils.base.language import get_user_lang
|
||||
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||
from liteyuki.utils.message.message import MarkdownMessage as md
|
||||
from liteyuki.utils.base.resource import (ResourceMetadata, add_resource_pack, change_priority, check_exist, check_status, get_loaded_resource_packs, get_resource_metadata, load_resources, remove_resource_pack)
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from nonebot_plugin_alconna import Alconna, Args, on_alconna, Arparma, Subcommand
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"资源包"},
|
||||
command=Alconna(
|
||||
"rpm",
|
||||
Subcommand(
|
||||
"list",
|
||||
Args["page", int, 1]["num", int, 10],
|
||||
alias=["ls", "列表", "列出"],
|
||||
),
|
||||
Subcommand(
|
||||
"load",
|
||||
Args["name", str],
|
||||
alias=["安装"],
|
||||
),
|
||||
Subcommand(
|
||||
"unload",
|
||||
Args["name", str],
|
||||
alias=["卸载"],
|
||||
),
|
||||
Subcommand(
|
||||
"up",
|
||||
Args["name", str],
|
||||
alias=["上移"],
|
||||
),
|
||||
Subcommand(
|
||||
"down",
|
||||
Args["name", str],
|
||||
alias=["下移"],
|
||||
),
|
||||
Subcommand(
|
||||
"top",
|
||||
Args["name", str],
|
||||
alias=["置顶"],
|
||||
),
|
||||
Subcommand(
|
||||
"reload",
|
||||
alias=["重载"],
|
||||
),
|
||||
),
|
||||
permission=SUPERUSER
|
||||
).handle()
|
||||
async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma):
|
||||
ulang = get_user_lang(str(event.user_id))
|
||||
reply = ""
|
||||
if result.subcommands.get("list"):
|
||||
loaded_rps = get_loaded_resource_packs()
|
||||
reply += f"{ulang.get('liteyuki.loaded_resources', NUM=len(loaded_rps))}\n"
|
||||
for rp in loaded_rps:
|
||||
btn_unload = md.btn_cmd(
|
||||
ulang.get("npm.uninstall"),
|
||||
f"rpm unload {rp.folder}"
|
||||
)
|
||||
btn_move_up = md.btn_cmd(
|
||||
ulang.get("rpm.move_up"),
|
||||
f"rpm up {rp.folder}"
|
||||
)
|
||||
btn_move_down = md.btn_cmd(
|
||||
ulang.get("rpm.move_down"),
|
||||
f"rpm down {rp.folder}"
|
||||
)
|
||||
btn_move_top = md.btn_cmd(
|
||||
ulang.get("rpm.move_top"),
|
||||
f"rpm top {rp.folder}"
|
||||
)
|
||||
# 添加新行
|
||||
reply += (f"\n**{md.escape(rp.name)}**({md.escape(rp.folder)})\n\n"
|
||||
f"> {btn_move_up} {btn_move_down} {btn_move_top} {btn_unload}\n\n***")
|
||||
reply += f"\n\n{ulang.get('liteyuki.unloaded_resources')}\n"
|
||||
loaded_folders = [rp.folder for rp in get_loaded_resource_packs()]
|
||||
for folder in os.listdir("resources"):
|
||||
if folder not in loaded_folders and os.path.exists(os.path.join("resources", folder, "metadata.yml")):
|
||||
metadata = ResourceMetadata(
|
||||
**yaml.load(
|
||||
open(
|
||||
os.path.join("resources", folder, "metadata.yml"),
|
||||
encoding="utf-8"
|
||||
),
|
||||
Loader=yaml.FullLoader
|
||||
)
|
||||
)
|
||||
metadata.folder = folder
|
||||
metadata.path = os.path.join("resources", folder)
|
||||
btn_load = md.btn_cmd(
|
||||
ulang.get("npm.install"),
|
||||
f"rpm load {metadata.folder}"
|
||||
)
|
||||
# 添加新行
|
||||
reply += (f"\n**{md.escape(metadata.name)}**({md.escape(metadata.folder)})\n\n"
|
||||
f"> {btn_load}\n\n***")
|
||||
elif result.subcommands.get("load") or result.subcommands.get("unload"):
|
||||
load = result.subcommands.get("load") is not None
|
||||
rp_name = result.args.get("name")
|
||||
r = False # 操作结果
|
||||
if check_exist(rp_name):
|
||||
if load != check_status(rp_name):
|
||||
# 状态不同
|
||||
if load:
|
||||
r = add_resource_pack(rp_name)
|
||||
else:
|
||||
r = remove_resource_pack(rp_name)
|
||||
rp_meta = get_resource_metadata(rp_name)
|
||||
reply += ulang.get(
|
||||
f"liteyuki.{'load' if load else 'unload'}_resource_{'success' if r else 'failed'}",
|
||||
NAME=rp_meta.name
|
||||
)
|
||||
else:
|
||||
# 重复操作
|
||||
reply += ulang.get(f"liteyuki.resource_already_{'load' if load else 'unload'}ed", NAME=rp_name)
|
||||
else:
|
||||
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
|
||||
if r:
|
||||
btn_reload = md.btn_cmd(
|
||||
ulang.get("liteyuki.reload_resources"),
|
||||
f"rpm reload"
|
||||
)
|
||||
reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload)
|
||||
elif result.subcommands.get("up") or result.subcommands.get("down") or result.subcommands.get("top"):
|
||||
rp_name = result.args.get("name")
|
||||
if result.subcommands.get("up"):
|
||||
delta = -1
|
||||
elif result.subcommands.get("down"):
|
||||
delta = 1
|
||||
else:
|
||||
delta = 0
|
||||
if check_exist(rp_name):
|
||||
if check_status(rp_name):
|
||||
r = change_priority(rp_name, delta)
|
||||
reply += ulang.get(f"liteyuki.change_priority_{'success' if r else 'failed'}", NAME=rp_name)
|
||||
if r:
|
||||
btn_reload = md.btn_cmd(
|
||||
ulang.get("liteyuki.reload_resources"),
|
||||
f"rpm reload"
|
||||
)
|
||||
reply += "\n" + ulang.get("liteyuki.need_reload", BTN=btn_reload)
|
||||
else:
|
||||
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
|
||||
else:
|
||||
reply += ulang.get("liteyuki.resource_not_found", NAME=rp_name)
|
||||
elif result.subcommands.get("reload"):
|
||||
load_resources()
|
||||
reply = ulang.get(
|
||||
"liteyuki.reload_resources_success",
|
||||
NUM=len(get_loaded_resource_packs())
|
||||
)
|
||||
else:
|
||||
btn_reload = md.btn_cmd(
|
||||
ulang.get("liteyuki.reload_resources"),
|
||||
f"rpm reload"
|
||||
)
|
||||
btn_list = md.btn_cmd(
|
||||
ulang.get("liteyuki.list_resources"),
|
||||
f"rpm list"
|
||||
)
|
||||
reply += f"{btn_list} \n {btn_reload}"
|
||||
await md.send_md(reply, bot, event=event)
|
@@ -1,24 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from .status import *
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="状态查看器",
|
||||
description="",
|
||||
usage=(
|
||||
"MARKDOWN### 状态查看器\n"
|
||||
"查看机器人的状态\n"
|
||||
"### 用法\n"
|
||||
"- `/status` 查看基本情况\n"
|
||||
"- `/status memory` 查看内存使用情况\n"
|
||||
"- `/status process` 查看进程情况\n"
|
||||
),
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki": True,
|
||||
"toggleable" : False,
|
||||
"default_enable" : True,
|
||||
}
|
||||
)
|
||||
|
@@ -1,257 +0,0 @@
|
||||
import platform
|
||||
import time
|
||||
|
||||
import nonebot
|
||||
import psutil
|
||||
from cpuinfo import cpuinfo
|
||||
from nonebot import require
|
||||
from liteyuki.utils import __NAME__, __VERSION__
|
||||
from liteyuki.utils.base.config import get_config
|
||||
from liteyuki.utils.base.data_manager import TempConfig, common_db
|
||||
from liteyuki.utils.base.language import Language
|
||||
from liteyuki.utils.base.resource import get_loaded_resource_packs, get_path
|
||||
from liteyuki.utils.message.html_tool import template2image
|
||||
|
||||
require("nonebot_plugin_apscheduler")
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
protocol_names = {
|
||||
0: "iPad",
|
||||
1: "Android Phone",
|
||||
2: "Android Watch",
|
||||
3: "Mac",
|
||||
5: "iPad",
|
||||
6: "Android Pad",
|
||||
}
|
||||
|
||||
"""
|
||||
Universal Interface
|
||||
data
|
||||
- bot
|
||||
- name: str
|
||||
icon: str
|
||||
id: int
|
||||
protocol_name: str
|
||||
groups: int
|
||||
friends: int
|
||||
message_sent: int
|
||||
message_received: int
|
||||
app_name: str
|
||||
- hardware
|
||||
- cpu
|
||||
- percent: float
|
||||
- name: str
|
||||
- mem
|
||||
- percent: float
|
||||
- total: int
|
||||
- used: int
|
||||
- free: int
|
||||
- swap
|
||||
- percent: float
|
||||
- total: int
|
||||
- used: int
|
||||
- free: int
|
||||
- disk: list
|
||||
- name: str
|
||||
- percent: float
|
||||
- total: int
|
||||
"""
|
||||
status_card_cache = {} # lang -> bytes
|
||||
|
||||
|
||||
# 60s刷新一次
|
||||
@scheduler.scheduled_job("cron", second="*/40")
|
||||
async def refresh_status_card():
|
||||
nonebot.logger.debug("Refreshing status card cache...")
|
||||
global status_card_cache
|
||||
bot_data = await get_bots_data()
|
||||
hardware_data = await get_hardware_data()
|
||||
liteyuki_data = await get_liteyuki_data()
|
||||
for lang in status_card_cache.keys():
|
||||
status_card_cache[lang] = await generate_status_card(
|
||||
bot_data,
|
||||
hardware_data,
|
||||
liteyuki_data,
|
||||
lang=lang,
|
||||
use_cache=False
|
||||
)
|
||||
|
||||
|
||||
async def generate_status_card(bot: dict, hardware: dict, liteyuki: dict, lang="zh-CN", bot_id="0", use_cache=False) -> bytes:
|
||||
if not use_cache:
|
||||
return await template2image(
|
||||
get_path("templates/status.html", abs_path=True),
|
||||
{
|
||||
"data": {
|
||||
"bot" : bot,
|
||||
"hardware" : hardware,
|
||||
"liteyuki" : liteyuki,
|
||||
"localization": get_local_data(lang)
|
||||
}
|
||||
},
|
||||
debug=True
|
||||
)
|
||||
else:
|
||||
if lang not in status_card_cache:
|
||||
status_card_cache[lang] = await generate_status_card(bot, hardware, liteyuki, lang=lang, bot_id=bot_id)
|
||||
return status_card_cache[lang]
|
||||
|
||||
|
||||
def get_local_data(lang_code) -> dict:
|
||||
lang = Language(lang_code)
|
||||
return {
|
||||
"friends" : lang.get("status.friends"),
|
||||
"groups" : lang.get("status.groups"),
|
||||
"plugins" : lang.get("status.plugins"),
|
||||
"bots" : lang.get("status.bots"),
|
||||
"message_sent" : lang.get("status.message_sent"),
|
||||
"message_received": lang.get("status.message_received"),
|
||||
"cpu" : lang.get("status.cpu"),
|
||||
"memory" : lang.get("status.memory"),
|
||||
"swap" : lang.get("status.swap"),
|
||||
"disk" : lang.get("status.disk"),
|
||||
|
||||
"usage" : lang.get("status.usage"),
|
||||
"total" : lang.get("status.total"),
|
||||
"used" : lang.get("status.used"),
|
||||
"free" : lang.get("status.free"),
|
||||
|
||||
"days" : lang.get("status.days"),
|
||||
"hours" : lang.get("status.hours"),
|
||||
"minutes" : lang.get("status.minutes"),
|
||||
"seconds" : lang.get("status.seconds"),
|
||||
"runtime" : lang.get("status.runtime"),
|
||||
"threads" : lang.get("status.threads"),
|
||||
"cores" : lang.get("status.cores"),
|
||||
"process" : lang.get("status.process"),
|
||||
"resources" : lang.get("status.resources"),
|
||||
"description" : lang.get("status.description"),
|
||||
}
|
||||
|
||||
|
||||
async def get_bots_data(self_id: str = "0") -> dict:
|
||||
"""获取当前所有机器人数据
|
||||
Returns:
|
||||
"""
|
||||
result = {
|
||||
"self_id": self_id,
|
||||
"bots" : [],
|
||||
}
|
||||
for bot_id, bot in nonebot.get_bots().items():
|
||||
groups = 0
|
||||
friends = 0
|
||||
status = {}
|
||||
bot_name = bot_id
|
||||
version_info = {}
|
||||
try:
|
||||
# API fetch
|
||||
bot_name = (await bot.get_login_info())["nickname"]
|
||||
groups = len(await bot.get_group_list())
|
||||
friends = len(await bot.get_friend_list())
|
||||
status = await bot.get_status()
|
||||
version_info = await bot.get_version_info()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
statistics = status.get("stat", {})
|
||||
app_name = version_info.get("app_name", "UnknownImplementation")
|
||||
if app_name in ["Lagrange.OneBot", "LLOneBot", "Shamrock"]:
|
||||
icon = f"https://q.qlogo.cn/g?b=qq&nk={bot_id}&s=640"
|
||||
else:
|
||||
icon = None
|
||||
bot_data = {
|
||||
"name" : bot_name,
|
||||
"icon" : icon,
|
||||
"id" : bot_id,
|
||||
"protocol_name" : protocol_names.get(version_info.get("protocol_name"), "Online"),
|
||||
"groups" : groups,
|
||||
"friends" : friends,
|
||||
"message_sent" : statistics.get("message_sent", 0),
|
||||
"message_received": statistics.get("message_received", 0),
|
||||
"app_name" : app_name
|
||||
}
|
||||
result["bots"].append(bot_data)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def get_hardware_data() -> dict:
|
||||
mem = psutil.virtual_memory()
|
||||
all_processes = psutil.Process().children(recursive=True)
|
||||
all_processes.append(psutil.Process())
|
||||
|
||||
mem_used_process = 0
|
||||
process_mem = {}
|
||||
for process in all_processes:
|
||||
try:
|
||||
ps_name = process.name().replace(".exe", "")
|
||||
if ps_name not in process_mem:
|
||||
process_mem[ps_name] = 0
|
||||
process_mem[ps_name] += process.memory_info().rss
|
||||
mem_used_process += process.memory_info().rss
|
||||
except Exception:
|
||||
pass
|
||||
swap = psutil.swap_memory()
|
||||
cpu_brand_raw = cpuinfo.get_cpu_info().get("brand_raw", "Unknown")
|
||||
if "AMD" in cpu_brand_raw:
|
||||
brand = "AMD"
|
||||
elif "Intel" in cpu_brand_raw:
|
||||
brand = "Intel"
|
||||
else:
|
||||
brand = "Unknown"
|
||||
result = {
|
||||
"cpu" : {
|
||||
"percent": psutil.cpu_percent(),
|
||||
"name" : f"{brand} {cpuinfo.get_cpu_info().get('arch', 'Unknown')}",
|
||||
"cores" : psutil.cpu_count(logical=False),
|
||||
"threads": psutil.cpu_count(logical=True),
|
||||
"freq" : psutil.cpu_freq().current # MHz
|
||||
},
|
||||
"memory": {
|
||||
"percent" : mem.percent,
|
||||
"total" : mem.total,
|
||||
"used" : mem.used,
|
||||
"free" : mem.free,
|
||||
"usedProcess": mem_used_process,
|
||||
},
|
||||
"swap" : {
|
||||
"percent": swap.percent,
|
||||
"total" : swap.total,
|
||||
"used" : swap.used,
|
||||
"free" : swap.free
|
||||
},
|
||||
"disk" : [],
|
||||
}
|
||||
|
||||
for disk in psutil.disk_partitions(all=True):
|
||||
try:
|
||||
disk_usage = psutil.disk_usage(disk.mountpoint)
|
||||
if disk_usage.total == 0:
|
||||
continue # 虚拟磁盘
|
||||
result["disk"].append({
|
||||
"name" : disk.mountpoint,
|
||||
"percent": disk_usage.percent,
|
||||
"total" : disk_usage.total,
|
||||
"used" : disk_usage.used,
|
||||
"free" : disk_usage.free
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def get_liteyuki_data() -> dict:
|
||||
temp_data: TempConfig = common_db.first(TempConfig(), default=TempConfig())
|
||||
result = {
|
||||
"name" : list(get_config("nickname", [__NAME__]))[0],
|
||||
"version" : __VERSION__,
|
||||
"plugins" : len(nonebot.get_loaded_plugins()),
|
||||
"resources": len(get_loaded_resource_packs()),
|
||||
"nonebot" : f"{nonebot.__version__}",
|
||||
"python" : f"{platform.python_implementation()} {platform.python_version()}",
|
||||
"system" : f"{platform.system()} {platform.release()}",
|
||||
"runtime" : time.time() - temp_data.data.get("start_time", time.time()), # 运行时间秒数
|
||||
"bots" : len(nonebot.get_bots())
|
||||
}
|
||||
return result
|
@@ -1,52 +0,0 @@
|
||||
from nonebot import require
|
||||
|
||||
from liteyuki.utils.base.resource import get_path
|
||||
from liteyuki.utils.message.html_tool import template2image
|
||||
from liteyuki.utils.base.language import get_user_lang
|
||||
from .api import *
|
||||
from ...utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from nonebot_plugin_alconna import on_alconna, Alconna, Args, Subcommand, Arparma, UniMessage
|
||||
|
||||
status_alc = on_alconna(
|
||||
aliases={"状态"},
|
||||
command=Alconna(
|
||||
"status",
|
||||
Subcommand(
|
||||
"memory",
|
||||
alias={"mem", "m", "内存"},
|
||||
),
|
||||
Subcommand(
|
||||
"process",
|
||||
alias={"proc", "p", "进程"},
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@status_alc.handle()
|
||||
async def _(event: T_MessageEvent, bot: T_Bot):
|
||||
ulang = get_user_lang(event.user_id)
|
||||
if ulang.lang_code in status_card_cache:
|
||||
image = status_card_cache[ulang.lang_code]
|
||||
else:
|
||||
image = await generate_status_card(
|
||||
bot=await get_bots_data(),
|
||||
hardware=await get_hardware_data(),
|
||||
liteyuki=await get_liteyuki_data(),
|
||||
lang=ulang.lang_code,
|
||||
bot_id=bot.self_id,
|
||||
use_cache=True
|
||||
)
|
||||
await status_alc.finish(UniMessage.image(raw=image))
|
||||
|
||||
|
||||
@status_alc.assign("memory")
|
||||
async def _():
|
||||
print("memory")
|
||||
|
||||
|
||||
@status_alc.assign("process")
|
||||
async def _():
|
||||
print("process")
|
@@ -1,17 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from .api import *
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="联合黑名单(测试中...)",
|
||||
description="",
|
||||
usage="",
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki": True,
|
||||
"toggleable" : True,
|
||||
"default_enable" : True,
|
||||
}
|
||||
)
|
||||
|
@@ -1,60 +0,0 @@
|
||||
import datetime
|
||||
|
||||
import aiohttp
|
||||
import httpx
|
||||
import nonebot
|
||||
from nonebot import require
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.message import event_preprocessor
|
||||
from nonebot_plugin_alconna.typings import Event
|
||||
|
||||
require("nonebot_plugin_apscheduler")
|
||||
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
blacklist_data: dict[str, set[str]] = {}
|
||||
blacklist: set[str] = set()
|
||||
|
||||
|
||||
@scheduler.scheduled_job("interval", minutes=10, next_run_time=datetime.datetime.now())
|
||||
async def update_blacklist():
|
||||
await request_for_blacklist()
|
||||
|
||||
|
||||
async def request_for_blacklist():
|
||||
global blacklist
|
||||
urls = [
|
||||
"https://cdn.liteyuki.icu/static/ubl/"
|
||||
]
|
||||
|
||||
platforms = [
|
||||
"qq"
|
||||
]
|
||||
|
||||
for plat in platforms:
|
||||
for url in urls:
|
||||
url += f"{plat}.txt"
|
||||
async with aiohttp.ClientSession() as client:
|
||||
resp = await client.get(url)
|
||||
blacklist_data[plat] = set((await resp.text()).splitlines())
|
||||
blacklist = get_uni_set()
|
||||
nonebot.logger.info("blacklists updated")
|
||||
|
||||
|
||||
def get_uni_set() -> set:
|
||||
s = set()
|
||||
for new_set in blacklist_data.values():
|
||||
s.update(new_set)
|
||||
return s
|
||||
|
||||
|
||||
@event_preprocessor
|
||||
async def pre_handle(event: Event):
|
||||
try:
|
||||
user_id = str(event.get_user_id())
|
||||
|
||||
except:
|
||||
return
|
||||
|
||||
if user_id in get_uni_set():
|
||||
raise IgnoredException("UserId in blacklist")
|
@@ -1,16 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from .profile_manager import *
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="轻雪用户管理",
|
||||
description="用户管理插件",
|
||||
usage="",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki" : True,
|
||||
"toggleable" : False,
|
||||
"default_enable": True,
|
||||
}
|
||||
)
|
@@ -1,23 +0,0 @@
|
||||
representative_timezones_list = [
|
||||
"Etc/GMT+12", # 国际日期变更线西
|
||||
"Pacific/Honolulu", # 夏威夷标准时间
|
||||
"America/Anchorage", # 阿拉斯加标准时间
|
||||
"America/Los_Angeles", # 美国太平洋标准时间
|
||||
"America/Denver", # 美国山地标准时间
|
||||
"America/Chicago", # 美国中部标准时间
|
||||
"America/New_York", # 美国东部标准时间
|
||||
"Europe/London", # 英国标准时间
|
||||
"Europe/Paris", # 中欧标准时间
|
||||
"Europe/Moscow", # 莫斯科标准时间
|
||||
"Asia/Dubai", # 阿联酋标准时间
|
||||
"Asia/Kolkata", # 印度标准时间
|
||||
"Asia/Shanghai", # 中国标准时间
|
||||
"Asia/Hong_Kong", # 中国香港标准时间
|
||||
"Asia/Chongqing", # 中国重庆标准时间
|
||||
"Asia/Macau", # 中国澳门标准时间
|
||||
"Asia/Taipei", # 中国台湾标准时间
|
||||
"Asia/Tokyo", # 日本标准时间
|
||||
"Australia/Sydney", # 澳大利亚东部标准时间
|
||||
"Pacific/Auckland" # 新西兰标准时间
|
||||
]
|
||||
representative_timezones_list.sort()
|
@@ -1,148 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytz
|
||||
from nonebot import require
|
||||
|
||||
from liteyuki.utils.base.data import LiteModel, Database
|
||||
from liteyuki.utils.base.data_manager import User, user_db, group_db
|
||||
from liteyuki.utils.base.language import Language, change_user_lang, get_all_lang, get_user_lang
|
||||
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
|
||||
from liteyuki.utils.message.message import MarkdownMessage as md
|
||||
from .const import representative_timezones_list
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
|
||||
|
||||
profile_alc = on_alconna(
|
||||
Alconna(
|
||||
"profile",
|
||||
Subcommand(
|
||||
"set",
|
||||
Args["key", str]["value", str, None],
|
||||
alias=["s", "设置"],
|
||||
),
|
||||
Subcommand(
|
||||
"get",
|
||||
Args["key", str],
|
||||
alias=["g", "查询"],
|
||||
),
|
||||
),
|
||||
aliases={"用户信息"}
|
||||
)
|
||||
|
||||
|
||||
# json储存
|
||||
class Profile(LiteModel):
|
||||
lang: str = "zh-CN"
|
||||
nickname: str = ""
|
||||
timezone: str = "Asia/Shanghai"
|
||||
location: str = ""
|
||||
|
||||
|
||||
@profile_alc.handle()
|
||||
async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
|
||||
user: User = user_db.first(User(), "user_id = ?", event.user_id, default=User(user_id=str(event.user_id)))
|
||||
ulang = get_user_lang(str(event.user_id))
|
||||
if result.subcommands.get("set"):
|
||||
if result.subcommands["set"].args.get("value"):
|
||||
# 对合法性进行校验后设置
|
||||
r = set_profile(result.args["key"], result.args["value"], str(event.user_id))
|
||||
if r:
|
||||
user.profile[result.args["key"]] = result.args["value"]
|
||||
user_db.save(user) # 数据库保存
|
||||
await profile_alc.finish(
|
||||
ulang.get(
|
||||
"user.profile.set_success",
|
||||
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
|
||||
VALUE=result.args["value"]
|
||||
)
|
||||
)
|
||||
else:
|
||||
await profile_alc.finish(ulang.get("user.profile.set_failed", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
|
||||
else:
|
||||
# 未输入值,尝试呼出菜单
|
||||
menu = get_profile_menu(result.args["key"], ulang)
|
||||
if menu:
|
||||
await md.send_md(menu, bot, event=event)
|
||||
else:
|
||||
await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
|
||||
|
||||
user.profile[result.args["key"]] = result.args["value"]
|
||||
|
||||
elif result.subcommands.get("get"):
|
||||
if result.args["key"] in user.profile:
|
||||
await profile_alc.finish(user.profile[result.args["key"]])
|
||||
else:
|
||||
await profile_alc.finish("无此键值")
|
||||
else:
|
||||
profile = Profile(**user.profile)
|
||||
|
||||
for k, v in user.profile.items():
|
||||
profile.__setattr__(k, v)
|
||||
|
||||
reply = f"# {ulang.get('user.profile.info')}\n***\n"
|
||||
|
||||
hidden_attr = ["id", "TABLE_NAME"]
|
||||
enter_attr = ["lang", "timezone"]
|
||||
|
||||
for key in sorted(profile.dict().keys()):
|
||||
if key in hidden_attr:
|
||||
continue
|
||||
val = profile.dict()[key]
|
||||
key_text = ulang.get(f"user.profile.{key}")
|
||||
btn_set = md.btn_cmd(ulang.get("user.profile.edit"), f"profile set {key}",
|
||||
enter=True if key in enter_attr else False)
|
||||
reply += (f"\n**{key_text}** **{val}**\n"
|
||||
f"\n> {ulang.get(f'user.profile.{key}.desc')}"
|
||||
f"\n> {btn_set} \n\n***\n")
|
||||
await md.send_md(reply, bot, event=event)
|
||||
|
||||
|
||||
def get_profile_menu(key: str, ulang: Language) -> Optional[str]:
|
||||
"""获取属性的markdown菜单
|
||||
Args:
|
||||
ulang: 用户语言
|
||||
key: 属性键
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
setting_name = ulang.get(f"user.profile.{key}")
|
||||
|
||||
no_menu = ["id", "nickname", "location"]
|
||||
|
||||
if key in no_menu:
|
||||
return None
|
||||
|
||||
reply = f"**{setting_name} {ulang.get('user.profile.settings')}**\n***\n"
|
||||
if key == "lang":
|
||||
for lang_code, lang_name in get_all_lang().items():
|
||||
btn_set_lang = md.btn_cmd(f"{lang_name}({lang_code})", f"profile set {key} {lang_code}")
|
||||
reply += f"\n{btn_set_lang}\n***\n"
|
||||
elif key == "timezone":
|
||||
for tz in representative_timezones_list:
|
||||
btn_set_tz = md.btn_cmd(tz, f"profile set {key} {tz}")
|
||||
reply += f"{btn_set_tz}\n***\n"
|
||||
return reply
|
||||
|
||||
|
||||
def set_profile(key: str, value: str, user_id: str) -> bool:
|
||||
"""设置属性,使用if分支对每一个合法性进行检查
|
||||
Args:
|
||||
user_id:
|
||||
key:
|
||||
value:
|
||||
|
||||
Returns:
|
||||
是否成功设置,输入合法性不通过返回False
|
||||
|
||||
"""
|
||||
if key == "lang":
|
||||
if value in get_all_lang():
|
||||
change_user_lang(user_id, value)
|
||||
return True
|
||||
elif key == "timezone":
|
||||
if value in pytz.all_timezones:
|
||||
return True
|
||||
elif key == "nickname":
|
||||
return True
|
@@ -1,27 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot import get_driver
|
||||
from .qweather import *
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="轻雪天气",
|
||||
description="基于和风天气api的天气插件",
|
||||
usage="",
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki" : True,
|
||||
"toggleable" : True,
|
||||
"default_enable": True,
|
||||
}
|
||||
)
|
||||
|
||||
from ...utils.base.data_manager import set_memory_data
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
async def _():
|
||||
# 检查是否为开发者模式
|
||||
is_dev = await check_key_dev(get_config("weather_key", ""))
|
||||
set_memory_data("weather.is_dev", is_dev)
|
@@ -1,171 +0,0 @@
|
||||
import aiohttp
|
||||
|
||||
from .qw_models import *
|
||||
import httpx
|
||||
|
||||
from ...utils.base.data_manager import get_memory_data
|
||||
from ...utils.base.language import Language
|
||||
|
||||
dev_url = "https://devapi.qweather.com/" # 开发HBa
|
||||
com_url = "https://api.qweather.com/" # 正式环境
|
||||
|
||||
|
||||
def get_qw_lang(lang: str) -> str:
|
||||
if lang in ["zh-HK", "zh-TW"]:
|
||||
return "zh-hant"
|
||||
elif lang.startswith("zh"):
|
||||
return "zh"
|
||||
elif lang.startswith("en"):
|
||||
return "en"
|
||||
else:
|
||||
return lang
|
||||
|
||||
|
||||
async def check_key_dev(key: str) -> bool:
|
||||
url = "https://api.qweather.com/v7/weather/now?"
|
||||
params = {
|
||||
"location": "101010100",
|
||||
"key" : key,
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url, params=params)
|
||||
return (resp.json()).get("code") != "200" # 查询不到付费数据为开发版
|
||||
|
||||
|
||||
def get_local_data(ulang_code: str) -> dict:
|
||||
"""
|
||||
获取本地化数据
|
||||
Args:
|
||||
ulang_code:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
ulang = Language(ulang_code)
|
||||
return {
|
||||
"monday" : ulang.get("weather.monday"),
|
||||
"tuesday" : ulang.get("weather.tuesday"),
|
||||
"wednesday": ulang.get("weather.wednesday"),
|
||||
"thursday" : ulang.get("weather.thursday"),
|
||||
"friday" : ulang.get("weather.friday"),
|
||||
"saturday" : ulang.get("weather.saturday"),
|
||||
"sunday" : ulang.get("weather.sunday"),
|
||||
"today" : ulang.get("weather.today"),
|
||||
"tomorrow" : ulang.get("weather.tomorrow"),
|
||||
"day" : ulang.get("weather.day"),
|
||||
"night" : ulang.get("weather.night"),
|
||||
"no_aqi" : ulang.get("weather.no_aqi"),
|
||||
}
|
||||
|
||||
|
||||
async def city_lookup(
|
||||
location: str,
|
||||
key: str,
|
||||
adm: str = "",
|
||||
number: int = 20,
|
||||
lang: str = "zh",
|
||||
) -> CityLookup:
|
||||
"""
|
||||
通过关键字搜索城市信息
|
||||
Args:
|
||||
location:
|
||||
key:
|
||||
adm:
|
||||
number:
|
||||
lang: 可传入标准i18n语言代码,如zh-CN、en-US等
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
url = "https://geoapi.qweather.com/v2/city/lookup?"
|
||||
params = {
|
||||
"location": location,
|
||||
"adm" : adm,
|
||||
"number" : number,
|
||||
"key" : key,
|
||||
"lang" : lang,
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url, params=params)
|
||||
return CityLookup.parse_obj(resp.json())
|
||||
|
||||
|
||||
async def get_weather_now(
|
||||
key: str,
|
||||
location: str,
|
||||
lang: str = "zh",
|
||||
unit: str = "m",
|
||||
dev: bool = get_memory_data("is_dev", True),
|
||||
) -> dict:
|
||||
url_path = "v7/weather/now?"
|
||||
url = dev_url + url_path if dev else com_url + url_path
|
||||
params = {
|
||||
"location": location,
|
||||
"key" : key,
|
||||
"lang" : lang,
|
||||
"unit" : unit,
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url, params=params)
|
||||
return resp.json()
|
||||
|
||||
|
||||
async def get_weather_daily(
|
||||
key: str,
|
||||
location: str,
|
||||
lang: str = "zh",
|
||||
unit: str = "m",
|
||||
dev: bool = get_memory_data("is_dev", True),
|
||||
) -> dict:
|
||||
url_path = "v7/weather/%dd?" % (7 if dev else 30)
|
||||
url = dev_url + url_path if dev else com_url + url_path
|
||||
params = {
|
||||
"location": location,
|
||||
"key" : key,
|
||||
"lang" : lang,
|
||||
"unit" : unit,
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url, params=params)
|
||||
return resp.json()
|
||||
|
||||
|
||||
async def get_weather_hourly(
|
||||
key: str,
|
||||
location: str,
|
||||
lang: str = "zh",
|
||||
unit: str = "m",
|
||||
dev: bool = get_memory_data("is_dev", True),
|
||||
) -> dict:
|
||||
url_path = "v7/weather/%dh?" % (24 if dev else 168)
|
||||
url = dev_url + url_path if dev else com_url + url_path
|
||||
params = {
|
||||
"location": location,
|
||||
"key" : key,
|
||||
"lang" : lang,
|
||||
"unit" : unit,
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url, params=params)
|
||||
return resp.json()
|
||||
|
||||
|
||||
async def get_airquality(
|
||||
key: str,
|
||||
location: str,
|
||||
lang: str,
|
||||
pollutant: bool = False,
|
||||
station: bool = False,
|
||||
dev: bool = get_memory_data("is_dev", True),
|
||||
) -> dict:
|
||||
url_path = f"airquality/v1/now/{location}?"
|
||||
url = dev_url + url_path if dev else com_url + url_path
|
||||
params = {
|
||||
"key" : key,
|
||||
"lang" : lang,
|
||||
"pollutant": pollutant,
|
||||
"station" : station,
|
||||
}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url, params=params)
|
||||
return resp.json()
|
@@ -1,62 +0,0 @@
|
||||
from liteyuki.utils.base.data import LiteModel
|
||||
|
||||
|
||||
class Location(LiteModel):
|
||||
name: str = ""
|
||||
id: str = ""
|
||||
lat: str = ""
|
||||
lon: str = ""
|
||||
adm2: str = ""
|
||||
adm1: str = ""
|
||||
country: str = ""
|
||||
tz: str = ""
|
||||
utcOffset: str = ""
|
||||
isDst: str = ""
|
||||
type: str = ""
|
||||
rank: str = ""
|
||||
fxLink: str = ""
|
||||
sources: str = ""
|
||||
license: str = ""
|
||||
|
||||
|
||||
class CityLookup(LiteModel):
|
||||
code: str = ""
|
||||
location: list[Location] = [Location()]
|
||||
|
||||
|
||||
class Now(LiteModel):
|
||||
obsTime: str = ""
|
||||
temp: str = ""
|
||||
feelsLike: str = ""
|
||||
icon: str = ""
|
||||
text: str = ""
|
||||
wind360: str = ""
|
||||
windDir: str = ""
|
||||
windScale: str = ""
|
||||
windSpeed: str = ""
|
||||
humidity: str = ""
|
||||
precip: str = ""
|
||||
pressure: str = ""
|
||||
vis: str = ""
|
||||
cloud: str = ""
|
||||
dew: str = ""
|
||||
sources: str = ""
|
||||
license: str = ""
|
||||
|
||||
|
||||
class WeatherNow(LiteModel):
|
||||
code: str = ""
|
||||
updateTime: str = ""
|
||||
fxLink: str = ""
|
||||
now: Now = Now()
|
||||
|
||||
|
||||
class Daily(LiteModel):
|
||||
pass
|
||||
|
||||
|
||||
class WeatherDaily(LiteModel):
|
||||
code: str = ""
|
||||
updateTime: str = ""
|
||||
fxLink: str = ""
|
||||
daily: list[str] = []
|
@@ -1,95 +0,0 @@
|
||||
from nonebot import require, on_endswith
|
||||
from nonebot.adapters.onebot.v11 import MessageSegment
|
||||
from nonebot.internal.matcher import Matcher
|
||||
|
||||
from liteyuki.utils.base.config import get_config
|
||||
from liteyuki.utils.base.ly_typing import T_MessageEvent
|
||||
|
||||
from .qw_api import *
|
||||
from liteyuki.utils.base.data_manager import User, user_db
|
||||
from liteyuki.utils.base.language import Language, get_user_lang
|
||||
from liteyuki.utils.base.resource import get_path
|
||||
from liteyuki.utils.message.html_tool import template2image
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma
|
||||
|
||||
|
||||
@on_alconna(
|
||||
aliases={"天气"},
|
||||
command=Alconna(
|
||||
"weather",
|
||||
Args["keywords", MultiVar(str), []],
|
||||
),
|
||||
).handle()
|
||||
async def _(result: Arparma, event: T_MessageEvent, matcher: Matcher):
|
||||
"""await alconna.send("weather", city)"""
|
||||
kws = result.main_args.get("keywords")
|
||||
image = await get_weather_now_card(matcher, event, kws)
|
||||
await matcher.finish(MessageSegment.image(image))
|
||||
|
||||
|
||||
@on_endswith(("天气", "weather")).handle()
|
||||
async def _(event: T_MessageEvent, matcher: Matcher):
|
||||
"""await alconna.send("weather", city)"""
|
||||
kws = event.message.extract_plain_text()
|
||||
image = await get_weather_now_card(matcher, event, [kws.replace("天气", "").replace("weather", "")], False)
|
||||
await matcher.finish(MessageSegment.image(image))
|
||||
|
||||
|
||||
async def get_weather_now_card(matcher: Matcher, event: T_MessageEvent, keyword: list[str], tip: bool = True):
|
||||
ulang = get_user_lang(event.user_id)
|
||||
qw_lang = get_qw_lang(ulang.lang_code)
|
||||
key = get_config("weather_key")
|
||||
is_dev = get_memory_data("weather.is_dev", True)
|
||||
user: User = user_db.first(User(), "user_id = ?", event.user_id, default=User())
|
||||
# params
|
||||
unit = user.profile.get("unit", "m")
|
||||
stored_location = user.profile.get("location", None)
|
||||
|
||||
if not key:
|
||||
await matcher.finish(ulang.get("weather.no_key") if tip else None)
|
||||
|
||||
if keyword:
|
||||
if len(keyword) >= 2:
|
||||
adm = keyword[0]
|
||||
city = keyword[-1]
|
||||
else:
|
||||
adm = ""
|
||||
city = keyword[0]
|
||||
city_info = await city_lookup(city, key, adm=adm, lang=qw_lang)
|
||||
city_name = " ".join(keyword)
|
||||
else:
|
||||
if not stored_location:
|
||||
await matcher.finish(ulang.get("liteyuki.invalid_command", TEXT="location") if tip else None)
|
||||
city_info = await city_lookup(stored_location, key, lang=qw_lang)
|
||||
city_name = stored_location
|
||||
if city_info.code == "200":
|
||||
location_data = city_info.location[0]
|
||||
else:
|
||||
await matcher.finish(ulang.get("weather.city_not_found", CITY=city_name) if tip else None)
|
||||
weather_now = await get_weather_now(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
|
||||
weather_daily = await get_weather_daily(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
|
||||
weather_hourly = await get_weather_hourly(key, location_data.id, lang=qw_lang, unit=unit, dev=is_dev)
|
||||
aqi = await get_airquality(key, location_data.id, lang=qw_lang, dev=is_dev)
|
||||
|
||||
image = await template2image(
|
||||
template=get_path("templates/weather_now.html", abs_path=True),
|
||||
templates={
|
||||
"data": {
|
||||
"params" : {
|
||||
"unit": unit,
|
||||
"lang": ulang.lang_code,
|
||||
},
|
||||
"weatherNow" : weather_now,
|
||||
"weatherDaily" : weather_daily,
|
||||
"weatherHourly": weather_hourly,
|
||||
"aqi" : aqi,
|
||||
"location" : location_data.dump(),
|
||||
"localization" : get_local_data(ulang.lang_code)
|
||||
}
|
||||
},
|
||||
debug=True,
|
||||
wait=1
|
||||
)
|
||||
return image
|
@@ -1,161 +0,0 @@
|
||||
import datetime
|
||||
import time
|
||||
|
||||
import aiohttp
|
||||
from nonebot import require
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from liteyuki.utils.base.config import get_config
|
||||
from liteyuki.utils.base.data import Database, LiteModel
|
||||
from liteyuki.utils.base.resource import get_path
|
||||
from liteyuki.utils.message.html_tool import template2image
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
require("nonebot_plugin_apscheduler")
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot_plugin_alconna import Alconna, AlconnaResult, CommandResult, Subcommand, UniMessage, on_alconna, Args
|
||||
|
||||
__author__ = "snowykami"
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="签名服务器状态",
|
||||
description="适用于ntqq的签名状态查看",
|
||||
usage="",
|
||||
type="application",
|
||||
homepage="https://github.com/snowykami/LiteyukiBot",
|
||||
extra={
|
||||
"liteyuki" : True,
|
||||
"toggleable" : True,
|
||||
"default_enable": True,
|
||||
}
|
||||
)
|
||||
|
||||
SIGN_COUNT_URLS: dict[str, str] = get_config("sign_count_urls", None)
|
||||
SIGN_COUNT_DURATION = get_config("sign_count_duration", 10)
|
||||
|
||||
|
||||
class SignCount(LiteModel):
|
||||
TABLE_NAME: str = "sign_count"
|
||||
time: float = 0.0
|
||||
count: int = 0
|
||||
sid: str = ""
|
||||
|
||||
|
||||
sign_db = Database("data/liteyuki/ntqq_sign.ldb")
|
||||
sign_db.auto_migrate(SignCount())
|
||||
|
||||
sign_status = on_alconna(Alconna(
|
||||
"sign",
|
||||
Subcommand(
|
||||
"chart",
|
||||
Args["limit", int, 10000]
|
||||
),
|
||||
Subcommand(
|
||||
"count"
|
||||
),
|
||||
Subcommand(
|
||||
"data"
|
||||
)
|
||||
))
|
||||
|
||||
cache_img: bytes = None
|
||||
|
||||
|
||||
@sign_status.assign("count")
|
||||
async def _():
|
||||
reply = "Current sign count:"
|
||||
for name, count in (await get_now_sign()).items():
|
||||
reply += f"\n{name}: {count[1]}"
|
||||
await sign_status.send(reply)
|
||||
|
||||
|
||||
@sign_status.assign("data")
|
||||
async def _():
|
||||
query_stamp = [1, 5, 10, 15]
|
||||
|
||||
reply = "Count from last " + ", ".join([str(i) for i in query_stamp]) + "mins"
|
||||
for name, url in SIGN_COUNT_URLS.items():
|
||||
count_data = []
|
||||
for stamp in query_stamp:
|
||||
count_rows = sign_db.all(SignCount(), "sid = ? and time > ?", url, time.time() - 60 * stamp)
|
||||
if len(count_rows) < 2:
|
||||
count_data.append(-1)
|
||||
else:
|
||||
count_data.append(count_rows[-1].count - count_rows[0].count)
|
||||
reply += f"\n{name}: " + ", ".join([str(i) for i in count_data])
|
||||
await sign_status.send(reply)
|
||||
|
||||
|
||||
@sign_status.assign("chart")
|
||||
async def _(arp: CommandResult = AlconnaResult()):
|
||||
limit = arp.result.subcommands.get("chart").args.get("limit")
|
||||
if limit == 10000:
|
||||
if cache_img:
|
||||
await sign_status.send(UniMessage.image(raw=cache_img))
|
||||
return
|
||||
img = await generate_chart(limit)
|
||||
await sign_status.send(UniMessage.image(raw=img))
|
||||
|
||||
|
||||
@scheduler.scheduled_job("interval", seconds=SIGN_COUNT_DURATION, next_run_time=datetime.datetime.now())
|
||||
async def update_sign_count():
|
||||
global cache_img
|
||||
if not SIGN_COUNT_URLS:
|
||||
return
|
||||
data = await get_now_sign()
|
||||
for name, count in data.items():
|
||||
await save_sign_count(count[0], count[1], SIGN_COUNT_URLS[name])
|
||||
|
||||
cache_img = await generate_chart(10000)
|
||||
|
||||
|
||||
async def get_now_sign() -> dict[str, tuple[float, int]]:
|
||||
"""
|
||||
Get the sign count and the time of the latest sign
|
||||
Returns:
|
||||
tuple[float, int] | None: (time, count)
|
||||
"""
|
||||
data = {}
|
||||
now = time.time()
|
||||
async with aiohttp.ClientSession() as client:
|
||||
for name, url in SIGN_COUNT_URLS.items():
|
||||
async with client.get(url) as resp:
|
||||
count = (await resp.json())["count"]
|
||||
data[name] = (now, count)
|
||||
return data
|
||||
|
||||
|
||||
async def save_sign_count(timestamp: float, count: int, sid: str):
|
||||
"""
|
||||
Save the sign count to the database
|
||||
Args:
|
||||
sid: the sign id, use url as the id
|
||||
count:
|
||||
timestamp (float): the time of the sign count (int): the count of the sign
|
||||
"""
|
||||
sign_db.save(SignCount(time=timestamp, count=count, sid=sid))
|
||||
|
||||
|
||||
async def generate_chart(limit):
|
||||
data = []
|
||||
for name, url in SIGN_COUNT_URLS.items():
|
||||
count_rows = sign_db.all(SignCount(), "sid = ? ORDER BY id DESC LIMIT ?", url, limit)
|
||||
count_rows.reverse()
|
||||
data.append(
|
||||
{
|
||||
"name" : name,
|
||||
# "data": [[row.time, row.count] for row in count_rows]
|
||||
"times" : [row.time for row in count_rows],
|
||||
"counts": [row.count for row in count_rows]
|
||||
}
|
||||
)
|
||||
print(len(count_rows))
|
||||
|
||||
img = await template2image(
|
||||
template=get_path("templates/sign_status.html", debug=True),
|
||||
templates={
|
||||
"data": data
|
||||
},
|
||||
debug=True
|
||||
)
|
||||
|
||||
return img
|
@@ -1,3 +0,0 @@
|
||||
name: Sign Status
|
||||
description: for Lagrange
|
||||
version: 2024.4.26
|
@@ -1,4 +0,0 @@
|
||||
.sign-chart {
|
||||
height: 400px;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
@@ -1,75 +0,0 @@
|
||||
// 数据类型声明
|
||||
// import * as echarts from 'echarts';
|
||||
|
||||
let data = JSON.parse(document.getElementById("data").innerText) // object
|
||||
const signChartDivTemplate = document.importNode(document.getElementById("sign-chart-template").content, true)
|
||||
data.forEach((item) => {
|
||||
let signChartDiv = signChartDivTemplate.cloneNode(true)
|
||||
let chartID = item["name"]
|
||||
// 初始化ECharts实例
|
||||
// 设置id
|
||||
signChartDiv.querySelector(".sign-chart").id = chartID
|
||||
document.body.appendChild(signChartDiv)
|
||||
|
||||
let signChart = echarts.init(document.getElementById(chartID))
|
||||
let timeCount = []
|
||||
|
||||
item["counts"].forEach((count, index) => {
|
||||
// 计算平均值,index - 1的count + index的count + index + 1的count /3
|
||||
if (index > 0) {
|
||||
timeCount.push((item["counts"][index] - item["counts"][index - 1]))
|
||||
}
|
||||
})
|
||||
|
||||
console.log(timeCount)
|
||||
|
||||
signChart.setOption(
|
||||
{
|
||||
animation: false,
|
||||
title: {
|
||||
text: item["name"],
|
||||
textStyle: {
|
||||
color: '#000000' // 设置标题文本颜色为红色
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: item["times"].map(timestampToTime),
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
min: Math.min(...item["counts"]),
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
min: Math.min(...timeCount),
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
data: item["counts"],
|
||||
type: 'line',
|
||||
yAxisIndex: 0
|
||||
},
|
||||
{
|
||||
data: timeCount,
|
||||
type: 'line',
|
||||
yAxisIndex: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
function timestampToTime(timestamp) {
|
||||
let date = new Date(timestamp * 1000)
|
||||
let Y = date.getFullYear() + '-'
|
||||
let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
|
||||
let D = date.getDate() + ' '
|
||||
let h = date.getHours() + ':'
|
||||
let m = date.getMinutes() + ':'
|
||||
let s = date.getSeconds()
|
||||
return M + D + h + m + s
|
||||
}
|