From e1509c4e0dc024467f183614aaca21dd4d07dea0 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 23 Aug 2026 21:24:34 +0800 Subject: [PATCH] refactor: reorganize startup persistence boundaries --- AGENTS.md | 19 +- app/api/context.py | 2 +- app/api/dependencies/agent.py | 2 +- app/api/dependencies/auth.py | 2 +- app/api/dependencies/history.py | 2 +- app/api/dependencies/site.py | 2 +- app/api/dependencies/subscription.py | 2 +- app/api/dependencies/workflow.py | 2 +- app/application/agent.py | 4 +- app/application/commands.py | 2 +- app/application/messaging/skill.py | 2 +- app/application/scheduling.py | 2 +- app/cli.py | 2 +- app/db/adapters/__init__.py | 1 + .../chain_events.py => db/adapters/chain.py} | 4 +- .../adapters/download.py} | 0 app/{startup => db/adapters}/outbox.py | 2 +- app/{startup => db/adapters}/site.py | 0 app/{startup => db/adapters}/subscription.py | 92 +-- app/{startup => db/adapters}/transaction.py | 0 app/{startup => db/adapters}/workflow.py | 0 app/db/base.py | 51 +- app/db/decorators.py | 96 ++- app/db/models/agenttask.py | 12 +- app/db/models/message.py | 14 +- app/db/models/passkey.py | 11 +- app/db/models/plugindata.py | 30 +- app/db/models/site.py | 26 +- app/db/models/siteicon.py | 7 +- app/db/models/sitestatistic.py | 7 +- app/db/models/subscribe.py | 53 +- app/db/models/transferhistory.py | 13 +- app/db/models/user.py | 21 +- app/runtime/compat/manifest.py | 4 +- app/startup/composition/__init__.py | 1 + .../{ => composition}/configuration.py | 0 app/startup/{ => composition}/context.py | 0 app/startup/{ => composition}/database.py | 0 app/startup/composition/subscription.py | 91 +++ app/startup/initializers/__init__.py | 1 + .../agent.py} | 0 .../cache.py} | 0 .../command.py} | 0 .../database.py} | 2 +- .../domain.py} | 0 .../managed_resources.py} | 0 .../modules.py} | 26 +- .../monitor.py} | 0 .../plugins.py} | 0 .../routers.py} | 0 .../scheduler.py} | 0 .../transfer.py} | 0 .../workflow.py} | 0 app/startup/lifecycle/__init__.py | 26 +- app/testing/bootstrap.py | 6 +- database/env.py | 2 +- docs/architecture-overview.md | 11 +- .../backend-architecture-governance.md | 6 +- .../backend-architecture-next-stage.md | 20 +- .../backend-module-refactor-compatibility.md | 2 +- docs/rules/05-architecture.md | 52 +- docs/rules/06-code-styles.md | 4 +- docs/rules/10-data-and-persistent.md | 63 +- mypy.ini | 12 +- scripts/local_setup.py | 8 +- tests/conftest.py | 12 +- .../configuration-debt-baseline.json | 2 +- .../architecture/dependency-baseline.json | 768 +++++++++--------- .../runtime-contract-baseline.json | 26 +- tests/test_agent_api_lazy_imports.py | 2 +- tests/test_agent_lazy_initializer.py | 2 +- tests/test_agent_lazy_runtime_boundary.py | 2 +- tests/test_agent_lifecycle.py | 3 +- tests/test_api_response.py | 12 +- tests/test_architecture_dependencies.py | 81 ++ tests/test_cache_system.py | 12 +- tests/test_chain_durable_events.py | 2 +- tests/test_configuration_initializer.py | 2 +- tests/test_database_backup_initializer.py | 2 +- tests/test_database_index_migration.py | 2 +- tests/test_database_migration_startup.py | 4 +- tests/test_db_config_user_queries.py | 8 +- tests/test_db_plugin_message_agent_queries.py | 9 +- tests/test_event_runtime_components.py | 2 +- tests/test_host_runtime_context.py | 2 +- tests/test_legacy_plugin_resource_imports.py | 2 +- tests/test_lifecycle_shutdown.py | 3 +- tests/test_managed_resources.py | 4 +- tests/test_monitor_lifecycle.py | 2 +- tests/test_plugin_monitor_lifecycle.py | 2 +- tests/test_resource_v3.py | 2 +- tests/test_router_aggregation.py | 8 +- tests/test_scheduler_cache_expiry.py | 2 +- tests/test_transactional_download_failure.py | 6 +- tests/test_transfer_worker_lifecycle.py | 2 +- tests/test_type_gate.py | 8 +- 96 files changed, 1036 insertions(+), 782 deletions(-) create mode 100644 app/db/adapters/__init__.py rename app/{startup/chain_events.py => db/adapters/chain.py} (97%) rename app/{startup/download_failure.py => db/adapters/download.py} (100%) rename app/{startup => db/adapters}/outbox.py (98%) rename app/{startup => db/adapters}/site.py (100%) rename app/{startup => db/adapters}/subscription.py (58%) rename app/{startup => db/adapters}/transaction.py (100%) rename app/{startup => db/adapters}/workflow.py (100%) create mode 100644 app/startup/composition/__init__.py rename app/startup/{ => composition}/configuration.py (100%) rename app/startup/{ => composition}/context.py (100%) rename app/startup/{ => composition}/database.py (100%) create mode 100644 app/startup/composition/subscription.py create mode 100644 app/startup/initializers/__init__.py rename app/startup/{agent_initializer.py => initializers/agent.py} (100%) rename app/startup/{cache_initializer.py => initializers/cache.py} (100%) rename app/startup/{command_initializer.py => initializers/command.py} (100%) rename app/startup/{database_initializer.py => initializers/database.py} (98%) rename app/startup/{domain_initializer.py => initializers/domain.py} (100%) rename app/startup/{managed_resources_initializer.py => initializers/managed_resources.py} (100%) rename app/startup/{modules_initializer.py => initializers/modules.py} (97%) rename app/startup/{monitor_initializer.py => initializers/monitor.py} (100%) rename app/startup/{plugins_initializer.py => initializers/plugins.py} (100%) rename app/startup/{routers_initializer.py => initializers/routers.py} (100%) rename app/startup/{scheduler_initializer.py => initializers/scheduler.py} (100%) rename app/startup/{transfer_initializer.py => initializers/transfer.py} (100%) rename app/startup/{workflow_initializer.py => initializers/workflow.py} (100%) diff --git a/AGENTS.md b/AGENTS.md index 01f810bb3..54c95e223 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,11 +66,13 @@ The legacy roots have no physical directories in the source tree. Current images | `app/adapters/cache/` | Redis 与文件缓存等具体持久化实现 | 缓存协议、装饰器和进程内缓存策略 | `backends.py`, `redis.py` | | `app/adapters/system/` | 操作系统、文件、进程、标准流、包/资源安装、显示和 Rust 加速适配 | 业务规则、进程重启决策 | `host.py`, `display/`, `stdio.py`, `package.py`, `resource.py`, `rust.py`, `fsproxy.py` | | `app/adapters/external/` | CookieCloud、插件市场、OCR、IP 归属和 MoviePilot Server 等命名外部生态 | 通用 HTTP/DNS/文件机制或可复用领域语义 | `market.py`, `server.py`, `cookiecloud.py`, `ocr.py`, `location.py`, `wechat_crypt.py` | -| `app/application/` | 读取配置/持久化状态的聚焦应用服务和服务族规则 | 多领域 Chain 编排、底层通用机制、通用传输协议 | `recognition.py`, `filter.py`, `filter_rules.py`, `notification.py`, `mediaserver.py`, `rss.py`, `site/sites.*` | +| `app/application/` | 聚焦应用服务、用例命令,以及由用例拥有的持久化 Port/Protocol | SQLAlchemy、Session、Oper 等具体 DB 实现,多领域 Chain 编排、底层通用机制、通用传输协议 | `recognition.py`, `filter.py`, `outbox.py`, `subscription/write.py`, `workflow.py` | | `app/application/messaging/` | 消息渲染/路由、交互和 Agent 到消息桥接:`interaction.py` 通用交互契约和视图工具;`router.py` 统一交互优先级和回调分发;`site.py`/`subscribe.py`/`skill.py` 对应命令的会话、输入解析和视图;`media.py` 媒体交互状态(业务工作流仍由 `MediaInteractionChain` 执行);`plugin.py` 插件输入接管和插件按钮回调;`agent.py` Agent 选择状态、回调协议和 WebAgent 消息桥接;`message.py` 通知渲染、模板和队列。不作为推荐给插件直接使用的公开 SDK | 认证策略、通用 HTTP、服务发现、仅端点使用的 Web Push 行为 | `message.py`, `interaction.py`, `router.py`, `agent.py` | | `app/application/security/` | 认证、授权、Cookie、Passkey、OTP/二次认证、路径/URL 安全、SSRF 和签名策略 | 通用 URL 解析、进程运行策略、普通业务校验 | `access.py`, `auth.py`, `cookie.py`, `passkey.py`, `otp.py`, `twofactor.py`, `url.py` | -| `app/chain/` | Reusable use-case orchestration across modules, services, Oper classes, events, and caches; chains reach modules only through `run_module` dispatch on method-name contracts | Transport schemas, backend-specific protocol details, generic primitives, direct imports of module internals (classes, exceptions, constants) | `media.py`, `download.py`, `subscribe.py`, `transfer.py` | -| `app/startup/` | Composition root: inject providers/adapters, order initialization and shutdown, decide restart/lifecycle policy | Reusable business rules or adapter implementation details | `lifecycle.py`, `domain_initializer.py`, `cache_initializer.py`, `managed_resources_initializer.py`, `modules_initializer.py` | +| `app/chain/` | Reusable use-case orchestration across modules, Application services, injected ports, events, and caches; chains reach modules only through `run_module` dispatch on method-name contracts | Transport schemas, backend-specific protocol details, generic primitives, direct Oper/DB imports, direct imports of module internals (classes, exceptions, constants) | `media.py`, `download.py`, `subscribe.py`, `transfer.py` | +| `app/db/oper/` | 面向表和持久化值的 SQLAlchemy 数据访问;接收调用方 Session,只查询、暂存或 flush | Application 业务规则、隐式事务所有权、外部副作用 | `subscribe.py`, `site.py`, `workflow.py` | +| `app/db/adapters/` | 实现 Application 持久化 Port,创建短生命周期 Session/UoW,并适配 Oper | 用例规则、启动顺序、进程生命周期 | `subscription.py`, `site.py`, `outbox.py`, `workflow.py` | +| `app/startup/` | Composition root: `composition/` 构造并注入跨层依赖,`initializers/` 按领域初始化,`lifecycle/` 编排启动关闭 | Reusable business rules or adapter implementation details | `composition/context.py`, `composition/database.py`, `initializers/modules.py`, `lifecycle/components.py` | | `app/sdk/` | Deliberately curated stable imports for new plugins | Canonical implementation logic or host-internal dependencies | `browser.py`, `cache.py`, `logging.py`, `media.py`, `network.py`, `services.py` | | `app/runtime/compat/` | 仅依赖标准库的精确旧导入路由、资源前置扫描和 DEBUG 诊断 | 业务实现、通配猜测、目标模块的提前导入 | `manifest.py`, `imports.py`, `resource_imports.py`, `diagnostics.py` | @@ -87,9 +89,10 @@ Use these questions in order before creating or moving a module: 5. Does it perform configured network, cache, OS/process, file, package/resource, stdio, or Rust I/O? Put it under the matching `adapters` technical boundary. 6. Does it implement a named external product/ecosystem workflow? Put it in `adapters/external`. 7. Does it own authentication, authorization, signing, SSRF, URL/path safety, OTP, passkeys, or two-factor behavior? Put it in `application/security`. -8. Does it read persisted user configuration, coordinate one bounded capability, or normalize/match one service family? Put it in `application`. -9. Does it coordinate several modules/services/Oper classes for one use case? Put it in `chain`. -10. Is it public to plugins or only preserving an old path? Curate it in `sdk` or map it in `runtime/compat`; do not move implementation there. +8. Does it define a use case or the persistence Port required by that use case? Put it in `application`; do not import concrete DB there. +9. Does it implement an Application persistence Port with SQLAlchemy Session/UoW/Oper? Put it in `db/adapters`. +10. Does it coordinate several modules/services/Oper classes for one use case? Put it in `chain`. +11. Is it public to plugins or only preserving an old path? Curate it in `sdk` or map it in `runtime/compat`; do not move implementation there. ### Enforced Split Examples @@ -97,11 +100,11 @@ These decisions are architectural constraints, not naming suggestions: * Cache contracts, memory backends, decorators, and proxies stay in `app/runtime/cache.py`; Redis and filesystem implementations stay in `app/adapters/cache/backends.py`. Startup registers concrete factories before decorated business modules are imported. Legacy `app.core.cache` resolves to the complete `app.sdk.cache` facade. * The complete logging runtime stays in `app/runtime/log.py`: policy, console/plugin routing, async rotating file output, and shutdown. `app.runtime.config` supplies the resolved settings and log path. `runtime/log.py` remains a dependency leaf with no `app.*` imports. Plugins use `app.sdk.logging`; legacy `app.log` resolves to that SDK facade. -* Recognition parsing stays pure in `app/domain/meta/` and `app/domain/metainfo.py`. `app/application/recognition.py` reads `SystemConfigOper`; `app/startup/domain_initializer.py` injects rules, extension policy, source defaults, TMDB image construction, and the optional Rust accelerator. +* Recognition parsing stays pure in `app/domain/meta/` and `app/domain/metainfo.py`. `app/application/recognition.py` consumes injected configuration; `app/startup/initializers/domain.py` injects rules, extension policy, source defaults, TMDB image construction, and the optional Rust accelerator. * Kodi-style NFO reading and metadata document generation are one domain capability and stay together in `app/domain/scraper.py`; a separate `domain/nfo.py` must not be recreated. * `app/application/mediaserver.py` is the single media-server service capability module. It owns configured service discovery together with Provider ID normalization and music-library matching, while reusing generic identity rules from `app/domain/media.py`. * Configured notification-service discovery belongs in `app/application/notification.py`. Web Push subscription and manual-send HTTP behavior stays in `app/api/endpoints/message.py`; it is not a reusable messaging capability module. -* `app/adapters/system/resource.py` detects/downloads/installs resources and returns whether installation occurred. Only `app/startup/modules_initializer.py` may decide to restart the process afterward. +* `app/adapters/system/resource.py` detects/downloads/installs resources and returns whether installation occurred. Only `app/startup/initializers/modules.py` may decide to restart the process afterward. * Process memory/GC policy belongs in `app/runtime/gc.py`; external IP-location APIs belong in `app/adapters/external/location.py`. * Security implementation filenames use package-context nouns: `app/application/security/url.py` and `app/application/security/twofactor.py`. Historical `app.utils.security` and `app.helper.twofa` remain compatibility mappings only. diff --git a/app/api/context.py b/app/api/context.py index 247d2cf1b..72bc9f022 100644 --- a/app/api/context.py +++ b/app/api/context.py @@ -17,7 +17,7 @@ from app.application.subscription.mutation import ( SubscriptionHistoryMutationRepository, SubscriptionMutationRepository, ) -from app.startup.context import ( +from app.startup.composition.context import ( AgentChatRuntime, HostRuntime, SubscriptionRuntime, diff --git a/app/api/dependencies/agent.py b/app/api/dependencies/agent.py index 9785d9e5c..cdfec0d8c 100644 --- a/app/api/dependencies/agent.py +++ b/app/api/dependencies/agent.py @@ -17,7 +17,7 @@ from app.application.messaging.chat import ( AsyncUnitOfWork, ) from app.application.messaging.message import MessageQueryService -from app.startup.context import AgentChatRuntime, HostRuntime +from app.startup.composition.context import AgentChatRuntime, HostRuntime def get_agent_chat_service( diff --git a/app/api/dependencies/auth.py b/app/api/dependencies/auth.py index ab83a4660..48e05fcb8 100644 --- a/app/api/dependencies/auth.py +++ b/app/api/dependencies/auth.py @@ -21,7 +21,7 @@ from app.application.security.user import ( UserService, ) from app.schemas.token import TokenPayload as _SchemaTokenPayload -from app.startup.context import HostRuntime +from app.startup.composition.context import HostRuntime def get_user_service( diff --git a/app/api/dependencies/history.py b/app/api/dependencies/history.py index 3367ac8af..5e86c79f4 100644 --- a/app/api/dependencies/history.py +++ b/app/api/dependencies/history.py @@ -18,7 +18,7 @@ from app.chain.storage import StorageChain from app.runtime.events import eventmanager from app.schemas.types import EventType from app.schemas.workflow import FileItem as _SchemaFileItem -from app.startup.context import HostRuntime +from app.startup.composition.context import HostRuntime def get_mediaserver_query_service( diff --git a/app/api/dependencies/site.py b/app/api/dependencies/site.py index 01e383e0e..08ba4bcc0 100644 --- a/app/api/dependencies/site.py +++ b/app/api/dependencies/site.py @@ -14,7 +14,7 @@ from app.domain import site as site_rules from app.foundation import url as url_tools from app.runtime.events import eventmanager from app.schemas.types import EventType -from app.startup.context import HostRuntime +from app.startup.composition.context import HostRuntime async def _publish_site_updated(payload: dict[str, Any]) -> None: diff --git a/app/api/dependencies/subscription.py b/app/api/dependencies/subscription.py index c82ea88f0..065908b05 100644 --- a/app/api/dependencies/subscription.py +++ b/app/api/dependencies/subscription.py @@ -37,7 +37,7 @@ from app.application.subscription.search import SearchSubscriptionsCommand from app.runtime.events import eventmanager from app.runtime.log import logger from app.schemas.types import EventType -from app.startup.context import HostRuntime +from app.startup.composition.context import HostRuntime from app.api.context import get_background_task_registry, resolve_background_task_registry from app.runtime.tasks import TaskRegistry diff --git a/app/api/dependencies/workflow.py b/app/api/dependencies/workflow.py index ff3acbda8..f5ce6a506 100644 --- a/app/api/dependencies/workflow.py +++ b/app/api/dependencies/workflow.py @@ -17,7 +17,7 @@ from app.application.workflow import ( ) from app.runtime.config import global_vars from app.workflow import WorkFlowManager -from app.startup.context import HostRuntime +from app.startup.composition.context import HostRuntime def get_workflow_mutation_command( diff --git a/app/application/agent.py b/app/application/agent.py index efec398d1..dba3e29a8 100644 --- a/app/application/agent.py +++ b/app/application/agent.py @@ -13,7 +13,7 @@ from typing import Any, Callable, Optional Provider = Callable[[], Any] -# provider 注册表由 startup/agent_initializer 在组合根装配。 +# provider 注册表由 startup/initializers/agent.py 在组合根装配。 _agent_manager_provider: Optional[Provider] = None _running_agent_manager_provider: Optional[Provider] = None _prompt_manager_provider: Optional[Provider] = None @@ -66,7 +66,7 @@ def _resolve(provider: Optional[Provider], service_name: str) -> Any: if provider is None: raise RuntimeError( f"Agent 服务 {service_name} 未注册:" - "请先导入 app.startup.agent_initializer 完成组合根装配" + "请先导入 app.startup.initializers.agent 完成组合根装配" ) return provider() diff --git a/app/application/commands.py b/app/application/commands.py index 7aebbe229..ff6e0400a 100644 --- a/app/application/commands.py +++ b/app/application/commands.py @@ -11,7 +11,7 @@ Command 实现由 startup 组合根在导入期注册,避免 application 层 from typing import Any, Dict, Optional -# Command 类:由 startup/command_initializer 在导入期注册。 +# Command 类:由 startup/initializers/command.py 在导入期注册。 _command_class: Any = None diff --git a/app/application/messaging/skill.py b/app/application/messaging/skill.py index 96dfc5822..da6b534ca 100644 --- a/app/application/messaging/skill.py +++ b/app/application/messaging/skill.py @@ -58,7 +58,7 @@ def _resolve_skill_catalog() -> SkillCatalogPort: """解析已注入的技能目录;缺少组合根装配时给出明确错误。""" if _skill_catalog_provider is None: raise RuntimeError( - "技能目录服务未注册:请先导入 app.startup.agent_initializer " + "技能目录服务未注册:请先导入 app.startup.initializers.agent " "完成组合根装配" ) return _skill_catalog_provider() diff --git a/app/application/scheduling.py b/app/application/scheduling.py index 97daf070e..b3ff11d18 100644 --- a/app/application/scheduling.py +++ b/app/application/scheduling.py @@ -17,7 +17,7 @@ from typing import Any, Awaitable, Callable, List, Optional, cast # Agent 自主定时任务在运行时调度器中的任务 ID 前缀。 AGENT_TASK_JOB_PREFIX = "agent-task" -# Scheduler 类:由 startup/scheduler_initializer 在导入期注册。 +# Scheduler 类:由 startup/initializers/scheduler.py 在导入期注册。 _scheduler_class: Any = None diff --git a/app/cli.py b/app/cli.py index bc5f805e1..988e337b6 100644 --- a/app/cli.py +++ b/app/cli.py @@ -21,7 +21,7 @@ from app.runtime.settings import RuntimeSettingsCompat settings = RuntimeSettingsCompat() from app.runtime.state import SystemHelper from app.application.backup import BackupArtifact -from app.startup.database import build_database_governance +from app.startup.composition.database import build_database_governance from version import APP_VERSION BACKEND_RUNTIME_FILE = settings.TEMP_PATH / "moviepilot.runtime.json" diff --git a/app/db/adapters/__init__.py b/app/db/adapters/__init__.py new file mode 100644 index 000000000..8631fcd40 --- /dev/null +++ b/app/db/adapters/__init__.py @@ -0,0 +1 @@ +"""实现 Application 持久化端口的 SQLAlchemy 适配器。""" diff --git a/app/startup/chain_events.py b/app/db/adapters/chain.py similarity index 97% rename from app/startup/chain_events.py rename to app/db/adapters/chain.py index f51dfdd4d..637882a30 100644 --- a/app/startup/chain_events.py +++ b/app/db/adapters/chain.py @@ -1,4 +1,4 @@ -"""Chain durable 事件写入端口的 SQLAlchemy 启动适配器。""" +"""Chain durable 事件写入端口的 SQLAlchemy 适配器。""" from __future__ import annotations @@ -17,10 +17,10 @@ from app.application.chain.durable_events import ( ) from app.application.history import TransferHistoryRecord, TransferHistoryWriter from app.application.outbox import DurableEventCommand, OutboxIntent +from app.db.adapters.outbox import SqlAlchemyOutboxRepository from app.db.oper.downloadhistory import DownloadHistoryOper from app.db.oper.transferhistory import TransferHistoryOper from app.db.uow import SqlAlchemyUnitOfWork -from app.startup.outbox import SqlAlchemyOutboxRepository class _StagingTransferHistoryWriter: diff --git a/app/startup/download_failure.py b/app/db/adapters/download.py similarity index 100% rename from app/startup/download_failure.py rename to app/db/adapters/download.py diff --git a/app/startup/outbox.py b/app/db/adapters/outbox.py similarity index 98% rename from app/startup/outbox.py rename to app/db/adapters/outbox.py index c51d7de32..c05b4b4e0 100644 --- a/app/startup/outbox.py +++ b/app/db/adapters/outbox.py @@ -1,4 +1,4 @@ -"""启动组合层使用的 SQLAlchemy outbox 持久化适配器。""" +"""Application outbox 端口的 SQLAlchemy 持久化适配器。""" from datetime import datetime diff --git a/app/startup/site.py b/app/db/adapters/site.py similarity index 100% rename from app/startup/site.py rename to app/db/adapters/site.py diff --git a/app/startup/subscription.py b/app/db/adapters/subscription.py similarity index 58% rename from app/startup/subscription.py rename to app/db/adapters/subscription.py index 7d643f79f..c8d34b4b6 100644 --- a/app/startup/subscription.py +++ b/app/db/adapters/subscription.py @@ -1,9 +1,8 @@ -"""订阅写入事务适配器的启动装配。""" +"""订阅写入端口的 SQLAlchemy 事务适配器。""" from collections.abc import Callable -from contextlib import AbstractAsyncContextManager, asynccontextmanager, contextmanager +from contextlib import AbstractAsyncContextManager from datetime import datetime, timezone -from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session @@ -17,30 +16,12 @@ from app.application.subscription.write import ( subscription_added_notification_key, subscription_added_report_key, ) -from app.application.subscription.delete import ( - DeleteSubscribeCommand, - configure_delete_subscribe_scope, -) -from app.application.subscription.complete import ( - CompleteSubscriptionCommand, - configure_subscription_completion_scope, -) -from app.application.subscription.mutation import ( - SubscriptionMutationService, - configure_subscription_mutation_scope, -) -from app.adapters.external.server import MoviePilotServerHelper -from app.db.oper.subscribe import SubscribeOper -from app.db.oper.subscribehistory import SubscribeHistoryOper -from app.db.session import async_session_scope -from app.db.session import SessionFactory -from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork -from app.startup.outbox import ( +from app.db.adapters.outbox import ( SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository, ) -from app.runtime.events import EventManager -from app.schemas.types import EventType +from app.db.oper.subscribe import SubscribeOper +from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork class TransactionalSubscribeWriter: @@ -146,66 +127,3 @@ class TransactionalSubscribeWriter: delivered, notification, ) - - -async def _publish_modified(payload: dict[str, Any]) -> None: - """发布事务已提交的订阅修改事件。""" - await EventManager().async_send_event(EventType.SubscribeModified, payload) - - -async def _publish_deleted(payload: dict[str, Any]) -> None: - """发布事务已提交的订阅删除事件。""" - await EventManager().async_send_event(EventType.SubscribeDeleted, payload) - - -def _publish_completed(payload: dict[str, Any]) -> None: - """发布已提交的订阅完成事件。""" - EventManager().send_event(EventType.SubscribeComplete, payload) - - -@contextmanager -def subscription_completion_scope(): - """为同步完成链创建独占 Session、UoW 与 durable outbox。""" - session = SessionFactory() - try: - yield CompleteSubscriptionCommand( - repository=SubscribeOper(session), - unit_of_work=SqlAlchemyUnitOfWork(session), - outbox=SqlAlchemyOutboxRepository(session), - publish=_publish_completed, - ) - finally: - session.close() - - -@asynccontextmanager -async def subscription_mutation_scope(): - """为非 HTTP 入口创建独占订阅修改会话、UoW 与 outbox。""" - async with async_session_scope() as session: - yield SubscriptionMutationService( - repository=SubscribeOper(session), - history_repository=SubscribeHistoryOper(session), - unit_of_work=SqlAlchemyAsyncUnitOfWork(session), - outbox=SqlAlchemyAsyncOutboxStager(session), - publish_modified=_publish_modified, - ) - - -@asynccontextmanager -async def delete_subscribe_scope(): - """为非 HTTP 入口创建独占订阅删除会话、UoW 与 outbox。""" - async with async_session_scope() as session: - yield DeleteSubscribeCommand( - repository=SubscribeOper(session), - unit_of_work=SqlAlchemyAsyncUnitOfWork(session), - publish_deleted=_publish_deleted, - report_deleted=MoviePilotServerHelper.async_sub_done_durable, - outbox=SqlAlchemyAsyncOutboxStager(session), - ) - - -def configure_transactional_subscription_scopes() -> None: - """登记 Agent 等非 HTTP 入口复用的订阅事务作用域。""" - configure_subscription_mutation_scope(subscription_mutation_scope) - configure_delete_subscribe_scope(delete_subscribe_scope) - configure_subscription_completion_scope(subscription_completion_scope) diff --git a/app/startup/transaction.py b/app/db/adapters/transaction.py similarity index 100% rename from app/startup/transaction.py rename to app/db/adapters/transaction.py diff --git a/app/startup/workflow.py b/app/db/adapters/workflow.py similarity index 100% rename from app/startup/workflow.py rename to app/db/adapters/workflow.py diff --git a/app/db/base.py b/app/db/base.py index 70ba83643..f28043057 100644 --- a/app/db/base.py +++ b/app/db/base.py @@ -1,7 +1,7 @@ """ ORM 基类与数据访问基类。 -Base 提供声明式基类与通用的行为(字典转换、增删改查便利方法); +Base 提供声明式基类与兼容行为(字典转换、旧增删改查便利方法); DbOper 是各业务 Oper 的基类,持有一个可注入的会话。 """ from collections.abc import Awaitable, Callable @@ -12,9 +12,14 @@ from sqlalchemy import (CursorResult, Executable, Identity, Integer, Sequence, from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import DeclarativeBase, Mapped, Session, declared_attr, mapped_column -from app.runtime.config import settings -from app.db.decorators import async_db_query, async_db_update, db_query, db_update +from app.db.decorators import ( + legacy_async_db_query, + legacy_async_db_update, + legacy_db_query, + legacy_db_update, +) from app.db.uow import run_async_transaction, run_sync_transaction +from app.runtime.config import settings T = TypeVar("T") @@ -64,88 +69,104 @@ class Base(DeclarativeBase): # type: ignore[misc] # SQLAlchemy 无 py.typed 继承本类的模型一律使用 mapped_column() + Mapped[] 注解;确需非映射的类级属性时 用 ClassVar 显式声明,而不是把这个标志加回来。 + + create/get/update/delete/list/truncate 及其异步版本仅保留旧插件 ABI。宿主新代码应由 + Application Command 定义事务边界,经显式 Session 调用 Oper,不得新增对这些方法的依赖。 """ # 由 get_id_column() 在各模型中提供实际的列定义,这里只声明类型供 IDE 使用 id: Mapped[int] - @db_update + @legacy_db_update def create(self, db: Session) -> None: + """兼容旧插件调用:新增当前模型并提交。""" db.add(self) - @async_db_update + @legacy_async_db_update async def async_create(self, db: AsyncSession) -> Self: + """兼容旧插件调用:异步新增当前模型、刷新主键并提交。""" db.add(self) await db.flush() return self @classmethod - @db_query + @legacy_db_query def get(cls, db: Session, rid: int) -> Optional[Self]: + """兼容旧插件调用:按主键查询当前模型。""" return cast( Optional[Self], db.execute(select(cls).where(and_(cls.id == rid))).scalars().first(), ) @classmethod - @async_db_query + @legacy_async_db_query async def async_get(cls, db: AsyncSession, rid: int) -> Optional[Self]: + """兼容旧插件调用:异步按主键查询当前模型。""" result = await db.execute(select(cls).where(and_(cls.id == rid))) return cast(Optional[Self], result.scalars().first()) - @db_update + @legacy_db_update def update(self, db: Session, payload: dict[str, Any]) -> None: + """兼容旧插件调用:更新当前模型字段并提交。""" for key, value in payload.items(): setattr(self, key, value) if inspect(self).detached: db.add(self) - @async_db_update + @legacy_async_db_update async def async_update( self, db: AsyncSession, payload: dict[str, Any], ) -> None: + """兼容旧插件调用:异步更新当前模型字段并提交。""" for key, value in payload.items(): setattr(self, key, value) if inspect(self).detached: db.add(self) @classmethod - @db_update + @legacy_db_update def delete(cls, db: Session, rid: Any) -> None: + """兼容旧插件调用:按主键删除当前模型并提交。""" db.execute(delete(cls).where(and_(cls.id == rid))) @classmethod - @async_db_update + @legacy_async_db_update async def async_delete(cls, db: AsyncSession, rid: Any) -> None: + """兼容旧插件调用:异步按主键删除当前模型并提交。""" result = await db.execute(select(cls).where(and_(cls.id == rid))) user = result.scalars().first() if user: await db.delete(user) @classmethod - @db_update + @legacy_db_update def truncate(cls, db: Session) -> None: + """兼容旧插件调用:清空当前模型表并提交。""" db.execute(delete(cls)) @classmethod - @async_db_update + @legacy_async_db_update async def async_truncate(cls, db: AsyncSession) -> None: + """兼容旧插件调用:异步清空当前模型表并提交。""" await db.execute(delete(cls)) @classmethod - @db_query + @legacy_db_query def list(cls, db: Session) -> List[Self]: + """兼容旧插件调用:查询当前模型的全部记录。""" return list(db.execute(select(cls)).scalars().all()) @classmethod - @async_db_query + @legacy_async_db_query async def async_list(cls, db: AsyncSession) -> List[Self]: + """兼容旧插件调用:异步查询当前模型的全部记录。""" result = await db.execute(select(cls)) return list(result.scalars().all()) def to_dict(self) -> dict[str, Any]: + """把当前模型的映射列转换为字典。""" return {c.name: getattr(self, c.name, None) for c in self.__table__.columns} # noqa @declared_attr.directive # type: ignore[misc] # SQLAlchemy decorator 缺少类型信息 diff --git a/app/db/decorators.py b/app/db/decorators.py index 168f497e7..3a858418e 100644 --- a/app/db/decorators.py +++ b/app/db/decorators.py @@ -5,8 +5,8 @@ 未显式传入会话时自动创建,并在结束时归还——异步路径经 async_session_scope 收口, 连接池与配额都在那里生效。 -收尾故障(rollback / close / __aexit__ 自身抛异常)一律只记日志、不上抛,四个装饰器 -的处理一致。理由与代价都要写明,别当成漏写的 raise: +收尾故障(rollback / close / __aexit__ 自身抛异常)一律只记日志、不上抛,正式装饰器 +和 legacy 兼容壳的处理一致。理由与代价都要写明,别当成漏写的 raise: - 连接断开、事务已失效这类故障恰恰最容易发生在「出错之后」的收尾阶段。裸写收尾语句时 它一抛错就顶替掉原始异常,调用方看到的只剩「connection reset」,业务异常连类型都被 @@ -28,31 +28,12 @@ from app.runtime.log import logger _R = TypeVar("_R") -# 四个装饰器都会重写实参列表:未传会话时自行创建一个并塞回 db 位置。因此包装后的可调用 +# 正式装饰器会重写实参列表:未传会话时自行创建一个并塞回 db 位置。因此包装后的可调用 # 对象接受的实参与被包装函数的签名并不一致——用 Callable[..., _R] 如实表达「参数由装饰器 # 接管、返回值原样透传」。否则调用方传 None 或传异步会话都会被判成类型不符,而这恰恰是 # 装饰器存在的理由(各 Oper 的 self._db 常态就是 None)。 -def run_legacy_sync_query(operation: Callable[[Session], _R]) -> _R: - """为已移除查询装饰器的旧 Model ABI 提供一次性同步会话。""" - db = ScopedSession() - try: - return operation(db) - finally: - try: - db.close() - except Exception as close_err: # noqa: BLE001 兼容查询释放失败不改变返回语义 - logger.error(f"释放数据库会话失败:{close_err}") - - -async def run_legacy_async_query( - operation: Callable[[AsyncSession], Awaitable[_R]], -) -> _R: - """为移除异步查询装饰器的旧 Model ABI 提供一次性异步会话。""" - async with async_session_scope() as db: - return await operation(db) - def _get_args_db( args: tuple[Any, ...], kwargs: dict[str, Any], @@ -345,6 +326,77 @@ def legacy_async_db_query( return wrapper +def legacy_db_update(func: Callable[..., _R]) -> Callable[..., _R]: + """保留旧 Model 同步写 ABI,并维持历史自动提交语义。 + + 该装饰器只供已经公开的 Model/Base 方法兼容仓外插件。宿主新写路径必须 + 通过 Application Command、显式 Session 和 UnitOfWork 完成事务收口。 + """ + + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> _R: + db = _get_args_db(args, kwargs) + owns_session = db is None + if db is None: + db = ScopedSession() + args, kwargs = _inject_legacy_db(func, args, kwargs, db) + try: + result = func(*args, **kwargs) + db.commit() + return result + except Exception: + try: + db.rollback() + except Exception as rollback_err: # noqa: BLE001 回滚失败不能掩盖原始异常 + logger.error(f"事务回滚失败,原始异常将原样上抛:{rollback_err}") + raise + finally: + if owns_session: + try: + db.close() + except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 结果 + logger.error(f"释放数据库会话失败:{close_err}") + + return wrapper + + +def legacy_async_db_update( + func: Callable[..., Awaitable[_R]], +) -> Callable[..., Awaitable[_R]]: + """保留旧 Model 异步写 ABI,并维持历史自动提交语义。 + + 该装饰器只承接既有兼容面;新宿主代码不得用它创建隐式事务。 + """ + + @wraps(func) + async def wrapper(*args: Any, **kwargs: Any) -> _R: + db = _get_args_async_db(args, kwargs) + owns_session = db is None + scope = None + if db is None: + scope = async_session_scope() + db = await scope.__aenter__() + args, kwargs = _inject_legacy_db(func, args, kwargs, db) + try: + result = await func(*args, **kwargs) + await db.commit() + return result + except Exception: + try: + await db.rollback() + except Exception as rollback_err: # noqa: BLE001 回滚失败不能掩盖原始异常 + logger.error(f"事务回滚失败,原始异常将原样上抛:{rollback_err}") + raise + finally: + if owns_session and scope is not None: + try: + await scope.__aexit__(None, None, None) + except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 结果 + logger.error(f"释放数据库会话失败:{close_err}") + + return wrapper + + def _inject_legacy_db( func: Callable[..., _R], args: tuple[Any, ...], diff --git a/app/db/models/agenttask.py b/app/db/models/agenttask.py index 947c91cf9..0073dbccb 100644 --- a/app/db/models/agenttask.py +++ b/app/db/models/agenttask.py @@ -4,7 +4,7 @@ from sqlalchemy import Boolean, Index, Integer, String, Text, select, update from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import Base, execute_dml, get_id_column -from app.db.decorators import run_legacy_sync_query +from app.db.decorators import legacy_db_query def _get_for_user_statement( @@ -85,6 +85,7 @@ class AgentTask(Base): return task.id @classmethod + @legacy_db_query def get_for_user( cls, db: Session | int | None = None, @@ -105,11 +106,10 @@ class AgentTask(Base): _get_for_user_statement(cls, task_id=task_id, user_id=user_id) ).scalars().first() - if isinstance(db, Session): - return query(db) - return run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_db_query def list_for_user( cls, db: Session | None = None, @@ -125,9 +125,7 @@ class AgentTask(Base): _list_for_user_statement(cls, user_id=user_id, enabled=enabled) ).scalars().all()) - if isinstance(db, Session): - return query(db) - return run_legacy_sync_query(query) + return query(db) @classmethod def update_task( diff --git a/app/db/models/message.py b/app/db/models/message.py index 31193b20f..034adf974 100644 --- a/app/db/models/message.py +++ b/app/db/models/message.py @@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import Base, execute_dml, get_id_column -from app.db.decorators import run_legacy_async_query, run_legacy_sync_query +from app.db.decorators import legacy_async_db_query, legacy_db_query class Message(Base): @@ -49,6 +49,7 @@ class Message(Base): return self.to_dict() @classmethod + @legacy_db_query def list_by_page( cls, db: Session | None = None, @@ -67,9 +68,10 @@ class Message(Base): .limit(count) ).scalars().all()) - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_db_query def exists_by_source( cls, db: Session | str | None = None, @@ -93,9 +95,10 @@ class Message(Base): select(cls.id).where(cls.source == source).limit(1) ).scalars().first() is not None - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_list_by_page( cls, db: AsyncSession | None = None, page: int = 1, count: int = 30 ) -> List["Message"]: @@ -112,9 +115,10 @@ class Message(Base): ) return list(result.scalars().all()) - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_async_db_query async def async_list_sent_by_page( cls, db: AsyncSession | None = None, @@ -155,7 +159,7 @@ class Message(Base): ) return list(result.scalars().all()) - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod def delete_before( diff --git a/app/db/models/passkey.py b/app/db/models/passkey.py index 158e9caec..06179467e 100644 --- a/app/db/models/passkey.py +++ b/app/db/models/passkey.py @@ -8,7 +8,6 @@ from app.db.base import Base, get_id_column from app.db.decorators import ( legacy_async_db_query, legacy_db_query, - run_legacy_sync_query, ) @@ -55,6 +54,7 @@ class PassKey(Base): transports: Mapped[Optional[str]] = mapped_column(String, nullable=True) @classmethod + @legacy_db_query def get_by_user_id( cls, db: Session | int | None = None, @@ -72,9 +72,7 @@ class PassKey(Base): _get_by_user_id_statement(cls, user_id) ).scalars().all()) - if isinstance(db, Session): - return query(db) - return run_legacy_sync_query(query) + return query(db) @classmethod @legacy_async_db_query @@ -86,6 +84,7 @@ class PassKey(Base): return list(result.scalars().all()) @classmethod + @legacy_db_query def get_by_credential_id( cls, db: Session | str | None = None, @@ -103,9 +102,7 @@ class PassKey(Base): _get_by_credential_id_statement(cls, credential_id) ).scalars().first() - if isinstance(db, Session): - return query(db) - return run_legacy_sync_query(query) + return query(db) @classmethod @legacy_async_db_query diff --git a/app/db/models/plugindata.py b/app/db/models/plugindata.py index 8bf4bd78b..4aa9c9985 100644 --- a/app/db/models/plugindata.py +++ b/app/db/models/plugindata.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import get_id_column, Base -from app.db.decorators import run_legacy_async_query, run_legacy_sync_query +from app.db.decorators import legacy_async_db_query, legacy_db_query class PluginData(Base): @@ -21,54 +21,44 @@ class PluginData(Base): ) @classmethod + @legacy_db_query def get_plugin_data(cls, db: Session | None = None, plugin_id: str | None = None): """在调用方 Session 中读取插件全部数据,并兼容旧无会话入口。""" if plugin_id is None: raise TypeError("plugin_id is required") - if not isinstance(db, Session): - return run_legacy_sync_query(lambda session: cls.get_plugin_data(session, plugin_id)) return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all()) @classmethod + @legacy_async_db_query async def async_get_plugin_data( cls, db: AsyncSession | None = None, plugin_id: str | None = None ): """在调用方 AsyncSession 中读取插件全部数据,并兼容旧无会话入口。""" if plugin_id is None: raise TypeError("plugin_id is required") - if not isinstance(db, AsyncSession): - return await run_legacy_async_query( - lambda session: cls.async_get_plugin_data(session, plugin_id) - ) result = await db.execute(select(cls).where(cls.plugin_id == plugin_id)) return list(result.scalars().all()) @classmethod + @legacy_db_query def get_plugin_data_by_key( cls, db: Session | None = None, plugin_id: str | None = None, key: str | None = None ): """在调用方 Session 中按键读取插件数据,并兼容旧无会话入口。""" if plugin_id is None or key is None: raise TypeError("plugin_id and key are required") - if not isinstance(db, Session): - return run_legacy_sync_query( - lambda session: cls.get_plugin_data_by_key(session, plugin_id, key) - ) return db.execute( select(cls).where(cls.plugin_id == plugin_id, cls.key == key) ).scalars().first() @classmethod + @legacy_async_db_query async def async_get_plugin_data_by_key( cls, db: AsyncSession | None = None, plugin_id: str | None = None, key: str | None = None ): """在调用方 AsyncSession 中按键读取插件数据,并兼容旧无会话入口。""" if plugin_id is None or key is None: raise TypeError("plugin_id and key are required") - if not isinstance(db, AsyncSession): - return await run_legacy_async_query( - lambda session: cls.async_get_plugin_data_by_key(session, plugin_id, key) - ) result = await db.execute( select(cls).where(cls.plugin_id == plugin_id, cls.key == key) ) @@ -85,28 +75,22 @@ class PluginData(Base): db.execute(delete(cls).where(cls.plugin_id == plugin_id)) @classmethod + @legacy_db_query def get_plugin_data_by_plugin_id( cls, db: Session | None = None, plugin_id: str | None = None ): """在调用方 Session 中按插件 ID 读取数据,并兼容旧无会话入口。""" if plugin_id is None: raise TypeError("plugin_id is required") - if not isinstance(db, Session): - return run_legacy_sync_query( - lambda session: cls.get_plugin_data_by_plugin_id(session, plugin_id) - ) return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all()) @classmethod + @legacy_async_db_query async def async_get_plugin_data_by_plugin_id( cls, db: AsyncSession | None = None, plugin_id: str | None = None ): """在调用方 AsyncSession 中按插件 ID 读取数据,并兼容旧无会话入口。""" if plugin_id is None: raise TypeError("plugin_id is required") - if not isinstance(db, AsyncSession): - return await run_legacy_async_query( - lambda session: cls.async_get_plugin_data_by_plugin_id(session, plugin_id) - ) result = await db.execute(select(cls).where(cls.plugin_id == plugin_id)) return list(result.scalars().all()) diff --git a/app/db/models/site.py b/app/db/models/site.py index 7f6b873be..206880b3e 100644 --- a/app/db/models/site.py +++ b/app/db/models/site.py @@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import Base, get_id_column -from app.db.decorators import run_legacy_async_query, run_legacy_sync_query +from app.db.decorators import legacy_async_db_query, legacy_db_query class Site(Base): @@ -58,6 +58,7 @@ class Site(Base): downloader: Mapped[Optional[str]] = mapped_column(String) @classmethod + @legacy_db_query def get_by_domain(cls, db: Session | str | None = None, domain: str | None = None): """按域名查询站点,兼容显式会话和旧插件无会话调用。""" if domain is None and isinstance(db, str): @@ -69,9 +70,10 @@ class Site(Base): """在给定同步会话中执行域名查询。""" return session.execute(select(cls).where(cls.domain == domain)).scalars().first() - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_get_by_domain( cls, db: AsyncSession | str | None = None, @@ -88,9 +90,10 @@ class Site(Base): result = await session.execute(select(cls).where(cls.domain == domain)) return result.scalar_one_or_none() - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_async_db_query async def async_get_by_name( cls, db: AsyncSession | str | None = None, @@ -107,18 +110,20 @@ class Site(Base): result = await session.execute(select(cls).where(cls.name == name)) return result.scalar_one_or_none() - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_db_query def get_actives(cls, db: Session | None = None): """查询启用站点,兼容显式会话和旧插件无会话调用。""" def query(session: Session): """在给定同步会话中执行启用站点查询。""" return list(session.execute(select(cls).where(cls.is_active.is_(True))).scalars().all()) - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_get_actives(cls, db: AsyncSession | None = None): """异步查询启用站点,兼容显式会话和旧插件无会话调用。""" async def query(session: AsyncSession): @@ -126,18 +131,20 @@ class Site(Base): result = await session.execute(select(cls).where(cls.is_active.is_(True))) return list(result.scalars().all()) - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_db_query def list_order_by_pri(cls, db: Session | None = None): """按优先级升序查询站点,兼容显式会话和旧插件无会话调用。""" def query(session: Session): """在给定同步会话中执行优先级查询。""" return list(session.execute(select(cls).order_by(cls.pri)).scalars().all()) - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_list_order_by_pri(cls, db: AsyncSession | None = None): """异步按优先级升序查询站点,兼容显式会话和旧插件无会话调用。""" async def query(session: AsyncSession): @@ -145,9 +152,10 @@ class Site(Base): result = await session.execute(select(cls).order_by(cls.pri)) return list(result.scalars().all()) - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_db_query def get_domains_by_ids( cls, db: Session | list[int] | None = None, @@ -165,7 +173,7 @@ class Site(Base): """在给定同步会话中执行域名投影查询。""" return list(session.execute(select(cls.domain).where(cls.id.in_(ids))).scalars().all()) - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod def reset(cls, db: Session): diff --git a/app/db/models/siteicon.py b/app/db/models/siteicon.py index 3d420755b..2882c5cbd 100644 --- a/app/db/models/siteicon.py +++ b/app/db/models/siteicon.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import Base, get_id_column -from app.db.decorators import run_legacy_async_query +from app.db.decorators import legacy_async_db_query class SiteIcon(Base): @@ -27,6 +27,7 @@ class SiteIcon(Base): return db.execute(select(cls).where(cls.domain == domain)).scalars().first() @classmethod + @legacy_async_db_query async def async_get_by_domain( cls, db: AsyncSession | None = None, @@ -41,6 +42,4 @@ class SiteIcon(Base): result = await session.execute(select(cls).where(cls.domain == domain)) return result.scalar_one_or_none() - if isinstance(db, AsyncSession): - return await query(db) - return await run_legacy_async_query(query) + return await query(db) diff --git a/app/db/models/sitestatistic.py b/app/db/models/sitestatistic.py index 7d7ddf061..cf187c1bb 100644 --- a/app/db/models/sitestatistic.py +++ b/app/db/models/sitestatistic.py @@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import get_id_column, Base -from app.db.decorators import run_legacy_async_query +from app.db.decorators import legacy_async_db_query class SiteStatistic(Base): @@ -35,6 +35,7 @@ class SiteStatistic(Base): return db.execute(select(cls).where(cls.domain == domain)).scalars().first() @classmethod + @legacy_async_db_query async def async_get_by_domain( cls, db: AsyncSession | None = None, @@ -49,9 +50,7 @@ class SiteStatistic(Base): result = await session.execute(select(cls).where(cls.domain == domain)) return result.scalar_one_or_none() - if isinstance(db, AsyncSession): - return await query(db) - return await run_legacy_async_query(query) + return await query(db) @classmethod def reset(cls, db: Session): diff --git a/app/db/models/subscribe.py b/app/db/models/subscribe.py index ec71b11e2..92c125d60 100644 --- a/app/db/models/subscribe.py +++ b/app/db/models/subscribe.py @@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import get_id_column, Base -from app.db.decorators import run_legacy_async_query, run_legacy_sync_query +from app.db.decorators import legacy_async_db_query, legacy_db_query from app.db.models._constraints import media_identity_constraint from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource @@ -140,6 +140,7 @@ class Subscribe(Base): return condition @classmethod + @legacy_db_query def exists( cls, db: Session | MediaSource | None = None, media_source: MediaSource | str | None = None, @@ -164,9 +165,10 @@ class Subscribe(Base): return session.execute( statement.where(cls.episode_group == episode_group) ).scalars().first() - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_exists( cls, db: AsyncSession | MediaSource | None = None, media_source: MediaSource | str | None = None, @@ -192,9 +194,10 @@ class Subscribe(Base): statement.where(cls.episode_group == episode_group) ) return result.scalars().first() - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_db_query def exists_by_username( cls, db: Session | str | None = None, username: str | MediaSource | None = None, @@ -224,9 +227,10 @@ class Subscribe(Base): return session.execute( statement.where(cls.episode_group == episode_group) ).scalars().first() - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_exists_by_username( cls, db: AsyncSession | str | None = None, username: str | MediaSource | None = None, @@ -256,9 +260,10 @@ class Subscribe(Base): statement.where(cls.episode_group == episode_group) ) return result.scalars().first() - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_db_query def get_by_state(cls, db: Session | str | None = None, state: str | None = None): """按状态列表查询订阅,兼容显式会话和旧插件无会话调用。""" if not isinstance(db, Session): @@ -269,9 +274,10 @@ class Subscribe(Base): if state: statement = statement.where(cls.state.in_(state.split(','))) return list(session.execute(statement).scalars().all()) - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_get_by_state( cls, db: AsyncSession | str | None = None, state: str | None = None ): @@ -285,9 +291,10 @@ class Subscribe(Base): statement = statement.where(cls.state.in_(state.split(','))) result = await session.execute(statement) return list(result.scalars().all()) - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_db_query def get_by_title( cls, db: Session | str | None = None, title: str | None = None, season: Optional[int] = None, @@ -301,9 +308,10 @@ class Subscribe(Base): if season is not None: statement = statement.where(cls.season == season) return session.execute(statement).scalars().first() - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_get_by_title( cls, db: AsyncSession | str | None = None, title: str | None = None, season: Optional[int] = None, @@ -318,9 +326,10 @@ class Subscribe(Base): statement = statement.where(cls.season == season) result = await session.execute(statement) return result.scalars().first() - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_async_db_query async def async_list_by_title( cls, db: AsyncSession | str | None = None, title: str | None = None, season: Optional[int] = None, @@ -335,9 +344,10 @@ class Subscribe(Base): statement = statement.where(cls.season == season) result = await session.execute(statement) return list(result.scalars().all()) - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_db_query def list_by_media_identity( cls, db: Session | MediaSource | None = None, media_source: MediaSource | str | None = None, @@ -357,9 +367,10 @@ class Subscribe(Base): def query(session: Session): """在给定会话中执行媒体身份列表查询。""" return list(session.execute(select(cls).where(condition)).scalars().all()) - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_list_by_media_identity( cls, db: AsyncSession | MediaSource | None = None, media_source: MediaSource | str | None = None, @@ -380,9 +391,10 @@ class Subscribe(Base): """在给定异步会话中执行媒体身份列表查询。""" result = await session.execute(select(cls).where(condition)) return list(result.scalars().all()) - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_db_query def get_by( cls, db: Session | str | None = None, type: str | MediaSource | None = None, @@ -407,9 +419,10 @@ class Subscribe(Base): def query(session: Session): """在给定会话中执行类型媒体查询。""" return session.execute(statement).scalars().first() - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_get_by( cls, db: AsyncSession | str | None = None, type: str | MediaSource | None = None, @@ -435,9 +448,10 @@ class Subscribe(Base): """在给定异步会话中执行类型媒体查询。""" result = await session.execute(query) return result.scalars().first() - return await execute_query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(execute_query) + return await execute_query(db) @classmethod + @legacy_db_query def list_by_username(cls, db: Session | str | None = None, username: str | None = None, state: Optional[str] = None, mtype: Optional[str] = None): """按用户筛选订阅,兼容显式会话和旧插件无会话调用。""" @@ -451,9 +465,10 @@ class Subscribe(Base): if mtype: statement = statement.where(cls.type == mtype) return list(session.execute(statement).scalars().all()) - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_list_by_username(cls, db: AsyncSession | str | None = None, username: str | None = None, state: Optional[str] = None, mtype: Optional[str] = None): @@ -469,9 +484,10 @@ class Subscribe(Base): statement = statement.where(cls.type == mtype) result = await session.execute(statement) return list(result.scalars().all()) - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_db_query def list_by_type(cls, db: Session | str | None = None, mtype: str | None = None, days: int = 7): """按类型查询最近时间窗内的订阅,兼容显式会话和旧插件无会话调用。""" if not isinstance(db, Session): @@ -483,9 +499,10 @@ class Subscribe(Base): cls.date >= time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 86400 * int(days))) )).scalars().all()) - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_list_by_type(cls, db: AsyncSession | str | None = None, mtype: str | None = None, days: int = 7): """异步按类型查询最近时间窗内的订阅,兼容显式会话和旧插件无会话调用。""" @@ -499,4 +516,4 @@ class Subscribe(Base): time.localtime(time.time() - 86400 * int(days))) )) return list(result.scalars().all()) - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) diff --git a/app/db/models/transferhistory.py b/app/db/models/transferhistory.py index fbcde0361..6d68caffd 100644 --- a/app/db/models/transferhistory.py +++ b/app/db/models/transferhistory.py @@ -11,7 +11,6 @@ from app.db.base import Base, execute_dml, get_id_column from app.db.decorators import ( legacy_async_db_query, legacy_db_query, - run_legacy_sync_query, ) from app.db.models._constraints import media_identity_constraint from app.schemas.types import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MediaSource, MediaType @@ -189,6 +188,7 @@ class TransferHistory(Base): return list(result.scalars().all()) @classmethod + @legacy_db_query def get_by_hash( cls, db: Session | str | None = None, @@ -206,9 +206,10 @@ class TransferHistory(Base): select(cls).where(cls.download_hash == download_hash) ).scalars().first() - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_db_query def get_by_src( cls, db: Session | str | None = None, src: str | None = None, storage: Optional[str] = None @@ -235,9 +236,10 @@ class TransferHistory(Base): statement.order_by(cls.id.desc()) ).scalars().first() - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_db_query def get_success_by_src( cls, db: Session | str | None = None, src: str | None = None, storage: Optional[str] = None @@ -266,9 +268,10 @@ class TransferHistory(Base): statement.order_by(cls.id.desc()) ).scalars().first() - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_db_query def get_by_dest( cls, db: Session | str | None = None, dest: str | None = None, storage: Optional[str] = None @@ -295,7 +298,7 @@ class TransferHistory(Base): statement.order_by(cls.id.desc()) ).scalars().first() - return query(db) if isinstance(db, Session) else run_legacy_sync_query(query) + return query(db) @classmethod @legacy_db_query diff --git a/app/db/models/user.py b/app/db/models/user.py index 087687f3a..e77a50aee 100644 --- a/app/db/models/user.py +++ b/app/db/models/user.py @@ -4,10 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import Base, get_id_column -from app.db.decorators import ( - run_legacy_async_query, - run_legacy_sync_query, -) +from app.db.decorators import legacy_async_db_query, legacy_db_query class User(Base): @@ -38,6 +35,7 @@ class User(Base): settings: Mapped[Optional[Any]] = mapped_column(JSON, default=dict) @classmethod + @legacy_db_query def get_by_name( cls, db: Session | str | None = None, @@ -53,11 +51,10 @@ class User(Base): """在给定会话中执行用户名查询。""" return session.execute(select(cls).where(cls.name == name)).scalars().first() - if isinstance(db, Session): - return query(db) - return run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_get_by_name( cls, db: AsyncSession | str | None = None, @@ -74,9 +71,10 @@ class User(Base): result = await session.execute(select(cls).filter(cls.name == name)) return result.scalars().first() - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) @classmethod + @legacy_db_query def get_by_id(cls, db: Session | int | None = None, user_id: int | None = None): """按用户 ID 查询用户,兼容显式会话和旧插件无会话调用。""" if user_id is None and isinstance(db, int): @@ -88,11 +86,10 @@ class User(Base): """在给定会话中执行用户 ID 查询。""" return session.execute(select(cls).where(cls.id == user_id)).scalars().first() - if isinstance(db, Session): - return query(db) - return run_legacy_sync_query(query) + return query(db) @classmethod + @legacy_async_db_query async def async_get_by_id( cls, db: AsyncSession | int | None = None, @@ -109,7 +106,7 @@ class User(Base): result = await session.execute(select(cls).filter(cls.id == user_id)) return result.scalars().first() - return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query) + return await query(db) def delete_by_name(self, db: Session, name: str): user = self.get_by_name(db, name) diff --git a/app/runtime/compat/manifest.py b/app/runtime/compat/manifest.py index fb1fdaa83..76149e127 100644 --- a/app/runtime/compat/manifest.py +++ b/app/runtime/compat/manifest.py @@ -81,8 +81,8 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = { owner="db", ), "app.db.init": ModuleAlias( - target="app.startup.database_initializer", - replacement="app.startup.database_initializer", + target="app.startup.initializers.database", + replacement="app.startup.initializers.database", introduced="v3.0.0", owner="startup", ), diff --git a/app/startup/composition/__init__.py b/app/startup/composition/__init__.py new file mode 100644 index 000000000..6bb9a6de2 --- /dev/null +++ b/app/startup/composition/__init__.py @@ -0,0 +1 @@ +"""宿主运行时对象、配置快照与跨层依赖的组合构建。""" diff --git a/app/startup/configuration.py b/app/startup/composition/configuration.py similarity index 100% rename from app/startup/configuration.py rename to app/startup/composition/configuration.py diff --git a/app/startup/context.py b/app/startup/composition/context.py similarity index 100% rename from app/startup/context.py rename to app/startup/composition/context.py diff --git a/app/startup/database.py b/app/startup/composition/database.py similarity index 100% rename from app/startup/database.py rename to app/startup/composition/database.py diff --git a/app/startup/composition/subscription.py b/app/startup/composition/subscription.py new file mode 100644 index 000000000..3c15e8cd0 --- /dev/null +++ b/app/startup/composition/subscription.py @@ -0,0 +1,91 @@ +"""订阅事务作用域及提交后回调的组合装配。""" + +from contextlib import asynccontextmanager, contextmanager +from typing import Any + +from app.adapters.external.server import MoviePilotServerHelper +from app.application.subscription.complete import ( + CompleteSubscriptionCommand, + configure_subscription_completion_scope, +) +from app.application.subscription.delete import ( + DeleteSubscribeCommand, + configure_delete_subscribe_scope, +) +from app.application.subscription.mutation import ( + SubscriptionMutationService, + configure_subscription_mutation_scope, +) +from app.db.adapters.outbox import ( + SqlAlchemyAsyncOutboxStager, + SqlAlchemyOutboxRepository, +) +from app.db.oper.subscribe import SubscribeOper +from app.db.oper.subscribehistory import SubscribeHistoryOper +from app.db.session import SessionFactory, async_session_scope +from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork +from app.runtime.events import EventManager +from app.schemas.types import EventType + + +async def _publish_modified(payload: dict[str, Any]) -> None: + """发布事务已提交的订阅修改事件。""" + await EventManager().async_send_event(EventType.SubscribeModified, payload) + + +async def _publish_deleted(payload: dict[str, Any]) -> None: + """发布事务已提交的订阅删除事件。""" + await EventManager().async_send_event(EventType.SubscribeDeleted, payload) + + +def _publish_completed(payload: dict[str, Any]) -> None: + """发布已提交的订阅完成事件。""" + EventManager().send_event(EventType.SubscribeComplete, payload) + + +@contextmanager +def subscription_completion_scope(): + """为同步完成链创建独占 Session、UoW 与 durable outbox。""" + session = SessionFactory() + try: + yield CompleteSubscriptionCommand( + repository=SubscribeOper(session), + unit_of_work=SqlAlchemyUnitOfWork(session), + outbox=SqlAlchemyOutboxRepository(session), + publish=_publish_completed, + ) + finally: + session.close() + + +@asynccontextmanager +async def subscription_mutation_scope(): + """为非 HTTP 入口创建独占订阅修改会话、UoW 与 outbox。""" + async with async_session_scope() as session: + yield SubscriptionMutationService( + repository=SubscribeOper(session), + history_repository=SubscribeHistoryOper(session), + unit_of_work=SqlAlchemyAsyncUnitOfWork(session), + outbox=SqlAlchemyAsyncOutboxStager(session), + publish_modified=_publish_modified, + ) + + +@asynccontextmanager +async def delete_subscribe_scope(): + """为非 HTTP 入口创建独占订阅删除会话、UoW 与 outbox。""" + async with async_session_scope() as session: + yield DeleteSubscribeCommand( + repository=SubscribeOper(session), + unit_of_work=SqlAlchemyAsyncUnitOfWork(session), + publish_deleted=_publish_deleted, + report_deleted=MoviePilotServerHelper.async_sub_done_durable, + outbox=SqlAlchemyAsyncOutboxStager(session), + ) + + +def configure_transactional_subscription_scopes() -> None: + """登记 Agent 等非 HTTP 入口复用的订阅事务作用域。""" + configure_subscription_mutation_scope(subscription_mutation_scope) + configure_delete_subscribe_scope(delete_subscribe_scope) + configure_subscription_completion_scope(subscription_completion_scope) diff --git a/app/startup/initializers/__init__.py b/app/startup/initializers/__init__.py new file mode 100644 index 000000000..698c070c2 --- /dev/null +++ b/app/startup/initializers/__init__.py @@ -0,0 +1 @@ +"""按领域组织的宿主初始化与关闭入口。""" diff --git a/app/startup/agent_initializer.py b/app/startup/initializers/agent.py similarity index 100% rename from app/startup/agent_initializer.py rename to app/startup/initializers/agent.py diff --git a/app/startup/cache_initializer.py b/app/startup/initializers/cache.py similarity index 100% rename from app/startup/cache_initializer.py rename to app/startup/initializers/cache.py diff --git a/app/startup/command_initializer.py b/app/startup/initializers/command.py similarity index 100% rename from app/startup/command_initializer.py rename to app/startup/initializers/command.py diff --git a/app/startup/database_initializer.py b/app/startup/initializers/database.py similarity index 98% rename from app/startup/database_initializer.py rename to app/startup/initializers/database.py index 32866e1b7..389dba094 100644 --- a/app/startup/database_initializer.py +++ b/app/startup/initializers/database.py @@ -17,7 +17,7 @@ from app.db.base import Base from app.db.engine import get_engine from app.db.models import load_all_models from app.runtime.log import logger -from app.startup.database import build_database_governance +from app.startup.composition.database import build_database_governance def _build_alembic_config(engine: Engine | None = None) -> Config: diff --git a/app/startup/domain_initializer.py b/app/startup/initializers/domain.py similarity index 100% rename from app/startup/domain_initializer.py rename to app/startup/initializers/domain.py diff --git a/app/startup/managed_resources_initializer.py b/app/startup/initializers/managed_resources.py similarity index 100% rename from app/startup/managed_resources_initializer.py rename to app/startup/initializers/managed_resources.py diff --git a/app/startup/modules_initializer.py b/app/startup/initializers/modules.py similarity index 97% rename from app/startup/modules_initializer.py rename to app/startup/initializers/modules.py index dc1becc12..c093b4cc0 100644 --- a/app/startup/modules_initializer.py +++ b/app/startup/initializers/modules.py @@ -53,7 +53,7 @@ from app.application.configuration import ( configure_system_config, configure_transfer_retry_config, ) -from app.startup.configuration import ( +from app.startup.composition.configuration import ( build_api_runtime_config, build_chain_runtime_config, build_scheduler_runtime_config, @@ -83,7 +83,7 @@ from app.application.security.userconfig import ( ) from app.application.history import configure_transfer_history_provider from app.application.outbox import OutboxDispatcher, configure_outbox_dispatcher -from app.startup.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository +from app.db.adapters.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository from app.application.site.query import SiteQueryService, configure_site_query_service from app.application.site.health import SiteHealthService, configure_site_health_service from app.application.workflow import WorkflowQueryService, configure_workflow_query @@ -129,22 +129,22 @@ from app.command import CommandChain from app.schemas.message import Message from app.schemas.message import MessageType from app.schemas.types import EventType, SystemConfigKey -from app.startup.agent_initializer import init_agent -from app.startup.database import build_database_governance -from app.startup.managed_resources_initializer import ( +from app.startup.initializers.agent import init_agent +from app.startup.composition.database import build_database_governance +from app.startup.initializers.managed_resources import ( init_managed_resources, stop_managed_resources, ) -from app.startup.subscription import ( - TransactionalSubscribeWriter, +from app.db.adapters.subscription import TransactionalSubscribeWriter +from app.startup.composition.subscription import ( configure_transactional_subscription_scopes, ) -from app.startup.chain_events import TransactionalChainDurableEventWriter -from app.startup.download_failure import TransactionalDownloadFailureRepository -from app.startup.site import TransactionalSiteRepository -from app.startup.workflow import TransactionalWorkflowExecutionService -from app.startup.transaction import TransactionalWriteRunner -from app.startup.context import ( +from app.db.adapters.chain import TransactionalChainDurableEventWriter +from app.db.adapters.download import TransactionalDownloadFailureRepository +from app.db.adapters.site import TransactionalSiteRepository +from app.db.adapters.workflow import TransactionalWorkflowExecutionService +from app.db.adapters.transaction import TransactionalWriteRunner +from app.startup.composition.context import ( AgentChatRuntime, AuthenticationRuntime, HistoryRuntime, diff --git a/app/startup/monitor_initializer.py b/app/startup/initializers/monitor.py similarity index 100% rename from app/startup/monitor_initializer.py rename to app/startup/initializers/monitor.py diff --git a/app/startup/plugins_initializer.py b/app/startup/initializers/plugins.py similarity index 100% rename from app/startup/plugins_initializer.py rename to app/startup/initializers/plugins.py diff --git a/app/startup/routers_initializer.py b/app/startup/initializers/routers.py similarity index 100% rename from app/startup/routers_initializer.py rename to app/startup/initializers/routers.py diff --git a/app/startup/scheduler_initializer.py b/app/startup/initializers/scheduler.py similarity index 100% rename from app/startup/scheduler_initializer.py rename to app/startup/initializers/scheduler.py diff --git a/app/startup/transfer_initializer.py b/app/startup/initializers/transfer.py similarity index 100% rename from app/startup/transfer_initializer.py rename to app/startup/initializers/transfer.py diff --git a/app/startup/workflow_initializer.py b/app/startup/initializers/workflow.py similarity index 100% rename from app/startup/workflow_initializer.py rename to app/startup/initializers/workflow.py diff --git a/app/startup/lifecycle/__init__.py b/app/startup/lifecycle/__init__.py index 6c0bcf448..9e06a4471 100644 --- a/app/startup/lifecycle/__init__.py +++ b/app/startup/lifecycle/__init__.py @@ -8,7 +8,7 @@ from typing import Callable from fastapi import FastAPI -from app.startup.cache_initializer import configure_cache_dependencies +from app.startup.initializers.cache import configure_cache_dependencies # 缓存装饰器会在业务模块导入时创建后端,必须先完成适配器装配。 configure_cache_dependencies() # urllib3-future 覆盖 urllib3 命名空间后删除了 format_header_param,导致 telebot 崩溃,需在加载模块前打补丁 @@ -37,17 +37,17 @@ from app.runtime.tasks import TaskRegistry, configure_task_registry from app.adapters.external.server import MoviePilotServerHelper from app.runtime.state import SystemHelper from app.runtime.log import logger, LoggerManager -from app.startup.command_initializer import init_command, stop_command, restart_command -from app.startup.agent_initializer import stop_agent -from app.startup.domain_initializer import configure_domain_dependencies -from app.startup.modules_initializer import ( +from app.startup.initializers.command import init_command, stop_command, restart_command +from app.startup.initializers.agent import stop_agent +from app.startup.initializers.domain import configure_domain_dependencies +from app.startup.initializers.modules import ( drain_events, init_modules, settle_events, stop_modules, ) -from app.startup.monitor_initializer import stop_monitor, init_monitor -from app.startup.plugins_initializer import ( +from app.startup.initializers.monitor import stop_monitor, init_monitor +from app.startup.initializers.plugins import ( configure_plugin_services, execute_task, finalize_plugins, @@ -57,18 +57,18 @@ from app.startup.plugins_initializer import ( stop_plugin_monitor, sync_plugins, ) -from app.startup.routers_initializer import init_routers -from app.startup.scheduler_initializer import ( +from app.startup.initializers.routers import init_routers +from app.startup.initializers.scheduler import ( stop_scheduler, init_scheduler, init_plugin_scheduler, ) from app.db.engine import check_connection_budget, get_engine, get_global_async_engine -from app.startup.transfer_initializer import ( +from app.startup.initializers.transfer import ( replay_pending_transfers, stop_transfer_runtime, ) -from app.startup.workflow_initializer import init_workflow, stop_workflow +from app.startup.initializers.workflow import init_workflow, stop_workflow from app.startup.lifecycle.components import ( LifecycleComponent, LifecycleFailurePolicy, @@ -236,7 +236,7 @@ async def initialize_modules_component(app: FastAPI) -> None: try: runtime = await init_modules() except BaseException: - from app.startup.modules_initializer import stop_database_worker + from app.startup.initializers.modules import stop_database_worker try: await stop_database_worker() @@ -276,7 +276,7 @@ def prepare_database_component(app: FastAPI) -> None: """完成数据库建表、迁移与 head 校验后发布数据库就绪状态。""" # Alembic 及全部 ORM 元数据只在 lifespan 真正启动时加载,create_app/import 阶段 # 继续保持不建库、不加载迁移运行时的纯 ASGI 结构语义。 - from app.startup.database_initializer import ( + from app.startup.initializers.database import ( prepare_database, verify_database_revision, ) diff --git a/app/testing/bootstrap.py b/app/testing/bootstrap.py index 5c4e7a030..155b61401 100644 --- a/app/testing/bootstrap.py +++ b/app/testing/bootstrap.py @@ -180,7 +180,7 @@ def prepare_backend() -> None: """ isolate_config_dir() ensure_sites_stub() - from app.startup.database_initializer import init_db + from app.startup.initializers.database import init_db init_db() from app.db.oper.systemconfig import SystemConfigOper from app.db.oper.userconfig import UserConfigOper @@ -188,10 +188,10 @@ def prepare_backend() -> None: SystemConfigOper().load_snapshot() UserConfigOper().load_snapshot() # 缓存装饰器在测试模块导入时即创建后端,先装配隔离配置对应的适配器。 - from app.startup.cache_initializer import configure_cache_dependencies + from app.startup.initializers.cache import configure_cache_dependencies configure_cache_dependencies() # 测试与生产使用同一组合入口,确保领域解析器获得隔离库和测试 settings。 - from app.startup.domain_initializer import configure_domain_dependencies + from app.startup.initializers.domain import configure_domain_dependencies configure_domain_dependencies() diff --git a/database/env.py b/database/env.py index f1091da3e..627d8fa32 100644 --- a/database/env.py +++ b/database/env.py @@ -6,7 +6,7 @@ from sqlalchemy import pool from alembic import context from app.db import Base -from app.startup.cache_initializer import configure_cache_dependencies +from app.startup.initializers.cache import configure_cache_dependencies # 历史 migration 会调用使用 FileCache 的业务清理链,Alembic 自身也是组合入口。 configure_cache_dependencies() diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 78e98cd9c..32cb65032 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -245,12 +245,12 @@ sequenceDiagram 和 TestClient 因而共享同一 fail-fast 语义。 - **引擎预热 fail-fast**:同步/异步数据库引擎在单线程期完成首次创建, 避免调度器放出大量线程后再创建引擎导致连接锁竞争。 -- **类型化请求装配**:`startup/context.py` 的 frozen slots `HostRuntime` 是 lifespan 内唯一宿主 +- **类型化请求装配**:`startup/composition/context.py` 的 frozen slots `HostRuntime` 是 lifespan 内唯一宿主 上下文,`api/context.py` 从 `app.state` 收窄到具体领域能力。认证、消息、历史、媒体服务器、站点、 订阅、工作流和请求事务均使用命名 runtime 字段,不再通过字符串仓储键定位;API、Scheduler、Chain 从 `HostRuntime.configuration` 获取 frozen 配置快照。系统设置管理 API 通过 `HostRuntime.settings` 的窄服务读写可变部署设置,业务域不接触 Settings 实例;生产与测试组合根统一 - 复用 `startup/configuration.py` 的映射。`ApiDataPorts` 仅保留旧导入 ABI,不参与正式请求链路。 + 复用 `startup/composition/configuration.py` 的映射。`ApiDataPorts` 仅保留旧导入 ABI,不参与正式请求链路。 - **安全模式**:`MOVIEPILOT_SAFE_MODE` 会跳过插件、定时器、监控器、命令与工作流,用于故障自救。 - **进程拓扑**:全功能 V3 强制 `API_WORKERS=1`,避免每个 worker 重复启动插件和后台控制面;安全模式可临时使用多 worker 诊断,但不是正式扩容方案。 - **健康语义**:`/health/live` 只确认进程和事件循环可响应;`/health/ready` 仅在数据库 @@ -375,7 +375,8 @@ flowchart LR 不再保留主题包之外的第二个写入入口。 - 规范写入口中的 Oper 只 stage mutation,不创建独立 Session、不提交;Application Command 通过请求或任务入口注入的 UnitOfWork 统一 `commit/rollback`,事件、刷新和上报只在 commit - 成功后执行。订阅新增样板由 `startup/subscription.py` 创建独占 Session, + 成功后执行。订阅新增 Port 位于 `application/subscription/write.py`,由 + `db/adapters/subscription.py` 创建独占 Session,`startup/composition/subscription.py` 只装配回调, `application/subscription/write.py` 决定事务与 post-commit 边界,`SubscribeOper.stage_add()` 只查重、`add` 和 `flush`。旧 SDK 显式构造的无会话 Oper 暂留兼容自动短会话,不得被新代码复用。 `transaction-debt-baseline.json` 当前要求正式只读查询装饰器保持为 0;原有同步/异步写装饰器 @@ -384,7 +385,7 @@ flowchart LR `create/update/delete/truncate` 兼容包装器;AST 门禁保证显式 Session 的提交权不会被底层抢走。 - 站点、历史、工作流、Agent 会话删除和插件数据重置已经形成同构事务切片;对应 Application Command/Service 持有 UoW,Oper 的 `stage_*` 方法只修改当前会话。插件数据重置从 - `startup/plugins_initializer.py` 创建独占会话,插件直接使用 `PluginDataOper` 的旧 ABI 仅作兼容。 + `startup/initializers/plugins.py` 注入事务能力,插件直接使用 `PluginDataOper` 的旧 ABI 仅作兼容。 - 每次表结构变更必须新增 `database/versions/` 下的 Alembic 迁移。 - 运行期业务配置使用 `SystemConfigKey` 枚举 + `SystemConfigOper`,禁止裸字符串键; 用户级配置使用 `UserConfigOper`。 @@ -489,7 +490,7 @@ Agent 采用**门面 + 惰性物化**设计,避免 `application → agent` 形 ```mermaid flowchart TB Entry["消息渠道 / API / MCP"] --> Facade["app/application/agent.py
编排门面(get_agent_manager 等)"] - Reg["app/startup/agent_initializer.py
导入期注册轻量 Provider"] + Reg["app/startup/initializers/agent.py
导入期注册轻量 Provider"] Facade -.能力启用或首次使用时物化.-> RT["app/agent/runtime_loader.py
能力发现与服务物化"] RT --> ORC["app/agent/orchestrator.py
会话编排"] ORC --> Tools["app/agent/tools
系统工具(经 application 门面)"] diff --git a/docs/refactor/backend-architecture-governance.md b/docs/refactor/backend-architecture-governance.md index 119968c7b..b22cbbecd 100644 --- a/docs/refactor/backend-architecture-governance.md +++ b/docs/refactor/backend-architecture-governance.md @@ -119,7 +119,7 @@ MoviePilot V3 已经完成一轮重要基础工作:原 `app/core`、`app/helpe | 模块 | 静态出度 | 主要原因 | | --- | ---: | --- | | `app.agent.tools.factory` | 99 | 一次性导入全部内置工具并维护集中注册表 | -| `app.startup.modules_initializer` | 55 | 组合根职责,这是合理高出度,但仍需声明式管理 | +| `app.startup.initializers.modules` | 55 | 组合根职责,这是合理高出度,但仍需声明式管理 | | `app.api.endpoints.system` | 54 | 系统设置、规则测试、日志、网络测试、运行控制混合 | | `app.api.deps` | 49 | 认证、插件配置和跨端点依赖装配集中 | | `app.agent.orchestrator` | 48 | Agent 构建、执行、工具、记忆、审计、用量混合 | @@ -425,7 +425,7 @@ app/chain/transfer.py # 保持 TransferChain 兼容门面 #### 已有进展 -`app/startup/modules_initializer.py:211-245` 已经承担托管资源、壁纸 Provider、认证载荷、DoH、站点、事件错误通知、模块、Agent 和前端的组合工作。`app/startup/lifecycle.py` 也显式规定数据库预热、路由、模块、插件、调度器、监控器、命令和工作流的顺序。这是正确方向。 +`app/startup/initializers/modules.py` 承担托管资源、壁纸 Provider、认证载荷、DoH、站点、事件错误通知、模块、Agent 和前端的组合工作。`app/startup/lifecycle/` 显式规定数据库预热、路由、模块、插件、调度器、监控器、命令和工作流的顺序。这是正确方向。 #### 历史泄漏与当前收口 @@ -701,7 +701,7 @@ app/application/server/share.py # 订阅/工作流等分享用例 #### 典型证据 -- `app/application/messaging/skill.py` 通过 `SkillCatalogPort` 消费技能目录,`app.startup.agent_initializer` 才导入并注入 `SkillHelper`。 +- `app/application/messaging/skill.py` 通过 `SkillCatalogPort` 消费技能目录,`app.startup.initializers.agent` 才导入并注入 `SkillHelper`。 - `app/application/plugin/routes.py` 持有 `DynamicRouteRegistry` Protocol 和注册/移除用例;FastAPI app、`app.routes`、`openapi_schema` 和 `setup()` 均封装在 `app/adapters/web/plugin/routes.py`。 - 多个 `modules` 直接导入 `app.application.messaging.agent`、`mediaserver`、`storage` 等;其中一部分是合理 SPI 消费,一部分表明应用能力接口和具体实现未区分。 - `SystemConfigOper()` 在大量文件中被直接构造,形成持久化配置服务定位器。 diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index fcf436e0f..8fb3314ed 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -15,8 +15,8 @@ ### 长期整改阶段 0:治理门禁恢复(2026-08-23) -- 宿主依赖基线已审查 TaskRegistry 与有界后台 owner 接入后的语义差异:当前为 `800` 个模块、`6482` 条内部导入边,12 组重点禁止边继续全部为 `0`,唯一非平凡 SCC 仍是隔离的 TMDB 移植包。 -- 启动性能探针会在隔离生命周期中真实创建并释放 TaskRegistry;normal/safe 组件数分别为 `20`/`10`,CI 只读检查使用稳定的宿主模块集合和生命周期组件顺序,不再把 Python/平台模块数量当作硬合同。 +- 宿主依赖基线已审查本阶段归位后的语义差异:当前为 `805` 个模块、`6514` 条内部导入边,12 组重点禁止边继续全部为 `0`,唯一非平凡 SCC 仍是隔离的 TMDB 移植包。 +- 启动性能探针会在隔离生命周期中真实创建并释放 TaskRegistry;normal/safe 组件数分别为 `23`/`11`,CI 只读检查使用稳定的宿主模块集合和生命周期组件顺序,不再把 Python/平台模块数量当作硬合同。 - 官方插件快照覆盖 `plugins.v3`、`plugins.v2` 以及 V3 实际会从 `package.json` 回退加载的 31 个默认实现;`app/plugins/**` 仍只是宿主运行副本,不进入扫描。 - SDK 快照以各模块显式 `__all__` 为公开合同,能够记录赋值别名;`typing`、`__future__` 等实现期导入不再被误冻结,既有数据库备份门面已补精确导出清单。 - async 阻塞实际债务已由 fixture 中的 10 项下降到 1 项并固化低水位;剩余项是 Scheduler Agent task 查询,后续阶段迁入异步查询边界后归零。 @@ -72,7 +72,7 @@ - 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。 - `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。 -- 依赖图当前为 `800` 个 Python 模块、`6479` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。 +- 依赖图当前为 `805` 个 Python 模块、`6514` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。 - 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。 综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。 @@ -82,7 +82,7 @@ 1. **后台任务的统一所有权已覆盖 API 入口,但仍有更深层任务机制待分级。** `app/runtime/tasks.py` 已建立 lifespan 级 TaskRegistry,启动收尾、插件 Release 刷新、Webhook E0 广播、CookieCloud E1 手工调度、消息入口、Seerr 订阅、整理历史 AI 重做、OpenAI/Anthropic 协议流和 WebAgent 断线后执行/快照保存均不再维护端点模块级任务集合或 Starlette 回调,shutdown 会停止接收、取消并有限等待,且生命周期清单明确登记其顺序。主仓 `app/` 已无裸 FastAPI `BackgroundTasks`;当前仍有约 `50` 个更底层 `create_task`/等价任务创建点,与线程池和 APScheduler 并存,后续需逐项确认 owner、取消、等待、重试、幂等和是否 durable,关键业务副作用优先接入已有 Outbox/恢复表。 2. **动态模块契约仍以 legacy 聚合语义为主。** 当前登记 `212` 个模块方法,其中 `194` 个仍使用 `legacy` aggregation,只有 `14` 个 `first_non_empty`、`4` 个 `ordered_list_merge`。`app/runtime/extensions/module/contracts.py:422-455` 已能登记 family、输入/结果标签和基础签名诊断,但 `193` 个方法没有 required parameters,调度器 `app/runtime/extensions/module/dispatcher.py:109-260` 仍主要依赖运行时反射、返回值形状和短路规则。未知第三方方法保留 legacy fallback 是兼容要求,不应删除;宿主高频能力则应逐族补齐可执行的输入校验、结果校验、超时和错误语义。 3. **查询侧数据库兼容 ABI 已完成正式装饰器清零。** 写事务装饰器和正式 `db_query/async_db_query` 均为 `0`。站点、消息、用户、订阅、下载/整理历史、工作流、MediaServer、SiteUserData、AgentChat、AgentTaskRun、TransferPending、SystemConfig、PassKey 和 SubscribeHistory 的宿主查询已迁到显式 Session 路径;对应旧插件 Model 调用由独立 `legacy_*` 外壳保留,可同时接受显式 Session 与无 Session 的位置/关键字参数。后续重点转为减少 ORM 对象跨层流转,并保持正式装饰器零回退。 -4. **组合根和全局状态仍形成复杂的隐式运行时图。** Singleton 实例、模块级 provider、`configure_*` 注册函数和兼容 Facade 同时存在;它们解决了旧 ABI 和启动顺序问题,但增加测试污染、重复装配、实例身份和初始化顺序风险。`app/startup/lifecycle/__init__.py:161-376` 已有声明式生命周期,`app/startup/modules_initializer.py:505-530` 也有分阶段关闭,但尚未做到所有进程级资源都只通过 typed HostRuntime 访问。后续应以“新代码禁止新增 Service Locator/Singleton 依赖、旧入口有命中观测”为 ratchet。 +4. **组合根和全局状态仍形成复杂的隐式运行时图。** Singleton 实例、模块级 provider、`configure_*` 注册函数和兼容 Facade 同时存在;它们解决了旧 ABI 和启动顺序问题,但增加测试污染、重复装配、实例身份和初始化顺序风险。`app/startup/lifecycle/__init__.py` 已有声明式生命周期,`app/startup/initializers/modules.py` 也有分阶段关闭,但尚未做到所有进程级资源都只通过 typed HostRuntime 访问。后续应以“新代码禁止新增 Service Locator/Singleton 依赖、旧入口有命中观测”为 ratchet。 ### P2:中长期可演进性债务 @@ -529,7 +529,7 @@ flowchart TB **实施记录(2026-08-21)**: -- `app/startup/subscription.py` 为每次规范新增创建独占同步/异步 Session; +- `app/application/subscription/write.py` 定义用例 Port,`app/db/adapters/subscription.py` 为每次规范新增创建独占同步/异步 Session,`app/startup/composition/subscription.py` 只负责注入; `CreateSubscriptionCommand` / `AsyncCreateSubscriptionCommand` 持有 UoW,Oper 只执行 查重、`add` 与 `flush`。 - `SubscribeOper.stage_add()` 的查重 SQL 已收口到 Oper,不再调用 Model 自动会话装饰器; @@ -584,7 +584,7 @@ flowchart TB **建议结构**: ```text -app/startup/context.py # HostRuntime 及构建结果 +app/startup/composition/context.py # HostRuntime 及构建结果 app/api/context.py # API 可见的最小 AppState / 读取依赖 app/api/dependencies/ # 按领域拆分依赖工厂 auth.py @@ -612,7 +612,7 @@ app/api/dependencies/ # 按领域拆分依赖工厂 **实施记录(2026-08-21)**: -- `app/startup/context.py` 定义 frozen slots `HostRuntime` 与首个窄能力 +- `app/startup/composition/context.py` 定义 frozen slots `HostRuntime` 与首个窄能力 `AgentChatRuntime`,仓储、Session、UoW 字段均为具体 Protocol 工厂,不是字符串字典。 - `init_modules()` 保留零参数兼容签名并返回本次 lifespan 唯一 Runtime;生命周期组件把结果挂到 `app.state.host_runtime`。`app/api/context.py` 只向 Depends 暴露 Agent chat 的最小能力。 @@ -707,7 +707,7 @@ app/api/dependencies/ # 按领域拆分依赖工厂 Startup 路径始终使用 HostRuntime 注入。插件 SDK 的 `app.sdk.config.settings`、动态 API 返回和事件字段未改。 - 收尾批次把 API 与 Chain 余下直接配置读取全部迁入类型化 snapshot;Scheduler 继续保持为零。 `HostRuntime` 新增可变部署设置服务,只供系统设置管理 API 使用,业务 API/Chain 只接收 frozen 字段。 - snapshot 构造集中到 `app/startup/configuration.py`,生产启动与测试组合根复用同一映射,避免测试默认值 + snapshot 构造集中到 `app/startup/composition/configuration.py`,生产启动与测试组合根复用同一映射,避免测试默认值 漂移。canonical `settings` 直接导入低水位从 154 降到 137,`SystemConfigOper()` 保持 14 个。 - `ApiRuntimeConfig` 已覆盖搜索来源、媒体/字幕/音频后缀、重命名格式、WebPush、CookieCloud、根目录和 版本标识;`ChainRuntimeConfig` 覆盖搜索、下载、整理、刮削、AI、代理、缓存、链接、路径和 TMDB 图片域。 @@ -1080,7 +1080,7 @@ OTel 初始化只能位于 Startup/Adapter;Domain/Application 只依赖 no-op- - 新增 Application command/port - `app/runtime/event/` - `app/runtime/extensions/module/contracts.py` - - `app/startup/context.py` / `app/api/context.py` + - `app/startup/composition/context.py` / `app/api/context.py` 3. 对第三方移植包、旧插件 Facade 和动态 SDK 设置精确豁免,不允许 `app.* = ignore_errors`。 4. CI 先检查严格目录;每次迁移扩大 include 范围。 5. 类型错误不能用无界 `Any`、`cast(Any, ...)` 或全文件 ignore 消音。 @@ -1099,7 +1099,7 @@ OTel 初始化只能位于 Startup/Adapter;Domain/Application 只依赖 no-op- **扩展实施记录(2026-08-22)**:mypy 目标运行时更新到 Python 3.14,严格清单扩大到 20 个源文件; 新增纳管配置快照和下载失败事务适配器,仍保持零错误、无全局 ignore。 -Workflow 执行状态 UoW 切片将 `app/application/workflow.py` 与 `app/startup/workflow.py` 纳入 strict 清单, +Workflow 执行状态 UoW 切片将 `app/application/workflow.py` 与 `app/db/adapters/workflow.py` 纳入 strict 清单, 治理范围扩大到 22 个源文件;事务命令、仓储 Protocol 和短会话适配器保持零错误。 异步安全与契约收口继续纳管 scheduling facade、Event error policy、Module dispatcher 和 async blocking diff --git a/docs/refactor/backend-module-refactor-compatibility.md b/docs/refactor/backend-module-refactor-compatibility.md index 727673101..f8126ceeb 100644 --- a/docs/refactor/backend-module-refactor-compatibility.md +++ b/docs/refactor/backend-module-refactor-compatibility.md @@ -519,7 +519,7 @@ SYMBOL_ALIASES = { 4. Event、模块、插件和安全边界改为由 startup composition root 注入 resolver、回调和错误处理器,迁移模块不再处于强连通分量。 5. 插件稳定入口收敛到 `app.sdk`;存量插件无需同步修改,官方插件可以按正常发布节奏迁移。 6. 站点二进制和数据资源迁到 `app/application/site`,Build 直接生成 `app.application.site.sites`,Build CI、Resources V3 manifest、Docker 和本地 CLI 使用同一目标路径。 -7. 媒体识别领域不再直接读取 DB/settings,也不导入 Rust 适配器;`startup/domain_initializer.py` 统一注入实时规则、后缀策略、TMDB 图片地址、默认媒体来源和加速器。 +7. 媒体识别领域不再直接读取 DB/settings,也不导入 Rust 适配器;`startup/initializers/domain.py` 统一注入实时规则、后缀策略、TMDB 图片地址、默认媒体来源和加速器。 8. 缓存按职责拆为 `runtime/cache.py`(契约、内存实现、装饰器、代理)和 `adapters/cache/backends.py`(Redis、文件 I/O);旧 `app.core.cache` 指向完整 `app.sdk.cache` 门面。 9. `application/mediaserver.py` 集中负责媒体服务器的配置化服务发现、Provider ID 规范化和音乐库匹配;通用媒体身份规则继续复用 `domain/media.py`。 10. GC 归入 `runtime/gc.py`,外部 IP 归属查询归入 `adapters/external/location.py`,安全能力统一在 `app/application/security/`,URL 安全策略为 `url.py`,二次认证文件为 `twofactor.py`。 diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index 5de5f6cd7..b45788283 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -72,10 +72,13 @@ to make the directory tree look symmetrical. | `app/application/messaging/` | Message rendering/routing, interactions and the Agent-to-message bridge: `interaction.py` shared interaction contracts and view helpers; `router.py` unified interaction priority and callback dispatch; `site.py`/`subscribe.py`/`skill.py` per-command sessions, input parsing and views; `media.py` media interaction state while the business workflow stays in `MediaInteractionChain`; `plugin.py` plugin input capture and plugin button callbacks; `agent.py` agent choice state, callback protocol and WebAgent bridge; `message.py` notification rendering, templates and queue. Not a public SDK recommended for direct plugin use | | `app/application/security/` | Authentication, authorization, cookies, passkeys, OTP/two-factor, path/URL safety, SSRF and signing policy | -Application services may use domain rules, runtime contracts, Oper classes and -adapters. Multi-domain workflows still belong in the existing `app/chain/` -package. `Chain`, `Service` and `Manager` remain class patterns; they do not -create additional top-level directory categories. +Application services may use domain rules and runtime contracts. They own the +persistence Protocol needed by a use case, but must not import `app.db`, +SQLAlchemy, Session, Oper classes or concrete adapters. `app/db/adapters/` +implements those Protocols and startup injects the implementation. Multi-domain +workflows still belong in the existing `app/chain/` package. `Chain`, `Service` +and `Manager` remain class patterns; they do not create additional top-level +directory categories. ### Runtime boundaries @@ -94,8 +97,11 @@ create additional top-level directory categories. | `app/runtime/compat/` | Standard-library-only exact legacy import routing, resource preflight scanning and DEBUG diagnostics | `app/startup/` remains the established composition root and is not nested under -runtime. It injects providers and callbacks, orders initialization/shutdown and -decides restart policy. Lower-level runtime modules must not import startup. +runtime. Its root contains only `composition/`, `initializers/` and `lifecycle/`: +composition constructs and injects cross-layer dependencies, initializers expose +domain-scoped startup/shutdown hooks, and lifecycle orders those hooks and decides +restart policy. Reusable persistence implementations belong in `app/db/adapters/`, +not startup. Lower-level runtime modules must not import startup. Startup publishes its frozen, slotted `HostRuntime` through FastAPI `app.state`. API dependencies must narrow that object to a domain runtime (for example, `AgentChatRuntime`) instead of adding a string key to a global service map. @@ -105,14 +111,20 @@ API, Scheduler and Chain deployment values are exposed as frozen snapshots from `HostRuntime.configuration`; canonical callers must not add a fresh direct `settings` import when the required field belongs to an existing snapshot. -`app.schemas` and `app.db` are compatibility facades, not implementation -dependency hubs. Host code imports concrete schema submodules; the schema root +`app.schemas` and the `app.db` package root are compatibility facades, not +implementation dependency hubs. Host code imports concrete schema submodules; the schema root resolves its generated export manifest lazily for plugins and legacy callers. DB internals import `base`, `decorators`, `engine`, `session`, concrete models and Oper modules directly. `app.db.models.load_all_models()` is the explicit composition entry used before metadata creation or migration; importing one model must not import every table. +`app/db/oper/` owns table-oriented SQLAlchemy access and receives a caller-owned +Session. `app/db/adapters/` is the concrete persistence-adapter layer: it may +depend on Application-owned Protocols, UoW/Session and Oper implementations. +This deliberate dependency inversion is the only `DB implementation -> +Application contract` direction; Application must remain free of DB imports. + ### Adapter boundaries | Path | Ownership | @@ -123,6 +135,7 @@ model must not import every table. | `app/adapters/external/` | CookieCloud, plugin market, OCR, IP-location providers and MoviePilot Server | | `app/adapters/external/plugin/client.py` | Read-only plugin-market and local-repository client over the established `PluginHelper` implementation | | `app/adapters/system/plugin/` | Plugin package and dependency I/O (`package.py`, `dependency.py`) | +| `app/db/adapters/` | SQLAlchemy implementations of Application-owned persistence Protocols | Generic protocol transport belongs in `adapters/network`; a named product or ecosystem workflow belongs in `adapters/external`. An adapter may depend on @@ -381,7 +394,7 @@ policy. `app/db` therefore has no dependency on `app/domain`. emits no runtime logs; upper-layer owners decide whether failures are operationally relevant. - `app/adapters/system/resource.py` only reports whether installation occurred; - `app/startup/modules_initializer.py` supplies the loaded site-resource + `app/startup/initializers/modules.py` supplies the loaded site-resource versions and decides whether to restart. The adapter never imports the site application service. - Configured notification discovery lives in @@ -407,13 +420,15 @@ policy. `app/db` therefore has no dependency on `app/domain`. | Direction | Status | |---|---| -| `entrypoint -> chain / application / Oper` | Allowed according to workflow complexity | -| `chain -> module (only via run_module dispatch) / application / Oper / canonical capability` | Allowed; direct `chain -> module` imports forbidden | -| `chain -> agent implementation` | Forbidden; chains reach Agent runtime only through `app/application/agent.py`; `app/startup/agent_initializer.py` registers lightweight providers at import time, and implementations are materialized only when the capability is enabled or first used | +| `entrypoint -> chain / application / injected persistence Port` | Allowed according to workflow complexity | +| `chain -> module (only via run_module dispatch) / application / injected Port / canonical capability` | Allowed; direct `chain -> module` and `chain -> Oper` imports forbidden | +| `chain -> agent implementation` | Forbidden; chains reach Agent runtime only through `app/application/agent.py`; `app/startup/initializers/agent.py` registers lightweight providers at import time, and implementations are materialized only when the capability is enabled or first used | | `agent.tools -> api / scheduler / command` | Forbidden; tools use `app/application/plugin/routes.py`, `plugin/folders.py`, `scheduling.py` and `commands.py` application services | | `api -> factory` | Forbidden; the FastAPI route adapter is injected into `app/application/plugin/routes.py` by the composition root after creation | -| `application -> domain / runtime / adapter / Oper` | Allowed | -| `module -> canonical capability / Oper` | Allowed | +| `application -> domain / runtime contract` | Allowed | +| `application -> DB / Oper / concrete adapter` | Forbidden; define a Protocol in Application and inject an implementation | +| `db.adapters -> application persistence Protocol / db.oper / UoW` | Allowed; this is dependency inversion, not an upper-layer use-case call | +| `module -> canonical capability / Application persistence Port` | Allowed; direct Oper imports are forbidden for new code | | `module -> module / chain` | Forbidden for new code | | `adapter -> application / runtime.extensions / sdk / compat` | Forbidden | | `domain -> runtime / adapter / application / DB` | Forbidden | @@ -426,11 +441,14 @@ policy. `app/db` therefore has no dependency on `app/domain`. | Path | Purpose | |---|---| -| `app/application/agent.py` | Agent orchestration facade (`get_agent_manager` / `get_prompt_manager` / capability queries / prompt builders); lightweight providers register through `app/startup/agent_initializer.py`, with no static `application -> agent` edge | +| `app/application/agent.py` | Agent orchestration facade (`get_agent_manager` / `get_prompt_manager` / capability queries / prompt builders); lightweight providers register through `app/startup/initializers/agent.py`, with no static `application -> agent` edge | | `app/agent/runtime_loader.py` | Agent-specific capability discovery and canonical entrypoint/service materialization; reuses the generic Capability Runtime while keeping Agent ownership under `app/agent/` | | `app/application/subscription/write.py` | Subscription media translation and sync/async write-port orchestration | -| `app/application/scheduling.py` | Runtime scheduler facade for Agent tools and endpoints; `Scheduler` class registered by `app/startup/scheduler_initializer.py` | -| `app/application/commands.py` | Command registry facade for Agent tools and endpoints; `Command` class registered by `app/startup/command_initializer.py` | +| `app/application/scheduling.py` | Runtime scheduler facade for Agent tools and endpoints; `Scheduler` class registered by `app/startup/initializers/scheduler.py` | +| `app/application/commands.py` | Command registry facade for Agent tools and endpoints; `Command` class registered by `app/startup/initializers/command.py` | +| `app/db/adapters/` | SQLAlchemy repository/UoW implementations for Application-owned persistence Protocols | +| `app/startup/composition/` | HostRuntime, configuration snapshots and cross-layer adapter wiring | +| `app/startup/initializers/` | Domain-scoped initialization and shutdown hooks | | `app/chain/agent.py` | `AgentChain(ChainBase)`: the chain-layer entry for Agent sessions; Agent runtime stays in `app/agent/` | | `app/runtime/config.py` | `ConfigModel`, `Settings` and deployment configuration | | `app/runtime/topology.py` | Single-worker full-runtime policy and safe-mode topology validation | diff --git a/docs/rules/06-code-styles.md b/docs/rules/06-code-styles.md index 4d4c32b89..92d382de6 100644 --- a/docs/rules/06-code-styles.md +++ b/docs/rules/06-code-styles.md @@ -116,7 +116,9 @@ except: - Do not introduce new third-party libraries without placing them in the correct `pyproject.toml` dependency group and updating `uv.lock`: runtime packages belong in `[project].dependencies`, test/lint/build tooling in `[dependency-groups].dev`. - Do not use `requests` or `httpx` directly for external HTTP calls - host code uses `RequestUtils` from `app/adapters/network/http.py`; plugins use `app.sdk.network`. -- Do not issue raw SQLAlchemy queries from chains, modules, or endpoints — use the Oper classes in `app/db/oper/`. +- Do not issue raw SQLAlchemy queries or import Oper classes from chains, modules, + or endpoints. Define/consume an Application persistence Port; its concrete + implementation under `app/db/adapters/` may use Oper classes from `app/db/oper/`. - Do not add TODO or FIXME without context. Only keep one if it is genuinely deferred and cannot be addressed in the current task. - Do not add noisy markers like `# change starts here`, `# important`, or `# this is a fix`. - Do not write comments that restate what the code already clearly says. diff --git a/docs/rules/10-data-and-persistent.md b/docs/rules/10-data-and-persistent.md index ac8854afa..2ec63db9c 100644 --- a/docs/rules/10-data-and-persistent.md +++ b/docs/rules/10-data-and-persistent.md @@ -81,33 +81,49 @@ the stub. Oper classes accept and return persistence values. Turning a `MediaInfo` or `MetaBase` into a row is business logic and lives in `app/application/`. +Application owns use-case commands and persistence Protocols, but does not import +`app.db`, SQLAlchemy, Session or Oper. Concrete persistence is used in +`app/db/adapters/`: adapters implement those Protocols with explicit Session, +UnitOfWork and Oper objects. `app/startup/composition/` creates and injects the +adapters; it does not retain reusable repository implementations. + ### Transaction ownership ratchet -- `tests/fixtures/architecture/transaction-debt-baseline.json` records the - existing Model transaction decorators. All formal query and write decorators - are now zero and must remain zero; compatibility-only `legacy_*` shells must - not be counted as new transaction ownership. -- `legacy_db_query` / `legacy_async_db_query` are compatibility-only shells for - existing plugin-facing Model methods. Host Oper code must pass an explicit - Session through `_execute_sync_query` / `_execute_async_query`; new Model - methods must not add either legacy decorator. +- `tests/fixtures/architecture/transaction-debt-baseline.json` records formal + decorators in concrete files under `app/db/models/`. Their count is zero and + must remain zero. Compatibility-only `legacy_*` shells are tracked separately + and must never be treated as the target design. +- `legacy_db_query` / `legacy_async_db_query` preserve an existing plugin-facing + Model method whose no-Session call shape cannot be removed yet. If a Model + method has no external ABI obligation, move the query into its Oper and remove + the Model method instead of adding `legacy_*`. +- `Base.create/get/update/delete/list/truncate` and their async forms are inherited + plugin ABI, so `app/db/base.py` deliberately uses legacy query/write wrappers. + New host code must not call these convenience methods; Oper staging methods and + explicit UoW are the canonical path. Removal requires plugin-usage evidence and + a separately announced compatibility break, not a mechanical rename. +- Host Oper code must pass an explicit Session through `_execute_sync_query` / + `_execute_async_query`; new Model methods must not add any legacy decorator. - New Model methods must not use `db_query`, `db_update`, `async_db_query`, or `async_db_update`, create a Session, or call `commit()` / `rollback()`. - Oper receives a caller-owned Session and may query, add, update, delete, or flush. A composable Oper method must not create its own Session and must not commit or roll back. -- The API, Scheduler, Agent, or another logical operation entry creates the - Session and adapts it through `app/db/uow.py`. Application command code owns - `commit()` / `rollback()`; events, scheduling refresh, reports, and other - external effects run only after a successful commit. +- API, Scheduler, Agent and Chain consume an injected Application Port; they do + not import or create a Session. The concrete `app/db/adapters/` implementation + creates the Session and adapts it through `app/db/uow.py`. Application command + code decides when the injected UoW commits or rolls back; events, scheduling + refresh, reports and other external effects run only after a successful commit. - A synchronous Session is private to one worker thread. An AsyncSession is private to one asyncio task/operation; neither may be stored in a process singleton or reused by concurrent work. -- Subscription creation is the reference slice: `app/startup/subscription.py` - creates an exclusive Session, `app/application/subscription/write.py` owns the - UoW and post-commit callback, and `SubscribeOper.stage_add()` only queries, - adds, and flushes. Preserve `SubscribeOper.add()` only for legacy SDK callers; - new host code must not use that auto-commit compatibility path. +- Subscription creation is the reference slice: + `app/application/subscription/write.py` owns the command and persistence Port, + `app/db/adapters/subscription.py` creates an exclusive Session and adapts Oper/UoW, + and `app/startup/composition/subscription.py` only wires scopes and post-commit + callbacks. `SubscribeOper.stage_add()` only queries, adds and flushes. Preserve + `SubscribeOper.add()` only for legacy SDK callers; new host code must not use + that auto-commit compatibility path. - The same rule applies to `SiteMutationCommand`, history/workflow commands, `AgentChatService.delete()`, and `DeletePluginDataCommand`: bind the repository and UoW to one request/operation Session. Legacy plugin-facing Oper methods may @@ -117,7 +133,18 @@ Run `./.venv/bin/python scripts/architecture/baseline.py --check-host` after persistence changes. A deliberate debt reduction may refresh the low-water mark with `--write-host`; never refresh it to accept newly introduced debt. -**Standard Oper method conventions:** +**Canonical explicit-session Oper conventions:** + +```python +with SessionFactory() as session: + oper = SubscribeOper(session) + subscribe = oper.get(sid=1) # Query in caller-owned Session + subscribes = oper.list() # List in caller-owned Session + oper.stage_add(Subscribe(...)) # Stage only; caller-owned UoW commits +``` + +The following no-Session form is legacy plugin ABI only and must not be copied +into host code: ```python oper = SubscribeOper() diff --git a/mypy.ini b/mypy.ini index 7c9dab870..38e528b31 100644 --- a/mypy.ini +++ b/mypy.ini @@ -35,12 +35,12 @@ files = app/db/decorators.py, app/db/base.py, app/db/uow.py, - app/startup/context.py, - app/startup/configuration.py, - app/startup/outbox.py, - app/startup/chain_events.py, - app/startup/download_failure.py, - app/startup/workflow.py, + app/startup/composition/context.py, + app/startup/composition/configuration.py, + app/db/adapters/outbox.py, + app/db/adapters/chain.py, + app/db/adapters/download.py, + app/db/adapters/workflow.py, app/api/context.py, app/api/dependencies/subscription.py, scripts/architecture/async_blocking.py diff --git a/scripts/local_setup.py b/scripts/local_setup.py index 1c0dcaa16..d45b6a961 100644 --- a/scripts/local_setup.py +++ b/scripts/local_setup.py @@ -2380,12 +2380,12 @@ def _apply_local_system_config_inner(config_payload: dict[str, Any]) -> None: sys.path.insert(0, str(ROOT)) try: - from app.startup.database_initializer import prepare_database + from app.startup.initializers.database import prepare_database from app.db.oper.systemconfig import SystemConfigOper from app.schemas.types import SystemConfigKey from app.db.session import SessionFactory, async_session_scope from app.db.uow import configure_transaction_runners - from app.startup.transaction import TransactionalWriteRunner + from app.db.adapters.transaction import TransactionalWriteRunner except ModuleNotFoundError as exc: raise RuntimeError( "当前环境尚未安装 MoviePilot 运行依赖,请先执行 moviepilot install deps 或 moviepilot setup" @@ -2560,7 +2560,7 @@ def _sync_superuser_account_inner() -> None: sys.path.insert(0, str(ROOT)) try: - from app.startup.database_initializer import prepare_database + from app.startup.initializers.database import prepare_database except ModuleNotFoundError as exc: raise RuntimeError( "当前环境尚未安装 MoviePilot 运行依赖,请先执行 moviepilot install deps 或 moviepilot setup" @@ -3693,7 +3693,7 @@ def run_agent_request( sys.path.insert(0, str(ROOT)) try: - from app.startup.database_initializer import prepare_database + from app.startup.initializers.database import prepare_database from app.agent import MoviePilotAgent from app.runtime.config import settings except ModuleNotFoundError as exc: diff --git a/tests/conftest.py b/tests/conftest.py index 9a746c245..034f76d56 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,7 +51,7 @@ def configure_plugin_system_services(): ) from app.runtime.config import settings from app.runtime.settings import configure_runtime_setting_provider - from app.startup.configuration import ( + from app.startup.composition.configuration import ( build_api_runtime_config, build_chain_runtime_config, build_scheduler_runtime_config, @@ -153,11 +153,11 @@ def configure_plugin_system_services(): from app.db.oper.workflow import WorkflowOper, configure_workflow_legacy_writer from app.db.oper.message import MessageOper from app.db.oper.passkey import PassKeyOper - from app.startup.subscription import TransactionalSubscribeWriter - from app.startup.download_failure import TransactionalDownloadFailureRepository - from app.startup.site import TransactionalSiteRepository - from app.startup.workflow import TransactionalWorkflowExecutionService - from app.startup.transaction import TransactionalWriteRunner + from app.db.adapters.subscription import TransactionalSubscribeWriter + from app.db.adapters.download import TransactionalDownloadFailureRepository + from app.db.adapters.site import TransactionalSiteRepository + from app.db.adapters.workflow import TransactionalWorkflowExecutionService + from app.db.adapters.transaction import TransactionalWriteRunner def compatibility_sync_session() -> Session: """动态读取可被存量隔离数据库用例替换的 ScopedSession。""" diff --git a/tests/fixtures/architecture/configuration-debt-baseline.json b/tests/fixtures/architecture/configuration-debt-baseline.json index beafebea2..70f792df2 100644 --- a/tests/fixtures/architecture/configuration-debt-baseline.json +++ b/tests/fixtures/architecture/configuration-debt-baseline.json @@ -25,7 +25,7 @@ "system_config_oper_constructions": { "calls": [ { - "file": "app/startup/modules_initializer.py", + "file": "app/startup/initializers/modules.py", "name": "SystemConfigOper" } ], diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 6df30e85c..c009c1f58 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -13,8 +13,8 @@ "runtime_to_db": [], "workflow_to_db": [] }, - "edge_count": 6483, - "edge_sha256": "0a2f82ca189468d5e954b06a2c188c4ea5380a76c7c64039c7f6eb843cb60523", + "edge_count": 6514, + "edge_sha256": "eda9bc500158baa80dc5013eb4e409aa57cb70446e59a52b7e782cb9c58f9c34", "edges": [ "app -> app.runtime", "app -> app.runtime.compat", @@ -1513,7 +1513,8 @@ "app.api.context -> app.runtime", "app.api.context -> app.runtime.tasks", "app.api.context -> app.startup", - "app.api.context -> app.startup.context", + "app.api.context -> app.startup.composition", + "app.api.context -> app.startup.composition.context", "app.api.dependencies.agent -> app.api", "app.api.dependencies.agent -> app.api.context", "app.api.dependencies.agent -> app.application", @@ -1521,7 +1522,8 @@ "app.api.dependencies.agent -> app.application.messaging.chat", "app.api.dependencies.agent -> app.application.messaging.message", "app.api.dependencies.agent -> app.startup", - "app.api.dependencies.agent -> app.startup.context", + "app.api.dependencies.agent -> app.startup.composition", + "app.api.dependencies.agent -> app.startup.composition.context", "app.api.dependencies.auth -> app.adapters", "app.api.dependencies.auth -> app.adapters.web", "app.api.dependencies.auth -> app.adapters.web.security", @@ -1536,7 +1538,8 @@ "app.api.dependencies.auth -> app.schemas", "app.api.dependencies.auth -> app.schemas.token", "app.api.dependencies.auth -> app.startup", - "app.api.dependencies.auth -> app.startup.context", + "app.api.dependencies.auth -> app.startup.composition", + "app.api.dependencies.auth -> app.startup.composition.context", "app.api.dependencies.data -> app.api", "app.api.dependencies.data -> app.api.data", "app.api.dependencies.history -> app.api", @@ -1554,7 +1557,8 @@ "app.api.dependencies.history -> app.schemas.types", "app.api.dependencies.history -> app.schemas.workflow", "app.api.dependencies.history -> app.startup", - "app.api.dependencies.history -> app.startup.context", + "app.api.dependencies.history -> app.startup.composition", + "app.api.dependencies.history -> app.startup.composition.context", "app.api.dependencies.plugin -> app.application", "app.api.dependencies.plugin -> app.application.commands", "app.api.dependencies.plugin -> app.application.plugin", @@ -1582,7 +1586,8 @@ "app.api.dependencies.site -> app.schemas", "app.api.dependencies.site -> app.schemas.types", "app.api.dependencies.site -> app.startup", - "app.api.dependencies.site -> app.startup.context", + "app.api.dependencies.site -> app.startup.composition", + "app.api.dependencies.site -> app.startup.composition.context", "app.api.dependencies.subscription -> app.adapters", "app.api.dependencies.subscription -> app.adapters.external", "app.api.dependencies.subscription -> app.adapters.external.server", @@ -1605,7 +1610,8 @@ "app.api.dependencies.subscription -> app.schemas", "app.api.dependencies.subscription -> app.schemas.types", "app.api.dependencies.subscription -> app.startup", - "app.api.dependencies.subscription -> app.startup.context", + "app.api.dependencies.subscription -> app.startup.composition", + "app.api.dependencies.subscription -> app.startup.composition.context", "app.api.dependencies.workflow -> app.adapters", "app.api.dependencies.workflow -> app.adapters.external", "app.api.dependencies.workflow -> app.adapters.external.server", @@ -1617,7 +1623,8 @@ "app.api.dependencies.workflow -> app.runtime", "app.api.dependencies.workflow -> app.runtime.config", "app.api.dependencies.workflow -> app.startup", - "app.api.dependencies.workflow -> app.startup.context", + "app.api.dependencies.workflow -> app.startup.composition", + "app.api.dependencies.workflow -> app.startup.composition.context", "app.api.dependencies.workflow -> app.workflow", "app.api.deps -> app.api", "app.api.deps -> app.api.dependencies", @@ -2666,6 +2673,8 @@ "app.application.notification -> app.schemas", "app.application.notification -> app.schemas.system", "app.application.notification -> app.schemas.types", + "app.application.plugin.config -> app.schemas", + "app.application.plugin.config -> app.schemas.exception", "app.application.plugin.folders -> app.application", "app.application.plugin.folders -> app.application.configuration", "app.application.plugin.folders -> app.runtime", @@ -3457,7 +3466,8 @@ "app.cli -> app.runtime.settings", "app.cli -> app.runtime.state", "app.cli -> app.startup", - "app.cli -> app.startup.database", + "app.cli -> app.startup.composition", + "app.cli -> app.startup.composition.database", "app.command -> app.application", "app.command -> app.application.messaging", "app.command -> app.application.messaging.message", @@ -3484,6 +3494,49 @@ "app.command -> app.schemas.event", "app.command -> app.schemas.message", "app.command -> app.schemas.types", + "app.db.adapters.chain -> app.application", + "app.db.adapters.chain -> app.application.chain", + "app.db.adapters.chain -> app.application.chain.durable_events", + "app.db.adapters.chain -> app.application.history", + "app.db.adapters.chain -> app.application.outbox", + "app.db.adapters.chain -> app.db", + "app.db.adapters.chain -> app.db.adapters", + "app.db.adapters.chain -> app.db.adapters.outbox", + "app.db.adapters.chain -> app.db.oper", + "app.db.adapters.chain -> app.db.oper.downloadhistory", + "app.db.adapters.chain -> app.db.oper.transferhistory", + "app.db.adapters.chain -> app.db.uow", + "app.db.adapters.download -> app.db", + "app.db.adapters.download -> app.db.oper", + "app.db.adapters.download -> app.db.oper.downloadfailure", + "app.db.adapters.download -> app.db.uow", + "app.db.adapters.outbox -> app.application", + "app.db.adapters.outbox -> app.application.outbox", + "app.db.adapters.outbox -> app.db", + "app.db.adapters.outbox -> app.db.base", + "app.db.adapters.outbox -> app.db.models", + "app.db.adapters.outbox -> app.db.models.outbox", + "app.db.adapters.site -> app.db", + "app.db.adapters.site -> app.db.oper", + "app.db.adapters.site -> app.db.oper.site", + "app.db.adapters.site -> app.db.uow", + "app.db.adapters.subscription -> app.application", + "app.db.adapters.subscription -> app.application.subscription", + "app.db.adapters.subscription -> app.application.subscription.write", + "app.db.adapters.subscription -> app.db", + "app.db.adapters.subscription -> app.db.adapters", + "app.db.adapters.subscription -> app.db.adapters.outbox", + "app.db.adapters.subscription -> app.db.oper", + "app.db.adapters.subscription -> app.db.oper.subscribe", + "app.db.adapters.subscription -> app.db.uow", + "app.db.adapters.transaction -> app.db", + "app.db.adapters.transaction -> app.db.uow", + "app.db.adapters.workflow -> app.application", + "app.db.adapters.workflow -> app.application.workflow", + "app.db.adapters.workflow -> app.db", + "app.db.adapters.workflow -> app.db.oper", + "app.db.adapters.workflow -> app.db.oper.workflow", + "app.db.adapters.workflow -> app.db.uow", "app.db.base -> app.db", "app.db.base -> app.db.decorators", "app.db.base -> app.db.uow", @@ -5587,6 +5640,8 @@ "app.runtime.extensions.module_manager -> app.runtime.settings", "app.runtime.extensions.module_manager -> app.schemas", "app.runtime.extensions.module_manager -> app.schemas.types", + "app.runtime.extensions.plugin.admission -> app.schemas", + "app.runtime.extensions.plugin.admission -> app.schemas.exception", "app.runtime.extensions.plugin.catalog -> app.foundation", "app.runtime.extensions.plugin.catalog -> app.foundation.version", "app.runtime.extensions.plugin.catalog -> app.runtime", @@ -5653,6 +5708,7 @@ "app.runtime.extensions.plugin_manager -> app.runtime.extensions", "app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin", "app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.access", + "app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.admission", "app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.catalog", "app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.clone", "app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.dependency", @@ -5673,6 +5729,7 @@ "app.runtime.extensions.plugin_manager -> app.runtime.settings", "app.runtime.extensions.plugin_manager -> app.runtime.thread", "app.runtime.extensions.plugin_manager -> app.schemas", + "app.runtime.extensions.plugin_manager -> app.schemas.exception", "app.runtime.extensions.plugin_manager -> app.schemas.plugin", "app.runtime.extensions.plugin_manager -> app.schemas.types", "app.runtime.extensions.service_config -> app.runtime", @@ -5962,109 +6019,295 @@ "app.sdk.utilities -> app.runtime.scheduling", "app.sdk.utilities -> app.sdk", "app.sdk.utilities -> app.sdk.string", - "app.startup.agent_initializer -> app.agent", - "app.startup.agent_initializer -> app.agent.llm", - "app.startup.agent_initializer -> app.agent.llm.capability", - "app.startup.agent_initializer -> app.agent.llm.gateway", - "app.startup.agent_initializer -> app.agent.llm.helper", - "app.startup.agent_initializer -> app.agent.llm.provider", - "app.startup.agent_initializer -> app.agent.prompt", - "app.startup.agent_initializer -> app.agent.prompt.transfer_redo", - "app.startup.agent_initializer -> app.agent.runtime_loader", - "app.startup.agent_initializer -> app.agent.skills", - "app.startup.agent_initializer -> app.agent.skills.registry", - "app.startup.agent_initializer -> app.agent.tools", - "app.startup.agent_initializer -> app.agent.tools.base", - "app.startup.agent_initializer -> app.application", - "app.startup.agent_initializer -> app.application.agent", - "app.startup.agent_initializer -> app.application.messaging", - "app.startup.agent_initializer -> app.application.messaging.skill", - "app.startup.agent_initializer -> app.runtime", - "app.startup.agent_initializer -> app.runtime.events", - "app.startup.agent_initializer -> app.runtime.log", - "app.startup.agent_initializer -> app.runtime.settings", - "app.startup.agent_initializer -> app.schemas", - "app.startup.agent_initializer -> app.schemas.types", - "app.startup.cache_initializer -> app.adapters", - "app.startup.cache_initializer -> app.adapters.cache", - "app.startup.cache_initializer -> app.adapters.cache.backends", - "app.startup.chain_events -> app.application", - "app.startup.chain_events -> app.application.chain", - "app.startup.chain_events -> app.application.chain.durable_events", - "app.startup.chain_events -> app.application.history", - "app.startup.chain_events -> app.application.outbox", - "app.startup.chain_events -> app.db", - "app.startup.chain_events -> app.db.oper", - "app.startup.chain_events -> app.db.oper.downloadhistory", - "app.startup.chain_events -> app.db.oper.transferhistory", - "app.startup.chain_events -> app.db.uow", - "app.startup.chain_events -> app.startup", - "app.startup.chain_events -> app.startup.outbox", - "app.startup.command_initializer -> app.application", - "app.startup.command_initializer -> app.application.commands", - "app.startup.command_initializer -> app.command", - "app.startup.configuration -> app.application", - "app.startup.configuration -> app.application.configuration", - "app.startup.configuration -> app.runtime", - "app.startup.configuration -> app.runtime.config", - "app.startup.configuration -> app.schemas", - "app.startup.configuration -> app.schemas.types", - "app.startup.context -> app.application", - "app.startup.context -> app.application.configuration", - "app.startup.context -> app.application.messaging", - "app.startup.context -> app.application.messaging.chat", - "app.startup.context -> app.application.outbox", - "app.startup.context -> app.application.subscription", - "app.startup.context -> app.application.subscription.delete", - "app.startup.context -> app.application.subscription.identity", - "app.startup.context -> app.application.subscription.mutation", - "app.startup.context -> app.application.workflow", - "app.startup.context -> app.runtime", - "app.startup.context -> app.runtime.tasks", - "app.startup.database -> app.adapters", - "app.startup.database -> app.adapters.system", - "app.startup.database -> app.adapters.system.backup", - "app.startup.database -> app.adapters.system.backup.database", - "app.startup.database -> app.application", - "app.startup.database -> app.application.backup", - "app.startup.database -> app.application.database", - "app.startup.database -> app.application.maintenance", - "app.startup.database -> app.db", - "app.startup.database -> app.db.engine", - "app.startup.database -> app.db.health", - "app.startup.database -> app.db.maintenance", - "app.startup.database -> app.db.session", - "app.startup.database -> app.runtime", - "app.startup.database -> app.runtime.settings", - "app.startup.database_initializer -> app.db", - "app.startup.database_initializer -> app.db.base", - "app.startup.database_initializer -> app.db.engine", - "app.startup.database_initializer -> app.db.models", - "app.startup.database_initializer -> app.runtime", - "app.startup.database_initializer -> app.runtime.log", - "app.startup.database_initializer -> app.runtime.settings", - "app.startup.database_initializer -> app.startup", - "app.startup.database_initializer -> app.startup.database", - "app.startup.domain_initializer -> app.adapters", - "app.startup.domain_initializer -> app.adapters.system", - "app.startup.domain_initializer -> app.adapters.system.rust", - "app.startup.domain_initializer -> app.application", - "app.startup.domain_initializer -> app.application.recognition", - "app.startup.domain_initializer -> app.domain", - "app.startup.domain_initializer -> app.domain.context", - "app.startup.domain_initializer -> app.domain.media", - "app.startup.domain_initializer -> app.domain.meta", - "app.startup.domain_initializer -> app.domain.meta.customization", - "app.startup.domain_initializer -> app.domain.meta.releasegroup", - "app.startup.domain_initializer -> app.domain.meta.runtime", - "app.startup.domain_initializer -> app.domain.meta.words", - "app.startup.domain_initializer -> app.domain.metainfo", - "app.startup.domain_initializer -> app.runtime", - "app.startup.domain_initializer -> app.runtime.settings", - "app.startup.download_failure -> app.db", - "app.startup.download_failure -> app.db.oper", - "app.startup.download_failure -> app.db.oper.downloadfailure", - "app.startup.download_failure -> app.db.uow", + "app.startup.composition.configuration -> app.application", + "app.startup.composition.configuration -> app.application.configuration", + "app.startup.composition.configuration -> app.runtime", + "app.startup.composition.configuration -> app.runtime.config", + "app.startup.composition.configuration -> app.schemas", + "app.startup.composition.configuration -> app.schemas.types", + "app.startup.composition.context -> app.application", + "app.startup.composition.context -> app.application.configuration", + "app.startup.composition.context -> app.application.messaging", + "app.startup.composition.context -> app.application.messaging.chat", + "app.startup.composition.context -> app.application.outbox", + "app.startup.composition.context -> app.application.subscription", + "app.startup.composition.context -> app.application.subscription.delete", + "app.startup.composition.context -> app.application.subscription.identity", + "app.startup.composition.context -> app.application.subscription.mutation", + "app.startup.composition.context -> app.application.workflow", + "app.startup.composition.context -> app.runtime", + "app.startup.composition.context -> app.runtime.tasks", + "app.startup.composition.database -> app.adapters", + "app.startup.composition.database -> app.adapters.system", + "app.startup.composition.database -> app.adapters.system.backup", + "app.startup.composition.database -> app.adapters.system.backup.database", + "app.startup.composition.database -> app.application", + "app.startup.composition.database -> app.application.backup", + "app.startup.composition.database -> app.application.database", + "app.startup.composition.database -> app.application.maintenance", + "app.startup.composition.database -> app.db", + "app.startup.composition.database -> app.db.engine", + "app.startup.composition.database -> app.db.health", + "app.startup.composition.database -> app.db.maintenance", + "app.startup.composition.database -> app.db.session", + "app.startup.composition.database -> app.runtime", + "app.startup.composition.database -> app.runtime.settings", + "app.startup.composition.subscription -> app.adapters", + "app.startup.composition.subscription -> app.adapters.external", + "app.startup.composition.subscription -> app.adapters.external.server", + "app.startup.composition.subscription -> app.application", + "app.startup.composition.subscription -> app.application.subscription", + "app.startup.composition.subscription -> app.application.subscription.complete", + "app.startup.composition.subscription -> app.application.subscription.delete", + "app.startup.composition.subscription -> app.application.subscription.mutation", + "app.startup.composition.subscription -> app.db", + "app.startup.composition.subscription -> app.db.adapters", + "app.startup.composition.subscription -> app.db.adapters.outbox", + "app.startup.composition.subscription -> app.db.oper", + "app.startup.composition.subscription -> app.db.oper.subscribe", + "app.startup.composition.subscription -> app.db.oper.subscribehistory", + "app.startup.composition.subscription -> app.db.session", + "app.startup.composition.subscription -> app.db.uow", + "app.startup.composition.subscription -> app.runtime", + "app.startup.composition.subscription -> app.runtime.events", + "app.startup.composition.subscription -> app.schemas", + "app.startup.composition.subscription -> app.schemas.types", + "app.startup.initializers.agent -> app.agent", + "app.startup.initializers.agent -> app.agent.llm", + "app.startup.initializers.agent -> app.agent.llm.capability", + "app.startup.initializers.agent -> app.agent.llm.gateway", + "app.startup.initializers.agent -> app.agent.llm.helper", + "app.startup.initializers.agent -> app.agent.llm.provider", + "app.startup.initializers.agent -> app.agent.prompt", + "app.startup.initializers.agent -> app.agent.prompt.transfer_redo", + "app.startup.initializers.agent -> app.agent.runtime_loader", + "app.startup.initializers.agent -> app.agent.skills", + "app.startup.initializers.agent -> app.agent.skills.registry", + "app.startup.initializers.agent -> app.agent.tools", + "app.startup.initializers.agent -> app.agent.tools.base", + "app.startup.initializers.agent -> app.application", + "app.startup.initializers.agent -> app.application.agent", + "app.startup.initializers.agent -> app.application.messaging", + "app.startup.initializers.agent -> app.application.messaging.skill", + "app.startup.initializers.agent -> app.runtime", + "app.startup.initializers.agent -> app.runtime.events", + "app.startup.initializers.agent -> app.runtime.log", + "app.startup.initializers.agent -> app.runtime.settings", + "app.startup.initializers.agent -> app.schemas", + "app.startup.initializers.agent -> app.schemas.types", + "app.startup.initializers.cache -> app.adapters", + "app.startup.initializers.cache -> app.adapters.cache", + "app.startup.initializers.cache -> app.adapters.cache.backends", + "app.startup.initializers.command -> app.application", + "app.startup.initializers.command -> app.application.commands", + "app.startup.initializers.command -> app.command", + "app.startup.initializers.database -> app.db", + "app.startup.initializers.database -> app.db.base", + "app.startup.initializers.database -> app.db.engine", + "app.startup.initializers.database -> app.db.models", + "app.startup.initializers.database -> app.runtime", + "app.startup.initializers.database -> app.runtime.log", + "app.startup.initializers.database -> app.runtime.settings", + "app.startup.initializers.database -> app.startup", + "app.startup.initializers.database -> app.startup.composition", + "app.startup.initializers.database -> app.startup.composition.database", + "app.startup.initializers.domain -> app.adapters", + "app.startup.initializers.domain -> app.adapters.system", + "app.startup.initializers.domain -> app.adapters.system.rust", + "app.startup.initializers.domain -> app.application", + "app.startup.initializers.domain -> app.application.recognition", + "app.startup.initializers.domain -> app.domain", + "app.startup.initializers.domain -> app.domain.context", + "app.startup.initializers.domain -> app.domain.media", + "app.startup.initializers.domain -> app.domain.meta", + "app.startup.initializers.domain -> app.domain.meta.customization", + "app.startup.initializers.domain -> app.domain.meta.releasegroup", + "app.startup.initializers.domain -> app.domain.meta.runtime", + "app.startup.initializers.domain -> app.domain.meta.words", + "app.startup.initializers.domain -> app.domain.metainfo", + "app.startup.initializers.domain -> app.runtime", + "app.startup.initializers.domain -> app.runtime.settings", + "app.startup.initializers.managed_resources -> app.runtime", + "app.startup.initializers.managed_resources -> app.runtime.capabilities", + "app.startup.initializers.managed_resources -> app.runtime.capabilities.runtime", + "app.startup.initializers.managed_resources -> app.runtime.extensions", + "app.startup.initializers.managed_resources -> app.runtime.extensions.managed_resource_adapter", + "app.startup.initializers.managed_resources -> app.runtime.managed_resources", + "app.startup.initializers.modules -> app.adapters", + "app.startup.initializers.modules -> app.adapters.cache", + "app.startup.initializers.modules -> app.adapters.cache.redis", + "app.startup.initializers.modules -> app.adapters.external", + "app.startup.initializers.modules -> app.adapters.external.server", + "app.startup.initializers.modules -> app.adapters.network", + "app.startup.initializers.modules -> app.adapters.network.browser", + "app.startup.initializers.modules -> app.adapters.network.doh", + "app.startup.initializers.modules -> app.adapters.system", + "app.startup.initializers.modules -> app.adapters.system.host", + "app.startup.initializers.modules -> app.adapters.system.resource", + "app.startup.initializers.modules -> app.adapters.web", + "app.startup.initializers.modules -> app.adapters.web.security", + "app.startup.initializers.modules -> app.adapters.web.security.access", + "app.startup.initializers.modules -> app.api", + "app.startup.initializers.modules -> app.api.data", + "app.startup.initializers.modules -> app.application", + "app.startup.initializers.modules -> app.application.agentdata", + "app.startup.initializers.modules -> app.application.chain", + "app.startup.initializers.modules -> app.application.chain.context", + "app.startup.initializers.modules -> app.application.chain.data", + "app.startup.initializers.modules -> app.application.chain.durable_events", + "app.startup.initializers.modules -> app.application.configuration", + "app.startup.initializers.modules -> app.application.database", + "app.startup.initializers.modules -> app.application.history", + "app.startup.initializers.modules -> app.application.image", + "app.startup.initializers.modules -> app.application.messaging", + "app.startup.initializers.modules -> app.application.messaging.agent", + "app.startup.initializers.modules -> app.application.messaging.chat", + "app.startup.initializers.modules -> app.application.messaging.message", + "app.startup.initializers.modules -> app.application.module", + "app.startup.initializers.modules -> app.application.outbox", + "app.startup.initializers.modules -> app.application.plugin", + "app.startup.initializers.modules -> app.application.plugin.runtime", + "app.startup.initializers.modules -> app.application.security", + "app.startup.initializers.modules -> app.application.security.auth", + "app.startup.initializers.modules -> app.application.security.passkeys", + "app.startup.initializers.modules -> app.application.security.user", + "app.startup.initializers.modules -> app.application.security.userconfig", + "app.startup.initializers.modules -> app.application.server", + "app.startup.initializers.modules -> app.application.server.report", + "app.startup.initializers.modules -> app.application.server.share", + "app.startup.initializers.modules -> app.application.service", + "app.startup.initializers.modules -> app.application.site", + "app.startup.initializers.modules -> app.application.site.health", + "app.startup.initializers.modules -> app.application.site.query", + "app.startup.initializers.modules -> app.application.subscription", + "app.startup.initializers.modules -> app.application.subscription.write", + "app.startup.initializers.modules -> app.application.workflow", + "app.startup.initializers.modules -> app.chain", + "app.startup.initializers.modules -> app.chain.download", + "app.startup.initializers.modules -> app.chain.mediaserver", + "app.startup.initializers.modules -> app.chain.scraping", + "app.startup.initializers.modules -> app.chain.search", + "app.startup.initializers.modules -> app.chain.site", + "app.startup.initializers.modules -> app.chain.subscribe", + "app.startup.initializers.modules -> app.chain.tmdb", + "app.startup.initializers.modules -> app.chain.workflow", + "app.startup.initializers.modules -> app.command", + "app.startup.initializers.modules -> app.db", + "app.startup.initializers.modules -> app.db.adapters", + "app.startup.initializers.modules -> app.db.adapters.chain", + "app.startup.initializers.modules -> app.db.adapters.download", + "app.startup.initializers.modules -> app.db.adapters.outbox", + "app.startup.initializers.modules -> app.db.adapters.site", + "app.startup.initializers.modules -> app.db.adapters.subscription", + "app.startup.initializers.modules -> app.db.adapters.transaction", + "app.startup.initializers.modules -> app.db.adapters.workflow", + "app.startup.initializers.modules -> app.db.oper", + "app.startup.initializers.modules -> app.db.oper.agentchat", + "app.startup.initializers.modules -> app.db.oper.agenttask", + "app.startup.initializers.modules -> app.db.oper.downloadhistory", + "app.startup.initializers.modules -> app.db.oper.mediaserver", + "app.startup.initializers.modules -> app.db.oper.message", + "app.startup.initializers.modules -> app.db.oper.passkey", + "app.startup.initializers.modules -> app.db.oper.plugindata", + "app.startup.initializers.modules -> app.db.oper.site", + "app.startup.initializers.modules -> app.db.oper.subscribe", + "app.startup.initializers.modules -> app.db.oper.subscribehistory", + "app.startup.initializers.modules -> app.db.oper.systemconfig", + "app.startup.initializers.modules -> app.db.oper.transferhistory", + "app.startup.initializers.modules -> app.db.oper.transferpending", + "app.startup.initializers.modules -> app.db.oper.user", + "app.startup.initializers.modules -> app.db.oper.userconfig", + "app.startup.initializers.modules -> app.db.oper.workflow", + "app.startup.initializers.modules -> app.db.session", + "app.startup.initializers.modules -> app.db.uow", + "app.startup.initializers.modules -> app.db.worker", + "app.startup.initializers.modules -> app.runtime", + "app.startup.initializers.modules -> app.runtime.cache", + "app.startup.initializers.modules -> app.runtime.config", + "app.startup.initializers.modules -> app.runtime.events", + "app.startup.initializers.modules -> app.runtime.extensions", + "app.startup.initializers.modules -> app.runtime.extensions.module", + "app.startup.initializers.modules -> app.runtime.extensions.module.dispatcher", + "app.startup.initializers.modules -> app.runtime.extensions.module_manager", + "app.startup.initializers.modules -> app.runtime.extensions.plugin_manager", + "app.startup.initializers.modules -> app.runtime.extensions.service_config", + "app.startup.initializers.modules -> app.runtime.log", + "app.startup.initializers.modules -> app.runtime.observability", + "app.startup.initializers.modules -> app.runtime.settings", + "app.startup.initializers.modules -> app.runtime.state", + "app.startup.initializers.modules -> app.runtime.tasks", + "app.startup.initializers.modules -> app.runtime.thread", + "app.startup.initializers.modules -> app.scheduler", + "app.startup.initializers.modules -> app.schemas", + "app.startup.initializers.modules -> app.schemas.message", + "app.startup.initializers.modules -> app.schemas.types", + "app.startup.initializers.modules -> app.startup", + "app.startup.initializers.modules -> app.startup.composition", + "app.startup.initializers.modules -> app.startup.composition.configuration", + "app.startup.initializers.modules -> app.startup.composition.context", + "app.startup.initializers.modules -> app.startup.composition.database", + "app.startup.initializers.modules -> app.startup.composition.subscription", + "app.startup.initializers.modules -> app.startup.initializers", + "app.startup.initializers.modules -> app.startup.initializers.agent", + "app.startup.initializers.modules -> app.startup.initializers.managed_resources", + "app.startup.initializers.monitor -> app.monitor", + "app.startup.initializers.monitor -> app.runtime", + "app.startup.initializers.monitor -> app.runtime.execution", + "app.startup.initializers.plugins -> app.adapters", + "app.startup.initializers.plugins -> app.adapters.external", + "app.startup.initializers.plugins -> app.adapters.external.market", + "app.startup.initializers.plugins -> app.adapters.external.plugin", + "app.startup.initializers.plugins -> app.adapters.external.plugin.client", + "app.startup.initializers.plugins -> app.adapters.external.server", + "app.startup.initializers.plugins -> app.adapters.system", + "app.startup.initializers.plugins -> app.adapters.system.host", + "app.startup.initializers.plugins -> app.adapters.system.plugin", + "app.startup.initializers.plugins -> app.adapters.system.plugin.dependency", + "app.startup.initializers.plugins -> app.adapters.system.plugin.manifest", + "app.startup.initializers.plugins -> app.adapters.system.plugin.package", + "app.startup.initializers.plugins -> app.application", + "app.startup.initializers.plugins -> app.application.configuration", + "app.startup.initializers.plugins -> app.application.plugin", + "app.startup.initializers.plugins -> app.application.plugin.catalog", + "app.startup.initializers.plugins -> app.application.plugin.data", + "app.startup.initializers.plugins -> app.application.plugin.routes", + "app.startup.initializers.plugins -> app.application.site", + "app.startup.initializers.plugins -> app.db", + "app.startup.initializers.plugins -> app.db.oper", + "app.startup.initializers.plugins -> app.db.oper.plugindata", + "app.startup.initializers.plugins -> app.db.session", + "app.startup.initializers.plugins -> app.db.uow", + "app.startup.initializers.plugins -> app.foundation", + "app.startup.initializers.plugins -> app.foundation.version", + "app.startup.initializers.plugins -> app.runtime", + "app.startup.initializers.plugins -> app.runtime.compat", + "app.startup.initializers.plugins -> app.runtime.compat.diagnostics", + "app.startup.initializers.plugins -> app.runtime.compat.resource_imports", + "app.startup.initializers.plugins -> app.runtime.config", + "app.startup.initializers.plugins -> app.runtime.execution", + "app.startup.initializers.plugins -> app.runtime.extensions", + "app.startup.initializers.plugins -> app.runtime.extensions.plugin", + "app.startup.initializers.plugins -> app.runtime.extensions.plugin.dependency", + "app.startup.initializers.plugins -> app.runtime.extensions.plugin.storage", + "app.startup.initializers.plugins -> app.runtime.extensions.plugin.system", + "app.startup.initializers.plugins -> app.runtime.extensions.plugin_manager", + "app.startup.initializers.plugins -> app.runtime.log", + "app.startup.initializers.plugins -> app.runtime.managed_resources", + "app.startup.initializers.plugins -> app.runtime.settings", + "app.startup.initializers.plugins -> app.schemas", + "app.startup.initializers.plugins -> app.schemas.exception", + "app.startup.initializers.plugins -> app.schemas.plugin", + "app.startup.initializers.plugins -> app.schemas.types", + "app.startup.initializers.routers -> app.api", + "app.startup.initializers.routers -> app.api.router_specs", + "app.startup.initializers.routers -> app.api.servarr", + "app.startup.initializers.routers -> app.api.servcookie", + "app.startup.initializers.scheduler -> app.application", + "app.startup.initializers.scheduler -> app.application.scheduling", + "app.startup.initializers.scheduler -> app.scheduler", + "app.startup.initializers.transfer -> app.chain", + "app.startup.initializers.transfer -> app.chain.transfer", + "app.startup.initializers.workflow -> app.workflow", "app.startup.lifecycle -> app.adapters", "app.startup.lifecycle -> app.adapters.external", "app.startup.lifecycle -> app.adapters.external.server", @@ -6087,233 +6330,20 @@ "app.startup.lifecycle -> app.runtime.tasks", "app.startup.lifecycle -> app.runtime.topology", "app.startup.lifecycle -> app.startup", - "app.startup.lifecycle -> app.startup.agent_initializer", - "app.startup.lifecycle -> app.startup.cache_initializer", - "app.startup.lifecycle -> app.startup.command_initializer", - "app.startup.lifecycle -> app.startup.database_initializer", - "app.startup.lifecycle -> app.startup.domain_initializer", + "app.startup.lifecycle -> app.startup.initializers", + "app.startup.lifecycle -> app.startup.initializers.agent", + "app.startup.lifecycle -> app.startup.initializers.cache", + "app.startup.lifecycle -> app.startup.initializers.command", + "app.startup.lifecycle -> app.startup.initializers.database", + "app.startup.lifecycle -> app.startup.initializers.domain", + "app.startup.lifecycle -> app.startup.initializers.modules", + "app.startup.lifecycle -> app.startup.initializers.monitor", + "app.startup.lifecycle -> app.startup.initializers.plugins", + "app.startup.lifecycle -> app.startup.initializers.routers", + "app.startup.lifecycle -> app.startup.initializers.scheduler", + "app.startup.lifecycle -> app.startup.initializers.transfer", + "app.startup.lifecycle -> app.startup.initializers.workflow", "app.startup.lifecycle -> app.startup.lifecycle.components", - "app.startup.lifecycle -> app.startup.modules_initializer", - "app.startup.lifecycle -> app.startup.monitor_initializer", - "app.startup.lifecycle -> app.startup.plugins_initializer", - "app.startup.lifecycle -> app.startup.routers_initializer", - "app.startup.lifecycle -> app.startup.scheduler_initializer", - "app.startup.lifecycle -> app.startup.transfer_initializer", - "app.startup.lifecycle -> app.startup.workflow_initializer", - "app.startup.managed_resources_initializer -> app.runtime", - "app.startup.managed_resources_initializer -> app.runtime.capabilities", - "app.startup.managed_resources_initializer -> app.runtime.capabilities.runtime", - "app.startup.managed_resources_initializer -> app.runtime.extensions", - "app.startup.managed_resources_initializer -> app.runtime.extensions.managed_resource_adapter", - "app.startup.managed_resources_initializer -> app.runtime.managed_resources", - "app.startup.modules_initializer -> app.adapters", - "app.startup.modules_initializer -> app.adapters.cache", - "app.startup.modules_initializer -> app.adapters.cache.redis", - "app.startup.modules_initializer -> app.adapters.external", - "app.startup.modules_initializer -> app.adapters.external.server", - "app.startup.modules_initializer -> app.adapters.network", - "app.startup.modules_initializer -> app.adapters.network.browser", - "app.startup.modules_initializer -> app.adapters.network.doh", - "app.startup.modules_initializer -> app.adapters.system", - "app.startup.modules_initializer -> app.adapters.system.host", - "app.startup.modules_initializer -> app.adapters.system.resource", - "app.startup.modules_initializer -> app.adapters.web", - "app.startup.modules_initializer -> app.adapters.web.security", - "app.startup.modules_initializer -> app.adapters.web.security.access", - "app.startup.modules_initializer -> app.api", - "app.startup.modules_initializer -> app.api.data", - "app.startup.modules_initializer -> app.application", - "app.startup.modules_initializer -> app.application.agentdata", - "app.startup.modules_initializer -> app.application.chain", - "app.startup.modules_initializer -> app.application.chain.context", - "app.startup.modules_initializer -> app.application.chain.data", - "app.startup.modules_initializer -> app.application.chain.durable_events", - "app.startup.modules_initializer -> app.application.configuration", - "app.startup.modules_initializer -> app.application.database", - "app.startup.modules_initializer -> app.application.history", - "app.startup.modules_initializer -> app.application.image", - "app.startup.modules_initializer -> app.application.messaging", - "app.startup.modules_initializer -> app.application.messaging.agent", - "app.startup.modules_initializer -> app.application.messaging.chat", - "app.startup.modules_initializer -> app.application.messaging.message", - "app.startup.modules_initializer -> app.application.module", - "app.startup.modules_initializer -> app.application.outbox", - "app.startup.modules_initializer -> app.application.plugin", - "app.startup.modules_initializer -> app.application.plugin.runtime", - "app.startup.modules_initializer -> app.application.security", - "app.startup.modules_initializer -> app.application.security.auth", - "app.startup.modules_initializer -> app.application.security.passkeys", - "app.startup.modules_initializer -> app.application.security.user", - "app.startup.modules_initializer -> app.application.security.userconfig", - "app.startup.modules_initializer -> app.application.server", - "app.startup.modules_initializer -> app.application.server.report", - "app.startup.modules_initializer -> app.application.server.share", - "app.startup.modules_initializer -> app.application.service", - "app.startup.modules_initializer -> app.application.site", - "app.startup.modules_initializer -> app.application.site.health", - "app.startup.modules_initializer -> app.application.site.query", - "app.startup.modules_initializer -> app.application.subscription", - "app.startup.modules_initializer -> app.application.subscription.write", - "app.startup.modules_initializer -> app.application.workflow", - "app.startup.modules_initializer -> app.chain", - "app.startup.modules_initializer -> app.chain.download", - "app.startup.modules_initializer -> app.chain.mediaserver", - "app.startup.modules_initializer -> app.chain.scraping", - "app.startup.modules_initializer -> app.chain.search", - "app.startup.modules_initializer -> app.chain.site", - "app.startup.modules_initializer -> app.chain.subscribe", - "app.startup.modules_initializer -> app.chain.tmdb", - "app.startup.modules_initializer -> app.chain.workflow", - "app.startup.modules_initializer -> app.command", - "app.startup.modules_initializer -> app.db", - "app.startup.modules_initializer -> app.db.oper", - "app.startup.modules_initializer -> app.db.oper.agentchat", - "app.startup.modules_initializer -> app.db.oper.agenttask", - "app.startup.modules_initializer -> app.db.oper.downloadhistory", - "app.startup.modules_initializer -> app.db.oper.mediaserver", - "app.startup.modules_initializer -> app.db.oper.message", - "app.startup.modules_initializer -> app.db.oper.passkey", - "app.startup.modules_initializer -> app.db.oper.plugindata", - "app.startup.modules_initializer -> app.db.oper.site", - "app.startup.modules_initializer -> app.db.oper.subscribe", - "app.startup.modules_initializer -> app.db.oper.subscribehistory", - "app.startup.modules_initializer -> app.db.oper.systemconfig", - "app.startup.modules_initializer -> app.db.oper.transferhistory", - "app.startup.modules_initializer -> app.db.oper.transferpending", - "app.startup.modules_initializer -> app.db.oper.user", - "app.startup.modules_initializer -> app.db.oper.userconfig", - "app.startup.modules_initializer -> app.db.oper.workflow", - "app.startup.modules_initializer -> app.db.session", - "app.startup.modules_initializer -> app.db.uow", - "app.startup.modules_initializer -> app.db.worker", - "app.startup.modules_initializer -> app.runtime", - "app.startup.modules_initializer -> app.runtime.cache", - "app.startup.modules_initializer -> app.runtime.config", - "app.startup.modules_initializer -> app.runtime.events", - "app.startup.modules_initializer -> app.runtime.extensions", - "app.startup.modules_initializer -> app.runtime.extensions.module", - "app.startup.modules_initializer -> app.runtime.extensions.module.dispatcher", - "app.startup.modules_initializer -> app.runtime.extensions.module_manager", - "app.startup.modules_initializer -> app.runtime.extensions.plugin_manager", - "app.startup.modules_initializer -> app.runtime.extensions.service_config", - "app.startup.modules_initializer -> app.runtime.log", - "app.startup.modules_initializer -> app.runtime.observability", - "app.startup.modules_initializer -> app.runtime.settings", - "app.startup.modules_initializer -> app.runtime.state", - "app.startup.modules_initializer -> app.runtime.tasks", - "app.startup.modules_initializer -> app.runtime.thread", - "app.startup.modules_initializer -> app.scheduler", - "app.startup.modules_initializer -> app.schemas", - "app.startup.modules_initializer -> app.schemas.message", - "app.startup.modules_initializer -> app.schemas.types", - "app.startup.modules_initializer -> app.startup", - "app.startup.modules_initializer -> app.startup.agent_initializer", - "app.startup.modules_initializer -> app.startup.chain_events", - "app.startup.modules_initializer -> app.startup.configuration", - "app.startup.modules_initializer -> app.startup.context", - "app.startup.modules_initializer -> app.startup.database", - "app.startup.modules_initializer -> app.startup.download_failure", - "app.startup.modules_initializer -> app.startup.managed_resources_initializer", - "app.startup.modules_initializer -> app.startup.outbox", - "app.startup.modules_initializer -> app.startup.site", - "app.startup.modules_initializer -> app.startup.subscription", - "app.startup.modules_initializer -> app.startup.transaction", - "app.startup.modules_initializer -> app.startup.workflow", - "app.startup.monitor_initializer -> app.monitor", - "app.startup.monitor_initializer -> app.runtime", - "app.startup.monitor_initializer -> app.runtime.execution", - "app.startup.outbox -> app.application", - "app.startup.outbox -> app.application.outbox", - "app.startup.outbox -> app.db", - "app.startup.outbox -> app.db.base", - "app.startup.outbox -> app.db.models", - "app.startup.outbox -> app.db.models.outbox", - "app.startup.plugins_initializer -> app.adapters", - "app.startup.plugins_initializer -> app.adapters.external", - "app.startup.plugins_initializer -> app.adapters.external.market", - "app.startup.plugins_initializer -> app.adapters.external.plugin", - "app.startup.plugins_initializer -> app.adapters.external.plugin.client", - "app.startup.plugins_initializer -> app.adapters.external.server", - "app.startup.plugins_initializer -> app.adapters.system", - "app.startup.plugins_initializer -> app.adapters.system.host", - "app.startup.plugins_initializer -> app.adapters.system.plugin", - "app.startup.plugins_initializer -> app.adapters.system.plugin.dependency", - "app.startup.plugins_initializer -> app.adapters.system.plugin.manifest", - "app.startup.plugins_initializer -> app.adapters.system.plugin.package", - "app.startup.plugins_initializer -> app.application", - "app.startup.plugins_initializer -> app.application.configuration", - "app.startup.plugins_initializer -> app.application.plugin", - "app.startup.plugins_initializer -> app.application.plugin.catalog", - "app.startup.plugins_initializer -> app.application.plugin.data", - "app.startup.plugins_initializer -> app.application.plugin.routes", - "app.startup.plugins_initializer -> app.application.site", - "app.startup.plugins_initializer -> app.db", - "app.startup.plugins_initializer -> app.db.oper", - "app.startup.plugins_initializer -> app.db.oper.plugindata", - "app.startup.plugins_initializer -> app.db.session", - "app.startup.plugins_initializer -> app.db.uow", - "app.startup.plugins_initializer -> app.foundation", - "app.startup.plugins_initializer -> app.foundation.version", - "app.startup.plugins_initializer -> app.runtime", - "app.startup.plugins_initializer -> app.runtime.compat", - "app.startup.plugins_initializer -> app.runtime.compat.diagnostics", - "app.startup.plugins_initializer -> app.runtime.compat.resource_imports", - "app.startup.plugins_initializer -> app.runtime.config", - "app.startup.plugins_initializer -> app.runtime.extensions", - "app.startup.plugins_initializer -> app.runtime.extensions.plugin", - "app.startup.plugins_initializer -> app.runtime.extensions.plugin.dependency", - "app.startup.plugins_initializer -> app.runtime.extensions.plugin.storage", - "app.startup.plugins_initializer -> app.runtime.extensions.plugin.system", - "app.startup.plugins_initializer -> app.runtime.extensions.plugin_manager", - "app.startup.plugins_initializer -> app.runtime.log", - "app.startup.plugins_initializer -> app.runtime.managed_resources", - "app.startup.plugins_initializer -> app.runtime.settings", - "app.startup.plugins_initializer -> app.schemas", - "app.startup.plugins_initializer -> app.schemas.plugin", - "app.startup.plugins_initializer -> app.schemas.types", - "app.startup.routers_initializer -> app.api", - "app.startup.routers_initializer -> app.api.router_specs", - "app.startup.routers_initializer -> app.api.servarr", - "app.startup.routers_initializer -> app.api.servcookie", - "app.startup.scheduler_initializer -> app.application", - "app.startup.scheduler_initializer -> app.application.scheduling", - "app.startup.scheduler_initializer -> app.scheduler", - "app.startup.site -> app.db", - "app.startup.site -> app.db.oper", - "app.startup.site -> app.db.oper.site", - "app.startup.site -> app.db.uow", - "app.startup.subscription -> app.adapters", - "app.startup.subscription -> app.adapters.external", - "app.startup.subscription -> app.adapters.external.server", - "app.startup.subscription -> app.application", - "app.startup.subscription -> app.application.subscription", - "app.startup.subscription -> app.application.subscription.complete", - "app.startup.subscription -> app.application.subscription.delete", - "app.startup.subscription -> app.application.subscription.mutation", - "app.startup.subscription -> app.application.subscription.write", - "app.startup.subscription -> app.db", - "app.startup.subscription -> app.db.oper", - "app.startup.subscription -> app.db.oper.subscribe", - "app.startup.subscription -> app.db.oper.subscribehistory", - "app.startup.subscription -> app.db.session", - "app.startup.subscription -> app.db.uow", - "app.startup.subscription -> app.runtime", - "app.startup.subscription -> app.runtime.events", - "app.startup.subscription -> app.schemas", - "app.startup.subscription -> app.schemas.types", - "app.startup.subscription -> app.startup", - "app.startup.subscription -> app.startup.outbox", - "app.startup.transaction -> app.db", - "app.startup.transaction -> app.db.uow", - "app.startup.transfer_initializer -> app.chain", - "app.startup.transfer_initializer -> app.chain.transfer", - "app.startup.workflow -> app.application", - "app.startup.workflow -> app.application.workflow", - "app.startup.workflow -> app.db", - "app.startup.workflow -> app.db.oper", - "app.startup.workflow -> app.db.oper.workflow", - "app.startup.workflow -> app.db.uow", - "app.startup.workflow_initializer -> app.workflow", "app.testing -> app.testing.stub", "app.testing.bootstrap -> app.application", "app.testing.bootstrap -> app.application.site", @@ -6322,9 +6352,10 @@ "app.testing.bootstrap -> app.db.oper.systemconfig", "app.testing.bootstrap -> app.db.oper.userconfig", "app.testing.bootstrap -> app.startup", - "app.testing.bootstrap -> app.startup.cache_initializer", - "app.testing.bootstrap -> app.startup.database_initializer", - "app.testing.bootstrap -> app.startup.domain_initializer", + "app.testing.bootstrap -> app.startup.initializers", + "app.testing.bootstrap -> app.startup.initializers.cache", + "app.testing.bootstrap -> app.startup.initializers.database", + "app.testing.bootstrap -> app.startup.initializers.domain", "app.workflow -> app.application", "app.workflow -> app.application.chain", "app.workflow -> app.application.chain.data", @@ -6500,7 +6531,7 @@ "app.workflow.actions.transfer_file -> app.workflow", "app.workflow.actions.transfer_file -> app.workflow.actions" ], - "module_count": 800, + "module_count": 805, "modules": [ "app", "app.adapters", @@ -6874,6 +6905,14 @@ "app.cli", "app.command", "app.db", + "app.db.adapters", + "app.db.adapters.chain", + "app.db.adapters.download", + "app.db.adapters.outbox", + "app.db.adapters.site", + "app.db.adapters.subscription", + "app.db.adapters.transaction", + "app.db.adapters.workflow", "app.db.base", "app.db.decorators", "app.db.diagnostics", @@ -7164,6 +7203,7 @@ "app.runtime.extensions.module_manager", "app.runtime.extensions.plugin", "app.runtime.extensions.plugin.access", + "app.runtime.extensions.plugin.admission", "app.runtime.extensions.plugin.catalog", "app.runtime.extensions.plugin.clone", "app.runtime.extensions.plugin.contracts", @@ -7256,31 +7296,27 @@ "app.sdk.string", "app.sdk.utilities", "app.startup", - "app.startup.agent_initializer", - "app.startup.cache_initializer", - "app.startup.chain_events", - "app.startup.command_initializer", - "app.startup.configuration", - "app.startup.context", - "app.startup.database", - "app.startup.database_initializer", - "app.startup.domain_initializer", - "app.startup.download_failure", + "app.startup.composition", + "app.startup.composition.configuration", + "app.startup.composition.context", + "app.startup.composition.database", + "app.startup.composition.subscription", + "app.startup.initializers", + "app.startup.initializers.agent", + "app.startup.initializers.cache", + "app.startup.initializers.command", + "app.startup.initializers.database", + "app.startup.initializers.domain", + "app.startup.initializers.managed_resources", + "app.startup.initializers.modules", + "app.startup.initializers.monitor", + "app.startup.initializers.plugins", + "app.startup.initializers.routers", + "app.startup.initializers.scheduler", + "app.startup.initializers.transfer", + "app.startup.initializers.workflow", "app.startup.lifecycle", "app.startup.lifecycle.components", - "app.startup.managed_resources_initializer", - "app.startup.modules_initializer", - "app.startup.monitor_initializer", - "app.startup.outbox", - "app.startup.plugins_initializer", - "app.startup.routers_initializer", - "app.startup.scheduler_initializer", - "app.startup.site", - "app.startup.subscription", - "app.startup.transaction", - "app.startup.transfer_initializer", - "app.startup.workflow", - "app.startup.workflow_initializer", "app.testing", "app.testing.bootstrap", "app.testing.network_guard", diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json index 3fe8cdd28..7b912deba 100644 --- a/tests/fixtures/architecture/runtime-contract-baseline.json +++ b/tests/fixtures/architecture/runtime-contract-baseline.json @@ -187,8 +187,8 @@ "introduced": "v3.0.0", "is_package": false, "owner": "startup", - "replacement": "app.startup.database_initializer", - "target": "app.startup.database_initializer" + "replacement": "app.startup.initializers.database", + "target": "app.startup.initializers.database" }, "app.db.mediaserver_oper": { "introduced": "v3.0.0", @@ -2029,7 +2029,7 @@ "count": 1 }, { - "caller": "app.startup.agent_initializer", + "caller": "app.startup.initializers.agent", "count": 1 } ], @@ -2043,7 +2043,7 @@ "count": 2 }, { - "caller": "app.startup.modules_initializer", + "caller": "app.startup.initializers.modules", "count": 1 } ] @@ -2194,7 +2194,7 @@ "count": 2 }, { - "caller": "app.startup.modules_initializer", + "caller": "app.startup.initializers.modules", "count": 1 } ] @@ -2203,11 +2203,11 @@ "consumers": [], "producers": [ { - "caller": "app.startup.modules_initializer", + "caller": "app.startup.composition.subscription", "count": 1 }, { - "caller": "app.startup.subscription", + "caller": "app.startup.initializers.modules", "count": 1 } ] @@ -2220,11 +2220,11 @@ "count": 1 }, { - "caller": "app.startup.modules_initializer", + "caller": "app.startup.composition.subscription", "count": 1 }, { - "caller": "app.startup.subscription", + "caller": "app.startup.initializers.modules", "count": 1 } ] @@ -2241,11 +2241,11 @@ "count": 3 }, { - "caller": "app.startup.modules_initializer", + "caller": "app.startup.composition.subscription", "count": 1 }, { - "caller": "app.startup.subscription", + "caller": "app.startup.initializers.modules", "count": 1 } ] @@ -2293,7 +2293,7 @@ "count": 2 }, { - "caller": "app.startup.modules_initializer", + "caller": "app.startup.initializers.modules", "count": 1 } ] @@ -2306,7 +2306,7 @@ "count": 2 }, { - "caller": "app.startup.modules_initializer", + "caller": "app.startup.initializers.modules", "count": 1 } ] diff --git a/tests/test_agent_api_lazy_imports.py b/tests/test_agent_api_lazy_imports.py index d1061fffb..14c859548 100644 --- a/tests/test_agent_api_lazy_imports.py +++ b/tests/test_agent_api_lazy_imports.py @@ -56,7 +56,7 @@ sites.__file__ = "" sys.modules["app.application.site.sites"] = sites from fastapi import FastAPI -from app.startup.routers_initializer import init_routers +from app.startup.initializers.routers import init_routers app = FastAPI() init_routers(app) diff --git a/tests/test_agent_lazy_initializer.py b/tests/test_agent_lazy_initializer.py index df8b9fd69..c0858cda6 100644 --- a/tests/test_agent_lazy_initializer.py +++ b/tests/test_agent_lazy_initializer.py @@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest from app.runtime.capabilities.errors import CapabilityRuntimeClosedError -from app.startup import agent_initializer +from app.startup.initializers import agent as agent_initializer @pytest.mark.anyio diff --git a/tests/test_agent_lazy_runtime_boundary.py b/tests/test_agent_lazy_runtime_boundary.py index 40827576e..dfda740d6 100644 --- a/tests/test_agent_lazy_runtime_boundary.py +++ b/tests/test_agent_lazy_runtime_boundary.py @@ -64,7 +64,7 @@ def test_agent_initializer_import_only_registers_lazy_providers() -> None: import json import sys -import app.startup.agent_initializer +import app.startup.initializers.agent forbidden = ( "app.agent.orchestrator", diff --git a/tests/test_agent_lifecycle.py b/tests/test_agent_lifecycle.py index e3195bed9..ab4525355 100644 --- a/tests/test_agent_lifecycle.py +++ b/tests/test_agent_lifecycle.py @@ -15,7 +15,8 @@ from app.agent.orchestrator import ( AgentManagerUnavailableError, ) from app.agent.memory import MemoryManager -from app.startup import agent_initializer, modules_initializer +from app.startup.initializers import agent as agent_initializer +from app.startup.initializers import modules as modules_initializer @pytest.mark.anyio diff --git a/tests/test_api_response.py b/tests/test_api_response.py index 4824b0421..dd535fda9 100644 --- a/tests/test_api_response.py +++ b/tests/test_api_response.py @@ -512,7 +512,7 @@ def test_v1_model_free_routes_match_audited_native_allowlist(): def test_native_protocol_openapi_has_explicit_response_schemas(): """OpenAI、Anthropic 与 MCP 原生协议响应必须在 OpenAPI 中明确建模。""" from app.factory import create_app - from app.startup.routers_initializer import init_routers + from app.startup.initializers.routers import init_routers app = create_app() init_routers(app) @@ -558,7 +558,7 @@ def test_native_protocol_openapi_has_explicit_response_schemas(): async def test_native_protocol_validation_errors_keep_native_shapes(): """OpenAI 与 Anthropic 的请求校验错误应保持各自协议的错误结构。""" from app.factory import create_app - from app.startup.routers_initializer import init_routers + from app.startup.initializers.routers import init_routers app = create_app() init_routers(app) @@ -603,7 +603,7 @@ async def test_native_protocol_validation_errors_keep_native_shapes(): async def test_mcp_root_auth_error_keeps_jsonrpc_shape(): """MCP 根端点的依赖异常应保持 JSON-RPC,REST 子端点仍由统一协议处理。""" from app.factory import create_app - from app.startup.routers_initializer import init_routers + from app.startup.initializers.routers import init_routers app = create_app() init_routers(app) @@ -678,7 +678,7 @@ async def test_native_ai_http_and_unhandled_errors_keep_protocol_shapes(): def test_servarr_and_cookiecloud_openapi_has_explicit_models(): """兼容协议成功响应必须显式建模,错误响应必须声明统一结构。""" from app.factory import create_app - from app.startup.routers_initializer import init_routers + from app.startup.initializers.routers import init_routers app = create_app() init_routers(app) @@ -712,7 +712,7 @@ def test_servarr_and_cookiecloud_openapi_has_explicit_models(): def test_all_openapi_error_responses_use_json_schemas(): """所有普通与原生协议错误响应都应在文档中声明 JSON 媒体类型和结构。""" from app.factory import create_app - from app.startup.routers_initializer import init_routers + from app.startup.initializers.routers import init_routers app = create_app() init_routers(app) @@ -739,7 +739,7 @@ def test_all_openapi_error_responses_use_json_schemas(): def test_openapi_success_models_have_no_implicit_empty_nested_schemas(): """2xx 响应可达模型不得包含裸 Any、裸数组或未声明值类型的开放映射。""" from app.factory import create_app - from app.startup.routers_initializer import init_routers + from app.startup.initializers.routers import init_routers app = create_app() init_routers(app) diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py index 5b3936e0b..02e4d2e7d 100644 --- a/tests/test_architecture_dependencies.py +++ b/tests/test_architecture_dependencies.py @@ -12,6 +12,7 @@ IMPLEMENTATION_ROOTS = ( "app.agent.skills", "app.adapters", "app.application", + "app.db.adapters", "app.domain", "app.foundation", "app.runtime", @@ -66,6 +67,29 @@ RETIRED_CANONICAL_FILES = ( "app/adapters/network/sites.pyi", "app/application/plugins.py", "app/application/subscribe.py", + "app/startup/agent_initializer.py", + "app/startup/cache_initializer.py", + "app/startup/chain_events.py", + "app/startup/command_initializer.py", + "app/startup/configuration.py", + "app/startup/context.py", + "app/startup/database.py", + "app/startup/database_initializer.py", + "app/startup/domain_initializer.py", + "app/startup/download_failure.py", + "app/startup/managed_resources_initializer.py", + "app/startup/modules_initializer.py", + "app/startup/monitor_initializer.py", + "app/startup/outbox.py", + "app/startup/plugins_initializer.py", + "app/startup/routers_initializer.py", + "app/startup/scheduler_initializer.py", + "app/startup/site.py", + "app/startup/subscription.py", + "app/startup/transaction.py", + "app/startup/transfer_initializer.py", + "app/startup/workflow.py", + "app/startup/workflow_initializer.py", ) PLUGIN_COMPONENT_ROOTS = ( "app/adapters/external/plugin", @@ -288,6 +312,20 @@ def test_retired_canonical_filenames_do_not_return(): assert leftovers == [] +def test_startup_root_contains_only_composition_packages(): + """组合根顶层只保留稳定分区,禁止再次堆叠扁平实现文件。""" + startup_root = APP_ROOT / "startup" + root_modules = sorted(path.name for path in startup_root.glob("*.py")) + python_packages = sorted( + path.name + for path in startup_root.iterdir() + if path.is_dir() and any(path.rglob("*.py")) + ) + + assert root_modules == ["__init__.py"] + assert python_packages == ["composition", "initializers", "lifecycle"] + + def test_retired_canonical_roots_contain_no_python_sources(): """已收敛的新增目录不得再次以顶级 Python 包形式出现。""" leftovers = sorted( @@ -394,6 +432,49 @@ def test_database_internals_do_not_import_db_facades(): assert violations == [] +def test_base_crud_is_explicitly_legacy_only(): + """Base 便利 CRUD 只能保留兼容壳,不得伪装成新的正式事务入口。""" + path = APP_ROOT / "db" / "base.py" + tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) + base_class = next( + node + for node in tree.body + if isinstance(node, ast.ClassDef) and node.name == "Base" + ) + formal_decorators = { + "db_query", + "db_update", + "async_db_query", + "async_db_update", + } + violations: list[str] = [] + for node in base_class.body: + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + decorators = { + decorator.id + for decorator in node.decorator_list + if isinstance(decorator, ast.Name) + } + if decorators & formal_decorators: + violations.append(node.name) + assert violations == [] + + +def test_models_use_one_legacy_query_compatibility_shell(): + """旧 Model 查询统一使用 legacy 装饰器,不得再手写隐式会话 runner。""" + retired_names = {"run_legacy_sync_query", "run_legacy_async_query"} + violations: list[str] = [] + for path in (APP_ROOT / "db" / "models").glob("*.py"): + tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) + if any( + isinstance(node, ast.Name) and node.id in retired_names + for node in ast.walk(tree) + ): + violations.append(str(path.relative_to(PROJECT_ROOT))) + assert violations == [] + + def test_entry_layers_do_not_import_database_implementations(): """API、应用、编排、Agent、监控、模块和 Runtime 只能经端口访问持久化。""" graph = _build_module_graph() diff --git a/tests/test_cache_system.py b/tests/test_cache_system.py index 2241cb5fb..9fe3ff8f8 100644 --- a/tests/test_cache_system.py +++ b/tests/test_cache_system.py @@ -40,7 +40,7 @@ def test_clear_package_tool_cache_only_removes_pip_and_uv_old_files(tmp_path, mo """ 包安装工具缓存清理只处理 pip/uv 子目录,不接管整个 .cache 或业务缓存。 """ - from app.startup.modules_initializer import clear_package_tool_cache + from app.startup.initializers.modules import clear_package_tool_cache old_time = time.time() - 40 * 24 * 3600 cache_root = tmp_path / ".cache" @@ -68,7 +68,7 @@ def test_clear_package_tool_cache_disabled_when_days_non_positive(tmp_path, monk """ PACKAGE_CACHE_DAYS 小于等于 0 时不清理包安装缓存。 """ - from app.startup.modules_initializer import clear_package_tool_cache + from app.startup.initializers.modules import clear_package_tool_cache old_time = time.time() - 40 * 24 * 3600 old_pip = tmp_path / ".cache" / "pip" / "old.whl" @@ -88,7 +88,7 @@ def test_clear_package_tool_cache_isolates_subdir_errors(tmp_path, monkeypatch): """ 单个工具缓存目录清理失败,不影响另一个工具缓存目录。 """ - from app.startup.modules_initializer import clear_package_tool_cache + from app.startup.initializers.modules import clear_package_tool_cache calls = [] @@ -100,7 +100,7 @@ def test_clear_package_tool_cache_isolates_subdir_errors(tmp_path, monkeypatch): monkeypatch.setattr(settings, "CONFIG_DIR", str(tmp_path)) monkeypatch.setattr(settings, "PACKAGE_CACHE_ROOT", str(tmp_path / "custom-package-cache")) monkeypatch.setattr(settings, "PACKAGE_CACHE_DAYS", 30) - monkeypatch.setattr("app.startup.modules_initializer.SystemUtils.clear", fake_clear) + monkeypatch.setattr("app.startup.initializers.modules.SystemUtils.clear", fake_clear) clear_package_tool_cache() @@ -110,7 +110,7 @@ def test_clear_package_tool_cache_uses_package_cache_root(tmp_path, monkeypatch) """ PACKAGE_CACHE_ROOT 用作 pip/uv 清理根目录,不扩大到配置目录下其他缓存。 """ - from app.startup.modules_initializer import clear_package_tool_cache + from app.startup.initializers.modules import clear_package_tool_cache old_time = time.time() - 40 * 24 * 3600 package_cache_root = tmp_path / "custom-package-cache" @@ -134,7 +134,7 @@ def test_init_modules_does_not_clear_package_tool_cache(monkeypatch): """ 包安装缓存清理由通用临时清理入口触发,模块启动路径不直接执行清理。 """ - from app.startup import modules_initializer + from app.startup.initializers import modules as modules_initializer called = False diff --git a/tests/test_chain_durable_events.py b/tests/test_chain_durable_events.py index df1fc5996..357d07bc3 100644 --- a/tests/test_chain_durable_events.py +++ b/tests/test_chain_durable_events.py @@ -22,7 +22,7 @@ from app.domain.metainfo import MetaInfo from app.schemas.file import FileItem from app.schemas.transfer import TransferInfo from app.schemas.types import MediaSource, MediaType -from app.startup.chain_events import TransactionalChainDurableEventWriter +from app.db.adapters.chain import TransactionalChainDurableEventWriter def _session_factory(): diff --git a/tests/test_configuration_initializer.py b/tests/test_configuration_initializer.py index 24a765f90..5ab7cbb6d 100644 --- a/tests/test_configuration_initializer.py +++ b/tests/test_configuration_initializer.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock import pytest -from app.startup import modules_initializer +from app.startup.initializers import modules as modules_initializer from app.startup.lifecycle import initialize_modules_component from app.application.configuration import configure_runtime_settings from app.runtime.settings import RuntimeSettingsCompat diff --git a/tests/test_database_backup_initializer.py b/tests/test_database_backup_initializer.py index 9f65ac35c..7bba4ef12 100644 --- a/tests/test_database_backup_initializer.py +++ b/tests/test_database_backup_initializer.py @@ -3,7 +3,7 @@ from types import SimpleNamespace from unittest.mock import Mock from app.application.database import get_database_governance -from app.startup import database as startup_database +from app.startup.composition import database as startup_database def test_builder_uses_cached_engine_as_database_fact_source(monkeypatch) -> None: diff --git a/tests/test_database_index_migration.py b/tests/test_database_index_migration.py index 32dd9effa..cf94593e3 100644 --- a/tests/test_database_index_migration.py +++ b/tests/test_database_index_migration.py @@ -76,7 +76,7 @@ from sqlalchemy.exc import IntegrityError from app.runtime.config import settings from app.db import get_engine -from app.startup.database_initializer import init_db, update_db +from app.startup.initializers.database import init_db, update_db media_tables = {media_tables!r} legacy_identity_columns = {legacy_identity_columns!r} diff --git a/tests/test_database_migration_startup.py b/tests/test_database_migration_startup.py index 8e6f9c8bc..2ac2fb296 100644 --- a/tests/test_database_migration_startup.py +++ b/tests/test_database_migration_startup.py @@ -27,8 +27,8 @@ from sqlalchemy import ( ) from sqlalchemy.engine.url import make_url -from app.startup import database_initializer as db_init -from app.startup import database as startup_database +from app.startup.composition import database as startup_database +from app.startup.initializers import database as db_init from app.startup import lifecycle from app.runtime.health import get_application_health from app.db.models.systemconfig import SystemConfig diff --git a/tests/test_db_config_user_queries.py b/tests/test_db_config_user_queries.py index 7ef0483e8..26547f0ed 100644 --- a/tests/test_db_config_user_queries.py +++ b/tests/test_db_config_user_queries.py @@ -292,13 +292,9 @@ def test_passkey_lookup_by_credential_id_skips_inactive(db): assert asyncio.run(PassKey.async_get_by_credential_id(credential_id="cred-dead")) is None -def test_passkey_model_sync_queries_keep_no_session_plugin_abi(db, monkeypatch): - """旧插件不传 Session 时仍应获得短会话查询,而不恢复 Model 装饰器。""" +def test_passkey_model_sync_queries_keep_no_session_plugin_abi(db): + """旧插件不传 Session 时仍由统一 legacy 装饰器获得短会话查询。""" db.add(_passkey(9004, "cred-legacy")) - monkeypatch.setattr( - "app.db.models.passkey.run_legacy_sync_query", - lambda operation: operation(db.session), - ) assert [item.credential_id for item in PassKey.get_by_user_id(user_id=9004)] == [ "cred-legacy" diff --git a/tests/test_db_plugin_message_agent_queries.py b/tests/test_db_plugin_message_agent_queries.py index 9e60c9e98..3074615bf 100644 --- a/tests/test_db_plugin_message_agent_queries.py +++ b/tests/test_db_plugin_message_agent_queries.py @@ -352,13 +352,10 @@ def test_agenttask_get_for_user_enforces_ownership(db): assert AgentTask.get_for_user(db.session, task_id, user_id="bob") is None -def test_agenttask_model_queries_keep_no_session_plugin_abi(db, monkeypatch): - """旧插件省略 Session 时仍可按原关键字参数查询 Agent 任务。""" +def test_agenttask_model_queries_keep_no_session_plugin_abi(db): + """旧插件省略 Session 时仍由统一 legacy 装饰器按原参数查询。""" task_id = AgentTask.add_task(db.session, **_task("legacy", user_id="legacy-user")) - monkeypatch.setattr( - "app.db.models.agenttask.run_legacy_sync_query", - lambda operation: operation(db.session), - ) + db.session.commit() assert AgentTask.get_for_user( task_id=task_id, diff --git a/tests/test_event_runtime_components.py b/tests/test_event_runtime_components.py index 750f95f1a..eb9f67e12 100644 --- a/tests/test_event_runtime_components.py +++ b/tests/test_event_runtime_components.py @@ -12,7 +12,7 @@ from app.runtime.event.binding import ( from app.runtime.event.errors import EventErrorPolicy from app.runtime.events import Event from app.schemas.types import EventType -from app.startup.modules_initializer import get_host_event_handler_factories +from app.startup.initializers.modules import get_host_event_handler_factories class _UnmanagedHandler: diff --git a/tests/test_host_runtime_context.py b/tests/test_host_runtime_context.py index c164e74b5..f4f4dd338 100644 --- a/tests/test_host_runtime_context.py +++ b/tests/test_host_runtime_context.py @@ -15,7 +15,7 @@ from app.api.context import ( ) from app.api.dependencies.agent import get_agent_chat_persistence from app.startup import lifecycle -from app.startup.context import ( +from app.startup.composition.context import ( AgentChatRuntime, AuthenticationRuntime, HistoryRuntime, diff --git a/tests/test_legacy_plugin_resource_imports.py b/tests/test_legacy_plugin_resource_imports.py index 5c874f7ab..dc1c040d3 100644 --- a/tests/test_legacy_plugin_resource_imports.py +++ b/tests/test_legacy_plugin_resource_imports.py @@ -17,7 +17,7 @@ from app.runtime.compat.resource_imports import ( ) from app.runtime.extensions import plugin_manager as plugin_manager_module from app.runtime.extensions.plugin_manager import PluginManager -from app.startup import plugins_initializer +from app.startup.initializers import plugins as plugins_initializer _HEADED_CLOAKBROWSER_ENTRYPOINTS = ( diff --git a/tests/test_lifecycle_shutdown.py b/tests/test_lifecycle_shutdown.py index 056802c62..733cf8966 100644 --- a/tests/test_lifecycle_shutdown.py +++ b/tests/test_lifecycle_shutdown.py @@ -6,7 +6,8 @@ from unittest.mock import AsyncMock, MagicMock import pytest from fastapi import FastAPI -from app.startup import lifecycle, modules_initializer +from app.startup import lifecycle +from app.startup.initializers import modules as modules_initializer from app.adapters.network import http as http_utils from app.runtime.tasks import get_task_registry diff --git a/tests/test_managed_resources.py b/tests/test_managed_resources.py index 0ba671c09..72bb9b547 100644 --- a/tests/test_managed_resources.py +++ b/tests/test_managed_resources.py @@ -294,7 +294,7 @@ def test_startup_initializer_discovers_manifest_without_importing_resource() -> script = """ import asyncio import sys -from app.startup.managed_resources_initializer import ( +from app.startup.initializers.managed_resources import ( init_managed_resources, stop_managed_resources, ) @@ -320,7 +320,7 @@ assert "pyvirtualdisplay" not in sys.modules def test_startup_shutdown_without_init_does_not_build_registry(monkeypatch) -> None: """未执行启动装配时,关闭入口不得通过发现声明反向初始化 Runtime。""" - from app.startup import managed_resources_initializer + from app.startup.initializers import managed_resources as managed_resources_initializer build_registry = MagicMock(side_effect=AssertionError("must not discover")) monkeypatch.setattr( diff --git a/tests/test_monitor_lifecycle.py b/tests/test_monitor_lifecycle.py index 5520d172c..7e6239eb0 100644 --- a/tests/test_monitor_lifecycle.py +++ b/tests/test_monitor_lifecycle.py @@ -12,7 +12,7 @@ import pytest from app.monitor.monitor import Monitor from app.monitor.recovery import RecoveryExecutor, RecoveryState from app.foundation.singleton import SingletonClass -from app.startup.monitor_initializer import init_monitor, stop_monitor +from app.startup.initializers.monitor import init_monitor, stop_monitor def _build_monitor() -> Monitor: diff --git a/tests/test_plugin_monitor_lifecycle.py b/tests/test_plugin_monitor_lifecycle.py index 7be90756c..2cc37c60d 100644 --- a/tests/test_plugin_monitor_lifecycle.py +++ b/tests/test_plugin_monitor_lifecycle.py @@ -21,7 +21,7 @@ from app.runtime.extensions.plugin.admission import PluginMutationAdmission from app.runtime.extensions.plugin.system import reset_plugin_system from app.runtime.extensions.plugin_manager import PluginManager from app.schemas.plugin import PluginRuntimeStatus -from app.startup import plugins_initializer +from app.startup.initializers import plugins as plugins_initializer def _reset_plugin_manager() -> None: diff --git a/tests/test_resource_v3.py b/tests/test_resource_v3.py index ff2fea07d..f80b5dd41 100644 --- a/tests/test_resource_v3.py +++ b/tests/test_resource_v3.py @@ -5,7 +5,7 @@ from app.adapters.system.resource import ( ResourceHelper, configure_resource_version_provider, ) -from app.startup import modules_initializer +from app.startup.initializers import modules as modules_initializer ROOT_DIR = Path(__file__).resolve().parents[1] diff --git a/tests/test_router_aggregation.py b/tests/test_router_aggregation.py index c97dba247..ac20de226 100644 --- a/tests/test_router_aggregation.py +++ b/tests/test_router_aggregation.py @@ -56,7 +56,7 @@ def _route_contract(route: APIRoute) -> tuple[Any, ...]: def test_init_routers_directly_includes_endpoint_router_specs(monkeypatch): """启动聚合应直接 include 原始端点路由器并一次性附加完整 v1 前缀。""" from app.api.router_specs import API_V1_ROUTER_SPECS - from app.startup.routers_initializer import init_routers + from app.startup.initializers.routers import init_routers app = FastAPI() include_calls = [] @@ -90,7 +90,7 @@ def test_init_routers_directly_includes_endpoint_router_specs(monkeypatch): def test_direct_v1_routes_and_openapi_match_compatibility_router(): """最终应用的 v1 路由合同与 OpenAPI 应和兼容聚合结果完全一致。""" from app.api.apiv1 import api_router - from app.startup.routers_initializer import init_routers + from app.startup.initializers.routers import init_routers compatibility_app = FastAPI() compatibility_app.include_router(api_router, prefix=settings.API_V1_STR) @@ -123,7 +123,7 @@ def test_direct_v1_routes_and_openapi_match_compatibility_router(): @pytest.mark.anyio async def test_direct_routes_honor_application_dependency_overrides(): """直接聚合后的路由仍应由最终 FastAPI 应用解析依赖覆盖。""" - from app.startup.routers_initializer import init_routers + from app.startup.initializers.routers import init_routers app = FastAPI() init_routers(app) @@ -163,7 +163,7 @@ def test_compatibility_api_router_keeps_public_contract(): def test_init_routers_accepts_composition_root_api_prefix(): """路由初始化应使用组合根传入的 API 前缀。""" - from app.startup.routers_initializer import init_routers + from app.startup.initializers.routers import init_routers app = FastAPI() init_routers(app, "/custom/v1") diff --git a/tests/test_scheduler_cache_expiry.py b/tests/test_scheduler_cache_expiry.py index de0a51490..6eb3c1190 100644 --- a/tests/test_scheduler_cache_expiry.py +++ b/tests/test_scheduler_cache_expiry.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock from app import scheduler as scheduler_module from app.scheduler import Scheduler from app.application.configuration import SchedulerRuntimeConfig -from app.startup import scheduler_initializer +from app.startup.initializers import scheduler as scheduler_initializer class _BackgroundSchedulerStub: diff --git a/tests/test_transactional_download_failure.py b/tests/test_transactional_download_failure.py index 0b1312aac..89522b4f3 100644 --- a/tests/test_transactional_download_failure.py +++ b/tests/test_transactional_download_failure.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch import pytest -from app.startup.download_failure import TransactionalDownloadFailureRepository +from app.db.adapters.download import TransactionalDownloadFailureRepository def _session_factory(session: MagicMock): @@ -22,7 +22,7 @@ def test_record_failure_commits_explicit_unit_of_work() -> None: repository = TransactionalDownloadFailureRepository(_session_factory(session)) with patch( - "app.startup.download_failure.DownloadFailureOper", + "app.db.adapters.download.DownloadFailureOper", return_value=oper, ): result = repository.record_failure("fp", "now", "next", title="片名") @@ -40,7 +40,7 @@ def test_record_failure_rolls_back_explicit_unit_of_work() -> None: repository = TransactionalDownloadFailureRepository(_session_factory(session)) with patch( - "app.startup.download_failure.DownloadFailureOper", + "app.db.adapters.download.DownloadFailureOper", return_value=oper, ), pytest.raises(ValueError, match="duplicate"): repository.record_failure("fp", "now", "next") diff --git a/tests/test_transfer_worker_lifecycle.py b/tests/test_transfer_worker_lifecycle.py index fe678fcc1..193d14cc1 100644 --- a/tests/test_transfer_worker_lifecycle.py +++ b/tests/test_transfer_worker_lifecycle.py @@ -15,7 +15,7 @@ from app.foundation.singleton import Singleton from app.runtime.config import global_vars from app.application.transfer import TransferQueue, TransferTask from app.schemas.file import FileItem -from app.startup import transfer_initializer +from app.startup.initializers import transfer as transfer_initializer def _build_chain(*, transfer_threads: int = 0) -> TransferChain: diff --git a/tests/test_type_gate.py b/tests/test_type_gate.py index 20ae1b9de..b388e8a4a 100644 --- a/tests/test_type_gate.py +++ b/tests/test_type_gate.py @@ -28,10 +28,10 @@ def test_mypy_gate_has_explicit_strict_scope_without_global_ignore() -> None: assert "app/runtime/event/errors.py" in governed_files assert "app/application/scheduling.py" in governed_files assert "scripts/architecture/async_blocking.py" in governed_files - assert "app/startup/context.py" in governed_files - assert "app/startup/configuration.py" in governed_files - assert "app/startup/download_failure.py" in governed_files - assert "app/startup/workflow.py" in governed_files + assert "app/startup/composition/context.py" in governed_files + assert "app/startup/composition/configuration.py" in governed_files + assert "app/db/adapters/download.py" in governed_files + assert "app/db/adapters/workflow.py" in governed_files assert "app/application/workflow.py" in governed_files assert "app/api/context.py" in governed_files assert "app/db/base.py" in governed_files