message / app /plugins /models.py
hunian
feat: 重构插件平台后端运行时和状态契约
10b8d56
Raw
History Blame Contribute Delete
17.7 kB
"""
插件数据模型
定义插件元数据规范和数据类
"""
from typing import Dict, List, Optional, Any
from enum import Enum
from pydantic import BaseModel, Field, field_validator, model_validator, ValidationError
from datetime import datetime
class PluginStatus(str, Enum):
"""插件状态枚举
状态流转:
- discovered: 启动扫描发现,未加载
- loaded: 加载完成,等待启用
- enabled: 用户启用,可用
- disabled: 用户禁用
- error: 加载/运行错误
- dependency_error: 依赖缺失或版本不匹配
- incomplete: 元数据不完整(缺分类、能力标签等)
"""
# 保留旧状态兼容
INSTALLED = "installed"
INSTALLING = "installing"
UNINSTALLING = "uninstalling"
# 新状态
DISCOVERED = "discovered"
LOADED = "loaded"
ENABLED = "enabled"
DISABLED = "disabled"
ERROR = "error"
DEPENDENCY_ERROR = "dependency_error"
INCOMPLETE = "incomplete"
class PluginDependency(BaseModel):
"""插件依赖"""
name: str
version: str
optional: bool = False
class PluginMetadata(BaseModel):
"""插件元数据 - plugin.json 结构"""
name: str = Field(..., description="插件唯一标识符")
version: str = Field(..., description="插件版本")
description: str = Field(..., description="插件描述")
author: str = Field(..., description="插件作者")
# 可选字段
homepage: Optional[str] = Field(None, description="插件主页URL")
ui_path: Optional[str] = Field(None, description="插件UI路径")
# 依赖和配置
requirements: List[str] = Field(default_factory=list, description="Python依赖")
dependencies: List[PluginDependency] = Field(
default_factory=list, description="插件依赖"
)
config_schema: Optional[Dict[str, Any]] = Field(None, description="配置模式")
@field_validator("name")
@classmethod
def validate_name(cls, v):
"""验证插件名称"""
if not v.replace("_", "").isalnum():
raise ValueError("插件名称只能包含字母、数字和下划线")
return v.lower()
@field_validator("version")
@classmethod
def validate_version(cls, v):
"""验证版本格式"""
# 简单的版本格式验证
if not all(part.isdigit() for part in v.split(".")[:3]):
raise ValueError("版本号格式应为 X.Y.Z")
return v
def __init__(self, **data):
"""初始化 - 自动映射 homepage 到 ui_path"""
# 如果没有 ui_path 但有 homepage,使用 homepage 作为 ui_path
if "ui_path" not in data and "homepage" in data:
data["ui_path"] = data["homepage"]
super().__init__(**data)
class PluginInfo(BaseModel):
"""插件完整信息"""
# 元数据
metadata: PluginMetadata
# 运行时信息
status: PluginStatus = PluginStatus.INSTALLED
install_path: str
install_time: datetime = Field(default_factory=datetime.now)
# 功能端点
api_routes: List[str] = Field(default_factory=list)
ui_routes: List[str] = Field(default_factory=list)
# 错误信息
error_message: Optional[str] = None
last_error: Optional[str] = None
class Config:
json_encoders = {datetime: lambda v: v.isoformat()}
class PluginConfig(BaseModel):
"""插件配置"""
enabled: bool = False
settings: Dict[str, Any] = Field(default_factory=dict)
class PluginOperationResult(BaseModel):
"""插件操作结果"""
success: bool
message: str
plugin_name: Optional[str] = None
error: Optional[str] = None
plugin_info: Optional[Dict[str, Any]] = None
@classmethod
def success_result(cls, plugin_name: str, message: str) -> "PluginOperationResult":
return cls(success=True, plugin_name=plugin_name, message=message)
@classmethod
def error_result(cls, plugin_name: str, error: str) -> "PluginOperationResult":
return cls(
success=False, plugin_name=plugin_name, message="操作失败", error=error
)
# ============================================================
# 新增:运行时状态和错误模型
# ============================================================
class PluginErrorStage(str, Enum):
"""错误发生阶段"""
STRUCTURE = "structure" # plugin.json 结构错误
DEPENDENCY = "dependency" # 依赖检查失败
LOAD = "load" # main.py/api.py/mcp.py 导入失败
RUNTIME = "runtime" # 运行时错误
MCP = "mcp" # MCP 工具调用错误
class PluginErrorInfo(BaseModel):
"""插件错误信息
用于记录单个插件的结构化错误,支持页面展示和诊断。
"""
error_code: str = Field(..., description="错误代码,如 MISSING_MAIN_PY")
message: str = Field(..., description="用户可读错误消息")
stage: PluginErrorStage = Field(..., description="错误发生阶段")
detail: Optional[str] = Field(None, description="技术细节,用于调试")
occurred_at: datetime = Field(default_factory=datetime.now, description="发生时间")
class DependencyCheckStatus(str, Enum):
"""依赖检查状态"""
SATISFIED = "satisfied" # 依赖满足
MISSING = "missing" # 包缺失
VERSION_MISMATCH = "version_mismatch" # 版本不匹配
CHECK_FAILED = "check_failed" # 检查过程失败
class DependencyItem(BaseModel):
"""单个依赖项检查结果"""
name: str = Field(..., description="包名")
required: Optional[str] = Field(None, description="要求版本,如 >=1.0.0")
installed: Optional[str] = Field(None, description="已安装版本")
status: DependencyCheckStatus = Field(..., description="检查状态")
message: Optional[str] = Field(None, description="状态说明")
class DependencyCheckResult(BaseModel):
"""依赖检查结果"""
plugin_name: str = Field(..., description="插件名")
overall_status: DependencyCheckStatus = Field(..., description="整体状态")
items: List[DependencyItem] = Field(default_factory=list, description="依赖项列表")
missing_count: int = Field(0, description="缺失数量")
mismatch_count: int = Field(0, description="版本不匹配数量")
checked_at: datetime = Field(default_factory=datetime.now, description="检查时间")
# ============================================================
# 新增:治理信息模型
# ============================================================
class PluginCategory(str, Enum):
"""插件分类"""
CONTENT_EXTRACTION = "content_extraction" # 内容提取
BROWSER = "browser" # 浏览器自动化
SANDBOX = "sandbox" # 沙盒执行
UTILITY = "utility" # 工具类
OTHER = "other" # 其他
class PluginGovernanceInfo(BaseModel):
"""插件治理信息
用于插件中心展示分类、能力标签、依赖风险和处理决策。
"""
category: PluginCategory = Field(
default=PluginCategory.OTHER, description="插件分类"
)
capabilities: List[str] = Field(default_factory=list, description="能力标签列表")
has_mcp_tools: bool = Field(False, description="是否声明 MCP 工具")
has_api_routes: bool = Field(False, description="是否提供 API 路由")
has_ui: bool = Field(False, description="是否有前端 UI")
dependency_risk: Optional[str] = Field(None, description="依赖风险说明")
handling_decision: Optional[str] = Field(None, description="处理决策说明")
# ============================================================
# 新增:Tool 和 MCP 摘要模型
# ============================================================
class PluginToolSummary(BaseModel):
"""插件 Tool 摘要
用于插件列表和详情页展示 Tool 状态。
"""
total: int = Field(0, description="Tool 总数")
available: int = Field(0, description="可用数量")
incomplete: int = Field(0, description="元数据不完整数量")
class PluginMcpSummary(BaseModel):
"""插件 MCP 摘要
用于插件列表和详情页展示 MCP 工具状态。
"""
total: int = Field(0, description="MCP 工具总数")
available: int = Field(0, description="可用数量")
unavailable: int = Field(0, description="不可用数量")
# ============================================================
# 新增:UI 入口模型
# ============================================================
class PluginUIType(str, Enum):
"""插件 UI 类型"""
STATIC = "static" # 静态 UI,后端挂载
SCHEMA = "schema" # schema 表单,平台渲染
NONE = "none" # 无 UI
class UISubmitContract(BaseModel):
"""Schema UI 提交契约
定义前端表单提交的目标和方式。
"""
method: str = Field(
default="POST", description="HTTP 方法,只允许 POST"
)
path: str = Field(..., description="提交路径,如 /plugins/{name}/api/run")
content_type: str = Field(
default="application/json", description="请求内容类型"
)
success_path: Optional[str] = Field(
None, description="成功响应结果的 JSON 路径"
)
error_path: Optional[str] = Field(
None, description="错误响应消息的 JSON 路径"
)
@field_validator("method")
@classmethod
def validate_method(cls, v):
"""只允许 POST 方法"""
if v.upper() != "POST":
raise ValueError("submit.method 只允许 POST")
return v.upper()
@field_validator("path")
@classmethod
def validate_path(cls, v):
"""路径必须是相对路径"""
if v.startswith("http://") or v.startswith("https://"):
raise ValueError("submit.path 不能是外部 URL")
if not v.startswith("/"):
raise ValueError("submit.path 必须以 / 开头")
return v
class UIFieldDefinition(BaseModel):
"""Schema UI 字段定义"""
name: str = Field(..., description="字段名")
label: str = Field(..., description="显示标签")
type: str = Field(default="string", description="字段类型:string/number/boolean/select")
required: bool = Field(default=False, description="是否必填")
default: Optional[Any] = Field(None, description="默认值")
placeholder: Optional[str] = Field(None, description="占位提示")
help_text: Optional[str] = Field(None, description="帮助文本")
validation: Optional[Dict[str, Any]] = Field(None, description="验证规则")
class UISchemaDefinition(BaseModel):
"""Schema UI 定义"""
title: str = Field(..., description="表单标题")
description: Optional[str] = Field(None, description="表单描述")
fields: List[UIFieldDefinition] = Field(
default_factory=list, description="字段列表"
)
result_schema: Optional[Dict[str, Any]] = Field(
None, description="结果展示 schema"
)
class PluginUIEntry(BaseModel):
"""插件 UI 入口
定义插件运行页的 UI 类型和入口信息。
"""
type: PluginUIType = Field(..., description="UI 类型")
entry_path: Optional[str] = Field(None, description="静态 UI 路径,type=static 时必填")
schema: Optional[UISchemaDefinition] = Field(
None, description="Schema UI 定义,type=schema 时必填"
)
submit: Optional[UISubmitContract] = Field(
None, description="提交契约,type=schema 时必填"
)
unavailable_reason: Optional[str] = Field(
None, description="UI 不可用原因"
)
@field_validator("entry_path")
@classmethod
def validate_entry_path(cls, v, info):
"""静态 UI 路径校验"""
# Pydantic V2 中 info.data 可能不包含 type 字段,使用 model_validator 替代
if v and (".." in v or v.startswith("http")):
raise ValueError("entry_path 不允许路径穿越或外部 URL")
return v
@model_validator(mode="after")
def validate_static_ui_entry_path(self):
"""验证 type=static 时 entry_path 必填"""
if self.type == PluginUIType.STATIC and not self.entry_path:
raise ValueError("type=static 时 entry_path 必填")
return self
# ============================================================
# 新增:状态文件模型
# ============================================================
class PluginStateEntry(BaseModel):
"""插件状态文件中的单条记录
保存用户启停状态,与 plugin.json 的 enabled 字段独立。
"""
enabled: bool = Field(False, description="是否启用")
updated_at: datetime = Field(
default_factory=datetime.now, description="最后更新时间"
)
updated_by: str = Field(
default="system", description="更新者:system/user"
)
class PluginStateStore(BaseModel):
"""插件状态文件模型
事实源:data/plugin_state.json
以插件名为 key 保存用户启停状态。
"""
plugins: Dict[str, PluginStateEntry] = Field(
default_factory=dict, description="插件状态字典"
)
version: int = Field(default=1, description="文件版本号")
# ============================================================
# 新增:插件详情和列表项模型
# ============================================================
class PluginListItem(BaseModel):
"""插件列表项
用于 GET /api/plugins/ 响应。
"""
metadata: PluginMetadata = Field(..., description="插件元数据")
status: PluginStatus = Field(..., description="插件状态")
dependency_summary: Optional[DependencyCheckResult] = Field(
None, description="依赖检查摘要"
)
governance_summary: Optional[PluginGovernanceInfo] = Field(
None, description="治理信息摘要"
)
tool_summary: PluginToolSummary = Field(
default_factory=PluginToolSummary, description="Tool 摘要"
)
mcp_summary: PluginMcpSummary = Field(
default_factory=PluginMcpSummary, description="MCP 摘要"
)
ui_entry: Optional[PluginUIEntry] = Field(None, description="UI 入口")
errors: List[PluginErrorInfo] = Field(
default_factory=list, description="错误列表"
)
class PluginDetail(BaseModel):
"""插件详情
用于 GET /api/plugins/{plugin_name} 响应。
"""
metadata: PluginMetadata = Field(..., description="插件元数据")
status: PluginStatus = Field(..., description="插件状态")
install_path: str = Field(..., description="安装路径")
install_time: Optional[datetime] = Field(None, description="安装时间")
# 依赖详情
dependency_check: Optional[DependencyCheckResult] = Field(
None, description="依赖检查详情"
)
# 治理信息
governance: Optional[PluginGovernanceInfo] = Field(
None, description="治理信息"
)
# API 路由
api_routes: List[str] = Field(default_factory=list, description="API 路由列表")
# Tool 列表
tools: List[Dict[str, Any]] = Field(
default_factory=list, description="Tool 详情列表"
)
# MCP 工具
mcp_tools: List[Dict[str, Any]] = Field(
default_factory=list, description="MCP 工具列表"
)
# UI 入口
ui_entry: Optional[PluginUIEntry] = Field(None, description="UI 入口")
# 错误信息
errors: List[PluginErrorInfo] = Field(
default_factory=list, description="错误列表"
)
class Config:
json_encoders = {datetime: lambda v: v.isoformat()}
# ============================================================
# 新增:Tool 目录模型
# ============================================================
class ToolAvailability(str, Enum):
"""Tool 可用状态"""
AVAILABLE = "available" # 可用
UNAVAILABLE = "unavailable" # 不可用(插件禁用/依赖错误等)
INCOMPLETE = "incomplete" # 元数据不完整
class ToolCatalogItem(BaseModel):
"""Tool 目录项
用于 GET /api/tools 和 GET /api/tools/{tool_name} 响应。
包含中文名称、用途、参数、示例、错误说明和可用状态。
"""
name: str = Field(..., description="工具标识符")
display_name: str = Field(..., description="中文显示名称")
source: str = Field(..., description="来源:plugin/platform")
plugin_name: str = Field(..., description="所属插件名")
purpose: str = Field(..., description="用途说明")
applies_to: List[str] = Field(
default_factory=list, description="适用场景列表"
)
not_for: List[str] = Field(
default_factory=list, description="不适用场景列表"
)
parameters: Dict[str, Any] = Field(
default_factory=dict, description="参数 schema"
)
input_examples: List[Dict[str, Any]] = Field(
default_factory=list, description="输入示例"
)
output_examples: List[Dict[str, Any]] = Field(
default_factory=list, description="输出示例"
)
common_errors: List[Dict[str, str]] = Field(
default_factory=list, description="常见错误列表"
)
risk_level: str = Field(
default="low", description="风险等级:low/medium/high"
)
availability: ToolAvailability = Field(
default=ToolAvailability.AVAILABLE, description="可用状态"
)
unavailable_reason: Optional[str] = Field(
None, description="不可用原因"
)
incomplete_reasons: Optional[List[str]] = Field(
None, description="元数据不完整原因列表"
)