mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 04:47:03 +08:00
830 lines
34 KiB
Python
830 lines
34 KiB
Python
"""
|
||
插件配置、运行状态、来源身份及前端扩展的数据结构。
|
||
"""
|
||
|
||
from datetime import datetime as _datetime
|
||
from enum import Enum as _Enum
|
||
from typing import Annotated as _Annotated
|
||
from typing import Dict, List, Literal, Optional, Union
|
||
|
||
from pydantic import AfterValidator as _AfterValidator
|
||
from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator
|
||
from pydantic import BeforeValidator as _BeforeValidator
|
||
from pydantic import PrivateAttr as _PrivateAttr
|
||
from pydantic import computed_field as _computed_field
|
||
|
||
from app.domain.plugin import is_physical_plugin_id as _is_physical_plugin_id
|
||
from app.domain.plugin import normalize_plugin_documentation_url as _normalize_plugin_documentation_url
|
||
from app.schemas.common import JsonData
|
||
|
||
|
||
class PluginRuntimeStatus(str, _Enum):
|
||
"""
|
||
插件从来源同步、依赖准备到运行激活的状态。
|
||
"""
|
||
|
||
# 插件来源同步失败
|
||
SYNC_FAILED = "sync_failed"
|
||
# 未找到插件源码
|
||
SOURCE_MISSING = "source_missing"
|
||
# 正在等待依赖准备完成
|
||
DEPENDENCY_PENDING = "dependency_pending"
|
||
# 已具备加载条件
|
||
READY = "ready"
|
||
# 插件已加载并运行
|
||
ACTIVE = "active"
|
||
# 被运行策略阻止加载
|
||
BLOCKED_BY_POLICY = "blocked_by_policy"
|
||
# 插件加载失败
|
||
LOAD_FAILED = "load_failed"
|
||
# 插件自身声明与当前解释器运行时不兼容(当前只有 free-threaded 一个维度)
|
||
INCOMPATIBLE_RUNTIME = "incompatible_runtime"
|
||
|
||
|
||
class PluginSourceBindingStatus(str, _Enum):
|
||
"""
|
||
已安装插件的在线更新仓库绑定状态。
|
||
"""
|
||
|
||
# 已绑定在线更新来源
|
||
BOUND = "bound"
|
||
# 需要选择并绑定更新来源
|
||
BINDING_REQUIRED = "binding_required"
|
||
# 仅使用本地源码,不绑定在线来源
|
||
LOCAL_ONLY = "local_only"
|
||
|
||
|
||
class PluginUpdateCandidate(BaseModel):
|
||
"""
|
||
插件市场为已安装插件选择的当前更新候选。
|
||
"""
|
||
|
||
source_type: Literal["official", "third_party"] = Field(description="候选仓库是官方来源还是第三方来源")
|
||
source_key: str = Field(description="候选仓库的规范来源键")
|
||
repo_url: str = Field(description="候选仓库的公开 GitHub 地址")
|
||
version: str = Field(description="候选仓库当前可安装版本")
|
||
is_bound: bool = Field(description="候选仓库是否为插件当前已绑定仓库")
|
||
|
||
|
||
def validate_plugin_id(value: str) -> str:
|
||
"""
|
||
限制插件实例标识为可安全用作 Python 类名和路由段的格式。
|
||
|
||
公开可调用:凡是把实例 ID 拼进文件系统路径、模块名或路由段的入口都应当先过这道
|
||
校验,而不是各自另写一套字符白名单——多套规则之间迟早出现缝隙。
|
||
实例与源插件复用安装侧的 ASCII 身份规则,兼容既有下划线 ID;不改写大小写,
|
||
以免改变配置、数据和路由命名空间。
|
||
:param value: 待校验的插件实例 ID
|
||
:return: 原样返回的合法 ID
|
||
:raise ValueError: ID 为空、不以字母开头、含 ASCII 字母数字及下划线以外的字符或超长
|
||
"""
|
||
if len(value) > 128:
|
||
raise ValueError("插件 ID 长度不能超过 128 个字符")
|
||
if not _is_physical_plugin_id(value):
|
||
raise ValueError("插件 ID 必须以字母开头且只能包含 ASCII 字母、数字或下划线")
|
||
return value
|
||
|
||
|
||
_PluginId = _Annotated[str, _AfterValidator(validate_plugin_id)]
|
||
|
||
|
||
class PluginInstance(BaseModel):
|
||
"""
|
||
持久化一个共享源码插件的独立运行实例。
|
||
"""
|
||
|
||
instance_id: _PluginId = Field(description="运行实例 ID,也是配置、数据和路由命名空间")
|
||
source_plugin_id: _PluginId = Field(description="提供代码与前端资源的源插件 ID")
|
||
plugin_name: Optional[str] = Field(default=None, description="实例展示名称")
|
||
plugin_desc: Optional[str] = Field(default=None, description="实例展示描述")
|
||
plugin_icon: Optional[str] = Field(default=None, description="实例展示图标")
|
||
is_default_target: bool = Field(
|
||
default=False,
|
||
description="该实例是否为所属源插件的默认调用目标",
|
||
)
|
||
# 默认为真而库列默认为假:这个模型是运行时描述,被构造出来就是要拿去装载的;
|
||
# 库列默认为假则是为了让「只写了配置」这类隐式建出的行不因此变成可装载
|
||
is_enabled: bool = Field(
|
||
default=True,
|
||
description="这份配置是否应当被实例化并启动;置假即停用,配置与展示信息留存待再次启用",
|
||
)
|
||
|
||
@property
|
||
def is_host(self) -> bool:
|
||
"""
|
||
该实例是否为源插件本体自身,而非共享其源码的分身。
|
||
"""
|
||
return self.instance_id == self.source_plugin_id
|
||
|
||
# mypy 尚不识别 computed_field 与 property 叠加时的装饰器顺序。
|
||
@_computed_field( # type: ignore[prop-decorator]
|
||
description="实例实现模式:virtual 为共享源码的分身,host 为源插件本体自身",
|
||
)
|
||
@property
|
||
def mode(self) -> Literal["virtual", "host"]:
|
||
"""
|
||
由一对身份 ID 派生实例角色,而非另存一份可能失步的副本。
|
||
"""
|
||
return "host" if self.is_host else "virtual"
|
||
|
||
|
||
class PluginInstanceEnabledRequest(BaseModel):
|
||
"""
|
||
启用或停用一个插件实例的请求参数。
|
||
"""
|
||
|
||
enabled: bool = Field(
|
||
description="目标启用状态;置假即停用,业务参数与展示信息原样留存等待再次启用"
|
||
)
|
||
|
||
|
||
class PluginInstancePurgeRequest(BaseModel):
|
||
"""
|
||
彻底清理一个插件实例时选定的删除范围。
|
||
|
||
各项默认为假:清理不可逆,漏选一项只是少删了东西,多选一项则可能毁掉用户特意
|
||
保留的数据,因而由调用方逐项明确给出,服务端不替它补默认值。
|
||
"""
|
||
|
||
config: bool = Field(default=False, description="是否删除该实例的业务参数")
|
||
plugin_data: bool = Field(default=False, description="是否删除该实例在插件数据表中的行")
|
||
own_database: bool = Field(default=False, description="是否销毁该实例的自有数据库")
|
||
data_directory: bool = Field(
|
||
default=False,
|
||
description="是否删除该实例在插件数据目录下的整个目录;选中时自有数据库必然一并销毁",
|
||
)
|
||
|
||
|
||
class PluginInstancePurgeOutcome(BaseModel):
|
||
"""
|
||
彻底清理的执行结果。
|
||
"""
|
||
|
||
purged: List[str] = Field(default_factory=list, description="实际清掉的范围标识")
|
||
instance_removed: bool = Field(
|
||
default=False,
|
||
description="实例行是否随之删除;分身会删,本体保留——它还承载着该插件的展示与启用状态",
|
||
)
|
||
|
||
|
||
class PluginInstanceLogLevel(BaseModel):
|
||
"""
|
||
单个实例的日志等级设置与生效结果。
|
||
"""
|
||
|
||
instance_id: str = Field(description="实例 ID")
|
||
configured_level: Optional[str] = Field(default=None, description="该实例设置的日志等级覆盖,None 表示未设置或已过期")
|
||
expires_at: Optional[_datetime] = Field(default=None, description="日志等级覆盖的失效时间,None 表示不过期")
|
||
effective_level: str = Field(description="按过期回落判定后实际生效的日志等级")
|
||
|
||
|
||
class PluginInstanceLogLevelOverview(BaseModel):
|
||
"""
|
||
插件全部实例(含本体)的日志等级设置总览。
|
||
"""
|
||
|
||
plugin_id: str = Field(description="插件 ID")
|
||
instances: List[PluginInstanceLogLevel] = Field(
|
||
default_factory=list, description="该插件全部实例的日志等级设置,首项固定是本体自身"
|
||
)
|
||
|
||
|
||
class PluginInstanceLogLevelUpdateRequest(BaseModel):
|
||
"""
|
||
设置实例日志等级覆盖的请求参数。
|
||
"""
|
||
|
||
level: str = Field(description="目标日志等级,如 DEBUG、INFO、WARNING、ERROR、CRITICAL")
|
||
expires_at: Optional[_datetime] = Field(
|
||
default=None,
|
||
description="覆盖失效时间,None 表示不过期;不带时区时按 UTC 解读",
|
||
)
|
||
|
||
|
||
class Plugin(BaseModel):
|
||
"""
|
||
插件信息。
|
||
|
||
:ivar _package_version: 本地安装包版本的缓存值
|
||
"""
|
||
|
||
_package_version: Optional[str] = _PrivateAttr(default=None)
|
||
|
||
id: Optional[str] = Field(default=None, description="记录标识")
|
||
plugin_name: Optional[str] = Field(default=None, description="插件名称")
|
||
plugin_desc: Optional[str] = Field(default=None, description="插件描述")
|
||
plugin_icon: Optional[str] = Field(default=None, description="插件图标")
|
||
plugin_version: Optional[str] = Field(default=None, description="插件版本")
|
||
plugin_label: Optional[str] = Field(default=None, description="插件标签")
|
||
plugin_author: Optional[str] = Field(default=None, description="插件作者")
|
||
author_url: Optional[str] = Field(default=None, description="作者主页")
|
||
plugin_readme: Optional[str] = Field(default=None, description="插件使用说明的 HTTP(S) 外链;未声明时为空")
|
||
plugin_config_prefix: Optional[str] = Field(default=None, description="插件配置项ID前缀")
|
||
plugin_order: Optional[int] = Field(default=0, description="加载顺序")
|
||
auth_level: Optional[int] = Field(default=0, description="可使用的用户级别")
|
||
installed: Optional[bool] = Field(default=False, description="是否已安装")
|
||
state: Optional[bool] = Field(default=False, description="运行状态")
|
||
runtime_status: Optional[PluginRuntimeStatus] = Field(default=None, description="插件源码、依赖和运行时加载状态")
|
||
has_page: Optional[bool] = Field(default=False, description="是否有详情页面")
|
||
has_update: Optional[bool] = Field(default=False, description="是否有新版本")
|
||
update_candidate: Optional[PluginUpdateCandidate] = Field(default=None, description="当前市场选择的更新候选;绑定仓库可更新时优先返回该仓库")
|
||
source_binding_status: PluginSourceBindingStatus = Field(
|
||
default=PluginSourceBindingStatus.BOUND,
|
||
description="插件仓库绑定状态;仅已安装物理插件由后端投影真实身份",
|
||
)
|
||
system_version_compatible: Optional[bool] = Field(default=True, description="主系统版本是否兼容")
|
||
system_version_message: Optional[str] = Field(default=None, description="主系统版本兼容提示")
|
||
system_version: Optional[str] = Field(default=None, description="主系统版本限定范围")
|
||
runtime_compatible: Optional[bool] = Field(
|
||
default=True,
|
||
description="插件声明与当前解释器运行时是否兼容;free-threaded(v3t)下显式声明不支持时为 False",
|
||
)
|
||
runtime_message: Optional[str] = Field(default=None, description="运行时不兼容的用户可读原因")
|
||
release: Optional[bool] = Field(default=False, description="是否声明支持通过 GitHub Release 资产安装")
|
||
is_local: Optional[bool] = Field(default=False, description="是否本地")
|
||
repo_url: Optional[str] = Field(default=None, description="仓库地址")
|
||
install_count: Optional[int] = Field(default=0, description="安装次数")
|
||
history: Optional[dict[str, str]] = Field(default_factory=dict, description="更新记录")
|
||
add_time: Optional[int] = Field(default=0, description="添加时间,值越小表示越靠后发布")
|
||
plugin_public_key: Optional[str] = Field(default=None, description="插件公钥")
|
||
source_plugin_id: Optional[str] = Field(default=None, description="共享代码与前端资源的源插件 ID;普通插件为空")
|
||
is_instance: Optional[bool] = Field(default=False, description="是否为共享源码的虚拟实例")
|
||
instance_mode: Optional[str] = Field(default=None, description="实例实现模式;存量物理分身为空")
|
||
is_default_target: Optional[bool] = Field(default=False, description="该实例是否为所属源插件的默认调用目标")
|
||
is_enabled: Optional[bool] = Field(default=True, description="该实例是否应当被实例化并启动;与 state 不同,后者说的是此刻在不在跑")
|
||
log_level_effective: Optional[str] = Field(default=None, description="该实例当前生效的日志等级覆盖;未设置覆盖或覆盖已过期回落全局等级时为空")
|
||
|
||
@field_validator("plugin_readme", mode="before")
|
||
@classmethod
|
||
def validate_plugin_readme(cls, value: object) -> Optional[str]:
|
||
"""
|
||
丢弃不可作为浏览器外链的文档地址,保持存量插件元数据兼容。
|
||
"""
|
||
return _normalize_plugin_documentation_url(value)
|
||
|
||
@property
|
||
def package_version(self) -> Optional[str]:
|
||
"""
|
||
读取仅供宿主内部候选选择使用的插件包代际。
|
||
"""
|
||
return self._package_version
|
||
|
||
@package_version.setter
|
||
def package_version(self, value: Optional[str]) -> None:
|
||
"""
|
||
保存插件包代际,但不把它暴露到 API 响应模型。
|
||
"""
|
||
self._package_version = value
|
||
|
||
|
||
class PluginDocumentation(BaseModel):
|
||
"""
|
||
插件详情按需查询的使用说明入口,不包含远端文档正文。
|
||
"""
|
||
|
||
url: Optional[str] = Field(default=None, description="显式声明或已确认存在的 README 外链;不可用时为空")
|
||
|
||
|
||
class PluginRuntimeSummary(BaseModel):
|
||
"""
|
||
插件后台收敛状态和前端刷新代次。
|
||
"""
|
||
|
||
ready: bool = Field(description="本轮插件源码、依赖和加载是否已收敛")
|
||
generation: int = Field(description="插件运行状态变化代次")
|
||
pending_count: int = Field(description="仍处于准备阶段的插件数量")
|
||
failed_count: int = Field(description="加载失败或被策略阻止的插件数量")
|
||
restart_required_plugin_ids: List[str] = Field(
|
||
default_factory=list,
|
||
description="重启后才能完整激活新原生依赖的物理插件 ID",
|
||
)
|
||
gil_enabled_plugin_ids: List[str] = Field(
|
||
default_factory=list,
|
||
description="当前进程内加载时导致 free-threaded 运行时回退到 GIL 的插件 ID",
|
||
)
|
||
|
||
|
||
class PluginRuntimeCommandCapability(BaseModel):
|
||
"""
|
||
插件运行时注册命令的安全只读投影。
|
||
"""
|
||
|
||
cmd: str = Field(description="命令标识")
|
||
desc: Optional[str] = Field(default=None, description="命令说明")
|
||
plugin_id: Optional[str] = Field(default=None, description="注册命令的插件 ID")
|
||
|
||
|
||
class PluginRuntimeActionCapability(BaseModel):
|
||
"""
|
||
插件运行时注册动作的安全只读投影。
|
||
"""
|
||
|
||
id: str = Field(description="动作标识")
|
||
name: Optional[str] = Field(default=None, description="动作名称")
|
||
|
||
|
||
class PluginRuntimeActionGroup(BaseModel):
|
||
"""
|
||
按插件归组的运行时动作投影。
|
||
"""
|
||
|
||
plugin_id: Optional[str] = Field(default=None, description="注册动作的插件 ID")
|
||
plugin_name: Optional[str] = Field(default=None, description="插件名称")
|
||
actions: List[PluginRuntimeActionCapability] = Field(default_factory=list, description="可用动作列表")
|
||
|
||
|
||
class PluginRuntimeServiceCapability(BaseModel):
|
||
"""
|
||
插件定时服务的安全只读投影。
|
||
"""
|
||
|
||
id: str = Field(description="服务标识")
|
||
name: Optional[str] = Field(default=None, description="服务名称")
|
||
trigger: Optional[str] = Field(default=None, description="定时触发器说明")
|
||
|
||
|
||
class PluginRuntimeCapabilities(BaseModel):
|
||
"""
|
||
插件命令、动作和定时服务的公共安全能力快照。
|
||
"""
|
||
|
||
commands: List[PluginRuntimeCommandCapability] = Field(default_factory=list, description="菜单命令")
|
||
actions: List[PluginRuntimeActionGroup] = Field(default_factory=list, description="可用动作列表")
|
||
services: List[PluginRuntimeServiceCapability] = Field(default_factory=list, description="插件声明的服务能力")
|
||
|
||
|
||
class PluginDataKeySummary(BaseModel):
|
||
"""
|
||
单个插件持久化键的不含值诊断摘要。
|
||
"""
|
||
|
||
key: str = Field(description="持久化数据键")
|
||
value_type: Literal["null", "boolean", "number", "string", "array", "object", "unknown"] = Field(
|
||
description="值的 JSON 类型"
|
||
)
|
||
serialized_chars: Optional[int] = Field(
|
||
default=None,
|
||
ge=0,
|
||
description="JSON 紧凑序列化字符数;异常值为空",
|
||
)
|
||
sensitive: bool = Field(description="键名是否符合凭据字段规则")
|
||
|
||
|
||
class PluginDataSummary(BaseModel):
|
||
"""
|
||
插件持久化数据的不含原值诊断摘要。
|
||
"""
|
||
|
||
plugin_id: str = Field(description="插件 ID")
|
||
plugin_name: Optional[str] = Field(default=None, description="插件名称")
|
||
plugin_version: Optional[str] = Field(default=None, description="插件版本")
|
||
state: Optional[bool] = Field(default=None, description="插件是否启用")
|
||
count: int = Field(ge=0, description="持久化数据项总数")
|
||
total_chars: int = Field(ge=0, description="所有可序列化值的字符数总和")
|
||
keys: List[PluginDataKeySummary] = Field(default_factory=list, description="有界键摘要")
|
||
keys_truncated: bool = Field(description="是否还有未返回的键摘要")
|
||
|
||
|
||
class PluginInstallOutcome(BaseModel):
|
||
"""
|
||
插件载荷写入成功后的前端反馈依据。
|
||
"""
|
||
|
||
restart_required: bool = Field(description="本次依赖更新是否需要重启 MoviePilot 才能完成")
|
||
|
||
|
||
def _blank_clone_suffix_to_none(value: object) -> object:
|
||
"""
|
||
把「没填后缀」的三种写法归一成同一个值。
|
||
|
||
前端的后缀输入框留空时可能整个字段不带、带 null,也可能带一个空串或只有空白;
|
||
三者表达的都是「由服务端挑一个」。不归一的话空串会撞上格式校验,用户看到的是
|
||
一条与他的操作对不上的格式错误,而不是自动分配。
|
||
"""
|
||
if isinstance(value, str):
|
||
stripped = value.strip()
|
||
return stripped or None
|
||
return value
|
||
|
||
|
||
_CloneSuffix = _Annotated[Optional[str], _BeforeValidator(_blank_clone_suffix_to_none)]
|
||
|
||
|
||
class PluginCloneRequest(BaseModel):
|
||
"""
|
||
创建虚拟插件分身的请求参数。
|
||
"""
|
||
|
||
# 格式与长度在这一层判定:非法后缀拼出的实例 ID 用不了 Python 类名与路由段,
|
||
# 等落库之后再报错意味着已经写进一行、还要靠回滚把它擦掉
|
||
suffix: _CloneSuffix = Field(
|
||
default=None,
|
||
max_length=20,
|
||
pattern=r"^[A-Za-z0-9]+$",
|
||
description="追加到当前插件 ID 后的 ASCII 字母或数字后缀;留空时由服务端自动分配",
|
||
)
|
||
name: str = Field(default="", description="分身展示名称")
|
||
description: str = Field(default="", description="分身展示描述")
|
||
icon: Optional[str] = Field(default=None, description="分身展示图标")
|
||
version: Optional[str] = Field(
|
||
default=None,
|
||
description="兼容旧客户端保留,虚拟分身始终跟随源插件版本",
|
||
)
|
||
restore_previous: bool = Field(
|
||
default=True,
|
||
description="该后缀名下留有一个已停用的分身时是否沿用它的业务参数,为假时按源插件模板重建",
|
||
)
|
||
|
||
|
||
class PluginCloneOutcome(BaseModel):
|
||
"""
|
||
一次分身创建或恢复的结果。
|
||
|
||
实例 ID 必须回传:后缀可以由服务端自动分配,调用方因此再也算不出它,而后续要
|
||
拿它去打开配置、刷新列表或跳转。
|
||
"""
|
||
|
||
instance_id: str = Field(description="新建或恢复出来的分身实例 ID")
|
||
|
||
|
||
class PluginRestorableInstance(BaseModel):
|
||
"""
|
||
一个已停用、其设置仍留存可被恢复的分身实例。
|
||
|
||
在册启用的分身不在此列:它们的配置正在被使用,拿来「恢复」没有意义,摆进选择器
|
||
只会让用户误以为能把一个活着的实例再创建一遍。
|
||
"""
|
||
|
||
instance_id: str = Field(description="分身实例 ID")
|
||
suffix: str = Field(description="该实例相对源插件 ID 的后缀")
|
||
plugin_name: Optional[str] = Field(default=None, description="停用前登记的展示名称")
|
||
plugin_desc: Optional[str] = Field(default=None, description="停用前登记的展示描述")
|
||
has_config: bool = Field(default=False, description="是否留有业务参数")
|
||
|
||
|
||
class PluginSourceIdentity(BaseModel):
|
||
"""
|
||
显式换源确认所需的插件来源身份投影。
|
||
"""
|
||
|
||
plugin_id: str = Field(description="物理插件 ID")
|
||
trusted_source_type: str = Field(description="当前可信在线来源类型")
|
||
trusted_source_key: Optional[str] = Field(
|
||
default=None,
|
||
description="规范化的可信在线来源键;未绑定时为空",
|
||
)
|
||
binding_basis: str = Field(description="当前可信来源的建立依据")
|
||
payload_source_type: str = Field(description="最近一次已提交载荷的来源类型")
|
||
payload_source_key: Optional[str] = Field(
|
||
default=None,
|
||
description="最近一次在线载荷的来源键;本地或未知载荷为空",
|
||
)
|
||
revision: int = Field(ge=1, description="显式换源使用的身份 CAS revision")
|
||
|
||
|
||
class PluginSourceCandidate(BaseModel):
|
||
"""
|
||
一个可供管理员识别的脱敏插件来源候选。
|
||
"""
|
||
|
||
source_type: Literal["official", "third_party", "local"] = Field(description="来源类型")
|
||
source_key: Optional[str] = Field(
|
||
default=None,
|
||
description="规范化在线来源键;本地候选为空",
|
||
)
|
||
repo_url: Optional[str] = Field(
|
||
default=None,
|
||
description="可明确选择的在线仓库地址或本地仓库标识",
|
||
)
|
||
package_generation: Literal["v1", "v2", "v3"] = Field(description="当前运行时会采用的插件包代际")
|
||
plugin_version: Optional[str] = Field(
|
||
default=None,
|
||
description="该来源当前可安装的插件版本",
|
||
)
|
||
|
||
|
||
class PluginSourceOptions(BaseModel):
|
||
"""
|
||
来源选择界面所需的当前身份、候选和准入状态。
|
||
"""
|
||
|
||
plugin_id: str = Field(description="物理插件 ID")
|
||
inventory_complete: bool = Field(description="本轮配置市场是否全部得到确定读取结果")
|
||
selection_status: Literal["selected", "unavailable", "conflict", "incomplete"] = Field(
|
||
description="未指定新来源时的当前准入状态"
|
||
)
|
||
selection_reason: str = Field(description="当前准入状态的人类可读原因")
|
||
identity: Optional[PluginSourceIdentity] = Field(
|
||
default=None,
|
||
description="已安装插件的来源身份;未建立身份时为空",
|
||
)
|
||
candidates: List[PluginSourceCandidate] = Field(
|
||
default_factory=list,
|
||
description="按来源归并后的在线候选及可选本地候选",
|
||
)
|
||
|
||
|
||
class PluginSourceInstallRequest(BaseModel):
|
||
"""
|
||
管理员为未绑定插件明确选择初始在线来源的请求参数。
|
||
"""
|
||
|
||
repo_url: str = Field(min_length=1, description="明确选择的目标插件仓库地址")
|
||
release_version: Optional[str] = Field(
|
||
default=None,
|
||
description="指定安装的 Release 资产版本;为空时使用当前索引版本",
|
||
)
|
||
force: bool = Field(
|
||
default=False,
|
||
description="是否强制重新下载并安装所选来源载荷",
|
||
)
|
||
|
||
@field_validator("repo_url")
|
||
@classmethod
|
||
def normalize_repo_url(cls, value: str) -> str:
|
||
"""
|
||
拒绝只含空白或本地路径标识的来源选择。
|
||
"""
|
||
normalized = value.strip()
|
||
if not normalized:
|
||
raise ValueError("显式安装必须指定目标在线来源")
|
||
if normalized.startswith("local://"):
|
||
raise ValueError("显式来源安装只接受在线插件仓库")
|
||
return normalized
|
||
|
||
|
||
class PluginSourceChangeRequest(BaseModel):
|
||
"""
|
||
管理员显式切换插件在线来源的请求参数。
|
||
"""
|
||
|
||
repo_url: str = Field(min_length=1, description="明确选择的目标插件仓库地址")
|
||
expected_revision: int = Field(
|
||
ge=1,
|
||
description="提交换源时必须匹配的当前身份 revision",
|
||
)
|
||
release_version: Optional[str] = Field(
|
||
default=None,
|
||
description="指定安装的 Release 资产版本;为空时使用当前索引版本",
|
||
)
|
||
|
||
@field_validator("repo_url")
|
||
@classmethod
|
||
def normalize_repo_url(cls, value: str) -> str:
|
||
"""
|
||
拒绝只含空白或本地路径标识的换源目标。
|
||
"""
|
||
normalized = value.strip()
|
||
if not normalized:
|
||
raise ValueError("显式换源必须指定目标在线来源")
|
||
if normalized.startswith("local://"):
|
||
raise ValueError("显式换源只接受在线插件仓库")
|
||
return normalized
|
||
|
||
|
||
class PluginDashboard(Plugin):
|
||
"""
|
||
插件仪表盘。
|
||
"""
|
||
|
||
id: Optional[str] = Field(default=None, description="记录标识")
|
||
name: Optional[str] = Field(default=None, description="名称")
|
||
key: Optional[str] = Field(default=None, description="仪表板key")
|
||
render_mode: Optional[str] = Field(default="vuetify", description="演染模式")
|
||
attrs: Optional[dict[str, JsonData]] = Field(default_factory=dict, description="全局配置")
|
||
cols: Optional[dict[str, JsonData]] = Field(default_factory=dict, description="col列数")
|
||
elements: Optional[List[dict[str, JsonData]]] = Field(default_factory=list, description="页面元素")
|
||
|
||
|
||
class PluginSidebarNavItem(BaseModel):
|
||
"""
|
||
插件侧栏导航项(前端全页路由)。
|
||
"""
|
||
|
||
plugin_id: str = Field(description="插件 ID")
|
||
nav_key: str = Field(description="导航键,对应 URL 段")
|
||
title: str = Field(description="侧栏标题")
|
||
icon: str = Field(default="mdi-puzzle", description="MDI 图标名")
|
||
section: str = Field(
|
||
description="分组:start / discovery / subscribe / organize / system",
|
||
)
|
||
permission: Optional[str] = Field(
|
||
default=None,
|
||
description="权限:subscribe / discovery / search / manage / admin",
|
||
)
|
||
order: int = Field(default=0, description="同组内排序,越小越靠前")
|
||
|
||
|
||
class PluginAgentPet(BaseModel):
|
||
"""
|
||
插件声明的 Agent 助手形象(前端按 plugin_id + key 选择并挂载联邦组件)。
|
||
"""
|
||
|
||
plugin_id: str = Field(description="插件实例 ID,分身为分身自身 ID")
|
||
source_plugin_id: str = Field(description="提供前端产物的源插件 ID,非分身时等于 plugin_id")
|
||
plugin_name: str = Field(description="插件展示名")
|
||
key: str = Field(description="形象 key,插件内唯一,匹配 [a-z0-9_-]{1,32}")
|
||
name: str = Field(description="形象展示名")
|
||
description: Optional[str] = Field(default=None, description="形象一句话说明")
|
||
mode: Literal["stage", "renderer"] = Field(
|
||
description="渲染模式:stage(插件拥有角色与视口图层)/ renderer(宿主负责入口行为,插件只绘制)",
|
||
)
|
||
component: str = Field(default="AgentPet", description="联邦暴露名,不含 ./ 前缀")
|
||
api_version: int = Field(default=1, description="形象契约版本,前端忽略不认识的版本")
|
||
plugin_version: Optional[str] = Field(
|
||
default=None,
|
||
description="提供形象的插件版本;前端据此判断插件是否升级,以决定是否重试加载失败的形象",
|
||
)
|
||
remote_url: str = Field(
|
||
description="插件联邦入口 remoteEntry.js 地址,规则同 GET /plugin/remotes 的 url;前端据此直接注册远程入口",
|
||
)
|
||
preview_url: Optional[str] = Field(
|
||
default=None,
|
||
description="预览图 URL:相对路径已解析为与 remoteEntry 同源的插件静态文件地址,或原样的 http(s)/data URL",
|
||
)
|
||
avatar_url: Optional[str] = Field(
|
||
default=None,
|
||
description="方形头像 URL,用于 Agent 面板头部与消息头像;解析规则同 preview_url,未声明为空",
|
||
)
|
||
bubbles: Optional[Literal["host", "self"]] = Field(
|
||
default=None,
|
||
description="气泡归属,仅 stage 模式:host(宿主按锚点绘制)/ self(插件自绘);renderer 模式为空",
|
||
)
|
||
random_actions: Optional[List[str]] = Field(
|
||
default=None,
|
||
description="宿主随机动作候选,仅 renderer 模式;为空表示使用宿主动作全集",
|
||
)
|
||
|
||
|
||
class PluginRatingRequest(BaseModel):
|
||
"""
|
||
插件评分请求。
|
||
"""
|
||
|
||
rating: float = Field(
|
||
ge=0.1,
|
||
le=5.0,
|
||
multiple_of=0.1,
|
||
description="评分,范围 0.1 至 5.0,精确到 0.1",
|
||
)
|
||
|
||
|
||
class PluginRating(BaseModel):
|
||
"""
|
||
插件评分结果。
|
||
"""
|
||
|
||
plugin_id: str = Field(description="插件 ID")
|
||
average_rating: float = Field(default=0.0, description="平均评分")
|
||
rating_count: int = Field(default=0, description="评分人数")
|
||
user_rating: Optional[float] = Field(default=None, description="当前安装实例评分")
|
||
|
||
|
||
class PluginRatingMap(RootModel[Dict[str, PluginRating]]):
|
||
"""
|
||
插件 ID 与评分结果的映射。
|
||
"""
|
||
|
||
|
||
class PluginMemoryInfo(BaseModel):
|
||
"""
|
||
插件内存信息。
|
||
"""
|
||
|
||
plugin_id: str = Field(description="插件ID")
|
||
plugin_name: str = Field(description="插件名称")
|
||
plugin_version: str = Field(description="插件版本")
|
||
total_memory_bytes: int = Field(description="总内存使用量(字节)")
|
||
total_memory_mb: float = Field(description="总内存使用量(MB)")
|
||
object_count: int = Field(description="对象数量")
|
||
calculation_time_ms: float = Field(description="计算耗时(毫秒)")
|
||
timestamp: float = Field(description="统计时间戳")
|
||
error: Optional[str] = Field(default=None, description="错误信息")
|
||
object_details: Optional[List[Dict[str, JsonData]]] = Field(default=None, description="大对象详情")
|
||
|
||
|
||
class PluginRemoteInfo(BaseModel):
|
||
"""
|
||
插件模块联邦远程入口。
|
||
"""
|
||
|
||
id: str = Field(description="记录标识")
|
||
url: str = Field(description="访问地址")
|
||
name: str = Field(description="显示名称")
|
||
source_plugin_id: Optional[str] = Field(default=None, description="静默丢弃,投影层写进去也到不了前端")
|
||
|
||
|
||
class PluginReleaseItem(BaseModel):
|
||
"""
|
||
可安装的插件 Release 版本。
|
||
"""
|
||
|
||
version: str = Field(description="版本号")
|
||
tag_name: str = Field(description="版本发布标签")
|
||
name: str = Field(description="显示名称")
|
||
published_at: Optional[str] = Field(default=None, description="版本发布时间")
|
||
body: str = Field(default="", description="正文内容")
|
||
asset_name: str = Field(description="发布版本的安装包文件名")
|
||
is_latest: bool = Field(default=False, description="是否为最新发布版本")
|
||
is_current: bool = Field(default=False, description="是否为当前安装的版本")
|
||
|
||
|
||
class PluginReleaseData(BaseModel):
|
||
"""
|
||
插件 Release 能力与版本列表。
|
||
"""
|
||
|
||
release_supported: bool = Field(default=False, description="插件是否支持按发布版本安装")
|
||
latest_version: Optional[str] = Field(default=None, description="最新可用版本")
|
||
current_version: Optional[str] = Field(default=None, description="当前已安装版本")
|
||
items: List[PluginReleaseItem] = Field(default_factory=list, description="可供安装的插件发布版本列表")
|
||
|
||
|
||
class PluginFolderConfigData(BaseModel):
|
||
"""
|
||
新版插件文件夹配置(对象格式,含展示配置与插件列表)。
|
||
"""
|
||
|
||
# 文件夹内插件 ID 列表
|
||
plugins: List[str] = Field(
|
||
default_factory=list,
|
||
description="按顺序分配到此文件夹的已安装插件标识",
|
||
)
|
||
# 文件夹排序值
|
||
order: Optional[int] = Field(default=None, description="文件夹显示顺序")
|
||
# 文件夹图标
|
||
icon: Optional[str] = Field(default=None, description="文件夹图标名称")
|
||
# 文件夹颜色
|
||
color: Optional[str] = Field(default=None, description="文件夹的显示颜色")
|
||
# 文件夹渐变
|
||
gradient: Optional[str] = Field(default=None, description="文件夹的渐变背景配置")
|
||
# 文件夹背景
|
||
background: Optional[str] = Field(default=None, description="文件夹的背景样式")
|
||
# 是否显示图标(前端字段为驼峰命名)
|
||
show_icon: Optional[bool] = Field(
|
||
default=None,
|
||
alias="showIcon",
|
||
description="是否显示文件夹图标",
|
||
)
|
||
|
||
|
||
class PluginFoldersData(RootModel[Dict[str, Union[List[str], PluginFolderConfigData]]]):
|
||
"""
|
||
插件文件夹与插件配置映射,兼容旧版数组格式与新版对象格式。
|
||
"""
|
||
|
||
|
||
class PluginFolderUpdateRequest(BaseModel):
|
||
"""
|
||
插件文件夹名称和展示字段的增量更新请求。
|
||
"""
|
||
|
||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||
|
||
new_name: Optional[str] = Field(
|
||
default=None,
|
||
min_length=1,
|
||
max_length=128,
|
||
description="修改后的名称",
|
||
)
|
||
icon: Optional[str] = Field(default=None, description="文件夹图标名称")
|
||
color: Optional[str] = Field(
|
||
default=None,
|
||
description="文件夹的显示颜色",
|
||
)
|
||
gradient: Optional[str] = Field(
|
||
default=None,
|
||
description="文件夹的渐变背景配置",
|
||
)
|
||
background: Optional[str] = Field(
|
||
default=None,
|
||
description="文件夹的背景样式",
|
||
)
|
||
show_icon: Optional[bool] = Field(
|
||
default=None,
|
||
alias="showIcon",
|
||
description="是否显示文件夹图标",
|
||
)
|
||
|
||
|
||
class PluginFolderPluginsUpdateRequest(BaseModel):
|
||
"""
|
||
插件文件夹成员顺序的条件替换请求。
|
||
"""
|
||
|
||
plugins: List[str] = Field(description="按顺序分配到此文件夹的已安装插件标识")
|
||
expected_plugins: Optional[List[str]] = Field(
|
||
default=None,
|
||
description="最近读取的插件标识顺序,用于拒绝过期替换",
|
||
)
|
||
|
||
|
||
class PluginDashboardMetaItem(BaseModel):
|
||
"""
|
||
插件仪表板入口摘要。
|
||
"""
|
||
|
||
id: str = Field(description="记录标识")
|
||
name: Optional[str] = Field(default=None, description="显示名称")
|
||
key: Optional[str] = Field(default=None, description="缓存条目的键")
|