diff --git a/app/api/endpoints/module.py b/app/api/endpoints/module.py new file mode 100644 index 000000000..8bbd0ac6b --- /dev/null +++ b/app/api/endpoints/module.py @@ -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) diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index 7b222df0f..a935d94f8 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -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), diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 4dbf12e06..58e26ae87 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -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) | diff --git a/docs/refactor/optimization-checklist.md b/docs/refactor/optimization-checklist.md index 4e4d30d30..82575c7be 100644 --- a/docs/refactor/optimization-checklist.md +++ b/docs/refactor/optimization-checklist.md @@ -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 等基线均为零 | diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 5feee68c1..89b8c9aef 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -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", diff --git a/tests/test_module_manager_capability_adapter.py b/tests/test_module_manager_capability_adapter.py index e22712c2e..69c5b47de 100644 --- a/tests/test_module_manager_capability_adapter.py +++ b/tests/test_module_manager_capability_adapter.py @@ -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 diff --git a/tests/test_system_i18n.py b/tests/test_system_i18n.py index 6ba0b1385..e65226678 100644 --- a/tests/test_system_i18n.py +++ b/tests/test_system_i18n.py @@ -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