mirror of https://github.com/Mai-with-u/MaiBot.git
增加对插件配置的后端api获取
parent
c86c5dfd54
commit
a43f4067ff
|
|
@ -3,9 +3,13 @@ from pydantic import BaseModel, Field
|
|||
from typing import Optional, List, Dict, Any
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import toml
|
||||
from src.plugin_system.base.config_types import ConfigField
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import MMC_VERSION
|
||||
from src.plugin_system.core.dependency_manager import plugin_dependency_manager
|
||||
from src.plugin_system.core.plugin_manager import plugin_manager
|
||||
from .git_mirror_service import get_git_mirror_service, set_update_progress_callback
|
||||
from .token_manager import get_token_manager
|
||||
from .plugin_progress_ws import update_progress
|
||||
|
|
@ -164,6 +168,31 @@ class UpdatePluginRequest(BaseModel):
|
|||
mirror_id: Optional[str] = Field(None, description="指定镜像源 ID")
|
||||
|
||||
|
||||
class PluginConfigItem(BaseModel):
|
||||
"""插件配置项"""
|
||||
key: str = Field(..., description="配置键(section.key)")
|
||||
label: str = Field(..., description="显示标签")
|
||||
value: Any = Field(..., description="当前值")
|
||||
description: str = Field(..., description="描述")
|
||||
section: str = Field(..., description="所属节")
|
||||
required: bool = Field(..., description="是否必需")
|
||||
default: Any = Field(..., description="默认值")
|
||||
type: str = Field(..., description="字段类型: input, number, switch, select, array")
|
||||
options: Optional[List[Any]] = Field(None, description="可选值列表")
|
||||
|
||||
|
||||
class PluginConfigResponse(BaseModel):
|
||||
"""插件配置响应"""
|
||||
success: bool = Field(..., description="是否成功")
|
||||
data: List[PluginConfigItem] = Field(..., description="配置项列表")
|
||||
error: Optional[str] = Field(None, description="错误信息")
|
||||
|
||||
|
||||
class UpdatePluginConfigRequest(BaseModel):
|
||||
"""更新插件配置请求"""
|
||||
configs: Dict[str, Any] = Field(..., description="配置键值对")
|
||||
|
||||
|
||||
# ============ API 路由 ============
|
||||
|
||||
@router.get("/version", response_model=VersionResponse)
|
||||
|
|
@ -549,7 +578,7 @@ async def install_plugin(
|
|||
await update_progress(
|
||||
stage="error",
|
||||
progress=0,
|
||||
message=f"插件已存在",
|
||||
message="插件已存在",
|
||||
operation="install",
|
||||
plugin_id=request.plugin_id,
|
||||
error="插件已安装,请先卸载"
|
||||
|
|
@ -1147,17 +1176,32 @@ async def get_installed_plugins(
|
|||
logger.warning(f"插件 {plugin_id} 的 _manifest.json 格式无效,跳过")
|
||||
continue
|
||||
|
||||
# 读取 config.toml 获取启用状态
|
||||
config_path = plugin_path / "config.toml"
|
||||
is_enabled = False
|
||||
try:
|
||||
if config_path.exists():
|
||||
import toml
|
||||
config = toml.load(config_path)
|
||||
is_enabled = config.get("plugin", {}).get("enabled", False)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
is_running = plugin_manager.get_plugin_instance(plugin_id) is not None
|
||||
|
||||
# 添加到已安装列表(返回完整的 manifest 信息)
|
||||
installed_plugins.append({
|
||||
"id": plugin_id,
|
||||
"manifest": manifest, # 返回完整的 manifest 对象
|
||||
"path": str(plugin_path.absolute())
|
||||
"path": str(plugin_path.absolute()),
|
||||
"enabled": is_enabled,
|
||||
"running": is_running
|
||||
})
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"插件 {plugin_id} 的 _manifest.json 解析失败: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.error(f"读取插件 {plugin_id} 信息时出错: {e}")
|
||||
continue
|
||||
|
||||
|
|
@ -1172,3 +1216,205 @@ async def get_installed_plugins(
|
|||
except Exception as e:
|
||||
logger.error(f"获取已安装插件列表失败: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
|
||||
|
||||
|
||||
@router.get("/{plugin_id}/config", response_model=PluginConfigResponse)
|
||||
async def get_plugin_config(
|
||||
plugin_id: str,
|
||||
authorization: Optional[str] = Header(None)
|
||||
) -> PluginConfigResponse:
|
||||
"""
|
||||
获取插件配置表单定义和当前值
|
||||
"""
|
||||
# Token 验证
|
||||
token = authorization.replace("Bearer ", "") if authorization else None
|
||||
token_manager = get_token_manager()
|
||||
if not token or not token_manager.verify_token(token):
|
||||
raise HTTPException(status_code=401, detail="未授权:无效的访问令牌")
|
||||
|
||||
plugin = plugin_manager.get_plugin_instance(plugin_id)
|
||||
|
||||
schema = None
|
||||
current_config = None
|
||||
|
||||
# 1. 尝试从已加载的插件获取
|
||||
if plugin:
|
||||
schema = plugin.config_schema
|
||||
current_config = plugin.config
|
||||
else:
|
||||
# 2. 尝试从已注册的类中加载(针对已禁用或加载失败的插件)
|
||||
logger.info(f"插件 {plugin_id} 未运行,尝试静态加载配置")
|
||||
|
||||
plugin_class = plugin_manager.plugin_classes.get(plugin_id)
|
||||
plugin_path = plugin_manager.plugin_paths.get(plugin_id)
|
||||
|
||||
# 如果没有路径信息,尝试推断标准路径
|
||||
if not plugin_path:
|
||||
possible_path = Path("plugins") / plugin_id
|
||||
if possible_path.exists():
|
||||
plugin_path = str(possible_path)
|
||||
|
||||
if plugin_class and plugin_path:
|
||||
try:
|
||||
# 临时实例化,仅用于获取 schema 和 config
|
||||
# 注意:这里可能会触发 __init__ 中的逻辑
|
||||
temp_plugin = plugin_class(plugin_dir=plugin_path)
|
||||
schema = temp_plugin.config_schema
|
||||
current_config = temp_plugin.config
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.warning(f"临时实例化插件 {plugin_id} 失败: {e}")
|
||||
|
||||
# 3. 如果还是没有 schema,尝试直接读取 config.toml 并推断
|
||||
if schema is None and plugin_path:
|
||||
config_path = Path(plugin_path) / "config.toml"
|
||||
if config_path.exists():
|
||||
try:
|
||||
|
||||
current_config = toml.load(config_path)
|
||||
# 根据 config 生成简单的 schema
|
||||
schema = {}
|
||||
for section, values in current_config.items():
|
||||
if isinstance(values, dict):
|
||||
schema[section] = {}
|
||||
for key, value in values.items():
|
||||
# 构造一个伪造的 ConfigField
|
||||
schema[section][key] = ConfigField(
|
||||
type=type(value),
|
||||
default=value,
|
||||
description=f"自动推断字段: {key}",
|
||||
required=False
|
||||
)
|
||||
logger.info(f"已从 config.toml 推断出插件 {plugin_id} 的配置结构")
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.error(f"直接读取配置文件失败: {e}")
|
||||
|
||||
if schema is None:
|
||||
raise HTTPException(status_code=404, detail=f"无法加载插件 {plugin_id} 的配置:插件未运行且无法读取配置文件")
|
||||
|
||||
try:
|
||||
items = []
|
||||
for section, fields in schema.items():
|
||||
if not isinstance(fields, dict):
|
||||
continue
|
||||
|
||||
for field_name, field_info in fields.items():
|
||||
# 获取当前值
|
||||
# 注意:current_config 结构是 {section: {key: value}}
|
||||
section_config = current_config.get(section, {}) if current_config else {}
|
||||
value = section_config.get(field_name, field_info.default)
|
||||
|
||||
# 确定前端控件类型
|
||||
field_type = "input"
|
||||
if field_info.choices:
|
||||
field_type = "select"
|
||||
elif field_info.type is bool:
|
||||
field_type = "switch"
|
||||
elif field_info.type in (int, float):
|
||||
field_type = "number"
|
||||
elif field_info.type is list:
|
||||
field_type = "array"
|
||||
|
||||
items.append(PluginConfigItem(
|
||||
key=f"{section}.{field_name}",
|
||||
label=field_name,
|
||||
value=value,
|
||||
description=field_info.description,
|
||||
section=section,
|
||||
required=field_info.required,
|
||||
default=field_info.default,
|
||||
type=field_type,
|
||||
options=field_info.choices
|
||||
))
|
||||
|
||||
return PluginConfigResponse(success=True, data=items)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取插件配置失败: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"获取配置失败: {str(e)}") from e
|
||||
|
||||
|
||||
@router.post("/{plugin_id}/config")
|
||||
async def update_plugin_config(
|
||||
plugin_id: str,
|
||||
request: UpdatePluginConfigRequest,
|
||||
authorization: Optional[str] = Header(None)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
更新插件配置
|
||||
"""
|
||||
# Token 验证
|
||||
token = authorization.replace("Bearer ", "") if authorization else None
|
||||
token_manager = get_token_manager()
|
||||
if not token or not token_manager.verify_token(token):
|
||||
raise HTTPException(status_code=401, detail="未授权:无效的访问令牌")
|
||||
|
||||
plugin = plugin_manager.get_plugin_instance(plugin_id)
|
||||
|
||||
# 准备配置更新逻辑
|
||||
config_file_name = "config.toml" # 默认值
|
||||
plugin_dir = None
|
||||
current_config = {}
|
||||
|
||||
if plugin:
|
||||
plugin_dir = plugin.plugin_dir
|
||||
config_file_name = plugin.config_file_name or "config.toml"
|
||||
current_config = plugin.config
|
||||
else:
|
||||
# 插件未加载,尝试查找路径
|
||||
plugin_path = plugin_manager.plugin_paths.get(plugin_id)
|
||||
if not plugin_path:
|
||||
possible_path = Path("plugins") / plugin_id
|
||||
if possible_path.exists():
|
||||
plugin_path = str(possible_path)
|
||||
|
||||
if not plugin_path:
|
||||
raise HTTPException(status_code=404, detail=f"找不到插件 {plugin_id} 的目录")
|
||||
|
||||
plugin_dir = plugin_path
|
||||
|
||||
# 尝试读取现有配置以进行合并
|
||||
try:
|
||||
|
||||
config_path = os.path.join(plugin_dir, config_file_name)
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
current_config = toml.load(f)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
try:
|
||||
|
||||
|
||||
# 1. 解析扁平化的配置键值对到嵌套字典
|
||||
# 前端传来的格式: {"section.key": value}
|
||||
# 目标格式: {"section": {"key": value}}
|
||||
new_config = current_config.copy()
|
||||
|
||||
for key, value in request.configs.items():
|
||||
if "." in key:
|
||||
section, field_name = key.split(".", 1)
|
||||
if section not in new_config:
|
||||
new_config[section] = {}
|
||||
|
||||
# 类型转换(如果需要)
|
||||
# 这里假设前端传来的类型已经是正确的,或者在保存时会自动处理
|
||||
new_config[section][field_name] = value
|
||||
|
||||
# 2. 保存到文件
|
||||
config_path = os.path.join(plugin_dir, config_file_name)
|
||||
|
||||
# 如果插件已加载,使用插件的方法保存
|
||||
if plugin:
|
||||
plugin._save_config_to_file(new_config, config_path)
|
||||
# 更新内存中的配置
|
||||
plugin.config = new_config
|
||||
else:
|
||||
# 否则直接使用 toml 库保存
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
toml.dump(new_config, f)
|
||||
|
||||
return {"success": True, "message": "配置已更新"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新插件配置失败: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"更新配置失败: {str(e)}") from e
|
||||
|
|
|
|||
Loading…
Reference in New Issue