refactor(api): isolate module endpoints

This commit is contained in:
jxxghp
2026-09-11 11:08:03 +08:00
parent 895fda2a01
commit 92d809f351
7 changed files with 186 additions and 156 deletions

153
app/api/endpoints/module.py Normal file
View 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)

View File

@@ -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),

View File

@@ -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) |

View File

@@ -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 等基线均为零 |

View File

@@ -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",

View File

@@ -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

View File

@@ -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