Merge branch 'zhenxun-org:main' into main

This commit is contained in:
尝生 2025-04-29 11:41:47 +08:00 committed by GitHub
commit 208792cfc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 254 additions and 46 deletions

View File

@ -29,8 +29,7 @@ from .public import init_public
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="WebUi", name="WebUi",
description="WebUi API", description="WebUi API",
usage=""" usage='"""\n """.strip(),',
""".strip(),
extra=PluginExtraData( extra=PluginExtraData(
author="HibiKier", author="HibiKier",
version="0.1", version="0.1",
@ -83,7 +82,6 @@ BaseApiRouter.include_router(plugin_router)
BaseApiRouter.include_router(system_router) BaseApiRouter.include_router(system_router)
BaseApiRouter.include_router(menu_router) BaseApiRouter.include_router(menu_router)
WsApiRouter = APIRouter(prefix="/zhenxun/socket") WsApiRouter = APIRouter(prefix="/zhenxun/socket")
WsApiRouter.include_router(ws_log_routes) WsApiRouter.include_router(ws_log_routes)
@ -94,6 +92,8 @@ WsApiRouter.include_router(chat_routes)
@driver.on_startup @driver.on_startup
async def _(): async def _():
try: try:
# 存储任务引用的列表,防止任务被垃圾回收
_tasks = []
async def log_sink(message: str): async def log_sink(message: str):
loop = None loop = None
@ -104,7 +104,8 @@ async def _():
logger.warning("Web Ui log_sink", e=e) logger.warning("Web Ui log_sink", e=e)
if not loop: if not loop:
loop = asyncio.new_event_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( logger_.add(
log_sink, colorize=True, filter=default_filter, format=default_format log_sink, colorize=True, filter=default_filter, format=default_format

View File

@ -9,10 +9,13 @@ from ....base_model import Result
from ....utils import authentication from ....utils import authentication
from .data_source import ApiDataSource from .data_source import ApiDataSource
from .model import ( from .model import (
BatchUpdatePlugins,
BatchUpdateResult,
PluginCount, PluginCount,
PluginDetail, PluginDetail,
PluginInfo, PluginInfo,
PluginSwitch, PluginSwitch,
RenameMenuTypePayload,
UpdatePlugin, UpdatePlugin,
) )
@ -30,9 +33,8 @@ async def _(
plugin_type: list[PluginType] = Query(None), menu_type: str | None = None plugin_type: list[PluginType] = Query(None), menu_type: str | None = None
) -> Result[list[PluginInfo]]: ) -> Result[list[PluginInfo]]:
try: try:
return Result.ok( result = await ApiDataSource.get_plugin_list(plugin_type, menu_type)
await ApiDataSource.get_plugin_list(plugin_type, menu_type), "拿到信息啦!" return Result.ok(result, "拿到信息啦!")
)
except Exception as e: except Exception as e:
logger.error(f"{router.prefix}/get_plugin_list 调用错误", "WebUi", e=e) logger.error(f"{router.prefix}/get_plugin_list 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@ -144,11 +146,66 @@ async def _() -> Result[list[str]]:
) )
async def _(module: str) -> Result[PluginDetail]: async def _(module: str) -> Result[PluginDetail]:
try: try:
return Result.ok( detail = await ApiDataSource.get_plugin_detail(module)
await ApiDataSource.get_plugin_detail(module), "已经帮你写好啦!" return Result.ok(detail, "已经帮你写好啦!")
)
except (ValueError, KeyError): except (ValueError, KeyError):
return Result.fail("插件数据不存在...") return Result.fail("插件数据不存在...")
except Exception as e: except Exception as e:
logger.error(f"{router.prefix}/get_plugin 调用错误", "WebUi", e=e) logger.error(f"{router.prefix}/get_plugin 调用错误", "WebUi", e=e)
return Result.fail(f"{type(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__}")

View File

@ -2,13 +2,20 @@ import re
import cattrs import cattrs
from fastapi import Query from fastapi import Query
from tortoise.exceptions import DoesNotExist
from zhenxun.configs.config import Config from zhenxun.configs.config import Config
from zhenxun.configs.utils import ConfigGroup from zhenxun.configs.utils import ConfigGroup
from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo
from zhenxun.utils.enum import BlockType, PluginType from zhenxun.utils.enum import BlockType, PluginType
from .model import PluginConfig, PluginDetail, PluginInfo, UpdatePlugin from .model import (
BatchUpdatePlugins,
PluginConfig,
PluginDetail,
PluginInfo,
UpdatePlugin,
)
class ApiDataSource: class ApiDataSource:
@ -44,6 +51,7 @@ class ApiDataSource:
level=plugin.level, level=plugin.level,
status=plugin.status, status=plugin.status,
author=plugin.author, author=plugin.author,
block_type=plugin.block_type,
) )
plugin_list.append(plugin_info) plugin_list.append(plugin_info)
return plugin_list return plugin_list
@ -69,7 +77,6 @@ class ApiDataSource:
db_plugin.block_type = param.block_type db_plugin.block_type = param.block_type
db_plugin.status = param.block_type != BlockType.ALL db_plugin.status = param.block_type != BlockType.ALL
await db_plugin.save() await db_plugin.save()
# 配置项
if param.configs and (configs := Config.get(param.module)): if param.configs and (configs := Config.get(param.module)):
for key in param.configs: for key in param.configs:
if c := configs.configs.get(key): if c := configs.configs.get(key):
@ -80,6 +87,87 @@ class ApiDataSource:
Config.save(save_simple_data=True) Config.save(save_simple_data=True)
return db_plugin 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 @classmethod
def __build_plugin_config( def __build_plugin_config(
cls, module: str, cfg: str, config: ConfigGroup cls, module: str, cfg: str, config: ConfigGroup
@ -115,6 +203,41 @@ class ApiDataSource:
type_inner=type_inner, # type: ignore 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 @classmethod
async def get_plugin_detail(cls, module: str) -> PluginDetail: async def get_plugin_detail(cls, module: str) -> PluginDetail:
"""获取插件详情 """获取插件详情

View File

@ -1,6 +1,6 @@
from typing import Any from typing import Any
from pydantic import BaseModel from pydantic import BaseModel, Field
from zhenxun.utils.enum import BlockType from zhenxun.utils.enum import BlockType
@ -37,19 +37,19 @@ class UpdatePlugin(BaseModel):
module: str module: str
"""模块""" """模块"""
default_status: bool default_status: bool
"""默认开关""" """是否默认开启"""
limit_superuser: bool limit_superuser: bool
"""限制超级用户""" """是否限制超级用户"""
cost_gold: int
"""金币花费"""
menu_type: str
"""插件菜单类型"""
level: int level: int
"""插件所需群权限""" """等级"""
cost_gold: int
"""花费金币"""
menu_type: str
"""菜单类型"""
block_type: BlockType | None = None block_type: BlockType | None = None
"""禁用类型""" """禁用类型"""
configs: dict[str, Any] | None = None configs: dict[str, Any] | None = None
"""配置项""" """置项"""
class PluginInfo(BaseModel): class PluginInfo(BaseModel):
@ -58,27 +58,26 @@ class PluginInfo(BaseModel):
""" """
module: str module: str
"""插件名称""" """模块"""
plugin_name: str plugin_name: str
"""插件中文名称""" """插件名称"""
default_status: bool default_status: bool
"""默认开关""" """是否默认开启"""
limit_superuser: bool limit_superuser: bool
"""限制超级用户""" """是否限制超级用户"""
level: int
"""等级"""
cost_gold: int cost_gold: int
"""花费金币""" """花费金币"""
menu_type: str menu_type: str
"""插件菜单类型""" """菜单类型"""
version: str version: str
"""插件版本""" """版本"""
level: int
"""群权限"""
status: bool status: bool
"""当前状态""" """状态"""
author: str | None = None author: str | None = None
"""作者""" """作者"""
block_type: BlockType | None = None block_type: BlockType | None = Field(None, description="插件禁用状态 (None: 启用)")
"""禁用类型"""
class PluginConfig(BaseModel): class PluginConfig(BaseModel):
@ -86,20 +85,13 @@ class PluginConfig(BaseModel):
插件配置项 插件配置项
""" """
module: str module: str = Field(..., description="模块名")
"""模块""" key: str = Field(..., description="")
key: str value: Any = Field(None, description="")
"""""" help: str | None = Field(None, description="帮助信息")
value: Any default_value: Any = Field(None, description="默认值")
"""""" type: str | None = Field(None, description="类型")
help: str | None = None type_inner: list[str] | None = Field(None, description="内部类型")
"""帮助"""
default_value: Any
"""默认值"""
type: Any = None
"""值类型"""
type_inner: list[str] | None = None
"""List Tuple等内部类型检验"""
class PluginCount(BaseModel): 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): class PluginDetail(PluginInfo):
""" """
插件详情 插件详情
@ -125,6 +132,26 @@ class PluginDetail(PluginInfo):
config_list: list[PluginConfig] config_list: list[PluginConfig]
class RenameMenuTypePayload(BaseModel):
old_name: str = Field(..., description="旧菜单类型名称")
new_name: str = Field(..., description="新菜单类型名称")
class PluginIr(BaseModel): class PluginIr(BaseModel):
id: int id: int
"""插件id""" """插件id"""
class BatchUpdateResult(BaseModel):
"""
批量更新插件结果
"""
success: bool = Field(..., description="是否全部成功")
"""是否全部成功"""
updated_count: int = Field(..., description="更新成功的数量")
"""更新成功的数量"""
errors: list[dict[str, str]] = Field(
default_factory=list, description="错误信息列表"
)
"""错误信息列表"""