merge: 同步 upstream/r-dev 并解决冲突
This commit is contained in:
@@ -6,11 +6,13 @@ from .catalog import router as catalog_router
|
||||
from .config_routes import router as config_router
|
||||
from .management import router as management_router
|
||||
from .progress import get_progress_router, update_progress
|
||||
from .runtime_routes import router as runtime_router
|
||||
|
||||
router = APIRouter(prefix="/plugins", tags=["插件管理"])
|
||||
router.include_router(catalog_router)
|
||||
router.include_router(management_router)
|
||||
router.include_router(config_router)
|
||||
router.include_router(runtime_router)
|
||||
|
||||
set_update_progress_callback(update_progress)
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import json
|
||||
"""插件配置相关 WebUI 路由。"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
import tomlkit
|
||||
from fastapi import APIRouter, Cookie, HTTPException
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_runtime.protocol.envelope import InspectPluginConfigResultPayload
|
||||
from src.webui.utils.toml_utils import save_toml_with_format
|
||||
|
||||
from .schemas import UpdatePluginConfigRequest, UpdatePluginRawConfigRequest
|
||||
from .support import (
|
||||
backup_file,
|
||||
coerce_types,
|
||||
find_plugin_instance,
|
||||
find_plugin_path_by_id,
|
||||
get_plugin_config_path,
|
||||
normalize_dotted_keys,
|
||||
@@ -39,6 +40,16 @@ def _to_builtin_data(obj: Any) -> Any:
|
||||
|
||||
|
||||
def _build_schema_from_current_config(plugin_id: str, current_config: Any) -> Dict[str, Any]:
|
||||
"""根据当前配置内容自动推断一个兜底 Schema。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
current_config: 当前配置对象。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 可供前端渲染的兜底 Schema。
|
||||
"""
|
||||
|
||||
schema: Dict[str, Any] = {
|
||||
"plugin_id": plugin_id,
|
||||
"plugin_info": {
|
||||
@@ -134,33 +145,187 @@ def _build_schema_from_current_config(plugin_id: str, current_config: Any) -> Di
|
||||
return schema
|
||||
|
||||
|
||||
def _coerce_scalar_value(field_schema: Dict[str, Any], value: Any) -> Any:
|
||||
"""根据字段 Schema 规范化单个字段值。
|
||||
|
||||
Args:
|
||||
field_schema: 单个字段 Schema。
|
||||
value: 当前字段值。
|
||||
|
||||
Returns:
|
||||
Any: 规范化后的字段值。
|
||||
"""
|
||||
|
||||
field_type = str(field_schema.get("type", "") or "").lower()
|
||||
if field_type == "boolean" and isinstance(value, str):
|
||||
normalized_value = value.strip().lower()
|
||||
if normalized_value in {"1", "true", "yes", "on"}:
|
||||
return True
|
||||
if normalized_value in {"0", "false", "no", "off"}:
|
||||
return False
|
||||
if field_type == "integer" and isinstance(value, str):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
if field_type == "number" and isinstance(value, str):
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return value
|
||||
if field_type == "array" and isinstance(value, str):
|
||||
return [item.strip() for item in value.split(",") if item.strip()]
|
||||
return value
|
||||
|
||||
|
||||
def _coerce_config_by_plugin_schema(schema: Dict[str, Any], config_data: Dict[str, Any]) -> None:
|
||||
"""根据插件配置 Schema 就地规范化配置值类型。
|
||||
|
||||
Args:
|
||||
schema: 插件配置 Schema。
|
||||
config_data: 待规范化的配置字典。
|
||||
"""
|
||||
|
||||
sections = schema.get("sections")
|
||||
if not isinstance(sections, dict):
|
||||
return
|
||||
|
||||
for section_name, section_schema in sections.items():
|
||||
if not isinstance(section_schema, dict):
|
||||
continue
|
||||
if section_name not in config_data or not isinstance(config_data[section_name], dict):
|
||||
continue
|
||||
|
||||
section_fields = section_schema.get("fields")
|
||||
if not isinstance(section_fields, dict):
|
||||
continue
|
||||
|
||||
section_config = cast(Dict[str, Any], config_data[section_name])
|
||||
for field_name, field_schema in section_fields.items():
|
||||
if field_name not in section_config or not isinstance(field_schema, dict):
|
||||
continue
|
||||
section_config[field_name] = _coerce_scalar_value(field_schema, section_config[field_name])
|
||||
|
||||
|
||||
def _build_toml_document(config_data: Dict[str, Any]) -> tomlkit.TOMLDocument:
|
||||
"""将普通字典转换为 TOML 文档对象。
|
||||
|
||||
Args:
|
||||
config_data: 原始配置字典。
|
||||
|
||||
Returns:
|
||||
tomlkit.TOMLDocument: 解析后的 TOML 文档。
|
||||
"""
|
||||
|
||||
if not config_data:
|
||||
return tomlkit.document()
|
||||
return tomlkit.parse(tomlkit.dumps(config_data))
|
||||
|
||||
|
||||
def _load_plugin_config_from_disk(plugin_path: Path) -> Dict[str, Any]:
|
||||
"""从磁盘读取插件配置。
|
||||
|
||||
Args:
|
||||
plugin_path: 插件目录。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 当前配置字典;文件不存在时返回空字典。
|
||||
"""
|
||||
|
||||
config_path = resolve_plugin_file_path(plugin_path, "config.toml")
|
||||
if not config_path.exists():
|
||||
return {}
|
||||
|
||||
with open(config_path, "r", encoding="utf-8") as file_obj:
|
||||
loaded_config = tomlkit.load(file_obj).unwrap()
|
||||
return loaded_config if isinstance(loaded_config, dict) else {}
|
||||
|
||||
|
||||
async def _inspect_plugin_config_via_runtime(
|
||||
plugin_id: str,
|
||||
config_data: Optional[Dict[str, Any]] = None,
|
||||
*,
|
||||
use_provided_config: bool = False,
|
||||
) -> InspectPluginConfigResultPayload | None:
|
||||
"""通过插件运行时解析配置元数据。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
config_data: 可选的配置内容。
|
||||
use_provided_config: 是否优先使用传入配置而不是磁盘配置。
|
||||
|
||||
Returns:
|
||||
InspectPluginConfigResultPayload | None: 运行时可用时返回解析结果,否则返回 ``None``。
|
||||
|
||||
Raises:
|
||||
ValueError: 插件运行时明确拒绝解析请求时抛出。
|
||||
"""
|
||||
|
||||
from src.plugin_runtime.integration import get_plugin_runtime_manager
|
||||
|
||||
runtime_manager = get_plugin_runtime_manager()
|
||||
return await runtime_manager.inspect_plugin_config(
|
||||
plugin_id,
|
||||
config_data,
|
||||
use_provided_config=use_provided_config,
|
||||
)
|
||||
|
||||
|
||||
async def _validate_plugin_config_via_runtime(plugin_id: str, config_data: Dict[str, Any]) -> Dict[str, Any] | None:
|
||||
"""通过插件运行时对配置进行校验。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
config_data: 待校验的配置内容。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any] | None: 校验成功时返回规范化后的配置;若运行时不可用则返回
|
||||
``None``,由调用方自行回退到静态 Schema 方案。
|
||||
|
||||
Raises:
|
||||
ValueError: 插件运行时明确判定配置非法时抛出。
|
||||
"""
|
||||
|
||||
from src.plugin_runtime.integration import get_plugin_runtime_manager
|
||||
|
||||
runtime_manager = get_plugin_runtime_manager()
|
||||
return await runtime_manager.validate_plugin_config(plugin_id, config_data)
|
||||
|
||||
|
||||
@router.get("/config/{plugin_id}/schema")
|
||||
async def get_plugin_config_schema(plugin_id: str, maibot_session: Optional[str] = Cookie(None)) -> Dict[str, Any]:
|
||||
"""按插件 ID 返回配置 Schema。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
maibot_session: 当前会话令牌。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 包含 Schema 的响应字典。
|
||||
"""
|
||||
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"获取插件配置 Schema: {plugin_id}")
|
||||
|
||||
try:
|
||||
plugin_instance = find_plugin_instance(plugin_id)
|
||||
if plugin_instance and hasattr(plugin_instance, "get_webui_config_schema"):
|
||||
return {"success": True, "schema": plugin_instance.get_webui_config_schema()}
|
||||
|
||||
plugin_path = find_plugin_path_by_id(plugin_id)
|
||||
if plugin_path is None:
|
||||
raise HTTPException(status_code=404, detail=f"未找到插件: {plugin_id}")
|
||||
|
||||
schema_json_path = resolve_plugin_file_path(plugin_path, "config_schema.json")
|
||||
if schema_json_path.exists():
|
||||
try:
|
||||
with open(schema_json_path, "r", encoding="utf-8") as file_obj:
|
||||
return {"success": True, "schema": json.load(file_obj)}
|
||||
except Exception as e:
|
||||
logger.warning(f"读取 config_schema.json 失败,回退到自动推断: {e}")
|
||||
try:
|
||||
runtime_snapshot = await _inspect_plugin_config_via_runtime(plugin_id)
|
||||
except ValueError as exc:
|
||||
logger.warning(f"插件 {plugin_id} 配置 Schema 解析失败,将回退到弱推断: {exc}")
|
||||
runtime_snapshot = None
|
||||
|
||||
current_config: Any = {}
|
||||
config_path = get_plugin_config_path(plugin_id, plugin_path)
|
||||
if config_path.exists():
|
||||
with open(config_path, "r", encoding="utf-8") as file_obj:
|
||||
current_config = tomlkit.load(file_obj)
|
||||
if runtime_snapshot is not None and runtime_snapshot.config_schema:
|
||||
return {"success": True, "schema": dict(runtime_snapshot.config_schema)}
|
||||
|
||||
current_config: Any = (
|
||||
dict(runtime_snapshot.normalized_config)
|
||||
if runtime_snapshot is not None
|
||||
else _load_plugin_config_from_disk(plugin_path)
|
||||
)
|
||||
|
||||
return {"success": True, "schema": _build_schema_from_current_config(plugin_id, current_config)}
|
||||
except HTTPException:
|
||||
@@ -172,6 +337,16 @@ 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]:
|
||||
"""获取插件原始 TOML 配置内容。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
maibot_session: 当前会话令牌。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 包含原始配置文本的响应字典。
|
||||
"""
|
||||
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"获取插件原始配置: {plugin_id}")
|
||||
|
||||
@@ -199,6 +374,17 @@ async def update_plugin_config_raw(
|
||||
request: UpdatePluginRawConfigRequest,
|
||||
maibot_session: Optional[str] = Cookie(None),
|
||||
) -> Dict[str, Any]:
|
||||
"""更新插件原始 TOML 配置内容。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
request: 原始配置更新请求。
|
||||
maibot_session: 当前会话令牌。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 更新结果。
|
||||
"""
|
||||
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"更新插件原始配置: {plugin_id}")
|
||||
|
||||
@@ -232,6 +418,16 @@ 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]:
|
||||
"""获取插件配置字典。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
maibot_session: 当前会话令牌。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 当前配置响应。
|
||||
"""
|
||||
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"获取插件配置: {plugin_id}")
|
||||
|
||||
@@ -241,12 +437,24 @@ async def get_plugin_config(plugin_id: str, maibot_session: Optional[str] = Cook
|
||||
raise HTTPException(status_code=404, detail=f"未找到插件: {plugin_id}")
|
||||
|
||||
config_path = get_plugin_config_path(plugin_id, plugin_path)
|
||||
try:
|
||||
runtime_snapshot = await _inspect_plugin_config_via_runtime(plugin_id)
|
||||
except ValueError as exc:
|
||||
logger.warning(f"插件 {plugin_id} 配置读取失败,将回退到磁盘内容: {exc}")
|
||||
runtime_snapshot = None
|
||||
|
||||
if runtime_snapshot is not None:
|
||||
message = "配置文件不存在,已返回默认配置" if not config_path.exists() else ""
|
||||
return {
|
||||
"success": True,
|
||||
"config": dict(runtime_snapshot.normalized_config),
|
||||
"message": message,
|
||||
}
|
||||
|
||||
if not config_path.exists():
|
||||
return {"success": True, "config": {}, "message": "配置文件不存在"}
|
||||
|
||||
with open(config_path, "r", encoding="utf-8") as file_obj:
|
||||
config = tomlkit.load(file_obj)
|
||||
return {"success": True, "config": _to_builtin_data(config)}
|
||||
return {"success": True, "config": _load_plugin_config_from_disk(plugin_path)}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -260,21 +468,40 @@ async def update_plugin_config(
|
||||
request: UpdatePluginConfigRequest,
|
||||
maibot_session: Optional[str] = Cookie(None),
|
||||
) -> Dict[str, Any]:
|
||||
"""更新插件结构化配置。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
request: 结构化配置更新请求。
|
||||
maibot_session: 当前会话令牌。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 更新结果。
|
||||
"""
|
||||
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"更新插件配置: {plugin_id}")
|
||||
|
||||
try:
|
||||
plugin_instance = find_plugin_instance(plugin_id)
|
||||
config_data = request.config or {}
|
||||
if plugin_instance and isinstance(config_data, dict):
|
||||
config_data = normalize_dotted_keys(config_data)
|
||||
if isinstance(plugin_instance.config_schema, dict):
|
||||
coerce_types(plugin_instance.config_schema, config_data)
|
||||
|
||||
plugin_path = find_plugin_path_by_id(plugin_id)
|
||||
if plugin_path is None:
|
||||
raise HTTPException(status_code=404, detail=f"未找到插件: {plugin_id}")
|
||||
|
||||
config_data = request.config or {}
|
||||
if isinstance(config_data, dict):
|
||||
config_data = normalize_dotted_keys(config_data)
|
||||
runtime_validated_config = await _validate_plugin_config_via_runtime(plugin_id, config_data)
|
||||
if isinstance(runtime_validated_config, dict):
|
||||
config_data = runtime_validated_config
|
||||
else:
|
||||
runtime_snapshot = await _inspect_plugin_config_via_runtime(
|
||||
plugin_id,
|
||||
config_data,
|
||||
use_provided_config=True,
|
||||
)
|
||||
if runtime_snapshot is not None and runtime_snapshot.config_schema:
|
||||
_coerce_config_by_plugin_schema(dict(runtime_snapshot.config_schema), config_data)
|
||||
|
||||
config_path = get_plugin_config_path(plugin_id, plugin_path)
|
||||
backup_path = backup_file(config_path, "backup")
|
||||
if backup_path is not None:
|
||||
@@ -284,6 +511,8 @@ async def update_plugin_config(
|
||||
save_toml_with_format(config_data, str(config_path))
|
||||
logger.info(f"已更新插件配置: {plugin_id}")
|
||||
return {"success": True, "message": "配置已保存", "note": "配置更改将自动热更新到对应插件"}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -293,6 +522,16 @@ 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]:
|
||||
"""重置插件配置文件。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
maibot_session: 当前会话令牌。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 重置结果。
|
||||
"""
|
||||
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"重置插件配置: {plugin_id}")
|
||||
|
||||
@@ -317,6 +556,16 @@ 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]:
|
||||
"""切换插件启用状态。
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID。
|
||||
maibot_session: 当前会话令牌。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 切换结果。
|
||||
"""
|
||||
|
||||
require_plugin_token(maibot_session)
|
||||
logger.info(f"切换插件状态: {plugin_id}")
|
||||
|
||||
@@ -326,16 +575,29 @@ async def toggle_plugin(plugin_id: str, maibot_session: Optional[str] = Cookie(N
|
||||
raise HTTPException(status_code=404, detail=f"未找到插件: {plugin_id}")
|
||||
|
||||
config_path = get_plugin_config_path(plugin_id, plugin_path)
|
||||
config = tomlkit.document()
|
||||
if config_path.exists():
|
||||
with open(config_path, "r", encoding="utf-8") as file_obj:
|
||||
config = tomlkit.load(file_obj)
|
||||
try:
|
||||
runtime_snapshot = await _inspect_plugin_config_via_runtime(plugin_id)
|
||||
except ValueError as exc:
|
||||
logger.warning(f"插件 {plugin_id} 状态切换前配置解析失败,将回退到磁盘内容: {exc}")
|
||||
runtime_snapshot = None
|
||||
|
||||
if "plugin" not in config:
|
||||
current_config = (
|
||||
dict(runtime_snapshot.normalized_config)
|
||||
if runtime_snapshot is not None
|
||||
else _load_plugin_config_from_disk(plugin_path)
|
||||
)
|
||||
config = _build_toml_document(current_config)
|
||||
|
||||
plugin_section = config.get("plugin")
|
||||
if plugin_section is None or not hasattr(plugin_section, "get"):
|
||||
config["plugin"] = tomlkit.table()
|
||||
|
||||
plugin_config = cast(Any, config["plugin"])
|
||||
current_enabled = bool(plugin_config.get("enabled", True))
|
||||
current_enabled = (
|
||||
bool(runtime_snapshot.enabled)
|
||||
if runtime_snapshot is not None
|
||||
else bool(plugin_config.get("enabled", True))
|
||||
)
|
||||
new_enabled = not current_enabled
|
||||
plugin_config["enabled"] = new_enabled
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -347,7 +609,7 @@ async def toggle_plugin(plugin_id: str, maibot_session: Optional[str] = Cookie(N
|
||||
"success": True,
|
||||
"enabled": new_enabled,
|
||||
"message": f"插件已{status}",
|
||||
"note": "状态更改将在下次加载插件时生效",
|
||||
"note": "状态更改将自动热更新到对应插件",
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""插件进度实时推送支持。"""
|
||||
|
||||
from typing import Any, Dict, Optional, Set
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Dict, Optional, Set
|
||||
|
||||
from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.webui.core import get_token_manager
|
||||
from src.webui.routers.websocket.auth import verify_ws_token
|
||||
from src.webui.routers.websocket.manager import websocket_manager
|
||||
|
||||
logger = get_logger("webui.plugin_progress")
|
||||
|
||||
@@ -25,25 +28,29 @@ current_progress: Dict[str, Any] = {
|
||||
}
|
||||
|
||||
|
||||
def get_current_progress() -> Dict[str, Any]:
|
||||
"""获取当前插件进度快照。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 当前插件进度数据副本。
|
||||
"""
|
||||
return current_progress.copy()
|
||||
|
||||
|
||||
async def broadcast_progress(progress_data: Dict[str, Any]) -> None:
|
||||
"""向统一连接层广播插件进度更新。
|
||||
|
||||
Args:
|
||||
progress_data: 插件进度数据。
|
||||
"""
|
||||
global current_progress
|
||||
current_progress = progress_data.copy()
|
||||
|
||||
if not active_connections:
|
||||
return
|
||||
|
||||
message = json.dumps(progress_data, ensure_ascii=False)
|
||||
disconnected: Set[WebSocket] = set()
|
||||
|
||||
for websocket in active_connections:
|
||||
try:
|
||||
await websocket.send_text(message)
|
||||
except Exception as e:
|
||||
logger.error(f"发送进度更新失败: {e}")
|
||||
disconnected.add(websocket)
|
||||
|
||||
for websocket in disconnected:
|
||||
active_connections.discard(websocket)
|
||||
await websocket_manager.broadcast_to_topic(
|
||||
domain="plugin_progress",
|
||||
topic="main",
|
||||
event="update",
|
||||
data={"progress": progress_data},
|
||||
)
|
||||
|
||||
|
||||
async def update_progress(
|
||||
@@ -56,6 +63,18 @@ async def update_progress(
|
||||
total_plugins: int = 0,
|
||||
loaded_plugins: int = 0,
|
||||
) -> None:
|
||||
"""更新当前插件进度并广播。
|
||||
|
||||
Args:
|
||||
stage: 当前阶段。
|
||||
progress: 当前进度百分比。
|
||||
message: 进度说明消息。
|
||||
operation: 当前操作类型。
|
||||
error: 可选的错误信息。
|
||||
plugin_id: 当前处理的插件 ID。
|
||||
total_plugins: 总插件数量。
|
||||
loaded_plugins: 已处理插件数量。
|
||||
"""
|
||||
progress_data = {
|
||||
"operation": operation,
|
||||
"stage": stage,
|
||||
@@ -74,6 +93,12 @@ async def update_progress(
|
||||
|
||||
@router.websocket("/ws/plugin-progress")
|
||||
async def websocket_plugin_progress(websocket: WebSocket, token: Optional[str] = Query(None)) -> None:
|
||||
"""旧版插件进度 WebSocket 入口。
|
||||
|
||||
Args:
|
||||
websocket: FastAPI WebSocket 对象。
|
||||
token: 可选的一次性握手 Token。
|
||||
"""
|
||||
is_authenticated = False
|
||||
|
||||
if token and verify_ws_token(token):
|
||||
@@ -105,17 +130,22 @@ async def websocket_plugin_progress(websocket: WebSocket, token: Optional[str] =
|
||||
data = await websocket.receive_text()
|
||||
if data == "ping":
|
||||
await websocket.send_text("pong")
|
||||
except Exception as e:
|
||||
logger.error(f"处理客户端消息时出错: {e}")
|
||||
except Exception as exc:
|
||||
logger.error(f"处理客户端消息时出错: {exc}")
|
||||
break
|
||||
|
||||
except WebSocketDisconnect:
|
||||
active_connections.discard(websocket)
|
||||
logger.info(f"📡 插件进度 WebSocket 客户端已断开,当前连接数: {len(active_connections)}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ WebSocket 错误: {e}")
|
||||
except Exception as exc:
|
||||
logger.error(f"❌ WebSocket 错误: {exc}")
|
||||
active_connections.discard(websocket)
|
||||
|
||||
|
||||
def get_progress_router() -> APIRouter:
|
||||
"""获取旧版插件进度路由对象。
|
||||
|
||||
Returns:
|
||||
APIRouter: 插件进度路由对象。
|
||||
"""
|
||||
return router
|
||||
|
||||
28
src/webui/routers/plugin/runtime_routes.py
Normal file
28
src/webui/routers/plugin/runtime_routes.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""插件运行时相关 WebUI 路由。"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Cookie
|
||||
|
||||
from src.plugin_runtime.component_query import component_query_service
|
||||
|
||||
from .schemas import HookSpecListResponse, HookSpecResponse
|
||||
from .support import require_plugin_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/runtime/hooks", response_model=HookSpecListResponse)
|
||||
async def list_runtime_hook_specs(maibot_session: Optional[str] = Cookie(None)) -> HookSpecListResponse:
|
||||
"""返回当前插件运行时公开的 Hook 规格清单。
|
||||
|
||||
Args:
|
||||
maibot_session: 当前 WebUI 会话令牌。
|
||||
|
||||
Returns:
|
||||
HookSpecListResponse: Hook 规格列表响应。
|
||||
"""
|
||||
|
||||
require_plugin_token(maibot_session)
|
||||
hooks = [HookSpecResponse(**hook_data) for hook_data in component_query_service.list_hook_specs()]
|
||||
return HookSpecListResponse(success=True, hooks=hooks)
|
||||
@@ -111,3 +111,19 @@ class UpdatePluginConfigRequest(BaseModel):
|
||||
|
||||
class UpdatePluginRawConfigRequest(BaseModel):
|
||||
config: str = Field(..., description="原始 TOML 配置内容")
|
||||
|
||||
|
||||
class HookSpecResponse(BaseModel):
|
||||
name: str = Field(..., description="Hook 名称")
|
||||
description: str = Field("", description="Hook 描述")
|
||||
parameters_schema: Dict[str, Any] = Field(default_factory=dict, description="Hook 参数模型")
|
||||
default_timeout_ms: int = Field(..., description="默认超时毫秒数")
|
||||
allow_blocking: bool = Field(..., description="是否允许 blocking 处理器")
|
||||
allow_observe: bool = Field(..., description="是否允许 observe 处理器")
|
||||
allow_abort: bool = Field(..., description="是否允许 abort")
|
||||
allow_kwargs_mutation: bool = Field(..., description="是否允许修改 kwargs")
|
||||
|
||||
|
||||
class HookSpecListResponse(BaseModel):
|
||||
success: bool = Field(..., description="是否成功")
|
||||
hooks: List[HookSpecResponse] = Field(default_factory=list, description="Hook 规格列表")
|
||||
|
||||
Reference in New Issue
Block a user