mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 04:47:03 +08:00
refactor(api): isolate module endpoints
This commit is contained in:
153
app/api/endpoints/module.py
Normal file
153
app/api/endpoints/module.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""系统内置模块目录、开关和可用性测试接口。"""
|
||||
|
||||
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)):
|
||||
"""查询当前配置下应参与健康检查的模块 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)):
|
||||
"""返回模块及其服务类型目录,供前端选择器统一构造选项。"""
|
||||
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),
|
||||
):
|
||||
"""查询没有其它激活配置、可由用户统一控制的内置模块。"""
|
||||
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)):
|
||||
"""运行指定模块的内置可用性测试。"""
|
||||
state, errmsg = get_module_manager().test(moduleid)
|
||||
return _SchemaResponse(success=state, message=errmsg)
|
||||
@@ -18,6 +18,7 @@ from app.api.dependencies.auth import (
|
||||
get_current_active_user_async,
|
||||
)
|
||||
from app.api.endpoints.identifier import router as system_identifiers_router
|
||||
from app.api.endpoints.module import router as module_router
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.api.response import CompatibleCountParam, CompatiblePageParam, ResponseAPIRouter
|
||||
from app.application.backup import DatabaseBackupInProgressError
|
||||
@@ -28,7 +29,6 @@ from app.application.configuration import (
|
||||
from app.application.database import get_database_governance
|
||||
from app.application.image import ImageHelper
|
||||
from app.application.messaging.message import MessageHelper
|
||||
from app.application.module import get_module_manager
|
||||
from app.application.network import get_configured_network_test_service
|
||||
from app.application.rules import RuleHelper
|
||||
from app.application.scheduling import get_scheduler
|
||||
@@ -44,11 +44,6 @@ from app.domain.metainfo import MetaInfo
|
||||
from app.foundation.crypto import HashUtils
|
||||
from app.foundation.environment import is_free_threaded_runtime, is_gil_enabled
|
||||
from app.runtime.execution import run_in_threadpool_to_completion
|
||||
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.runtime.log import logger
|
||||
from app.runtime.progress import AsyncProgressHelper
|
||||
@@ -66,9 +61,6 @@ from app.schemas.system import PluginMarketSyncData as _SchemaPluginMarketSyncDa
|
||||
from app.schemas.system import PluginMarketSyncRequest as _SchemaPluginMarketSyncRequest
|
||||
from app.schemas.system import RuleTestData as _SchemaRuleTestData
|
||||
from app.schemas.system import SystemEnvironmentUpdateData as _SchemaSystemEnvironmentUpdateData
|
||||
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.system import SystemSettingsUpdateRequest as _SchemaSystemSettingsUpdateRequest
|
||||
from app.schemas.system import SystemUpdateRequest as _SchemaSystemUpdateRequest
|
||||
from app.schemas.system import SystemUpdateStatus as _SchemaSystemUpdateStatus
|
||||
@@ -78,6 +70,7 @@ from app.startup.composition.context import HostRuntime
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
router.routes.extend(system_identifiers_router.routes)
|
||||
router.routes.extend(module_router.routes)
|
||||
|
||||
_PUBLIC_SYSTEM_CONFIG_KEYS = {
|
||||
item.value: item
|
||||
@@ -94,25 +87,6 @@ _PUBLIC_SYSTEM_CONFIG_KEYS = {
|
||||
}
|
||||
_PUBLIC_SETTINGS_KEYS = {"PLUGIN_MARKET"}
|
||||
|
||||
_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,
|
||||
)
|
||||
|
||||
|
||||
def _database_backup_artifact_data(artifact: Any) -> _SchemaDatabaseBackupArtifactData:
|
||||
"""将内部备份制品映射为不含宿主路径的 Web DTO。"""
|
||||
return _SchemaDatabaseBackupArtifactData(
|
||||
@@ -996,122 +970,6 @@ async def nettest(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/modulelist",
|
||||
summary="查询已启用的模块ID列表",
|
||||
response_model=_SchemaResponse[_SchemaSystemModuleListData],
|
||||
)
|
||||
def modulelist(_: _SchemaTokenPayload = Depends(verify_token)):
|
||||
"""
|
||||
查询当前配置下应参与健康检查的模块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)):
|
||||
"""返回模块及其服务类型目录,供前端选择器统一构造选项。"""
|
||||
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),
|
||||
):
|
||||
"""查询没有其它激活配置、可由用户统一控制的内置模块。"""
|
||||
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)):
|
||||
"""
|
||||
模块可用性测试接口
|
||||
"""
|
||||
state, errmsg = get_module_manager().test(moduleid)
|
||||
return _SchemaResponse(success=state, message=errmsg)
|
||||
|
||||
|
||||
@router.get("/restart", summary="重启系统", response_model=_SchemaResponse[None])
|
||||
def restart_system(
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser),
|
||||
|
||||
@@ -756,8 +756,8 @@ flowchart LR
|
||||
|
||||
| 指标 | 当前值 |
|
||||
|---|---:|
|
||||
| Python 模块 | 1007 |
|
||||
| 内部导入边 | 8,535 |
|
||||
| Python 模块 | 1008 |
|
||||
| 内部导入边 | 8,553 |
|
||||
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
|
||||
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
|
||||
| Direct egress | 53(债务已清零,53 条精确 containment) |
|
||||
|
||||
@@ -94,7 +94,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
|
||||
|
||||
| 指标 | 当前值 | 解释 |
|
||||
|---|---:|---|
|
||||
| 宿主 Python 模块 / 内部依赖边 | 1007 / 8,535 | `dependency-baseline.json` 当前快照;分类、下载资源归类、订阅搜索、整理恢复、Agent 计划、工具视觉、终端生命周期与终端作用域模块的受控依赖 |
|
||||
| 宿主 Python 模块 / 内部依赖边 | 1008 / 8,553 | `dependency-baseline.json` 当前快照;分类、下载资源归类、订阅搜索、整理恢复、Agent 计划、工具视觉、终端生命周期与终端作用域模块的受控依赖 |
|
||||
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
|
||||
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
|
||||
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
|
||||
|
||||
@@ -1077,8 +1077,8 @@
|
||||
"runtime_only": true
|
||||
}
|
||||
},
|
||||
"edge_count": 8535,
|
||||
"edge_sha256": "001d6178d5320f297ec9595a9af7eebfa3168ae8ac4ad68f27ffe930788ded51",
|
||||
"edge_count": 8553,
|
||||
"edge_sha256": "5b4077c44b41aaa13e5b10e41ffd5e29df274a9c40fefe14ba41189f731caffa",
|
||||
"edges": [
|
||||
"app -> app.foundation",
|
||||
"app -> app.foundation.environment",
|
||||
@@ -2556,6 +2556,27 @@
|
||||
"app.api.endpoints.mfa -> app.schemas.mfa",
|
||||
"app.api.endpoints.mfa -> app.schemas.response",
|
||||
"app.api.endpoints.mfa -> app.schemas.token",
|
||||
"app.api.endpoints.module -> app.adapters",
|
||||
"app.api.endpoints.module -> app.adapters.web",
|
||||
"app.api.endpoints.module -> app.adapters.web.security",
|
||||
"app.api.endpoints.module -> app.adapters.web.security.access",
|
||||
"app.api.endpoints.module -> app.api",
|
||||
"app.api.endpoints.module -> app.api.dependencies",
|
||||
"app.api.endpoints.module -> app.api.dependencies.auth",
|
||||
"app.api.endpoints.module -> app.api.principal",
|
||||
"app.api.endpoints.module -> app.api.response",
|
||||
"app.api.endpoints.module -> app.application",
|
||||
"app.api.endpoints.module -> app.application.configuration",
|
||||
"app.api.endpoints.module -> app.application.module",
|
||||
"app.api.endpoints.module -> app.runtime",
|
||||
"app.api.endpoints.module -> app.runtime.extensions",
|
||||
"app.api.endpoints.module -> app.runtime.extensions.module",
|
||||
"app.api.endpoints.module -> app.runtime.extensions.module.adapter",
|
||||
"app.api.endpoints.module -> app.runtime.localization",
|
||||
"app.api.endpoints.module -> app.schemas",
|
||||
"app.api.endpoints.module -> app.schemas.response",
|
||||
"app.api.endpoints.module -> app.schemas.system",
|
||||
"app.api.endpoints.module -> app.schemas.token",
|
||||
"app.api.endpoints.music -> app.adapters",
|
||||
"app.api.endpoints.music -> app.adapters.web",
|
||||
"app.api.endpoints.music -> app.adapters.web.security",
|
||||
@@ -2877,6 +2898,7 @@
|
||||
"app.api.endpoints.system -> app.api.dependencies.auth",
|
||||
"app.api.endpoints.system -> app.api.endpoints",
|
||||
"app.api.endpoints.system -> app.api.endpoints.identifier",
|
||||
"app.api.endpoints.system -> app.api.endpoints.module",
|
||||
"app.api.endpoints.system -> app.api.principal",
|
||||
"app.api.endpoints.system -> app.api.response",
|
||||
"app.api.endpoints.system -> app.application",
|
||||
@@ -2886,7 +2908,6 @@
|
||||
"app.api.endpoints.system -> app.application.image",
|
||||
"app.api.endpoints.system -> app.application.messaging",
|
||||
"app.api.endpoints.system -> app.application.messaging.message",
|
||||
"app.api.endpoints.system -> app.application.module",
|
||||
"app.api.endpoints.system -> app.application.network",
|
||||
"app.api.endpoints.system -> app.application.rules",
|
||||
"app.api.endpoints.system -> app.application.scheduling",
|
||||
@@ -2908,9 +2929,6 @@
|
||||
"app.api.endpoints.system -> app.foundation.environment",
|
||||
"app.api.endpoints.system -> app.runtime",
|
||||
"app.api.endpoints.system -> app.runtime.execution",
|
||||
"app.api.endpoints.system -> app.runtime.extensions",
|
||||
"app.api.endpoints.system -> app.runtime.extensions.module",
|
||||
"app.api.endpoints.system -> app.runtime.extensions.module.adapter",
|
||||
"app.api.endpoints.system -> app.runtime.localization",
|
||||
"app.api.endpoints.system -> app.runtime.log",
|
||||
"app.api.endpoints.system -> app.runtime.progress",
|
||||
@@ -9616,7 +9634,7 @@
|
||||
"app.workflow.actions.transfer_file -> app.workflow",
|
||||
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
||||
],
|
||||
"module_count": 1007,
|
||||
"module_count": 1008,
|
||||
"modules": [
|
||||
"app",
|
||||
"app.adapters",
|
||||
@@ -9797,6 +9815,7 @@
|
||||
"app.api.endpoints.mediaserver",
|
||||
"app.api.endpoints.message",
|
||||
"app.api.endpoints.mfa",
|
||||
"app.api.endpoints.module",
|
||||
"app.api.endpoints.music",
|
||||
"app.api.endpoints.notification",
|
||||
"app.api.endpoints.openai",
|
||||
|
||||
@@ -934,7 +934,7 @@ manager = ModuleManager()
|
||||
assert len(manager.list_specs()) == 41
|
||||
assert manager.get_specs() == manager.list_specs()
|
||||
|
||||
from app.api.endpoints.system import modulelist
|
||||
from app.api.endpoints.module import modulelist
|
||||
response = modulelist(None)
|
||||
assert len(response.data["modules"]) == 17
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.api.endpoints import system as system_endpoint
|
||||
from app.api.endpoints import module as system_endpoint
|
||||
from app.runtime.localization import LocaleHelper
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user