mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-09-29 00:32:06 +08:00
Compare commits
97
Commits
1.0a
..
062ee545b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
062ee545b1 | ||
|
|
ddbe75704d | ||
|
|
50c04f3625 | ||
|
|
c36c161b0e | ||
|
|
b4df0c0fe9 | ||
|
|
412654d165 | ||
|
|
2581e335af | ||
|
|
3ee0f6f2b1 | ||
|
|
0bd1d6c59c | ||
|
|
c839b44256 | ||
|
|
70bde00757 | ||
|
|
eb6d90ae88 | ||
|
|
4b8013d2d6 | ||
|
|
d528711641 | ||
|
|
1cc18bb195 | ||
|
|
74a9f3a843 | ||
|
|
e7f3c210df | ||
|
|
f94121080f | ||
|
|
761c8daac4 | ||
|
|
c667fc215e | ||
|
|
07be73c1b7 | ||
|
|
7e6896fa01 | ||
|
|
3cc882b116 | ||
|
|
ee699fb345 | ||
|
|
631e66d54f | ||
|
|
c7ef6fdb17 | ||
|
|
fb0a9813e1 | ||
|
|
6940c2f37b | ||
|
|
74ce848127 | ||
|
|
9e5c4aa3e7 | ||
|
|
7f460296dd | ||
|
|
b505307f2f | ||
|
|
4ab9382205 | ||
|
|
1e2aa99207 | ||
|
|
7472cabd48 | ||
|
|
d9e65057cf | ||
|
|
b12168b6b9 | ||
|
|
a63f26c3b6 | ||
|
|
095a123c3c | ||
|
|
f9a38a26b2 | ||
|
|
6124e217d0 | ||
|
|
11524bcb04 | ||
|
|
d5e5fac02d | ||
|
|
55da0046a2 | ||
|
|
977f0b13b3 | ||
|
|
2fed781350 | ||
|
|
6d1789bbee | ||
|
|
3deffcb46c | ||
|
|
be86e0bb7f | ||
|
|
67a2560de4 | ||
|
|
5c96761fd0 | ||
|
|
872d2499a2 | ||
|
|
7719be9866 | ||
|
|
7c153721f0 | ||
|
|
59d72c3b3d | ||
|
|
c571bfb133 | ||
|
|
da6d5b4be4 | ||
|
|
62fac483f2 | ||
|
|
61251ce137 | ||
|
|
30fe5a5393 | ||
|
|
3cf7c1d237 | ||
|
|
91f35ad63a | ||
|
|
a0b57b6bea | ||
|
|
205f4ff1fa | ||
|
|
b993450a23 | ||
|
|
d218c569d4 | ||
|
|
faa91b8bd4 | ||
|
|
582ad8c996 | ||
|
|
46a0768a45 | ||
|
|
8649aaaa54 | ||
|
|
6283c3d13d | ||
|
|
8f1e35954b | ||
|
|
9686a31419 | ||
|
|
632ec3e46e | ||
|
|
fb8811207e | ||
|
|
99eacdfc12 | ||
|
|
acfed0837a | ||
|
|
4bcc5aeea5 | ||
|
|
bd62698ea5 | ||
|
|
2921aed248 | ||
|
|
579558e59b | ||
|
|
fcb385cf01 | ||
|
|
c3193dd784 | ||
|
|
48cbb2bf1d | ||
|
|
1e7ae38684 | ||
|
|
1c5f66beee | ||
|
|
e0773174d0 | ||
|
|
8996cdf8f1 | ||
|
|
87f02fd0ef | ||
|
|
9625bdcd26 | ||
|
|
8b9ae7255b | ||
|
|
b537ec15b7 | ||
|
|
d0f296bc9c | ||
|
|
a020ea5c87 | ||
|
|
14f5842f10 | ||
|
|
6298685e09 | ||
|
|
96db5bf2a5 |
+18
-2
@@ -10,6 +10,9 @@ SESSION_EXPIRE_TIMEOUT=00:00:30
|
||||
|
||||
ALCONNA_USE_COMMAND_START=True
|
||||
|
||||
# ws连接密钥,若bot能被公网访问则建议打开该注释并设置该配置项
|
||||
# ONEBOT_ACCESS_TOKEN=""
|
||||
|
||||
# 全局图片统一使用bytes发送,当真寻与协议端不在同一服务器上时为True
|
||||
IMAGE_TO_BYTES = True
|
||||
|
||||
@@ -27,6 +30,19 @@ QBOT_ID_DATA = '{
|
||||
# 示例: "sqlite:data/db/zhenxun.db" 在data目录下建立db文件夹
|
||||
DB_URL = ""
|
||||
|
||||
# NONE: 不使用缓存, MEMORY: 使用内存缓存, REDIS: 使用Redis缓存
|
||||
CACHE_MODE = NONE
|
||||
|
||||
# REDIS配置,使用REDIS替换Cache内存缓存
|
||||
# REDIS地址
|
||||
# REDIS_HOST = "127.0.0.1"
|
||||
# REDIS端口
|
||||
# REDIS_PORT = 6379
|
||||
# REDIS密码
|
||||
# REDIS_PASSWORD = ""
|
||||
# REDIS过期时间
|
||||
# REDIS_EXPIRE = 600
|
||||
|
||||
# 系统代理
|
||||
# SYSTEM_PROXY = "http://127.0.0.1:7890"
|
||||
|
||||
@@ -40,7 +56,7 @@ PLATFORM_SUPERUSERS = '
|
||||
DRIVER=~fastapi+~httpx+~websockets
|
||||
|
||||
|
||||
# LOG_LEVEL=DEBUG
|
||||
# LOG_LEVEL = DEBUG
|
||||
# 服务器和端口
|
||||
HOST = 127.0.0.1
|
||||
PORT = 8080
|
||||
@@ -74,4 +90,4 @@ PORT = 8080
|
||||
# '
|
||||
|
||||
# application_commands的{"*": ["*"]}代表将全部应用命令注册为全局应用命令
|
||||
# {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册
|
||||
# {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册
|
||||
@@ -0,0 +1,58 @@
|
||||
#
|
||||
name: Create and publish a Docker image
|
||||
|
||||
# Configures this workflow to run on demand via workflow_dispatch.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
#
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
|
||||
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository.
|
||||
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Force Sync to Aliyun
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global http.postBuffer 524288000
|
||||
git config --global core.compression 0
|
||||
|
||||
- name: Add aliyun remote
|
||||
run: |
|
||||
git remote add aliyun https://${{secrets.ALIYUN_ACCOUNT}}:${{secrets.ALIYUN_PASSWORD}}@codeup.aliyun.com/67a361cf556e6cdab537117a/zhenxun-org/zhenxun_bot.git
|
||||
git fetch aliyun main --force # 强制更新本地引用
|
||||
|
||||
- name: Force push
|
||||
run: git push --progress --force aliyun HEAD:main
|
||||
+3
-13
@@ -32,6 +32,7 @@ MANIFEST
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
!resources.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
@@ -139,22 +140,11 @@ dmypy.json
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
demo.py
|
||||
test.py
|
||||
server_ip.py
|
||||
member_activity_handle.py
|
||||
Yu-Gi-Oh/
|
||||
csgo/
|
||||
fantasy_card/
|
||||
data/
|
||||
log/
|
||||
backup/
|
||||
extensive_plugin/
|
||||
test/
|
||||
bot.py
|
||||
.idea/
|
||||
resources/
|
||||
/configs/config.py
|
||||
configs/config.yaml
|
||||
.vscode/launch.json
|
||||
plugins_/
|
||||
|
||||
./.env.dev
|
||||
Vendored
+3
@@ -11,6 +11,8 @@
|
||||
"displayname",
|
||||
"flmt",
|
||||
"getbbox",
|
||||
"gitcode",
|
||||
"GITEE",
|
||||
"hibiapi",
|
||||
"httpx",
|
||||
"jsdelivr",
|
||||
@@ -27,6 +29,7 @@
|
||||
"unban",
|
||||
"Uninfo",
|
||||
"userinfo",
|
||||
"webui",
|
||||
"zhenxun"
|
||||
],
|
||||
"python.analysis.autoImportCompletions": true,
|
||||
|
||||
@@ -131,20 +131,20 @@ AccessToken: PUBLIC_ZHENXUN_TEST
|
||||
如果你系统是 **Windows** 且不想下载 Python
|
||||
可以使用整合包(Python3.10+zhenxun+webui)
|
||||
|
||||
文档地址:[整合包文档](https://hibikier.github.io/zhenxun_bot/beginner/)
|
||||
文档地址:[整合包文档](https://zhenxun-org.github.io/zhenxun_bot/beginner)
|
||||
|
||||
<details>
|
||||
<summary>下载地址</summary>
|
||||
|
||||
- **百度云:**
|
||||
https://pan.baidu.com/s/1ph4yzx1vdNbkxm9VBKDdgQ?pwd=971j
|
||||
https://pan.baidu.com/s/1MKGOoIgQW1qom_KT3rNhlg?pwd=t7iz
|
||||
|
||||
- **天翼云:**
|
||||
https://cloud.189.cn/web/share?code=jq67r2i2E7Fb
|
||||
访问码:8wxm
|
||||
- **夸克网盘:**
|
||||
https://pan.quark.cn/s/b4dc6cb8fb08
|
||||
访问码:Yi46
|
||||
|
||||
- **Google Drive:**
|
||||
https://drive.google.com/file/d/1cc3Dqjk0x5hWGLNeMkrFwWl8BvsK6KfD/view?usp=drive_link
|
||||
https://drive.google.com/drive/folders/1dcTWhPZhSt9WqLehyjF6Gj0CXQCS4OWh?usp=drive_link
|
||||
|
||||
</details>
|
||||
|
||||
@@ -287,6 +287,18 @@ DB_URL 是基于 Tortoise ORM 的数据库连接字符串,用于指定项目
|
||||
|
||||
[Zer](https://afdian.com/u/6bccdb2a60b411ec9ad452540025c377) [爱发电用户\_HTjk](https://afdian.com/u/6c7d0208064511ec8d7b52540025c377) [shenghuo2](https://afdian.com/u/bca13286102111eda2a052540025c377) [术樱](https://afdian.com/u/414da63a09a311ec8eb752540025c377) [飞火](https://afdian.com/u/404135f48ed711ec962152540025c377) [shenqi](https://afdian.net/u/fa923a8cfe3d11eba61752540025c377) [A_Kyuu](https://afdian.net/u/b83954fc2c1211eba9eb52540025c377) [疯狂混沌](https://afdian.net/u/789a2f9200cd11edb38352540025c377) [投冥](https://afdian.net/a/144514mm) [茶喵](https://afdian.net/u/fd22382eac4d11ecbfc652540025c377) [AemokpaTNR](https://afdian.net/u/1169bb8c8a9611edb0c152540025c377) [爱发电用户\_wrxn](https://afdian.net/u/4aa03d20db4311ecb1e752540025c377) [qqw](https://afdian.net/u/b71db4e2cc3e11ebb76652540025c377) [溫一壺月光下酒](https://afdian.net/u/ad667a5c650c11ed89bf52540025c377) [伝木](https://afdian.net/u/246b80683f9511edba7552540025c377) [阿奎](https://afdian.net/u/da41f72845d511ed930d52540025c377) [醉梦尘逸](https://afdian.net/u/bc11d2683cd011ed99b552540025c377) [Abc](https://afdian.net/u/870dc10a3cd311ed828852540025c377) [本喵无敌哒](https://afdian.net/u/dffaa9005bc911ebb69b52540025c377) [椎名冬羽](https://afdian.net/u/ca1ebd64395e11ed81b452540025c377) [kaito](https://afdian.net/u/a055e20a498811eab1f052540025c377) [笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) [请问一份爱多少钱](https://afdian.net/u/f57ef6602dbd11ed977f52540025c377) [咸鱼鱼鱼鱼](https://afdian.net/u/8e39b9a400e011ed9f4a52540025c377) [Kafka](https://afdian.net/u/41d66798ef6911ecbc5952540025c377) [墨然](https://afdian.net/u/8aa5874a644d11eb8a6752540025c377) [爱发电用户\_T9e4](https://afdian.net/u/2ad1bb82f3a711eca22852540025c377) [笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) [noahzark](https://afdian.net/a/noahzark) [腊条](https://afdian.net/u/f739c4d69eca11eba94b52540025c377) [ze roller](https://afdian.net/u/0e599e96257211ed805152540025c377) [爱发电用户\_4jrf](https://afdian.net/u/6b2cdcc817c611ed949152540025c377) [爱发电用户\_TBsd](https://afdian.net/u/db638b60217911ed9efd52540025c377) [烟寒若雨](https://afdian.net/u/067bd2161eec11eda62b52540025c377) [ln](https://afdian.net/u/b51914ba1c6611ed8a4e52540025c377) [爱发电用户\_b9S4](https://afdian.net/u/3d8f30581a2911edba6d52540025c377) [爱发电用户\_c58s](https://afdian.net/u/a6ad8dda195e11ed9a4152540025c377) [爱发电用户\_eNr9](https://afdian.net/u/05fdb41c0c9a11ed814952540025c377) [MangataAkihi](https://github.com/Sakuracio) [炀](https://afdian.net/u/69b76e9ec77b11ec874f52540025c377) [爱发电用户\_Bc6j](https://afdian.net/u/8546be24f44111eca64052540025c377) [大魔王](https://github.com/xipesoy) [CopilotLaLaLa](https://github.com/CopilotLaLaLa) [嘿小欧](https://afdian.net/u/daa4bec4f24911ec82e552540025c377) [回忆的秋千](https://afdian.net/u/e315d9c6f14f11ecbeef52540025c377) [十年くん](https://github.com/shinianj) [哇](https://afdian.net/u/9b266244f23911eca19052540025c377) [yajiwa](https://github.com/yajiwa) [爆金币](https://afdian.net/u/0d78879ef23711ecb22452540025c377)...
|
||||
|
||||
### 特别赞助
|
||||
|
||||
<div align=center>
|
||||
|
||||
<img width="60%" src="https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png" />
|
||||
|
||||
[亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne](https://edgeone.ai/zh?from=github)
|
||||
|
||||
**本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助**
|
||||
|
||||
</div>
|
||||
|
||||
## 📜 贡献指南
|
||||
|
||||
欢迎查看我们的 [贡献指南](CONTRIBUTING.md) 和 [行为守则](CODE_OF_CONDUCT.md) 以了解如何参与贡献。
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__: v0.2.4-2c97eea
|
||||
__version__: v0.2.4-da6d5b4
|
||||
|
||||
-1889
File diff suppressed because it is too large
Load Diff
Generated
+2331
-1564
File diff suppressed because it is too large
Load Diff
@@ -14,21 +14,21 @@ priority = "primary"
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
playwright = "^1.41.1"
|
||||
nonebot-adapter-onebot = "^2.3.1"
|
||||
nonebot-adapter-onebot = ">=2.3.1"
|
||||
nonebot-plugin-apscheduler = "^0.5"
|
||||
tortoise-orm = { extras = ["asyncpg"], version = "^0.20.0" }
|
||||
tortoise-orm = "^0.20.0"
|
||||
cattrs = "^23.2.3"
|
||||
ruamel-yaml = "^0.18.5"
|
||||
strenum = "^0.4.15"
|
||||
nonebot-plugin-session = "^0.2.3"
|
||||
ujson = "^5.9.0"
|
||||
nb-cli = "^1.3.0"
|
||||
nonebot2 = { extras = ["fastapi"], version = "^2.3.3" }
|
||||
nonebot-plugin-session = "^0.3.2"
|
||||
ujson = ">=5.9.0"
|
||||
nb-cli = ">=1.3.0"
|
||||
nonebot2 = { extras = ["fastapi"], version = ">=2.3.3" }
|
||||
pillow = "^10.0.0"
|
||||
retrying = "^1.3.4"
|
||||
aiofiles = "^23.2.1"
|
||||
nonebot-plugin-htmlrender = ">=0.6.0,<1.0.0"
|
||||
pypinyin = "^0.51.0"
|
||||
pypinyin = ">=0.51.0"
|
||||
beautifulsoup4 = "^4.12.3"
|
||||
lxml = "^5.1.0"
|
||||
psutil = "^5.9.8"
|
||||
@@ -36,15 +36,21 @@ feedparser = "^6.0.11"
|
||||
imagehash = "^4.3.1"
|
||||
cn2an = "^0.5.22"
|
||||
dateparser = "^1.2.0"
|
||||
bilireq = "0.2.3post0"
|
||||
bilireq = ">=0.2.10"
|
||||
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
|
||||
python-multipart = "^0.0.9"
|
||||
aiocache = "^0.12.2"
|
||||
aiocache = {extras = ["redis"], version = "^0.12.3"}
|
||||
py-cpuinfo = "^9.0.0"
|
||||
nonebot-plugin-alconna = "^0.54.0"
|
||||
nonebot-plugin-alconna = ">=0.56.0"
|
||||
tenacity = "^9.0.0"
|
||||
nonebot-plugin-uninfo = ">0.4.1"
|
||||
pydantic = "1.10.18"
|
||||
nonebot-plugin-uninfo = ">=0.7.3"
|
||||
nonebot-plugin-waiter = "^0.8.1"
|
||||
multidict = ">=6.0.0,!=6.3.2"
|
||||
pydantic = ">=1.0.0, <2.0.0"
|
||||
|
||||
redis = { version = ">=5", optional = true }
|
||||
asyncpg = { version = ">=0.20.0", optional = true }
|
||||
alibabacloud-devops20210625 = "^5.0.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
nonebug = "^0.4"
|
||||
@@ -56,7 +62,6 @@ respx = "^0.21.1"
|
||||
ruff = "^0.8.0"
|
||||
pre-commit = "^4.0.0"
|
||||
|
||||
|
||||
[tool.nonebot]
|
||||
plugins = [
|
||||
"nonebot_plugin_apscheduler",
|
||||
|
||||
Generated
+2414
-1631
File diff suppressed because it is too large
Load Diff
@@ -14,21 +14,21 @@ priority = "primary"
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
playwright = "^1.41.1"
|
||||
nonebot-adapter-onebot = "^2.3.1"
|
||||
nonebot-adapter-onebot = ">=2.3.1"
|
||||
nonebot-plugin-apscheduler = "^0.5"
|
||||
tortoise-orm = { extras = ["asyncpg"], version = "^0.20.0" }
|
||||
tortoise-orm = "^0.20.0"
|
||||
cattrs = "^23.2.3"
|
||||
ruamel-yaml = "^0.18.5"
|
||||
strenum = "^0.4.15"
|
||||
nonebot-plugin-session = "^0.2.3"
|
||||
ujson = "^5.9.0"
|
||||
nb-cli = "^1.3.0"
|
||||
nonebot2 = { extras = ["fastapi"], version = "^2.3.3" }
|
||||
nonebot-plugin-session = "^0.3.2"
|
||||
ujson = ">=5.9.0"
|
||||
nb-cli = ">=1.3.0"
|
||||
nonebot2 = { extras = ["fastapi"], version = ">=2.3.3" }
|
||||
pillow = "^10.0.0"
|
||||
retrying = "^1.3.4"
|
||||
aiofiles = "^23.2.1"
|
||||
nonebot-plugin-htmlrender = ">=0.6.0,<1.0.0"
|
||||
pypinyin = "^0.51.0"
|
||||
pypinyin = ">=0.51.0"
|
||||
beautifulsoup4 = "^4.12.3"
|
||||
lxml = "^5.1.0"
|
||||
psutil = "^5.9.8"
|
||||
@@ -36,15 +36,21 @@ feedparser = "^6.0.11"
|
||||
imagehash = "^4.3.1"
|
||||
cn2an = "^0.5.22"
|
||||
dateparser = "^1.2.0"
|
||||
bilireq = "0.2.3post0"
|
||||
bilireq = ">=0.2.10"
|
||||
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
|
||||
python-multipart = "^0.0.9"
|
||||
aiocache = "^0.12.2"
|
||||
aiocache = {extras = ["redis"], version = "^0.12.3"}
|
||||
py-cpuinfo = "^9.0.0"
|
||||
nonebot-plugin-alconna = "^0.54.0"
|
||||
nonebot-plugin-alconna = ">=0.56.0"
|
||||
tenacity = "^9.0.0"
|
||||
nonebot-plugin-uninfo = ">0.4.1"
|
||||
pydantic = "2.10.6"
|
||||
nonebot-plugin-uninfo = ">=0.7.3"
|
||||
nonebot-plugin-waiter = "^0.8.1"
|
||||
multidict = ">=6.0.0,!=6.3.2"
|
||||
pydantic = ">=2.0.0, <3.0.0"
|
||||
|
||||
redis = { version = ">=5", optional = true }
|
||||
asyncpg = { version = ">=0.20.0", optional = true }
|
||||
alibabacloud-devops20210625 = "^5.0.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
nonebug = "^0.4"
|
||||
|
||||
Generated
+2087
-1501
File diff suppressed because it is too large
Load Diff
+19
-12
@@ -14,21 +14,21 @@ priority = "primary"
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
playwright = "^1.41.1"
|
||||
nonebot-adapter-onebot = "^2.3.1"
|
||||
nonebot-adapter-onebot = ">=2.3.1"
|
||||
nonebot-plugin-apscheduler = "^0.5"
|
||||
tortoise-orm = { extras = ["asyncpg"], version = "^0.20.0" }
|
||||
tortoise-orm = "^0.20.0"
|
||||
cattrs = "^23.2.3"
|
||||
ruamel-yaml = "^0.18.5"
|
||||
strenum = "^0.4.15"
|
||||
nonebot-plugin-session = "^0.2.3"
|
||||
ujson = "^5.9.0"
|
||||
nb-cli = "^1.3.0"
|
||||
nonebot2 = { extras = ["fastapi"], version = "^2.3.3" }
|
||||
nonebot-plugin-session = "^0.3.2"
|
||||
ujson = ">=5.9.0"
|
||||
nb-cli = ">=1.3.0"
|
||||
nonebot2 = { extras = ["fastapi"], version = ">=2.3.3" }
|
||||
pillow = "^10.0.0"
|
||||
retrying = "^1.3.4"
|
||||
aiofiles = "^23.2.1"
|
||||
nonebot-plugin-htmlrender = ">=0.6.0,<1.0.0"
|
||||
pypinyin = "^0.51.0"
|
||||
pypinyin = ">=0.51.0"
|
||||
beautifulsoup4 = "^4.12.3"
|
||||
lxml = "^5.1.0"
|
||||
psutil = "^5.9.8"
|
||||
@@ -36,17 +36,21 @@ feedparser = "^6.0.11"
|
||||
imagehash = "^4.3.1"
|
||||
cn2an = "^0.5.22"
|
||||
dateparser = "^1.2.0"
|
||||
bilireq = "0.2.3post0"
|
||||
bilireq = ">=0.2.10"
|
||||
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
|
||||
python-multipart = "^0.0.9"
|
||||
aiocache = "^0.12.2"
|
||||
aiocache = {extras = ["redis"], version = "^0.12.3"}
|
||||
py-cpuinfo = "^9.0.0"
|
||||
nonebot-plugin-alconna = "^0.54.0"
|
||||
nonebot-plugin-alconna = ">=0.56.0"
|
||||
tenacity = "^9.0.0"
|
||||
nonebot-plugin-uninfo = ">0.4.1"
|
||||
nonebot-plugin-uninfo = ">=0.7.3"
|
||||
nonebot-plugin-waiter = "^0.8.1"
|
||||
multidict = ">=6.0.0,!=6.3.2"
|
||||
|
||||
redis = { version = ">=5", optional = true }
|
||||
asyncpg = { version = ">=0.20.0", optional = true }
|
||||
alibabacloud-devops20210625 = "^5.0.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
nonebug = "^0.4"
|
||||
pytest-cov = "^5.0.0"
|
||||
@@ -57,6 +61,9 @@ respx = "^0.21.1"
|
||||
ruff = "^0.8.0"
|
||||
pre-commit = "^4.0.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
redis = ["redis"]
|
||||
postgresql = ["asyncpg"]
|
||||
|
||||
[tool.nonebot]
|
||||
plugins = [
|
||||
@@ -138,4 +145,4 @@ asyncio_default_fixture_loop_scope = "session"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
+37
-130
@@ -1,130 +1,37 @@
|
||||
aiocache==0.12.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
aiofiles==23.2.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
aiosqlite==0.17.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
anyio==4.8.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
apscheduler==3.11.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
arclet-alconna-tools==0.7.10 ; python_version >= "3.10" and python_version < "4.0"
|
||||
arclet-alconna==1.8.35 ; python_version >= "3.10" and python_version < "4.0"
|
||||
arrow==1.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
async-timeout==5.0.1 ; python_version == "3.10"
|
||||
asyncpg==0.30.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
attrs==25.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
beautifulsoup4==4.13.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
bilireq==0.2.3.post0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
binaryornot==0.4.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cashews==7.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cattrs==23.2.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
certifi==2025.1.31 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cffi==1.17.1 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
|
||||
chardet==5.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
charset-normalizer==3.4.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
click==8.1.8 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cn2an==0.5.23 ; python_version >= "3.10" and python_version < "4.0"
|
||||
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and (platform_system == "Windows" or sys_platform == "win32")
|
||||
cookiecutter==2.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cryptography==44.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
dateparser==1.2.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
distlib==0.3.9 ; python_version >= "3.10" and python_version < "4.0"
|
||||
ecdsa==0.19.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
exceptiongroup==1.2.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
fastapi==0.115.8 ; python_version >= "3.10" and python_version < "4.0"
|
||||
feedparser==6.0.11 ; python_version >= "3.10" and python_version < "4.0"
|
||||
filelock==3.17.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
greenlet==3.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
grpcio==1.70.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
httpcore==0.16.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
httptools==0.6.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
httpx==0.23.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
idna==3.10 ; python_version >= "3.10" and python_version < "4.0"
|
||||
imagehash==4.3.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
importlib-metadata==8.6.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
iso8601==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
jinja2==3.1.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||
loguru==0.7.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
lxml==5.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
markdown-it-py==3.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
markdown==3.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||
markupsafe==3.0.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mdurl==0.1.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
msgpack==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
multidict==6.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nb-cli==1.4.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nepattern==0.7.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nonebot-adapter-onebot==2.4.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nonebot-plugin-alconna==0.54.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nonebot-plugin-apscheduler==0.5.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nonebot-plugin-htmlrender==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nonebot-plugin-session==0.2.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nonebot-plugin-uninfo==0.6.8 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nonebot-plugin-waiter==0.8.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nonebot2==2.4.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nonebot2[fastapi]==2.4.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
noneprompt==0.1.9 ; python_version >= "3.10" and python_version < "4.0"
|
||||
numpy==2.2.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pillow==10.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
platformdirs==4.3.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
playwright==1.50.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
proces==0.1.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||
prompt-toolkit==3.0.50 ; python_version >= "3.10" and python_version < "4.0"
|
||||
propcache==0.2.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
protobuf==4.25.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
psutil==5.9.8 ; python_version >= "3.10" and python_version < "4.0"
|
||||
py-cpuinfo==9.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pyasn1==0.6.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pycparser==2.22 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy"
|
||||
pydantic-core==2.27.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pydantic==2.10.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pyee==12.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pyfiglet==1.0.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pygments==2.19.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pygtrie==2.5.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pymdown-extensions==10.14.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pypika-tortoise==0.1.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pypinyin==0.51.0 ; python_version >= "3.10" and python_version < "4"
|
||||
python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
python-jose[cryptography]==3.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
python-markdown-math==0.8 ; python_version >= "3.10" and python_version < "4.0"
|
||||
python-multipart==0.0.9 ; python_version >= "3.10" and python_version < "4.0"
|
||||
python-slugify==8.0.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pytz==2025.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pywavelets==1.8.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pyyaml==6.0.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
regex==2024.11.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
requests==2.32.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
retrying==1.3.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
rfc3986[idna2008]==1.5.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
rich==13.9.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
rsa==4.9 ; python_version >= "3.10" and python_version < "4"
|
||||
ruamel-yaml-clib==0.2.12 ; platform_python_implementation == "CPython" and python_version < "3.13" and python_version >= "3.10"
|
||||
ruamel-yaml==0.18.10 ; python_version >= "3.10" and python_version < "4.0"
|
||||
scipy==1.15.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
sgmllib3k==1.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
six==1.17.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
sniffio==1.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
soupsieve==2.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
starlette==0.45.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
strenum==0.4.15 ; python_version >= "3.10" and python_version < "4.0"
|
||||
tarina==0.6.8 ; python_version >= "3.10" and python_version < "4.0"
|
||||
tenacity==9.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
text-unidecode==1.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
tomli==2.2.1 ; python_version == "3.10"
|
||||
tomlkit==0.13.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
tortoise-orm[asyncpg]==0.20.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
types-python-dateutil==2.9.0.20241206 ; python_version >= "3.10" and python_version < "4.0"
|
||||
typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
tzdata==2025.1 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows"
|
||||
tzlocal==5.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
ujson==5.10.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
urllib3==2.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
uvicorn[standard]==0.34.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
uvloop==0.21.0 ; sys_platform != "win32" and sys_platform != "cygwin" and platform_python_implementation != "PyPy" and python_version >= "3.10" and python_version < "4.0"
|
||||
virtualenv==20.29.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
watchfiles==0.24.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
wcwidth==0.2.13 ; python_version >= "3.10" and python_version < "4.0"
|
||||
websockets==14.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
win32-setctime==1.2.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32"
|
||||
yarl==1.18.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
zipp==3.21.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
playwright>=1.41.1,<2.0.0
|
||||
nonebot-adapter-onebot>=2.3.1
|
||||
nonebot-plugin-apscheduler>=0.5,<0.6
|
||||
tortoise-orm>=0.20.0,<0.21.0
|
||||
cattrs>=23.2.3,<24.0.0
|
||||
ruamel.yaml>=0.18.5,<0.19.0
|
||||
strenum>=0.4.15,<0.5.0
|
||||
nonebot-plugin-session>=0.3.2,<0.4.0
|
||||
ujson>=5.9.0
|
||||
nb-cli>=1.3.0
|
||||
nonebot2[fastapi]>=2.3.3
|
||||
Pillow>=10.0.0,<11.0.0
|
||||
retrying>=1.3.4,<2.0.0
|
||||
aiofiles>=23.2.1,<24.0.0
|
||||
nonebot-plugin-htmlrender>=0.6.0,<1.0.0
|
||||
pypinyin>=0.51.0
|
||||
beautifulsoup4>=4.12.3,<5.0.0
|
||||
lxml>=5.1.0,<6.0.0
|
||||
psutil>=5.9.8,<6.0.0
|
||||
feedparser>=6.0.11,<7.0.0
|
||||
ImageHash>=4.3.1,<5.0.0
|
||||
cn2an>=0.5.22,<0.6.0
|
||||
dateparser>=1.2.0,<2.0.0
|
||||
bilireq>=0.2.10
|
||||
python-jose[cryptography]>=3.3.0,<4.0.0
|
||||
python-multipart>=0.0.9,<0.1.0
|
||||
aiocache[redis]>=0.12.3,<0.13.0
|
||||
py-cpuinfo>=9.0.0,<10.0.0
|
||||
nonebot-plugin-alconna>=0.56.0
|
||||
tenacity>=9.0.0,<10.0.0
|
||||
nonebot-plugin-uninfo>=0.7.3
|
||||
nonebot-plugin-waiter>=0.8.1,<0.9.0
|
||||
multidict>=6.0.0,<7.0.0,!=6.3.2
|
||||
alibabacloud-devops20210625>=5.0.2,<6.0.0
|
||||
|
||||
redis>=5
|
||||
asyncpg>=0.20.0
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
require_resources_version: ">=1.0.0"
|
||||
@@ -9,11 +9,16 @@ import zipfile
|
||||
from nonebot.adapters.onebot.v11 import Bot
|
||||
from nonebot.adapters.onebot.v11.message import Message
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from respx import MockRouter
|
||||
|
||||
from tests.config import BotId, GroupId, MessageId, UserId
|
||||
from tests.utils import _v11_group_message_event, _v11_private_message_send
|
||||
from tests.utils import (
|
||||
_v11_group_message_event,
|
||||
_v11_private_message_send,
|
||||
get_reply_cq,
|
||||
)
|
||||
from tests.utils import get_response_json as _get_response_json
|
||||
|
||||
|
||||
@@ -27,60 +32,32 @@ def init_mocked_api(mocked_api: MockRouter) -> None:
|
||||
name="release_latest",
|
||||
).respond(json=get_response_json("release_latest.json"))
|
||||
|
||||
mocked_api.head(
|
||||
url="https://raw.githubusercontent.com/",
|
||||
name="head_raw",
|
||||
).respond(text="")
|
||||
mocked_api.head(
|
||||
url="https://github.com/",
|
||||
name="head_github",
|
||||
).respond(text="")
|
||||
mocked_api.head(
|
||||
url="https://codeload.github.com/",
|
||||
name="head_codeload",
|
||||
).respond(text="")
|
||||
|
||||
mocked_api.get(
|
||||
url="https://raw.githubusercontent.com/HibiKier/zhenxun_bot/dev/__version__",
|
||||
name="dev_branch_version",
|
||||
).respond(text="__version__: v0.2.2-e6f17c4")
|
||||
mocked_api.get(
|
||||
url="https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/__version__",
|
||||
name="main_branch_version",
|
||||
).respond(text="__version__: v0.2.2-e6f17c4")
|
||||
mocked_api.get(
|
||||
url="https://api.github.com/repos/HibiKier/zhenxun_bot/tarball/v0.2.2",
|
||||
name="release_download_url",
|
||||
).respond(
|
||||
status_code=302,
|
||||
headers={
|
||||
"Location": "https://codeload.github.com/HibiKier/zhenxun_bot/legacy.tar.gz/refs/tags/v0.2.2"
|
||||
},
|
||||
)
|
||||
|
||||
tar_buffer = io.BytesIO()
|
||||
zip_bytes = io.BytesIO()
|
||||
|
||||
from zhenxun.builtin_plugins.auto_update.config import (
|
||||
PYPROJECT_FILE_STRING,
|
||||
PYPROJECT_LOCK_FILE_STRING,
|
||||
REPLACE_FOLDERS,
|
||||
REQ_TXT_FILE_STRING,
|
||||
)
|
||||
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
|
||||
|
||||
# 指定要添加到压缩文件中的文件路径列表
|
||||
file_paths: list[str] = [
|
||||
PYPROJECT_FILE_STRING,
|
||||
PYPROJECT_LOCK_FILE_STRING,
|
||||
REQ_TXT_FILE_STRING,
|
||||
ZhenxunRepoManager.config.PYPROJECT_FILE_STRING,
|
||||
ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING,
|
||||
ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING,
|
||||
]
|
||||
|
||||
# 打开一个tarfile对象,写入到上面创建的BytesIO对象中
|
||||
with tarfile.open(mode="w:gz", fileobj=tar_buffer) as tar:
|
||||
add_files_and_folders_to_tar(tar, file_paths, folders=REPLACE_FOLDERS)
|
||||
add_files_and_folders_to_tar(
|
||||
tar,
|
||||
file_paths,
|
||||
folders=ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS,
|
||||
)
|
||||
|
||||
with zipfile.ZipFile(zip_bytes, mode="w", compression=zipfile.ZIP_DEFLATED) as zipf:
|
||||
add_files_and_folders_to_zip(zipf, file_paths, folders=REPLACE_FOLDERS)
|
||||
add_files_and_folders_to_zip(
|
||||
zipf,
|
||||
file_paths,
|
||||
folders=ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS,
|
||||
)
|
||||
|
||||
mocked_api.get(
|
||||
url="https://codeload.github.com/HibiKier/zhenxun_bot/legacy.tar.gz/refs/tags/v0.2.2",
|
||||
@@ -88,12 +65,6 @@ def init_mocked_api(mocked_api: MockRouter) -> None:
|
||||
).respond(
|
||||
content=tar_buffer.getvalue(),
|
||||
)
|
||||
mocked_api.get(
|
||||
url="https://github.com/HibiKier/zhenxun_bot/archive/refs/heads/dev.zip",
|
||||
name="dev_download_url",
|
||||
).respond(
|
||||
content=zip_bytes.getvalue(),
|
||||
)
|
||||
mocked_api.get(
|
||||
url="https://github.com/HibiKier/zhenxun_bot/archive/refs/heads/main.zip",
|
||||
name="main_download_url",
|
||||
@@ -195,54 +166,52 @@ def add_directory_to_tar(tarinfo, tar):
|
||||
|
||||
|
||||
def init_mocker_path(mocker: MockerFixture, tmp_path: Path):
|
||||
from zhenxun.builtin_plugins.auto_update.config import (
|
||||
PYPROJECT_FILE_STRING,
|
||||
PYPROJECT_LOCK_FILE_STRING,
|
||||
REQ_TXT_FILE_STRING,
|
||||
VERSION_FILE_STRING,
|
||||
)
|
||||
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
|
||||
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.auto_update._data_source.install_requirement",
|
||||
"zhenxun.utils.manager.virtual_env_package_manager.VirtualEnvPackageManager.install_requirement",
|
||||
return_value=None,
|
||||
)
|
||||
mock_tmp_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.auto_update._data_source.TMP_PATH",
|
||||
"zhenxun.configs.path_config.TEMP_PATH",
|
||||
new=tmp_path / "auto_update",
|
||||
)
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.auto_update._data_source.BASE_PATH",
|
||||
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_CODE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
mock_backup_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.auto_update._data_source.BACKUP_PATH",
|
||||
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_BACKUP_PATH",
|
||||
new=tmp_path / "backup",
|
||||
)
|
||||
mock_download_gz_file = mocker.patch(
|
||||
"zhenxun.builtin_plugins.auto_update._data_source.DOWNLOAD_GZ_FILE",
|
||||
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_DOWNLOAD_FILE",
|
||||
new=mock_tmp_path / "download_latest_file.tar.gz",
|
||||
)
|
||||
mock_download_zip_file = mocker.patch(
|
||||
"zhenxun.builtin_plugins.auto_update._data_source.DOWNLOAD_ZIP_FILE",
|
||||
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_UNZIP_PATH",
|
||||
new=mock_tmp_path / "download_latest_file.zip",
|
||||
)
|
||||
mock_pyproject_file = mocker.patch(
|
||||
"zhenxun.builtin_plugins.auto_update._data_source.PYPROJECT_FILE",
|
||||
new=tmp_path / PYPROJECT_FILE_STRING,
|
||||
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.PYPROJECT_FILE",
|
||||
new=tmp_path / ZhenxunRepoManager.config.PYPROJECT_FILE_STRING,
|
||||
)
|
||||
mock_pyproject_lock_file = mocker.patch(
|
||||
"zhenxun.builtin_plugins.auto_update._data_source.PYPROJECT_LOCK_FILE",
|
||||
new=tmp_path / PYPROJECT_LOCK_FILE_STRING,
|
||||
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE",
|
||||
new=tmp_path / ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING,
|
||||
)
|
||||
mock_req_txt_file = mocker.patch(
|
||||
"zhenxun.builtin_plugins.auto_update._data_source.REQ_TXT_FILE",
|
||||
new=tmp_path / REQ_TXT_FILE_STRING,
|
||||
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.REQUIREMENTS_FILE",
|
||||
new=tmp_path / ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING,
|
||||
)
|
||||
mock_version_file = mocker.patch(
|
||||
"zhenxun.builtin_plugins.auto_update._data_source.VERSION_FILE",
|
||||
new=tmp_path / VERSION_FILE_STRING,
|
||||
"zhenxun.utils.manager.zhenxun_repo_manager.ZhenxunRepoManager.config.ZHENXUN_BOT_VERSION_FILE",
|
||||
new=tmp_path / ZhenxunRepoManager.config.ZHENXUN_BOT_VERSION_FILE_STRING,
|
||||
)
|
||||
open(mock_version_file, "w").write("__version__: v0.2.2")
|
||||
open(ZhenxunRepoManager.config.ZHENXUN_BOT_VERSION_FILE, "w").write(
|
||||
"__version__: v0.2.2"
|
||||
)
|
||||
return (
|
||||
mock_tmp_path,
|
||||
mock_base_path,
|
||||
@@ -256,6 +225,7 @@ def init_mocker_path(mocker: MockerFixture, tmp_path: Path):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_check_update_release(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
@@ -267,12 +237,7 @@ async def test_check_update_release(
|
||||
测试检查更新(release)
|
||||
"""
|
||||
from zhenxun.builtin_plugins.auto_update import _matcher
|
||||
from zhenxun.builtin_plugins.auto_update.config import (
|
||||
PYPROJECT_FILE_STRING,
|
||||
PYPROJECT_LOCK_FILE_STRING,
|
||||
REPLACE_FOLDERS,
|
||||
REQ_TXT_FILE_STRING,
|
||||
)
|
||||
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
|
||||
@@ -291,7 +256,7 @@ async def test_check_update_release(
|
||||
# 确保目录下有一个子目录,以便 os.listdir() 能返回一个目录名
|
||||
mock_tmp_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for folder in REPLACE_FOLDERS:
|
||||
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
|
||||
(mock_base_path / folder).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
mock_pyproject_file.write_bytes(b"")
|
||||
@@ -301,7 +266,7 @@ async def test_check_update_release(
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
bot = create_bot(ctx)
|
||||
bot = cast(Bot, bot)
|
||||
raw_message = "检查更新 release"
|
||||
raw_message = "检查更新 release -z"
|
||||
event = _v11_group_message_event(
|
||||
raw_message,
|
||||
self_id=BotId.QQ_BOT,
|
||||
@@ -311,17 +276,23 @@ async def test_check_update_release(
|
||||
to_me=True,
|
||||
)
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(f"{get_reply_cq(MessageId.MESSAGE_ID)}正在进行检查更新..."),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
ctx.should_call_api(
|
||||
"send_msg",
|
||||
_v11_private_message_send(
|
||||
message="检测真寻已更新,版本更新:v0.2.2 -> v0.2.2\n开始更新...",
|
||||
message="检测真寻已更新,当前版本:v0.2.2\n开始更新...",
|
||||
user_id=UserId.SUPERUSER,
|
||||
),
|
||||
)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(
|
||||
"版本更新完成\n版本: v0.2.2 -> v0.2.2\n请重新启动真寻以完成更新!"
|
||||
"版本更新完成!\n版本: v0.2.2 -> v0.2.2\n请重新启动真寻以完成更新!"
|
||||
),
|
||||
result=None,
|
||||
bot=bot,
|
||||
@@ -330,9 +301,13 @@ async def test_check_update_release(
|
||||
assert mocked_api["release_latest"].called
|
||||
assert mocked_api["release_download_url_redirect"].called
|
||||
|
||||
assert (mock_backup_path / PYPROJECT_FILE_STRING).exists()
|
||||
assert (mock_backup_path / PYPROJECT_LOCK_FILE_STRING).exists()
|
||||
assert (mock_backup_path / REQ_TXT_FILE_STRING).exists()
|
||||
assert (mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_FILE_STRING).exists()
|
||||
assert (
|
||||
mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING
|
||||
).exists()
|
||||
assert (
|
||||
mock_backup_path / ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING
|
||||
).exists()
|
||||
|
||||
assert not mock_download_gz_file.exists()
|
||||
assert not mock_download_zip_file.exists()
|
||||
@@ -341,12 +316,13 @@ async def test_check_update_release(
|
||||
assert mock_pyproject_lock_file.read_bytes() == b"new"
|
||||
assert mock_req_txt_file.read_bytes() == b"new"
|
||||
|
||||
for folder in REPLACE_FOLDERS:
|
||||
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
|
||||
assert not (mock_base_path / folder).exists()
|
||||
for folder in REPLACE_FOLDERS:
|
||||
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
|
||||
assert (mock_backup_path / folder).exists()
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_check_update_main(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
@@ -358,12 +334,9 @@ async def test_check_update_main(
|
||||
测试检查更新(正式环境)
|
||||
"""
|
||||
from zhenxun.builtin_plugins.auto_update import _matcher
|
||||
from zhenxun.builtin_plugins.auto_update.config import (
|
||||
PYPROJECT_FILE_STRING,
|
||||
PYPROJECT_LOCK_FILE_STRING,
|
||||
REPLACE_FOLDERS,
|
||||
REQ_TXT_FILE_STRING,
|
||||
)
|
||||
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
|
||||
|
||||
ZhenxunRepoManager.zhenxun_zip_update = mocker.Mock(return_value="v0.2.2-e6f17c4")
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
|
||||
@@ -381,7 +354,7 @@ async def test_check_update_main(
|
||||
|
||||
# 确保目录下有一个子目录,以便 os.listdir() 能返回一个目录名
|
||||
mock_tmp_path.mkdir(parents=True, exist_ok=True)
|
||||
for folder in REPLACE_FOLDERS:
|
||||
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
|
||||
(mock_base_path / folder).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
mock_pyproject_file.write_bytes(b"")
|
||||
@@ -391,7 +364,7 @@ async def test_check_update_main(
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
bot = create_bot(ctx)
|
||||
bot = cast(Bot, bot)
|
||||
raw_message = "检查更新 main -r"
|
||||
raw_message = "检查更新 main -r -z"
|
||||
event = _v11_group_message_event(
|
||||
raw_message,
|
||||
self_id=BotId.QQ_BOT,
|
||||
@@ -401,30 +374,39 @@ async def test_check_update_main(
|
||||
to_me=True,
|
||||
)
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(f"{get_reply_cq(MessageId.MESSAGE_ID)}正在进行检查更新..."),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
ctx.should_call_api(
|
||||
"send_msg",
|
||||
_v11_private_message_send(
|
||||
message="检测真寻已更新,版本更新:v0.2.2 -> v0.2.2-e6f17c4\n"
|
||||
"开始更新...",
|
||||
message="检测真寻已更新,当前版本:v0.2.2\n开始更新...",
|
||||
user_id=UserId.SUPERUSER,
|
||||
),
|
||||
)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(
|
||||
"版本更新完成\n"
|
||||
"版本更新完成!\n"
|
||||
"版本: v0.2.2 -> v0.2.2-e6f17c4\n"
|
||||
"请重新启动真寻以完成更新!\n"
|
||||
"资源文件更新成功!"
|
||||
"真寻资源更新完成!"
|
||||
),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
ctx.should_finished(_matcher)
|
||||
assert mocked_api["main_download_url"].called
|
||||
assert (mock_backup_path / PYPROJECT_FILE_STRING).exists()
|
||||
assert (mock_backup_path / PYPROJECT_LOCK_FILE_STRING).exists()
|
||||
assert (mock_backup_path / REQ_TXT_FILE_STRING).exists()
|
||||
assert (mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_FILE_STRING).exists()
|
||||
assert (
|
||||
mock_backup_path / ZhenxunRepoManager.config.PYPROJECT_LOCK_FILE_STRING
|
||||
).exists()
|
||||
assert (
|
||||
mock_backup_path / ZhenxunRepoManager.config.REQUIREMENTS_FILE_STRING
|
||||
).exists()
|
||||
|
||||
assert not mock_download_gz_file.exists()
|
||||
assert not mock_download_zip_file.exists()
|
||||
@@ -433,7 +415,7 @@ async def test_check_update_main(
|
||||
assert mock_pyproject_lock_file.read_bytes() == b"new"
|
||||
assert mock_req_txt_file.read_bytes() == b"new"
|
||||
|
||||
for folder in REPLACE_FOLDERS:
|
||||
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
|
||||
assert (mock_base_path / folder).exists()
|
||||
for folder in REPLACE_FOLDERS:
|
||||
for folder in ZhenxunRepoManager.config.ZHENXUN_BOT_UPDATE_FOLDERS:
|
||||
assert (mock_backup_path / folder).exists()
|
||||
|
||||
@@ -4,12 +4,11 @@ from pathlib import Path
|
||||
import platform
|
||||
from typing import cast
|
||||
|
||||
import nonebot
|
||||
from nonebot.adapters.onebot.v11 import Bot
|
||||
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from respx import MockRouter
|
||||
|
||||
from tests.config import BotId, GroupId, MessageId, UserId
|
||||
from tests.utils import _v11_group_message_event
|
||||
@@ -66,9 +65,11 @@ def init_mocker(mocker: MockerFixture, tmp_path: Path):
|
||||
mock_platform = mocker.patch("zhenxun.builtin_plugins.check.data_source.platform")
|
||||
mock_platform.uname.return_value = platform_uname
|
||||
|
||||
mock_template_to_pic = mocker.patch("zhenxun.builtin_plugins.check.template_to_pic")
|
||||
mock_template_to_pic_return = mocker.AsyncMock()
|
||||
mock_template_to_pic.return_value = mock_template_to_pic_return
|
||||
mock_render_service = mocker.patch(
|
||||
"zhenxun.builtin_plugins.check.renderer_service.render"
|
||||
)
|
||||
mock_render_service_return = mocker.AsyncMock()
|
||||
mock_render_service.return_value = mock_render_service_return
|
||||
|
||||
mock_build_message = mocker.patch(
|
||||
"zhenxun.builtin_plugins.check.MessageUtils.build_message"
|
||||
@@ -76,26 +77,21 @@ def init_mocker(mocker: MockerFixture, tmp_path: Path):
|
||||
mock_build_message_return = mocker.AsyncMock()
|
||||
mock_build_message.return_value = mock_build_message_return
|
||||
|
||||
mock_template_path_new = tmp_path / "resources" / "template"
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.check.TEMPLATE_PATH", new=mock_template_path_new
|
||||
)
|
||||
return (
|
||||
mock_psutil,
|
||||
mock_cpuinfo,
|
||||
mock_platform,
|
||||
mock_template_to_pic,
|
||||
mock_template_to_pic_return,
|
||||
mock_render_service,
|
||||
mock_render_service_return,
|
||||
mock_build_message,
|
||||
mock_build_message_return,
|
||||
mock_template_path_new,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_check(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -103,18 +99,15 @@ async def test_check(
|
||||
测试自检
|
||||
"""
|
||||
from zhenxun.builtin_plugins.check import _self_check_matcher
|
||||
from zhenxun.builtin_plugins.check.data_source import __get_version
|
||||
from zhenxun.configs.config import BotConfig
|
||||
|
||||
(
|
||||
mock_psutil,
|
||||
mock_cpuinfo,
|
||||
mock_platform,
|
||||
mock_template_to_pic,
|
||||
mock_template_to_pic_return,
|
||||
mock_render_service,
|
||||
mock_render_service_return,
|
||||
mock_build_message,
|
||||
mock_build_message_return,
|
||||
mock_template_path_new,
|
||||
) = init_mocker(mocker, tmp_path)
|
||||
async with app.test_matcher(_self_check_matcher) as ctx:
|
||||
bot = create_bot(ctx)
|
||||
@@ -131,49 +124,15 @@ async def test_check(
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
ctx.should_ignore_rule(_self_check_matcher)
|
||||
|
||||
data = {
|
||||
"cpu_info": f"{mock_psutil.cpu_percent.return_value}% "
|
||||
+ f"- {mock_psutil.cpu_freq.return_value.current}Ghz "
|
||||
+ f"[{mock_psutil.cpu_count.return_value} core]",
|
||||
"cpu_process": mock_psutil.cpu_percent.return_value,
|
||||
"ram_info": f"{round(mock_psutil.virtual_memory.return_value.used / (1024 ** 3), 1)}" # noqa: E501
|
||||
+ f" / {round(mock_psutil.virtual_memory.return_value.total / (1024 ** 3), 1)}"
|
||||
+ " GB",
|
||||
"ram_process": mock_psutil.virtual_memory.return_value.percent,
|
||||
"swap_info": f"{round(mock_psutil.swap_memory.return_value.used / (1024 ** 3), 1)}" # noqa: E501
|
||||
+ f" / {round(mock_psutil.swap_memory.return_value.total / (1024 ** 3), 1)} GB",
|
||||
"swap_process": mock_psutil.swap_memory.return_value.percent,
|
||||
"disk_info": f"{round(mock_psutil.disk_usage.return_value.used / (1024 ** 3), 1)}" # noqa: E501
|
||||
+ f" / {round(mock_psutil.disk_usage.return_value.total / (1024 ** 3), 1)} GB",
|
||||
"disk_process": mock_psutil.disk_usage.return_value.percent,
|
||||
"brand_raw": cpuinfo_get_cpu_info["brand_raw"],
|
||||
"baidu": "red",
|
||||
"google": "red",
|
||||
"system": f"{platform_uname.system} " f"{platform_uname.release}",
|
||||
"version": __get_version(),
|
||||
"plugin_count": len(nonebot.get_loaded_plugins()),
|
||||
"nickname": BotConfig.self_nickname,
|
||||
}
|
||||
|
||||
mock_template_to_pic.assert_awaited_once_with(
|
||||
template_path=str((mock_template_path_new / "check").absolute()),
|
||||
template_name="main.html",
|
||||
templates={"data": data},
|
||||
pages={
|
||||
"viewport": {"width": 195, "height": 750},
|
||||
"base_url": f"file://{mock_template_path_new.absolute()}",
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
mock_template_to_pic.assert_awaited_once()
|
||||
mock_build_message.assert_called_once_with(mock_template_to_pic_return)
|
||||
mock_render_service.assert_awaited_once()
|
||||
mock_build_message.assert_called_once_with(mock_render_service_return)
|
||||
mock_build_message_return.send.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_check_arm(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -181,8 +140,6 @@ async def test_check_arm(
|
||||
测试自检(arm)
|
||||
"""
|
||||
from zhenxun.builtin_plugins.check import _self_check_matcher
|
||||
from zhenxun.builtin_plugins.check.data_source import __get_version
|
||||
from zhenxun.configs.config import BotConfig
|
||||
|
||||
platform_uname_arm = platform.uname_result(
|
||||
system="Linux",
|
||||
@@ -203,11 +160,10 @@ async def test_check_arm(
|
||||
mock_psutil,
|
||||
mock_cpuinfo,
|
||||
mock_platform,
|
||||
mock_template_to_pic,
|
||||
mock_template_to_pic_return,
|
||||
mock_render_service,
|
||||
mock_render_service_return,
|
||||
mock_build_message,
|
||||
mock_build_message_return,
|
||||
mock_template_path_new,
|
||||
) = init_mocker(mocker, tmp_path)
|
||||
|
||||
mock_platform.uname.return_value = platform_uname_arm
|
||||
@@ -228,35 +184,6 @@ async def test_check_arm(
|
||||
)
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
ctx.should_ignore_rule(_self_check_matcher)
|
||||
mock_template_to_pic.assert_awaited_once_with(
|
||||
template_path=str((mock_template_path_new / "check").absolute()),
|
||||
template_name="main.html",
|
||||
templates={
|
||||
"data": {
|
||||
"cpu_info": "1.0% - 0.0Ghz [1 core]",
|
||||
"cpu_process": 1.0,
|
||||
"ram_info": "1.0 / 1.0 GB",
|
||||
"ram_process": 100.0,
|
||||
"swap_info": "1.0 / 1.0 GB",
|
||||
"swap_process": 100.0,
|
||||
"disk_info": "1.0 / 1.0 GB",
|
||||
"disk_process": 100.0,
|
||||
"brand_raw": "",
|
||||
"baidu": "red",
|
||||
"google": "red",
|
||||
"system": f"{platform_uname_arm.system} "
|
||||
f"{platform_uname_arm.release}",
|
||||
"version": __get_version(),
|
||||
"plugin_count": len(nonebot.get_loaded_plugins()),
|
||||
"nickname": BotConfig.self_nickname,
|
||||
}
|
||||
},
|
||||
pages={
|
||||
"viewport": {"width": 195, "height": 750},
|
||||
"base_url": f"file://{mock_template_path_new.absolute()}",
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
mock_subprocess_check_output.assert_has_calls(
|
||||
[
|
||||
mocker.call(["lscpu"], env=mock_environ_copy_return),
|
||||
@@ -270,6 +197,6 @@ async def test_check_arm(
|
||||
mocker.call().decode().split().__getitem__().__float__(),
|
||||
] # type: ignore
|
||||
)
|
||||
mock_template_to_pic.assert_awaited_once()
|
||||
mock_build_message.assert_called_once_with(mock_template_to_pic_return)
|
||||
mock_render_service.assert_awaited_once()
|
||||
mock_build_message.assert_called_once_with(mock_render_service_return)
|
||||
mock_build_message_return.send.assert_awaited_once()
|
||||
|
||||
@@ -8,21 +8,17 @@ from nonebot.adapters.onebot.v11.message import Message
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from respx import MockRouter
|
||||
|
||||
from tests.builtin_plugins.plugin_store.utils import init_mocked_api
|
||||
from tests.config import BotId, GroupId, MessageId, UserId
|
||||
from tests.utils import _v11_group_message_event
|
||||
|
||||
test_path = Path(__file__).parent.parent.parent
|
||||
|
||||
@pytest.mark.parametrize("package_api", ["jsd", "gh"])
|
||||
@pytest.mark.parametrize("is_commit", [True, False])
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_add_plugin_basic(
|
||||
package_api: str,
|
||||
is_commit: bool,
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -31,24 +27,12 @@ async def test_add_plugin_basic(
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
|
||||
if package_api != "jsd":
|
||||
mocked_api["zhenxun_bot_plugins_metadata"].respond(404)
|
||||
if package_api != "gh":
|
||||
mocked_api["zhenxun_bot_plugins_tree"].respond(404)
|
||||
|
||||
if not is_commit:
|
||||
mocked_api["zhenxun_bot_plugins_commit"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_commit_proxy"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_index_commit"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_index_commit_proxy"].respond(404)
|
||||
|
||||
plugin_id = 1
|
||||
plugin_id = "search_image"
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
bot = create_bot(ctx)
|
||||
@@ -65,7 +49,7 @@ async def test_add_plugin_basic(
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(message=f"正在添加插件 Id: {plugin_id}"),
|
||||
message=Message(message=f"正在添加插件 Module: {plugin_id}"),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
@@ -75,25 +59,13 @@ async def test_add_plugin_basic(
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
if is_commit:
|
||||
assert mocked_api["search_image_plugin_file_init_commit"].called
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
else:
|
||||
assert mocked_api["search_image_plugin_file_init"].called
|
||||
assert mocked_api["basic_plugins_no_commit"].called
|
||||
assert mocked_api["extra_plugins_no_commit"].called
|
||||
assert (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("package_api", ["jsd", "gh"])
|
||||
@pytest.mark.parametrize("is_commit", [True, False])
|
||||
@pytest.mark.xfail
|
||||
async def test_add_plugin_basic_commit_version(
|
||||
package_api: str,
|
||||
is_commit: bool,
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -102,23 +74,12 @@ async def test_add_plugin_basic_commit_version(
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
|
||||
if package_api != "jsd":
|
||||
mocked_api["zhenxun_bot_plugins_metadata_commit"].respond(404)
|
||||
if package_api != "gh":
|
||||
mocked_api["zhenxun_bot_plugins_tree_commit"].respond(404)
|
||||
|
||||
if not is_commit:
|
||||
mocked_api["zhenxun_bot_plugins_commit"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_commit_proxy"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_index_commit"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_index_commit_proxy"].respond(404)
|
||||
plugin_id = 3
|
||||
plugin_id = "bilibili_sub"
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
bot = create_bot(ctx)
|
||||
@@ -135,7 +96,7 @@ async def test_add_plugin_basic_commit_version(
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(message=f"正在添加插件 Id: {plugin_id}"),
|
||||
message=Message(message=f"正在添加插件 Module: {plugin_id}"),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
@@ -145,28 +106,13 @@ async def test_add_plugin_basic_commit_version(
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
if package_api == "jsd":
|
||||
assert mocked_api["zhenxun_bot_plugins_metadata_commit"].called
|
||||
if package_api == "gh":
|
||||
assert mocked_api["zhenxun_bot_plugins_tree_commit"].called
|
||||
if is_commit:
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
else:
|
||||
assert mocked_api["basic_plugins_no_commit"].called
|
||||
assert mocked_api["extra_plugins_no_commit"].called
|
||||
assert mocked_api["bilibili_sub_plugin_file_init"].called
|
||||
assert (mock_base_path / "plugins" / "bilibili_sub" / "__init__.py").is_file()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("package_api", ["jsd", "gh"])
|
||||
@pytest.mark.parametrize("is_commit", [True, False])
|
||||
@pytest.mark.xfail
|
||||
async def test_add_plugin_basic_is_not_dir(
|
||||
package_api: str,
|
||||
is_commit: bool,
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -175,24 +121,12 @@ async def test_add_plugin_basic_is_not_dir(
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
|
||||
if package_api != "jsd":
|
||||
mocked_api["zhenxun_bot_plugins_metadata"].respond(404)
|
||||
if package_api != "gh":
|
||||
mocked_api["zhenxun_bot_plugins_tree"].respond(404)
|
||||
|
||||
if not is_commit:
|
||||
mocked_api["zhenxun_bot_plugins_commit"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_commit_proxy"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_index_commit"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_index_commit_proxy"].respond(404)
|
||||
|
||||
plugin_id = 0
|
||||
plugin_id = "jitang"
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
bot = create_bot(ctx)
|
||||
@@ -209,7 +143,7 @@ async def test_add_plugin_basic_is_not_dir(
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(message=f"正在添加插件 Id: {plugin_id}"),
|
||||
message=Message(message=f"正在添加插件 Module: {plugin_id}"),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
@@ -219,25 +153,13 @@ async def test_add_plugin_basic_is_not_dir(
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
if is_commit:
|
||||
assert mocked_api["jitang_plugin_file_commit"].called
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
else:
|
||||
assert mocked_api["jitang_plugin_file"].called
|
||||
assert mocked_api["basic_plugins_no_commit"].called
|
||||
assert mocked_api["extra_plugins_no_commit"].called
|
||||
assert (mock_base_path / "plugins" / "alapi" / "jitang.py").is_file()
|
||||
assert (mock_base_path / "plugins" / "jitang.py").is_file()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("package_api", ["jsd", "gh"])
|
||||
@pytest.mark.parametrize("is_commit", [True, False])
|
||||
@pytest.mark.xfail
|
||||
async def test_add_plugin_extra(
|
||||
package_api: str,
|
||||
is_commit: bool,
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -246,26 +168,12 @@ async def test_add_plugin_extra(
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
|
||||
if package_api != "jsd":
|
||||
mocked_api["zhenxun_github_sub_metadata"].respond(404)
|
||||
if package_api != "gh":
|
||||
mocked_api["zhenxun_github_sub_tree"].respond(404)
|
||||
|
||||
if not is_commit:
|
||||
mocked_api["zhenxun_github_sub_commit"].respond(404)
|
||||
mocked_api["zhenxun_github_sub_commit_proxy"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_commit"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_commit_proxy"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_index_commit"].respond(404)
|
||||
mocked_api["zhenxun_bot_plugins_index_commit_proxy"].respond(404)
|
||||
|
||||
plugin_id = 4
|
||||
plugin_id = "github_sub"
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
bot = create_bot(ctx)
|
||||
@@ -282,7 +190,7 @@ async def test_add_plugin_extra(
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(message=f"正在添加插件 Id: {plugin_id}"),
|
||||
message=Message(message=f"正在添加插件 Module: {plugin_id}"),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
@@ -292,30 +200,19 @@ async def test_add_plugin_extra(
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
if is_commit:
|
||||
assert mocked_api["github_sub_plugin_file_init_commit"].called
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
else:
|
||||
assert mocked_api["github_sub_plugin_file_init"].called
|
||||
assert mocked_api["basic_plugins_no_commit"].called
|
||||
assert mocked_api["extra_plugins_no_commit"].called
|
||||
assert (mock_base_path / "plugins" / "github_sub" / "__init__.py").is_file()
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_plugin_not_exist_add(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
测试插件不存在,添加插件
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
plugin_id = -1
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
@@ -339,27 +236,25 @@ async def test_plugin_not_exist_add(
|
||||
)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(message="插件ID不存在..."),
|
||||
message=Message(message="添加插件 Id: -1 失败 e: 插件ID不存在..."),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_add_plugin_exist(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
测试插件已经存在,添加插件
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins",
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.1")],
|
||||
)
|
||||
plugin_id = 1
|
||||
@@ -385,7 +280,9 @@ async def test_add_plugin_exist(
|
||||
)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(message="插件 识图 已安装,无需重复安装"),
|
||||
message=Message(
|
||||
message="添加插件 Id: 1 失败 e: 插件 识图 已安装,无需重复安装"
|
||||
),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from nonebot.adapters.onebot.v11 import Bot, Message
|
||||
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
|
||||
from nonebug import App
|
||||
from pytest_mock import MockerFixture
|
||||
from respx import MockRouter
|
||||
|
||||
from tests.builtin_plugins.plugin_store.utils import init_mocked_api
|
||||
from tests.config import BotId, GroupId, MessageId, UserId
|
||||
from tests.utils import _v11_group_message_event
|
||||
|
||||
|
||||
async def test_plugin_store(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
测试插件商店
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
from zhenxun.builtin_plugins.plugin_store.data_source import row_style
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
|
||||
mock_table_page = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ImageTemplate.table_page"
|
||||
)
|
||||
mock_table_page_return = mocker.AsyncMock()
|
||||
mock_table_page.return_value = mock_table_page_return
|
||||
|
||||
mock_build_message = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.MessageUtils.build_message"
|
||||
)
|
||||
mock_build_message_return = mocker.AsyncMock()
|
||||
mock_build_message.return_value = mock_build_message_return
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
bot = create_bot(ctx)
|
||||
bot: Bot = cast(Bot, bot)
|
||||
raw_message = "插件商店"
|
||||
event: GroupMessageEvent = _v11_group_message_event(
|
||||
message=raw_message,
|
||||
self_id=BotId.QQ_BOT,
|
||||
user_id=UserId.SUPERUSER,
|
||||
group_id=GroupId.GROUP_ID_LEVEL_5,
|
||||
message_id=MessageId.MESSAGE_ID_3,
|
||||
to_me=True,
|
||||
)
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
mock_table_page.assert_awaited_once_with(
|
||||
"插件列表",
|
||||
"通过添加/移除插件 ID 来管理插件",
|
||||
["-", "ID", "名称", "简介", "作者", "版本", "类型"],
|
||||
[
|
||||
["", 0, "鸡汤", "喏,亲手为你煮的鸡汤", "HibiKier", "0.1", "普通插件"],
|
||||
["", 1, "识图", "以图搜图,看破本源", "HibiKier", "0.1", "普通插件"],
|
||||
["", 2, "网易云热评", "生了个人,我很抱歉", "HibiKier", "0.1", "普通插件"],
|
||||
[
|
||||
"",
|
||||
3,
|
||||
"B站订阅",
|
||||
"非常便利的B站订阅通知",
|
||||
"HibiKier",
|
||||
"0.3-b101fbc",
|
||||
"普通插件",
|
||||
],
|
||||
[
|
||||
"",
|
||||
4,
|
||||
"github订阅",
|
||||
"订阅github用户或仓库",
|
||||
"xuanerwa",
|
||||
"0.7",
|
||||
"普通插件",
|
||||
],
|
||||
[
|
||||
"",
|
||||
5,
|
||||
"Minecraft查服",
|
||||
"Minecraft服务器状态查询,支持IPv6",
|
||||
"molanp",
|
||||
"1.13",
|
||||
"普通插件",
|
||||
],
|
||||
],
|
||||
text_style=row_style,
|
||||
)
|
||||
mock_build_message.assert_called_once_with(mock_table_page_return)
|
||||
mock_build_message_return.send.assert_awaited_once()
|
||||
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
|
||||
|
||||
async def test_plugin_store_fail(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
测试插件商店
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins/b101fbc/plugins.json",
|
||||
name="basic_plugins",
|
||||
).respond(404)
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
bot = create_bot(ctx)
|
||||
bot: Bot = cast(Bot, bot)
|
||||
raw_message = "插件商店"
|
||||
event: GroupMessageEvent = _v11_group_message_event(
|
||||
message=raw_message,
|
||||
self_id=BotId.QQ_BOT,
|
||||
user_id=UserId.SUPERUSER,
|
||||
group_id=GroupId.GROUP_ID_LEVEL_5,
|
||||
message_id=MessageId.MESSAGE_ID_3,
|
||||
to_me=True,
|
||||
)
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message("获取插件列表失败..."),
|
||||
result=None,
|
||||
exception=None,
|
||||
bot=bot,
|
||||
)
|
||||
|
||||
assert mocked_api["basic_plugins"].called
|
||||
@@ -8,18 +8,17 @@ from nonebot.adapters.onebot.v11 import Bot
|
||||
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
|
||||
from nonebot.adapters.onebot.v11.message import Message
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from respx import MockRouter
|
||||
|
||||
from tests.builtin_plugins.plugin_store.utils import get_content_bytes, init_mocked_api
|
||||
from tests.config import BotId, GroupId, MessageId, UserId
|
||||
from tests.utils import _v11_group_message_event
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_remove_plugin(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -28,7 +27,6 @@ async def test_remove_plugin(
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
@@ -38,7 +36,7 @@ async def test_remove_plugin(
|
||||
plugin_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(plugin_path / "__init__.py", "wb") as f:
|
||||
f.write(get_content_bytes("search_image.py"))
|
||||
f.write(b"A_nmi")
|
||||
|
||||
plugin_id = 1
|
||||
|
||||
@@ -61,24 +59,19 @@ async def test_remove_plugin(
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
assert not (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file()
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_plugin_not_exist_remove(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
测试插件不存在,移除插件
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
plugin_id = -1
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
@@ -96,16 +89,16 @@ async def test_plugin_not_exist_remove(
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(message="插件ID不存在..."),
|
||||
message=Message(message="移除插件 Id: -1 失败 e: 插件ID不存在..."),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_remove_plugin_not_install(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -114,7 +107,6 @@ async def test_remove_plugin_not_install(
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
_ = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
|
||||
@@ -1,33 +1,27 @@
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from nonebot.adapters.onebot.v11 import Bot
|
||||
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
|
||||
from nonebot.adapters.onebot.v11.message import Message
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from respx import MockRouter
|
||||
|
||||
from tests.builtin_plugins.plugin_store.utils import init_mocked_api
|
||||
from tests.config import BotId, GroupId, MessageId, UserId
|
||||
from tests.utils import _v11_group_message_event
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_search_plugin_name(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
测试搜索插件
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
from zhenxun.builtin_plugins.plugin_store.data_source import row_style
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
|
||||
mock_table_page = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ImageTemplate.table_page"
|
||||
@@ -56,44 +50,20 @@ async def test_search_plugin_name(
|
||||
to_me=True,
|
||||
)
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
mock_table_page.assert_awaited_once_with(
|
||||
"插件列表",
|
||||
"通过添加/移除插件 ID 来管理插件",
|
||||
["-", "ID", "名称", "简介", "作者", "版本", "类型"],
|
||||
[
|
||||
[
|
||||
"",
|
||||
4,
|
||||
"github订阅",
|
||||
"订阅github用户或仓库",
|
||||
"xuanerwa",
|
||||
"0.7",
|
||||
"普通插件",
|
||||
]
|
||||
],
|
||||
text_style=row_style,
|
||||
)
|
||||
mock_build_message.assert_called_once_with(mock_table_page_return)
|
||||
mock_build_message_return.send.assert_awaited_once()
|
||||
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_search_plugin_author(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
测试搜索插件,作者
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
from zhenxun.builtin_plugins.plugin_store.data_source import row_style
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
|
||||
mock_table_page = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ImageTemplate.table_page"
|
||||
@@ -122,43 +92,20 @@ async def test_search_plugin_author(
|
||||
to_me=True,
|
||||
)
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
mock_table_page.assert_awaited_once_with(
|
||||
"插件列表",
|
||||
"通过添加/移除插件 ID 来管理插件",
|
||||
["-", "ID", "名称", "简介", "作者", "版本", "类型"],
|
||||
[
|
||||
[
|
||||
"",
|
||||
4,
|
||||
"github订阅",
|
||||
"订阅github用户或仓库",
|
||||
"xuanerwa",
|
||||
"0.7",
|
||||
"普通插件",
|
||||
]
|
||||
],
|
||||
text_style=row_style,
|
||||
)
|
||||
mock_build_message.assert_called_once_with(mock_table_page_return)
|
||||
mock_build_message_return.send.assert_awaited_once()
|
||||
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_plugin_not_exist_search(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
测试插件不存在,搜索插件
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
plugin_name = "not_exist_plugin_name"
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
|
||||
@@ -6,18 +6,17 @@ from nonebot.adapters.onebot.v11 import Bot
|
||||
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
|
||||
from nonebot.adapters.onebot.v11.message import Message
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from respx import MockRouter
|
||||
|
||||
from tests.builtin_plugins.plugin_store.utils import init_mocked_api
|
||||
from tests.config import BotId, GroupId, MessageId, UserId
|
||||
from tests.utils import _v11_group_message_event
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_update_all_plugin_basic_need_update(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -26,13 +25,12 @@ async def test_update_all_plugin_basic_need_update(
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins",
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.0")],
|
||||
)
|
||||
|
||||
@@ -63,16 +61,13 @@ async def test_update_all_plugin_basic_need_update(
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
assert mocked_api["search_image_plugin_file_init_commit"].called
|
||||
assert (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file()
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_update_all_plugin_basic_is_new(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -81,14 +76,13 @@ async def test_update_all_plugin_basic_is_new(
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.1")],
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.2")],
|
||||
)
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
@@ -116,5 +110,3 @@ async def test_update_all_plugin_basic_is_new(
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
|
||||
@@ -6,18 +6,17 @@ from nonebot.adapters.onebot.v11 import Bot
|
||||
from nonebot.adapters.onebot.v11.event import GroupMessageEvent
|
||||
from nonebot.adapters.onebot.v11.message import Message
|
||||
from nonebug import App
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from respx import MockRouter
|
||||
|
||||
from tests.builtin_plugins.plugin_store.utils import init_mocked_api
|
||||
from tests.config import BotId, GroupId, MessageId, UserId
|
||||
from tests.utils import _v11_group_message_event
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_update_plugin_basic_need_update(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -26,13 +25,12 @@ async def test_update_plugin_basic_need_update(
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins",
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.0")],
|
||||
)
|
||||
|
||||
@@ -63,16 +61,13 @@ async def test_update_plugin_basic_need_update(
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
assert mocked_api["search_image_plugin_file_init_commit"].called
|
||||
assert (mock_base_path / "plugins" / "search_image" / "__init__.py").is_file()
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_update_plugin_basic_is_new(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -81,14 +76,13 @@ async def test_update_plugin_basic_is_new(
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.1")],
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.2")],
|
||||
)
|
||||
|
||||
plugin_id = 1
|
||||
@@ -118,23 +112,18 @@ async def test_update_plugin_basic_is_new(
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
assert mocked_api["basic_plugins"].called
|
||||
assert mocked_api["extra_plugins"].called
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_plugin_not_exist_update(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
测试插件不存在,更新插件
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
plugin_id = -1
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
@@ -158,25 +147,22 @@ async def test_plugin_not_exist_update(
|
||||
)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(message="插件ID不存在..."),
|
||||
message=Message(message="更新插件 Id: -1 失败 e: 插件ID不存在..."),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
async def test_update_plugin_not_install(
|
||||
app: App,
|
||||
mocker: MockerFixture,
|
||||
mocked_api: MockRouter,
|
||||
create_bot: Callable,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
测试插件不存在,更新插件
|
||||
"""
|
||||
from zhenxun.builtin_plugins.plugin_store import _matcher
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
plugin_id = 1
|
||||
|
||||
async with app.test_matcher(_matcher) as ctx:
|
||||
@@ -200,7 +186,9 @@ async def test_update_plugin_not_install(
|
||||
)
|
||||
ctx.should_call_send(
|
||||
event=event,
|
||||
message=Message(message="插件 识图 未安装,无法更新"),
|
||||
message=Message(
|
||||
message="更新插件 Id: 1 失败 e: 插件 识图 未安装,无法更新"
|
||||
),
|
||||
result=None,
|
||||
bot=bot,
|
||||
)
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
# ruff: noqa: ASYNC230
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from respx import MockRouter
|
||||
|
||||
from tests.utils import get_content_bytes as _get_content_bytes
|
||||
from tests.utils import get_response_json as _get_response_json
|
||||
|
||||
|
||||
def get_response_json(file: str) -> dict:
|
||||
return _get_response_json(Path() / "plugin_store", file=file)
|
||||
|
||||
|
||||
def get_content_bytes(file: str) -> bytes:
|
||||
return _get_content_bytes(Path() / "plugin_store", file)
|
||||
|
||||
|
||||
def init_mocked_api(mocked_api: MockRouter) -> None:
|
||||
# metadata
|
||||
mocked_api.get(
|
||||
"https://data.jsdelivr.com/v1/packages/gh/zhenxun-org/zhenxun_bot_plugins@main",
|
||||
name="zhenxun_bot_plugins_metadata",
|
||||
).respond(json=get_response_json("zhenxun_bot_plugins_metadata.json"))
|
||||
mocked_api.get(
|
||||
"https://data.jsdelivr.com/v1/packages/gh/xuanerwa/zhenxun_github_sub@main",
|
||||
name="zhenxun_github_sub_metadata",
|
||||
).respond(json=get_response_json("zhenxun_github_sub_metadata.json"))
|
||||
mocked_api.get(
|
||||
"https://data.jsdelivr.com/v1/packages/gh/zhenxun-org/zhenxun_bot_plugins@b101fbc",
|
||||
name="zhenxun_bot_plugins_metadata_commit",
|
||||
).respond(json=get_response_json("zhenxun_bot_plugins_metadata.json"))
|
||||
mocked_api.get(
|
||||
"https://data.jsdelivr.com/v1/packages/gh/xuanerwa/zhenxun_github_sub@f524632f78d27f9893beebdf709e0e7885cd08f1",
|
||||
name="zhenxun_github_sub_metadata_commit",
|
||||
).respond(json=get_response_json("zhenxun_github_sub_metadata.json"))
|
||||
|
||||
# tree
|
||||
mocked_api.get(
|
||||
"https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins/git/trees/main?recursive=1",
|
||||
name="zhenxun_bot_plugins_tree",
|
||||
).respond(json=get_response_json("zhenxun_bot_plugins_tree.json"))
|
||||
mocked_api.get(
|
||||
"https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/trees/main?recursive=1",
|
||||
name="zhenxun_github_sub_tree",
|
||||
).respond(json=get_response_json("zhenxun_github_sub_tree.json"))
|
||||
mocked_api.get(
|
||||
"https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins/git/trees/b101fbc?recursive=1",
|
||||
name="zhenxun_bot_plugins_tree_commit",
|
||||
).respond(json=get_response_json("zhenxun_bot_plugins_tree.json"))
|
||||
mocked_api.get(
|
||||
"https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/trees/f524632f78d27f9893beebdf709e0e7885cd08f1?recursive=1",
|
||||
name="zhenxun_github_sub_tree_commit",
|
||||
).respond(json=get_response_json("zhenxun_github_sub_tree.json"))
|
||||
|
||||
mocked_api.head(
|
||||
"https://raw.githubusercontent.com/",
|
||||
name="head_raw",
|
||||
).respond(200, text="")
|
||||
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins/b101fbc/plugins.json",
|
||||
name="basic_plugins",
|
||||
).respond(json=get_response_json("basic_plugins.json"))
|
||||
mocked_api.get(
|
||||
"https://cdn.jsdelivr.net/gh/zhenxun-org/zhenxun_bot_plugins@b101fbc/plugins.json",
|
||||
name="basic_plugins_jsdelivr",
|
||||
).respond(200, json=get_response_json("basic_plugins.json"))
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins/main/plugins.json",
|
||||
name="basic_plugins_no_commit",
|
||||
).respond(json=get_response_json("basic_plugins.json"))
|
||||
mocked_api.get(
|
||||
"https://cdn.jsdelivr.net/gh/zhenxun-org/zhenxun_bot_plugins@main/plugins.json",
|
||||
name="basic_plugins_jsdelivr_no_commit",
|
||||
).respond(200, json=get_response_json("basic_plugins.json"))
|
||||
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins_index/2ed61284873c526802752b12a3fd3b5e1a59d948/plugins.json",
|
||||
name="extra_plugins",
|
||||
).respond(200, json=get_response_json("extra_plugins.json"))
|
||||
mocked_api.get(
|
||||
"https://cdn.jsdelivr.net/gh/zhenxun-org/zhenxun_bot_plugins_index@2ed61284873c526802752b12a3fd3b5e1a59d948/plugins.json",
|
||||
name="extra_plugins_jsdelivr",
|
||||
).respond(200, json=get_response_json("extra_plugins.json"))
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins_index/index/plugins.json",
|
||||
name="extra_plugins_no_commit",
|
||||
).respond(200, json=get_response_json("extra_plugins.json"))
|
||||
mocked_api.get(
|
||||
"https://cdn.jsdelivr.net/gh/zhenxun-org/zhenxun_bot_plugins_index@index/plugins.json",
|
||||
name="extra_plugins_jsdelivr_no_commit",
|
||||
).respond(200, json=get_response_json("extra_plugins.json"))
|
||||
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins/main/plugins/search_image/__init__.py",
|
||||
name="search_image_plugin_file_init",
|
||||
).respond(content=get_content_bytes("search_image.py"))
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins/b101fbc/plugins/search_image/__init__.py",
|
||||
name="search_image_plugin_file_init_commit",
|
||||
).respond(content=get_content_bytes("search_image.py"))
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins/main/plugins/alapi/jitang.py",
|
||||
name="jitang_plugin_file",
|
||||
).respond(content=get_content_bytes("jitang.py"))
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins/b101fbc/plugins/alapi/jitang.py",
|
||||
name="jitang_plugin_file_commit",
|
||||
).respond(content=get_content_bytes("jitang.py"))
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/xuanerwa/zhenxun_github_sub/main/github_sub/__init__.py",
|
||||
name="github_sub_plugin_file_init",
|
||||
).respond(content=get_content_bytes("github_sub.py"))
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/xuanerwa/zhenxun_github_sub/f524632f78d27f9893beebdf709e0e7885cd08f1/github_sub/__init__.py",
|
||||
name="github_sub_plugin_file_init_commit",
|
||||
).respond(content=get_content_bytes("github_sub.py"))
|
||||
mocked_api.get(
|
||||
"https://raw.githubusercontent.com/zhenxun-org/zhenxun_bot_plugins/b101fbc/plugins/bilibili_sub/__init__.py",
|
||||
name="bilibili_sub_plugin_file_init",
|
||||
).respond(content=get_content_bytes("bilibili_sub.py"))
|
||||
|
||||
mocked_api.get(
|
||||
"https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins/commits/main",
|
||||
name="zhenxun_bot_plugins_commit",
|
||||
).respond(json=get_response_json("zhenxun_bot_plugins_commit.json"))
|
||||
mocked_api.get(
|
||||
"https://git-api.zhenxun.org/repos/zhenxun-org/zhenxun_bot_plugins/commits/main",
|
||||
name="zhenxun_bot_plugins_commit_proxy",
|
||||
).respond(json=get_response_json("zhenxun_bot_plugins_commit.json"))
|
||||
mocked_api.get(
|
||||
"https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins_index/commits/index",
|
||||
name="zhenxun_bot_plugins_index_commit",
|
||||
).respond(json=get_response_json("zhenxun_bot_plugins_index_commit.json"))
|
||||
mocked_api.get(
|
||||
"https://git-api.zhenxun.org/repos/zhenxun-org/zhenxun_bot_plugins_index/commits/index",
|
||||
name="zhenxun_bot_plugins_index_commit_proxy",
|
||||
).respond(json=get_response_json("zhenxun_bot_plugins_index_commit.json"))
|
||||
mocked_api.get(
|
||||
"https://api.github.com/repos/xuanerwa/zhenxun_github_sub/commits/main",
|
||||
name="zhenxun_github_sub_commit",
|
||||
).respond(json=get_response_json("zhenxun_github_sub_commit.json"))
|
||||
mocked_api.get(
|
||||
"https://git-api.zhenxun.org/repos/xuanerwa/zhenxun_github_sub/commits/main",
|
||||
name="zhenxun_github_sub_commit_proxy",
|
||||
).respond(json=get_response_json("zhenxun_github_sub_commit.json"))
|
||||
@@ -116,6 +116,7 @@ async def app(app: App, tmp_path: Path, mocker: MockerFixture):
|
||||
await init()
|
||||
# await driver._lifespan.startup()
|
||||
os.environ["AIOCACHE_DISABLE"] = "1"
|
||||
os.environ["PYTEST_CURRENT_TEST"] = "1"
|
||||
|
||||
yield app
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="B站订阅",
|
||||
description="非常便利的B站订阅通知",
|
||||
usage="""
|
||||
usage:
|
||||
B站直播,番剧,UP动态开播等提醒
|
||||
主播订阅相当于 直播间订阅 + UP订阅
|
||||
指令:
|
||||
添加订阅 ['主播'/'UP'/'番剧'] [id/链接/番名]
|
||||
删除订阅 ['主播'/'UP'/'id'] [id]
|
||||
查看订阅
|
||||
示例:
|
||||
添加订阅主播 2345344 <-(直播房间id)
|
||||
添加订阅UP 2355543 <-(个人主页id)
|
||||
添加订阅番剧 史莱姆 <-(支持模糊搜索)
|
||||
添加订阅番剧 125344 <-(番剧id)
|
||||
删除订阅id 2324344 <-(任意id,通过查看订阅获取)
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.3-b101fbc",
|
||||
superuser_help="""
|
||||
登录b站获取cookie防止风控:
|
||||
bil_check/检测b站
|
||||
bil_login/登录b站
|
||||
bil_logout/退出b站 uid
|
||||
示例:
|
||||
登录b站
|
||||
检测b站
|
||||
bil_logout 12345<-(退出登录的b站uid,通过检测b站获取)
|
||||
""",
|
||||
).to_dict(),
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="github订阅",
|
||||
description="订阅github用户或仓库",
|
||||
usage="""
|
||||
usage:
|
||||
github新Comment,PR,Issue等提醒
|
||||
指令:
|
||||
添加github ['用户'/'仓库'] [用户名/{owner/repo}]
|
||||
删除github [用户名/{owner/repo}]
|
||||
查看github
|
||||
示例:添加github订阅 用户 HibiKier
|
||||
示例:添加gb订阅 仓库 HibiKier/zhenxun_bot
|
||||
示例:添加github 用户 HibiKier
|
||||
示例:删除gb订阅 HibiKier
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="xuanerwa",
|
||||
version="0.7",
|
||||
).to_dict(),
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="鸡汤",
|
||||
description="喏,亲手为你煮的鸡汤",
|
||||
usage="""
|
||||
不喝点什么感觉有点不舒服
|
||||
指令:
|
||||
鸡汤
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
).to_dict(),
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="识图",
|
||||
description="以图搜图,看破本源",
|
||||
usage="""
|
||||
识别图片 [二次元图片]
|
||||
指令:
|
||||
识图 [图片]
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="一些工具",
|
||||
).to_dict(),
|
||||
)
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"鸡汤": {
|
||||
"module": "jitang",
|
||||
"module_path": "plugins.alapi.jitang",
|
||||
"description": "喏,亲手为你煮的鸡汤",
|
||||
"usage": "不喝点什么感觉有点不舒服\n 指令:\n 鸡汤",
|
||||
"author": "HibiKier",
|
||||
"version": "0.1",
|
||||
"plugin_type": "NORMAL",
|
||||
"is_dir": false
|
||||
},
|
||||
"识图": {
|
||||
"module": "search_image",
|
||||
"module_path": "plugins.search_image",
|
||||
"description": "以图搜图,看破本源",
|
||||
"usage": "识别图片 [二次元图片]\n 指令:\n 识图 [图片]",
|
||||
"author": "HibiKier",
|
||||
"version": "0.1",
|
||||
"plugin_type": "NORMAL",
|
||||
"is_dir": true
|
||||
},
|
||||
"网易云热评": {
|
||||
"module": "comments_163",
|
||||
"module_path": "plugins.alapi.comments_163",
|
||||
"description": "生了个人,我很抱歉",
|
||||
"usage": "到点了,还是防不了下塔\n 指令:\n 网易云热评/到点了/12点了",
|
||||
"author": "HibiKier",
|
||||
"version": "0.1",
|
||||
"plugin_type": "NORMAL",
|
||||
"is_dir": false
|
||||
},
|
||||
"B站订阅": {
|
||||
"module": "bilibili_sub",
|
||||
"module_path": "plugins.bilibili_sub",
|
||||
"description": "非常便利的B站订阅通知",
|
||||
"usage": "B站直播,番剧,UP动态开播等提醒",
|
||||
"author": "HibiKier",
|
||||
"version": "0.3-b101fbc",
|
||||
"plugin_type": "NORMAL",
|
||||
"is_dir": true
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"github订阅": {
|
||||
"module": "github_sub",
|
||||
"module_path": "github_sub",
|
||||
"description": "订阅github用户或仓库",
|
||||
"usage": "usage:\n github新Comment,PR,Issue等提醒\n 指令:\n 添加github ['用户'/'仓库'] [用户名/{owner/repo}]\n 删除github [用户名/{owner/repo}]\n 查看github\n 示例:添加github订阅 用户 HibiKier\n 示例:添加gb订阅 仓库 HibiKier/zhenxun_bot\n 示例:添加github 用户 HibiKier\n 示例:删除gb订阅 HibiKier",
|
||||
"author": "xuanerwa",
|
||||
"version": "0.7",
|
||||
"plugin_type": "NORMAL",
|
||||
"is_dir": true,
|
||||
"github_url": "https://github.com/xuanerwa/zhenxun_github_sub"
|
||||
},
|
||||
"Minecraft查服": {
|
||||
"module": "mc_check",
|
||||
"module_path": "mc_check",
|
||||
"description": "Minecraft服务器状态查询,支持IPv6",
|
||||
"usage": "Minecraft服务器状态查询,支持IPv6\n用法:\n\t查服 [ip]:[端口] / 查服 [ip]\n\t设置语言 zh-cn\n\t当前语言\n\t语言列表\neg:\t\nmcheck ip:port / mcheck ip\n\tset_lang en\n\tlang_now\n\tlang_list",
|
||||
"author": "molanp",
|
||||
"version": "1.13",
|
||||
"plugin_type": "NORMAL",
|
||||
"is_dir": true,
|
||||
"github_url": "https://github.com/molanp/zhenxun_check_Minecraft"
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
{
|
||||
"sha": "b101fbc",
|
||||
"node_id": "C_kwDOMndPGNoAKGIxMDFmYmNlODg4NjA4ZTJiYmU1YjVmZDI3OWUxNDY1MTY4ODEyYzc",
|
||||
"commit": {
|
||||
"author": {
|
||||
"name": "xuaner",
|
||||
"email": "xuaner_wa@qq.com",
|
||||
"date": "2024-09-20T12:08:27Z"
|
||||
},
|
||||
"committer": {
|
||||
"name": "xuaner",
|
||||
"email": "xuaner_wa@qq.com",
|
||||
"date": "2024-09-20T12:08:27Z"
|
||||
},
|
||||
"message": "🐛修复B站订阅bug",
|
||||
"tree": {
|
||||
"sha": "0566306219a434f7122798647498faef692c1879",
|
||||
"url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins/git/trees/0566306219a434f7122798647498faef692c1879"
|
||||
},
|
||||
"url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins/git/commits/b101fbce888608e2bbe5b5fd279e1465168812c7",
|
||||
"comment_count": 0,
|
||||
"verification": {
|
||||
"verified": false,
|
||||
"reason": "unsigned",
|
||||
"signature": null,
|
||||
"payload": null,
|
||||
"verified_at": null
|
||||
}
|
||||
},
|
||||
"url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins/commits/b101fbce888608e2bbe5b5fd279e1465168812c7",
|
||||
"html_url": "https://github.com/zhenxun-org/zhenxun_bot_plugins/commit/b101fbce888608e2bbe5b5fd279e1465168812c7",
|
||||
"comments_url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins/commits/b101fbce888608e2bbe5b5fd279e1465168812c7/comments",
|
||||
"author": {
|
||||
"login": "xuanerwa",
|
||||
"id": 58063798,
|
||||
"node_id": "MDQ6VXNlcjU4MDYzNzk4",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/58063798?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/xuanerwa",
|
||||
"html_url": "https://github.com/xuanerwa",
|
||||
"followers_url": "https://api.github.com/users/xuanerwa/followers",
|
||||
"following_url": "https://api.github.com/users/xuanerwa/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/xuanerwa/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/xuanerwa/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/xuanerwa/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/xuanerwa/orgs",
|
||||
"repos_url": "https://api.github.com/users/xuanerwa/repos",
|
||||
"events_url": "https://api.github.com/users/xuanerwa/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/xuanerwa/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"committer": {
|
||||
"login": "xuanerwa",
|
||||
"id": 58063798,
|
||||
"node_id": "MDQ6VXNlcjU4MDYzNzk4",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/58063798?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/xuanerwa",
|
||||
"html_url": "https://github.com/xuanerwa",
|
||||
"followers_url": "https://api.github.com/users/xuanerwa/followers",
|
||||
"following_url": "https://api.github.com/users/xuanerwa/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/xuanerwa/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/xuanerwa/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/xuanerwa/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/xuanerwa/orgs",
|
||||
"repos_url": "https://api.github.com/users/xuanerwa/repos",
|
||||
"events_url": "https://api.github.com/users/xuanerwa/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/xuanerwa/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"parents": [
|
||||
{
|
||||
"sha": "a545dfa0c4e149595f7ddd50dc34c55513738fb9",
|
||||
"url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins/commits/a545dfa0c4e149595f7ddd50dc34c55513738fb9",
|
||||
"html_url": "https://github.com/zhenxun-org/zhenxun_bot_plugins/commit/a545dfa0c4e149595f7ddd50dc34c55513738fb9"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"total": 4,
|
||||
"additions": 2,
|
||||
"deletions": 2
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"sha": "0fbc9695db04c56174e3bff933f670d8d2df2abc",
|
||||
"filename": "plugins/bilibili_sub/data_source.py",
|
||||
"status": "modified",
|
||||
"additions": 2,
|
||||
"deletions": 2,
|
||||
"changes": 4,
|
||||
"blob_url": "https://github.com/zhenxun-org/zhenxun_bot_plugins/blob/b101fbce888608e2bbe5b5fd279e1465168812c7/plugins%2Fbilibili_sub%2Fdata_source.py",
|
||||
"raw_url": "https://github.com/zhenxun-org/zhenxun_bot_plugins/raw/b101fbce888608e2bbe5b5fd279e1465168812c7/plugins%2Fbilibili_sub%2Fdata_source.py",
|
||||
"contents_url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins/contents/plugins%2Fbilibili_sub%2Fdata_source.py?ref=b101fbce888608e2bbe5b5fd279e1465168812c7",
|
||||
"patch": "@@ -271,14 +271,14 @@ async def _get_live_status(id_: int) -> list:\n sub = await BilibiliSub.get_or_none(sub_id=id_)\n msg_list = []\n if sub.live_status != live_status:\n+ await BilibiliSub.sub_handle(id_, live_status=live_status)\n image = None\n try:\n image_bytes = await fetch_image_bytes(cover)\n image = BuildImage(background = image_bytes)\n except Exception as e:\n logger.error(f\"图片构造失败,错误信息:{e}\")\n if sub.live_status in [0, 2] and live_status == 1 and image:\n- await BilibiliSub.sub_handle(id_, live_status=live_status)\n msg_list = [\n image,\n \"\\n\",\n@@ -322,7 +322,7 @@ async def _get_up_status(id_: int) -> list:\n video = video_info[\"list\"][\"vlist\"][0]\n latest_video_created = video[\"created\"]\n msg_list = []\n- if dynamic_img:\n+ if dynamic_img and _user.dynamic_upload_time < dynamic_upload_time:\n await BilibiliSub.sub_handle(id_, dynamic_upload_time=dynamic_upload_time)\n msg_list = [f\"{uname} 发布了动态!📢\\n\", dynamic_img, f\"\\n查看详情:{link}\"]\n if ("
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
{
|
||||
"sha": "2ed61284873c526802752b12a3fd3b5e1a59d948",
|
||||
"node_id": "C_kwDOGK5Du9oAKDJlZDYxMjg0ODczYzUyNjgwMjc1MmIxMmEzZmQzYjVlMWE1OWQ5NDg",
|
||||
"commit": {
|
||||
"author": {
|
||||
"name": "zhenxunflow[bot]",
|
||||
"email": "179375394+zhenxunflow[bot]@users.noreply.github.com",
|
||||
"date": "2025-01-26T09:04:55Z"
|
||||
},
|
||||
"committer": {
|
||||
"name": "GitHub",
|
||||
"email": "noreply@github.com",
|
||||
"date": "2025-01-26T09:04:55Z"
|
||||
},
|
||||
"message": ":beers: publish plugin AI全家桶 (#235) (#236)\n\nCo-authored-by: molanp <molanp@users.noreply.github.com>",
|
||||
"tree": {
|
||||
"sha": "64ea463e084b6ab0def0322c6ad53799054ec9b3",
|
||||
"url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins_index/git/trees/64ea463e084b6ab0def0322c6ad53799054ec9b3"
|
||||
},
|
||||
"url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins_index/git/commits/2ed61284873c526802752b12a3fd3b5e1a59d948",
|
||||
"comment_count": 0,
|
||||
"verification": {
|
||||
"verified": true,
|
||||
"reason": "valid",
|
||||
"signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnlfq3CRC1aQ7uu5UhlAAA+n0QADPVjQQIHFlNcTEgdq3LGQ1X\nm8+H5N07E5JD+83LdyU9/YOvqY/WURwFsQ0T4+23icUWEOD4LB5qZIdVJBYHseto\nbJNmYd1kZxpvsONoiK/2Uk6JoeVnEQIR+dTbB0wBlbL0lRt1WtTXHpLQbFXuXn3q\nJh4SdSj283UZ6D2sBADblPZ7DqaTmLlpgwrTPx0OH5wIhcuORkzOl6x0DabcVAYu\nu5zHSKM9c7g+jEmrqRuVy+ZlZMDPN4S3gDNzEhoTn4tn+KNzSIja4n7ZMRD+1a5X\nMIP3aXcVBqCyuYc6DU76IvjlaL/MjnlPwfOtx1zu+pNxZKNaSpojtqopp3blfk0E\n8s8lD9utDgUaUrdPWgpiMDjj+oNMye91CGomNDfv0fNGUlBGT6r48qaq1z8BwAAR\nzgDsF13kDuKTTkT/6T8CdgCpJtwvxMptUr2XFRtn4xwf/gJdqrbEc4fHTOSHqxzh\ncDfXuP+Sorla4oJ0duygTsulpr/zguX8RJWJml35VjERw54ARAVvhZn19G9qQVJo\n2QIp+xtyTjkM3yTeN4UDXFt4lDuxz3+l1MBduj+CHn+WTgxyJUpX2TA1GVfni9xT\npOMOtzuDQfDIxTNB6hFjSWATb1/E5ys1lfK09n+dRhmvC/Be+b5M4WlyX3cqy/za\ns0XxuZ+CHzLfHaPxFUem\n=VYpl\n-----END PGP SIGNATURE-----\n",
|
||||
"payload": "tree 64ea463e084b6ab0def0322c6ad53799054ec9b3\nparent 5df26081d40e3000a7beedb73954d4df397c93fa\nauthor zhenxunflow[bot] <179375394+zhenxunflow[bot]@users.noreply.github.com> 1737882295 +0800\ncommitter GitHub <noreply@github.com> 1737882295 +0800\n\n:beers: publish plugin AI全家桶 (#235) (#236)\n\nCo-authored-by: molanp <molanp@users.noreply.github.com>",
|
||||
"verified_at": "2025-01-26T09:04:58Z"
|
||||
}
|
||||
},
|
||||
"url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins_index/commits/2ed61284873c526802752b12a3fd3b5e1a59d948",
|
||||
"html_url": "https://github.com/zhenxun-org/zhenxun_bot_plugins_index/commit/2ed61284873c526802752b12a3fd3b5e1a59d948",
|
||||
"comments_url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins_index/commits/2ed61284873c526802752b12a3fd3b5e1a59d948/comments",
|
||||
"author": {
|
||||
"login": "zhenxunflow[bot]",
|
||||
"id": 179375394,
|
||||
"node_id": "BOT_kgDOCrENIg",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/in/978723?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/zhenxunflow%5Bbot%5D",
|
||||
"html_url": "https://github.com/apps/zhenxunflow",
|
||||
"followers_url": "https://api.github.com/users/zhenxunflow%5Bbot%5D/followers",
|
||||
"following_url": "https://api.github.com/users/zhenxunflow%5Bbot%5D/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/zhenxunflow%5Bbot%5D/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/zhenxunflow%5Bbot%5D/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/zhenxunflow%5Bbot%5D/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/zhenxunflow%5Bbot%5D/orgs",
|
||||
"repos_url": "https://api.github.com/users/zhenxunflow%5Bbot%5D/repos",
|
||||
"events_url": "https://api.github.com/users/zhenxunflow%5Bbot%5D/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/zhenxunflow%5Bbot%5D/received_events",
|
||||
"type": "Bot",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"committer": {
|
||||
"login": "web-flow",
|
||||
"id": 19864447,
|
||||
"node_id": "MDQ6VXNlcjE5ODY0NDQ3",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/web-flow",
|
||||
"html_url": "https://github.com/web-flow",
|
||||
"followers_url": "https://api.github.com/users/web-flow/followers",
|
||||
"following_url": "https://api.github.com/users/web-flow/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/web-flow/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/web-flow/orgs",
|
||||
"repos_url": "https://api.github.com/users/web-flow/repos",
|
||||
"events_url": "https://api.github.com/users/web-flow/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/web-flow/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"parents": [
|
||||
{
|
||||
"sha": "5df26081d40e3000a7beedb73954d4df397c93fa",
|
||||
"url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins_index/commits/5df26081d40e3000a7beedb73954d4df397c93fa",
|
||||
"html_url": "https://github.com/zhenxun-org/zhenxun_bot_plugins_index/commit/5df26081d40e3000a7beedb73954d4df397c93fa"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"total": 11,
|
||||
"additions": 11,
|
||||
"deletions": 0
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"sha": "3d98392c25d38f5d375b830aed6e2298e47e5601",
|
||||
"filename": "plugins.json",
|
||||
"status": "modified",
|
||||
"additions": 11,
|
||||
"deletions": 0,
|
||||
"changes": 11,
|
||||
"blob_url": "https://github.com/zhenxun-org/zhenxun_bot_plugins_index/blob/2ed61284873c526802752b12a3fd3b5e1a59d948/plugins.json",
|
||||
"raw_url": "https://github.com/zhenxun-org/zhenxun_bot_plugins_index/raw/2ed61284873c526802752b12a3fd3b5e1a59d948/plugins.json",
|
||||
"contents_url": "https://api.github.com/repos/zhenxun-org/zhenxun_bot_plugins_index/contents/plugins.json?ref=2ed61284873c526802752b12a3fd3b5e1a59d948",
|
||||
"patch": "@@ -53,5 +53,16 @@\n \"plugin_type\": \"NORMAL\",\n \"is_dir\": true,\n \"github_url\": \"https://github.com/PackageInstaller/zhenxun_plugin_draw_painting/tree/master\"\n+ },\n+ \"AI全家桶\": {\n+ \"module\": \"zhipu_toolkit\",\n+ \"module_path\": \"zhipu_toolkit\",\n+ \"description\": \"AI全家桶,一次安装,到处使用,省时省力省心\",\n+ \"usage\": \"AI全家桶,一次安装,到处使用,省时省力省心\\n usage:\\n 生成图片 <prompt>\\n 生成视频 <prompt>\\n 清理我的会话: 用于清理你与AI的聊天记录\\n 或者与机器人聊天,\\n 例如;\\n @Bot抱抱\\n 小真寻老婆\",\n+ \"author\": \"molanp\",\n+ \"version\": \"0.1\",\n+ \"plugin_type\": \"NORMAL\",\n+ \"is_dir\": true,\n+ \"github_url\": \"https://github.com/molanp/zhenxun_plugin_zhipu_toolkit\"\n }\n }"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
{
|
||||
"type": "gh",
|
||||
"name": "zhenxun-org/zhenxun_bot_plugins",
|
||||
"version": "main",
|
||||
"default": null,
|
||||
"files": [
|
||||
{
|
||||
"type": "directory",
|
||||
"name": "plugins",
|
||||
"files": [
|
||||
{
|
||||
"type": "directory",
|
||||
"name": "search_image",
|
||||
"files": [
|
||||
{
|
||||
"type": "file",
|
||||
"name": "__init__.py",
|
||||
"hash": "a4Yp9HPoBzMwvnQDT495u0yYqTQWofkOyHxEi1FdVb0=",
|
||||
"size": 3010
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "directory",
|
||||
"name": "alapi",
|
||||
"files": [
|
||||
{
|
||||
"type": "file",
|
||||
"name": "__init__.py",
|
||||
"hash": "ndDxtO0pAq3ZTb4RdqW7FTDgOGC/RjS1dnwdaQfT0uQ=",
|
||||
"size": 284
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "_data_source.py",
|
||||
"hash": "KOLqtj4TQWWQco5bA4tWFc7A0z1ruMyDk1RiKeqJHRA=",
|
||||
"size": 919
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "comments_163.py",
|
||||
"hash": "Q5pZsj1Pj+EJMdKYcPtLqejcXAWUQIoXVQG49PZPaSI=",
|
||||
"size": 1593
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "cover.py",
|
||||
"hash": "QSjtcy0oVrjaRiAWZKmUJlp0L4DQqEcdYNmExNo9mgc=",
|
||||
"size": 1438
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "jitang.py",
|
||||
"hash": "xh43Osxt0xogTH448gUMC+/DaSGmCFme8DWUqC25IbU=",
|
||||
"size": 1411
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "poetry.py",
|
||||
"hash": "Aj2unoNQboj3/0LhIrYU+dCa5jvMdpjMYXYUayhjuz4=",
|
||||
"size": 1530
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "directory",
|
||||
"name": "bilibili_sub",
|
||||
"files": [
|
||||
{
|
||||
"type": "file",
|
||||
"name": "__init__.py",
|
||||
"hash": "407DCgNFcZnuEK+d716j8EWrFQc4Nlxa35V3yemy3WQ=",
|
||||
"size": 14293
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"stats": "https://data.jsdelivr.com/v1/stats/packages/gh/zhenxun-org/zhenxun_bot_plugins@main"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,101 +0,0 @@
|
||||
{
|
||||
"sha": "f524632f78d27f9893beebdf709e0e7885cd08f1",
|
||||
"node_id": "C_kwDOJAjBPdoAKGY1MjQ2MzJmNzhkMjdmOTg5M2JlZWJkZjcwOWUwZTc4ODVjZDA4ZjE",
|
||||
"commit": {
|
||||
"author": {
|
||||
"name": "xuaner",
|
||||
"email": "xuaner_wa@qq.com",
|
||||
"date": "2024-11-18T18:17:15Z"
|
||||
},
|
||||
"committer": {
|
||||
"name": "xuaner",
|
||||
"email": "xuaner_wa@qq.com",
|
||||
"date": "2024-11-18T18:17:15Z"
|
||||
},
|
||||
"message": "fix bug",
|
||||
"tree": {
|
||||
"sha": "b6b1b4f06cc869b9f38d7b51bdca3a2c575255e4",
|
||||
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/trees/b6b1b4f06cc869b9f38d7b51bdca3a2c575255e4"
|
||||
},
|
||||
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/commits/f524632f78d27f9893beebdf709e0e7885cd08f1",
|
||||
"comment_count": 0,
|
||||
"verification": {
|
||||
"verified": false,
|
||||
"reason": "unsigned",
|
||||
"signature": null,
|
||||
"payload": null,
|
||||
"verified_at": null
|
||||
}
|
||||
},
|
||||
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/commits/f524632f78d27f9893beebdf709e0e7885cd08f1",
|
||||
"html_url": "https://github.com/xuanerwa/zhenxun_github_sub/commit/f524632f78d27f9893beebdf709e0e7885cd08f1",
|
||||
"comments_url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/commits/f524632f78d27f9893beebdf709e0e7885cd08f1/comments",
|
||||
"author": {
|
||||
"login": "xuanerwa",
|
||||
"id": 58063798,
|
||||
"node_id": "MDQ6VXNlcjU4MDYzNzk4",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/58063798?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/xuanerwa",
|
||||
"html_url": "https://github.com/xuanerwa",
|
||||
"followers_url": "https://api.github.com/users/xuanerwa/followers",
|
||||
"following_url": "https://api.github.com/users/xuanerwa/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/xuanerwa/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/xuanerwa/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/xuanerwa/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/xuanerwa/orgs",
|
||||
"repos_url": "https://api.github.com/users/xuanerwa/repos",
|
||||
"events_url": "https://api.github.com/users/xuanerwa/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/xuanerwa/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"committer": {
|
||||
"login": "xuanerwa",
|
||||
"id": 58063798,
|
||||
"node_id": "MDQ6VXNlcjU4MDYzNzk4",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/58063798?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/xuanerwa",
|
||||
"html_url": "https://github.com/xuanerwa",
|
||||
"followers_url": "https://api.github.com/users/xuanerwa/followers",
|
||||
"following_url": "https://api.github.com/users/xuanerwa/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/xuanerwa/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/xuanerwa/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/xuanerwa/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/xuanerwa/orgs",
|
||||
"repos_url": "https://api.github.com/users/xuanerwa/repos",
|
||||
"events_url": "https://api.github.com/users/xuanerwa/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/xuanerwa/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"parents": [
|
||||
{
|
||||
"sha": "91e5e2c792e79193830441d555769aa54acd2d15",
|
||||
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/commits/91e5e2c792e79193830441d555769aa54acd2d15",
|
||||
"html_url": "https://github.com/xuanerwa/zhenxun_github_sub/commit/91e5e2c792e79193830441d555769aa54acd2d15"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"total": 2,
|
||||
"additions": 1,
|
||||
"deletions": 1
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"sha": "764a5f7b81554c4c10d29486ea5d9105e505cec3",
|
||||
"filename": "github_sub/__init__.py",
|
||||
"status": "modified",
|
||||
"additions": 1,
|
||||
"deletions": 1,
|
||||
"changes": 2,
|
||||
"blob_url": "https://github.com/xuanerwa/zhenxun_github_sub/blob/f524632f78d27f9893beebdf709e0e7885cd08f1/github_sub%2F__init__.py",
|
||||
"raw_url": "https://github.com/xuanerwa/zhenxun_github_sub/raw/f524632f78d27f9893beebdf709e0e7885cd08f1/github_sub%2F__init__.py",
|
||||
"contents_url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/contents/github_sub%2F__init__.py?ref=f524632f78d27f9893beebdf709e0e7885cd08f1",
|
||||
"patch": "@@ -168,7 +168,7 @@ async def _(session: EventSession):\n # 推送\n @scheduler.scheduled_job(\n \"interval\",\n- seconds=base_config.get(\"CHECK_API_TIME\") if base_config.get(\"CHECK_TIME\") else 30,\n+ seconds=base_config.get(\"CHECK_API_TIME\") if base_config.get(\"CHECK_API_TIME\") else 30,\n )\n async def _():\n bots = nonebot.get_bots()"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"type": "gh",
|
||||
"name": "xuanerwa/zhenxun_github_sub",
|
||||
"version": "main",
|
||||
"default": null,
|
||||
"files": [
|
||||
{
|
||||
"type": "directory",
|
||||
"name": "github_sub",
|
||||
"files": [
|
||||
{
|
||||
"type": "file",
|
||||
"name": "__init__.py",
|
||||
"hash": "z1C5BBK0+atbDghbyRlF2xIDwk0HQdHM1yXQZkF7/t8=",
|
||||
"size": 7551
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"stats": "https://data.jsdelivr.com/v1/stats/packages/gh/xuanerwa/zhenxun_github_sub@main"
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"sha": "438298b9e88f9dafa7020e99d7c7b4c98f93aea6",
|
||||
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/trees/438298b9e88f9dafa7020e99d7c7b4c98f93aea6",
|
||||
"tree": [
|
||||
{
|
||||
"path": "LICENSE",
|
||||
"mode": "100644",
|
||||
"type": "blob",
|
||||
"sha": "f288702d2fa16d3cdf0035b15a9fcbc552cd88e7",
|
||||
"size": 35149,
|
||||
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/blobs/f288702d2fa16d3cdf0035b15a9fcbc552cd88e7"
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"mode": "100644",
|
||||
"type": "blob",
|
||||
"sha": "e974cfc9b973d4a041f03e693ea20563a933b7ca",
|
||||
"size": 955,
|
||||
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/blobs/e974cfc9b973d4a041f03e693ea20563a933b7ca"
|
||||
},
|
||||
{
|
||||
"path": "github_sub",
|
||||
"mode": "040000",
|
||||
"type": "tree",
|
||||
"sha": "0f7d76bcf472e2ab0610fa542b067633d6e3ae7e",
|
||||
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/trees/0f7d76bcf472e2ab0610fa542b067633d6e3ae7e"
|
||||
},
|
||||
{
|
||||
"path": "github_sub/__init__.py",
|
||||
"mode": "100644",
|
||||
"type": "blob",
|
||||
"sha": "7d17fd49fe82fa3897afcef61b2c694ed93a4ba3",
|
||||
"size": 7551,
|
||||
"url": "https://api.github.com/repos/xuanerwa/zhenxun_github_sub/git/blobs/7d17fd49fe82fa3897afcef61b2c694ed93a4ba3"
|
||||
}
|
||||
],
|
||||
"truncated": false
|
||||
}
|
||||
@@ -5,6 +5,10 @@ from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageSegme
|
||||
from nonebot.adapters.onebot.v11.event import Sender
|
||||
|
||||
|
||||
def get_reply_cq(uid: int | str) -> str:
|
||||
return f"[CQ:reply,id={uid}]"
|
||||
|
||||
|
||||
def get_response_json(base_path: Path, file: str) -> dict:
|
||||
try:
|
||||
return json.loads(
|
||||
|
||||
@@ -5,7 +5,7 @@ import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.drivers import Driver
|
||||
from tortoise import Tortoise
|
||||
from tortoise.exceptions import OperationalError
|
||||
from tortoise.exceptions import IntegrityError, OperationalError
|
||||
import ujson as json
|
||||
|
||||
from zhenxun.models.bot_connect_log import BotConnectLog
|
||||
@@ -17,7 +17,7 @@ from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.decorator.shop import shop_register
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
from zhenxun.utils.manager.resource_manager import ResourceManager
|
||||
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
driver: Driver = nonebot.get_driver()
|
||||
@@ -30,9 +30,12 @@ async def _(bot: Bot):
|
||||
bot_id=bot.self_id, platform=bot.adapter, connect_time=datetime.now(), type=1
|
||||
)
|
||||
if not await BotConsole.exists(bot_id=bot.self_id):
|
||||
await BotConsole.create(
|
||||
bot_id=bot.self_id, platform=PlatformUtils.get_platform(bot)
|
||||
)
|
||||
try:
|
||||
await BotConsole.create(
|
||||
bot_id=bot.self_id, platform=PlatformUtils.get_platform(bot)
|
||||
)
|
||||
except IntegrityError as e:
|
||||
logger.warning(f"记录bot: {bot.self_id} 数据已存在...", e=e)
|
||||
|
||||
|
||||
@driver.on_bot_disconnect
|
||||
@@ -50,22 +53,31 @@ async def _(bot: Bot):
|
||||
|
||||
|
||||
SIGN_SQL = """
|
||||
select distinct on("user_id") t1.user_id, t1.checkin_count, t1.add_probability,
|
||||
t1.specify_probability, t1.impression
|
||||
from public.sign_group_users t1
|
||||
join (
|
||||
select user_id, max(t2.impression) as max_impression
|
||||
from public.sign_group_users t2
|
||||
group by user_id
|
||||
) t on t.user_id = t1.user_id and t.max_impression = t1.impression
|
||||
SELECT user_id, checkin_count, add_probability, specify_probability, impression
|
||||
FROM (
|
||||
SELECT
|
||||
t1.user_id,
|
||||
t1.checkin_count,
|
||||
t1.add_probability,
|
||||
t1.specify_probability,
|
||||
t1.impression,
|
||||
ROW_NUMBER() OVER(PARTITION BY t1.user_id ORDER BY t1.impression DESC) AS rn
|
||||
FROM sign_group_users t1
|
||||
INNER JOIN (
|
||||
SELECT user_id, MAX(impression) AS max_impression
|
||||
FROM sign_group_users
|
||||
GROUP BY user_id
|
||||
) t2 ON t2.user_id = t1.user_id AND t2.max_impression = t1.impression
|
||||
) t
|
||||
WHERE rn = 1
|
||||
"""
|
||||
|
||||
BAG_SQL = """
|
||||
select t1.user_id, t1.gold, t1.property
|
||||
from public.bag_users t1
|
||||
from bag_users t1
|
||||
join (
|
||||
select user_id, max(t2.gold) as max_gold
|
||||
from public.bag_users t2
|
||||
from bag_users t2
|
||||
group by user_id
|
||||
) t on t.user_id = t1.user_id and t.max_gold = t1.gold
|
||||
"""
|
||||
@@ -73,7 +85,8 @@ from public.bag_users t1
|
||||
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
await ResourceManager.init_resources()
|
||||
if not ZhenxunRepoManager.check_resources_exists():
|
||||
await ZhenxunRepoManager.resources_update()
|
||||
"""签到与用户的数据迁移"""
|
||||
if goods_list := await GoodsInfo.filter(uuid__isnull=True).all():
|
||||
for goods in goods_list:
|
||||
|
||||
+21
-24
@@ -2,18 +2,14 @@ from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.help_service import create_plugin_help_image
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.exception import EmptyError
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.rules import admin_check, ensure_group
|
||||
|
||||
from .config import ADMIN_HELP_IMAGE
|
||||
from .html_help import build_html_help
|
||||
from .normal_help import build_help
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="群组管理员帮助",
|
||||
description="管理员帮助列表",
|
||||
@@ -25,17 +21,24 @@ __plugin_meta__ = PluginMetadata(
|
||||
version="0.1",
|
||||
plugin_type=PluginType.ADMIN,
|
||||
admin_level=1,
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="type",
|
||||
value="zhenxun",
|
||||
help="管理员帮助样式,normal, zhenxun",
|
||||
default_value="zhenxun",
|
||||
)
|
||||
introduction="""这是 群主/群管理 的帮助列表,里面记录了群组内开关功能的
|
||||
方法帮助以及群管特权方法,建议首次时在群组中发送 '管理员帮助' 查看""",
|
||||
precautions=[
|
||||
"只有群主/群管理 才能使用哦,群主拥有6级权限,管理员拥有5级权限!"
|
||||
],
|
||||
configs=[],
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
|
||||
async def build_html_help() -> bytes:
|
||||
"""构建管理员帮助图片"""
|
||||
return await create_plugin_help_image(
|
||||
plugin_types=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN],
|
||||
page_title="群管理员帮助手册",
|
||||
)
|
||||
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("管理员帮助"),
|
||||
rule=admin_check(1) & ensure_group,
|
||||
@@ -49,15 +52,9 @@ async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
):
|
||||
if not ADMIN_HELP_IMAGE.exists():
|
||||
try:
|
||||
if Config.get_config("admin_help", "type") == "zhenxun":
|
||||
await build_html_help()
|
||||
else:
|
||||
await build_help()
|
||||
except EmptyError:
|
||||
await MessageUtils.build_message("当前管理员帮助为空...").finish(
|
||||
reply_to=True
|
||||
)
|
||||
await MessageUtils.build_message(ADMIN_HELP_IMAGE).send()
|
||||
try:
|
||||
image_bytes = await build_html_help()
|
||||
await MessageUtils.build_message(image_bytes).send()
|
||||
except EmptyError:
|
||||
await MessageUtils.build_message("当前管理员帮助为空...").finish(reply_to=True)
|
||||
logger.info("查看管理员帮助", arparma.header_result, session=session)
|
||||
@@ -1,23 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
|
||||
ADMIN_HELP_IMAGE = IMAGE_PATH / "ADMIN_HELP.png"
|
||||
if ADMIN_HELP_IMAGE.exists():
|
||||
ADMIN_HELP_IMAGE.unlink()
|
||||
|
||||
|
||||
class PluginData(BaseModel):
|
||||
"""
|
||||
插件信息
|
||||
"""
|
||||
|
||||
plugin: PluginInfo
|
||||
"""插件信息"""
|
||||
metadata: PluginMetadata
|
||||
"""元数据"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
@@ -1,56 +0,0 @@
|
||||
from nonebot_plugin_htmlrender import template_to_pic
|
||||
|
||||
from zhenxun.builtin_plugins.admin.admin_help.config import ADMIN_HELP_IMAGE
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.path_config import TEMPLATE_PATH
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.utils._build_image import BuildImage
|
||||
|
||||
from .utils import get_plugins
|
||||
|
||||
|
||||
async def get_task() -> dict[str, str] | None:
|
||||
"""获取被动技能帮助"""
|
||||
if task_list := await TaskInfo.all():
|
||||
return {
|
||||
"name": "被动技能",
|
||||
"description": "控制群组中的被动技能状态",
|
||||
"usage": "通过 开启/关闭群被动 来控制群被动 <br>"
|
||||
+ " 示例:开启/关闭群被动早晚安 <br> ---------- <br> "
|
||||
+ "<br>".join([task.name for task in task_list]),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
async def build_html_help():
|
||||
"""构建帮助图片"""
|
||||
plugins = await get_plugins()
|
||||
plugin_list = [
|
||||
{
|
||||
"name": data.plugin.name,
|
||||
"description": data.metadata.description.replace("\n", "<br>"),
|
||||
"usage": data.metadata.usage.replace("\n", "<br>"),
|
||||
}
|
||||
for data in plugins
|
||||
]
|
||||
if task := await get_task():
|
||||
plugin_list.append(task)
|
||||
plugin_list.sort(key=lambda p: len(p["description"]) + len(p["usage"]))
|
||||
pic = await template_to_pic(
|
||||
template_path=str((TEMPLATE_PATH / "help").absolute()),
|
||||
template_name="main.html",
|
||||
templates={
|
||||
"data": {
|
||||
"plugin_list": plugin_list,
|
||||
"nickname": BotConfig.self_nickname,
|
||||
"help_name": "群管理员",
|
||||
}
|
||||
},
|
||||
pages={
|
||||
"viewport": {"width": 1024, "height": 1024},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
result = await BuildImage.open(pic).resize(0.5)
|
||||
await result.save(ADMIN_HELP_IMAGE)
|
||||
@@ -1,127 +0,0 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from PIL.ImageFont import FreeTypeFont
|
||||
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils._build_image import BuildImage
|
||||
from zhenxun.utils.image_utils import build_sort_image, group_image, text2image
|
||||
|
||||
from .config import ADMIN_HELP_IMAGE
|
||||
from .utils import get_plugins
|
||||
|
||||
|
||||
async def build_usage_des_image(
|
||||
metadata: PluginMetadata,
|
||||
) -> tuple[BuildImage | None, BuildImage | None]:
|
||||
"""构建用法和描述图片
|
||||
|
||||
参数:
|
||||
metadata: PluginMetadata
|
||||
|
||||
返回:
|
||||
tuple[BuildImage | None, BuildImage | None]: 用法和描述图片
|
||||
"""
|
||||
usage = None
|
||||
description = None
|
||||
if metadata.usage:
|
||||
usage = await text2image(
|
||||
metadata.usage,
|
||||
padding=5,
|
||||
color=(255, 255, 255),
|
||||
font_color=(0, 0, 0),
|
||||
)
|
||||
if metadata.description:
|
||||
description = await text2image(
|
||||
metadata.description,
|
||||
padding=5,
|
||||
color=(255, 255, 255),
|
||||
font_color=(0, 0, 0),
|
||||
)
|
||||
return usage, description
|
||||
|
||||
|
||||
async def build_image(
|
||||
plugin: PluginInfo, metadata: PluginMetadata, font: FreeTypeFont
|
||||
) -> BuildImage:
|
||||
"""构建帮助图片
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
metadata: PluginMetadata
|
||||
font: FreeTypeFont
|
||||
|
||||
返回:
|
||||
BuildImage: 帮助图片
|
||||
|
||||
"""
|
||||
usage, description = await build_usage_des_image(metadata)
|
||||
width = 0
|
||||
height = 100
|
||||
if usage:
|
||||
width = usage.width
|
||||
height += usage.height
|
||||
if description and description.width > width:
|
||||
width = description.width
|
||||
height += description.height
|
||||
font_width, _ = BuildImage.get_text_size(f"{plugin.name}[{plugin.level}]", font)
|
||||
if font_width > width:
|
||||
width = font_width
|
||||
A = BuildImage(width + 30, height + 120, "#EAEDF2")
|
||||
await A.text((15, 10), f"{plugin.name}[{plugin.level}]")
|
||||
await A.text((15, 70), "简介:")
|
||||
if not description:
|
||||
description = BuildImage(A.width - 30, 30, (255, 255, 255))
|
||||
await description.circle_corner(10)
|
||||
await A.paste(description, (15, 100))
|
||||
if not usage:
|
||||
usage = BuildImage(A.width - 30, 30, (255, 255, 255))
|
||||
await usage.circle_corner(10)
|
||||
await A.text((15, description.height + 115), "用法:")
|
||||
await A.paste(usage, (15, description.height + 145))
|
||||
await A.circle_corner(10)
|
||||
return A
|
||||
|
||||
|
||||
async def build_help():
|
||||
"""构造管理员帮助图片
|
||||
|
||||
返回:
|
||||
BuildImage: 管理员帮助图片
|
||||
"""
|
||||
font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
|
||||
image_list = []
|
||||
for data in await get_plugins():
|
||||
plugin = data.plugin
|
||||
metadata = data.metadata
|
||||
try:
|
||||
A = await build_image(plugin, metadata, font)
|
||||
image_list.append(A)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"获取群管理员插件 {plugin.module}: {plugin.name} 设置失败...",
|
||||
"管理员帮助",
|
||||
e=e,
|
||||
)
|
||||
if task_list := await TaskInfo.all():
|
||||
task_str = "\n".join([task.name for task in task_list])
|
||||
task_str = "通过 开启/关闭群被动 来控制群被动\n----------\n" + task_str
|
||||
task_image = await text2image(task_str, padding=5, color=(255, 255, 255))
|
||||
await task_image.circle_corner(10)
|
||||
A = BuildImage(task_image.width + 50, task_image.height + 85, "#EAEDF2")
|
||||
await A.text((25, 10), "被动技能")
|
||||
await A.paste(task_image, (25, 50))
|
||||
await A.circle_corner(10)
|
||||
image_list.append(A)
|
||||
image_group, _ = group_image(image_list)
|
||||
A = await build_sort_image(image_group, color=(255, 255, 255), padding_top=160)
|
||||
text = await BuildImage.build_text_image(
|
||||
"群管理员帮助",
|
||||
size=40,
|
||||
)
|
||||
tip = await BuildImage.build_text_image(
|
||||
"注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", size=25, font_color="red"
|
||||
)
|
||||
await A.paste(text, (50, 30))
|
||||
await A.paste(tip, (50, 90))
|
||||
await A.save(ADMIN_HELP_IMAGE)
|
||||
@@ -1,22 +0,0 @@
|
||||
import nonebot
|
||||
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.exception import EmptyError
|
||||
|
||||
from .config import PluginData
|
||||
|
||||
|
||||
async def get_plugins() -> list[PluginData]:
|
||||
"""获取插件数据"""
|
||||
plugin_list = await PluginInfo.filter(
|
||||
plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN]
|
||||
).all()
|
||||
data_list = []
|
||||
for plugin in plugin_list:
|
||||
if _plugin := nonebot.get_plugin_by_module_name(plugin.module_path):
|
||||
if _plugin.metadata:
|
||||
data_list.append(PluginData(plugin=plugin, metadata=_plugin.metadata))
|
||||
if not data_list:
|
||||
raise EmptyError()
|
||||
return data_list
|
||||
@@ -36,11 +36,12 @@ __plugin_meta__ = PluginMetadata(
|
||||
usage="""
|
||||
普通管理员
|
||||
格式:
|
||||
ban [At用户] ?[-t [时长(分钟)]]
|
||||
ban [At用户] ?[-t [时长(分钟)]] ?[-r [理由]]
|
||||
|
||||
示例:
|
||||
ban @用户 : 永久拉黑用户
|
||||
ban @用户 -t 100 : 拉黑用户100分钟
|
||||
ban @用户 -t 10 -r 坏 : 拉黑用户10分钟并携带理由
|
||||
unban @用户 : 从小黑屋中拉出来
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
@@ -50,7 +51,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
superuser_help="""
|
||||
超级管理员额外命令
|
||||
格式:
|
||||
ban [At用户/用户Id] ?[-t [时长]]
|
||||
ban [At用户/用户Id] ?[-t [时长]] ?[-r [理由]]
|
||||
unban --id [idx] : 通过id来进行unban操作
|
||||
ban列表: 获取所有Ban数据
|
||||
|
||||
@@ -66,10 +67,13 @@ __plugin_meta__ = PluginMetadata(
|
||||
私聊下:
|
||||
示例:
|
||||
ban 123456789 : 永久拉黑用户123456789
|
||||
ban 123456789 -r 坏 : 永久拉黑用户123456789并携带理由
|
||||
ban 123456789 -t 100 : 拉黑用户123456789 100分钟
|
||||
|
||||
ban -g 999999 : 拉黑群组为999999的群组
|
||||
ban -g 999999 -t 100 : 拉黑群组为999999的群组 100分钟
|
||||
ban -g 999999 -r 坏 : 永久拉黑群组为999999的群组并携带理由
|
||||
|
||||
|
||||
unban 123456789 : 从小黑屋中拉出来
|
||||
unban -g 999999 : 将群组9999999从小黑屋中拉出来
|
||||
@@ -87,13 +91,20 @@ __plugin_meta__ = PluginMetadata(
|
||||
smart_tools=[
|
||||
AICallableTag(
|
||||
name="call_ban",
|
||||
description="某人多次(至少三次)辱骂你,调用此方法进行封禁",
|
||||
description="如果你讨厌某个人(好感度过低并让你感到困扰,或者多次辱骂你),调用此方法进行封禁,调用该方法后要告知用户被封禁和原因",
|
||||
parameters=AICallableParam(
|
||||
type="object",
|
||||
properties={
|
||||
"user_id": AICallableProperties(
|
||||
type="string", description="用户的id"
|
||||
),
|
||||
"reason": AICallableProperties(
|
||||
type="string", description="封禁理由"
|
||||
),
|
||||
"duration": AICallableProperties(
|
||||
type="integer",
|
||||
description="封禁时长(选择的值只能是1-360),单位为分钟,如果频繁触发,按情况增加",
|
||||
),
|
||||
},
|
||||
required=["user_id"],
|
||||
),
|
||||
@@ -108,6 +119,7 @@ _ban_matcher = on_alconna(
|
||||
Alconna(
|
||||
"ban",
|
||||
Args["user?", [str, At]],
|
||||
Option("-r|--reason", Args["reason", str]),
|
||||
Option("-g|--group", Args["group_id", str]),
|
||||
Option("-t|--time", Args["duration", int]),
|
||||
),
|
||||
@@ -181,6 +193,7 @@ async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
user: Match[str | At],
|
||||
reason: Match[str],
|
||||
duration: Match[int],
|
||||
group_id: Match[str],
|
||||
):
|
||||
@@ -196,13 +209,14 @@ async def _(
|
||||
user_id = user.result
|
||||
_duration = duration.result * 60 if duration.available else -1
|
||||
_duration_text = f"{duration.result} 分钟" if duration.available else " 到世界湮灭"
|
||||
ban_reason = reason.result if reason.available else None
|
||||
if (gid := session.id3 or session.id2) and not group_id.available:
|
||||
if not user_id or (
|
||||
user_id == bot.self_id and session.id1 not in bot.config.superusers
|
||||
):
|
||||
_duration = 0.5
|
||||
await MessageUtils.build_message("倒反天罡,小小管理速速退下!").send()
|
||||
await BanManage.ban(session.id1, gid, 30, session, True)
|
||||
await BanManage.ban(session.id1, gid, ban_reason, 30, session, True)
|
||||
_duration_text = "半 分钟"
|
||||
logger.info(
|
||||
f"尝试ban {BotConfig.self_nickname} 反被拿下",
|
||||
@@ -218,7 +232,12 @@ async def _(
|
||||
]
|
||||
).finish(reply_to=True)
|
||||
await BanManage.ban(
|
||||
user_id, gid, _duration, session, session.id1 in bot.config.superusers
|
||||
user_id,
|
||||
gid,
|
||||
ban_reason,
|
||||
_duration,
|
||||
session,
|
||||
session.id1 in bot.config.superusers,
|
||||
)
|
||||
logger.info(
|
||||
"管理员Ban",
|
||||
@@ -240,7 +259,7 @@ async def _(
|
||||
).finish(reply_to=True)
|
||||
elif session.id1 in bot.config.superusers:
|
||||
_group_id = group_id.result if group_id.available else None
|
||||
await BanManage.ban(user_id, _group_id, _duration, session, True)
|
||||
await BanManage.ban(user_id, _group_id, ban_reason, _duration, session, True)
|
||||
logger.info(
|
||||
"超级用户Ban",
|
||||
arparma.header_result,
|
||||
@@ -292,7 +311,7 @@ async def _(
|
||||
At(flag="user", target=user_id)
|
||||
if isinstance(user.result, At)
|
||||
else result
|
||||
), # type: ignore
|
||||
),
|
||||
" 从黑屋中拉了出来并急救了一下!",
|
||||
]
|
||||
).finish(reply_to=True)
|
||||
|
||||
@@ -9,14 +9,14 @@ from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
|
||||
|
||||
async def call_ban(user_id: str):
|
||||
async def call_ban(user_id: str, reason: str | None = None, duration: int = 1):
|
||||
"""调用ban
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
"""
|
||||
await BanConsole.ban(user_id, None, 9, 60 * 12)
|
||||
logger.info("辱骂次数过多,已将用户加入黑名单...", "ban", session=user_id)
|
||||
await BanConsole.ban(user_id, None, 9, reason, duration * 60)
|
||||
logger.info("被讨厌了,已将用户加入黑名单...", "ban", session=user_id)
|
||||
|
||||
|
||||
class BanManage:
|
||||
@@ -55,20 +55,26 @@ class BanManage:
|
||||
"用户ID",
|
||||
"群组ID",
|
||||
"BAN LEVEL",
|
||||
"封禁原因",
|
||||
"剩余时长(分钟)",
|
||||
"操作员ID",
|
||||
]
|
||||
row_data = []
|
||||
for data in data_list:
|
||||
duration = int((data.ban_time + data.duration - time.time()) / 60)
|
||||
if data.duration < 0:
|
||||
if data.duration == -1:
|
||||
duration = "∞"
|
||||
else:
|
||||
duration = int((data.ban_time + data.duration - time.time()) / 60)
|
||||
if isinstance(duration, int) and duration < 0:
|
||||
await data.delete()
|
||||
continue
|
||||
row_data.append(
|
||||
[
|
||||
data.id,
|
||||
data.user_id,
|
||||
data.group_id,
|
||||
data.ban_level,
|
||||
data.ban_reason,
|
||||
duration,
|
||||
data.operator,
|
||||
]
|
||||
@@ -108,22 +114,22 @@ class BanManage:
|
||||
is_superuser: 是否为超级用户操作
|
||||
|
||||
返回:
|
||||
tuple[bool, str]: 是否unban成功, 群组/用户id或提示
|
||||
tuple[bool, str | Non]: 是否unban成功, 群组/用户id或提示
|
||||
"""
|
||||
user_level = 9999
|
||||
if not is_superuser and user_id and session.id1:
|
||||
user_level = await LevelUser.get_user_level(session.id1, group_id)
|
||||
if idx:
|
||||
ban_data = await BanConsole.get_or_none(id=idx)
|
||||
ban_data = await BanConsole.get_ban(id=idx)
|
||||
if not ban_data:
|
||||
return False, "该用户/群组不在黑名单中捏..."
|
||||
if ban_data.ban_level > user_level:
|
||||
return False, "unBan权限等级不足捏..."
|
||||
await ban_data.delete()
|
||||
return True, str(ban_data.user_id or ban_data.group_id)
|
||||
return True, ban_data.user_id or ban_data.group_id
|
||||
elif await BanConsole.check_ban_level(user_id, group_id, user_level):
|
||||
await BanConsole.unban(user_id, group_id)
|
||||
return True, str(group_id)
|
||||
return True, group_id or ""
|
||||
return False, "该用户/群组不在黑名单中不足捏..."
|
||||
|
||||
@classmethod
|
||||
@@ -131,6 +137,7 @@ class BanManage:
|
||||
cls,
|
||||
user_id: str | None,
|
||||
group_id: str | None,
|
||||
reason: str | None,
|
||||
duration: int,
|
||||
session: EventSession,
|
||||
is_superuser: bool,
|
||||
@@ -140,6 +147,7 @@ class BanManage:
|
||||
参数:
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
reason: 理由
|
||||
duration: 时长,秒
|
||||
session: Session
|
||||
is_superuser: 是否为超级用户操作
|
||||
@@ -147,4 +155,4 @@ class BanManage:
|
||||
level = 9999
|
||||
if not is_superuser and user_id and session.id1:
|
||||
level = await LevelUser.get_user_level(session.id1, group_id)
|
||||
await BanConsole.ban(user_id, group_id, level, duration, session.id1)
|
||||
await BanConsole.ban(user_id, group_id, level, reason, duration, session.id1)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
import nonebot
|
||||
from nonebot import on_notice
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.adapters.onebot.v11 import GroupIncreaseNoticeEvent
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
@@ -10,6 +14,7 @@ from nonebot_plugin_session import EventSession
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.services.tags import tag_manager
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
@@ -45,12 +50,79 @@ _matcher = on_alconna(
|
||||
_notice = on_notice(priority=1, block=False, rule=notice_rule(GroupIncreaseNoticeEvent))
|
||||
|
||||
|
||||
_update_all_matcher = on_alconna(
|
||||
Alconna("更新所有群组信息"),
|
||||
permission=SUPERUSER,
|
||||
priority=1,
|
||||
block=True,
|
||||
)
|
||||
|
||||
|
||||
async def _update_all_groups_task(bot: Bot, session: EventSession):
|
||||
"""
|
||||
在后台执行所有群组的更新任务,并向超级用户发送最终报告。
|
||||
"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
total_count = 0
|
||||
bot_id = bot.self_id
|
||||
|
||||
logger.info(f"Bot {bot_id}: 开始执行所有群组信息更新任务...", "更新所有群组")
|
||||
try:
|
||||
group_list, _ = await PlatformUtils.get_group_list(bot)
|
||||
total_count = len(group_list)
|
||||
for i, group in enumerate(group_list):
|
||||
try:
|
||||
logger.debug(
|
||||
f"Bot {bot_id}: 正在更新第 {i + 1}/{total_count} 个群组: "
|
||||
f"{group.group_id}",
|
||||
"更新所有群组",
|
||||
)
|
||||
await MemberUpdateManage.update_group_member(bot, group.group_id)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
fail_count += 1
|
||||
logger.error(
|
||||
f"Bot {bot_id}: 更新群组 {group.group_id} 信息失败",
|
||||
"更新所有群组",
|
||||
e=e,
|
||||
)
|
||||
await asyncio.sleep(random.uniform(1.5, 3.0))
|
||||
except Exception as e:
|
||||
logger.error(f"Bot {bot_id}: 获取群组列表失败,任务中断", "更新所有群组", e=e)
|
||||
await PlatformUtils.send_superuser(
|
||||
bot,
|
||||
f"Bot {bot_id} 更新所有群组信息任务失败:无法获取群组列表。",
|
||||
session.id1,
|
||||
)
|
||||
return
|
||||
|
||||
await tag_manager._invalidate_cache()
|
||||
summary_message = (
|
||||
f"🤖 Bot {bot_id} 所有群组信息更新任务完成!\n"
|
||||
f"总计群组: {total_count}\n"
|
||||
f"✅ 成功: {success_count}\n"
|
||||
f"❌ 失败: {fail_count}"
|
||||
)
|
||||
logger.info(summary_message.replace("\n", " | "), "更新所有群组")
|
||||
await PlatformUtils.send_superuser(bot, summary_message, session.id1)
|
||||
|
||||
|
||||
@_update_all_matcher.handle()
|
||||
async def _(bot: Bot, session: EventSession):
|
||||
await MessageUtils.build_message(
|
||||
"已开始在后台更新所有群组信息,过程可能需要几分钟到几十分钟,完成后将私聊通知您。"
|
||||
).send(reply_to=True)
|
||||
asyncio.create_task(_update_all_groups_task(bot, session)) # noqa: RUF006
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(bot: Bot, session: EventSession, arparma: Arparma):
|
||||
if gid := session.id3 or session.id2:
|
||||
logger.info("更新群组成员信息", arparma.header_result, session=session)
|
||||
result = await MemberUpdateManage.update_group_member(bot, gid)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
await tag_manager._invalidate_cache()
|
||||
await MessageUtils.build_message("群组id为空...").send()
|
||||
|
||||
|
||||
@@ -64,6 +136,7 @@ async def _(bot: Bot, event: GroupIncreaseNoticeEvent):
|
||||
session=event.user_id,
|
||||
group_id=event.group_id,
|
||||
)
|
||||
await tag_manager._invalidate_cache()
|
||||
|
||||
|
||||
@scheduler.scheduled_job(
|
||||
@@ -91,3 +164,5 @@ async def _():
|
||||
except Exception as e:
|
||||
logger.error(f"Bot: {bot.self_id} 自动更新群组信息", e=e)
|
||||
logger.debug(f"自动 Bot: {bot.self_id} 更新群组成员信息成功...")
|
||||
|
||||
await tag_manager._invalidate_cache()
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot_plugin_uninfo import Member, SceneType, get_interface
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.services.log import logger
|
||||
@@ -32,7 +34,9 @@ class MemberUpdateManage:
|
||||
"""
|
||||
driver = nonebot.get_driver()
|
||||
default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH")
|
||||
nickname = member.nick or member.user.name or ""
|
||||
nickname = re.sub(
|
||||
r"[\x00-\x09\x0b-\x1f\x7f-\x9f]", "", member.nick or member.user.name or ""
|
||||
)
|
||||
role = member.role
|
||||
db_user_uid = [u.user_id for u in db_user]
|
||||
uid2name = {u.user_id: u.user_name for u in db_user}
|
||||
@@ -91,6 +95,25 @@ class MemberUpdateManage:
|
||||
)
|
||||
return "更新群组失败,群组不存在..."
|
||||
members = await interface.get_members(SceneType.GROUP, group_list[0].id)
|
||||
|
||||
try:
|
||||
group_console, _ = await GroupConsole.get_or_create(
|
||||
group_id=group_id, defaults={"platform": platform}
|
||||
)
|
||||
group_console.member_count = len(members)
|
||||
group_console.group_name = group_list[0].name or ""
|
||||
await group_console.save(update_fields=["member_count", "group_name"])
|
||||
logger.debug(
|
||||
f"已更新群组 {group_id} 的成员总数为 {len(members)}",
|
||||
"更新群组成员信息",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"更新群组 {group_id} 的 GroupConsole 信息失败",
|
||||
"更新群组成员信息",
|
||||
e=e,
|
||||
)
|
||||
|
||||
db_user = await GroupInfoUser.filter(group_id=group_id).all()
|
||||
db_user_uid = [u.user_id for u in db_user]
|
||||
data_list = ([], [], [])
|
||||
@@ -101,25 +124,16 @@ class MemberUpdateManage:
|
||||
exist_member_list.append(member.id)
|
||||
if data_list[0]:
|
||||
try:
|
||||
await GroupInfoUser.bulk_create(data_list[0], 30)
|
||||
await GroupInfoUser.bulk_create(
|
||||
data_list[0], 30, ignore_conflicts=True
|
||||
)
|
||||
logger.debug(
|
||||
f"创建用户数据 {len(data_list[0])} 条",
|
||||
"更新群组成员信息",
|
||||
target=group_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"批量创建用户数据失败: {e},开始进行逐个存储",
|
||||
"更新群组成员信息",
|
||||
)
|
||||
for u in data_list[0]:
|
||||
try:
|
||||
await u.save()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"创建用户 {u.user_name}({u.user_id}) 数据失败: {e}",
|
||||
"更新群组成员信息",
|
||||
)
|
||||
logger.error("批量创建用户数据失败", "更新群组成员信息", e=e)
|
||||
if data_list[1]:
|
||||
await GroupInfoUser.bulk_update(data_list[1], ["user_name"], 30)
|
||||
logger.debug(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import AlconnaQuery, Arparma, Match, Query
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
@@ -9,7 +9,7 @@ from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import PluginManage, build_plugin, build_task, delete_help_image
|
||||
from ._data_source import PluginManager, build_plugin, build_task
|
||||
from .command import _group_status_matcher, _status_matcher
|
||||
|
||||
base_config = Config.get("plugin_switch")
|
||||
@@ -57,6 +57,11 @@ __plugin_meta__ = PluginMetadata(
|
||||
关闭群被动早晚安
|
||||
关闭群被动早晚安 -g 12355555
|
||||
|
||||
开启/关闭默认群被动 [被动名称]
|
||||
私聊下: 开启/关闭群被动默认状态
|
||||
示例:
|
||||
关闭默认群被动 早晚安
|
||||
|
||||
开启/关闭所有群被动 ?[-g [group_id]]
|
||||
私聊中: 开启/关闭全局或指定群组被动状态
|
||||
示例:
|
||||
@@ -87,10 +92,10 @@ __plugin_meta__ = PluginMetadata(
|
||||
@_status_matcher.assign("$main")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
):
|
||||
if session.id1 in bot.config.superusers:
|
||||
if session.user.id in bot.config.superusers:
|
||||
image = await build_plugin()
|
||||
logger.info(
|
||||
"查看功能列表",
|
||||
@@ -105,7 +110,7 @@ async def _(
|
||||
@_status_matcher.assign("open")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
plugin_name: Match[str],
|
||||
group: Match[str],
|
||||
@@ -114,22 +119,23 @@ async def _(
|
||||
all: Query[bool] = AlconnaQuery("all.value", False),
|
||||
):
|
||||
if not all.result and not plugin_name.available:
|
||||
await MessageUtils.build_message("请输入功能名称").finish(reply_to=True)
|
||||
await MessageUtils.build_message("请输入功能/被动名称").finish(reply_to=True)
|
||||
name = plugin_name.result
|
||||
if gid := session.id3 or session.id2:
|
||||
if session.group:
|
||||
group_id = session.group.id
|
||||
"""修改当前群组的数据"""
|
||||
if task.result:
|
||||
if all.result:
|
||||
result = await PluginManage.unblock_group_all_task(gid)
|
||||
result = await PluginManager.unblock_group_all_task(group_id)
|
||||
logger.info("开启所有群组被动", arparma.header_result, session=session)
|
||||
else:
|
||||
result = await PluginManage.unblock_group_task(name, gid)
|
||||
result = await PluginManager.unblock_group_task(name, group_id)
|
||||
logger.info(
|
||||
f"开启群组被动 {name}", arparma.header_result, session=session
|
||||
)
|
||||
elif session.id1 in bot.config.superusers and default_status.result:
|
||||
elif session.user.id in bot.config.superusers and default_status.result:
|
||||
"""单个插件的进群默认修改"""
|
||||
result = await PluginManage.set_default_status(name, True)
|
||||
result = await PluginManager.set_default_status(name, True)
|
||||
logger.info(
|
||||
f"超级用户开启 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
@@ -137,8 +143,8 @@ async def _(
|
||||
)
|
||||
elif all.result:
|
||||
"""所有插件"""
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
True, default_status.result, gid
|
||||
result = await PluginManager.set_all_plugin_status(
|
||||
True, default_status.result, group_id
|
||||
)
|
||||
logger.info(
|
||||
"开启群组中全部功能",
|
||||
@@ -146,22 +152,23 @@ async def _(
|
||||
session=session,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.unblock_group_plugin(name, gid)
|
||||
result = await PluginManager.unblock_group_plugin(name, group_id)
|
||||
logger.info(f"开启功能 {name}", arparma.header_result, session=session)
|
||||
delete_help_image(gid)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
elif session.id1 in bot.config.superusers:
|
||||
elif session.user.id in bot.config.superusers:
|
||||
"""私聊"""
|
||||
group_id = group.result if group.available else None
|
||||
if all.result:
|
||||
if task.result:
|
||||
"""关闭全局或指定群全部被动"""
|
||||
if group_id:
|
||||
result = await PluginManage.unblock_group_all_task(group_id)
|
||||
result = await PluginManager.unblock_group_all_task(group_id)
|
||||
else:
|
||||
result = await PluginManage.unblock_global_all_task()
|
||||
result = await PluginManager.unblock_global_all_task(
|
||||
default_status.result
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
result = await PluginManager.set_all_plugin_status(
|
||||
True, default_status.result, group_id
|
||||
)
|
||||
logger.info(
|
||||
@@ -171,8 +178,8 @@ async def _(
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
if default_status.result:
|
||||
result = await PluginManage.set_default_status(name, True)
|
||||
if default_status.result and not task.result:
|
||||
result = await PluginManager.set_default_status(name, True)
|
||||
logger.info(
|
||||
f"超级用户开启 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
@@ -186,7 +193,7 @@ async def _(
|
||||
name = split_list[0]
|
||||
group_id = split_list[1]
|
||||
if group_id:
|
||||
result = await PluginManage.superuser_task_handle(name, group_id, True)
|
||||
result = await PluginManager.superuser_task_handle(name, group_id, True)
|
||||
logger.info(
|
||||
f"超级用户开启被动技能 {name}",
|
||||
arparma.header_result,
|
||||
@@ -194,28 +201,29 @@ async def _(
|
||||
target=group_id,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.unblock_global_task(name)
|
||||
result = await PluginManager.unblock_global_task(
|
||||
name, default_status.result
|
||||
)
|
||||
logger.info(
|
||||
f"超级用户开启全局被动技能 {name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.superuser_unblock(name, None, group_id)
|
||||
result = await PluginManager.superuser_unblock(name, None, group_id)
|
||||
logger.info(
|
||||
f"超级用户开启功能 {name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
target=group_id,
|
||||
)
|
||||
delete_help_image()
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
|
||||
|
||||
@_status_matcher.assign("close")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
plugin_name: Match[str],
|
||||
block_type: Match[str],
|
||||
@@ -225,22 +233,23 @@ async def _(
|
||||
all: Query[bool] = AlconnaQuery("all.value", False),
|
||||
):
|
||||
if not all.result and not plugin_name.available:
|
||||
await MessageUtils.build_message("请输入功能名称").finish(reply_to=True)
|
||||
await MessageUtils.build_message("请输入功能/被动名称").finish(reply_to=True)
|
||||
name = plugin_name.result
|
||||
if gid := session.id3 or session.id2:
|
||||
if session.group:
|
||||
group_id = session.group.id
|
||||
"""修改当前群组的数据"""
|
||||
if task.result:
|
||||
if all.result:
|
||||
result = await PluginManage.block_group_all_task(gid)
|
||||
result = await PluginManager.block_group_all_task(group_id)
|
||||
logger.info("开启所有群组被动", arparma.header_result, session=session)
|
||||
else:
|
||||
result = await PluginManage.block_group_task(name, gid)
|
||||
result = await PluginManager.block_group_task(name, group_id)
|
||||
logger.info(
|
||||
f"关闭群组被动 {name}", arparma.header_result, session=session
|
||||
)
|
||||
elif session.id1 in bot.config.superusers and default_status.result:
|
||||
elif session.user.id in bot.config.superusers and default_status.result:
|
||||
"""单个插件的进群默认修改"""
|
||||
result = await PluginManage.set_default_status(name, False)
|
||||
result = await PluginManager.set_default_status(name, False)
|
||||
logger.info(
|
||||
f"超级用户开启 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
@@ -248,26 +257,27 @@ async def _(
|
||||
)
|
||||
elif all.result:
|
||||
"""所有插件"""
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
False, default_status.result, gid
|
||||
result = await PluginManager.set_all_plugin_status(
|
||||
False, default_status.result, group_id
|
||||
)
|
||||
logger.info("关闭群组中全部功能", arparma.header_result, session=session)
|
||||
else:
|
||||
result = await PluginManage.block_group_plugin(name, gid)
|
||||
result = await PluginManager.block_group_plugin(name, group_id)
|
||||
logger.info(f"关闭功能 {name}", arparma.header_result, session=session)
|
||||
delete_help_image(gid)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
elif session.id1 in bot.config.superusers:
|
||||
elif session.user.id in bot.config.superusers:
|
||||
group_id = group.result if group.available else None
|
||||
if all.result:
|
||||
if task.result:
|
||||
"""关闭全局或指定群全部被动"""
|
||||
if group_id:
|
||||
result = await PluginManage.block_group_all_task(group_id)
|
||||
result = await PluginManager.block_group_all_task(group_id)
|
||||
else:
|
||||
result = await PluginManage.block_global_all_task()
|
||||
result = await PluginManager.block_global_all_task(
|
||||
default_status.result
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
result = await PluginManager.set_all_plugin_status(
|
||||
False, default_status.result, group_id
|
||||
)
|
||||
logger.info(
|
||||
@@ -277,8 +287,8 @@ async def _(
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
if default_status.result:
|
||||
result = await PluginManage.set_default_status(name, False)
|
||||
if default_status.result and not task.result:
|
||||
result = await PluginManager.set_default_status(name, False)
|
||||
logger.info(
|
||||
f"超级用户关闭 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
@@ -292,7 +302,9 @@ async def _(
|
||||
name = split_list[0]
|
||||
group_id = split_list[1]
|
||||
if group_id:
|
||||
result = await PluginManage.superuser_task_handle(name, group_id, False)
|
||||
result = await PluginManager.superuser_task_handle(
|
||||
name, group_id, False
|
||||
)
|
||||
logger.info(
|
||||
f"超级用户关闭被动技能 {name}",
|
||||
arparma.header_result,
|
||||
@@ -300,7 +312,9 @@ async def _(
|
||||
target=group_id,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.block_global_task(name)
|
||||
result = await PluginManager.block_global_task(
|
||||
name, default_status.result
|
||||
)
|
||||
logger.info(
|
||||
f"超级用户关闭全局被动技能 {name}",
|
||||
arparma.header_result,
|
||||
@@ -314,32 +328,32 @@ async def _(
|
||||
elif block_type.result in ["g", "group"]:
|
||||
if block_type.available:
|
||||
_type = BlockType.GROUP
|
||||
result = await PluginManage.superuser_block(name, _type, group_id)
|
||||
result = await PluginManager.superuser_block(name, _type, group_id)
|
||||
logger.info(
|
||||
f"超级用户关闭功能 {name}, 禁用类型: {_type}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
target=group_id,
|
||||
)
|
||||
delete_help_image()
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
|
||||
|
||||
@_group_status_matcher.handle()
|
||||
async def _(
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
status: str,
|
||||
):
|
||||
if gid := session.id3 or session.id2:
|
||||
if session.group:
|
||||
group_id = session.group.id
|
||||
if status == "sleep":
|
||||
await PluginManage.sleep(gid)
|
||||
await PluginManager.sleep(group_id)
|
||||
logger.info("进行休眠", arparma.header_result, session=session)
|
||||
await MessageUtils.build_message("那我先睡觉了...").finish()
|
||||
else:
|
||||
if await PluginManage.is_wake(gid):
|
||||
if await PluginManager.is_wake(group_id):
|
||||
await MessageUtils.build_message("我还醒着呢!").finish()
|
||||
await PluginManage.wake(gid)
|
||||
await PluginManager.wake(group_id)
|
||||
logger.info("醒来", arparma.header_result, session=session)
|
||||
await MessageUtils.build_message("呜..醒来了...").finish()
|
||||
return MessageUtils.build_message("群组id为空...").send()
|
||||
@@ -347,10 +361,10 @@ async def _(
|
||||
|
||||
@_status_matcher.assign("task")
|
||||
async def _(
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
):
|
||||
image = await build_task(session.id3 or session.id2)
|
||||
image = await build_task(session.group.id if session.group else None)
|
||||
if image:
|
||||
logger.info("查看群被动列表", arparma.header_result, session=session)
|
||||
await MessageUtils.build_message(image).finish(reply_to=True)
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
from zhenxun.services.cache import CacheRoot
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.enum import BlockType, CacheType, PluginType
|
||||
from zhenxun.utils.exception import GroupInfoNotFound
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle
|
||||
|
||||
HELP_FILE = IMAGE_PATH / "SIMPLE_HELP.png"
|
||||
|
||||
GROUP_HELP_PATH = DATA_PATH / "group_help"
|
||||
|
||||
|
||||
def delete_help_image(gid: str | None = None):
|
||||
"""删除帮助图片"""
|
||||
if gid:
|
||||
for file in os.listdir(GROUP_HELP_PATH):
|
||||
if file.startswith(f"{gid}"):
|
||||
os.remove(GROUP_HELP_PATH / file)
|
||||
else:
|
||||
if HELP_FILE.exists():
|
||||
HELP_FILE.unlink()
|
||||
for file in GROUP_HELP_PATH.iterdir():
|
||||
file.unlink()
|
||||
|
||||
|
||||
def plugin_row_style(column: str, text: str) -> RowStyle:
|
||||
"""被动技能文本风格
|
||||
@@ -116,9 +100,7 @@ async def build_task(group_id: str | None) -> BuildImage:
|
||||
column_name = ["ID", "模块", "名称", "群组状态", "全局状态", "运行时间"]
|
||||
group = None
|
||||
if group_id:
|
||||
group = await GroupConsole.get_or_none(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
)
|
||||
group = await GroupConsole.get_group(group_id=group_id)
|
||||
if not group:
|
||||
raise GroupInfoNotFound()
|
||||
else:
|
||||
@@ -155,7 +137,7 @@ async def build_task(group_id: str | None) -> BuildImage:
|
||||
)
|
||||
|
||||
|
||||
class PluginManage:
|
||||
class PluginManager:
|
||||
@classmethod
|
||||
async def set_default_status(cls, plugin_name: str, status: bool) -> str:
|
||||
"""设置插件进群默认状态
|
||||
@@ -200,26 +182,26 @@ class PluginManage:
|
||||
)
|
||||
return f"成功将所有功能进群默认状态修改为: {'开启' if status else '关闭'}"
|
||||
if group_id:
|
||||
if group := await GroupConsole.get_or_none(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
):
|
||||
module_list = await PluginInfo.filter(
|
||||
plugin_type=PluginType.NORMAL
|
||||
).values_list("module", flat=True)
|
||||
if group := await GroupConsole.get_group(group_id=group_id):
|
||||
module_list = cast(
|
||||
list[str],
|
||||
await PluginInfo.filter(plugin_type=PluginType.NORMAL).values_list(
|
||||
"module", flat=True
|
||||
),
|
||||
)
|
||||
if status:
|
||||
for module in module_list:
|
||||
group.block_plugin = group.block_plugin.replace(
|
||||
f"<{module},", ""
|
||||
)
|
||||
# 开启所有功能 - 清空禁用列表
|
||||
group.block_plugin = ""
|
||||
else:
|
||||
module_list = [f"<{module}" for module in module_list]
|
||||
group.block_plugin = ",".join(module_list) + "," # type: ignore
|
||||
# 关闭所有功能 - 将模块列表转换为禁用格式
|
||||
group.block_plugin = CommonUtils.convert_module_format(module_list)
|
||||
await group.save(update_fields=["block_plugin"])
|
||||
return f"成功将此群组所有功能状态修改为: {'开启' if status else '关闭'}"
|
||||
return "获取群组失败..."
|
||||
await PluginInfo.filter(plugin_type=PluginType.NORMAL).update(
|
||||
status=status, block_type=None if status else BlockType.ALL
|
||||
)
|
||||
await CacheRoot.invalidate_cache(CacheType.PLUGINS)
|
||||
return f"成功将所有功能全局状态修改为: {'开启' if status else '关闭'}"
|
||||
|
||||
@classmethod
|
||||
@@ -232,9 +214,7 @@ class PluginManage:
|
||||
返回:
|
||||
bool: 是否醒来
|
||||
"""
|
||||
if c := await GroupConsole.get_or_none(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
):
|
||||
if c := await GroupConsole.get_group(group_id=group_id):
|
||||
return c.status
|
||||
return False
|
||||
|
||||
@@ -245,9 +225,11 @@ class PluginManage:
|
||||
参数:
|
||||
group_id: 群组id
|
||||
"""
|
||||
await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update(
|
||||
status=False
|
||||
group, _ = await GroupConsole.get_or_create(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
)
|
||||
group.status = False
|
||||
await group.save(update_fields=["status"])
|
||||
|
||||
@classmethod
|
||||
async def wake(cls, group_id: str):
|
||||
@@ -256,9 +238,11 @@ class PluginManage:
|
||||
参数:
|
||||
group_id: 群组id
|
||||
"""
|
||||
await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update(
|
||||
status=True
|
||||
group, _ = await GroupConsole.get_or_create(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
)
|
||||
group.status = True
|
||||
await group.save(update_fields=["status"])
|
||||
|
||||
@classmethod
|
||||
async def block(cls, module: str):
|
||||
@@ -267,7 +251,9 @@ class PluginManage:
|
||||
参数:
|
||||
module: 模块名
|
||||
"""
|
||||
await PluginInfo.filter(module=module).update(status=False)
|
||||
if plugin := await PluginInfo.get_plugin(module=module):
|
||||
plugin.status = False
|
||||
await plugin.save(update_fields=["status"])
|
||||
|
||||
@classmethod
|
||||
async def unblock(cls, module: str):
|
||||
@@ -276,7 +262,9 @@ class PluginManage:
|
||||
参数:
|
||||
module: 模块名
|
||||
"""
|
||||
await PluginInfo.filter(module=module).update(status=True)
|
||||
if plugin := await PluginInfo.get_plugin(module=module):
|
||||
plugin.status = True
|
||||
await plugin.save(update_fields=["status"])
|
||||
|
||||
@classmethod
|
||||
async def block_group_plugin(cls, plugin_name: str, group_id: str) -> str:
|
||||
@@ -342,17 +330,21 @@ class PluginManage:
|
||||
return await cls._change_group_task("", group_id, True, True)
|
||||
|
||||
@classmethod
|
||||
async def block_global_all_task(cls) -> str:
|
||||
async def block_global_all_task(cls, is_default: bool) -> str:
|
||||
"""禁用全局被动技能
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.all().update(status=False)
|
||||
return "已全局禁用所有被动状态"
|
||||
if is_default:
|
||||
await TaskInfo.all().update(default_status=False)
|
||||
return "已禁用所有被动进群默认状态"
|
||||
else:
|
||||
await TaskInfo.all().update(status=False)
|
||||
return "已全局禁用所有被动状态"
|
||||
|
||||
@classmethod
|
||||
async def block_global_task(cls, name: str) -> str:
|
||||
async def block_global_task(cls, name: str, is_default: bool = False) -> str:
|
||||
"""禁用全局被动技能
|
||||
|
||||
参数:
|
||||
@@ -361,31 +353,47 @@ class PluginManage:
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.filter(name=name).update(status=False)
|
||||
return f"已全局禁用被动状态 {name}"
|
||||
if is_default:
|
||||
await TaskInfo.filter(name=name).update(default_status=False)
|
||||
return f"已禁用被动进群默认状态 {name}"
|
||||
else:
|
||||
await TaskInfo.filter(name=name).update(status=False)
|
||||
return f"已全局禁用被动状态 {name}"
|
||||
|
||||
@classmethod
|
||||
async def unblock_global_all_task(cls) -> str:
|
||||
async def unblock_global_all_task(cls, is_default: bool) -> str:
|
||||
"""开启全局被动技能
|
||||
|
||||
参数:
|
||||
is_default: 是否为默认状态
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.all().update(status=True)
|
||||
return "已全局开启所有被动状态"
|
||||
if is_default:
|
||||
await TaskInfo.all().update(default_status=True)
|
||||
return "已开启所有被动进群默认状态"
|
||||
else:
|
||||
await TaskInfo.all().update(status=True)
|
||||
return "已全局开启所有被动状态"
|
||||
|
||||
@classmethod
|
||||
async def unblock_global_task(cls, name: str) -> str:
|
||||
async def unblock_global_task(cls, name: str, is_default: bool = False) -> str:
|
||||
"""开启全局被动技能
|
||||
|
||||
参数:
|
||||
name: 被动技能名称
|
||||
is_default: 是否为默认状态
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.filter(name=name).update(status=True)
|
||||
return f"已全局开启被动状态 {name}"
|
||||
if is_default:
|
||||
await TaskInfo.filter(name=name).update(default_status=True)
|
||||
return f"已开启被动进群默认状态 {name}"
|
||||
else:
|
||||
await TaskInfo.filter(name=name).update(status=True)
|
||||
return f"已全局开启被动状态 {name}"
|
||||
|
||||
@classmethod
|
||||
async def unblock_group_plugin(cls, plugin_name: str, group_id: str) -> str:
|
||||
@@ -417,17 +425,18 @@ class PluginManage:
|
||||
"""
|
||||
status_str = "关闭" if status else "开启"
|
||||
if is_all:
|
||||
modules = await TaskInfo.annotate().values_list("module", flat=True)
|
||||
if modules:
|
||||
module_list = cast(
|
||||
list[str], await TaskInfo.annotate().values_list("module", flat=True)
|
||||
)
|
||||
if module_list:
|
||||
group, _ = await GroupConsole.get_or_create(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
)
|
||||
modules = [f"<{module}" for module in modules]
|
||||
if status:
|
||||
group.block_task = ",".join(modules) + "," # type: ignore
|
||||
group.block_task = CommonUtils.convert_module_format(module_list)
|
||||
else:
|
||||
for module in modules:
|
||||
group.block_task = group.block_task.replace(f"{module},", "")
|
||||
# 开启所有模块 - 清空禁用列表
|
||||
group.block_task = ""
|
||||
await group.save(update_fields=["block_task"])
|
||||
return f"已成功{status_str}全部被动技能!"
|
||||
elif task := await TaskInfo.get_or_none(name=task_name):
|
||||
|
||||
@@ -58,6 +58,19 @@ _status_matcher.shortcut(
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启(所有|全部)默认群被动",
|
||||
command="switch",
|
||||
arguments=["open", "--task", "--all", "-df"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"关闭(所有|全部)默认群被动",
|
||||
command="switch",
|
||||
arguments=["close", "--task", "--all", "-df"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启群被动\s*(?P<name>.+)",
|
||||
@@ -73,6 +86,20 @@ _status_matcher.shortcut(
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启默认群被动\s*(?P<name>.+)",
|
||||
command="switch",
|
||||
arguments=["open", "{name}", "--task", "-df"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"关闭默认群被动\s*(?P<name>.+)",
|
||||
command="switch",
|
||||
arguments=["close", "{name}", "--task", "-df"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启(所有|全部)群被动",
|
||||
|
||||
@@ -11,18 +11,14 @@ from nonebot_plugin_alconna import (
|
||||
on_alconna,
|
||||
store_true,
|
||||
)
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.manager.resource_manager import (
|
||||
DownloadResourceException,
|
||||
ResourceManager,
|
||||
)
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import UpdateManage
|
||||
from ._data_source import UpdateManager
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="自动更新",
|
||||
@@ -32,16 +28,26 @@ __plugin_meta__ = PluginMetadata(
|
||||
检查更新真寻最新版本,包括了自动更新
|
||||
资源文件大小一般在130mb左右,除非必须更新一般仅更新代码文件
|
||||
指令:
|
||||
检查更新 [main|release|resource] ?[-r]
|
||||
检查更新 [main|release|resource|webui] ?[-r] ?[-f] ?[-z] ?[-t]
|
||||
main: main分支
|
||||
release: 最新release
|
||||
resource: 资源文件
|
||||
webui: webui文件
|
||||
-r: 下载资源文件,一般在更新main或release时使用
|
||||
-f: 强制更新,一般用于更新main时使用(仅git更新时有效)
|
||||
-s: 更新源,为 git 或 ali(默认使用ali)
|
||||
-z: 下载zip文件进行更新(仅git有效)
|
||||
-t: 更新方式,git或download(默认使用git)
|
||||
git: 使用git pull(推荐)
|
||||
download: 通过commit hash比较文件后下载更新(仅git有效)
|
||||
|
||||
示例:
|
||||
检查更新 main
|
||||
检查更新 main -r
|
||||
检查更新 main -f
|
||||
检查更新 release -r
|
||||
检查更新 resource
|
||||
检查更新 webui
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
@@ -53,8 +59,11 @@ __plugin_meta__ = PluginMetadata(
|
||||
_matcher = on_alconna(
|
||||
Alconna(
|
||||
"检查更新",
|
||||
Args["ver_type?", ["main", "release", "resource"]],
|
||||
Args["ver_type?", ["main", "release", "resource", "webui"]],
|
||||
Option("-r|--resource", action=store_true, help_text="下载资源文件"),
|
||||
Option("-f|--force", action=store_true, help_text="强制更新"),
|
||||
Option("-s", Args["source?", ["git", "ali"]], help_text="更新源"),
|
||||
Option("-z|--zip", action=store_true, help_text="下载zip文件"),
|
||||
),
|
||||
priority=1,
|
||||
block=True,
|
||||
@@ -66,32 +75,61 @@ _matcher = on_alconna(
|
||||
@_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
ver_type: Match[str],
|
||||
resource: Query[bool] = Query("resource", False),
|
||||
force: Query[bool] = Query("force", False),
|
||||
source: Query[str] = Query("source", "ali"),
|
||||
zip: Query[bool] = Query("zip", False),
|
||||
):
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
result = ""
|
||||
if ver_type.result in {"main", "release"}:
|
||||
if not ver_type.available:
|
||||
result = await UpdateManage.check_version()
|
||||
logger.info("查看当前版本...", "检查更新", session=session)
|
||||
await MessageUtils.build_message(result).finish()
|
||||
await MessageUtils.build_message("正在进行检查更新...").send(reply_to=True)
|
||||
|
||||
if not ver_type.available:
|
||||
result += await UpdateManager.check_version()
|
||||
logger.info("查看当前版本...", "检查更新", session=session)
|
||||
await MessageUtils.build_message(result).finish()
|
||||
return
|
||||
|
||||
ver_type_str = ver_type.result
|
||||
source_str = source.result
|
||||
if ver_type_str in {"main", "release"}:
|
||||
try:
|
||||
result = await UpdateManage.update(bot, session.id1, ver_type.result)
|
||||
result += await UpdateManager.update_zhenxun(
|
||||
bot,
|
||||
session.user.id,
|
||||
ver_type_str, # type: ignore
|
||||
force.result,
|
||||
source_str, # type: ignore
|
||||
zip.result,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("版本更新失败...", "检查更新", session=session, e=e)
|
||||
await MessageUtils.build_message(f"更新版本失败...e: {e}").finish()
|
||||
elif ver_type.result == "webui":
|
||||
if zip.result:
|
||||
source_str = None
|
||||
try:
|
||||
result += await UpdateManager.update_webui(
|
||||
source_str, # type: ignore
|
||||
"test",
|
||||
True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("WebUI更新失败...", "检查更新", session=session, e=e)
|
||||
result += "\nWebUI更新错误..."
|
||||
if resource.result or ver_type.result == "resource":
|
||||
try:
|
||||
await ResourceManager.init_resources(True)
|
||||
result += "\n资源文件更新成功!"
|
||||
except DownloadResourceException:
|
||||
result += "\n资源更新下载失败..."
|
||||
if zip.result:
|
||||
source_str = None
|
||||
result += await UpdateManager.update_resources(
|
||||
source_str, # type: ignore
|
||||
"main",
|
||||
force.result,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("资源更新下载失败...", "检查更新", session=session, e=e)
|
||||
result += "\n资源更新未知错误..."
|
||||
result += "\n资源更新错误..."
|
||||
if result:
|
||||
await MessageUtils.build_message(result.strip()).finish()
|
||||
await MessageUtils.build_message("更新版本失败...").finish()
|
||||
|
||||
@@ -1,217 +1,307 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import zipfile
|
||||
import asyncio
|
||||
from typing import Literal
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.utils import run_sync
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.github_utils import GithubUtils
|
||||
from zhenxun.utils.github_utils.models import RepoInfo
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from .config import (
|
||||
BACKUP_PATH,
|
||||
BASE_PATH,
|
||||
BASE_PATH_STRING,
|
||||
DEFAULT_GITHUB_URL,
|
||||
DOWNLOAD_GZ_FILE,
|
||||
DOWNLOAD_ZIP_FILE,
|
||||
PYPROJECT_FILE,
|
||||
PYPROJECT_FILE_STRING,
|
||||
PYPROJECT_LOCK_FILE,
|
||||
PYPROJECT_LOCK_FILE_STRING,
|
||||
RELEASE_URL,
|
||||
REPLACE_FOLDERS,
|
||||
REQ_TXT_FILE,
|
||||
REQ_TXT_FILE_STRING,
|
||||
TMP_PATH,
|
||||
VERSION_FILE,
|
||||
from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager
|
||||
from zhenxun.utils.manager.zhenxun_repo_manager import (
|
||||
ZhenxunRepoConfig,
|
||||
ZhenxunRepoManager,
|
||||
)
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.repo_utils import RepoFileManager
|
||||
|
||||
LOG_COMMAND = "AutoUpdate"
|
||||
|
||||
|
||||
def install_requirement():
|
||||
requirement_path = (REQ_TXT_FILE).absolute()
|
||||
class UpdateManager:
|
||||
@staticmethod
|
||||
async def _get_latest_commit_date(owner: str, repo: str, path: str) -> str:
|
||||
"""获取文件最新 commit 日期"""
|
||||
api_url = f"https://api.github.com/repos/{owner}/{repo}/commits"
|
||||
params = {"path": path, "page": 1, "per_page": 1}
|
||||
try:
|
||||
data = await AsyncHttpx.get_json(api_url, params=params)
|
||||
if data and isinstance(data, list) and data[0]:
|
||||
date_str = data[0]["commit"]["committer"]["date"]
|
||||
return date_str.split("T")[0]
|
||||
except Exception as e:
|
||||
logger.warning(f"获取 {owner}/{repo}/{path} 的 commit 日期失败", e=e)
|
||||
return "获取失败"
|
||||
|
||||
if not requirement_path.exists():
|
||||
logger.debug(
|
||||
f"没有找到zhenxun的requirement.txt,目标路径为{requirement_path}", "插件管理"
|
||||
)
|
||||
return
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["pip", "install", "-r", str(requirement_path)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
logger.debug(f"成功安装真寻依赖,日志:\n{result.stdout}", "插件管理")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"安装真寻依赖失败,错误:\n{e.stderr}", "插件管理", e=e)
|
||||
|
||||
|
||||
@run_sync
|
||||
def _file_handle(latest_version: str | None):
|
||||
"""文件移动操作
|
||||
|
||||
参数:
|
||||
latest_version: 版本号
|
||||
"""
|
||||
BACKUP_PATH.mkdir(exist_ok=True, parents=True)
|
||||
logger.debug("开始解压文件压缩包...", "检查更新")
|
||||
download_file = DOWNLOAD_GZ_FILE
|
||||
if DOWNLOAD_GZ_FILE.exists():
|
||||
tf = tarfile.open(DOWNLOAD_GZ_FILE)
|
||||
else:
|
||||
download_file = DOWNLOAD_ZIP_FILE
|
||||
tf = zipfile.ZipFile(DOWNLOAD_ZIP_FILE)
|
||||
tf.extractall(TMP_PATH)
|
||||
logger.debug("解压文件压缩包完成...", "检查更新")
|
||||
download_file_path = TMP_PATH / next(
|
||||
x for x in os.listdir(TMP_PATH) if (TMP_PATH / x).is_dir()
|
||||
)
|
||||
_pyproject = download_file_path / PYPROJECT_FILE_STRING
|
||||
_lock_file = download_file_path / PYPROJECT_LOCK_FILE_STRING
|
||||
_req_file = download_file_path / REQ_TXT_FILE_STRING
|
||||
extract_path = download_file_path / BASE_PATH_STRING
|
||||
target_path = BASE_PATH
|
||||
if PYPROJECT_FILE.exists():
|
||||
logger.debug(f"移除备份文件: {PYPROJECT_FILE}", "检查更新")
|
||||
shutil.move(PYPROJECT_FILE, BACKUP_PATH / PYPROJECT_FILE_STRING)
|
||||
if PYPROJECT_LOCK_FILE.exists():
|
||||
logger.debug(f"移除备份文件: {PYPROJECT_LOCK_FILE}", "检查更新")
|
||||
shutil.move(PYPROJECT_LOCK_FILE, BACKUP_PATH / PYPROJECT_LOCK_FILE_STRING)
|
||||
if REQ_TXT_FILE.exists():
|
||||
logger.debug(f"移除备份文件: {REQ_TXT_FILE}", "检查更新")
|
||||
shutil.move(REQ_TXT_FILE, BACKUP_PATH / REQ_TXT_FILE_STRING)
|
||||
if _pyproject.exists():
|
||||
logger.debug("移动文件: pyproject.toml", "检查更新")
|
||||
shutil.move(_pyproject, PYPROJECT_FILE)
|
||||
if _lock_file.exists():
|
||||
logger.debug("移动文件: poetry.lock", "检查更新")
|
||||
shutil.move(_lock_file, PYPROJECT_LOCK_FILE)
|
||||
if _req_file.exists():
|
||||
logger.debug("移动文件: requirements.txt", "检查更新")
|
||||
shutil.move(_req_file, REQ_TXT_FILE)
|
||||
for folder in REPLACE_FOLDERS:
|
||||
"""移动指定文件夹"""
|
||||
_dir = BASE_PATH / folder
|
||||
_backup_dir = BACKUP_PATH / folder
|
||||
if _backup_dir.exists():
|
||||
logger.debug(f"删除备份文件夹 {_backup_dir}", "检查更新")
|
||||
shutil.rmtree(_backup_dir)
|
||||
if _dir.exists():
|
||||
logger.debug(f"移动旧文件夹 {_dir}", "检查更新")
|
||||
shutil.move(_dir, _backup_dir)
|
||||
else:
|
||||
logger.warning(f"文件夹 {_dir} 不存在,跳过删除", "检查更新")
|
||||
for folder in REPLACE_FOLDERS:
|
||||
src_folder_path = extract_path / folder
|
||||
dest_folder_path = target_path / folder
|
||||
if src_folder_path.exists():
|
||||
logger.debug(
|
||||
f"移动文件夹: {src_folder_path} -> {dest_folder_path}", "检查更新"
|
||||
)
|
||||
shutil.move(src_folder_path, dest_folder_path)
|
||||
else:
|
||||
logger.debug(f"源文件夹不存在: {src_folder_path}", "检查更新")
|
||||
if tf:
|
||||
tf.close()
|
||||
if download_file.exists():
|
||||
logger.debug(f"删除下载文件: {download_file}", "检查更新")
|
||||
download_file.unlink()
|
||||
if extract_path.exists():
|
||||
logger.debug(f"删除解压文件夹: {extract_path}", "检查更新")
|
||||
shutil.rmtree(extract_path)
|
||||
if TMP_PATH.exists():
|
||||
shutil.rmtree(TMP_PATH)
|
||||
if latest_version:
|
||||
with open(VERSION_FILE, "w", encoding="utf8") as f:
|
||||
f.write(f"__version__: {latest_version}")
|
||||
install_requirement()
|
||||
|
||||
|
||||
class UpdateManage:
|
||||
@classmethod
|
||||
async def check_version(cls) -> str:
|
||||
"""检查更新版本
|
||||
"""检查真寻和资源的版本"""
|
||||
bot_cur_version = cls.__get_version()
|
||||
|
||||
返回:
|
||||
str: 更新信息
|
||||
"""
|
||||
cur_version = cls.__get_version()
|
||||
data = await cls.__get_latest_data()
|
||||
if not data:
|
||||
return "检查更新获取版本失败..."
|
||||
return (
|
||||
"检测到当前版本更新\n"
|
||||
f"当前版本:{cur_version}\n"
|
||||
f"最新版本:{data.get('name')}\n"
|
||||
f"创建日期:{data.get('created_at')}\n"
|
||||
f"更新内容:\n{data.get('body')}"
|
||||
release_task = ZhenxunRepoManager.zhenxun_get_latest_releases_data()
|
||||
dev_version_task = RepoFileManager.get_file_content(
|
||||
ZhenxunRepoConfig.ZHENXUN_BOT_GITHUB_URL, "__version__"
|
||||
)
|
||||
bot_commit_date_task = cls._get_latest_commit_date(
|
||||
"HibiKier", "zhenxun_bot", "__version__"
|
||||
)
|
||||
res_commit_date_task = cls._get_latest_commit_date(
|
||||
"zhenxun-org", "zhenxun-bot-resources", "__version__"
|
||||
)
|
||||
|
||||
(
|
||||
release_data,
|
||||
dev_version_text,
|
||||
bot_commit_date,
|
||||
res_commit_date,
|
||||
) = await asyncio.gather(
|
||||
release_task,
|
||||
dev_version_task,
|
||||
bot_commit_date_task,
|
||||
res_commit_date_task,
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
if isinstance(release_data, dict):
|
||||
bot_release_version = release_data.get("name", "获取失败")
|
||||
bot_release_date = release_data.get("created_at", "").split("T")[0]
|
||||
else:
|
||||
bot_release_version = "获取失败"
|
||||
bot_release_date = "获取失败"
|
||||
logger.warning(f"获取 Bot release 信息失败: {release_data}")
|
||||
|
||||
if isinstance(dev_version_text, str):
|
||||
bot_dev_version = dev_version_text.split(":")[-1].strip()
|
||||
else:
|
||||
bot_dev_version = "获取失败"
|
||||
bot_commit_date = "获取失败"
|
||||
logger.warning(f"获取 Bot dev 版本信息失败: {dev_version_text}")
|
||||
|
||||
bot_update_hint = ""
|
||||
try:
|
||||
cur_base_v = bot_cur_version.split("-")[0].lstrip("v")
|
||||
dev_base_v = bot_dev_version.split("-")[0].lstrip("v")
|
||||
|
||||
if Version(cur_base_v) < Version(dev_base_v):
|
||||
bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新"
|
||||
elif (
|
||||
Version(cur_base_v) == Version(dev_base_v)
|
||||
and bot_cur_version != bot_dev_version
|
||||
):
|
||||
bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新"
|
||||
except (InvalidVersion, TypeError, IndexError):
|
||||
if bot_cur_version != bot_dev_version and bot_dev_version != "获取失败":
|
||||
bot_update_hint = "\n-> 发现新开发版本, 可用 `检查更新 main` 更新"
|
||||
|
||||
bot_update_info = (
|
||||
f"当前版本: {bot_cur_version}\n"
|
||||
f"最新开发版: {bot_dev_version} (更新于: {bot_commit_date})\n"
|
||||
f"最新正式版: {bot_release_version} (发布于: {bot_release_date})"
|
||||
f"{bot_update_hint}"
|
||||
)
|
||||
|
||||
res_version_file = ZhenxunRepoConfig.RESOURCE_PATH / "__version__"
|
||||
res_cur_version = "未找到"
|
||||
if res_version_file.exists():
|
||||
if text := res_version_file.open(encoding="utf8").readline():
|
||||
res_cur_version = text.split(":")[-1].strip()
|
||||
|
||||
res_latest_version = "获取失败"
|
||||
try:
|
||||
res_latest_version_text = await RepoFileManager.get_file_content(
|
||||
ZhenxunRepoConfig.RESOURCE_GITHUB_URL, "__version__"
|
||||
)
|
||||
res_latest_version = res_latest_version_text.split(":")[-1].strip()
|
||||
except Exception as e:
|
||||
res_commit_date = "获取失败"
|
||||
logger.warning(f"获取资源版本信息失败: {e}")
|
||||
|
||||
res_update_hint = ""
|
||||
try:
|
||||
if Version(res_cur_version) < Version(res_latest_version):
|
||||
res_update_hint = "\n-> 发现新资源版本, 可用 `检查更新 resource` 更新"
|
||||
except (InvalidVersion, TypeError):
|
||||
pass
|
||||
|
||||
res_update_info = (
|
||||
f"当前版本: {res_cur_version}\n"
|
||||
f"最新版本: {res_latest_version} (更新于: {res_commit_date})"
|
||||
f"{res_update_hint}"
|
||||
)
|
||||
|
||||
return f"『绪山真寻 Bot』\n{bot_update_info}\n\n『真寻资源』\n{res_update_info}"
|
||||
|
||||
@classmethod
|
||||
async def update(cls, bot: Bot, user_id: str, version_type: str) -> str:
|
||||
async def update_webui(
|
||||
cls,
|
||||
source: Literal["git", "ali"] | None,
|
||||
branch: str = "dist",
|
||||
force: bool = False,
|
||||
):
|
||||
"""更新WebUI
|
||||
|
||||
参数:
|
||||
source: 更新源
|
||||
branch: 分支
|
||||
force: 是否强制更新
|
||||
|
||||
返回:
|
||||
str: 返回消息
|
||||
"""
|
||||
if not source:
|
||||
await ZhenxunRepoManager.webui_zip_update()
|
||||
return "WebUI更新完成!"
|
||||
result = await ZhenxunRepoManager.webui_git_update(
|
||||
source,
|
||||
branch=branch,
|
||||
force=force,
|
||||
)
|
||||
if not result.success:
|
||||
logger.error(f"WebUI更新失败...错误: {result.error_message}", LOG_COMMAND)
|
||||
return f"WebUI更新失败...错误: {result.error_message}"
|
||||
return "WebUI更新完成!"
|
||||
|
||||
@classmethod
|
||||
async def update_resources(
|
||||
cls,
|
||||
source: Literal["git", "ali"] | None,
|
||||
branch: str = "main",
|
||||
force: bool = False,
|
||||
) -> str:
|
||||
"""更新资源
|
||||
|
||||
参数:
|
||||
source: 更新源
|
||||
branch: 分支
|
||||
force: 是否强制更新
|
||||
|
||||
返回:
|
||||
str: 返回消息
|
||||
"""
|
||||
if not source:
|
||||
await ZhenxunRepoManager.resources_zip_update()
|
||||
return "真寻资源更新完成!"
|
||||
result = await ZhenxunRepoManager.resources_git_update(
|
||||
source,
|
||||
branch=branch,
|
||||
force=force,
|
||||
)
|
||||
if not result.success:
|
||||
logger.error(
|
||||
f"真寻资源更新失败...错误: {result.error_message}", LOG_COMMAND
|
||||
)
|
||||
return f"真寻资源更新失败...错误: {result.error_message}"
|
||||
return "真寻资源更新完成!"
|
||||
|
||||
@classmethod
|
||||
async def update_zhenxun(
|
||||
cls,
|
||||
bot: Bot,
|
||||
user_id: str,
|
||||
version_type: Literal["main", "release"],
|
||||
force: bool,
|
||||
source: Literal["git", "ali"],
|
||||
zip: bool,
|
||||
) -> str:
|
||||
"""更新操作
|
||||
|
||||
参数:
|
||||
bot: Bot
|
||||
user_id: 用户id
|
||||
version_type: 更新版本类型
|
||||
force: 是否强制更新
|
||||
source: 更新源
|
||||
zip: 是否下载zip文件
|
||||
update_type: 更新方式
|
||||
|
||||
返回:
|
||||
str | None: 返回消息
|
||||
"""
|
||||
logger.info("开始下载真寻最新版文件....", "检查更新")
|
||||
cur_version = cls.__get_version()
|
||||
url = None
|
||||
new_version = None
|
||||
repo_info = GithubUtils.parse_github_url(DEFAULT_GITHUB_URL)
|
||||
if version_type in {"main"}:
|
||||
repo_info.branch = version_type
|
||||
new_version = await cls.__get_version_from_repo(repo_info)
|
||||
if new_version:
|
||||
new_version = new_version.split(":")[-1].strip()
|
||||
url = await repo_info.get_archive_download_urls()
|
||||
elif version_type == "release":
|
||||
data = await cls.__get_latest_data()
|
||||
if not data:
|
||||
return "获取更新版本失败..."
|
||||
new_version = data.get("name", "")
|
||||
url = await repo_info.get_release_source_download_urls_tgz(new_version)
|
||||
if not url:
|
||||
return "获取版本下载链接失败..."
|
||||
if TMP_PATH.exists():
|
||||
logger.debug(f"删除临时文件夹 {TMP_PATH}", "检查更新")
|
||||
shutil.rmtree(TMP_PATH)
|
||||
logger.debug(
|
||||
f"开始更新版本:{cur_version} -> {new_version} | 下载链接:{url}",
|
||||
"检查更新",
|
||||
)
|
||||
await PlatformUtils.send_superuser(
|
||||
bot,
|
||||
f"检测真寻已更新,版本更新:{cur_version} -> {new_version}\n开始更新...",
|
||||
f"检测真寻已更新,当前版本:{cur_version}\n开始更新...",
|
||||
user_id,
|
||||
)
|
||||
download_file = (
|
||||
DOWNLOAD_GZ_FILE if version_type == "release" else DOWNLOAD_ZIP_FILE
|
||||
)
|
||||
if await AsyncHttpx.download_file(url, download_file, stream=True):
|
||||
logger.debug("下载真寻最新版文件完成...", "检查更新")
|
||||
await _file_handle(new_version)
|
||||
result = "版本更新完成"
|
||||
return (
|
||||
f"{result}\n"
|
||||
f"版本: {cur_version} -> {new_version}\n"
|
||||
result_message = ""
|
||||
if zip:
|
||||
new_version = await ZhenxunRepoManager.zhenxun_zip_update(version_type)
|
||||
await PlatformUtils.send_superuser(
|
||||
bot, "真寻更新完成,开始安装依赖...", user_id
|
||||
)
|
||||
await VirtualEnvPackageManager.install_requirement(
|
||||
ZhenxunRepoConfig.REQUIREMENTS_FILE
|
||||
)
|
||||
result_message = (
|
||||
f"版本更新完成!\n版本: {cur_version} -> {new_version}\n"
|
||||
"请重新启动真寻以完成更新!"
|
||||
)
|
||||
else:
|
||||
logger.debug("下载真寻最新版文件失败...", "检查更新")
|
||||
return ""
|
||||
result = await ZhenxunRepoManager.zhenxun_git_update(
|
||||
source,
|
||||
branch=version_type,
|
||||
force=force,
|
||||
)
|
||||
if not result.success:
|
||||
logger.error(
|
||||
f"真寻版本更新失败...错误: {result.error_message}",
|
||||
LOG_COMMAND,
|
||||
)
|
||||
return f"版本更新失败...错误: {result.error_message}"
|
||||
await PlatformUtils.send_superuser(
|
||||
bot, "真寻更新完成,开始安装依赖...", user_id
|
||||
)
|
||||
await VirtualEnvPackageManager.install_requirement(
|
||||
ZhenxunRepoConfig.REQUIREMENTS_FILE
|
||||
)
|
||||
result_message = (
|
||||
f"版本更新完成!\n"
|
||||
f"版本: {cur_version} -> {result.new_version}\n"
|
||||
f"变更文件个数: {len(result.changed_files)}"
|
||||
f"{'' if source == 'git' else '(阿里云更新不支持查看变更文件)'}\n"
|
||||
"请重新启动真寻以完成更新!"
|
||||
)
|
||||
resource_warning = ""
|
||||
if version_type == "main":
|
||||
try:
|
||||
spec_content = await RepoFileManager.get_file_content(
|
||||
ZhenxunRepoConfig.ZHENXUN_BOT_GITHUB_URL, "resources.spec"
|
||||
)
|
||||
required_spec_str = None
|
||||
for line in spec_content.splitlines():
|
||||
if line.startswith("require_resources_version:"):
|
||||
required_spec_str = line.split(":", 1)[1].strip().strip("\"'")
|
||||
break
|
||||
if required_spec_str:
|
||||
res_version_file = ZhenxunRepoConfig.RESOURCE_PATH / "__version__"
|
||||
local_res_version_str = "0.0.0"
|
||||
if res_version_file.exists():
|
||||
if text := res_version_file.open(encoding="utf8").readline():
|
||||
local_res_version_str = text.split(":")[-1].strip()
|
||||
|
||||
spec = SpecifierSet(required_spec_str)
|
||||
local_ver = Version(local_res_version_str)
|
||||
if not spec.contains(local_ver):
|
||||
warning_header = (
|
||||
f"⚠️ **资源版本不兼容!**\n"
|
||||
f"当前代码需要资源版本: `{required_spec_str}`\n"
|
||||
f"您当前的资源版本是: `{local_res_version_str}`\n"
|
||||
"**将自动为您更新资源文件...**"
|
||||
)
|
||||
await PlatformUtils.send_superuser(bot, warning_header, user_id)
|
||||
resource_update_source = None if zip else source
|
||||
resource_update_result = await cls.update_resources(
|
||||
source=resource_update_source, force=force
|
||||
)
|
||||
resource_warning = (
|
||||
f"\n\n{warning_header}\n{resource_update_result}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"检查资源版本兼容性时出错: {e}", LOG_COMMAND, e=e)
|
||||
resource_warning = (
|
||||
"\n\n⚠️ 检查资源版本兼容性时出错,建议手动运行 `检查更新 resource`"
|
||||
)
|
||||
return result_message + resource_warning
|
||||
|
||||
@classmethod
|
||||
def __get_version(cls) -> str:
|
||||
@@ -221,44 +311,9 @@ class UpdateManage:
|
||||
str: 当前版本号
|
||||
"""
|
||||
_version = "v0.0.0"
|
||||
if VERSION_FILE.exists():
|
||||
if text := VERSION_FILE.open(encoding="utf8").readline():
|
||||
if ZhenxunRepoConfig.ZHENXUN_BOT_VERSION_FILE.exists():
|
||||
if text := ZhenxunRepoConfig.ZHENXUN_BOT_VERSION_FILE.open(
|
||||
encoding="utf8"
|
||||
).readline():
|
||||
_version = text.split(":")[-1].strip()
|
||||
return _version
|
||||
|
||||
@classmethod
|
||||
async def __get_latest_data(cls) -> dict:
|
||||
"""获取最新版本信息
|
||||
|
||||
返回:
|
||||
dict: 最新版本数据
|
||||
"""
|
||||
for _ in range(3):
|
||||
try:
|
||||
res = await AsyncHttpx.get(RELEASE_URL)
|
||||
if res.status_code == 200:
|
||||
return res.json()
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error("检查更新真寻获取版本失败", e=e)
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
async def __get_version_from_repo(cls, repo_info: RepoInfo) -> str:
|
||||
"""从指定分支获取版本号
|
||||
|
||||
参数:
|
||||
branch: 分支名称
|
||||
|
||||
返回:
|
||||
str: 版本号
|
||||
"""
|
||||
version_url = await repo_info.get_raw_download_urls(path="__version__")
|
||||
try:
|
||||
res = await AsyncHttpx.get(version_url)
|
||||
if res.status_code == 200:
|
||||
return res.text.strip()
|
||||
except Exception as e:
|
||||
logger.error(f"获取 {repo_info.branch} 分支版本失败", e=e)
|
||||
return "未知版本"
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from zhenxun.configs.path_config import TEMP_PATH
|
||||
|
||||
DEFAULT_GITHUB_URL = "https://github.com/HibiKier/zhenxun_bot/tree/main"
|
||||
RELEASE_URL = "https://api.github.com/repos/HibiKier/zhenxun_bot/releases/latest"
|
||||
|
||||
VERSION_FILE_STRING = "__version__"
|
||||
VERSION_FILE = Path() / VERSION_FILE_STRING
|
||||
|
||||
PYPROJECT_FILE_STRING = "pyproject.toml"
|
||||
PYPROJECT_FILE = Path() / PYPROJECT_FILE_STRING
|
||||
PYPROJECT_LOCK_FILE_STRING = "poetry.lock"
|
||||
PYPROJECT_LOCK_FILE = Path() / PYPROJECT_LOCK_FILE_STRING
|
||||
REQ_TXT_FILE_STRING = "requirements.txt"
|
||||
REQ_TXT_FILE = Path() / REQ_TXT_FILE_STRING
|
||||
|
||||
BASE_PATH_STRING = "zhenxun"
|
||||
BASE_PATH = Path() / BASE_PATH_STRING
|
||||
|
||||
TMP_PATH = TEMP_PATH / "auto_update"
|
||||
|
||||
BACKUP_PATH = Path() / "backup"
|
||||
|
||||
DOWNLOAD_GZ_FILE_STRING = "download_latest_file.tar.gz"
|
||||
DOWNLOAD_ZIP_FILE_STRING = "download_latest_file.zip"
|
||||
DOWNLOAD_GZ_FILE = TMP_PATH / DOWNLOAD_GZ_FILE_STRING
|
||||
DOWNLOAD_ZIP_FILE = TMP_PATH / DOWNLOAD_ZIP_FILE_STRING
|
||||
|
||||
REPLACE_FOLDERS = [
|
||||
"builtin_plugins",
|
||||
"services",
|
||||
"utils",
|
||||
"models",
|
||||
"configs",
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.manager.bot_profile_manager import BotProfileManager
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="自我介绍",
|
||||
description=f"这是{BotConfig.self_nickname}的深情告白",
|
||||
usage="""
|
||||
指令:
|
||||
自我介绍
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="其他",
|
||||
superuser_help="""
|
||||
在data/bot_profile/bot_id/profile.txt 中编辑BOT自我介绍
|
||||
在data/bot_profile/bot_id/bot_id.png 中编辑BOT头像
|
||||
指令:
|
||||
重载自我介绍
|
||||
""".strip(),
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
|
||||
_matcher = on_alconna(Alconna("自我介绍"), priority=5, block=True, rule=to_me())
|
||||
|
||||
_reload_matcher = on_alconna(
|
||||
Alconna("重载自我介绍"), priority=1, block=True, permission=SUPERUSER
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: Uninfo, arparma: Arparma):
|
||||
file_path = await BotProfileManager.build_bot_profile_image(session.self_id)
|
||||
if not file_path:
|
||||
await MessageUtils.build_message(
|
||||
f"{BotConfig.self_nickname}当前没有自我简介哦"
|
||||
).finish(reply_to=True)
|
||||
await MessageUtils.build_message(file_path).send()
|
||||
logger.info("BOT自我介绍", arparma.header_result, session=session)
|
||||
|
||||
|
||||
@_reload_matcher.handle()
|
||||
async def _(session: Uninfo, arparma: Arparma):
|
||||
BotProfileManager.clear_profile_image(session.self_id)
|
||||
await MessageUtils.build_message(f"重载{BotConfig.self_nickname}自我介绍成功").send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info("重载BOT自我介绍", arparma.header_result, session=session)
|
||||
@@ -1,13 +1,15 @@
|
||||
from nonebot import on_message
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.models.chat_history import ChatHistory
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.utils import get_entity_ids
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="消息存储",
|
||||
@@ -37,18 +39,34 @@ def rule(message: UniMsg) -> bool:
|
||||
|
||||
chat_history = on_message(rule=rule, priority=1, block=False)
|
||||
|
||||
TEMP_LIST = []
|
||||
|
||||
|
||||
@chat_history.handle()
|
||||
async def handle_message(message: UniMsg, session: EventSession):
|
||||
"""处理消息存储"""
|
||||
try:
|
||||
await ChatHistory.create(
|
||||
user_id=session.id1,
|
||||
group_id=session.id2,
|
||||
async def _(message: UniMsg, session: Uninfo):
|
||||
entity = get_entity_ids(session)
|
||||
TEMP_LIST.append(
|
||||
ChatHistory(
|
||||
user_id=entity.user_id,
|
||||
group_id=entity.group_id,
|
||||
text=str(message),
|
||||
plain_text=message.extract_plain_text(),
|
||||
bot_id=session.bot_id,
|
||||
bot_id=session.self_id,
|
||||
platform=session.platform,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@scheduler.scheduled_job(
|
||||
"interval",
|
||||
minutes=1,
|
||||
)
|
||||
async def _():
|
||||
try:
|
||||
message_list = TEMP_LIST.copy()
|
||||
TEMP_LIST.clear()
|
||||
if message_list:
|
||||
await ChatHistory.bulk_create(message_list)
|
||||
logger.debug(f"批量添加聊天记录 {len(message_list)} 条", "定时任务")
|
||||
except Exception as e:
|
||||
logger.warning("存储聊天记录失败", "chat_history", e=e)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from io import BytesIO
|
||||
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import (
|
||||
@@ -15,15 +14,17 @@ from nonebot_plugin_alconna import (
|
||||
from nonebot_plugin_session import EventSession
|
||||
import pytz
|
||||
|
||||
from zhenxun import ui
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import Command, PluginExtraData, RegisterConfig
|
||||
from zhenxun.models.chat_history import ChatHistory
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
from zhenxun.services import avatar_service
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.ui.builders import TableBuilder
|
||||
from zhenxun.ui.models import ImageCell, TextCell
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="消息统计",
|
||||
@@ -123,64 +124,63 @@ async def _(
|
||||
if rank_data := await ChatHistory.get_group_msg_rank(
|
||||
group_id, fetch_count, "DES" if arparma.find("des") else "DESC", date_scope
|
||||
):
|
||||
idx = 1
|
||||
data_list = []
|
||||
rows_data = []
|
||||
platform = "qq"
|
||||
|
||||
for uid, num in rank_data:
|
||||
if len(data_list) >= count.result:
|
||||
user_ids_in_rank = [str(uid) for uid, _ in rank_data]
|
||||
users_in_group_query = GroupInfoUser.filter(
|
||||
user_id__in=user_ids_in_rank, group_id=group_id
|
||||
)
|
||||
users_in_group = {u.user_id: u for u in await users_in_group_query}
|
||||
|
||||
for idx, (uid, num) in enumerate(rank_data):
|
||||
if len(rows_data) >= count.result:
|
||||
break
|
||||
|
||||
user_in_group = await GroupInfoUser.filter(
|
||||
user_id=uid, group_id=group_id
|
||||
).first()
|
||||
uid_str = str(uid)
|
||||
user_in_group = users_in_group.get(uid_str)
|
||||
|
||||
if not user_in_group and not show_quit_member:
|
||||
continue
|
||||
|
||||
if user_in_group:
|
||||
user_name = user_in_group.user_name
|
||||
else:
|
||||
user_name = f"{uid}(已退群)"
|
||||
user_name = (
|
||||
user_in_group.user_name if user_in_group else f"{uid_str}(已退群)"
|
||||
)
|
||||
|
||||
avatar_size = 40
|
||||
try:
|
||||
avatar_bytes = await PlatformUtils.get_user_avatar(str(uid), "qq")
|
||||
if avatar_bytes:
|
||||
avatar_img = BuildImage(
|
||||
avatar_size, avatar_size, background=BytesIO(avatar_bytes)
|
||||
)
|
||||
await avatar_img.circle()
|
||||
avatar_tuple = (avatar_img, avatar_size, avatar_size)
|
||||
else:
|
||||
avatar_img = BuildImage(avatar_size, avatar_size, color="#CCCCCC")
|
||||
await avatar_img.circle()
|
||||
avatar_tuple = (avatar_img, avatar_size, avatar_size)
|
||||
except Exception as e:
|
||||
logger.warning(f"获取用户头像失败: {e}", "chat_history")
|
||||
avatar_img = BuildImage(avatar_size, avatar_size, color="#CCCCCC")
|
||||
await avatar_img.circle()
|
||||
avatar_tuple = (avatar_img, avatar_size, avatar_size)
|
||||
avatar_path = await avatar_service.get_avatar_path(platform, uid_str)
|
||||
|
||||
data_list.append([idx, avatar_tuple, user_name, num])
|
||||
idx += 1
|
||||
rows_data.append(
|
||||
[
|
||||
TextCell(content=str(len(rows_data) + 1)),
|
||||
ImageCell(
|
||||
src=avatar_path.as_uri() if avatar_path else "", shape="circle"
|
||||
),
|
||||
TextCell(content=user_name),
|
||||
TextCell(content=str(num), bold=True),
|
||||
]
|
||||
)
|
||||
if not date_scope:
|
||||
if date_scope := await ChatHistory.get_group_first_msg_datetime(group_id):
|
||||
date_scope = date_scope.astimezone(
|
||||
first_msg_time = await ChatHistory.get_group_first_msg_datetime(group_id)
|
||||
if first_msg_time:
|
||||
date_scope_start = first_msg_time.astimezone(
|
||||
pytz.timezone("Asia/Shanghai")
|
||||
).replace(microsecond=0)
|
||||
date_str = f"{str(date_scope_start).split('+')[0]} - 至今"
|
||||
else:
|
||||
date_scope = time_now.replace(microsecond=0)
|
||||
date_str = f"{str(date_scope).split('+')[0]} - 至今"
|
||||
date_str = f"{time_now.replace(microsecond=0)} - 至今"
|
||||
else:
|
||||
date_str = (
|
||||
f"{date_scope[0].replace(microsecond=0)} - "
|
||||
f"{date_scope[1].replace(microsecond=0)}"
|
||||
)
|
||||
A = await ImageTemplate.table_page(
|
||||
f"消息排行({count.result})", date_str, column_name, data_list
|
||||
)
|
||||
|
||||
builder = TableBuilder(f"消息排行({count.result})", date_str)
|
||||
builder.set_headers(column_name).add_rows(rows_data)
|
||||
|
||||
image_bytes = await ui.render(builder.build())
|
||||
|
||||
logger.info(
|
||||
f"查看消息排行 数量={count.result}", arparma.header_result, session=session
|
||||
)
|
||||
await MessageUtils.build_message(A).finish(reply_to=True)
|
||||
await MessageUtils.build_message(image_bytes).finish(reply_to=True)
|
||||
await MessageUtils.build_message("群组消息记录为空...").finish()
|
||||
|
||||
@@ -4,10 +4,9 @@ from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import Rule, to_me
|
||||
from nonebot_plugin_alconna import Alconna, on_alconna
|
||||
from nonebot_plugin_htmlrender import template_to_pic
|
||||
|
||||
from zhenxun import ui
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import TEMPLATE_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
@@ -27,7 +26,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
version="0.2",
|
||||
plugin_type=PluginType.SUPERUSER,
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
@@ -67,18 +66,14 @@ _self_check_poke_matcher = on_notice(
|
||||
|
||||
async def handle_self_check():
|
||||
try:
|
||||
data = await get_status_info()
|
||||
image = await template_to_pic(
|
||||
template_path=str((TEMPLATE_PATH / "check").absolute()),
|
||||
template_name="main.html",
|
||||
templates={"data": data},
|
||||
pages={
|
||||
"viewport": {"width": 195, "height": 750},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
data_dict = await get_status_info()
|
||||
|
||||
image_bytes = await ui.render_template(
|
||||
"pages/builtin/check",
|
||||
data=data_dict,
|
||||
)
|
||||
await MessageUtils.build_message(image).send()
|
||||
|
||||
await MessageUtils.build_message(image_bytes).send()
|
||||
logger.info("自检成功", "自检")
|
||||
except Exception as e:
|
||||
await MessageUtils.build_message(f"自检失败: {e}").send()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -18,7 +19,47 @@ BAIDU_URL = "https://www.baidu.com/"
|
||||
GOOGLE_URL = "https://www.google.com/"
|
||||
|
||||
VERSION_FILE = Path() / "__version__"
|
||||
ARM_KEY = "aarch64"
|
||||
|
||||
|
||||
def get_arm_cpu_freq_safe():
|
||||
"""获取ARM设备CPU频率"""
|
||||
# 方法1: 优先从系统频率文件读取
|
||||
freq_files = [
|
||||
"/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq",
|
||||
"/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq",
|
||||
"/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq",
|
||||
"/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq",
|
||||
]
|
||||
|
||||
for freq_file in freq_files:
|
||||
try:
|
||||
with open(freq_file) as f:
|
||||
frequency = int(f.read().strip())
|
||||
return round(frequency / 1000000, 2) # 转换为GHz
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
|
||||
# 方法2: 解析/proc/cpuinfo
|
||||
with contextlib.suppress(OSError, FileNotFoundError, ValueError, PermissionError):
|
||||
with open("/proc/cpuinfo") as f:
|
||||
for line in f:
|
||||
if "CPU MHz" in line:
|
||||
freq = float(line.split(":")[1].strip())
|
||||
return round(freq / 1000, 2) # 转换为GHz
|
||||
# 方法3: 使用lscpu命令
|
||||
with contextlib.suppress(OSError, subprocess.SubprocessError, ValueError):
|
||||
env = os.environ.copy()
|
||||
env["LC_ALL"] = "C"
|
||||
result = subprocess.run(
|
||||
["lscpu"], capture_output=True, text=True, env=env, timeout=10
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.split("\n"):
|
||||
if "CPU max MHz" in line or "CPU MHz" in line:
|
||||
freq = float(line.split(":")[1].strip())
|
||||
return round(freq / 1000, 2) # 转换为GHz
|
||||
return 0 # 如果所有方法都失败,返回0
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -37,7 +78,7 @@ class CPUInfo:
|
||||
if _cpu_freq := psutil.cpu_freq():
|
||||
cpu_freq = round(_cpu_freq.current / 1000, 2)
|
||||
else:
|
||||
cpu_freq = 0
|
||||
cpu_freq = get_arm_cpu_freq_safe()
|
||||
return CPUInfo(core=cpu_core, usage=cpu_usage, freq=cpu_freq)
|
||||
|
||||
|
||||
@@ -160,44 +201,13 @@ def __get_version() -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def __get_arm_cpu():
|
||||
env = os.environ.copy()
|
||||
env["LC_ALL"] = "en_US.UTF-8"
|
||||
cpu_info = subprocess.check_output(["lscpu"], env=env).decode()
|
||||
model_name = ""
|
||||
cpu_freq = 0
|
||||
for line in cpu_info.splitlines():
|
||||
if "Model name" in line:
|
||||
model_name = line.split(":")[1].strip()
|
||||
if "CPU MHz" in line:
|
||||
cpu_freq = float(line.split(":")[1].strip())
|
||||
return model_name, cpu_freq
|
||||
|
||||
|
||||
def __get_arm_oracle_cpu_freq():
|
||||
cpu_freq = subprocess.check_output(
|
||||
["dmidecode", "-s", "processor-frequency"]
|
||||
).decode()
|
||||
return round(float(cpu_freq.split()[0]) / 1000, 2)
|
||||
|
||||
|
||||
async def get_status_info() -> dict:
|
||||
"""获取信息"""
|
||||
data = await __build_status()
|
||||
|
||||
system = platform.uname()
|
||||
if system.machine == ARM_KEY and not (
|
||||
cpuinfo.get_cpu_info().get("brand_raw") and data.cpu.freq
|
||||
):
|
||||
model_name, cpu_freq = __get_arm_cpu()
|
||||
if not data.cpu.freq:
|
||||
data.cpu.freq = cpu_freq or __get_arm_oracle_cpu_freq()
|
||||
data = data.get_system_info()
|
||||
data["brand_raw"] = model_name
|
||||
else:
|
||||
data = data.get_system_info()
|
||||
data["brand_raw"] = cpuinfo.get_cpu_info().get("brand_raw", "Unknown")
|
||||
|
||||
data = data.get_system_info()
|
||||
data["brand_raw"] = cpuinfo.get_cpu_info().get("brand_raw", "Unknown")
|
||||
baidu, google = await __get_network_info()
|
||||
data["baidu"] = "#8CC265" if baidu else "red"
|
||||
data["google"] = "#8CC265" if google else "red"
|
||||
|
||||
@@ -13,17 +13,13 @@ from nonebot_plugin_alconna import (
|
||||
)
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.builtin_plugins.help._config import (
|
||||
GROUP_HELP_PATH,
|
||||
SIMPLE_DETAIL_HELP_IMAGE,
|
||||
SIMPLE_HELP_IMAGE,
|
||||
)
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import create_help_img, get_plugin_help
|
||||
from ._data_source import create_help_img, get_llm_help, get_plugin_help
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="帮助",
|
||||
@@ -36,11 +32,40 @@ __plugin_meta__ = PluginMetadata(
|
||||
is_show=False,
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="type",
|
||||
value="zhenxun",
|
||||
help="帮助图片样式 [normal, HTML, zhenxun]",
|
||||
default_value="zhenxun",
|
||||
)
|
||||
key="ENABLE_LLM_HELPER",
|
||||
value=False,
|
||||
help="是否开启LLM智能帮助功能",
|
||||
default_value=False,
|
||||
type=bool,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="DEFAULT_LLM_MODEL",
|
||||
value="Gemini/gemini-2.5-flash-lite-preview-06-17",
|
||||
help="智能帮助功能使用的默认LLM模型",
|
||||
default_value="Gemini/gemini-2.5-flash-lite-preview-06-17",
|
||||
type=str,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="LLM_HELPER_STYLE",
|
||||
value="绪山真寻",
|
||||
help="设置智能帮助功能的回复口吻或风格",
|
||||
default_value="绪山真寻",
|
||||
type=str,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="LLM_HELPER_REPLY_AS_IMAGE_THRESHOLD",
|
||||
value=100,
|
||||
help="AI帮助回复超过多少字时转为图片发送",
|
||||
default_value=100,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="HELP_STYLE",
|
||||
value="default",
|
||||
help="帮助页面的显示样式 (可选值: 'default', 'simple')",
|
||||
default_value="default",
|
||||
type=str,
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
)
|
||||
@@ -77,26 +102,43 @@ async def _(
|
||||
is_detail: Query[bool] = AlconnaQuery("detail.value", False),
|
||||
):
|
||||
_is_superuser = is_superuser.result if is_superuser.available else False
|
||||
|
||||
if _is_superuser and session.user.id not in bot.config.superusers:
|
||||
await MessageUtils.build_message("权限不足,无法查看超级用户帮助").finish(
|
||||
reply_to=True
|
||||
)
|
||||
|
||||
if name.available:
|
||||
if _is_superuser and session.user.id not in bot.config.superusers:
|
||||
_is_superuser = False
|
||||
if result := await get_plugin_help(session.user.id, name.result, _is_superuser):
|
||||
await MessageUtils.build_message(result).send(reply_to=True)
|
||||
else:
|
||||
await MessageUtils.build_message("没有此功能的帮助信息...").send(
|
||||
help_style = Config.get_config("help", "HELP_STYLE")
|
||||
variant = help_style if help_style != "default" else None
|
||||
|
||||
traditional_help_result = await get_plugin_help(
|
||||
session.user.id, name.result, _is_superuser, variant=variant
|
||||
)
|
||||
|
||||
is_plugin_found = not (
|
||||
isinstance(traditional_help_result, str)
|
||||
and "没有查找到这个功能噢..." in traditional_help_result
|
||||
)
|
||||
if is_plugin_found:
|
||||
await MessageUtils.build_message(traditional_help_result).send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info(f"查看帮助详情: {name.result}", "帮助", session=session)
|
||||
elif session.group and (gid := session.group.id):
|
||||
_image_path = GROUP_HELP_PATH / f"{gid}_{is_detail.result}.png"
|
||||
if not _image_path.exists():
|
||||
result = await create_help_img(session, gid, is_detail.result)
|
||||
await MessageUtils.build_message(_image_path).finish()
|
||||
else:
|
||||
if is_detail.result:
|
||||
_image_path = SIMPLE_DETAIL_HELP_IMAGE
|
||||
logger.info(f"查看帮助详情: {name.result}", "帮助", session=session)
|
||||
elif Config.get_config("help", "ENABLE_LLM_HELPER"):
|
||||
logger.info(f"智能帮助处理问题: {name.result}", "帮助", session=session)
|
||||
llm_answer = await get_llm_help(name.result, session.user.id)
|
||||
await MessageUtils.build_message(llm_answer).send(reply_to=True)
|
||||
else:
|
||||
_image_path = SIMPLE_HELP_IMAGE
|
||||
if not _image_path.exists():
|
||||
result = await create_help_img(session, None, is_detail.result)
|
||||
await MessageUtils.build_message(_image_path).finish()
|
||||
await MessageUtils.build_message(traditional_help_result).send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info(
|
||||
f"查看帮助详情失败,未找到: {name.result}", "帮助", session=session
|
||||
)
|
||||
elif session.group and (gid := session.group.id):
|
||||
image_bytes = await create_help_img(session, gid, is_detail.result)
|
||||
await MessageUtils.build_message(image_bytes).finish()
|
||||
else:
|
||||
image_bytes = await create_help_img(session, None, is_detail.result)
|
||||
await MessageUtils.build_message(image_bytes).finish()
|
||||
|
||||
@@ -1,62 +1,126 @@
|
||||
from pathlib import Path
|
||||
|
||||
import nonebot
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun import ui
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
|
||||
from ._config import (
|
||||
GROUP_HELP_PATH,
|
||||
SIMPLE_DETAIL_HELP_IMAGE,
|
||||
SIMPLE_HELP_IMAGE,
|
||||
base_config,
|
||||
from zhenxun.models.statistics import Statistics
|
||||
from zhenxun.services import (
|
||||
LLMException,
|
||||
LLMMessage,
|
||||
avatar_service,
|
||||
generate,
|
||||
)
|
||||
from .html_help import build_html_image
|
||||
from .normal_help import build_normal_image
|
||||
from .zhenxun_help import build_zhenxun_image
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.ui.builders import (
|
||||
NotebookBuilder,
|
||||
PluginMenuBuilder,
|
||||
)
|
||||
from zhenxun.ui.models import PluginMenuCategory
|
||||
from zhenxun.utils.common_utils import format_usage_for_markdown
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from ._utils import classify_plugin
|
||||
|
||||
random_bk_path = IMAGE_PATH / "background" / "help" / "simple_help"
|
||||
|
||||
background = IMAGE_PATH / "background" / "0.png"
|
||||
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
|
||||
def _create_plugin_menu_item(
|
||||
bot: BotConsole | None,
|
||||
plugin: PluginInfo,
|
||||
group: GroupConsole | None,
|
||||
is_detail: bool,
|
||||
) -> dict:
|
||||
"""为插件菜单构造一个插件菜单项数据字典"""
|
||||
status = True
|
||||
has_superuser_help = False
|
||||
nb_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
|
||||
if nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
|
||||
extra_data = PluginExtraData(**nb_plugin.metadata.extra)
|
||||
if extra_data.superuser_help:
|
||||
has_superuser_help = True
|
||||
|
||||
if not plugin.status:
|
||||
if plugin.block_type == BlockType.ALL:
|
||||
status = False
|
||||
elif group and plugin.block_type == BlockType.GROUP:
|
||||
status = False
|
||||
elif not group and plugin.block_type == BlockType.PRIVATE:
|
||||
status = False
|
||||
elif group and f"{plugin.module}," in group.block_plugin:
|
||||
status = False
|
||||
elif bot and f"{plugin.module}," in bot.block_plugins:
|
||||
status = False
|
||||
|
||||
commands = []
|
||||
if is_detail and nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
|
||||
extra_data = PluginExtraData(**nb_plugin.metadata.extra)
|
||||
commands = [cmd.command for cmd in extra_data.commands]
|
||||
|
||||
return {
|
||||
"id": str(plugin.id),
|
||||
"name": plugin.name,
|
||||
"status": status,
|
||||
"has_superuser_help": has_superuser_help,
|
||||
"commands": commands,
|
||||
}
|
||||
|
||||
|
||||
async def create_help_img(
|
||||
session: Uninfo, group_id: str | None, is_detail: bool
|
||||
) -> Path:
|
||||
"""生成帮助图片
|
||||
) -> bytes:
|
||||
"""使用渲染服务生成帮助图片"""
|
||||
classified_data = await classify_plugin(
|
||||
session, group_id, is_detail, _create_plugin_menu_item
|
||||
)
|
||||
|
||||
参数:
|
||||
session: Uninfo
|
||||
group_id: 群号
|
||||
"""
|
||||
help_type = base_config.get("type", "").strip().lower()
|
||||
sorted_categories = dict(
|
||||
sorted(classified_data.items(), key=lambda x: len(x[1]), reverse=True)
|
||||
)
|
||||
categories_for_model = []
|
||||
plugin_count = 0
|
||||
active_count = 0
|
||||
|
||||
match help_type:
|
||||
case "html":
|
||||
result = BuildImage.open(
|
||||
await build_html_image(session, group_id, is_detail)
|
||||
)
|
||||
case "zhenxun":
|
||||
result = BuildImage.open(
|
||||
await build_zhenxun_image(session, group_id, is_detail)
|
||||
)
|
||||
case _:
|
||||
result = await build_normal_image(group_id, is_detail)
|
||||
if group_id:
|
||||
save_path = GROUP_HELP_PATH / f"{group_id}_{is_detail}.png"
|
||||
elif is_detail:
|
||||
save_path = SIMPLE_DETAIL_HELP_IMAGE
|
||||
else:
|
||||
save_path = SIMPLE_HELP_IMAGE
|
||||
await result.save(save_path)
|
||||
return save_path
|
||||
if sorted_categories:
|
||||
menu_key = next(iter(sorted_categories.keys()))
|
||||
max_data = sorted_categories.pop(menu_key)
|
||||
main_category_name = "主要功能" if menu_key in ["normal", "功能"] else menu_key
|
||||
categories_for_model.append({"name": main_category_name, "items": max_data})
|
||||
plugin_count += len(max_data)
|
||||
active_count += sum(1 for item in max_data if item["status"])
|
||||
|
||||
for menu, value in sorted_categories.items():
|
||||
category_name = "主要功能" if menu in ["normal", "功能"] else menu
|
||||
categories_for_model.append({"name": category_name, "items": value})
|
||||
plugin_count += len(value)
|
||||
active_count += sum(1 for item in value if item["status"])
|
||||
|
||||
platform = PlatformUtils.get_platform(session)
|
||||
bot_id = BotConfig.get_qbot_uid(session.self_id) or session.self_id
|
||||
bot_avatar_path = await avatar_service.get_avatar_path(platform, bot_id)
|
||||
bot_avatar_url = bot_avatar_path.as_uri() if bot_avatar_path else ""
|
||||
|
||||
builder = PluginMenuBuilder(
|
||||
bot_name=BotConfig.self_nickname,
|
||||
bot_avatar_url=bot_avatar_url,
|
||||
is_detail=is_detail,
|
||||
)
|
||||
|
||||
for category in categories_for_model:
|
||||
builder.add_category(
|
||||
PluginMenuCategory(name=category["name"], items=category["items"])
|
||||
)
|
||||
|
||||
return await ui.render(builder.build())
|
||||
|
||||
|
||||
async def get_user_allow_help(user_id: str) -> list[PluginType]:
|
||||
@@ -80,15 +144,36 @@ async def get_user_allow_help(user_id: str) -> list[PluginType]:
|
||||
return type_list
|
||||
|
||||
|
||||
def min_leading_spaces(str_list: list[str]) -> int:
|
||||
min_spaces = 9999
|
||||
|
||||
for s in str_list:
|
||||
leading_spaces = len(s) - len(s.lstrip(" "))
|
||||
|
||||
if leading_spaces < min_spaces:
|
||||
min_spaces = leading_spaces
|
||||
|
||||
return min_spaces if min_spaces != 9999 else 0
|
||||
|
||||
|
||||
def split_text(text: str):
|
||||
split_text = text.split("\n")
|
||||
min_spaces = min_leading_spaces(split_text)
|
||||
if min_spaces > 0:
|
||||
split_text = [s[min_spaces:] for s in split_text]
|
||||
return [s.replace(" ", " ") for s in split_text]
|
||||
|
||||
|
||||
async def get_plugin_help(
|
||||
user_id: str, name: str, is_superuser: bool
|
||||
) -> str | BuildImage:
|
||||
user_id: str, name: str, is_superuser: bool, variant: str | None = None
|
||||
) -> str | bytes:
|
||||
"""获取功能的帮助信息
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
name: 插件名称或id
|
||||
is_superuser: 是否为超级用户
|
||||
variant: 使用的皮肤/变体名称
|
||||
"""
|
||||
type_list = await get_user_allow_help(user_id)
|
||||
if name.isdigit():
|
||||
@@ -97,23 +182,132 @@ async def get_plugin_help(
|
||||
plugin = await PluginInfo.get_or_none(
|
||||
name__iexact=name, load_status=True, plugin_type__in=type_list
|
||||
)
|
||||
|
||||
if plugin:
|
||||
_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
|
||||
if _plugin and _plugin.metadata:
|
||||
items = None
|
||||
extra_data = PluginExtraData(**_plugin.metadata.extra)
|
||||
|
||||
call_count = await Statistics.filter(plugin_name=plugin.module).count()
|
||||
usage = _plugin.metadata.usage
|
||||
if is_superuser:
|
||||
extra = _plugin.metadata.extra
|
||||
if usage := extra.get("superuser_help"):
|
||||
items = {
|
||||
"简介": _plugin.metadata.description,
|
||||
"用法": usage,
|
||||
}
|
||||
else:
|
||||
items = {
|
||||
"简介": _plugin.metadata.description,
|
||||
"用法": _plugin.metadata.usage,
|
||||
}
|
||||
if items:
|
||||
return await ImageTemplate.hl_page(plugin.name, items)
|
||||
if not extra_data.superuser_help:
|
||||
return "该功能没有超级用户帮助信息"
|
||||
usage = extra_data.superuser_help
|
||||
|
||||
metadata_items = [
|
||||
{"label": "作者", "value": extra_data.author or "未知"},
|
||||
{"label": "版本", "value": extra_data.version or "未知"},
|
||||
{"label": "调用次数", "value": call_count},
|
||||
]
|
||||
|
||||
processed_description = format_usage_for_markdown(
|
||||
_plugin.metadata.description.strip()
|
||||
)
|
||||
processed_usage = format_usage_for_markdown(usage.strip())
|
||||
|
||||
sections = [
|
||||
{"title": "简介", "content": [processed_description]},
|
||||
{"title": "使用方法", "content": [processed_usage]},
|
||||
]
|
||||
|
||||
page_data = {
|
||||
"title": _plugin.metadata.name,
|
||||
"metadata": metadata_items,
|
||||
"sections": sections,
|
||||
}
|
||||
|
||||
component = ui.template("pages/builtin/help", data=page_data)
|
||||
if variant:
|
||||
component.variant = variant
|
||||
return await ui.render(component, use_cache=True, device_scale_factor=2)
|
||||
return "糟糕! 该功能没有帮助喔..."
|
||||
return "没有查找到这个功能噢..."
|
||||
|
||||
|
||||
async def get_llm_help(question: str, user_id: str) -> str | bytes:
|
||||
"""
|
||||
使用LLM来回答用户的自然语言求助。
|
||||
|
||||
参数:
|
||||
question: 用户的问题。
|
||||
user_id: 提问用户的ID。
|
||||
|
||||
返回:
|
||||
str | bytes: LLM生成的回答或错误提示。
|
||||
"""
|
||||
|
||||
try:
|
||||
allowed_types = await get_user_allow_help(user_id)
|
||||
|
||||
plugins = await PluginInfo.filter(
|
||||
is_show=True, plugin_type__in=allowed_types
|
||||
).all()
|
||||
|
||||
knowledge_base_parts = []
|
||||
for p in plugins:
|
||||
meta = nonebot.get_plugin_by_module_name(p.module_path)
|
||||
if not meta or not meta.metadata:
|
||||
continue
|
||||
usage = meta.metadata.usage.strip() or "无"
|
||||
desc = meta.metadata.description.strip() or "无"
|
||||
part = f"功能名称: {p.name}\n功能描述: {desc}\n用法示例:\n{usage}"
|
||||
knowledge_base_parts.append(part)
|
||||
|
||||
if not knowledge_base_parts:
|
||||
return "抱歉,根据您的权限,当前没有可供查询的功能信息。"
|
||||
|
||||
knowledge_base = "\n\n---\n\n".join(knowledge_base_parts)
|
||||
|
||||
user_role = "普通用户"
|
||||
if PluginType.SUPERUSER in allowed_types:
|
||||
user_role = "超级管理员"
|
||||
elif PluginType.ADMIN in allowed_types:
|
||||
user_role = "管理员"
|
||||
|
||||
base_system_prompt = (
|
||||
f"你是一个精通机器人功能的AI助手。当前向你提问的用户是一位「{user_role}」。\n"
|
||||
"你的任务是根据下面提供的功能列表和详细说明,来回答用户关于如何使用机器人的问题。\n"
|
||||
"请仔细阅读每个功能的描述和用法,然后用简洁、清晰的语言告诉用户应该使用哪个或哪些命令来解决他们的问题。\n"
|
||||
"如果找不到完全匹配的功能,可以推荐最相关的一个或几个。直接给出操作指令和简要解释即可。"
|
||||
)
|
||||
|
||||
if (
|
||||
Config.get_config("help", "LLM_HELPER_STYLE")
|
||||
and Config.get_config("help", "LLM_HELPER_STYLE").strip()
|
||||
):
|
||||
style = Config.get_config("help", "LLM_HELPER_STYLE")
|
||||
style_instruction = f"请务必使用「{style}」的风格和口吻来回答。"
|
||||
system_prompt = f"{base_system_prompt}\n{style_instruction}"
|
||||
else:
|
||||
system_prompt = base_system_prompt
|
||||
|
||||
full_instruction = (
|
||||
f"{system_prompt}\n\n=== 功能列表和说明 ===\n{knowledge_base}"
|
||||
)
|
||||
|
||||
messages = [
|
||||
LLMMessage.system(full_instruction),
|
||||
LLMMessage.user(question),
|
||||
]
|
||||
response = await generate(
|
||||
messages=messages,
|
||||
model=Config.get_config("help", "DEFAULT_LLM_MODEL"),
|
||||
)
|
||||
|
||||
reply_text = response.text if response else "抱歉,我暂时无法回答这个问题。"
|
||||
threshold = Config.get_config("help", "LLM_HELPER_REPLY_AS_IMAGE_THRESHOLD", 50)
|
||||
|
||||
if len(reply_text) > threshold:
|
||||
builder = NotebookBuilder()
|
||||
builder.text(reply_text)
|
||||
return await ui.render(builder.build())
|
||||
|
||||
return reply_text
|
||||
|
||||
except LLMException as e:
|
||||
logger.error(f"LLM智能帮助出错: {e}", "帮助", e=e)
|
||||
return "抱歉,智能帮助功能当前不可用,请稍后再试或联系管理员。"
|
||||
except Exception as e:
|
||||
logger.error(f"构建LLM帮助时发生未知错误: {e}", "帮助", e=e)
|
||||
return "抱歉,智能帮助功能遇到了一点小问题,正在紧急处理中!"
|
||||
|
||||
@@ -45,11 +45,13 @@ async def classify_plugin(
|
||||
"""
|
||||
sort_data = await sort_type()
|
||||
classify: dict[str, list] = {}
|
||||
group = await GroupConsole.get_or_none(group_id=group_id) if group_id else None
|
||||
group = await GroupConsole.get_group(group_id=group_id) if group_id else None
|
||||
bot = await BotConsole.get_or_none(bot_id=session.self_id)
|
||||
for menu, value in sort_data.items():
|
||||
for plugin in value:
|
||||
if not classify.get(menu):
|
||||
classify[menu] = []
|
||||
classify[menu].append(handle(bot, plugin, group, is_detail))
|
||||
for value in classify.values():
|
||||
value.sort(key=lambda x: int(x["id"]))
|
||||
return classify
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import os
|
||||
import random
|
||||
|
||||
from nonebot_plugin_htmlrender import template_to_pic
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.configs.path_config import TEMPLATE_PATH
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.utils.enum import BlockType
|
||||
|
||||
from ._utils import classify_plugin
|
||||
|
||||
LOGO_PATH = TEMPLATE_PATH / "menu" / "res" / "logo"
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
plugin_name: str
|
||||
"""插件名称"""
|
||||
sta: int
|
||||
"""插件状态"""
|
||||
|
||||
|
||||
class PluginList(BaseModel):
|
||||
plugin_type: str
|
||||
"""菜单名称"""
|
||||
icon: str
|
||||
"""图标"""
|
||||
logo: str
|
||||
"""logo"""
|
||||
items: list[Item]
|
||||
"""插件列表"""
|
||||
|
||||
|
||||
ICON2STR = {
|
||||
"normal": "fa fa-cog",
|
||||
"原神相关": "fa fa-circle-o",
|
||||
"常规插件": "fa fa-cubes",
|
||||
"联系管理员": "fa fa-envelope-o",
|
||||
"抽卡相关": "fa fa-credit-card-alt",
|
||||
"来点好康的": "fa fa-picture-o",
|
||||
"数据统计": "fa fa-bar-chart",
|
||||
"一些工具": "fa fa-shopping-cart",
|
||||
"商店": "fa fa-shopping-cart",
|
||||
"其它": "fa fa-tags",
|
||||
"群内小游戏": "fa fa-gamepad",
|
||||
}
|
||||
|
||||
|
||||
def __handle_item(
|
||||
bot: BotConsole, plugin: PluginInfo, group: GroupConsole | None, is_detail: bool
|
||||
) -> Item:
|
||||
"""构造Item
|
||||
|
||||
参数:
|
||||
bot: BotConsole
|
||||
plugin: PluginInfo
|
||||
group: 群组
|
||||
is_detail: 是否详细
|
||||
|
||||
返回:
|
||||
Item: Item
|
||||
"""
|
||||
sta = 0
|
||||
if not plugin.status:
|
||||
if group and plugin.block_type in [
|
||||
BlockType.ALL,
|
||||
BlockType.GROUP,
|
||||
]:
|
||||
sta = 2
|
||||
if not group and plugin.block_type in [
|
||||
BlockType.ALL,
|
||||
BlockType.PRIVATE,
|
||||
]:
|
||||
sta = 2
|
||||
if group:
|
||||
if f"{plugin.module}," in group.superuser_block_plugin:
|
||||
sta = 2
|
||||
if f"{plugin.module}," in group.block_plugin:
|
||||
sta = 1
|
||||
if bot:
|
||||
if f"{plugin.module}," in bot.block_plugins:
|
||||
sta = 2
|
||||
return Item(plugin_name=plugin.name, sta=sta)
|
||||
|
||||
|
||||
def build_plugin_data(classify: dict[str, list[Item]]) -> list[dict[str, str]]:
|
||||
"""构建前端插件数据
|
||||
|
||||
参数:
|
||||
classify: 插件数据
|
||||
|
||||
返回:
|
||||
list[dict[str, str]]: 前端插件数据
|
||||
"""
|
||||
lengths = [len(classify[c]) for c in classify]
|
||||
index = lengths.index(max(lengths))
|
||||
menu_key = list(classify.keys())[index]
|
||||
max_data = classify[menu_key]
|
||||
del classify[menu_key]
|
||||
plugin_list = []
|
||||
for menu_type in classify:
|
||||
icon = "fa fa-pencil-square-o"
|
||||
if menu_type in ICON2STR.keys():
|
||||
icon = ICON2STR[menu_type]
|
||||
logo = LOGO_PATH / random.choice(os.listdir(LOGO_PATH))
|
||||
data = {
|
||||
"name": menu_type if menu_type != "normal" else "功能",
|
||||
"items": classify[menu_type],
|
||||
"icon": icon,
|
||||
"logo": str(logo.absolute()),
|
||||
}
|
||||
plugin_list.append(data)
|
||||
plugin_list.insert(
|
||||
0,
|
||||
{
|
||||
"name": menu_key if menu_key != "normal" else "功能",
|
||||
"items": max_data,
|
||||
"icon": "fa fa-pencil-square-o",
|
||||
"logo": str((LOGO_PATH / random.choice(os.listdir(LOGO_PATH))).absolute()),
|
||||
},
|
||||
)
|
||||
return plugin_list
|
||||
|
||||
|
||||
async def build_html_image(
|
||||
session: Uninfo, group_id: str | None, is_detail: bool
|
||||
) -> bytes:
|
||||
"""构造HTML帮助图片
|
||||
|
||||
参数:
|
||||
session: Uninfo
|
||||
group_id: 群号
|
||||
is_detail: 是否详细帮助
|
||||
"""
|
||||
classify = await classify_plugin(session, group_id, is_detail, __handle_item)
|
||||
plugin_list = build_plugin_data(classify)
|
||||
return await template_to_pic(
|
||||
template_path=str((TEMPLATE_PATH / "menu").absolute()),
|
||||
template_name="zhenxun_menu.html",
|
||||
templates={"plugin_list": plugin_list},
|
||||
pages={
|
||||
"viewport": {"width": 1903, "height": 975},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
@@ -1,100 +0,0 @@
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.utils._build_image import BuildImage
|
||||
from zhenxun.utils.enum import BlockType
|
||||
from zhenxun.utils.image_utils import build_sort_image, group_image
|
||||
|
||||
from ._utils import sort_type
|
||||
|
||||
BACKGROUND_PATH = IMAGE_PATH / "background" / "help" / "simple_help"
|
||||
|
||||
|
||||
async def build_normal_image(group_id: str | None, is_detail: bool) -> BuildImage:
|
||||
"""构造PIL帮助图片
|
||||
|
||||
参数:
|
||||
group_id: 群号
|
||||
is_detail: 详细帮助
|
||||
"""
|
||||
image_list = []
|
||||
font_size = 24
|
||||
font = BuildImage.load_font("HYWenHei-85W.ttf", 20)
|
||||
sort_data = await sort_type()
|
||||
for idx, menu_type in enumerate(sort_data):
|
||||
plugin_list = sort_data[menu_type]
|
||||
"""拿到最大宽度和结算高度"""
|
||||
wh_list = [
|
||||
BuildImage.get_text_size(f"{x.id}.{x.name}", font) for x in plugin_list
|
||||
]
|
||||
wh_list.append(BuildImage.get_text_size(menu_type, font))
|
||||
sum_height = (font_size + 6) * len(plugin_list) + 10
|
||||
max_width = max(x[0] for x in wh_list) + 30
|
||||
bk = BuildImage(
|
||||
max_width + 40,
|
||||
sum_height + 50,
|
||||
font_size=30,
|
||||
color="#a7d1fc",
|
||||
font="CJGaoDeGuo.otf",
|
||||
)
|
||||
title_size = bk.getsize(menu_type)
|
||||
max_width = max_width if max_width > title_size[0] else title_size[0]
|
||||
row = BuildImage(
|
||||
max_width + 40,
|
||||
sum_height,
|
||||
font_size=font_size,
|
||||
color="black" if idx % 2 else "white",
|
||||
)
|
||||
curr_h = 10
|
||||
group = await GroupConsole.get_or_none(group_id=group_id)
|
||||
for _, plugin in enumerate(plugin_list):
|
||||
text_color = (255, 255, 255) if idx % 2 else (0, 0, 0)
|
||||
if group and f"{plugin.module}," in group.block_plugin:
|
||||
text_color = (252, 75, 13)
|
||||
pos = None
|
||||
# 禁用状态划线
|
||||
if plugin.block_type in [BlockType.ALL, BlockType.GROUP] or (
|
||||
group and f"super:{plugin.module}," in group.block_plugin
|
||||
):
|
||||
w = curr_h + int(row.getsize(plugin.name)[1] / 2) + 2
|
||||
line_width = row.getsize(plugin.name)[0] + 35
|
||||
pos = (7, w, line_width, w)
|
||||
await row.text((10, curr_h), f"{plugin.id}.{plugin.name}", text_color)
|
||||
if pos:
|
||||
await row.line(pos, (236, 66, 7), 3)
|
||||
curr_h += font_size + 5
|
||||
await bk.text((0, 14), menu_type, center_type="width")
|
||||
await bk.paste(row, (0, 50))
|
||||
await bk.transparent(2)
|
||||
image_list.append(bk)
|
||||
image_group, h = group_image(image_list)
|
||||
|
||||
async def _a(image: BuildImage):
|
||||
await image.filter("GaussianBlur", 5)
|
||||
|
||||
result = await build_sort_image(
|
||||
image_group,
|
||||
h,
|
||||
background_path=BACKGROUND_PATH,
|
||||
background_handle=_a,
|
||||
)
|
||||
width, height = 10, 10
|
||||
for s in [
|
||||
"目前支持的功能列表:",
|
||||
"可以通过 ‘帮助 [功能名称或功能Id]’ 来获取对应功能的使用方法",
|
||||
]:
|
||||
text = await BuildImage.build_text_image(s, "HYWenHei-85W.ttf", 24)
|
||||
await result.paste(text, (width, height))
|
||||
height += 50
|
||||
if s == "目前支持的功能列表:":
|
||||
width += 50
|
||||
text = await BuildImage.build_text_image(
|
||||
"注: 红字代表功能被群管理员禁用,红线代表功能正在维护",
|
||||
"HYWenHei-85W.ttf",
|
||||
24,
|
||||
(231, 74, 57),
|
||||
)
|
||||
await result.paste(
|
||||
text,
|
||||
(300, 10),
|
||||
)
|
||||
return result
|
||||
@@ -1,180 +0,0 @@
|
||||
import nonebot
|
||||
from nonebot_plugin_htmlrender import template_to_pic
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.path_config import TEMPLATE_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.utils.enum import BlockType
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from ._utils import classify_plugin
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
plugin_name: str
|
||||
"""插件名称"""
|
||||
commands: list[str]
|
||||
"""插件命令"""
|
||||
|
||||
|
||||
def __handle_item(
|
||||
bot: BotConsole | None,
|
||||
plugin: PluginInfo,
|
||||
group: GroupConsole | None,
|
||||
is_detail: bool,
|
||||
):
|
||||
"""构造Item
|
||||
|
||||
参数:
|
||||
bot: BotConsole
|
||||
plugin: PluginInfo
|
||||
group: 群组
|
||||
is_detail: 是否为详细
|
||||
|
||||
返回:
|
||||
Item: Item
|
||||
"""
|
||||
if not plugin.status:
|
||||
if plugin.block_type == BlockType.ALL:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
elif group and plugin.block_type == BlockType.GROUP:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
elif not group and plugin.block_type == BlockType.PRIVATE:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
elif group and f"{plugin.module}," in group.block_plugin:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
elif bot and f"{plugin.module}," in bot.block_plugins:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
commands = []
|
||||
nb_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
|
||||
if is_detail and nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
|
||||
extra_data = PluginExtraData(**nb_plugin.metadata.extra)
|
||||
commands = [cmd.command for cmd in extra_data.commands]
|
||||
return Item(plugin_name=f"{plugin.id}-{plugin.name}", commands=commands)
|
||||
|
||||
|
||||
def build_plugin_data(classify: dict[str, list[Item]]) -> list[dict[str, str]]:
|
||||
"""构建前端插件数据
|
||||
|
||||
参数:
|
||||
classify: 插件数据
|
||||
|
||||
返回:
|
||||
list[dict[str, str]]: 前端插件数据
|
||||
"""
|
||||
classify = dict(sorted(classify.items(), key=lambda x: len(x[1]), reverse=True))
|
||||
menu_key = next(iter(classify.keys()))
|
||||
max_data = classify[menu_key]
|
||||
del classify[menu_key]
|
||||
plugin_list = [
|
||||
{
|
||||
"name": "主要功能" if menu in ["normal", "功能"] else menu,
|
||||
"items": value,
|
||||
}
|
||||
for menu, value in classify.items()
|
||||
]
|
||||
plugin_list = build_line_data(plugin_list)
|
||||
plugin_list.insert(
|
||||
0,
|
||||
build_plugin_line(
|
||||
menu_key if menu_key not in ["normal", "功能"] else "主要功能",
|
||||
max_data,
|
||||
30,
|
||||
100,
|
||||
True,
|
||||
),
|
||||
)
|
||||
return plugin_list
|
||||
|
||||
|
||||
def build_plugin_line(
|
||||
name: str, items: list, left: int, width: int | None = None, is_max: bool = False
|
||||
) -> dict:
|
||||
"""构造插件行数据
|
||||
|
||||
参数:
|
||||
name: 菜单名称
|
||||
items: 插件名称列表
|
||||
left: 左边距
|
||||
width: 总插件长度.
|
||||
is_max: 是否为最大长度的插件菜单
|
||||
|
||||
返回:
|
||||
dict: 插件数据
|
||||
"""
|
||||
_plugins = []
|
||||
width = width or 50
|
||||
if len(items) // 2 > 6 or is_max:
|
||||
width = 100
|
||||
plugin_list1 = []
|
||||
plugin_list2 = []
|
||||
for i in range(len(items)):
|
||||
if i % 2:
|
||||
plugin_list1.append(items[i])
|
||||
else:
|
||||
plugin_list2.append(items[i])
|
||||
_plugins = [(30, 50, plugin_list1), (0, 50, plugin_list2)]
|
||||
else:
|
||||
_plugins = [(left, 100, items)]
|
||||
return {"name": name, "items": _plugins, "width": width}
|
||||
|
||||
|
||||
def build_line_data(plugin_list: list[dict]) -> list[dict]:
|
||||
"""构造插件数据
|
||||
|
||||
参数:
|
||||
plugin_list: 插件列表
|
||||
|
||||
返回:
|
||||
list[dict]: 插件数据
|
||||
"""
|
||||
left = 30
|
||||
data = []
|
||||
for plugin in plugin_list:
|
||||
data.append(build_plugin_line(plugin["name"], plugin["items"], left))
|
||||
if len(plugin["items"]) // 2 <= 6:
|
||||
left = 15 if left == 30 else 30
|
||||
return data
|
||||
|
||||
|
||||
async def build_zhenxun_image(
|
||||
session: Uninfo, group_id: str | None, is_detail: bool
|
||||
) -> bytes:
|
||||
"""构造真寻帮助图片
|
||||
|
||||
参数:
|
||||
bot_id: bot_id
|
||||
group_id: 群号
|
||||
is_detail: 是否详细帮助
|
||||
"""
|
||||
classify = await classify_plugin(session, group_id, is_detail, __handle_item)
|
||||
plugin_list = build_plugin_data(classify)
|
||||
platform = PlatformUtils.get_platform(session)
|
||||
bot_id = BotConfig.get_qbot_uid(session.self_id) or session.self_id
|
||||
bot_ava = PlatformUtils.get_user_avatar_url(bot_id, platform)
|
||||
width = int(637 * 1.5) if is_detail else 637
|
||||
title_font = int(53 * 1.5) if is_detail else 53
|
||||
tip_font = int(19 * 1.5) if is_detail else 19
|
||||
return await template_to_pic(
|
||||
template_path=str((TEMPLATE_PATH / "ss_menu").absolute()),
|
||||
template_name="main.html",
|
||||
templates={
|
||||
"data": {
|
||||
"plugin_list": plugin_list,
|
||||
"ava": bot_ava,
|
||||
"width": width,
|
||||
"font_size": (title_font, tip_font),
|
||||
"is_detail": is_detail,
|
||||
}
|
||||
},
|
||||
pages={
|
||||
"viewport": {"width": width, "height": 453},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
@@ -74,8 +74,8 @@ async def _(matcher: Matcher, message: UniMsg, session: EventSession):
|
||||
message_list.append(image)
|
||||
message_list.append(
|
||||
"桀桀桀,预判到会有 '笨蛋' 把功能名称当命令用,特地前来嘲笑!"
|
||||
f"但还是好心来帮帮你啦!\n请at我发送 '帮助{plugin.name}' 或者"
|
||||
f" '帮助{plugin.id}' 来获取该功能帮助!"
|
||||
f"但还是好心来帮帮你啦!\n请at我发送 '帮助 {plugin.name}' 或者"
|
||||
f" '帮助 {plugin.id}' 来获取该功能帮助!"
|
||||
)
|
||||
logger.info("检测到功能名称当命令使用,已发送帮助信息", "功能帮助", session=session)
|
||||
await MessageUtils.build_message(message_list).send(reply_to=True)
|
||||
|
||||
@@ -58,5 +58,14 @@ Config.add_plugin_config(
|
||||
type=bool,
|
||||
)
|
||||
|
||||
Config.add_plugin_config(
|
||||
"hook",
|
||||
"AUTH_HOOKS_CONCURRENCY_LIMIT",
|
||||
5,
|
||||
help="同步进入权限钩子最大并发数",
|
||||
default_value=5,
|
||||
type=int,
|
||||
)
|
||||
|
||||
|
||||
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
|
||||
|
||||
@@ -1,597 +0,0 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.adapters.onebot.v11 import PokeNotifyEvent
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot_plugin_alconna import At, UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
from pydantic import BaseModel
|
||||
from tortoise.exceptions import IntegrityError
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.plugin_limit import PluginLimit
|
||||
from zhenxun.models.sign_user import SignUser
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import (
|
||||
BlockType,
|
||||
GoldHandle,
|
||||
LimitWatchType,
|
||||
PluginLimitType,
|
||||
PluginType,
|
||||
)
|
||||
from zhenxun.utils.exception import InsufficientGold
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.utils import CountLimiter, FreqLimiter, UserBlockLimiter
|
||||
|
||||
base_config = Config.get("hook")
|
||||
|
||||
|
||||
class Limit(BaseModel):
|
||||
limit: PluginLimit
|
||||
limiter: FreqLimiter | UserBlockLimiter | CountLimiter
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class LimitManage:
|
||||
add_module: ClassVar[list] = []
|
||||
|
||||
cd_limit: ClassVar[dict[str, Limit]] = {}
|
||||
block_limit: ClassVar[dict[str, Limit]] = {}
|
||||
count_limit: ClassVar[dict[str, Limit]] = {}
|
||||
|
||||
@classmethod
|
||||
def add_limit(cls, limit: PluginLimit):
|
||||
"""添加限制
|
||||
|
||||
参数:
|
||||
limit: PluginLimit
|
||||
"""
|
||||
if limit.module not in cls.add_module:
|
||||
cls.add_module.append(limit.module)
|
||||
if limit.limit_type == PluginLimitType.BLOCK:
|
||||
cls.block_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=UserBlockLimiter()
|
||||
)
|
||||
elif limit.limit_type == PluginLimitType.CD:
|
||||
cls.cd_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=FreqLimiter(limit.cd)
|
||||
)
|
||||
elif limit.limit_type == PluginLimitType.COUNT:
|
||||
cls.count_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=CountLimiter(limit.max_count)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def unblock(
|
||||
cls, module: str, user_id: str, group_id: str | None, channel_id: str | None
|
||||
):
|
||||
"""解除插件block
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
"""
|
||||
if limit_model := cls.block_limit.get(module):
|
||||
limit = limit_model.limit
|
||||
limiter: UserBlockLimiter = limit_model.limiter # type: ignore
|
||||
key_type = user_id
|
||||
if group_id and limit.watch_type == LimitWatchType.GROUP:
|
||||
key_type = channel_id or group_id
|
||||
logger.debug(
|
||||
f"解除对象: {key_type} 的block限制",
|
||||
"AuthChecker",
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
limiter.set_false(key_type)
|
||||
|
||||
@classmethod
|
||||
async def check(
|
||||
cls,
|
||||
module: str,
|
||||
user_id: str,
|
||||
group_id: str | None,
|
||||
channel_id: str | None,
|
||||
session: EventSession,
|
||||
):
|
||||
"""检测限制
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
session: Session
|
||||
|
||||
异常:
|
||||
IgnoredException: IgnoredException
|
||||
"""
|
||||
if limit_model := cls.cd_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id, session)
|
||||
if limit_model := cls.block_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id, session)
|
||||
if limit_model := cls.count_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id, session)
|
||||
|
||||
@classmethod
|
||||
async def __check(
|
||||
cls,
|
||||
limit_model: Limit | None,
|
||||
user_id: str,
|
||||
group_id: str | None,
|
||||
channel_id: str | None,
|
||||
session: EventSession,
|
||||
):
|
||||
"""检测限制
|
||||
|
||||
参数:
|
||||
limit_model: Limit
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
session: Session
|
||||
|
||||
异常:
|
||||
IgnoredException: IgnoredException
|
||||
"""
|
||||
if not limit_model:
|
||||
return
|
||||
limit = limit_model.limit
|
||||
limiter = limit_model.limiter
|
||||
is_limit = (
|
||||
LimitWatchType.ALL
|
||||
or (group_id and limit.watch_type == LimitWatchType.GROUP)
|
||||
or (not group_id and limit.watch_type == LimitWatchType.USER)
|
||||
)
|
||||
key_type = user_id
|
||||
if group_id and limit.watch_type == LimitWatchType.GROUP:
|
||||
key_type = channel_id or group_id
|
||||
if is_limit and not limiter.check(key_type):
|
||||
if limit.result:
|
||||
await MessageUtils.build_message(limit.result).send()
|
||||
logger.debug(
|
||||
f"{limit.module}({limit.limit_type}) 正在限制中...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException(f"{limit.module} 正在限制中...")
|
||||
else:
|
||||
logger.debug(
|
||||
f"开始进行限制 {limit.module}({limit.limit_type})...",
|
||||
"AuthChecker",
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
if isinstance(limiter, FreqLimiter):
|
||||
limiter.start_cd(key_type)
|
||||
if isinstance(limiter, UserBlockLimiter):
|
||||
limiter.set_true(key_type)
|
||||
if isinstance(limiter, CountLimiter):
|
||||
limiter.increase(key_type)
|
||||
|
||||
|
||||
class IsSuperuserException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AuthChecker:
|
||||
"""
|
||||
权限检查
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
check_notice_info_cd = Config.get_config("hook", "CHECK_NOTICE_INFO_CD")
|
||||
if check_notice_info_cd is None or check_notice_info_cd < 0:
|
||||
raise ValueError("模块: [hook], 配置项: [CHECK_NOTICE_INFO_CD] 为空或小于0")
|
||||
self._flmt = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_g = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_s = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_c = FreqLimiter(check_notice_info_cd)
|
||||
|
||||
def is_send_limit_message(self, plugin: PluginInfo, sid: str) -> bool:
|
||||
"""是否发送提示消息
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
|
||||
返回:
|
||||
bool: 是否发送提示消息
|
||||
"""
|
||||
if not base_config.get("IS_SEND_TIP_MESSAGE"):
|
||||
return False
|
||||
if plugin.plugin_type == PluginType.DEPENDANT:
|
||||
return False
|
||||
if plugin.ignore_prompt:
|
||||
return False
|
||||
return self._flmt_s.check(sid)
|
||||
|
||||
async def auth(
|
||||
self,
|
||||
matcher: Matcher,
|
||||
event: Event,
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
message: UniMsg,
|
||||
):
|
||||
"""权限检查
|
||||
|
||||
参数:
|
||||
matcher: matcher
|
||||
bot: bot
|
||||
session: EventSession
|
||||
message: UniMsg
|
||||
"""
|
||||
is_ignore = False
|
||||
cost_gold = 0
|
||||
user_id = session.id1
|
||||
group_id = session.id3
|
||||
channel_id = session.id2
|
||||
if not group_id:
|
||||
group_id = channel_id
|
||||
channel_id = None
|
||||
if matcher.type == "notice" and not isinstance(event, PokeNotifyEvent):
|
||||
"""过滤除poke外的notice"""
|
||||
return
|
||||
if user_id and matcher.plugin and (module_path := matcher.plugin.module_name):
|
||||
try:
|
||||
user = await UserConsole.get_user(user_id, session.platform)
|
||||
except IntegrityError as e:
|
||||
logger.debug(
|
||||
"重复创建用户,已跳过该次权限...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
return
|
||||
if plugin := await PluginInfo.get_or_none(module_path=module_path):
|
||||
if plugin.plugin_type == PluginType.HIDDEN:
|
||||
logger.debug(
|
||||
f"插件: {plugin.name}:{plugin.module} "
|
||||
"为HIDDEN,已跳过权限检查..."
|
||||
)
|
||||
return
|
||||
try:
|
||||
cost_gold = await self.auth_cost(user, plugin, session)
|
||||
if session.id1 in bot.config.superusers:
|
||||
if plugin.plugin_type == PluginType.SUPERUSER:
|
||||
raise IsSuperuserException()
|
||||
if not plugin.limit_superuser:
|
||||
cost_gold = 0
|
||||
raise IsSuperuserException()
|
||||
await self.auth_bot(plugin, bot.self_id)
|
||||
await self.auth_group(plugin, session, message)
|
||||
await self.auth_admin(plugin, session)
|
||||
await self.auth_plugin(plugin, session, event)
|
||||
await self.auth_limit(plugin, session)
|
||||
except IsSuperuserException:
|
||||
logger.debug(
|
||||
"超级用户或被ban跳过权限检测...", "AuthChecker", session=session
|
||||
)
|
||||
except IgnoredException:
|
||||
is_ignore = True
|
||||
LimitManage.unblock(
|
||||
matcher.plugin.name, user_id, group_id, channel_id
|
||||
)
|
||||
except AssertionError as e:
|
||||
is_ignore = True
|
||||
logger.debug("消息无法发送", session=session, e=e)
|
||||
if cost_gold and user_id:
|
||||
"""花费金币"""
|
||||
try:
|
||||
await UserConsole.reduce_gold(
|
||||
user_id,
|
||||
cost_gold,
|
||||
GoldHandle.PLUGIN,
|
||||
matcher.plugin.name if matcher.plugin else "",
|
||||
session.platform,
|
||||
)
|
||||
except InsufficientGold:
|
||||
if u := await UserConsole.get_user(user_id):
|
||||
u.gold = 0
|
||||
await u.save(update_fields=["gold"])
|
||||
logger.debug(
|
||||
f"调用功能花费金币: {cost_gold}", "AuthChecker", session=session
|
||||
)
|
||||
if is_ignore:
|
||||
raise IgnoredException("权限检测 ignore")
|
||||
|
||||
async def auth_bot(self, plugin: PluginInfo, bot_id: str):
|
||||
"""机器人权限
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
bot_id: bot_id
|
||||
"""
|
||||
if not await BotConsole.get_bot_status(bot_id):
|
||||
logger.debug("Bot休眠中阻断权限检测...", "AuthChecker")
|
||||
raise IgnoredException("BotConsole休眠权限检测 ignore")
|
||||
if await BotConsole.is_block_plugin(bot_id, plugin.module):
|
||||
logger.debug(
|
||||
f"Bot插件 {plugin.name}({plugin.module}) 权限检查结果为关闭...",
|
||||
"AuthChecker",
|
||||
)
|
||||
raise IgnoredException("BotConsole插件权限检测 ignore")
|
||||
|
||||
async def auth_limit(self, plugin: PluginInfo, session: EventSession):
|
||||
"""插件限制
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: EventSession
|
||||
"""
|
||||
user_id = session.id1
|
||||
group_id = session.id3
|
||||
channel_id = session.id2
|
||||
if not group_id:
|
||||
group_id = channel_id
|
||||
channel_id = None
|
||||
if plugin.module not in LimitManage.add_module:
|
||||
limit_list: list[PluginLimit] = await plugin.plugin_limit.filter(
|
||||
status=True
|
||||
).all() # type: ignore
|
||||
for limit in limit_list:
|
||||
LimitManage.add_limit(limit)
|
||||
if user_id:
|
||||
await LimitManage.check(
|
||||
plugin.module, user_id, group_id, channel_id, session
|
||||
)
|
||||
|
||||
async def auth_plugin(
|
||||
self, plugin: PluginInfo, session: EventSession, event: Event
|
||||
):
|
||||
"""插件状态
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: EventSession
|
||||
"""
|
||||
group_id = session.id3
|
||||
channel_id = session.id2
|
||||
if not group_id:
|
||||
group_id = channel_id
|
||||
channel_id = None
|
||||
if user_id := session.id1:
|
||||
if plugin.impression > 0:
|
||||
sign_user = await SignUser.get_user(user_id)
|
||||
if float(sign_user.impression) < plugin.impression:
|
||||
if self.is_send_limit_message(plugin, user_id):
|
||||
self._flmt_s.start_cd(user_id)
|
||||
await MessageUtils.build_message(
|
||||
f"好感度不足哦,当前功能需要好感度: {plugin.impression},"
|
||||
"请继续签到提升好感度吧!"
|
||||
).send(reply_to=True)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 用户好感度不足...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("好感度不足...")
|
||||
if group_id:
|
||||
sid = group_id or user_id
|
||||
if await GroupConsole.is_superuser_block_plugin(
|
||||
group_id, plugin.module
|
||||
):
|
||||
"""超级用户群组插件状态"""
|
||||
if self.is_send_limit_message(plugin, sid):
|
||||
self._flmt_s.start_cd(group_id or user_id)
|
||||
await MessageUtils.build_message(
|
||||
"超级管理员禁用了该群此功能..."
|
||||
).send(reply_to=True)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 超级管理员禁用了该群此功能...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("超级管理员禁用了该群此功能...")
|
||||
if await GroupConsole.is_normal_block_plugin(group_id, plugin.module):
|
||||
"""群组插件状态"""
|
||||
if self.is_send_limit_message(plugin, sid):
|
||||
self._flmt_s.start_cd(group_id or user_id)
|
||||
await MessageUtils.build_message("该群未开启此功能...").send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 未开启此功能...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("该群未开启此功能...")
|
||||
if plugin.block_type == BlockType.GROUP:
|
||||
"""全局群组禁用"""
|
||||
try:
|
||||
if self.is_send_limit_message(plugin, sid):
|
||||
self._flmt_c.start_cd(group_id)
|
||||
await MessageUtils.build_message(
|
||||
"该功能在群组中已被禁用..."
|
||||
).send(reply_to=True)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"auth_plugin 发送消息失败",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 该插件在群组中已被禁用...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("该插件在群组中已被禁用...")
|
||||
else:
|
||||
sid = user_id
|
||||
if plugin.block_type == BlockType.PRIVATE:
|
||||
"""全局私聊禁用"""
|
||||
try:
|
||||
if self.is_send_limit_message(plugin, sid):
|
||||
self._flmt_c.start_cd(user_id)
|
||||
await MessageUtils.build_message(
|
||||
"该功能在私聊中已被禁用..."
|
||||
).send()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"auth_admin 发送消息失败",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 该插件在私聊中已被禁用...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("该插件在私聊中已被禁用...")
|
||||
if not plugin.status and plugin.block_type == BlockType.ALL:
|
||||
"""全局状态"""
|
||||
if group_id and await GroupConsole.is_super_group(group_id):
|
||||
raise IsSuperuserException()
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 全局未开启此功能...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
if self.is_send_limit_message(plugin, sid):
|
||||
self._flmt_s.start_cd(group_id or user_id)
|
||||
await MessageUtils.build_message("全局未开启此功能...").send()
|
||||
raise IgnoredException("全局未开启此功能...")
|
||||
|
||||
async def auth_admin(self, plugin: PluginInfo, session: EventSession):
|
||||
"""管理员命令 个人权限
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: EventSession
|
||||
"""
|
||||
user_id = session.id1
|
||||
if user_id and plugin.admin_level:
|
||||
if group_id := session.id3 or session.id2:
|
||||
if not await LevelUser.check_level(
|
||||
user_id, group_id, plugin.admin_level
|
||||
):
|
||||
try:
|
||||
if self._flmt.check(user_id):
|
||||
self._flmt.start_cd(user_id)
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
At(flag="user", target=user_id),
|
||||
f"你的权限不足喔,"
|
||||
f"该功能需要的权限等级: {plugin.admin_level}",
|
||||
]
|
||||
).send(reply_to=True)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"auth_admin 发送消息失败",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 管理员权限不足...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("管理员权限不足...")
|
||||
elif not await LevelUser.check_level(user_id, None, plugin.admin_level):
|
||||
try:
|
||||
await MessageUtils.build_message(
|
||||
f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}"
|
||||
).send()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"auth_admin 发送消息失败", "AuthChecker", session=session, e=e
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 管理员权限不足...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("权限不足")
|
||||
|
||||
async def auth_group(
|
||||
self, plugin: PluginInfo, session: EventSession, message: UniMsg
|
||||
):
|
||||
"""群黑名单检测 群总开关检测
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: EventSession
|
||||
message: UniMsg
|
||||
"""
|
||||
if not (group_id := session.id3 or session.id2):
|
||||
return
|
||||
text = message.extract_plain_text()
|
||||
group = await GroupConsole.get_group(group_id)
|
||||
if not group:
|
||||
"""群不存在"""
|
||||
logger.debug(
|
||||
"群组信息不存在...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("群不存在")
|
||||
if group.level < 0:
|
||||
"""群权限小于0"""
|
||||
logger.debug(
|
||||
"群黑名单, 群权限-1...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("群黑名单")
|
||||
if not group.status:
|
||||
"""群休眠"""
|
||||
if text.strip() != "醒来":
|
||||
logger.debug("群休眠状态...", "AuthChecker", session=session)
|
||||
raise IgnoredException("群休眠状态")
|
||||
if plugin.level > group.level:
|
||||
"""插件等级大于群等级"""
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 群等级限制.."
|
||||
f"该功能需要的群等级: {plugin.level}..",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException(f"{plugin.name}({plugin.module}) 群等级限制...")
|
||||
|
||||
async def auth_cost(
|
||||
self, user: UserConsole, plugin: PluginInfo, session: EventSession
|
||||
) -> int:
|
||||
"""检测是否满足金币条件
|
||||
|
||||
参数:
|
||||
user: UserConsole
|
||||
plugin: PluginInfo
|
||||
session: EventSession
|
||||
|
||||
返回:
|
||||
int: 需要消耗的金币
|
||||
"""
|
||||
if user.gold < plugin.cost_gold:
|
||||
"""插件消耗金币不足"""
|
||||
try:
|
||||
await MessageUtils.build_message(
|
||||
f"金币不足..该功能需要{plugin.cost_gold}金币.."
|
||||
).send()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"auth_cost 发送消息失败", "AuthChecker", session=session, e=e
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 金币限制.."
|
||||
f"该功能需要{plugin.cost_gold}金币..",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException(f"{plugin.name}({plugin.module}) 金币限制...")
|
||||
return plugin.cost_gold
|
||||
|
||||
|
||||
checker = AuthChecker()
|
||||
@@ -0,0 +1,99 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from nonebot_plugin_alconna import At
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.data_access import DataAccess
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.utils import get_entity_ids
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import SkipPluginException
|
||||
from .utils import send_message
|
||||
|
||||
|
||||
async def auth_admin(plugin: PluginInfo, session: Uninfo):
|
||||
"""管理员命令 个人权限
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: Uninfo
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
if not plugin.admin_level:
|
||||
return
|
||||
|
||||
try:
|
||||
entity = get_entity_ids(session)
|
||||
level_dao = DataAccess(LevelUser)
|
||||
|
||||
# 并行查询用户权限数据
|
||||
global_user: LevelUser | None = None
|
||||
group_users: LevelUser | None = None
|
||||
|
||||
# 查询全局权限
|
||||
global_user_task = level_dao.safe_get_or_none(
|
||||
user_id=session.user.id, group_id__isnull=True
|
||||
)
|
||||
|
||||
# 如果在群组中,查询群组权限
|
||||
group_users_task = None
|
||||
if entity.group_id:
|
||||
group_users_task = level_dao.safe_get_or_none(
|
||||
user_id=session.user.id, group_id=entity.group_id
|
||||
)
|
||||
|
||||
# 等待查询完成,添加超时控制
|
||||
try:
|
||||
results = await asyncio.wait_for(
|
||||
asyncio.gather(global_user_task, group_users_task or asyncio.sleep(0)),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
global_user = results[0]
|
||||
group_users = results[1] if group_users_task else None
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"查询用户权限超时: user_id={session.user.id}", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
return
|
||||
|
||||
user_level = global_user.user_level if global_user else 0
|
||||
if entity.group_id and group_users:
|
||||
user_level = max(user_level, group_users.user_level)
|
||||
|
||||
if user_level < plugin.admin_level:
|
||||
await send_message(
|
||||
session,
|
||||
[
|
||||
At(flag="user", target=session.user.id),
|
||||
f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}",
|
||||
],
|
||||
entity.user_id,
|
||||
)
|
||||
|
||||
raise SkipPluginException(
|
||||
f"{plugin.name}({plugin.module}) 管理员权限不足..."
|
||||
)
|
||||
elif global_user:
|
||||
if global_user.user_level < plugin.admin_level:
|
||||
await send_message(
|
||||
session,
|
||||
f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}",
|
||||
)
|
||||
|
||||
raise SkipPluginException(
|
||||
f"{plugin.name}({plugin.module}) 管理员权限不足..."
|
||||
)
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_admin 耗时: {elapsed:.3f}s, plugin={plugin.module}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
@@ -0,0 +1,297 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot_plugin_alconna import At
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.data_access import DataAccess
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.utils import EntityIDs, get_entity_ids
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import SkipPluginException
|
||||
from .utils import freq, send_message
|
||||
|
||||
Config.add_plugin_config(
|
||||
"hook",
|
||||
"BAN_RESULT",
|
||||
"才不会给你发消息.",
|
||||
help="对被ban用户发送的消息",
|
||||
)
|
||||
|
||||
|
||||
async def calculate_ban_time(ban_record: BanConsole | None) -> int:
|
||||
"""根据ban记录计算剩余ban时间
|
||||
|
||||
参数:
|
||||
ban_record: BanConsole记录
|
||||
|
||||
返回:
|
||||
int: ban剩余时长,-1时为永久ban,0表示未被ban
|
||||
"""
|
||||
if not ban_record:
|
||||
return 0
|
||||
|
||||
if ban_record.duration == -1:
|
||||
return -1
|
||||
|
||||
_time = time.time() - (ban_record.ban_time + ban_record.duration)
|
||||
if _time < 0:
|
||||
return int(abs(_time))
|
||||
await ban_record.delete()
|
||||
return 0
|
||||
|
||||
|
||||
async def is_ban(user_id: str | None, group_id: str | None) -> int:
|
||||
"""检查用户或群组是否被ban
|
||||
|
||||
参数:
|
||||
user_id: 用户ID
|
||||
group_id: 群组ID
|
||||
|
||||
返回:
|
||||
int: ban的剩余时间,0表示未被ban
|
||||
"""
|
||||
if not user_id and not group_id:
|
||||
return 0
|
||||
|
||||
start_time = time.time()
|
||||
ban_dao = DataAccess(BanConsole)
|
||||
|
||||
# 分别获取用户在群组中的ban记录和全局ban记录
|
||||
group_user = None
|
||||
user = None
|
||||
|
||||
try:
|
||||
# 并行查询用户和群组的 ban 记录
|
||||
tasks = []
|
||||
if user_id and group_id:
|
||||
tasks.append(ban_dao.safe_get_or_none(user_id=user_id, group_id=group_id))
|
||||
if user_id:
|
||||
tasks.append(
|
||||
ban_dao.safe_get_or_none(user_id=user_id, group_id__isnull=True)
|
||||
)
|
||||
|
||||
# 等待所有查询完成,添加超时控制
|
||||
if tasks:
|
||||
try:
|
||||
ban_records = await asyncio.wait_for(
|
||||
asyncio.gather(*tasks), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
if len(tasks) == 2:
|
||||
group_user, user = ban_records
|
||||
elif user_id and group_id:
|
||||
group_user = ban_records[0]
|
||||
else:
|
||||
user = ban_records[0]
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"查询ban记录超时: user_id={user_id}, group_id={group_id}",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
return 0
|
||||
|
||||
# 检查记录并计算ban时间
|
||||
results = []
|
||||
if group_user:
|
||||
results.append(group_user)
|
||||
if user:
|
||||
results.append(user)
|
||||
|
||||
# 如果没有找到记录,返回0
|
||||
if not results:
|
||||
return 0
|
||||
|
||||
logger.debug(f"查询到的ban记录: {results}", LOGGER_COMMAND)
|
||||
# 检查所有记录,找出最严格的ban(时间最长的)
|
||||
max_ban_time: int = 0
|
||||
for result in results:
|
||||
if result.duration > 0 or result.duration == -1:
|
||||
# 直接计算ban时间,避免再次查询数据库
|
||||
ban_time = await calculate_ban_time(result)
|
||||
if ban_time == -1 or ban_time > max_ban_time:
|
||||
max_ban_time = ban_time
|
||||
|
||||
return max_ban_time
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"is_ban 耗时: {elapsed:.3f}s",
|
||||
LOGGER_COMMAND,
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
|
||||
|
||||
def check_plugin_type(matcher: Matcher) -> bool:
|
||||
"""判断插件类型是否是隐藏插件
|
||||
|
||||
参数:
|
||||
matcher: Matcher
|
||||
|
||||
返回:
|
||||
bool: 是否为隐藏插件
|
||||
"""
|
||||
if plugin := matcher.plugin:
|
||||
if metadata := plugin.metadata:
|
||||
extra = metadata.extra
|
||||
if extra.get("plugin_type") in [PluginType.HIDDEN]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def format_time(time_val: float) -> str:
|
||||
"""格式化时间
|
||||
|
||||
参数:
|
||||
time_val: ban时长
|
||||
|
||||
返回:
|
||||
str: 格式化时间文本
|
||||
"""
|
||||
if time_val == -1:
|
||||
return "∞"
|
||||
time_val = abs(int(time_val))
|
||||
if time_val < 60:
|
||||
time_str = f"{time_val!s} 秒"
|
||||
else:
|
||||
minute = int(time_val / 60)
|
||||
if minute > 60:
|
||||
hours = minute // 60
|
||||
minute %= 60
|
||||
time_str = f"{hours} 小时 {minute}分钟"
|
||||
else:
|
||||
time_str = f"{minute} 分钟"
|
||||
return time_str
|
||||
|
||||
|
||||
async def group_handle(group_id: str) -> None:
|
||||
"""群组ban检查
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
|
||||
异常:
|
||||
SkipPluginException: 群组处于黑名单
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
if await is_ban(None, group_id):
|
||||
raise SkipPluginException("群组处于黑名单中...")
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"group_handle 耗时: {elapsed:.3f}s",
|
||||
LOGGER_COMMAND,
|
||||
group_id=group_id,
|
||||
)
|
||||
|
||||
|
||||
async def user_handle(plugin: PluginInfo, entity: EntityIDs, session: Uninfo) -> None:
|
||||
"""用户ban检查
|
||||
|
||||
参数:
|
||||
module: 插件模块名
|
||||
entity: 实体ID信息
|
||||
session: Uninfo
|
||||
|
||||
异常:
|
||||
SkipPluginException: 用户处于黑名单
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
ban_result = Config.get_config("hook", "BAN_RESULT")
|
||||
time_val = await is_ban(entity.user_id, entity.group_id)
|
||||
if not time_val:
|
||||
return
|
||||
time_str = format_time(time_val)
|
||||
|
||||
if (
|
||||
plugin
|
||||
and time_val != -1
|
||||
and ban_result
|
||||
and freq.is_send_limit_message(plugin, entity.user_id, False)
|
||||
):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(
|
||||
session,
|
||||
[
|
||||
At(flag="user", target=entity.user_id),
|
||||
f"{ban_result}\n在..在 {time_str} 后才会理你喔",
|
||||
],
|
||||
entity.user_id,
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送消息超时: {entity.user_id}", LOGGER_COMMAND)
|
||||
raise SkipPluginException("用户处于黑名单中...")
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"user_handle 耗时: {elapsed:.3f}s",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
async def auth_ban(
|
||||
matcher: Matcher, bot: Bot, session: Uninfo, plugin: PluginInfo
|
||||
) -> None:
|
||||
"""权限检查 - ban 检查
|
||||
|
||||
参数:
|
||||
matcher: Matcher
|
||||
bot: Bot
|
||||
session: Uninfo
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
if not check_plugin_type(matcher):
|
||||
return
|
||||
if not matcher.plugin_name:
|
||||
return
|
||||
entity = get_entity_ids(session)
|
||||
if entity.user_id in bot.config.superusers:
|
||||
return
|
||||
if entity.group_id:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
group_handle(entity.group_id), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"群组ban检查超时: {entity.group_id}", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
|
||||
if entity.user_id:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
user_handle(plugin, entity, session),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"用户ban检查超时: {entity.user_id}", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
finally:
|
||||
# 记录总执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_ban 总耗时: {elapsed:.3f}s, plugin={matcher.plugin_name}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.data_access import DataAccess
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import SkipPluginException
|
||||
|
||||
|
||||
async def auth_bot(plugin: PluginInfo, bot_id: str):
|
||||
"""bot层面的权限检查
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
bot_id: bot id
|
||||
|
||||
异常:
|
||||
SkipPluginException: 忽略插件
|
||||
SkipPluginException: 忽略插件
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 从数据库或缓存中获取 bot 信息
|
||||
bot_dao = DataAccess(BotConsole)
|
||||
|
||||
try:
|
||||
bot: BotConsole | None = await asyncio.wait_for(
|
||||
bot_dao.safe_get_or_none(bot_id=bot_id), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"查询Bot信息超时: bot_id={bot_id}", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
return
|
||||
|
||||
if not bot or not bot.status:
|
||||
raise SkipPluginException("Bot不存在或休眠中阻断权限检测...")
|
||||
if CommonUtils.format(plugin.module) in bot.block_plugins:
|
||||
raise SkipPluginException(
|
||||
f"Bot插件 {plugin.name}({plugin.module}) 权限检查结果为关闭..."
|
||||
)
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_bot 耗时: {elapsed:.3f}s, "
|
||||
f"bot_id={bot_id}, plugin={plugin.module}",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
import time
|
||||
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import SkipPluginException
|
||||
from .utils import send_message
|
||||
|
||||
|
||||
async def auth_cost(user: UserConsole, plugin: PluginInfo, session: Uninfo) -> int:
|
||||
"""检测是否满足金币条件
|
||||
|
||||
参数:
|
||||
user: UserConsole
|
||||
plugin: PluginInfo
|
||||
session: Uninfo
|
||||
|
||||
返回:
|
||||
int: 需要消耗的金币
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
if user.gold < plugin.cost_gold:
|
||||
"""插件消耗金币不足"""
|
||||
await send_message(session, f"金币不足..该功能需要{plugin.cost_gold}金币..")
|
||||
raise SkipPluginException(f"{plugin.name}({plugin.module}) 金币限制...")
|
||||
return plugin.cost_gold
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_cost 耗时: {elapsed:.3f}s, plugin={plugin.module}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
import time
|
||||
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD, SwitchEnum
|
||||
from .exception import SkipPluginException
|
||||
|
||||
|
||||
async def auth_group(
|
||||
plugin: PluginInfo,
|
||||
group: GroupConsole | None,
|
||||
message: UniMsg,
|
||||
group_id: str | None,
|
||||
):
|
||||
"""群黑名单检测 群总开关检测
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
group: GroupConsole
|
||||
message: UniMsg
|
||||
"""
|
||||
if not group_id:
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
text = message.extract_plain_text()
|
||||
|
||||
if not group:
|
||||
raise SkipPluginException("群组信息不存在...")
|
||||
if group.level < 0:
|
||||
raise SkipPluginException("群组黑名单, 目标群组群权限权限-1...")
|
||||
if text.strip() != SwitchEnum.ENABLE and not group.status:
|
||||
raise SkipPluginException("群组休眠状态...")
|
||||
if plugin.level > group.level:
|
||||
raise SkipPluginException(
|
||||
f"{plugin.name}({plugin.module}) 群等级限制,"
|
||||
f"该功能需要的群等级: {plugin.level}..."
|
||||
)
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_group 耗时: {elapsed:.3f}s, plugin={plugin.module}",
|
||||
LOGGER_COMMAND,
|
||||
group_id=group_id,
|
||||
)
|
||||
@@ -0,0 +1,322 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import ClassVar
|
||||
|
||||
import nonebot
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.plugin_limit import PluginLimit
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import LimitWatchType, PluginLimitType
|
||||
from zhenxun.utils.limiters import CountLimiter, FreqLimiter, UserBlockLimiter
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.time_utils import TimeUtils
|
||||
from zhenxun.utils.utils import get_entity_ids
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import SkipPluginException
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
"""初始化限制"""
|
||||
await LimitManager.init_limit()
|
||||
|
||||
|
||||
class Limit(BaseModel):
|
||||
limit: PluginLimit
|
||||
limiter: FreqLimiter | UserBlockLimiter | CountLimiter
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class LimitManager:
|
||||
add_module: ClassVar[list] = []
|
||||
last_update_time: ClassVar[float] = 0
|
||||
update_interval: ClassVar[float] = 6000 # 1小时更新一次
|
||||
is_updating: ClassVar[bool] = False # 防止并发更新
|
||||
|
||||
cd_limit: ClassVar[dict[str, Limit]] = {}
|
||||
block_limit: ClassVar[dict[str, Limit]] = {}
|
||||
count_limit: ClassVar[dict[str, Limit]] = {}
|
||||
|
||||
# 模块限制缓存,避免频繁查询数据库
|
||||
module_limit_cache: ClassVar[dict[str, tuple[float, list[PluginLimit]]]] = {}
|
||||
module_cache_ttl: ClassVar[float] = 60 # 模块缓存有效期(秒)
|
||||
|
||||
@classmethod
|
||||
async def init_limit(cls):
|
||||
"""初始化限制"""
|
||||
cls.last_update_time = time.time()
|
||||
try:
|
||||
await asyncio.wait_for(cls.update_limits(), timeout=DB_TIMEOUT_SECONDS * 2)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("初始化限制超时", LOGGER_COMMAND)
|
||||
|
||||
@classmethod
|
||||
async def update_limits(cls):
|
||||
"""更新限制信息"""
|
||||
# 防止并发更新
|
||||
if cls.is_updating:
|
||||
return
|
||||
|
||||
cls.is_updating = True
|
||||
try:
|
||||
start_time = time.time()
|
||||
try:
|
||||
limit_list = await asyncio.wait_for(
|
||||
PluginLimit.filter(status=True).all(), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("查询限制信息超时", LOGGER_COMMAND)
|
||||
cls.is_updating = False
|
||||
return
|
||||
|
||||
# 清空旧数据
|
||||
cls.add_module = []
|
||||
cls.cd_limit = {}
|
||||
cls.block_limit = {}
|
||||
cls.count_limit = {}
|
||||
# 添加新数据
|
||||
for limit in limit_list:
|
||||
cls.add_limit(limit)
|
||||
|
||||
cls.last_update_time = time.time()
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的更新
|
||||
logger.warning(f"更新限制信息耗时: {elapsed:.3f}s", LOGGER_COMMAND)
|
||||
finally:
|
||||
cls.is_updating = False
|
||||
|
||||
@classmethod
|
||||
def add_limit(cls, limit: PluginLimit):
|
||||
"""添加限制
|
||||
|
||||
参数:
|
||||
limit: PluginLimit
|
||||
"""
|
||||
if limit.module not in cls.add_module:
|
||||
cls.add_module.append(limit.module)
|
||||
if limit.limit_type == PluginLimitType.BLOCK:
|
||||
cls.block_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=UserBlockLimiter()
|
||||
)
|
||||
elif limit.limit_type == PluginLimitType.CD:
|
||||
cls.cd_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=FreqLimiter(limit.cd)
|
||||
)
|
||||
elif limit.limit_type == PluginLimitType.COUNT:
|
||||
cls.count_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=CountLimiter(limit.max_count)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def unblock(
|
||||
cls, module: str, user_id: str, group_id: str | None, channel_id: str | None
|
||||
):
|
||||
"""解除插件block
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
"""
|
||||
if limit_model := cls.block_limit.get(module):
|
||||
limit = limit_model.limit
|
||||
limiter: UserBlockLimiter = limit_model.limiter # type: ignore
|
||||
key_type = user_id
|
||||
if group_id and limit.watch_type == LimitWatchType.GROUP:
|
||||
key_type = channel_id or group_id
|
||||
logger.debug(
|
||||
f"解除对象: {key_type} 的block限制",
|
||||
LOGGER_COMMAND,
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
limiter.set_false(key_type)
|
||||
|
||||
@classmethod
|
||||
async def get_module_limits(cls, module: str) -> list[PluginLimit]:
|
||||
"""获取模块的限制信息,使用缓存减少数据库查询
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
|
||||
返回:
|
||||
list[PluginLimit]: 限制列表
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
# 检查缓存
|
||||
if module in cls.module_limit_cache:
|
||||
cache_time, limits = cls.module_limit_cache[module]
|
||||
if current_time - cache_time < cls.module_cache_ttl:
|
||||
return limits
|
||||
|
||||
# 缓存不存在或已过期,从数据库查询
|
||||
try:
|
||||
start_time = time.time()
|
||||
limits = await asyncio.wait_for(
|
||||
PluginLimit.filter(module=module, status=True).all(),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的查询
|
||||
logger.warning(
|
||||
f"查询模块限制信息耗时: {elapsed:.3f}s, 模块: {module}",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
|
||||
# 更新缓存
|
||||
cls.module_limit_cache[module] = (current_time, limits)
|
||||
return limits
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"查询模块限制信息超时: {module}", LOGGER_COMMAND)
|
||||
# 超时时返回空列表,避免阻塞
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
async def check(
|
||||
cls,
|
||||
module: str,
|
||||
user_id: str,
|
||||
group_id: str | None,
|
||||
channel_id: str | None,
|
||||
):
|
||||
"""检测限制
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
|
||||
异常:
|
||||
IgnoredException: IgnoredException
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
# 定期更新全局限制信息
|
||||
if (
|
||||
time.time() - cls.last_update_time > cls.update_interval
|
||||
and not cls.is_updating
|
||||
):
|
||||
# 使用异步任务更新,避免阻塞当前请求
|
||||
asyncio.create_task(cls.update_limits()) # noqa: RUF006
|
||||
|
||||
# 如果模块不在已加载列表中,只加载该模块的限制
|
||||
if module not in cls.add_module:
|
||||
limits = await cls.get_module_limits(module)
|
||||
for limit in limits:
|
||||
cls.add_limit(limit)
|
||||
|
||||
# 检查各种限制
|
||||
try:
|
||||
if limit_model := cls.cd_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id)
|
||||
if limit_model := cls.block_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id)
|
||||
if limit_model := cls.count_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id)
|
||||
finally:
|
||||
# 记录总执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"限制检查耗时: {elapsed:.3f}s, 模块: {module}",
|
||||
LOGGER_COMMAND,
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def __check(
|
||||
cls,
|
||||
limit_model: Limit | None,
|
||||
user_id: str,
|
||||
group_id: str | None,
|
||||
channel_id: str | None,
|
||||
):
|
||||
"""检测限制
|
||||
|
||||
参数:
|
||||
limit_model: Limit
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
|
||||
异常:
|
||||
IgnoredException: IgnoredException
|
||||
"""
|
||||
if not limit_model:
|
||||
return
|
||||
limit = limit_model.limit
|
||||
limiter = limit_model.limiter
|
||||
is_limit = (
|
||||
LimitWatchType.ALL
|
||||
or (group_id and limit.watch_type == LimitWatchType.GROUP)
|
||||
or (not group_id and limit.watch_type == LimitWatchType.USER)
|
||||
)
|
||||
key_type = user_id
|
||||
if group_id and limit.watch_type == LimitWatchType.GROUP:
|
||||
key_type = channel_id or group_id
|
||||
if is_limit and not limiter.check(key_type):
|
||||
if limit.result:
|
||||
format_kwargs = {}
|
||||
if isinstance(limiter, FreqLimiter):
|
||||
left_time = limiter.left_time(key_type)
|
||||
cd_str = TimeUtils.format_duration(left_time)
|
||||
format_kwargs = {"cd": cd_str}
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
MessageUtils.build_message(
|
||||
limit.result, format_args=format_kwargs
|
||||
).send(),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送限制消息超时: {limit.module}", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{limit.module}({limit.limit_type}) 正在限制中..."
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"开始进行限制 {limit.module}({limit.limit_type})...",
|
||||
LOGGER_COMMAND,
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
if isinstance(limiter, FreqLimiter):
|
||||
limiter.start_cd(key_type)
|
||||
if isinstance(limiter, UserBlockLimiter):
|
||||
limiter.set_true(key_type)
|
||||
if isinstance(limiter, CountLimiter):
|
||||
limiter.increase(key_type)
|
||||
|
||||
|
||||
async def auth_limit(plugin: PluginInfo, session: Uninfo):
|
||||
"""插件限制
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: Uninfo
|
||||
"""
|
||||
entity = get_entity_ids(session)
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
LimitManager.check(
|
||||
plugin.module, entity.user_id, entity.group_id, entity.channel_id
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS * 2, # 给予更长的超时时间
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"检查插件限制超时: {plugin.module}", LOGGER_COMMAND)
|
||||
# 超时时不抛出异常,允许继续执行
|
||||
@@ -0,0 +1,205 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.enum import BlockType
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import IsSuperuserException, SkipPluginException
|
||||
from .utils import freq, is_poke, send_message
|
||||
|
||||
|
||||
class GroupCheck:
|
||||
def __init__(
|
||||
self, plugin: PluginInfo, group: GroupConsole, session: Uninfo, is_poke: bool
|
||||
) -> None:
|
||||
self.session = session
|
||||
self.is_poke = is_poke
|
||||
self.plugin = plugin
|
||||
self.group_data = group
|
||||
self.group_id = group.group_id
|
||||
|
||||
async def check(self):
|
||||
start_time = time.time()
|
||||
try:
|
||||
# 检查超级用户禁用
|
||||
if (
|
||||
self.group_data
|
||||
and CommonUtils.format(self.plugin.module)
|
||||
in self.group_data.superuser_block_plugin
|
||||
):
|
||||
if freq.is_send_limit_message(self.plugin, self.group_id, self.is_poke):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(
|
||||
self.session,
|
||||
"超级管理员禁用了该群此功能...",
|
||||
self.group_id,
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送消息超时: {self.group_id}", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{self.plugin.name}({self.plugin.module})"
|
||||
f" 超级管理员禁用了该群此功能..."
|
||||
)
|
||||
|
||||
# 检查普通禁用
|
||||
if (
|
||||
self.group_data
|
||||
and CommonUtils.format(self.plugin.module)
|
||||
in self.group_data.block_plugin
|
||||
):
|
||||
if freq.is_send_limit_message(self.plugin, self.group_id, self.is_poke):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(
|
||||
self.session, "该群未开启此功能...", self.group_id
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送消息超时: {self.group_id}", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{self.plugin.name}({self.plugin.module}) 未开启此功能..."
|
||||
)
|
||||
|
||||
# 检查全局禁用
|
||||
if self.plugin.block_type == BlockType.GROUP:
|
||||
if freq.is_send_limit_message(self.plugin, self.group_id, self.is_poke):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(
|
||||
self.session, "该功能在群组中已被禁用...", self.group_id
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送消息超时: {self.group_id}", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{self.plugin.name}({self.plugin.module})该插件在群组中已被禁用..."
|
||||
)
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"GroupCheck.check 耗时: {elapsed:.3f}s, 群组: {self.group_id}",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
|
||||
|
||||
class PluginCheck:
|
||||
def __init__(self, group: GroupConsole | None, session: Uninfo, is_poke: bool):
|
||||
self.session = session
|
||||
self.is_poke = is_poke
|
||||
self.group_data = group
|
||||
self.group_id = None
|
||||
if group:
|
||||
self.group_id = group.group_id
|
||||
|
||||
async def check_user(self, plugin: PluginInfo):
|
||||
"""全局私聊禁用检测
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
|
||||
异常:
|
||||
IgnoredException: 忽略插件
|
||||
"""
|
||||
if plugin.block_type == BlockType.PRIVATE:
|
||||
if freq.is_send_limit_message(plugin, self.session.user.id, self.is_poke):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(self.session, "该功能在私聊中已被禁用..."),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("发送消息超时", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{plugin.name}({plugin.module}) 该插件在私聊中已被禁用..."
|
||||
)
|
||||
|
||||
async def check_global(self, plugin: PluginInfo):
|
||||
"""全局状态
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
|
||||
异常:
|
||||
IgnoredException: 忽略插件
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
if plugin.status or plugin.block_type != BlockType.ALL:
|
||||
return
|
||||
"""全局状态"""
|
||||
if self.group_data and self.group_data.is_super:
|
||||
raise IsSuperuserException()
|
||||
|
||||
sid = self.group_id or self.session.user.id
|
||||
if freq.is_send_limit_message(plugin, sid, self.is_poke):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(self.session, "全局未开启此功能...", sid),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送消息超时: {sid}", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{plugin.name}({plugin.module}) 全局未开启此功能..."
|
||||
)
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"PluginCheck.check_global 耗时: {elapsed:.3f}s", LOGGER_COMMAND
|
||||
)
|
||||
|
||||
|
||||
async def auth_plugin(
|
||||
plugin: PluginInfo, group: GroupConsole | None, session: Uninfo, event: Event
|
||||
):
|
||||
"""插件状态
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: Uninfo
|
||||
event: Event
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
is_poke_event = is_poke(event)
|
||||
user_check = PluginCheck(group, session, is_poke_event)
|
||||
|
||||
tasks = []
|
||||
if group:
|
||||
tasks.append(GroupCheck(plugin, group, session, is_poke_event).check())
|
||||
else:
|
||||
tasks.append(user_check.check_user(plugin))
|
||||
tasks.append(user_check.check_global(plugin))
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(*tasks), timeout=DB_TIMEOUT_SECONDS * 2
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("插件用户/群组/全局检查超时...", LOGGER_COMMAND)
|
||||
|
||||
finally:
|
||||
# 记录总执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_plugin 总耗时: {elapsed:.3f}s, 模块: {plugin.module}",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
import nonebot
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
|
||||
from .exception import SkipPluginException
|
||||
|
||||
Config.add_plugin_config(
|
||||
"hook",
|
||||
"FILTER_BOT",
|
||||
True,
|
||||
help="过滤当前连接bot(防止bot互相调用)",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
)
|
||||
|
||||
|
||||
def bot_filter(session: Uninfo):
|
||||
"""过滤bot调用bot
|
||||
|
||||
参数:
|
||||
session: Uninfo
|
||||
|
||||
异常:
|
||||
SkipPluginException: bot互相调用
|
||||
"""
|
||||
if not Config.get_config("hook", "FILTER_BOT"):
|
||||
return
|
||||
bot_ids = list(nonebot.get_bots().keys())
|
||||
if session.user.id == session.self_id:
|
||||
return
|
||||
if session.user.id in bot_ids:
|
||||
raise SkipPluginException(
|
||||
f"bot:{session.self_id} 尝试调用 bot:{session.user.id}"
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from enum import StrEnum
|
||||
else:
|
||||
from strenum import StrEnum
|
||||
|
||||
LOGGER_COMMAND = "AuthChecker"
|
||||
|
||||
|
||||
class SwitchEnum(StrEnum):
|
||||
ENABLE = "醒来"
|
||||
DISABLE = "休息吧"
|
||||
|
||||
|
||||
WARNING_THRESHOLD = 0.5 # 警告阈值(秒)
|
||||
@@ -0,0 +1,26 @@
|
||||
class IsSuperuserException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SkipPluginException(Exception):
|
||||
def __init__(self, info: str, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
self.info = info
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.info
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.info
|
||||
|
||||
|
||||
class PermissionExemption(Exception):
|
||||
def __init__(self, info: str, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
self.info = info
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.info
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.info
|
||||
@@ -0,0 +1,91 @@
|
||||
import contextlib
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.utils import FreqLimiter
|
||||
|
||||
from .config import LOGGER_COMMAND
|
||||
|
||||
base_config = Config.get("hook")
|
||||
|
||||
|
||||
def is_poke(event: Event) -> bool:
|
||||
"""判断是否为poke类型
|
||||
|
||||
参数:
|
||||
event: Event
|
||||
|
||||
返回:
|
||||
bool: 是否为poke类型
|
||||
"""
|
||||
with contextlib.suppress(ImportError):
|
||||
from nonebot.adapters.onebot.v11 import PokeNotifyEvent
|
||||
|
||||
return isinstance(event, PokeNotifyEvent)
|
||||
return False
|
||||
|
||||
|
||||
async def send_message(
|
||||
session: Uninfo, message: list | str, check_tag: str | None = None
|
||||
):
|
||||
"""发送消息
|
||||
|
||||
参数:
|
||||
session: Uninfo
|
||||
message: 消息
|
||||
check_tag: cd flag
|
||||
"""
|
||||
try:
|
||||
if not check_tag:
|
||||
await MessageUtils.build_message(message).send(reply_to=True)
|
||||
elif freq._flmt.check(check_tag):
|
||||
freq._flmt.start_cd(check_tag)
|
||||
await MessageUtils.build_message(message).send(reply_to=True)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"发送消息失败",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
|
||||
|
||||
class FreqUtils:
|
||||
def __init__(self):
|
||||
check_notice_info_cd = Config.get_config("hook", "CHECK_NOTICE_INFO_CD")
|
||||
if check_notice_info_cd is None or check_notice_info_cd < 0:
|
||||
raise ValueError("模块: [hook], 配置项: [CHECK_NOTICE_INFO_CD] 为空或小于0")
|
||||
self._flmt = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_g = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_s = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_c = FreqLimiter(check_notice_info_cd)
|
||||
|
||||
def is_send_limit_message(
|
||||
self, plugin: PluginInfo, sid: str, is_poke: bool
|
||||
) -> bool:
|
||||
"""是否发送提示消息
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
sid: 检测键
|
||||
is_poke: 是否是戳一戳
|
||||
|
||||
返回:
|
||||
bool: 是否发送提示消息
|
||||
"""
|
||||
if is_poke:
|
||||
return False
|
||||
if not base_config.get("IS_SEND_TIP_MESSAGE"):
|
||||
return False
|
||||
if plugin.plugin_type == PluginType.DEPENDANT:
|
||||
return False
|
||||
return False if plugin.ignore_prompt else self._flmt_s.check(sid)
|
||||
|
||||
|
||||
freq = FreqUtils()
|
||||
@@ -0,0 +1,457 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from tortoise.exceptions import IntegrityError
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.data_access import DataAccess
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import GoldHandle, PluginType
|
||||
from zhenxun.utils.exception import InsufficientGold
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.utils import get_entity_ids
|
||||
|
||||
from .auth.auth_admin import auth_admin
|
||||
from .auth.auth_ban import auth_ban
|
||||
from .auth.auth_bot import auth_bot
|
||||
from .auth.auth_cost import auth_cost
|
||||
from .auth.auth_group import auth_group
|
||||
from .auth.auth_limit import LimitManager, auth_limit
|
||||
from .auth.auth_plugin import auth_plugin
|
||||
from .auth.bot_filter import bot_filter
|
||||
from .auth.config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .auth.exception import (
|
||||
IsSuperuserException,
|
||||
PermissionExemption,
|
||||
SkipPluginException,
|
||||
)
|
||||
from .auth.utils import base_config
|
||||
|
||||
# 超时设置(秒)
|
||||
TIMEOUT_SECONDS = 5.0
|
||||
# 熔断计数器
|
||||
CIRCUIT_BREAKERS = {
|
||||
"auth_ban": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
"auth_bot": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
"auth_group": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
"auth_admin": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
"auth_plugin": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
"auth_limit": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
}
|
||||
# 熔断重置时间(秒)
|
||||
CIRCUIT_RESET_TIME = 300 # 5分钟
|
||||
|
||||
# 并发控制:限制同时进入 hooks 并行检查的协程数
|
||||
|
||||
# 默认为 6,可通过环境变量 AUTH_HOOKS_CONCURRENCY_LIMIT 调整
|
||||
HOOKS_CONCURRENCY_LIMIT = base_config.get("AUTH_HOOKS_CONCURRENCY_LIMIT")
|
||||
|
||||
# 全局信号量与计数器
|
||||
HOOKS_SEMAPHORE = asyncio.Semaphore(HOOKS_CONCURRENCY_LIMIT)
|
||||
HOOKS_ACTIVE_COUNT = 0
|
||||
HOOKS_ACTIVE_LOCK = asyncio.Lock()
|
||||
|
||||
|
||||
# 超时装饰器
|
||||
async def with_timeout(coro, timeout=TIMEOUT_SECONDS, name=None):
|
||||
"""带超时控制的协程执行
|
||||
|
||||
参数:
|
||||
coro: 要执行的协程
|
||||
timeout: 超时时间(秒)
|
||||
name: 操作名称,用于日志记录
|
||||
|
||||
返回:
|
||||
协程的返回值,或者在超时时抛出 TimeoutError
|
||||
"""
|
||||
try:
|
||||
return await asyncio.wait_for(coro, timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
if name:
|
||||
logger.error(f"{name} 操作超时 (>{timeout}s)", LOGGER_COMMAND)
|
||||
# 更新熔断计数器
|
||||
if name in CIRCUIT_BREAKERS:
|
||||
CIRCUIT_BREAKERS[name]["failures"] += 1
|
||||
if (
|
||||
CIRCUIT_BREAKERS[name]["failures"]
|
||||
>= CIRCUIT_BREAKERS[name]["threshold"]
|
||||
and not CIRCUIT_BREAKERS[name]["active"]
|
||||
):
|
||||
CIRCUIT_BREAKERS[name]["active"] = True
|
||||
CIRCUIT_BREAKERS[name]["reset_time"] = (
|
||||
time.time() + CIRCUIT_RESET_TIME
|
||||
)
|
||||
logger.warning(
|
||||
f"{name} 熔断器已激活,将在 {CIRCUIT_RESET_TIME} 秒后重置",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
# 检查熔断状态
|
||||
def check_circuit_breaker(name):
|
||||
"""检查熔断器状态
|
||||
|
||||
参数:
|
||||
name: 操作名称
|
||||
|
||||
返回:
|
||||
bool: 是否已熔断
|
||||
"""
|
||||
if name not in CIRCUIT_BREAKERS:
|
||||
return False
|
||||
|
||||
# 检查是否需要重置熔断器
|
||||
if (
|
||||
CIRCUIT_BREAKERS[name]["active"]
|
||||
and time.time() > CIRCUIT_BREAKERS[name]["reset_time"]
|
||||
):
|
||||
CIRCUIT_BREAKERS[name]["active"] = False
|
||||
CIRCUIT_BREAKERS[name]["failures"] = 0
|
||||
logger.info(f"{name} 熔断器已重置", LOGGER_COMMAND)
|
||||
|
||||
return CIRCUIT_BREAKERS[name]["active"]
|
||||
|
||||
|
||||
async def get_plugin_and_user(
|
||||
module: str, user_id: str
|
||||
) -> tuple[PluginInfo, UserConsole]:
|
||||
"""获取用户数据和插件信息
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
user_id: 用户id
|
||||
|
||||
异常:
|
||||
PermissionExemption: 插件数据不存在
|
||||
PermissionExemption: 插件类型为HIDDEN
|
||||
PermissionExemption: 重复创建用户
|
||||
PermissionExemption: 用户数据不存在
|
||||
|
||||
返回:
|
||||
tuple[PluginInfo, UserConsole]: 插件信息,用户信息
|
||||
"""
|
||||
user_dao = DataAccess(UserConsole)
|
||||
plugin_dao = DataAccess(PluginInfo)
|
||||
|
||||
# 并行查询插件和用户数据
|
||||
plugin_task = plugin_dao.safe_get_or_none(module=module)
|
||||
user_task = user_dao.get_by_func_or_none(
|
||||
UserConsole.get_user, False, user_id=user_id
|
||||
)
|
||||
|
||||
try:
|
||||
plugin, user = await with_timeout(
|
||||
asyncio.gather(plugin_task, user_task), name="get_plugin_and_user"
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
# 如果并行查询超时,尝试串行查询
|
||||
logger.warning("并行查询超时,尝试串行查询", LOGGER_COMMAND)
|
||||
plugin = await with_timeout(
|
||||
plugin_dao.safe_get_or_none(module=module), name="get_plugin"
|
||||
)
|
||||
user = await with_timeout(
|
||||
user_dao.safe_get_or_none(user_id=user_id), name="get_user"
|
||||
)
|
||||
except IntegrityError:
|
||||
await asyncio.sleep(0.5)
|
||||
plugin_task = plugin_dao.safe_get_or_none(module=module)
|
||||
user_task = user_dao.get_by_func_or_none(
|
||||
UserConsole.get_user, False, user_id=user_id
|
||||
)
|
||||
plugin, user = await with_timeout(
|
||||
asyncio.gather(plugin_task, user_task), name="get_plugin_and_user"
|
||||
)
|
||||
|
||||
if not plugin:
|
||||
raise PermissionExemption(f"插件:{module} 数据不存在,已跳过权限检查...")
|
||||
if plugin.plugin_type == PluginType.HIDDEN:
|
||||
raise PermissionExemption(
|
||||
f"插件: {plugin.name}:{plugin.module} 为HIDDEN,已跳过权限检查..."
|
||||
)
|
||||
user = None
|
||||
try:
|
||||
user = await user_dao.get_by_func_or_none(
|
||||
UserConsole.get_user, False, user_id=user_id
|
||||
)
|
||||
except IntegrityError as e:
|
||||
raise PermissionExemption("重复创建用户,已跳过该次权限检查...") from e
|
||||
if not user:
|
||||
raise PermissionExemption("用户数据不存在,已跳过权限检查...")
|
||||
return plugin, user
|
||||
|
||||
|
||||
async def get_plugin_cost(
|
||||
bot: Bot, user: UserConsole, plugin: PluginInfo, session: Uninfo
|
||||
) -> int:
|
||||
"""获取插件费用
|
||||
|
||||
参数:
|
||||
bot: Bot
|
||||
user: 用户数据
|
||||
plugin: 插件数据
|
||||
session: Uninfo
|
||||
|
||||
异常:
|
||||
IsSuperuserException: 超级用户
|
||||
IsSuperuserException: 超级用户
|
||||
|
||||
返回:
|
||||
int: 调用插件金币费用
|
||||
"""
|
||||
cost_gold = await with_timeout(auth_cost(user, plugin, session), name="auth_cost")
|
||||
if session.user.id in bot.config.superusers:
|
||||
if plugin.plugin_type == PluginType.SUPERUSER:
|
||||
raise IsSuperuserException()
|
||||
if not plugin.limit_superuser:
|
||||
raise IsSuperuserException()
|
||||
return cost_gold
|
||||
|
||||
|
||||
async def reduce_gold(user_id: str, module: str, cost_gold: int, session: Uninfo):
|
||||
"""扣除用户金币
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
module: 插件模块名称
|
||||
cost_gold: 消耗金币
|
||||
session: Uninfo
|
||||
"""
|
||||
user_dao = DataAccess(UserConsole)
|
||||
try:
|
||||
await with_timeout(
|
||||
UserConsole.reduce_gold(
|
||||
user_id,
|
||||
cost_gold,
|
||||
GoldHandle.PLUGIN,
|
||||
module,
|
||||
PlatformUtils.get_platform(session),
|
||||
),
|
||||
name="reduce_gold",
|
||||
)
|
||||
except InsufficientGold:
|
||||
if u := await UserConsole.get_user(user_id):
|
||||
u.gold = 0
|
||||
await u.save(update_fields=["gold"])
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"扣除金币超时,用户: {user_id}, 金币: {cost_gold}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
|
||||
# 清除缓存,使下次查询时从数据库获取最新数据
|
||||
await user_dao.clear_cache(user_id=user_id)
|
||||
logger.debug(f"调用功能花费金币: {cost_gold}", LOGGER_COMMAND, session=session)
|
||||
|
||||
|
||||
# 辅助函数,用于记录每个 hook 的执行时间
|
||||
async def time_hook(coro, name, time_dict):
|
||||
start = time.time()
|
||||
try:
|
||||
# 检查熔断状态
|
||||
if check_circuit_breaker(name):
|
||||
logger.info(f"{name} 熔断器激活中,跳过执行", LOGGER_COMMAND)
|
||||
time_dict[name] = "熔断跳过"
|
||||
return
|
||||
|
||||
# 添加超时控制
|
||||
return await with_timeout(coro, name=name)
|
||||
except asyncio.TimeoutError:
|
||||
time_dict[name] = f"超时 (>{TIMEOUT_SECONDS}s)"
|
||||
finally:
|
||||
if name not in time_dict:
|
||||
time_dict[name] = f"{time.time() - start:.3f}s"
|
||||
|
||||
|
||||
async def _enter_hooks_section():
|
||||
"""尝试获取全局信号量并更新计数器,超时则抛出 PermissionExemption。"""
|
||||
global HOOKS_ACTIVE_COUNT
|
||||
# 队列模式:如果达到上限,协程将排队等待直到获取到信号量
|
||||
await HOOKS_SEMAPHORE.acquire()
|
||||
async with HOOKS_ACTIVE_LOCK:
|
||||
HOOKS_ACTIVE_COUNT += 1
|
||||
logger.debug(f"当前并发权限检查数量: {HOOKS_ACTIVE_COUNT}", LOGGER_COMMAND)
|
||||
|
||||
|
||||
async def _leave_hooks_section():
|
||||
"""释放信号量并更新计数器。"""
|
||||
global HOOKS_ACTIVE_COUNT
|
||||
from contextlib import suppress
|
||||
|
||||
with suppress(Exception):
|
||||
HOOKS_SEMAPHORE.release()
|
||||
async with HOOKS_ACTIVE_LOCK:
|
||||
HOOKS_ACTIVE_COUNT -= 1
|
||||
# 保证计数不为负
|
||||
HOOKS_ACTIVE_COUNT = max(HOOKS_ACTIVE_COUNT, 0)
|
||||
logger.debug(f"当前并发权限检查数量: {HOOKS_ACTIVE_COUNT}", LOGGER_COMMAND)
|
||||
|
||||
|
||||
async def auth(
|
||||
matcher: Matcher,
|
||||
event: Event,
|
||||
bot: Bot,
|
||||
session: Uninfo,
|
||||
message: UniMsg,
|
||||
):
|
||||
"""权限检查
|
||||
|
||||
参数:
|
||||
matcher: matcher
|
||||
event: Event
|
||||
bot: bot
|
||||
session: Uninfo
|
||||
message: UniMsg
|
||||
"""
|
||||
start_time = time.time()
|
||||
cost_gold = 0
|
||||
ignore_flag = False
|
||||
entity = get_entity_ids(session)
|
||||
module = matcher.plugin_name or ""
|
||||
|
||||
# 用于记录各个 hook 的执行时间
|
||||
hook_times = {}
|
||||
hooks_time = 0 # 初始化 hooks_time 变量
|
||||
|
||||
# 记录是否已进入 hooks 区域(用于 finally 中释放)
|
||||
entered_hooks = False
|
||||
|
||||
try:
|
||||
if not module:
|
||||
raise PermissionExemption("Matcher插件名称不存在...")
|
||||
|
||||
# 获取插件和用户数据
|
||||
plugin_user_start = time.time()
|
||||
try:
|
||||
plugin, user = await with_timeout(
|
||||
get_plugin_and_user(module, entity.user_id), name="get_plugin_and_user"
|
||||
)
|
||||
hook_times["get_plugin_user"] = f"{time.time() - plugin_user_start:.3f}s"
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"获取插件和用户数据超时,模块: {module}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
raise PermissionExemption("获取插件和用户数据超时,请稍后再试...")
|
||||
|
||||
# 进入 hooks 并行检查区域(会在高并发时排队)
|
||||
await _enter_hooks_section()
|
||||
entered_hooks = True
|
||||
|
||||
# 获取插件费用
|
||||
cost_start = time.time()
|
||||
try:
|
||||
cost_gold = await with_timeout(
|
||||
get_plugin_cost(bot, user, plugin, session), name="get_plugin_cost"
|
||||
)
|
||||
hook_times["cost_gold"] = f"{time.time() - cost_start:.3f}s"
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"获取插件费用超时,模块: {module}", LOGGER_COMMAND, session=session
|
||||
)
|
||||
# 继续执行,不阻止权限检查
|
||||
|
||||
# 执行 bot_filter
|
||||
bot_filter(session)
|
||||
|
||||
group = None
|
||||
if entity.group_id:
|
||||
group_dao = DataAccess(GroupConsole)
|
||||
group = await with_timeout(
|
||||
group_dao.safe_get_or_none(
|
||||
group_id=entity.group_id, channel_id__isnull=True
|
||||
),
|
||||
name="get_group",
|
||||
)
|
||||
|
||||
# 并行执行所有 hook 检查,并记录执行时间
|
||||
hooks_start = time.time()
|
||||
|
||||
# 创建所有 hook 任务
|
||||
hook_tasks = [
|
||||
time_hook(auth_ban(matcher, bot, session, plugin), "auth_ban", hook_times),
|
||||
time_hook(auth_bot(plugin, bot.self_id), "auth_bot", hook_times),
|
||||
time_hook(
|
||||
auth_group(plugin, group, message, entity.group_id),
|
||||
"auth_group",
|
||||
hook_times,
|
||||
),
|
||||
time_hook(auth_admin(plugin, session), "auth_admin", hook_times),
|
||||
time_hook(
|
||||
auth_plugin(plugin, group, session, event), "auth_plugin", hook_times
|
||||
),
|
||||
time_hook(auth_limit(plugin, session), "auth_limit", hook_times),
|
||||
]
|
||||
|
||||
# 使用 gather 并行执行所有 hook,但添加总体超时控制
|
||||
try:
|
||||
await with_timeout(
|
||||
asyncio.gather(*hook_tasks),
|
||||
timeout=TIMEOUT_SECONDS * 2, # 给总体执行更多时间
|
||||
name="auth_hooks_gather",
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"权限检查 hooks 总体执行超时,模块: {module}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
# 不抛出异常,允许继续执行
|
||||
|
||||
hooks_time = time.time() - hooks_start
|
||||
|
||||
except SkipPluginException as e:
|
||||
LimitManager.unblock(module, entity.user_id, entity.group_id, entity.channel_id)
|
||||
logger.info(str(e), LOGGER_COMMAND, session=session)
|
||||
ignore_flag = True
|
||||
except IsSuperuserException:
|
||||
logger.debug("超级用户跳过权限检测...", LOGGER_COMMAND, session=session)
|
||||
except PermissionExemption as e:
|
||||
logger.info(str(e), LOGGER_COMMAND, session=session)
|
||||
finally:
|
||||
# 如果进入过 hooks 区域,确保释放信号量(即使上层处理抛出了异常)
|
||||
if entered_hooks:
|
||||
try:
|
||||
await _leave_hooks_section()
|
||||
except Exception:
|
||||
logger.error(
|
||||
"释放 hooks 信号量时出错",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
# 扣除金币
|
||||
if not ignore_flag and cost_gold > 0:
|
||||
gold_start = time.time()
|
||||
try:
|
||||
await with_timeout(
|
||||
reduce_gold(entity.user_id, module, cost_gold, session),
|
||||
name="reduce_gold",
|
||||
)
|
||||
hook_times["reduce_gold"] = f"{time.time() - gold_start:.3f}s"
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"扣除金币超时,模块: {module}", LOGGER_COMMAND, session=session
|
||||
)
|
||||
|
||||
# 记录总执行时间
|
||||
total_time = time.time() - start_time
|
||||
if total_time > WARNING_THRESHOLD: # 如果总时间超过500ms,记录详细信息
|
||||
logger.warning(
|
||||
f"权限检查耗时过长: {total_time:.3f}s, 模块: {module}, "
|
||||
f"hooks时间: {hooks_time:.3f}s, "
|
||||
f"详情: {hook_times}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
|
||||
if ignore_flag:
|
||||
raise IgnoredException("权限检测 ignore")
|
||||
@@ -1,41 +1,43 @@
|
||||
from nonebot.adapters.onebot.v11 import Bot, Event
|
||||
import time
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.message import run_postprocessor, run_preprocessor
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from ._auth_checker import LimitManage, checker
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
from .auth.config import LOGGER_COMMAND
|
||||
from .auth_checker import LimitManager, auth
|
||||
|
||||
|
||||
# # 权限检测
|
||||
@run_preprocessor
|
||||
async def _(
|
||||
matcher: Matcher, event: Event, bot: Bot, session: EventSession, message: UniMsg
|
||||
):
|
||||
await checker.auth(
|
||||
async def _(matcher: Matcher, event: Event, bot: Bot, session: Uninfo, message: UniMsg):
|
||||
start_time = time.time()
|
||||
await auth(
|
||||
matcher,
|
||||
event,
|
||||
bot,
|
||||
session,
|
||||
message,
|
||||
)
|
||||
logger.debug(f"权限检测耗时:{time.time() - start_time}秒", LOGGER_COMMAND)
|
||||
|
||||
|
||||
# 解除命令block阻塞
|
||||
@run_postprocessor
|
||||
async def _(
|
||||
matcher: Matcher,
|
||||
exception: Exception | None,
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
session: EventSession,
|
||||
):
|
||||
user_id = session.id1
|
||||
group_id = session.id3
|
||||
channel_id = session.id2
|
||||
if not group_id:
|
||||
group_id = channel_id
|
||||
channel_id = None
|
||||
async def _(matcher: Matcher, session: Uninfo):
|
||||
user_id = session.user.id
|
||||
group_id = None
|
||||
channel_id = None
|
||||
if session.group:
|
||||
if session.group.parent:
|
||||
group_id = session.group.parent.id
|
||||
channel_id = session.group.id
|
||||
else:
|
||||
group_id = session.group.id
|
||||
if user_id and matcher.plugin:
|
||||
module = matcher.plugin.name
|
||||
LimitManage.unblock(module, user_id, group_id, channel_id)
|
||||
LimitManager.unblock(module, user_id, group_id, channel_id)
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.message import run_preprocessor
|
||||
from nonebot.typing import T_State
|
||||
from nonebot_plugin_alconna import At
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.utils import FreqLimiter
|
||||
|
||||
Config.add_plugin_config(
|
||||
"hook",
|
||||
"BAN_RESULT",
|
||||
"才不会给你发消息.",
|
||||
help="对被ban用户发送的消息",
|
||||
)
|
||||
|
||||
_flmt = FreqLimiter(300)
|
||||
|
||||
|
||||
# 检查是否被ban
|
||||
@run_preprocessor
|
||||
async def _(
|
||||
matcher: Matcher, bot: Bot, event: Event, state: T_State, session: EventSession
|
||||
):
|
||||
extra = {}
|
||||
if plugin := matcher.plugin:
|
||||
if metadata := plugin.metadata:
|
||||
extra = metadata.extra
|
||||
if extra.get("plugin_type") in [PluginType.HIDDEN]:
|
||||
return
|
||||
user_id = session.id1
|
||||
group_id = session.id3 or session.id2
|
||||
if group_id:
|
||||
if user_id in bot.config.superusers:
|
||||
return
|
||||
if await BanConsole.is_ban(None, group_id):
|
||||
logger.debug("群组处于黑名单中...", "ban_hook")
|
||||
raise IgnoredException("群组处于黑名单中...")
|
||||
if g := await GroupConsole.get_group(group_id):
|
||||
if g.level < 0:
|
||||
logger.debug("群黑名单, 群权限-1...", "ban_hook")
|
||||
raise IgnoredException("群黑名单, 群权限-1..")
|
||||
if user_id:
|
||||
ban_result = Config.get_config("hook", "BAN_RESULT")
|
||||
if user_id in bot.config.superusers:
|
||||
return
|
||||
if await BanConsole.is_ban(user_id, group_id):
|
||||
time = await BanConsole.check_ban_time(user_id, group_id)
|
||||
if time == -1:
|
||||
time_str = "∞"
|
||||
else:
|
||||
time = abs(int(time))
|
||||
if time < 60:
|
||||
time_str = f"{time!s} 秒"
|
||||
else:
|
||||
minute = int(time / 60)
|
||||
if minute > 60:
|
||||
hours = minute // 60
|
||||
minute %= 60
|
||||
time_str = f"{hours} 小时 {minute}分钟"
|
||||
else:
|
||||
time_str = f"{minute} 分钟"
|
||||
if (
|
||||
not extra.get("ignore_prompt")
|
||||
and time != -1
|
||||
and ban_result
|
||||
and _flmt.check(user_id)
|
||||
):
|
||||
_flmt.start_cd(user_id)
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
At(flag="user", target=user_id),
|
||||
f"{ban_result}\n在..在 {time_str} 后才会理你喔",
|
||||
]
|
||||
).send()
|
||||
logger.debug("用户处于黑名单中...", "ban_hook")
|
||||
raise IgnoredException("用户处于黑名单中...")
|
||||
@@ -6,9 +6,12 @@ from zhenxun.configs.config import Config
|
||||
from zhenxun.models.bot_message_store import BotMessageStore
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import BotSentType
|
||||
from zhenxun.utils.log_sanitizer import sanitize_for_logging
|
||||
from zhenxun.utils.manager.message_manager import MessageManager
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
LOG_COMMAND = "MessageHook"
|
||||
|
||||
|
||||
def replace_message(message: Message) -> str:
|
||||
"""将消息中的at、image、record、face替换为字符串
|
||||
@@ -50,15 +53,14 @@ async def handle_api_result(
|
||||
message: Message = data.get("message", "")
|
||||
message_type = data.get("message_type")
|
||||
try:
|
||||
# 记录消息id
|
||||
if user_id and message_id:
|
||||
MessageManager.add(str(user_id), str(message_id))
|
||||
logger.debug(
|
||||
f"收集消息id,user_id: {user_id}, msg_id: {message_id}", "msg_hook"
|
||||
f"收集消息id,user_id: {user_id}, msg_id: {message_id}", LOG_COMMAND
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"收集消息id发生错误...data: {data}, result: {result}", "msg_hook", e=e
|
||||
f"收集消息id发生错误...data: {data}, result: {result}", LOG_COMMAND, e=e
|
||||
)
|
||||
if not Config.get_config("hook", "RECORD_BOT_SENT_MESSAGES"):
|
||||
return
|
||||
@@ -76,10 +78,11 @@ async def handle_api_result(
|
||||
else replace_message(message),
|
||||
platform=PlatformUtils.get_platform(bot),
|
||||
)
|
||||
logger.debug(f"消息发送记录,message: {message}")
|
||||
sanitized_message = sanitize_for_logging(message, context="nonebot_message")
|
||||
logger.debug(f"消息发送记录,message: {sanitized_message}")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"消息发送记录发生错误...data: {data}, result: {result}",
|
||||
"msg_hook",
|
||||
LOG_COMMAND,
|
||||
e=e,
|
||||
)
|
||||
|
||||
@@ -43,18 +43,20 @@ class BanCheckLimiter:
|
||||
|
||||
def check(self, key: str | float) -> bool:
|
||||
if time.time() - self.mtime[key] > self.default_check_time:
|
||||
self.mtime[key] = time.time()
|
||||
self.mint[key] = 0
|
||||
return False
|
||||
return self._extracted_from_check_3(key, False)
|
||||
if (
|
||||
self.mint[key] >= self.default_count
|
||||
and time.time() - self.mtime[key] < self.default_check_time
|
||||
):
|
||||
self.mtime[key] = time.time()
|
||||
self.mint[key] = 0
|
||||
return True
|
||||
return self._extracted_from_check_3(key, True)
|
||||
return False
|
||||
|
||||
# TODO Rename this here and in `check`
|
||||
def _extracted_from_check_3(self, key, arg1):
|
||||
self.mtime[key] = time.time()
|
||||
self.mint[key] = 0
|
||||
return arg1
|
||||
|
||||
|
||||
_blmt = BanCheckLimiter(
|
||||
malicious_check_time,
|
||||
@@ -70,16 +72,15 @@ async def _(
|
||||
module = None
|
||||
if plugin := matcher.plugin:
|
||||
module = plugin.module_name
|
||||
if metadata := plugin.metadata:
|
||||
extra = metadata.extra
|
||||
if extra.get("plugin_type") in [
|
||||
PluginType.HIDDEN,
|
||||
PluginType.DEPENDANT,
|
||||
PluginType.ADMIN,
|
||||
PluginType.SUPERUSER,
|
||||
]:
|
||||
return
|
||||
else:
|
||||
if not (metadata := plugin.metadata):
|
||||
return
|
||||
extra = metadata.extra
|
||||
if extra.get("plugin_type") in [
|
||||
PluginType.HIDDEN,
|
||||
PluginType.DEPENDANT,
|
||||
PluginType.ADMIN,
|
||||
PluginType.SUPERUSER,
|
||||
]:
|
||||
return
|
||||
if matcher.type == "notice":
|
||||
return
|
||||
@@ -88,27 +89,31 @@ async def _(
|
||||
malicious_ban_time = Config.get_config("hook", "MALICIOUS_BAN_TIME")
|
||||
if not malicious_ban_time:
|
||||
raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_TIME] 为空或小于0")
|
||||
if user_id:
|
||||
if module:
|
||||
if _blmt.check(f"{user_id}__{module}"):
|
||||
await BanConsole.ban(
|
||||
user_id, group_id, 9, malicious_ban_time * 60, bot.self_id
|
||||
)
|
||||
logger.info(
|
||||
f"触发了恶意触发检测: {matcher.plugin_name}",
|
||||
"HOOK",
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
At(flag="user", target=user_id),
|
||||
"检测到恶意触发命令,您将被封禁 30 分钟",
|
||||
]
|
||||
).send()
|
||||
logger.debug(
|
||||
f"触发了恶意触发检测: {matcher.plugin_name}",
|
||||
"HOOK",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("检测到恶意触发命令")
|
||||
_blmt.add(f"{user_id}__{module}")
|
||||
if user_id and module:
|
||||
if _blmt.check(f"{user_id}__{module}"):
|
||||
await BanConsole.ban(
|
||||
user_id,
|
||||
group_id,
|
||||
9,
|
||||
"恶意触发命令检测",
|
||||
malicious_ban_time * 60,
|
||||
bot.self_id,
|
||||
)
|
||||
logger.info(
|
||||
f"触发了恶意触发检测: {matcher.plugin_name}",
|
||||
"HOOK",
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
At(flag="user", target=user_id),
|
||||
"检测到恶意触发命令,您将被封禁 30 分钟",
|
||||
]
|
||||
).send()
|
||||
logger.debug(
|
||||
f"触发了恶意触发检测: {matcher.plugin_name}",
|
||||
"HOOK",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("检测到恶意触发命令")
|
||||
_blmt.add(f"{user_id}__{module}")
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.message import run_postprocessor
|
||||
|
||||
from zhenxun.utils.limiters import ConcurrencyLimiter
|
||||
|
||||
|
||||
@run_postprocessor
|
||||
async def _concurrency_release_hook(matcher: Matcher):
|
||||
"""
|
||||
后处理器:在事件处理结束后,释放并发限制的信号量。
|
||||
"""
|
||||
if concurrency_info := matcher.state.get("_concurrency_limiter_info"):
|
||||
limiter: ConcurrencyLimiter = concurrency_info["limiter"]
|
||||
key = concurrency_info["key"]
|
||||
limiter.release(key)
|
||||
@@ -1,17 +1,17 @@
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
|
||||
from nonebot_plugin_htmlrender import template_to_pic
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from tortoise.expressions import RawSQL
|
||||
from tortoise.functions import Count
|
||||
|
||||
from zhenxun.configs.path_config import TEMPLATE_PATH
|
||||
from zhenxun import ui
|
||||
from zhenxun.models.chat_history import ChatHistory
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.sign_user import SignUser
|
||||
from zhenxun.models.statistics import Statistics
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services import avatar_service
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
RACE = [
|
||||
@@ -90,7 +90,7 @@ def get_level(impression: float) -> int:
|
||||
|
||||
async def get_chat_history(
|
||||
user_id: str, group_id: str | None
|
||||
) -> tuple[list[str], list[str]]:
|
||||
) -> tuple[list[str], list[int]]:
|
||||
"""获取用户聊天记录
|
||||
|
||||
参数:
|
||||
@@ -98,11 +98,11 @@ async def get_chat_history(
|
||||
group_id: 群id
|
||||
|
||||
返回:
|
||||
tuple[list[str], list[str]]: 日期列表, 次数列表
|
||||
tuple[list[str], list[int]]: 日期列表, 次数列表
|
||||
|
||||
"""
|
||||
now = datetime.now()
|
||||
filter_date = now - timedelta(days=7, hours=now.hour, minutes=now.minute)
|
||||
filter_date = now - timedelta(days=7)
|
||||
date_list = (
|
||||
await ChatHistory.filter(
|
||||
user_id=user_id, group_id=group_id, create_time__gte=filter_date
|
||||
@@ -111,19 +111,15 @@ async def get_chat_history(
|
||||
.group_by("date")
|
||||
.values("date", "count")
|
||||
)
|
||||
chart_date = []
|
||||
count_list = []
|
||||
date2cnt = {str(date["date"]): date["count"] for date in date_list}
|
||||
date = now.date()
|
||||
chart_date: list[str] = []
|
||||
count_list: list[int] = []
|
||||
date2cnt = {str(item["date"]): item["count"] for item in date_list}
|
||||
current_date = now.date()
|
||||
for _ in range(7):
|
||||
if str(date) in date2cnt:
|
||||
count_list.append(date2cnt[str(date)])
|
||||
else:
|
||||
count_list.append(0)
|
||||
chart_date.append(str(date))
|
||||
date -= timedelta(days=1)
|
||||
for c in chart_date:
|
||||
chart_date[chart_date.index(c)] = c[5:]
|
||||
date_str = str(current_date)
|
||||
count_list.append(date2cnt.get(date_str, 0))
|
||||
chart_date.append(date_str[5:])
|
||||
current_date -= timedelta(days=1)
|
||||
chart_date.reverse()
|
||||
count_list.reverse()
|
||||
return chart_date, count_list
|
||||
@@ -136,7 +132,6 @@ async def get_user_info(
|
||||
|
||||
参数:
|
||||
session: Uninfo
|
||||
bot: Bot
|
||||
user_id: 用户id
|
||||
group_id: 群id
|
||||
nickname: 用户昵称
|
||||
@@ -145,50 +140,62 @@ async def get_user_info(
|
||||
bytes: 图片数据
|
||||
"""
|
||||
platform = PlatformUtils.get_platform(session) or "qq"
|
||||
ava_url = PlatformUtils.get_user_avatar_url(user_id, platform, session.self_id)
|
||||
avatar_path = await avatar_service.get_avatar_path(platform, user_id)
|
||||
avatar_url = avatar_path.as_uri() if avatar_path else ""
|
||||
|
||||
user = await UserConsole.get_user(user_id, platform)
|
||||
level = await LevelUser.get_user_level(user_id, group_id)
|
||||
permission_level = await LevelUser.get_user_level(user_id, group_id)
|
||||
|
||||
sign_level = 0
|
||||
if sign_user := await SignUser.get_or_none(user_id=user_id):
|
||||
sign_level = get_level(float(sign_user.impression))
|
||||
|
||||
chat_count = await ChatHistory.filter(user_id=user_id, group_id=group_id).count()
|
||||
stat_count = await Statistics.filter(user_id=user_id, group_id=group_id).count()
|
||||
select_index = ["" for _ in range(9)]
|
||||
select_index[sign_level] = "select"
|
||||
|
||||
selected_indices = [""] * 9
|
||||
selected_indices[sign_level] = "select"
|
||||
|
||||
uid = f"{user.uid}".rjust(8, "0")
|
||||
uid = f"{uid[:4]} {uid[4:]}"
|
||||
uid_formatted = f"{uid[:4]} {uid[4:]}"
|
||||
|
||||
now = datetime.now()
|
||||
weather = "moon" if now.hour < 6 or now.hour > 19 else "sun"
|
||||
chart_date, count_list = await get_chat_history(user_id, group_id)
|
||||
data = {
|
||||
"date": now.date(),
|
||||
"weather": weather,
|
||||
"ava_url": ava_url,
|
||||
"nickname": nickname,
|
||||
"title": "勇 者",
|
||||
"race": random.choice(RACE),
|
||||
"sex": random.choice(SEX),
|
||||
"occ": random.choice(OCC),
|
||||
"uid": uid,
|
||||
"description": "这是一个传奇的故事,"
|
||||
"人类的赞歌是勇气的赞歌,人类的伟大是勇气的伟译。",
|
||||
"sign_level": sign_level,
|
||||
"level": level,
|
||||
"gold": user.gold,
|
||||
"prop": len(user.props),
|
||||
"call": stat_count,
|
||||
"say": chat_count,
|
||||
"select_index": select_index,
|
||||
"chart_date": chart_date,
|
||||
"count_list": count_list,
|
||||
}
|
||||
return await template_to_pic(
|
||||
template_path=str((TEMPLATE_PATH / "my_info").absolute()),
|
||||
template_name="main.html",
|
||||
templates={"data": data},
|
||||
pages={
|
||||
"viewport": {"width": 1754, "height": 1240},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
weather_icon_name = "moon" if now.hour < 6 or now.hour > 19 else "sun"
|
||||
|
||||
chart_labels, chart_data = await get_chat_history(user_id, group_id)
|
||||
|
||||
profile_data = {
|
||||
"page": {
|
||||
"date": str(now.date()),
|
||||
"weather_icon_name": weather_icon_name,
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
"info": {
|
||||
"avatar_url": avatar_url,
|
||||
"nickname": nickname,
|
||||
"title": "勇 者",
|
||||
"race": random.choice(RACE),
|
||||
"sex": random.choice(SEX),
|
||||
"occupation": random.choice(OCC),
|
||||
"uid": uid_formatted,
|
||||
"description": (
|
||||
"这是一个传奇的故事,人类的赞歌是勇气的赞歌,人类的伟大是勇气的伟大"
|
||||
),
|
||||
},
|
||||
"stats": {
|
||||
"gold": user.gold,
|
||||
"prop_count": len(user.props),
|
||||
"call_count": stat_count,
|
||||
"chat_count": chat_count,
|
||||
},
|
||||
"favorability": {
|
||||
"level": sign_level,
|
||||
"selected_indices": selected_indices,
|
||||
},
|
||||
"permission_level": permission_level,
|
||||
"chart": {
|
||||
"labels": chart_labels,
|
||||
"data": chart_data,
|
||||
},
|
||||
}
|
||||
|
||||
return await ui.render_template("pages/builtin/my_info", data=profile_data)
|
||||
|
||||
@@ -4,15 +4,27 @@ import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.cache import CacheException
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
|
||||
|
||||
try:
|
||||
from .__init_cache import register_cache_types
|
||||
except CacheException as e:
|
||||
raise SystemError(f"ERROR:{e}")
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
register_cache_types()
|
||||
logger.info("缓存类型注册完成")
|
||||
|
||||
|
||||
@driver.on_bot_connect
|
||||
async def _(bot: Bot):
|
||||
"""将bot已存在的群组添加群认证
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
缓存初始化模块
|
||||
|
||||
负责注册各种缓存类型,实现按需缓存机制
|
||||
"""
|
||||
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.cache import CacheRegistry, cache_config
|
||||
from zhenxun.services.cache.config import CacheMode
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import CacheType
|
||||
|
||||
|
||||
# 注册缓存类型
|
||||
def register_cache_types():
|
||||
"""注册所有缓存类型"""
|
||||
CacheRegistry.register(CacheType.PLUGINS, PluginInfo)
|
||||
CacheRegistry.register(CacheType.GROUPS, GroupConsole)
|
||||
CacheRegistry.register(CacheType.BOT, BotConsole)
|
||||
CacheRegistry.register(CacheType.USERS, UserConsole)
|
||||
CacheRegistry.register(
|
||||
CacheType.LEVEL, LevelUser, key_format="{user_id}_{group_id}"
|
||||
)
|
||||
CacheRegistry.register(CacheType.BAN, BanConsole, key_format="{user_id}_{group_id}")
|
||||
|
||||
if cache_config.cache_mode == CacheMode.NONE:
|
||||
logger.info("缓存功能已禁用,将直接从数据库获取数据")
|
||||
else:
|
||||
logger.info(f"已注册所有缓存类型,缓存模式: {cache_config.cache_mode}")
|
||||
logger.info("使用增量缓存模式,数据将按需加载到缓存中")
|
||||
@@ -46,7 +46,7 @@ def _handle_config(plugin: Plugin, exists_module: list[str]):
|
||||
reg_config.value,
|
||||
help=reg_config.help,
|
||||
default_value=reg_config.default_value,
|
||||
type=reg_config.type,
|
||||
type=reg_config.type, # type: ignore
|
||||
arg_parser=reg_config.arg_parser,
|
||||
_override=False,
|
||||
)
|
||||
@@ -58,7 +58,7 @@ def _generate_simple_config(exists_module: list[str]):
|
||||
生成简易配置
|
||||
|
||||
异常:
|
||||
AttributeError: _description_
|
||||
AttributeError: AttributeError
|
||||
"""
|
||||
# 读取用户配置
|
||||
_data = {}
|
||||
@@ -74,7 +74,9 @@ def _generate_simple_config(exists_module: list[str]):
|
||||
if _data.get(module) and k in _data[module].keys():
|
||||
Config.set_config(module, k, _data[module][k])
|
||||
if f"{module}:{k}".lower() in exists_module:
|
||||
_tmp_data[module][k] = Config.get_config(module, k)
|
||||
_tmp_data[module][k] = Config.get_config(
|
||||
module, k, build_model=False
|
||||
)
|
||||
except AttributeError as e:
|
||||
raise AttributeError(f"{e}\n可能为config.yaml配置文件填写不规范") from e
|
||||
if not _tmp_data[module]:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import asyncio
|
||||
|
||||
import aiofiles
|
||||
import nonebot
|
||||
from nonebot import get_loaded_plugins
|
||||
@@ -112,24 +114,29 @@ async def _():
|
||||
await _handle_setting(plugin, plugin_list, limit_list)
|
||||
create_list = []
|
||||
update_list = []
|
||||
update_task_list = []
|
||||
for plugin in plugin_list:
|
||||
if plugin.module_path not in module2id:
|
||||
create_list.append(plugin)
|
||||
else:
|
||||
plugin.id = module2id[plugin.module_path]
|
||||
await plugin.save(
|
||||
update_fields=[
|
||||
"name",
|
||||
"author",
|
||||
"version",
|
||||
"admin_level",
|
||||
"plugin_type",
|
||||
"is_show",
|
||||
]
|
||||
update_task_list.append(
|
||||
plugin.save(
|
||||
update_fields=[
|
||||
"name",
|
||||
"author",
|
||||
"version",
|
||||
"admin_level",
|
||||
"plugin_type",
|
||||
"is_show",
|
||||
]
|
||||
)
|
||||
)
|
||||
update_list.append(plugin)
|
||||
if create_list:
|
||||
await PluginInfo.bulk_create(create_list, 10)
|
||||
if update_task_list:
|
||||
await asyncio.gather(*update_task_list)
|
||||
# if update_list:
|
||||
# # TODO: 批量更新无法更新plugin_type: tortoise.exceptions.OperationalError:
|
||||
# column "superuser" does not exist
|
||||
|
||||
@@ -205,7 +205,7 @@ class Manager:
|
||||
self.cd_data: dict[str, PluginCdBlock] = {}
|
||||
if self.cd_file.exists():
|
||||
with open(self.cd_file, encoding="utf8") as f:
|
||||
temp = _yaml.load(f)
|
||||
temp = _yaml.load(f) or {}
|
||||
if "PluginCdLimit" in temp.keys():
|
||||
for k, v in temp["PluginCdLimit"].items():
|
||||
if "." in k:
|
||||
@@ -216,7 +216,7 @@ class Manager:
|
||||
self.block_data: dict[str, BaseBlock] = {}
|
||||
if self.block_file.exists():
|
||||
with open(self.block_file, encoding="utf8") as f:
|
||||
temp = _yaml.load(f)
|
||||
temp = _yaml.load(f) or {}
|
||||
if "PluginBlockLimit" in temp.keys():
|
||||
for k, v in temp["PluginBlockLimit"].items():
|
||||
if "." in k:
|
||||
@@ -227,7 +227,7 @@ class Manager:
|
||||
self.count_data: dict[str, PluginCountBlock] = {}
|
||||
if self.count_file.exists():
|
||||
with open(self.count_file, encoding="utf8") as f:
|
||||
temp = _yaml.load(f)
|
||||
temp = _yaml.load(f) or {}
|
||||
if "PluginCountLimit" in temp.keys():
|
||||
for k, v in temp["PluginCountLimit"].items():
|
||||
if "." in k:
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
Args,
|
||||
Arparma,
|
||||
Match,
|
||||
Option,
|
||||
Query,
|
||||
Subcommand,
|
||||
on_alconna,
|
||||
store_true,
|
||||
)
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .data_source import DataSource
|
||||
from .presenters import Presenters
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="LLM模型管理",
|
||||
description="查看和管理大语言模型服务。",
|
||||
usage="""
|
||||
LLM模型管理 (SUPERUSER)
|
||||
|
||||
llm list [--all]
|
||||
- 查看可用模型列表。
|
||||
- --all: 显示包括不可用在内的所有模型。
|
||||
|
||||
llm info <Provider/ModelName>
|
||||
- 查看指定模型的详细信息和能力。
|
||||
|
||||
llm default [Provider/ModelName]
|
||||
- 查看或设置全局默认模型。
|
||||
- 不带参数: 查看当前默认模型。
|
||||
- 带参数: 设置新的默认模型。
|
||||
- 例子: llm default Gemini/gemini-2.0-flash
|
||||
|
||||
llm test <Provider/ModelName>
|
||||
- 测试指定模型的连通性和API Key有效性。
|
||||
|
||||
llm keys <ProviderName>
|
||||
- 查看指定提供商的所有API Key状态。
|
||||
|
||||
llm reset-key <ProviderName> [--key <api_key>]
|
||||
- 重置提供商的所有或指定API Key的失败状态。
|
||||
""",
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="1.0.0",
|
||||
plugin_type=PluginType.SUPERUSER,
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
llm_cmd = on_alconna(
|
||||
Alconna(
|
||||
"llm",
|
||||
Subcommand(
|
||||
"list",
|
||||
Option("--text", action=store_true, help_text="以纯文本格式输出模型列表"),
|
||||
alias=["ls"],
|
||||
help_text="查看模型列表",
|
||||
),
|
||||
Subcommand("info", Args["model_name", str], help_text="查看模型详情"),
|
||||
Subcommand("default", Args["model_name?", str], help_text="查看或设置默认模型"),
|
||||
Subcommand(
|
||||
"test", Args["model_name", str], alias=["ping"], help_text="测试模型连通性"
|
||||
),
|
||||
Subcommand("keys", Args["provider_name", str], help_text="查看API密钥状态"),
|
||||
Subcommand(
|
||||
"reset-key",
|
||||
Args["provider_name", str],
|
||||
Option("--key", Args["api_key", str], help_text="指定要重置的API Key"),
|
||||
help_text="重置API Key状态",
|
||||
),
|
||||
Option("--all", action=store_true, help_text="显示所有条目"),
|
||||
),
|
||||
permission=SUPERUSER,
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
|
||||
@llm_cmd.assign("list")
|
||||
async def handle_list(
|
||||
arp: Arparma,
|
||||
show_all: Query[bool] = Query("all"),
|
||||
text_mode: Query[bool] = Query("list.text.value", False),
|
||||
):
|
||||
"""处理 'llm list' 命令"""
|
||||
logger.info("获取LLM模型列表", command="LLM Manage", session=arp.header_result)
|
||||
models = await DataSource.get_model_list(show_all=show_all.result)
|
||||
|
||||
if text_mode.result:
|
||||
if not models:
|
||||
await llm_cmd.finish("当前没有配置任何LLM模型。")
|
||||
|
||||
grouped_models = defaultdict(list)
|
||||
for model in models:
|
||||
grouped_models[model["provider_name"]].append(model)
|
||||
|
||||
response_parts = ["可用的LLM模型列表:"]
|
||||
for provider, model_list in grouped_models.items():
|
||||
response_parts.append(f"\n{provider}:")
|
||||
for model in model_list:
|
||||
response_parts.append(
|
||||
f" {model['provider_name']}/{model['model_name']}"
|
||||
)
|
||||
|
||||
response_text = "\n".join(response_parts)
|
||||
await llm_cmd.finish(response_text)
|
||||
else:
|
||||
image = await Presenters.format_model_list_as_image(models, show_all.result)
|
||||
await llm_cmd.finish(MessageUtils.build_message(image))
|
||||
|
||||
|
||||
@llm_cmd.assign("info")
|
||||
async def handle_info(arp: Arparma, model_name: Match[str]):
|
||||
"""处理 'llm info' 命令"""
|
||||
logger.info(
|
||||
f"获取模型详情: {model_name.result}",
|
||||
command="LLM Manage",
|
||||
session=arp.header_result,
|
||||
)
|
||||
details = await DataSource.get_model_details(model_name.result)
|
||||
if not details:
|
||||
await llm_cmd.finish(f"未找到模型: {model_name.result}")
|
||||
|
||||
image_bytes = await Presenters.format_model_details_as_markdown_image(details)
|
||||
await llm_cmd.finish(MessageUtils.build_message(image_bytes))
|
||||
|
||||
|
||||
@llm_cmd.assign("default")
|
||||
async def handle_default(arp: Arparma, model_name: Match[str]):
|
||||
"""处理 'llm default' 命令"""
|
||||
if model_name.available:
|
||||
logger.info(
|
||||
f"设置默认模型为: {model_name.result}",
|
||||
command="LLM Manage",
|
||||
session=arp.header_result,
|
||||
)
|
||||
_success, message = await DataSource.set_default_model(model_name.result)
|
||||
await llm_cmd.finish(message)
|
||||
else:
|
||||
logger.info("查看默认模型", command="LLM Manage", session=arp.header_result)
|
||||
current_default = await DataSource.get_default_model()
|
||||
await llm_cmd.finish(f"当前全局默认模型为: {current_default or '未设置'}")
|
||||
|
||||
|
||||
@llm_cmd.assign("test")
|
||||
async def handle_test(arp: Arparma, model_name: Match[str]):
|
||||
"""处理 'llm test' 命令"""
|
||||
logger.info(
|
||||
f"测试模型连通性: {model_name.result}",
|
||||
command="LLM Manage",
|
||||
session=arp.header_result,
|
||||
)
|
||||
await llm_cmd.send(f"正在测试模型 '{model_name.result}',请稍候...")
|
||||
|
||||
_success, message = await DataSource.test_model_connectivity(model_name.result)
|
||||
await llm_cmd.finish(message)
|
||||
|
||||
|
||||
@llm_cmd.assign("keys")
|
||||
async def handle_keys(arp: Arparma, provider_name: Match[str]):
|
||||
"""处理 'llm keys' 命令"""
|
||||
logger.info(
|
||||
f"查看提供商API Key状态: {provider_name.result}",
|
||||
command="LLM Manage",
|
||||
session=arp.header_result,
|
||||
)
|
||||
sorted_stats = await DataSource.get_key_status(provider_name.result)
|
||||
if not sorted_stats:
|
||||
await llm_cmd.finish(
|
||||
f"未找到提供商 '{provider_name.result}' 或其没有配置API Keys。"
|
||||
)
|
||||
|
||||
image = await Presenters.format_key_status_as_image(
|
||||
provider_name.result, sorted_stats
|
||||
)
|
||||
await llm_cmd.finish(MessageUtils.build_message(image))
|
||||
|
||||
|
||||
@llm_cmd.assign("reset-key")
|
||||
async def handle_reset_key(
|
||||
arp: Arparma, provider_name: Match[str], api_key: Match[str]
|
||||
):
|
||||
"""处理 'llm reset-key' 命令"""
|
||||
key_to_reset = api_key.result if api_key.available else None
|
||||
log_msg = f"重置 {provider_name.result} 的 " + (
|
||||
"指定API Key" if key_to_reset else "所有API Keys"
|
||||
)
|
||||
logger.info(log_msg, command="LLM Manage", session=arp.header_result)
|
||||
|
||||
_success, message = await DataSource.reset_key(provider_name.result, key_to_reset)
|
||||
await llm_cmd.finish(message)
|
||||
@@ -0,0 +1,121 @@
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services.llm import (
|
||||
LLMException,
|
||||
get_global_default_model_name,
|
||||
get_model_instance,
|
||||
list_available_models,
|
||||
set_global_default_model_name,
|
||||
)
|
||||
from zhenxun.services.llm.core import KeyStatus
|
||||
from zhenxun.services.llm.manager import (
|
||||
reset_key_status,
|
||||
)
|
||||
from zhenxun.services.llm.types import LLMMessage
|
||||
|
||||
|
||||
class DataSource:
|
||||
"""LLM管理插件的数据源和业务逻辑"""
|
||||
|
||||
@staticmethod
|
||||
async def get_model_list(show_all: bool = False) -> list[dict[str, Any]]:
|
||||
"""获取模型列表"""
|
||||
models = list_available_models()
|
||||
if show_all:
|
||||
return models
|
||||
return [m for m in models if m.get("is_available", True)]
|
||||
|
||||
@staticmethod
|
||||
async def get_model_details(model_name_str: str) -> dict[str, Any] | None:
|
||||
"""获取指定模型的详细信息"""
|
||||
try:
|
||||
model = await get_model_instance(model_name_str)
|
||||
return {
|
||||
"provider_config": model.provider_config,
|
||||
"model_detail": model.model_detail,
|
||||
"capabilities": model.capabilities,
|
||||
}
|
||||
except LLMException:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def get_default_model() -> str | None:
|
||||
"""获取全局默认模型"""
|
||||
return get_global_default_model_name()
|
||||
|
||||
@staticmethod
|
||||
async def set_default_model(model_name_str: str) -> tuple[bool, str]:
|
||||
"""设置全局默认模型"""
|
||||
success = set_global_default_model_name(model_name_str)
|
||||
if success:
|
||||
return True, f"✅ 成功将默认模型设置为: {model_name_str}"
|
||||
else:
|
||||
return False, f"❌ 设置失败,模型 '{model_name_str}' 不存在或无效。"
|
||||
|
||||
@staticmethod
|
||||
async def test_model_connectivity(model_name_str: str) -> tuple[bool, str]:
|
||||
"""测试模型连通性"""
|
||||
start_time = time.monotonic()
|
||||
try:
|
||||
async with await get_model_instance(model_name_str) as model:
|
||||
await model.generate_response([LLMMessage.user("你好")])
|
||||
end_time = time.monotonic()
|
||||
latency = (end_time - start_time) * 1000
|
||||
return (
|
||||
True,
|
||||
f"✅ 模型 '{model_name_str}' 连接成功!\n响应延迟: {latency:.2f} ms",
|
||||
)
|
||||
except LLMException as e:
|
||||
return (
|
||||
False,
|
||||
f"❌ 模型 '{model_name_str}' 连接测试失败:\n"
|
||||
f"{e.user_friendly_message}\n错误码: {e.code.name}",
|
||||
)
|
||||
except Exception as e:
|
||||
return False, f"❌ 测试时发生未知错误: {e!s}"
|
||||
|
||||
@staticmethod
|
||||
async def get_key_status(provider_name: str) -> list[dict[str, Any]] | None:
|
||||
"""获取并排序指定提供商的API Key状态"""
|
||||
from zhenxun.services.llm.manager import get_key_usage_stats
|
||||
|
||||
all_stats = await get_key_usage_stats()
|
||||
provider_stats = all_stats.get(provider_name)
|
||||
|
||||
if not provider_stats or not provider_stats.get("key_stats"):
|
||||
return None
|
||||
|
||||
key_stats_dict = provider_stats["key_stats"]
|
||||
|
||||
stats_list = [
|
||||
{"key_id": key_id, **stats} for key_id, stats in key_stats_dict.items()
|
||||
]
|
||||
|
||||
def sort_key(item: dict[str, Any]):
|
||||
status_priority = item.get("status_enum", KeyStatus.UNUSED).value
|
||||
return (
|
||||
status_priority,
|
||||
100 - item.get("success_rate", 100.0),
|
||||
-item.get("total_calls", 0),
|
||||
)
|
||||
|
||||
sorted_stats_list = sorted(stats_list, key=sort_key)
|
||||
|
||||
return sorted_stats_list
|
||||
|
||||
@staticmethod
|
||||
async def reset_key(provider_name: str, api_key: str | None) -> tuple[bool, str]:
|
||||
"""重置API Key状态"""
|
||||
success = await reset_key_status(provider_name, api_key)
|
||||
if success:
|
||||
if api_key:
|
||||
if len(api_key) > 8:
|
||||
target = f"API Key '{api_key[:4]}...{api_key[-4:]}'"
|
||||
else:
|
||||
target = f"API Key '{api_key}'"
|
||||
else:
|
||||
target = "所有API Keys"
|
||||
return True, f"✅ 成功重置提供商 '{provider_name}' 的 {target} 的状态。"
|
||||
else:
|
||||
return False, "❌ 重置失败,请检查提供商名称或API Key是否正确。"
|
||||
@@ -0,0 +1,185 @@
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services import renderer_service
|
||||
from zhenxun.services.llm.core import KeyStatus
|
||||
from zhenxun.services.llm.types import ModelModality
|
||||
from zhenxun.ui.builders import MarkdownBuilder, TableBuilder
|
||||
from zhenxun.ui.models import StatusBadgeCell, TextCell
|
||||
|
||||
|
||||
def _format_seconds(seconds: int) -> str:
|
||||
"""将秒数格式化为 'Xm Ys' 或 'Xh Ym' 的形式"""
|
||||
if seconds <= 0:
|
||||
return "0s"
|
||||
if seconds < 60:
|
||||
return f"{seconds}s"
|
||||
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
if minutes < 60:
|
||||
return f"{minutes}m {seconds}s"
|
||||
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
return f"{hours}h {minutes}m"
|
||||
|
||||
|
||||
class Presenters:
|
||||
"""格式化LLM管理插件的输出 (图片格式)"""
|
||||
|
||||
@staticmethod
|
||||
async def format_model_list_as_image(
|
||||
models: list[dict[str, Any]], show_all: bool
|
||||
) -> bytes:
|
||||
"""将模型列表格式化为表格图片"""
|
||||
title = "LLM模型列表" + (" (所有已配置模型)" if show_all else " (仅可用)")
|
||||
|
||||
if not models:
|
||||
builder = TableBuilder(
|
||||
title=title, tip="当前没有配置任何LLM模型。"
|
||||
).set_headers(["提供商", "模型名称", "API类型", "状态"])
|
||||
return await renderer_service.render(builder.build())
|
||||
|
||||
column_name = ["提供商", "模型名称", "API类型", "状态"]
|
||||
rows_data = []
|
||||
for model in models:
|
||||
is_available = model.get("is_available", True)
|
||||
embed_tag = " (Embed)" if model.get("is_embedding_model", False) else ""
|
||||
rows_data.append(
|
||||
[
|
||||
TextCell(content=model.get("provider_name", "N/A")),
|
||||
TextCell(content=f"{model.get('model_name', 'N/A')}{embed_tag}"),
|
||||
TextCell(content=model.get("api_type", "N/A")),
|
||||
StatusBadgeCell(
|
||||
text="可用" if is_available else "不可用",
|
||||
status_type="ok" if is_available else "error",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
builder = TableBuilder(
|
||||
title=title, tip="使用 `llm info <Provider/ModelName>` 查看详情"
|
||||
)
|
||||
builder.set_headers(column_name)
|
||||
builder.set_column_alignments(["left", "left", "left", "center"])
|
||||
builder.add_rows(rows_data)
|
||||
return await renderer_service.render(builder.build(), use_cache=True)
|
||||
|
||||
@staticmethod
|
||||
async def format_model_details_as_markdown_image(details: dict[str, Any]) -> bytes:
|
||||
"""将模型详情格式化为Markdown图片"""
|
||||
provider = details["provider_config"]
|
||||
model = details["model_detail"]
|
||||
caps = details["capabilities"]
|
||||
|
||||
cap_list = []
|
||||
if ModelModality.IMAGE in caps.input_modalities:
|
||||
cap_list.append("视觉")
|
||||
if ModelModality.VIDEO in caps.input_modalities:
|
||||
cap_list.append("视频")
|
||||
if ModelModality.AUDIO in caps.input_modalities:
|
||||
cap_list.append("音频")
|
||||
if caps.supports_tool_calling:
|
||||
cap_list.append("工具调用")
|
||||
if caps.is_embedding_model:
|
||||
cap_list.append("文本嵌入")
|
||||
|
||||
builder = MarkdownBuilder()
|
||||
builder.head(f"🔎 模型详情: {provider.name}/{model.model_name}", 1)
|
||||
builder.text("---")
|
||||
builder.head("提供商信息", 2)
|
||||
builder.text(f"- **名称**: {provider.name}")
|
||||
builder.text(f"- **API 类型**: {provider.api_type}")
|
||||
builder.text(f"- **API Base**: {provider.api_base or '默认'}")
|
||||
|
||||
builder.head("模型详情", 2)
|
||||
|
||||
temp_value = model.temperature or provider.temperature or "未设置"
|
||||
token_value = model.max_tokens or provider.max_tokens or "未设置"
|
||||
|
||||
builder.text(f"- **名称**: {model.model_name}")
|
||||
builder.text(f"- **默认温度**: {temp_value}")
|
||||
builder.text(f"- **最大Token**: {token_value}")
|
||||
builder.text(f"- **核心能力**: {', '.join(cap_list) or '纯文本'}")
|
||||
|
||||
return await renderer_service.render(builder.with_style("light").build())
|
||||
|
||||
@staticmethod
|
||||
async def format_key_status_as_image(
|
||||
provider_name: str, sorted_stats: list[dict[str, Any]]
|
||||
) -> bytes:
|
||||
"""将已排序的、详细的API Key状态格式化为表格图片"""
|
||||
title = f"🔑 '{provider_name}' API Key 状态"
|
||||
|
||||
data_list = []
|
||||
|
||||
for key_info in sorted_stats:
|
||||
status_enum: KeyStatus = key_info["status_enum"]
|
||||
|
||||
if status_enum == KeyStatus.COOLDOWN:
|
||||
cooldown_seconds = int(key_info["cooldown_seconds_left"])
|
||||
formatted_time = _format_seconds(cooldown_seconds)
|
||||
status_cell = StatusBadgeCell(
|
||||
text=f"冷却中({formatted_time})", status_type="info"
|
||||
)
|
||||
else:
|
||||
status_map = {
|
||||
KeyStatus.DISABLED: ("永久禁用", "error"),
|
||||
KeyStatus.ERROR: ("错误", "error"),
|
||||
KeyStatus.WARNING: ("告警", "warning"),
|
||||
KeyStatus.HEALTHY: ("健康", "ok"),
|
||||
KeyStatus.UNUSED: ("未使用", "info"),
|
||||
}
|
||||
text, status_type = status_map.get(status_enum, ("未知", "info"))
|
||||
status_cell = StatusBadgeCell(text=text, status_type=status_type) # type: ignore
|
||||
|
||||
total_calls = key_info["total_calls"]
|
||||
total_calls_text = (
|
||||
f"{key_info['success_count']}/{total_calls}"
|
||||
if total_calls > 0
|
||||
else "0/0"
|
||||
)
|
||||
|
||||
success_rate = key_info["success_rate"]
|
||||
success_rate_text = f"{success_rate:.1f}%" if total_calls > 0 else "N/A"
|
||||
rate_color = None
|
||||
if total_calls > 0:
|
||||
if success_rate < 80:
|
||||
rate_color = "#F56C6C"
|
||||
elif success_rate < 95:
|
||||
rate_color = "#E6A23C"
|
||||
success_rate_cell = TextCell(content=success_rate_text, color=rate_color)
|
||||
|
||||
avg_latency = key_info["avg_latency"]
|
||||
avg_latency_text = f"{avg_latency / 1000:.2f}" if avg_latency > 0 else "N/A"
|
||||
|
||||
last_error = key_info.get("last_error") or "-"
|
||||
if len(last_error) > 25:
|
||||
last_error = last_error[:22] + "..."
|
||||
|
||||
data_list.append(
|
||||
[
|
||||
TextCell(content=key_info["key_id"]),
|
||||
status_cell,
|
||||
TextCell(content=total_calls_text),
|
||||
success_rate_cell,
|
||||
TextCell(content=avg_latency_text),
|
||||
TextCell(content=last_error),
|
||||
TextCell(content=key_info["suggested_action"]),
|
||||
]
|
||||
)
|
||||
|
||||
builder = TableBuilder(
|
||||
title=title, tip="使用 `llm reset-key <Provider>` 重置Key状态"
|
||||
)
|
||||
builder.set_headers(
|
||||
[
|
||||
"Key (部分)",
|
||||
"状态",
|
||||
"总调用",
|
||||
"成功率",
|
||||
"平均延迟(s)",
|
||||
"上次错误",
|
||||
"建议操作",
|
||||
]
|
||||
)
|
||||
builder.add_rows(data_list)
|
||||
return await renderer_service.render(builder.build(), use_cache=False)
|
||||
@@ -6,6 +6,7 @@ from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from nonebot_plugin_waiter import prompt_until
|
||||
|
||||
from zhenxun import ui
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.depends import UserName
|
||||
@@ -188,15 +189,33 @@ async def _(session: Uninfo, arparma: Arparma, amount: Match[int]):
|
||||
|
||||
@_matcher.assign("user-info")
|
||||
async def _(session: Uninfo, arparma: Arparma, uname: str = UserName()):
|
||||
result = await BankManager.get_user_info(session, uname)
|
||||
await MessageUtils.build_message(result).send()
|
||||
user_payload = await BankManager.get_user_info_data(session, uname)
|
||||
|
||||
render_data = {"page_type": "user", "payload": user_payload}
|
||||
|
||||
image_bytes = await ui.render_template(
|
||||
"pages/builtin/mahiro_bank",
|
||||
data=render_data,
|
||||
viewport={"width": 386, "height": 10},
|
||||
)
|
||||
|
||||
await MessageUtils.build_message(image_bytes).send()
|
||||
logger.info("查看银行个人信息", arparma.header_result, session=session)
|
||||
|
||||
|
||||
@_matcher.assign("bank-info")
|
||||
async def _(session: Uninfo, arparma: Arparma):
|
||||
result = await BankManager.get_bank_info()
|
||||
await MessageUtils.build_message(result).send()
|
||||
overview_payload = await BankManager.get_bank_info_data()
|
||||
|
||||
render_data = {"page_type": "overview", "payload": overview_payload}
|
||||
|
||||
image_bytes = await ui.render_template(
|
||||
"pages/builtin/mahiro_bank",
|
||||
data=render_data,
|
||||
viewport={"width": 450, "height": 10},
|
||||
)
|
||||
|
||||
await MessageUtils.build_message(image_bytes).send()
|
||||
logger.info("查看银行信息", arparma.header_result, session=session)
|
||||
|
||||
|
||||
|
||||
@@ -2,17 +2,16 @@ import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
|
||||
from nonebot_plugin_htmlrender import template_to_pic
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from tortoise.expressions import RawSQL
|
||||
from tortoise.functions import Count, Sum
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import TEMPLATE_PATH
|
||||
from zhenxun.models.mahiro_bank import MahiroBank
|
||||
from zhenxun.models.mahiro_bank_log import MahiroBankLog
|
||||
from zhenxun.models.sign_user import SignUser
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services import avatar_service
|
||||
from zhenxun.utils.enum import BankHandleType, GoldHandle
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
@@ -158,15 +157,15 @@ class BankManager:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def get_user_info(cls, session: Uninfo, uname: str) -> bytes:
|
||||
"""获取用户数据
|
||||
async def get_user_info_data(cls, session: Uninfo, uname: str) -> dict:
|
||||
"""获取用户数据(返回字典)
|
||||
|
||||
参数:
|
||||
session: Uninfo
|
||||
uname: 用户id
|
||||
|
||||
返回:
|
||||
bytes: 图片数据
|
||||
dict: 用户银行数据字典
|
||||
"""
|
||||
user_id = session.user.id
|
||||
user = await cls.get_user(user_id=user_id)
|
||||
@@ -199,9 +198,9 @@ class BankManager:
|
||||
deposit_list = [
|
||||
{
|
||||
"id": deposit.id,
|
||||
"date": now.date(),
|
||||
"date": str(now.date()),
|
||||
"start_time": str(deposit.create_time).split(".")[0],
|
||||
"end_time": end_time.replace(microsecond=0),
|
||||
"end_time": str(end_time.replace(microsecond=0)),
|
||||
"amount": deposit.amount,
|
||||
"rate": f"{deposit.rate * 100:.2f}",
|
||||
"projected_revenue": int(
|
||||
@@ -212,12 +211,12 @@ class BankManager:
|
||||
for deposit in user_today_deposit
|
||||
]
|
||||
platform = PlatformUtils.get_platform(session)
|
||||
data = {
|
||||
avatar_path = await avatar_service.get_avatar_path(platform, user_id)
|
||||
avatar_url = avatar_path.as_uri() if avatar_path else ""
|
||||
return {
|
||||
"name": uname,
|
||||
"rank": rank + 1,
|
||||
"avatar_url": PlatformUtils.get_user_avatar_url(
|
||||
user_id, platform, session.self_id
|
||||
),
|
||||
"avatar_url": avatar_url or "",
|
||||
"amount": user.amount,
|
||||
"deposit_count": deposit_count,
|
||||
"today_deposit_count": len(user_today_deposit),
|
||||
@@ -225,21 +224,16 @@ class BankManager:
|
||||
"projected_revenue": projected_revenue,
|
||||
"today_deposit_amount": today_deposit_amount,
|
||||
"deposit_list": deposit_list,
|
||||
"create_time": now.replace(microsecond=0),
|
||||
"create_time": str(now.replace(microsecond=0)),
|
||||
}
|
||||
return await template_to_pic(
|
||||
template_path=str((TEMPLATE_PATH / "mahiro_bank").absolute()),
|
||||
template_name="user.html",
|
||||
templates={"data": data},
|
||||
pages={
|
||||
"viewport": {"width": 386, "height": 700},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def get_bank_info(cls) -> bytes:
|
||||
async def get_bank_info_data(cls) -> dict:
|
||||
"""获取银行总览数据(返回字典)
|
||||
|
||||
返回:
|
||||
dict: 银行总览数据字典
|
||||
"""
|
||||
now = datetime.now()
|
||||
now_start = now - timedelta(
|
||||
hours=now.hour, minutes=now.minute, seconds=now.second
|
||||
@@ -293,27 +287,17 @@ class BankManager:
|
||||
if lasted_log:
|
||||
date = now.date() - lasted_log.create_time.date()
|
||||
date = (date.days or 1) + 1
|
||||
data = {
|
||||
"amount_sum": bank_data[0]["amount_sum"],
|
||||
"user_count": bank_data[0]["user_count"],
|
||||
return {
|
||||
"amount_sum": bank_data[0]["amount_sum"] or 0,
|
||||
"user_count": bank_data[0]["user_count"] or 0,
|
||||
"today_count": today_count,
|
||||
"day_amount": int(bank_data[0]["amount_sum"] / date),
|
||||
"day_amount": int((bank_data[0]["amount_sum"] or 0) / date),
|
||||
"interest_amount": interest_amount[0]["amount_sum"] or 0,
|
||||
"active_user_count": active_user_count[0]["count"] or 0,
|
||||
"e_data": e_date,
|
||||
"e_amount": e_amount,
|
||||
"create_time": now.replace(microsecond=0),
|
||||
"create_time": str(now.replace(microsecond=0)),
|
||||
}
|
||||
return await template_to_pic(
|
||||
template_path=str((TEMPLATE_PATH / "mahiro_bank").absolute()),
|
||||
template_name="bank.html",
|
||||
templates={"data": data},
|
||||
pages={
|
||||
"viewport": {"width": 450, "height": 750},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def deposit(
|
||||
@@ -406,7 +390,6 @@ class BankManager:
|
||||
bank_data[log.user_id].append(log)
|
||||
log_create_list = []
|
||||
log_update_list = []
|
||||
# 计算每日默认金币
|
||||
for bank_user in bank_user_list:
|
||||
if user := user_data.get(bank_user.user_id):
|
||||
amount = bank_user.amount
|
||||
@@ -414,7 +397,6 @@ class BankManager:
|
||||
amount -= sum(log.amount for log in logs)
|
||||
if not amount:
|
||||
continue
|
||||
# 计算每日默认金币
|
||||
gold = int(amount * bank_user.rate)
|
||||
user.gold += gold
|
||||
log_create_list.append(
|
||||
@@ -426,7 +408,6 @@ class BankManager:
|
||||
is_completed=True,
|
||||
)
|
||||
)
|
||||
# 计算每日存款金币
|
||||
for user_id, logs in bank_data.items():
|
||||
if user := user_data.get(user_id):
|
||||
for log in logs:
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from nonebot import on_regex
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.params import Depends, RegexGroup
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot_plugin_alconna import Alconna, Option, on_alconna, store_true
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
Args,
|
||||
Arparma,
|
||||
CommandMeta,
|
||||
Option,
|
||||
on_alconna,
|
||||
store_true,
|
||||
)
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
@@ -54,15 +59,22 @@ __plugin_meta__ = PluginMetadata(
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
_nickname_matcher = on_regex(
|
||||
"(?:以后)?(?:叫我|请叫我|称呼我)(.*)",
|
||||
_nickname_matcher = on_alconna(
|
||||
Alconna(
|
||||
"re:(?:以后)?(?:叫我|请叫我|称呼我)",
|
||||
Args["name?", str],
|
||||
meta=CommandMeta(compact=True),
|
||||
),
|
||||
rule=to_me(),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_global_nickname_matcher = on_regex(
|
||||
"设置全局昵称(.*)", rule=to_me(), priority=5, block=True
|
||||
_global_nickname_matcher = on_alconna(
|
||||
Alconna("设置全局昵称", Args["name?", str], meta=CommandMeta(compact=True)),
|
||||
rule=to_me(),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_matcher = on_alconna(
|
||||
@@ -117,34 +129,32 @@ CANCEL = [
|
||||
]
|
||||
|
||||
|
||||
def CheckNickname():
|
||||
async def CheckNickname(
|
||||
bot: Bot,
|
||||
session: Uninfo,
|
||||
params: Arparma,
|
||||
):
|
||||
"""
|
||||
检查名称是否合法
|
||||
"""
|
||||
|
||||
async def dependency(
|
||||
bot: Bot,
|
||||
session: Uninfo,
|
||||
reg_group: tuple[Any, ...] = RegexGroup(),
|
||||
):
|
||||
black_word = Config.get_config("nickname", "BLACK_WORD")
|
||||
(name,) = reg_group
|
||||
logger.debug(f"昵称检查: {name}", "昵称设置", session=session)
|
||||
if not name:
|
||||
await MessageUtils.build_message("叫你空白?叫你虚空?叫你无名??").finish(
|
||||
at_sender=True
|
||||
)
|
||||
if session.user.id in bot.config.superusers:
|
||||
logger.debug(
|
||||
f"超级用户设置昵称, 跳过合法检测: {name}", "昵称设置", session=session
|
||||
)
|
||||
return
|
||||
black_word = Config.get_config("nickname", "BLACK_WORD")
|
||||
name = params.query("name")
|
||||
logger.debug(f"昵称检查: {name}", "昵称设置", session=session)
|
||||
if not name:
|
||||
await MessageUtils.build_message("叫你空白?叫你虚空?叫你无名??").finish(
|
||||
at_sender=True
|
||||
)
|
||||
if session.user.id in bot.config.superusers:
|
||||
logger.debug(
|
||||
f"超级用户设置昵称, 跳过合法检测: {name}", "昵称设置", session=session
|
||||
)
|
||||
else:
|
||||
if len(name) > 20:
|
||||
await MessageUtils.build_message("昵称可不能超过20个字!").finish(
|
||||
at_sender=True
|
||||
)
|
||||
if name in bot.config.nickname:
|
||||
await MessageUtils.build_message("笨蛋!休想占用我的名字! #").finish(
|
||||
await MessageUtils.build_message("笨蛋!休想占用我的名字! ").finish(
|
||||
at_sender=True
|
||||
)
|
||||
if black_word:
|
||||
@@ -162,17 +172,17 @@ def CheckNickname():
|
||||
await MessageUtils.build_message(
|
||||
f"字符 [{word}] 为禁止字符!"
|
||||
).finish(at_sender=True)
|
||||
|
||||
return Depends(dependency)
|
||||
return name
|
||||
|
||||
|
||||
@_nickname_matcher.handle(parameterless=[CheckNickname()])
|
||||
@_nickname_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: Uninfo,
|
||||
name_: Arparma,
|
||||
uname: str = UserName(),
|
||||
reg_group: tuple[Any, ...] = RegexGroup(),
|
||||
):
|
||||
(name,) = reg_group
|
||||
name = await CheckNickname(bot, session, name_)
|
||||
if len(name) < 5 and random.random() < 0.3:
|
||||
name = "~".join(name)
|
||||
group_id = None
|
||||
@@ -200,13 +210,14 @@ async def _(
|
||||
)
|
||||
|
||||
|
||||
@_global_nickname_matcher.handle(parameterless=[CheckNickname()])
|
||||
@_global_nickname_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: Uninfo,
|
||||
name_: Arparma,
|
||||
nickname: str = UserName(),
|
||||
reg_group: tuple[Any, ...] = RegexGroup(),
|
||||
):
|
||||
(name,) = reg_group
|
||||
name = await CheckNickname(bot, session, name_)
|
||||
await FriendUser.set_user_nickname(
|
||||
session.user.id,
|
||||
name,
|
||||
@@ -227,15 +238,14 @@ async def _(session: Uninfo, uname: str = UserName()):
|
||||
group_id = session.group.parent.id if session.group.parent else session.group.id
|
||||
if group_id:
|
||||
nickname = await GroupInfoUser.get_user_nickname(session.user.id, group_id)
|
||||
card = uname
|
||||
else:
|
||||
nickname = await FriendUser.get_user_nickname(session.user.id)
|
||||
card = uname
|
||||
if nickname:
|
||||
await MessageUtils.build_message(random.choice(REMIND).format(nickname)).finish(
|
||||
reply_to=True
|
||||
)
|
||||
else:
|
||||
card = uname
|
||||
await MessageUtils.build_message(
|
||||
random.choice(
|
||||
[
|
||||
@@ -265,7 +275,9 @@ async def _(bot: Bot, session: Uninfo):
|
||||
await GroupInfoUser.set_user_nickname(session.user.id, group_id, "")
|
||||
else:
|
||||
await FriendUser.set_user_nickname(session.user.id, "")
|
||||
await BanConsole.ban(session.user.id, group_id, 9, 60, bot.self_id)
|
||||
await BanConsole.ban(
|
||||
session.user.id, group_id, 9, "用户昵称违规", 60, bot.self_id
|
||||
)
|
||||
return
|
||||
else:
|
||||
await MessageUtils.build_message("你在做梦吗?你没有昵称啊").finish(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user