mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2025-12-15 14:22:55 +08:00
Merge branch 'zhenxun-org:main' into main
This commit is contained in:
commit
208792cfc4
@ -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
|
||||||
|
|||||||
@ -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__}")
|
||||||
|
|||||||
@ -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:
|
||||||
"""获取插件详情
|
"""获取插件详情
|
||||||
|
|||||||
@ -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="错误信息列表"
|
||||||
|
)
|
||||||
|
"""错误信息列表"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user