refactor(api): standardize responses and restore media config contracts

This commit is contained in:
jxxghp
2026-08-12 23:15:13 +08:00
parent 0ac641c05d
commit baf8850d63
100 changed files with 4422 additions and 1286 deletions
+8
View File
@@ -1,4 +1,7 @@
from .agent import *
from .cache import *
from .category import *
from .common import *
from .context import *
from .dashboard import *
from .download import *
@@ -6,13 +9,18 @@ from .event import *
from .exception import *
from .file import *
from .history import *
from .llm import *
from .mediaserver import *
from .message import *
from .mfa import *
from .music import *
from .monitoring import *
from .notification import *
from .plugin import *
from .response import *
from .rule import *
from .search import *
from .storage import *
from .openai import *
from .servarr import *
from .servcookie import *
+86 -3
View File
@@ -6,6 +6,8 @@ from typing import Any, List, Literal, Optional, Union
from langchain_core.messages import BaseMessage
from pydantic import BaseModel, Field, ConfigDict, field_serializer
from app.schemas.common import JsonData
class ConversationMemory(BaseModel):
"""对话记忆模型"""
@@ -97,7 +99,7 @@ class AgentMcpServerToolInfo(BaseModel):
name: str = Field(..., description="原始 MCP 工具名称")
agent_tool_name: str = Field(..., description="注入 Agent 后的工具名称")
description: str = Field(default="", description="工具说明")
input_schema: dict[str, Any] = Field(default_factory=dict, description="工具参数 Schema")
input_schema: dict[str, JsonData] = Field(default_factory=dict, description="工具参数 Schema")
class AgentMcpServerTestResult(BaseModel):
@@ -217,8 +219,8 @@ class AgentChatSession(BaseModel):
username: Optional[str] = Field(None, description="用户名")
original_chat_id: Optional[str] = Field(None, description="原聊天 ID")
message_count: int = Field(default=0, description="展示消息数量")
created_at: Optional[str] = Field(None, description="创建时间")
updated_at: Optional[str] = Field(None, description="更新时间")
created_at: Optional[datetime | str] = Field(None, description="创建时间")
updated_at: Optional[datetime | str] = Field(None, description="更新时间")
messages: list[AgentChatMessage] = Field(default_factory=list, description="展示消息列表")
@@ -229,3 +231,84 @@ class AgentChatDisplaySaveRequest(BaseModel):
messages: list[AgentChatMessage] = Field(default_factory=list, description="展示消息列表")
title: Optional[str] = Field(None, description="会话标题")
class AgentMcpServerListData(BaseModel):
"""Agent MCP 服务器列表与启用统计。"""
servers: list[AgentMcpServerConfig] = Field(default_factory=list, description="服务器列表")
enabled_count: int = Field(default=0, description="已启用服务器数量")
total_count: int = Field(default=0, description="服务器总数")
class AgentChatUploadAttachment(AgentChatAttachment):
"""Web Agent 上传完成后的附件描述。"""
ref: str = Field(description="供 Agent 消费的附件引用")
status: str = Field(default="ready", description="附件处理状态")
class AgentWebChoiceFeedback(BaseModel):
"""Web Agent 选择回调的反馈快照。"""
request_id: str = Field(description="选择请求 ID")
title: Optional[str] = Field(default=None, description="选择标题")
prompt: str = Field(default="", description="选择提示")
selected_label: str = Field(description="已选择文案")
selected_value: str = Field(description="已选择值")
selected_description: Optional[str] = Field(default=None, description="已选择说明")
buttons: list[AgentChatChoiceButton] = Field(default_factory=list, description="按钮列表")
button_rows: list[list[AgentChatChoiceButton]] = Field(default_factory=list, description="按钮行")
class AgentWebCallbackData(BaseModel):
"""Web Agent 按钮回调后供前端继续发送的数据。"""
message: str = Field(description="下一条用户消息")
display_message: str = Field(default="", description="前端展示消息")
session_id: Optional[str] = Field(default=None, description="Agent 会话 ID")
traditional: bool = Field(default=False, description="是否为传统消息链回调")
original_message_id: Optional[str | int] = Field(default=None, description="原消息 ID")
original_chat_id: Optional[str | int] = Field(default=None, description="原聊天 ID")
choice_selection: Optional[AgentChatChoiceSelection] = Field(default=None, description="选择结果快照")
feedback: Optional[AgentWebChoiceFeedback] = Field(default=None, description="选择反馈")
class AgentWebCommandInfo(BaseModel):
"""Web Agent 可用斜杠命令摘要。"""
command: str = Field(description="命令")
description: str = Field(default="", description="命令说明")
category: str = Field(default="其他", description="命令分类")
type: str = Field(default="", description="命令类型")
pid: Optional[str | int] = Field(default=None, description="插件 ID")
class AgentChatSessionSummary(BaseModel):
"""Agent 历史会话摘要。"""
id: Optional[int] = Field(default=None, description="数据库 ID")
session_id: str = Field(description="Agent 内部会话 ID")
client_session_id: Optional[str] = Field(default=None, description="客户端会话 ID")
title: Optional[str] = Field(default=None, description="会话标题")
channel: Optional[str] = Field(default=None, description="消息渠道")
source: Optional[str] = Field(default=None, description="渠道来源")
user_id: Optional[str] = Field(default=None, description="用户 ID")
username: Optional[str] = Field(default=None, description="用户名")
original_chat_id: Optional[str] = Field(default=None, description="原聊天 ID")
message_count: int = Field(default=0, description="展示消息数量")
created_at: Optional[datetime | str] = Field(default=None, description="创建时间")
updated_at: Optional[datetime | str] = Field(default=None, description="更新时间")
class AgentChatSessionDetail(AgentChatSessionSummary):
"""Agent 历史会话详情。"""
messages: list[AgentChatMessage] = Field(default_factory=list, description="展示消息列表")
is_processing: bool = Field(default=False, description="会话是否正在处理")
class AgentSessionStopData(BaseModel):
"""Agent 会话停止结果。"""
stopped: bool = Field(description="是否停止了正在执行的任务")
+50
View File
@@ -0,0 +1,50 @@
"""种子缓存 API 输出模型。"""
from typing import Optional
from pydantic import BaseModel, Field
from app.schemas.types import MediaSource
class TorrentCacheItem(BaseModel):
"""单条站点种子缓存。"""
hash: str
domain: str
title: Optional[str] = None
description: Optional[str] = None
size: Optional[int] = None
pubdate: Optional[str] = None
site_name: Optional[str] = None
media_name: Optional[str] = None
media_year: Optional[str | int] = None
media_type: Optional[str] = None
media_source: Optional[MediaSource] = None
media_id: Optional[str] = None
music_type: Optional[str] = None
season_episode: Optional[str] = None
resource_term: Optional[str] = None
enclosure: Optional[str] = None
page_url: Optional[str] = None
poster_path: Optional[str] = None
backdrop_path: Optional[str] = None
class TorrentCacheData(BaseModel):
"""种子缓存统计及明细。"""
count: int = 0
sites: int = 0
data: list[TorrentCacheItem] = Field(default_factory=list)
class TorrentReidentifyData(BaseModel):
"""种子重新识别后的媒体身份。"""
media_name: Optional[str] = None
media_year: Optional[str | int] = None
media_type: Optional[str] = None
media_source: Optional[MediaSource] = None
media_id: Optional[str] = None
music_type: Optional[str] = None
+5 -1
View File
@@ -1,6 +1,6 @@
from typing import Dict, Optional
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, RootModel
class CategoryRule(BaseModel):
@@ -29,3 +29,7 @@ class CategoryConfig(BaseModel):
movie: Optional[Dict[str, Optional[CategoryRule]]] = {}
# 电视剧分类策略
tv: Optional[Dict[str, Optional[CategoryRule]]] = {}
class MediaCategoryMap(RootModel[Dict[str, list[str]]]):
"""媒体类型与自动分类名称列表的映射。"""
+78
View File
@@ -0,0 +1,78 @@
"""API 端点共享的小型业务数据模型。"""
from typing import Optional, Union
from pydantic import BaseModel, Field, RootModel
from typing_extensions import TypeAliasType
JsonData = TypeAliasType(
"JsonData",
Union[
dict[str, "JsonData"],
list["JsonData"],
str,
int,
float,
bool,
None,
],
)
"""可递归序列化的 JSON 数据;OpenAPI 会展示每一种合法 JSON 结构。"""
class JsonObject(RootModel[dict[str, JsonData]]):
"""字段由运行时扩展点决定的 JSON 对象。"""
class JsonObjectList(RootModel[list[JsonObject]]):
"""字段由运行时扩展点决定的 JSON 对象列表。"""
class IdData(BaseModel):
"""创建资源后返回的资源 ID。"""
id: Optional[int | str] = Field(default=None, description="资源 ID")
class ValueData(BaseModel):
"""单个动态配置值。"""
value: JsonData = Field(default=None, description="配置值")
class FileNameData(BaseModel):
"""文件操作结果中的文件名。"""
filename: Optional[str] = Field(default=None, description="文件名")
class NameData(BaseModel):
"""名称计算结果。"""
name: Optional[str] = Field(default=None, description="名称")
class ServiceClientInfo(BaseModel):
"""可选择的下载器或媒体服务器摘要。"""
name: Optional[str] = Field(default=None, description="实例名称")
type: Optional[str] = Field(default=None, description="服务类型")
class ProgressKeyData(BaseModel):
"""异步任务进度查询标识。"""
progress_key: str = Field(description="进度查询标识")
class BatchProgressKeyData(ProgressKeyData):
"""批量异步任务进度查询标识。"""
history_ids: list[int] = Field(default_factory=list, description="历史记录 ID 列表")
class TimeData(BaseModel):
"""网络请求耗时。"""
time: int | float = Field(description="耗时毫秒数")
+207 -40
View File
@@ -1,7 +1,8 @@
from typing import Optional, Dict, List, Union, Any
from typing import Annotated, Optional, Dict, List, Union, Any
from pydantic import BaseModel, Field
from pydantic import BaseModel, Discriminator, Field, RootModel, Tag
from app.schemas.common import JsonData
from app.schemas.music import MusicInfo, MusicMeta
from app.schemas.media import OptionalMediaIdentityMixin
from app.schemas.types import MediaSource
@@ -73,6 +74,144 @@ class MetaInfo(OptionalMediaIdentityMixin, BaseModel):
media_id: Optional[str] = None
class MediaImageSet(BaseModel):
"""跨媒体源兼容的人物图片尺寸集合。"""
large: Optional[str] = None
common: Optional[str] = None
medium: Optional[str] = None
normal: Optional[str] = None
small: Optional[str] = None
grid: Optional[str] = None
class MediaCredit(BaseModel):
"""影视条目中的演职员摘要。"""
id: Optional[int | str] = None
name: Optional[str] = None
original_name: Optional[str] = None
character: Optional[str] = None
type: Optional[str | int] = None
gender: Optional[str | int] = None
adult: Optional[bool] = None
known_for_department: Optional[str] = None
profile_path: Optional[str] = None
credit_id: Optional[str] = None
cast_id: Optional[int] = None
order: Optional[int] = None
department: Optional[str] = None
job: Optional[str] = None
popularity: Optional[float] = None
roles: list[str] = Field(default_factory=list)
title: Optional[str] = None
url: Optional[str] = None
uri: Optional[str] = None
sharing_url: Optional[str] = None
avatar: Optional[str | MediaImageSet] = None
images: Optional[MediaImageSet] = None
latin_name: Optional[str] = None
career: list[str] = Field(default_factory=list)
relation: Optional[str] = None
user: Optional[JsonData] = None
class MediaGenre(BaseModel):
"""影视风格摘要。"""
id: Optional[int | str] = None
name: Optional[str] = None
class MediaCompany(BaseModel):
"""电视网或制作公司的标准摘要。"""
id: Optional[int | str] = None
name: Optional[str] = None
logo_path: Optional[str] = None
origin_country: Optional[str] = None
class MediaCountry(BaseModel):
"""影视制作国家或地区。"""
id: Optional[int | str] = None
iso_3166_1: Optional[str] = None
name: Optional[str] = None
class MediaLanguage(BaseModel):
"""影视内容使用的语言。"""
english_name: Optional[str] = None
iso_639_1: Optional[str] = None
name: Optional[str] = None
class MediaReleaseDate(BaseModel):
"""电影在单个地区的一次发行记录。"""
date: str
iso_code: Optional[str] = None
note: Optional[str] = None
type: Optional[int] = None
class MediaEpisode(BaseModel):
"""电视剧即将播出的单集摘要。"""
id: Optional[int] = None
air_date: Optional[str] = None
episode_number: Optional[int] = None
episode_type: Optional[str] = None
name: Optional[str] = None
overview: Optional[str] = None
production_code: Optional[str] = None
runtime: Optional[int] = None
season_number: Optional[int] = None
show_id: Optional[int] = None
still_path: Optional[str] = None
vote_average: Optional[float] = None
vote_count: Optional[int] = None
class MediaSeason(BaseModel):
"""标准季信息以及剧集组季信息。"""
id: Optional[int | str] = None
air_date: Optional[str] = None
episode_count: Optional[int] = None
name: Optional[str] = None
overview: Optional[str] = None
poster_path: Optional[str] = None
season_number: Optional[int] = None
vote_average: Optional[float] = None
order: Optional[int] = None
locked: Optional[bool] = None
episodes: list[MediaEpisode] = Field(default_factory=list)
class MediaEpisodeGroupNetwork(BaseModel):
"""TMDB 剧集组所属电视网信息。"""
id: Optional[int] = None
name: Optional[str] = None
logo_path: Optional[str] = None
origin_country: Optional[str] = None
class MediaEpisodeGroup(BaseModel):
"""TMDB 电视剧的剧集分组摘要。"""
description: str = ""
episode_count: int = 0
group_count: int = 0
id: str
name: str
network: Optional[MediaEpisodeGroupNetwork] = None
type: int
class MediaInfo(OptionalMediaIdentityMixin, BaseModel):
"""
识别媒体信息
@@ -87,6 +226,10 @@ class MediaInfo(OptionalMediaIdentityMixin, BaseModel):
title: Optional[str] = None
# 英文标题
en_title: Optional[str] = None
# 香港、台湾、新加坡地区标题
hk_title: Optional[str] = None
tw_title: Optional[str] = None
sg_title: Optional[str] = None
# 年份
year: Optional[str] = None
# 标题(年份)
@@ -116,6 +259,8 @@ class MediaInfo(OptionalMediaIdentityMixin, BaseModel):
backdrop_path: Optional[str] = None
# 海报图片
poster_path: Optional[str] = None
# 标题 LOGO
logo_path: Optional[str] = None
# 评分
vote_average: Optional[float] = 0.0
# 描述
@@ -123,58 +268,60 @@ class MediaInfo(OptionalMediaIdentityMixin, BaseModel):
# 二级分类
category: Optional[str] = ""
# 季季集清单
seasons: Optional[Dict[int, list]] = Field(default_factory=dict)
seasons: Optional[Dict[int, list[int]]] = Field(default_factory=dict)
# 季详情
season_info: Optional[List[dict]] = Field(default_factory=list)
season_info: Optional[List[MediaSeason]] = Field(default_factory=list)
# 各季首播年份
season_years: Optional[Dict[int, str]] = Field(default_factory=dict)
# 别名和译名
names: Optional[list] = Field(default_factory=list)
names: Optional[list[str]] = Field(default_factory=list)
# 演员
actors: Optional[list] = Field(default_factory=list)
actors: Optional[list[MediaCredit]] = Field(default_factory=list)
# 导演
directors: Optional[list] = Field(default_factory=list)
directors: Optional[list[MediaCredit]] = Field(default_factory=list)
# 详情链接
detail_link: Optional[str] = None
# 其它TMDB属性
# 是否成人内容
adult: Optional[bool] = False
# 创建人
created_by: Optional[list] = Field(default_factory=list)
created_by: Optional[list[MediaCredit]] = Field(default_factory=list)
# 集时长
episode_run_time: Optional[list] = Field(default_factory=list)
episode_run_time: Optional[list[int]] = Field(default_factory=list)
# 风格
genres: Optional[List[dict]] = Field(default_factory=list)
genres: Optional[List[MediaGenre]] = Field(default_factory=list)
# 首播日期
first_air_date: Optional[str] = None
# 首页
homepage: Optional[str] = None
# 语种
languages: Optional[list] = Field(default_factory=list)
languages: Optional[list[str]] = Field(default_factory=list)
# 最后上映日期
last_air_date: Optional[str] = None
# 流媒体平台
networks: Optional[list] = Field(default_factory=list)
networks: Optional[list[MediaCompany]] = Field(default_factory=list)
# 集数
number_of_episodes: Optional[int] = 0
# 季数
number_of_seasons: Optional[int] = 0
# 原产国
origin_country: Optional[list] = Field(default_factory=list)
origin_country: Optional[list[str]] = Field(default_factory=list)
# 原名
original_name: Optional[str] = None
# 出品公司
production_companies: Optional[list] = Field(default_factory=list)
production_companies: Optional[list[MediaCompany]] = Field(default_factory=list)
# 出品国
production_countries: Optional[list] = Field(default_factory=list)
production_countries: Optional[list[MediaCountry]] = Field(default_factory=list)
# 语种
spoken_languages: Optional[list] = Field(default_factory=list)
spoken_languages: Optional[list[MediaLanguage]] = Field(default_factory=list)
# 所有发行日期
release_dates: list = Field(default_factory=list)
release_dates: list[MediaReleaseDate] = Field(default_factory=list)
# 状态
status: Optional[str] = None
# 标签
tagline: Optional[str] = None
# 风格ID
genre_ids: Optional[list] = Field(default_factory=list)
genre_ids: Optional[list[int | str]] = Field(default_factory=list)
# 评价数量
vote_count: Optional[int] = 0
# 流行度
@@ -182,11 +329,18 @@ class MediaInfo(OptionalMediaIdentityMixin, BaseModel):
# 时长
runtime: Optional[int] = None
# 下一集
next_episode_to_air: Optional[dict] = Field(default_factory=dict)
next_episode_to_air: Optional[MediaEpisode] = None
# 内容分级
content_rating: Optional[str] = None
# 全部剧集组
episode_groups: Optional[list] = Field(default_factory=list)
episode_groups: Optional[list[MediaEpisodeGroup | MediaSeason]] = Field(default_factory=list)
# 剧集组
episode_group: Optional[str] = None
# 各数据源原始信息;Core MediaInfo.to_dict() 保留这些键供兼容调用方使用。
tmdb_info: Optional[dict[str, JsonData]] = None
douban_info: Optional[dict[str, JsonData]] = None
bangumi_info: Optional[dict[str, JsonData]] = None
anilist_info: Optional[dict[str, JsonData]] = None
class TorrentInfo(OptionalMediaIdentityMixin, BaseModel):
@@ -239,7 +393,7 @@ class TorrentInfo(OptionalMediaIdentityMixin, BaseModel):
# HR
hit_and_run: Optional[bool] = False
# 种子标签
labels: Optional[list] = Field(default_factory=list)
labels: Optional[list[str]] = Field(default_factory=list)
# 种子优先级
pri_order: Optional[int] = 0
# 种子分类 电影/电视剧/音乐
@@ -326,19 +480,6 @@ class Context(BaseModel):
confirmed_full_coverage: Optional[bool] = False
class MediaSeason(BaseModel):
"""
季信息
"""
air_date: Optional[str] = None
episode_count: Optional[int] = None
name: Optional[str] = None
overview: Optional[str] = None
poster_path: Optional[str] = None
season_number: Optional[int] = None
vote_average: Optional[float] = None
class MediaPerson(BaseModel):
"""
媒体人物信息
@@ -346,17 +487,17 @@ class MediaPerson(BaseModel):
# 来源:themoviedb、douban、bangumi、anilist
source: Optional[str] = None
# 公共
id: Optional[int] = None
id: Optional[int | str] = None
type: Optional[Union[str, int]] = 1
name: Optional[str] = None
character: Optional[str] = None
images: Optional[dict] = Field(default_factory=dict)
images: Optional[MediaImageSet] = None
# themoviedb
profile_path: Optional[str] = None
gender: Optional[Union[str, int]] = None
original_name: Optional[str] = None
credit_id: Optional[str] = None
also_known_as: Optional[list] = Field(default_factory=list)
also_known_as: Optional[list[str]] = Field(default_factory=list)
birthday: Optional[str] = None
deathday: Optional[str] = None
imdb_id: Optional[str] = None
@@ -365,11 +506,37 @@ class MediaPerson(BaseModel):
popularity: Optional[float] = None
biography: Optional[str] = None
# douban
roles: Optional[list] = Field(default_factory=list)
roles: Optional[list[str]] = Field(default_factory=list)
title: Optional[str] = None
url: Optional[str] = None
avatar: Optional[Union[str, dict]] = None
avatar: Optional[Union[str, MediaImageSet]] = None
latin_name: Optional[str] = None
# bangumi
career: Optional[list] = Field(default_factory=list)
career: Optional[list[str]] = Field(default_factory=list)
relation: Optional[str] = None
def _media_search_result_kind(value: Any) -> str:
"""按稳定字段区分音乐、人物与影视/合集搜索结果。"""
if isinstance(value, BaseModel):
value = value.model_dump()
if isinstance(value, dict):
if value.get("type") == "音乐" or "music_type" in value:
return "music"
if "source" in value and "media_source" not in value:
return "person"
return "media"
MediaSearchResult = Annotated[
Union[
Annotated[MusicInfo, Tag("music")],
Annotated[MediaPerson, Tag("person")],
Annotated[MediaInfo, Tag("media")],
],
Discriminator(_media_search_result_kind),
]
class MediaSearchResults(RootModel[List[MediaSearchResult]]):
"""媒体、音乐、合集与人物的统一搜索结果列表。"""
+2 -1
View File
@@ -3,6 +3,7 @@ from typing import Optional
from pydantic import BaseModel, Field, model_validator
from app.helper.locale import LocaleHelper
from app.schemas.common import JsonData
class Statistic(BaseModel):
@@ -120,7 +121,7 @@ class ScheduleProgress(BaseModel):
# 多语言错误信息
error_i18n: Optional[str] = None
# 扩展数据
data: Optional[dict] = Field(default_factory=dict)
data: Optional[dict[str, JsonData]] = Field(default_factory=dict)
@model_validator(mode="after")
def fill_i18n_fields(self) -> "ScheduleProgress":
+12
View File
@@ -25,3 +25,15 @@ class DownloadDirectory(BaseModel):
priority: Optional[int] = Field(default=0, description="目录优先级")
media_type: Optional[str] = Field(default=None, description="适用媒体类型")
media_category: Optional[str] = Field(default=None, description="适用媒体分类")
class DownloadAddedData(BaseModel):
"""下载任务添加结果。"""
download_id: Optional[str] = Field(default=None, description="下载任务 ID")
class SubtitleDownloadData(BaseModel):
"""字幕下载结果。"""
files: list[str] = Field(default_factory=list, description="已保存字幕文件列表")
+4 -3
View File
@@ -3,6 +3,7 @@ from typing import Iterable, Optional, Dict, Any, List, Set, Callable
from pydantic import BaseModel, Field, field_validator, model_validator
from app.schemas.common import JsonData
from app.schemas.message import MessageChannel
from app.schemas.file import FileItem
from app.schemas.media import OptionalMediaIdentityMixin, RequiredMediaIdentityMixin
@@ -484,11 +485,11 @@ class DiscoverMediaSource(BaseModel):
name: str = Field(..., description="数据源名称")
media_source: MediaSource = Field(..., description="媒体来源枚举")
api_path: str = Field(..., description="媒体数据源API地址")
filter_params: Optional[Dict[str, Any]] = Field(
filter_params: Optional[Dict[str, JsonData]] = Field(
default=None, description="过滤参数"
)
filter_ui: Optional[List[dict]] = Field(default=[], description="过滤参数UI配置")
depends: Optional[Dict[str, list]] = Field(
filter_ui: Optional[List[Dict[str, JsonData]]] = Field(default=[], description="过滤参数UI配置")
depends: Optional[Dict[str, list[str]]] = Field(
default=None, description="UI依赖关系字典"
)
+10 -2
View File
@@ -10,6 +10,8 @@ WINDOWS_DRIVE_PATTERN = re.compile(r"^[A-Za-z]:[\\/]")
class FileURI(BaseModel):
"""带存储类型的文件 URI。"""
# 文件路径
path: Optional[str] = "/"
# 存储类型
@@ -45,6 +47,8 @@ class FileURI(BaseModel):
class FileItem(FileURI):
"""文件或目录条目,目录可递归包含子条目。"""
# 类型 dir/file
type: Optional[str] = None
# 文件名
@@ -58,7 +62,7 @@ class FileItem(FileURI):
# 修改时间
modify_time: Optional[float] = None
# 子节点
children: Optional[list] = Field(default_factory=list)
children: Optional[list["FileItem"]] = Field(default_factory=list)
# ID
fileid: Optional[str] = None
# 父ID
@@ -74,6 +78,8 @@ class FileItem(FileURI):
class StorageUsage(BaseModel):
"""存储空间使用情况。"""
# 总空间
total: float = 0.0
# 剩余空间
@@ -81,5 +87,7 @@ class StorageUsage(BaseModel):
class StorageTransType(BaseModel):
"""存储支持的传输类型及其显示名称。"""
# 传输类型
transtype: Optional[dict] = Field(default_factory=dict)
transtype: Optional[dict[str, str]] = Field(default_factory=dict)
+12 -2
View File
@@ -1,7 +1,8 @@
from typing import Optional, Any
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
from app.schemas.common import JsonData
from app.schemas.media import OptionalMediaIdentityMixin
from app.schemas.types import MediaSource
@@ -52,7 +53,7 @@ class DownloadHistory(OptionalMediaIdentityMixin, BaseModel):
# 创建时间
date: Optional[str] = None
# 备注
note: Optional[Any] = None
note: Optional[JsonData] = None
# 自定义媒体类别
media_category: Optional[str] = None
# 自定义剧集组
@@ -121,4 +122,13 @@ class TransferHistory(OptionalMediaIdentityMixin, BaseModel):
class BatchTransferHistoryRedoRequest(BaseModel):
"""批量重新整理历史请求。"""
history_ids: list[int] = Field(default_factory=list)
class TransferHistoryPage(BaseModel):
"""整理历史分页数据。"""
list: List[TransferHistory] = Field(default_factory=list, description="整理历史列表")
total: int = Field(default=0, description="记录总数")
+115
View File
@@ -0,0 +1,115 @@
"""LLM 配置、目录和测试 API 输出模型。"""
from typing import Optional
from pydantic import BaseModel, Field
class LLMAuthStatus(BaseModel):
"""LLM 提供商授权状态摘要。"""
connected: bool = False
type: Optional[str] = None
label: Optional[str] = None
expires_at: Optional[int | float | str] = None
updated_at: Optional[int | float | str] = None
class LLMServerToolCapability(BaseModel):
"""模型支持的服务端工具能力。"""
id: str
required_api_protocol: Optional[str] = None
client_adapter: Optional[str] = None
class LLMModelInfo(BaseModel):
"""标准化 LLM 模型目录项。"""
id: str
name: str
family: Optional[str] = None
context_tokens: Optional[int] = None
input_tokens: Optional[int] = None
output_tokens: Optional[int] = None
context_tokens_k: Optional[int] = None
supports_reasoning: bool = False
supports_tools: bool = False
supports_image_input: bool = False
supports_audio_input: bool = False
transport: Optional[str] = None
source: Optional[str] = None
release_date: Optional[str] = None
status: Optional[str] = None
server_tools: list[LLMServerToolCapability] = Field(default_factory=list)
class LLMModelCatalogData(BaseModel):
"""指定提供商的模型目录。"""
provider: str
models: list[LLMModelInfo] = Field(default_factory=list)
auth_status: LLMAuthStatus
class LLMProviderAuthMethod(BaseModel):
"""LLM 提供商可用的交互授权方式。"""
id: str
type: str
label: str
description: Optional[str] = None
class LLMProviderBaseUrlPreset(BaseModel):
"""LLM 提供商预设基础地址。"""
id: str
label: str
value: str
runtime: Optional[str] = None
model_list_strategy: Optional[str] = None
class LLMProviderInfo(BaseModel):
"""前端可配置的 LLM 提供商定义。"""
id: str
name: str
runtime: str
default_base_url: str = ""
base_url_presets: list[LLMProviderBaseUrlPreset] = Field(default_factory=list)
base_url_editable: bool = True
requires_base_url: bool = False
supports_api_key: bool = True
api_key_label: Optional[str] = None
api_key_hint: Optional[str] = None
supports_model_refresh: bool = True
oauth_methods: list[LLMProviderAuthMethod] = Field(default_factory=list)
description: Optional[str] = None
auth_status: LLMAuthStatus
class LLMProviderAuthSession(BaseModel):
"""LLM 提供商交互授权会话。"""
session_id: str
provider_id: Optional[str] = None
flow_type: Optional[str] = None
status: Optional[str] = None
message: Optional[str] = None
authorize_url: Optional[str] = None
verification_url: Optional[str] = None
user_code: Optional[str] = None
instructions: Optional[str] = None
interval_seconds: Optional[int] = None
expires_at: Optional[int | float] = None
class LLMTestResult(BaseModel):
"""LLM 连通性测试结果。"""
provider: str
model: str
duration_ms: Optional[int] = None
reply_preview: Optional[str] = None
+185 -8
View File
@@ -1,16 +1,193 @@
from typing import Any, Dict, Optional
from typing import Dict, Literal, Optional, TypeAlias, Union
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, RootModel, TypeAdapter
from app.schemas.common import JsonData
class ToolCallRequest(BaseModel):
"""工具调用请求模型"""
tool_name: str = Field(..., description="工具名称")
arguments: Dict[str, Any] = Field(default_factory=dict, description="工具参数")
arguments: Dict[str, JsonData] = Field(default_factory=dict, description="工具参数")
class ToolCallResponse(BaseModel):
"""工具调用响应模型"""
success: bool = Field(..., description="是否成功")
result: Optional[str] = Field(None, description="工具执行结果")
error: Optional[str] = Field(None, description="错误信息")
class McpJsonSchema(RootModel[dict[str, JsonData]]):
"""MCP 工具的 JSON Schema。"""
class McpToolInfo(BaseModel):
"""MCP REST 工具摘要。"""
name: str = Field(description="工具名称")
description: str = Field(default="", description="工具说明")
inputSchema: McpJsonSchema = Field(description="工具参数 JSON Schema")
class ToolCallData(BaseModel):
"""MCP REST 工具调用成功后的业务数据。"""
result: str = Field(description="工具执行结果")
class McpJsonRpcClientInfo(BaseModel):
"""MCP JSON-RPC 客户端信息。"""
name: str
version: Optional[str] = None
class McpJsonRpcInitializeParams(BaseModel):
"""MCP initialize 请求参数。"""
protocolVersion: str
capabilities: dict[str, JsonData] = Field(default_factory=dict)
clientInfo: Optional[McpJsonRpcClientInfo] = None
class McpJsonRpcToolCallParams(BaseModel):
"""MCP tools/call 请求参数。"""
name: str
arguments: dict[str, JsonData] = Field(default_factory=dict)
class McpJsonRpcInitializeRequest(BaseModel):
"""MCP initialize JSON-RPC 请求。"""
jsonrpc: Literal["2.0"]
id: str | int
method: Literal["initialize"]
params: McpJsonRpcInitializeParams
class McpJsonRpcInitializedNotification(BaseModel):
"""MCP initialized JSON-RPC 通知。"""
jsonrpc: Literal["2.0"]
method: Literal["notifications/initialized"]
params: dict[str, JsonData] = Field(default_factory=dict)
class McpJsonRpcToolsListRequest(BaseModel):
"""MCP tools/list JSON-RPC 请求。"""
jsonrpc: Literal["2.0"]
id: str | int
method: Literal["tools/list"]
params: dict[str, JsonData] = Field(default_factory=dict)
class McpJsonRpcToolsCallRequest(BaseModel):
"""MCP tools/call JSON-RPC 请求。"""
jsonrpc: Literal["2.0"]
id: str | int
method: Literal["tools/call"]
params: McpJsonRpcToolCallParams
class McpJsonRpcPingRequest(BaseModel):
"""MCP ping JSON-RPC 请求。"""
jsonrpc: Literal["2.0"]
id: str | int
method: Literal["ping"]
params: dict[str, JsonData] = Field(default_factory=dict)
McpJsonRpcRequest: TypeAlias = Union[
McpJsonRpcInitializeRequest,
McpJsonRpcInitializedNotification,
McpJsonRpcToolsListRequest,
McpJsonRpcToolsCallRequest,
McpJsonRpcPingRequest,
]
class McpJsonRpcServerInfo(BaseModel):
"""MCP 服务器信息。"""
name: str
version: str
description: Optional[str] = None
class McpJsonRpcToolsCapability(BaseModel):
"""MCP 工具能力声明。"""
listChanged: bool = False
class McpJsonRpcCapabilities(BaseModel):
"""MCP 服务器能力声明。"""
tools: McpJsonRpcToolsCapability
logging: dict[str, JsonData] = Field(default_factory=dict)
class McpJsonRpcInitializeResult(BaseModel):
"""MCP initialize 响应结果。"""
protocolVersion: str
capabilities: McpJsonRpcCapabilities
serverInfo: McpJsonRpcServerInfo
instructions: str
class McpJsonRpcToolsListResult(BaseModel):
"""MCP tools/list 响应结果。"""
tools: list[McpToolInfo] = Field(default_factory=list)
class McpJsonRpcTextContent(BaseModel):
"""MCP 工具调用文本内容块。"""
type: Literal["text"] = "text"
text: str
class McpJsonRpcToolCallResult(BaseModel):
"""MCP tools/call 响应结果。"""
content: list[McpJsonRpcTextContent] = Field(default_factory=list)
isError: bool = False
class McpJsonRpcEmptyResult(BaseModel):
"""MCP ping 的空结果。"""
class McpJsonRpcSuccess(BaseModel):
"""MCP JSON-RPC 成功响应。"""
jsonrpc: Literal["2.0"] = "2.0"
id: Optional[str | int] = None
result: Union[
McpJsonRpcInitializeResult,
McpJsonRpcToolsListResult,
McpJsonRpcToolCallResult,
McpJsonRpcEmptyResult,
]
class McpJsonRpcErrorDetail(BaseModel):
"""MCP JSON-RPC 错误详情。"""
code: int
message: str
data: Optional[JsonData] = None
class McpJsonRpcError(BaseModel):
"""MCP JSON-RPC 错误响应。"""
jsonrpc: Literal["2.0"] = "2.0"
id: Optional[str | int] = None
error: McpJsonRpcErrorDetail
McpJsonRpcResponse: TypeAlias = Union[McpJsonRpcSuccess, McpJsonRpcError]
# 保持协议端点自行解析并返回 JSON-RPC 错误,同时从同一组 Pydantic 模型生成请求文档。
MCP_JSONRPC_REQUEST_SCHEMA = TypeAdapter(McpJsonRpcRequest).json_schema()
+30 -8
View File
@@ -1,8 +1,9 @@
from pathlib import Path
from typing import Optional, Dict, Union, List, Any
from pydantic import BaseModel, Field, ConfigDict, model_validator
from pydantic import BaseModel, Field, ConfigDict, RootModel, model_validator
from app.schemas.common import JsonData
from app.schemas.media import OptionalMediaIdentityMixin
from app.schemas.types import MediaSource, MediaType
@@ -14,7 +15,7 @@ class ExistMediaInfo(BaseModel):
# 类型 电影、电视剧、音乐
type: Optional[MediaType] = None
# 季
seasons: Optional[Dict[int, list]] = Field(default_factory=dict)
seasons: Optional[Dict[int, List[int]]] = Field(default_factory=dict)
# 媒体服务器类型:plex、jellyfin、emby、zspace、trimemedia、ugreen、navidrome
server_type: Optional[str] = None
# 媒体服务器名称
@@ -23,6 +24,25 @@ class ExistMediaInfo(BaseModel):
itemid: Optional[Union[str, int]] = None
class MediaServerPlayData(BaseModel):
"""媒体服务器在线播放地址。"""
url: str = Field(description="播放地址")
item_id: Optional[str] = Field(default=None, description="媒体项目 ID")
server_id: Optional[str] = Field(default=None, description="媒体服务器 ID")
server_type: Optional[str] = Field(default=None, description="媒体服务器类型")
class MediaServerExistsData(BaseModel):
"""本地媒体存在性查询结果。"""
item: Dict[str, str] = Field(default_factory=dict, description="命中的媒体项目")
class MediaServerExistingEpisodes(RootModel[Dict[int, List[int]]]):
"""媒体服务器中按季号归组的已存在集号。"""
class NotExistMediaInfo(BaseModel):
"""
媒体服务器不存在媒体信息
@@ -30,7 +50,7 @@ class NotExistMediaInfo(BaseModel):
# 季
season: Optional[int] = None
# 剧集列表
episodes: Optional[list] = Field(default_factory=list)
episodes: Optional[List[int]] = Field(default_factory=list)
# 总集数
total_episode: Optional[int] = 0
# 开始集
@@ -70,7 +90,7 @@ class MediaServerLibrary(BaseModel):
# 名称
name: Optional[str] = None
# 路径
path: Optional[Union[str, list]] = None
path: Optional[Union[str, List[str]]] = None
# 类型
type: Optional[str] = None
# 媒体库内媒体数量
@@ -88,6 +108,8 @@ class MediaServerLibrary(BaseModel):
class MediaServerItemUserState(BaseModel):
"""媒体服务器条目的用户播放状态。"""
# 已播放
played: Optional[bool] = None
# 继续播放
@@ -128,9 +150,9 @@ class MediaServerItem(OptionalMediaIdentityMixin, BaseModel):
# 路径
path: Optional[str] = None
# 季集
seasoninfo: Optional[Dict[int, list]] = None
seasoninfo: Optional[Dict[int, List[int]]] = None
# 备注
note: Optional[Any] = None
note: Optional[JsonData] = None
# 同步时间
lst_mod_date: Optional[str] = None
user_state: Optional[MediaServerItemUserState] = None
@@ -172,7 +194,7 @@ class WebhookEventInfo(BaseModel):
save_reason: Optional[str] = None
item_isvirtual: Optional[bool] = None
media_type: Optional[str] = None
json_object: Optional[dict] = Field(default_factory=dict)
json_object: Optional[dict[str, JsonData]] = Field(default_factory=dict)
@model_validator(mode="before")
@classmethod
@@ -236,7 +258,7 @@ class MediaServerPlayItem(BaseModel):
image: Optional[str] = None
link: Optional[str] = None
percent: Optional[float] = None
BackdropImageTags: Optional[list] = Field(default_factory=list)
BackdropImageTags: Optional[List[str]] = Field(default_factory=list)
server_type: Optional[str] = None
# 飞牛的图片需要Cookies
use_cookies: Optional[bool] = None
+16 -5
View File
@@ -4,6 +4,7 @@ from typing import Optional, Union, List, Dict, Set, Any
from pydantic import BaseModel, Field, field_validator
from app.schemas.common import JsonData
from app.schemas.types import ContentType, NotificationType, MessageChannel
@@ -47,7 +48,7 @@ class MessageResponse(BaseModel):
# 消息来源
source: Optional[str] = None
# 渠道自定义上下文(如飞书流式卡片 card_id/element_id/sequence
metadata: Optional[Dict[str, Any]] = None
metadata: Optional[Dict[str, JsonData]] = None
# 是否发送成功
success: bool = False
@@ -80,7 +81,17 @@ class NotificationHistoryItem(BaseModel):
# 消息方向:0-接收消息,1-发送消息
action: Optional[int] = None
# 附件json
note: Optional[Union[list, dict]] = None
note: Optional[JsonData] = None
class WebMessageItem(NotificationHistoryItem):
"""Web 消息历史记录。"""
class NotificationClearData(BaseModel):
"""通知中心各范围的清理时间。"""
clear_before: NotificationClearBefore = Field(description="各范围清理时间")
class CommingMessage(BaseModel):
@@ -300,7 +311,7 @@ class Subscription(BaseModel):
"""
endpoint: Optional[str] = None
keys: Optional[dict] = Field(default_factory=dict)
keys: Optional[dict[str, str]] = Field(default_factory=dict)
class SubscriptionMessage(BaseModel):
@@ -312,7 +323,7 @@ class SubscriptionMessage(BaseModel):
body: Optional[str] = None
icon: Optional[str] = None
url: Optional[str] = None
data: Optional[dict] = Field(default_factory=dict)
data: Optional[dict[str, JsonData]] = Field(default_factory=dict)
class AgentWebChatRequest(BaseModel):
@@ -345,7 +356,7 @@ class AgentWebChatRequest(BaseModel):
# 文件附件列表
files: Optional[List[AgentWebChatFile]] = Field(default_factory=list)
# 用户通过按钮选择时的完整选择快照
choice_selection: Optional[Dict[str, Any]] = Field(default=None)
choice_selection: Optional[Dict[str, JsonData]] = Field(default=None)
# WebAgent 按钮回调关联的原消息 ID,用于传统交互原地编辑卡片
original_message_id: Optional[Union[str, int]] = Field(default=None)
# WebAgent 按钮回调关联的原聊天 ID,用于传统交互原地编辑卡片
+42
View File
@@ -0,0 +1,42 @@
"""多因素认证 API 输出模型。"""
from typing import Optional
from pydantic import BaseModel, Field, RootModel
from app.schemas.common import JsonData
class PasskeyOptions(RootModel[dict[str, JsonData]]):
"""浏览器 WebAuthn API 使用的动态选项。"""
class OtpGenerateData(BaseModel):
"""OTP 绑定密钥和验证 URI。"""
secret: str = Field(description="OTP 密钥")
uri: str = Field(description="OTP 验证 URI")
class MfaStatusData(BaseModel):
"""用户是否启用多因素认证。"""
enabled: bool = Field(description="是否启用多因素认证")
class PasskeyStartData(BaseModel):
"""PassKey 注册或认证的启动数据。"""
options: PasskeyOptions = Field(description="WebAuthn 选项")
transaction_token: str = Field(description="一次性事务令牌")
class PasskeyInfo(BaseModel):
"""当前用户绑定的 PassKey 摘要。"""
id: int
name: str
created_at: Optional[str] = None
last_used_at: Optional[str] = None
aaguid: Optional[str] = None
transports: Optional[str] = None
+27 -4
View File
@@ -1,7 +1,8 @@
from typing import Any, Literal, Optional, Union
from typing import Literal, Optional, Union
from pydantic import BaseModel, Field
from app.schemas.common import JsonData
from app.schemas.media import OptionalMediaIdentityMixin, RequiredMediaIdentityMixin
from app.schemas.types import MediaSource, MusicEntityType, MusicTargetEntityType
@@ -75,7 +76,7 @@ class MusicInfo(OptionalMediaIdentityMixin, BaseModel):
names: list[str] = Field(default_factory=list)
detail_link: Optional[str] = None
listen_count: Optional[int] = None
raw_data: dict[str, Any] = Field(default_factory=dict)
raw_data: dict[str, JsonData] = Field(default_factory=dict)
title_year: Optional[str] = None
poster_path: Optional[str] = None
backdrop_path: Optional[str] = None
@@ -125,7 +126,7 @@ class MusicAlbumInfo(OptionalMediaIdentityMixin, BaseModel):
detail_link: Optional[str] = None
tracks: list[MusicInfo] = Field(default_factory=list)
releases: list[MusicRelease] = Field(default_factory=list)
raw_data: dict[str, Any] = Field(default_factory=dict)
raw_data: dict[str, JsonData] = Field(default_factory=dict)
title_year: Optional[str] = None
poster_path: Optional[str] = None
backdrop_path: Optional[str] = None
@@ -160,7 +161,7 @@ class MusicArtistInfo(OptionalMediaIdentityMixin, BaseModel):
detail_link: Optional[str] = None
external_links: dict[str, str] = Field(default_factory=dict)
album_count: Optional[int] = None
raw_data: dict[str, Any] = Field(default_factory=dict)
raw_data: dict[str, JsonData] = Field(default_factory=dict)
poster_path: Optional[str] = None
overview: Optional[str] = None
@@ -171,3 +172,25 @@ class MusicRecognizeRequest(RequiredMediaIdentityMixin, BaseModel):
media_source: MediaSource
media_id: str
music_type: Optional[MusicTargetEntityType] = None
class MusicRecognitionCacheItem(BaseModel):
"""单条 MusicBrainz 识别缓存。"""
key: str
media_id: str = ""
title: str = ""
artists: list[str] = Field(default_factory=list)
album: str = ""
year: str | int = ""
music_type: str = "recording"
cover_url: str = ""
class MusicRecognitionCacheData(BaseModel):
"""MusicBrainz 识别缓存统计及明细。"""
count: int = 0
recognized: int = 0
unrecognized: int = 0
data: list[MusicRecognitionCacheItem] = Field(default_factory=list)
+29
View File
@@ -0,0 +1,29 @@
"""通知渠道 API 输出模型。"""
from typing import Optional
from pydantic import BaseModel, Field
class WechatClawBotKnownTarget(BaseModel):
"""微信 ClawBot 已知消息目标。"""
userid: str
username: str
last_active: Optional[int | float] = None
class WechatClawBotData(BaseModel):
"""微信 ClawBot 登录状态或操作结果。"""
success: bool
message: Optional[str] = None
connected: Optional[bool] = None
account_id: Optional[str] = None
qrcode: Optional[str] = None
qrcode_url: Optional[str] = None
qrcode_status: Optional[str] = None
qrcode_updated_at: Optional[int | float] = None
known_targets: list[WechatClawBotKnownTarget] = Field(default_factory=list)
default_target: Optional[str] = None
base_url: Optional[str] = None
+44 -9
View File
@@ -1,7 +1,9 @@
from typing import Any, Dict, List, Optional
from typing import Dict, List, Optional
from pydantic import BaseModel, ConfigDict, Field
from app.schemas.common import JsonData
class OpenAIModelInfo(BaseModel):
id: str
@@ -16,14 +18,18 @@ class OpenAIModelListResponse(BaseModel):
class OpenAIChatMessage(BaseModel):
"""OpenAI Chat Completions 请求中的一条消息。"""
role: str
content: Any
content: JsonData
name: Optional[str] = None
model_config = ConfigDict(extra="allow")
class OpenAIChatCompletionsRequest(BaseModel):
"""OpenAI Chat Completions 兼容请求。"""
model: Optional[str] = None
messages: List[OpenAIChatMessage]
user: Optional[str] = None
@@ -33,8 +39,10 @@ class OpenAIChatCompletionsRequest(BaseModel):
class OpenAIResponsesRequest(BaseModel):
"""OpenAI Responses API 兼容请求。"""
model: Optional[str] = None
input: Any
input: JsonData
instructions: Optional[str] = None
user: Optional[str] = None
stream: bool = False
@@ -69,9 +77,30 @@ class OpenAIChatCompletionResponse(BaseModel):
class OpenAIResponsesOutputText(BaseModel):
"""Responses API 输出中的文本内容块。"""
type: str = "output_text"
text: str
annotations: List[Dict[str, Any]] = Field(default_factory=list)
annotations: List["OpenAIResponseAnnotation"] = Field(default_factory=list)
class OpenAIResponseAnnotation(BaseModel):
"""Responses API 文本内容关联的引用或文件注解。"""
type: str
index: Optional[int] = None
start_index: Optional[int] = None
end_index: Optional[int] = None
url: Optional[str] = None
title: Optional[str] = None
file_id: Optional[str] = None
filename: Optional[str] = None
class OpenAIIncompleteDetails(BaseModel):
"""Responses API 未完整结束时的原因。"""
reason: str
class OpenAIResponsesOutputMessage(BaseModel):
@@ -83,14 +112,16 @@ class OpenAIResponsesOutputMessage(BaseModel):
class OpenAIResponsesResponse(BaseModel):
"""OpenAI Responses API 的非流式成功响应。"""
id: str
object: str = "response"
created_at: int
status: str = "completed"
model: str
output: List[OpenAIResponsesOutputMessage] = Field(default_factory=list)
error: Optional[Any] = None
incomplete_details: Optional[Any] = None
error: Optional["OpenAIErrorDetail"] = None
incomplete_details: Optional[OpenAIIncompleteDetails] = None
usage: OpenAIUsage
@@ -105,20 +136,24 @@ class OpenAIErrorResponse(BaseModel):
error: OpenAIErrorDetail
OpenAIChatContentPart = Dict[str, Any]
OpenAIChatContentPart = Dict[str, JsonData]
class AnthropicMessage(BaseModel):
"""Anthropic Messages 请求中的一条消息。"""
role: str
content: Any
content: JsonData
model_config = ConfigDict(extra="allow")
class AnthropicMessagesRequest(BaseModel):
"""Anthropic Messages 兼容请求。"""
model: Optional[str] = None
messages: List[AnthropicMessage]
system: Optional[Any] = None
system: Optional[JsonData] = None
max_tokens: Optional[int] = 1024
stream: bool = False
+55 -7
View File
@@ -1,6 +1,8 @@
from typing import Optional, List, Dict, Any
from typing import Optional, List, Dict
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, RootModel
from app.schemas.common import JsonData
class Plugin(BaseModel):
@@ -51,7 +53,7 @@ class Plugin(BaseModel):
# 安装次数
install_count: Optional[int] = 0
# 更新记录
history: Optional[dict] = Field(default_factory=dict)
history: Optional[dict[str, str]] = Field(default_factory=dict)
# 添加时间,值越小表示越靠后发布
add_time: Optional[int] = 0
# 插件公钥
@@ -70,11 +72,11 @@ class PluginDashboard(Plugin):
# 演染模式
render_mode: Optional[str] = Field(default="vuetify")
# 全局配置
attrs: Optional[dict] = Field(default_factory=dict)
attrs: Optional[dict[str, JsonData]] = Field(default_factory=dict)
# col列数
cols: Optional[dict] = Field(default_factory=dict)
cols: Optional[dict[str, JsonData]] = Field(default_factory=dict)
# 页面元素
elements: Optional[List[dict]] = Field(default_factory=list)
elements: Optional[List[dict[str, JsonData]]] = Field(default_factory=list)
class PluginSidebarNavItem(BaseModel):
@@ -115,6 +117,10 @@ class PluginRating(BaseModel):
user_rating: Optional[float] = Field(default=None, description="当前安装实例评分")
class PluginRatingMap(RootModel[Dict[str, PluginRating]]):
"""插件 ID 与评分结果的映射。"""
class PluginMemoryInfo(BaseModel):
"""插件内存信息"""
plugin_id: str = Field(description="插件ID")
@@ -126,4 +132,46 @@ class PluginMemoryInfo(BaseModel):
calculation_time_ms: float = Field(description="计算耗时(毫秒)")
timestamp: float = Field(description="统计时间戳")
error: Optional[str] = Field(default=None, description="错误信息")
object_details: Optional[List[Dict[str, Any]]] = Field(default=None, description="大对象详情")
object_details: Optional[List[Dict[str, JsonData]]] = Field(default=None, description="大对象详情")
class PluginRemoteInfo(BaseModel):
"""插件模块联邦远程入口。"""
id: str
url: str
name: str
class PluginReleaseItem(BaseModel):
"""可安装的插件 Release 版本。"""
version: str
tag_name: str
name: str
published_at: Optional[str] = None
body: str = ""
asset_name: str
is_latest: bool = False
is_current: bool = False
class PluginReleaseData(BaseModel):
"""插件 Release 能力与版本列表。"""
release_supported: bool = False
latest_version: Optional[str] = None
current_version: Optional[str] = None
items: List[PluginReleaseItem] = Field(default_factory=list)
class PluginFoldersData(RootModel[Dict[str, List[str]]]):
"""插件文件夹与插件 ID 列表映射。"""
class PluginDashboardMetaItem(BaseModel):
"""插件仪表板入口摘要。"""
id: str
name: Optional[str] = None
key: Optional[str] = None
+37 -18
View File
@@ -1,29 +1,48 @@
from typing import Any, Optional
from typing import Any, Generic, Optional, TypeVar
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, ConfigDict, field_validator
from app.helper.locale import LocaleHelper
class Response(BaseModel):
"""通用接口响应结构"""
DataT = TypeVar("DataT")
class Response(BaseModel, Generic[DataT]):
"""统一接口响应结构,仅允许业务数据类型随接口变化。"""
model_config = ConfigDict(
extra="forbid",
json_schema_extra={"required": ["success", "message", "data"]}
)
# 状态
success: bool
# 消息文本
message: Optional[str] = None
# 多语言消息文本
message_i18n: Optional[str] = None
message: str = ""
# 数据
data: Optional[Any] = Field(default_factory=dict)
data: Optional[DataT] = None
@model_validator(mode="after")
def fill_message_i18n(self) -> "Response":
"""
自动补充响应消息的多语言文本。
"""
if self.message and self.message_i18n is None:
self.message_i18n = LocaleHelper.translate_text(
self.message, locale=LocaleHelper.get_current_locale()
)
return self
@field_validator("message", mode="before")
@classmethod
def localize_message(cls, value: Any) -> str:
"""按当前请求语言直接本地化消息文本,并将空消息归一为空字符串。"""
if value is None:
return ""
message = str(value)
if not message:
return ""
return LocaleHelper.translate_text(
message, locale=LocaleHelper.get_current_locale()
)
class ValidationIssue(BaseModel):
"""请求参数校验失败时返回的单项错误信息。"""
# 参数位置
location: list[str | int]
# 错误说明
message: str
# 错误类型
error_type: str
+22
View File
@@ -0,0 +1,22 @@
"""搜索 API 输出模型。"""
from typing import Literal, Union
from pydantic import BaseModel, Field
from app.schemas.common import JsonData
from app.schemas.context import SubtitleInfo, TorrentInfo
class SearchLastContextData(BaseModel):
"""上一次搜索的请求参数与结果。"""
params: dict[str, JsonData] = Field(default_factory=dict)
results: list[Union[TorrentInfo, SubtitleInfo]] = Field(default_factory=list)
class SearchRecommendStatusData(BaseModel):
"""AI 搜索结果推荐任务状态。"""
status: Literal["disabled", "idle", "running", "completed", "error"]
results: list[int] = Field(default_factory=list)
+185 -6
View File
@@ -1,8 +1,185 @@
from typing import Optional
from pydantic import BaseModel, Field
class ServarrVersion(BaseModel):
"""Servarr 兼容接口使用的版本号结构。"""
major: int = 0
minor: int = 0
build: int = 0
revision: int = 0
majorRevision: int = 0
minorRevision: int = 0
class ServarrSystemStatus(BaseModel):
"""Servarr 系统状态响应。"""
appName: str
instanceName: str
version: str
buildTime: str
isDebug: bool
isProduction: bool
isAdmin: bool
isUserInteractive: bool
startupPath: str
appData: str
osName: str
osVersion: str
isNetCore: bool
isLinux: bool
isOsx: bool
isWindows: bool
isDocker: bool
mode: str
branch: str
databaseType: str
databaseVersion: ServarrVersion
authentication: str
migrationVersion: int
urlBase: str
runtimeVersion: ServarrVersion
runtimeName: str
startTime: str
packageVersion: str
packageAuthor: str
packageUpdateMechanism: str
packageUpdateMechanismMessage: str
class ServarrQuality(BaseModel):
"""Servarr 质量定义。"""
id: int
name: str
source: str
resolution: int
class ServarrQualityProfileItem(BaseModel):
"""Servarr 质量配置中的可选质量项。"""
id: int
name: str
quality: ServarrQuality
items: list[str] = Field(default_factory=list)
allowed: bool
class ServarrFormatItem(BaseModel):
"""Servarr 自定义格式评分项。"""
id: int
format: int
name: str
score: int
class ServarrQualityProfile(BaseModel):
"""Servarr 质量配置响应项。"""
id: int
name: str
upgradeAllowed: bool
cutoff: int
items: list[ServarrQualityProfileItem] = Field(default_factory=list)
minFormatScore: int
cutoffFormatScore: int
formatItems: list[ServarrFormatItem] = Field(default_factory=list)
class ServarrRootFolder(BaseModel):
"""Servarr 根目录响应项。"""
id: int
path: str
accessible: bool
freeSpace: int
unmappedFolders: list[str] = Field(default_factory=list)
class ServarrTag(BaseModel):
"""Servarr 标签响应项。"""
id: int
label: str
class ServarrLanguage(BaseModel):
"""Servarr 语言定义。"""
id: int
name: str
class ServarrLanguageProfileItem(BaseModel):
"""Servarr 语言配置中的可选语言项。"""
id: int
language: ServarrLanguage
allowed: bool
class ServarrLanguageProfile(BaseModel):
"""Servarr 语言配置响应项。"""
id: int
name: str
upgradeAllowed: bool
cutoff: ServarrLanguage
languages: list[ServarrLanguageProfileItem] = Field(default_factory=list)
class ServarrIdResponse(BaseModel):
"""Servarr 新增资源后返回的资源标识。"""
id: int
class ServarrImage(BaseModel):
"""Servarr 媒体图片。"""
coverType: Optional[str] = None
url: Optional[str] = None
remoteUrl: Optional[str] = None
class SonarrStatistics(BaseModel):
"""Sonarr 剧集或季度统计信息。"""
seasonCount: Optional[int] = None
episodeFileCount: Optional[int] = None
episodeCount: Optional[int] = None
totalEpisodeCount: Optional[int] = None
sizeOnDisk: Optional[int] = None
releaseGroups: list[str] = Field(default_factory=list)
percentOfEpisodes: Optional[float] = None
nextAiring: Optional[str] = None
previousAiring: Optional[str] = None
class SonarrSeason(BaseModel):
"""Sonarr 季度监控信息。"""
seasonNumber: Optional[int] = None
monitored: bool = False
statistics: Optional[SonarrStatistics] = None
images: list[ServarrImage] = Field(default_factory=list)
class SonarrRatings(BaseModel):
"""Sonarr 剧集评分信息。"""
votes: Optional[int] = None
value: Optional[float] = None
class RadarrMovie(BaseModel):
"""Radarr 兼容接口的电影结构。"""
id: Optional[int] = None
title: Optional[str] = None
year: Optional[str | int] = None
@@ -20,6 +197,8 @@ class RadarrMovie(BaseModel):
class SonarrSeries(BaseModel):
"""Sonarr 兼容接口的剧集结构。"""
id: Optional[int] = None
title: Optional[str] = None
sortTitle: Optional[str] = None
@@ -28,9 +207,9 @@ class SonarrSeries(BaseModel):
overview: Optional[str] = None
network: Optional[str] = None
airTime: Optional[str] = None
images: list = Field(default_factory=list)
images: list[ServarrImage] = Field(default_factory=list)
remotePoster: Optional[str] = None
seasons: list = Field(default_factory=list)
seasons: list[SonarrSeason] = Field(default_factory=list)
year: Optional[str | int] = None
path: Optional[str] = None
profileId: Optional[int] = None
@@ -49,11 +228,11 @@ class SonarrSeries(BaseModel):
cleanTitle: Optional[str] = None
titleSlug: Optional[str] = None
certification: Optional[str] = None
genres: list = Field(default_factory=list)
tags: list = Field(default_factory=list)
genres: list[str] = Field(default_factory=list)
tags: list[int] = Field(default_factory=list)
added: Optional[str] = None
ratings: Optional[dict] = None
ratings: Optional[SonarrRatings] = None
qualityProfileId: Optional[int] = None
statistics: dict = Field(default_factory=dict)
statistics: SonarrStatistics = Field(default_factory=SonarrStatistics)
isAvailable: Optional[bool] = False
hasFile: Optional[bool] = False
+31 -4
View File
@@ -1,11 +1,38 @@
from fastapi import Query
from pydantic import BaseModel
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
from app.schemas.common import JsonData
class CookieData(BaseModel):
encrypted: str = Query(min_length=1, max_length=1024 * 1024 * 50)
uuid: str = Query(min_length=5, pattern="^[a-zA-Z0-9]+$")
"""CookieCloud 上传的加密数据。"""
encrypted: str = Field(min_length=1, max_length=1024 * 1024 * 50)
uuid: str = Field(min_length=5, pattern="^[a-zA-Z0-9]+$")
class CookiePassword(BaseModel):
"""CookieCloud 下载并解密数据所需的密码。"""
password: str
class CookieActionResponse(BaseModel):
"""CookieCloud 上传操作结果。"""
action: Literal["done", "error"]
class CookieEncryptedPayload(BaseModel):
"""CookieCloud 保存和下载的加密载荷。"""
encrypted: str
class CookieDecryptedPayload(BaseModel):
"""CookieCloud 解密后的 Cookie 数据载荷。"""
model_config = ConfigDict(extra="allow")
cookie_data: JsonData
+35 -6
View File
@@ -1,9 +1,20 @@
from typing import Optional, Any, Union, Dict
from typing import Optional, Union, Dict
from pydantic import BaseModel, Field, ConfigDict
from pydantic import BaseModel, Field, ConfigDict, RootModel
from app.schemas.common import JsonData
SiteUnreadMessage = Union[
tuple[Optional[str], Optional[str], Optional[str]],
tuple[Optional[str], Optional[str], Optional[str], Optional[str]],
]
"""站点未读消息,第四项为部分站点提供的持久化去重来源。"""
class Site(BaseModel):
"""站点配置及运行状态。"""
# ID
id: Optional[int] = None
# 站点名称
@@ -33,7 +44,7 @@ class Site(BaseModel):
# 是否公开站点
public: Optional[int] = 0
# 备注
note: Optional[Any] = None
note: Optional[JsonData] = None
# 超时时间
timeout: Optional[int] = 15
# 流控单位周期
@@ -51,6 +62,8 @@ class Site(BaseModel):
class SiteStatistic(BaseModel):
"""单个站点的访问成功率与耗时统计。"""
# 站点ID
domain: Optional[str] = None
# 成功次数
@@ -64,12 +77,14 @@ class SiteStatistic(BaseModel):
# 最后修改时间
lst_mod_date: Optional[str] = None
# 备注
note: Optional[Any] = None
note: Optional[Dict[str, int]] = None
model_config = ConfigDict(from_attributes=True)
class SiteUserData(BaseModel):
"""站点用户账户、流量、做种和未读消息数据。"""
# 站点域名
domain: Optional[str] = None
# 用户名
@@ -97,11 +112,11 @@ class SiteUserData(BaseModel):
# 下载体积
leeching_size: Optional[int] = 0
# 做种人数, 种子大小
seeding_info: Optional[list] = Field(default_factory=list)
seeding_info: Optional[list[tuple[int, int]]] = Field(default_factory=list)
# 未读消息
message_unread: Optional[int] = 0
# 未读消息内容
message_unread_contents: Optional[list] = Field(default_factory=list)
message_unread_contents: Optional[list[SiteUnreadMessage]] = Field(default_factory=list)
# 错误信息
err_msg: Optional[str] = None
# 更新日期
@@ -111,6 +126,8 @@ class SiteUserData(BaseModel):
class SiteAuth(BaseModel):
"""站点认证模块及其参数。"""
site: Optional[str] = None
params: Optional[Dict[str, Union[int, str]]] = Field(default_factory=dict)
@@ -125,6 +142,18 @@ class SiteCookieUpdate(BaseModel):
class SiteCategory(BaseModel):
"""站点资源分类。"""
id: Optional[int] = None
cat: Optional[str] = None
desc: Optional[str] = None
class SiteIconData(BaseModel):
"""站点图标地址或 Base64 内容。"""
icon: str
class SiteMappingData(RootModel[dict[str, str]]):
"""站点域名到显示名称的映射。"""
+26
View File
@@ -0,0 +1,26 @@
"""存储授权 API 输出模型。"""
from typing import Optional
from pydantic import BaseModel, Field
class StorageQrCodeData(BaseModel):
"""云存储扫码授权二维码。"""
codeContent: Optional[str] = Field(default=None, description="二维码原始内容")
codeUrl: Optional[str] = Field(default=None, description="二维码图片地址")
class StorageAuthUrlData(BaseModel):
"""云存储 OAuth 授权入口。"""
authUrl: str = Field(description="授权地址")
state: str = Field(description="授权状态校验值")
class StorageLoginStatusData(BaseModel):
"""云存储扫码或 OAuth 登录状态。"""
status: int | str = Field(description="授权状态")
tip: str = Field(description="状态提示")
+13 -1
View File
@@ -110,7 +110,7 @@ class Subscribe(OptionalMediaIdentityMixin, BaseModel):
# 已完成集数
completed_episode: Optional[int] = None
# 附加信息
note: Optional[Any] = None
note: Optional[List[int]] = None
# 状态:N-新建, R-订阅中
state: Optional[str] = None
# 最后更新时间
@@ -197,6 +197,8 @@ class Subscribe(OptionalMediaIdentityMixin, BaseModel):
class SubscribeShare(OptionalMediaIdentityMixin, BaseModel):
"""可供其他用户复用的订阅分享信息。"""
# 分享ID
id: Optional[int] = None
# 订阅ID
@@ -268,6 +270,8 @@ class SubscribeShare(OptionalMediaIdentityMixin, BaseModel):
class SubscribeShareStatistics(BaseModel):
"""单个用户的订阅分享数量与复用统计。"""
# 分享人
share_user: Optional[str] = None
# 分享数量
@@ -277,6 +281,8 @@ class SubscribeShareStatistics(BaseModel):
class SubscribeDownloadFileInfo(BaseModel):
"""订阅剧集关联的下载文件信息。"""
# 种子名称
torrent_title: Optional[str] = None
# 站点名称
@@ -290,6 +296,8 @@ class SubscribeDownloadFileInfo(BaseModel):
class SubscribeLibraryFileInfo(BaseModel):
"""订阅剧集关联的媒体库文件信息。"""
# 存储
storage: Optional[str] = "local"
# 文件路径
@@ -303,6 +311,8 @@ class SubscribeLibraryFileInfo(BaseModel):
class SubscribeEpisodeInfo(BaseModel):
"""订阅单集的元数据及关联文件。"""
# 标题
title: Optional[str] = None
# 描述
@@ -316,6 +326,8 @@ class SubscribeEpisodeInfo(BaseModel):
class SubscrbieInfo(BaseModel):
"""订阅详情及按集号归组的文件信息。"""
# 订阅信息
subscribe: Optional[Subscribe] = None
# 集信息 {集号: {download: 文件路径,library: 文件路径, backdrop: url, title: 标题, description: 描述}}
+58
View File
@@ -3,6 +3,9 @@ from typing import Optional, Any
from pydantic import BaseModel, Field, field_validator
from app.schemas.context import MediaInfo, MetaInfo, TorrentInfo
from app.schemas.rule import FilterRuleGroup
@dataclass
class ServiceInfo:
@@ -132,6 +135,61 @@ class StorageConf(BaseModel):
config: Optional[dict] = Field(default_factory=dict)
class SystemEnvironmentUpdateData(BaseModel):
"""环境配置更新的成功项和失败项。"""
success_updates: dict[str, tuple[Optional[bool], str]] = Field(default_factory=dict)
failed_updates: dict[str, tuple[Optional[bool], str]] = Field(default_factory=dict)
class PluginMarketSyncData(BaseModel):
"""Wiki 插件市场仓库同步结果。"""
value: str
repos: list[str] = Field(default_factory=list)
wiki_repos: list[str] = Field(default_factory=list)
added_count: int = 0
total_count: int = 0
source_url: str
class RuleTestData(BaseModel):
"""过滤规则测试的输入、识别和匹配明细。"""
title: str
subtitle: Optional[str] = None
rulegroup_name: str
rulegroup: Optional[FilterRuleGroup] = None
meta_info: MetaInfo
media_info: Optional[MediaInfo] = None
torrent_info: TorrentInfo
priority: Optional[int] = None
matched: bool = False
class NetTestTarget(BaseModel):
"""前端可选择的网络测试目标。"""
id: str
name: str
icon: str
class SystemModuleInfo(BaseModel):
"""已加载系统模块摘要。"""
id: str
name: str
name_i18n: str
name_key: str
class SystemModuleListData(BaseModel):
"""已加载系统模块列表。"""
modules: list[SystemModuleInfo] = Field(default_factory=list)
class TransferDirectoryConf(BaseModel):
"""
文件整理目录配置
+53 -2
View File
@@ -16,6 +16,34 @@ class TmdbSeason(BaseModel):
vote_average: Optional[float] = None
class TmdbEpisodeCredit(BaseModel):
"""TMDB 剧集演职人员的公共信息。"""
adult: Optional[bool] = None
gender: Optional[int] = None
id: Optional[int] = None
known_for_department: Optional[str] = None
name: Optional[str] = None
original_name: Optional[str] = None
popularity: Optional[float] = None
profile_path: Optional[str] = None
credit_id: Optional[str] = None
class TmdbEpisodeCrew(TmdbEpisodeCredit):
"""TMDB 剧集幕后人员信息。"""
department: Optional[str] = None
job: Optional[str] = None
class TmdbEpisodeGuestStar(TmdbEpisodeCredit):
"""TMDB 剧集客串演员信息。"""
character: Optional[str] = None
order: Optional[int] = None
class TmdbEpisode(BaseModel):
"""
TMDB集信息
@@ -29,5 +57,28 @@ class TmdbEpisode(BaseModel):
season_number: Optional[int] = None
still_path: Optional[str] = None
vote_average: Optional[float] = None
crew: Optional[list] = Field(default_factory=list)
guest_stars: Optional[list] = Field(default_factory=list)
crew: Optional[list[TmdbEpisodeCrew]] = Field(default_factory=list)
guest_stars: Optional[list[TmdbEpisodeGuestStar]] = Field(default_factory=list)
class TmdbRecognitionCacheItem(BaseModel):
"""单条 TMDB 识别缓存。"""
key: str
tmdb_id: int = 0
title: str = ""
year: str | int = ""
media_type: str = "unknown"
poster_path: str = ""
backdrop_path: str = ""
class TmdbRecognitionCacheData(BaseModel):
"""TMDB 识别缓存统计及明细。"""
count: int = 0
recognized: int = 0
unrecognized: int = 0
shared_recognized: int = 0
shared_recognize_enabled: bool = False
data: list[TmdbRecognitionCacheItem] = Field(default_factory=list)
+12 -1
View File
@@ -3,7 +3,16 @@ from typing import Optional
from pydantic import BaseModel, Field
class MfaChallenge(BaseModel):
"""密码认证通过后需要继续完成的二次验证信息。"""
# 可用的二次验证方式
mfa_methods: list[str] = Field(default_factory=list)
class Token(BaseModel):
"""OAuth2 登录成功后返回的访问令牌。"""
# 令牌
access_token: str
# 令牌类型
@@ -19,12 +28,14 @@ class Token(BaseModel):
# 权限级别
level: int = 1
# 详细权限
permissions: Optional[dict] = Field(default_factory=dict)
permissions: Optional[dict[str, bool]] = Field(default_factory=dict)
# 是否显示配置向导
wizard: Optional[bool] = None
class TokenPayload(BaseModel):
"""访问令牌中携带的用户身份与授权信息。"""
# 用户ID
sub: Optional[int] = None
# 用户名
+78 -1
View File
@@ -41,12 +41,32 @@ class DownloaderTorrent(BaseModel):
ratio_limit: Optional[float] = None
seeding_time_limit: Optional[int] = None
trackers: Optional[List[str]] = Field(default_factory=list)
media: Optional[dict] = Field(default_factory=dict)
media: Optional["DownloadTaskMedia"] = None
userid: Optional[str] = None
username: Optional[str] = None
left_time: Optional[str] = None
class DownloadTaskMedia(OptionalMediaIdentityMixin, BaseModel):
"""下载任务关联的影视或音乐媒体摘要。"""
type: Optional[str] = None
title: Optional[str] = None
season: Optional[list[int] | int | str] = None
episode: Optional[list[int] | int | str] = None
image: Optional[str] = None
poster: Optional[str] = None
backdrop: Optional[str] = None
media_source: Optional[MediaSource] = None
media_id: Optional[str] = None
music_type: Optional[str] = None
artists: list[str] = Field(default_factory=list)
album: Optional[str] = None
album_id: Optional[str] = None
total_tracks: Optional[int] = None
track_number: Optional[int] = None
class TransferTorrent(DownloaderTorrent):
"""
待转移任务信息
@@ -281,3 +301,60 @@ class ManualTransferTargetPath(BaseModel):
library_type_folder: Optional[bool] = False
# 媒体库类别子目录
library_category_folder: Optional[bool] = False
class ManualTransferPreviewSummary(BaseModel):
"""手动整理预览数量统计。"""
total: int = 0
success: int = 0
failed: int = 0
class ManualTransferPreviewItem(BaseModel):
"""单个文件的手动整理预览。"""
source: Optional[str] = None
target: Optional[str] = None
target_dir: Optional[str] = None
success: bool = False
message: Optional[str] = None
type: Optional[str] = None
title: Optional[str] = None
season: Optional[int] = None
episode: Optional[int] = None
episode_end: Optional[int] = None
part: Optional[int | str] = None
org_string: Optional[str] = None
apply_words: list[str] = Field(default_factory=list)
resource_team: Optional[str] = None
customization: Optional[str] = None
class ManualTransferResultData(BaseModel):
"""手动整理预览或执行结果数据。"""
summary: Optional[ManualTransferPreviewSummary] = None
items: list[ManualTransferPreviewItem] = Field(default_factory=list)
message: Optional[str] = None
class EpisodeFormatRecommendData(BaseModel):
"""集数定位模板推荐结果。"""
rule_name: str
episode_format: str
sample_file: str
pattern: Optional[str] = None
rule_index: Optional[int] = None
min_file_size_mb: Optional[int] = None
sample_count: Optional[int] = None
majority_count: Optional[int] = None
confidence: Optional[str] = None
size_filter_relaxed: Optional[bool] = None
native_verified_count: Optional[int] = None
native_fallback_count: Optional[int] = None
native_conflict_count: Optional[int] = None
reason: Optional[str] = None
reasons: list[str] = Field(default_factory=list)
message: Optional[str] = None
+42 -6
View File
@@ -2,9 +2,13 @@ from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
from app.schemas.common import JsonData
# Shared properties
class UserBase(BaseModel):
"""用户公共资料、权限和个性化设置。"""
# 用户名
name: str
# 邮箱,未启用
@@ -18,33 +22,39 @@ class UserBase(BaseModel):
# 是否开启二次验证
is_otp: Optional[bool] = False
# 权限
permissions: Optional[dict] = Field(default_factory=dict)
permissions: Optional[dict[str, bool]] = Field(default_factory=dict)
# 个性化设置
settings: Optional[dict] = Field(default_factory=dict)
settings: Optional[dict[str, JsonData]] = Field(default_factory=dict)
model_config = ConfigDict(from_attributes=True)
# Properties to receive via API on creation
class UserCreate(UserBase):
"""创建用户时接收的资料和初始凭据。"""
name: str
email: Optional[str] = None
password: Optional[str] = None
settings: Optional[dict] = Field(default_factory=dict)
permissions: Optional[dict] = Field(default_factory=dict)
settings: Optional[dict[str, JsonData]] = Field(default_factory=dict)
permissions: Optional[dict[str, bool]] = Field(default_factory=dict)
# Properties to receive via API on update
class UserUpdate(UserBase):
"""更新用户时接收的完整资料。"""
id: int
name: str
email: Optional[str] = None
password: Optional[str] = None
settings: Optional[dict] = Field(default_factory=dict)
permissions: Optional[dict] = Field(default_factory=dict)
settings: Optional[dict[str, JsonData]] = Field(default_factory=dict)
permissions: Optional[dict[str, bool]] = Field(default_factory=dict)
class UserInDBBase(UserBase):
"""包含数据库主键的用户公共记录。"""
id: Optional[int] = None
model_config = ConfigDict(from_attributes=True)
@@ -52,10 +62,36 @@ class UserInDBBase(UserBase):
# Additional properties to return via API
class User(UserInDBBase):
"""对 API 调用方公开的用户资料。"""
name: str
email: Optional[str] = None
# Additional properties stored in DB
class UserInDB(UserInDBBase):
"""包含密码哈希的内部用户记录。"""
hashed_password: str
class AuthProviderRemote(BaseModel):
"""插件认证提供方的远程组件信息。"""
id: str
url: str
name: str
class AuthProviderInfo(BaseModel):
"""匿名登录页可展示的认证提供方摘要。"""
id: str
type: str
name: str
enabled: bool = True
method: Optional[str] = None
icon: Optional[str] = None
component: Optional[str] = None
plugin_id: Optional[str] = None
remote: Optional[AuthProviderRemote] = None
+112 -14
View File
@@ -2,6 +2,7 @@ from typing import Any, List, Optional
from pydantic import BaseModel, ConfigDict, Field
from app.schemas.common import JsonData
from app.schemas.context import Context, MediaInfo
from app.schemas.download import DownloadTask
from app.schemas.file import FileItem
@@ -9,6 +10,58 @@ from app.schemas.site import Site
from app.schemas.subscribe import Subscribe
class WorkflowExecutionConfig(BaseModel):
"""工作流调度器的执行参数。"""
max_workers: Optional[int] = Field(default=None, ge=1, description="最大并发动作数")
class WorkflowNodeState(BaseModel):
"""工作流单个动作的持久化执行状态。"""
state: Optional[str] = None
attempt: int = 0
started_at: Optional[str] = None
finished_at: Optional[str] = None
message: Optional[str] = None
class WorkflowRuntimeState(BaseModel):
"""工作流调度器的实时进度摘要。"""
progress: int = 0
finished_actions: int = 0
running_tasks: int = 0
errors: dict[str, str] = Field(default_factory=dict)
node_states: dict[str, str] = Field(default_factory=dict)
attempts: dict[str, int] = Field(default_factory=dict)
class WorkflowExecutionState(BaseModel):
"""可恢复的工作流结构化执行快照。"""
version: int = 1
nodes: dict[str, WorkflowNodeState] = Field(default_factory=dict)
outputs: dict[str, JsonData] = Field(default_factory=dict)
errors: dict[str, str] = Field(default_factory=dict)
runtime: WorkflowRuntimeState = Field(default_factory=WorkflowRuntimeState)
class ActionPosition(BaseModel):
"""工作流画布中的动作坐标。"""
x: float = 0
y: float = 0
class ActionRetry(BaseModel):
"""动作失败后的重试策略。"""
max_attempts: int = Field(default=1, ge=1)
interval: float = Field(default=0, ge=0)
backoff: float = Field(default=1, ge=1)
class Workflow(BaseModel):
"""
工作流信息
@@ -19,15 +72,15 @@ class Workflow(BaseModel):
timer: Optional[str] = Field(default=None, description="定时器")
trigger_type: Optional[str] = Field(default='timer', description="触发类型:timer-定时触发 event-事件触发 manual-手动触发")
event_type: Optional[str] = Field(default=None, description="事件类型(当trigger_type为event时使用)")
event_conditions: Optional[dict] = Field(default_factory=dict, description="事件条件(JSON格式,用于过滤事件)")
event_conditions: Optional[dict[str, JsonData]] = Field(default_factory=dict, description="事件条件(JSON格式,用于过滤事件)")
state: Optional[str] = Field(default=None, description="状态")
current_action: Optional[str] = Field(default=None, description="已执行动作")
result: Optional[str] = Field(default=None, description="任务执行结果")
run_count: Optional[int] = Field(default=0, description="已执行次数")
actions: Optional[list] = Field(default_factory=list, description="任务列表")
flows: Optional[list] = Field(default_factory=list, description="任务流")
execution_config: Optional[dict] = Field(default_factory=dict, description="工作流执行配置")
execution_state: Optional[dict] = Field(default_factory=dict, description="工作流结构化执行状态")
actions: Optional[list["Action"]] = Field(default_factory=list, description="任务列表")
flows: Optional[list["ActionFlow"]] = Field(default_factory=list, description="任务流")
execution_config: Optional[WorkflowExecutionConfig] = Field(default_factory=WorkflowExecutionConfig, description="工作流执行配置")
execution_state: Optional[WorkflowExecutionState] = Field(default_factory=WorkflowExecutionState, description="工作流结构化执行状态")
add_time: Optional[str] = Field(default=None, description="创建时间")
last_time: Optional[str] = Field(default=None, description="最后执行时间")
@@ -50,16 +103,16 @@ class Action(BaseModel):
type: Optional[str] = Field(default=None, description="动作类型 (类名)")
name: Optional[str] = Field(default=None, description="动作名称")
description: Optional[str] = Field(default=None, description="动作描述")
position: Optional[dict] = Field(default_factory=dict, description="位置")
data: Optional[dict] = Field(default_factory=dict, description="参数")
position: Optional[ActionPosition] = Field(default_factory=ActionPosition, description="位置")
data: Optional[dict[str, JsonData]] = Field(default_factory=dict, description="参数")
inputs: Optional[List[str]] = Field(default_factory=list, description="动作输入声明")
outputs: Optional[dict] = Field(default_factory=dict, description="动作输出声明")
outputs: Optional[dict[str, JsonData]] = Field(default_factory=dict, description="动作输出声明")
join_policy: Optional[str] = Field(default=None, description="多上游节点汇合策略")
fail_policy: Optional[str] = Field(default=None, description="动作失败后的工作流处理策略")
branch_policy: Optional[str] = Field(default=None, description="多出边分支策略")
concurrency_key: Optional[str] = Field(default=None, description="并发互斥键")
timeout: Optional[int] = Field(default=None, description="动作执行超时时间(秒)")
retry: Optional[dict] = Field(default_factory=dict, description="动作重试策略")
retry: Optional[ActionRetry] = Field(default=None, description="动作重试策略")
class ActionExecution(BaseModel):
@@ -82,10 +135,10 @@ class ActionContext(BaseModel):
downloads: Optional[List[DownloadTask]] = Field(default_factory=list, description="下载任务列表")
sites: Optional[List[Site]] = Field(default_factory=list, description="站点列表")
subscribes: Optional[List[Subscribe]] = Field(default_factory=list, description="订阅列表")
workflow_context: Optional[dict] = Field(default_factory=dict, description="工作流全局上下文")
node_outputs: Optional[dict] = Field(default_factory=dict, description="节点输出数据")
runtime_state: Optional[dict] = Field(default_factory=dict, description="运行期状态")
artifacts: Optional[dict] = Field(default_factory=dict, description="大对象引用与产物数据")
workflow_context: Optional[dict[str, JsonData]] = Field(default_factory=dict, description="工作流全局上下文")
node_outputs: Optional[dict[str, JsonData]] = Field(default_factory=dict, description="节点输出数据")
runtime_state: Optional[dict[str, JsonData]] = Field(default_factory=dict, description="运行期状态")
artifacts: Optional[dict[str, JsonData]] = Field(default_factory=dict, description="大对象引用与产物数据")
execute_history: Optional[List[ActionExecution]] = Field(default_factory=list, description="执行历史")
progress: Optional[int] = Field(default=0, description="执行进度(%")
@@ -97,6 +150,7 @@ class ActionResult(BaseModel):
success: Optional[bool] = Field(default=True, description="动作是否执行成功")
message: Optional[str] = Field(default=None, description="动作执行消息")
context: Optional[ActionContext] = Field(default=None, description="动作执行后的上下文")
# 动作内部可暂存待序列化对象;API 输出前由工作流序列化器转为 JsonData。
outputs: Optional[dict[str, Any]] = Field(default_factory=dict, description="当前节点显式输出")
next_policy: Optional[str] = Field(default=None, description="动作完成后的调度策略")
attempts: Optional[int] = Field(default=1, description="动作实际尝试次数")
@@ -110,7 +164,7 @@ class ActionFlow(BaseModel):
source: Optional[str] = Field(default=None, description="源动作")
target: Optional[str] = Field(default=None, description="目标动作")
animated: Optional[bool] = Field(default=True, description="是否动画流程")
data: Optional[dict] = Field(default_factory=dict, description="流程扩展配置")
data: Optional[dict[str, JsonData]] = Field(default_factory=dict, description="流程扩展配置")
condition: Optional[str] = Field(default=None, description="流转条件表达式")
join_policy: Optional[str] = Field(default=None, description="目标节点汇合策略")
branch_policy: Optional[str] = Field(default=None, description="源节点分支策略")
@@ -138,3 +192,47 @@ class WorkflowShare(BaseModel):
count: Optional[int] = Field(default=0, description="复用人次")
model_config = ConfigDict(from_attributes=True)
class PluginWorkflowActionGroup(BaseModel):
"""单个插件声明的工作流动作组。"""
plugin_id: str
plugin_name: str
actions: list[dict[str, JsonData]] = Field(default_factory=list)
class ActionContractField(BaseModel):
"""动作契约中的单个输入或输出字段。"""
name: str
label: str
kind: str = "scalar"
merge: Optional[str] = None
identity: Optional[str] = None
class ActionContract(BaseModel):
"""工作流动作对编辑器公开的输入输出契约。"""
inputs: list[ActionContractField] = Field(default_factory=list)
outputs: list[ActionContractField] = Field(default_factory=list)
condition_fields: list[ActionContractField] = Field(default_factory=list)
concurrency_key: Optional[str] = None
class WorkflowActionDefinition(BaseModel):
"""可用于工作流编辑器的动作定义。"""
type: str
name: str
description: Optional[str] = None
contract: ActionContract = Field(default_factory=ActionContract)
data: dict[str, JsonData] = Field(default_factory=dict)
class NameValueOption(BaseModel):
"""前端选项的显示文本和值。"""
title: str
value: str