File size: 17,652 Bytes
b6db694 454f1d5 b6db694 10b8d56 b6db694 454f1d5 b6db694 10b8d56 454f1d5 10b8d56 b6db694 10b8d56 b6db694 10b8d56 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 27beae4 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 b6db694 454f1d5 10b8d56 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 | """
插件数据模型
定义插件元数据规范和数据类
"""
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="元数据不完整原因列表"
) |