mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2025-12-15 14:22:55 +08:00
Compare commits
33 Commits
f66bd9b83e
...
c0a92bff46
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0a92bff46 | ||
|
|
2445cd8515 | ||
|
|
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 |
14
.env.dev
14
.env.dev
@ -27,6 +27,18 @@ QBOT_ID_DATA = '{
|
||||
# 示例: "sqlite:data/db/zhenxun.db" 在data目录下建立db文件夹
|
||||
DB_URL = ""
|
||||
|
||||
# NONE: 不使用缓存, MEMORY: 使用内存缓存, REDIS: 使用Redis缓存
|
||||
CACHE_MODE = NONE
|
||||
# REDIS配置,使用REDIS替换Cache内存缓存
|
||||
# REDIS地址
|
||||
# REDIS_HOST = "127.0.0.1"
|
||||
# REDIS端口
|
||||
# REDIS_PORT = 6379
|
||||
# REDIS密码
|
||||
# REDIS_PASSWORD = ""
|
||||
# REDIS过期时间
|
||||
# REDIS_EXPIRE = 600
|
||||
|
||||
# 系统代理
|
||||
# SYSTEM_PROXY = "http://127.0.0.1:7890"
|
||||
|
||||
@ -40,7 +52,7 @@ PLATFORM_SUPERUSERS = '
|
||||
DRIVER=~fastapi+~httpx+~websockets
|
||||
|
||||
|
||||
# LOG_LEVEL=DEBUG
|
||||
# LOG_LEVEL = DEBUG
|
||||
# 服务器和端口
|
||||
HOST = 127.0.0.1
|
||||
PORT = 8080
|
||||
|
||||
26
.github/workflows/sync-to-aliyun.yml
vendored
Normal file
26
.github/workflows/sync-to-aliyun.yml
vendored
Normal file
@ -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
|
||||
@ -7,7 +7,7 @@ ci:
|
||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.2
|
||||
rev: v0.12.7
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -29,6 +29,7 @@
|
||||
"unban",
|
||||
"Uninfo",
|
||||
"userinfo",
|
||||
"webui",
|
||||
"zhenxun"
|
||||
],
|
||||
"python.analysis.autoImportCompletions": true,
|
||||
|
||||
12
README.md
12
README.md
@ -287,6 +287,18 @@ DB_URL 是基于 Tortoise ORM 的数据库连接字符串,用于指定项目
|
||||
|
||||
[Zer](https://afdian.com/u/6bccdb2a60b411ec9ad452540025c377) [爱发电用户\_HTjk](https://afdian.com/u/6c7d0208064511ec8d7b52540025c377) [shenghuo2](https://afdian.com/u/bca13286102111eda2a052540025c377) [术樱](https://afdian.com/u/414da63a09a311ec8eb752540025c377) [飞火](https://afdian.com/u/404135f48ed711ec962152540025c377) [shenqi](https://afdian.net/u/fa923a8cfe3d11eba61752540025c377) [A_Kyuu](https://afdian.net/u/b83954fc2c1211eba9eb52540025c377) [疯狂混沌](https://afdian.net/u/789a2f9200cd11edb38352540025c377) [投冥](https://afdian.net/a/144514mm) [茶喵](https://afdian.net/u/fd22382eac4d11ecbfc652540025c377) [AemokpaTNR](https://afdian.net/u/1169bb8c8a9611edb0c152540025c377) [爱发电用户\_wrxn](https://afdian.net/u/4aa03d20db4311ecb1e752540025c377) [qqw](https://afdian.net/u/b71db4e2cc3e11ebb76652540025c377) [溫一壺月光下酒](https://afdian.net/u/ad667a5c650c11ed89bf52540025c377) [伝木](https://afdian.net/u/246b80683f9511edba7552540025c377) [阿奎](https://afdian.net/u/da41f72845d511ed930d52540025c377) [醉梦尘逸](https://afdian.net/u/bc11d2683cd011ed99b552540025c377) [Abc](https://afdian.net/u/870dc10a3cd311ed828852540025c377) [本喵无敌哒](https://afdian.net/u/dffaa9005bc911ebb69b52540025c377) [椎名冬羽](https://afdian.net/u/ca1ebd64395e11ed81b452540025c377) [kaito](https://afdian.net/u/a055e20a498811eab1f052540025c377) [笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) [请问一份爱多少钱](https://afdian.net/u/f57ef6602dbd11ed977f52540025c377) [咸鱼鱼鱼鱼](https://afdian.net/u/8e39b9a400e011ed9f4a52540025c377) [Kafka](https://afdian.net/u/41d66798ef6911ecbc5952540025c377) [墨然](https://afdian.net/u/8aa5874a644d11eb8a6752540025c377) [爱发电用户\_T9e4](https://afdian.net/u/2ad1bb82f3a711eca22852540025c377) [笑柒 XIAO_Q7](https://afdian.net/u/4696db5c529111ec84ea52540025c377) [noahzark](https://afdian.net/a/noahzark) [腊条](https://afdian.net/u/f739c4d69eca11eba94b52540025c377) [ze roller](https://afdian.net/u/0e599e96257211ed805152540025c377) [爱发电用户\_4jrf](https://afdian.net/u/6b2cdcc817c611ed949152540025c377) [爱发电用户\_TBsd](https://afdian.net/u/db638b60217911ed9efd52540025c377) [烟寒若雨](https://afdian.net/u/067bd2161eec11eda62b52540025c377) [ln](https://afdian.net/u/b51914ba1c6611ed8a4e52540025c377) [爱发电用户\_b9S4](https://afdian.net/u/3d8f30581a2911edba6d52540025c377) [爱发电用户\_c58s](https://afdian.net/u/a6ad8dda195e11ed9a4152540025c377) [爱发电用户\_eNr9](https://afdian.net/u/05fdb41c0c9a11ed814952540025c377) [MangataAkihi](https://github.com/Sakuracio) [炀](https://afdian.net/u/69b76e9ec77b11ec874f52540025c377) [爱发电用户\_Bc6j](https://afdian.net/u/8546be24f44111eca64052540025c377) [大魔王](https://github.com/xipesoy) [CopilotLaLaLa](https://github.com/CopilotLaLaLa) [嘿小欧](https://afdian.net/u/daa4bec4f24911ec82e552540025c377) [回忆的秋千](https://afdian.net/u/e315d9c6f14f11ecbeef52540025c377) [十年くん](https://github.com/shinianj) [哇](https://afdian.net/u/9b266244f23911eca19052540025c377) [yajiwa](https://github.com/yajiwa) [爆金币](https://afdian.net/u/0d78879ef23711ecb22452540025c377)...
|
||||
|
||||
### 特别赞助
|
||||
|
||||
<div align=center>
|
||||
|
||||
<img width="60%" src="https://edgeone.ai/media/34fe3a45-492d-4ea4-ae5d-ea1087ca7b4b.png" />
|
||||
|
||||
[亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne](https://edgeone.ai/zh?from=github)
|
||||
|
||||
**本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助**
|
||||
|
||||
</div>
|
||||
|
||||
## 📜 贡献指南
|
||||
|
||||
欢迎查看我们的 [贡献指南](CONTRIBUTING.md) 和 [行为守则](CODE_OF_CONDUCT.md) 以了解如何参与贡献。
|
||||
|
||||
@ -1 +1 @@
|
||||
__version__: v0.2.4-2c97eea
|
||||
__version__: v0.2.4-da6d5b4
|
||||
|
||||
3176
envs/pydantic-v1/poetry.lock
generated
3176
envs/pydantic-v1/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -45,6 +45,7 @@ nonebot-plugin-alconna = "^0.54.0"
|
||||
tenacity = "^9.0.0"
|
||||
nonebot-plugin-uninfo = ">0.4.1"
|
||||
pydantic = "1.10.18"
|
||||
alibabacloud-devops20210625 = "^5.0.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
nonebug = "^0.4"
|
||||
|
||||
3187
envs/pydantic-v2/poetry.lock
generated
3187
envs/pydantic-v2/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -45,6 +45,7 @@ nonebot-plugin-alconna = "^0.54.0"
|
||||
tenacity = "^9.0.0"
|
||||
nonebot-plugin-uninfo = ">0.4.1"
|
||||
pydantic = "2.10.6"
|
||||
alibabacloud-devops20210625 = "^5.0.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
nonebug = "^0.4"
|
||||
|
||||
3147
poetry.lock
generated
3147
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,7 @@ 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" }
|
||||
tortoise-orm = "^0.20.0"
|
||||
cattrs = "^23.2.3"
|
||||
ruamel-yaml = "^0.18.5"
|
||||
strenum = "^0.4.15"
|
||||
@ -39,7 +39,7 @@ 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"
|
||||
aiocache = {extras = ["redis"], version = "^0.12.3"}
|
||||
py-cpuinfo = "^9.0.0"
|
||||
nonebot-plugin-alconna = "^0.54.0"
|
||||
tenacity = "^9.0.0"
|
||||
@ -47,6 +47,10 @@ nonebot-plugin-uninfo = ">0.4.1"
|
||||
nonebot-plugin-waiter = "^0.8.1"
|
||||
multidict = ">=6.0.0,!=6.3.2"
|
||||
|
||||
redis = { version = ">=5", optional = true }
|
||||
asyncpg = { version = ">=0.20.0", optional = true }
|
||||
alibabacloud-devops20210625 = "^5.0.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
nonebug = "^0.4"
|
||||
pytest-cov = "^5.0.0"
|
||||
@ -57,6 +61,9 @@ respx = "^0.21.1"
|
||||
ruff = "^0.8.0"
|
||||
pre-commit = "^4.0.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
redis = ["redis"]
|
||||
postgresql = ["asyncpg"]
|
||||
|
||||
[tool.nonebot]
|
||||
plugins = [
|
||||
|
||||
@ -2,6 +2,7 @@ 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"
|
||||
alibabacloud-devops20210625==5.0.2 ; 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"
|
||||
|
||||
@ -13,7 +13,11 @@ 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
|
||||
|
||||
|
||||
@ -311,6 +315,12 @@ 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(
|
||||
@ -401,6 +411,12 @@ 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(
|
||||
|
||||
@ -136,20 +136,20 @@ async def test_check(
|
||||
+ 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)}"
|
||||
"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_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_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}",
|
||||
"system": f"{platform_uname.system} {platform_uname.release}",
|
||||
"version": __get_version(),
|
||||
"plugin_count": len(nonebot.get_loaded_plugins()),
|
||||
"nickname": BotConfig.self_nickname,
|
||||
@ -244,8 +244,7 @@ async def test_check_arm(
|
||||
"brand_raw": "",
|
||||
"baidu": "red",
|
||||
"google": "red",
|
||||
"system": f"{platform_uname_arm.system} "
|
||||
f"{platform_uname_arm.release}",
|
||||
"system": f"{platform_uname_arm.system} {platform_uname_arm.release}",
|
||||
"version": __get_version(),
|
||||
"plugin_count": len(nonebot.get_loaded_plugins()),
|
||||
"nickname": BotConfig.self_nickname,
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
# ruff: noqa: ASYNC230
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from respx import MockRouter
|
||||
|
||||
@ -5,6 +5,10 @@ from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageSegme
|
||||
from nonebot.adapters.onebot.v11.event import Sender
|
||||
|
||||
|
||||
def get_reply_cq(uid: int | str) -> str:
|
||||
return f"[CQ:reply,id={uid}]"
|
||||
|
||||
|
||||
def get_response_json(base_path: Path, file: str) -> dict:
|
||||
try:
|
||||
return json.loads(
|
||||
|
||||
@ -5,7 +5,7 @@ import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.drivers import Driver
|
||||
from tortoise import Tortoise
|
||||
from tortoise.exceptions import OperationalError
|
||||
from tortoise.exceptions import IntegrityError, OperationalError
|
||||
import ujson as json
|
||||
|
||||
from zhenxun.models.bot_connect_log import BotConnectLog
|
||||
@ -30,9 +30,12 @@ async def _(bot: Bot):
|
||||
bot_id=bot.self_id, platform=bot.adapter, connect_time=datetime.now(), type=1
|
||||
)
|
||||
if not await BotConsole.exists(bot_id=bot.self_id):
|
||||
await BotConsole.create(
|
||||
bot_id=bot.self_id, platform=PlatformUtils.get_platform(bot)
|
||||
)
|
||||
try:
|
||||
await BotConsole.create(
|
||||
bot_id=bot.self_id, platform=PlatformUtils.get_platform(bot)
|
||||
)
|
||||
except IntegrityError as e:
|
||||
logger.warning(f"记录bot: {bot.self_id} 数据已存在...", e=e)
|
||||
|
||||
|
||||
@driver.on_bot_disconnect
|
||||
@ -50,22 +53,31 @@ async def _(bot: Bot):
|
||||
|
||||
|
||||
SIGN_SQL = """
|
||||
select distinct on("user_id") t1.user_id, t1.checkin_count, t1.add_probability,
|
||||
t1.specify_probability, t1.impression
|
||||
from public.sign_group_users t1
|
||||
join (
|
||||
select user_id, max(t2.impression) as max_impression
|
||||
from public.sign_group_users t2
|
||||
group by user_id
|
||||
) t on t.user_id = t1.user_id and t.max_impression = t1.impression
|
||||
SELECT user_id, checkin_count, add_probability, specify_probability, impression
|
||||
FROM (
|
||||
SELECT
|
||||
t1.user_id,
|
||||
t1.checkin_count,
|
||||
t1.add_probability,
|
||||
t1.specify_probability,
|
||||
t1.impression,
|
||||
ROW_NUMBER() OVER(PARTITION BY t1.user_id ORDER BY t1.impression DESC) AS rn
|
||||
FROM sign_group_users t1
|
||||
INNER JOIN (
|
||||
SELECT user_id, MAX(impression) AS max_impression
|
||||
FROM sign_group_users
|
||||
GROUP BY user_id
|
||||
) t2 ON t2.user_id = t1.user_id AND t2.max_impression = t1.impression
|
||||
) t
|
||||
WHERE rn = 1
|
||||
"""
|
||||
|
||||
BAG_SQL = """
|
||||
select t1.user_id, t1.gold, t1.property
|
||||
from public.bag_users t1
|
||||
from bag_users t1
|
||||
join (
|
||||
select user_id, max(t2.gold) as max_gold
|
||||
from public.bag_users t2
|
||||
from bag_users t2
|
||||
group by user_id
|
||||
) t on t.user_id = t1.user_id and t.max_gold = t1.gold
|
||||
"""
|
||||
|
||||
@ -25,6 +25,11 @@ __plugin_meta__ = PluginMetadata(
|
||||
version="0.1",
|
||||
plugin_type=PluginType.ADMIN,
|
||||
admin_level=1,
|
||||
introduction="""这是 群主/群管理 的帮助列表,里面记录了群组内开关功能的
|
||||
方法帮助以及群管特权方法,建议首次时在群组中发送 '管理员帮助' 查看""",
|
||||
precautions=[
|
||||
"只有群主/群管理 才能使用哦,群主拥有6级权限,管理员拥有5级权限!"
|
||||
],
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="type",
|
||||
|
||||
@ -16,7 +16,8 @@ async def get_task() -> dict[str, str] | None:
|
||||
"name": "被动技能",
|
||||
"description": "控制群组中的被动技能状态",
|
||||
"usage": "通过 开启/关闭群被动 来控制群被动 <br>"
|
||||
+ " 示例:开启/关闭群被动早晚安 <br> ---------- <br> "
|
||||
+ " 示例:开启/关闭群被动早晚安 <br> 示例:开启/关闭全部群被动"
|
||||
+ " <br> ---------- <br> "
|
||||
+ "<br>".join([task.name for task in task_list]),
|
||||
}
|
||||
return None
|
||||
@ -47,7 +48,7 @@ async def build_html_help():
|
||||
}
|
||||
},
|
||||
pages={
|
||||
"viewport": {"width": 1024, "height": 1024},
|
||||
"viewport": {"width": 824, "height": 10},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
|
||||
@ -36,11 +36,12 @@ __plugin_meta__ = PluginMetadata(
|
||||
usage="""
|
||||
普通管理员
|
||||
格式:
|
||||
ban [At用户] ?[-t [时长(分钟)]]
|
||||
ban [At用户] ?[-t [时长(分钟)]] ?[-r [理由]]
|
||||
|
||||
示例:
|
||||
ban @用户 : 永久拉黑用户
|
||||
ban @用户 -t 100 : 拉黑用户100分钟
|
||||
ban @用户 -t 10 -r 坏 : 拉黑用户10分钟并携带理由
|
||||
unban @用户 : 从小黑屋中拉出来
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
@ -50,7 +51,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
superuser_help="""
|
||||
超级管理员额外命令
|
||||
格式:
|
||||
ban [At用户/用户Id] ?[-t [时长]]
|
||||
ban [At用户/用户Id] ?[-t [时长]] ?[-r [理由]]
|
||||
unban --id [idx] : 通过id来进行unban操作
|
||||
ban列表: 获取所有Ban数据
|
||||
|
||||
@ -66,10 +67,13 @@ __plugin_meta__ = PluginMetadata(
|
||||
私聊下:
|
||||
示例:
|
||||
ban 123456789 : 永久拉黑用户123456789
|
||||
ban 123456789 -r 坏 : 永久拉黑用户123456789并携带理由
|
||||
ban 123456789 -t 100 : 拉黑用户123456789 100分钟
|
||||
|
||||
ban -g 999999 : 拉黑群组为999999的群组
|
||||
ban -g 999999 -t 100 : 拉黑群组为999999的群组 100分钟
|
||||
ban -g 999999 -r 坏 : 永久拉黑群组为999999的群组并携带理由
|
||||
|
||||
|
||||
unban 123456789 : 从小黑屋中拉出来
|
||||
unban -g 999999 : 将群组9999999从小黑屋中拉出来
|
||||
@ -87,13 +91,20 @@ __plugin_meta__ = PluginMetadata(
|
||||
smart_tools=[
|
||||
AICallableTag(
|
||||
name="call_ban",
|
||||
description="某人多次(至少三次)辱骂你,调用此方法进行封禁",
|
||||
description="如果你讨厌某个人(好感度过低并让你感到困扰,或者多次辱骂你),调用此方法进行封禁,调用该方法后要告知用户被封禁和原因",
|
||||
parameters=AICallableParam(
|
||||
type="object",
|
||||
properties={
|
||||
"user_id": AICallableProperties(
|
||||
type="string", description="用户的id"
|
||||
),
|
||||
"reason": AICallableProperties(
|
||||
type="string", description="封禁理由"
|
||||
),
|
||||
"duration": AICallableProperties(
|
||||
type="integer",
|
||||
description="封禁时长(选择的值只能是1-360),单位为分钟,如果频繁触发,按情况增加",
|
||||
),
|
||||
},
|
||||
required=["user_id"],
|
||||
),
|
||||
@ -108,6 +119,7 @@ _ban_matcher = on_alconna(
|
||||
Alconna(
|
||||
"ban",
|
||||
Args["user?", [str, At]],
|
||||
Option("-r|--reason", Args["reason", str]),
|
||||
Option("-g|--group", Args["group_id", str]),
|
||||
Option("-t|--time", Args["duration", int]),
|
||||
),
|
||||
@ -181,6 +193,7 @@ async def _(
|
||||
session: EventSession,
|
||||
arparma: Arparma,
|
||||
user: Match[str | At],
|
||||
reason: Match[str],
|
||||
duration: Match[int],
|
||||
group_id: Match[str],
|
||||
):
|
||||
@ -196,13 +209,14 @@ async def _(
|
||||
user_id = user.result
|
||||
_duration = duration.result * 60 if duration.available else -1
|
||||
_duration_text = f"{duration.result} 分钟" if duration.available else " 到世界湮灭"
|
||||
ban_reason = reason.result if reason.available else None
|
||||
if (gid := session.id3 or session.id2) and not group_id.available:
|
||||
if not user_id or (
|
||||
user_id == bot.self_id and session.id1 not in bot.config.superusers
|
||||
):
|
||||
_duration = 0.5
|
||||
await MessageUtils.build_message("倒反天罡,小小管理速速退下!").send()
|
||||
await BanManage.ban(session.id1, gid, 30, session, True)
|
||||
await BanManage.ban(session.id1, gid, ban_reason, 30, session, True)
|
||||
_duration_text = "半 分钟"
|
||||
logger.info(
|
||||
f"尝试ban {BotConfig.self_nickname} 反被拿下",
|
||||
@ -218,7 +232,12 @@ async def _(
|
||||
]
|
||||
).finish(reply_to=True)
|
||||
await BanManage.ban(
|
||||
user_id, gid, _duration, session, session.id1 in bot.config.superusers
|
||||
user_id,
|
||||
gid,
|
||||
ban_reason,
|
||||
_duration,
|
||||
session,
|
||||
session.id1 in bot.config.superusers,
|
||||
)
|
||||
logger.info(
|
||||
"管理员Ban",
|
||||
@ -240,7 +259,7 @@ async def _(
|
||||
).finish(reply_to=True)
|
||||
elif session.id1 in bot.config.superusers:
|
||||
_group_id = group_id.result if group_id.available else None
|
||||
await BanManage.ban(user_id, _group_id, _duration, session, True)
|
||||
await BanManage.ban(user_id, _group_id, ban_reason, _duration, session, True)
|
||||
logger.info(
|
||||
"超级用户Ban",
|
||||
arparma.header_result,
|
||||
@ -292,7 +311,7 @@ async def _(
|
||||
At(flag="user", target=user_id)
|
||||
if isinstance(user.result, At)
|
||||
else result
|
||||
), # type: ignore
|
||||
),
|
||||
" 从黑屋中拉了出来并急救了一下!",
|
||||
]
|
||||
).finish(reply_to=True)
|
||||
|
||||
@ -9,14 +9,14 @@ from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
|
||||
|
||||
async def call_ban(user_id: str):
|
||||
async def call_ban(user_id: str, reason: str | None = None, duration: int = 1):
|
||||
"""调用ban
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
"""
|
||||
await BanConsole.ban(user_id, None, 9, 60 * 12)
|
||||
logger.info("辱骂次数过多,已将用户加入黑名单...", "ban", session=user_id)
|
||||
await BanConsole.ban(user_id, None, 9, reason, duration * 60)
|
||||
logger.info("被讨厌了,已将用户加入黑名单...", "ban", session=user_id)
|
||||
|
||||
|
||||
class BanManage:
|
||||
@ -55,20 +55,23 @@ 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)
|
||||
row_data.append(
|
||||
[
|
||||
data.id,
|
||||
data.user_id,
|
||||
data.group_id,
|
||||
data.ban_level,
|
||||
data.ban_reason,
|
||||
duration,
|
||||
data.operator,
|
||||
]
|
||||
@ -114,16 +117,21 @@ class BanManage:
|
||||
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,
|
||||
f"用户 {ban_data.user_id}"
|
||||
if ban_data.user_id
|
||||
else f"群组 {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, f"群组 {group_id}"
|
||||
return False, "该用户/群组不在黑名单中不足捏..."
|
||||
|
||||
@classmethod
|
||||
@ -131,6 +139,7 @@ class BanManage:
|
||||
cls,
|
||||
user_id: str | None,
|
||||
group_id: str | None,
|
||||
reason: str | None,
|
||||
duration: int,
|
||||
session: EventSession,
|
||||
is_superuser: bool,
|
||||
@ -140,6 +149,7 @@ class BanManage:
|
||||
参数:
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
reason: 理由
|
||||
duration: 时长,秒
|
||||
session: Session
|
||||
is_superuser: 是否为超级用户操作
|
||||
@ -147,4 +157,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,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
@ -32,7 +33,9 @@ class MemberUpdateManage:
|
||||
"""
|
||||
driver = nonebot.get_driver()
|
||||
default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH")
|
||||
nickname = member.nick or member.user.name or ""
|
||||
nickname = re.sub(
|
||||
r"[\x00-\x09\x0b-\x1f\x7f-\x9f]", "", member.nick or member.user.name or ""
|
||||
)
|
||||
role = member.role
|
||||
db_user_uid = [u.user_id for u in db_user]
|
||||
uid2name = {u.user_id: u.user_name for u in db_user}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import AlconnaQuery, Arparma, Match, Query
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
@ -9,7 +9,7 @@ from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import PluginManage, build_plugin, build_task, delete_help_image
|
||||
from ._data_source import PluginManager, build_plugin, build_task, delete_help_image
|
||||
from .command import _group_status_matcher, _status_matcher
|
||||
|
||||
base_config = Config.get("plugin_switch")
|
||||
@ -57,6 +57,11 @@ __plugin_meta__ = PluginMetadata(
|
||||
关闭群被动早晚安
|
||||
关闭群被动早晚安 -g 12355555
|
||||
|
||||
开启/关闭默认群被动 [被动名称]
|
||||
私聊下: 开启/关闭群被动默认状态
|
||||
示例:
|
||||
关闭默认群被动 早晚安
|
||||
|
||||
开启/关闭所有群被动 ?[-g [group_id]]
|
||||
私聊中: 开启/关闭全局或指定群组被动状态
|
||||
示例:
|
||||
@ -87,10 +92,10 @@ __plugin_meta__ = PluginMetadata(
|
||||
@_status_matcher.assign("$main")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
):
|
||||
if session.id1 in bot.config.superusers:
|
||||
if session.user.id in bot.config.superusers:
|
||||
image = await build_plugin()
|
||||
logger.info(
|
||||
"查看功能列表",
|
||||
@ -105,7 +110,7 @@ async def _(
|
||||
@_status_matcher.assign("open")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
plugin_name: Match[str],
|
||||
group: Match[str],
|
||||
@ -114,22 +119,23 @@ async def _(
|
||||
all: Query[bool] = AlconnaQuery("all.value", False),
|
||||
):
|
||||
if not all.result and not plugin_name.available:
|
||||
await MessageUtils.build_message("请输入功能名称").finish(reply_to=True)
|
||||
await MessageUtils.build_message("请输入功能/被动名称").finish(reply_to=True)
|
||||
name = plugin_name.result
|
||||
if gid := session.id3 or session.id2:
|
||||
if session.group:
|
||||
group_id = session.group.id
|
||||
"""修改当前群组的数据"""
|
||||
if task.result:
|
||||
if all.result:
|
||||
result = await PluginManage.unblock_group_all_task(gid)
|
||||
result = await PluginManager.unblock_group_all_task(group_id)
|
||||
logger.info("开启所有群组被动", arparma.header_result, session=session)
|
||||
else:
|
||||
result = await PluginManage.unblock_group_task(name, gid)
|
||||
result = await PluginManager.unblock_group_task(name, group_id)
|
||||
logger.info(
|
||||
f"开启群组被动 {name}", arparma.header_result, session=session
|
||||
)
|
||||
elif session.id1 in bot.config.superusers and default_status.result:
|
||||
elif session.user.id in bot.config.superusers and default_status.result:
|
||||
"""单个插件的进群默认修改"""
|
||||
result = await PluginManage.set_default_status(name, True)
|
||||
result = await PluginManager.set_default_status(name, True)
|
||||
logger.info(
|
||||
f"超级用户开启 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
@ -137,8 +143,8 @@ async def _(
|
||||
)
|
||||
elif all.result:
|
||||
"""所有插件"""
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
True, default_status.result, gid
|
||||
result = await PluginManager.set_all_plugin_status(
|
||||
True, default_status.result, group_id
|
||||
)
|
||||
logger.info(
|
||||
"开启群组中全部功能",
|
||||
@ -146,22 +152,24 @@ async def _(
|
||||
session=session,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.unblock_group_plugin(name, gid)
|
||||
result = await PluginManager.unblock_group_plugin(name, group_id)
|
||||
logger.info(f"开启功能 {name}", arparma.header_result, session=session)
|
||||
delete_help_image(gid)
|
||||
delete_help_image(group_id)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
elif session.id1 in bot.config.superusers:
|
||||
elif session.user.id in bot.config.superusers:
|
||||
"""私聊"""
|
||||
group_id = group.result if group.available else None
|
||||
if all.result:
|
||||
if task.result:
|
||||
"""关闭全局或指定群全部被动"""
|
||||
if group_id:
|
||||
result = await PluginManage.unblock_group_all_task(group_id)
|
||||
result = await PluginManager.unblock_group_all_task(group_id)
|
||||
else:
|
||||
result = await PluginManage.unblock_global_all_task()
|
||||
result = await PluginManager.unblock_global_all_task(
|
||||
default_status.result
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
result = await PluginManager.set_all_plugin_status(
|
||||
True, default_status.result, group_id
|
||||
)
|
||||
logger.info(
|
||||
@ -171,8 +179,8 @@ async def _(
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
if default_status.result:
|
||||
result = await PluginManage.set_default_status(name, True)
|
||||
if default_status.result and not task.result:
|
||||
result = await PluginManager.set_default_status(name, True)
|
||||
logger.info(
|
||||
f"超级用户开启 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
@ -186,7 +194,7 @@ async def _(
|
||||
name = split_list[0]
|
||||
group_id = split_list[1]
|
||||
if group_id:
|
||||
result = await PluginManage.superuser_task_handle(name, group_id, True)
|
||||
result = await PluginManager.superuser_task_handle(name, group_id, True)
|
||||
logger.info(
|
||||
f"超级用户开启被动技能 {name}",
|
||||
arparma.header_result,
|
||||
@ -194,14 +202,16 @@ async def _(
|
||||
target=group_id,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.unblock_global_task(name)
|
||||
result = await PluginManager.unblock_global_task(
|
||||
name, default_status.result
|
||||
)
|
||||
logger.info(
|
||||
f"超级用户开启全局被动技能 {name}",
|
||||
arparma.header_result,
|
||||
session=session,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.superuser_unblock(name, None, group_id)
|
||||
result = await PluginManager.superuser_unblock(name, None, group_id)
|
||||
logger.info(
|
||||
f"超级用户开启功能 {name}",
|
||||
arparma.header_result,
|
||||
@ -215,7 +225,7 @@ async def _(
|
||||
@_status_matcher.assign("close")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
plugin_name: Match[str],
|
||||
block_type: Match[str],
|
||||
@ -225,22 +235,23 @@ async def _(
|
||||
all: Query[bool] = AlconnaQuery("all.value", False),
|
||||
):
|
||||
if not all.result and not plugin_name.available:
|
||||
await MessageUtils.build_message("请输入功能名称").finish(reply_to=True)
|
||||
await MessageUtils.build_message("请输入功能/被动名称").finish(reply_to=True)
|
||||
name = plugin_name.result
|
||||
if gid := session.id3 or session.id2:
|
||||
if session.group:
|
||||
group_id = session.group.id
|
||||
"""修改当前群组的数据"""
|
||||
if task.result:
|
||||
if all.result:
|
||||
result = await PluginManage.block_group_all_task(gid)
|
||||
result = await PluginManager.block_group_all_task(group_id)
|
||||
logger.info("开启所有群组被动", arparma.header_result, session=session)
|
||||
else:
|
||||
result = await PluginManage.block_group_task(name, gid)
|
||||
result = await PluginManager.block_group_task(name, group_id)
|
||||
logger.info(
|
||||
f"关闭群组被动 {name}", arparma.header_result, session=session
|
||||
)
|
||||
elif session.id1 in bot.config.superusers and default_status.result:
|
||||
elif session.user.id in bot.config.superusers and default_status.result:
|
||||
"""单个插件的进群默认修改"""
|
||||
result = await PluginManage.set_default_status(name, False)
|
||||
result = await PluginManager.set_default_status(name, False)
|
||||
logger.info(
|
||||
f"超级用户开启 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
@ -248,26 +259,28 @@ async def _(
|
||||
)
|
||||
elif all.result:
|
||||
"""所有插件"""
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
False, default_status.result, gid
|
||||
result = await PluginManager.set_all_plugin_status(
|
||||
False, default_status.result, group_id
|
||||
)
|
||||
logger.info("关闭群组中全部功能", arparma.header_result, session=session)
|
||||
else:
|
||||
result = await PluginManage.block_group_plugin(name, gid)
|
||||
result = await PluginManager.block_group_plugin(name, group_id)
|
||||
logger.info(f"关闭功能 {name}", arparma.header_result, session=session)
|
||||
delete_help_image(gid)
|
||||
delete_help_image(group_id)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
elif session.id1 in bot.config.superusers:
|
||||
elif session.user.id in bot.config.superusers:
|
||||
group_id = group.result if group.available else None
|
||||
if all.result:
|
||||
if task.result:
|
||||
"""关闭全局或指定群全部被动"""
|
||||
if group_id:
|
||||
result = await PluginManage.block_group_all_task(group_id)
|
||||
result = await PluginManager.block_group_all_task(group_id)
|
||||
else:
|
||||
result = await PluginManage.block_global_all_task()
|
||||
result = await PluginManager.block_global_all_task(
|
||||
default_status.result
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.set_all_plugin_status(
|
||||
result = await PluginManager.set_all_plugin_status(
|
||||
False, default_status.result, group_id
|
||||
)
|
||||
logger.info(
|
||||
@ -277,8 +290,8 @@ async def _(
|
||||
session=session,
|
||||
)
|
||||
await MessageUtils.build_message(result).finish(reply_to=True)
|
||||
if default_status.result:
|
||||
result = await PluginManage.set_default_status(name, False)
|
||||
if default_status.result and not task.result:
|
||||
result = await PluginManager.set_default_status(name, False)
|
||||
logger.info(
|
||||
f"超级用户关闭 {name} 功能进群默认开关",
|
||||
arparma.header_result,
|
||||
@ -292,7 +305,9 @@ async def _(
|
||||
name = split_list[0]
|
||||
group_id = split_list[1]
|
||||
if group_id:
|
||||
result = await PluginManage.superuser_task_handle(name, group_id, False)
|
||||
result = await PluginManager.superuser_task_handle(
|
||||
name, group_id, False
|
||||
)
|
||||
logger.info(
|
||||
f"超级用户关闭被动技能 {name}",
|
||||
arparma.header_result,
|
||||
@ -300,7 +315,9 @@ async def _(
|
||||
target=group_id,
|
||||
)
|
||||
else:
|
||||
result = await PluginManage.block_global_task(name)
|
||||
result = await PluginManager.block_global_task(
|
||||
name, default_status.result
|
||||
)
|
||||
logger.info(
|
||||
f"超级用户关闭全局被动技能 {name}",
|
||||
arparma.header_result,
|
||||
@ -314,7 +331,7 @@ async def _(
|
||||
elif block_type.result in ["g", "group"]:
|
||||
if block_type.available:
|
||||
_type = BlockType.GROUP
|
||||
result = await PluginManage.superuser_block(name, _type, group_id)
|
||||
result = await PluginManager.superuser_block(name, _type, group_id)
|
||||
logger.info(
|
||||
f"超级用户关闭功能 {name}, 禁用类型: {_type}",
|
||||
arparma.header_result,
|
||||
@ -327,19 +344,20 @@ async def _(
|
||||
|
||||
@_group_status_matcher.handle()
|
||||
async def _(
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
status: str,
|
||||
):
|
||||
if gid := session.id3 or session.id2:
|
||||
if session.group:
|
||||
group_id = session.group.id
|
||||
if status == "sleep":
|
||||
await PluginManage.sleep(gid)
|
||||
await PluginManager.sleep(group_id)
|
||||
logger.info("进行休眠", arparma.header_result, session=session)
|
||||
await MessageUtils.build_message("那我先睡觉了...").finish()
|
||||
else:
|
||||
if await PluginManage.is_wake(gid):
|
||||
if await PluginManager.is_wake(group_id):
|
||||
await MessageUtils.build_message("我还醒着呢!").finish()
|
||||
await PluginManage.wake(gid)
|
||||
await PluginManager.wake(group_id)
|
||||
logger.info("醒来", arparma.header_result, session=session)
|
||||
await MessageUtils.build_message("呜..醒来了...").finish()
|
||||
return MessageUtils.build_message("群组id为空...").send()
|
||||
@ -347,10 +365,10 @@ async def _(
|
||||
|
||||
@_status_matcher.assign("task")
|
||||
async def _(
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
arparma: Arparma,
|
||||
):
|
||||
image = await build_task(session.id3 or session.id2)
|
||||
image = await build_task(session.group.id if session.group else None)
|
||||
if image:
|
||||
logger.info("查看群被动列表", arparma.header_result, session=session)
|
||||
await MessageUtils.build_message(image).finish(reply_to=True)
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
from zhenxun.services.cache import CacheRoot
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.enum import BlockType, CacheType, PluginType
|
||||
from zhenxun.utils.exception import GroupInfoNotFound
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle
|
||||
|
||||
@ -116,9 +119,7 @@ async def build_task(group_id: str | None) -> BuildImage:
|
||||
column_name = ["ID", "模块", "名称", "群组状态", "全局状态", "运行时间"]
|
||||
group = None
|
||||
if group_id:
|
||||
group = await GroupConsole.get_or_none(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
)
|
||||
group = await GroupConsole.get_group(group_id=group_id)
|
||||
if not group:
|
||||
raise GroupInfoNotFound()
|
||||
else:
|
||||
@ -155,7 +156,7 @@ async def build_task(group_id: str | None) -> BuildImage:
|
||||
)
|
||||
|
||||
|
||||
class PluginManage:
|
||||
class PluginManager:
|
||||
@classmethod
|
||||
async def set_default_status(cls, plugin_name: str, status: bool) -> str:
|
||||
"""设置插件进群默认状态
|
||||
@ -200,26 +201,26 @@ class PluginManage:
|
||||
)
|
||||
return f"成功将所有功能进群默认状态修改为: {'开启' if status else '关闭'}"
|
||||
if group_id:
|
||||
if group := await GroupConsole.get_or_none(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
):
|
||||
module_list = await PluginInfo.filter(
|
||||
plugin_type=PluginType.NORMAL
|
||||
).values_list("module", flat=True)
|
||||
if group := await GroupConsole.get_group(group_id=group_id):
|
||||
module_list = cast(
|
||||
list[str],
|
||||
await PluginInfo.filter(plugin_type=PluginType.NORMAL).values_list(
|
||||
"module", flat=True
|
||||
),
|
||||
)
|
||||
if status:
|
||||
for module in module_list:
|
||||
group.block_plugin = group.block_plugin.replace(
|
||||
f"<{module},", ""
|
||||
)
|
||||
# 开启所有功能 - 清空禁用列表
|
||||
group.block_plugin = ""
|
||||
else:
|
||||
module_list = [f"<{module}" for module in module_list]
|
||||
group.block_plugin = ",".join(module_list) + "," # type: ignore
|
||||
# 关闭所有功能 - 将模块列表转换为禁用格式
|
||||
group.block_plugin = CommonUtils.convert_module_format(module_list)
|
||||
await group.save(update_fields=["block_plugin"])
|
||||
return f"成功将此群组所有功能状态修改为: {'开启' if status else '关闭'}"
|
||||
return "获取群组失败..."
|
||||
await PluginInfo.filter(plugin_type=PluginType.NORMAL).update(
|
||||
status=status, block_type=None if status else BlockType.ALL
|
||||
)
|
||||
await CacheRoot.invalidate_cache(CacheType.PLUGINS)
|
||||
return f"成功将所有功能全局状态修改为: {'开启' if status else '关闭'}"
|
||||
|
||||
@classmethod
|
||||
@ -232,9 +233,7 @@ class PluginManage:
|
||||
返回:
|
||||
bool: 是否醒来
|
||||
"""
|
||||
if c := await GroupConsole.get_or_none(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
):
|
||||
if c := await GroupConsole.get_group(group_id=group_id):
|
||||
return c.status
|
||||
return False
|
||||
|
||||
@ -245,9 +244,11 @@ class PluginManage:
|
||||
参数:
|
||||
group_id: 群组id
|
||||
"""
|
||||
await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update(
|
||||
status=False
|
||||
group, _ = await GroupConsole.get_or_create(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
)
|
||||
group.status = False
|
||||
await group.save(update_fields=["status"])
|
||||
|
||||
@classmethod
|
||||
async def wake(cls, group_id: str):
|
||||
@ -256,9 +257,11 @@ class PluginManage:
|
||||
参数:
|
||||
group_id: 群组id
|
||||
"""
|
||||
await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update(
|
||||
status=True
|
||||
group, _ = await GroupConsole.get_or_create(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
)
|
||||
group.status = True
|
||||
await group.save(update_fields=["status"])
|
||||
|
||||
@classmethod
|
||||
async def block(cls, module: str):
|
||||
@ -267,7 +270,9 @@ class PluginManage:
|
||||
参数:
|
||||
module: 模块名
|
||||
"""
|
||||
await PluginInfo.filter(module=module).update(status=False)
|
||||
if plugin := await PluginInfo.get_plugin(module=module):
|
||||
plugin.status = False
|
||||
await plugin.save(update_fields=["status"])
|
||||
|
||||
@classmethod
|
||||
async def unblock(cls, module: str):
|
||||
@ -276,7 +281,9 @@ class PluginManage:
|
||||
参数:
|
||||
module: 模块名
|
||||
"""
|
||||
await PluginInfo.filter(module=module).update(status=True)
|
||||
if plugin := await PluginInfo.get_plugin(module=module):
|
||||
plugin.status = True
|
||||
await plugin.save(update_fields=["status"])
|
||||
|
||||
@classmethod
|
||||
async def block_group_plugin(cls, plugin_name: str, group_id: str) -> str:
|
||||
@ -342,17 +349,21 @@ class PluginManage:
|
||||
return await cls._change_group_task("", group_id, True, True)
|
||||
|
||||
@classmethod
|
||||
async def block_global_all_task(cls) -> str:
|
||||
async def block_global_all_task(cls, is_default: bool) -> str:
|
||||
"""禁用全局被动技能
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.all().update(status=False)
|
||||
return "已全局禁用所有被动状态"
|
||||
if is_default:
|
||||
await TaskInfo.all().update(default_status=False)
|
||||
return "已禁用所有被动进群默认状态"
|
||||
else:
|
||||
await TaskInfo.all().update(status=False)
|
||||
return "已全局禁用所有被动状态"
|
||||
|
||||
@classmethod
|
||||
async def block_global_task(cls, name: str) -> str:
|
||||
async def block_global_task(cls, name: str, is_default: bool = False) -> str:
|
||||
"""禁用全局被动技能
|
||||
|
||||
参数:
|
||||
@ -361,31 +372,47 @@ class PluginManage:
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.filter(name=name).update(status=False)
|
||||
return f"已全局禁用被动状态 {name}"
|
||||
if is_default:
|
||||
await TaskInfo.filter(name=name).update(default_status=False)
|
||||
return f"已禁用被动进群默认状态 {name}"
|
||||
else:
|
||||
await TaskInfo.filter(name=name).update(status=False)
|
||||
return f"已全局禁用被动状态 {name}"
|
||||
|
||||
@classmethod
|
||||
async def unblock_global_all_task(cls) -> str:
|
||||
async def unblock_global_all_task(cls, is_default: bool) -> str:
|
||||
"""开启全局被动技能
|
||||
|
||||
参数:
|
||||
is_default: 是否为默认状态
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.all().update(status=True)
|
||||
return "已全局开启所有被动状态"
|
||||
if is_default:
|
||||
await TaskInfo.all().update(default_status=True)
|
||||
return "已开启所有被动进群默认状态"
|
||||
else:
|
||||
await TaskInfo.all().update(status=True)
|
||||
return "已全局开启所有被动状态"
|
||||
|
||||
@classmethod
|
||||
async def unblock_global_task(cls, name: str) -> str:
|
||||
async def unblock_global_task(cls, name: str, is_default: bool = False) -> str:
|
||||
"""开启全局被动技能
|
||||
|
||||
参数:
|
||||
name: 被动技能名称
|
||||
is_default: 是否为默认状态
|
||||
|
||||
返回:
|
||||
str: 返回信息
|
||||
"""
|
||||
await TaskInfo.filter(name=name).update(status=True)
|
||||
return f"已全局开启被动状态 {name}"
|
||||
if is_default:
|
||||
await TaskInfo.filter(name=name).update(default_status=True)
|
||||
return f"已开启被动进群默认状态 {name}"
|
||||
else:
|
||||
await TaskInfo.filter(name=name).update(status=True)
|
||||
return f"已全局开启被动状态 {name}"
|
||||
|
||||
@classmethod
|
||||
async def unblock_group_plugin(cls, plugin_name: str, group_id: str) -> str:
|
||||
@ -417,17 +444,18 @@ class PluginManage:
|
||||
"""
|
||||
status_str = "关闭" if status else "开启"
|
||||
if is_all:
|
||||
modules = await TaskInfo.annotate().values_list("module", flat=True)
|
||||
if modules:
|
||||
module_list = cast(
|
||||
list[str], await TaskInfo.annotate().values_list("module", flat=True)
|
||||
)
|
||||
if module_list:
|
||||
group, _ = await GroupConsole.get_or_create(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
)
|
||||
modules = [f"<{module}" for module in modules]
|
||||
if status:
|
||||
group.block_task = ",".join(modules) + "," # type: ignore
|
||||
group.block_task = CommonUtils.convert_module_format(module_list)
|
||||
else:
|
||||
for module in modules:
|
||||
group.block_task = group.block_task.replace(f"{module},", "")
|
||||
# 开启所有模块 - 清空禁用列表
|
||||
group.block_task = ""
|
||||
await group.save(update_fields=["block_task"])
|
||||
return f"已成功{status_str}全部被动技能!"
|
||||
elif task := await TaskInfo.get_or_none(name=task_name):
|
||||
|
||||
@ -58,6 +58,19 @@ _status_matcher.shortcut(
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启(所有|全部)默认群被动",
|
||||
command="switch",
|
||||
arguments=["open", "--task", "--all", "-df"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"关闭(所有|全部)默认群被动",
|
||||
command="switch",
|
||||
arguments=["close", "--task", "--all", "-df"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启群被动\s*(?P<name>.+)",
|
||||
@ -73,6 +86,20 @@ _status_matcher.shortcut(
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启默认群被动\s*(?P<name>.+)",
|
||||
command="switch",
|
||||
arguments=["open", "{name}", "--task", "-df"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"关闭默认群被动\s*(?P<name>.+)",
|
||||
command="switch",
|
||||
arguments=["close", "{name}", "--task", "-df"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
_status_matcher.shortcut(
|
||||
r"开启(所有|全部)群被动",
|
||||
|
||||
@ -11,7 +11,7 @@ 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
|
||||
@ -22,7 +22,7 @@ from zhenxun.utils.manager.resource_manager import (
|
||||
)
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import UpdateManage
|
||||
from ._data_source import UpdateManager
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="自动更新",
|
||||
@ -32,16 +32,18 @@ __plugin_meta__ = PluginMetadata(
|
||||
检查更新真寻最新版本,包括了自动更新
|
||||
资源文件大小一般在130mb左右,除非必须更新一般仅更新代码文件
|
||||
指令:
|
||||
检查更新 [main|release|resource] ?[-r]
|
||||
检查更新 [main|release|resource|webui] ?[-r]
|
||||
main: main分支
|
||||
release: 最新release
|
||||
resource: 资源文件
|
||||
webui: webui文件
|
||||
-r: 下载资源文件,一般在更新main或release时使用
|
||||
示例:
|
||||
检查更新 main
|
||||
检查更新 main -r
|
||||
检查更新 release -r
|
||||
检查更新 resource
|
||||
检查更新 webui
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
@ -53,7 +55,7 @@ __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="下载资源文件"),
|
||||
),
|
||||
priority=1,
|
||||
@ -66,23 +68,24 @@ _matcher = on_alconna(
|
||||
@_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
session: Uninfo,
|
||||
ver_type: Match[str],
|
||||
resource: Query[bool] = Query("resource", False),
|
||||
):
|
||||
if not session.id1:
|
||||
await MessageUtils.build_message("用户id为空...").finish()
|
||||
result = ""
|
||||
await MessageUtils.build_message("正在进行检查更新...").send(reply_to=True)
|
||||
if ver_type.result in {"main", "release"}:
|
||||
if not ver_type.available:
|
||||
result = await UpdateManage.check_version()
|
||||
result = await UpdateManager.check_version()
|
||||
logger.info("查看当前版本...", "检查更新", session=session)
|
||||
await MessageUtils.build_message(result).finish()
|
||||
try:
|
||||
result = await UpdateManage.update(bot, session.id1, ver_type.result)
|
||||
result = await UpdateManager.update(bot, session.user.id, ver_type.result)
|
||||
except Exception as e:
|
||||
logger.error("版本更新失败...", "检查更新", session=session, e=e)
|
||||
await MessageUtils.build_message(f"更新版本失败...e: {e}").finish()
|
||||
elif ver_type.result == "webui":
|
||||
result = await UpdateManager.update_webui()
|
||||
if resource.result or ver_type.result == "resource":
|
||||
try:
|
||||
await ResourceManager.init_resources(True)
|
||||
|
||||
@ -7,6 +7,7 @@ import zipfile
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.utils import run_sync
|
||||
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.github_utils import GithubUtils
|
||||
from zhenxun.utils.github_utils.models import RepoInfo
|
||||
@ -17,6 +18,7 @@ from .config import (
|
||||
BACKUP_PATH,
|
||||
BASE_PATH,
|
||||
BASE_PATH_STRING,
|
||||
COMMAND,
|
||||
DEFAULT_GITHUB_URL,
|
||||
DOWNLOAD_GZ_FILE,
|
||||
DOWNLOAD_ZIP_FILE,
|
||||
@ -38,7 +40,7 @@ def install_requirement():
|
||||
|
||||
if not requirement_path.exists():
|
||||
logger.debug(
|
||||
f"没有找到zhenxun的requirement.txt,目标路径为{requirement_path}", "插件管理"
|
||||
f"没有找到zhenxun的requirement.txt,目标路径为{requirement_path}", COMMAND
|
||||
)
|
||||
return
|
||||
try:
|
||||
@ -48,9 +50,9 @@ def install_requirement():
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
logger.debug(f"成功安装真寻依赖,日志:\n{result.stdout}", "插件管理")
|
||||
logger.debug(f"成功安装真寻依赖,日志:\n{result.stdout}", COMMAND)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"安装真寻依赖失败,错误:\n{e.stderr}", "插件管理", e=e)
|
||||
logger.error(f"安装真寻依赖失败,错误:\n{e.stderr}", COMMAND, e=e)
|
||||
|
||||
|
||||
@run_sync
|
||||
@ -61,7 +63,7 @@ def _file_handle(latest_version: str | None):
|
||||
latest_version: 版本号
|
||||
"""
|
||||
BACKUP_PATH.mkdir(exist_ok=True, parents=True)
|
||||
logger.debug("开始解压文件压缩包...", "检查更新")
|
||||
logger.debug("开始解压文件压缩包...", COMMAND)
|
||||
download_file = DOWNLOAD_GZ_FILE
|
||||
if DOWNLOAD_GZ_FILE.exists():
|
||||
tf = tarfile.open(DOWNLOAD_GZ_FILE)
|
||||
@ -69,7 +71,7 @@ def _file_handle(latest_version: str | None):
|
||||
download_file = DOWNLOAD_ZIP_FILE
|
||||
tf = zipfile.ZipFile(DOWNLOAD_ZIP_FILE)
|
||||
tf.extractall(TMP_PATH)
|
||||
logger.debug("解压文件压缩包完成...", "检查更新")
|
||||
logger.debug("解压文件压缩包完成...", COMMAND)
|
||||
download_file_path = TMP_PATH / next(
|
||||
x for x in os.listdir(TMP_PATH) if (TMP_PATH / x).is_dir()
|
||||
)
|
||||
@ -79,52 +81,52 @@ def _file_handle(latest_version: str | None):
|
||||
extract_path = download_file_path / BASE_PATH_STRING
|
||||
target_path = BASE_PATH
|
||||
if PYPROJECT_FILE.exists():
|
||||
logger.debug(f"移除备份文件: {PYPROJECT_FILE}", "检查更新")
|
||||
logger.debug(f"移除备份文件: {PYPROJECT_FILE}", COMMAND)
|
||||
shutil.move(PYPROJECT_FILE, BACKUP_PATH / PYPROJECT_FILE_STRING)
|
||||
if PYPROJECT_LOCK_FILE.exists():
|
||||
logger.debug(f"移除备份文件: {PYPROJECT_LOCK_FILE}", "检查更新")
|
||||
logger.debug(f"移除备份文件: {PYPROJECT_LOCK_FILE}", COMMAND)
|
||||
shutil.move(PYPROJECT_LOCK_FILE, BACKUP_PATH / PYPROJECT_LOCK_FILE_STRING)
|
||||
if REQ_TXT_FILE.exists():
|
||||
logger.debug(f"移除备份文件: {REQ_TXT_FILE}", "检查更新")
|
||||
logger.debug(f"移除备份文件: {REQ_TXT_FILE}", COMMAND)
|
||||
shutil.move(REQ_TXT_FILE, BACKUP_PATH / REQ_TXT_FILE_STRING)
|
||||
if _pyproject.exists():
|
||||
logger.debug("移动文件: pyproject.toml", "检查更新")
|
||||
logger.debug("移动文件: pyproject.toml", COMMAND)
|
||||
shutil.move(_pyproject, PYPROJECT_FILE)
|
||||
if _lock_file.exists():
|
||||
logger.debug("移动文件: poetry.lock", "检查更新")
|
||||
logger.debug("移动文件: poetry.lock", COMMAND)
|
||||
shutil.move(_lock_file, PYPROJECT_LOCK_FILE)
|
||||
if _req_file.exists():
|
||||
logger.debug("移动文件: requirements.txt", "检查更新")
|
||||
logger.debug("移动文件: requirements.txt", COMMAND)
|
||||
shutil.move(_req_file, REQ_TXT_FILE)
|
||||
for folder in REPLACE_FOLDERS:
|
||||
"""移动指定文件夹"""
|
||||
_dir = BASE_PATH / folder
|
||||
_backup_dir = BACKUP_PATH / folder
|
||||
if _backup_dir.exists():
|
||||
logger.debug(f"删除备份文件夹 {_backup_dir}", "检查更新")
|
||||
logger.debug(f"删除备份文件夹 {_backup_dir}", COMMAND)
|
||||
shutil.rmtree(_backup_dir)
|
||||
if _dir.exists():
|
||||
logger.debug(f"移动旧文件夹 {_dir}", "检查更新")
|
||||
logger.debug(f"移动旧文件夹 {_dir}", COMMAND)
|
||||
shutil.move(_dir, _backup_dir)
|
||||
else:
|
||||
logger.warning(f"文件夹 {_dir} 不存在,跳过删除", "检查更新")
|
||||
logger.warning(f"文件夹 {_dir} 不存在,跳过删除", COMMAND)
|
||||
for folder in REPLACE_FOLDERS:
|
||||
src_folder_path = extract_path / folder
|
||||
dest_folder_path = target_path / folder
|
||||
if src_folder_path.exists():
|
||||
logger.debug(
|
||||
f"移动文件夹: {src_folder_path} -> {dest_folder_path}", "检查更新"
|
||||
f"移动文件夹: {src_folder_path} -> {dest_folder_path}", COMMAND
|
||||
)
|
||||
shutil.move(src_folder_path, dest_folder_path)
|
||||
else:
|
||||
logger.debug(f"源文件夹不存在: {src_folder_path}", "检查更新")
|
||||
logger.debug(f"源文件夹不存在: {src_folder_path}", COMMAND)
|
||||
if tf:
|
||||
tf.close()
|
||||
if download_file.exists():
|
||||
logger.debug(f"删除下载文件: {download_file}", "检查更新")
|
||||
logger.debug(f"删除下载文件: {download_file}", COMMAND)
|
||||
download_file.unlink()
|
||||
if extract_path.exists():
|
||||
logger.debug(f"删除解压文件夹: {extract_path}", "检查更新")
|
||||
logger.debug(f"删除解压文件夹: {extract_path}", COMMAND)
|
||||
shutil.rmtree(extract_path)
|
||||
if TMP_PATH.exists():
|
||||
shutil.rmtree(TMP_PATH)
|
||||
@ -134,7 +136,35 @@ def _file_handle(latest_version: str | None):
|
||||
install_requirement()
|
||||
|
||||
|
||||
class UpdateManage:
|
||||
class UpdateManager:
|
||||
@classmethod
|
||||
async def update_webui(cls) -> str:
|
||||
from zhenxun.builtin_plugins.web_ui.public.data_source import (
|
||||
update_webui_assets,
|
||||
)
|
||||
|
||||
WEBUI_PATH = DATA_PATH / "web_ui" / "public"
|
||||
BACKUP_PATH = DATA_PATH / "web_ui" / "backup_public"
|
||||
if WEBUI_PATH.exists():
|
||||
if BACKUP_PATH.exists():
|
||||
logger.debug(f"删除旧的备份webui文件夹 {BACKUP_PATH}", COMMAND)
|
||||
shutil.rmtree(BACKUP_PATH)
|
||||
WEBUI_PATH.rename(BACKUP_PATH)
|
||||
try:
|
||||
await update_webui_assets()
|
||||
logger.info("更新webui成功...", COMMAND)
|
||||
if BACKUP_PATH.exists():
|
||||
logger.debug(f"删除旧的webui文件夹 {BACKUP_PATH}", COMMAND)
|
||||
shutil.rmtree(BACKUP_PATH)
|
||||
return "Webui更新成功!"
|
||||
except Exception as e:
|
||||
logger.error("更新webui失败...", COMMAND, e=e)
|
||||
if BACKUP_PATH.exists():
|
||||
logger.debug(f"恢复旧的webui文件夹 {BACKUP_PATH}", COMMAND)
|
||||
BACKUP_PATH.rename(WEBUI_PATH)
|
||||
raise e
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
async def check_version(cls) -> str:
|
||||
"""检查更新版本
|
||||
@ -166,7 +196,7 @@ class UpdateManage:
|
||||
返回:
|
||||
str | None: 返回消息
|
||||
"""
|
||||
logger.info("开始下载真寻最新版文件....", "检查更新")
|
||||
logger.info("开始下载真寻最新版文件....", COMMAND)
|
||||
cur_version = cls.__get_version()
|
||||
url = None
|
||||
new_version = None
|
||||
@ -186,11 +216,11 @@ class UpdateManage:
|
||||
if not url:
|
||||
return "获取版本下载链接失败..."
|
||||
if TMP_PATH.exists():
|
||||
logger.debug(f"删除临时文件夹 {TMP_PATH}", "检查更新")
|
||||
logger.debug(f"删除临时文件夹 {TMP_PATH}", COMMAND)
|
||||
shutil.rmtree(TMP_PATH)
|
||||
logger.debug(
|
||||
f"开始更新版本:{cur_version} -> {new_version} | 下载链接:{url}",
|
||||
"检查更新",
|
||||
COMMAND,
|
||||
)
|
||||
await PlatformUtils.send_superuser(
|
||||
bot,
|
||||
@ -201,7 +231,7 @@ class UpdateManage:
|
||||
DOWNLOAD_GZ_FILE if version_type == "release" else DOWNLOAD_ZIP_FILE
|
||||
)
|
||||
if await AsyncHttpx.download_file(url, download_file, stream=True):
|
||||
logger.debug("下载真寻最新版文件完成...", "检查更新")
|
||||
logger.debug("下载真寻最新版文件完成...", COMMAND)
|
||||
await _file_handle(new_version)
|
||||
result = "版本更新完成"
|
||||
return (
|
||||
@ -210,7 +240,7 @@ class UpdateManage:
|
||||
"请重新启动真寻以完成更新!"
|
||||
)
|
||||
else:
|
||||
logger.debug("下载真寻最新版文件失败...", "检查更新")
|
||||
logger.debug("下载真寻最新版文件失败...", COMMAND)
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
|
||||
@ -34,3 +34,5 @@ REPLACE_FOLDERS = [
|
||||
"models",
|
||||
"configs",
|
||||
]
|
||||
|
||||
COMMAND = "检查更新"
|
||||
|
||||
58
zhenxun/builtin_plugins/bot_profile.py
Normal file
58
zhenxun/builtin_plugins/bot_profile.py
Normal file
@ -0,0 +1,58 @@
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.manager.bot_profile_manager import BotProfileManager
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="自我介绍",
|
||||
description=f"这是{BotConfig.self_nickname}的深情告白",
|
||||
usage="""
|
||||
指令:
|
||||
自我介绍
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
menu_type="其他",
|
||||
superuser_help="""
|
||||
在data/bot_profile/bot_id/profile.txt 中编辑BOT自我介绍
|
||||
在data/bot_profile/bot_id/bot_id.png 中编辑BOT头像
|
||||
指令:
|
||||
重载自我介绍
|
||||
""".strip(),
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
|
||||
_matcher = on_alconna(Alconna("自我介绍"), priority=5, block=True, rule=to_me())
|
||||
|
||||
_reload_matcher = on_alconna(
|
||||
Alconna("重载自我介绍"), priority=1, block=True, permission=SUPERUSER
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: Uninfo, arparma: Arparma):
|
||||
file_path = await BotProfileManager.build_bot_profile_image(session.self_id)
|
||||
if not file_path:
|
||||
await MessageUtils.build_message(
|
||||
f"{BotConfig.self_nickname}当前没有自我简介哦"
|
||||
).finish(reply_to=True)
|
||||
await MessageUtils.build_message(file_path).send()
|
||||
logger.info("BOT自我介绍", arparma.header_result, session=session)
|
||||
|
||||
|
||||
@_reload_matcher.handle()
|
||||
async def _(session: Uninfo, arparma: Arparma):
|
||||
BotProfileManager.clear_profile_image(session.self_id)
|
||||
await MessageUtils.build_message(f"重载{BotConfig.self_nickname}自我介绍成功").send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info("重载BOT自我介绍", arparma.header_result, session=session)
|
||||
@ -1,13 +1,15 @@
|
||||
from nonebot import on_message
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.models.chat_history import ChatHistory
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.utils import get_entity_ids
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="消息存储",
|
||||
@ -37,18 +39,34 @@ def rule(message: UniMsg) -> bool:
|
||||
|
||||
chat_history = on_message(rule=rule, priority=1, block=False)
|
||||
|
||||
TEMP_LIST = []
|
||||
|
||||
|
||||
@chat_history.handle()
|
||||
async def handle_message(message: UniMsg, session: EventSession):
|
||||
"""处理消息存储"""
|
||||
try:
|
||||
await ChatHistory.create(
|
||||
user_id=session.id1,
|
||||
group_id=session.id2,
|
||||
async def _(message: UniMsg, session: Uninfo):
|
||||
entity = get_entity_ids(session)
|
||||
TEMP_LIST.append(
|
||||
ChatHistory(
|
||||
user_id=entity.user_id,
|
||||
group_id=entity.group_id,
|
||||
text=str(message),
|
||||
plain_text=message.extract_plain_text(),
|
||||
bot_id=session.bot_id,
|
||||
bot_id=session.self_id,
|
||||
platform=session.platform,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@scheduler.scheduled_job(
|
||||
"interval",
|
||||
minutes=1,
|
||||
)
|
||||
async def _():
|
||||
try:
|
||||
message_list = TEMP_LIST.copy()
|
||||
TEMP_LIST.clear()
|
||||
if message_list:
|
||||
await ChatHistory.bulk_create(message_list)
|
||||
logger.debug(f"批量添加聊天记录 {len(message_list)} 条", "定时任务")
|
||||
except Exception as e:
|
||||
logger.warning("存储聊天记录失败", "chat_history", e=e)
|
||||
|
||||
@ -18,12 +18,13 @@ from zhenxun.builtin_plugins.help._config import (
|
||||
SIMPLE_DETAIL_HELP_IMAGE,
|
||||
SIMPLE_HELP_IMAGE,
|
||||
)
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import create_help_img, get_plugin_help
|
||||
from ._data_source import create_help_img, get_llm_help, get_plugin_help
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="帮助",
|
||||
@ -47,6 +48,34 @@ __plugin_meta__ = PluginMetadata(
|
||||
help="帮助详情图片样式 ['normal', 'zhenxun']",
|
||||
default_value="zhenxun",
|
||||
),
|
||||
RegisterConfig(
|
||||
key="ENABLE_LLM_HELPER",
|
||||
value=False,
|
||||
help="是否开启LLM智能帮助功能",
|
||||
default_value=False,
|
||||
type=bool,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="DEFAULT_LLM_MODEL",
|
||||
value="Gemini/gemini-2.5-flash-lite-preview-06-17",
|
||||
help="智能帮助功能使用的默认LLM模型",
|
||||
default_value="Gemini/gemini-2.5-flash-lite-preview-06-17",
|
||||
type=str,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="LLM_HELPER_STYLE",
|
||||
value="绪山真寻",
|
||||
help="设置智能帮助功能的回复口吻或风格",
|
||||
default_value="绪山真寻",
|
||||
type=str,
|
||||
),
|
||||
RegisterConfig(
|
||||
key="LLM_HELPER_REPLY_AS_IMAGE_THRESHOLD",
|
||||
value=100,
|
||||
help="AI帮助回复超过多少字时转为图片发送",
|
||||
default_value=100,
|
||||
type=int,
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
)
|
||||
@ -83,20 +112,36 @@ async def _(
|
||||
is_detail: Query[bool] = AlconnaQuery("detail.value", False),
|
||||
):
|
||||
_is_superuser = is_superuser.result if is_superuser.available else False
|
||||
|
||||
if name.available:
|
||||
if _is_superuser and session.user.id not in bot.config.superusers:
|
||||
_is_superuser = False
|
||||
if result := await get_plugin_help(session.user.id, name.result, _is_superuser):
|
||||
await MessageUtils.build_message(result).send(reply_to=True)
|
||||
else:
|
||||
await MessageUtils.build_message("没有此功能的帮助信息...").send(
|
||||
traditional_help_result = await get_plugin_help(
|
||||
session.user.id, name.result, _is_superuser
|
||||
)
|
||||
|
||||
is_plugin_found = not (
|
||||
isinstance(traditional_help_result, str)
|
||||
and "没有查找到这个功能噢..." in traditional_help_result
|
||||
)
|
||||
if is_plugin_found:
|
||||
await MessageUtils.build_message(traditional_help_result).send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info(f"查看帮助详情: {name.result}", "帮助", session=session)
|
||||
logger.info(f"查看帮助详情: {name.result}", "帮助", session=session)
|
||||
elif Config.get_config("help", "ENABLE_LLM_HELPER"):
|
||||
logger.info(f"智能帮助处理问题: {name.result}", "帮助", session=session)
|
||||
llm_answer = await get_llm_help(name.result, session.user.id)
|
||||
await MessageUtils.build_message(llm_answer).send(reply_to=True)
|
||||
else:
|
||||
await MessageUtils.build_message(traditional_help_result).send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info(
|
||||
f"查看帮助详情失败,未找到: {name.result}", "帮助", session=session
|
||||
)
|
||||
elif session.group and (gid := session.group.id):
|
||||
_image_path = GROUP_HELP_PATH / f"{gid}_{is_detail.result}.png"
|
||||
if not _image_path.exists():
|
||||
result = await create_help_img(session, gid, is_detail.result)
|
||||
await create_help_img(session, gid, is_detail.result)
|
||||
await MessageUtils.build_message(_image_path).finish()
|
||||
else:
|
||||
if is_detail.result:
|
||||
@ -104,5 +149,5 @@ async def _(
|
||||
else:
|
||||
_image_path = SIMPLE_HELP_IMAGE
|
||||
if not _image_path.exists():
|
||||
result = await create_help_img(session, None, is_detail.result)
|
||||
await create_help_img(session, None, is_detail.result)
|
||||
await MessageUtils.build_message(_image_path).finish()
|
||||
|
||||
@ -11,9 +11,15 @@ from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.statistics import Statistics
|
||||
from zhenxun.utils._image_template import ImageTemplate
|
||||
from zhenxun.services import (
|
||||
LLMException,
|
||||
LLMMessage,
|
||||
generate,
|
||||
)
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils._image_template import Markdown
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
|
||||
from ._config import (
|
||||
GROUP_HELP_PATH,
|
||||
@ -202,3 +208,89 @@ async def get_plugin_help(user_id: str, name: str, is_superuser: bool) -> str |
|
||||
return await get_normal_help(_plugin.metadata, extra_data, is_superuser)
|
||||
return "糟糕! 该功能没有帮助喔..."
|
||||
return "没有查找到这个功能噢..."
|
||||
|
||||
|
||||
async def get_llm_help(question: str, user_id: str) -> str | bytes:
|
||||
"""
|
||||
使用LLM来回答用户的自然语言求助。
|
||||
|
||||
参数:
|
||||
question: 用户的问题。
|
||||
user_id: 提问用户的ID。
|
||||
|
||||
返回:
|
||||
str | bytes: LLM生成的回答或错误提示。
|
||||
"""
|
||||
|
||||
try:
|
||||
allowed_types = await get_user_allow_help(user_id)
|
||||
|
||||
plugins = await PluginInfo.filter(
|
||||
is_show=True, plugin_type__in=allowed_types
|
||||
).all()
|
||||
|
||||
knowledge_base_parts = []
|
||||
for p in plugins:
|
||||
meta = nonebot.get_plugin_by_module_name(p.module_path)
|
||||
if not meta or not meta.metadata:
|
||||
continue
|
||||
usage = meta.metadata.usage.strip() or "无"
|
||||
desc = meta.metadata.description.strip() or "无"
|
||||
part = f"功能名称: {p.name}\n功能描述: {desc}\n用法示例:\n{usage}"
|
||||
knowledge_base_parts.append(part)
|
||||
|
||||
if not knowledge_base_parts:
|
||||
return "抱歉,根据您的权限,当前没有可供查询的功能信息。"
|
||||
|
||||
knowledge_base = "\n\n---\n\n".join(knowledge_base_parts)
|
||||
|
||||
user_role = "普通用户"
|
||||
if PluginType.SUPERUSER in allowed_types:
|
||||
user_role = "超级管理员"
|
||||
elif PluginType.ADMIN in allowed_types:
|
||||
user_role = "管理员"
|
||||
|
||||
base_system_prompt = (
|
||||
f"你是一个精通机器人功能的AI助手。当前向你提问的用户是一位「{user_role}」。\n"
|
||||
"你的任务是根据下面提供的功能列表和详细说明,来回答用户关于如何使用机器人的问题。\n"
|
||||
"请仔细阅读每个功能的描述和用法,然后用简洁、清晰的语言告诉用户应该使用哪个或哪些命令来解决他们的问题。\n"
|
||||
"如果找不到完全匹配的功能,可以推荐最相关的一个或几个。直接给出操作指令和简要解释即可。"
|
||||
)
|
||||
|
||||
if (
|
||||
Config.get_config("help", "LLM_HELPER_STYLE")
|
||||
and Config.get_config("help", "LLM_HELPER_STYLE").strip()
|
||||
):
|
||||
style = Config.get_config("help", "LLM_HELPER_STYLE")
|
||||
style_instruction = f"请务必使用「{style}」的风格和口吻来回答。"
|
||||
system_prompt = f"{base_system_prompt}\n{style_instruction}"
|
||||
else:
|
||||
system_prompt = base_system_prompt
|
||||
|
||||
full_instruction = (
|
||||
f"{system_prompt}\n\n=== 功能列表和说明 ===\n{knowledge_base}"
|
||||
)
|
||||
|
||||
messages = [
|
||||
LLMMessage.system(full_instruction),
|
||||
LLMMessage.user(question),
|
||||
]
|
||||
response = await generate(
|
||||
messages=messages,
|
||||
model=Config.get_config("help", "DEFAULT_LLM_MODEL"),
|
||||
)
|
||||
|
||||
reply_text = response.text if response else "抱歉,我暂时无法回答这个问题。"
|
||||
threshold = Config.get_config("help", "LLM_HELPER_REPLY_AS_IMAGE_THRESHOLD", 50)
|
||||
if len(reply_text) > threshold:
|
||||
markdown = Markdown()
|
||||
markdown.text(reply_text)
|
||||
return await markdown.build()
|
||||
return reply_text
|
||||
|
||||
except LLMException as e:
|
||||
logger.error(f"LLM智能帮助出错: {e}", "帮助", e=e)
|
||||
return "抱歉,智能帮助功能当前不可用,请稍后再试或联系管理员。"
|
||||
except Exception as e:
|
||||
logger.error(f"构建LLM帮助时发生未知错误: {e}", "帮助", e=e)
|
||||
return "抱歉,智能帮助功能遇到了一点小问题,正在紧急处理中!"
|
||||
|
||||
@ -45,11 +45,13 @@ async def classify_plugin(
|
||||
"""
|
||||
sort_data = await sort_type()
|
||||
classify: dict[str, list] = {}
|
||||
group = await GroupConsole.get_or_none(group_id=group_id) if group_id else None
|
||||
group = await GroupConsole.get_group(group_id=group_id) if group_id else None
|
||||
bot = await BotConsole.get_or_none(bot_id=session.self_id)
|
||||
for menu, value in sort_data.items():
|
||||
for plugin in value:
|
||||
if not classify.get(menu):
|
||||
classify[menu] = []
|
||||
classify[menu].append(handle(bot, plugin, group, is_detail))
|
||||
for value in classify.values():
|
||||
value.sort(key=lambda x: x.id)
|
||||
return classify
|
||||
|
||||
@ -21,6 +21,8 @@ class Item(BaseModel):
|
||||
"""插件名称"""
|
||||
sta: int
|
||||
"""插件状态"""
|
||||
id: int
|
||||
"""插件id"""
|
||||
|
||||
|
||||
class PluginList(BaseModel):
|
||||
@ -80,10 +82,9 @@ def __handle_item(
|
||||
sta = 2
|
||||
if f"{plugin.module}," in group.block_plugin:
|
||||
sta = 1
|
||||
if bot:
|
||||
if f"{plugin.module}," in bot.block_plugins:
|
||||
sta = 2
|
||||
return Item(plugin_name=plugin.name, sta=sta)
|
||||
if bot and f"{plugin.module}," in bot.block_plugins:
|
||||
sta = 2
|
||||
return Item(plugin_name=plugin.name, sta=sta, id=plugin.id)
|
||||
|
||||
|
||||
def build_plugin_data(classify: dict[str, list[Item]]) -> list[dict[str, str]]:
|
||||
@ -142,7 +143,7 @@ async def build_html_image(
|
||||
template_name="zhenxun_menu.html",
|
||||
templates={"plugin_list": plugin_list},
|
||||
pages={
|
||||
"viewport": {"width": 1903, "height": 975},
|
||||
"viewport": {"width": 1903, "height": 10},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
|
||||
@ -45,7 +45,7 @@ async def build_normal_image(group_id: str | None, is_detail: bool) -> BuildImag
|
||||
color="black" if idx % 2 else "white",
|
||||
)
|
||||
curr_h = 10
|
||||
group = await GroupConsole.get_or_none(group_id=group_id)
|
||||
group = await GroupConsole.get_group(group_id=group_id) if group_id else None
|
||||
for _, plugin in enumerate(plugin_list):
|
||||
text_color = (255, 255, 255) if idx % 2 else (0, 0, 0)
|
||||
if group and f"{plugin.module}," in group.block_plugin:
|
||||
@ -80,7 +80,7 @@ async def build_normal_image(group_id: str | None, is_detail: bool) -> BuildImag
|
||||
width, height = 10, 10
|
||||
for s in [
|
||||
"目前支持的功能列表:",
|
||||
"可以通过 ‘帮助 [功能名称或功能Id]’ 来获取对应功能的使用方法",
|
||||
"可以通过 '帮助 [功能名称或功能Id]' 来获取对应功能的使用方法",
|
||||
]:
|
||||
text = await BuildImage.build_text_image(s, "HYWenHei-85W.ttf", 24)
|
||||
await result.paste(text, (width, height))
|
||||
|
||||
@ -20,6 +20,12 @@ class Item(BaseModel):
|
||||
"""插件名称"""
|
||||
commands: list[str]
|
||||
"""插件命令"""
|
||||
id: str
|
||||
"""插件id"""
|
||||
status: bool
|
||||
"""插件状态"""
|
||||
has_superuser_help: bool
|
||||
"""插件是否拥有超级用户帮助"""
|
||||
|
||||
|
||||
def __handle_item(
|
||||
@ -39,23 +45,36 @@ def __handle_item(
|
||||
返回:
|
||||
Item: Item
|
||||
"""
|
||||
status = True
|
||||
has_superuser_help = False
|
||||
nb_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
|
||||
if nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
|
||||
extra_data = PluginExtraData(**nb_plugin.metadata.extra)
|
||||
if extra_data.superuser_help:
|
||||
has_superuser_help = True
|
||||
if not plugin.status:
|
||||
if plugin.block_type == BlockType.ALL:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
status = False
|
||||
elif group and plugin.block_type == BlockType.GROUP:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
status = False
|
||||
elif not group and plugin.block_type == BlockType.PRIVATE:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
status = False
|
||||
elif group and f"{plugin.module}," in group.block_plugin:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
status = False
|
||||
elif bot and f"{plugin.module}," in bot.block_plugins:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
status = False
|
||||
commands = []
|
||||
nb_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
|
||||
if is_detail and nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
|
||||
extra_data = PluginExtraData(**nb_plugin.metadata.extra)
|
||||
commands = [cmd.command for cmd in extra_data.commands]
|
||||
return Item(plugin_name=f"{plugin.id}-{plugin.name}", commands=commands)
|
||||
return Item(
|
||||
plugin_name=plugin.name,
|
||||
commands=commands,
|
||||
id=str(plugin.id),
|
||||
status=status,
|
||||
has_superuser_help=has_superuser_help,
|
||||
)
|
||||
|
||||
|
||||
def build_plugin_data(classify: dict[str, list[Item]]) -> list[dict[str, str]]:
|
||||
@ -78,68 +97,10 @@ def build_plugin_data(classify: dict[str, list[Item]]) -> list[dict[str, str]]:
|
||||
}
|
||||
for menu, value in classify.items()
|
||||
]
|
||||
plugin_list = build_line_data(plugin_list)
|
||||
plugin_list.insert(
|
||||
0,
|
||||
build_plugin_line(
|
||||
menu_key if menu_key not in ["normal", "功能"] else "主要功能",
|
||||
max_data,
|
||||
30,
|
||||
100,
|
||||
True,
|
||||
),
|
||||
)
|
||||
return plugin_list
|
||||
|
||||
|
||||
def build_plugin_line(
|
||||
name: str, items: list, left: int, width: int | None = None, is_max: bool = False
|
||||
) -> dict:
|
||||
"""构造插件行数据
|
||||
|
||||
参数:
|
||||
name: 菜单名称
|
||||
items: 插件名称列表
|
||||
left: 左边距
|
||||
width: 总插件长度.
|
||||
is_max: 是否为最大长度的插件菜单
|
||||
|
||||
返回:
|
||||
dict: 插件数据
|
||||
"""
|
||||
_plugins = []
|
||||
width = width or 50
|
||||
if len(items) // 2 > 6 or is_max:
|
||||
width = 100
|
||||
plugin_list1 = []
|
||||
plugin_list2 = []
|
||||
for i in range(len(items)):
|
||||
if i % 2:
|
||||
plugin_list1.append(items[i])
|
||||
else:
|
||||
plugin_list2.append(items[i])
|
||||
_plugins = [(30, 50, plugin_list1), (0, 50, plugin_list2)]
|
||||
else:
|
||||
_plugins = [(left, 100, items)]
|
||||
return {"name": name, "items": _plugins, "width": width}
|
||||
|
||||
|
||||
def build_line_data(plugin_list: list[dict]) -> list[dict]:
|
||||
"""构造插件数据
|
||||
|
||||
参数:
|
||||
plugin_list: 插件列表
|
||||
|
||||
返回:
|
||||
list[dict]: 插件数据
|
||||
"""
|
||||
left = 30
|
||||
data = []
|
||||
plugin_list.insert(0, {"name": menu_key, "items": max_data})
|
||||
for plugin in plugin_list:
|
||||
data.append(build_plugin_line(plugin["name"], plugin["items"], left))
|
||||
if len(plugin["items"]) // 2 <= 6:
|
||||
left = 15 if left == 30 else 30
|
||||
return data
|
||||
plugin["items"].sort(key=lambda x: x.id)
|
||||
return plugin_list
|
||||
|
||||
|
||||
async def build_zhenxun_image(
|
||||
@ -160,6 +121,7 @@ async def build_zhenxun_image(
|
||||
width = int(637 * 1.5) if is_detail else 637
|
||||
title_font = int(53 * 1.5) if is_detail else 53
|
||||
tip_font = int(19 * 1.5) if is_detail else 19
|
||||
plugin_count = sum(len(plugin["items"]) for plugin in plugin_list)
|
||||
return await template_to_pic(
|
||||
template_path=str((TEMPLATE_PATH / "ss_menu").absolute()),
|
||||
template_name="main.html",
|
||||
@ -170,10 +132,11 @@ async def build_zhenxun_image(
|
||||
"width": width,
|
||||
"font_size": (title_font, tip_font),
|
||||
"is_detail": is_detail,
|
||||
"plugin_count": plugin_count,
|
||||
}
|
||||
},
|
||||
pages={
|
||||
"viewport": {"width": width, "height": 453},
|
||||
"viewport": {"width": width, "height": 10},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
|
||||
@ -1,597 +0,0 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.adapters.onebot.v11 import PokeNotifyEvent
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot_plugin_alconna import At, UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
from pydantic import BaseModel
|
||||
from tortoise.exceptions import IntegrityError
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.plugin_limit import PluginLimit
|
||||
from zhenxun.models.sign_user import SignUser
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import (
|
||||
BlockType,
|
||||
GoldHandle,
|
||||
LimitWatchType,
|
||||
PluginLimitType,
|
||||
PluginType,
|
||||
)
|
||||
from zhenxun.utils.exception import InsufficientGold
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.utils import CountLimiter, FreqLimiter, UserBlockLimiter
|
||||
|
||||
base_config = Config.get("hook")
|
||||
|
||||
|
||||
class Limit(BaseModel):
|
||||
limit: PluginLimit
|
||||
limiter: FreqLimiter | UserBlockLimiter | CountLimiter
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class LimitManage:
|
||||
add_module: ClassVar[list] = []
|
||||
|
||||
cd_limit: ClassVar[dict[str, Limit]] = {}
|
||||
block_limit: ClassVar[dict[str, Limit]] = {}
|
||||
count_limit: ClassVar[dict[str, Limit]] = {}
|
||||
|
||||
@classmethod
|
||||
def add_limit(cls, limit: PluginLimit):
|
||||
"""添加限制
|
||||
|
||||
参数:
|
||||
limit: PluginLimit
|
||||
"""
|
||||
if limit.module not in cls.add_module:
|
||||
cls.add_module.append(limit.module)
|
||||
if limit.limit_type == PluginLimitType.BLOCK:
|
||||
cls.block_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=UserBlockLimiter()
|
||||
)
|
||||
elif limit.limit_type == PluginLimitType.CD:
|
||||
cls.cd_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=FreqLimiter(limit.cd)
|
||||
)
|
||||
elif limit.limit_type == PluginLimitType.COUNT:
|
||||
cls.count_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=CountLimiter(limit.max_count)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def unblock(
|
||||
cls, module: str, user_id: str, group_id: str | None, channel_id: str | None
|
||||
):
|
||||
"""解除插件block
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
"""
|
||||
if limit_model := cls.block_limit.get(module):
|
||||
limit = limit_model.limit
|
||||
limiter: UserBlockLimiter = limit_model.limiter # type: ignore
|
||||
key_type = user_id
|
||||
if group_id and limit.watch_type == LimitWatchType.GROUP:
|
||||
key_type = channel_id or group_id
|
||||
logger.debug(
|
||||
f"解除对象: {key_type} 的block限制",
|
||||
"AuthChecker",
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
limiter.set_false(key_type)
|
||||
|
||||
@classmethod
|
||||
async def check(
|
||||
cls,
|
||||
module: str,
|
||||
user_id: str,
|
||||
group_id: str | None,
|
||||
channel_id: str | None,
|
||||
session: EventSession,
|
||||
):
|
||||
"""检测限制
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
session: Session
|
||||
|
||||
异常:
|
||||
IgnoredException: IgnoredException
|
||||
"""
|
||||
if limit_model := cls.cd_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id, session)
|
||||
if limit_model := cls.block_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id, session)
|
||||
if limit_model := cls.count_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id, session)
|
||||
|
||||
@classmethod
|
||||
async def __check(
|
||||
cls,
|
||||
limit_model: Limit | None,
|
||||
user_id: str,
|
||||
group_id: str | None,
|
||||
channel_id: str | None,
|
||||
session: EventSession,
|
||||
):
|
||||
"""检测限制
|
||||
|
||||
参数:
|
||||
limit_model: Limit
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
session: Session
|
||||
|
||||
异常:
|
||||
IgnoredException: IgnoredException
|
||||
"""
|
||||
if not limit_model:
|
||||
return
|
||||
limit = limit_model.limit
|
||||
limiter = limit_model.limiter
|
||||
is_limit = (
|
||||
LimitWatchType.ALL
|
||||
or (group_id and limit.watch_type == LimitWatchType.GROUP)
|
||||
or (not group_id and limit.watch_type == LimitWatchType.USER)
|
||||
)
|
||||
key_type = user_id
|
||||
if group_id and limit.watch_type == LimitWatchType.GROUP:
|
||||
key_type = channel_id or group_id
|
||||
if is_limit and not limiter.check(key_type):
|
||||
if limit.result:
|
||||
await MessageUtils.build_message(limit.result).send()
|
||||
logger.debug(
|
||||
f"{limit.module}({limit.limit_type}) 正在限制中...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException(f"{limit.module} 正在限制中...")
|
||||
else:
|
||||
logger.debug(
|
||||
f"开始进行限制 {limit.module}({limit.limit_type})...",
|
||||
"AuthChecker",
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
if isinstance(limiter, FreqLimiter):
|
||||
limiter.start_cd(key_type)
|
||||
if isinstance(limiter, UserBlockLimiter):
|
||||
limiter.set_true(key_type)
|
||||
if isinstance(limiter, CountLimiter):
|
||||
limiter.increase(key_type)
|
||||
|
||||
|
||||
class IsSuperuserException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AuthChecker:
|
||||
"""
|
||||
权限检查
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
check_notice_info_cd = Config.get_config("hook", "CHECK_NOTICE_INFO_CD")
|
||||
if check_notice_info_cd is None or check_notice_info_cd < 0:
|
||||
raise ValueError("模块: [hook], 配置项: [CHECK_NOTICE_INFO_CD] 为空或小于0")
|
||||
self._flmt = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_g = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_s = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_c = FreqLimiter(check_notice_info_cd)
|
||||
|
||||
def is_send_limit_message(self, plugin: PluginInfo, sid: str) -> bool:
|
||||
"""是否发送提示消息
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
|
||||
返回:
|
||||
bool: 是否发送提示消息
|
||||
"""
|
||||
if not base_config.get("IS_SEND_TIP_MESSAGE"):
|
||||
return False
|
||||
if plugin.plugin_type == PluginType.DEPENDANT:
|
||||
return False
|
||||
if plugin.ignore_prompt:
|
||||
return False
|
||||
return self._flmt_s.check(sid)
|
||||
|
||||
async def auth(
|
||||
self,
|
||||
matcher: Matcher,
|
||||
event: Event,
|
||||
bot: Bot,
|
||||
session: EventSession,
|
||||
message: UniMsg,
|
||||
):
|
||||
"""权限检查
|
||||
|
||||
参数:
|
||||
matcher: matcher
|
||||
bot: bot
|
||||
session: EventSession
|
||||
message: UniMsg
|
||||
"""
|
||||
is_ignore = False
|
||||
cost_gold = 0
|
||||
user_id = session.id1
|
||||
group_id = session.id3
|
||||
channel_id = session.id2
|
||||
if not group_id:
|
||||
group_id = channel_id
|
||||
channel_id = None
|
||||
if matcher.type == "notice" and not isinstance(event, PokeNotifyEvent):
|
||||
"""过滤除poke外的notice"""
|
||||
return
|
||||
if user_id and matcher.plugin and (module_path := matcher.plugin.module_name):
|
||||
try:
|
||||
user = await UserConsole.get_user(user_id, session.platform)
|
||||
except IntegrityError as e:
|
||||
logger.debug(
|
||||
"重复创建用户,已跳过该次权限...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
return
|
||||
if plugin := await PluginInfo.get_or_none(module_path=module_path):
|
||||
if plugin.plugin_type == PluginType.HIDDEN:
|
||||
logger.debug(
|
||||
f"插件: {plugin.name}:{plugin.module} "
|
||||
"为HIDDEN,已跳过权限检查..."
|
||||
)
|
||||
return
|
||||
try:
|
||||
cost_gold = await self.auth_cost(user, plugin, session)
|
||||
if session.id1 in bot.config.superusers:
|
||||
if plugin.plugin_type == PluginType.SUPERUSER:
|
||||
raise IsSuperuserException()
|
||||
if not plugin.limit_superuser:
|
||||
cost_gold = 0
|
||||
raise IsSuperuserException()
|
||||
await self.auth_bot(plugin, bot.self_id)
|
||||
await self.auth_group(plugin, session, message)
|
||||
await self.auth_admin(plugin, session)
|
||||
await self.auth_plugin(plugin, session, event)
|
||||
await self.auth_limit(plugin, session)
|
||||
except IsSuperuserException:
|
||||
logger.debug(
|
||||
"超级用户或被ban跳过权限检测...", "AuthChecker", session=session
|
||||
)
|
||||
except IgnoredException:
|
||||
is_ignore = True
|
||||
LimitManage.unblock(
|
||||
matcher.plugin.name, user_id, group_id, channel_id
|
||||
)
|
||||
except AssertionError as e:
|
||||
is_ignore = True
|
||||
logger.debug("消息无法发送", session=session, e=e)
|
||||
if cost_gold and user_id:
|
||||
"""花费金币"""
|
||||
try:
|
||||
await UserConsole.reduce_gold(
|
||||
user_id,
|
||||
cost_gold,
|
||||
GoldHandle.PLUGIN,
|
||||
matcher.plugin.name if matcher.plugin else "",
|
||||
session.platform,
|
||||
)
|
||||
except InsufficientGold:
|
||||
if u := await UserConsole.get_user(user_id):
|
||||
u.gold = 0
|
||||
await u.save(update_fields=["gold"])
|
||||
logger.debug(
|
||||
f"调用功能花费金币: {cost_gold}", "AuthChecker", session=session
|
||||
)
|
||||
if is_ignore:
|
||||
raise IgnoredException("权限检测 ignore")
|
||||
|
||||
async def auth_bot(self, plugin: PluginInfo, bot_id: str):
|
||||
"""机器人权限
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
bot_id: bot_id
|
||||
"""
|
||||
if not await BotConsole.get_bot_status(bot_id):
|
||||
logger.debug("Bot休眠中阻断权限检测...", "AuthChecker")
|
||||
raise IgnoredException("BotConsole休眠权限检测 ignore")
|
||||
if await BotConsole.is_block_plugin(bot_id, plugin.module):
|
||||
logger.debug(
|
||||
f"Bot插件 {plugin.name}({plugin.module}) 权限检查结果为关闭...",
|
||||
"AuthChecker",
|
||||
)
|
||||
raise IgnoredException("BotConsole插件权限检测 ignore")
|
||||
|
||||
async def auth_limit(self, plugin: PluginInfo, session: EventSession):
|
||||
"""插件限制
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: EventSession
|
||||
"""
|
||||
user_id = session.id1
|
||||
group_id = session.id3
|
||||
channel_id = session.id2
|
||||
if not group_id:
|
||||
group_id = channel_id
|
||||
channel_id = None
|
||||
if plugin.module not in LimitManage.add_module:
|
||||
limit_list: list[PluginLimit] = await plugin.plugin_limit.filter(
|
||||
status=True
|
||||
).all() # type: ignore
|
||||
for limit in limit_list:
|
||||
LimitManage.add_limit(limit)
|
||||
if user_id:
|
||||
await LimitManage.check(
|
||||
plugin.module, user_id, group_id, channel_id, session
|
||||
)
|
||||
|
||||
async def auth_plugin(
|
||||
self, plugin: PluginInfo, session: EventSession, event: Event
|
||||
):
|
||||
"""插件状态
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: EventSession
|
||||
"""
|
||||
group_id = session.id3
|
||||
channel_id = session.id2
|
||||
if not group_id:
|
||||
group_id = channel_id
|
||||
channel_id = None
|
||||
if user_id := session.id1:
|
||||
if plugin.impression > 0:
|
||||
sign_user = await SignUser.get_user(user_id)
|
||||
if float(sign_user.impression) < plugin.impression:
|
||||
if self.is_send_limit_message(plugin, user_id):
|
||||
self._flmt_s.start_cd(user_id)
|
||||
await MessageUtils.build_message(
|
||||
f"好感度不足哦,当前功能需要好感度: {plugin.impression},"
|
||||
"请继续签到提升好感度吧!"
|
||||
).send(reply_to=True)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 用户好感度不足...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("好感度不足...")
|
||||
if group_id:
|
||||
sid = group_id or user_id
|
||||
if await GroupConsole.is_superuser_block_plugin(
|
||||
group_id, plugin.module
|
||||
):
|
||||
"""超级用户群组插件状态"""
|
||||
if self.is_send_limit_message(plugin, sid):
|
||||
self._flmt_s.start_cd(group_id or user_id)
|
||||
await MessageUtils.build_message(
|
||||
"超级管理员禁用了该群此功能..."
|
||||
).send(reply_to=True)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 超级管理员禁用了该群此功能...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("超级管理员禁用了该群此功能...")
|
||||
if await GroupConsole.is_normal_block_plugin(group_id, plugin.module):
|
||||
"""群组插件状态"""
|
||||
if self.is_send_limit_message(plugin, sid):
|
||||
self._flmt_s.start_cd(group_id or user_id)
|
||||
await MessageUtils.build_message("该群未开启此功能...").send(
|
||||
reply_to=True
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 未开启此功能...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("该群未开启此功能...")
|
||||
if plugin.block_type == BlockType.GROUP:
|
||||
"""全局群组禁用"""
|
||||
try:
|
||||
if self.is_send_limit_message(plugin, sid):
|
||||
self._flmt_c.start_cd(group_id)
|
||||
await MessageUtils.build_message(
|
||||
"该功能在群组中已被禁用..."
|
||||
).send(reply_to=True)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"auth_plugin 发送消息失败",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 该插件在群组中已被禁用...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("该插件在群组中已被禁用...")
|
||||
else:
|
||||
sid = user_id
|
||||
if plugin.block_type == BlockType.PRIVATE:
|
||||
"""全局私聊禁用"""
|
||||
try:
|
||||
if self.is_send_limit_message(plugin, sid):
|
||||
self._flmt_c.start_cd(user_id)
|
||||
await MessageUtils.build_message(
|
||||
"该功能在私聊中已被禁用..."
|
||||
).send()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"auth_admin 发送消息失败",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 该插件在私聊中已被禁用...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("该插件在私聊中已被禁用...")
|
||||
if not plugin.status and plugin.block_type == BlockType.ALL:
|
||||
"""全局状态"""
|
||||
if group_id and await GroupConsole.is_super_group(group_id):
|
||||
raise IsSuperuserException()
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 全局未开启此功能...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
if self.is_send_limit_message(plugin, sid):
|
||||
self._flmt_s.start_cd(group_id or user_id)
|
||||
await MessageUtils.build_message("全局未开启此功能...").send()
|
||||
raise IgnoredException("全局未开启此功能...")
|
||||
|
||||
async def auth_admin(self, plugin: PluginInfo, session: EventSession):
|
||||
"""管理员命令 个人权限
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: EventSession
|
||||
"""
|
||||
user_id = session.id1
|
||||
if user_id and plugin.admin_level:
|
||||
if group_id := session.id3 or session.id2:
|
||||
if not await LevelUser.check_level(
|
||||
user_id, group_id, plugin.admin_level
|
||||
):
|
||||
try:
|
||||
if self._flmt.check(user_id):
|
||||
self._flmt.start_cd(user_id)
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
At(flag="user", target=user_id),
|
||||
f"你的权限不足喔,"
|
||||
f"该功能需要的权限等级: {plugin.admin_level}",
|
||||
]
|
||||
).send(reply_to=True)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"auth_admin 发送消息失败",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 管理员权限不足...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("管理员权限不足...")
|
||||
elif not await LevelUser.check_level(user_id, None, plugin.admin_level):
|
||||
try:
|
||||
await MessageUtils.build_message(
|
||||
f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}"
|
||||
).send()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"auth_admin 发送消息失败", "AuthChecker", session=session, e=e
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 管理员权限不足...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("权限不足")
|
||||
|
||||
async def auth_group(
|
||||
self, plugin: PluginInfo, session: EventSession, message: UniMsg
|
||||
):
|
||||
"""群黑名单检测 群总开关检测
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: EventSession
|
||||
message: UniMsg
|
||||
"""
|
||||
if not (group_id := session.id3 or session.id2):
|
||||
return
|
||||
text = message.extract_plain_text()
|
||||
group = await GroupConsole.get_group(group_id)
|
||||
if not group:
|
||||
"""群不存在"""
|
||||
logger.debug(
|
||||
"群组信息不存在...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("群不存在")
|
||||
if group.level < 0:
|
||||
"""群权限小于0"""
|
||||
logger.debug(
|
||||
"群黑名单, 群权限-1...",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException("群黑名单")
|
||||
if not group.status:
|
||||
"""群休眠"""
|
||||
if text.strip() != "醒来":
|
||||
logger.debug("群休眠状态...", "AuthChecker", session=session)
|
||||
raise IgnoredException("群休眠状态")
|
||||
if plugin.level > group.level:
|
||||
"""插件等级大于群等级"""
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 群等级限制.."
|
||||
f"该功能需要的群等级: {plugin.level}..",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException(f"{plugin.name}({plugin.module}) 群等级限制...")
|
||||
|
||||
async def auth_cost(
|
||||
self, user: UserConsole, plugin: PluginInfo, session: EventSession
|
||||
) -> int:
|
||||
"""检测是否满足金币条件
|
||||
|
||||
参数:
|
||||
user: UserConsole
|
||||
plugin: PluginInfo
|
||||
session: EventSession
|
||||
|
||||
返回:
|
||||
int: 需要消耗的金币
|
||||
"""
|
||||
if user.gold < plugin.cost_gold:
|
||||
"""插件消耗金币不足"""
|
||||
try:
|
||||
await MessageUtils.build_message(
|
||||
f"金币不足..该功能需要{plugin.cost_gold}金币.."
|
||||
).send()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"auth_cost 发送消息失败", "AuthChecker", session=session, e=e
|
||||
)
|
||||
logger.debug(
|
||||
f"{plugin.name}({plugin.module}) 金币限制.."
|
||||
f"该功能需要{plugin.cost_gold}金币..",
|
||||
"AuthChecker",
|
||||
session=session,
|
||||
)
|
||||
raise IgnoredException(f"{plugin.name}({plugin.module}) 金币限制...")
|
||||
return plugin.cost_gold
|
||||
|
||||
|
||||
checker = AuthChecker()
|
||||
99
zhenxun/builtin_plugins/hooks/auth/auth_admin.py
Normal file
99
zhenxun/builtin_plugins/hooks/auth/auth_admin.py
Normal file
@ -0,0 +1,99 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from nonebot_plugin_alconna import At
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.data_access import DataAccess
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.utils import get_entity_ids
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import SkipPluginException
|
||||
from .utils import send_message
|
||||
|
||||
|
||||
async def auth_admin(plugin: PluginInfo, session: Uninfo):
|
||||
"""管理员命令 个人权限
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: Uninfo
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
if not plugin.admin_level:
|
||||
return
|
||||
|
||||
try:
|
||||
entity = get_entity_ids(session)
|
||||
level_dao = DataAccess(LevelUser)
|
||||
|
||||
# 并行查询用户权限数据
|
||||
global_user: LevelUser | None = None
|
||||
group_users: LevelUser | None = None
|
||||
|
||||
# 查询全局权限
|
||||
global_user_task = level_dao.safe_get_or_none(
|
||||
user_id=session.user.id, group_id__isnull=True
|
||||
)
|
||||
|
||||
# 如果在群组中,查询群组权限
|
||||
group_users_task = None
|
||||
if entity.group_id:
|
||||
group_users_task = level_dao.safe_get_or_none(
|
||||
user_id=session.user.id, group_id=entity.group_id
|
||||
)
|
||||
|
||||
# 等待查询完成,添加超时控制
|
||||
try:
|
||||
results = await asyncio.wait_for(
|
||||
asyncio.gather(global_user_task, group_users_task or asyncio.sleep(0)),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
global_user = results[0]
|
||||
group_users = results[1] if group_users_task else None
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"查询用户权限超时: user_id={session.user.id}", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
return
|
||||
|
||||
user_level = global_user.user_level if global_user else 0
|
||||
if entity.group_id and group_users:
|
||||
user_level = max(user_level, group_users.user_level)
|
||||
|
||||
if user_level < plugin.admin_level:
|
||||
await send_message(
|
||||
session,
|
||||
[
|
||||
At(flag="user", target=session.user.id),
|
||||
f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}",
|
||||
],
|
||||
entity.user_id,
|
||||
)
|
||||
|
||||
raise SkipPluginException(
|
||||
f"{plugin.name}({plugin.module}) 管理员权限不足..."
|
||||
)
|
||||
elif global_user:
|
||||
if global_user.user_level < plugin.admin_level:
|
||||
await send_message(
|
||||
session,
|
||||
f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}",
|
||||
)
|
||||
|
||||
raise SkipPluginException(
|
||||
f"{plugin.name}({plugin.module}) 管理员权限不足..."
|
||||
)
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_admin 耗时: {elapsed:.3f}s, plugin={plugin.module}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
306
zhenxun/builtin_plugins/hooks/auth/auth_ban.py
Normal file
306
zhenxun/builtin_plugins/hooks/auth/auth_ban.py
Normal file
@ -0,0 +1,306 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot_plugin_alconna import At
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.data_access import DataAccess
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.utils import EntityIDs, get_entity_ids
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import SkipPluginException
|
||||
from .utils import freq, send_message
|
||||
|
||||
Config.add_plugin_config(
|
||||
"hook",
|
||||
"BAN_RESULT",
|
||||
"才不会给你发消息.",
|
||||
help="对被ban用户发送的消息",
|
||||
)
|
||||
|
||||
|
||||
async def calculate_ban_time(ban_record: BanConsole | None) -> int:
|
||||
"""根据ban记录计算剩余ban时间
|
||||
|
||||
参数:
|
||||
ban_record: BanConsole记录
|
||||
|
||||
返回:
|
||||
int: ban剩余时长,-1时为永久ban,0表示未被ban
|
||||
"""
|
||||
if not ban_record:
|
||||
return 0
|
||||
|
||||
if ban_record.duration == -1:
|
||||
return -1
|
||||
|
||||
_time = time.time() - (ban_record.ban_time + ban_record.duration)
|
||||
if _time < 0:
|
||||
return int(abs(_time))
|
||||
await ban_record.delete()
|
||||
return 0
|
||||
|
||||
|
||||
async def is_ban(user_id: str | None, group_id: str | None) -> int:
|
||||
"""检查用户或群组是否被ban
|
||||
|
||||
参数:
|
||||
user_id: 用户ID
|
||||
group_id: 群组ID
|
||||
|
||||
返回:
|
||||
int: ban的剩余时间,0表示未被ban
|
||||
"""
|
||||
if not user_id and not group_id:
|
||||
return 0
|
||||
|
||||
start_time = time.time()
|
||||
ban_dao = DataAccess(BanConsole)
|
||||
|
||||
# 分别获取用户在群组中的ban记录和全局ban记录
|
||||
group_user = None
|
||||
user = None
|
||||
|
||||
try:
|
||||
# 并行查询用户和群组的 ban 记录
|
||||
tasks = []
|
||||
if user_id and group_id:
|
||||
tasks.append(ban_dao.safe_get_or_none(user_id=user_id, group_id=group_id))
|
||||
if user_id:
|
||||
tasks.append(
|
||||
ban_dao.safe_get_or_none(user_id=user_id, group_id__isnull=True)
|
||||
)
|
||||
|
||||
# 等待所有查询完成,添加超时控制
|
||||
if tasks:
|
||||
try:
|
||||
ban_records = await asyncio.wait_for(
|
||||
asyncio.gather(*tasks), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
if len(tasks) == 2:
|
||||
group_user, user = ban_records
|
||||
elif user_id and group_id:
|
||||
group_user = ban_records[0]
|
||||
else:
|
||||
user = ban_records[0]
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"查询ban记录超时: user_id={user_id}, group_id={group_id}",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
# 超时时返回0,避免阻塞
|
||||
return 0
|
||||
|
||||
# 检查记录并计算ban时间
|
||||
results = []
|
||||
if group_user:
|
||||
results.append(group_user)
|
||||
if user:
|
||||
results.append(user)
|
||||
|
||||
# 如果没有找到记录,返回0
|
||||
if not results:
|
||||
return 0
|
||||
|
||||
logger.debug(f"查询到的ban记录: {results}", LOGGER_COMMAND)
|
||||
# 检查所有记录,找出最严格的ban(时间最长的)
|
||||
max_ban_time: int = 0
|
||||
for result in results:
|
||||
if result.duration > 0 or result.duration == -1:
|
||||
# 直接计算ban时间,避免再次查询数据库
|
||||
ban_time = await calculate_ban_time(result)
|
||||
if ban_time == -1 or ban_time > max_ban_time:
|
||||
max_ban_time = ban_time
|
||||
|
||||
return max_ban_time
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"is_ban 耗时: {elapsed:.3f}s",
|
||||
LOGGER_COMMAND,
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
|
||||
|
||||
def check_plugin_type(matcher: Matcher) -> bool:
|
||||
"""判断插件类型是否是隐藏插件
|
||||
|
||||
参数:
|
||||
matcher: Matcher
|
||||
|
||||
返回:
|
||||
bool: 是否为隐藏插件
|
||||
"""
|
||||
if plugin := matcher.plugin:
|
||||
if metadata := plugin.metadata:
|
||||
extra = metadata.extra
|
||||
if extra.get("plugin_type") in [PluginType.HIDDEN]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def format_time(time_val: float) -> str:
|
||||
"""格式化时间
|
||||
|
||||
参数:
|
||||
time_val: ban时长
|
||||
|
||||
返回:
|
||||
str: 格式化时间文本
|
||||
"""
|
||||
if time_val == -1:
|
||||
return "∞"
|
||||
time_val = abs(int(time_val))
|
||||
if time_val < 60:
|
||||
time_str = f"{time_val!s} 秒"
|
||||
else:
|
||||
minute = int(time_val / 60)
|
||||
if minute > 60:
|
||||
hours = minute // 60
|
||||
minute %= 60
|
||||
time_str = f"{hours} 小时 {minute}分钟"
|
||||
else:
|
||||
time_str = f"{minute} 分钟"
|
||||
return time_str
|
||||
|
||||
|
||||
async def group_handle(group_id: str) -> None:
|
||||
"""群组ban检查
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
|
||||
异常:
|
||||
SkipPluginException: 群组处于黑名单
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
if await is_ban(None, group_id):
|
||||
raise SkipPluginException("群组处于黑名单中...")
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"group_handle 耗时: {elapsed:.3f}s",
|
||||
LOGGER_COMMAND,
|
||||
group_id=group_id,
|
||||
)
|
||||
|
||||
|
||||
async def user_handle(module: str, entity: EntityIDs, session: Uninfo) -> None:
|
||||
"""用户ban检查
|
||||
|
||||
参数:
|
||||
module: 插件模块名
|
||||
entity: 实体ID信息
|
||||
session: Uninfo
|
||||
|
||||
异常:
|
||||
SkipPluginException: 用户处于黑名单
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
ban_result = Config.get_config("hook", "BAN_RESULT")
|
||||
time_val = await is_ban(entity.user_id, entity.group_id)
|
||||
if not time_val:
|
||||
return
|
||||
time_str = format_time(time_val)
|
||||
plugin_dao = DataAccess(PluginInfo)
|
||||
try:
|
||||
db_plugin = await asyncio.wait_for(
|
||||
plugin_dao.safe_get_or_none(module=module), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"查询插件信息超时: {module}", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
raise SkipPluginException("用户处于黑名单中...")
|
||||
|
||||
if (
|
||||
db_plugin
|
||||
and not db_plugin.ignore_prompt
|
||||
and time_val != -1
|
||||
and ban_result
|
||||
and freq.is_send_limit_message(db_plugin, entity.user_id, False)
|
||||
):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(
|
||||
session,
|
||||
[
|
||||
At(flag="user", target=entity.user_id),
|
||||
f"{ban_result}\n在..在 {time_str} 后才会理你喔",
|
||||
],
|
||||
entity.user_id,
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送消息超时: {entity.user_id}", LOGGER_COMMAND)
|
||||
raise SkipPluginException("用户处于黑名单中...")
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"user_handle 耗时: {elapsed:.3f}s",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
async def auth_ban(matcher: Matcher, bot: Bot, session: Uninfo) -> None:
|
||||
"""权限检查 - ban 检查
|
||||
|
||||
参数:
|
||||
matcher: Matcher
|
||||
bot: Bot
|
||||
session: Uninfo
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
if not check_plugin_type(matcher):
|
||||
return
|
||||
if not matcher.plugin_name:
|
||||
return
|
||||
entity = get_entity_ids(session)
|
||||
if entity.user_id in bot.config.superusers:
|
||||
return
|
||||
if entity.group_id:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
group_handle(entity.group_id), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"群组ban检查超时: {entity.group_id}", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
|
||||
if entity.user_id:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
user_handle(matcher.plugin_name, entity, session),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"用户ban检查超时: {entity.user_id}", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
finally:
|
||||
# 记录总执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_ban 总耗时: {elapsed:.3f}s, plugin={matcher.plugin_name}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
55
zhenxun/builtin_plugins/hooks/auth/auth_bot.py
Normal file
55
zhenxun/builtin_plugins/hooks/auth/auth_bot.py
Normal file
@ -0,0 +1,55 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.data_access import DataAccess
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import SkipPluginException
|
||||
|
||||
|
||||
async def auth_bot(plugin: PluginInfo, bot_id: str):
|
||||
"""bot层面的权限检查
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
bot_id: bot id
|
||||
|
||||
异常:
|
||||
SkipPluginException: 忽略插件
|
||||
SkipPluginException: 忽略插件
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 从数据库或缓存中获取 bot 信息
|
||||
bot_dao = DataAccess(BotConsole)
|
||||
|
||||
try:
|
||||
bot: BotConsole | None = await asyncio.wait_for(
|
||||
bot_dao.safe_get_or_none(bot_id=bot_id), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"查询Bot信息超时: bot_id={bot_id}", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
return
|
||||
|
||||
if not bot or not bot.status:
|
||||
raise SkipPluginException("Bot不存在或休眠中阻断权限检测...")
|
||||
if CommonUtils.format(plugin.module) in bot.block_plugins:
|
||||
raise SkipPluginException(
|
||||
f"Bot插件 {plugin.name}({plugin.module}) 权限检查结果为关闭..."
|
||||
)
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_bot 耗时: {elapsed:.3f}s, "
|
||||
f"bot_id={bot_id}, plugin={plugin.module}",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
41
zhenxun/builtin_plugins/hooks/auth/auth_cost.py
Normal file
41
zhenxun/builtin_plugins/hooks/auth/auth_cost.py
Normal file
@ -0,0 +1,41 @@
|
||||
import time
|
||||
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import SkipPluginException
|
||||
from .utils import send_message
|
||||
|
||||
|
||||
async def auth_cost(user: UserConsole, plugin: PluginInfo, session: Uninfo) -> int:
|
||||
"""检测是否满足金币条件
|
||||
|
||||
参数:
|
||||
user: UserConsole
|
||||
plugin: PluginInfo
|
||||
session: Uninfo
|
||||
|
||||
返回:
|
||||
int: 需要消耗的金币
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
if user.gold < plugin.cost_gold:
|
||||
"""插件消耗金币不足"""
|
||||
await send_message(session, f"金币不足..该功能需要{plugin.cost_gold}金币..")
|
||||
raise SkipPluginException(f"{plugin.name}({plugin.module}) 金币限制...")
|
||||
return plugin.cost_gold
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_cost 耗时: {elapsed:.3f}s, plugin={plugin.module}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
68
zhenxun/builtin_plugins/hooks/auth/auth_group.py
Normal file
68
zhenxun/builtin_plugins/hooks/auth/auth_group.py
Normal file
@ -0,0 +1,68 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.data_access import DataAccess
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.utils import EntityIDs
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD, SwitchEnum
|
||||
from .exception import SkipPluginException
|
||||
|
||||
|
||||
async def auth_group(plugin: PluginInfo, entity: EntityIDs, message: UniMsg):
|
||||
"""群黑名单检测 群总开关检测
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
entity: EntityIDs
|
||||
message: UniMsg
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
if not entity.group_id:
|
||||
return
|
||||
|
||||
try:
|
||||
text = message.extract_plain_text()
|
||||
|
||||
# 从数据库或缓存中获取群组信息
|
||||
group_dao = DataAccess(GroupConsole)
|
||||
|
||||
try:
|
||||
group: GroupConsole | None = await asyncio.wait_for(
|
||||
group_dao.safe_get_or_none(
|
||||
group_id=entity.group_id, channel_id__isnull=True
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("查询群组信息超时", LOGGER_COMMAND, session=entity.user_id)
|
||||
# 超时时不阻塞,继续执行
|
||||
return
|
||||
|
||||
if not group:
|
||||
raise SkipPluginException("群组信息不存在...")
|
||||
if group.level < 0:
|
||||
raise SkipPluginException("群组黑名单, 目标群组群权限权限-1...")
|
||||
if text.strip() != SwitchEnum.ENABLE and not group.status:
|
||||
raise SkipPluginException("群组休眠状态...")
|
||||
if plugin.level > group.level:
|
||||
raise SkipPluginException(
|
||||
f"{plugin.name}({plugin.module}) 群等级限制,"
|
||||
f"该功能需要的群等级: {plugin.level}..."
|
||||
)
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_group 耗时: {elapsed:.3f}s, plugin={plugin.module}",
|
||||
LOGGER_COMMAND,
|
||||
session=entity.user_id,
|
||||
group_id=entity.group_id,
|
||||
)
|
||||
322
zhenxun/builtin_plugins/hooks/auth/auth_limit.py
Normal file
322
zhenxun/builtin_plugins/hooks/auth/auth_limit.py
Normal file
@ -0,0 +1,322 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import ClassVar
|
||||
|
||||
import nonebot
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.plugin_limit import PluginLimit
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import LimitWatchType, PluginLimitType
|
||||
from zhenxun.utils.limiters import CountLimiter, FreqLimiter, UserBlockLimiter
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.time_utils import TimeUtils
|
||||
from zhenxun.utils.utils import get_entity_ids
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import SkipPluginException
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
"""初始化限制"""
|
||||
await LimitManager.init_limit()
|
||||
|
||||
|
||||
class Limit(BaseModel):
|
||||
limit: PluginLimit
|
||||
limiter: FreqLimiter | UserBlockLimiter | CountLimiter
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class LimitManager:
|
||||
add_module: ClassVar[list] = []
|
||||
last_update_time: ClassVar[float] = 0
|
||||
update_interval: ClassVar[float] = 6000 # 1小时更新一次
|
||||
is_updating: ClassVar[bool] = False # 防止并发更新
|
||||
|
||||
cd_limit: ClassVar[dict[str, Limit]] = {}
|
||||
block_limit: ClassVar[dict[str, Limit]] = {}
|
||||
count_limit: ClassVar[dict[str, Limit]] = {}
|
||||
|
||||
# 模块限制缓存,避免频繁查询数据库
|
||||
module_limit_cache: ClassVar[dict[str, tuple[float, list[PluginLimit]]]] = {}
|
||||
module_cache_ttl: ClassVar[float] = 60 # 模块缓存有效期(秒)
|
||||
|
||||
@classmethod
|
||||
async def init_limit(cls):
|
||||
"""初始化限制"""
|
||||
cls.last_update_time = time.time()
|
||||
try:
|
||||
await asyncio.wait_for(cls.update_limits(), timeout=DB_TIMEOUT_SECONDS * 2)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("初始化限制超时", LOGGER_COMMAND)
|
||||
|
||||
@classmethod
|
||||
async def update_limits(cls):
|
||||
"""更新限制信息"""
|
||||
# 防止并发更新
|
||||
if cls.is_updating:
|
||||
return
|
||||
|
||||
cls.is_updating = True
|
||||
try:
|
||||
start_time = time.time()
|
||||
try:
|
||||
limit_list = await asyncio.wait_for(
|
||||
PluginLimit.filter(status=True).all(), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("查询限制信息超时", LOGGER_COMMAND)
|
||||
cls.is_updating = False
|
||||
return
|
||||
|
||||
# 清空旧数据
|
||||
cls.add_module = []
|
||||
cls.cd_limit = {}
|
||||
cls.block_limit = {}
|
||||
cls.count_limit = {}
|
||||
# 添加新数据
|
||||
for limit in limit_list:
|
||||
cls.add_limit(limit)
|
||||
|
||||
cls.last_update_time = time.time()
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的更新
|
||||
logger.warning(f"更新限制信息耗时: {elapsed:.3f}s", LOGGER_COMMAND)
|
||||
finally:
|
||||
cls.is_updating = False
|
||||
|
||||
@classmethod
|
||||
def add_limit(cls, limit: PluginLimit):
|
||||
"""添加限制
|
||||
|
||||
参数:
|
||||
limit: PluginLimit
|
||||
"""
|
||||
if limit.module not in cls.add_module:
|
||||
cls.add_module.append(limit.module)
|
||||
if limit.limit_type == PluginLimitType.BLOCK:
|
||||
cls.block_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=UserBlockLimiter()
|
||||
)
|
||||
elif limit.limit_type == PluginLimitType.CD:
|
||||
cls.cd_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=FreqLimiter(limit.cd)
|
||||
)
|
||||
elif limit.limit_type == PluginLimitType.COUNT:
|
||||
cls.count_limit[limit.module] = Limit(
|
||||
limit=limit, limiter=CountLimiter(limit.max_count)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def unblock(
|
||||
cls, module: str, user_id: str, group_id: str | None, channel_id: str | None
|
||||
):
|
||||
"""解除插件block
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
"""
|
||||
if limit_model := cls.block_limit.get(module):
|
||||
limit = limit_model.limit
|
||||
limiter: UserBlockLimiter = limit_model.limiter # type: ignore
|
||||
key_type = user_id
|
||||
if group_id and limit.watch_type == LimitWatchType.GROUP:
|
||||
key_type = channel_id or group_id
|
||||
logger.debug(
|
||||
f"解除对象: {key_type} 的block限制",
|
||||
LOGGER_COMMAND,
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
limiter.set_false(key_type)
|
||||
|
||||
@classmethod
|
||||
async def get_module_limits(cls, module: str) -> list[PluginLimit]:
|
||||
"""获取模块的限制信息,使用缓存减少数据库查询
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
|
||||
返回:
|
||||
list[PluginLimit]: 限制列表
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
# 检查缓存
|
||||
if module in cls.module_limit_cache:
|
||||
cache_time, limits = cls.module_limit_cache[module]
|
||||
if current_time - cache_time < cls.module_cache_ttl:
|
||||
return limits
|
||||
|
||||
# 缓存不存在或已过期,从数据库查询
|
||||
try:
|
||||
start_time = time.time()
|
||||
limits = await asyncio.wait_for(
|
||||
PluginLimit.filter(module=module, status=True).all(),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的查询
|
||||
logger.warning(
|
||||
f"查询模块限制信息耗时: {elapsed:.3f}s, 模块: {module}",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
|
||||
# 更新缓存
|
||||
cls.module_limit_cache[module] = (current_time, limits)
|
||||
return limits
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"查询模块限制信息超时: {module}", LOGGER_COMMAND)
|
||||
# 超时时返回空列表,避免阻塞
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
async def check(
|
||||
cls,
|
||||
module: str,
|
||||
user_id: str,
|
||||
group_id: str | None,
|
||||
channel_id: str | None,
|
||||
):
|
||||
"""检测限制
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
|
||||
异常:
|
||||
IgnoredException: IgnoredException
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
# 定期更新全局限制信息
|
||||
if (
|
||||
time.time() - cls.last_update_time > cls.update_interval
|
||||
and not cls.is_updating
|
||||
):
|
||||
# 使用异步任务更新,避免阻塞当前请求
|
||||
asyncio.create_task(cls.update_limits()) # noqa: RUF006
|
||||
|
||||
# 如果模块不在已加载列表中,只加载该模块的限制
|
||||
if module not in cls.add_module:
|
||||
limits = await cls.get_module_limits(module)
|
||||
for limit in limits:
|
||||
cls.add_limit(limit)
|
||||
|
||||
# 检查各种限制
|
||||
try:
|
||||
if limit_model := cls.cd_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id)
|
||||
if limit_model := cls.block_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id)
|
||||
if limit_model := cls.count_limit.get(module):
|
||||
await cls.__check(limit_model, user_id, group_id, channel_id)
|
||||
finally:
|
||||
# 记录总执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"限制检查耗时: {elapsed:.3f}s, 模块: {module}",
|
||||
LOGGER_COMMAND,
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def __check(
|
||||
cls,
|
||||
limit_model: Limit | None,
|
||||
user_id: str,
|
||||
group_id: str | None,
|
||||
channel_id: str | None,
|
||||
):
|
||||
"""检测限制
|
||||
|
||||
参数:
|
||||
limit_model: Limit
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
channel_id: 频道id
|
||||
|
||||
异常:
|
||||
IgnoredException: IgnoredException
|
||||
"""
|
||||
if not limit_model:
|
||||
return
|
||||
limit = limit_model.limit
|
||||
limiter = limit_model.limiter
|
||||
is_limit = (
|
||||
LimitWatchType.ALL
|
||||
or (group_id and limit.watch_type == LimitWatchType.GROUP)
|
||||
or (not group_id and limit.watch_type == LimitWatchType.USER)
|
||||
)
|
||||
key_type = user_id
|
||||
if group_id and limit.watch_type == LimitWatchType.GROUP:
|
||||
key_type = channel_id or group_id
|
||||
if is_limit and not limiter.check(key_type):
|
||||
if limit.result:
|
||||
format_kwargs = {}
|
||||
if isinstance(limiter, FreqLimiter):
|
||||
left_time = limiter.left_time(key_type)
|
||||
cd_str = TimeUtils.format_duration(left_time)
|
||||
format_kwargs = {"cd": cd_str}
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
MessageUtils.build_message(
|
||||
limit.result, format_args=format_kwargs
|
||||
).send(),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送限制消息超时: {limit.module}", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{limit.module}({limit.limit_type}) 正在限制中..."
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"开始进行限制 {limit.module}({limit.limit_type})...",
|
||||
LOGGER_COMMAND,
|
||||
session=user_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
if isinstance(limiter, FreqLimiter):
|
||||
limiter.start_cd(key_type)
|
||||
if isinstance(limiter, UserBlockLimiter):
|
||||
limiter.set_true(key_type)
|
||||
if isinstance(limiter, CountLimiter):
|
||||
limiter.increase(key_type)
|
||||
|
||||
|
||||
async def auth_limit(plugin: PluginInfo, session: Uninfo):
|
||||
"""插件限制
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: Uninfo
|
||||
"""
|
||||
entity = get_entity_ids(session)
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
LimitManager.check(
|
||||
plugin.module, entity.user_id, entity.group_id, entity.channel_id
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS * 2, # 给予更长的超时时间
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"检查插件限制超时: {plugin.module}", LOGGER_COMMAND)
|
||||
# 超时时不抛出异常,允许继续执行
|
||||
242
zhenxun/builtin_plugins/hooks/auth/auth_plugin.py
Normal file
242
zhenxun/builtin_plugins/hooks/auth/auth_plugin.py
Normal file
@ -0,0 +1,242 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.data_access import DataAccess
|
||||
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.enum import BlockType
|
||||
from zhenxun.utils.utils import get_entity_ids
|
||||
|
||||
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .exception import IsSuperuserException, SkipPluginException
|
||||
from .utils import freq, is_poke, send_message
|
||||
|
||||
|
||||
class GroupCheck:
|
||||
def __init__(
|
||||
self, plugin: PluginInfo, group_id: str, session: Uninfo, is_poke: bool
|
||||
) -> None:
|
||||
self.group_id = group_id
|
||||
self.session = session
|
||||
self.is_poke = is_poke
|
||||
self.plugin = plugin
|
||||
self.group_dao = DataAccess(GroupConsole)
|
||||
self.group_data = None
|
||||
|
||||
async def check(self):
|
||||
start_time = time.time()
|
||||
try:
|
||||
# 只查询一次数据库,使用 DataAccess 的缓存机制
|
||||
try:
|
||||
self.group_data = await asyncio.wait_for(
|
||||
self.group_dao.safe_get_or_none(
|
||||
group_id=self.group_id, channel_id__isnull=True
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"查询群组数据超时: {self.group_id}", LOGGER_COMMAND)
|
||||
return # 超时时不阻塞,继续执行
|
||||
|
||||
# 检查超级用户禁用
|
||||
if (
|
||||
self.group_data
|
||||
and CommonUtils.format(self.plugin.module)
|
||||
in self.group_data.superuser_block_plugin
|
||||
):
|
||||
if freq.is_send_limit_message(self.plugin, self.group_id, self.is_poke):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(
|
||||
self.session,
|
||||
"超级管理员禁用了该群此功能...",
|
||||
self.group_id,
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送消息超时: {self.group_id}", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{self.plugin.name}({self.plugin.module})"
|
||||
f" 超级管理员禁用了该群此功能..."
|
||||
)
|
||||
|
||||
# 检查普通禁用
|
||||
if (
|
||||
self.group_data
|
||||
and CommonUtils.format(self.plugin.module)
|
||||
in self.group_data.block_plugin
|
||||
):
|
||||
if freq.is_send_limit_message(self.plugin, self.group_id, self.is_poke):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(
|
||||
self.session, "该群未开启此功能...", self.group_id
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送消息超时: {self.group_id}", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{self.plugin.name}({self.plugin.module}) 未开启此功能..."
|
||||
)
|
||||
|
||||
# 检查全局禁用
|
||||
if self.plugin.block_type == BlockType.GROUP:
|
||||
if freq.is_send_limit_message(self.plugin, self.group_id, self.is_poke):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(
|
||||
self.session, "该功能在群组中已被禁用...", self.group_id
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送消息超时: {self.group_id}", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{self.plugin.name}({self.plugin.module})该插件在群组中已被禁用..."
|
||||
)
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"GroupCheck.check 耗时: {elapsed:.3f}s, 群组: {self.group_id}",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
|
||||
|
||||
class PluginCheck:
|
||||
def __init__(self, group_id: str | None, session: Uninfo, is_poke: bool):
|
||||
self.session = session
|
||||
self.is_poke = is_poke
|
||||
self.group_id = group_id
|
||||
self.group_dao = DataAccess(GroupConsole)
|
||||
self.group_data = None
|
||||
|
||||
async def check_user(self, plugin: PluginInfo):
|
||||
"""全局私聊禁用检测
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
|
||||
异常:
|
||||
IgnoredException: 忽略插件
|
||||
"""
|
||||
if plugin.block_type == BlockType.PRIVATE:
|
||||
if freq.is_send_limit_message(plugin, self.session.user.id, self.is_poke):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(self.session, "该功能在私聊中已被禁用..."),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("发送消息超时", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{plugin.name}({plugin.module}) 该插件在私聊中已被禁用..."
|
||||
)
|
||||
|
||||
async def check_global(self, plugin: PluginInfo):
|
||||
"""全局状态
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
|
||||
异常:
|
||||
IgnoredException: 忽略插件
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
if plugin.status or plugin.block_type != BlockType.ALL:
|
||||
return
|
||||
"""全局状态"""
|
||||
if self.group_id:
|
||||
# 使用 DataAccess 的缓存机制
|
||||
try:
|
||||
self.group_data = await asyncio.wait_for(
|
||||
self.group_dao.safe_get_or_none(
|
||||
group_id=self.group_id, channel_id__isnull=True
|
||||
),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"查询群组数据超时: {self.group_id}", LOGGER_COMMAND)
|
||||
return # 超时时不阻塞,继续执行
|
||||
|
||||
if self.group_data and self.group_data.is_super:
|
||||
raise IsSuperuserException()
|
||||
|
||||
sid = self.group_id or self.session.user.id
|
||||
if freq.is_send_limit_message(plugin, sid, self.is_poke):
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
send_message(self.session, "全局未开启此功能...", sid),
|
||||
timeout=DB_TIMEOUT_SECONDS,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"发送消息超时: {sid}", LOGGER_COMMAND)
|
||||
raise SkipPluginException(
|
||||
f"{plugin.name}({plugin.module}) 全局未开启此功能..."
|
||||
)
|
||||
finally:
|
||||
# 记录执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"PluginCheck.check_global 耗时: {elapsed:.3f}s", LOGGER_COMMAND
|
||||
)
|
||||
|
||||
|
||||
async def auth_plugin(plugin: PluginInfo, session: Uninfo, event: Event):
|
||||
"""插件状态
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
session: Uninfo
|
||||
event: Event
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
entity = get_entity_ids(session)
|
||||
is_poke_event = is_poke(event)
|
||||
user_check = PluginCheck(entity.group_id, session, is_poke_event)
|
||||
|
||||
if entity.group_id:
|
||||
group_check = GroupCheck(plugin, entity.group_id, session, is_poke_event)
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
group_check.check(), timeout=DB_TIMEOUT_SECONDS * 2
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"群组检查超时: {entity.group_id}", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
else:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
user_check.check_user(plugin), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("用户检查超时", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
user_check.check_global(plugin), timeout=DB_TIMEOUT_SECONDS
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error("全局检查超时", LOGGER_COMMAND)
|
||||
# 超时时不阻塞,继续执行
|
||||
finally:
|
||||
# 记录总执行时间
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > WARNING_THRESHOLD: # 记录耗时超过500ms的检查
|
||||
logger.warning(
|
||||
f"auth_plugin 总耗时: {elapsed:.3f}s, 模块: {plugin.module}",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
35
zhenxun/builtin_plugins/hooks/auth/bot_filter.py
Normal file
35
zhenxun/builtin_plugins/hooks/auth/bot_filter.py
Normal file
@ -0,0 +1,35 @@
|
||||
import nonebot
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
|
||||
from .exception import SkipPluginException
|
||||
|
||||
Config.add_plugin_config(
|
||||
"hook",
|
||||
"FILTER_BOT",
|
||||
True,
|
||||
help="过滤当前连接bot(防止bot互相调用)",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
)
|
||||
|
||||
|
||||
def bot_filter(session: Uninfo):
|
||||
"""过滤bot调用bot
|
||||
|
||||
参数:
|
||||
session: Uninfo
|
||||
|
||||
异常:
|
||||
SkipPluginException: bot互相调用
|
||||
"""
|
||||
if not Config.get_config("hook", "FILTER_BOT"):
|
||||
return
|
||||
bot_ids = list(nonebot.get_bots().keys())
|
||||
if session.user.id == session.self_id:
|
||||
return
|
||||
if session.user.id in bot_ids:
|
||||
raise SkipPluginException(
|
||||
f"bot:{session.self_id} 尝试调用 bot:{session.user.id}"
|
||||
)
|
||||
16
zhenxun/builtin_plugins/hooks/auth/config.py
Normal file
16
zhenxun/builtin_plugins/hooks/auth/config.py
Normal file
@ -0,0 +1,16 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from enum import StrEnum
|
||||
else:
|
||||
from strenum import StrEnum
|
||||
|
||||
LOGGER_COMMAND = "AuthChecker"
|
||||
|
||||
|
||||
class SwitchEnum(StrEnum):
|
||||
ENABLE = "醒来"
|
||||
DISABLE = "休息吧"
|
||||
|
||||
|
||||
WARNING_THRESHOLD = 0.5 # 警告阈值(秒)
|
||||
26
zhenxun/builtin_plugins/hooks/auth/exception.py
Normal file
26
zhenxun/builtin_plugins/hooks/auth/exception.py
Normal file
@ -0,0 +1,26 @@
|
||||
class IsSuperuserException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SkipPluginException(Exception):
|
||||
def __init__(self, info: str, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
self.info = info
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.info
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.info
|
||||
|
||||
|
||||
class PermissionExemption(Exception):
|
||||
def __init__(self, info: str, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
self.info = info
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.info
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.info
|
||||
91
zhenxun/builtin_plugins/hooks/auth/utils.py
Normal file
91
zhenxun/builtin_plugins/hooks/auth/utils.py
Normal file
@ -0,0 +1,91 @@
|
||||
import contextlib
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.utils import FreqLimiter
|
||||
|
||||
from .config import LOGGER_COMMAND
|
||||
|
||||
base_config = Config.get("hook")
|
||||
|
||||
|
||||
def is_poke(event: Event) -> bool:
|
||||
"""判断是否为poke类型
|
||||
|
||||
参数:
|
||||
event: Event
|
||||
|
||||
返回:
|
||||
bool: 是否为poke类型
|
||||
"""
|
||||
with contextlib.suppress(ImportError):
|
||||
from nonebot.adapters.onebot.v11 import PokeNotifyEvent
|
||||
|
||||
return isinstance(event, PokeNotifyEvent)
|
||||
return False
|
||||
|
||||
|
||||
async def send_message(
|
||||
session: Uninfo, message: list | str, check_tag: str | None = None
|
||||
):
|
||||
"""发送消息
|
||||
|
||||
参数:
|
||||
session: Uninfo
|
||||
message: 消息
|
||||
check_tag: cd flag
|
||||
"""
|
||||
try:
|
||||
if not check_tag:
|
||||
await MessageUtils.build_message(message).send(reply_to=True)
|
||||
elif freq._flmt.check(check_tag):
|
||||
freq._flmt.start_cd(check_tag)
|
||||
await MessageUtils.build_message(message).send(reply_to=True)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"发送消息失败",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
e=e,
|
||||
)
|
||||
|
||||
|
||||
class FreqUtils:
|
||||
def __init__(self):
|
||||
check_notice_info_cd = Config.get_config("hook", "CHECK_NOTICE_INFO_CD")
|
||||
if check_notice_info_cd is None or check_notice_info_cd < 0:
|
||||
raise ValueError("模块: [hook], 配置项: [CHECK_NOTICE_INFO_CD] 为空或小于0")
|
||||
self._flmt = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_g = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_s = FreqLimiter(check_notice_info_cd)
|
||||
self._flmt_c = FreqLimiter(check_notice_info_cd)
|
||||
|
||||
def is_send_limit_message(
|
||||
self, plugin: PluginInfo, sid: str, is_poke: bool
|
||||
) -> bool:
|
||||
"""是否发送提示消息
|
||||
|
||||
参数:
|
||||
plugin: PluginInfo
|
||||
sid: 检测键
|
||||
is_poke: 是否是戳一戳
|
||||
|
||||
返回:
|
||||
bool: 是否发送提示消息
|
||||
"""
|
||||
if is_poke:
|
||||
return False
|
||||
if not base_config.get("IS_SEND_TIP_MESSAGE"):
|
||||
return False
|
||||
if plugin.plugin_type == PluginType.DEPENDANT:
|
||||
return False
|
||||
return plugin.module != "ai" if self._flmt_s.check(sid) else False
|
||||
|
||||
|
||||
freq = FreqUtils()
|
||||
379
zhenxun/builtin_plugins/hooks/auth_checker.py
Normal file
379
zhenxun/builtin_plugins/hooks/auth_checker.py
Normal file
@ -0,0 +1,379 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from tortoise.exceptions import IntegrityError
|
||||
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.data_access import DataAccess
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import GoldHandle, PluginType
|
||||
from zhenxun.utils.exception import InsufficientGold
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.utils import get_entity_ids
|
||||
|
||||
from .auth.auth_admin import auth_admin
|
||||
from .auth.auth_ban import auth_ban
|
||||
from .auth.auth_bot import auth_bot
|
||||
from .auth.auth_cost import auth_cost
|
||||
from .auth.auth_group import auth_group
|
||||
from .auth.auth_limit import LimitManager, auth_limit
|
||||
from .auth.auth_plugin import auth_plugin
|
||||
from .auth.bot_filter import bot_filter
|
||||
from .auth.config import LOGGER_COMMAND, WARNING_THRESHOLD
|
||||
from .auth.exception import (
|
||||
IsSuperuserException,
|
||||
PermissionExemption,
|
||||
SkipPluginException,
|
||||
)
|
||||
|
||||
# 超时设置(秒)
|
||||
TIMEOUT_SECONDS = 5.0
|
||||
# 熔断计数器
|
||||
CIRCUIT_BREAKERS = {
|
||||
"auth_ban": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
"auth_bot": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
"auth_group": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
"auth_admin": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
"auth_plugin": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
"auth_limit": {"failures": 0, "threshold": 3, "active": False, "reset_time": 0},
|
||||
}
|
||||
# 熔断重置时间(秒)
|
||||
CIRCUIT_RESET_TIME = 300 # 5分钟
|
||||
|
||||
|
||||
# 超时装饰器
|
||||
async def with_timeout(coro, timeout=TIMEOUT_SECONDS, name=None):
|
||||
"""带超时控制的协程执行
|
||||
|
||||
参数:
|
||||
coro: 要执行的协程
|
||||
timeout: 超时时间(秒)
|
||||
name: 操作名称,用于日志记录
|
||||
|
||||
返回:
|
||||
协程的返回值,或者在超时时抛出 TimeoutError
|
||||
"""
|
||||
try:
|
||||
return await asyncio.wait_for(coro, timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
if name:
|
||||
logger.error(f"{name} 操作超时 (>{timeout}s)", LOGGER_COMMAND)
|
||||
# 更新熔断计数器
|
||||
if name in CIRCUIT_BREAKERS:
|
||||
CIRCUIT_BREAKERS[name]["failures"] += 1
|
||||
if (
|
||||
CIRCUIT_BREAKERS[name]["failures"]
|
||||
>= CIRCUIT_BREAKERS[name]["threshold"]
|
||||
and not CIRCUIT_BREAKERS[name]["active"]
|
||||
):
|
||||
CIRCUIT_BREAKERS[name]["active"] = True
|
||||
CIRCUIT_BREAKERS[name]["reset_time"] = (
|
||||
time.time() + CIRCUIT_RESET_TIME
|
||||
)
|
||||
logger.warning(
|
||||
f"{name} 熔断器已激活,将在 {CIRCUIT_RESET_TIME} 秒后重置",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
# 检查熔断状态
|
||||
def check_circuit_breaker(name):
|
||||
"""检查熔断器状态
|
||||
|
||||
参数:
|
||||
name: 操作名称
|
||||
|
||||
返回:
|
||||
bool: 是否已熔断
|
||||
"""
|
||||
if name not in CIRCUIT_BREAKERS:
|
||||
return False
|
||||
|
||||
# 检查是否需要重置熔断器
|
||||
if (
|
||||
CIRCUIT_BREAKERS[name]["active"]
|
||||
and time.time() > CIRCUIT_BREAKERS[name]["reset_time"]
|
||||
):
|
||||
CIRCUIT_BREAKERS[name]["active"] = False
|
||||
CIRCUIT_BREAKERS[name]["failures"] = 0
|
||||
logger.info(f"{name} 熔断器已重置", LOGGER_COMMAND)
|
||||
|
||||
return CIRCUIT_BREAKERS[name]["active"]
|
||||
|
||||
|
||||
async def get_plugin_and_user(
|
||||
module: str, user_id: str
|
||||
) -> tuple[PluginInfo, UserConsole]:
|
||||
"""获取用户数据和插件信息
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
user_id: 用户id
|
||||
|
||||
异常:
|
||||
PermissionExemption: 插件数据不存在
|
||||
PermissionExemption: 插件类型为HIDDEN
|
||||
PermissionExemption: 重复创建用户
|
||||
PermissionExemption: 用户数据不存在
|
||||
|
||||
返回:
|
||||
tuple[PluginInfo, UserConsole]: 插件信息,用户信息
|
||||
"""
|
||||
user_dao = DataAccess(UserConsole)
|
||||
plugin_dao = DataAccess(PluginInfo)
|
||||
|
||||
# 并行查询插件和用户数据
|
||||
plugin_task = plugin_dao.safe_get_or_none(module=module)
|
||||
user_task = user_dao.get_by_func_or_none(
|
||||
UserConsole.get_user, False, user_id=user_id
|
||||
)
|
||||
|
||||
try:
|
||||
plugin, user = await with_timeout(
|
||||
asyncio.gather(plugin_task, user_task), name="get_plugin_and_user"
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
# 如果并行查询超时,尝试串行查询
|
||||
logger.warning("并行查询超时,尝试串行查询", LOGGER_COMMAND)
|
||||
plugin = await with_timeout(
|
||||
plugin_dao.safe_get_or_none(module=module), name="get_plugin"
|
||||
)
|
||||
user = await with_timeout(
|
||||
user_dao.safe_get_or_none(user_id=user_id), name="get_user"
|
||||
)
|
||||
|
||||
if not plugin:
|
||||
raise PermissionExemption(f"插件:{module} 数据不存在,已跳过权限检查...")
|
||||
if plugin.plugin_type == PluginType.HIDDEN:
|
||||
raise PermissionExemption(
|
||||
f"插件: {plugin.name}:{plugin.module} 为HIDDEN,已跳过权限检查..."
|
||||
)
|
||||
user = None
|
||||
try:
|
||||
user = await user_dao.get_by_func_or_none(
|
||||
UserConsole.get_user, False, user_id=user_id
|
||||
)
|
||||
except IntegrityError as e:
|
||||
raise PermissionExemption("重复创建用户,已跳过该次权限检查...") from e
|
||||
if not user:
|
||||
raise PermissionExemption("用户数据不存在,已跳过权限检查...")
|
||||
return plugin, user
|
||||
|
||||
|
||||
async def get_plugin_cost(
|
||||
bot: Bot, user: UserConsole, plugin: PluginInfo, session: Uninfo
|
||||
) -> int:
|
||||
"""获取插件费用
|
||||
|
||||
参数:
|
||||
bot: Bot
|
||||
user: 用户数据
|
||||
plugin: 插件数据
|
||||
session: Uninfo
|
||||
|
||||
异常:
|
||||
IsSuperuserException: 超级用户
|
||||
IsSuperuserException: 超级用户
|
||||
|
||||
返回:
|
||||
int: 调用插件金币费用
|
||||
"""
|
||||
cost_gold = await with_timeout(auth_cost(user, plugin, session), name="auth_cost")
|
||||
if session.user.id in bot.config.superusers:
|
||||
if plugin.plugin_type == PluginType.SUPERUSER:
|
||||
raise IsSuperuserException()
|
||||
if not plugin.limit_superuser:
|
||||
raise IsSuperuserException()
|
||||
return cost_gold
|
||||
|
||||
|
||||
async def reduce_gold(user_id: str, module: str, cost_gold: int, session: Uninfo):
|
||||
"""扣除用户金币
|
||||
|
||||
参数:
|
||||
user_id: 用户id
|
||||
module: 插件模块名称
|
||||
cost_gold: 消耗金币
|
||||
session: Uninfo
|
||||
"""
|
||||
user_dao = DataAccess(UserConsole)
|
||||
try:
|
||||
await with_timeout(
|
||||
UserConsole.reduce_gold(
|
||||
user_id,
|
||||
cost_gold,
|
||||
GoldHandle.PLUGIN,
|
||||
module,
|
||||
PlatformUtils.get_platform(session),
|
||||
),
|
||||
name="reduce_gold",
|
||||
)
|
||||
except InsufficientGold:
|
||||
if u := await UserConsole.get_user(user_id):
|
||||
u.gold = 0
|
||||
await u.save(update_fields=["gold"])
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"扣除金币超时,用户: {user_id}, 金币: {cost_gold}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
|
||||
# 清除缓存,使下次查询时从数据库获取最新数据
|
||||
await user_dao.clear_cache(user_id=user_id)
|
||||
logger.debug(f"调用功能花费金币: {cost_gold}", LOGGER_COMMAND, session=session)
|
||||
|
||||
|
||||
# 辅助函数,用于记录每个 hook 的执行时间
|
||||
async def time_hook(coro, name, time_dict):
|
||||
start = time.time()
|
||||
try:
|
||||
# 检查熔断状态
|
||||
if check_circuit_breaker(name):
|
||||
logger.info(f"{name} 熔断器激活中,跳过执行", LOGGER_COMMAND)
|
||||
time_dict[name] = "熔断跳过"
|
||||
return
|
||||
|
||||
# 添加超时控制
|
||||
return await with_timeout(coro, name=name)
|
||||
except asyncio.TimeoutError:
|
||||
time_dict[name] = f"超时 (>{TIMEOUT_SECONDS}s)"
|
||||
finally:
|
||||
if name not in time_dict:
|
||||
time_dict[name] = f"{time.time() - start:.3f}s"
|
||||
|
||||
|
||||
async def auth(
|
||||
matcher: Matcher,
|
||||
event: Event,
|
||||
bot: Bot,
|
||||
session: Uninfo,
|
||||
message: UniMsg,
|
||||
):
|
||||
"""权限检查
|
||||
|
||||
参数:
|
||||
matcher: matcher
|
||||
event: Event
|
||||
bot: bot
|
||||
session: Uninfo
|
||||
message: UniMsg
|
||||
"""
|
||||
start_time = time.time()
|
||||
cost_gold = 0
|
||||
ignore_flag = False
|
||||
entity = get_entity_ids(session)
|
||||
module = matcher.plugin_name or ""
|
||||
|
||||
# 用于记录各个 hook 的执行时间
|
||||
hook_times = {}
|
||||
hooks_time = 0 # 初始化 hooks_time 变量
|
||||
|
||||
try:
|
||||
if not module:
|
||||
raise PermissionExemption("Matcher插件名称不存在...")
|
||||
|
||||
# 获取插件和用户数据
|
||||
plugin_user_start = time.time()
|
||||
try:
|
||||
plugin, user = await with_timeout(
|
||||
get_plugin_and_user(module, entity.user_id), name="get_plugin_and_user"
|
||||
)
|
||||
hook_times["get_plugin_user"] = f"{time.time() - plugin_user_start:.3f}s"
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"获取插件和用户数据超时,模块: {module}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
raise PermissionExemption("获取插件和用户数据超时,请稍后再试...")
|
||||
|
||||
# 获取插件费用
|
||||
cost_start = time.time()
|
||||
try:
|
||||
cost_gold = await with_timeout(
|
||||
get_plugin_cost(bot, user, plugin, session), name="get_plugin_cost"
|
||||
)
|
||||
hook_times["cost_gold"] = f"{time.time() - cost_start:.3f}s"
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"获取插件费用超时,模块: {module}", LOGGER_COMMAND, session=session
|
||||
)
|
||||
# 继续执行,不阻止权限检查
|
||||
|
||||
# 执行 bot_filter
|
||||
bot_filter(session)
|
||||
|
||||
# 并行执行所有 hook 检查,并记录执行时间
|
||||
hooks_start = time.time()
|
||||
|
||||
# 创建所有 hook 任务
|
||||
hook_tasks = [
|
||||
time_hook(auth_ban(matcher, bot, session), "auth_ban", hook_times),
|
||||
time_hook(auth_bot(plugin, bot.self_id), "auth_bot", hook_times),
|
||||
time_hook(auth_group(plugin, entity, message), "auth_group", hook_times),
|
||||
time_hook(auth_admin(plugin, session), "auth_admin", hook_times),
|
||||
time_hook(auth_plugin(plugin, session, event), "auth_plugin", hook_times),
|
||||
time_hook(auth_limit(plugin, session), "auth_limit", hook_times),
|
||||
]
|
||||
|
||||
# 使用 gather 并行执行所有 hook,但添加总体超时控制
|
||||
try:
|
||||
await with_timeout(
|
||||
asyncio.gather(*hook_tasks),
|
||||
timeout=TIMEOUT_SECONDS * 2, # 给总体执行更多时间
|
||||
name="auth_hooks_gather",
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"权限检查 hooks 总体执行超时,模块: {module}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
# 不抛出异常,允许继续执行
|
||||
|
||||
hooks_time = time.time() - hooks_start
|
||||
|
||||
except SkipPluginException as e:
|
||||
LimitManager.unblock(module, entity.user_id, entity.group_id, entity.channel_id)
|
||||
logger.info(str(e), LOGGER_COMMAND, session=session)
|
||||
ignore_flag = True
|
||||
except IsSuperuserException:
|
||||
logger.debug("超级用户跳过权限检测...", LOGGER_COMMAND, session=session)
|
||||
except PermissionExemption as e:
|
||||
logger.info(str(e), LOGGER_COMMAND, session=session)
|
||||
|
||||
# 扣除金币
|
||||
if not ignore_flag and cost_gold > 0:
|
||||
gold_start = time.time()
|
||||
try:
|
||||
await with_timeout(
|
||||
reduce_gold(entity.user_id, module, cost_gold, session),
|
||||
name="reduce_gold",
|
||||
)
|
||||
hook_times["reduce_gold"] = f"{time.time() - gold_start:.3f}s"
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
f"扣除金币超时,模块: {module}", LOGGER_COMMAND, session=session
|
||||
)
|
||||
|
||||
# 记录总执行时间
|
||||
total_time = time.time() - start_time
|
||||
if total_time > WARNING_THRESHOLD: # 如果总时间超过500ms,记录详细信息
|
||||
logger.warning(
|
||||
f"权限检查耗时过长: {total_time:.3f}s, 模块: {module}, "
|
||||
f"hooks时间: {hooks_time:.3f}s, "
|
||||
f"详情: {hook_times}",
|
||||
LOGGER_COMMAND,
|
||||
session=session,
|
||||
)
|
||||
|
||||
if ignore_flag:
|
||||
raise IgnoredException("权限检测 ignore")
|
||||
@ -1,41 +1,43 @@
|
||||
from nonebot.adapters.onebot.v11 import Bot, Event
|
||||
import time
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.message import run_postprocessor, run_preprocessor
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from ._auth_checker import LimitManage, checker
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
from .auth.config import LOGGER_COMMAND
|
||||
from .auth_checker import LimitManager, auth
|
||||
|
||||
|
||||
# # 权限检测
|
||||
@run_preprocessor
|
||||
async def _(
|
||||
matcher: Matcher, event: Event, bot: Bot, session: EventSession, message: UniMsg
|
||||
):
|
||||
await checker.auth(
|
||||
async def _(matcher: Matcher, event: Event, bot: Bot, session: Uninfo, message: UniMsg):
|
||||
start_time = time.time()
|
||||
await auth(
|
||||
matcher,
|
||||
event,
|
||||
bot,
|
||||
session,
|
||||
message,
|
||||
)
|
||||
logger.debug(f"权限检测耗时:{time.time() - start_time}秒", LOGGER_COMMAND)
|
||||
|
||||
|
||||
# 解除命令block阻塞
|
||||
@run_postprocessor
|
||||
async def _(
|
||||
matcher: Matcher,
|
||||
exception: Exception | None,
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
session: EventSession,
|
||||
):
|
||||
user_id = session.id1
|
||||
group_id = session.id3
|
||||
channel_id = session.id2
|
||||
if not group_id:
|
||||
group_id = channel_id
|
||||
channel_id = None
|
||||
async def _(matcher: Matcher, session: Uninfo):
|
||||
user_id = session.user.id
|
||||
group_id = None
|
||||
channel_id = None
|
||||
if session.group:
|
||||
if session.group.parent:
|
||||
group_id = session.group.parent.id
|
||||
channel_id = session.group.id
|
||||
else:
|
||||
group_id = session.group.id
|
||||
if user_id and matcher.plugin:
|
||||
module = matcher.plugin.name
|
||||
LimitManage.unblock(module, user_id, group_id, channel_id)
|
||||
LimitManager.unblock(module, user_id, group_id, channel_id)
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.message import run_preprocessor
|
||||
from nonebot.typing import T_State
|
||||
from nonebot_plugin_alconna import At
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.utils import FreqLimiter
|
||||
|
||||
Config.add_plugin_config(
|
||||
"hook",
|
||||
"BAN_RESULT",
|
||||
"才不会给你发消息.",
|
||||
help="对被ban用户发送的消息",
|
||||
)
|
||||
|
||||
_flmt = FreqLimiter(300)
|
||||
|
||||
|
||||
# 检查是否被ban
|
||||
@run_preprocessor
|
||||
async def _(
|
||||
matcher: Matcher, bot: Bot, event: Event, state: T_State, session: EventSession
|
||||
):
|
||||
extra = {}
|
||||
if plugin := matcher.plugin:
|
||||
if metadata := plugin.metadata:
|
||||
extra = metadata.extra
|
||||
if extra.get("plugin_type") in [PluginType.HIDDEN]:
|
||||
return
|
||||
user_id = session.id1
|
||||
group_id = session.id3 or session.id2
|
||||
if group_id:
|
||||
if user_id in bot.config.superusers:
|
||||
return
|
||||
if await BanConsole.is_ban(None, group_id):
|
||||
logger.debug("群组处于黑名单中...", "ban_hook")
|
||||
raise IgnoredException("群组处于黑名单中...")
|
||||
if g := await GroupConsole.get_group(group_id):
|
||||
if g.level < 0:
|
||||
logger.debug("群黑名单, 群权限-1...", "ban_hook")
|
||||
raise IgnoredException("群黑名单, 群权限-1..")
|
||||
if user_id:
|
||||
ban_result = Config.get_config("hook", "BAN_RESULT")
|
||||
if user_id in bot.config.superusers:
|
||||
return
|
||||
if await BanConsole.is_ban(user_id, group_id):
|
||||
time = await BanConsole.check_ban_time(user_id, group_id)
|
||||
if time == -1:
|
||||
time_str = "∞"
|
||||
else:
|
||||
time = abs(int(time))
|
||||
if time < 60:
|
||||
time_str = f"{time!s} 秒"
|
||||
else:
|
||||
minute = int(time / 60)
|
||||
if minute > 60:
|
||||
hours = minute // 60
|
||||
minute %= 60
|
||||
time_str = f"{hours} 小时 {minute}分钟"
|
||||
else:
|
||||
time_str = f"{minute} 分钟"
|
||||
if (
|
||||
not extra.get("ignore_prompt")
|
||||
and time != -1
|
||||
and ban_result
|
||||
and _flmt.check(user_id)
|
||||
):
|
||||
_flmt.start_cd(user_id)
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
At(flag="user", target=user_id),
|
||||
f"{ban_result}\n在..在 {time_str} 后才会理你喔",
|
||||
]
|
||||
).send()
|
||||
logger.debug("用户处于黑名单中...", "ban_hook")
|
||||
raise IgnoredException("用户处于黑名单中...")
|
||||
@ -9,6 +9,8 @@ from zhenxun.utils.enum import BotSentType
|
||||
from zhenxun.utils.manager.message_manager import MessageManager
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
LOG_COMMAND = "MessageHook"
|
||||
|
||||
|
||||
def replace_message(message: Message) -> str:
|
||||
"""将消息中的at、image、record、face替换为字符串
|
||||
@ -54,11 +56,11 @@ async def handle_api_result(
|
||||
if user_id and message_id:
|
||||
MessageManager.add(str(user_id), str(message_id))
|
||||
logger.debug(
|
||||
f"收集消息id,user_id: {user_id}, msg_id: {message_id}", "msg_hook"
|
||||
f"收集消息id,user_id: {user_id}, msg_id: {message_id}", LOG_COMMAND
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"收集消息id发生错误...data: {data}, result: {result}", "msg_hook", e=e
|
||||
f"收集消息id发生错误...data: {data}, result: {result}", LOG_COMMAND, e=e
|
||||
)
|
||||
if not Config.get_config("hook", "RECORD_BOT_SENT_MESSAGES"):
|
||||
return
|
||||
@ -80,6 +82,6 @@ async def handle_api_result(
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"消息发送记录发生错误...data: {data}, result: {result}",
|
||||
"msg_hook",
|
||||
LOG_COMMAND,
|
||||
e=e,
|
||||
)
|
||||
|
||||
@ -92,7 +92,12 @@ async def _(
|
||||
if module:
|
||||
if _blmt.check(f"{user_id}__{module}"):
|
||||
await BanConsole.ban(
|
||||
user_id, group_id, 9, malicious_ban_time * 60, bot.self_id
|
||||
user_id,
|
||||
group_id,
|
||||
9,
|
||||
"恶意触发命令检测",
|
||||
malicious_ban_time * 60,
|
||||
bot.self_id,
|
||||
)
|
||||
logger.info(
|
||||
f"触发了恶意触发检测: {matcher.plugin_name}",
|
||||
|
||||
15
zhenxun/builtin_plugins/hooks/limiter_hook.py
Normal file
15
zhenxun/builtin_plugins/hooks/limiter_hook.py
Normal file
@ -0,0 +1,15 @@
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.message import run_postprocessor
|
||||
|
||||
from zhenxun.utils.limiters import ConcurrencyLimiter
|
||||
|
||||
|
||||
@run_postprocessor
|
||||
async def _concurrency_release_hook(matcher: Matcher):
|
||||
"""
|
||||
后处理器:在事件处理结束后,释放并发限制的信号量。
|
||||
"""
|
||||
if concurrency_info := matcher.state.get("_concurrency_limiter_info"):
|
||||
limiter: ConcurrencyLimiter = concurrency_info["limiter"]
|
||||
key = concurrency_info["key"]
|
||||
limiter.release(key)
|
||||
@ -4,15 +4,27 @@ import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.cache import CacheException
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
|
||||
|
||||
try:
|
||||
from .__init_cache import register_cache_types
|
||||
except CacheException as e:
|
||||
raise SystemError(f"ERROR:{e}")
|
||||
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
register_cache_types()
|
||||
logger.info("缓存类型注册完成")
|
||||
|
||||
|
||||
@driver.on_bot_connect
|
||||
async def _(bot: Bot):
|
||||
"""将bot已存在的群组添加群认证
|
||||
|
||||
35
zhenxun/builtin_plugins/init/__init_cache.py
Normal file
35
zhenxun/builtin_plugins/init/__init_cache.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""
|
||||
缓存初始化模块
|
||||
|
||||
负责注册各种缓存类型,实现按需缓存机制
|
||||
"""
|
||||
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.cache import CacheRegistry, cache_config
|
||||
from zhenxun.services.cache.config import CacheMode
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import CacheType
|
||||
|
||||
|
||||
# 注册缓存类型
|
||||
def register_cache_types():
|
||||
"""注册所有缓存类型"""
|
||||
CacheRegistry.register(CacheType.PLUGINS, PluginInfo)
|
||||
CacheRegistry.register(CacheType.GROUPS, GroupConsole)
|
||||
CacheRegistry.register(CacheType.BOT, BotConsole)
|
||||
CacheRegistry.register(CacheType.USERS, UserConsole)
|
||||
CacheRegistry.register(
|
||||
CacheType.LEVEL, LevelUser, key_format="{user_id}_{group_id}"
|
||||
)
|
||||
CacheRegistry.register(CacheType.BAN, BanConsole, key_format="{user_id}_{group_id}")
|
||||
|
||||
if cache_config.cache_mode == CacheMode.NONE:
|
||||
logger.info("缓存功能已禁用,将直接从数据库获取数据")
|
||||
else:
|
||||
logger.info(f"已注册所有缓存类型,缓存模式: {cache_config.cache_mode}")
|
||||
logger.info("使用增量缓存模式,数据将按需加载到缓存中")
|
||||
@ -46,7 +46,7 @@ def _handle_config(plugin: Plugin, exists_module: list[str]):
|
||||
reg_config.value,
|
||||
help=reg_config.help,
|
||||
default_value=reg_config.default_value,
|
||||
type=reg_config.type,
|
||||
type=reg_config.type, # type: ignore
|
||||
arg_parser=reg_config.arg_parser,
|
||||
_override=False,
|
||||
)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import asyncio
|
||||
|
||||
import aiofiles
|
||||
import nonebot
|
||||
from nonebot import get_loaded_plugins
|
||||
@ -112,24 +114,29 @@ async def _():
|
||||
await _handle_setting(plugin, plugin_list, limit_list)
|
||||
create_list = []
|
||||
update_list = []
|
||||
update_task_list = []
|
||||
for plugin in plugin_list:
|
||||
if plugin.module_path not in module2id:
|
||||
create_list.append(plugin)
|
||||
else:
|
||||
plugin.id = module2id[plugin.module_path]
|
||||
await plugin.save(
|
||||
update_fields=[
|
||||
"name",
|
||||
"author",
|
||||
"version",
|
||||
"admin_level",
|
||||
"plugin_type",
|
||||
"is_show",
|
||||
]
|
||||
update_task_list.append(
|
||||
plugin.save(
|
||||
update_fields=[
|
||||
"name",
|
||||
"author",
|
||||
"version",
|
||||
"admin_level",
|
||||
"plugin_type",
|
||||
"is_show",
|
||||
]
|
||||
)
|
||||
)
|
||||
update_list.append(plugin)
|
||||
if create_list:
|
||||
await PluginInfo.bulk_create(create_list, 10)
|
||||
if update_task_list:
|
||||
await asyncio.gather(*update_task_list)
|
||||
# if update_list:
|
||||
# # TODO: 批量更新无法更新plugin_type: tortoise.exceptions.OperationalError:
|
||||
# column "superuser" does not exist
|
||||
|
||||
@ -205,7 +205,7 @@ class Manager:
|
||||
self.cd_data: dict[str, PluginCdBlock] = {}
|
||||
if self.cd_file.exists():
|
||||
with open(self.cd_file, encoding="utf8") as f:
|
||||
temp = _yaml.load(f)
|
||||
temp = _yaml.load(f) or {}
|
||||
if "PluginCdLimit" in temp.keys():
|
||||
for k, v in temp["PluginCdLimit"].items():
|
||||
if "." in k:
|
||||
@ -216,7 +216,7 @@ class Manager:
|
||||
self.block_data: dict[str, BaseBlock] = {}
|
||||
if self.block_file.exists():
|
||||
with open(self.block_file, encoding="utf8") as f:
|
||||
temp = _yaml.load(f)
|
||||
temp = _yaml.load(f) or {}
|
||||
if "PluginBlockLimit" in temp.keys():
|
||||
for k, v in temp["PluginBlockLimit"].items():
|
||||
if "." in k:
|
||||
@ -227,7 +227,7 @@ class Manager:
|
||||
self.count_data: dict[str, PluginCountBlock] = {}
|
||||
if self.count_file.exists():
|
||||
with open(self.count_file, encoding="utf8") as f:
|
||||
temp = _yaml.load(f)
|
||||
temp = _yaml.load(f) or {}
|
||||
if "PluginCountLimit" in temp.keys():
|
||||
for k, v in temp["PluginCountLimit"].items():
|
||||
if "." in k:
|
||||
|
||||
171
zhenxun/builtin_plugins/llm_manager/__init__.py
Normal file
171
zhenxun/builtin_plugins/llm_manager/__init__.py
Normal file
@ -0,0 +1,171 @@
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
Args,
|
||||
Arparma,
|
||||
Match,
|
||||
Option,
|
||||
Query,
|
||||
Subcommand,
|
||||
on_alconna,
|
||||
store_true,
|
||||
)
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .data_source import DataSource
|
||||
from .presenters import Presenters
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="LLM模型管理",
|
||||
description="查看和管理大语言模型服务。",
|
||||
usage="""
|
||||
LLM模型管理 (SUPERUSER)
|
||||
|
||||
llm list [--all]
|
||||
- 查看可用模型列表。
|
||||
- --all: 显示包括不可用在内的所有模型。
|
||||
|
||||
llm info <Provider/ModelName>
|
||||
- 查看指定模型的详细信息和能力。
|
||||
|
||||
llm default [Provider/ModelName]
|
||||
- 查看或设置全局默认模型。
|
||||
- 不带参数: 查看当前默认模型。
|
||||
- 带参数: 设置新的默认模型。
|
||||
- 例子: llm default Gemini/gemini-2.0-flash
|
||||
|
||||
llm test <Provider/ModelName>
|
||||
- 测试指定模型的连通性和API Key有效性。
|
||||
|
||||
llm keys <ProviderName>
|
||||
- 查看指定提供商的所有API Key状态。
|
||||
|
||||
llm reset-key <ProviderName> [--key <api_key>]
|
||||
- 重置提供商的所有或指定API Key的失败状态。
|
||||
""",
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="1.0.0",
|
||||
plugin_type=PluginType.SUPERUSER,
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
llm_cmd = on_alconna(
|
||||
Alconna(
|
||||
"llm",
|
||||
Subcommand("list", alias=["ls"], help_text="查看模型列表"),
|
||||
Subcommand("info", Args["model_name", str], help_text="查看模型详情"),
|
||||
Subcommand("default", Args["model_name?", str], help_text="查看或设置默认模型"),
|
||||
Subcommand(
|
||||
"test", Args["model_name", str], alias=["ping"], help_text="测试模型连通性"
|
||||
),
|
||||
Subcommand("keys", Args["provider_name", str], help_text="查看API密钥状态"),
|
||||
Subcommand(
|
||||
"reset-key",
|
||||
Args["provider_name", str],
|
||||
Option("--key", Args["api_key", str], help_text="指定要重置的API Key"),
|
||||
help_text="重置API Key状态",
|
||||
),
|
||||
Option("--all", action=store_true, help_text="显示所有条目"),
|
||||
),
|
||||
permission=SUPERUSER,
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
|
||||
@llm_cmd.assign("list")
|
||||
async def handle_list(arp: Arparma, show_all: Query[bool] = Query("all")):
|
||||
"""处理 'llm list' 命令"""
|
||||
logger.info("获取LLM模型列表", command="LLM Manage", session=arp.header_result)
|
||||
models = await DataSource.get_model_list(show_all=show_all.result)
|
||||
|
||||
image = await Presenters.format_model_list_as_image(models, show_all.result)
|
||||
await llm_cmd.finish(MessageUtils.build_message(image))
|
||||
|
||||
|
||||
@llm_cmd.assign("info")
|
||||
async def handle_info(arp: Arparma, model_name: Match[str]):
|
||||
"""处理 'llm info' 命令"""
|
||||
logger.info(
|
||||
f"获取模型详情: {model_name.result}",
|
||||
command="LLM Manage",
|
||||
session=arp.header_result,
|
||||
)
|
||||
details = await DataSource.get_model_details(model_name.result)
|
||||
if not details:
|
||||
await llm_cmd.finish(f"未找到模型: {model_name.result}")
|
||||
|
||||
image_bytes = await Presenters.format_model_details_as_markdown_image(details)
|
||||
await llm_cmd.finish(MessageUtils.build_message(image_bytes))
|
||||
|
||||
|
||||
@llm_cmd.assign("default")
|
||||
async def handle_default(arp: Arparma, model_name: Match[str]):
|
||||
"""处理 'llm default' 命令"""
|
||||
if model_name.available:
|
||||
logger.info(
|
||||
f"设置默认模型为: {model_name.result}",
|
||||
command="LLM Manage",
|
||||
session=arp.header_result,
|
||||
)
|
||||
success, message = await DataSource.set_default_model(model_name.result)
|
||||
await llm_cmd.finish(message)
|
||||
else:
|
||||
logger.info("查看默认模型", command="LLM Manage", session=arp.header_result)
|
||||
current_default = await DataSource.get_default_model()
|
||||
await llm_cmd.finish(f"当前全局默认模型为: {current_default or '未设置'}")
|
||||
|
||||
|
||||
@llm_cmd.assign("test")
|
||||
async def handle_test(arp: Arparma, model_name: Match[str]):
|
||||
"""处理 'llm test' 命令"""
|
||||
logger.info(
|
||||
f"测试模型连通性: {model_name.result}",
|
||||
command="LLM Manage",
|
||||
session=arp.header_result,
|
||||
)
|
||||
await llm_cmd.send(f"正在测试模型 '{model_name.result}',请稍候...")
|
||||
|
||||
success, message = await DataSource.test_model_connectivity(model_name.result)
|
||||
await llm_cmd.finish(message)
|
||||
|
||||
|
||||
@llm_cmd.assign("keys")
|
||||
async def handle_keys(arp: Arparma, provider_name: Match[str]):
|
||||
"""处理 'llm keys' 命令"""
|
||||
logger.info(
|
||||
f"查看提供商API Key状态: {provider_name.result}",
|
||||
command="LLM Manage",
|
||||
session=arp.header_result,
|
||||
)
|
||||
sorted_stats = await DataSource.get_key_status(provider_name.result)
|
||||
if not sorted_stats:
|
||||
await llm_cmd.finish(
|
||||
f"未找到提供商 '{provider_name.result}' 或其没有配置API Keys。"
|
||||
)
|
||||
|
||||
image = await Presenters.format_key_status_as_image(
|
||||
provider_name.result, sorted_stats
|
||||
)
|
||||
await llm_cmd.finish(MessageUtils.build_message(image))
|
||||
|
||||
|
||||
@llm_cmd.assign("reset-key")
|
||||
async def handle_reset_key(
|
||||
arp: Arparma, provider_name: Match[str], api_key: Match[str]
|
||||
):
|
||||
"""处理 'llm reset-key' 命令"""
|
||||
key_to_reset = api_key.result if api_key.available else None
|
||||
log_msg = f"重置 {provider_name.result} 的 " + (
|
||||
"指定API Key" if key_to_reset else "所有API Keys"
|
||||
)
|
||||
logger.info(log_msg, command="LLM Manage", session=arp.header_result)
|
||||
|
||||
success, message = await DataSource.reset_key(provider_name.result, key_to_reset)
|
||||
await llm_cmd.finish(message)
|
||||
121
zhenxun/builtin_plugins/llm_manager/data_source.py
Normal file
121
zhenxun/builtin_plugins/llm_manager/data_source.py
Normal file
@ -0,0 +1,121 @@
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services.llm import (
|
||||
LLMException,
|
||||
get_global_default_model_name,
|
||||
get_model_instance,
|
||||
list_available_models,
|
||||
set_global_default_model_name,
|
||||
)
|
||||
from zhenxun.services.llm.core import KeyStatus
|
||||
from zhenxun.services.llm.manager import (
|
||||
reset_key_status,
|
||||
)
|
||||
from zhenxun.services.llm.types import LLMMessage
|
||||
|
||||
|
||||
class DataSource:
|
||||
"""LLM管理插件的数据源和业务逻辑"""
|
||||
|
||||
@staticmethod
|
||||
async def get_model_list(show_all: bool = False) -> list[dict[str, Any]]:
|
||||
"""获取模型列表"""
|
||||
models = list_available_models()
|
||||
if show_all:
|
||||
return models
|
||||
return [m for m in models if m.get("is_available", True)]
|
||||
|
||||
@staticmethod
|
||||
async def get_model_details(model_name_str: str) -> dict[str, Any] | None:
|
||||
"""获取指定模型的详细信息"""
|
||||
try:
|
||||
model = await get_model_instance(model_name_str)
|
||||
return {
|
||||
"provider_config": model.provider_config,
|
||||
"model_detail": model.model_detail,
|
||||
"capabilities": model.capabilities,
|
||||
}
|
||||
except LLMException:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def get_default_model() -> str | None:
|
||||
"""获取全局默认模型"""
|
||||
return get_global_default_model_name()
|
||||
|
||||
@staticmethod
|
||||
async def set_default_model(model_name_str: str) -> tuple[bool, str]:
|
||||
"""设置全局默认模型"""
|
||||
success = set_global_default_model_name(model_name_str)
|
||||
if success:
|
||||
return True, f"✅ 成功将默认模型设置为: {model_name_str}"
|
||||
else:
|
||||
return False, f"❌ 设置失败,模型 '{model_name_str}' 不存在或无效。"
|
||||
|
||||
@staticmethod
|
||||
async def test_model_connectivity(model_name_str: str) -> tuple[bool, str]:
|
||||
"""测试模型连通性"""
|
||||
start_time = time.monotonic()
|
||||
try:
|
||||
async with await get_model_instance(model_name_str) as model:
|
||||
await model.generate_response([LLMMessage.user("你好")])
|
||||
end_time = time.monotonic()
|
||||
latency = (end_time - start_time) * 1000
|
||||
return (
|
||||
True,
|
||||
f"✅ 模型 '{model_name_str}' 连接成功!\n响应延迟: {latency:.2f} ms",
|
||||
)
|
||||
except LLMException as e:
|
||||
return (
|
||||
False,
|
||||
f"❌ 模型 '{model_name_str}' 连接测试失败:\n"
|
||||
f"{e.user_friendly_message}\n错误码: {e.code.name}",
|
||||
)
|
||||
except Exception as e:
|
||||
return False, f"❌ 测试时发生未知错误: {e!s}"
|
||||
|
||||
@staticmethod
|
||||
async def get_key_status(provider_name: str) -> list[dict[str, Any]] | None:
|
||||
"""获取并排序指定提供商的API Key状态"""
|
||||
from zhenxun.services.llm.manager import get_key_usage_stats
|
||||
|
||||
all_stats = await get_key_usage_stats()
|
||||
provider_stats = all_stats.get(provider_name)
|
||||
|
||||
if not provider_stats or not provider_stats.get("key_stats"):
|
||||
return None
|
||||
|
||||
key_stats_dict = provider_stats["key_stats"]
|
||||
|
||||
stats_list = [
|
||||
{"key_id": key_id, **stats} for key_id, stats in key_stats_dict.items()
|
||||
]
|
||||
|
||||
def sort_key(item: dict[str, Any]):
|
||||
status_priority = item.get("status_enum", KeyStatus.UNUSED).value
|
||||
return (
|
||||
status_priority,
|
||||
100 - item.get("success_rate", 100.0),
|
||||
-item.get("total_calls", 0),
|
||||
)
|
||||
|
||||
sorted_stats_list = sorted(stats_list, key=sort_key)
|
||||
|
||||
return sorted_stats_list
|
||||
|
||||
@staticmethod
|
||||
async def reset_key(provider_name: str, api_key: str | None) -> tuple[bool, str]:
|
||||
"""重置API Key状态"""
|
||||
success = await reset_key_status(provider_name, api_key)
|
||||
if success:
|
||||
if api_key:
|
||||
if len(api_key) > 8:
|
||||
target = f"API Key '{api_key[:4]}...{api_key[-4:]}'"
|
||||
else:
|
||||
target = f"API Key '{api_key}'"
|
||||
else:
|
||||
target = "所有API Keys"
|
||||
return True, f"✅ 成功重置提供商 '{provider_name}' 的 {target} 的状态。"
|
||||
else:
|
||||
return False, "❌ 重置失败,请检查提供商名称或API Key是否正确。"
|
||||
204
zhenxun/builtin_plugins/llm_manager/presenters.py
Normal file
204
zhenxun/builtin_plugins/llm_manager/presenters.py
Normal file
@ -0,0 +1,204 @@
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services.llm.core import KeyStatus
|
||||
from zhenxun.services.llm.types import ModelModality
|
||||
from zhenxun.utils._build_image import BuildImage
|
||||
from zhenxun.utils._image_template import ImageTemplate, Markdown, RowStyle
|
||||
|
||||
|
||||
def _format_seconds(seconds: int) -> str:
|
||||
"""将秒数格式化为 'Xm Ys' 或 'Xh Ym' 的形式"""
|
||||
if seconds <= 0:
|
||||
return "0s"
|
||||
if seconds < 60:
|
||||
return f"{seconds}s"
|
||||
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
if minutes < 60:
|
||||
return f"{minutes}m {seconds}s"
|
||||
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
return f"{hours}h {minutes}m"
|
||||
|
||||
|
||||
class Presenters:
|
||||
"""格式化LLM管理插件的输出 (图片格式)"""
|
||||
|
||||
@staticmethod
|
||||
async def format_model_list_as_image(
|
||||
models: list[dict[str, Any]], show_all: bool
|
||||
) -> BuildImage:
|
||||
"""将模型列表格式化为表格图片"""
|
||||
title = "📋 LLM模型列表" + (" (所有已配置模型)" if show_all else " (仅可用)")
|
||||
|
||||
if not models:
|
||||
return await BuildImage.build_text_image(
|
||||
f"{title}\n\n当前没有配置任何LLM模型。"
|
||||
)
|
||||
|
||||
column_name = ["提供商", "模型名称", "API类型", "状态"]
|
||||
data_list = []
|
||||
for model in models:
|
||||
status_text = "✅ 可用" if model.get("is_available", True) else "❌ 不可用"
|
||||
embed_tag = " (Embed)" if model.get("is_embedding_model", False) else ""
|
||||
data_list.append(
|
||||
[
|
||||
model.get("provider_name", "N/A"),
|
||||
f"{model.get('model_name', 'N/A')}{embed_tag}",
|
||||
model.get("api_type", "N/A"),
|
||||
status_text,
|
||||
]
|
||||
)
|
||||
|
||||
return await ImageTemplate.table_page(
|
||||
head_text=title,
|
||||
tip_text="使用 `llm info <Provider/ModelName>` 查看详情",
|
||||
column_name=column_name,
|
||||
data_list=data_list,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def format_model_details_as_markdown_image(details: dict[str, Any]) -> bytes:
|
||||
"""将模型详情格式化为Markdown图片"""
|
||||
provider = details["provider_config"]
|
||||
model = details["model_detail"]
|
||||
caps = details["capabilities"]
|
||||
|
||||
cap_list = []
|
||||
if ModelModality.IMAGE in caps.input_modalities:
|
||||
cap_list.append("视觉")
|
||||
if ModelModality.VIDEO in caps.input_modalities:
|
||||
cap_list.append("视频")
|
||||
if ModelModality.AUDIO in caps.input_modalities:
|
||||
cap_list.append("音频")
|
||||
if caps.supports_tool_calling:
|
||||
cap_list.append("工具调用")
|
||||
if caps.is_embedding_model:
|
||||
cap_list.append("文本嵌入")
|
||||
|
||||
md = Markdown()
|
||||
md.head(f"🔎 模型详情: {provider.name}/{model.model_name}", level=1)
|
||||
md.text("---")
|
||||
md.head("提供商信息", level=2)
|
||||
md.list(
|
||||
[
|
||||
f"**名称**: {provider.name}",
|
||||
f"**API 类型**: {provider.api_type}",
|
||||
f"**API Base**: {provider.api_base or '默认'}",
|
||||
]
|
||||
)
|
||||
md.head("模型详情", level=2)
|
||||
|
||||
temp_value = model.temperature or provider.temperature or "未设置"
|
||||
token_value = model.max_tokens or provider.max_tokens or "未设置"
|
||||
|
||||
md.list(
|
||||
[
|
||||
f"**名称**: {model.model_name}",
|
||||
f"**默认温度**: {temp_value}",
|
||||
f"**最大Token**: {token_value}",
|
||||
f"**核心能力**: {', '.join(cap_list) or '纯文本'}",
|
||||
]
|
||||
)
|
||||
|
||||
return await md.build()
|
||||
|
||||
@staticmethod
|
||||
async def format_key_status_as_image(
|
||||
provider_name: str, sorted_stats: list[dict[str, Any]]
|
||||
) -> BuildImage:
|
||||
"""将已排序的、详细的API Key状态格式化为表格图片"""
|
||||
title = f"🔑 '{provider_name}' API Key 状态"
|
||||
|
||||
if not sorted_stats:
|
||||
return await BuildImage.build_text_image(
|
||||
f"{title}\n\n该提供商没有配置API Keys。"
|
||||
)
|
||||
|
||||
def _status_row_style(column: str, text: str) -> RowStyle:
|
||||
style = RowStyle()
|
||||
if column == "状态":
|
||||
if "✅ 健康" in text:
|
||||
style.font_color = "#67C23A"
|
||||
elif "⚠️ 告警" in text:
|
||||
style.font_color = "#E6A23C"
|
||||
elif "❌ 错误" in text or "🚫" in text:
|
||||
style.font_color = "#F56C6C"
|
||||
elif "❄️ 冷却中" in text:
|
||||
style.font_color = "#409EFF"
|
||||
elif column == "成功率":
|
||||
try:
|
||||
if text != "N/A":
|
||||
rate = float(text.replace("%", ""))
|
||||
if rate < 80:
|
||||
style.font_color = "#F56C6C"
|
||||
elif rate < 95:
|
||||
style.font_color = "#E6A23C"
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return style
|
||||
|
||||
column_name = [
|
||||
"Key (部分)",
|
||||
"状态",
|
||||
"总调用",
|
||||
"成功率",
|
||||
"平均延迟(s)",
|
||||
"上次错误",
|
||||
"建议操作",
|
||||
]
|
||||
data_list = []
|
||||
|
||||
for key_info in sorted_stats:
|
||||
status_enum: KeyStatus = key_info["status_enum"]
|
||||
|
||||
if status_enum == KeyStatus.COOLDOWN:
|
||||
cooldown_seconds = int(key_info["cooldown_seconds_left"])
|
||||
formatted_time = _format_seconds(cooldown_seconds)
|
||||
status_text = f"❄️ 冷却中({formatted_time})"
|
||||
else:
|
||||
status_text = {
|
||||
KeyStatus.DISABLED: "🚫 永久禁用",
|
||||
KeyStatus.ERROR: "❌ 错误",
|
||||
KeyStatus.WARNING: "⚠️ 告警",
|
||||
KeyStatus.HEALTHY: "✅ 健康",
|
||||
KeyStatus.UNUSED: "⚪️ 未使用",
|
||||
}.get(status_enum, "❔ 未知")
|
||||
|
||||
total_calls = key_info["total_calls"]
|
||||
total_calls_text = (
|
||||
f"{key_info['success_count']}/{total_calls}"
|
||||
if total_calls > 0
|
||||
else "0/0"
|
||||
)
|
||||
|
||||
success_rate = key_info["success_rate"]
|
||||
success_rate_text = f"{success_rate:.1f}%" if total_calls > 0 else "N/A"
|
||||
|
||||
avg_latency = key_info["avg_latency"]
|
||||
avg_latency_text = f"{avg_latency / 1000:.2f}" if avg_latency > 0 else "N/A"
|
||||
|
||||
last_error = key_info.get("last_error") or "-"
|
||||
if len(last_error) > 25:
|
||||
last_error = last_error[:22] + "..."
|
||||
|
||||
data_list.append(
|
||||
[
|
||||
key_info["key_id"],
|
||||
status_text,
|
||||
total_calls_text,
|
||||
success_rate_text,
|
||||
avg_latency_text,
|
||||
last_error,
|
||||
key_info["suggested_action"],
|
||||
]
|
||||
)
|
||||
|
||||
return await ImageTemplate.table_page(
|
||||
head_text=title,
|
||||
tip_text="使用 `llm reset-key <Provider>` 重置Key状态",
|
||||
column_name=column_name,
|
||||
data_list=data_list,
|
||||
text_style=_status_row_style,
|
||||
column_space=15,
|
||||
)
|
||||
@ -275,7 +275,9 @@ async def _(bot: Bot, session: Uninfo):
|
||||
await GroupInfoUser.set_user_nickname(session.user.id, group_id, "")
|
||||
else:
|
||||
await FriendUser.set_user_nickname(session.user.id, "")
|
||||
await BanConsole.ban(session.user.id, group_id, 9, 60, bot.self_id)
|
||||
await BanConsole.ban(
|
||||
session.user.id, group_id, 9, "用户昵称违规", 60, bot.self_id
|
||||
)
|
||||
return
|
||||
else:
|
||||
await MessageUtils.build_message("你在做梦吗?你没有昵称啊").finish(
|
||||
|
||||
@ -54,22 +54,6 @@ __plugin_meta__ = PluginMetadata(
|
||||
default_value=5,
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
module="_task",
|
||||
key="DEFAULT_GROUP_WELCOME",
|
||||
value=True,
|
||||
help="被动 进群欢迎 进群默认开关状态",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
),
|
||||
RegisterConfig(
|
||||
module="_task",
|
||||
key="DEFAULT_REFUND_GROUP_REMIND",
|
||||
value=True,
|
||||
help="被动 退群提醒 进群默认开关状态",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
),
|
||||
],
|
||||
tasks=[
|
||||
Task(
|
||||
|
||||
@ -10,7 +10,7 @@ from nonebot_plugin_uninfo import Uninfo
|
||||
import ujson as json
|
||||
|
||||
from zhenxun.builtin_plugins.platform.qq.exception import ForceAddGroupError
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
|
||||
from zhenxun.models.fg_request import FgRequest
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
@ -20,6 +20,7 @@ from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.enum import RequestHandleType
|
||||
from zhenxun.utils.manager.bot_profile_manager import BotProfileManager
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.utils import FreqLimiter
|
||||
@ -55,15 +56,17 @@ class GroupManager:
|
||||
if plugin_list := await PluginInfo.filter(default_status=False).all():
|
||||
for plugin in plugin_list:
|
||||
block_plugin += f"<{plugin.module},"
|
||||
group_info = await bot.get_group_info(group_id=group_id, no_cache=True)
|
||||
await GroupConsole.create(
|
||||
group_info = await bot.get_group_info(group_id=group_id)
|
||||
await GroupConsole.update_or_create(
|
||||
group_id=group_info["group_id"],
|
||||
group_name=group_info["group_name"],
|
||||
max_member_count=group_info["max_member_count"],
|
||||
member_count=group_info["member_count"],
|
||||
group_flag=1,
|
||||
block_plugin=block_plugin,
|
||||
platform="qq",
|
||||
defaults={
|
||||
"group_name": group_info["group_name"],
|
||||
"max_member_count": group_info["max_member_count"],
|
||||
"member_count": group_info["member_count"],
|
||||
"group_flag": 1,
|
||||
"block_plugin": block_plugin,
|
||||
"platform": "qq",
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@ -145,12 +148,23 @@ class GroupManager:
|
||||
e=e,
|
||||
)
|
||||
raise ForceAddGroupError("强制拉群或未有群信息,退出群聊失败...") from e
|
||||
await GroupConsole.filter(group_id=group_id).delete()
|
||||
# await GroupConsole.filter(group_id=group_id).delete()
|
||||
raise ForceAddGroupError(f"触发强制入群保护,已成功退出群聊 {group_id}...")
|
||||
else:
|
||||
await cls.__handle_add_group(bot, group_id, group)
|
||||
"""刷新群管理员权限"""
|
||||
await cls.__refresh_level(bot, group_id)
|
||||
if BotProfileManager.is_auto_send_profile():
|
||||
file_path = await BotProfileManager.build_bot_profile_image(bot.self_id)
|
||||
if file_path:
|
||||
await MessageUtils.build_message(
|
||||
[
|
||||
f"嗨,大家好,我是{BotConfig.self_nickname}, "
|
||||
"希望我们可以友好相处(眨眼眨眼)!",
|
||||
file_path,
|
||||
]
|
||||
).send()
|
||||
logger.info("加入群组自动发送BOT自我介绍图片", session=group_id)
|
||||
|
||||
@classmethod
|
||||
def get_path(cls, session: Uninfo) -> Path | None:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from nonebot.message import run_preprocessor
|
||||
from nonebot import on_message
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.models.friend_user import FriendUser
|
||||
@ -8,24 +8,27 @@ from zhenxun.services.log import logger
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
|
||||
@run_preprocessor
|
||||
async def do_something(session: Uninfo):
|
||||
def rule(session: Uninfo) -> bool:
|
||||
return PlatformUtils.is_qbot(session)
|
||||
|
||||
|
||||
_matcher = on_message(priority=999, block=False, rule=rule)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: Uninfo):
|
||||
platform = PlatformUtils.get_platform(session)
|
||||
if session.group:
|
||||
if not await GroupConsole.exists(group_id=session.group.id):
|
||||
await GroupConsole.create(group_id=session.group.id)
|
||||
logger.info("添加当前群组ID信息" "", session=session)
|
||||
|
||||
if not await GroupInfoUser.exists(
|
||||
user_id=session.user.id, group_id=session.group.id
|
||||
):
|
||||
await GroupInfoUser.create(
|
||||
user_id=session.user.id, group_id=session.group.id, platform=platform
|
||||
)
|
||||
logger.info("添加当前用户群组ID信息", "", session=session)
|
||||
logger.info("添加当前群组ID信息", session=session)
|
||||
await GroupInfoUser.update_or_create(
|
||||
user_id=session.user.id,
|
||||
group_id=session.group.id,
|
||||
platform=PlatformUtils.get_platform(session),
|
||||
)
|
||||
elif not await FriendUser.exists(user_id=session.user.id, platform=platform):
|
||||
try:
|
||||
await FriendUser.create(user_id=session.user.id, platform=platform)
|
||||
logger.info("添加当前好友用户信息", "", session=session)
|
||||
except Exception as e:
|
||||
logger.error("添加当前好友用户信息失败", session=session, e=e)
|
||||
await FriendUser.create(
|
||||
user_id=session.user.id, platform=PlatformUtils.get_platform(session)
|
||||
)
|
||||
logger.info("添加当前好友用户信息", "", session=session)
|
||||
|
||||
@ -198,7 +198,9 @@ class StoreManager:
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
db_plugin_list = await cls.get_loaded_plugins("module")
|
||||
plugin_info = next(p for p in plugin_list if p.module == plugin_key)
|
||||
plugin_info = next((p for p in plugin_list if p.module == plugin_key), None)
|
||||
if plugin_info is None:
|
||||
return f"未找到插件 {plugin_key}"
|
||||
if plugin_info.module in [p[0] for p in db_plugin_list]:
|
||||
return f"插件 {plugin_info.name} 已安装,无需重复安装"
|
||||
is_external = True
|
||||
@ -307,7 +309,9 @@ class StoreManager:
|
||||
plugin_key = await cls._resolve_plugin_key(plugin_id)
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
plugin_info = next(p for p in plugin_list if p.module == plugin_key)
|
||||
plugin_info = next((p for p in plugin_list if p.module == plugin_key), None)
|
||||
if plugin_info is None:
|
||||
return f"未找到插件 {plugin_key}"
|
||||
path = BASE_PATH
|
||||
if plugin_info.github_url:
|
||||
path = BASE_PATH / "plugins"
|
||||
@ -383,7 +387,9 @@ class StoreManager:
|
||||
plugin_key = await cls._resolve_plugin_key(plugin_id)
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
plugin_info = next(p for p in plugin_list if p.module == plugin_key)
|
||||
plugin_info = next((p for p in plugin_list if p.module == plugin_key), None)
|
||||
if plugin_info is None:
|
||||
return f"未找到插件 {plugin_key}"
|
||||
logger.info(f"尝试更新插件 {plugin_info.name}", LOG_COMMAND)
|
||||
db_plugin_list = await cls.get_loaded_plugins("module", "version")
|
||||
suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.utils.enum import PluginType
|
||||
|
||||
from . import command # noqa: F401
|
||||
from . import commands, handlers
|
||||
|
||||
__all__ = ["commands", "handlers"]
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="定时任务管理",
|
||||
@ -27,6 +29,8 @@ __plugin_meta__ = PluginMetadata(
|
||||
定时任务 恢复 <任务ID> | -p <插件> [-g <群号>] | -all
|
||||
定时任务 执行 <任务ID>
|
||||
定时任务 更新 <任务ID> [时间选项] [--kwargs <参数>]
|
||||
# [修改] 增加说明
|
||||
• 说明: -p 选项可单独使用,用于操作指定插件的所有任务
|
||||
|
||||
📝 时间选项 (三选一):
|
||||
--cron "<分> <时> <日> <月> <周>" # 例: --cron "0 8 * * *"
|
||||
@ -47,5 +51,35 @@ __plugin_meta__ = PluginMetadata(
|
||||
version="0.1.2",
|
||||
plugin_type=PluginType.SUPERUSER,
|
||||
is_show=False,
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
module="SchedulerManager",
|
||||
key="ALL_GROUPS_CONCURRENCY_LIMIT",
|
||||
value=5,
|
||||
help="“所有群组”类型定时任务的并发执行数量限制",
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
module="SchedulerManager",
|
||||
key="JOB_MAX_RETRIES",
|
||||
value=2,
|
||||
help="定时任务执行失败时的最大重试次数",
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
module="SchedulerManager",
|
||||
key="JOB_RETRY_DELAY",
|
||||
value=10,
|
||||
help="定时任务执行重试的间隔时间(秒)",
|
||||
type=int,
|
||||
),
|
||||
RegisterConfig(
|
||||
module="SchedulerManager",
|
||||
key="SCHEDULER_TIMEZONE",
|
||||
value="Asia/Shanghai",
|
||||
help="定时任务使用的时区,默认为 Asia/Shanghai",
|
||||
type=str,
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
@ -1,836 +0,0 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.adapters.onebot.v11 import Bot
|
||||
from nonebot.params import Depends
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
AlconnaMatch,
|
||||
Args,
|
||||
Arparma,
|
||||
Match,
|
||||
Option,
|
||||
Query,
|
||||
Subcommand,
|
||||
on_alconna,
|
||||
)
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from zhenxun.utils._image_template import ImageTemplate
|
||||
from zhenxun.utils.manager.schedule_manager import scheduler_manager
|
||||
|
||||
|
||||
def _get_type_name(annotation) -> str:
|
||||
"""获取类型注解的名称"""
|
||||
if hasattr(annotation, "__name__"):
|
||||
return annotation.__name__
|
||||
elif hasattr(annotation, "_name"):
|
||||
return annotation._name
|
||||
else:
|
||||
return str(annotation)
|
||||
|
||||
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.rules import admin_check
|
||||
|
||||
|
||||
def _format_trigger(schedule_status: dict) -> str:
|
||||
"""将触发器配置格式化为人类可读的字符串"""
|
||||
trigger_type = schedule_status["trigger_type"]
|
||||
config = schedule_status["trigger_config"]
|
||||
|
||||
if trigger_type == "cron":
|
||||
minute = config.get("minute", "*")
|
||||
hour = config.get("hour", "*")
|
||||
day = config.get("day", "*")
|
||||
month = config.get("month", "*")
|
||||
day_of_week = config.get("day_of_week", "*")
|
||||
|
||||
if day == "*" and month == "*" and day_of_week == "*":
|
||||
formatted_hour = hour if hour == "*" else f"{int(hour):02d}"
|
||||
formatted_minute = minute if minute == "*" else f"{int(minute):02d}"
|
||||
return f"每天 {formatted_hour}:{formatted_minute}"
|
||||
else:
|
||||
return f"Cron: {minute} {hour} {day} {month} {day_of_week}"
|
||||
elif trigger_type == "interval":
|
||||
seconds = config.get("seconds", 0)
|
||||
minutes = config.get("minutes", 0)
|
||||
hours = config.get("hours", 0)
|
||||
days = config.get("days", 0)
|
||||
if days:
|
||||
trigger_str = f"每 {days} 天"
|
||||
elif hours:
|
||||
trigger_str = f"每 {hours} 小时"
|
||||
elif minutes:
|
||||
trigger_str = f"每 {minutes} 分钟"
|
||||
else:
|
||||
trigger_str = f"每 {seconds} 秒"
|
||||
elif trigger_type == "date":
|
||||
run_date = config.get("run_date", "未知时间")
|
||||
trigger_str = f"在 {run_date}"
|
||||
else:
|
||||
trigger_str = f"{trigger_type}: {config}"
|
||||
|
||||
return trigger_str
|
||||
|
||||
|
||||
def _format_params(schedule_status: dict) -> str:
|
||||
"""将任务参数格式化为人类可读的字符串"""
|
||||
if kwargs := schedule_status.get("job_kwargs"):
|
||||
kwargs_str = " | ".join(f"{k}: {v}" for k, v in kwargs.items())
|
||||
return kwargs_str
|
||||
return "-"
|
||||
|
||||
|
||||
def _parse_interval(interval_str: str) -> dict:
|
||||
"""增强版解析器,支持 d(天)"""
|
||||
match = re.match(r"(\d+)([smhd])", interval_str.lower())
|
||||
if not match:
|
||||
raise ValueError("时间间隔格式错误, 请使用如 '30m', '2h', '1d', '10s' 的格式。")
|
||||
|
||||
value, unit = int(match.group(1)), match.group(2)
|
||||
if unit == "s":
|
||||
return {"seconds": value}
|
||||
if unit == "m":
|
||||
return {"minutes": value}
|
||||
if unit == "h":
|
||||
return {"hours": value}
|
||||
if unit == "d":
|
||||
return {"days": value}
|
||||
return {}
|
||||
|
||||
|
||||
def _parse_daily_time(time_str: str) -> dict:
|
||||
"""解析 HH:MM 或 HH:MM:SS 格式的时间为 cron 配置"""
|
||||
if match := re.match(r"^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$", time_str):
|
||||
hour, minute, second = match.groups()
|
||||
hour, minute = int(hour), int(minute)
|
||||
|
||||
if not (0 <= hour <= 23 and 0 <= minute <= 59):
|
||||
raise ValueError("小时或分钟数值超出范围。")
|
||||
|
||||
cron_config = {
|
||||
"minute": str(minute),
|
||||
"hour": str(hour),
|
||||
"day": "*",
|
||||
"month": "*",
|
||||
"day_of_week": "*",
|
||||
}
|
||||
if second is not None:
|
||||
if not (0 <= int(second) <= 59):
|
||||
raise ValueError("秒数值超出范围。")
|
||||
cron_config["second"] = str(second)
|
||||
|
||||
return cron_config
|
||||
else:
|
||||
raise ValueError("时间格式错误,请使用 'HH:MM' 或 'HH:MM:SS' 格式。")
|
||||
|
||||
|
||||
async def GetBotId(
|
||||
bot: Bot,
|
||||
bot_id_match: Match[str] = AlconnaMatch("bot_id"),
|
||||
) -> str:
|
||||
"""获取要操作的Bot ID"""
|
||||
if bot_id_match.available:
|
||||
return bot_id_match.result
|
||||
return bot.self_id
|
||||
|
||||
|
||||
class ScheduleTarget:
|
||||
"""定时任务操作目标的基类"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TargetByID(ScheduleTarget):
|
||||
"""按任务ID操作"""
|
||||
|
||||
def __init__(self, id: int):
|
||||
self.id = id
|
||||
|
||||
|
||||
class TargetByPlugin(ScheduleTarget):
|
||||
"""按插件名操作"""
|
||||
|
||||
def __init__(
|
||||
self, plugin: str, group_id: str | None = None, all_groups: bool = False
|
||||
):
|
||||
self.plugin = plugin
|
||||
self.group_id = group_id
|
||||
self.all_groups = all_groups
|
||||
|
||||
|
||||
class TargetAll(ScheduleTarget):
|
||||
"""操作所有任务"""
|
||||
|
||||
def __init__(self, for_group: str | None = None):
|
||||
self.for_group = for_group
|
||||
|
||||
|
||||
TargetScope = TargetByID | TargetByPlugin | TargetAll | None
|
||||
|
||||
|
||||
def create_target_parser(subcommand_name: str):
|
||||
"""
|
||||
创建一个依赖注入函数,用于解析删除、暂停、恢复等命令的操作目标。
|
||||
"""
|
||||
|
||||
async def dependency(
|
||||
event: Event,
|
||||
schedule_id: Match[int] = AlconnaMatch("schedule_id"),
|
||||
plugin_name: Match[str] = AlconnaMatch("plugin_name"),
|
||||
group_id: Match[str] = AlconnaMatch("group_id"),
|
||||
all_enabled: Query[bool] = Query(f"{subcommand_name}.all"),
|
||||
) -> TargetScope:
|
||||
if schedule_id.available:
|
||||
return TargetByID(schedule_id.result)
|
||||
|
||||
if plugin_name.available:
|
||||
p_name = plugin_name.result
|
||||
if all_enabled.available:
|
||||
return TargetByPlugin(plugin=p_name, all_groups=True)
|
||||
elif group_id.available:
|
||||
gid = group_id.result
|
||||
if gid.lower() == "all":
|
||||
return TargetByPlugin(plugin=p_name, all_groups=True)
|
||||
return TargetByPlugin(plugin=p_name, group_id=gid)
|
||||
else:
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
if current_group_id:
|
||||
return TargetByPlugin(plugin=p_name, group_id=str(current_group_id))
|
||||
else:
|
||||
await schedule_cmd.finish(
|
||||
"私聊中操作插件任务必须使用 -g <群号> 或 -all 选项。"
|
||||
)
|
||||
|
||||
if all_enabled.available:
|
||||
return TargetAll(for_group=group_id.result if group_id.available else None)
|
||||
|
||||
return None
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
schedule_cmd = on_alconna(
|
||||
Alconna(
|
||||
"定时任务",
|
||||
Subcommand(
|
||||
"查看",
|
||||
Option("-g", Args["target_group_id", str]),
|
||||
Option("-all", help_text="查看所有群聊 (SUPERUSER)"),
|
||||
Option("-p", Args["plugin_name", str], help_text="按插件名筛选"),
|
||||
Option("--page", Args["page", int, 1], help_text="指定页码"),
|
||||
alias=["ls", "list"],
|
||||
help_text="查看定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"设置",
|
||||
Args["plugin_name", str],
|
||||
Option("--cron", Args["cron_expr", str], help_text="设置 cron 表达式"),
|
||||
Option("--interval", Args["interval_expr", str], help_text="设置时间间隔"),
|
||||
Option("--date", Args["date_expr", str], help_text="设置特定执行日期"),
|
||||
Option(
|
||||
"--daily",
|
||||
Args["daily_expr", str],
|
||||
help_text="设置每天执行的时间 (如 08:20)",
|
||||
),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID或'all'"),
|
||||
Option("-all", help_text="对所有群生效 (等同于 -g all)"),
|
||||
Option("--kwargs", Args["kwargs_str", str], help_text="设置任务参数"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["add", "开启"],
|
||||
help_text="设置/开启一个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"删除",
|
||||
Args["schedule_id?", int],
|
||||
Option("-p", Args["plugin_name", str], help_text="指定插件名"),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID"),
|
||||
Option("-all", help_text="对所有群生效"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["del", "rm", "remove", "关闭", "取消"],
|
||||
help_text="删除一个或多个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"暂停",
|
||||
Args["schedule_id?", int],
|
||||
Option("-all", help_text="对当前群所有任务生效"),
|
||||
Option("-p", Args["plugin_name", str], help_text="指定插件名"),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID (SUPERUSER)"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["pause"],
|
||||
help_text="暂停一个或多个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"恢复",
|
||||
Args["schedule_id?", int],
|
||||
Option("-all", help_text="对当前群所有任务生效"),
|
||||
Option("-p", Args["plugin_name", str], help_text="指定插件名"),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID (SUPERUSER)"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["resume"],
|
||||
help_text="恢复一个或多个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"执行",
|
||||
Args["schedule_id", int],
|
||||
alias=["trigger", "run"],
|
||||
help_text="立即执行一次任务",
|
||||
),
|
||||
Subcommand(
|
||||
"更新",
|
||||
Args["schedule_id", int],
|
||||
Option("--cron", Args["cron_expr", str], help_text="设置 cron 表达式"),
|
||||
Option("--interval", Args["interval_expr", str], help_text="设置时间间隔"),
|
||||
Option("--date", Args["date_expr", str], help_text="设置特定执行日期"),
|
||||
Option(
|
||||
"--daily",
|
||||
Args["daily_expr", str],
|
||||
help_text="更新每天执行的时间 (如 08:20)",
|
||||
),
|
||||
Option("--kwargs", Args["kwargs_str", str], help_text="更新参数"),
|
||||
alias=["update", "modify", "修改"],
|
||||
help_text="更新任务配置",
|
||||
),
|
||||
Subcommand(
|
||||
"状态",
|
||||
Args["schedule_id", int],
|
||||
alias=["status", "info"],
|
||||
help_text="查看单个任务的详细状态",
|
||||
),
|
||||
Subcommand(
|
||||
"插件列表",
|
||||
alias=["plugins"],
|
||||
help_text="列出所有可用的插件",
|
||||
),
|
||||
),
|
||||
priority=5,
|
||||
block=True,
|
||||
rule=admin_check(1),
|
||||
)
|
||||
|
||||
schedule_cmd.shortcut(
|
||||
"任务状态",
|
||||
command="定时任务",
|
||||
arguments=["状态", "{%0}"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
@schedule_cmd.handle()
|
||||
async def _handle_time_options_mutex(arp: Arparma):
|
||||
time_options = ["cron", "interval", "date", "daily"]
|
||||
provided_options = [opt for opt in time_options if arp.query(opt) is not None]
|
||||
if len(provided_options) > 1:
|
||||
await schedule_cmd.finish(
|
||||
f"时间选项 --{', --'.join(provided_options)} 不能同时使用,请只选择一个。"
|
||||
)
|
||||
|
||||
|
||||
@schedule_cmd.assign("查看")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
target_group_id: Match[str] = AlconnaMatch("target_group_id"),
|
||||
all_groups: Query[bool] = Query("查看.all"),
|
||||
plugin_name: Match[str] = AlconnaMatch("plugin_name"),
|
||||
page: Match[int] = AlconnaMatch("page"),
|
||||
):
|
||||
is_superuser = await SUPERUSER(bot, event)
|
||||
schedules = []
|
||||
title = ""
|
||||
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
if not (all_groups.available or target_group_id.available) and not current_group_id:
|
||||
await schedule_cmd.finish("私聊中查看任务必须使用 -g <群号> 或 -all 选项。")
|
||||
|
||||
if all_groups.available:
|
||||
if not is_superuser:
|
||||
await schedule_cmd.finish("需要超级用户权限才能查看所有群组的定时任务。")
|
||||
schedules = await scheduler_manager.get_all_schedules()
|
||||
title = "所有群组的定时任务"
|
||||
elif target_group_id.available:
|
||||
if not is_superuser:
|
||||
await schedule_cmd.finish("需要超级用户权限才能查看指定群组的定时任务。")
|
||||
gid = target_group_id.result
|
||||
schedules = [
|
||||
s for s in await scheduler_manager.get_all_schedules() if s.group_id == gid
|
||||
]
|
||||
title = f"群 {gid} 的定时任务"
|
||||
else:
|
||||
gid = str(current_group_id)
|
||||
schedules = [
|
||||
s for s in await scheduler_manager.get_all_schedules() if s.group_id == gid
|
||||
]
|
||||
title = "本群的定时任务"
|
||||
|
||||
if plugin_name.available:
|
||||
schedules = [s for s in schedules if s.plugin_name == plugin_name.result]
|
||||
title += f" [插件: {plugin_name.result}]"
|
||||
|
||||
if not schedules:
|
||||
await schedule_cmd.finish("没有找到任何相关的定时任务。")
|
||||
|
||||
page_size = 15
|
||||
current_page = page.result
|
||||
total_items = len(schedules)
|
||||
total_pages = (total_items + page_size - 1) // page_size
|
||||
start_index = (current_page - 1) * page_size
|
||||
end_index = start_index + page_size
|
||||
paginated_schedules = schedules[start_index:end_index]
|
||||
|
||||
if not paginated_schedules:
|
||||
await schedule_cmd.finish("这一页没有内容了哦~")
|
||||
|
||||
status_tasks = [
|
||||
scheduler_manager.get_schedule_status(s.id) for s in paginated_schedules
|
||||
]
|
||||
all_statuses = await asyncio.gather(*status_tasks)
|
||||
data_list = [
|
||||
[
|
||||
s["id"],
|
||||
s["plugin_name"],
|
||||
s.get("bot_id") or "N/A",
|
||||
s["group_id"] or "全局",
|
||||
s["next_run_time"],
|
||||
_format_trigger(s),
|
||||
_format_params(s),
|
||||
"✔️ 已启用" if s["is_enabled"] else "⏸️ 已暂停",
|
||||
]
|
||||
for s in all_statuses
|
||||
if s
|
||||
]
|
||||
|
||||
if not data_list:
|
||||
await schedule_cmd.finish("没有找到任何相关的定时任务。")
|
||||
|
||||
img = await ImageTemplate.table_page(
|
||||
head_text=title,
|
||||
tip_text=f"第 {current_page}/{total_pages} 页,共 {total_items} 条任务",
|
||||
column_name=[
|
||||
"ID",
|
||||
"插件",
|
||||
"Bot ID",
|
||||
"群组/目标",
|
||||
"下次运行",
|
||||
"触发规则",
|
||||
"参数",
|
||||
"状态",
|
||||
],
|
||||
data_list=data_list,
|
||||
column_space=20,
|
||||
)
|
||||
await MessageUtils.build_message(img).send(reply_to=True)
|
||||
|
||||
|
||||
@schedule_cmd.assign("设置")
|
||||
async def _(
|
||||
event: Event,
|
||||
plugin_name: str,
|
||||
cron_expr: str | None = None,
|
||||
interval_expr: str | None = None,
|
||||
date_expr: str | None = None,
|
||||
daily_expr: str | None = None,
|
||||
group_id: str | None = None,
|
||||
kwargs_str: str | None = None,
|
||||
all_enabled: Query[bool] = Query("设置.all"),
|
||||
bot_id_to_operate: str = Depends(GetBotId),
|
||||
):
|
||||
if plugin_name not in scheduler_manager._registered_tasks:
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{plugin_name}' 没有注册可用的定时任务。\n"
|
||||
f"可用插件: {list(scheduler_manager._registered_tasks.keys())}"
|
||||
)
|
||||
|
||||
trigger_type = ""
|
||||
trigger_config = {}
|
||||
|
||||
try:
|
||||
if cron_expr:
|
||||
trigger_type = "cron"
|
||||
parts = cron_expr.split()
|
||||
if len(parts) != 5:
|
||||
raise ValueError("Cron 表达式必须有5个部分 (分 时 日 月 周)")
|
||||
cron_keys = ["minute", "hour", "day", "month", "day_of_week"]
|
||||
trigger_config = dict(zip(cron_keys, parts))
|
||||
elif interval_expr:
|
||||
trigger_type = "interval"
|
||||
trigger_config = _parse_interval(interval_expr)
|
||||
elif date_expr:
|
||||
trigger_type = "date"
|
||||
trigger_config = {"run_date": datetime.fromisoformat(date_expr)}
|
||||
elif daily_expr:
|
||||
trigger_type = "cron"
|
||||
trigger_config = _parse_daily_time(daily_expr)
|
||||
else:
|
||||
await schedule_cmd.finish(
|
||||
"必须提供一种时间选项: --cron, --interval, --date, 或 --daily。"
|
||||
)
|
||||
except ValueError as e:
|
||||
await schedule_cmd.finish(f"时间参数解析错误: {e}")
|
||||
|
||||
job_kwargs = {}
|
||||
if kwargs_str:
|
||||
task_meta = scheduler_manager._registered_tasks[plugin_name]
|
||||
params_model = task_meta.get("model")
|
||||
if not params_model:
|
||||
await schedule_cmd.finish(f"插件 '{plugin_name}' 不支持设置额外参数。")
|
||||
|
||||
if not (isinstance(params_model, type) and issubclass(params_model, BaseModel)):
|
||||
await schedule_cmd.finish(f"插件 '{plugin_name}' 的参数模型配置错误。")
|
||||
|
||||
raw_kwargs = {}
|
||||
try:
|
||||
for item in kwargs_str.split(","):
|
||||
key, value = item.strip().split("=", 1)
|
||||
raw_kwargs[key.strip()] = value
|
||||
except Exception as e:
|
||||
await schedule_cmd.finish(
|
||||
f"参数格式错误,请使用 'key=value,key2=value2' 格式。错误: {e}"
|
||||
)
|
||||
|
||||
try:
|
||||
model_validate = getattr(params_model, "model_validate", None)
|
||||
if not model_validate:
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{plugin_name}' 的参数模型不支持验证。"
|
||||
)
|
||||
return
|
||||
|
||||
validated_model = model_validate(raw_kwargs)
|
||||
|
||||
model_dump = getattr(validated_model, "model_dump", None)
|
||||
if not model_dump:
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{plugin_name}' 的参数模型不支持导出。"
|
||||
)
|
||||
return
|
||||
|
||||
job_kwargs = model_dump()
|
||||
except ValidationError as e:
|
||||
errors = [f" - {err['loc'][0]}: {err['msg']}" for err in e.errors()]
|
||||
error_str = "\n".join(errors)
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{plugin_name}' 的任务参数验证失败:\n{error_str}"
|
||||
)
|
||||
return
|
||||
|
||||
target_group_id: str | None
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
|
||||
if group_id and group_id.lower() == "all":
|
||||
target_group_id = "__ALL_GROUPS__"
|
||||
elif all_enabled.available:
|
||||
target_group_id = "__ALL_GROUPS__"
|
||||
elif group_id:
|
||||
target_group_id = group_id
|
||||
elif current_group_id:
|
||||
target_group_id = str(current_group_id)
|
||||
else:
|
||||
await schedule_cmd.finish(
|
||||
"私聊中设置定时任务时,必须使用 -g <群号> 或 --all 选项指定目标。"
|
||||
)
|
||||
return
|
||||
|
||||
success, msg = await scheduler_manager.add_schedule(
|
||||
plugin_name,
|
||||
target_group_id,
|
||||
trigger_type,
|
||||
trigger_config,
|
||||
job_kwargs,
|
||||
bot_id=bot_id_to_operate,
|
||||
)
|
||||
|
||||
if target_group_id == "__ALL_GROUPS__":
|
||||
target_desc = f"所有群组 (Bot: {bot_id_to_operate})"
|
||||
elif target_group_id is None:
|
||||
target_desc = "全局"
|
||||
else:
|
||||
target_desc = f"群组 {target_group_id}"
|
||||
|
||||
if success:
|
||||
await schedule_cmd.finish(f"已成功为 [{target_desc}] {msg}")
|
||||
else:
|
||||
await schedule_cmd.finish(f"为 [{target_desc}] 设置任务失败: {msg}")
|
||||
|
||||
|
||||
@schedule_cmd.assign("删除")
|
||||
async def _(
|
||||
target: TargetScope = Depends(create_target_parser("删除")),
|
||||
bot_id_to_operate: str = Depends(GetBotId),
|
||||
):
|
||||
if isinstance(target, TargetByID):
|
||||
_, message = await scheduler_manager.remove_schedule_by_id(target.id)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetByPlugin):
|
||||
p_name = target.plugin
|
||||
if p_name not in scheduler_manager.get_registered_plugins():
|
||||
await schedule_cmd.finish(f"未找到插件 '{p_name}'。")
|
||||
|
||||
if target.all_groups:
|
||||
removed_count = await scheduler_manager.remove_schedule_for_all(
|
||||
p_name, bot_id=bot_id_to_operate
|
||||
)
|
||||
message = (
|
||||
f"已取消了 {removed_count} 个群组的插件 '{p_name}' 定时任务。"
|
||||
if removed_count > 0
|
||||
else f"没有找到插件 '{p_name}' 的定时任务。"
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.remove_schedule(
|
||||
p_name, target.group_id, bot_id=bot_id_to_operate
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetAll):
|
||||
if target.for_group:
|
||||
_, message = await scheduler_manager.remove_schedules_by_group(
|
||||
target.for_group
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.remove_all_schedules()
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
else:
|
||||
await schedule_cmd.finish(
|
||||
"删除任务失败:请提供任务ID,或通过 -p <插件> 或 -all 指定要删除的任务。"
|
||||
)
|
||||
|
||||
|
||||
@schedule_cmd.assign("暂停")
|
||||
async def _(
|
||||
target: TargetScope = Depends(create_target_parser("暂停")),
|
||||
bot_id_to_operate: str = Depends(GetBotId),
|
||||
):
|
||||
if isinstance(target, TargetByID):
|
||||
_, message = await scheduler_manager.pause_schedule(target.id)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetByPlugin):
|
||||
p_name = target.plugin
|
||||
if p_name not in scheduler_manager.get_registered_plugins():
|
||||
await schedule_cmd.finish(f"未找到插件 '{p_name}'。")
|
||||
|
||||
if target.all_groups:
|
||||
_, message = await scheduler_manager.pause_schedules_by_plugin(p_name)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.pause_schedule_by_plugin_group(
|
||||
p_name, target.group_id, bot_id=bot_id_to_operate
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetAll):
|
||||
if target.for_group:
|
||||
_, message = await scheduler_manager.pause_schedules_by_group(
|
||||
target.for_group
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.pause_all_schedules()
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
else:
|
||||
await schedule_cmd.finish("请提供任务ID、使用 -p <插件> 或 -all 选项。")
|
||||
|
||||
|
||||
@schedule_cmd.assign("恢复")
|
||||
async def _(
|
||||
target: TargetScope = Depends(create_target_parser("恢复")),
|
||||
bot_id_to_operate: str = Depends(GetBotId),
|
||||
):
|
||||
if isinstance(target, TargetByID):
|
||||
_, message = await scheduler_manager.resume_schedule(target.id)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetByPlugin):
|
||||
p_name = target.plugin
|
||||
if p_name not in scheduler_manager.get_registered_plugins():
|
||||
await schedule_cmd.finish(f"未找到插件 '{p_name}'。")
|
||||
|
||||
if target.all_groups:
|
||||
_, message = await scheduler_manager.resume_schedules_by_plugin(p_name)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.resume_schedule_by_plugin_group(
|
||||
p_name, target.group_id, bot_id=bot_id_to_operate
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetAll):
|
||||
if target.for_group:
|
||||
_, message = await scheduler_manager.resume_schedules_by_group(
|
||||
target.for_group
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.resume_all_schedules()
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
else:
|
||||
await schedule_cmd.finish("请提供任务ID、使用 -p <插件> 或 -all 选项。")
|
||||
|
||||
|
||||
@schedule_cmd.assign("执行")
|
||||
async def _(schedule_id: int):
|
||||
_, message = await scheduler_manager.trigger_now(schedule_id)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
|
||||
@schedule_cmd.assign("更新")
|
||||
async def _(
|
||||
schedule_id: int,
|
||||
cron_expr: str | None = None,
|
||||
interval_expr: str | None = None,
|
||||
date_expr: str | None = None,
|
||||
daily_expr: str | None = None,
|
||||
kwargs_str: str | None = None,
|
||||
):
|
||||
if not any([cron_expr, interval_expr, date_expr, daily_expr, kwargs_str]):
|
||||
await schedule_cmd.finish(
|
||||
"请提供需要更新的时间 (--cron/--interval/--date/--daily) 或参数 (--kwargs)"
|
||||
)
|
||||
|
||||
trigger_config = None
|
||||
trigger_type = None
|
||||
try:
|
||||
if cron_expr:
|
||||
trigger_type = "cron"
|
||||
parts = cron_expr.split()
|
||||
if len(parts) != 5:
|
||||
raise ValueError("Cron 表达式必须有5个部分")
|
||||
cron_keys = ["minute", "hour", "day", "month", "day_of_week"]
|
||||
trigger_config = dict(zip(cron_keys, parts))
|
||||
elif interval_expr:
|
||||
trigger_type = "interval"
|
||||
trigger_config = _parse_interval(interval_expr)
|
||||
elif date_expr:
|
||||
trigger_type = "date"
|
||||
trigger_config = {"run_date": datetime.fromisoformat(date_expr)}
|
||||
elif daily_expr:
|
||||
trigger_type = "cron"
|
||||
trigger_config = _parse_daily_time(daily_expr)
|
||||
except ValueError as e:
|
||||
await schedule_cmd.finish(f"时间参数解析错误: {e}")
|
||||
|
||||
job_kwargs = None
|
||||
if kwargs_str:
|
||||
schedule = await scheduler_manager.get_schedule_by_id(schedule_id)
|
||||
if not schedule:
|
||||
await schedule_cmd.finish(f"未找到 ID 为 {schedule_id} 的任务。")
|
||||
|
||||
task_meta = scheduler_manager._registered_tasks.get(schedule.plugin_name)
|
||||
if not task_meta or not (params_model := task_meta.get("model")):
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{schedule.plugin_name}' 未定义参数模型,无法更新参数。"
|
||||
)
|
||||
|
||||
if not (isinstance(params_model, type) and issubclass(params_model, BaseModel)):
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{schedule.plugin_name}' 的参数模型配置错误。"
|
||||
)
|
||||
|
||||
raw_kwargs = {}
|
||||
try:
|
||||
for item in kwargs_str.split(","):
|
||||
key, value = item.strip().split("=", 1)
|
||||
raw_kwargs[key.strip()] = value
|
||||
except Exception as e:
|
||||
await schedule_cmd.finish(
|
||||
f"参数格式错误,请使用 'key=value,key2=value2' 格式。错误: {e}"
|
||||
)
|
||||
|
||||
try:
|
||||
model_validate = getattr(params_model, "model_validate", None)
|
||||
if not model_validate:
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{schedule.plugin_name}' 的参数模型不支持验证。"
|
||||
)
|
||||
return
|
||||
|
||||
validated_model = model_validate(raw_kwargs)
|
||||
|
||||
model_dump = getattr(validated_model, "model_dump", None)
|
||||
if not model_dump:
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{schedule.plugin_name}' 的参数模型不支持导出。"
|
||||
)
|
||||
return
|
||||
|
||||
job_kwargs = model_dump(exclude_unset=True)
|
||||
except ValidationError as e:
|
||||
errors = [f" - {err['loc'][0]}: {err['msg']}" for err in e.errors()]
|
||||
error_str = "\n".join(errors)
|
||||
await schedule_cmd.finish(f"更新的参数验证失败:\n{error_str}")
|
||||
return
|
||||
|
||||
_, message = await scheduler_manager.update_schedule(
|
||||
schedule_id, trigger_type, trigger_config, job_kwargs
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
|
||||
@schedule_cmd.assign("插件列表")
|
||||
async def _():
|
||||
registered_plugins = scheduler_manager.get_registered_plugins()
|
||||
if not registered_plugins:
|
||||
await schedule_cmd.finish("当前没有已注册的定时任务插件。")
|
||||
|
||||
message_parts = ["📋 已注册的定时任务插件:"]
|
||||
for i, plugin_name in enumerate(registered_plugins, 1):
|
||||
task_meta = scheduler_manager._registered_tasks[plugin_name]
|
||||
params_model = task_meta.get("model")
|
||||
|
||||
if not params_model:
|
||||
message_parts.append(f"{i}. {plugin_name} - 无参数")
|
||||
continue
|
||||
|
||||
if not (isinstance(params_model, type) and issubclass(params_model, BaseModel)):
|
||||
message_parts.append(f"{i}. {plugin_name} - ⚠️ 参数模型配置错误")
|
||||
continue
|
||||
|
||||
model_fields = getattr(params_model, "model_fields", None)
|
||||
if model_fields:
|
||||
param_info = ", ".join(
|
||||
f"{field_name}({_get_type_name(field_info.annotation)})"
|
||||
for field_name, field_info in model_fields.items()
|
||||
)
|
||||
message_parts.append(f"{i}. {plugin_name} - 参数: {param_info}")
|
||||
else:
|
||||
message_parts.append(f"{i}. {plugin_name} - 无参数")
|
||||
|
||||
await schedule_cmd.finish("\n".join(message_parts))
|
||||
|
||||
|
||||
@schedule_cmd.assign("状态")
|
||||
async def _(schedule_id: int):
|
||||
status = await scheduler_manager.get_schedule_status(schedule_id)
|
||||
if not status:
|
||||
await schedule_cmd.finish(f"未找到ID为 {schedule_id} 的定时任务。")
|
||||
|
||||
info_lines = [
|
||||
f"📋 定时任务详细信息 (ID: {schedule_id})",
|
||||
"--------------------",
|
||||
f"▫️ 插件: {status['plugin_name']}",
|
||||
f"▫️ Bot ID: {status.get('bot_id') or '默认'}",
|
||||
f"▫️ 目标: {status['group_id'] or '全局'}",
|
||||
f"▫️ 状态: {'✔️ 已启用' if status['is_enabled'] else '⏸️ 已暂停'}",
|
||||
f"▫️ 下次运行: {status['next_run_time']}",
|
||||
f"▫️ 触发规则: {_format_trigger(status)}",
|
||||
f"▫️ 任务参数: {_format_params(status)}",
|
||||
]
|
||||
await schedule_cmd.finish("\n".join(info_lines))
|
||||
298
zhenxun/builtin_plugins/scheduler_admin/commands.py
Normal file
298
zhenxun/builtin_plugins/scheduler_admin/commands.py
Normal file
@ -0,0 +1,298 @@
|
||||
import re
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.adapters.onebot.v11 import Bot
|
||||
from nonebot.params import Depends
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
AlconnaMatch,
|
||||
Args,
|
||||
Match,
|
||||
Option,
|
||||
Query,
|
||||
Subcommand,
|
||||
on_alconna,
|
||||
)
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.services.scheduler import scheduler_manager
|
||||
from zhenxun.services.scheduler.targeter import ScheduleTargeter
|
||||
from zhenxun.utils.rules import admin_check
|
||||
|
||||
schedule_cmd = on_alconna(
|
||||
Alconna(
|
||||
"定时任务",
|
||||
Subcommand(
|
||||
"查看",
|
||||
Option("-g", Args["target_group_id", str]),
|
||||
Option("-all", help_text="查看所有群聊 (SUPERUSER)"),
|
||||
Option("-p", Args["plugin_name", str], help_text="按插件名筛选"),
|
||||
Option("--page", Args["page", int, 1], help_text="指定页码"),
|
||||
alias=["ls", "list"],
|
||||
help_text="查看定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"设置",
|
||||
Args["plugin_name", str],
|
||||
Option("--cron", Args["cron_expr", str], help_text="设置 cron 表达式"),
|
||||
Option("--interval", Args["interval_expr", str], help_text="设置时间间隔"),
|
||||
Option("--date", Args["date_expr", str], help_text="设置特定执行日期"),
|
||||
Option(
|
||||
"--daily",
|
||||
Args["daily_expr", str],
|
||||
help_text="设置每天执行的时间 (如 08:20)",
|
||||
),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID或'all'"),
|
||||
Option("-all", help_text="对所有群生效 (等同于 -g all)"),
|
||||
Option("--kwargs", Args["kwargs_str", str], help_text="设置任务参数"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["add", "开启"],
|
||||
help_text="设置/开启一个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"删除",
|
||||
Args["schedule_id?", int],
|
||||
Option("-p", Args["plugin_name", str], help_text="指定插件名"),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID"),
|
||||
Option("-all", help_text="对所有群生效"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["del", "rm", "remove", "关闭", "取消"],
|
||||
help_text="删除一个或多个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"暂停",
|
||||
Args["schedule_id?", int],
|
||||
Option("-all", help_text="对当前群所有任务生效"),
|
||||
Option("-p", Args["plugin_name", str], help_text="指定插件名"),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID (SUPERUSER)"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["pause"],
|
||||
help_text="暂停一个或多个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"恢复",
|
||||
Args["schedule_id?", int],
|
||||
Option("-all", help_text="对当前群所有任务生效"),
|
||||
Option("-p", Args["plugin_name", str], help_text="指定插件名"),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID (SUPERUSER)"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["resume"],
|
||||
help_text="恢复一个或多个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"执行",
|
||||
Args["schedule_id", int],
|
||||
alias=["trigger", "run"],
|
||||
help_text="立即执行一次任务",
|
||||
),
|
||||
Subcommand(
|
||||
"更新",
|
||||
Args["schedule_id", int],
|
||||
Option("--cron", Args["cron_expr", str], help_text="设置 cron 表达式"),
|
||||
Option("--interval", Args["interval_expr", str], help_text="设置时间间隔"),
|
||||
Option("--date", Args["date_expr", str], help_text="设置特定执行日期"),
|
||||
Option(
|
||||
"--daily",
|
||||
Args["daily_expr", str],
|
||||
help_text="更新每天执行的时间 (如 08:20)",
|
||||
),
|
||||
Option("--kwargs", Args["kwargs_str", str], help_text="更新参数"),
|
||||
alias=["update", "modify", "修改"],
|
||||
help_text="更新任务配置",
|
||||
),
|
||||
Subcommand(
|
||||
"状态",
|
||||
Args["schedule_id", int],
|
||||
alias=["status", "info"],
|
||||
help_text="查看单个任务的详细状态",
|
||||
),
|
||||
Subcommand(
|
||||
"插件列表",
|
||||
alias=["plugins"],
|
||||
help_text="列出所有可用的插件",
|
||||
),
|
||||
),
|
||||
priority=5,
|
||||
block=True,
|
||||
rule=admin_check(1),
|
||||
)
|
||||
|
||||
schedule_cmd.shortcut(
|
||||
"任务状态",
|
||||
command="定时任务",
|
||||
arguments=["状态", "{%0}"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
class ScheduleTarget:
|
||||
pass
|
||||
|
||||
|
||||
class TargetByID(ScheduleTarget):
|
||||
def __init__(self, id: int):
|
||||
self.id = id
|
||||
|
||||
|
||||
class TargetByPlugin(ScheduleTarget):
|
||||
def __init__(
|
||||
self, plugin: str, group_id: str | None = None, all_groups: bool = False
|
||||
):
|
||||
self.plugin = plugin
|
||||
self.group_id = group_id
|
||||
self.all_groups = all_groups
|
||||
|
||||
|
||||
class TargetAll(ScheduleTarget):
|
||||
def __init__(self, for_group: str | None = None):
|
||||
self.for_group = for_group
|
||||
|
||||
|
||||
TargetScope = TargetByID | TargetByPlugin | TargetAll | None
|
||||
|
||||
|
||||
def create_target_parser(subcommand_name: str):
|
||||
async def dependency(
|
||||
event: Event,
|
||||
schedule_id: Match[int] = AlconnaMatch("schedule_id"),
|
||||
plugin_name: Match[str] = AlconnaMatch("plugin_name"),
|
||||
group_id: Match[str] = AlconnaMatch("group_id"),
|
||||
all_enabled: Query[bool] = Query(f"{subcommand_name}.all"),
|
||||
) -> TargetScope:
|
||||
if schedule_id.available:
|
||||
return TargetByID(schedule_id.result)
|
||||
|
||||
if plugin_name.available:
|
||||
p_name = plugin_name.result
|
||||
if all_enabled.available:
|
||||
return TargetByPlugin(plugin=p_name, all_groups=True)
|
||||
elif group_id.available:
|
||||
gid = group_id.result
|
||||
if gid.lower() == "all":
|
||||
return TargetByPlugin(plugin=p_name, all_groups=True)
|
||||
return TargetByPlugin(plugin=p_name, group_id=gid)
|
||||
else:
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
return TargetByPlugin(
|
||||
plugin=p_name,
|
||||
group_id=str(current_group_id) if current_group_id else None,
|
||||
)
|
||||
|
||||
if all_enabled.available:
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
if not current_group_id:
|
||||
await schedule_cmd.finish(
|
||||
"私聊中单独使用 -all 选项时,必须使用 -g <群号> 指定目标。"
|
||||
)
|
||||
return TargetAll(for_group=str(current_group_id))
|
||||
|
||||
return None
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
def parse_interval(interval_str: str) -> dict:
|
||||
match = re.match(r"(\d+)([smhd])", interval_str.lower())
|
||||
if not match:
|
||||
raise ValueError("时间间隔格式错误, 请使用如 '30m', '2h', '1d', '10s' 的格式。")
|
||||
value, unit = int(match.group(1)), match.group(2)
|
||||
if unit == "s":
|
||||
return {"seconds": value}
|
||||
if unit == "m":
|
||||
return {"minutes": value}
|
||||
if unit == "h":
|
||||
return {"hours": value}
|
||||
if unit == "d":
|
||||
return {"days": value}
|
||||
return {}
|
||||
|
||||
|
||||
def parse_daily_time(time_str: str) -> dict:
|
||||
if match := re.match(r"^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$", time_str):
|
||||
hour, minute, second = match.groups()
|
||||
hour, minute = int(hour), int(minute)
|
||||
if not (0 <= hour <= 23 and 0 <= minute <= 59):
|
||||
raise ValueError("小时或分钟数值超出范围。")
|
||||
cron_config = {
|
||||
"minute": str(minute),
|
||||
"hour": str(hour),
|
||||
"day": "*",
|
||||
"month": "*",
|
||||
"day_of_week": "*",
|
||||
"timezone": Config.get_config("SchedulerManager", "SCHEDULER_TIMEZONE"),
|
||||
}
|
||||
if second is not None:
|
||||
if not (0 <= int(second) <= 59):
|
||||
raise ValueError("秒数值超出范围。")
|
||||
cron_config["second"] = str(second)
|
||||
return cron_config
|
||||
else:
|
||||
raise ValueError("时间格式错误,请使用 'HH:MM' 或 'HH:MM:SS' 格式。")
|
||||
|
||||
|
||||
async def GetBotId(bot: Bot, bot_id_match: Match[str] = AlconnaMatch("bot_id")) -> str:
|
||||
if bot_id_match.available:
|
||||
return bot_id_match.result
|
||||
return bot.self_id
|
||||
|
||||
|
||||
def GetTargeter(subcommand: str):
|
||||
"""
|
||||
依赖注入函数,用于解析命令参数并返回一个配置好的 ScheduleTargeter 实例。
|
||||
"""
|
||||
|
||||
async def dependency(
|
||||
event: Event,
|
||||
bot: Bot,
|
||||
schedule_id: Match[int] = AlconnaMatch("schedule_id"),
|
||||
plugin_name: Match[str] = AlconnaMatch("plugin_name"),
|
||||
group_id: Match[str] = AlconnaMatch("group_id"),
|
||||
all_enabled: Query[bool] = Query(f"{subcommand}.all"),
|
||||
bot_id_to_operate: str = Depends(GetBotId),
|
||||
) -> ScheduleTargeter:
|
||||
if schedule_id.available:
|
||||
return scheduler_manager.target(id=schedule_id.result)
|
||||
|
||||
if plugin_name.available:
|
||||
if all_enabled.available:
|
||||
return scheduler_manager.target(plugin_name=plugin_name.result)
|
||||
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
gid = group_id.result if group_id.available else current_group_id
|
||||
return scheduler_manager.target(
|
||||
plugin_name=plugin_name.result,
|
||||
group_id=str(gid) if gid else None,
|
||||
bot_id=bot_id_to_operate,
|
||||
)
|
||||
|
||||
if all_enabled.available:
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
gid = group_id.result if group_id.available else current_group_id
|
||||
is_su = await SUPERUSER(bot, event)
|
||||
if not gid and not is_su:
|
||||
await schedule_cmd.finish(
|
||||
f"在私聊中对所有任务进行'{subcommand}'操作需要超级用户权限。"
|
||||
)
|
||||
|
||||
if (gid and str(gid).lower() == "all") or (not gid and is_su):
|
||||
return scheduler_manager.target()
|
||||
|
||||
return scheduler_manager.target(
|
||||
group_id=str(gid) if gid else None, bot_id=bot_id_to_operate
|
||||
)
|
||||
|
||||
await schedule_cmd.finish(
|
||||
f"'{subcommand}'操作失败:请提供任务ID,"
|
||||
f"或通过 -p <插件名> 或 -all 指定要操作的任务。"
|
||||
)
|
||||
|
||||
return Depends(dependency)
|
||||
380
zhenxun/builtin_plugins/scheduler_admin/handlers.py
Normal file
380
zhenxun/builtin_plugins/scheduler_admin/handlers.py
Normal file
@ -0,0 +1,380 @@
|
||||
from datetime import datetime
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.adapters.onebot.v11 import Bot
|
||||
from nonebot.params import Depends
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot_plugin_alconna import AlconnaMatch, Arparma, Match, Query
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from zhenxun.models.schedule_info import ScheduleInfo
|
||||
from zhenxun.services.scheduler import scheduler_manager
|
||||
from zhenxun.services.scheduler.targeter import ScheduleTargeter
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from . import presenters
|
||||
from .commands import (
|
||||
GetBotId,
|
||||
GetTargeter,
|
||||
parse_daily_time,
|
||||
parse_interval,
|
||||
schedule_cmd,
|
||||
)
|
||||
|
||||
|
||||
@schedule_cmd.handle()
|
||||
async def _handle_time_options_mutex(arp: Arparma):
|
||||
time_options = ["cron", "interval", "date", "daily"]
|
||||
provided_options = [opt for opt in time_options if arp.query(opt) is not None]
|
||||
if len(provided_options) > 1:
|
||||
await schedule_cmd.finish(
|
||||
f"时间选项 --{', --'.join(provided_options)} 不能同时使用,请只选择一个。"
|
||||
)
|
||||
|
||||
|
||||
@schedule_cmd.assign("查看")
|
||||
async def handle_view(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
target_group_id: Match[str] = AlconnaMatch("target_group_id"),
|
||||
all_groups: Query[bool] = Query("查看.all"),
|
||||
plugin_name: Match[str] = AlconnaMatch("plugin_name"),
|
||||
page: Match[int] = AlconnaMatch("page"),
|
||||
):
|
||||
is_superuser = await SUPERUSER(bot, event)
|
||||
title = ""
|
||||
gid_filter = None
|
||||
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
if not (all_groups.available or target_group_id.available) and not current_group_id:
|
||||
await schedule_cmd.finish("私聊中查看任务必须使用 -g <群号> 或 -all 选项。")
|
||||
|
||||
if all_groups.available:
|
||||
if not is_superuser:
|
||||
await schedule_cmd.finish("需要超级用户权限才能查看所有群组的定时任务。")
|
||||
title = "所有群组的定时任务"
|
||||
elif target_group_id.available:
|
||||
if not is_superuser:
|
||||
await schedule_cmd.finish("需要超级用户权限才能查看指定群组的定时任务。")
|
||||
gid_filter = target_group_id.result
|
||||
title = f"群 {gid_filter} 的定时任务"
|
||||
else:
|
||||
gid_filter = str(current_group_id)
|
||||
title = "本群的定时任务"
|
||||
|
||||
p_name_filter = plugin_name.result if plugin_name.available else None
|
||||
|
||||
schedules = await scheduler_manager.get_schedules(
|
||||
plugin_name=p_name_filter, group_id=gid_filter
|
||||
)
|
||||
|
||||
if p_name_filter:
|
||||
title += f" [插件: {p_name_filter}]"
|
||||
|
||||
if not schedules:
|
||||
await schedule_cmd.finish("没有找到任何相关的定时任务。")
|
||||
|
||||
img = await presenters.format_schedule_list_as_image(
|
||||
schedules=schedules, title=title, current_page=page.result
|
||||
)
|
||||
await MessageUtils.build_message(img).send(reply_to=True)
|
||||
|
||||
|
||||
@schedule_cmd.assign("设置")
|
||||
async def handle_set(
|
||||
event: Event,
|
||||
plugin_name: Match[str] = AlconnaMatch("plugin_name"),
|
||||
cron_expr: Match[str] = AlconnaMatch("cron_expr"),
|
||||
interval_expr: Match[str] = AlconnaMatch("interval_expr"),
|
||||
date_expr: Match[str] = AlconnaMatch("date_expr"),
|
||||
daily_expr: Match[str] = AlconnaMatch("daily_expr"),
|
||||
group_id: Match[str] = AlconnaMatch("group_id"),
|
||||
kwargs_str: Match[str] = AlconnaMatch("kwargs_str"),
|
||||
all_enabled: Query[bool] = Query("设置.all"),
|
||||
bot_id_to_operate: str = Depends(GetBotId),
|
||||
):
|
||||
if not plugin_name.available:
|
||||
await schedule_cmd.finish("设置任务时必须提供插件名称。")
|
||||
|
||||
has_time_option = any(
|
||||
[
|
||||
cron_expr.available,
|
||||
interval_expr.available,
|
||||
date_expr.available,
|
||||
daily_expr.available,
|
||||
]
|
||||
)
|
||||
if not has_time_option:
|
||||
await schedule_cmd.finish(
|
||||
"必须提供一种时间选项: --cron, --interval, --date, 或 --daily。"
|
||||
)
|
||||
|
||||
p_name = plugin_name.result
|
||||
if p_name not in scheduler_manager.get_registered_plugins():
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{p_name}' 没有注册可用的定时任务。\n"
|
||||
f"可用插件: {list(scheduler_manager.get_registered_plugins())}"
|
||||
)
|
||||
|
||||
trigger_type, trigger_config = "", {}
|
||||
try:
|
||||
if cron_expr.available:
|
||||
trigger_type, trigger_config = (
|
||||
"cron",
|
||||
dict(
|
||||
zip(
|
||||
["minute", "hour", "day", "month", "day_of_week"],
|
||||
cron_expr.result.split(),
|
||||
)
|
||||
),
|
||||
)
|
||||
elif interval_expr.available:
|
||||
trigger_type, trigger_config = (
|
||||
"interval",
|
||||
parse_interval(interval_expr.result),
|
||||
)
|
||||
elif date_expr.available:
|
||||
trigger_type, trigger_config = (
|
||||
"date",
|
||||
{"run_date": datetime.fromisoformat(date_expr.result)},
|
||||
)
|
||||
elif daily_expr.available:
|
||||
trigger_type, trigger_config = "cron", parse_daily_time(daily_expr.result)
|
||||
else:
|
||||
await schedule_cmd.finish(
|
||||
"必须提供一种时间选项: --cron, --interval, --date, 或 --daily。"
|
||||
)
|
||||
except ValueError as e:
|
||||
await schedule_cmd.finish(f"时间参数解析错误: {e}")
|
||||
|
||||
job_kwargs = {}
|
||||
if kwargs_str.available:
|
||||
task_meta = scheduler_manager._registered_tasks[p_name]
|
||||
params_model = task_meta.get("model")
|
||||
if not (
|
||||
params_model
|
||||
and isinstance(params_model, type)
|
||||
and issubclass(params_model, BaseModel)
|
||||
):
|
||||
await schedule_cmd.finish(f"插件 '{p_name}' 不支持或配置了无效的参数模型。")
|
||||
try:
|
||||
raw_kwargs = dict(
|
||||
item.strip().split("=", 1) for item in kwargs_str.result.split(",")
|
||||
)
|
||||
|
||||
model_validate = getattr(params_model, "model_validate", None)
|
||||
if not model_validate:
|
||||
await schedule_cmd.finish(f"插件 '{p_name}' 的参数模型不支持验证")
|
||||
|
||||
validated_model = model_validate(raw_kwargs)
|
||||
|
||||
model_dump = getattr(validated_model, "model_dump", None)
|
||||
if not model_dump:
|
||||
await schedule_cmd.finish(f"插件 '{p_name}' 的参数模型不支持导出")
|
||||
|
||||
job_kwargs = model_dump()
|
||||
except ValidationError as e:
|
||||
errors = [f" - {err['loc'][0]}: {err['msg']}" for err in e.errors()]
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{p_name}' 的任务参数验证失败:\n" + "\n".join(errors)
|
||||
)
|
||||
except Exception as e:
|
||||
await schedule_cmd.finish(
|
||||
f"参数格式错误,请使用 'key=value,key2=value2' 格式。错误: {e}"
|
||||
)
|
||||
|
||||
gid_str = group_id.result if group_id.available else None
|
||||
target_group_id = (
|
||||
scheduler_manager.ALL_GROUPS
|
||||
if (gid_str and gid_str.lower() == "all") or all_enabled.available
|
||||
else gid_str or getattr(event, "group_id", None)
|
||||
)
|
||||
if not target_group_id:
|
||||
await schedule_cmd.finish(
|
||||
"私聊中设置定时任务时,必须使用 -g <群号> 或 --all 选项指定目标。"
|
||||
)
|
||||
|
||||
schedule = await scheduler_manager.add_schedule(
|
||||
p_name,
|
||||
str(target_group_id),
|
||||
trigger_type,
|
||||
trigger_config,
|
||||
job_kwargs,
|
||||
bot_id=bot_id_to_operate,
|
||||
)
|
||||
|
||||
target_desc = (
|
||||
f"所有群组 (Bot: {bot_id_to_operate})"
|
||||
if target_group_id == scheduler_manager.ALL_GROUPS
|
||||
else f"群组 {target_group_id}"
|
||||
)
|
||||
|
||||
if schedule:
|
||||
await schedule_cmd.finish(
|
||||
f"为 [{target_desc}] 已成功设置插件 '{p_name}' 的定时任务 "
|
||||
f"(ID: {schedule.id})。"
|
||||
)
|
||||
else:
|
||||
await schedule_cmd.finish(f"为 [{target_desc}] 设置任务失败。")
|
||||
|
||||
|
||||
@schedule_cmd.assign("删除")
|
||||
async def handle_delete(targeter: ScheduleTargeter = GetTargeter("删除")):
|
||||
schedules_to_remove: list[ScheduleInfo] = await targeter._get_schedules()
|
||||
if not schedules_to_remove:
|
||||
await schedule_cmd.finish("没有找到可删除的任务。")
|
||||
|
||||
count, _ = await targeter.remove()
|
||||
|
||||
if count > 0 and schedules_to_remove:
|
||||
if len(schedules_to_remove) == 1:
|
||||
message = presenters.format_remove_success(schedules_to_remove[0])
|
||||
else:
|
||||
target_desc = targeter._generate_target_description()
|
||||
message = f"✅ 成功移除了{target_desc} {count} 个任务。"
|
||||
else:
|
||||
message = "没有任务被移除。"
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
|
||||
@schedule_cmd.assign("暂停")
|
||||
async def handle_pause(targeter: ScheduleTargeter = GetTargeter("暂停")):
|
||||
schedules_to_pause: list[ScheduleInfo] = await targeter._get_schedules()
|
||||
if not schedules_to_pause:
|
||||
await schedule_cmd.finish("没有找到可暂停的任务。")
|
||||
|
||||
count, _ = await targeter.pause()
|
||||
|
||||
if count > 0 and schedules_to_pause:
|
||||
if len(schedules_to_pause) == 1:
|
||||
message = presenters.format_pause_success(schedules_to_pause[0])
|
||||
else:
|
||||
target_desc = targeter._generate_target_description()
|
||||
message = f"✅ 成功暂停了{target_desc} {count} 个任务。"
|
||||
else:
|
||||
message = "没有任务被暂停。"
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
|
||||
@schedule_cmd.assign("恢复")
|
||||
async def handle_resume(targeter: ScheduleTargeter = GetTargeter("恢复")):
|
||||
schedules_to_resume: list[ScheduleInfo] = await targeter._get_schedules()
|
||||
if not schedules_to_resume:
|
||||
await schedule_cmd.finish("没有找到可恢复的任务。")
|
||||
|
||||
count, _ = await targeter.resume()
|
||||
|
||||
if count > 0 and schedules_to_resume:
|
||||
if len(schedules_to_resume) == 1:
|
||||
message = presenters.format_resume_success(schedules_to_resume[0])
|
||||
else:
|
||||
target_desc = targeter._generate_target_description()
|
||||
message = f"✅ 成功恢复了{target_desc} {count} 个任务。"
|
||||
else:
|
||||
message = "没有任务被恢复。"
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
|
||||
@schedule_cmd.assign("执行")
|
||||
async def handle_trigger(schedule_id: Match[int] = AlconnaMatch("schedule_id")):
|
||||
from zhenxun.services.scheduler.repository import ScheduleRepository
|
||||
|
||||
schedule_info = await ScheduleRepository.get_by_id(schedule_id.result)
|
||||
if not schedule_info:
|
||||
await schedule_cmd.finish(f"未找到 ID 为 {schedule_id.result} 的任务。")
|
||||
|
||||
success, message = await scheduler_manager.trigger_now(schedule_id.result)
|
||||
|
||||
if success:
|
||||
final_message = presenters.format_trigger_success(schedule_info)
|
||||
else:
|
||||
final_message = f"❌ 手动触发失败: {message}"
|
||||
await schedule_cmd.finish(final_message)
|
||||
|
||||
|
||||
@schedule_cmd.assign("更新")
|
||||
async def handle_update(
|
||||
schedule_id: Match[int] = AlconnaMatch("schedule_id"),
|
||||
cron_expr: Match[str] = AlconnaMatch("cron_expr"),
|
||||
interval_expr: Match[str] = AlconnaMatch("interval_expr"),
|
||||
date_expr: Match[str] = AlconnaMatch("date_expr"),
|
||||
daily_expr: Match[str] = AlconnaMatch("daily_expr"),
|
||||
kwargs_str: Match[str] = AlconnaMatch("kwargs_str"),
|
||||
):
|
||||
if not any(
|
||||
[
|
||||
cron_expr.available,
|
||||
interval_expr.available,
|
||||
date_expr.available,
|
||||
daily_expr.available,
|
||||
kwargs_str.available,
|
||||
]
|
||||
):
|
||||
await schedule_cmd.finish(
|
||||
"请提供需要更新的时间 (--cron/--interval/--date/--daily) 或参数 (--kwargs)"
|
||||
)
|
||||
|
||||
trigger_type, trigger_config, job_kwargs = None, None, None
|
||||
try:
|
||||
if cron_expr.available:
|
||||
trigger_type, trigger_config = (
|
||||
"cron",
|
||||
dict(
|
||||
zip(
|
||||
["minute", "hour", "day", "month", "day_of_week"],
|
||||
cron_expr.result.split(),
|
||||
)
|
||||
),
|
||||
)
|
||||
elif interval_expr.available:
|
||||
trigger_type, trigger_config = (
|
||||
"interval",
|
||||
parse_interval(interval_expr.result),
|
||||
)
|
||||
elif date_expr.available:
|
||||
trigger_type, trigger_config = (
|
||||
"date",
|
||||
{"run_date": datetime.fromisoformat(date_expr.result)},
|
||||
)
|
||||
elif daily_expr.available:
|
||||
trigger_type, trigger_config = "cron", parse_daily_time(daily_expr.result)
|
||||
except ValueError as e:
|
||||
await schedule_cmd.finish(f"时间参数解析错误: {e}")
|
||||
|
||||
if kwargs_str.available:
|
||||
job_kwargs = dict(
|
||||
item.strip().split("=", 1) for item in kwargs_str.result.split(",")
|
||||
)
|
||||
|
||||
success, message = await scheduler_manager.update_schedule(
|
||||
schedule_id.result, trigger_type, trigger_config, job_kwargs
|
||||
)
|
||||
|
||||
if success:
|
||||
from zhenxun.services.scheduler.repository import ScheduleRepository
|
||||
|
||||
updated_schedule = await ScheduleRepository.get_by_id(schedule_id.result)
|
||||
if updated_schedule:
|
||||
final_message = presenters.format_update_success(updated_schedule)
|
||||
else:
|
||||
final_message = "✅ 更新成功,但无法获取更新后的任务详情。"
|
||||
else:
|
||||
final_message = f"❌ 更新失败: {message}"
|
||||
|
||||
await schedule_cmd.finish(final_message)
|
||||
|
||||
|
||||
@schedule_cmd.assign("插件列表")
|
||||
async def handle_plugins_list():
|
||||
message = await presenters.format_plugins_list()
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
|
||||
@schedule_cmd.assign("状态")
|
||||
async def handle_status(schedule_id: Match[int] = AlconnaMatch("schedule_id")):
|
||||
status = await scheduler_manager.get_schedule_status(schedule_id.result)
|
||||
if not status:
|
||||
await schedule_cmd.finish(f"未找到ID为 {schedule_id.result} 的定时任务。")
|
||||
|
||||
message = presenters.format_single_status_message(status)
|
||||
await schedule_cmd.finish(message)
|
||||
274
zhenxun/builtin_plugins/scheduler_admin/presenters.py
Normal file
274
zhenxun/builtin_plugins/scheduler_admin/presenters.py
Normal file
@ -0,0 +1,274 @@
|
||||
import asyncio
|
||||
|
||||
from zhenxun.models.schedule_info import ScheduleInfo
|
||||
from zhenxun.services.scheduler import scheduler_manager
|
||||
from zhenxun.utils._image_template import ImageTemplate, RowStyle
|
||||
|
||||
|
||||
def _get_type_name(annotation) -> str:
|
||||
"""获取类型注解的名称"""
|
||||
if hasattr(annotation, "__name__"):
|
||||
return annotation.__name__
|
||||
elif hasattr(annotation, "_name"):
|
||||
return annotation._name
|
||||
else:
|
||||
return str(annotation)
|
||||
|
||||
|
||||
def _format_trigger(schedule: dict) -> str:
|
||||
"""格式化触发器信息为可读字符串"""
|
||||
trigger_type = schedule.get("trigger_type")
|
||||
config = schedule.get("trigger_config")
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return f"配置错误: {config}"
|
||||
|
||||
if trigger_type == "cron":
|
||||
hour = config.get("hour", "??")
|
||||
minute = config.get("minute", "??")
|
||||
try:
|
||||
hour_int = int(hour)
|
||||
minute_int = int(minute)
|
||||
return f"每天 {hour_int:02d}:{minute_int:02d}"
|
||||
except (ValueError, TypeError):
|
||||
return f"每天 {hour}:{minute}"
|
||||
elif trigger_type == "interval":
|
||||
units = {
|
||||
"weeks": "周",
|
||||
"days": "天",
|
||||
"hours": "小时",
|
||||
"minutes": "分钟",
|
||||
"seconds": "秒",
|
||||
}
|
||||
for unit, unit_name in units.items():
|
||||
if value := config.get(unit):
|
||||
return f"每 {value} {unit_name}"
|
||||
return "未知间隔"
|
||||
elif trigger_type == "date":
|
||||
run_date = config.get("run_date", "N/A")
|
||||
return f"特定时间 {run_date}"
|
||||
else:
|
||||
return f"未知触发器类型: {trigger_type}"
|
||||
|
||||
|
||||
def _format_trigger_for_card(schedule_info: ScheduleInfo | dict) -> str:
|
||||
"""为信息卡片格式化触发器规则"""
|
||||
trigger_type = (
|
||||
schedule_info.get("trigger_type")
|
||||
if isinstance(schedule_info, dict)
|
||||
else schedule_info.trigger_type
|
||||
)
|
||||
config = (
|
||||
schedule_info.get("trigger_config")
|
||||
if isinstance(schedule_info, dict)
|
||||
else schedule_info.trigger_config
|
||||
)
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return f"配置错误: {config}"
|
||||
|
||||
if trigger_type == "cron":
|
||||
hour = config.get("hour", "??")
|
||||
minute = config.get("minute", "??")
|
||||
try:
|
||||
hour_int = int(hour)
|
||||
minute_int = int(minute)
|
||||
return f"每天 {hour_int:02d}:{minute_int:02d}"
|
||||
except (ValueError, TypeError):
|
||||
return f"每天 {hour}:{minute}"
|
||||
elif trigger_type == "interval":
|
||||
units = {
|
||||
"weeks": "周",
|
||||
"days": "天",
|
||||
"hours": "小时",
|
||||
"minutes": "分钟",
|
||||
"seconds": "秒",
|
||||
}
|
||||
for unit, unit_name in units.items():
|
||||
if value := config.get(unit):
|
||||
return f"每 {value} {unit_name}"
|
||||
return "未知间隔"
|
||||
elif trigger_type == "date":
|
||||
run_date = config.get("run_date", "N/A")
|
||||
return f"特定时间 {run_date}"
|
||||
else:
|
||||
return f"未知规则: {trigger_type}"
|
||||
|
||||
|
||||
def _format_operation_result_card(
|
||||
title: str, schedule_info: ScheduleInfo, extra_info: list[str] | None = None
|
||||
) -> str:
|
||||
"""
|
||||
生成一个标准的操作结果信息卡片。
|
||||
|
||||
参数:
|
||||
title: 卡片的标题 (例如 "✅ 成功暂停定时任务!")
|
||||
schedule_info: 相关的 ScheduleInfo 对象
|
||||
extra_info: (可选) 额外的补充信息行
|
||||
"""
|
||||
target_desc = (
|
||||
f"群组 {schedule_info.group_id}"
|
||||
if schedule_info.group_id
|
||||
and schedule_info.group_id != scheduler_manager.ALL_GROUPS
|
||||
else "所有群组"
|
||||
if schedule_info.group_id == scheduler_manager.ALL_GROUPS
|
||||
else "全局"
|
||||
)
|
||||
|
||||
info_lines = [
|
||||
title,
|
||||
f"✓ 任务 ID: {schedule_info.id}",
|
||||
f"🖋 插件: {schedule_info.plugin_name}",
|
||||
f"🎯 目标: {target_desc}",
|
||||
f"⏰ 时间: {_format_trigger_for_card(schedule_info)}",
|
||||
]
|
||||
if extra_info:
|
||||
info_lines.extend(extra_info)
|
||||
|
||||
return "\n".join(info_lines)
|
||||
|
||||
|
||||
def format_pause_success(schedule_info: ScheduleInfo) -> str:
|
||||
"""格式化暂停成功的消息"""
|
||||
return _format_operation_result_card("✅ 成功暂停定时任务!", schedule_info)
|
||||
|
||||
|
||||
def format_resume_success(schedule_info: ScheduleInfo) -> str:
|
||||
"""格式化恢复成功的消息"""
|
||||
return _format_operation_result_card("▶️ 成功恢复定时任务!", schedule_info)
|
||||
|
||||
|
||||
def format_remove_success(schedule_info: ScheduleInfo) -> str:
|
||||
"""格式化删除成功的消息"""
|
||||
return _format_operation_result_card("❌ 成功删除定时任务!", schedule_info)
|
||||
|
||||
|
||||
def format_trigger_success(schedule_info: ScheduleInfo) -> str:
|
||||
"""格式化手动触发成功的消息"""
|
||||
return _format_operation_result_card("🚀 成功手动触发定时任务!", schedule_info)
|
||||
|
||||
|
||||
def format_update_success(schedule_info: ScheduleInfo) -> str:
|
||||
"""格式化更新成功的消息"""
|
||||
return _format_operation_result_card("🔄️ 成功更新定时任务配置!", schedule_info)
|
||||
|
||||
|
||||
def _status_row_style(column: str, text: str) -> RowStyle:
|
||||
"""为状态列设置颜色"""
|
||||
style = RowStyle()
|
||||
if column == "状态":
|
||||
if text == "启用":
|
||||
style.font_color = "#67C23A"
|
||||
elif text == "暂停":
|
||||
style.font_color = "#F56C6C"
|
||||
elif text == "运行中":
|
||||
style.font_color = "#409EFF"
|
||||
return style
|
||||
|
||||
|
||||
def _format_params(schedule_status: dict) -> str:
|
||||
"""将任务参数格式化为人类可读的字符串"""
|
||||
if kwargs := schedule_status.get("job_kwargs"):
|
||||
return " | ".join(f"{k}: {v}" for k, v in kwargs.items())
|
||||
return "-"
|
||||
|
||||
|
||||
async def format_schedule_list_as_image(
|
||||
schedules: list[ScheduleInfo], title: str, current_page: int
|
||||
):
|
||||
"""将任务列表格式化为图片"""
|
||||
page_size = 15
|
||||
total_items = len(schedules)
|
||||
total_pages = (total_items + page_size - 1) // page_size
|
||||
start_index = (current_page - 1) * page_size
|
||||
end_index = start_index + page_size
|
||||
paginated_schedules = schedules[start_index:end_index]
|
||||
|
||||
if not paginated_schedules:
|
||||
return "这一页没有内容了哦~"
|
||||
|
||||
status_tasks = [
|
||||
scheduler_manager.get_schedule_status(s.id) for s in paginated_schedules
|
||||
]
|
||||
all_statuses = await asyncio.gather(*status_tasks)
|
||||
|
||||
def get_status_text(status_value):
|
||||
if isinstance(status_value, bool):
|
||||
return "启用" if status_value else "暂停"
|
||||
return str(status_value)
|
||||
|
||||
data_list = [
|
||||
[
|
||||
s["id"],
|
||||
s["plugin_name"],
|
||||
s.get("bot_id") or "N/A",
|
||||
s["group_id"] or "全局",
|
||||
s["next_run_time"],
|
||||
_format_trigger(s),
|
||||
_format_params(s),
|
||||
get_status_text(s["is_enabled"]),
|
||||
]
|
||||
for s in all_statuses
|
||||
if s
|
||||
]
|
||||
|
||||
if not data_list:
|
||||
return "没有找到任何相关的定时任务。"
|
||||
|
||||
return await ImageTemplate.table_page(
|
||||
head_text=title,
|
||||
tip_text=f"第 {current_page}/{total_pages} 页,共 {total_items} 条任务",
|
||||
column_name=["ID", "插件", "Bot", "目标", "下次运行", "规则", "参数", "状态"],
|
||||
data_list=data_list,
|
||||
column_space=20,
|
||||
text_style=_status_row_style,
|
||||
)
|
||||
|
||||
|
||||
def format_single_status_message(status: dict) -> str:
|
||||
"""格式化单个任务状态为文本消息"""
|
||||
info_lines = [
|
||||
f"📋 定时任务详细信息 (ID: {status['id']})",
|
||||
"--------------------",
|
||||
f"▫️ 插件: {status['plugin_name']}",
|
||||
f"▫️ Bot ID: {status.get('bot_id') or '默认'}",
|
||||
f"▫️ 目标: {status['group_id'] or '全局'}",
|
||||
f"▫️ 状态: {'✔️ 已启用' if status['is_enabled'] else '⏸️ 已暂停'}",
|
||||
f"▫️ 下次运行: {status['next_run_time']}",
|
||||
f"▫️ 触发规则: {_format_trigger(status)}",
|
||||
f"▫️ 任务参数: {_format_params(status)}",
|
||||
]
|
||||
return "\n".join(info_lines)
|
||||
|
||||
|
||||
async def format_plugins_list() -> str:
|
||||
"""格式化可用插件列表为文本消息"""
|
||||
from pydantic import BaseModel
|
||||
|
||||
registered_plugins = scheduler_manager.get_registered_plugins()
|
||||
if not registered_plugins:
|
||||
return "当前没有已注册的定时任务插件。"
|
||||
|
||||
message_parts = ["📋 已注册的定时任务插件:"]
|
||||
for i, plugin_name in enumerate(registered_plugins, 1):
|
||||
task_meta = scheduler_manager._registered_tasks[plugin_name]
|
||||
params_model = task_meta.get("model")
|
||||
|
||||
param_info_str = "无参数"
|
||||
if (
|
||||
params_model
|
||||
and isinstance(params_model, type)
|
||||
and issubclass(params_model, BaseModel)
|
||||
):
|
||||
model_fields = getattr(params_model, "model_fields", None)
|
||||
if model_fields:
|
||||
param_info_str = "参数: " + ", ".join(
|
||||
f"{field_name}({_get_type_name(field_info.annotation)})"
|
||||
for field_name, field_info in model_fields.items()
|
||||
)
|
||||
elif params_model:
|
||||
param_info_str = "⚠️ 参数模型配置错误"
|
||||
|
||||
message_parts.append(f"{i}. {plugin_name} - {param_info_str}")
|
||||
|
||||
return "\n".join(message_parts)
|
||||
@ -1,30 +0,0 @@
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
|
||||
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
"""开启/禁用插件格式修改"""
|
||||
_, is_create = await GroupConsole.get_or_create(group_id=133133133)
|
||||
"""标记"""
|
||||
if is_create:
|
||||
data_list = []
|
||||
for group in await GroupConsole.all():
|
||||
if group.block_plugin:
|
||||
if modules := group.block_plugin.split(","):
|
||||
block_plugin = "".join(
|
||||
(f"{module}," if module.startswith("<") else f"<{module},")
|
||||
for module in modules
|
||||
if module.strip()
|
||||
)
|
||||
group.block_plugin = block_plugin.replace("<,", "")
|
||||
if group.block_task:
|
||||
if modules := group.block_task.split(","):
|
||||
block_task = "".join(
|
||||
(f"{module}," if module.startswith("<") else f"<{module},")
|
||||
for module in modules
|
||||
if module.strip()
|
||||
)
|
||||
group.block_task = block_task.replace("<,", "")
|
||||
data_list.append(group)
|
||||
await GroupConsole.bulk_update(data_list, ["block_plugin", "block_task"], 10)
|
||||
@ -344,6 +344,16 @@ class ShopManage:
|
||||
if goods_name.isdigit():
|
||||
try:
|
||||
user = await UserConsole.get_user(user_id=session.user.id)
|
||||
goods_list = await GoodsInfo.filter(uuid__in=user.props.keys()).all()
|
||||
goods_by_uuid = {item.uuid: item for item in goods_list}
|
||||
props_str = str(user.props)
|
||||
user.props = {
|
||||
uuid: count
|
||||
for uuid, count in user.props.items()
|
||||
if count > 0 and goods_by_uuid.get(uuid)
|
||||
}
|
||||
if props_str != str(user.props):
|
||||
await user.save(update_fields=["props"])
|
||||
uuid = list(user.props.keys())[int(goods_name)]
|
||||
goods_info = await GoodsInfo.get_or_none(uuid=uuid)
|
||||
except IndexError:
|
||||
@ -501,11 +511,14 @@ class ShopManage:
|
||||
|
||||
goods_list = await GoodsInfo.filter(uuid__in=user.props.keys()).all()
|
||||
goods_by_uuid = {item.uuid: item for item in goods_list}
|
||||
props_str = str(user.props)
|
||||
user.props = {
|
||||
uuid: count
|
||||
for uuid, count in user.props.items()
|
||||
if count > 0 and goods_by_uuid.get(uuid)
|
||||
}
|
||||
if props_str != str(user.props):
|
||||
await user.save(update_fields=["props"])
|
||||
|
||||
table_rows = []
|
||||
for i, prop_uuid in enumerate(user.props):
|
||||
|
||||
@ -29,9 +29,9 @@ from .config import (
|
||||
lik2relation,
|
||||
)
|
||||
|
||||
assert (
|
||||
len(level2attitude) == len(lik2level) == len(lik2relation)
|
||||
), "好感度态度、等级、关系长度不匹配!"
|
||||
assert len(level2attitude) == len(lik2level) == len(lik2relation), (
|
||||
"好感度态度、等级、关系长度不匹配!"
|
||||
)
|
||||
|
||||
AVA_URL = "http://q1.qlogo.cn/g?b=qq&nk={}&s=160"
|
||||
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from tortoise.functions import Count
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
@ -10,6 +8,7 @@ from zhenxun.utils.echart_utils import ChartUtils
|
||||
from zhenxun.utils.echart_utils.models import Barh
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.time_utils import TimeUtils
|
||||
|
||||
|
||||
class StatisticsManage:
|
||||
@ -45,9 +44,7 @@ class StatisticsManage:
|
||||
title = f"{user.user_name if user else user_id} {day_type}功能调用统计"
|
||||
elif group_id:
|
||||
"""查群组"""
|
||||
group = await GroupConsole.get_or_none(
|
||||
group_id=group_id, channel_id__isnull=True
|
||||
)
|
||||
group = await GroupConsole.get_group(group_id=group_id)
|
||||
title = f"{group.group_name if group else group_id} {day_type}功能调用统计"
|
||||
else:
|
||||
title = "功能调用统计"
|
||||
@ -68,8 +65,7 @@ class StatisticsManage:
|
||||
if plugin_name:
|
||||
query = query.filter(plugin_name=plugin_name)
|
||||
if day:
|
||||
time = datetime.now() - timedelta(days=day)
|
||||
query = query.filter(create_time__gte=time)
|
||||
query = query.filter(create_time__gte=TimeUtils.get_day_start())
|
||||
data_list = (
|
||||
await query.annotate(count=Count("id"))
|
||||
.group_by("plugin_name")
|
||||
@ -89,8 +85,7 @@ class StatisticsManage:
|
||||
if group_id:
|
||||
query = query.filter(group_id=group_id)
|
||||
if day:
|
||||
time = datetime.now() - timedelta(days=day)
|
||||
query = query.filter(create_time__gte=time)
|
||||
query = query.filter(create_time__gte=TimeUtils.get_day_start())
|
||||
data_list = (
|
||||
await query.annotate(count=Count("id"))
|
||||
.group_by("plugin_name")
|
||||
@ -106,8 +101,7 @@ class StatisticsManage:
|
||||
async def get_group_statistics(cls, group_id: str, day: int | None, title: str):
|
||||
query = Statistics.filter(group_id=group_id)
|
||||
if day:
|
||||
time = datetime.now() - timedelta(days=day)
|
||||
query = query.filter(create_time__gte=time)
|
||||
query = query.filter(create_time__gte=TimeUtils.get_day_start())
|
||||
data_list = (
|
||||
await query.annotate(count=Count("id"))
|
||||
.group_by("plugin_name")
|
||||
|
||||
@ -28,7 +28,7 @@ from nonebot_plugin_alconna.uniseg.segment import (
|
||||
)
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
|
||||
from zhenxun.configs.utils import PluginExtraData, Task
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
@ -73,16 +73,6 @@ __plugin_meta__ = PluginMetadata(
|
||||
author="HibiKier",
|
||||
version="1.2",
|
||||
plugin_type=PluginType.SUPERUSER,
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
module="_task",
|
||||
key="DEFAULT_BROADCAST",
|
||||
value=True,
|
||||
help="被动 广播 进群默认开关状态",
|
||||
default_value=True,
|
||||
type=bool,
|
||||
)
|
||||
],
|
||||
tasks=[Task(module="broadcast", name="广播")],
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
@ -163,7 +163,7 @@ async def _(session: EventSession, arparma: Arparma, state: T_State, level: int)
|
||||
@_matcher.assign("super-handle", parameterless=[CheckGroupId()])
|
||||
async def _(session: EventSession, arparma: Arparma, state: T_State):
|
||||
gid = state["group_id"]
|
||||
group = await GroupConsole.get_or_none(group_id=gid)
|
||||
group = await GroupConsole.get_group(group_id=gid)
|
||||
if not group:
|
||||
await MessageUtils.build_message("群组信息不存在, 请更新群组信息...").finish()
|
||||
s = "删除" if arparma.find("delete") else "添加"
|
||||
@ -177,7 +177,9 @@ async def _(session: EventSession, arparma: Arparma, state: T_State):
|
||||
async def _(session: EventSession, arparma: Arparma, state: T_State):
|
||||
gid = state["group_id"]
|
||||
await GroupConsole.update_or_create(
|
||||
group_id=gid, defaults={"group_flag": 0 if arparma.find("delete") else 1}
|
||||
group_id=gid,
|
||||
channel_id__isnull=True,
|
||||
defaults={"group_flag": 0 if arparma.find("delete") else 1},
|
||||
)
|
||||
s = "删除" if arparma.find("delete") else "添加"
|
||||
await MessageUtils.build_message(f"{s}群认证成功!").send(reply_to=True)
|
||||
|
||||
@ -163,15 +163,20 @@ async def _(
|
||||
req = await FgRequest.ignore(handle_id)
|
||||
except NotFoundError:
|
||||
await MessageUtils.build_message("未发现此id的请求...").finish(reply_to=True)
|
||||
except Exception:
|
||||
await MessageUtils.build_message("其他错误, 可能flag已失效...").finish(
|
||||
except Exception as e:
|
||||
logger.error(f"处理请求失败 ID: {handle_id}", session=session, e=e)
|
||||
await MessageUtils.build_message(f"其他错误, 可能flag已失效...: {e}").finish(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info(
|
||||
f"处理请求 Id: {req.id if req else ''}", arparma.header_result, session=session
|
||||
)
|
||||
await MessageUtils.build_message("成功处理请求!").send(reply_to=True)
|
||||
if req and handle_type == RequestHandleType.APPROVE:
|
||||
if (
|
||||
req
|
||||
and req.request_type == RequestType.GROUP
|
||||
and handle_type == RequestHandleType.APPROVE
|
||||
):
|
||||
await bot.send_private_msg(
|
||||
user_id=req.user_id,
|
||||
message=f"管理员已同意此次群组邀请,请不要让{BotConfig.self_nickname}受委屈哦(狠狠监控)"
|
||||
|
||||
@ -51,7 +51,7 @@ async def build_html_help():
|
||||
}
|
||||
},
|
||||
pages={
|
||||
"viewport": {"width": 1024, "height": 1024},
|
||||
"viewport": {"width": 824, "height": 10},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
|
||||
@ -119,7 +119,7 @@ class ApiDataSource:
|
||||
(await PlatformUtils.get_friend_list(select_bot.bot))[0]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("获取bot好友/群组信息失败...", "WebUi", e=e)
|
||||
logger.warning("获取bot好友/群组数量失败...", "WebUi", e=e)
|
||||
select_bot.group_count = 0
|
||||
select_bot.friend_count = 0
|
||||
select_bot.status = await BotConsole.get_bot_status(select_bot.self_id)
|
||||
|
||||
@ -250,7 +250,7 @@ class ApiDataSource:
|
||||
返回:
|
||||
GroupDetail | None: 群组详情数据
|
||||
"""
|
||||
group = await GroupConsole.get_or_none(group_id=group_id)
|
||||
group = await GroupConsole.get_group(group_id=group_id)
|
||||
if not group:
|
||||
return None
|
||||
like_plugin = await cls.__get_group_detail_like_plugin(group_id)
|
||||
|
||||
@ -4,6 +4,7 @@ from fastapi.responses import JSONResponse
|
||||
from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager
|
||||
|
||||
from ....base_model import Result
|
||||
from ....utils import authentication, clear_help_image
|
||||
@ -11,6 +12,7 @@ from .data_source import ApiDataSource
|
||||
from .model import (
|
||||
BatchUpdatePlugins,
|
||||
BatchUpdateResult,
|
||||
InstallDependenciesPayload,
|
||||
PluginCount,
|
||||
PluginDetail,
|
||||
PluginInfo,
|
||||
@ -162,9 +164,9 @@ async def _(module: str) -> Result[PluginDetail]:
|
||||
dependencies=[authentication()],
|
||||
response_model=Result[BatchUpdateResult],
|
||||
response_class=JSONResponse,
|
||||
summary="批量更新插件配置",
|
||||
description="批量更新插件配置",
|
||||
)
|
||||
async def batch_update_plugin_config_api(
|
||||
async def _(
|
||||
params: BatchUpdatePlugins,
|
||||
) -> Result[BatchUpdateResult]:
|
||||
"""批量更新插件配置,如开关、类型等"""
|
||||
@ -187,9 +189,9 @@ async def batch_update_plugin_config_api(
|
||||
"/menu_type/rename",
|
||||
dependencies=[authentication()],
|
||||
response_model=Result,
|
||||
summary="重命名菜单类型",
|
||||
description="重命名菜单类型",
|
||||
)
|
||||
async def rename_menu_type_api(payload: RenameMenuTypePayload) -> Result:
|
||||
async def _(payload: RenameMenuTypePayload) -> Result[str]:
|
||||
try:
|
||||
result = await ApiDataSource.rename_menu_type(
|
||||
old_name=payload.old_name, new_name=payload.new_name
|
||||
@ -213,3 +215,24 @@ async def rename_menu_type_api(payload: RenameMenuTypePayload) -> Result:
|
||||
except Exception as e:
|
||||
logger.error(f"{router.prefix}/menu_type/rename 调用错误", "WebUi", e=e)
|
||||
return Result.fail(info=f"发生未知错误: {type(e).__name__}")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/install_dependencies",
|
||||
dependencies=[authentication()],
|
||||
response_model=Result,
|
||||
response_class=JSONResponse,
|
||||
description="安装/卸载依赖",
|
||||
)
|
||||
async def _(payload: InstallDependenciesPayload) -> Result:
|
||||
try:
|
||||
if not payload.dependencies:
|
||||
return Result.fail("依赖列表不能为空")
|
||||
if payload.handle_type == "install":
|
||||
result = VirtualEnvPackageManager.install(payload.dependencies)
|
||||
else:
|
||||
result = VirtualEnvPackageManager.uninstall(payload.dependencies)
|
||||
return Result.ok(result)
|
||||
except Exception as e:
|
||||
logger.error(f"{router.prefix}/install_dependencies 调用错误", "WebUi", e=e)
|
||||
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
|
||||
|
||||
@ -167,7 +167,7 @@ class ApiDataSource:
|
||||
)
|
||||
|
||||
return {
|
||||
"success": len(errors) == 0,
|
||||
"success": not errors,
|
||||
"updated_count": updated_count + bulk_updated_count,
|
||||
"errors": errors,
|
||||
}
|
||||
@ -184,19 +184,24 @@ class ApiDataSource:
|
||||
config: ConfigGroup
|
||||
|
||||
返回:
|
||||
lPluginConfig: 配置数据
|
||||
PluginConfig: 配置数据
|
||||
"""
|
||||
type_str = ""
|
||||
type_inner = None
|
||||
if r := re.search(r"<class '(.*)'>", str(config.configs[cfg].type)):
|
||||
ct = str(config.configs[cfg].type)
|
||||
if r := re.search(r"<class '(.*)'>", ct):
|
||||
type_str = r[1]
|
||||
elif r := re.search(r"typing\.(.*)\[(.*)\]", str(config.configs[cfg].type)):
|
||||
elif (r := re.search(r"typing\.(.*)\[(.*)\]", ct)) or (
|
||||
r := re.search(r"(.*)\[(.*)\]", ct)
|
||||
):
|
||||
type_str = r[1]
|
||||
if type_str:
|
||||
type_str = type_str.lower()
|
||||
type_inner = r[2]
|
||||
if type_inner:
|
||||
type_inner = [x.strip() for x in type_inner.split(",")]
|
||||
else:
|
||||
type_str = ct
|
||||
return PluginConfig(
|
||||
module=module,
|
||||
key=cfg,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@ -162,3 +162,15 @@ class BatchUpdateResult(BaseModel):
|
||||
default_factory=list, description="错误信息列表"
|
||||
)
|
||||
"""错误信息列表"""
|
||||
|
||||
|
||||
class InstallDependenciesPayload(BaseModel):
|
||||
"""
|
||||
安装依赖
|
||||
"""
|
||||
|
||||
handle_type: Literal["install", "uninstall"] = Field(..., description="处理类型")
|
||||
"""处理类型"""
|
||||
|
||||
dependencies: list[str] = Field(..., description="依赖列表")
|
||||
"""依赖列表"""
|
||||
|
||||
@ -45,6 +45,7 @@ async def _(path: str | None = None) -> Result[list[DirFile]]:
|
||||
mtime=file_path.stat().st_mtime,
|
||||
)
|
||||
)
|
||||
data_list.sort(key=lambda f: f.name)
|
||||
return Result.ok(data_list)
|
||||
except Exception as e:
|
||||
return Result.fail(f"获取文件列表失败: {e!s}")
|
||||
|
||||
@ -13,8 +13,8 @@ class BotSetting(BaseModel):
|
||||
"""回复时NICKNAME"""
|
||||
system_proxy: str | None = None
|
||||
"""系统代理"""
|
||||
db_url: str = ""
|
||||
"""数据库链接"""
|
||||
db_url: str = "sqlite:data/zhenxun.db"
|
||||
"""数据库链接, 默认值为sqlite:data/zhenxun.db"""
|
||||
platform_superusers: dict[str, list[str]] = Field(default_factory=dict)
|
||||
"""平台超级用户"""
|
||||
qbot_id_data: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
from collections.abc import Callable
|
||||
import copy
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeVar, get_args, get_origin
|
||||
from typing import Any, TypeVar
|
||||
|
||||
import cattrs
|
||||
from nonebot.compat import model_dump
|
||||
from pydantic import VERSION, BaseModel, Field
|
||||
from pydantic import BaseModel, Field
|
||||
from ruamel.yaml import YAML
|
||||
from ruamel.yaml.scanner import ScannerError
|
||||
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.pydantic_compat import (
|
||||
_dump_pydantic_obj,
|
||||
_is_pydantic_type,
|
||||
model_dump,
|
||||
parse_as,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
AICallableParam,
|
||||
@ -39,46 +44,6 @@ class NoSuchConfig(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _dump_pydantic_obj(obj: Any) -> Any:
|
||||
"""
|
||||
递归地将一个对象内部的 Pydantic BaseModel 实例转换为字典。
|
||||
支持单个实例、实例列表、实例字典等情况。
|
||||
"""
|
||||
if isinstance(obj, BaseModel):
|
||||
return model_dump(obj)
|
||||
if isinstance(obj, list):
|
||||
return [_dump_pydantic_obj(item) for item in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {key: _dump_pydantic_obj(value) for key, value in obj.items()}
|
||||
return obj
|
||||
|
||||
|
||||
def _is_pydantic_type(t: Any) -> bool:
|
||||
"""
|
||||
递归检查一个类型注解是否与 Pydantic BaseModel 相关。
|
||||
"""
|
||||
if t is None:
|
||||
return False
|
||||
origin = get_origin(t)
|
||||
if origin:
|
||||
return any(_is_pydantic_type(arg) for arg in get_args(t))
|
||||
return isinstance(t, type) and issubclass(t, BaseModel)
|
||||
|
||||
|
||||
def parse_as(type_: type[T], obj: Any) -> T:
|
||||
"""
|
||||
一个兼容 Pydantic V1 的 parse_obj_as 和V2的TypeAdapter.validate_python 的辅助函数。
|
||||
"""
|
||||
if VERSION.startswith("1"):
|
||||
from pydantic import parse_obj_as
|
||||
|
||||
return parse_obj_as(type_, obj)
|
||||
else:
|
||||
from pydantic import TypeAdapter # type: ignore
|
||||
|
||||
return TypeAdapter(type_).validate_python(obj)
|
||||
|
||||
|
||||
class ConfigGroup(BaseModel):
|
||||
"""
|
||||
配置组
|
||||
@ -106,21 +71,34 @@ class ConfigGroup(BaseModel):
|
||||
if value_to_process is None:
|
||||
return default
|
||||
|
||||
if cfg.type:
|
||||
if _is_pydantic_type(cfg.type):
|
||||
if build_model:
|
||||
try:
|
||||
return parse_as(cfg.type, value_to_process)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Pydantic 模型解析失败 (key: {c.upper()}). ", e=e
|
||||
)
|
||||
if cfg.arg_parser:
|
||||
try:
|
||||
return cattrs.structure(value_to_process, cfg.type)
|
||||
return cfg.arg_parser(value_to_process)
|
||||
except Exception as e:
|
||||
logger.warning(f"Cattrs 结构化失败 (key: {key}),返回原始值。", e=e)
|
||||
logger.debug(
|
||||
f"配置项类型转换 MODULE: [<u><y>{self.module}</y></u>] | "
|
||||
f"KEY: [<u><y>{key}</y></u>] 的自定义解析器失败,将使用原始值",
|
||||
e=e,
|
||||
)
|
||||
return value_to_process
|
||||
|
||||
return value_to_process
|
||||
if not build_model or not cfg.type:
|
||||
return value_to_process
|
||||
|
||||
try:
|
||||
if _is_pydantic_type(cfg.type):
|
||||
parsed_value = parse_as(cfg.type, value_to_process)
|
||||
return parsed_value
|
||||
else:
|
||||
structured_value = cattrs.structure(value_to_process, cfg.type)
|
||||
return structured_value
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"❌ 配置项 '{self.module}.{key}' 自动类型转换失败 "
|
||||
f"(目标类型: {cfg.type}),将返回原始值。请检查配置文件格式。错误: {e}",
|
||||
e=e,
|
||||
)
|
||||
return value_to_process
|
||||
|
||||
def to_dict(self, **kwargs):
|
||||
return model_dump(self, **kwargs)
|
||||
@ -167,6 +145,48 @@ class ConfigsManager:
|
||||
if data := self._data.get(module):
|
||||
data.name = name
|
||||
|
||||
def _merge_dicts(self, new_data: dict, original_data: dict) -> dict:
|
||||
"""合并两个字典,只进行key值的新增和删除操作,不修改原有key的值
|
||||
|
||||
递归处理嵌套字典,确保所有层级的key保持一致
|
||||
|
||||
参数:
|
||||
new_data: 新数据字典
|
||||
original_data: 原数据字典
|
||||
|
||||
返回:
|
||||
合并后的字典
|
||||
"""
|
||||
result = dict(original_data)
|
||||
|
||||
for key, value in new_data.items():
|
||||
if key not in original_data:
|
||||
result[key] = value
|
||||
elif isinstance(value, dict) and isinstance(original_data[key], dict):
|
||||
result[key] = self._merge_dicts(value, original_data[key])
|
||||
|
||||
return result
|
||||
|
||||
def _normalize_config_data(self, value: Any, original_value: Any = None) -> Any:
|
||||
"""标准化配置数据,处理BaseModel和字典的情况
|
||||
|
||||
参数:
|
||||
value: 要标准化的值
|
||||
original_value: 原始值,用于合并字典
|
||||
|
||||
返回:
|
||||
标准化后的值
|
||||
"""
|
||||
processed_value = _dump_pydantic_obj(value)
|
||||
|
||||
if isinstance(processed_value, dict) and original_value is not None:
|
||||
processed_original = _dump_pydantic_obj(original_value)
|
||||
|
||||
if isinstance(processed_original, dict):
|
||||
return self._merge_dicts(processed_value, processed_original)
|
||||
|
||||
return processed_value
|
||||
|
||||
def add_plugin_config(
|
||||
self,
|
||||
module: str,
|
||||
@ -195,16 +215,16 @@ class ConfigsManager:
|
||||
ValueError: module和key不能为为空
|
||||
ValueError: 填写错误
|
||||
"""
|
||||
|
||||
key = key.upper()
|
||||
if not module or not key:
|
||||
raise ValueError("add_plugin_config: module和key不能为为空")
|
||||
if isinstance(value, BaseModel):
|
||||
value = model_dump(value)
|
||||
if isinstance(default_value, BaseModel):
|
||||
default_value = model_dump(default_value)
|
||||
|
||||
processed_value = _dump_pydantic_obj(value)
|
||||
processed_default_value = _dump_pydantic_obj(default_value)
|
||||
existing_value = None
|
||||
if module in self._data and (config := self._data[module].configs.get(key)):
|
||||
existing_value = config.value
|
||||
|
||||
processed_value = self._normalize_config_data(value, existing_value)
|
||||
processed_default_value = self._normalize_config_data(default_value)
|
||||
|
||||
self.add_module.append(f"{module}:{key}".lower())
|
||||
if module in self._data and (config := self._data[module].configs.get(key)):
|
||||
@ -282,7 +302,6 @@ class ConfigsManager:
|
||||
if value_to_process is None:
|
||||
return default
|
||||
|
||||
# 1. 最高优先级:自定义的参数解析器
|
||||
if config.arg_parser:
|
||||
try:
|
||||
return config.arg_parser(value_to_process)
|
||||
@ -338,14 +357,13 @@ class ConfigsManager:
|
||||
with open(self._simple_file, "w", encoding="utf8") as f:
|
||||
_yaml.dump(self._simple_data, f)
|
||||
path = path or self.file
|
||||
save_data = {}
|
||||
for module, config_group in self._data.items():
|
||||
save_data[module] = {}
|
||||
for config_key, config_model in config_group.configs.items():
|
||||
save_data[module][config_key] = model_dump(
|
||||
config_model, exclude={"type", "arg_parser"}
|
||||
)
|
||||
|
||||
save_data = {
|
||||
module: {
|
||||
config_key: model_dump(config_model, exclude={"type", "arg_parser"})
|
||||
for config_key, config_model in config_group.configs.items()
|
||||
}
|
||||
for module, config_group in self._data.items()
|
||||
}
|
||||
with open(path, "w", encoding="utf8") as f:
|
||||
_yaml.dump(save_data, f)
|
||||
|
||||
|
||||
@ -2,10 +2,10 @@ from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from nonebot.compat import model_dump
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from zhenxun.utils.enum import BlockType, LimitWatchType, PluginLimitType, PluginType
|
||||
from zhenxun.utils.pydantic_compat import model_dump
|
||||
|
||||
__all__ = [
|
||||
"AICallableParam",
|
||||
@ -65,7 +65,7 @@ class RegisterConfig(BaseModel):
|
||||
"""配置注解"""
|
||||
default_value: Any | None = None
|
||||
"""默认值"""
|
||||
type: Any = None
|
||||
type: object = None
|
||||
"""参数类型"""
|
||||
arg_parser: Callable | None = None
|
||||
"""参数解析"""
|
||||
@ -155,8 +155,6 @@ class AICallableProperties(BaseModel):
|
||||
"""参数类型"""
|
||||
description: str
|
||||
"""参数描述"""
|
||||
enums: list[str] | None = None
|
||||
"""参数枚举"""
|
||||
|
||||
|
||||
class AICallableParam(BaseModel):
|
||||
@ -265,6 +263,10 @@ class PluginExtraData(BaseModel):
|
||||
"""是否显示在菜单中"""
|
||||
smart_tools: list[AICallableTag] | None = None
|
||||
"""智能模式函数工具集"""
|
||||
introduction: str | None = None
|
||||
"""BOT自我介绍时插件的自我介绍"""
|
||||
precautions: list[str] | None = None
|
||||
"""BOT自我介绍时插件的注意事项"""
|
||||
|
||||
def to_dict(self, **kwargs):
|
||||
return model_dump(self, **kwargs)
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import time
|
||||
from typing import ClassVar
|
||||
from typing_extensions import Self
|
||||
|
||||
from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import CacheType, DbLockType
|
||||
from zhenxun.utils.exception import UserAndGroupIsNone
|
||||
|
||||
|
||||
@ -19,6 +21,8 @@ class BanConsole(Model):
|
||||
"""使用ban命令的用户等级"""
|
||||
ban_time = fields.BigIntField()
|
||||
"""ban开始的时间"""
|
||||
ban_reason = fields.TextField(null=True, default=None)
|
||||
"""ban的理由"""
|
||||
duration = fields.BigIntField()
|
||||
"""ban时长"""
|
||||
operator = fields.CharField(255)
|
||||
@ -27,6 +31,15 @@ class BanConsole(Model):
|
||||
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
|
||||
table = "ban_console"
|
||||
table_description = "封禁人员/群组数据表"
|
||||
unique_together = ("user_id", "group_id")
|
||||
indexes = [("user_id",), ("group_id",)] # noqa: RUF012
|
||||
|
||||
cache_type = CacheType.BAN
|
||||
"""缓存类型"""
|
||||
cache_key_field = ("user_id", "group_id")
|
||||
"""缓存键字段"""
|
||||
enable_lock: ClassVar[list[DbLockType]] = [DbLockType.CREATE, DbLockType.UPSERT]
|
||||
"""开启锁"""
|
||||
|
||||
@classmethod
|
||||
async def _get_data(cls, user_id: str | None, group_id: str | None) -> Self | None:
|
||||
@ -46,12 +59,12 @@ class BanConsole(Model):
|
||||
raise UserAndGroupIsNone()
|
||||
if user_id:
|
||||
return (
|
||||
await cls.get_or_none(user_id=user_id, group_id=group_id)
|
||||
await cls.safe_get_or_none(user_id=user_id, group_id=group_id)
|
||||
if group_id
|
||||
else await cls.get_or_none(user_id=user_id, group_id__isnull=True)
|
||||
else await cls.safe_get_or_none(user_id=user_id, group_id__isnull=True)
|
||||
)
|
||||
else:
|
||||
return await cls.get_or_none(user_id="", group_id=group_id)
|
||||
return await cls.safe_get_or_none(user_id="", group_id=group_id)
|
||||
|
||||
@classmethod
|
||||
async def check_ban_level(
|
||||
@ -96,7 +109,9 @@ class BanConsole(Model):
|
||||
if user.duration == -1:
|
||||
return -1
|
||||
_time = time.time() - (user.ban_time + user.duration)
|
||||
return 0 if _time > 0 else int(time.time() - user.ban_time - user.duration)
|
||||
if _time < 0:
|
||||
return int(time.time() - user.ban_time - user.duration)
|
||||
await user.delete()
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
@ -122,6 +137,7 @@ class BanConsole(Model):
|
||||
user_id: str | None,
|
||||
group_id: str | None,
|
||||
ban_level: int,
|
||||
reason: str | None,
|
||||
duration: int,
|
||||
operator: str | None = None,
|
||||
):
|
||||
@ -146,6 +162,7 @@ class BanConsole(Model):
|
||||
group_id=group_id,
|
||||
ban_level=ban_level,
|
||||
ban_time=int(time.time()),
|
||||
ban_reason=reason,
|
||||
duration=duration,
|
||||
operator=operator or 0,
|
||||
)
|
||||
@ -167,3 +184,33 @@ class BanConsole(Model):
|
||||
await user.delete()
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def get_ban(
|
||||
cls,
|
||||
*,
|
||||
id: int | None = None,
|
||||
user_id: str | None = None,
|
||||
group_id: str | None = None,
|
||||
) -> Self | None:
|
||||
"""安全地获取ban记录
|
||||
|
||||
参数:
|
||||
id: 记录id
|
||||
user_id: 用户id
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
Self | None: ban记录
|
||||
"""
|
||||
if id is not None:
|
||||
return await cls.safe_get_or_none(id=id)
|
||||
return await cls._get_data(user_id, group_id)
|
||||
|
||||
@classmethod
|
||||
async def _run_script(cls):
|
||||
return [
|
||||
"CREATE INDEX idx_ban_console_user_id ON ban_console(user_id);",
|
||||
"CREATE INDEX idx_ban_console_group_id ON ban_console(group_id);",
|
||||
"ALTER TABLE ban_console ADD COLUMN ban_reason TEXT DEFAULT NULL;",
|
||||
]
|
||||
|
||||
@ -3,6 +3,7 @@ from typing import Literal, overload
|
||||
from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
from zhenxun.utils.enum import CacheType
|
||||
|
||||
|
||||
class BotConsole(Model):
|
||||
@ -29,6 +30,11 @@ class BotConsole(Model):
|
||||
table = "bot_console"
|
||||
table_description = "Bot数据表"
|
||||
|
||||
cache_type = CacheType.BOT
|
||||
"""缓存类型"""
|
||||
cache_key_field = "bot_id"
|
||||
"""缓存键字段"""
|
||||
|
||||
@staticmethod
|
||||
def format(name: str) -> str:
|
||||
return f"<{name},"
|
||||
|
||||
@ -49,7 +49,8 @@ class ChatHistory(Model):
|
||||
o = "-" if order == "DESC" else ""
|
||||
query = cls.filter(group_id=gid) if gid else cls
|
||||
if date_scope:
|
||||
query = query.filter(create_time__range=date_scope)
|
||||
filter_scope = (date_scope[0].isoformat(" "), date_scope[1].isoformat(" "))
|
||||
query = query.filter(create_time__range=filter_scope)
|
||||
return list(
|
||||
await query.annotate(count=Count("user_id"))
|
||||
.order_by(f"{o}count")
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
from typing_extensions import Self
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
@ -6,9 +7,13 @@ from tortoise import fields
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.db_context import Model
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.common_utils import SqlUtils
|
||||
from zhenxun.utils.enum import RequestHandleType, RequestType
|
||||
from zhenxun.utils.exception import NotFoundError
|
||||
from zhenxun.utils.manager.bot_profile_manager import BotProfileManager
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
|
||||
class FgRequest(Model):
|
||||
@ -123,6 +128,27 @@ class FgRequest(Model):
|
||||
await bot.set_friend_add_request(
|
||||
flag=req.flag, approve=handle_type == RequestHandleType.APPROVE
|
||||
)
|
||||
if BotProfileManager.is_auto_send_profile():
|
||||
file_path = await BotProfileManager.build_bot_profile_image(
|
||||
bot.self_id
|
||||
)
|
||||
if file_path:
|
||||
await asyncio.sleep(2)
|
||||
await PlatformUtils.send_message(
|
||||
bot,
|
||||
req.user_id,
|
||||
None,
|
||||
MessageUtils.build_message(
|
||||
[
|
||||
f"你好,我是{BotConfig.self_nickname}, "
|
||||
"初次见面,希望我们可以好好相处!",
|
||||
file_path,
|
||||
]
|
||||
),
|
||||
)
|
||||
logger.info(
|
||||
"添加好友自动发送BOT自我介绍图片", session=req.user_id
|
||||
)
|
||||
else:
|
||||
await GroupConsole.update_or_create(
|
||||
group_id=req.group_id, defaults={"group_flag": 1}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Any, cast, overload
|
||||
from typing import Any, ClassVar, cast, overload
|
||||
from typing_extensions import Self
|
||||
|
||||
from tortoise import fields
|
||||
@ -6,8 +6,9 @@ from tortoise.backends.base.client import BaseDBAsyncClient
|
||||
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.services.cache import CacheRoot
|
||||
from zhenxun.services.db_context import Model
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.enum import CacheType, DbLockType, PluginType
|
||||
|
||||
|
||||
def add_disable_marker(name: str) -> str:
|
||||
@ -86,6 +87,16 @@ class GroupConsole(Model):
|
||||
table = "group_console"
|
||||
table_description = "群组信息表"
|
||||
unique_together = ("group_id", "channel_id")
|
||||
indexes = [ # noqa: RUF012
|
||||
("group_id",)
|
||||
]
|
||||
|
||||
cache_type = CacheType.GROUPS
|
||||
"""缓存类型"""
|
||||
cache_key_field = ("group_id", "channel_id")
|
||||
"""缓存键字段"""
|
||||
enable_lock: ClassVar[list[DbLockType]] = [DbLockType.CREATE, DbLockType.UPSERT]
|
||||
"""开启锁"""
|
||||
|
||||
@classmethod
|
||||
async def _get_task_modules(cls, *, default_status: bool) -> list[str]:
|
||||
@ -116,6 +127,18 @@ class GroupConsole(Model):
|
||||
).values_list("module", flat=True),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def _update_cache(cls, instance):
|
||||
"""更新缓存
|
||||
|
||||
参数:
|
||||
instance: 需要更新缓存的实例
|
||||
"""
|
||||
if cache_type := cls.get_cache_type():
|
||||
key = cls.get_cache_key(instance)
|
||||
if key is not None:
|
||||
await CacheRoot.invalidate_cache(cache_type, key)
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
cls, using_db: BaseDBAsyncClient | None = None, **kwargs: Any
|
||||
@ -129,6 +152,9 @@ class GroupConsole(Model):
|
||||
if task_modules or plugin_modules:
|
||||
await cls._update_modules(group, task_modules, plugin_modules, using_db)
|
||||
|
||||
# 更新缓存
|
||||
await cls._update_cache(group)
|
||||
|
||||
return group
|
||||
|
||||
@classmethod
|
||||
@ -180,6 +206,10 @@ class GroupConsole(Model):
|
||||
if task_modules or plugin_modules:
|
||||
await cls._update_modules(group, task_modules, plugin_modules, using_db)
|
||||
|
||||
# 更新缓存
|
||||
if is_create:
|
||||
await cls._update_cache(group)
|
||||
|
||||
return group, is_create
|
||||
|
||||
@classmethod
|
||||
@ -202,24 +232,39 @@ class GroupConsole(Model):
|
||||
if task_modules or plugin_modules:
|
||||
await cls._update_modules(group, task_modules, plugin_modules, using_db)
|
||||
|
||||
# 更新缓存
|
||||
await cls._update_cache(group)
|
||||
|
||||
return group, is_create
|
||||
|
||||
@classmethod
|
||||
async def get_group(
|
||||
cls, group_id: str, channel_id: str | None = None
|
||||
cls,
|
||||
group_id: str,
|
||||
channel_id: str | None = None,
|
||||
clean_duplicates: bool = True,
|
||||
) -> Self | None:
|
||||
"""获取群组
|
||||
|
||||
参数:
|
||||
group_id: 群组id
|
||||
channel_id: 频道id.
|
||||
channel_id: 频道id
|
||||
clean_duplicates: 是否删除重复的记录,仅保留最新的
|
||||
|
||||
返回:
|
||||
Self: GroupConsole
|
||||
"""
|
||||
if channel_id:
|
||||
return await cls.get_or_none(group_id=group_id, channel_id=channel_id)
|
||||
return await cls.get_or_none(group_id=group_id, channel_id__isnull=True)
|
||||
return await cls.safe_get_or_none(
|
||||
group_id=group_id,
|
||||
channel_id=channel_id,
|
||||
clean_duplicates=clean_duplicates,
|
||||
)
|
||||
return await cls.safe_get_or_none(
|
||||
group_id=group_id,
|
||||
channel_id__isnull=True,
|
||||
clean_duplicates=clean_duplicates,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def is_super_group(cls, group_id: str) -> bool:
|
||||
@ -303,6 +348,9 @@ class GroupConsole(Model):
|
||||
if update_fields:
|
||||
await group.save(update_fields=update_fields)
|
||||
|
||||
# 更新缓存
|
||||
await cls._update_cache(group)
|
||||
|
||||
@classmethod
|
||||
async def set_unblock_plugin(
|
||||
cls,
|
||||
@ -339,6 +387,9 @@ class GroupConsole(Model):
|
||||
if update_fields:
|
||||
await group.save(update_fields=update_fields)
|
||||
|
||||
# 更新缓存
|
||||
await cls._update_cache(group)
|
||||
|
||||
@classmethod
|
||||
async def is_normal_block_plugin(
|
||||
cls, group_id: str, module: str, channel_id: str | None = None
|
||||
@ -442,6 +493,9 @@ class GroupConsole(Model):
|
||||
if update_fields:
|
||||
await group.save(update_fields=update_fields)
|
||||
|
||||
# 更新缓存
|
||||
await cls._update_cache(group)
|
||||
|
||||
@classmethod
|
||||
async def set_unblock_task(
|
||||
cls,
|
||||
@ -476,6 +530,9 @@ class GroupConsole(Model):
|
||||
if update_fields:
|
||||
await group.save(update_fields=update_fields)
|
||||
|
||||
# 更新缓存
|
||||
await cls._update_cache(group)
|
||||
|
||||
@classmethod
|
||||
def _run_script(cls):
|
||||
return [
|
||||
@ -483,4 +540,6 @@ class GroupConsole(Model):
|
||||
" character varying(255) NOT NULL DEFAULT '';",
|
||||
"ALTER TABLE group_console ADD superuser_block_task"
|
||||
" character varying(255) NOT NULL DEFAULT '';",
|
||||
"CREATE INDEX idx_group_console_group_id ON group_console(group_id);",
|
||||
"CREATE INDEX idx_group_console_group_null_channel ON group_console(group_id) WHERE channel_id IS NULL;", # 单独创建channel为空的索引 # noqa: E501
|
||||
]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from tortoise import fields
|
||||
|
||||
from zhenxun.services.db_context import Model
|
||||
from zhenxun.utils.enum import CacheType
|
||||
|
||||
|
||||
class LevelUser(Model):
|
||||
@ -20,6 +21,11 @@ class LevelUser(Model):
|
||||
table_description = "用户权限数据库"
|
||||
unique_together = ("user_id", "group_id")
|
||||
|
||||
cache_type = CacheType.LEVEL
|
||||
"""缓存类型"""
|
||||
cache_key_field = ("user_id", "group_id")
|
||||
"""缓存键字段"""
|
||||
|
||||
@classmethod
|
||||
async def get_user_level(cls, user_id: str, group_id: str | None) -> int:
|
||||
"""获取用户在群内的等级
|
||||
@ -53,6 +59,9 @@ class LevelUser(Model):
|
||||
level: 权限等级
|
||||
group_flag: 是否被自动更新刷新权限 0:是, 1:否.
|
||||
"""
|
||||
if await cls.exists(user_id=user_id, group_id=group_id, user_level=level):
|
||||
# 权限相同时跳过
|
||||
return
|
||||
await cls.update_or_create(
|
||||
user_id=user_id,
|
||||
group_id=group_id,
|
||||
@ -90,13 +99,14 @@ class LevelUser(Model):
|
||||
返回:
|
||||
bool: 是否大于level
|
||||
"""
|
||||
if level == 0:
|
||||
return True
|
||||
if group_id:
|
||||
if user := await cls.get_or_none(user_id=user_id, group_id=group_id):
|
||||
return user.user_level >= level
|
||||
else:
|
||||
if user_list := await cls.filter(user_id=user_id).all():
|
||||
user = max(user_list, key=lambda x: x.user_level)
|
||||
return user.user_level >= level
|
||||
elif user_list := await cls.filter(user_id=user_id).all():
|
||||
user = max(user_list, key=lambda x: x.user_level)
|
||||
return user.user_level >= level
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
@ -119,8 +129,7 @@ class LevelUser(Model):
|
||||
return [
|
||||
# 将user_id改为user_id
|
||||
"ALTER TABLE level_users RENAME COLUMN user_qq TO user_id;",
|
||||
"ALTER TABLE level_users "
|
||||
"ALTER COLUMN user_id TYPE character varying(255);",
|
||||
"ALTER TABLE level_users ALTER COLUMN user_id TYPE character varying(255);",
|
||||
# 将user_id字段类型改为character varying(255)
|
||||
"ALTER TABLE level_users "
|
||||
"ALTER COLUMN group_id TYPE character varying(255);",
|
||||
|
||||
@ -4,7 +4,7 @@ from tortoise import fields
|
||||
|
||||
from zhenxun.models.plugin_limit import PluginLimit # noqa: F401
|
||||
from zhenxun.services.db_context import Model
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
from zhenxun.utils.enum import BlockType, CacheType, PluginType
|
||||
|
||||
|
||||
class PluginInfo(Model):
|
||||
@ -59,6 +59,11 @@ class PluginInfo(Model):
|
||||
table = "plugin_info"
|
||||
table_description = "插件基本信息"
|
||||
|
||||
cache_type = CacheType.PLUGINS
|
||||
"""缓存类型"""
|
||||
cache_key_field = "module"
|
||||
"""缓存键字段"""
|
||||
|
||||
@classmethod
|
||||
async def get_plugin(
|
||||
cls, load_status: bool = True, filter_parent: bool = True, **kwargs
|
||||
|
||||
@ -2,7 +2,7 @@ from tortoise import fields
|
||||
|
||||
from zhenxun.models.goods_info import GoodsInfo
|
||||
from zhenxun.services.db_context import Model
|
||||
from zhenxun.utils.enum import GoldHandle
|
||||
from zhenxun.utils.enum import CacheType, GoldHandle
|
||||
from zhenxun.utils.exception import GoodsNotFound, InsufficientGold
|
||||
|
||||
from .user_gold_log import UserGoldLog
|
||||
@ -29,6 +29,12 @@ class UserConsole(Model):
|
||||
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
|
||||
table = "user_console"
|
||||
table_description = "用户数据表"
|
||||
indexes = [("user_id",), ("uid",)] # noqa: RUF012
|
||||
|
||||
cache_type = CacheType.USERS
|
||||
"""缓存类型"""
|
||||
cache_key_field = "user_id"
|
||||
"""缓存键字段"""
|
||||
|
||||
@classmethod
|
||||
async def get_user(cls, user_id: str, platform: str | None = None) -> "UserConsole":
|
||||
@ -193,3 +199,10 @@ class UserConsole(Model):
|
||||
if goods := await GoodsInfo.get_or_none(goods_name=name):
|
||||
return await cls.use_props(user_id, goods.uuid, num, platform)
|
||||
raise GoodsNotFound("未找到商品...")
|
||||
|
||||
@classmethod
|
||||
async def _run_script(cls):
|
||||
return [
|
||||
"CREATE INDEX idx_user_console_user_id ON user_console(user_id);",
|
||||
"CREATE INDEX idx_user_console_uid ON user_console(uid);",
|
||||
]
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
"""
|
||||
Zhenxun Bot - 核心服务模块
|
||||
|
||||
主要服务包括:
|
||||
- 数据库上下文 (db_context): 提供数据库模型基类和连接管理。
|
||||
- 日志服务 (log): 提供增强的、带上下文的日志记录器。
|
||||
- LLM服务 (llm): 提供与大语言模型交互的统一API。
|
||||
- 插件生命周期管理 (plugin_init): 支持插件安装和卸载时的钩子函数。
|
||||
- 定时任务调度器 (scheduler): 提供持久化的、可管理的定时任务服务。
|
||||
"""
|
||||
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_apscheduler")
|
||||
@ -6,3 +17,60 @@ require("nonebot_plugin_session")
|
||||
require("nonebot_plugin_htmlrender")
|
||||
require("nonebot_plugin_uninfo")
|
||||
require("nonebot_plugin_waiter")
|
||||
|
||||
from .db_context import Model, disconnect, with_db_timeout
|
||||
from .llm import (
|
||||
AI,
|
||||
AIConfig,
|
||||
CommonOverrides,
|
||||
LLMContentPart,
|
||||
LLMException,
|
||||
LLMGenerationConfig,
|
||||
LLMMessage,
|
||||
chat,
|
||||
clear_model_cache,
|
||||
code,
|
||||
create_multimodal_message,
|
||||
embed,
|
||||
generate,
|
||||
generate_structured,
|
||||
get_cache_stats,
|
||||
get_model_instance,
|
||||
list_available_models,
|
||||
list_embedding_models,
|
||||
search,
|
||||
set_global_default_model_name,
|
||||
)
|
||||
from .log import logger
|
||||
from .plugin_init import PluginInit, PluginInitManager
|
||||
from .scheduler import scheduler_manager
|
||||
|
||||
__all__ = [
|
||||
"AI",
|
||||
"AIConfig",
|
||||
"CommonOverrides",
|
||||
"LLMContentPart",
|
||||
"LLMException",
|
||||
"LLMGenerationConfig",
|
||||
"LLMMessage",
|
||||
"Model",
|
||||
"PluginInit",
|
||||
"PluginInitManager",
|
||||
"chat",
|
||||
"clear_model_cache",
|
||||
"code",
|
||||
"create_multimodal_message",
|
||||
"disconnect",
|
||||
"embed",
|
||||
"generate",
|
||||
"generate_structured",
|
||||
"get_cache_stats",
|
||||
"get_model_instance",
|
||||
"list_available_models",
|
||||
"list_embedding_models",
|
||||
"logger",
|
||||
"scheduler_manager",
|
||||
"search",
|
||||
"set_global_default_model_name",
|
||||
"with_db_timeout",
|
||||
]
|
||||
|
||||
1056
zhenxun/services/cache/__init__.py
vendored
Normal file
1056
zhenxun/services/cache/__init__.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user