From ff75e2ee92bd5706a8b6fec7eb1f0d5b048d7137 Mon Sep 17 00:00:00 2001 From: Rumio <32546670+webjoin111@users.noreply.github.com> Date: Sat, 26 Apr 2025 20:15:44 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=20=E5=A2=9E=E5=8A=A0webui?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/web_ui/__init__.py | 9 +- .../web_ui/api/tabs/plugin_manage/__init__.py | 69 +++++++++- .../api/tabs/plugin_manage/data_source.py | 127 +++++++++++++++++- .../web_ui/api/tabs/plugin_manage/model.py | 95 ++++++++----- 4 files changed, 254 insertions(+), 46 deletions(-) diff --git a/zhenxun/builtin_plugins/web_ui/__init__.py b/zhenxun/builtin_plugins/web_ui/__init__.py index d8d71025..90772bc5 100644 --- a/zhenxun/builtin_plugins/web_ui/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/__init__.py @@ -29,8 +29,7 @@ from .public import init_public __plugin_meta__ = PluginMetadata( name="WebUi", description="WebUi API", - usage=""" - """.strip(), + usage='"""\n """.strip(),', extra=PluginExtraData( author="HibiKier", version="0.1", @@ -83,7 +82,6 @@ BaseApiRouter.include_router(plugin_router) BaseApiRouter.include_router(system_router) BaseApiRouter.include_router(menu_router) - WsApiRouter = APIRouter(prefix="/zhenxun/socket") WsApiRouter.include_router(ws_log_routes) @@ -94,6 +92,8 @@ WsApiRouter.include_router(chat_routes) @driver.on_startup async def _(): try: + # 存储任务引用的列表,防止任务被垃圾回收 + _tasks = [] async def log_sink(message: str): loop = None @@ -104,7 +104,8 @@ async def _(): logger.warning("Web Ui log_sink", e=e) if not loop: loop = asyncio.new_event_loop() - loop.create_task(LOG_STORAGE.add(message.rstrip("\n"))) # noqa: RUF006 + # 存储任务引用到外部列表中 + _tasks.append(loop.create_task(LOG_STORAGE.add(message.rstrip("\n")))) logger_.add( log_sink, colorize=True, filter=default_filter, format=default_format diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py index e011e67f..45878880 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py @@ -9,10 +9,13 @@ from ....base_model import Result from ....utils import authentication from .data_source import ApiDataSource from .model import ( + BatchUpdatePlugins, + BatchUpdateResult, PluginCount, PluginDetail, PluginInfo, PluginSwitch, + RenameMenuTypePayload, UpdatePlugin, ) @@ -30,9 +33,8 @@ async def _( plugin_type: list[PluginType] = Query(None), menu_type: str | None = None ) -> Result[list[PluginInfo]]: try: - return Result.ok( - await ApiDataSource.get_plugin_list(plugin_type, menu_type), "拿到信息啦!" - ) + result = await ApiDataSource.get_plugin_list(plugin_type, menu_type) + return Result.ok(result, "拿到信息啦!") except Exception as e: logger.error(f"{router.prefix}/get_plugin_list 调用错误", "WebUi", e=e) return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") @@ -144,11 +146,66 @@ async def _() -> Result[list[str]]: ) async def _(module: str) -> Result[PluginDetail]: try: - return Result.ok( - await ApiDataSource.get_plugin_detail(module), "已经帮你写好啦!" - ) + detail = await ApiDataSource.get_plugin_detail(module) + return Result.ok(detail, "已经帮你写好啦!") except (ValueError, KeyError): return Result.fail("插件数据不存在...") except Exception as e: logger.error(f"{router.prefix}/get_plugin 调用错误", "WebUi", e=e) return Result.fail(f"{type(e)}: {e}") + + +@router.put( + "/plugins/batch_update", + dependencies=[authentication()], + response_model=Result[BatchUpdateResult], + response_class=JSONResponse, + summary="批量更新插件配置", +) +async def batch_update_plugin_config_api( + params: BatchUpdatePlugins, +) -> Result[BatchUpdateResult]: + """批量更新插件配置,如开关、类型等""" + try: + result_dict = await ApiDataSource.batch_update_plugins(params=params) + result_model = BatchUpdateResult( + success=result_dict["success"], + updated_count=result_dict["updated_count"], + errors=result_dict["errors"], + ) + return Result.ok(result_model, "插件配置更新完成") + except Exception as e: + logger.error(f"{router.prefix}/plugins/batch_update 调用错误", "WebUi", e=e) + return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") + + +# 新增:重命名菜单类型路由 +@router.put( + "/menu_type/rename", + dependencies=[authentication()], + response_model=Result, + summary="重命名菜单类型", +) +async def rename_menu_type_api(payload: RenameMenuTypePayload) -> Result: + try: + result = await ApiDataSource.rename_menu_type( + old_name=payload.old_name, new_name=payload.new_name + ) + if result.get("success"): + return Result.ok( + info=result.get( + "info", + f"成功将 {result.get('updated_count', 0)} 个插件的菜单类型从 " + f"'{payload.old_name}' 修改为 '{payload.new_name}'", + ) + ) + else: + return Result.fail(info=result.get("info", "重命名失败")) + except ValueError as ve: + return Result.fail(info=str(ve)) + except RuntimeError as re: + logger.error(f"{router.prefix}/menu_type/rename 调用错误", "WebUi", e=re) + return Result.fail(info=str(re)) + except Exception as e: + logger.error(f"{router.prefix}/menu_type/rename 调用错误", "WebUi", e=e) + return Result.fail(info=f"发生未知错误: {type(e).__name__}") diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py index ee0992d6..d525c9bf 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py @@ -2,13 +2,20 @@ import re import cattrs from fastapi import Query +from tortoise.exceptions import DoesNotExist from zhenxun.configs.config import Config from zhenxun.configs.utils import ConfigGroup from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo from zhenxun.utils.enum import BlockType, PluginType -from .model import PluginConfig, PluginDetail, PluginInfo, UpdatePlugin +from .model import ( + BatchUpdatePlugins, + PluginConfig, + PluginDetail, + PluginInfo, + UpdatePlugin, +) class ApiDataSource: @@ -44,6 +51,7 @@ class ApiDataSource: level=plugin.level, status=plugin.status, author=plugin.author, + block_type=plugin.block_type, ) plugin_list.append(plugin_info) return plugin_list @@ -69,7 +77,6 @@ class ApiDataSource: db_plugin.block_type = param.block_type db_plugin.status = param.block_type != BlockType.ALL await db_plugin.save() - # 配置项 if param.configs and (configs := Config.get(param.module)): for key in param.configs: if c := configs.configs.get(key): @@ -80,6 +87,87 @@ class ApiDataSource: Config.save(save_simple_data=True) return db_plugin + @classmethod + async def batch_update_plugins(cls, params: BatchUpdatePlugins) -> dict: + """批量更新插件数据 + + 参数: + params: BatchUpdatePlugins + + 返回: + dict: 更新结果, 例如 {'success': True, 'updated_count': 5, 'errors': []} + """ + plugins_to_update_other_fields = [] + other_update_fields = set() + updated_count = 0 + errors = [] + + for item in params.updates: + try: + db_plugin = await DbPluginInfo.get(module=item.module) + plugin_changed_other = False + plugin_changed_block = False + + if db_plugin.block_type != item.block_type: + db_plugin.block_type = item.block_type + db_plugin.status = item.block_type != BlockType.ALL + plugin_changed_block = True + + if item.menu_type is not None and db_plugin.menu_type != item.menu_type: + db_plugin.menu_type = item.menu_type + other_update_fields.add("menu_type") + plugin_changed_other = True + + if ( + item.default_status is not None + and db_plugin.default_status != item.default_status + ): + db_plugin.default_status = item.default_status + other_update_fields.add("default_status") + plugin_changed_other = True + + if plugin_changed_block: + try: + await db_plugin.save(update_fields=["block_type", "status"]) + updated_count += 1 + except Exception as e_save: + errors.append( + { + "module": item.module, + "error": f"Save block_type failed: {e_save!s}", + } + ) + plugin_changed_other = False + + if plugin_changed_other: + plugins_to_update_other_fields.append(db_plugin) + + except DoesNotExist: + errors.append({"module": item.module, "error": "Plugin not found"}) + except Exception as e: + errors.append({"module": item.module, "error": str(e)}) + + bulk_updated_count = 0 + if plugins_to_update_other_fields and other_update_fields: + try: + await DbPluginInfo.bulk_update( + plugins_to_update_other_fields, list(other_update_fields) + ) + bulk_updated_count = len(plugins_to_update_other_fields) + except Exception as e_bulk: + errors.append( + { + "module": "batch_update_other", + "error": f"Bulk update failed: {e_bulk!s}", + } + ) + + return { + "success": len(errors) == 0, + "updated_count": updated_count + bulk_updated_count, + "errors": errors, + } + @classmethod def __build_plugin_config( cls, module: str, cfg: str, config: ConfigGroup @@ -115,6 +203,41 @@ class ApiDataSource: type_inner=type_inner, # type: ignore ) + @classmethod + async def rename_menu_type(cls, old_name: str, new_name: str) -> dict: + """重命名菜单类型,并更新所有相关插件 + + 参数: + old_name: 旧菜单类型名称 + new_name: 新菜单类型名称 + + 返回: + dict: 更新结果, 例如 {'success': True, 'updated_count': 3} + """ + if not old_name or not new_name: + raise ValueError("旧名称和新名称都不能为空") + if old_name == new_name: + return { + "success": True, + "updated_count": 0, + "info": "新旧名称相同,无需更新", + } + + # 检查新名称是否已存在(理论上前端会校验,后端再保险一次) + exists = await DbPluginInfo.filter(menu_type=new_name).exists() + if exists: + raise ValueError(f"新的菜单类型名称 '{new_name}' 已被其他插件使用") + + try: + # 使用 filter().update() 进行批量更新 + updated_count = await DbPluginInfo.filter(menu_type=old_name).update( + menu_type=new_name + ) + return {"success": True, "updated_count": updated_count} + except Exception as e: + # 可以添加更详细的日志记录 + raise RuntimeError(f"数据库更新菜单类型失败: {e!s}") + @classmethod async def get_plugin_detail(cls, module: str) -> PluginDetail: """获取插件详情 diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py index 662814c9..c2bcc4bb 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py @@ -1,6 +1,6 @@ from typing import Any -from pydantic import BaseModel +from pydantic import BaseModel, Field from zhenxun.utils.enum import BlockType @@ -37,19 +37,19 @@ class UpdatePlugin(BaseModel): module: str """模块""" default_status: bool - """默认开关""" + """是否默认开启""" limit_superuser: bool - """限制超级用户""" - cost_gold: int - """金币花费""" - menu_type: str - """插件菜单类型""" + """是否限制超级用户""" level: int - """插件所需群权限""" + """等级""" + cost_gold: int + """花费金币""" + menu_type: str + """菜单类型""" block_type: BlockType | None = None """禁用类型""" configs: dict[str, Any] | None = None - """配置项""" + """设置项""" class PluginInfo(BaseModel): @@ -58,27 +58,26 @@ class PluginInfo(BaseModel): """ module: str - """插件名称""" + """模块""" plugin_name: str - """插件中文名称""" + """插件名称""" default_status: bool - """默认开关""" + """是否默认开启""" limit_superuser: bool - """限制超级用户""" + """是否限制超级用户""" + level: int + """等级""" cost_gold: int """花费金币""" menu_type: str - """插件菜单类型""" + """菜单类型""" version: str - """插件版本""" - level: int - """群权限""" + """版本""" status: bool - """当前状态""" + """状态""" author: str | None = None """作者""" - block_type: BlockType | None = None - """禁用类型""" + block_type: BlockType | None = Field(None, description="插件禁用状态 (None: 启用)") class PluginConfig(BaseModel): @@ -86,20 +85,13 @@ class PluginConfig(BaseModel): 插件配置项 """ - module: str - """模块""" - key: str - """键""" - value: Any - """值""" - help: str | None = None - """帮助""" - default_value: Any - """默认值""" - type: Any = None - """值类型""" - type_inner: list[str] | None = None - """List Tuple等内部类型检验""" + module: str = Field(..., description="模块名") + key: str = Field(..., description="键") + value: Any = Field(None, description="值") + help: str | None = Field(None, description="帮助信息") + default_value: Any = Field(None, description="默认值") + type: str | None = Field(None, description="类型") + type_inner: list[str] | None = Field(None, description="内部类型") class PluginCount(BaseModel): @@ -117,6 +109,21 @@ class PluginCount(BaseModel): """其他插件""" +class BatchUpdatePluginItem(BaseModel): + module: str = Field(..., description="插件模块名") + default_status: bool | None = Field(None, description="默认状态(开关)") + menu_type: str | None = Field(None, description="菜单类型") + block_type: BlockType | None = Field( + None, description="插件禁用状态 (None: 启用, ALL: 禁用)" + ) + + +class BatchUpdatePlugins(BaseModel): + updates: list[BatchUpdatePluginItem] = Field( + ..., description="要批量更新的插件列表" + ) + + class PluginDetail(PluginInfo): """ 插件详情 @@ -125,6 +132,26 @@ class PluginDetail(PluginInfo): config_list: list[PluginConfig] +class RenameMenuTypePayload(BaseModel): + old_name: str = Field(..., description="旧菜单类型名称") + new_name: str = Field(..., description="新菜单类型名称") + + class PluginIr(BaseModel): id: int """插件id""" + + +class BatchUpdateResult(BaseModel): + """ + 批量更新插件结果 + """ + + success: bool = Field(..., description="是否全部成功") + """是否全部成功""" + updated_count: int = Field(..., description="更新成功的数量") + """更新成功的数量""" + errors: list[dict[str, str]] = Field( + default_factory=list, description="错误信息列表" + ) + """错误信息列表"""