Files
2026-10-30 16:14:49 +08:00

976 lines
46 KiB
Python

"""
Agent 结构化 MoviePilot API 操作与固定路由注册表。
"""
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from app.agent.policy.contracts import (
ActionEffect,
ConfirmationMode,
PrincipalRole,
RecoveryMode,
ResultSensitivity,
)
class ApiIdentityScope(str, Enum):
"""
区分公共资料、绑定用户数据和受保护的实例管理身份。
"""
# 无需绑定用户的公共资料查询
PUBLIC = "public"
# 使用可信入口绑定的用户身份
BOUND_USER = "bound_user"
# 使用经过授权的实例管理身份
ADMIN = "admin"
@dataclass(frozen=True)
class ApiOperationSpec:
"""
描述一个可由 Agent API 网关调用的稳定业务操作。
:param operation_id: 稳定业务操作标识,与固定路由表对应
:param effect: 该操作的实际副作用类别
:param required_role: 执行操作所需的最低授权角色
:param confirmation: 授权通过后需要的确认方式
:param recovery: 该操作提供的恢复或结果核对机制
:param result_sensitivity: 操作结果的敏感等级
:param identity_scope: API 应使用的身份范围,默认绑定当前用户
"""
operation_id: str
effect: ActionEffect
required_role: PrincipalRole
confirmation: ConfirmationMode
recovery: RecoveryMode
result_sensitivity: ResultSensitivity
identity_scope: ApiIdentityScope = ApiIdentityScope.BOUND_USER
def __post_init__(self) -> None:
"""
公共身份只接受无需确认、无私密结果的普通用户级安全查询。
"""
if self.identity_scope is ApiIdentityScope.PUBLIC and (
self.effect is not ActionEffect.SAFE_READ
or self.required_role is not PrincipalRole.USER
or self.confirmation is not ConfirmationMode.NONE
or self.result_sensitivity is not ResultSensitivity.NORMAL
):
raise ValueError("公共 API 身份不能用于敏感读取、管理员操作或写操作")
@dataclass(frozen=True)
class ApiOperationRoute:
"""
描述一个固定 API 路由,禁止调用方注入任意 URL 或 HTTP 方法。
:param method: 宿主登记的固定 HTTP 方法
:param path: 宿主登记的 API 路径模板,只允许替换声明的路径参数
"""
method: str
path: str
_ADMIN = PrincipalRole.SYSTEM_ADMIN
_CONFIRM = ConfirmationMode.REQUIRED
_IDEMPOTENT = RecoveryMode.IDEMPOTENT
_DELETE_RECOVERABLE = RecoveryMode.RECOVERABLE_DELETE
def _spec(
operation_id: str,
*,
effect: ActionEffect = ActionEffect.SAFE_READ,
required_role: PrincipalRole = PrincipalRole.USER,
confirmation: ConfirmationMode = ConfirmationMode.NONE,
recovery: RecoveryMode = RecoveryMode.NONE,
result_sensitivity: ResultSensitivity = ResultSensitivity.NORMAL,
identity_scope: Optional[ApiIdentityScope] = None,
) -> ApiOperationSpec:
"""
构造不可变 API 操作描述。
"""
return ApiOperationSpec(
operation_id=operation_id,
effect=effect,
required_role=required_role,
confirmation=confirmation,
recovery=recovery,
result_sensitivity=result_sensitivity,
identity_scope=identity_scope or (
ApiIdentityScope.ADMIN if required_role is _ADMIN else ApiIdentityScope.BOUND_USER
),
)
def _public_read(operation_id: str) -> ApiOperationSpec:
"""
仅为已审计、不读取本地用户数据的公共资料声明无绑定查询。
"""
return _spec(operation_id, identity_scope=ApiIdentityScope.PUBLIC)
def _write(
operation_id: str,
*,
effect: ActionEffect = ActionEffect.REVERSIBLE_WRITE,
recovery: RecoveryMode = _IDEMPOTENT,
sensitivity: ResultSensitivity = ResultSensitivity.NORMAL,
) -> ApiOperationSpec:
"""
构造管理员确认写操作。
"""
return _spec(
operation_id,
effect=effect,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=recovery,
result_sensitivity=sensitivity,
)
def _admin_read(
operation_id: str,
*,
sensitivity: ResultSensitivity = ResultSensitivity.NORMAL,
) -> ApiOperationSpec:
"""
构造管理员读取操作。
"""
return _spec(operation_id, required_role=_ADMIN, result_sensitivity=sensitivity)
def _user_write(
operation_id: str,
*,
effect: ActionEffect = ActionEffect.REVERSIBLE_WRITE,
recovery: RecoveryMode = _IDEMPOTENT,
sensitivity: ResultSensitivity = ResultSensitivity.NORMAL,
) -> ApiOperationSpec:
"""
构造保留普通用户 API 权限的确认写操作。
"""
return _spec(
operation_id,
effect=effect,
confirmation=_CONFIRM,
recovery=recovery,
result_sensitivity=sensitivity,
)
API_FIRST_BATCH_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = (
_public_read("media.search"),
_public_read("media.person.search"),
_public_read("media.person.credits"),
_spec("media.recognize"),
_spec("media.scrape", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, required_role=_ADMIN, confirmation=_CONFIRM),
_public_read("media.episode_schedule"),
_public_read("media.detail"),
# 订阅归属由 API 当前用户决定,渠道管理员也必须使用绑定用户身份。
_user_write("subscription.add"),
_user_write("subscription.update"),
_user_write("subscription.search", effect=ActionEffect.EXTERNAL_SIDE_EFFECT),
_spec("subscription.list"),
_public_read("subscription.shares"),
_public_read("subscription.popular"),
_spec("subscription.history"),
_user_write("subscription.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE),
_spec("download.add", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, confirmation=_CONFIRM, recovery=_IDEMPOTENT),
_spec(
"download.artist_collection",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
confirmation=_CONFIRM,
recovery=_IDEMPOTENT,
),
_spec("download.tasks.active"),
_spec("download.clients"),
_spec("download.paths"),
_spec("download.history.list"),
_write("download.history.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE),
_write("transfer.history.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE),
_write("site.update"),
_spec("site.list"),
_admin_read("site.userdata", sensitivity=ResultSensitivity.PRIVATE),
_spec("site.test"),
_write("site.cookie.update", sensitivity=ResultSensitivity.PRIVATE),
_public_read("recommendation.list"),
_spec("library.exists"),
_spec("library.latest"),
_admin_read("storage.settings"),
_spec("storage.list", result_sensitivity=ResultSensitivity.PRIVATE),
_spec("transfer.history", result_sensitivity=ResultSensitivity.PRIVATE),
_spec(
"transfer.file",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_admin_read("scheduler.list"),
_spec(
"scheduler.run",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec("workflow.list"),
_spec(
"workflow.run",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_admin_read("plugin.installed"),
_admin_read("plugin.market"),
_admin_read("plugin.capabilities"),
_admin_read("plugin.config.get", sensitivity=ResultSensitivity.PRIVATE),
_write("plugin.config.update", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("plugin.source.options"),
_spec(
"plugin.source.install",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"plugin.source.change",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"plugin.reload",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"plugin.install",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_write("plugin.uninstall", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE),
_admin_read("slash.list"),
_admin_read("config.identifiers.get"),
_write("config.identifiers.update"),
)
API_PARITY_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = (
_spec("search.torrents"),
_spec("search.results"),
_spec("filter.builtin"),
_spec("filter.custom"),
_spec("filter.groups"),
_write("filter.custom.add", recovery=RecoveryMode.TRANSACTION),
_write("filter.custom.update", recovery=RecoveryMode.TRANSACTION),
_write("filter.custom.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.TRANSACTION),
_write("filter.group.add", recovery=RecoveryMode.TRANSACTION),
_write("filter.group.update", recovery=RecoveryMode.TRANSACTION),
_write("filter.group.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.TRANSACTION),
_admin_read("plugin.data", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("config.system.list", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("config.system.describe", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("config.system.get", sensitivity=ResultSensitivity.PRIVATE),
_write("config.system.update", recovery=RecoveryMode.TRANSACTION, sensitivity=ResultSensitivity.PRIVATE),
_spec(
"slash.run",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
)
API_MUSIC_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = (
_public_read("music.recognize"),
_public_read("music.explore"),
_public_read("music.album.get"),
_public_read("music.album.related"),
_public_read("music.artist.get"),
_public_read("music.artist.albums"),
_public_read("music.artist.related"),
_admin_read("music.cache.get", sensitivity=ResultSensitivity.PRIVATE),
_write(
"music.cache.delete",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=RecoveryMode.NONE,
),
_write(
"music.cache.clear",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=RecoveryMode.NONE,
),
)
API_SYSTEM_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = (
_admin_read("system.versions"),
_admin_read("system.update.status"),
_spec(
"system.update.check",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
recovery=_IDEMPOTENT,
),
_spec(
"system.update.download",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"system.restart",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"system.update.install",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"system.upgrade.dev",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
)
API_EXTENDED_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = (
_admin_read("dashboard.media.statistics", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("dashboard.storage", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("dashboard.processes", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("dashboard.system", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("dashboard.downloader", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("scheduler.progress"),
_admin_read("dashboard.transfer.statistics"),
_admin_read("dashboard.cpu"),
_admin_read("dashboard.memory"),
_admin_read("dashboard.network"),
_spec("media.sources"),
_spec("media.recognize_file"),
_admin_read("media.cache.get", sensitivity=ResultSensitivity.PRIVATE),
_write("media.cache.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.NONE),
_write("media.cache.clear", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.NONE),
_spec("media.classification.fields"),
_spec("media.classification.policy.get"),
_admin_read("media.classification.policy.validate"),
_spec("media.classification.policy.preview"),
_admin_read("media.classification.policy.impact", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("media.classification.policy.history", sensitivity=ResultSensitivity.PRIVATE),
_write("media.classification.policy.update", recovery=RecoveryMode.TRANSACTION),
_write("media.classification.policy.rollback", recovery=RecoveryMode.TRANSACTION),
_public_read("media.episode_groups"),
_public_read("media.episode_group.seasons"),
_public_read("media.seasons"),
_spec("search.title", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT),
_spec("search.recommend", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT),
_spec("subtitle.search.title", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT),
_spec("subtitle.search.media", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT),
_write("site.add", recovery=RecoveryMode.TRANSACTION, sensitivity=ResultSensitivity.PRIVATE),
_write(
"site.delete",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=_DELETE_RECOVERABLE,
),
_spec("site.auth.options"),
_spec(
"site.authenticate",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
result_sensitivity=ResultSensitivity.PRIVATE,
),
_spec(
"site.cookiecloud.sync",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_write("site.cookie.set", sensitivity=ResultSensitivity.PRIVATE),
_write(
"site.reset",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=RecoveryMode.RECONCILE,
),
_write("site.priorities.update", recovery=RecoveryMode.TRANSACTION),
_spec(
"site.userdata.refresh",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=_IDEMPOTENT,
result_sensitivity=ResultSensitivity.PRIVATE,
),
_admin_read("site.userdata.latest", sensitivity=ResultSensitivity.PRIVATE),
_spec("site.category"),
_spec(
"site.resource",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
recovery=_IDEMPOTENT,
result_sensitivity=ResultSensitivity.PRIVATE,
),
_admin_read("site.searchable", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("site.rss", sensitivity=ResultSensitivity.PRIVATE),
_spec("site.statistics", result_sensitivity=ResultSensitivity.PRIVATE),
_spec("site.statistic", result_sensitivity=ResultSensitivity.PRIVATE),
_admin_read("site.mapping", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("site.supporting"),
_spec("subscription.get"),
_spec("subscription.find"),
_user_write(
"subscription.delete_by_media",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=_DELETE_RECOVERABLE,
),
_user_write("subscription.status.update"),
_user_write("subscription.reset"),
_user_write("subscription.search_all", effect=ActionEffect.EXTERNAL_SIDE_EFFECT),
_spec(
"subscription.refresh",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"subscription.metadata.refresh",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_user_write(
"subscription.history.delete",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=_DELETE_RECOVERABLE,
),
_spec("subscription.user.list"),
_spec("subscription.files", result_sensitivity=ResultSensitivity.PRIVATE),
_spec("subscription.execution.list", result_sensitivity=ResultSensitivity.PRIVATE),
_spec("subscription.execution.get", result_sensitivity=ResultSensitivity.PRIVATE),
_user_write(
"subscription.execution.cancel",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
recovery=RecoveryMode.RECONCILE,
sensitivity=ResultSensitivity.PRIVATE,
),
_user_write("subscription.share", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=RecoveryMode.RECONCILE),
_write(
"subscription.share.delete",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
recovery=RecoveryMode.RECONCILE,
),
_user_write("subscription.fork", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=RecoveryMode.RECONCILE),
_admin_read("subscription.follow.list"),
_write("subscription.follow.add"),
_write("subscription.follow.delete"),
_public_read("subscription.share.statistics"),
_spec(
"storage.manage",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
result_sensitivity=ResultSensitivity.PRIVATE,
),
_write("storage.mkdir", recovery=RecoveryMode.TRANSACTION),
_write("storage.rename", recovery=RecoveryMode.RECONCILE),
_write(
"storage.delete",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=RecoveryMode.MANUAL_ONLY,
),
_spec("transfer.queue", result_sensitivity=ResultSensitivity.PRIVATE),
_write(
"transfer.queue.delete",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=RecoveryMode.RECONCILE,
),
_admin_read("transfer.name"),
_admin_read("transfer.target_path", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("transfer.manual_history", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("transfer.episode_format.recommend", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("transfer.manual_reviews", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("transfer.manual_review", sensitivity=ResultSensitivity.PRIVATE),
_write("transfer.manual_review.resolve", recovery=RecoveryMode.RECONCILE),
_spec(
"transfer.history.redo",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"transfer.history.redo_batch",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"transfer.history.replan",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_write(
"transfer.history.clear",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=RecoveryMode.MANUAL_ONLY,
),
_write("workflow.create", recovery=RecoveryMode.TRANSACTION),
_admin_read("workflow.get", sensitivity=ResultSensitivity.PRIVATE),
_write("workflow.update", recovery=RecoveryMode.TRANSACTION),
_write("workflow.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE),
_admin_read("workflow.actions"),
_admin_read("workflow.event_types"),
_admin_read("workflow.plugin.actions"),
_write("workflow.start"),
_write("workflow.pause"),
_write("workflow.reset", recovery=RecoveryMode.TRANSACTION),
_spec("workflow.shares", required_role=_ADMIN),
_spec(
"workflow.share",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"workflow.share.delete",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_spec(
"workflow.fork",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_admin_read("torrent.cache.get", sensitivity=ResultSensitivity.PRIVATE),
_write("torrent.cache.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.NONE),
_write("torrent.cache.clear", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.NONE),
_spec(
"torrent.cache.refresh",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_write("torrent.cache.reidentify", recovery=RecoveryMode.RECONCILE),
_admin_read("database.backups.list", sensitivity=ResultSensitivity.PRIVATE),
_spec(
"database.backups.create",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
result_sensitivity=ResultSensitivity.PRIVATE,
),
_admin_read("database.backups.verify", sensitivity=ResultSensitivity.PRIVATE),
_write(
"database.backups.delete",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=RecoveryMode.MANUAL_ONLY,
),
_spec("filter.test", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT),
_spec("system.network.targets"),
_spec("system.network.test", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT),
_spec("system.module.list"),
_spec("system.module.catalog"),
_admin_read("system.module.settings"),
_spec("system.module.test", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT),
_spec(
"plugin.market.sync_wiki",
effect=ActionEffect.EXTERNAL_SIDE_EFFECT,
required_role=_ADMIN,
confirmation=_CONFIRM,
recovery=RecoveryMode.RECONCILE,
),
_admin_read("plugin.runtime.status", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("plugin.history"),
_admin_read("plugin.releases"),
_admin_read("plugin.ratings", sensitivity=ResultSensitivity.PRIVATE),
_admin_read("plugin.rating", sensitivity=ResultSensitivity.PRIVATE),
_write("plugin.rating.submit", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT, sensitivity=ResultSensitivity.PRIVATE),
_public_read("plugin.statistics"),
_write(
"plugin.reset",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=RecoveryMode.MANUAL_ONLY,
),
_admin_read("plugin.clone.restorable", sensitivity=ResultSensitivity.PRIVATE),
_write("plugin.clone", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=RecoveryMode.RECONCILE),
_spec("config.user.get", result_sensitivity=ResultSensitivity.PRIVATE),
_spec("config.public.get"),
_spec("system.usage.statistics", result_sensitivity=ResultSensitivity.PRIVATE),
_admin_read("plugin.folders.get", sensitivity=ResultSensitivity.PRIVATE),
_write("plugin.folders.update", recovery=RecoveryMode.TRANSACTION),
_write("plugin.folder.create", recovery=RecoveryMode.TRANSACTION),
_write("plugin.folder.update", recovery=RecoveryMode.TRANSACTION),
_write("plugin.folder.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE),
_write("plugin.folder.plugins.update", recovery=RecoveryMode.TRANSACTION),
_write("plugin.folder.plugin.assign", recovery=RecoveryMode.TRANSACTION),
_write("plugin.folder.plugin.remove", recovery=RecoveryMode.TRANSACTION),
_admin_read("plugin.loglevel.get", sensitivity=ResultSensitivity.PRIVATE),
_write("plugin.loglevel.set"),
_write("plugin.loglevel.clear"),
_write("plugin.default_target.set"),
_write("plugin.default_target.clear"),
_write("plugin.instance.set_enabled"),
# 彻底清理按勾选范围真删用户数据且不可回滚,与重置同档:要确认、且只能人工补救
_write(
"plugin.instance.purge",
effect=ActionEffect.DESTRUCTIVE_WRITE,
recovery=RecoveryMode.MANUAL_ONLY,
),
)
API_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = (
*API_FIRST_BATCH_OPERATION_SPECS,
*API_PARITY_OPERATION_SPECS,
*API_MUSIC_OPERATION_SPECS,
*API_SYSTEM_OPERATION_SPECS,
*API_EXTENDED_OPERATION_SPECS,
)
API_OPERATION_BY_ID = {spec.operation_id: spec for spec in API_OPERATION_SPECS}
# 身份范围是唯一事实源;不能按 safe_read 批量放行实例、文件或用户数据。
API_UNBOUND_READ_OPERATIONS = frozenset(
spec.operation_id for spec in API_OPERATION_SPECS if spec.identity_scope is ApiIdentityScope.PUBLIC
)
# 所有路径均由宿主注册,调用方只能传路径参数、查询参数和 JSON body。
API_OPERATION_ROUTES: dict[str, ApiOperationRoute] = {
"media.search": ApiOperationRoute("GET", "/api/v1/media/search"),
"media.person.search": ApiOperationRoute("GET", "/api/v1/media/search"),
"media.person.credits": ApiOperationRoute("GET", "/api/v1/{source}/person/credits/{person_id}"),
"media.recognize": ApiOperationRoute("GET", "/api/v1/media/recognize"),
"media.scrape": ApiOperationRoute("POST", "/api/v1/media/scrape/{storage}"),
"media.episode_schedule": ApiOperationRoute("GET", "/api/v1/tmdb/{tmdbid}/{season}"),
"media.detail": ApiOperationRoute("GET", "/api/v1/media/{media_id}"),
"music.recognize": ApiOperationRoute("POST", "/api/v1/music/recognize"),
"music.explore": ApiOperationRoute("GET", "/api/v1/music/explore"),
"music.album.get": ApiOperationRoute("GET", "/api/v1/music/album/{album_id}"),
"music.album.related": ApiOperationRoute("GET", "/api/v1/music/album/{album_id}/related"),
"music.artist.get": ApiOperationRoute("GET", "/api/v1/music/artist/{artist_id}"),
"music.artist.albums": ApiOperationRoute("GET", "/api/v1/music/artist/{artist_id}/albums"),
"music.artist.related": ApiOperationRoute("GET", "/api/v1/music/artist/{artist_id}/related"),
"music.cache.get": ApiOperationRoute("GET", "/api/v1/music/cache"),
"music.cache.delete": ApiOperationRoute("DELETE", "/api/v1/music/cache/{cache_key}"),
"music.cache.clear": ApiOperationRoute("DELETE", "/api/v1/music/cache"),
"subscription.add": ApiOperationRoute("POST", "/api/v1/subscribe/"),
"subscription.update": ApiOperationRoute("PUT", "/api/v1/subscribe/"),
"subscription.search": ApiOperationRoute("POST", "/api/v1/subscribe/search/{subscribe_id}"),
"subscription.list": ApiOperationRoute("GET", "/api/v1/subscribe/"),
"subscription.shares": ApiOperationRoute("GET", "/api/v1/subscribe/shares"),
"subscription.popular": ApiOperationRoute("GET", "/api/v1/subscribe/popular"),
"subscription.history": ApiOperationRoute("GET", "/api/v1/subscribe/history/{mtype}"),
"subscription.delete": ApiOperationRoute("DELETE", "/api/v1/subscribe/{subscribe_id}"),
"download.add": ApiOperationRoute("POST", "/api/v1/download/add"),
"download.artist_collection": ApiOperationRoute("POST", "/api/v1/download/artist-collection"),
"download.tasks.active": ApiOperationRoute("GET", "/api/v1/download/"),
"download.clients": ApiOperationRoute("GET", "/api/v1/download/clients"),
"download.paths": ApiOperationRoute("GET", "/api/v1/download/paths"),
"download.history.list": ApiOperationRoute("GET", "/api/v1/history/download"),
"download.history.delete": ApiOperationRoute("DELETE", "/api/v1/history/download"),
"transfer.history.delete": ApiOperationRoute("DELETE", "/api/v1/history/transfer"),
"site.list": ApiOperationRoute("GET", "/api/v1/site/agent"),
"site.update": ApiOperationRoute("PUT", "/api/v1/site/"),
"site.userdata": ApiOperationRoute("GET", "/api/v1/site/userdata/{site_id}"),
"site.test": ApiOperationRoute("GET", "/api/v1/site/test/{site_id}"),
"site.cookie.update": ApiOperationRoute("POST", "/api/v1/site/cookie/{site_id}"),
"recommendation.list": ApiOperationRoute("GET", "/api/v1/recommend/agent"),
"library.exists": ApiOperationRoute("GET", "/api/v1/mediaserver/exists"),
"library.latest": ApiOperationRoute("GET", "/api/v1/mediaserver/latest"),
"storage.settings": ApiOperationRoute("GET", "/api/v1/storage/directories"),
"storage.list": ApiOperationRoute("POST", "/api/v1/storage/agent/list"),
"transfer.history": ApiOperationRoute("GET", "/api/v1/history/transfer"),
"transfer.file": ApiOperationRoute("POST", "/api/v1/transfer/manual"),
"scheduler.list": ApiOperationRoute("GET", "/api/v1/dashboard/schedule"),
"scheduler.run": ApiOperationRoute("GET", "/api/v1/system/runscheduler"),
"workflow.list": ApiOperationRoute("GET", "/api/v1/workflow/agent"),
"workflow.run": ApiOperationRoute("POST", "/api/v1/workflow/{workflow_id}/run"),
"plugin.installed": ApiOperationRoute("GET", "/api/v1/plugin/"),
"plugin.market": ApiOperationRoute("GET", "/api/v1/plugin/"),
"plugin.capabilities": ApiOperationRoute("GET", "/api/v1/plugin/runtime/capabilities"),
"plugin.config.get": ApiOperationRoute("GET", "/api/v1/plugin/form/{plugin_id}"),
"plugin.config.update": ApiOperationRoute("PUT", "/api/v1/plugin/{plugin_id}"),
"plugin.source.options": ApiOperationRoute("GET", "/api/v1/plugin/source/{plugin_id}"),
"plugin.source.install": ApiOperationRoute("POST", "/api/v1/plugin/source/{plugin_id}/install"),
"plugin.source.change": ApiOperationRoute("POST", "/api/v1/plugin/source/{plugin_id}"),
"plugin.reload": ApiOperationRoute("POST", "/api/v1/plugin/reload/{plugin_id}"),
"plugin.install": ApiOperationRoute("GET", "/api/v1/plugin/install/{plugin_id}"),
"plugin.uninstall": ApiOperationRoute("DELETE", "/api/v1/plugin/{plugin_id}"),
"slash.list": ApiOperationRoute("GET", "/api/v1/message/agent/commands"),
"config.identifiers.get": ApiOperationRoute("GET", "/api/v1/system/identifiers"),
"config.identifiers.update": ApiOperationRoute("POST", "/api/v1/system/identifiers"),
"search.torrents": ApiOperationRoute("GET", "/api/v1/search/media/{media_id}"),
"search.results": ApiOperationRoute("GET", "/api/v1/search/last/context"),
"filter.builtin": ApiOperationRoute("GET", "/api/v1/rule/builtin"),
"filter.custom": ApiOperationRoute("GET", "/api/v1/rule/custom"),
"filter.groups": ApiOperationRoute("GET", "/api/v1/rule/groups"),
"filter.custom.add": ApiOperationRoute("POST", "/api/v1/rule/custom"),
"filter.custom.update": ApiOperationRoute("PUT", "/api/v1/rule/custom/{rule_id}"),
"filter.custom.delete": ApiOperationRoute("DELETE", "/api/v1/rule/custom/{rule_id}"),
"filter.group.add": ApiOperationRoute("POST", "/api/v1/rule/groups"),
"filter.group.update": ApiOperationRoute("PUT", "/api/v1/rule/groups/{name}"),
"filter.group.delete": ApiOperationRoute("DELETE", "/api/v1/rule/groups/{name}"),
"plugin.data": ApiOperationRoute("GET", "/api/v1/plugin/runtime/{plugin_id}/data"),
"config.system.list": ApiOperationRoute("GET", "/api/v1/system/settings/catalog"),
"config.system.describe": ApiOperationRoute(
"GET", "/api/v1/system/settings/describe/{setting_key}"
),
"config.system.get": ApiOperationRoute("GET", "/api/v1/system/settings"),
"config.system.update": ApiOperationRoute("POST", "/api/v1/system/settings"),
"slash.run": ApiOperationRoute("POST", "/api/v1/message/agent/commands/run"),
"system.versions": ApiOperationRoute("GET", "/api/v1/system/versions"),
"system.restart": ApiOperationRoute("GET", "/api/v1/system/restart"),
"system.update.status": ApiOperationRoute("GET", "/api/v1/system/update/status"),
"system.update.check": ApiOperationRoute("POST", "/api/v1/system/update/check"),
"system.update.download": ApiOperationRoute("POST", "/api/v1/system/update/download"),
"system.update.install": ApiOperationRoute("POST", "/api/v1/system/update/install"),
"system.upgrade.dev": ApiOperationRoute("POST", "/api/v1/system/upgrade"),
"dashboard.media.statistics": ApiOperationRoute("GET", "/api/v1/dashboard/statistic"),
"dashboard.storage": ApiOperationRoute("GET", "/api/v1/dashboard/storage"),
"dashboard.processes": ApiOperationRoute("GET", "/api/v1/dashboard/processes"),
"dashboard.system": ApiOperationRoute("GET", "/api/v1/dashboard/system"),
"dashboard.downloader": ApiOperationRoute("GET", "/api/v1/dashboard/downloader"),
"scheduler.progress": ApiOperationRoute("GET", "/api/v1/dashboard/schedule/{job_id}/progress"),
"dashboard.transfer.statistics": ApiOperationRoute("GET", "/api/v1/dashboard/transfer"),
"dashboard.cpu": ApiOperationRoute("GET", "/api/v1/dashboard/cpu"),
"dashboard.memory": ApiOperationRoute("GET", "/api/v1/dashboard/memory"),
"dashboard.network": ApiOperationRoute("GET", "/api/v1/dashboard/network"),
"media.sources": ApiOperationRoute("GET", "/api/v1/media/source"),
"media.recognize_file": ApiOperationRoute("GET", "/api/v1/media/recognize_file"),
"media.cache.get": ApiOperationRoute("GET", "/api/v1/tmdb/cache"),
"media.cache.delete": ApiOperationRoute("DELETE", "/api/v1/tmdb/cache/{cache_key}"),
"media.cache.clear": ApiOperationRoute("DELETE", "/api/v1/tmdb/cache"),
"media.classification.fields": ApiOperationRoute("GET", "/api/v1/media/classification/fields"),
"media.classification.policy.get": ApiOperationRoute("GET", "/api/v1/media/classification/policy"),
"media.classification.policy.update": ApiOperationRoute("PUT", "/api/v1/media/classification/policy"),
"media.classification.policy.validate": ApiOperationRoute("POST", "/api/v1/media/classification/validate"),
"media.classification.policy.preview": ApiOperationRoute("POST", "/api/v1/media/classification/preview"),
"media.classification.policy.impact": ApiOperationRoute("POST", "/api/v1/media/classification/impact"),
"media.classification.policy.history": ApiOperationRoute("GET", "/api/v1/media/classification/history"),
"media.classification.policy.rollback": ApiOperationRoute(
"POST", "/api/v1/media/classification/rollback/{revision}"
),
"media.episode_groups": ApiOperationRoute("GET", "/api/v1/media/groups/{tmdbid}"),
"media.episode_group.seasons": ApiOperationRoute("GET", "/api/v1/media/group/seasons/{episode_group}"),
"media.seasons": ApiOperationRoute("GET", "/api/v1/media/seasons"),
"search.title": ApiOperationRoute("GET", "/api/v1/search/title"),
"search.recommend": ApiOperationRoute("POST", "/api/v1/search/recommend"),
"subtitle.search.title": ApiOperationRoute("GET", "/api/v1/search/subtitle/title"),
"subtitle.search.media": ApiOperationRoute("GET", "/api/v1/search/subtitle/media/{media_id}"),
"site.add": ApiOperationRoute("POST", "/api/v1/site/"),
"site.delete": ApiOperationRoute("DELETE", "/api/v1/site/{site_id}"),
"site.auth.options": ApiOperationRoute("GET", "/api/v1/site/auth"),
"site.authenticate": ApiOperationRoute("POST", "/api/v1/site/auth"),
"site.cookiecloud.sync": ApiOperationRoute("POST", "/api/v1/site/cookiecloud"),
"site.cookie.set": ApiOperationRoute("POST", "/api/v1/site/cookie/{site_id}/set"),
"site.reset": ApiOperationRoute("POST", "/api/v1/site/reset"),
"site.priorities.update": ApiOperationRoute("POST", "/api/v1/site/priorities"),
"site.userdata.refresh": ApiOperationRoute("POST", "/api/v1/site/userdata/{site_id}"),
"site.userdata.latest": ApiOperationRoute("GET", "/api/v1/site/userdata/latest"),
"site.category": ApiOperationRoute("GET", "/api/v1/site/category/{site_id}"),
"site.resource": ApiOperationRoute("GET", "/api/v1/site/resource/{site_id}"),
"site.searchable": ApiOperationRoute("GET", "/api/v1/site/media/{media_type}"),
"site.rss": ApiOperationRoute("GET", "/api/v1/site/rss"),
"site.statistics": ApiOperationRoute("GET", "/api/v1/site/statistic"),
"site.statistic": ApiOperationRoute("GET", "/api/v1/site/statistic/{site_url}"),
"site.mapping": ApiOperationRoute("GET", "/api/v1/site/mapping"),
"site.supporting": ApiOperationRoute("GET", "/api/v1/site/supporting"),
"subscription.get": ApiOperationRoute("GET", "/api/v1/subscribe/{subscribe_id}"),
"subscription.find": ApiOperationRoute("GET", "/api/v1/subscribe/media/{media_id}"),
"subscription.delete_by_media": ApiOperationRoute("DELETE", "/api/v1/subscribe/media/{media_id}"),
"subscription.status.update": ApiOperationRoute("PUT", "/api/v1/subscribe/status/{subid}"),
"subscription.reset": ApiOperationRoute("POST", "/api/v1/subscribe/reset/{subid}"),
"subscription.search_all": ApiOperationRoute("POST", "/api/v1/subscribe/search"),
"subscription.refresh": ApiOperationRoute("POST", "/api/v1/subscribe/refresh"),
"subscription.metadata.refresh": ApiOperationRoute("POST", "/api/v1/subscribe/check"),
"subscription.history.delete": ApiOperationRoute("DELETE", "/api/v1/subscribe/history/{history_id}"),
"subscription.user.list": ApiOperationRoute("GET", "/api/v1/subscribe/user/{username}"),
"subscription.files": ApiOperationRoute("GET", "/api/v1/subscribe/files/{subscribe_id}"),
"subscription.execution.list": ApiOperationRoute("GET", "/api/v1/subscribe/execution/batches"),
"subscription.execution.get": ApiOperationRoute(
"GET", "/api/v1/subscribe/execution/batches/{batch_id}"
),
"subscription.execution.cancel": ApiOperationRoute(
"PUT", "/api/v1/subscribe/execution/batches/{batch_id}/cancel"
),
"subscription.share": ApiOperationRoute("POST", "/api/v1/subscribe/share"),
"subscription.share.delete": ApiOperationRoute("DELETE", "/api/v1/subscribe/share/{share_id}"),
"subscription.fork": ApiOperationRoute("POST", "/api/v1/subscribe/fork"),
"subscription.follow.list": ApiOperationRoute("GET", "/api/v1/subscribe/follow"),
"subscription.follow.add": ApiOperationRoute("POST", "/api/v1/subscribe/follow"),
"subscription.follow.delete": ApiOperationRoute("DELETE", "/api/v1/subscribe/follow"),
"subscription.share.statistics": ApiOperationRoute("GET", "/api/v1/subscribe/share/statistics"),
"storage.manage": ApiOperationRoute("POST", "/api/v1/storage/manage"),
"storage.mkdir": ApiOperationRoute("POST", "/api/v1/storage/mkdir"),
"storage.rename": ApiOperationRoute("POST", "/api/v1/storage/rename"),
"storage.delete": ApiOperationRoute("POST", "/api/v1/storage/delete"),
"transfer.queue": ApiOperationRoute("GET", "/api/v1/transfer/queue"),
"transfer.queue.delete": ApiOperationRoute("DELETE", "/api/v1/transfer/queue"),
"transfer.name": ApiOperationRoute("GET", "/api/v1/transfer/name"),
"transfer.target_path": ApiOperationRoute("POST", "/api/v1/transfer/manual/target-path"),
"transfer.manual_history": ApiOperationRoute("POST", "/api/v1/transfer/manual/history"),
"transfer.episode_format.recommend": ApiOperationRoute("POST", "/api/v1/transfer/episode-format/recommend"),
"transfer.manual_reviews": ApiOperationRoute("GET", "/api/v1/transfer/tasks/manual-reviews"),
"transfer.manual_review": ApiOperationRoute("GET", "/api/v1/transfer/tasks/{task_id}/manual-review"),
"transfer.manual_review.resolve": ApiOperationRoute("POST", "/api/v1/transfer/tasks/{task_id}/manual-review"),
"transfer.history.redo": ApiOperationRoute("POST", "/api/v1/history/transfer/{history_id}/ai-redo"),
"transfer.history.redo_batch": ApiOperationRoute("POST", "/api/v1/history/transfer/ai-redo"),
"transfer.history.replan": ApiOperationRoute("POST", "/api/v1/history/transfer/{history_id}/replan"),
"transfer.history.clear": ApiOperationRoute("DELETE", "/api/v1/history/transfer/all"),
"workflow.create": ApiOperationRoute("POST", "/api/v1/workflow/"),
"workflow.get": ApiOperationRoute("GET", "/api/v1/workflow/{workflow_id}"),
"workflow.update": ApiOperationRoute("PUT", "/api/v1/workflow/{workflow_id}"),
"workflow.delete": ApiOperationRoute("DELETE", "/api/v1/workflow/{workflow_id}"),
"workflow.actions": ApiOperationRoute("GET", "/api/v1/workflow/actions"),
"workflow.event_types": ApiOperationRoute("GET", "/api/v1/workflow/event_types"),
"workflow.plugin.actions": ApiOperationRoute("GET", "/api/v1/workflow/plugin/actions"),
"workflow.start": ApiOperationRoute("POST", "/api/v1/workflow/{workflow_id}/start"),
"workflow.pause": ApiOperationRoute("POST", "/api/v1/workflow/{workflow_id}/pause"),
"workflow.reset": ApiOperationRoute("POST", "/api/v1/workflow/{workflow_id}/reset"),
"workflow.shares": ApiOperationRoute("GET", "/api/v1/workflow/shares"),
"workflow.share": ApiOperationRoute("POST", "/api/v1/workflow/share"),
"workflow.share.delete": ApiOperationRoute("DELETE", "/api/v1/workflow/share/{share_id}"),
"workflow.fork": ApiOperationRoute("POST", "/api/v1/workflow/fork"),
"torrent.cache.get": ApiOperationRoute("GET", "/api/v1/torrent/cache"),
"torrent.cache.delete": ApiOperationRoute("DELETE", "/api/v1/torrent/cache/{domain}/{torrent_hash}"),
"torrent.cache.clear": ApiOperationRoute("DELETE", "/api/v1/torrent/cache"),
"torrent.cache.refresh": ApiOperationRoute("POST", "/api/v1/torrent/cache/refresh"),
"torrent.cache.reidentify": ApiOperationRoute("POST", "/api/v1/torrent/cache/reidentify/{domain}/{torrent_hash}"),
"database.backups.list": ApiOperationRoute("GET", "/api/v1/system/database/backups"),
"database.backups.create": ApiOperationRoute("POST", "/api/v1/system/database/backups"),
"database.backups.verify": ApiOperationRoute("POST", "/api/v1/system/database/backups/{name}/verify"),
"database.backups.delete": ApiOperationRoute("DELETE", "/api/v1/system/database/backups/{name}"),
"filter.test": ApiOperationRoute("GET", "/api/v1/system/ruletest"),
"system.network.targets": ApiOperationRoute("GET", "/api/v1/system/nettest/targets"),
"system.network.test": ApiOperationRoute("GET", "/api/v1/system/nettest"),
"system.module.list": ApiOperationRoute("GET", "/api/v1/system/modulelist"),
"system.module.catalog": ApiOperationRoute("GET", "/api/v1/system/module-catalog"),
"system.module.settings": ApiOperationRoute("GET", "/api/v1/system/module-settings"),
"system.module.test": ApiOperationRoute("GET", "/api/v1/system/moduletest/{moduleid}"),
"plugin.market.sync_wiki": ApiOperationRoute("POST", "/api/v1/system/setting/PLUGIN_MARKET/sync-wiki"),
"plugin.runtime.status": ApiOperationRoute("GET", "/api/v1/plugin/runtime"),
"plugin.history": ApiOperationRoute("GET", "/api/v1/plugin/history/{plugin_id}"),
"plugin.releases": ApiOperationRoute("GET", "/api/v1/plugin/releases/{plugin_id}"),
"plugin.ratings": ApiOperationRoute("GET", "/api/v1/plugin/rating"),
"plugin.rating": ApiOperationRoute("GET", "/api/v1/plugin/rating/{plugin_id}"),
"plugin.rating.submit": ApiOperationRoute("POST", "/api/v1/plugin/rating/{plugin_id}"),
"plugin.statistics": ApiOperationRoute("GET", "/api/v1/plugin/statistic"),
"plugin.reset": ApiOperationRoute("GET", "/api/v1/plugin/reset/{plugin_id}"),
"plugin.clone": ApiOperationRoute("POST", "/api/v1/plugin/clone/{plugin_id}"),
"plugin.clone.restorable": ApiOperationRoute(
"GET", "/api/v1/plugin/clone/{plugin_id}/restorable"
),
"config.user.get": ApiOperationRoute("GET", "/api/v1/system/global/user"),
"config.public.get": ApiOperationRoute("GET", "/api/v1/system/setting/public/{key}"),
"system.usage.statistics": ApiOperationRoute("GET", "/api/v1/system/usage/statistic"),
"plugin.folders.get": ApiOperationRoute("GET", "/api/v1/plugin/folders"),
"plugin.folders.update": ApiOperationRoute("POST", "/api/v1/plugin/folders"),
"plugin.folder.create": ApiOperationRoute("POST", "/api/v1/plugin/folders/{folder_name}"),
"plugin.folder.update": ApiOperationRoute("PATCH", "/api/v1/plugin/folders/{folder_name}"),
"plugin.folder.delete": ApiOperationRoute("DELETE", "/api/v1/plugin/folders/{folder_name}"),
"plugin.folder.plugins.update": ApiOperationRoute("PUT", "/api/v1/plugin/folders/{folder_name}/plugins"),
"plugin.folder.plugin.assign": ApiOperationRoute(
"PUT",
"/api/v1/plugin/folders/{folder_name}/plugins/{plugin_id}",
),
"plugin.folder.plugin.remove": ApiOperationRoute(
"DELETE",
"/api/v1/plugin/folders/{folder_name}/plugins/{plugin_id}",
),
"plugin.loglevel.get": ApiOperationRoute("GET", "/api/v1/plugin/loglevel/{plugin_id}"),
"plugin.loglevel.set": ApiOperationRoute(
"PUT", "/api/v1/plugin/loglevel/{plugin_id}/{instance_id}"
),
"plugin.loglevel.clear": ApiOperationRoute(
"DELETE", "/api/v1/plugin/loglevel/{plugin_id}/{instance_id}"
),
"plugin.default_target.set": ApiOperationRoute(
"PUT", "/api/v1/plugin/instances/{plugin_id}/{instance_id}/default_target"
),
"plugin.default_target.clear": ApiOperationRoute(
"DELETE", "/api/v1/plugin/instances/{plugin_id}/{instance_id}/default_target"
),
"plugin.instance.set_enabled": ApiOperationRoute(
"POST", "/api/v1/plugin/instance/{instance_id}/enabled"
),
"plugin.instance.purge": ApiOperationRoute(
"POST", "/api/v1/plugin/instance/{instance_id}/purge"
),
}
def resolve_api_operation(operation_id: str) -> Optional[ApiOperationSpec]:
"""
按稳定 operation ID 查找 API 操作。
"""
return API_OPERATION_BY_ID.get(operation_id)
def resolve_api_route(operation_id: str) -> Optional[ApiOperationRoute]:
"""
按稳定 operation ID 查找固定 API 路由。
"""
return API_OPERATION_ROUTES.get(operation_id)
def list_api_operation_ids() -> tuple[str, ...]:
"""
返回按注册顺序排列的 API 操作 ID。
"""
return tuple(spec.operation_id for spec in API_OPERATION_SPECS)
__all__ = [
"API_UNBOUND_READ_OPERATIONS",
"API_FIRST_BATCH_OPERATION_SPECS",
"API_EXTENDED_OPERATION_SPECS",
"API_MUSIC_OPERATION_SPECS",
"API_OPERATION_BY_ID",
"API_OPERATION_ROUTES",
"API_OPERATION_SPECS",
"API_PARITY_OPERATION_SPECS",
"API_SYSTEM_OPERATION_SPECS",
"ApiIdentityScope",
"ApiOperationRoute",
"ApiOperationSpec",
"list_api_operation_ids",
"resolve_api_operation",
"resolve_api_route",
]