Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebd772351f | ||
|
|
33d6ea1335 | ||
|
|
39ed1ade14 | ||
|
|
29979a9b21 | ||
|
|
0cb27d7183 | ||
|
|
f47bdc90d6 | ||
|
|
e5fa0f0335 | ||
|
|
9f202666aa | ||
|
|
023e865f34 | ||
|
|
cd5fa065d3 | ||
|
|
52f7dbdedf | ||
|
|
922d092650 | ||
|
|
0b32d69c9c | ||
|
|
80fc5b86a7 | ||
|
|
bdc1374848 | ||
|
|
f4d2342693 | ||
|
|
73cbe2a609 | ||
|
|
a2c0cfdf5d | ||
|
|
8afc8f8673 | ||
|
|
381d497c6d | ||
|
|
5596497947 | ||
|
|
12fc5663fb | ||
|
|
3bebf1c5e5 | ||
|
|
eb0403b9d4 | ||
|
|
0c89aa4e27 | ||
|
|
9cb40f0432 | ||
|
|
98bc636a39 | ||
|
|
e7aaec861f | ||
|
|
aeb1a7d0e9 | ||
|
|
5d92ccd3b0 | ||
|
|
24c316cd2c | ||
|
|
e53cae09b6 | ||
|
|
4000389a60 | ||
|
|
8b16126e40 | ||
|
|
74bf912d04 | ||
|
|
808abefbf6 | ||
|
|
eab79bdb52 | ||
|
|
c9efdaedcf | ||
|
|
480848fada | ||
|
|
d1e32cd820 | ||
|
|
6da4f27b12 | ||
|
|
65b125dd07 | ||
|
|
9802271b0a | ||
|
|
49c3ef5545 | ||
|
|
0f91ce03e4 | ||
|
|
ce94f63d9a | ||
|
|
51f4773e14 | ||
|
|
d9f8305540 | ||
|
|
3db3d63cc9 | ||
|
|
d1c24436ce | ||
|
|
5e30694663 | ||
|
|
bc8e1659ae | ||
|
|
5c067bcf04 | ||
|
|
b95acce800 | ||
|
|
4f152638b0 | ||
|
|
662d61a672 | ||
|
|
8378921c71 | ||
|
|
203754e300 | ||
|
|
7890b39002 | ||
|
|
837330e30a | ||
|
|
c9f0a8b9d9 | ||
|
|
e5b2a872d3 | ||
|
|
68460d18cc | ||
|
|
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 | ||
|
|
d6d54175f6 | ||
|
|
10e883f0ca | ||
|
|
a4ddfcd8ac | ||
|
|
ee9a2a6cb0 | ||
|
|
62b0b02466 | ||
|
|
4cc800c832 | ||
|
|
13579f5842 | ||
|
|
99f1388e23 | ||
|
|
bcfb47d9fd | ||
|
|
e1bf1ba87f | ||
|
|
46a652bb27 | ||
|
|
e7bae73c8f | ||
|
|
611f0b62ba | ||
|
|
a8447b7074 | ||
|
|
7baf9f296c | ||
|
|
53eb8c8484 | ||
|
|
b0461865a5 | ||
|
|
b5c2850e28 | ||
|
|
8c0a600525 | ||
|
|
9cda0e5d8f | ||
|
|
6546eb990b | ||
|
|
ff75e2ee92 | ||
|
|
6769c724cb | ||
|
|
b38509b2f5 | ||
|
|
5a0af6a64b | ||
|
|
bc2e06a9ec | ||
|
|
b5f101546a | ||
|
|
ccc4f27e3d | ||
|
|
2d8320b5a0 |
@@ -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 = MEMORY
|
||||
|
||||
# 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,11 +56,36 @@ PLATFORM_SUPERUSERS = '
|
||||
DRIVER=~fastapi+~httpx+~websockets
|
||||
|
||||
|
||||
# LOG_LEVEL=DEBUG
|
||||
# LOG_LEVEL = DEBUG
|
||||
# 服务器和端口
|
||||
HOST = 127.0.0.1
|
||||
PORT = 8080
|
||||
|
||||
# 第三方插件路径,如果多个目录用, 隔开
|
||||
# EXT_PATH=[""]
|
||||
|
||||
# qq adapter load = True
|
||||
QQ_ADAPTER_LOAD=False
|
||||
# QQ官方适配器配置,启用 QQ_ADAPTER_LOAD 后填写
|
||||
# QQ_BOTS='
|
||||
# [
|
||||
# {
|
||||
# "id": "",
|
||||
# "token": "",
|
||||
# "secret": "",
|
||||
# "use_websocket": true,
|
||||
# "intent": {
|
||||
# "guilds": true,
|
||||
# "guild_members": true,
|
||||
# "message_audit": true,
|
||||
# "at_messages": true,
|
||||
# "c2c_group_at_messages": false,
|
||||
# "direct_message": false
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# '
|
||||
|
||||
# kook adapter toekn
|
||||
# kaiheila_bots =[{"token": ""}]
|
||||
|
||||
@@ -74,4 +115,5 @@ PORT = 8080
|
||||
# '
|
||||
|
||||
# application_commands的{"*": ["*"]}代表将全部应用命令注册为全局应用命令
|
||||
# {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册
|
||||
# {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册
|
||||
|
||||
@@ -18,23 +18,25 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install poetry
|
||||
run: pipx install poetry
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
- name: Setup Python
|
||||
run: uv python install ${{ inputs.python-version }}
|
||||
shell: bash
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- name: Cache uv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
cache: "poetry"
|
||||
cache-dependency-path: |
|
||||
./poetry.lock
|
||||
${{ inputs.env-dir }}/poetry.lock
|
||||
path: ~/.cache/uv
|
||||
key: uv-${{ runner.os }}-${{ inputs.python-version }}-${{ hashFiles('uv.lock', format('{0}/uv.lock', inputs.env-dir)) }}
|
||||
restore-keys: uv-${{ runner.os }}-${{ inputs.python-version }}-
|
||||
|
||||
- run: |
|
||||
cd ${{ inputs.env-dir }}
|
||||
if [ "${{ inputs.no-root }}" = "true" ]; then
|
||||
poetry install --all-extras --no-root
|
||||
uv sync --frozen --all-extras --no-install-project
|
||||
else
|
||||
poetry install --all-extras
|
||||
uv sync --frozen --all-extras
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -28,7 +28,7 @@ autolabeler:
|
||||
files:
|
||||
- "pyproject.toml"
|
||||
- "requirements.txt"
|
||||
- "poetry.lock"
|
||||
- "uv.lock"
|
||||
title:
|
||||
- "/:wrench:.+/"
|
||||
- "/🔧.+/"
|
||||
|
||||
@@ -7,78 +7,71 @@ on:
|
||||
- zhenxun/**
|
||||
- tests/**
|
||||
- .github/workflows/bot_check.yml
|
||||
- bot.py
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- zhenxun/**
|
||||
- tests/**
|
||||
- .github/workflows/bot_check.yml
|
||||
- bot.py
|
||||
|
||||
jobs:
|
||||
bot-check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
name: bot check
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
- name: Setup Python
|
||||
id: setup_python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
run: uv python install 3.10
|
||||
|
||||
- name: Install Poetry
|
||||
run: pip install poetry
|
||||
|
||||
# Poetry cache depends on OS, Python version and Poetry version.
|
||||
- name: Cache Poetry cache
|
||||
id: cache-poetry
|
||||
uses: actions/cache@v3
|
||||
- name: Cache uv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pypoetry
|
||||
key: poetry-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}
|
||||
path: ~/.cache/uv
|
||||
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||
restore-keys: uv-${{ runner.os }}-
|
||||
|
||||
- name: Cache playwright cache
|
||||
id: cache-playwright
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}
|
||||
key: playwright-cache-${{ runner.os }}
|
||||
|
||||
- name: Cache Data cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: data
|
||||
key: data-cache-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}
|
||||
key: data-cache-${{ runner.os }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-poetry.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
rm -rf poetry.lock
|
||||
poetry source remove aliyun
|
||||
poetry install --no-root
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Install playwright
|
||||
if: steps.cache-playwright.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
poetry run sudo apt-get update
|
||||
poetry run sudo apt-get install -y libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav flite x264 libx264-dev
|
||||
poetry run pip install playwright
|
||||
poetry run playwright install-deps
|
||||
poetry run playwright install
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav flite x264 libx264-dev
|
||||
uv run playwright install-deps
|
||||
uv run playwright install
|
||||
|
||||
- name: Run tests
|
||||
run: poetry run pytest --cov=zhenxun --cov-report xml
|
||||
timeout-minutes: 10
|
||||
run: uv run pytest --cov=zhenxun --cov-report xml
|
||||
|
||||
- name: Check bot run
|
||||
timeout-minutes: 3
|
||||
id: bot_check_run
|
||||
run: |
|
||||
mv scripts/bot_check.py bot_check.py
|
||||
cp .env.example .env.dev
|
||||
sed -i "s|^.*\?DB_URL.*|DB_URL=\"${{ env.DB_URL }}\"|g" .env.dev
|
||||
sed -i "s/^.*\?LOG_LEVEL.*/LOG_LEVEL=${{ env.LOG_LEVEL }}/g" .env.dev
|
||||
poetry run python3 bot_check.py
|
||||
uv run python3 bot_check.py
|
||||
env:
|
||||
DB_URL: "sqlite://:memory:"
|
||||
LOG_LEVEL: DEBUG
|
||||
|
||||
@@ -45,12 +45,9 @@ jobs:
|
||||
include:
|
||||
- language: python
|
||||
build-mode: none
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
|
||||
@@ -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
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
no-root: true
|
||||
|
||||
- run: |
|
||||
(cd ./envs/${{ matrix.env }} && echo "$(poetry env info --path)/bin" >> $GITHUB_PATH)
|
||||
(cd ./envs/${{ matrix.env }} && echo "$(dirname $(uv run which python))" >> $GITHUB_PATH)
|
||||
if [ "${{ matrix.env }}" = "pydantic-v1" ]; then
|
||||
sed -i 's/PYDANTIC_V2 = true/PYDANTIC_V2 = false/g' ./pyproject.toml
|
||||
fi
|
||||
|
||||
@@ -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
|
||||
@@ -6,7 +6,6 @@ on:
|
||||
- .github/workflows/update_version_pr.yml
|
||||
- zhenxun/**
|
||||
- resources/**
|
||||
- bot.py
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
@@ -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
|
||||
/resources
|
||||
.vscode/launch.json
|
||||
plugins_/
|
||||
|
||||
./.env.dev
|
||||
@@ -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,
|
||||
|
||||
@@ -1,30 +1,3 @@
|
||||
FROM python:3.11-bookworm AS requirements-stage
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
ENV POETRY_HOME="/opt/poetry" PATH="${PATH}:/opt/poetry/bin"
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python - -y && \
|
||||
poetry self add poetry-plugin-export
|
||||
|
||||
COPY ./pyproject.toml ./poetry.lock* /tmp/
|
||||
|
||||
RUN poetry export \
|
||||
-f requirements.txt \
|
||||
--output requirements.txt \
|
||||
--without-hashes \
|
||||
--without-urls
|
||||
|
||||
FROM python:3.11-bookworm AS build-stage
|
||||
|
||||
WORKDIR /wheel
|
||||
|
||||
COPY --from=requirements-stage /tmp/requirements.txt /wheel/requirements.txt
|
||||
|
||||
# RUN python3 -m pip config set global.index-url https://mirrors.aliyun.com/pypi/simple
|
||||
|
||||
RUN pip wheel --wheel-dir=/wheel --no-cache-dir --requirement /wheel/requirements.txt
|
||||
|
||||
FROM python:3.11-bookworm AS metadata-stage
|
||||
|
||||
WORKDIR /tmp
|
||||
@@ -39,11 +12,12 @@ FROM python:3.11-slim-bookworm
|
||||
WORKDIR /app/zhenxun
|
||||
|
||||
ENV TZ=Asia/Shanghai PYTHONUNBUFFERED=1
|
||||
#COPY ./scripts/docker/start.sh /start.sh
|
||||
#RUN chmod +x /start.sh
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
# 安装 uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
RUN apt update && \
|
||||
apt install -y --no-install-recommends curl fontconfig fonts-noto-color-emoji \
|
||||
&& apt clean \
|
||||
@@ -51,17 +25,21 @@ RUN apt update && \
|
||||
&& apt-get purge -y --auto-remove curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖项和应用代码
|
||||
COPY --from=build-stage /wheel /wheel
|
||||
# 先复制依赖声明文件,利用 Docker layer cache
|
||||
COPY pyproject.toml uv.lock ./
|
||||
|
||||
# 安装依赖(--frozen 锁定版本,--no-install-project 不安装本项目,--no-dev 不安装开发依赖)
|
||||
RUN uv sync --frozen --no-install-project --no-dev
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
RUN pip install --no-cache-dir --no-index --find-links=/wheel -r /wheel/requirements.txt && rm -rf /wheel
|
||||
|
||||
RUN playwright install --with-deps chromium \
|
||||
# 安装 Playwright 和 Chromium
|
||||
RUN uv run playwright install --with-deps chromium \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/*
|
||||
|
||||
COPY --from=metadata-stage /tmp/VERSION /app/VERSION
|
||||
|
||||
VOLUME ["/app/zhenxun/data", "/app/zhenxun/resources", "/app/zhenxun/log"]
|
||||
|
||||
CMD ["python", "bot.py"]
|
||||
CMD ["uv", "run", "zx", "run"]
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<div align=center>
|
||||
|
||||
[文档](https://hibikier.github.io/zhenxun_bot/)
|
||||
[文档](https://zhenxun-org.github.io/zhenxun_bot/)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -112,7 +112,7 @@ AccessToken: PUBLIC_ZHENXUN_TEST
|
||||
| [插件库](https://github.com/zhenxun-org/zhenxun_bot_plugins) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 原 plugins 文件夹插件 |
|
||||
| [插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 扩展插件索引库 |
|
||||
| [一键安装](https://github.com/soloxiaoye2022/zhenxun_bot-deploy) | 安装 | [soloxiaoye2022](https://github.com/soloxiaoye2022) | 第三方 |
|
||||
| [WebUi](https://github.com/HibiKier/zhenxun_bot_webui) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 [预览](#-webui界面展示) |
|
||||
| [WebUi](https://github.com/zhenxun-org/zhenxun_bot) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 [预览](#-webui界面展示) |
|
||||
| [安卓 app(WebUi)](https://github.com/YuS1aN/zhenxun_bot_android_ui) | 安装 | [YuS1aN](https://github.com/YuS1aN) | 第三方 |
|
||||
|
||||
</div>
|
||||
@@ -121,26 +121,51 @@ AccessToken: PUBLIC_ZHENXUN_TEST
|
||||
|
||||
- 实现了许多功能,且提供了大量功能管理命令,进行了多平台适配,兼容 nb2 商店插件
|
||||
- 拥有完善可用的 webui
|
||||
- 通过 Config 配置项将所有插件配置统计保存至 config.yaml,利于统一用户修改
|
||||
- 通过 Config 配置项将所有插件配置统一保存至 config.yaml,利于统一用户修改
|
||||
- 方便增删插件,原生 nonebot2 matcher,不需要额外修改,仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息`
|
||||
- 提供了 cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`PluginCdBlock` 等
|
||||
- **更多详细请通过 [传送门](https://hibikier.github.io/zhenxun_bot/) 查看文档!**
|
||||
- **更多详细请通过 [传送门](https://zhenxun-org.github.io/zhenxun_bot/) 查看文档!**
|
||||
|
||||
## 🐣 小白整合
|
||||
|
||||
如果你系统是 **Windows** 且对于指令一类不熟
|
||||
可以使用整合包
|
||||
### 注意
|
||||
```***Python需要自行安装且版本大于等于3.11***```
|
||||
|
||||
|
||||
文档地址:[整合包文档](https://zhenxun-org.github.io/zhenxun_bot/beginner)
|
||||
|
||||
<details>
|
||||
<summary>下载地址</summary>
|
||||
|
||||
- **百度云:**
|
||||
https://pan.baidu.com/s/1MKGOoIgQW1qom_KT3rNhlg?pwd=t7iz
|
||||
|
||||
- **夸克网盘:**
|
||||
https://pan.quark.cn/s/b4dc6cb8fb08
|
||||
访问码:Yi46
|
||||
|
||||
- **Google Drive:**
|
||||
https://drive.google.com/drive/folders/1dcTWhPZhSt9WqLehyjF6Gj0CXQCS4OWh?usp=drive_link
|
||||
|
||||
</details>
|
||||
|
||||
## 🛠️ 简单部署
|
||||
|
||||
```bash
|
||||
# 获取代码
|
||||
git clone https://github.com/HibiKier/zhenxun_bot.git
|
||||
git clone https://github.com/zhenxun-org/zhenxun_bot.git
|
||||
|
||||
# 进入目录
|
||||
cd zhenxun_bot
|
||||
|
||||
# 安装依赖
|
||||
pip install poetry # 安装 poetry
|
||||
poetry install # 安装依赖
|
||||
pip install uv # 安装 uv
|
||||
uv sync # 安装依赖
|
||||
|
||||
# 开始运行
|
||||
poetry run python bot.py
|
||||
uv run zx
|
||||
```
|
||||
|
||||
## 📝 简单配置
|
||||
@@ -150,7 +175,7 @@ poetry run python bot.py
|
||||
|
||||
1.在 .env.dev 文件中填写你的机器人配置项
|
||||
|
||||
2.在 configs/config.yaml 文件中修改你需要修改的插件配置项
|
||||
2.在 data/config.yaml 文件中修改你需要修改的插件配置项
|
||||
|
||||
<details>
|
||||
<summary>数据库地址(DB_URL)配置说明</summary>
|
||||
@@ -265,6 +290,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) 以了解如何参与贡献。
|
||||
@@ -272,12 +309,12 @@ DB_URL 是基于 Tortoise ORM 的数据库连接字符串,用于指定项目
|
||||
## ❔ 需要帮助?
|
||||
|
||||
> [!TIP]
|
||||
> 发起 [issue](https://github.com/HibiKier/zhenxun_bot/issues/new/choose) 前,我们希望你能够阅读过或者了解 [提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
|
||||
> 发起 [issue](https://github.com/zhenxun-org/zhenxun_bot/issues/new/choose) 前,我们希望你能够阅读过或者了解 [提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
|
||||
>
|
||||
> - 善用[搜索引擎](https://www.google.com/)
|
||||
> - 查阅 issue 中是否有类似问题,如果没有请按照模板发起 issue
|
||||
|
||||
欢迎前往 [issue](https://github.com/HibiKier/zhenxun_bot/issues/new/choose) 中提出你遇到的问题,或者加入我们的 [用户群](https://qm.qq.com/q/mRNtLSl6uc) 或 [技术群](https://qm.qq.com/q/YYYt5rkMYc)与我们联系
|
||||
欢迎前往 [issue](https://github.com/zhenxun-org/zhenxun_bot/issues/new/choose) 中提出你遇到的问题,或者加入我们的 [用户群](https://qm.qq.com/q/mRNtLSl6uc) 或 [技术群](https://qm.qq.com/q/YYYt5rkMYc)与我们联系
|
||||
|
||||
## 🛠️ 进度追踪
|
||||
|
||||
@@ -287,6 +324,8 @@ Project [zhenxun_bot](https://github.com/users/HibiKier/projects/2)
|
||||
|
||||
首席设计师:[酥酥/coldly-ss](https://github.com/coldly-ss)
|
||||
|
||||
LOGO 设计:[FrostN0v0](https://github.com/FrostN0v0)
|
||||
|
||||
## 🙏 感谢
|
||||
|
||||
[botuniverse / onebot](https://github.com/botuniverse/onebot) :超棒的机器人协议
|
||||
@@ -326,34 +365,68 @@ Project [zhenxun_bot](https://github.com/users/HibiKier/projects/2)
|
||||
<img src="https://contrib.rocks/image?repo=HibiKier/zhenxun_bot&max=1000" alt="contributors"/>
|
||||
</a>
|
||||
|
||||
## 📸 WebUI 界面展示
|
||||
## 📸 WebUI 界面展示(仅展示默认主题下的 pc 端)
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; justify-content: space-between;">
|
||||
<div style="width: 48%; margin-bottom: 10px;">
|
||||
<img src="./docs_image/webui00.png" alt="webui00" style="width: 100%; height: auto;">
|
||||
</div>
|
||||
<div style="width: 48%; margin-bottom: 10px;">
|
||||
<img src="./docs_image/webui01.png" alt="webui01" style="width: 100%; height: auto;">
|
||||
</div>
|
||||
|
||||
<div style="width: 48%; margin-bottom: 10px;">
|
||||
<img src="./docs_image/webui02.png" alt="webui02" style="width: 100%; height: auto;">
|
||||
</div>
|
||||
<div style="width: 48%; margin-bottom: 10px;">
|
||||
<img src="./docs_image/webui03.png" alt="webui03" style="width: 100%; height: auto;">
|
||||
</div>
|
||||
#### 登录界面
|
||||
|
||||
<div style="width: 48%; margin-bottom: 10px;">
|
||||
<img src="./docs_image/webui04.png" alt="webui04" style="width: 100%; height: auto;">
|
||||
</div>
|
||||
<div style="width: 48%; margin-bottom: 10px;">
|
||||
<img src="./docs_image/webui05.png" alt="webui05" style="width: 100%; height: auto;">
|
||||
</div>
|
||||

|
||||
|
||||
#### API 设置
|
||||
|
||||

|
||||
|
||||
#### 仪表盘
|
||||
|
||||

|
||||
|
||||
#### 仪表盘(展开)
|
||||
|
||||

|
||||
|
||||
#### 控制台
|
||||
|
||||

|
||||
|
||||
#### 插件列表
|
||||
|
||||

|
||||
|
||||
#### 插件列表(配置项)
|
||||
|
||||

|
||||
|
||||
#### 插件商店
|
||||
|
||||

|
||||
|
||||
#### 好友/群组管理
|
||||
|
||||

|
||||
|
||||
#### 请求管理
|
||||
|
||||

|
||||
|
||||
#### 数据库管理
|
||||
|
||||

|
||||
|
||||
### 文件管理
|
||||
|
||||

|
||||
|
||||
### 文件管理(文本查看)
|
||||
|
||||

|
||||
|
||||
### 文件管理(图片查看)
|
||||
|
||||

|
||||
|
||||
### 关于
|
||||
|
||||

|
||||
|
||||
<div style="width: 48%; margin-bottom: 10px;">
|
||||
<img src="./docs_image/webui06.png" alt="webui06" style="width: 100%; height: auto;">
|
||||
</div>
|
||||
<div style="width: 48%; margin-bottom: 10px;">
|
||||
<img src="./docs_image/webui07.png" alt="webui07" style="width: 100%; height: auto;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__: v0.2.4-2c97eea
|
||||
__version__: v0.2.4-33d6ea1
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import nonebot
|
||||
|
||||
# from nonebot.adapters.discord import Adapter as DiscordAdapter
|
||||
# from nonebot.adapters.dodo import Adapter as DoDoAdapter
|
||||
# from nonebot.adapters.kaiheila import Adapter as KaiheilaAdapter
|
||||
from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter
|
||||
|
||||
nonebot.init()
|
||||
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
driver.register_adapter(OneBotV11Adapter)
|
||||
# driver.register_adapter(KaiheilaAdapter)
|
||||
# driver.register_adapter(DoDoAdapter)
|
||||
# driver.register_adapter(DiscordAdapter)
|
||||
|
||||
from zhenxun.services.db_context import disconnect, init
|
||||
|
||||
driver.on_startup(init)
|
||||
driver.on_shutdown(disconnect)
|
||||
|
||||
# nonebot.load_builtin_plugins("echo")
|
||||
nonebot.load_plugins("zhenxun/builtin_plugins")
|
||||
nonebot.load_plugins("zhenxun/plugins")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
nonebot.run()
|
||||
@@ -0,0 +1,67 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: zhenxun
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
labels:
|
||||
- "prometheus.io/scrape=true"
|
||||
- "prometheus.io/port=9187"
|
||||
|
||||
postgres-exporter:
|
||||
image: prometheuscommunity/postgres-exporter
|
||||
environment:
|
||||
DATA_SOURCE_NAME: "postgresql://postgres:password@db:5432/zhenxun?sslmode=disable"
|
||||
ports:
|
||||
- "9187:9187"
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- "6379:6379"
|
||||
labels:
|
||||
- "prometheus.io/scrape=true"
|
||||
- "prometheus.io/port=9121"
|
||||
|
||||
redis-exporter:
|
||||
image: oliver006/redis_exporter
|
||||
environment:
|
||||
REDIS_ADDR: redis://redis:6379
|
||||
ports:
|
||||
- "9121:9121"
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||
- '--web.console.templates=/etc/prometheus/consoles'
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
depends_on:
|
||||
- prometheus
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
prometheus_data:
|
||||
grafana_data:
|
||||
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 315 KiB |
|
After Width: | Height: | Size: 630 KiB |
|
After Width: | Height: | Size: 708 KiB |
|
After Width: | Height: | Size: 598 KiB |
|
After Width: | Height: | Size: 405 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 504 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 551 KiB |
|
After Width: | Height: | Size: 453 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 193 KiB |
@@ -1,61 +1,72 @@
|
||||
[tool.poetry]
|
||||
name = "zhenxun_bot"
|
||||
[project]
|
||||
name = "zhenxun-bot-env-pydantic-v1"
|
||||
version = "0.2.4"
|
||||
description = "基于 Nonebot2 和 go-cqhttp 开发,以 postgresql 作为数据库,非常可爱的绪山真寻bot"
|
||||
authors = ["HibiKier <775757368@qq.com>"]
|
||||
license = "AGPL"
|
||||
package-mode = false
|
||||
authors = [{ name = "HibiKier", email = "775757368@qq.com" }]
|
||||
license = { text = "AGPL-3.0" }
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"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",
|
||||
"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,!=6.3.2",
|
||||
"pydantic>=1.0.0,<2.0.0",
|
||||
"json-repair>=0.54.0,<0.55.0",
|
||||
"alibabacloud-devops20210625>=5.0.2,<6.0.0",
|
||||
"jieba>=0.42.1",
|
||||
"aiodocker>=0.24.0",
|
||||
]
|
||||
|
||||
[[tool.poetry.source]]
|
||||
[project.optional-dependencies]
|
||||
redis = ["redis>=5"]
|
||||
postgresql = ["asyncpg>=0.20.0"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"nonebug>=0.4,<0.5",
|
||||
"pytest-cov>=5.0.0,<6.0.0",
|
||||
"pytest-mock>=3.6.1,<4.0.0",
|
||||
"pytest-asyncio>=0.25,<0.26",
|
||||
"pytest-xdist>=3.3.1,<4.0.0",
|
||||
"respx>=0.21.1,<0.22.0",
|
||||
"ruff>=0.8.0,<0.9.0",
|
||||
"pre-commit>=4.0.0,<5.0.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "aliyun"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple/"
|
||||
priority = "primary"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
playwright = "^1.41.1"
|
||||
nonebot-adapter-onebot = "^2.3.1"
|
||||
nonebot-plugin-apscheduler = "^0.5"
|
||||
tortoise-orm = { extras = ["asyncpg"], version = "^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" }
|
||||
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"
|
||||
beautifulsoup4 = "^4.12.3"
|
||||
lxml = "^5.1.0"
|
||||
psutil = "^5.9.8"
|
||||
feedparser = "^6.0.11"
|
||||
imagehash = "^4.3.1"
|
||||
cn2an = "^0.5.22"
|
||||
dateparser = "^1.2.0"
|
||||
bilireq = "0.2.3post0"
|
||||
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
|
||||
python-multipart = "^0.0.9"
|
||||
aiocache = "^0.12.2"
|
||||
py-cpuinfo = "^9.0.0"
|
||||
nonebot-plugin-alconna = "^0.54.0"
|
||||
tenacity = "^9.0.0"
|
||||
nonebot-plugin-uninfo = ">0.4.1"
|
||||
pydantic = "1.10.18"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
nonebug = "^0.4"
|
||||
pytest-cov = "^5.0.0"
|
||||
pytest-mock = "^3.6.1"
|
||||
pytest-asyncio = "^0.25"
|
||||
pytest-xdist = "^3.3.1"
|
||||
respx = "^0.21.1"
|
||||
ruff = "^0.8.0"
|
||||
pre-commit = "^4.0.0"
|
||||
|
||||
|
||||
[tool.nonebot]
|
||||
plugins = [
|
||||
@@ -129,6 +140,7 @@ executionEnvironments = [
|
||||
|
||||
typeCheckingMode = "standard"
|
||||
reportShadowedImports = false
|
||||
reportMissingImports = "none"
|
||||
disableBytesTypePromotions = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
@@ -136,5 +148,5 @@ asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "session"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
@@ -1,61 +1,73 @@
|
||||
[tool.poetry]
|
||||
name = "zhenxun_bot"
|
||||
[project]
|
||||
name = "zhenxun-bot-env-pydantic-v2"
|
||||
version = "0.2.4"
|
||||
description = "基于 Nonebot2 和 go-cqhttp 开发,以 postgresql 作为数据库,非常可爱的绪山真寻bot"
|
||||
authors = ["HibiKier <775757368@qq.com>"]
|
||||
license = "AGPL"
|
||||
package-mode = false
|
||||
authors = [{ name = "HibiKier", email = "775757368@qq.com" }]
|
||||
license = { text = "AGPL-3.0" }
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"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",
|
||||
"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,!=6.3.2",
|
||||
"pydantic>=2.0.0,<3.0.0",
|
||||
"json-repair>=0.54.0,<0.55.0",
|
||||
"alibabacloud-devops20210625>=5.0.2,<6.0.0",
|
||||
"mcp>=1.8.0",
|
||||
"jieba>=0.42.1",
|
||||
"aiodocker>=0.24.0",
|
||||
]
|
||||
|
||||
[[tool.poetry.source]]
|
||||
[project.optional-dependencies]
|
||||
redis = ["redis>=5"]
|
||||
postgresql = ["asyncpg>=0.20.0"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"nonebug>=0.4,<0.5",
|
||||
"pytest-cov>=5.0.0,<6.0.0",
|
||||
"pytest-mock>=3.6.1,<4.0.0",
|
||||
"pytest-asyncio>=0.25,<0.26",
|
||||
"pytest-xdist>=3.3.1,<4.0.0",
|
||||
"respx>=0.21.1,<0.22.0",
|
||||
"ruff>=0.8.0,<0.9.0",
|
||||
"pre-commit>=4.0.0,<5.0.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "aliyun"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple/"
|
||||
priority = "primary"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
playwright = "^1.41.1"
|
||||
nonebot-adapter-onebot = "^2.3.1"
|
||||
nonebot-plugin-apscheduler = "^0.5"
|
||||
tortoise-orm = { extras = ["asyncpg"], version = "^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" }
|
||||
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"
|
||||
beautifulsoup4 = "^4.12.3"
|
||||
lxml = "^5.1.0"
|
||||
psutil = "^5.9.8"
|
||||
feedparser = "^6.0.11"
|
||||
imagehash = "^4.3.1"
|
||||
cn2an = "^0.5.22"
|
||||
dateparser = "^1.2.0"
|
||||
bilireq = "0.2.3post0"
|
||||
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
|
||||
python-multipart = "^0.0.9"
|
||||
aiocache = "^0.12.2"
|
||||
py-cpuinfo = "^9.0.0"
|
||||
nonebot-plugin-alconna = "^0.54.0"
|
||||
tenacity = "^9.0.0"
|
||||
nonebot-plugin-uninfo = ">0.4.1"
|
||||
pydantic = "2.10.6"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
nonebug = "^0.4"
|
||||
pytest-cov = "^5.0.0"
|
||||
pytest-mock = "^3.6.1"
|
||||
pytest-asyncio = "^0.25"
|
||||
pytest-xdist = "^3.3.1"
|
||||
respx = "^0.21.1"
|
||||
ruff = "^0.8.0"
|
||||
pre-commit = "^4.0.0"
|
||||
|
||||
|
||||
[tool.nonebot]
|
||||
plugins = [
|
||||
@@ -129,6 +141,7 @@ executionEnvironments = [
|
||||
|
||||
typeCheckingMode = "standard"
|
||||
reportShadowedImports = false
|
||||
reportMissingImports = "none"
|
||||
disableBytesTypePromotions = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
@@ -136,5 +149,5 @@ asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "session"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'postgresql'
|
||||
static_configs:
|
||||
- targets: [ 'postgres-exporter:9187' ]
|
||||
|
||||
- job_name: 'redis'
|
||||
static_configs:
|
||||
- targets: [ 'redis-exporter:9121' ]
|
||||
@@ -1,61 +1,79 @@
|
||||
[tool.poetry]
|
||||
name = "zhenxun_bot"
|
||||
[project]
|
||||
name = "zhenxun-bot"
|
||||
version = "0.2.4"
|
||||
description = "基于 Nonebot2 和 go-cqhttp 开发,以 postgresql 作为数据库,非常可爱的绪山真寻bot"
|
||||
authors = ["HibiKier <775757368@qq.com>"]
|
||||
license = "AGPL"
|
||||
package-mode = false
|
||||
authors = [{ name = "HibiKier", email = "775757368@qq.com" }]
|
||||
license = { text = "AGPL-3.0" }
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"playwright==1.57.0",
|
||||
"nonebot-adapter-onebot>=2.3.1",
|
||||
"nonebot-plugin-apscheduler>=0.5",
|
||||
"tortoise-orm>=0.20.0,<0.21.0",
|
||||
"cattrs>=23.2.3,<24.0.0",
|
||||
"ruamel-yaml>=0.18.5",
|
||||
"strenum>=0.4.15,<0.5.0",
|
||||
"nonebot-plugin-session>=0.3.2",
|
||||
"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",
|
||||
"nonebot-plugin-htmlrender>=0.6.0,<0.7.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",
|
||||
"numpy>=1.26,<2.3",
|
||||
"cn2an>=0.5.22,<0.6.0",
|
||||
"dateparser>=1.2.0,<2.0.0",
|
||||
"python-jose[cryptography]>=3.3.0,<4.0.0",
|
||||
"python-multipart>=0.0.9,<0.1.0",
|
||||
"aiocache[redis]>=0.12.3",
|
||||
"asyncpg>=0.20.0",
|
||||
"redis>=5",
|
||||
"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",
|
||||
"multidict>=6.0.0,!=6.3.2",
|
||||
"json-repair>=0.54.0,<0.55.0",
|
||||
"alibabacloud-devops20210625>=5.0.2,<6.0.0",
|
||||
"uvloop>=0.21.0; sys_platform != 'win32'",
|
||||
"pytest-timeout>=2.4.0",
|
||||
"aiomysql>=0.3.2",
|
||||
"mcp>=1.8.0",
|
||||
"jieba>=0.42.1",
|
||||
"aiodocker>=0.24.0",
|
||||
]
|
||||
|
||||
[[tool.poetry.source]]
|
||||
[project.scripts]
|
||||
zx = "zhenxun.cli:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"nonebug>=0.4,<0.5",
|
||||
"pytest-cov>=5.0.0,<6.0.0",
|
||||
"pytest-mock>=3.6.1,<4.0.0",
|
||||
"pytest-asyncio>=0.25,<0.26",
|
||||
"pytest-xdist>=3.3.1,<4.0.0",
|
||||
"respx>=0.21.1,<0.22.0",
|
||||
"ruff>=0.8.0,<0.9.0",
|
||||
"pre-commit>=4.0.0,<5.0.0",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["zhenxun"]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "aliyun"
|
||||
url = "https://mirrors.aliyun.com/pypi/simple/"
|
||||
priority = "primary"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
playwright = "^1.41.1"
|
||||
nonebot-adapter-onebot = "^2.3.1"
|
||||
nonebot-plugin-apscheduler = "^0.5"
|
||||
tortoise-orm = { extras = ["asyncpg"], version = "^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" }
|
||||
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"
|
||||
beautifulsoup4 = "^4.12.3"
|
||||
lxml = "^5.1.0"
|
||||
psutil = "^5.9.8"
|
||||
feedparser = "^6.0.11"
|
||||
imagehash = "^4.3.1"
|
||||
cn2an = "^0.5.22"
|
||||
dateparser = "^1.2.0"
|
||||
bilireq = "0.2.3post0"
|
||||
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
|
||||
python-multipart = "^0.0.9"
|
||||
aiocache = "^0.12.2"
|
||||
py-cpuinfo = "^9.0.0"
|
||||
nonebot-plugin-alconna = "^0.54.0"
|
||||
tenacity = "^9.0.0"
|
||||
nonebot-plugin-uninfo = ">0.4.1"
|
||||
nonebot-plugin-waiter = "^0.8.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
nonebug = "^0.4"
|
||||
pytest-cov = "^5.0.0"
|
||||
pytest-mock = "^3.6.1"
|
||||
pytest-asyncio = "^0.25"
|
||||
pytest-xdist = "^3.3.1"
|
||||
respx = "^0.21.1"
|
||||
ruff = "^0.8.0"
|
||||
pre-commit = "^4.0.0"
|
||||
|
||||
|
||||
[tool.nonebot]
|
||||
plugins = [
|
||||
@@ -129,12 +147,14 @@ executionEnvironments = [
|
||||
|
||||
typeCheckingMode = "standard"
|
||||
reportShadowedImports = false
|
||||
reportMissingImports = "none"
|
||||
disableBytesTypePromotions = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "session"
|
||||
timeout = 120
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
@@ -1,130 +1,39 @@
|
||||
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.57.0
|
||||
nonebot-adapter-onebot>=2.3.1
|
||||
nonebot-plugin-apscheduler>=0.5
|
||||
tortoise-orm>=0.20.0,<0.21.0
|
||||
cattrs>=23.2.3,<24.0.0
|
||||
ruamel.yaml>=0.18.5
|
||||
strenum>=0.4.15,<0.5.0
|
||||
nonebot-plugin-session>=0.3.2
|
||||
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
|
||||
nonebot-plugin-htmlrender>=0.6.0,<0.7.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
|
||||
python-jose[cryptography]>=3.3.0,<4.0.0
|
||||
python-multipart>=0.0.9,<0.1.0
|
||||
aiocache[redis]>=0.12.3
|
||||
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
|
||||
multidict>=6.0.0,<7.0.0,!=6.3.2
|
||||
alibabacloud-devops20210625>=5.0.2,<6.0.0
|
||||
json_repair>=0.54.0,<0.55.0
|
||||
redis>=5
|
||||
asyncpg>=0.20.0
|
||||
mcp>=1.8.0
|
||||
jieba>=0.42.1
|
||||
aiodocker>=0.24.0
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
require_resources_version: ">=1.1.1"
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _load_json(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _num(value: Any) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _safe_div(left: float, right: float) -> float:
|
||||
return round(left / right, 4) if right else 0.0
|
||||
|
||||
|
||||
def _extract(path: Path) -> dict[str, Any]:
|
||||
payload = _load_json(path)
|
||||
summary = payload.get("summary") or {}
|
||||
trace = summary.get("db_trace") or {}
|
||||
events = _num(summary.get("events_sent_total"))
|
||||
commands = _num(summary.get("commands_sent_total"))
|
||||
return {
|
||||
"path": str(path),
|
||||
"status": payload.get("status"),
|
||||
"elapsed_seconds": payload.get("elapsed_seconds"),
|
||||
"events": int(events),
|
||||
"commands": int(commands),
|
||||
"throughput_eps": summary.get("throughput_events_per_sec"),
|
||||
"command_success_rate": summary.get("command_success_rate"),
|
||||
"latency_avg_ms": summary.get("latency_avg_ms"),
|
||||
"latency_p50_ms": summary.get("latency_p50_ms"),
|
||||
"latency_p95_ms": summary.get("latency_p95_ms"),
|
||||
"latency_p99_ms": summary.get("latency_p99_ms"),
|
||||
"db_timeouts": summary.get("db_timeouts"),
|
||||
"db_slow_queries": summary.get("db_slow_queries"),
|
||||
"chat_history_failures": summary.get("chat_history_failures"),
|
||||
"statistics_flush_failures": summary.get("statistics_flush_failures"),
|
||||
"db_calls": int(_num(trace.get("calls"))),
|
||||
"db_reads": int(_num(trace.get("reads"))),
|
||||
"db_writes": int(_num(trace.get("writes"))),
|
||||
"db_scripts": int(_num(trace.get("scripts"))),
|
||||
"db_calls_per_event": _safe_div(_num(trace.get("calls")), events),
|
||||
"db_reads_per_event": _safe_div(_num(trace.get("reads")), events),
|
||||
"db_writes_per_event": _safe_div(_num(trace.get("writes")), events),
|
||||
"db_calls_per_command": _safe_div(_num(trace.get("calls")), commands),
|
||||
"db_writes_per_command": _safe_div(_num(trace.get("writes")), commands),
|
||||
"db_avg_elapsed_ms": trace.get("avg_elapsed_ms"),
|
||||
"db_avg_wait_ms": trace.get("avg_wait_ms"),
|
||||
"db_max_elapsed_ms": trace.get("max_elapsed_ms"),
|
||||
"db_max_wait_ms": trace.get("max_wait_ms"),
|
||||
"db_max_active": trace.get("max_active"),
|
||||
"db_max_waiting": trace.get("max_waiting"),
|
||||
"db_connection_creates": trace.get("connection_creates"),
|
||||
"db_top_tables": trace.get("top_tables", [])[:12],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("reports", nargs="+")
|
||||
parser.add_argument("--output")
|
||||
args = parser.parse_args()
|
||||
rows = [_extract(Path(item).resolve()) for item in args.reports]
|
||||
payload = {"reports": rows}
|
||||
text = json.dumps(payload, ensure_ascii=False, indent=2)
|
||||
if args.output:
|
||||
output = Path(args.output).resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(text, encoding="utf-8")
|
||||
sys.stdout.write(text + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -23,18 +23,38 @@ driver.on_shutdown(disconnect)
|
||||
nonebot.load_plugins("zhenxun/builtin_plugins")
|
||||
nonebot.load_plugins("zhenxun/plugins")
|
||||
|
||||
all_plugins = [name.replace(":", ".") for name in nonebot.get_available_plugin_names()]
|
||||
|
||||
def _normalize_plugin_name(name: str) -> str:
|
||||
return name.replace(":", ".")
|
||||
|
||||
|
||||
def _collect_loaded_plugin_names() -> set[str]:
|
||||
loaded_names: set[str] = set()
|
||||
for plugin in nonebot.get_loaded_plugins():
|
||||
loaded_names.add(_normalize_plugin_name(plugin.name))
|
||||
loaded_names.add(
|
||||
_normalize_plugin_name(
|
||||
re.sub(
|
||||
r"^zhenxun\.(plugins|builtin_plugins)\.",
|
||||
"",
|
||||
plugin.module_name,
|
||||
)
|
||||
)
|
||||
)
|
||||
return loaded_names
|
||||
|
||||
|
||||
all_plugins = [
|
||||
_normalize_plugin_name(name) for name in nonebot.get_available_plugin_names()
|
||||
]
|
||||
logger.info(f"所有插件:{all_plugins}")
|
||||
loaded_plugins = tuple(
|
||||
re.sub(r"^zhenxun\.(plugins|builtin_plugins)\.", "", plugin.module_name)
|
||||
for plugin in nonebot.get_loaded_plugins()
|
||||
)
|
||||
loaded_plugins = _collect_loaded_plugin_names()
|
||||
logger.info(f"已加载插件:{loaded_plugins}")
|
||||
|
||||
for plugin in all_plugins.copy():
|
||||
if plugin.startswith(("platform",)):
|
||||
logger.info(f"平台插件:{plugin}")
|
||||
elif plugin.endswith(loaded_plugins):
|
||||
elif plugin in loaded_plugins:
|
||||
logger.info(f"已加载插件:{plugin}")
|
||||
else:
|
||||
logger.info(f"未加载插件:{plugin}")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
|
||||
async def _run_git(*args: str) -> None:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"git",
|
||||
*args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_, stderr = await process.communicate()
|
||||
assert process.returncode == 0, stderr.decode(errors="replace")
|
||||
|
||||
|
||||
def _plugin_info(*, ali_url: str | None = None):
|
||||
from zhenxun.builtin_plugins.plugin_store.models import StorePluginInfo
|
||||
from zhenxun.utils.enum import PluginType
|
||||
|
||||
return StorePluginInfo(
|
||||
name="测试插件",
|
||||
module="demo",
|
||||
module_path="demo",
|
||||
description="",
|
||||
usage="",
|
||||
author="tester",
|
||||
version="1.0.0",
|
||||
plugin_type=PluginType.NORMAL,
|
||||
is_dir=True,
|
||||
github_url="https://github.com/example/demo",
|
||||
ali_url=ali_url,
|
||||
)
|
||||
|
||||
|
||||
def test_source_order() -> None:
|
||||
from zhenxun.builtin_plugins.plugin_store.data_source import StoreManager
|
||||
from zhenxun.utils.repo_utils.models import RepoType
|
||||
|
||||
assert StoreManager._get_source_order(None) == (
|
||||
RepoType.ALIYUN,
|
||||
RepoType.GITHUB,
|
||||
)
|
||||
assert StoreManager._get_source_order("ali") == (RepoType.ALIYUN,)
|
||||
assert StoreManager._get_source_order("git") == (RepoType.GITHUB,)
|
||||
|
||||
|
||||
def test_repository_branch_is_resolved_per_source() -> None:
|
||||
from zhenxun.builtin_plugins.plugin_store.data_source import StoreManager
|
||||
from zhenxun.utils.repo_utils.models import RepoType
|
||||
|
||||
plugin_info = _plugin_info()
|
||||
plugin_info.github_url = "https://github.com/example/demo/tree/master"
|
||||
|
||||
assert (
|
||||
StoreManager._get_plugin_repository_branch(plugin_info, RepoType.ALIYUN, "main")
|
||||
== "main"
|
||||
)
|
||||
assert (
|
||||
StoreManager._get_plugin_repository_branch(plugin_info, RepoType.GITHUB, "main")
|
||||
== "master"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_external", [False, True])
|
||||
async def test_default_source_falls_back_to_github_for_all_plugins(
|
||||
mocker: MockerFixture,
|
||||
tmp_path: Path,
|
||||
is_external: bool,
|
||||
) -> None:
|
||||
from zhenxun.builtin_plugins.plugin_store.data_source import StoreManager
|
||||
from zhenxun.utils.repo_utils.models import (
|
||||
FileDownloadResult,
|
||||
RepoFileInfo,
|
||||
RepoType,
|
||||
)
|
||||
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
source_calls: list[RepoType] = []
|
||||
download_calls: list[tuple[RepoType, list[str]]] = []
|
||||
files = [
|
||||
RepoFileInfo(path="demo/__init__.py", is_dir=False),
|
||||
RepoFileInfo(path="demo/assets/icon.png", is_dir=False),
|
||||
RepoFileInfo(path="demo/requirements.txt", is_dir=False),
|
||||
]
|
||||
|
||||
async def list_directory_files(
|
||||
repo_url: str,
|
||||
directory_path: str,
|
||||
branch: str,
|
||||
repo_type: RepoType,
|
||||
) -> list[RepoFileInfo]:
|
||||
source_calls.append(repo_type)
|
||||
if repo_type == RepoType.ALIYUN:
|
||||
raise RuntimeError("aliyun unavailable")
|
||||
return files
|
||||
|
||||
async def download_files(
|
||||
repo_url: str,
|
||||
file_path: list[tuple[str, Path]],
|
||||
branch: str,
|
||||
repo_type: RepoType,
|
||||
ignore_error: bool = False,
|
||||
) -> FileDownloadResult:
|
||||
download_calls.append((repo_type, [path for path, _ in file_path]))
|
||||
for source_path, destination_path in file_path:
|
||||
destination_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if source_path.endswith(".png"):
|
||||
destination_path.write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||
else:
|
||||
destination_path.write_text(source_path, encoding="utf-8")
|
||||
return FileDownloadResult(
|
||||
repo_type=repo_type,
|
||||
repo_name="demo",
|
||||
file_path=file_path,
|
||||
version=branch,
|
||||
success=True,
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source."
|
||||
"RepoFileManager.list_directory_files",
|
||||
side_effect=list_directory_files,
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source."
|
||||
"RepoFileManager.download_files",
|
||||
side_effect=download_files,
|
||||
)
|
||||
install_requirement = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source."
|
||||
"VirtualEnvPackageManager.install_requirement",
|
||||
)
|
||||
|
||||
await StoreManager.install_plugin_with_repo(
|
||||
_plugin_info(),
|
||||
is_external=is_external,
|
||||
)
|
||||
|
||||
assert source_calls == [RepoType.ALIYUN, RepoType.GITHUB]
|
||||
assert download_calls == [
|
||||
(
|
||||
RepoType.GITHUB,
|
||||
[
|
||||
"demo/__init__.py",
|
||||
"demo/assets/icon.png",
|
||||
"demo/requirements.txt",
|
||||
],
|
||||
)
|
||||
]
|
||||
assert (
|
||||
mock_base_path / "plugins" / "demo" / "assets" / "icon.png"
|
||||
).read_bytes() == b"\x89PNG\r\n\x1a\n"
|
||||
install_requirement.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_zero_byte_aliyun_binary_falls_back_to_github(
|
||||
mocker: MockerFixture,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from zhenxun.builtin_plugins.plugin_store.data_source import StoreManager
|
||||
from zhenxun.utils.repo_utils.models import (
|
||||
FileDownloadResult,
|
||||
RepoFileInfo,
|
||||
RepoType,
|
||||
)
|
||||
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
calls: list[RepoType] = []
|
||||
files = [
|
||||
RepoFileInfo(path="demo/__init__.py", is_dir=False),
|
||||
RepoFileInfo(path="demo/icon.png", is_dir=False),
|
||||
]
|
||||
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source."
|
||||
"RepoFileManager.list_directory_files",
|
||||
return_value=files,
|
||||
)
|
||||
|
||||
async def download_files(
|
||||
repo_url: str,
|
||||
file_path: list[tuple[str, Path]],
|
||||
branch: str,
|
||||
repo_type: RepoType,
|
||||
ignore_error: bool = False,
|
||||
) -> FileDownloadResult:
|
||||
calls.append(repo_type)
|
||||
for source_path, destination_path in file_path:
|
||||
destination_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if source_path.endswith(".png"):
|
||||
content = b"" if repo_type == RepoType.ALIYUN else b"image"
|
||||
destination_path.write_bytes(content)
|
||||
else:
|
||||
destination_path.write_text("", encoding="utf-8")
|
||||
return FileDownloadResult(
|
||||
repo_type=repo_type,
|
||||
repo_name="demo",
|
||||
file_path=file_path,
|
||||
version=branch,
|
||||
success=True,
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source."
|
||||
"RepoFileManager.download_files",
|
||||
side_effect=download_files,
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source."
|
||||
"VirtualEnvPackageManager.install_requirement",
|
||||
)
|
||||
|
||||
await StoreManager.install_plugin_with_repo(_plugin_info(), is_external=True)
|
||||
|
||||
assert calls[:2] == [RepoType.ALIYUN, RepoType.GITHUB]
|
||||
assert (mock_base_path / "plugins" / "demo" / "icon.png").read_bytes() == b"image"
|
||||
|
||||
|
||||
async def test_forced_aliyun_does_not_fall_back(
|
||||
mocker: MockerFixture,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from zhenxun.builtin_plugins.plugin_store.data_source import StoreManager
|
||||
from zhenxun.builtin_plugins.plugin_store.exceptions import PluginStoreException
|
||||
from zhenxun.utils.repo_utils.models import RepoType
|
||||
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
list_directory_files = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source."
|
||||
"RepoFileManager.list_directory_files",
|
||||
side_effect=RuntimeError("aliyun unavailable"),
|
||||
)
|
||||
|
||||
with pytest.raises(PluginStoreException, match="阿里云"):
|
||||
await StoreManager.install_plugin_with_repo(
|
||||
_plugin_info(),
|
||||
source="ali",
|
||||
)
|
||||
|
||||
assert list_directory_files.await_count == 1
|
||||
await_args = list_directory_files.await_args
|
||||
assert await_args is not None
|
||||
assert await_args.kwargs["repo_type"] == RepoType.ALIYUN
|
||||
|
||||
|
||||
async def test_root_plugin_uses_exact_sparse_paths(
|
||||
mocker: MockerFixture,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from zhenxun.builtin_plugins.plugin_store.data_source import StoreManager
|
||||
from zhenxun.utils.repo_utils.models import (
|
||||
FileDownloadResult,
|
||||
RepoFileInfo,
|
||||
RepoType,
|
||||
)
|
||||
|
||||
mock_base_path = mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.BASE_PATH",
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
plugin_info = _plugin_info(
|
||||
ali_url="https://codeup.aliyun.com/organization/group/demo-mirror"
|
||||
)
|
||||
plugin_info.module_path = "."
|
||||
files = [
|
||||
RepoFileInfo(path="__init__.py", is_dir=False),
|
||||
RepoFileInfo(path="assets/icon.png", is_dir=False),
|
||||
RepoFileInfo(path="requirements.txt", is_dir=False),
|
||||
]
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source."
|
||||
"RepoFileManager.list_directory_files",
|
||||
return_value=files,
|
||||
)
|
||||
downloaded_paths: list[str] = []
|
||||
|
||||
async def download_files(
|
||||
repo_url: str,
|
||||
file_path: list[tuple[str, Path]],
|
||||
branch: str,
|
||||
repo_type: RepoType,
|
||||
ignore_error: bool = False,
|
||||
) -> FileDownloadResult:
|
||||
downloaded_paths.extend(path for path, _ in file_path)
|
||||
for source_path, destination_path in file_path:
|
||||
destination_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
content = b"image" if source_path.endswith(".png") else b""
|
||||
destination_path.write_bytes(content)
|
||||
return FileDownloadResult(
|
||||
repo_type=repo_type,
|
||||
repo_name="demo-mirror",
|
||||
file_path=file_path,
|
||||
version=branch,
|
||||
success=True,
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source."
|
||||
"RepoFileManager.download_files",
|
||||
side_effect=download_files,
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source."
|
||||
"VirtualEnvPackageManager.install_requirement",
|
||||
)
|
||||
|
||||
await StoreManager.install_plugin_with_repo(plugin_info, source="ali")
|
||||
|
||||
assert downloaded_paths == [
|
||||
"__init__.py",
|
||||
"assets/icon.png",
|
||||
"requirements.txt",
|
||||
]
|
||||
assert (
|
||||
mock_base_path / "plugins" / "demo" / "assets" / "icon.png"
|
||||
).read_bytes() == b"image"
|
||||
|
||||
|
||||
async def test_repo_manager_sparse_checkout_preserves_exact_paths(
|
||||
mocker: MockerFixture,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from zhenxun.utils.repo_utils import RepoFileManager
|
||||
from zhenxun.utils.repo_utils.models import RepoType
|
||||
|
||||
async def sparse_checkout_clone(
|
||||
repo_url: str,
|
||||
branch: str,
|
||||
sparse_path: list[str],
|
||||
target_dir: Path,
|
||||
) -> list[str]:
|
||||
assert repo_url == "https://github.com/example/demo.git"
|
||||
assert branch == "master"
|
||||
assert sparse_path == ["demo/assets/icon.png"]
|
||||
source = target_dir / sparse_path[0]
|
||||
source.parent.mkdir(parents=True, exist_ok=True)
|
||||
source.write_bytes(b"image")
|
||||
return sparse_path
|
||||
|
||||
sparse_checkout = mocker.patch(
|
||||
"zhenxun.utils.repo_utils.file_manager.sparse_checkout_clone",
|
||||
side_effect=sparse_checkout_clone,
|
||||
)
|
||||
target = tmp_path / "target" / "icon.png"
|
||||
result = await RepoFileManager.download_files(
|
||||
"https://github.com/example/demo/tree/master",
|
||||
[("demo/assets/icon.png", target)],
|
||||
"master",
|
||||
repo_type=RepoType.GITHUB,
|
||||
)
|
||||
|
||||
assert result.success
|
||||
assert target.read_bytes() == b"image"
|
||||
sparse_checkout.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_sparse_checkout_retries_git_fetch(
|
||||
mocker: MockerFixture,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
from zhenxun.utils.repo_utils.utils import sparse_checkout_clone
|
||||
|
||||
mocker.patch("zhenxun.utils.repo_utils.utils.check_git", return_value=True)
|
||||
sleep = mocker.patch("zhenxun.utils.repo_utils.utils.asyncio.sleep")
|
||||
fetch_attempts = 0
|
||||
fetch_timeouts: list[float | None] = []
|
||||
|
||||
async def run_git_command(
|
||||
command: str | list[str],
|
||||
cwd: Path | None = None,
|
||||
timeout_seconds: float | None = None,
|
||||
) -> tuple[bool, str, str]:
|
||||
nonlocal fetch_attempts
|
||||
if isinstance(command, list) and "fetch" in command:
|
||||
fetch_attempts += 1
|
||||
fetch_timeouts.append(timeout_seconds)
|
||||
if fetch_attempts < 3:
|
||||
return False, "", "connection reset"
|
||||
return True, "", ""
|
||||
|
||||
mocker.patch(
|
||||
"zhenxun.utils.repo_utils.utils.run_git_command",
|
||||
side_effect=run_git_command,
|
||||
)
|
||||
|
||||
downloaded = await sparse_checkout_clone(
|
||||
repo_url="https://github.com/example/demo",
|
||||
branch="main",
|
||||
sparse_path=["demo/__init__.py"],
|
||||
target_dir=tmp_path / "target",
|
||||
)
|
||||
|
||||
assert downloaded == []
|
||||
assert fetch_attempts == 3
|
||||
assert fetch_timeouts == [60, 60, 60]
|
||||
assert sleep.await_count == 2
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("git") is None, reason="git is not installed")
|
||||
async def test_git_checkout_preserves_binary(tmp_path: Path) -> None:
|
||||
from zhenxun.utils.repo_utils.utils import sparse_checkout_clone
|
||||
|
||||
source_repo = tmp_path / "source"
|
||||
source_repo.mkdir()
|
||||
await _run_git("init", "-b", "main", str(source_repo))
|
||||
await _run_git("-C", str(source_repo), "config", "user.name", "test")
|
||||
await _run_git("-C", str(source_repo), "config", "user.email", "test@example.com")
|
||||
binary_content = b"\x89PNG\r\n\x1a\n\x00\x01\xffbinary"
|
||||
(source_repo / "icon.png").write_bytes(binary_content)
|
||||
(source_repo / "__init__.py").write_text("", encoding="utf-8")
|
||||
await _run_git("-C", str(source_repo), "add", ".")
|
||||
await _run_git("-C", str(source_repo), "commit", "-m", "test")
|
||||
|
||||
target_dir = tmp_path / "target"
|
||||
downloaded = await sparse_checkout_clone(
|
||||
repo_url=source_repo.as_uri(),
|
||||
branch="main",
|
||||
sparse_path=["icon.png", "__init__.py"],
|
||||
target_dir=target_dir,
|
||||
)
|
||||
|
||||
assert downloaded == ["icon.png", "__init__.py"]
|
||||
assert (target_dir / "icon.png").read_bytes() == binary_content
|
||||
assert not (target_dir / ".git").exists()
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""绪山真寻 Bot — 基于 NoneBot2 的 QQ 机器人"""
|
||||
@@ -1,11 +1,14 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.drivers import Driver
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.version import Version
|
||||
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
|
||||
@@ -16,7 +19,8 @@ from zhenxun.models.sign_user import SignUser
|
||||
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.resource_manager import ResourceManager
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
from zhenxun.utils.manager.zhenxun_repo_manager import ZhenxunRepoManager
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
driver: Driver = nonebot.get_driver()
|
||||
@@ -29,9 +33,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
|
||||
@@ -49,30 +56,94 @@ 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
|
||||
"""
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
await ResourceManager.init_resources()
|
||||
try:
|
||||
should_update = False
|
||||
resource_path = ZhenxunRepoManager.config.RESOURCE_PATH
|
||||
default_theme_path = resource_path / "themes" / "default"
|
||||
version_file = resource_path / "__version__"
|
||||
|
||||
if (
|
||||
not ZhenxunRepoManager.check_resources_exists()
|
||||
or not default_theme_path.exists()
|
||||
or not version_file.exists()
|
||||
):
|
||||
should_update = True
|
||||
logger.info(
|
||||
"检测到资源文件(字体/主题/版本信息)缺失,准备进行初始化下载...",
|
||||
"资源检查",
|
||||
)
|
||||
else:
|
||||
spec_file = Path("resources.spec")
|
||||
req_ver_str = ">=0.0.0"
|
||||
if spec_file.exists():
|
||||
try:
|
||||
for line in spec_file.read_text("utf-8").splitlines():
|
||||
if line.strip().startswith("require_resources_version:"):
|
||||
req_ver_str = line.split(":", 1)[1].strip().strip("'\"")
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
local_ver_str = "0.0.0"
|
||||
try:
|
||||
content = version_file.read_text("utf-8").strip()
|
||||
local_ver_str = (
|
||||
content.split(":", 1)[1].strip() if ":" in content else content
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not SpecifierSet(req_ver_str).contains(Version(local_ver_str)):
|
||||
should_update = True
|
||||
logger.info(
|
||||
f"资源版本({local_ver_str})不满足要求({req_ver_str}),准备强制更新...",
|
||||
"资源检查",
|
||||
)
|
||||
|
||||
if should_update:
|
||||
logger.info("开始下载资源文件,请耐心等待...", "资源检查")
|
||||
result = await ZhenxunRepoManager.resources_update()
|
||||
if result and not result.success:
|
||||
logger.error(
|
||||
f"资源下载失败: {result.error_message}",
|
||||
"资源检查",
|
||||
)
|
||||
else:
|
||||
logger.info("资源文件下载/更新完成", "资源检查")
|
||||
except Exception as e:
|
||||
logger.error(f"资源检查或更新失败: {e}", "资源检查")
|
||||
"""签到与用户的数据迁移"""
|
||||
if goods_list := await GoodsInfo.filter(uuid__isnull=True).all():
|
||||
for goods in goods_list:
|
||||
@@ -91,8 +162,23 @@ async def _():
|
||||
logger.warning("获取GroupInfoUser数据uid失败...", e=e)
|
||||
user2uid = {u.user_id: u.uid for u in group_user}
|
||||
db = Tortoise.get_connection("default")
|
||||
old_sign_list = await db.execute_query_dict(SIGN_SQL)
|
||||
old_bag_list = await db.execute_query_dict(BAG_SQL)
|
||||
try:
|
||||
old_sign_list = await db.execute_query_dict(SIGN_SQL)
|
||||
except OperationalError as e:
|
||||
if "no such table" in str(e).lower() or "sign_group_users" in str(e):
|
||||
# 旧签到表不存在,说明是全新环境或已完成过迁移,正常跳过
|
||||
logger.debug("旧签到表 sign_group_users 不存在,跳过数据迁移")
|
||||
old_sign_list = []
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
old_bag_list = await db.execute_query_dict(BAG_SQL)
|
||||
except OperationalError as e:
|
||||
if "no such table" in str(e).lower() or "bag_users" in str(e):
|
||||
logger.debug("旧背包表 bag_users 不存在,跳过数据迁移")
|
||||
old_bag_list = []
|
||||
else:
|
||||
raise
|
||||
goods = {
|
||||
g["goods_name"]: g["uuid"]
|
||||
for g in await GoodsInfo.annotate().values("goods_name", "uuid")
|
||||
|
||||
@@ -7,7 +7,7 @@ from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.configs.utils import PluginCdBlock, PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
@@ -19,13 +19,33 @@ __plugin_meta__ = PluginMetadata(
|
||||
指令:
|
||||
关于
|
||||
""".strip(),
|
||||
extra=PluginExtraData(author="HibiKier", version="0.1", menu_type="其他").to_dict(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="其他",
|
||||
limits=[PluginCdBlock(cd=10, result="每10秒只能查看一次哦~")],
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
|
||||
_matcher = on_alconna(Alconna("关于"), priority=5, block=True, rule=to_me())
|
||||
|
||||
|
||||
QQ_INFO = """
|
||||
『绪山真寻Bot』
|
||||
版本:{version}
|
||||
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
|
||||
""".strip()
|
||||
|
||||
INFO = """
|
||||
『绪山真寻Bot』
|
||||
版本:{version}
|
||||
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
|
||||
项目地址:https://github.com/zhenxun-org/zhenxun_bot
|
||||
文档地址:https://zhenxun-org.github.io/zhenxun_bot/
|
||||
""".strip()
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: Uninfo, arparma: Arparma):
|
||||
ver_file = Path() / "__version__"
|
||||
@@ -35,25 +55,11 @@ async def _(session: Uninfo, arparma: Arparma):
|
||||
if text := await f.read():
|
||||
version = text.split(":")[-1].strip()
|
||||
if PlatformUtils.is_qbot(session):
|
||||
info: list[str | Path] = [
|
||||
f"""
|
||||
『绪山真寻Bot』
|
||||
版本:{version}
|
||||
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
|
||||
""".strip()
|
||||
]
|
||||
result: list[str | Path] = [QQ_INFO.format(version=version)]
|
||||
path = DATA_PATH / "about.png"
|
||||
if path.exists():
|
||||
info.append(path)
|
||||
result.append(path)
|
||||
await MessageUtils.build_message(result).send() # type: ignore
|
||||
else:
|
||||
info = [
|
||||
f"""
|
||||
『绪山真寻Bot』
|
||||
版本:{version}
|
||||
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
|
||||
项目地址:https://github.com/HibiKier/zhenxun_bot
|
||||
文档地址:https://hibikier.github.io/zhenxun_bot/
|
||||
""".strip()
|
||||
]
|
||||
await MessageUtils.build_message(info).send() # type: ignore
|
||||
logger.info("查看关于", arparma.header_result, session=session)
|
||||
await MessageUtils.build_message(INFO.format(version=version)).send()
|
||||
logger.info("查看关于", arparma.header_result, session=session)
|
||||
|
||||
@@ -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
|
||||
@@ -14,13 +14,19 @@ from nonebot_plugin_alconna import (
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.configs.utils import (
|
||||
AICallableParam,
|
||||
AICallableProperties,
|
||||
AICallableTag,
|
||||
PluginExtraData,
|
||||
RegisterConfig,
|
||||
)
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.rules import admin_check
|
||||
|
||||
from ._data_source import BanManage
|
||||
from ._data_source import BanManage, call_ban
|
||||
|
||||
base_config = Config.get("ban")
|
||||
|
||||
@@ -30,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(
|
||||
@@ -44,7 +51,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
superuser_help="""
|
||||
超级管理员额外命令
|
||||
格式:
|
||||
ban [At用户/用户Id] ?[-t [时长]]
|
||||
ban [At用户/用户Id] ?[-t [时长]] ?[-r [理由]]
|
||||
unban --id [idx] : 通过id来进行unban操作
|
||||
ban列表: 获取所有Ban数据
|
||||
|
||||
@@ -60,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从小黑屋中拉出来
|
||||
@@ -78,6 +88,29 @@ __plugin_meta__ = PluginMetadata(
|
||||
type=int,
|
||||
)
|
||||
],
|
||||
smart_tools=[
|
||||
AICallableTag(
|
||||
name="call_ban",
|
||||
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"],
|
||||
),
|
||||
func=call_ban,
|
||||
)
|
||||
],
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
@@ -86,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]),
|
||||
),
|
||||
@@ -159,6 +193,7 @@ async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
user: Match[str | At],
|
||||
reason: Match[str],
|
||||
duration: Match[int],
|
||||
group_id: Match[str],
|
||||
):
|
||||
@@ -174,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} 反被拿下",
|
||||
@@ -196,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",
|
||||
@@ -218,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,
|
||||
@@ -270,7 +311,7 @@ async def _(
|
||||
At(flag="user", target=user_id)
|
||||
if isinstance(user.result, At)
|
||||
else result
|
||||
), # type: ignore
|
||||
),
|
||||
" 从黑屋中拉了出来并急救了一下!",
|
||||
]
|
||||
).finish(reply_to=True)
|
||||
|
||||
@@ -5,9 +5,20 @@ from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
|
||||
|
||||
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, reason, duration * 60)
|
||||
logger.info("被讨厌了,已将用户加入黑名单...", "ban", session=user_id)
|
||||
|
||||
|
||||
class BanManage:
|
||||
@classmethod
|
||||
async def build_ban_image(
|
||||
@@ -44,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,
|
||||
]
|
||||
@@ -97,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
|
||||
@@ -120,6 +137,7 @@ class BanManage:
|
||||
cls,
|
||||
user_id: str | None,
|
||||
group_id: str | None,
|
||||
reason: str | None,
|
||||
duration: int,
|
||||
session: EventSession,
|
||||
is_superuser: bool,
|
||||
@@ -129,6 +147,7 @@ class BanManage:
|
||||
参数:
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
reason: 理由
|
||||
duration: 时长,秒
|
||||
session: Session
|
||||
is_superuser: 是否为超级用户操作
|
||||
@@ -136,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,15 +1,23 @@
|
||||
import asyncio
|
||||
import random
|
||||
import time
|
||||
|
||||
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
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_uninfo import Scene, SceneType, get_interface
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.services.message_load import should_pause_tasks
|
||||
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
|
||||
@@ -33,6 +41,11 @@ __plugin_meta__ = PluginMetadata(
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
_FULL_REFRESH_INTERVAL_SECONDS = 24 * 60 * 60
|
||||
|
||||
_GROUP_LAST_UPDATE: dict[tuple[str, str], float] = {}
|
||||
_UPDATE_SEMAPHORE = asyncio.Semaphore(1)
|
||||
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna("更新群组成员信息"),
|
||||
@@ -45,49 +58,177 @@ _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,
|
||||
)
|
||||
|
||||
|
||||
def _group_key(bot_id: str, group_id: str) -> tuple[str, str]:
|
||||
return bot_id, group_id
|
||||
|
||||
|
||||
async def _build_scene_map(bot: Bot) -> dict[str, Scene]:
|
||||
if not (interface := get_interface(bot)):
|
||||
return {}
|
||||
scenes = await interface.get_scenes(SceneType.GROUP)
|
||||
return {scene.id: scene for scene in scenes if scene.is_group}
|
||||
|
||||
|
||||
async def _run_update(
|
||||
bot: Bot,
|
||||
group_id: str,
|
||||
*,
|
||||
scene_map: dict[str, Scene] | None = None,
|
||||
platform: str | None = None,
|
||||
force: bool = False,
|
||||
) -> str | None:
|
||||
key = _group_key(bot.self_id, group_id)
|
||||
async with _UPDATE_SEMAPHORE:
|
||||
result = await MemberUpdateManage.update_group_member(
|
||||
bot, group_id, scene_map=scene_map, platform=platform
|
||||
)
|
||||
_GROUP_LAST_UPDATE[key] = time.time()
|
||||
return result
|
||||
|
||||
|
||||
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:
|
||||
scene_map = await _build_scene_map(bot)
|
||||
platform = PlatformUtils.get_platform(bot)
|
||||
group_ids = list(scene_map.keys())
|
||||
total_count = len(group_ids)
|
||||
for i, group_id in enumerate(group_ids):
|
||||
try:
|
||||
logger.debug(
|
||||
f"Bot {bot_id}: 正在更新第 {i + 1}/{total_count} 个群组: "
|
||||
f"{group_id}",
|
||||
"更新所有群组",
|
||||
)
|
||||
await _run_update(
|
||||
bot,
|
||||
group_id,
|
||||
scene_map=scene_map,
|
||||
platform=platform,
|
||||
force=True,
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
fail_count += 1
|
||||
logger.error(
|
||||
f"Bot {bot_id}: 更新群组 {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 MessageUtils.build_message("群组id为空...").send()
|
||||
if not (gid := session.id3 or session.id2):
|
||||
await MessageUtils.build_message("群组id为空...").send()
|
||||
return
|
||||
logger.info("更新群组成员信息", arparma.header_result, session=session)
|
||||
result = await _run_update(bot, gid, force=True)
|
||||
await MessageUtils.build_message(result or "更新已完成").finish(reply_to=True)
|
||||
await tag_manager._invalidate_cache()
|
||||
|
||||
|
||||
@_notice.handle()
|
||||
async def _(bot: Bot, event: GroupIncreaseNoticeEvent):
|
||||
if str(event.user_id) == bot.self_id:
|
||||
await MemberUpdateManage.update_group_member(bot, str(event.group_id))
|
||||
await _run_update(bot, str(event.group_id), force=True)
|
||||
logger.info(
|
||||
f"{BotConfig.self_nickname}加入群聊更新群组信息",
|
||||
"更新群组成员列表",
|
||||
session=event.user_id,
|
||||
group_id=event.group_id,
|
||||
)
|
||||
await tag_manager._invalidate_cache()
|
||||
|
||||
|
||||
@scheduler.scheduled_job(
|
||||
"interval",
|
||||
minutes=5,
|
||||
"cron",
|
||||
hour=3,
|
||||
minute=0,
|
||||
max_instances=1,
|
||||
coalesce=True,
|
||||
)
|
||||
async def _():
|
||||
for bot in nonebot.get_bots().values():
|
||||
if PlatformUtils.get_platform(bot) == "qq":
|
||||
try:
|
||||
group_list, _ = await PlatformUtils.get_group_list(bot)
|
||||
if group_list:
|
||||
for group in group_list:
|
||||
try:
|
||||
await MemberUpdateManage.update_group_member(
|
||||
bot, group.group_id
|
||||
)
|
||||
logger.debug("自动更新群组成员信息成功...")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Bot: {bot.self_id} 自动更新群组成员信息失败",
|
||||
target=group.group_id,
|
||||
e=e,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Bot: {bot.self_id} 自动更新群组信息", e=e)
|
||||
logger.debug(f"自动 Bot: {bot.self_id} 更新群组成员信息成功...")
|
||||
async def _nightly_full_refresh():
|
||||
if should_pause_tasks():
|
||||
return
|
||||
now = time.time()
|
||||
bots = nonebot.get_bots()
|
||||
if not bots:
|
||||
return
|
||||
updated = 0
|
||||
for bot in bots.values():
|
||||
platform = PlatformUtils.get_platform(bot)
|
||||
if platform != "qq":
|
||||
continue
|
||||
try:
|
||||
scene_map = await _build_scene_map(bot)
|
||||
if not scene_map:
|
||||
continue
|
||||
for group_id in scene_map:
|
||||
key = _group_key(bot.self_id, group_id)
|
||||
last_update = _GROUP_LAST_UPDATE.get(key, 0)
|
||||
if now - last_update < _FULL_REFRESH_INTERVAL_SECONDS:
|
||||
continue
|
||||
try:
|
||||
result = await _run_update(
|
||||
bot,
|
||||
group_id,
|
||||
scene_map=scene_map,
|
||||
platform=platform,
|
||||
force=True,
|
||||
)
|
||||
if result is not None:
|
||||
updated += 1
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Bot: {bot.self_id} 夜间更新群组成员信息失败",
|
||||
target=group_id,
|
||||
e=e,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Bot: {bot.self_id} 夜间更新群组信息", e=e)
|
||||
if updated:
|
||||
await tag_manager._invalidate_cache()
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot_plugin_uninfo import Member, SceneType, get_interface
|
||||
from nonebot_plugin_uninfo import Member, Scene, 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.hot_query_cache import (
|
||||
invalidate_group_members,
|
||||
invalidate_member_names,
|
||||
)
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
@@ -16,10 +22,13 @@ class MemberUpdateManage:
|
||||
async def __handle_user(
|
||||
cls,
|
||||
member: Member,
|
||||
db_user: list[GroupInfoUser],
|
||||
db_user_map: dict[str, list[GroupInfoUser]],
|
||||
group_id: str,
|
||||
data_list: tuple[list, list, list],
|
||||
data_list: tuple[list[GroupInfoUser], list[GroupInfoUser], list[int]],
|
||||
platform: str | None,
|
||||
*,
|
||||
default_auth: int | None,
|
||||
superusers: set[str],
|
||||
):
|
||||
"""单个成员操作
|
||||
|
||||
@@ -30,35 +39,32 @@ class MemberUpdateManage:
|
||||
data_list: 数据列表
|
||||
platform: 平台
|
||||
"""
|
||||
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}
|
||||
if member.id in driver.config.superusers:
|
||||
await LevelUser.set_level(member.id, group_id, 9)
|
||||
member_id = str(member.id)
|
||||
if member_id in superusers:
|
||||
await LevelUser.set_level(member_id, group_id, 9)
|
||||
elif role and default_auth:
|
||||
if role.id != "MEMBER" and not await LevelUser.is_group_flag(
|
||||
member.id, group_id
|
||||
member_id, group_id
|
||||
):
|
||||
if role.id == "OWNER":
|
||||
await LevelUser.set_level(member.id, group_id, default_auth + 1)
|
||||
await LevelUser.set_level(member_id, group_id, default_auth + 1)
|
||||
elif role.id == "ADMINISTRATOR":
|
||||
await LevelUser.set_level(member.id, group_id, default_auth)
|
||||
if cnt := db_user_uid.count(member.id):
|
||||
users = [u for u in db_user if u.user_id == member.id]
|
||||
if cnt > 1:
|
||||
for u in users[1:]:
|
||||
data_list[2].append(u.id)
|
||||
if nickname != uid2name.get(member.id):
|
||||
await LevelUser.set_level(member_id, group_id, default_auth)
|
||||
if users := db_user_map.get(member_id):
|
||||
if len(users) > 1:
|
||||
data_list[2].extend(u.id for u in users[1:])
|
||||
if nickname != users[0].user_name:
|
||||
user = users[0]
|
||||
user.user_name = nickname
|
||||
data_list[1].append(user)
|
||||
else:
|
||||
data_list[0].append(
|
||||
GroupInfoUser(
|
||||
user_id=member.id,
|
||||
user_id=member_id,
|
||||
group_id=group_id,
|
||||
user_name=nickname,
|
||||
user_join_time=member.joined_at or datetime.now(),
|
||||
@@ -67,7 +73,14 @@ class MemberUpdateManage:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def update_group_member(cls, bot: Bot, group_id: str) -> str:
|
||||
async def update_group_member(
|
||||
cls,
|
||||
bot: Bot,
|
||||
group_id: str,
|
||||
*,
|
||||
scene_map: dict[str, Scene] | None = None,
|
||||
platform: str | None = None,
|
||||
) -> str:
|
||||
"""更新群组成员信息
|
||||
|
||||
参数:
|
||||
@@ -81,45 +94,76 @@ class MemberUpdateManage:
|
||||
logger.warning(f"bot: {bot.self_id},group_id为空,无法更新群成员信息...")
|
||||
return "群组id为空..."
|
||||
if interface := get_interface(bot):
|
||||
scenes = await interface.get_scenes()
|
||||
platform = PlatformUtils.get_platform(bot)
|
||||
group_list = [s for s in scenes if s.is_group and s.id == group_id]
|
||||
if not group_list:
|
||||
if scene_map is None:
|
||||
scenes = await interface.get_scenes(SceneType.GROUP)
|
||||
scene_map = {scene.id: scene for scene in scenes if scene.is_group}
|
||||
if platform is None:
|
||||
platform = PlatformUtils.get_platform(bot)
|
||||
group_scene = scene_map.get(group_id) if scene_map else None
|
||||
if not group_scene:
|
||||
logger.warning(
|
||||
f"bot: {bot.self_id},group_id: {group_id},群组不存在,"
|
||||
"无法更新群成员信息..."
|
||||
)
|
||||
return "更新群组失败,群组不存在..."
|
||||
members = await interface.get_members(SceneType.GROUP, group_list[0].id)
|
||||
members = await interface.get_members(SceneType.GROUP, group_scene.id)
|
||||
|
||||
try:
|
||||
group_console, _ = await GroupConsole.get_or_create_root_group(
|
||||
group_id=group_id, defaults={"platform": platform}
|
||||
)
|
||||
group_console.member_count = len(members)
|
||||
group_console.group_name = group_scene.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 = ([], [], [])
|
||||
exist_member_list = []
|
||||
db_user_map: dict[str, list[GroupInfoUser]] = {}
|
||||
for user in db_user:
|
||||
db_user_map.setdefault(user.user_id, []).append(user)
|
||||
db_user_ids = set(db_user_map)
|
||||
data_list: tuple[list[GroupInfoUser], list[GroupInfoUser], list[int]] = (
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
)
|
||||
exist_member_ids: set[str] = set()
|
||||
driver = nonebot.get_driver()
|
||||
superusers = set(driver.config.superusers)
|
||||
default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH")
|
||||
for member in members:
|
||||
logger.debug(f"即将更新群组成员: {member}", "更新群组成员信息")
|
||||
await cls.__handle_user(member, db_user, group_id, data_list, platform)
|
||||
exist_member_list.append(member.id)
|
||||
member_id = str(member.id)
|
||||
await cls.__handle_user(
|
||||
member,
|
||||
db_user_map,
|
||||
group_id,
|
||||
data_list,
|
||||
platform,
|
||||
default_auth=default_auth,
|
||||
superusers=superusers,
|
||||
)
|
||||
exist_member_ids.add(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(
|
||||
@@ -131,16 +175,22 @@ class MemberUpdateManage:
|
||||
await GroupInfoUser.filter(id__in=data_list[2]).delete()
|
||||
logger.debug(f"删除重复数据 Ids: {data_list[2]}", "更新群组成员信息")
|
||||
|
||||
if delete_member_list := [
|
||||
uid for uid in db_user_uid if uid not in exist_member_list
|
||||
]:
|
||||
if delete_member_ids := db_user_ids - exist_member_ids:
|
||||
await GroupInfoUser.filter(
|
||||
user_id__in=delete_member_list, group_id=group_id
|
||||
user_id__in=list(delete_member_ids), group_id=group_id
|
||||
).delete()
|
||||
logger.info(
|
||||
f"删除已退群用户 {len(delete_member_list)} 条",
|
||||
f"删除已退群用户 {len(delete_member_ids)} 条",
|
||||
"更新群组成员信息",
|
||||
group_id=group_id,
|
||||
platform="qq",
|
||||
)
|
||||
changed_user_ids = (
|
||||
{user.user_id for user in data_list[0]}
|
||||
| {user.user_id for user in data_list[1]}
|
||||
| delete_member_ids
|
||||
)
|
||||
if data_list[0] or data_list[1] or data_list[2] or delete_member_ids:
|
||||
await invalidate_group_members(group_id, changed_user_ids)
|
||||
await invalidate_member_names(changed_user_ids)
|
||||
return "群组成员信息更新完成!"
|
||||
|
||||
@@ -39,6 +39,11 @@ _matcher = on_alconna(
|
||||
async def _(bot: Bot, session: EventSession, arparma: Arparma):
|
||||
logger.info("更新群组信息", arparma.header_result, session=session)
|
||||
try:
|
||||
if PlatformUtils.get_platform_scope(bot) != "qq_client":
|
||||
await MessageUtils.build_message(
|
||||
"当前平台不支持旧群组信息同步,仅 OneBot 协议端可用。"
|
||||
).send(reply_to=True)
|
||||
return
|
||||
await PlatformUtils.update_group(bot)
|
||||
await MessageUtils.build_message("已经成功更新了群组信息!").send(reply_to=True)
|
||||
except Exception:
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.exception import FinishedException
|
||||
from nonebot.permission import SUPERUSER as SUPERUSER_PERM
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import AlconnaQuery, Arparma, Match, Query
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_alconna import AlconnaMatch, AlconnaQuery, Arparma, Match, Query
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.services.tags import tag_manager
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from ._data_source import PluginManage, build_plugin, build_task, delete_help_image
|
||||
from .command import _group_status_matcher, _status_matcher
|
||||
from .data_source import PluginManager
|
||||
from .ui import (
|
||||
build_plugin,
|
||||
build_task,
|
||||
render_global_status,
|
||||
render_group_active_status,
|
||||
)
|
||||
|
||||
base_config = Config.get("plugin_switch")
|
||||
|
||||
@@ -18,59 +28,57 @@ base_config = Config.get("plugin_switch")
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="功能开关",
|
||||
description="对群组内的功能限制,超级用户可以对群组以及全局的功能被动开关限制",
|
||||
usage="""
|
||||
普通管理员
|
||||
格式:
|
||||
开启/关闭[功能名称] : 开关功能
|
||||
开启/关闭群被动[被动名称] : 群被动开关
|
||||
开启/关闭所有插件 : 开启/关闭当前群组所有插件状态
|
||||
开启/关闭所有群被动 : 开启/关闭当前群组所有群被动
|
||||
群被动状态 : 查看被动技能开关状态
|
||||
醒来 : 结束休眠
|
||||
休息吧 : 群组休眠, 不会再响应命令
|
||||
usage="""### 基础开关控制
|
||||
- `开启/关闭 [功能名...]`:在当前群开启/关闭指定功能
|
||||
- `开启/关闭被动 [被动名...]`:在当前群开启/关闭指定被动
|
||||
- `开启/关闭所有功能`:在当前群开启/关闭所有功能
|
||||
- `开启/关闭所有被动`:在当前群开启/关闭所有被动
|
||||
|
||||
示例:
|
||||
开启签到 : 开启签到
|
||||
关闭签到 : 关闭签到
|
||||
开启群被动早晚安 : 关闭被动任务早晚安
|
||||
**操作示例:**
|
||||
- `关闭 签到 抽卡 色图`:在当前群批量关闭指定功能
|
||||
|
||||
""".strip(),
|
||||
### 机器人状态控制
|
||||
- `醒来`:让机器人在当前群恢复工作
|
||||
- `休息吧`:让机器人在当前群进入休眠状态
|
||||
""",
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
version="1.0",
|
||||
plugin_type=PluginType.SUPER_AND_ADMIN,
|
||||
superuser_help="""
|
||||
格式:
|
||||
插件列表
|
||||
开启/关闭[功能名称] ?[-t ["private", "p", "group", "g"](关闭类型)] ?[-g 群组Id]
|
||||
|
||||
开启/关闭插件df[功能名称]: 开启/关闭指定插件进群默认状态
|
||||
开启/关闭所有插件df: 开启/关闭所有插件进群默认状态
|
||||
开启/关闭所有插件:
|
||||
私聊中: 开启/关闭所有插件全局状态
|
||||
群组中: 开启/关闭当前群组所有插件状态
|
||||
|
||||
开启/关闭群被动[name] ?[-g [group_id]]
|
||||
私聊中: 开启/关闭全局指定的被动状态
|
||||
群组中: 开启/关闭当前群组指定的被动状态
|
||||
示例:
|
||||
关闭群被动早晚安
|
||||
关闭群被动早晚安 -g 12355555
|
||||
|
||||
开启/关闭所有群被动 ?[-g [group_id]]
|
||||
私聊中: 开启/关闭全局或指定群组被动状态
|
||||
示例:
|
||||
开启所有群被动: 开启全局所有被动
|
||||
开启所有群被动 -g 12345678: 开启群组12345678所有被动
|
||||
|
||||
私聊下:
|
||||
示例:
|
||||
开启签到 : 全局开启签到
|
||||
关闭签到 : 全局关闭签到
|
||||
关闭签到 p : 全局私聊关闭签到
|
||||
关闭签到 -g 12345678 : 关闭群组12345678的签到功能(普通管理员无法开启)
|
||||
""",
|
||||
admin_level=base_config.get("CHANGE_GROUP_SWITCH_LEVEL", 2),
|
||||
superuser_help="""### 状态查询
|
||||
- `插件列表`:查看所有插件的全局状态、群聊状态
|
||||
- `被动状态`:查看所有被动技能的状态
|
||||
- `查看功能状态 [功能名]`:查看指定功能在所有群组中的开关状态
|
||||
- `查看被动状态 [被动名]`:查看指定被动在所有群组中的开关状态
|
||||
- `查看群状态`:查看所有群组的休眠/工作状态
|
||||
|
||||
### 高级开关控制 (跨群/全局)
|
||||
支持在指令后追加以下参数进行批量操作:
|
||||
- `-g <群号>`:指定操作目标群(可多个)
|
||||
- `-t <标签>`:指定操作带有特定标签的群
|
||||
- `--all`:操作所有群组
|
||||
- `--only`:白名单模式,仅在指定群组开启,其他群组自动关闭
|
||||
- `-s`:**强制管控**。使用系统级字段禁用功能,群管理员无法通过普通指令自行开启
|
||||
|
||||
**操作示例:**
|
||||
- `关闭 签到 抽卡 -t 游戏群`:关闭所有带有"游戏群"标签的群的签到和抽卡功能
|
||||
- `开启 色图 --only -g 123456 654321`:仅在这两个群开启色图,其余群全部关闭
|
||||
- `关闭 色图 -s`:在当前群强制锁定关闭色图,群管无法开启
|
||||
|
||||
### 系统级开关
|
||||
追加 `--type [范围]` 或使用特定快捷词实现系统级控制。
|
||||
范围:`p` (私聊), `g` (所有群聊), `a` (全局)
|
||||
|
||||
- `关闭 签到 --type a`:全局彻底禁用签到功能
|
||||
- `开启/关闭默认 [功能名]`:修改功能进群时的默认开关状态
|
||||
- `开启/关闭所有默认功能`:批量修改所有功能的进群默认状态
|
||||
|
||||
### 强制唤醒/休眠
|
||||
同样支持高级目标参数。
|
||||
- `休息吧 --all`:所有群组进入休眠
|
||||
- `醒来 -t 内部测试群`:唤醒带有该标签的群组
|
||||
""",
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="CHANGE_GROUP_SWITCH_LEVEL",
|
||||
@@ -87,10 +95,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(
|
||||
"查看功能列表",
|
||||
@@ -98,261 +106,345 @@ async def _(
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(image).finish(reply_to=True)
|
||||
else:
|
||||
await MessageUtils.build_message("权限不足捏...").finish(reply_to=True)
|
||||
|
||||
|
||||
async def get_target_groups(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
session: Uninfo,
|
||||
tag: str | None,
|
||||
groups: tuple[str, ...] | None,
|
||||
all_scope: bool,
|
||||
) -> set[str] | None:
|
||||
"""解析目标群组列表,包含标签、群号和全量选项。"""
|
||||
targets: set[str] = set()
|
||||
is_superuser = await SUPERUSER_PERM(bot, event)
|
||||
|
||||
if (tag or groups or all_scope) and not is_superuser:
|
||||
return None
|
||||
|
||||
if groups:
|
||||
targets.update(str(group_id) for group_id in groups if group_id)
|
||||
|
||||
if tag:
|
||||
tag_groups = await tag_manager.resolve_tag_to_group_ids(tag, bot=bot)
|
||||
targets.update(str(group_id) for group_id in tag_groups)
|
||||
|
||||
if all_scope:
|
||||
all_groups, _ = await PlatformUtils.get_group_list(bot)
|
||||
targets.update(str(group.group_id) for group in all_groups if group.group_id)
|
||||
|
||||
if not targets and session.group:
|
||||
targets.add(str(session.group.id))
|
||||
|
||||
return targets
|
||||
|
||||
|
||||
async def _handle_switch_command(
|
||||
status: bool,
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
plugin_names: Match[tuple[str, ...]],
|
||||
groups: Match[tuple[str, ...]] = AlconnaMatch("groups"),
|
||||
tag: Match[str] = AlconnaMatch("tag"),
|
||||
task: Query[bool] = AlconnaQuery("task.value", False),
|
||||
default_status: Query[bool] = AlconnaQuery("default.value", False),
|
||||
all_groups_flag: Query[bool] = AlconnaQuery("all.value", False),
|
||||
all_plugins_flag: Query[bool] = AlconnaQuery("all-plugins.value", False),
|
||||
only_flag: Query[bool] | None = None,
|
||||
use_su_field: Query[bool] = AlconnaQuery("su.value", False),
|
||||
):
|
||||
is_superuser = await SUPERUSER_PERM(bot, event)
|
||||
only_flag_value = only_flag.result if only_flag else False
|
||||
|
||||
is_remote = bool(
|
||||
tag.available or groups.available or all_groups_flag.result or only_flag_value
|
||||
)
|
||||
use_su_field_final = is_remote or use_su_field.result
|
||||
|
||||
sub_name = "open" if status else "close"
|
||||
block_type_val = arparma.query(f"{sub_name}.type.block_type")
|
||||
|
||||
if block_type_val is not None:
|
||||
if not is_superuser:
|
||||
return
|
||||
if task.result:
|
||||
await MessageUtils.build_message(
|
||||
"被动技能不支持指定禁用范围,请直接使用 开启/关闭"
|
||||
).finish(reply_to=True)
|
||||
|
||||
if not all_plugins_flag.result and not plugin_names.available:
|
||||
await MessageUtils.build_message("请输入功能/被动名称").finish(reply_to=True)
|
||||
|
||||
targets = await get_target_groups(
|
||||
bot,
|
||||
event,
|
||||
session,
|
||||
tag.result if tag and tag.available else None,
|
||||
groups.result if groups and groups.available else None,
|
||||
all_groups_flag.result,
|
||||
)
|
||||
if targets is None:
|
||||
return
|
||||
|
||||
if all_plugins_flag.result:
|
||||
if targets:
|
||||
messages = []
|
||||
for gid in targets:
|
||||
messages.append(
|
||||
await PluginManager.set_all_plugin_status(
|
||||
status=status,
|
||||
is_default=default_status.result if is_superuser else False,
|
||||
group_id=gid,
|
||||
is_task=task.result,
|
||||
is_superuser=is_superuser,
|
||||
use_su_field=use_su_field_final,
|
||||
)
|
||||
)
|
||||
await MessageUtils.build_message("\n".join(messages)).finish(reply_to=True)
|
||||
if is_superuser and not session.group:
|
||||
result = await PluginManager.set_all_plugin_status(
|
||||
status=status,
|
||||
is_default=default_status.result,
|
||||
group_id=None,
|
||||
is_task=task.result,
|
||||
is_superuser=is_superuser,
|
||||
use_su_field=use_su_field_final,
|
||||
)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
await MessageUtils.build_message("请输入目标群组").finish(reply_to=True)
|
||||
|
||||
names = plugin_names.result if plugin_names.available else ()
|
||||
if isinstance(names, str):
|
||||
names = (names,)
|
||||
|
||||
if (
|
||||
not targets
|
||||
and (not is_superuser or session.group)
|
||||
and not default_status.result
|
||||
and block_type_val is None
|
||||
):
|
||||
await MessageUtils.build_message("请选择一个目标群组").finish(reply_to=True)
|
||||
|
||||
messages = []
|
||||
for name in names:
|
||||
name_str = str(name)
|
||||
if is_superuser and default_status.result:
|
||||
result = await PluginManager.set_default_status(
|
||||
name_str, status, is_task=task.result
|
||||
)
|
||||
messages.append(result)
|
||||
continue
|
||||
|
||||
if block_type_val is not None:
|
||||
_type = BlockType.ALL
|
||||
if block_type_val in ["p", "private"]:
|
||||
_type = BlockType.PRIVATE
|
||||
elif block_type_val in ["g", "group"]:
|
||||
_type = BlockType.GROUP
|
||||
result = await PluginManager.superuser_set_status(
|
||||
name_str, status, _type, None, is_task=task.result
|
||||
)
|
||||
messages.append(result)
|
||||
continue
|
||||
|
||||
if not targets:
|
||||
if is_superuser and not session.group:
|
||||
target_block_type = None if status else BlockType.ALL
|
||||
result = await PluginManager.superuser_set_status(
|
||||
name_str, status, target_block_type, None, is_task=task.result
|
||||
)
|
||||
messages.append(result)
|
||||
continue
|
||||
messages.append(f"{name_str}: 请选择一个目标群组")
|
||||
continue
|
||||
|
||||
msg = await PluginManager.batch_update_status(
|
||||
name_str,
|
||||
targets,
|
||||
status=status,
|
||||
is_task=task.result,
|
||||
is_superuser=is_superuser,
|
||||
is_whitelist_mode=only_flag_value,
|
||||
use_su_field=use_su_field_final,
|
||||
bot=bot,
|
||||
)
|
||||
action_name = "开启" if status else "关闭"
|
||||
logger.info(
|
||||
f"{action_name}操作: {name_str}, targets={targets}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
messages.append(msg)
|
||||
|
||||
await MessageUtils.build_message("\n".join(messages)).finish(reply_to=True)
|
||||
|
||||
|
||||
@_status_matcher.assign("open")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
event: Event,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
plugin_name: Match[str],
|
||||
group: Match[str],
|
||||
plugin_names: Match[tuple[str, ...]],
|
||||
groups: Match[tuple[str, ...]] = AlconnaMatch("groups"),
|
||||
tag: Match[str] = AlconnaMatch("tag"),
|
||||
task: Query[bool] = AlconnaQuery("task.value", False),
|
||||
default_status: Query[bool] = AlconnaQuery("default.value", False),
|
||||
all: Query[bool] = AlconnaQuery("all.value", False),
|
||||
all_groups_flag: Query[bool] = AlconnaQuery("all.value", False),
|
||||
all_plugins_flag: Query[bool] = AlconnaQuery("all-plugins.value", False),
|
||||
only_flag: Query[bool] = AlconnaQuery("only.value", False),
|
||||
use_su_field: Query[bool] = AlconnaQuery("su.value", False),
|
||||
):
|
||||
if not all.result and not plugin_name.available:
|
||||
await MessageUtils.build_message("请输入功能名称").finish(reply_to=True)
|
||||
name = plugin_name.result
|
||||
if gid := session.id3 or session.id2:
|
||||
"""修改当前群组的数据"""
|
||||
if task.result:
|
||||
if all.result:
|
||||
result = await PluginManage.unblock_group_all_task(gid)
|
||||
logger.info("开启所有群组被动", arparma.header_result, session=session)
|
||||
else:
|
||||
result = await PluginManage.unblock_group_task(name, gid)
|
||||
logger.info(
|
||||
f"开启群组被动 {name}", arparma.header_result, session=session
|
||||
)
|
||||
elif session.id1 in bot.config.superusers and default_status.result:
|
||||
"""单个插件的进群默认修改"""
|
||||
result = await PluginManage.set_default_status(name, True)
|
||||
logger.info(
|
||||
f"超级用户开启 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
elif all.result:
|
||||
"""所有插件"""
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
True, default_status.result, gid
|
||||
)
|
||||
logger.info(
|
||||
"开启群组中全部功能",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.unblock_group_plugin(name, gid)
|
||||
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:
|
||||
"""私聊"""
|
||||
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)
|
||||
else:
|
||||
result = await PluginManage.unblock_global_all_task()
|
||||
else:
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
True, default_status.result, group_id
|
||||
)
|
||||
logger.info(
|
||||
"超级用户开启全部功能全局开关"
|
||||
f" {f'指定群组: {group_id}' if group_id else ''}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
if default_status.result:
|
||||
result = await PluginManage.set_default_status(name, True)
|
||||
logger.info(
|
||||
f"超级用户开启 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
target=group_id,
|
||||
)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
if task.result:
|
||||
split_list = name.split()
|
||||
if len(split_list) > 1:
|
||||
name = split_list[0]
|
||||
group_id = split_list[1]
|
||||
if group_id:
|
||||
result = await PluginManage.superuser_task_handle(name, group_id, True)
|
||||
logger.info(
|
||||
f"超级用户开启被动技能 {name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
target=group_id,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.unblock_global_task(name)
|
||||
logger.info(
|
||||
f"超级用户开启全局被动技能 {name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.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)
|
||||
await _handle_switch_command(
|
||||
True,
|
||||
bot,
|
||||
event,
|
||||
session,
|
||||
arparma,
|
||||
plugin_names,
|
||||
groups,
|
||||
tag,
|
||||
task,
|
||||
default_status,
|
||||
all_groups_flag,
|
||||
all_plugins_flag,
|
||||
only_flag=only_flag,
|
||||
use_su_field=use_su_field,
|
||||
)
|
||||
|
||||
|
||||
@_status_matcher.assign("close")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
event: Event,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
plugin_name: Match[str],
|
||||
block_type: Match[str],
|
||||
group: Match[str],
|
||||
plugin_names: Match[tuple[str, ...]],
|
||||
groups: Match[tuple[str, ...]] = AlconnaMatch("groups"),
|
||||
tag: Match[str] = AlconnaMatch("tag"),
|
||||
task: Query[bool] = AlconnaQuery("task.value", False),
|
||||
default_status: Query[bool] = AlconnaQuery("default.value", False),
|
||||
all: Query[bool] = AlconnaQuery("all.value", False),
|
||||
all_groups_flag: Query[bool] = AlconnaQuery("all.value", False),
|
||||
all_plugins_flag: Query[bool] = AlconnaQuery("all-plugins.value", False),
|
||||
use_su_field: Query[bool] = AlconnaQuery("su.value", False),
|
||||
):
|
||||
if not all.result and not plugin_name.available:
|
||||
await MessageUtils.build_message("请输入功能名称").finish(reply_to=True)
|
||||
name = plugin_name.result
|
||||
if gid := session.id3 or session.id2:
|
||||
"""修改当前群组的数据"""
|
||||
if task.result:
|
||||
if all.result:
|
||||
result = await PluginManage.block_group_all_task(gid)
|
||||
logger.info("开启所有群组被动", arparma.header_result, session=session)
|
||||
else:
|
||||
result = await PluginManage.block_group_task(name, gid)
|
||||
logger.info(
|
||||
f"关闭群组被动 {name}", arparma.header_result, session=session
|
||||
)
|
||||
elif session.id1 in bot.config.superusers and default_status.result:
|
||||
"""单个插件的进群默认修改"""
|
||||
result = await PluginManage.set_default_status(name, False)
|
||||
logger.info(
|
||||
f"超级用户开启 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
elif all.result:
|
||||
"""所有插件"""
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
False, default_status.result, gid
|
||||
)
|
||||
logger.info("关闭群组中全部功能", arparma.header_result, session=session)
|
||||
else:
|
||||
result = await PluginManage.block_group_plugin(name, gid)
|
||||
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:
|
||||
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)
|
||||
else:
|
||||
result = await PluginManage.block_global_all_task()
|
||||
else:
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
False, default_status.result, group_id
|
||||
)
|
||||
logger.info(
|
||||
"超级用户关闭全部功能全局开关"
|
||||
f" {f'指定群组: {group_id}' if group_id else ''}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
if default_status.result:
|
||||
result = await PluginManage.set_default_status(name, False)
|
||||
logger.info(
|
||||
f"超级用户关闭 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
target=group_id,
|
||||
)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
if task.result:
|
||||
split_list = name.split()
|
||||
if len(split_list) > 1:
|
||||
name = split_list[0]
|
||||
group_id = split_list[1]
|
||||
if group_id:
|
||||
result = await PluginManage.superuser_task_handle(name, group_id, False)
|
||||
logger.info(
|
||||
f"超级用户关闭被动技能 {name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
target=group_id,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.block_global_task(name)
|
||||
logger.info(
|
||||
f"超级用户关闭全局被动技能 {name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
else:
|
||||
_type = BlockType.ALL
|
||||
if block_type.result in ["p", "private"]:
|
||||
if block_type.available:
|
||||
_type = BlockType.PRIVATE
|
||||
elif block_type.result in ["g", "group"]:
|
||||
if block_type.available:
|
||||
_type = BlockType.GROUP
|
||||
result = await PluginManage.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)
|
||||
await _handle_switch_command(
|
||||
False,
|
||||
bot,
|
||||
event,
|
||||
session,
|
||||
arparma,
|
||||
plugin_names,
|
||||
groups,
|
||||
tag,
|
||||
task,
|
||||
default_status,
|
||||
all_groups_flag,
|
||||
all_plugins_flag,
|
||||
use_su_field=use_su_field,
|
||||
)
|
||||
|
||||
|
||||
@_group_status_matcher.handle()
|
||||
async def _(
|
||||
session: EventSession,
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
status: str,
|
||||
groups: Match[tuple[str, ...]] = AlconnaMatch("groups"),
|
||||
tag: Match[str] = AlconnaMatch("tag"),
|
||||
all_flag: Query[bool] = AlconnaQuery("all.value", False),
|
||||
only_flag: Query[bool] = AlconnaQuery("only.value", False),
|
||||
):
|
||||
if gid := session.id3 or session.id2:
|
||||
if status == "sleep":
|
||||
await PluginManage.sleep(gid)
|
||||
logger.info("进行休眠", arparma.header_result, session=session)
|
||||
await MessageUtils.build_message("那我先睡觉了...").finish()
|
||||
else:
|
||||
if await PluginManage.is_wake(gid):
|
||||
await MessageUtils.build_message("我还醒着呢!").finish()
|
||||
await PluginManage.wake(gid)
|
||||
logger.info("醒来", arparma.header_result, session=session)
|
||||
await MessageUtils.build_message("呜..醒来了...").finish()
|
||||
return MessageUtils.build_message("群组id为空...").send()
|
||||
is_wake = status == "wake"
|
||||
|
||||
if status == "check":
|
||||
if not await SUPERUSER_PERM(bot, event):
|
||||
return
|
||||
|
||||
try:
|
||||
image = await render_group_active_status(bot)
|
||||
logger.info(
|
||||
"查看全服群组工作状态报表", arparma.header_result, session=session
|
||||
)
|
||||
await MessageUtils.build_message(image).finish(reply_to=True)
|
||||
except FinishedException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"渲染群组激活状态报表失败: {e}", e=e)
|
||||
await MessageUtils.build_message("生成状态报表失败,请检查日志").finish(
|
||||
reply_to=True
|
||||
)
|
||||
return
|
||||
|
||||
targets = await get_target_groups(
|
||||
bot,
|
||||
event,
|
||||
session,
|
||||
tag.result if tag and tag.available else None,
|
||||
groups.result if groups and groups.available else None,
|
||||
all_flag.result,
|
||||
)
|
||||
|
||||
if not targets:
|
||||
await MessageUtils.build_message("请指定目标群组或在群聊中使用").finish(
|
||||
reply_to=True
|
||||
)
|
||||
return
|
||||
|
||||
msg = await PluginManager.batch_set_group_active_status(
|
||||
targets, status=is_wake, is_whitelist_mode=only_flag.result, bot=bot
|
||||
)
|
||||
|
||||
action_name = "醒来" if is_wake else "进行休眠"
|
||||
reply_msg = "呜..醒来了..." if is_wake else "那我先睡觉了..."
|
||||
if len(targets) > 1 or only_flag.result:
|
||||
reply_msg = msg
|
||||
|
||||
logger.info(action_name, arparma.header_result, session=session)
|
||||
await MessageUtils.build_message(reply_msg).finish(reply_to=True)
|
||||
|
||||
|
||||
@_status_matcher.assign("task")
|
||||
async def _(
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
):
|
||||
image = await build_task(session.id3 or session.id2)
|
||||
if arparma.find("check") or arparma.find("open") or arparma.find("close"):
|
||||
return
|
||||
|
||||
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)
|
||||
else:
|
||||
await MessageUtils.build_message("获取群被动任务失败...").finish(reply_to=True)
|
||||
|
||||
|
||||
@_status_matcher.assign("check")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
plugin_name: Match[str],
|
||||
task: Query[bool] = AlconnaQuery("task.value", False),
|
||||
):
|
||||
if not await SUPERUSER_PERM(bot, event):
|
||||
return
|
||||
|
||||
name = plugin_name.result
|
||||
try:
|
||||
img = await render_global_status(name, is_task=task.result, bot=bot)
|
||||
await MessageUtils.build_message(img).finish(reply_to=True)
|
||||
except FinishedException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
await MessageUtils.build_message(str(e)).finish(reply_to=True)
|
||||
except Exception as e:
|
||||
logger.error(f"渲染状态图表失败: {e}", e=e)
|
||||
await MessageUtils.build_message("生成状态报表失败,请检查日志").finish(
|
||||
reply_to=True
|
||||
)
|
||||
|
||||
@@ -1,581 +0,0 @@
|
||||
import os
|
||||
|
||||
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.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:
|
||||
"""被动技能文本风格
|
||||
|
||||
参数:
|
||||
column: 表头
|
||||
text: 文本内容
|
||||
|
||||
返回:
|
||||
RowStyle: RowStyle
|
||||
"""
|
||||
style = RowStyle()
|
||||
if (column == "全局状态" and text == "开启") or (
|
||||
column != "全局状态" and column == "加载状态" and text == "SUCCESS"
|
||||
):
|
||||
style.font_color = "#67C23A"
|
||||
elif column in {"全局状态", "加载状态"}:
|
||||
style.font_color = "#F56C6C"
|
||||
return style
|
||||
|
||||
|
||||
async def build_plugin() -> BuildImage:
|
||||
column_name = [
|
||||
"ID",
|
||||
"模块",
|
||||
"名称",
|
||||
"全局状态",
|
||||
"禁用类型",
|
||||
"加载状态",
|
||||
"菜单分类",
|
||||
"作者",
|
||||
"版本",
|
||||
"金币花费",
|
||||
]
|
||||
plugin_list = await PluginInfo.filter(plugin_type__not=PluginType.HIDDEN).all()
|
||||
column_data = [
|
||||
[
|
||||
plugin.id,
|
||||
plugin.module,
|
||||
plugin.name,
|
||||
"开启" if plugin.status else "关闭",
|
||||
plugin.block_type,
|
||||
"SUCCESS" if plugin.load_status else "ERROR",
|
||||
plugin.menu_type,
|
||||
plugin.author,
|
||||
plugin.version,
|
||||
plugin.cost_gold,
|
||||
]
|
||||
for plugin in plugin_list
|
||||
]
|
||||
return await ImageTemplate.table_page(
|
||||
"Plugin",
|
||||
"插件状态",
|
||||
column_name,
|
||||
column_data,
|
||||
text_style=plugin_row_style,
|
||||
)
|
||||
|
||||
|
||||
def task_row_style(column: str, text: str) -> RowStyle:
|
||||
"""被动技能文本风格
|
||||
|
||||
参数:
|
||||
column: 表头
|
||||
text: 文本内容
|
||||
|
||||
返回:
|
||||
RowStyle: RowStyle
|
||||
"""
|
||||
style = RowStyle()
|
||||
if column in {"群组状态", "全局状态"}:
|
||||
style.font_color = "#67C23A" if text == "开启" else "#F56C6C"
|
||||
return style
|
||||
|
||||
|
||||
async def build_task(group_id: str | None) -> BuildImage:
|
||||
"""构造被动技能状态图片
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
|
||||
异常:
|
||||
GroupInfoNotFound: 未找到群组
|
||||
|
||||
返回:
|
||||
BuildImage: 被动技能状态图片
|
||||
"""
|
||||
task_list = await TaskInfo.all()
|
||||
column_name = ["ID", "模块", "名称", "群组状态", "全局状态", "运行时间"]
|
||||
group = None
|
||||
if group_id:
|
||||
group = await GroupConsole.get_or_none(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
)
|
||||
if not group:
|
||||
raise GroupInfoNotFound()
|
||||
else:
|
||||
column_name.remove("群组状态")
|
||||
column_data = []
|
||||
for task in task_list:
|
||||
if group:
|
||||
column_data.append(
|
||||
[
|
||||
task.id,
|
||||
task.module,
|
||||
task.name,
|
||||
"开启" if f"<{task.module}," not in group.block_task else "关闭",
|
||||
"开启" if task.status else "关闭",
|
||||
task.run_time or "-",
|
||||
]
|
||||
)
|
||||
else:
|
||||
column_data.append(
|
||||
[
|
||||
task.id,
|
||||
task.module,
|
||||
task.name,
|
||||
"开启" if task.status else "关闭",
|
||||
task.run_time or "-",
|
||||
]
|
||||
)
|
||||
return await ImageTemplate.table_page(
|
||||
"Task",
|
||||
"被动技能状态",
|
||||
column_name,
|
||||
column_data,
|
||||
text_style=task_row_style,
|
||||
)
|
||||
|
||||
|
||||
class PluginManage:
|
||||
@classmethod
|
||||
async def set_default_status(cls, plugin_name: str, status: bool) -> str:
|
||||
"""设置插件进群默认状态
|
||||
|
||||
参数:
|
||||
plugin_name: 插件名称
|
||||
status: 状态
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
if plugin_name.isdigit():
|
||||
plugin = await PluginInfo.get_or_none(id=int(plugin_name))
|
||||
else:
|
||||
plugin = await PluginInfo.get_or_none(
|
||||
name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT
|
||||
)
|
||||
if plugin:
|
||||
plugin.default_status = status
|
||||
await plugin.save(update_fields=["default_status"])
|
||||
status_text = "开启" if status else "关闭"
|
||||
return f"成功将 {plugin.name} 进群默认状态修改为: {status_text}"
|
||||
return "没有找到这个功能喔..."
|
||||
|
||||
@classmethod
|
||||
async def set_all_plugin_status(
|
||||
cls, status: bool, is_default: bool = False, group_id: str | None = None
|
||||
) -> str:
|
||||
"""修改所有插件状态
|
||||
|
||||
参数:
|
||||
status: 状态
|
||||
is_default: 是否进群默认.
|
||||
group_id: 指定群组id.
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
if is_default:
|
||||
await PluginInfo.filter(plugin_type=PluginType.NORMAL).update(
|
||||
default_status=status
|
||||
)
|
||||
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 status:
|
||||
for module in module_list:
|
||||
group.block_plugin = group.block_plugin.replace(
|
||||
f"<{module},", ""
|
||||
)
|
||||
else:
|
||||
module_list = [f"<{module}" for module in module_list]
|
||||
group.block_plugin = ",".join(module_list) + "," # type: ignore
|
||||
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
|
||||
)
|
||||
return f"成功将所有功能全局状态修改为: {'开启' if status else '关闭'}"
|
||||
|
||||
@classmethod
|
||||
async def is_wake(cls, group_id: str) -> bool:
|
||||
"""是否醒来
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
bool: 是否醒来
|
||||
"""
|
||||
if c := await GroupConsole.get_or_none(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
):
|
||||
return c.status
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def sleep(cls, group_id: str):
|
||||
"""休眠
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
"""
|
||||
await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update(
|
||||
status=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def wake(cls, group_id: str):
|
||||
"""醒来
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
"""
|
||||
await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update(
|
||||
status=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def block(cls, module: str):
|
||||
"""禁用
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
"""
|
||||
await PluginInfo.filter(module=module).update(status=False)
|
||||
|
||||
@classmethod
|
||||
async def unblock(cls, module: str):
|
||||
"""启用
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
"""
|
||||
await PluginInfo.filter(module=module).update(status=True)
|
||||
|
||||
@classmethod
|
||||
async def block_group_plugin(cls, plugin_name: str, group_id: str) -> str:
|
||||
"""禁用群组插件
|
||||
|
||||
参数:
|
||||
plugin_name: 插件名称
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
return await cls._change_group_plugin(plugin_name, group_id, False)
|
||||
|
||||
@classmethod
|
||||
async def unblock_group_task(cls, task_name: str, group_id: str) -> str:
|
||||
"""启用被动技能
|
||||
|
||||
参数:
|
||||
task_name: 被动技能名称
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
return await cls._change_group_task(task_name, group_id, False)
|
||||
|
||||
@classmethod
|
||||
async def unblock_group_all_task(cls, group_id: str) -> str:
|
||||
"""启用被动技能
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
return await cls._change_group_task("", group_id, False, True)
|
||||
|
||||
@classmethod
|
||||
async def block_group_task(cls, task_name: str, group_id: str) -> str:
|
||||
"""禁用被动技能
|
||||
|
||||
参数:
|
||||
task_name: 被动技能名称
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
return await cls._change_group_task(task_name, group_id, True)
|
||||
|
||||
@classmethod
|
||||
async def block_group_all_task(cls, group_id: str) -> str:
|
||||
"""禁用被动技能
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
return await cls._change_group_task("", group_id, True, True)
|
||||
|
||||
@classmethod
|
||||
async def block_global_all_task(cls) -> str:
|
||||
"""禁用全局被动技能
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.all().update(status=False)
|
||||
return "已全局禁用所有被动状态"
|
||||
|
||||
@classmethod
|
||||
async def block_global_task(cls, name: str) -> str:
|
||||
"""禁用全局被动技能
|
||||
|
||||
参数:
|
||||
name: 被动技能名称
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.filter(name=name).update(status=False)
|
||||
return f"已全局禁用被动状态 {name}"
|
||||
|
||||
@classmethod
|
||||
async def unblock_global_all_task(cls) -> str:
|
||||
"""开启全局被动技能
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.all().update(status=True)
|
||||
return "已全局开启所有被动状态"
|
||||
|
||||
@classmethod
|
||||
async def unblock_global_task(cls, name: str) -> str:
|
||||
"""开启全局被动技能
|
||||
|
||||
参数:
|
||||
name: 被动技能名称
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
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:
|
||||
"""启用群组插件
|
||||
|
||||
参数:
|
||||
plugin_name: 插件名称
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
return await cls._change_group_plugin(plugin_name, group_id, True)
|
||||
|
||||
@classmethod
|
||||
async def _change_group_task(
|
||||
cls, task_name: str, group_id: str, status: bool, is_all: bool = False
|
||||
) -> str:
|
||||
"""改变群组被动技能状态
|
||||
|
||||
参数:
|
||||
task_name: 被动技能名称
|
||||
group_id: 群组Id
|
||||
status: 状态,为True时是关闭
|
||||
is_all: 所有群被动
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
status_str = "关闭" if status else "开启"
|
||||
if is_all:
|
||||
modules = await TaskInfo.annotate().values_list("module", flat=True)
|
||||
if modules:
|
||||
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
|
||||
else:
|
||||
for module in modules:
|
||||
group.block_task = group.block_task.replace(f"{module},", "")
|
||||
await group.save(update_fields=["block_task"])
|
||||
return f"已成功{status_str}全部被动技能!"
|
||||
elif task := await TaskInfo.get_or_none(name=task_name):
|
||||
if status:
|
||||
await GroupConsole.set_block_task(group_id, task.module)
|
||||
elif await GroupConsole.is_superuser_block_task(group_id, task.module):
|
||||
return f"{status_str} {task_name} 被动技能失败,当前群组该被动已被管理员禁用" # noqa: E501
|
||||
else:
|
||||
await GroupConsole.set_unblock_task(group_id, task.module)
|
||||
return f"已成功{status_str} {task_name} 被动技能!"
|
||||
return "没有找到这个被动技能喔..."
|
||||
|
||||
@classmethod
|
||||
async def _change_group_plugin(
|
||||
cls, plugin_name: str, group_id: str, status: bool
|
||||
) -> str:
|
||||
"""修改群组插件状态
|
||||
|
||||
参数:
|
||||
plugin_name: 插件名称
|
||||
group_id: 群组id
|
||||
status: 插件状态
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
|
||||
if plugin_name.isdigit():
|
||||
plugin = await PluginInfo.get_or_none(id=int(plugin_name))
|
||||
else:
|
||||
plugin = await PluginInfo.get_or_none(
|
||||
name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT
|
||||
)
|
||||
if plugin:
|
||||
status_str = "开启" if status else "关闭"
|
||||
if status:
|
||||
if await GroupConsole.is_normal_block_plugin(group_id, plugin.module):
|
||||
await GroupConsole.set_unblock_plugin(group_id, plugin.module)
|
||||
return f"已成功{status_str} {plugin.name} 功能!"
|
||||
elif not await GroupConsole.is_normal_block_plugin(group_id, plugin.module):
|
||||
await GroupConsole.set_block_plugin(group_id, plugin.module)
|
||||
return f"已成功{status_str} {plugin.name} 功能!"
|
||||
return f"该功能已经{status_str}了喔,不要重复{status_str}..."
|
||||
return "没有找到这个功能喔..."
|
||||
|
||||
@classmethod
|
||||
async def superuser_task_handle(
|
||||
cls, task_name: str, group_id: str | None, status: bool
|
||||
) -> str:
|
||||
"""超级用户禁用被动技能
|
||||
|
||||
参数:
|
||||
task_name: 被动技能名称
|
||||
group_id: 群组id
|
||||
status: 状态
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
if not (task := await TaskInfo.get_or_none(name=task_name)):
|
||||
return "没有找到这个功能喔..."
|
||||
if group_id:
|
||||
if status:
|
||||
await GroupConsole.set_unblock_task(group_id, task.module, True)
|
||||
else:
|
||||
await GroupConsole.set_block_task(group_id, task.module, True)
|
||||
status_str = "开启" if status else "关闭"
|
||||
return f"已成功将群组 {group_id} 被动技能 {task_name} {status_str}!"
|
||||
return "没有找到这个群组喔..."
|
||||
|
||||
@classmethod
|
||||
async def superuser_block(
|
||||
cls, plugin_name: str, block_type: BlockType | None, group_id: str | None
|
||||
) -> str:
|
||||
"""超级用户禁用插件
|
||||
|
||||
参数:
|
||||
plugin_name: 插件名称
|
||||
block_type: 禁用类型
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
if plugin_name.isdigit():
|
||||
plugin = await PluginInfo.get_or_none(id=int(plugin_name))
|
||||
else:
|
||||
plugin = await PluginInfo.get_or_none(
|
||||
name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT
|
||||
)
|
||||
if plugin:
|
||||
if group_id:
|
||||
if not await GroupConsole.is_superuser_block_plugin(
|
||||
group_id, plugin.module
|
||||
):
|
||||
await GroupConsole.set_block_plugin(group_id, plugin.module, True)
|
||||
return f"已成功关闭群组 {group_id} 的 {plugin_name} 功能!"
|
||||
return "此群组该功能已被超级用户关闭,不要重复关闭..."
|
||||
plugin.block_type = block_type
|
||||
plugin.status = not bool(block_type)
|
||||
await plugin.save(update_fields=["status", "block_type"])
|
||||
if not block_type:
|
||||
return f"已成功将 {plugin.name} 全局启用!"
|
||||
if block_type == BlockType.ALL:
|
||||
return f"已成功将 {plugin.name} 全局关闭!"
|
||||
if block_type == BlockType.GROUP:
|
||||
return f"已成功将 {plugin.name} 全局群组关闭!"
|
||||
if block_type == BlockType.PRIVATE:
|
||||
return f"已成功将 {plugin.name} 全局私聊关闭!"
|
||||
return "没有找到这个功能喔..."
|
||||
|
||||
@classmethod
|
||||
async def superuser_unblock(
|
||||
cls, plugin_name: str, block_type: BlockType | None, group_id: str | None
|
||||
) -> str:
|
||||
"""超级用户开启插件
|
||||
|
||||
参数:
|
||||
plugin_name: 插件名称
|
||||
block_type: 禁用类型
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
if plugin_name.isdigit():
|
||||
plugin = await PluginInfo.get_or_none(id=int(plugin_name))
|
||||
else:
|
||||
plugin = await PluginInfo.get_or_none(
|
||||
name=plugin_name, load_status=True, plugin_type__not=PluginType.PARENT
|
||||
)
|
||||
if plugin:
|
||||
if group_id:
|
||||
if await GroupConsole.is_superuser_block_plugin(
|
||||
group_id, plugin.module
|
||||
):
|
||||
await GroupConsole.set_unblock_plugin(group_id, plugin.module, True)
|
||||
return f"已成功开启群组 {group_id} 的 {plugin_name} 功能!"
|
||||
return "此群组该功能已被超级用户开启,不要重复开启..."
|
||||
plugin.block_type = block_type
|
||||
plugin.status = not bool(block_type)
|
||||
await plugin.save(update_fields=["status", "block_type"])
|
||||
if not block_type:
|
||||
return f"已成功将 {plugin.name} 全局启用!"
|
||||
if block_type == BlockType.ALL:
|
||||
return f"已成功将 {plugin.name} 全局开启!"
|
||||
if block_type == BlockType.GROUP:
|
||||
return f"已成功将 {plugin.name} 全局群组开启!"
|
||||
if block_type == BlockType.PRIVATE:
|
||||
return f"已成功将 {plugin.name} 全局私聊开启!"
|
||||
return "没有找到这个功能喔..."
|
||||
@@ -2,31 +2,46 @@ from nonebot.rule import to_me
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
Args,
|
||||
MultiVar,
|
||||
Option,
|
||||
Subcommand,
|
||||
on_alconna,
|
||||
store_true,
|
||||
)
|
||||
|
||||
from zhenxun.utils.rules import admin_check, ensure_group
|
||||
from zhenxun.utils.rules import admin_check
|
||||
|
||||
_status_matcher = on_alconna(
|
||||
Alconna(
|
||||
"switch",
|
||||
Option("-t|--task", action=store_true, help_text="被动技能"),
|
||||
Option("--task", action=store_true, help_text="被动技能"),
|
||||
Option("-df|--default", action=store_true, help_text="进群默认开关"),
|
||||
Option("--all", action=store_true, help_text="全部插件/被动"),
|
||||
Option("-g|--group", Args["group?", str], help_text="指定群组"),
|
||||
Option("--all-plugins", action=store_true, help_text="所有插件/功能"),
|
||||
Option("--all", action=store_true, help_text="所有群组 (超级用户专用)"),
|
||||
Option("-g|--group", Args["groups", MultiVar(str)], help_text="指定群组"),
|
||||
Option("-t|--tag", Args["tag", str], help_text="指定标签"),
|
||||
Option("-o|--only", action=store_true, help_text="白名单模式(仅在目标群开启)"),
|
||||
Option("-s|--su", action=store_true, help_text="操作超级用户专用字段"),
|
||||
Subcommand(
|
||||
"check",
|
||||
Args["plugin_name", [str, int]],
|
||||
),
|
||||
Subcommand(
|
||||
"open",
|
||||
Args["plugin_name?", [str, int]],
|
||||
Args["plugin_names?", MultiVar(str)],
|
||||
Option(
|
||||
"--type",
|
||||
Args["block_type?", ["all", "a", "private", "p", "group", "g"]],
|
||||
help_text="全局禁用范围",
|
||||
),
|
||||
),
|
||||
Subcommand(
|
||||
"close",
|
||||
Args["plugin_name?", [str, int]],
|
||||
Args["plugin_names?", MultiVar(str)],
|
||||
Option(
|
||||
"-t|--type",
|
||||
"--type",
|
||||
Args["block_type?", ["all", "a", "private", "p", "group", "g"]],
|
||||
help_text="全局禁用范围",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -36,10 +51,15 @@ _status_matcher = on_alconna(
|
||||
)
|
||||
|
||||
_group_status_matcher = on_alconna(
|
||||
Alconna("group-status", Args["status", ["sleep", "wake"]]),
|
||||
rule=admin_check("plugin_switch", "CHANGE_GROUP_SWITCH_LEVEL")
|
||||
& ensure_group
|
||||
& to_me(),
|
||||
Alconna(
|
||||
"group-status",
|
||||
Args["status", ["sleep", "wake", "check"]],
|
||||
Option("-g|--group", Args["groups", MultiVar(str)], help_text="指定群组"),
|
||||
Option("-t|--tag", Args["tag", str], help_text="指定标签"),
|
||||
Option("--all", action=store_true, help_text="所有群组"),
|
||||
Option("-o|--only", action=store_true, help_text="白名单模式(仅在目标群醒来)"),
|
||||
),
|
||||
rule=admin_check("plugin_switch", "CHANGE_GROUP_SWITCH_LEVEL") & to_me(),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
@@ -52,97 +72,42 @@ _status_matcher.shortcut(
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"群被动状态",
|
||||
r"查看(功能|插件)?状态",
|
||||
command="switch check {*}",
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"查看(群)?被动状态",
|
||||
command="switch check {*} --task",
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"(群)?被动状态",
|
||||
command="switch",
|
||||
arguments=["--task"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启群被动\s*(?P<name>.+)",
|
||||
command="switch",
|
||||
arguments=["open", "{name}", "--task"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"关闭群被动\s*(?P<name>.+)",
|
||||
command="switch",
|
||||
arguments=["close", "{name}", "--task"],
|
||||
prefix=True,
|
||||
)
|
||||
def _switch_wrapper(slot: str, content: str | None, context: dict) -> str:
|
||||
"""动态映射转换函数"""
|
||||
if slot == "action":
|
||||
return "open" if content == "开启" else "close"
|
||||
if slot == "all" and content:
|
||||
return "--all-plugins"
|
||||
if slot == "default" and content:
|
||||
return "-df"
|
||||
if slot == "type" and content:
|
||||
return "--task" if "被动" in content else ""
|
||||
return ""
|
||||
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启(所有|全部)群被动",
|
||||
command="switch",
|
||||
arguments=["open", "--task", "--all"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"关闭(所有|全部)群被动",
|
||||
command="switch",
|
||||
arguments=["close", "--task", "--all"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启所有(插件|功能)",
|
||||
command="switch",
|
||||
arguments=["open", "s", "--all"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启所有(插件|功能)df",
|
||||
command="switch",
|
||||
arguments=["open", "s", "-df", "--all"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启(插件|功能)df(?P<name>.+)",
|
||||
command="switch",
|
||||
arguments=["open", "{name}", "-df"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启(?P<name>.+)",
|
||||
command="switch",
|
||||
arguments=["open", "{name}"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"关闭所有(插件|功能)",
|
||||
command="switch",
|
||||
arguments=["close", "s", "--all"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"关闭所有(插件|功能)df",
|
||||
command="switch",
|
||||
arguments=["close", "s", "-df", "--all"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"关闭(插件|功能)df(?P<name>.+)",
|
||||
command="switch",
|
||||
arguments=["close", "{name}", "-df"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"关闭(?P<name>.+)",
|
||||
command="switch",
|
||||
arguments=["close", "{name}"],
|
||||
r"(?P<action>开启|关闭)\s*(?P<all>所有|全部)?\s*(?P<default>默认)?\s*(?P<type>群被动|被动|插件|功能)?\s*",
|
||||
command="switch {all} {default} {type} {action} {* }",
|
||||
wrapper=_switch_wrapper, # type: ignore
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
@@ -155,8 +120,15 @@ _group_status_matcher.shortcut(
|
||||
)
|
||||
|
||||
_group_status_matcher.shortcut(
|
||||
r"休息吧",
|
||||
r"休息(吧)?",
|
||||
command="group-status",
|
||||
arguments=["sleep"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_group_status_matcher.shortcut(
|
||||
r"查看群(状态|信息)",
|
||||
command="group-status",
|
||||
arguments=["check"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.cache.runtime_cache import GroupMemoryCache
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.enum import BlockType
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from .strategy import get_strategy
|
||||
|
||||
|
||||
class PluginManager:
|
||||
@staticmethod
|
||||
def _modify_block_string(current_str: str, module: str, add: bool) -> str:
|
||||
"""辅助: 添加或移除禁用模块字符串"""
|
||||
items = CommonUtils.convert_module_format(current_str)
|
||||
if add:
|
||||
if module not in items:
|
||||
items.append(module)
|
||||
else:
|
||||
if module in items:
|
||||
items.remove(module)
|
||||
return CommonUtils.convert_module_format(items)
|
||||
|
||||
@classmethod
|
||||
async def _calculate_affected_groups(
|
||||
cls,
|
||||
target_groups: set[str],
|
||||
status: bool,
|
||||
is_whitelist_mode: bool,
|
||||
bot: Bot | None,
|
||||
) -> tuple[set[str], set[str]]:
|
||||
"""提取公用的目标群组计算逻辑(白名单/普通模式交并集)"""
|
||||
groups_to_open = set()
|
||||
groups_to_close = set()
|
||||
clean_targets = {str(gid) for gid in target_groups if gid}
|
||||
|
||||
if is_whitelist_mode and status:
|
||||
if bot:
|
||||
active_groups, _ = await PlatformUtils.get_group_list(
|
||||
bot, only_group=True
|
||||
)
|
||||
all_group_set = {str(g.group_id) for g in active_groups if g.group_id}
|
||||
else:
|
||||
all_group_ids = await GroupConsole.all().values_list(
|
||||
"group_id", flat=True
|
||||
)
|
||||
all_group_set = {str(gid) for gid in all_group_ids}
|
||||
groups_to_open = clean_targets
|
||||
groups_to_close = all_group_set - clean_targets
|
||||
else:
|
||||
if status:
|
||||
groups_to_open = clean_targets
|
||||
else:
|
||||
groups_to_close = clean_targets
|
||||
return groups_to_open, groups_to_close
|
||||
|
||||
@classmethod
|
||||
async def batch_update_status(
|
||||
cls,
|
||||
name: str,
|
||||
target_groups: set[str],
|
||||
status: bool,
|
||||
is_task: bool = False,
|
||||
is_superuser: bool = False,
|
||||
is_whitelist_mode: bool = False,
|
||||
bot: Bot | None = None,
|
||||
use_su_field: bool = False,
|
||||
) -> str:
|
||||
"""批量更新状态 (已用策略模式完全重构)"""
|
||||
strategy = get_strategy(is_task)
|
||||
entity = await strategy.get_entity(name)
|
||||
if not entity:
|
||||
return f"未找到{strategy.entity_type_name}: {name}"
|
||||
|
||||
module_name = entity.module
|
||||
norm_field = strategy.norm_field
|
||||
su_field = strategy.su_field
|
||||
|
||||
groups_to_open, groups_to_close = await cls._calculate_affected_groups(
|
||||
target_groups, status, is_whitelist_mode, bot
|
||||
)
|
||||
|
||||
affected_ids = groups_to_open | groups_to_close
|
||||
if not affected_ids:
|
||||
return "没有目标群组需要操作。"
|
||||
|
||||
for gid in groups_to_open | groups_to_close:
|
||||
platform = bot.adapter.get_name() if bot else "qq"
|
||||
await GroupConsole.get_or_create_root_group(
|
||||
group_id=gid, defaults={"platform": platform}
|
||||
)
|
||||
|
||||
groups_obj = await GroupConsole.filter(group_id__in=list(affected_ids)).all()
|
||||
update_list = []
|
||||
opened_groups: set[str] = set()
|
||||
closed_groups: set[str] = set()
|
||||
|
||||
for group in groups_obj:
|
||||
gid = str(group.group_id)
|
||||
norm_val = getattr(group, norm_field)
|
||||
su_val = getattr(group, su_field)
|
||||
new_norm_val, new_su_val = norm_val, su_val
|
||||
is_changed = False
|
||||
change_type = None
|
||||
|
||||
if gid in groups_to_open:
|
||||
new_norm_val = cls._modify_block_string(norm_val, module_name, False)
|
||||
if is_superuser:
|
||||
new_su_val = cls._modify_block_string(su_val, module_name, False)
|
||||
if norm_val != new_norm_val or su_val != new_su_val:
|
||||
is_changed = True
|
||||
change_type = "open"
|
||||
elif gid in groups_to_close:
|
||||
if is_superuser and use_su_field:
|
||||
new_su_val = cls._modify_block_string(su_val, module_name, True)
|
||||
else:
|
||||
new_norm_val = cls._modify_block_string(norm_val, module_name, True)
|
||||
if norm_val != new_norm_val or su_val != new_su_val:
|
||||
is_changed = True
|
||||
change_type = "close"
|
||||
|
||||
if is_changed:
|
||||
setattr(group, norm_field, new_norm_val)
|
||||
setattr(group, su_field, new_su_val)
|
||||
update_list.append(group)
|
||||
if change_type == "open":
|
||||
opened_groups.add(gid)
|
||||
elif change_type == "close":
|
||||
closed_groups.add(gid)
|
||||
|
||||
if update_list:
|
||||
await GroupConsole.bulk_update(
|
||||
update_list, [norm_field, su_field], batch_size=500
|
||||
)
|
||||
for group in update_list:
|
||||
await GroupMemoryCache.upsert_from_model(group)
|
||||
|
||||
item_str = strategy.entity_type_name
|
||||
mode_str = "(白名单模式)" if is_whitelist_mode else ""
|
||||
|
||||
if not update_list:
|
||||
if is_whitelist_mode:
|
||||
return f"目标群组的 {item_str} {name} 已符合白名单配置,无需重复操作。"
|
||||
status_desc = "开启" if status else ("系统禁用" if use_su_field else "关闭")
|
||||
return (
|
||||
f"目标群组的 {item_str} {name} 均已处于 {status_desc} 状态,"
|
||||
"无需重复操作。"
|
||||
)
|
||||
|
||||
opened_count, closed_count = len(opened_groups), len(closed_groups)
|
||||
|
||||
if status:
|
||||
su_hint = " (已同步解除系统禁用)" if is_superuser else ""
|
||||
success_msg = f"已开启 {opened_count} 个群组的 {item_str} {name}{su_hint}"
|
||||
else:
|
||||
if is_superuser and use_su_field:
|
||||
success_msg = f"已系统级禁用 {closed_count} 个群组的 {item_str} {name}"
|
||||
else:
|
||||
success_msg = f"已在 {closed_count} 个群组中关闭了 {item_str} {name}"
|
||||
|
||||
if is_whitelist_mode:
|
||||
msg_parts = []
|
||||
if opened_count > 0:
|
||||
msg_parts.append(f"已开启 {opened_count} 个群组")
|
||||
if closed_count > 0:
|
||||
msg_parts.append(f"已关闭 {closed_count} 个群组")
|
||||
return f"{','.join(msg_parts)} 的 {item_str} {name} {mode_str}。"
|
||||
|
||||
return f"{success_msg}。"
|
||||
|
||||
@classmethod
|
||||
async def set_default_status(
|
||||
cls, plugin_name: str, status: bool, is_task: bool = False
|
||||
) -> str:
|
||||
strategy = get_strategy(is_task)
|
||||
entity = await strategy.get_entity(plugin_name)
|
||||
if entity:
|
||||
await strategy.set_default_status(entity, status)
|
||||
status_text = "开启" if status else "关闭"
|
||||
return (
|
||||
f"成功将 {getattr(entity, 'name', plugin_name)} "
|
||||
f"进群默认状态修改为: {status_text}"
|
||||
)
|
||||
return "没有找到这个功能喔..."
|
||||
|
||||
@classmethod
|
||||
async def set_all_plugin_status(
|
||||
cls,
|
||||
status: bool,
|
||||
is_default: bool = False,
|
||||
group_id: str | None = None,
|
||||
is_task: bool = False,
|
||||
is_superuser: bool = False,
|
||||
use_su_field: bool = False,
|
||||
) -> str:
|
||||
strategy = get_strategy(is_task)
|
||||
type_str = strategy.entity_type_name
|
||||
|
||||
if is_default:
|
||||
await strategy.set_all_default_status(status)
|
||||
return (
|
||||
f"成功将所有{type_str}进群默认状态修改为: "
|
||||
f"{'开启' if status else '关闭'}"
|
||||
)
|
||||
|
||||
if group_id:
|
||||
if group := await GroupConsole.get_group_db(group_id=group_id):
|
||||
norm_field = strategy.norm_field
|
||||
su_field = strategy.su_field
|
||||
module_list = await strategy.get_all_modules()
|
||||
all_modules_str = CommonUtils.convert_module_format(module_list)
|
||||
update_fields = []
|
||||
|
||||
if status:
|
||||
if is_superuser:
|
||||
setattr(group, norm_field, "")
|
||||
setattr(group, su_field, "")
|
||||
update_fields.extend([norm_field, su_field])
|
||||
msg = f"成功将此群组所有{type_str}完全开启 (包括解除系统禁用)"
|
||||
else:
|
||||
setattr(group, norm_field, "")
|
||||
update_fields.append(norm_field)
|
||||
msg = f"成功开启此群组所有{type_str}"
|
||||
else:
|
||||
if is_superuser and use_su_field:
|
||||
setattr(group, su_field, all_modules_str)
|
||||
update_fields.append(su_field)
|
||||
msg = f"已由超级用户系统级禁用此群组所有{type_str}"
|
||||
else:
|
||||
setattr(group, norm_field, all_modules_str)
|
||||
update_fields.append(norm_field)
|
||||
msg = f"成功关闭此群组所有{type_str}"
|
||||
|
||||
await group.save(update_fields=update_fields)
|
||||
return f"{msg}。"
|
||||
return "获取群组失败..."
|
||||
|
||||
await strategy.set_all_global_status(status)
|
||||
return f"成功将所有{type_str}全局状态修改为: {'开启' if status else '关闭'}"
|
||||
|
||||
@classmethod
|
||||
async def superuser_set_status(
|
||||
cls,
|
||||
plugin_name: str,
|
||||
status: bool,
|
||||
block_type: BlockType | None,
|
||||
group_id: str | None,
|
||||
is_task: bool = False,
|
||||
) -> str:
|
||||
strategy = get_strategy(is_task)
|
||||
entity = await strategy.get_entity(plugin_name)
|
||||
action_cn = "开启" if status else "关闭"
|
||||
|
||||
if entity:
|
||||
if group_id:
|
||||
is_su_blocked, _ = await strategy.check_block_status(
|
||||
group_id, entity.module
|
||||
)
|
||||
if status and is_su_blocked:
|
||||
await cls.batch_update_status(
|
||||
plugin_name,
|
||||
{group_id},
|
||||
True,
|
||||
is_task=is_task,
|
||||
is_superuser=True,
|
||||
)
|
||||
return f"已成功{action_cn}群组 {group_id} 的 {plugin_name} 功能!"
|
||||
if not status and not is_su_blocked:
|
||||
await cls.batch_update_status(
|
||||
plugin_name,
|
||||
{group_id},
|
||||
False,
|
||||
is_task=is_task,
|
||||
is_superuser=True,
|
||||
use_su_field=True,
|
||||
)
|
||||
return f"已成功{action_cn}群组 {group_id} 的 {plugin_name} 功能!"
|
||||
return f"此群组该功能已被超级用户{action_cn},不要重复操作..."
|
||||
|
||||
await strategy.set_global_status(entity, status, block_type)
|
||||
await strategy.refresh_cache()
|
||||
|
||||
if not block_type or block_type == BlockType.ALL:
|
||||
return f"已成功将 {entity.name} 全局{action_cn}!"
|
||||
if block_type == BlockType.GROUP:
|
||||
return f"已成功将 {entity.name} 全局群组{action_cn}!"
|
||||
if block_type == BlockType.PRIVATE:
|
||||
return f"已成功将 {entity.name} 全局私聊{action_cn}!"
|
||||
|
||||
return "没有找到这个功能喔..."
|
||||
|
||||
@classmethod
|
||||
async def batch_set_group_active_status(
|
||||
cls,
|
||||
target_groups: set[str],
|
||||
status: bool,
|
||||
is_whitelist_mode: bool = False,
|
||||
bot: Bot | None = None,
|
||||
) -> str:
|
||||
"""批量设置群组激活状态 (休眠/醒来) - 采用与插件相同的目标计算逻辑"""
|
||||
groups_to_wake, groups_to_sleep = await cls._calculate_affected_groups(
|
||||
target_groups, status, is_whitelist_mode, bot
|
||||
)
|
||||
|
||||
affected_ids = groups_to_wake | groups_to_sleep
|
||||
if not affected_ids:
|
||||
return "没有目标群组需要操作。"
|
||||
|
||||
if groups_to_wake:
|
||||
await GroupConsole.filter(group_id__in=list(groups_to_wake)).update(
|
||||
status=True
|
||||
)
|
||||
if groups_to_sleep:
|
||||
await GroupConsole.filter(group_id__in=list(groups_to_sleep)).update(
|
||||
status=False
|
||||
)
|
||||
|
||||
await GroupMemoryCache.refresh()
|
||||
|
||||
action_str = "醒来" if status else "休眠"
|
||||
return f"已完成目标群组的 {action_str} 操作。"
|
||||
@@ -0,0 +1,190 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, cast
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.services.cache.runtime_cache import (
|
||||
PluginInfoMemoryCache,
|
||||
TaskInfoMemoryCache,
|
||||
)
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
|
||||
|
||||
class SwitchStrategy(ABC):
|
||||
"""插件与被动技能切换策略基类"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def entity_type_name(self) -> str:
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def norm_field(self) -> str:
|
||||
"""普通的群组禁用字段名"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def su_field(self) -> str:
|
||||
"""超级用户群组禁用字段名"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_entity(self, name: str) -> Any:
|
||||
"""通过名称获取实体信息"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def check_block_status(self, group_id: str, module: str) -> tuple[bool, bool]:
|
||||
"""检查目标群组的禁用状态,返回 (is_su_blocked, is_norm_blocked)"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_all_modules(self) -> list[str]:
|
||||
"""获取所有模块的名称列表"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def set_default_status(self, entity: Any, status: bool) -> None:
|
||||
"""设置单个实体的进群默认状态"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def set_global_status(
|
||||
self, entity: Any, status: bool, block_type: BlockType | None = None
|
||||
) -> None:
|
||||
"""设置单个实体的全局状态"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def set_all_default_status(self, status: bool) -> None:
|
||||
"""设置所有实体的进群默认状态"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def set_all_global_status(self, status: bool) -> None:
|
||||
"""设置所有实体的全局状态"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def refresh_cache(self) -> None:
|
||||
"""刷新相关的内存缓存"""
|
||||
pass
|
||||
|
||||
|
||||
class PluginStrategy(SwitchStrategy):
|
||||
@property
|
||||
def entity_type_name(self) -> str:
|
||||
return "功能"
|
||||
|
||||
@property
|
||||
def norm_field(self) -> str:
|
||||
return "block_plugin"
|
||||
|
||||
@property
|
||||
def su_field(self) -> str:
|
||||
return "superuser_block_plugin"
|
||||
|
||||
async def get_entity(self, name: str) -> Any:
|
||||
if name.isdigit():
|
||||
return await PluginInfo.get_or_none(id=int(name))
|
||||
return await PluginInfo.get_or_none(
|
||||
name=name, load_status=True, plugin_type__not=PluginType.PARENT
|
||||
)
|
||||
|
||||
async def check_block_status(self, group_id: str, module: str) -> tuple[bool, bool]:
|
||||
is_su_blocked = await GroupConsole.is_superuser_block_plugin(group_id, module)
|
||||
is_norm_blocked = await GroupConsole.is_normal_block_plugin(group_id, module)
|
||||
return is_su_blocked, is_norm_blocked
|
||||
|
||||
async def get_all_modules(self) -> list[str]:
|
||||
return cast(
|
||||
list[str],
|
||||
await PluginInfo.get_plugins_values_list(
|
||||
"module",
|
||||
load_status=None,
|
||||
filter_parent=False,
|
||||
plugin_type=PluginType.NORMAL,
|
||||
),
|
||||
)
|
||||
|
||||
async def set_default_status(self, entity: PluginInfo, status: bool) -> None:
|
||||
entity.default_status = status
|
||||
await entity.save(update_fields=["default_status"])
|
||||
|
||||
async def set_global_status(
|
||||
self, entity: PluginInfo, status: bool, block_type: BlockType | None = None
|
||||
) -> None:
|
||||
entity.block_type = block_type
|
||||
entity.status = not bool(block_type)
|
||||
await entity.save(update_fields=["status", "block_type"])
|
||||
|
||||
async def set_all_default_status(self, status: bool) -> None:
|
||||
await PluginInfo.filter(plugin_type=PluginType.NORMAL).update(
|
||||
default_status=status
|
||||
)
|
||||
await self.refresh_cache()
|
||||
|
||||
async def set_all_global_status(self, status: bool) -> None:
|
||||
await PluginInfo.filter(plugin_type=PluginType.NORMAL).update(
|
||||
status=status, block_type=None if status else BlockType.ALL
|
||||
)
|
||||
await self.refresh_cache()
|
||||
|
||||
async def refresh_cache(self) -> None:
|
||||
await PluginInfoMemoryCache.refresh()
|
||||
|
||||
|
||||
class TaskStrategy(SwitchStrategy):
|
||||
@property
|
||||
def entity_type_name(self) -> str:
|
||||
return "被动"
|
||||
|
||||
@property
|
||||
def norm_field(self) -> str:
|
||||
return "block_task"
|
||||
|
||||
@property
|
||||
def su_field(self) -> str:
|
||||
return "superuser_block_task"
|
||||
|
||||
async def get_entity(self, name: str) -> Any:
|
||||
return await TaskInfo.get_or_none(name=name)
|
||||
|
||||
async def check_block_status(self, group_id: str, module: str) -> tuple[bool, bool]:
|
||||
is_su_blocked = await GroupConsole.is_superuser_block_task(group_id, module)
|
||||
is_norm_blocked = await GroupConsole.is_block_task(group_id, module)
|
||||
return is_su_blocked, is_norm_blocked
|
||||
|
||||
async def get_all_modules(self) -> list[str]:
|
||||
return await TaskInfo.get_modules(load_status=None)
|
||||
|
||||
async def set_default_status(self, entity: TaskInfo, status: bool) -> None:
|
||||
entity.default_status = status
|
||||
await entity.save(update_fields=["default_status"])
|
||||
|
||||
async def set_global_status(
|
||||
self, entity: TaskInfo, status: bool, block_type: BlockType | None = None
|
||||
) -> None:
|
||||
entity.status = status
|
||||
await entity.save(update_fields=["status"])
|
||||
|
||||
async def set_all_default_status(self, status: bool) -> None:
|
||||
await TaskInfo.all().update(default_status=status)
|
||||
# Bulk updates bypass model save hooks; keep TaskInfoMemoryCache in sync.
|
||||
await self.refresh_cache()
|
||||
|
||||
async def set_all_global_status(self, status: bool) -> None:
|
||||
await TaskInfo.all().update(status=status)
|
||||
# Bulk updates bypass model save hooks; keep TaskInfoMemoryCache in sync.
|
||||
await self.refresh_cache()
|
||||
|
||||
async def refresh_cache(self) -> None:
|
||||
await TaskInfoMemoryCache.refresh()
|
||||
|
||||
|
||||
def get_strategy(is_task: bool) -> SwitchStrategy:
|
||||
"""工厂方法:获取对应的处理策略"""
|
||||
return TaskStrategy() if is_task else PluginStrategy()
|
||||
@@ -0,0 +1,458 @@
|
||||
from typing import Any
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
from zhenxun import ui
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.ui.models import LayoutData, StatusBadgeCell, TextCell
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.exception import GroupConsoleNotFound
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from .strategy import get_strategy
|
||||
|
||||
|
||||
async def build_plugin() -> bytes:
|
||||
"""构造插件状态图片"""
|
||||
column_name = [
|
||||
"ID",
|
||||
"模块",
|
||||
"名称",
|
||||
"全局状态",
|
||||
"禁用类型",
|
||||
"加载状态",
|
||||
"菜单分类",
|
||||
"作者",
|
||||
"版本",
|
||||
"金币花费",
|
||||
]
|
||||
plugin_list = await PluginInfo.get_plugins(
|
||||
load_status=None,
|
||||
filter_parent=False,
|
||||
plugin_type__not=PluginType.HIDDEN,
|
||||
)
|
||||
rows = []
|
||||
for plugin in plugin_list:
|
||||
status_cell = StatusBadgeCell(
|
||||
text="开启" if plugin.status else "关闭",
|
||||
status_type="ok" if plugin.status else "error",
|
||||
)
|
||||
load_cell = StatusBadgeCell(
|
||||
text="SUCCESS" if plugin.load_status else "ERROR",
|
||||
status_type="ok" if plugin.load_status else "error",
|
||||
)
|
||||
rows.append(
|
||||
[
|
||||
plugin.id,
|
||||
plugin.module,
|
||||
plugin.name,
|
||||
status_cell,
|
||||
plugin.block_type.value if plugin.block_type else "-",
|
||||
load_cell,
|
||||
plugin.menu_type or "-",
|
||||
plugin.author or "-",
|
||||
plugin.version or "-",
|
||||
plugin.cost_gold,
|
||||
]
|
||||
)
|
||||
|
||||
table = ui.table("Plugin List", "插件状态概览")
|
||||
table.set_headers(column_name)
|
||||
table.add_rows(rows)
|
||||
table.set_column_widths(
|
||||
[
|
||||
"60px",
|
||||
"150px",
|
||||
"150px",
|
||||
"80px",
|
||||
"100px",
|
||||
"100px",
|
||||
"100px",
|
||||
"100px",
|
||||
"80px",
|
||||
"80px",
|
||||
]
|
||||
)
|
||||
return await ui.render(table, viewport={"width": 1400, "height": 10})
|
||||
|
||||
|
||||
async def build_task(group_id: str | None) -> bytes:
|
||||
"""构造被动技能状态图片"""
|
||||
task_list = await TaskInfo.get_tasks(load_status=None)
|
||||
column_name = ["ID", "模块", "名称", "群组状态", "全局状态", "运行时间"]
|
||||
group = None
|
||||
if group_id:
|
||||
group = await GroupConsole.get_group_db(group_id=group_id)
|
||||
if not group:
|
||||
raise GroupConsoleNotFound()
|
||||
else:
|
||||
column_name.remove("群组状态")
|
||||
rows = []
|
||||
for task in task_list:
|
||||
global_status_cell = StatusBadgeCell(
|
||||
text="开启" if task.status else "关闭",
|
||||
status_type="ok" if task.status else "error",
|
||||
)
|
||||
row = [task.id, task.module, task.name]
|
||||
if group:
|
||||
is_group_open = f"<{task.module}," not in group.block_task
|
||||
group_status_cell = StatusBadgeCell(
|
||||
text="开启" if is_group_open else "关闭",
|
||||
status_type="ok" if is_group_open else "error",
|
||||
)
|
||||
row.append(group_status_cell)
|
||||
row.extend([global_status_cell, task.run_time or "-"])
|
||||
rows.append(row)
|
||||
|
||||
table = ui.table("Task List", "被动技能状态概览")
|
||||
table.set_headers(column_name)
|
||||
table.add_rows(rows)
|
||||
if group:
|
||||
table.set_column_widths(["60px", "150px", "150px", "100px", "100px", "auto"])
|
||||
viewport_width = 1200
|
||||
else:
|
||||
table.set_column_widths(["60px", "150px", "150px", "100px", "auto"])
|
||||
viewport_width = 1000
|
||||
|
||||
return await ui.render(table, viewport={"width": viewport_width, "height": 10})
|
||||
|
||||
|
||||
async def render_global_status(name: str, is_task: bool, bot: Bot) -> bytes:
|
||||
"""渲染全局状态报表,含差异化过滤和双栏展示"""
|
||||
strategy = get_strategy(is_task)
|
||||
info = await strategy.get_entity(name)
|
||||
if not info:
|
||||
raise ValueError(f"未找到{strategy.entity_type_name}: {name}")
|
||||
|
||||
module = info.module
|
||||
default_status = info.status
|
||||
|
||||
online_groups, _ = await PlatformUtils.get_group_list(bot)
|
||||
valid_keys = {(str(g.group_id), g.channel_id) for g in online_groups}
|
||||
|
||||
all_db_groups = await GroupConsole.all()
|
||||
target_groups = [
|
||||
g for g in all_db_groups if (str(g.group_id), g.channel_id) in valid_keys
|
||||
]
|
||||
|
||||
total_count = len(target_groups)
|
||||
status_data = []
|
||||
for group in target_groups:
|
||||
gid = str(group.group_id)
|
||||
is_su_blocked, is_norm_blocked = await strategy.check_block_status(gid, module)
|
||||
is_open = bool(default_status) and not is_su_blocked and not is_norm_blocked
|
||||
|
||||
if not default_status:
|
||||
status_text, badge_color = "全局关闭", "error"
|
||||
elif is_su_blocked:
|
||||
status_text, badge_color = "系统禁用", "error"
|
||||
elif is_norm_blocked:
|
||||
status_text, badge_color = "群内关闭", "warning"
|
||||
else:
|
||||
status_text, badge_color = "开启", "success"
|
||||
|
||||
status_data.append(
|
||||
{
|
||||
"id": str(group.group_id),
|
||||
"name": group.group_name,
|
||||
"status": is_open,
|
||||
"status_text": status_text,
|
||||
"badge_color": badge_color,
|
||||
}
|
||||
)
|
||||
|
||||
open_list = [item for item in status_data if item["status"]]
|
||||
close_list = [item for item in status_data if not item["status"]]
|
||||
open_count = len(open_list)
|
||||
open_rate = open_count / total_count if total_count > 0 else 0
|
||||
|
||||
global_alert = None
|
||||
if not default_status:
|
||||
global_alert = ui.alert(
|
||||
"全局已禁用",
|
||||
f"{strategy.entity_type_name} [{name}] 当前处于全局关闭状态。",
|
||||
type="error",
|
||||
)
|
||||
|
||||
display_list = []
|
||||
list_title = "群组状态详情"
|
||||
if total_count > 0 and default_status:
|
||||
if open_rate > 0.9:
|
||||
display_list, list_title = (
|
||||
close_list,
|
||||
f"异常状态列表 (其余 {open_count} 个群均正常开启)",
|
||||
)
|
||||
elif open_rate < 0.1:
|
||||
display_list, list_title = (
|
||||
open_list,
|
||||
f"异常状态列表 (其余 {len(close_list)} 个群均已禁用)",
|
||||
)
|
||||
else:
|
||||
display_list = sorted(status_data, key=lambda x: not x["status"])
|
||||
|
||||
return await build_dashboard_report(
|
||||
page_title=f"{strategy.entity_type_name}状态报告: {name}",
|
||||
total_count=total_count,
|
||||
active_count=open_count,
|
||||
inactive_count=len(close_list),
|
||||
active_rate=open_rate,
|
||||
active_label="已开启",
|
||||
active_color="var(--color-accent-green)",
|
||||
inactive_label="已关闭",
|
||||
inactive_color="var(--color-accent-red)",
|
||||
progress_label=f"功能 [{name}] 全局覆盖率",
|
||||
summary_tip=(
|
||||
f"总群数: {total_count} | 🟢 开启: {open_count} | "
|
||||
f"🔴 关闭: {len(close_list)}"
|
||||
),
|
||||
display_list=display_list,
|
||||
list_title=list_title,
|
||||
global_alert=global_alert,
|
||||
perfect_state_alert=ui.alert(
|
||||
"状态完美", f"所有 {total_count} 个群组状态一致。", type="success"
|
||||
)
|
||||
if not global_alert
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
async def render_group_active_status(bot: Bot) -> bytes:
|
||||
"""渲染群组醒来/休眠状态报表"""
|
||||
online_groups, _ = await PlatformUtils.get_group_list(bot)
|
||||
valid_keys = {(str(g.group_id), g.channel_id) for g in online_groups}
|
||||
all_db_groups = await GroupConsole.all()
|
||||
target_groups = [
|
||||
g for g in all_db_groups if (str(g.group_id), g.channel_id) in valid_keys
|
||||
]
|
||||
|
||||
total_count = len(target_groups)
|
||||
status_data = [
|
||||
{
|
||||
"id": str(group.group_id),
|
||||
"name": group.group_name,
|
||||
"status": group.status,
|
||||
"status_text": "工作中" if group.status else "休息中",
|
||||
"badge_color": "success" if group.status else "info",
|
||||
}
|
||||
for group in target_groups
|
||||
]
|
||||
|
||||
wake_list = [item for item in status_data if item["status"]]
|
||||
sleep_list = [item for item in status_data if not item["status"]]
|
||||
wake_rate = len(wake_list) / total_count if total_count > 0 else 0
|
||||
|
||||
display_list, list_title = status_data, "群组状态详情"
|
||||
if wake_rate > 0.9:
|
||||
display_list, list_title = (
|
||||
sleep_list,
|
||||
f"休息中的群组 (其余 {len(wake_list)} 个群正常工作中)",
|
||||
)
|
||||
elif wake_rate < 0.1:
|
||||
display_list, list_title = (
|
||||
wake_list,
|
||||
f"工作中/已醒来的群组 (其余 {len(sleep_list)} 个群休息中)",
|
||||
)
|
||||
|
||||
return await build_dashboard_report(
|
||||
page_title="真寻工作状态统计",
|
||||
total_count=total_count,
|
||||
active_count=len(wake_list),
|
||||
inactive_count=len(sleep_list),
|
||||
active_rate=wake_rate,
|
||||
active_label="当前工作中",
|
||||
active_color="var(--color-accent-green)",
|
||||
inactive_label="当前休息中",
|
||||
inactive_color="var(--color-text-muted)",
|
||||
progress_label="全服群组活跃覆盖率",
|
||||
display_list=display_list,
|
||||
list_title=list_title,
|
||||
no_record_alert=ui.alert("无记录", "当前没有已加入的群组记录。", type="info"),
|
||||
perfect_state_alert=ui.alert(
|
||||
"状态统一",
|
||||
(
|
||||
f"所有 {total_count} 个群组当前均处于 "
|
||||
f"{'工作中' if wake_rate > 0.5 else '休息中'} 状态。"
|
||||
),
|
||||
type="success",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def build_dashboard_report(
|
||||
page_title: str,
|
||||
total_count: int,
|
||||
active_count: int,
|
||||
inactive_count: int,
|
||||
active_rate: float,
|
||||
active_label: str,
|
||||
active_color: str,
|
||||
inactive_label: str,
|
||||
inactive_color: str,
|
||||
progress_label: str,
|
||||
display_list: list[dict],
|
||||
list_title: str,
|
||||
summary_tip: str = "",
|
||||
global_alert: Any = None,
|
||||
no_record_alert: Any = None,
|
||||
perfect_state_alert: Any = None,
|
||||
) -> bytes:
|
||||
"""通用的 Dashboard 报表构建器,用于替代原先冗余的 UI 代码"""
|
||||
|
||||
kpi_row = LayoutData.row(gap="12px", align_items="stretch")
|
||||
|
||||
def _build_kpi_card(title: str, value: str, val_color: str):
|
||||
header = LayoutData.row(justify_content="space-between", width="100%")
|
||||
header.add_item(
|
||||
ui.text(title, font_size="13px", color="var(--color-text-muted)")
|
||||
)
|
||||
if title != "总群数" and title != "管理群总数":
|
||||
rate_str = (
|
||||
f"{active_rate:.1%}"
|
||||
if "已开启" in title or "当前工作" in title
|
||||
else f"{(1 - active_rate):.1%}"
|
||||
)
|
||||
header.add_item(
|
||||
ui.text(rate_str, font_size="13px", bold=True, color=val_color)
|
||||
)
|
||||
|
||||
content = ui.vstack(
|
||||
[
|
||||
header.build()
|
||||
if "已开启" in title or "已关闭" in title
|
||||
else ui.text(title, font_size="13px", color="var(--color-text-muted)"),
|
||||
ui.text(value, font_size="24px", bold=True, color=val_color),
|
||||
],
|
||||
gap="2px",
|
||||
align_items="start" if "总" in title else "stretch",
|
||||
padding="0",
|
||||
)
|
||||
|
||||
return ui.card(content).with_inline_style({"--card-padding": "12px 16px"})
|
||||
|
||||
kpi_row.add_item(
|
||||
_build_kpi_card(
|
||||
"总群数" if "功能" in progress_label else "管理群总数",
|
||||
str(total_count),
|
||||
"var(--color-text-dark)",
|
||||
),
|
||||
metadata={"flex": True},
|
||||
)
|
||||
kpi_row.add_item(
|
||||
_build_kpi_card(active_label, str(active_count), active_color),
|
||||
metadata={"flex": True},
|
||||
)
|
||||
kpi_row.add_item(
|
||||
_build_kpi_card(inactive_label, str(inactive_count), inactive_color),
|
||||
metadata={"flex": True},
|
||||
)
|
||||
|
||||
progress_scheme = "primary" if "功能" in progress_label else "success"
|
||||
progress_section = ui.vstack(
|
||||
[
|
||||
ui.text(progress_label, font_size="14px", color="var(--color-text-muted)"),
|
||||
ui.progress_bar(
|
||||
progress=active_rate * 100,
|
||||
label=f"{active_count}/{total_count}",
|
||||
color_scheme=progress_scheme,
|
||||
),
|
||||
],
|
||||
gap="8px",
|
||||
)
|
||||
|
||||
content_area = None
|
||||
if not display_list:
|
||||
if total_count == 0 and no_record_alert:
|
||||
content_area = no_record_alert
|
||||
elif global_alert and "功能" in progress_label:
|
||||
content_area = global_alert
|
||||
elif perfect_state_alert:
|
||||
content_area = perfect_state_alert
|
||||
elif len(display_list) <= 15:
|
||||
rows = []
|
||||
for item in display_list:
|
||||
status_cell = StatusBadgeCell(
|
||||
text=item["status_text"], status_type=item["badge_color"]
|
||||
)
|
||||
rows.append(
|
||||
[
|
||||
TextCell(content=str(item["id"])),
|
||||
TextCell(content=str(item["name"])),
|
||||
status_cell,
|
||||
]
|
||||
)
|
||||
content_area = (
|
||||
ui.table(list_title, None)
|
||||
.set_headers(["群号", "群名", "状态"])
|
||||
.set_column_widths(["160px", "auto", "100px"])
|
||||
.add_rows(rows)
|
||||
)
|
||||
else:
|
||||
grid = LayoutData.grid(columns=3, gap="15px")
|
||||
MAX_SHOW = 60
|
||||
for item in display_list[:MAX_SHOW]:
|
||||
card_content = ui.vstack(
|
||||
[
|
||||
ui.text(str(item["name"]), bold=True, font_size="15px"),
|
||||
LayoutData.row(justify_content="space-between", width="100%")
|
||||
.add_item(ui.text(str(item["id"]), font_size="12px", color="#999"))
|
||||
.add_item(
|
||||
ui.badge(item["status_text"], color_scheme=item["badge_color"])
|
||||
),
|
||||
],
|
||||
gap="8px",
|
||||
align_items="start",
|
||||
)
|
||||
grid.add_item(ui.card(card_content))
|
||||
|
||||
container = LayoutData.column(gap="10px")
|
||||
container.add_item(grid.build())
|
||||
if len(display_list) > MAX_SHOW:
|
||||
container.add_item(
|
||||
ui.text(
|
||||
f"... 还有 {len(display_list) - MAX_SHOW} 个群组未显示 ...",
|
||||
align="center",
|
||||
color="#ccc",
|
||||
)
|
||||
)
|
||||
content_area = container.build()
|
||||
|
||||
main_layout = LayoutData.column(padding="40px", gap="30px")
|
||||
main_layout.add_item(
|
||||
ui.text(
|
||||
page_title,
|
||||
font_size="32px",
|
||||
bold=True,
|
||||
align="center",
|
||||
color="var(--color-primary)",
|
||||
)
|
||||
)
|
||||
|
||||
stats_items = []
|
||||
if global_alert and "功能" in progress_label:
|
||||
stats_items.append(global_alert)
|
||||
|
||||
stats_items.extend(
|
||||
[
|
||||
kpi_row.build(),
|
||||
ui.divider(margin="15px 0"),
|
||||
progress_section,
|
||||
]
|
||||
)
|
||||
|
||||
if summary_tip:
|
||||
stats_items.append(
|
||||
ui.text(
|
||||
summary_tip,
|
||||
font_size="13px",
|
||||
color="var(--color-text-muted)",
|
||||
align="center",
|
||||
)
|
||||
)
|
||||
|
||||
main_layout.add_item(ui.card(ui.vstack(stats_items)))
|
||||
if content_area:
|
||||
main_layout.add_item(content_area)
|
||||
|
||||
return await ui.render(main_layout.build(), viewport={"width": 900, "height": 10})
|
||||
@@ -14,6 +14,7 @@ from zhenxun.services.log import logger
|
||||
from zhenxun.utils._build_image import BuildImage
|
||||
from zhenxun.utils._image_template import ImageTemplate
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
BASE_PATH = DATA_PATH / "welcome_message"
|
||||
@@ -91,7 +92,7 @@ def migrate(path: Path):
|
||||
json.dump(new_data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
def _():
|
||||
"""数据迁移
|
||||
|
||||
|
||||
@@ -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
|
||||
"dist",
|
||||
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()
|
||||
|
||||