WebUI 后端类型注解补全,使用全 typing 库类型注解
This commit is contained in:
@@ -14,4 +14,4 @@ router.include_router(config_router)
|
||||
|
||||
set_update_progress_callback(update_progress)
|
||||
|
||||
__all__ = ["get_progress_router", "router"]
|
||||
__all__ = ["get_progress_router", "router"]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Cookie, HTTPException
|
||||
|
||||
@@ -28,7 +27,7 @@ logger = get_logger("webui.plugin_routes")
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _mirror_to_response(mirror: dict[str, Any]) -> MirrorConfigResponse:
|
||||
def _mirror_to_response(mirror: Dict[str, Any]) -> MirrorConfigResponse:
|
||||
return MirrorConfigResponse(
|
||||
id=mirror["id"],
|
||||
name=mirror["name"],
|
||||
@@ -116,7 +115,7 @@ async def update_mirror(
|
||||
|
||||
|
||||
@router.delete("/mirrors/{mirror_id}")
|
||||
async def delete_mirror(mirror_id: str, maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def delete_mirror(mirror_id: str, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
|
||||
service = get_git_mirror_service()
|
||||
@@ -172,12 +171,16 @@ async def fetch_raw_file(
|
||||
loaded_plugins=total,
|
||||
)
|
||||
except Exception:
|
||||
await update_progress(stage="success", progress=100, message="加载完成", total_plugins=0, loaded_plugins=0)
|
||||
await update_progress(
|
||||
stage="success", progress=100, message="加载完成", total_plugins=0, loaded_plugins=0
|
||||
)
|
||||
|
||||
return FetchRawFileResponse(**result)
|
||||
except Exception as e:
|
||||
logger.error(f"获取 Raw 文件失败: {e}")
|
||||
await update_progress(stage="error", progress=0, message="加载失败", error=str(e), total_plugins=0, loaded_plugins=0)
|
||||
await update_progress(
|
||||
stage="error", progress=0, message="加载失败", error=str(e), total_plugins=0, loaded_plugins=0
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
|
||||
|
||||
@@ -204,4 +207,4 @@ async def clone_repository(
|
||||
return CloneRepositoryResponse(**result)
|
||||
except Exception as e:
|
||||
logger.error(f"克隆仓库失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
import json
|
||||
import tomlkit
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
import tomlkit
|
||||
from fastapi import APIRouter, Cookie, HTTPException
|
||||
|
||||
from src.common.logger import get_logger
|
||||
@@ -24,8 +23,8 @@ logger = get_logger("webui.plugin_routes")
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _build_schema_from_current_config(plugin_id: str, current_config: Any) -> dict[str, Any]:
|
||||
schema: dict[str, Any] = {
|
||||
def _build_schema_from_current_config(plugin_id: str, current_config: Any) -> Dict[str, Any]:
|
||||
schema: Dict[str, Any] = {
|
||||
"plugin_id": plugin_id,
|
||||
"plugin_info": {
|
||||
"name": plugin_id,
|
||||
@@ -41,7 +40,7 @@ def _build_schema_from_current_config(plugin_id: str, current_config: Any) -> di
|
||||
for section_name, section_data in current_config.items():
|
||||
if not isinstance(section_data, dict):
|
||||
continue
|
||||
section_fields: dict[str, Any] = {}
|
||||
section_fields: Dict[str, Any] = {}
|
||||
for field_name, field_value in section_data.items():
|
||||
field_type = type(field_value).__name__
|
||||
ui_type = "text"
|
||||
@@ -121,7 +120,7 @@ def _build_schema_from_current_config(plugin_id: str, current_config: Any) -> di
|
||||
|
||||
|
||||
@router.get("/config/{plugin_id}/schema")
|
||||
async def get_plugin_config_schema(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def get_plugin_config_schema(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"获取插件配置 Schema: {plugin_id}")
|
||||
|
||||
@@ -157,7 +156,7 @@ async def get_plugin_config_schema(plugin_id: str, maibot_session: Optional[str]
|
||||
|
||||
|
||||
@router.get("/config/{plugin_id}/raw")
|
||||
async def get_plugin_config_raw(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def get_plugin_config_raw(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"获取插件原始配置: {plugin_id}")
|
||||
|
||||
@@ -184,7 +183,7 @@ async def update_plugin_config_raw(
|
||||
plugin_id: str,
|
||||
request: UpdatePluginRawConfigRequest,
|
||||
maibot_session: Optional[str] = Cookie(None),
|
||||
) -> dict[str, Any]:
|
||||
) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"更新插件原始配置: {plugin_id}")
|
||||
|
||||
@@ -216,7 +215,7 @@ async def update_plugin_config_raw(
|
||||
|
||||
|
||||
@router.get("/config/{plugin_id}")
|
||||
async def get_plugin_config(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def get_plugin_config(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"获取插件配置: {plugin_id}")
|
||||
|
||||
@@ -244,7 +243,7 @@ async def update_plugin_config(
|
||||
plugin_id: str,
|
||||
request: UpdatePluginConfigRequest,
|
||||
maibot_session: Optional[str] = Cookie(None),
|
||||
) -> dict[str, Any]:
|
||||
) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"更新插件配置: {plugin_id}")
|
||||
|
||||
@@ -276,7 +275,7 @@ async def update_plugin_config(
|
||||
|
||||
|
||||
@router.post("/config/{plugin_id}/reset")
|
||||
async def reset_plugin_config(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def reset_plugin_config(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"重置插件配置: {plugin_id}")
|
||||
|
||||
@@ -300,7 +299,7 @@ async def reset_plugin_config(plugin_id: str, maibot_session: Optional[str] = Co
|
||||
|
||||
|
||||
@router.post("/config/{plugin_id}/toggle")
|
||||
async def toggle_plugin(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def toggle_plugin(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"切换插件状态: {plugin_id}")
|
||||
|
||||
@@ -326,9 +325,14 @@ async def toggle_plugin(plugin_id: str, maibot_session: Optional[str] = Cookie(N
|
||||
|
||||
status = "启用" if new_enabled else "禁用"
|
||||
logger.info(f"已{status}插件: {plugin_id}")
|
||||
return {"success": True, "enabled": new_enabled, "message": f"插件已{status}", "note": "状态更改将在下次加载插件时生效"}
|
||||
return {
|
||||
"success": True,
|
||||
"enabled": new_enabled,
|
||||
"message": f"插件已{status}",
|
||||
"note": "状态更改将在下次加载插件时生效",
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"切换插件状态失败: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Cookie, HTTPException
|
||||
|
||||
@@ -18,8 +17,8 @@ from .support import (
|
||||
parse_repository_url,
|
||||
remove_tree,
|
||||
require_plugin_token,
|
||||
resolve_plugin_file_path,
|
||||
resolve_installed_plugin_path,
|
||||
resolve_plugin_file_path,
|
||||
validate_plugin_id,
|
||||
)
|
||||
|
||||
@@ -28,7 +27,7 @@ logger = get_logger("webui.plugin_routes")
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _infer_plugin_id(folder_name: str, manifest: dict[str, Any], manifest_path: Path) -> str:
|
||||
def _infer_plugin_id(folder_name: str, manifest: Dict[str, Any], manifest_path: Path) -> str:
|
||||
if "id" in manifest:
|
||||
return str(manifest["id"])
|
||||
|
||||
@@ -66,43 +65,87 @@ def _infer_plugin_id(folder_name: str, manifest: dict[str, Any], manifest_path:
|
||||
|
||||
|
||||
@router.post("/install")
|
||||
async def install_plugin(request: InstallPluginRequest, maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def install_plugin(request: InstallPluginRequest, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"收到安装插件请求: {request.plugin_id}")
|
||||
plugin_id = request.plugin_id
|
||||
|
||||
try:
|
||||
plugin_id = validate_plugin_id(request.plugin_id)
|
||||
await update_progress(stage="loading", progress=5, message=f"开始安装插件: {plugin_id}", operation="install", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading", progress=5, message=f"开始安装插件: {plugin_id}", operation="install", plugin_id=plugin_id
|
||||
)
|
||||
|
||||
repo_url, owner, repo = parse_repository_url(request.repository_url)
|
||||
await update_progress(stage="loading", progress=10, message=f"解析仓库信息: {owner}/{repo}", operation="install", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading",
|
||||
progress=10,
|
||||
message=f"解析仓库信息: {owner}/{repo}",
|
||||
operation="install",
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
|
||||
target_path, old_format_path = get_plugin_candidate_paths(plugin_id)
|
||||
if target_path.exists() or old_format_path.exists():
|
||||
await update_progress(stage="error", progress=0, message="插件已存在", operation="install", plugin_id=plugin_id, error="插件已安装,请先卸载")
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="插件已存在",
|
||||
operation="install",
|
||||
plugin_id=plugin_id,
|
||||
error="插件已安装,请先卸载",
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="插件已安装")
|
||||
|
||||
await update_progress(stage="loading", progress=15, message=f"准备克隆到: {target_path}", operation="install", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading", progress=15, message=f"准备克隆到: {target_path}", operation="install", plugin_id=plugin_id
|
||||
)
|
||||
service = get_git_mirror_service()
|
||||
if "github.com" in repo_url:
|
||||
result = await service.clone_repository(owner=owner, repo=repo, target_path=target_path, branch=request.branch, mirror_id=request.mirror_id, depth=1)
|
||||
result = await service.clone_repository(
|
||||
owner=owner,
|
||||
repo=repo,
|
||||
target_path=target_path,
|
||||
branch=request.branch,
|
||||
mirror_id=request.mirror_id,
|
||||
depth=1,
|
||||
)
|
||||
else:
|
||||
result = await service.clone_repository(owner=owner, repo=repo, target_path=target_path, branch=request.branch, custom_url=repo_url, depth=1)
|
||||
result = await service.clone_repository(
|
||||
owner=owner, repo=repo, target_path=target_path, branch=request.branch, custom_url=repo_url, depth=1
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
error_msg = str(result.get("error", "克隆失败"))
|
||||
await update_progress(stage="error", progress=0, message="克隆仓库失败", operation="install", plugin_id=plugin_id, error=error_msg)
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="克隆仓库失败",
|
||||
operation="install",
|
||||
plugin_id=plugin_id,
|
||||
error=error_msg,
|
||||
)
|
||||
raise HTTPException(status_code=int(result.get("status_code", 500)), detail=error_msg)
|
||||
|
||||
await update_progress(stage="loading", progress=85, message="验证插件文件...", operation="install", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading", progress=85, message="验证插件文件...", operation="install", plugin_id=plugin_id
|
||||
)
|
||||
manifest_path = resolve_plugin_file_path(target_path, "_manifest.json")
|
||||
if not manifest_path.exists():
|
||||
remove_tree(target_path)
|
||||
await update_progress(stage="error", progress=0, message="插件缺少 _manifest.json", operation="install", plugin_id=plugin_id, error="无效的插件格式")
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="插件缺少 _manifest.json",
|
||||
operation="install",
|
||||
plugin_id=plugin_id,
|
||||
error="无效的插件格式",
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="无效的插件:缺少 _manifest.json")
|
||||
|
||||
await update_progress(stage="loading", progress=90, message="读取插件配置...", operation="install", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading", progress=90, message="读取插件配置...", operation="install", plugin_id=plugin_id
|
||||
)
|
||||
try:
|
||||
with open(manifest_path, "r", encoding="utf-8") as file_obj:
|
||||
manifest = json.load(file_obj)
|
||||
@@ -114,91 +157,199 @@ async def install_plugin(request: InstallPluginRequest, maibot_session: Optional
|
||||
json.dump(manifest, file_obj, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
remove_tree(target_path)
|
||||
await update_progress(stage="error", progress=0, message="_manifest.json 无效", operation="install", plugin_id=plugin_id, error=str(e))
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="_manifest.json 无效",
|
||||
operation="install",
|
||||
plugin_id=plugin_id,
|
||||
error=str(e),
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"无效的 _manifest.json: {e}") from e
|
||||
|
||||
await update_progress(stage="success", progress=100, message=f"成功安装插件: {manifest['name']} v{manifest['version']}", operation="install", plugin_id=plugin_id)
|
||||
return {"success": True, "message": "插件安装成功", "plugin_id": plugin_id, "plugin_name": manifest["name"], "version": manifest["version"], "path": str(target_path)}
|
||||
await update_progress(
|
||||
stage="success",
|
||||
progress=100,
|
||||
message=f"成功安装插件: {manifest['name']} v{manifest['version']}",
|
||||
operation="install",
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "插件安装成功",
|
||||
"plugin_id": plugin_id,
|
||||
"plugin_name": manifest["name"],
|
||||
"version": manifest["version"],
|
||||
"path": str(target_path),
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"安装插件失败: {e}", exc_info=True)
|
||||
await update_progress(stage="error", progress=0, message="安装失败", operation="install", plugin_id=plugin_id, error=str(e))
|
||||
await update_progress(
|
||||
stage="error", progress=0, message="安装失败", operation="install", plugin_id=plugin_id, error=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
|
||||
|
||||
@router.post("/uninstall")
|
||||
async def uninstall_plugin(request: UninstallPluginRequest, maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def uninstall_plugin(
|
||||
request: UninstallPluginRequest, maibot_session: Optional[str] = Cookie(None)
|
||||
) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"收到卸载插件请求: {request.plugin_id}")
|
||||
plugin_id = request.plugin_id
|
||||
|
||||
try:
|
||||
plugin_id = validate_plugin_id(request.plugin_id)
|
||||
await update_progress(stage="loading", progress=10, message=f"开始卸载插件: {plugin_id}", operation="uninstall", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading",
|
||||
progress=10,
|
||||
message=f"开始卸载插件: {plugin_id}",
|
||||
operation="uninstall",
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
plugin_path = resolve_installed_plugin_path(plugin_id)
|
||||
if plugin_path is None:
|
||||
await update_progress(stage="error", progress=0, message="插件不存在", operation="uninstall", plugin_id=plugin_id, error="插件未安装或已被删除")
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="插件不存在",
|
||||
operation="uninstall",
|
||||
plugin_id=plugin_id,
|
||||
error="插件未安装或已被删除",
|
||||
)
|
||||
raise HTTPException(status_code=404, detail="插件未安装")
|
||||
|
||||
await update_progress(stage="loading", progress=30, message=f"正在删除插件文件: {plugin_path}", operation="uninstall", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading",
|
||||
progress=30,
|
||||
message=f"正在删除插件文件: {plugin_path}",
|
||||
operation="uninstall",
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
manifest = load_manifest_json(resolve_plugin_file_path(plugin_path, "_manifest.json"))
|
||||
plugin_name = str(manifest.get("name", plugin_id)) if manifest is not None else plugin_id
|
||||
await update_progress(stage="loading", progress=50, message=f"正在删除 {plugin_name}...", operation="uninstall", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading",
|
||||
progress=50,
|
||||
message=f"正在删除 {plugin_name}...",
|
||||
operation="uninstall",
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
remove_tree(plugin_path)
|
||||
logger.info(f"成功卸载插件: {plugin_id} ({plugin_name})")
|
||||
await update_progress(stage="success", progress=100, message=f"成功卸载插件: {plugin_name}", operation="uninstall", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="success",
|
||||
progress=100,
|
||||
message=f"成功卸载插件: {plugin_name}",
|
||||
operation="uninstall",
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
return {"success": True, "message": "插件卸载成功", "plugin_id": plugin_id, "plugin_name": plugin_name}
|
||||
except HTTPException:
|
||||
raise
|
||||
except PermissionError as e:
|
||||
logger.error(f"卸载插件失败(权限错误): {e}")
|
||||
await update_progress(stage="error", progress=0, message="卸载失败", operation="uninstall", plugin_id=plugin_id, error="权限不足,无法删除插件文件")
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="卸载失败",
|
||||
operation="uninstall",
|
||||
plugin_id=plugin_id,
|
||||
error="权限不足,无法删除插件文件",
|
||||
)
|
||||
raise HTTPException(status_code=500, detail="权限不足,无法删除插件文件") from e
|
||||
except Exception as e:
|
||||
logger.error(f"卸载插件失败: {e}", exc_info=True)
|
||||
await update_progress(stage="error", progress=0, message="卸载失败", operation="uninstall", plugin_id=plugin_id, error=str(e))
|
||||
await update_progress(
|
||||
stage="error", progress=0, message="卸载失败", operation="uninstall", plugin_id=plugin_id, error=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
|
||||
|
||||
@router.post("/update")
|
||||
async def update_plugin(request: UpdatePluginRequest, maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def update_plugin(request: UpdatePluginRequest, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"收到更新插件请求: {request.plugin_id}")
|
||||
plugin_id = request.plugin_id
|
||||
|
||||
try:
|
||||
plugin_id = validate_plugin_id(request.plugin_id)
|
||||
await update_progress(stage="loading", progress=5, message=f"开始更新插件: {plugin_id}", operation="update", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading", progress=5, message=f"开始更新插件: {plugin_id}", operation="update", plugin_id=plugin_id
|
||||
)
|
||||
plugin_path = resolve_installed_plugin_path(plugin_id)
|
||||
if plugin_path is None:
|
||||
await update_progress(stage="error", progress=0, message="插件不存在", operation="update", plugin_id=plugin_id, error="插件未安装,请先安装")
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="插件不存在",
|
||||
operation="update",
|
||||
plugin_id=plugin_id,
|
||||
error="插件未安装,请先安装",
|
||||
)
|
||||
raise HTTPException(status_code=404, detail="插件未安装")
|
||||
|
||||
manifest = load_manifest_json(resolve_plugin_file_path(plugin_path, "_manifest.json"))
|
||||
old_version = str(manifest.get("version", "unknown")) if manifest is not None else "unknown"
|
||||
await update_progress(stage="loading", progress=10, message=f"当前版本: {old_version},准备更新...", operation="update", plugin_id=plugin_id)
|
||||
await update_progress(stage="loading", progress=20, message="正在删除旧版本...", operation="update", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading",
|
||||
progress=10,
|
||||
message=f"当前版本: {old_version},准备更新...",
|
||||
operation="update",
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
await update_progress(
|
||||
stage="loading", progress=20, message="正在删除旧版本...", operation="update", plugin_id=plugin_id
|
||||
)
|
||||
remove_tree(plugin_path)
|
||||
|
||||
await update_progress(stage="loading", progress=30, message="正在准备下载新版本...", operation="update", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading", progress=30, message="正在准备下载新版本...", operation="update", plugin_id=plugin_id
|
||||
)
|
||||
repo_url, owner, repo = parse_repository_url(request.repository_url)
|
||||
service = get_git_mirror_service()
|
||||
if "github.com" in repo_url:
|
||||
result = await service.clone_repository(owner=owner, repo=repo, target_path=plugin_path, branch=request.branch, mirror_id=request.mirror_id, depth=1)
|
||||
result = await service.clone_repository(
|
||||
owner=owner,
|
||||
repo=repo,
|
||||
target_path=plugin_path,
|
||||
branch=request.branch,
|
||||
mirror_id=request.mirror_id,
|
||||
depth=1,
|
||||
)
|
||||
else:
|
||||
result = await service.clone_repository(owner=owner, repo=repo, target_path=plugin_path, branch=request.branch, custom_url=repo_url, depth=1)
|
||||
result = await service.clone_repository(
|
||||
owner=owner, repo=repo, target_path=plugin_path, branch=request.branch, custom_url=repo_url, depth=1
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
error_msg = str(result.get("error", "克隆失败"))
|
||||
await update_progress(stage="error", progress=0, message="下载新版本失败", operation="update", plugin_id=plugin_id, error=error_msg)
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="下载新版本失败",
|
||||
operation="update",
|
||||
plugin_id=plugin_id,
|
||||
error=error_msg,
|
||||
)
|
||||
raise HTTPException(status_code=int(result.get("status_code", 500)), detail=error_msg)
|
||||
|
||||
await update_progress(stage="loading", progress=90, message="验证新版本...", operation="update", plugin_id=plugin_id)
|
||||
await update_progress(
|
||||
stage="loading", progress=90, message="验证新版本...", operation="update", plugin_id=plugin_id
|
||||
)
|
||||
new_manifest_path = resolve_plugin_file_path(plugin_path, "_manifest.json")
|
||||
if not new_manifest_path.exists():
|
||||
remove_tree(plugin_path)
|
||||
await update_progress(stage="error", progress=0, message="新版本缺少 _manifest.json", operation="update", plugin_id=plugin_id, error="无效的插件格式")
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="新版本缺少 _manifest.json",
|
||||
operation="update",
|
||||
plugin_id=plugin_id,
|
||||
error="无效的插件格式",
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="无效的插件:缺少 _manifest.json")
|
||||
|
||||
try:
|
||||
@@ -207,27 +358,49 @@ async def update_plugin(request: UpdatePluginRequest, maibot_session: Optional[s
|
||||
new_version = str(new_manifest.get("version", "unknown"))
|
||||
new_name = str(new_manifest.get("name", plugin_id))
|
||||
logger.info(f"成功更新插件: {plugin_id} {old_version} → {new_version}")
|
||||
await update_progress(stage="success", progress=100, message=f"成功更新 {new_name}: {old_version} → {new_version}", operation="update", plugin_id=plugin_id)
|
||||
return {"success": True, "message": "插件更新成功", "plugin_id": plugin_id, "plugin_name": new_name, "old_version": old_version, "new_version": new_version}
|
||||
await update_progress(
|
||||
stage="success",
|
||||
progress=100,
|
||||
message=f"成功更新 {new_name}: {old_version} → {new_version}",
|
||||
operation="update",
|
||||
plugin_id=plugin_id,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "插件更新成功",
|
||||
"plugin_id": plugin_id,
|
||||
"plugin_name": new_name,
|
||||
"old_version": old_version,
|
||||
"new_version": new_version,
|
||||
}
|
||||
except Exception as e:
|
||||
remove_tree(plugin_path)
|
||||
await update_progress(stage="error", progress=0, message="_manifest.json 无效", operation="update", plugin_id=plugin_id, error=str(e))
|
||||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message="_manifest.json 无效",
|
||||
operation="update",
|
||||
plugin_id=plugin_id,
|
||||
error=str(e),
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"无效的 _manifest.json: {e}") from e
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"更新插件失败: {e}", exc_info=True)
|
||||
await update_progress(stage="error", progress=0, message="更新失败", operation="update", plugin_id=plugin_id, error=str(e))
|
||||
await update_progress(
|
||||
stage="error", progress=0, message="更新失败", operation="update", plugin_id=plugin_id, error=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
|
||||
|
||||
@router.get("/installed")
|
||||
async def get_installed_plugins(maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def get_installed_plugins(maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info("收到获取已安装插件列表请求")
|
||||
|
||||
try:
|
||||
installed_plugins: list[dict[str, Any]] = []
|
||||
installed_plugins: List[Dict[str, Any]] = []
|
||||
for plugin_path in iter_plugin_directories():
|
||||
folder_name = plugin_path.name
|
||||
if folder_name.startswith(".") or folder_name.startswith("__"):
|
||||
@@ -253,9 +426,9 @@ async def get_installed_plugins(maibot_session: Optional[str] = Cookie(None)) ->
|
||||
except Exception as e:
|
||||
logger.error(f"读取插件 {folder_name} 信息时出错: {e}")
|
||||
|
||||
seen_ids: dict[str, str] = {}
|
||||
unique_plugins: list[dict[str, Any]] = []
|
||||
duplicates: list[dict[str, Any]] = []
|
||||
seen_ids: Dict[str, str] = {}
|
||||
unique_plugins: List[Dict[str, Any]] = []
|
||||
duplicates: List[Dict[str, Any]] = []
|
||||
for plugin in installed_plugins:
|
||||
plugin_id = str(plugin["id"])
|
||||
plugin_path = str(plugin["path"])
|
||||
@@ -277,7 +450,7 @@ async def get_installed_plugins(maibot_session: Optional[str] = Cookie(None)) ->
|
||||
|
||||
|
||||
@router.get("/local-readme/{plugin_id}")
|
||||
async def get_local_plugin_readme(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> dict[str, Any]:
|
||||
async def get_local_plugin_readme(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"获取本地插件 README: {plugin_id}")
|
||||
|
||||
@@ -300,4 +473,4 @@ async def get_local_plugin_readme(plugin_id: str, maibot_session: Optional[str]
|
||||
return {"success": False, "error": "本地未找到 README 文件"}
|
||||
except Exception as e:
|
||||
logger.error(f"获取本地 README 失败: {e}", exc_info=True)
|
||||
return {"success": False, "error": str(e)}
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from typing import Any, Optional, Set
|
||||
from typing import Any, Dict, Optional, Set
|
||||
|
||||
from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
@@ -14,7 +13,7 @@ logger = get_logger("webui.plugin_progress")
|
||||
router = APIRouter()
|
||||
|
||||
active_connections: Set[WebSocket] = set()
|
||||
current_progress: dict[str, Any] = {
|
||||
current_progress: Dict[str, Any] = {
|
||||
"operation": "idle",
|
||||
"stage": "idle",
|
||||
"progress": 0,
|
||||
@@ -26,7 +25,7 @@ current_progress: dict[str, Any] = {
|
||||
}
|
||||
|
||||
|
||||
async def broadcast_progress(progress_data: dict[str, Any]) -> None:
|
||||
async def broadcast_progress(progress_data: Dict[str, Any]) -> None:
|
||||
global current_progress
|
||||
current_progress = progress_data.copy()
|
||||
|
||||
@@ -34,7 +33,7 @@ async def broadcast_progress(progress_data: dict[str, Any]) -> None:
|
||||
return
|
||||
|
||||
message = json.dumps(progress_data, ensure_ascii=False)
|
||||
disconnected: set[WebSocket] = set()
|
||||
disconnected: Set[WebSocket] = set()
|
||||
|
||||
for websocket in active_connections:
|
||||
try:
|
||||
@@ -119,4 +118,4 @@ async def websocket_plugin_progress(websocket: WebSocket, token: Optional[str] =
|
||||
|
||||
|
||||
def get_progress_router() -> APIRouter:
|
||||
return router
|
||||
return router
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -51,8 +51,8 @@ class MirrorConfigResponse(BaseModel):
|
||||
|
||||
|
||||
class AvailableMirrorsResponse(BaseModel):
|
||||
mirrors: list[MirrorConfigResponse] = Field(..., description="镜像源列表")
|
||||
default_priority: list[str] = Field(..., description="默认优先级顺序(ID 列表)")
|
||||
mirrors: List[MirrorConfigResponse] = Field(..., description="镜像源列表")
|
||||
default_priority: List[str] = Field(..., description="默认优先级顺序(ID 列表)")
|
||||
|
||||
|
||||
class AddMirrorRequest(BaseModel):
|
||||
@@ -106,8 +106,8 @@ class UpdatePluginRequest(BaseModel):
|
||||
|
||||
class UpdatePluginConfigRequest(BaseModel):
|
||||
enabled: Optional[bool] = None
|
||||
config: Optional[dict[str, Any]] = None
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class UpdatePluginRawConfigRequest(BaseModel):
|
||||
config: str = Field(..., description="原始 TOML 配置内容")
|
||||
config: str = Field(..., description="原始 TOML 配置内容")
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, cast, get_origin
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, cast, get_origin
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -53,10 +52,7 @@ def _resolve_safe_plugin_directory(plugin_path: Path, plugins_dir: Path, strict:
|
||||
resolved_plugin_path = plugin_path.resolve()
|
||||
resolved_plugin_path.relative_to(resolved_plugins_dir)
|
||||
|
||||
if not resolved_plugin_path.is_dir():
|
||||
return None
|
||||
|
||||
return resolved_plugin_path
|
||||
return resolved_plugin_path if resolved_plugin_path.is_dir() else None
|
||||
except HTTPException:
|
||||
if strict:
|
||||
raise
|
||||
@@ -64,7 +60,7 @@ def _resolve_safe_plugin_directory(plugin_path: Path, plugins_dir: Path, strict:
|
||||
return None
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
if strict:
|
||||
raise HTTPException(status_code=400, detail="插件目录超出允许范围")
|
||||
raise HTTPException(status_code=400, detail="插件目录超出允许范围") from None
|
||||
logger.warning(f"已跳过越界的插件目录: {plugin_path}")
|
||||
return None
|
||||
|
||||
@@ -109,7 +105,7 @@ def validate_plugin_id(plugin_id: str) -> str:
|
||||
return plugin_id
|
||||
|
||||
|
||||
def parse_version(version_str: str) -> tuple[int, int, int]:
|
||||
def parse_version(version_str: str) -> Tuple[int, int, int]:
|
||||
base_version = re.split(r"[-.](?:snapshot|dev|alpha|beta|rc)", version_str, flags=re.IGNORECASE)[0]
|
||||
parts = base_version.split(".")
|
||||
if len(parts) < 3:
|
||||
@@ -122,7 +118,7 @@ def parse_version(version_str: str) -> tuple[int, int, int]:
|
||||
return 0, 0, 0
|
||||
|
||||
|
||||
def deep_merge(dst: dict[str, Any], src: dict[str, Any]) -> None:
|
||||
def deep_merge(dst: Dict[str, Any], src: Dict[str, Any]) -> None:
|
||||
for key, value in src.items():
|
||||
if key in dst and isinstance(dst[key], dict) and isinstance(value, dict):
|
||||
deep_merge(dst[key], value)
|
||||
@@ -130,9 +126,9 @@ def deep_merge(dst: dict[str, Any], src: dict[str, Any]) -> None:
|
||||
dst[key] = value
|
||||
|
||||
|
||||
def normalize_dotted_keys(obj: dict[str, Any]) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {}
|
||||
dotted_items: list[tuple[str, Any]] = []
|
||||
def normalize_dotted_keys(obj: Dict[str, Any]) -> Dict[str, Any]:
|
||||
result: Dict[str, Any] = {}
|
||||
dotted_items: List[Tuple[str, Any]] = []
|
||||
|
||||
for key, value in obj.items():
|
||||
if "." in key:
|
||||
@@ -167,7 +163,7 @@ def normalize_dotted_keys(obj: dict[str, Any]) -> dict[str, Any]:
|
||||
return result
|
||||
|
||||
|
||||
def coerce_types(schema_part: dict[str, Any], config_part: dict[str, Any]) -> None:
|
||||
def coerce_types(schema_part: Dict[str, Any], config_part: Dict[str, Any]) -> None:
|
||||
def is_list_type(tp: Any) -> bool:
|
||||
origin = get_origin(tp)
|
||||
return tp is list or origin is list
|
||||
@@ -200,7 +196,7 @@ def get_plugins_dir() -> Path:
|
||||
return plugins_dir
|
||||
|
||||
|
||||
def get_plugin_candidate_paths(plugin_id: str) -> tuple[Path, Path]:
|
||||
def get_plugin_candidate_paths(plugin_id: str) -> Tuple[Path, Path]:
|
||||
plugins_dir = get_plugins_dir()
|
||||
folder_name = plugin_id.replace(".", "_")
|
||||
return validate_safe_path(folder_name, plugins_dir), validate_safe_path(plugin_id, plugins_dir)
|
||||
@@ -217,7 +213,7 @@ def resolve_installed_plugin_path(plugin_id: str) -> Optional[Path]:
|
||||
return None
|
||||
|
||||
|
||||
def parse_repository_url(repository_url: str) -> tuple[str, str, str]:
|
||||
def parse_repository_url(repository_url: str) -> Tuple[str, str, str]:
|
||||
repo_url = repository_url.rstrip("/").removesuffix(".git")
|
||||
parts = repo_url.split("/")
|
||||
if len(parts) < 2:
|
||||
@@ -225,7 +221,7 @@ def parse_repository_url(repository_url: str) -> tuple[str, str, str]:
|
||||
return repo_url, parts[-2], parts[-1]
|
||||
|
||||
|
||||
def load_manifest_json(manifest_path: Path) -> Optional[dict[str, Any]]:
|
||||
def load_manifest_json(manifest_path: Path) -> Optional[Dict[str, Any]]:
|
||||
if not manifest_path.exists():
|
||||
return None
|
||||
|
||||
@@ -246,9 +242,9 @@ def load_manifest_json(manifest_path: Path) -> Optional[dict[str, Any]]:
|
||||
return None
|
||||
|
||||
|
||||
def iter_plugin_directories() -> list[Path]:
|
||||
def iter_plugin_directories() -> List[Path]:
|
||||
plugins_dir = get_plugins_dir()
|
||||
plugin_directories: list[Path] = []
|
||||
plugin_directories: List[Path] = []
|
||||
for path in plugins_dir.iterdir():
|
||||
safe_path = _resolve_safe_plugin_directory(path, plugins_dir, strict=False)
|
||||
if safe_path is not None:
|
||||
@@ -286,4 +282,4 @@ def remove_tree(path: Path) -> None:
|
||||
os.chmod(target_path, stat.S_IWRITE)
|
||||
func(target_path)
|
||||
|
||||
shutil.rmtree(path, onerror=remove_readonly)
|
||||
shutil.rmtree(path, onerror=remove_readonly)
|
||||
|
||||
Reference in New Issue
Block a user