Files
MoviePilot/app/api/endpoints/module.py
jxxghp 6d4ce83837 fix(types): clear host mypy debt and prevent typing regressions
严格检查全部宿主源码,清零类型错误预算并移除按模块类型豁免。
按原因、所属方法、错误码及语法指纹锁定必要兼容例外和站点类型桩,
CI 阻断新增忽略、检查范围退化、本地桩替代真实接口及新的类型错误。

修复空值、异步和供应方类型边界,保持插件与 API 兼容合同,
并补充存储、订阅、通知、Agent 和工作流边界的回归覆盖。
2026-10-21 22:03:18 +08:00

179 lines
6.3 KiB
Python

"""
系统内置模块目录、开关和可用性测试接口。
"""
from typing import Any
from fastapi import Depends
from app.adapters.web.security.access import verify_token
from app.api.dependencies.auth import get_current_active_superuser_async
from app.api.principal import ApiPrincipal
from app.api.response import ResponseAPIRouter
from app.application.configuration import get_runtime_settings
from app.application.module import get_module_manager
from app.runtime.extensions.module.adapter import (
capture_host_module_config,
should_expose_host_module_option,
should_run_host_module,
)
from app.runtime.localization import LocaleHelper
from app.schemas.response import Response as _SchemaResponse
from app.schemas.system import SystemModuleCatalogListData as _SchemaSystemModuleCatalogListData
from app.schemas.system import SystemModuleListData as _SchemaSystemModuleListData
from app.schemas.system import SystemModuleSettingListData as _SchemaSystemModuleSettingListData
from app.schemas.token import TokenPayload as _SchemaTokenPayload
router = ResponseAPIRouter()
_MODULE_DESCRIPTION_FALLBACKS = {
"downloader": "下载器适配器,负责创建和管理下载任务。",
"indexer": "站点索引模块,负责搜索站点资源并解析结果。",
"mediarecognize": "媒体数据源,负责媒体识别、搜索或探索数据。",
"mediaserver": "媒体服务器适配器,负责同步媒体库并执行媒体操作。",
"notification": "消息通知通道,负责发送系统事件和任务结果。",
"other": "媒体处理或运行基础模块。",
}
def _module_description(module_id: str, name: str, module_type: str) -> str:
"""
读取模块的本地化职责说明,并为新增模块按类型提供可读兜底。
"""
fallback = _MODULE_DESCRIPTION_FALLBACKS.get(module_type, f"{name} 模块。")
return LocaleHelper.translate(
f"system.modules.{module_id}.description",
default=fallback,
)
@router.get(
"/modulelist",
summary="查询已启用的模块ID列表",
response_model=_SchemaResponse[_SchemaSystemModuleListData],
)
def modulelist(
_: _SchemaTokenPayload = Depends(verify_token),
) -> _SchemaResponse[dict[str, Any]]:
"""
查询当前配置下应参与健康检查的模块 ID 列表。
"""
modules = []
for spec in get_module_manager().list_enabled_specs():
module_id = spec.id
name = str(spec.metadata["name"])
modules.append(
{
"id": module_id,
"name": name,
"name_i18n": LocaleHelper.translate(
f"system.modules.{module_id}.name",
default=name,
),
"name_key": f"system.modules.{module_id}.name",
}
)
return _SchemaResponse(success=True, data={"modules": modules})
@router.get(
"/module-catalog",
summary="查询宿主模块目录",
response_model=_SchemaResponse[_SchemaSystemModuleCatalogListData],
)
def module_catalog(
_: _SchemaTokenPayload = Depends(verify_token),
) -> _SchemaResponse[dict[str, Any]]:
"""
返回模块及其服务类型目录,供前端选择器统一构造选项。
"""
manager = get_module_manager()
specs = manager.list_specs()
snapshot = capture_host_module_config(specs)
modules = []
for spec in specs:
name = str(spec.metadata["name"])
selector = spec.selector
option_value = None
if selector is not None and selector.kind == "system_config_item":
option_value = str(selector.config["match_value"])
modules.append(
{
"id": spec.id,
"name": name,
"name_i18n": LocaleHelper.translate(
f"system.modules.{spec.id}.name",
default=name,
),
"name_key": f"system.modules.{spec.id}.name",
"description_i18n": _module_description(
spec.id,
name,
str(spec.metadata.get("type", "")),
),
"description_key": f"system.modules.{spec.id}.description",
"type": str(spec.metadata["type"]),
"subtype": str(spec.metadata["subtype"]),
"option_value": option_value,
"enabled": should_expose_host_module_option(spec, snapshot),
"active": should_run_host_module(spec, snapshot),
}
)
return _SchemaResponse(success=True, data={"modules": modules})
@router.get(
"/module-settings",
summary="查询可手动开关的内置模块",
response_model=_SchemaResponse[_SchemaSystemModuleSettingListData],
)
async def module_settings(
_: ApiPrincipal = Depends(get_current_active_superuser_async),
) -> _SchemaResponse[dict[str, Any]]:
"""
查询没有其它激活配置、可由用户统一控制的内置模块。
"""
configured = get_runtime_settings().get("MODULE_ENABLE", {})
if not isinstance(configured, dict):
configured = {}
modules = []
for spec in get_module_manager().list_switchable_specs():
module_id = spec.id
name = str(spec.metadata["name"])
modules.append(
{
"id": module_id,
"name": name,
"name_i18n": LocaleHelper.translate(
f"system.modules.{module_id}.name",
default=name,
),
"name_key": f"system.modules.{module_id}.name",
"description_i18n": _module_description(
module_id,
name,
str(spec.metadata.get("type", "")),
),
"description_key": f"system.modules.{module_id}.description",
"enabled": configured.get(module_id, True) is not False,
}
)
return _SchemaResponse(success=True, data={"modules": modules})
@router.get(
"/moduletest/{moduleid}",
summary="模块可用性测试",
response_model=_SchemaResponse[None],
)
def moduletest(
moduleid: str,
_: _SchemaTokenPayload = Depends(verify_token),
) -> _SchemaResponse[None]:
"""
运行指定模块的内置可用性测试。
"""
state, errmsg = get_module_manager().test(moduleid)
return _SchemaResponse(success=state, message=errmsg)