""" 插件数据模型 定义插件元数据规范和数据类 """ 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="元数据不完整原因列表" )