bugfix:修复插件商店插件卸载问题,显示问题,移除权限检查多余部分 (#2124)

* bugfix:修复插件商店插件卸载问题,显示问题,移除权限检查多余部分

* bugfix:修复撤回问题,插件商店显示问题
This commit is contained in:
Copaan
2026-04-21 09:30:47 +08:00
committed by GitHub
parent 4000389a60
commit e53cae09b6
11 changed files with 198 additions and 260 deletions
@@ -11,7 +11,6 @@ from zhenxun.utils.utils import get_entity_ids
from .config import LOGGER_COMMAND, WARNING_THRESHOLD from .config import LOGGER_COMMAND, WARNING_THRESHOLD
from .exception import SkipPluginException from .exception import SkipPluginException
from .utils import send_message
async def auth_admin( async def auth_admin(
@@ -51,29 +50,24 @@ async def auth_admin(
user_level = max(user_level, group_users.user_level) user_level = max(user_level, group_users.user_level)
if user_level < plugin.admin_level: if user_level < plugin.admin_level:
await send_message( raise SkipPluginException(
session, f"{plugin.name}({plugin.module}) 管理员权限不足...",
[ tip_message=[
At(flag="user", target=session.user.id), At(flag="user", target=session.user.id),
f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}", f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}",
], ],
entity.user_id, tip_check_tag=entity.user_id,
background=True, tip_background=True,
)
raise SkipPluginException(
f"{plugin.name}({plugin.module}) 管理员权限不足..."
) )
elif global_user: elif global_user:
if global_user.user_level < plugin.admin_level: if global_user.user_level < plugin.admin_level:
await send_message(
session,
f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}",
background=True,
)
raise SkipPluginException( raise SkipPluginException(
f"{plugin.name}({plugin.module}) 管理员权限不足..." f"{plugin.name}({plugin.module}) 管理员权限不足...",
tip_message=(
f"你的权限不足喔,该功能需要的权限等级: "
f"{plugin.admin_level}"
),
tip_background=True,
) )
finally: finally:
# 记录执行时间 # 记录执行时间
+9 -15
View File
@@ -1,7 +1,6 @@
import asyncio import asyncio
import time import time
from nonebot.adapters import Bot
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot_plugin_alconna import At from nonebot_plugin_alconna import At
from nonebot_plugin_uninfo import Uninfo from nonebot_plugin_uninfo import Uninfo
@@ -18,7 +17,7 @@ from zhenxun.utils.utils import EntityIDs, get_entity_ids
from .config import LOGGER_COMMAND, WARNING_THRESHOLD from .config import LOGGER_COMMAND, WARNING_THRESHOLD
from .exception import SkipPluginException from .exception import SkipPluginException
from .utils import freq, send_message from .utils import freq
Config.add_plugin_config( Config.add_plugin_config(
"hook", "hook",
@@ -189,20 +188,15 @@ async def user_handle(plugin: PluginInfo, entity: EntityIDs, session: Uninfo) ->
and ban_result and ban_result
and freq.is_send_limit_message(plugin, entity.user_id, False) and freq.is_send_limit_message(plugin, entity.user_id, False)
): ):
try: raise SkipPluginException(
await asyncio.wait_for( "用户处于黑名单中...",
send_message( tip_message=[
session,
[
At(flag="user", target=entity.user_id), At(flag="user", target=entity.user_id),
f"{ban_result}\n在..在 {time_str} 后才会理你喔", f"{ban_result}\n在..在 {time_str} 后才会理你喔",
], ],
entity.user_id, tip_check_tag=entity.user_id,
), tip_timeout=DB_TIMEOUT_SECONDS,
timeout=DB_TIMEOUT_SECONDS,
) )
except asyncio.TimeoutError:
logger.error(f"发送消息超时: {entity.user_id}", LOGGER_COMMAND)
raise SkipPluginException("用户处于黑名单中...") raise SkipPluginException("用户处于黑名单中...")
finally: finally:
# 记录执行时间 # 记录执行时间
@@ -217,16 +211,16 @@ async def user_handle(plugin: PluginInfo, entity: EntityIDs, session: Uninfo) ->
async def auth_ban( async def auth_ban(
matcher: Matcher, matcher: Matcher,
bot: Bot,
session: Uninfo, session: Uninfo,
plugin: PluginInfo, plugin: PluginInfo,
*,
entity: EntityIDs | None = None, entity: EntityIDs | None = None,
is_superuser: bool = False,
) -> None: ) -> None:
"""权限检查 - ban 检查 """权限检查 - ban 检查
参数: 参数:
matcher: Matcher matcher: Matcher
bot: Bot
session: Uninfo session: Uninfo
""" """
start_time = time.time() start_time = time.time()
@@ -237,7 +231,7 @@ async def auth_ban(
return return
if entity is None: if entity is None:
entity = get_entity_ids(session) entity = get_entity_ids(session)
if entity.user_id in bot.config.superusers: if is_superuser:
return return
if entity.group_id: if entity.group_id:
try: try:
@@ -8,7 +8,6 @@ from zhenxun.services.log import logger
from .config import LOGGER_COMMAND, WARNING_THRESHOLD from .config import LOGGER_COMMAND, WARNING_THRESHOLD
from .exception import SkipPluginException from .exception import SkipPluginException
from .utils import send_message
DEFAULT_GOLD = 100 DEFAULT_GOLD = 100
@@ -32,8 +31,10 @@ async def auth_cost(
user_gold = user.gold if user else DEFAULT_GOLD user_gold = user.gold if user else DEFAULT_GOLD
if user_gold < plugin.cost_gold: if user_gold < plugin.cost_gold:
"""插件消耗金币不足""" """插件消耗金币不足"""
await send_message(session, f"金币不足..该功能需要{plugin.cost_gold}金币..") raise SkipPluginException(
raise SkipPluginException(f"{plugin.name}({plugin.module}) 金币限制...") f"{plugin.name}({plugin.module}) 金币限制...",
tip_message=f"金币不足..该功能需要{plugin.cost_gold}金币..",
)
return plugin.cost_gold return plugin.cost_gold
finally: finally:
# 记录执行时间 # 记录执行时间
@@ -11,7 +11,7 @@ from zhenxun.utils.enum import BlockType
from .config import LOGGER_COMMAND, WARNING_THRESHOLD from .config import LOGGER_COMMAND, WARNING_THRESHOLD
from .exception import IsSuperuserException, SkipPluginException from .exception import IsSuperuserException, SkipPluginException
from .utils import freq, is_poke, send_message from .utils import freq, is_poke
def _get_group_block_sets( def _get_group_block_sets(
@@ -59,46 +59,41 @@ class GroupCheck:
self.group_data self.group_data
and self.plugin.module in self.superuser_block_plugin_set and self.plugin.module in self.superuser_block_plugin_set
): ):
if freq.is_send_limit_message( should_tip = freq.is_send_limit_message(
self.plugin, self.group_id, self.is_poke self.plugin, self.group_id, self.is_poke
):
await send_message(
self.session,
"超级管理员禁用了该群此功能...",
self.group_id,
background=True,
) )
raise SkipPluginException( raise SkipPluginException(
f"{self.plugin.name}({self.plugin.module})" f"{self.plugin.name}({self.plugin.module})"
f" 超级管理员禁用了该群此功能..." f" 超级管理员禁用了该群此功能...",
tip_message=(
"超级管理员禁用了该群此功能..." if should_tip else None
),
tip_check_tag=self.group_id if should_tip else None,
tip_background=should_tip,
) )
# 检查普通禁用 # 检查普通禁用
if self.group_data and self.plugin.module in self.block_plugin_set: if self.group_data and self.plugin.module in self.block_plugin_set:
if freq.is_send_limit_message( should_tip = freq.is_send_limit_message(
self.plugin, self.group_id, self.is_poke self.plugin, self.group_id, self.is_poke
):
await send_message(
self.session,
"该群未开启此功能...",
self.group_id,
background=True,
) )
raise SkipPluginException( raise SkipPluginException(
f"{self.plugin.name}({self.plugin.module}) 未开启此功能..." f"{self.plugin.name}({self.plugin.module}) 未开启此功能...",
tip_message="该群未开启此功能..." if should_tip else None,
tip_check_tag=self.group_id if should_tip else None,
tip_background=should_tip,
) )
# 检查全局禁用 # 检查全局禁用
if self.plugin.block_type == BlockType.GROUP: if self.plugin.block_type == BlockType.GROUP:
if freq.is_send_limit_message(self.plugin, self.group_id, self.is_poke): should_tip = freq.is_send_limit_message(
await send_message( self.plugin, self.group_id, self.is_poke
self.session,
"该功能在群组中已被禁用...",
self.group_id,
background=True,
) )
raise SkipPluginException( raise SkipPluginException(
f"{self.plugin.name}({self.plugin.module})该插件在群组中已被禁用..." f"{self.plugin.name}({self.plugin.module})该插件在群组中已被禁用...",
tip_message="该功能在群组中已被禁用..." if should_tip else None,
tip_check_tag=self.group_id if should_tip else None,
tip_background=should_tip,
) )
finally: finally:
# 记录执行时间 # 记录执行时间
@@ -131,14 +126,14 @@ class PluginCheck:
IgnoredException: 忽略插件 IgnoredException: 忽略插件
""" """
if plugin.block_type == BlockType.PRIVATE: if plugin.block_type == BlockType.PRIVATE:
if freq.is_send_limit_message(plugin, self.session.user.id, self.is_poke): should_tip = freq.is_send_limit_message(
await send_message( plugin, self.session.user.id, self.is_poke
self.session,
"该功能在私聊中已被禁用...",
background=True,
) )
raise SkipPluginException( raise SkipPluginException(
f"{plugin.name}({plugin.module}) 该插件在私聊中已被禁用..." f"{plugin.name}({plugin.module}) 该插件在私聊中已被禁用...",
tip_message="该功能在私聊中已被禁用..." if should_tip else None,
tip_check_tag=self.session.user.id if should_tip else None,
tip_background=should_tip,
) )
async def check_global(self, plugin: PluginInfo): async def check_global(self, plugin: PluginInfo):
@@ -159,15 +154,12 @@ class PluginCheck:
raise IsSuperuserException() raise IsSuperuserException()
sid = self.group_id or self.session.user.id sid = self.group_id or self.session.user.id
if freq.is_send_limit_message(plugin, sid, self.is_poke): should_tip = freq.is_send_limit_message(plugin, sid, self.is_poke)
await send_message(
self.session,
"全局未开启此功能...",
sid,
background=True,
)
raise SkipPluginException( raise SkipPluginException(
f"{plugin.name}({plugin.module}) 全局未开启此功能..." f"{plugin.name}({plugin.module}) 全局未开启此功能...",
tip_message="全局未开启此功能..." if should_tip else None,
tip_check_tag=sid if should_tip else None,
tip_background=should_tip,
) )
finally: finally:
# 记录执行时间 # 记录执行时间
@@ -3,9 +3,21 @@ class IsSuperuserException(Exception):
class SkipPluginException(Exception): class SkipPluginException(Exception):
def __init__(self, info: str, *args: object) -> None: def __init__(
self,
info: str,
*args: object,
tip_message: list | str | None = None,
tip_check_tag: str | None = None,
tip_background: bool = False,
tip_timeout: float | None = None,
) -> None:
super().__init__(*args) super().__init__(*args)
self.info = info self.info = info
self.tip_message = tip_message
self.tip_check_tag = tip_check_tag
self.tip_background = tip_background
self.tip_timeout = tip_timeout
def __str__(self) -> str: def __str__(self) -> str:
return self.info return self.info
+50 -95
View File
@@ -36,7 +36,7 @@ from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.utils import get_entity_ids from zhenxun.utils.utils import get_entity_ids
from .auth.auth_admin import auth_admin from .auth.auth_admin import auth_admin
from .auth.auth_ban import auth_ban, is_ban from .auth.auth_ban import auth_ban
from .auth.auth_bot import auth_bot from .auth.auth_bot import auth_bot
from .auth.auth_cost import auth_cost from .auth.auth_cost import auth_cost
from .auth.auth_group import auth_group from .auth.auth_group import auth_group
@@ -49,6 +49,7 @@ from .auth.exception import (
PermissionExemption, PermissionExemption,
SkipPluginException, SkipPluginException,
) )
from .auth.utils import send_message
AUTH_HOOKS_CONCURRENCY_LIMIT = 5 AUTH_HOOKS_CONCURRENCY_LIMIT = 5
AUTH_DB_CONCURRENCY_LIMIT = 6 AUTH_DB_CONCURRENCY_LIMIT = 6
@@ -461,14 +462,26 @@ def _matcher_route_cache_key(event: Event) -> str:
def _event_plain_text(event: Event) -> str: def _event_plain_text(event: Event) -> str:
def _normalize(text: str) -> str:
normalized = text.strip()
if not normalized:
return ""
# strip leading placeholders like "[reply:id=10004]撤回"
normalized = re.sub(
r"^(?:\s*(?:\[[^\]]*]|\<[^>]*>))+\s*",
"",
normalized,
)
return normalized.strip()
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
# Use raw_message if available (OneBot v11) to get the original text # Use raw_message if available (OneBot v11) to get the original text
# before nickname stripping. This ensures command matching works correctly # before nickname stripping. This ensures command matching works correctly
# for commands like "真寻日报" when "真寻" is a bot nickname. # for commands like "真寻日报" when "真寻" is a bot nickname.
raw = getattr(event, "raw_message", None) raw = getattr(event, "raw_message", None)
if isinstance(raw, str) and raw: if isinstance(raw, str) and raw:
return raw.strip() return _normalize(raw)
return (event.get_plaintext() or "").strip() return _normalize(event.get_plaintext() or "")
return "" return ""
@@ -1028,12 +1041,6 @@ async def _fetch_user_readonly(
) )
async def _fetch_plugin(plugin_dao: DataAccess, module: str) -> PluginInfo | None:
return await with_timeout(
plugin_dao.safe_get_or_none(module=module), name="get_plugin"
)
async def get_plugin_and_user( async def get_plugin_and_user(
module: str, module: str,
user_id: str, user_id: str,
@@ -1078,7 +1085,11 @@ async def get_plugin_and_user(
async def get_plugin_cost( async def get_plugin_cost(
bot: Bot, user: UserConsole | None, plugin: PluginInfo, session: Uninfo user: UserConsole | None,
plugin: PluginInfo,
session: Uninfo,
*,
is_superuser: bool = False,
) -> int: ) -> int:
"""获取插件费用 """获取插件费用
@@ -1096,7 +1107,7 @@ async def get_plugin_cost(
int: 调用插件金币费用 int: 调用插件金币费用
""" """
cost_gold = await with_timeout(auth_cost(user, plugin, session), name="auth_cost") cost_gold = await with_timeout(auth_cost(user, plugin, session), name="auth_cost")
if session.user.id in bot.config.superusers: if is_superuser:
if plugin.plugin_type == PluginType.SUPERUSER: if plugin.plugin_type == PluginType.SUPERUSER:
raise IsSuperuserException() raise IsSuperuserException()
if not plugin.limit_superuser: if not plugin.limit_superuser:
@@ -1184,30 +1195,6 @@ async def _leave_hooks_section():
HOOKS_ACTIVE_COUNT = max(HOOKS_ACTIVE_COUNT - 1, 0) HOOKS_ACTIVE_COUNT = max(HOOKS_ACTIVE_COUNT - 1, 0)
async def auth_ban_fast(
matcher: Matcher, event: Event, bot: Bot, session: Uninfo
) -> None:
"""快速 ban 检测(仅使用内存缓存),用于前置快速裁决。"""
entity = get_entity_ids(session)
event_cache = _get_event_cache(event, session, entity)
if event_cache is not None and event_cache.get("ban_state") is True:
raise SkipPluginException("user or group banned (cached)")
if entity.user_id in bot.config.superusers:
if event_cache is not None:
event_cache["ban_state"] = False
return
if entity.group_id and await is_ban(None, entity.group_id):
if event_cache is not None:
event_cache["ban_state"] = True
raise SkipPluginException("group banned (fast)")
if entity.user_id and await is_ban(entity.user_id, entity.group_id):
if event_cache is not None:
event_cache["ban_state"] = True
raise SkipPluginException("user banned (fast)")
if event_cache is not None:
event_cache["ban_state"] = False
async def route_precheck( async def route_precheck(
matcher: Matcher, matcher: Matcher,
event: Event, event: Event,
@@ -1243,60 +1230,6 @@ async def route_precheck(
return False return False
async def auth_precheck(
matcher: Matcher,
event: Event,
bot: Bot,
session: Uninfo,
message: UniMsg,
) -> None:
"""轻量前置检查:命令路由 + 必要管理员权限。"""
module = matcher.plugin_name or ""
if not module:
return
if _is_hidden_plugin(matcher):
return
entity = get_entity_ids(session)
if session.user.id in bot.config.superusers:
return
plugin = cast(PluginInfo | None, await PluginInfoMemoryCache.get_by_module(module))
if not plugin:
return
if plugin.plugin_type == PluginType.SUPERUSER:
raise SkipPluginException("超级管理员权限不足...")
if _needs_admin_check(plugin):
await LevelUserMemoryCache.ensure_fresh()
levels = await LevelUserMemoryCache.get_levels(session.user.id, entity.group_id)
await auth_admin(plugin, session, cached_levels=levels)
# 缓存 admin 检查结果到 event_cache,避免 auth() 重复执行
event_cache = _get_event_cache(event, session, entity)
if event_cache is not None:
event_cache["admin_levels"] = levels
event_cache["admin_timeout"] = False
event_cache["admin_precheck_done"] = True
async def _call_auth_ban_compat(
matcher: Matcher,
bot: Bot,
session: Uninfo,
plugin: PluginInfo,
*,
entity,
) -> None:
"""兼容旧签名 auth_ban(matcher, bot, session, plugin)。"""
try:
await auth_ban(matcher, bot, session, plugin, entity=entity)
except TypeError as exc:
if "unexpected keyword argument 'entity'" not in str(exc):
raise
await auth_ban(matcher, bot, session, plugin)
async def auth( async def auth(
matcher: Matcher, matcher: Matcher,
event: Event, event: Event,
@@ -1309,7 +1242,7 @@ async def auth(
event_cache: dict | None = None, event_cache: dict | None = None,
text: str | None = None, text: str | None = None,
route_modules: set[str] | None = None, route_modules: set[str] | None = None,
is_superuser: bool | None = None, is_superuser: bool = False,
): ):
"""权限检查 """权限检查
@@ -1325,8 +1258,6 @@ async def auth(
ignore_flag = False ignore_flag = False
if entity is None: if entity is None:
entity = get_entity_ids(session) entity = get_entity_ids(session)
if is_superuser is None:
is_superuser = session.user.id in bot.config.superusers
module = matcher.plugin_name or "" module = matcher.plugin_name or ""
is_command_matcher = _is_command_matcher_class(type(matcher)) is_command_matcher = _is_command_matcher_class(type(matcher))
if event_cache is None: if event_cache is None:
@@ -1451,8 +1382,12 @@ async def auth(
else: else:
ban_start = time.time() ban_start = time.time()
try: try:
await _call_auth_ban_compat( await auth_ban(
matcher, bot, session, plugin, entity=entity matcher,
session,
plugin,
entity=entity,
is_superuser=is_superuser,
) )
hook_recorder.set("auth_ban", f"{time.time() - ban_start:.3f}s") hook_recorder.set("auth_ban", f"{time.time() - ban_start:.3f}s")
if event_cache is not None: if event_cache is not None:
@@ -1468,7 +1403,13 @@ async def auth(
cost_start = time.time() cost_start = time.time()
try: try:
cost_gold = await with_timeout( cost_gold = await with_timeout(
get_plugin_cost(bot, user, plugin, session), name="get_plugin_cost" get_plugin_cost(
user,
plugin,
session,
is_superuser=is_superuser,
),
name="get_plugin_cost",
) )
hook_recorder.set("cost_gold", f"{time.time() - cost_start:.3f}s") hook_recorder.set("cost_gold", f"{time.time() - cost_start:.3f}s")
except asyncio.TimeoutError: except asyncio.TimeoutError:
@@ -1622,6 +1563,20 @@ async def auth(
except SkipPluginException as e: except SkipPluginException as e:
LimitManager.unblock(module, entity.user_id, entity.group_id, entity.channel_id) LimitManager.unblock(module, entity.user_id, entity.group_id, entity.channel_id)
if e.tip_message:
try:
tip_coro = send_message(
session,
e.tip_message,
e.tip_check_tag,
background=e.tip_background,
)
if e.tip_timeout and not e.tip_background:
await asyncio.wait_for(tip_coro, timeout=e.tip_timeout)
else:
await tip_coro
except asyncio.TimeoutError:
logger.error("发送权限提示超时", LOGGER_COMMAND, session=session)
logger.info(str(e), LOGGER_COMMAND, session=session) logger.info(str(e), LOGGER_COMMAND, session=session)
ignore_flag = True ignore_flag = True
auth_allowed = False auth_allowed = False
+2 -52
View File
@@ -1,4 +1,3 @@
import asyncio
import contextlib import contextlib
import time import time
@@ -30,13 +29,6 @@ from .auth_checker import (
_SKIP_AUTH_PLUGINS = {"chat_history", "chat_message"} _SKIP_AUTH_PLUGINS = {"chat_history", "chat_message"}
_BOT_CONNECT_TS: float | None = None _BOT_CONNECT_TS: float | None = None
_AUTH_QUEUE_MAXSIZE = 200
_AUTH_QUEUE: asyncio.Queue[tuple[Matcher, Event, Bot, Uninfo, UniMsg | None]] = (
asyncio.Queue(maxsize=_AUTH_QUEUE_MAXSIZE)
)
_AUTH_QUEUE_STARTED = False
_AUTH_WORKERS: list[asyncio.Task] = []
_LAST_DROP_LOG = 0.0
driver = get_driver() driver = get_driver()
register_runtime_bootstrap(driver) register_runtime_bootstrap(driver)
@@ -49,27 +41,6 @@ async def _mark_bot_connected(bot: Bot):
_BOT_CONNECT_TS = time.time() _BOT_CONNECT_TS = time.time()
async def _auth_worker(worker_id: int) -> None:
while True:
matcher, event, bot, session, message = await _AUTH_QUEUE.get()
try:
await auth(
matcher,
event,
bot,
session,
message,
skip_ban=True,
)
except IgnoredException:
pass
except Exception as exc:
if not is_overloaded():
logger.error("async auth failed", LOGGER_COMMAND, e=exc)
finally:
_AUTH_QUEUE.task_done()
def _extract_plain_text(message: UniMsg | None, event: Event) -> str: def _extract_plain_text(message: UniMsg | None, event: Event) -> str:
if message is not None: if message is not None:
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
@@ -82,33 +53,12 @@ def _extract_plain_text(message: UniMsg | None, event: Event) -> str:
@driver.on_startup @driver.on_startup
async def _start_auth_queue(): async def _start_auth_runtime_tasks():
global _AUTH_QUEUE_STARTED
if _AUTH_QUEUE_STARTED:
return
_AUTH_QUEUE_STARTED = True
worker_count = max(1, min(6, _AUTH_QUEUE_MAXSIZE // 50))
for idx in range(worker_count):
_AUTH_WORKERS.append(asyncio.create_task(_auth_worker(idx)))
await start_auth_runtime_tasks() await start_auth_runtime_tasks()
@driver.on_shutdown @driver.on_shutdown
async def _stop_auth_queue(): async def _stop_auth_runtime_tasks():
global _AUTH_QUEUE_STARTED
_AUTH_QUEUE_STARTED = False
workers = _AUTH_WORKERS.copy()
_AUTH_WORKERS.clear()
for task in workers:
task.cancel()
if workers:
await asyncio.gather(*workers, return_exceptions=True)
while not _AUTH_QUEUE.empty():
with contextlib.suppress(Exception):
_AUTH_QUEUE.get_nowait()
_AUTH_QUEUE.task_done()
await stop_auth_runtime_tasks() await stop_auth_runtime_tasks()
+1 -1
View File
@@ -158,7 +158,7 @@ async def _():
# if limit_create: # if limit_create:
# await PluginLimit.bulk_create(limit_create, 10) # await PluginLimit.bulk_create(limit_create, 10)
await PluginInfo.filter(module_path__in=load_plugin).update(load_status=True) await PluginInfo.filter(module_path__in=load_plugin).update(load_status=True)
await PluginInfo.filter(module_path__not_in=load_plugin).delete() await PluginInfo.filter(module_path__not_in=load_plugin).update(load_status=False)
from zhenxun.services.cache.runtime_cache import PluginInfoMemoryCache from zhenxun.services.cache.runtime_cache import PluginInfoMemoryCache
await PluginInfoMemoryCache.refresh() await PluginInfoMemoryCache.refresh()
@@ -44,6 +44,30 @@ def row_style(column: str, text: str) -> RowStyle:
class StoreManager: class StoreManager:
@classmethod
def _resolve_local_plugin_path(
cls, plugin_info: StorePluginInfo, *, is_external: bool
) -> Path:
"""将商店插件信息映射到本地插件文件/目录路径。"""
plugin_name = plugin_info.module_path.split(".")[-1] or plugin_info.module
if is_external and plugin_info.is_dir and plugin_info.module_path == ".":
return BASE_PATH / "plugins" / plugin_name
relative_parts = [part for part in plugin_info.module_path.split(".") if part]
relative_path = Path(*relative_parts) if relative_parts else Path(plugin_name)
path = BASE_PATH.parent / relative_path
if plugin_info.is_dir:
return path
return path.parent / f"{plugin_name}.py"
@classmethod
def _is_plugin_installed(
cls, plugin_info: StorePluginInfo, *, is_external: bool
) -> bool:
return cls._resolve_local_plugin_path(
plugin_info, is_external=is_external
).exists()
@classmethod @classmethod
@cached(60) @cached(60)
async def get_data(cls) -> tuple[list[StorePluginInfo], list[StorePluginInfo]]: async def get_data(cls) -> tuple[list[StorePluginInfo], list[StorePluginInfo]]:
@@ -98,13 +122,16 @@ class StoreManager:
return suc_plugin.get(module) and plugin_info.version == suc_plugin[module] return suc_plugin.get(module) and plugin_info.version == suc_plugin[module]
@classmethod @classmethod
async def get_loaded_plugins(cls, *args) -> list[tuple[str, str]]: async def get_installed_plugins(cls) -> dict[str, str]:
"""获取已加载的插件 """获取已安装插件的模块与版本。
返回: 返回:
list[str]: 已加载的插件 dict[str, str]: 模块 -> 版本
""" """
return await PluginInfo.get_plugins_values_list(*args, load_status=True) db_plugin_list = await PluginInfo.get_plugins_values_list(
"module", "version", load_status=True, filter_parent=False
)
return {p[0]: (p[1] or "0.1") for p in db_plugin_list}
@classmethod @classmethod
async def get_plugins_info(cls) -> list[BuildImage] | str: async def get_plugins_info(cls) -> list[BuildImage] | str:
@@ -115,8 +142,7 @@ class StoreManager:
""" """
plugin_list, extra_plugin_list = await cls.get_data() plugin_list, extra_plugin_list = await cls.get_data()
column_name = ["-", "ID", "名称", "简介", "作者", "版本", "类型"] column_name = ["-", "ID", "名称", "简介", "作者", "版本", "类型"]
db_plugin_list = await cls.get_loaded_plugins("module", "version") suc_plugin = await cls.get_installed_plugins()
suc_plugin = {p[0]: (p[1] or "0.1") for p in db_plugin_list}
index = 0 index = 0
data_list = [] data_list = []
extra_data_list = [] extra_data_list = []
@@ -190,7 +216,6 @@ class StoreManager:
plugin_list, extra_plugin_list = await cls.get_data() plugin_list, extra_plugin_list = await cls.get_data()
plugin_info = None plugin_info = None
is_external = False is_external = False
db_plugin_list = await cls.get_loaded_plugins("module")
try: try:
plugin_key = await cls._resolve_plugin_key(index_or_module) plugin_key = await cls._resolve_plugin_key(index_or_module)
except PluginStoreException: except PluginStoreException:
@@ -211,7 +236,7 @@ class StoreManager:
plugin_info = p plugin_info = p
break break
modules = [p[0] for p in db_plugin_list] installed_modules = set((await cls.get_installed_plugins()).keys())
if is_remove: if is_remove:
# 商店列表中找不到时,从数据库构建最小插件信息 # 商店列表中找不到时,从数据库构建最小插件信息
@@ -237,13 +262,17 @@ class StoreManager:
is_dir=_path.is_dir(), is_dir=_path.is_dir(),
) )
is_external = True is_external = True
if plugin_info.module not in modules: if plugin_info.module not in installed_modules:
raise PluginStoreException(f"插件 {plugin_info.name} 未安装,无法移除") raise PluginStoreException(f"插件 {plugin_info.name} 未安装,无法移除")
if plugin_obj := await PluginInfo.get_plugin( if plugin_obj := await PluginInfo.get_plugin(
module=plugin_info.module, plugin_type=PluginType.PARENT module=plugin_info.module,
plugin_type=PluginType.PARENT,
load_status=True,
): ):
plugin_info.module_path = plugin_obj.module_path plugin_info.module_path = plugin_obj.module_path
elif plugin_obj := await PluginInfo.get_plugin(module=plugin_info.module): elif plugin_obj := await PluginInfo.get_plugin(
module=plugin_info.module, load_status=True
):
plugin_info.module_path = plugin_obj.module_path plugin_info.module_path = plugin_obj.module_path
return plugin_info, is_external return plugin_info, is_external
@@ -251,11 +280,11 @@ class StoreManager:
raise PluginStoreException(f"插件不存在: {plugin_key}") raise PluginStoreException(f"插件不存在: {plugin_key}")
if is_update: if is_update:
if plugin_info.module not in modules: if plugin_info.module not in installed_modules:
raise PluginStoreException(f"插件 {plugin_info.name} 未安装,无法更新") raise PluginStoreException(f"插件 {plugin_info.name} 未安装,无法更新")
return plugin_info, is_external return plugin_info, is_external
if plugin_info.module in modules: if plugin_info.module in installed_modules:
raise PluginStoreException(f"插件 {plugin_info.name} 已安装,无需重复安装") raise PluginStoreException(f"插件 {plugin_info.name} 已安装,无需重复安装")
return plugin_info, is_external return plugin_info, is_external
@@ -391,11 +420,8 @@ class StoreManager:
str: 返回消息 str: 返回消息
""" """
plugin_info, _ = await cls.get_plugin_by_value(index_or_module, is_remove=True) plugin_info, _ = await cls.get_plugin_by_value(index_or_module, is_remove=True)
module_path = plugin_info.module_path is_external = not plugin_info.module_path.startswith("zhenxun.")
module = module_path.split(".")[-1] path = cls._resolve_local_plugin_path(plugin_info, is_external=is_external)
path = BASE_PATH.parent / Path(module_path.replace(".", os.sep))
if not plugin_info.is_dir:
path = path.parent / f"{module}.py"
if not path.exists(): if not path.exists():
return f"插件 {plugin_info.name} 不存在..." return f"插件 {plugin_info.name} 不存在..."
logger.debug(f"尝试移除插件 {plugin_info.name} 文件: {path}", LOG_COMMAND) logger.debug(f"尝试移除插件 {plugin_info.name} 文件: {path}", LOG_COMMAND)
@@ -404,7 +430,14 @@ class StoreManager:
shutil.rmtree(path, onerror=win_on_rm_error) shutil.rmtree(path, onerror=win_on_rm_error)
else: else:
path.unlink() path.unlink()
await PluginInitManager.remove(module_path) await PluginInitManager.remove(plugin_info.module_path)
plugin_records = await PluginInfo.get_plugins(
load_status=None,
filter_parent=False,
module_path=plugin_info.module_path,
)
for plugin_record in plugin_records:
await plugin_record.delete()
return f"插件 {plugin_info.name} 移除成功! 重启后生效" return f"插件 {plugin_info.name} 移除成功! 重启后生效"
@classmethod @classmethod
@@ -419,8 +452,7 @@ class StoreManager:
""" """
plugin_list, extra_plugin_list = await cls.get_data() plugin_list, extra_plugin_list = await cls.get_data()
all_plugin_list = plugin_list + extra_plugin_list all_plugin_list = plugin_list + extra_plugin_list
db_plugin_list = await cls.get_loaded_plugins("module", "version") suc_plugin = await cls.get_installed_plugins()
suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list}
filtered_data = [ filtered_data = [
(id, plugin_info) (id, plugin_info)
for id, plugin_info in enumerate(all_plugin_list) for id, plugin_info in enumerate(all_plugin_list)
@@ -463,8 +495,7 @@ class StoreManager:
""" """
plugin_info, is_external = await cls.get_plugin_by_value(index_or_module, True) plugin_info, is_external = await cls.get_plugin_by_value(index_or_module, True)
logger.info(f"尝试更新插件 {plugin_info.name}", LOG_COMMAND) logger.info(f"尝试更新插件 {plugin_info.name}", LOG_COMMAND)
db_plugin_list = await cls.get_loaded_plugins("module", "version") suc_plugin = await cls.get_installed_plugins()
suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list}
logger.debug(f"当前插件列表: {suc_plugin}", LOG_COMMAND) logger.debug(f"当前插件列表: {suc_plugin}", LOG_COMMAND)
if cls.check_version_is_new(plugin_info, suc_plugin): if cls.check_version_is_new(plugin_info, suc_plugin):
return f"插件 {plugin_info.name} 已是最新版本" return f"插件 {plugin_info.name} 已是最新版本"
@@ -493,11 +524,10 @@ class StoreManager:
update_success_list = [] update_success_list = []
result = "--已更新{}个插件 {}个失败 {}个成功--" result = "--已更新{}个插件 {}个失败 {}个成功--"
logger.info(f"尝试更新全部插件 {plugin_name_list}", LOG_COMMAND) logger.info(f"尝试更新全部插件 {plugin_name_list}", LOG_COMMAND)
suc_plugin = await cls.get_installed_plugins()
for plugin_info in all_plugin_list: for plugin_info in all_plugin_list:
try: try:
db_plugin_list = await cls.get_loaded_plugins("module", "version") if plugin_info.module not in suc_plugin:
suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list}
if plugin_info.module not in [p[0] for p in db_plugin_list]:
logger.debug( logger.debug(
f"插件 {plugin_info.name}({plugin_info.module}) 未安装,跳过", f"插件 {plugin_info.name}({plugin_info.module}) 未安装,跳过",
LOG_COMMAND, LOG_COMMAND,
@@ -3,7 +3,6 @@ from fastapi.responses import JSONResponse
from nonebot import require from nonebot import require
from nonebot.compat import model_dump from nonebot.compat import model_dump
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.services.log import logger from zhenxun.services.log import logger
from ....base_model import Result from ....base_model import Result
@@ -30,7 +29,7 @@ async def _() -> Result[dict]:
{**model_dump(plugin), "name": plugin.name, "id": idx} {**model_dump(plugin), "name": plugin.name, "id": idx}
for idx, plugin in enumerate(plugin_list + extra_plugin_list) for idx, plugin in enumerate(plugin_list + extra_plugin_list)
] ]
modules = await PluginInfo.get_plugins_values_list("module", load_status=True) modules = list((await StoreManager.get_installed_plugins()).keys())
return Result.ok({"install_module": modules, "plugin_list": plugin_list}) return Result.ok({"install_module": modules, "plugin_list": plugin_list})
except Exception as e: except Exception as e:
logger.error("获取插件商店插件信息失败", "WebUi", e=e) logger.error("获取插件商店插件信息失败", "WebUi", e=e)
+13 -2
View File
@@ -1,4 +1,6 @@
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot
from nonebot.adapters.onebot.v12 import Bot as OneBotV12Bot
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from nonebot.rule import Rule from nonebot.rule import Rule
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
@@ -48,18 +50,27 @@ def reply_check() -> Rule:
_matcher = on_alconna(Alconna("撤回"), priority=5, block=True, rule=reply_check()) _matcher = on_alconna(Alconna("撤回"), priority=5, block=True, rule=reply_check())
def _resolve_delete_message_id(bot: Bot, reply_id: str | int):
if isinstance(bot, OneBotV11Bot):
return int(reply_id)
if isinstance(bot, OneBotV12Bot):
return str(reply_id)
return reply_id
@_matcher.handle() @_matcher.handle()
async def _(bot: Bot, event: Event, session: Uninfo, arparma: Arparma): async def _(bot: Bot, event: Event, session: Uninfo, arparma: Arparma):
if reply := await reply_fetch(event, bot): if reply := await reply_fetch(event, bot):
message_id = _resolve_delete_message_id(bot, reply.id)
if session.user.id in bot.config.superusers: if session.user.id in bot.config.superusers:
try: try:
await bot.delete_msg(message_id=reply.id) await bot.delete_msg(message_id=message_id)
logger.info("撤回消息", arparma.header_result, session=session) logger.info("撤回消息", arparma.header_result, session=session)
except Exception: except Exception:
await MessageUtils.build_message("撤回失败,可能消息已过期...").send() await MessageUtils.build_message("撤回失败,可能消息已过期...").send()
elif MessageManager.check(session.user.id, reply.id): elif MessageManager.check(session.user.id, reply.id):
try: try:
await bot.delete_msg(message_id=reply.id) await bot.delete_msg(message_id=message_id)
logger.info("撤回消息", arparma.header_result, session=session) logger.info("撤回消息", arparma.header_result, session=session)
except Exception: except Exception:
await MessageUtils.build_message("撤回失败,可能消息已过期...").send() await MessageUtils.build_message("撤回失败,可能消息已过期...").send()