mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-31 12:24:39 +08:00
refactor(startup): centralize runtime composition
This commit is contained in:
@@ -13,7 +13,6 @@ from app.application.agenttask import (
|
||||
configure_agent_task_execution,
|
||||
reset_agent_task_execution,
|
||||
)
|
||||
from app.application.history import DownloadHistoryRepository, TransferHistoryRepository
|
||||
from app.application.messaging.chat import (
|
||||
AgentChatPersistenceService,
|
||||
AgentChatService,
|
||||
@@ -22,12 +21,6 @@ from app.application.messaging.chat import (
|
||||
reset_agent_chat_persistence,
|
||||
reset_agent_chat_service,
|
||||
)
|
||||
from app.application.site.contract import SiteRepository
|
||||
from app.application.subscription.contract import (
|
||||
SubscriptionHistoryQueryPort,
|
||||
SubscriptionRepository,
|
||||
)
|
||||
from app.application.transfer.execution import TransferExecutionRepository
|
||||
from app.db.adapters.agent import (
|
||||
SessionAgentTaskRepository,
|
||||
TransactionalAgentTaskRepository,
|
||||
@@ -35,16 +28,15 @@ from app.db.adapters.agent import (
|
||||
)
|
||||
from app.db.oper.agentchat import AgentChatOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.db.session import SessionFactory, async_session_scope, get_async_db
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork
|
||||
from app.db.session import SessionFactory, async_session_scope
|
||||
from app.startup.composition.context import (
|
||||
AgentChatRepositoryFactory,
|
||||
AgentChatRuntime,
|
||||
)
|
||||
from app.startup.composition.database import (
|
||||
DatabaseRuntime,
|
||||
build_transactional_user_repository,
|
||||
)
|
||||
from app.startup.composition.runtime import RuntimeDependencies
|
||||
from app.startup.composition.subscription import (
|
||||
async_rule_group_mutation_scope,
|
||||
delete_subscribe_scope,
|
||||
@@ -60,7 +52,8 @@ class AgentComposition:
|
||||
"""保存一个 lifespan 内共享的 Agent 数据、会话与任务对象。"""
|
||||
|
||||
data: AgentDataContext
|
||||
chat: AgentChatRuntime
|
||||
chat_repository: AgentChatRepositoryFactory
|
||||
persistence: AgentChatPersistenceService
|
||||
tasks: TransactionalAgentTaskRepository
|
||||
execution: AgentTaskExecutionService
|
||||
|
||||
@@ -69,12 +62,7 @@ def compose_agent(
|
||||
*,
|
||||
runtime: DatabaseRuntime,
|
||||
system_config: SystemConfigOper,
|
||||
site: SiteRepository,
|
||||
subscription: SubscriptionRepository,
|
||||
subscription_history: SubscriptionHistoryQueryPort,
|
||||
transfer_history: TransferHistoryRepository,
|
||||
transfer_execution: TransferExecutionRepository,
|
||||
download_history: DownloadHistoryRepository,
|
||||
dependencies: RuntimeDependencies,
|
||||
) -> AgentComposition:
|
||||
"""在数据库 worker 启动后构造共享的 Agent 数据与任务服务。"""
|
||||
persistence = AgentChatPersistenceService(
|
||||
@@ -91,28 +79,24 @@ def compose_agent(
|
||||
chat_persistence=persistence,
|
||||
tasks=task_repository,
|
||||
users=build_transactional_user_repository(),
|
||||
sites=site,
|
||||
subscriptions=subscription,
|
||||
sites=dependencies.site,
|
||||
subscriptions=dependencies.subscription,
|
||||
subscription_mutation_scope=subscription_mutation_scope,
|
||||
subscription_delete_scope=delete_subscribe_scope,
|
||||
async_rule_group_mutation_scope=partial(
|
||||
async_rule_group_mutation_scope,
|
||||
system_config.publish_many,
|
||||
),
|
||||
subscription_history=subscription_history,
|
||||
transfer_history=transfer_history,
|
||||
transfer_execution=transfer_execution,
|
||||
download_history=download_history,
|
||||
subscription_history=dependencies.subscription_history,
|
||||
transfer_history=dependencies.transfer_history,
|
||||
transfer_execution=dependencies.transfer_execution,
|
||||
download_history=dependencies.download_history,
|
||||
plugin_data=TransactionalPluginDataRepository(async_session_scope),
|
||||
)
|
||||
return AgentComposition(
|
||||
data=data,
|
||||
chat=AgentChatRuntime(
|
||||
async_session=get_async_db,
|
||||
repository=chat_repository,
|
||||
transaction=SqlAlchemyAsyncUnitOfWork,
|
||||
persistence=persistence,
|
||||
),
|
||||
chat_repository=chat_repository,
|
||||
persistence=persistence,
|
||||
tasks=task_repository,
|
||||
execution=AgentTaskExecutionService(
|
||||
repository=SessionAgentTaskRepository,
|
||||
|
||||
@@ -12,17 +12,11 @@ from app.application.image import (
|
||||
configure_wallpaper_providers,
|
||||
reset_wallpaper_providers,
|
||||
)
|
||||
from app.application.messaging.message import MessageHelper, MessageQueueManager
|
||||
from app.application.transfer.execution import TransferExecutionRepository
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.db.adapters.chain import TransactionalChainDurableEventWriter
|
||||
from app.db.adapters.download import TransactionalDownloadFailureRepository
|
||||
from app.db.adapters.history.download import TransactionalDownloadHistoryRepository
|
||||
from app.db.adapters.history.transfer import TransactionalTransferHistoryRepository
|
||||
from app.db.adapters.mediaserver import TransactionalMediaServerRepository
|
||||
from app.db.adapters.site import TransactionalSiteRepository
|
||||
from app.db.adapters.subscription import TransactionalSubscriptionRepository
|
||||
from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
@@ -34,6 +28,7 @@ from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.runtime.stop import runtime_stop_state
|
||||
from app.startup.composition.database import build_transactional_user_repository
|
||||
from app.startup.composition.runtime import RuntimeDependencies
|
||||
from app.startup.composition.subscription import (
|
||||
delete_subscribe_scope,
|
||||
rule_group_mutation_scope,
|
||||
@@ -56,14 +51,8 @@ def execute_legacy_transfer_command(**kwargs: Any) -> Any:
|
||||
|
||||
def build_chain_runtime_context(
|
||||
*,
|
||||
message_helper: MessageHelper,
|
||||
message_queue: MessageQueueManager,
|
||||
dependencies: RuntimeDependencies,
|
||||
system_config: SystemConfigOper,
|
||||
site: TransactionalSiteRepository,
|
||||
subscription: TransactionalSubscriptionRepository,
|
||||
download_history: TransactionalDownloadHistoryRepository,
|
||||
transfer_history: TransactionalTransferHistoryRepository,
|
||||
transfer_execution: TransferExecutionRepository,
|
||||
configuration: Callable[[], ChainRuntimeConfig],
|
||||
) -> ChainRuntimeContext:
|
||||
"""创建 Chain 无参兼容入口共享的运行时对象与数据端口。"""
|
||||
@@ -72,13 +61,13 @@ def build_chain_runtime_context(
|
||||
plugin_manager=PluginManager(),
|
||||
event_manager=_event_manager_factory(),
|
||||
message_oper=MessageOper(),
|
||||
message_helper=message_helper,
|
||||
message_helper=dependencies.message_helper,
|
||||
file_cache=FileCache(),
|
||||
async_file_cache=AsyncFileCache(),
|
||||
message_queue=message_queue,
|
||||
message_queue=dependencies.message_queue,
|
||||
module_dispatcher_factory=ModuleInvocationDispatcher,
|
||||
site_repository=site,
|
||||
subscription_repository=subscription,
|
||||
site_repository=dependencies.site,
|
||||
subscription_repository=dependencies.subscription,
|
||||
subscription_mutation_scope=subscription_mutation_scope,
|
||||
sync_subscription_mutation_scope=sync_subscription_mutation_scope,
|
||||
subscription_delete_scope=delete_subscribe_scope,
|
||||
@@ -92,10 +81,10 @@ def build_chain_runtime_context(
|
||||
site_reference_mutation_scope,
|
||||
system_config.publish_many,
|
||||
),
|
||||
download_history_repository=download_history,
|
||||
transfer_history_repository=transfer_history,
|
||||
download_history_repository=dependencies.download_history,
|
||||
transfer_history_repository=dependencies.transfer_history,
|
||||
transfer_admission_repository=TransactionalTransferAdmissionRepository(SessionFactory),
|
||||
transfer_execution_repository=transfer_execution,
|
||||
transfer_execution_repository=dependencies.transfer_execution,
|
||||
media_server_repository=TransactionalMediaServerRepository(SessionFactory),
|
||||
download_failure_repository=TransactionalDownloadFailureRepository(SessionFactory),
|
||||
user_repository=build_transactional_user_repository(),
|
||||
@@ -118,27 +107,15 @@ def configure_wallpaper_services() -> None:
|
||||
|
||||
def configure_chain_runtime_context(
|
||||
*,
|
||||
message_helper: MessageHelper,
|
||||
message_queue: MessageQueueManager,
|
||||
dependencies: RuntimeDependencies,
|
||||
system_config: SystemConfigOper,
|
||||
site: TransactionalSiteRepository,
|
||||
subscription: TransactionalSubscriptionRepository,
|
||||
download_history: TransactionalDownloadHistoryRepository,
|
||||
transfer_history: TransactionalTransferHistoryRepository,
|
||||
transfer_execution: TransferExecutionRepository,
|
||||
configuration: Callable[[], ChainRuntimeConfig],
|
||||
) -> None:
|
||||
"""登记按需构造的 Chain 上下文,保持无参 Chain 的插件兼容合同。"""
|
||||
configure_chain_runtime_context_provider(
|
||||
lambda: build_chain_runtime_context(
|
||||
message_helper=message_helper,
|
||||
message_queue=message_queue,
|
||||
dependencies=dependencies,
|
||||
system_config=system_config,
|
||||
site=site,
|
||||
subscription=subscription,
|
||||
download_history=download_history,
|
||||
transfer_history=transfer_history,
|
||||
transfer_execution=transfer_execution,
|
||||
configuration=configuration,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator, Callable, Generator
|
||||
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
from app.application.agent import AgentDataContext
|
||||
@@ -172,6 +172,7 @@ class AuthenticationRuntime:
|
||||
"""认证、用户管理与 PassKey API 的显式数据工厂。"""
|
||||
|
||||
user_repository: RepositoryFactory
|
||||
passkey_repository: RepositoryFactory
|
||||
standalone_user: StandaloneRepositoryFactory
|
||||
system_config: StandaloneRepositoryFactory
|
||||
passkey: StandaloneRepositoryFactory
|
||||
@@ -225,15 +226,9 @@ class SubscriptionRuntime:
|
||||
outbox: AsyncOutboxFactory
|
||||
dispatch_store: AsyncOutboxDispatchStore
|
||||
batch_writer: SubscriptionBatchWriterFactory
|
||||
rule_group_mutation_scope: Callable[
|
||||
[], AbstractContextManager[SyncRuleGroupMutationService]
|
||||
]
|
||||
async_rule_group_mutation_scope: Callable[
|
||||
[], AbstractAsyncContextManager[AsyncRuleGroupMutationService]
|
||||
]
|
||||
site_reference_mutation_scope: Callable[
|
||||
[], AbstractContextManager[SyncSiteReferenceMutationService]
|
||||
]
|
||||
rule_group_mutation_scope: Callable[[], AbstractContextManager[SyncRuleGroupMutationService]]
|
||||
async_rule_group_mutation_scope: Callable[[], AbstractAsyncContextManager[AsyncRuleGroupMutationService]]
|
||||
site_reference_mutation_scope: Callable[[], AbstractContextManager[SyncSiteReferenceMutationService]]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -251,4 +246,4 @@ class HostRuntime:
|
||||
workflow: WorkflowRuntime
|
||||
configuration: RuntimeConfiguration
|
||||
settings: RuntimeSettingsService
|
||||
tasks: TaskRegistry = field(default_factory=TaskRegistry)
|
||||
tasks: TaskRegistry
|
||||
|
||||
@@ -9,11 +9,6 @@ from app.adapters.system.backup.database import (
|
||||
SQLiteBackupBackend,
|
||||
)
|
||||
from app.adapters.system.backup.files import BackupFiles
|
||||
from app.api.data import (
|
||||
ApiDataPorts,
|
||||
configure_api_data_runtime,
|
||||
reset_api_data_runtime,
|
||||
)
|
||||
from app.application.backup import BackupPolicy, DatabaseBackupService
|
||||
from app.application.database import (
|
||||
DatabaseGovernance,
|
||||
@@ -39,35 +34,21 @@ from app.application.workflow import (
|
||||
configure_workflow_query,
|
||||
reset_workflow_query,
|
||||
)
|
||||
from app.db.adapters.history.download import SessionDownloadHistoryRepository
|
||||
from app.db.adapters.pluginidentity import TransactionalPluginIdentityStore
|
||||
from app.db.adapters.plugininstallation import TransactionalPluginInstallationStore
|
||||
from app.db.adapters.query import SqlAlchemyDataQueryAdapter
|
||||
from app.db.adapters.site import SessionSiteRepository
|
||||
from app.db.adapters.subscription import (
|
||||
SessionSubscriptionHistoryRepository,
|
||||
SessionSubscriptionRepository,
|
||||
)
|
||||
from app.db.adapters.transaction import TransactionalWriteRunner
|
||||
from app.db.adapters.user import SqlAlchemyUserRepository, TransactionalUserRepository
|
||||
from app.db.adapters.user import TransactionalUserRepository
|
||||
from app.db.adapters.workflow import TransactionalWorkflowQueryRepository
|
||||
from app.db.engine import get_engine
|
||||
from app.db.health import probe_database
|
||||
from app.db.maintenance import DatabaseCleanupRepository
|
||||
from app.db.oper.mediaserver import MediaServerOper
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.db.oper.passkey import PassKeyOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.db.session import (
|
||||
SessionFactory,
|
||||
async_session_scope,
|
||||
get_async_db,
|
||||
get_db,
|
||||
)
|
||||
from app.db.uow import (
|
||||
SqlAlchemyAsyncUnitOfWork,
|
||||
SqlAlchemyUnitOfWork,
|
||||
configure_transaction_runners,
|
||||
reset_transaction_runners,
|
||||
)
|
||||
@@ -87,7 +68,6 @@ class DatabaseRuntime:
|
||||
class DatabaseComposition:
|
||||
"""保存初始化器后续装配所需的数据库查询能力。"""
|
||||
|
||||
api_data: ApiDataPorts
|
||||
data_query: DataQueryService
|
||||
plugin_persistence: PluginPersistenceService
|
||||
workflow_query: WorkflowQueryService
|
||||
@@ -148,7 +128,7 @@ def compose_database_services(
|
||||
runtime: DatabaseRuntime,
|
||||
system_config: SystemConfigOper,
|
||||
) -> DatabaseComposition:
|
||||
"""组合查询、插件持久化与旧 API 数据 Facade 所需实现。"""
|
||||
"""组合查询、插件持久化与工作流查询所需实现。"""
|
||||
data_query_adapter = SqlAlchemyDataQueryAdapter(SessionFactory)
|
||||
data_query = DataQueryService(
|
||||
subscriptions=data_query_adapter,
|
||||
@@ -170,30 +150,6 @@ def compose_database_services(
|
||||
)
|
||||
)
|
||||
return DatabaseComposition(
|
||||
api_data=ApiDataPorts(
|
||||
sync_session=get_db,
|
||||
async_session=get_async_db,
|
||||
repositories={
|
||||
"download_history": SessionDownloadHistoryRepository,
|
||||
"media_server": MediaServerOper,
|
||||
"message": MessageOper,
|
||||
"passkey": PassKeyOper,
|
||||
"site": SessionSiteRepository,
|
||||
"subscribe": SessionSubscriptionRepository,
|
||||
"subscribe_history": SessionSubscriptionHistoryRepository,
|
||||
"user": SqlAlchemyUserRepository,
|
||||
"workflow": WorkflowOper,
|
||||
},
|
||||
standalone={
|
||||
"passkey": PassKeyOper,
|
||||
"system_config": SystemConfigOper,
|
||||
"user": build_transactional_user_repository,
|
||||
},
|
||||
unit_of_work={
|
||||
"async": SqlAlchemyAsyncUnitOfWork,
|
||||
"sync": SqlAlchemyUnitOfWork,
|
||||
},
|
||||
),
|
||||
data_query=data_query,
|
||||
plugin_persistence=plugin_persistence,
|
||||
workflow_query=workflow_query,
|
||||
@@ -201,17 +157,15 @@ def compose_database_services(
|
||||
|
||||
|
||||
def publish_database_services(composition: DatabaseComposition) -> None:
|
||||
"""一次性发布当前 lifespan 的查询、插件持久化和旧 API 数据端口。"""
|
||||
"""一次性发布当前 lifespan 的查询与插件持久化服务。"""
|
||||
configure_data_query_service(composition.data_query)
|
||||
configure_plugin_persistence(composition.plugin_persistence)
|
||||
configure_workflow_query(composition.workflow_query)
|
||||
configure_api_data_runtime(composition.api_data)
|
||||
|
||||
|
||||
def reset_database_services() -> None:
|
||||
"""撤销当前 lifespan 发布的全部数据库派生服务。"""
|
||||
reset_transaction_runners()
|
||||
reset_api_data_runtime()
|
||||
reset_workflow_query()
|
||||
reset_plugin_persistence()
|
||||
reset_data_query_service()
|
||||
@@ -250,7 +204,7 @@ def configure_database() -> None:
|
||||
def read_backup_policy() -> BackupPolicy:
|
||||
"""读取一次可热更新的数据库备份目录与保留策略。"""
|
||||
return BackupPolicy(
|
||||
root=get_runtime_setting('DATABASE_BACKUP_PATH'),
|
||||
retention_days=get_runtime_setting('DB_BACKUP_RETENTION_DAYS'),
|
||||
max_count=get_runtime_setting('DB_BACKUP_MAX_COUNT'),
|
||||
root=get_runtime_setting("DATABASE_BACKUP_PATH"),
|
||||
retention_days=get_runtime_setting("DB_BACKUP_RETENTION_DAYS"),
|
||||
max_count=get_runtime_setting("DB_BACKUP_MAX_COUNT"),
|
||||
)
|
||||
|
||||
280
app/startup/composition/runtime.py
Normal file
280
app/startup/composition/runtime.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""宿主运行时、领域投影与共享依赖的唯一组合根。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Callable, cast
|
||||
|
||||
from app.api.data import (
|
||||
ApiDataPorts,
|
||||
configure_api_data_runtime,
|
||||
reset_api_data_runtime,
|
||||
)
|
||||
from app.application.history import (
|
||||
DownloadHistoryRepository,
|
||||
TransferHistoryRepository,
|
||||
configure_transfer_history_repository,
|
||||
reset_transfer_history_repository,
|
||||
)
|
||||
from app.application.site.contract import SiteRepository
|
||||
from app.application.site.health import (
|
||||
SiteHealthService,
|
||||
configure_site_health_service,
|
||||
reset_site_health_service,
|
||||
)
|
||||
from app.application.site.query import (
|
||||
SiteQueryService,
|
||||
configure_site_query_service,
|
||||
reset_site_query_service,
|
||||
)
|
||||
from app.application.subscription.contract import (
|
||||
SubscriptionHistoryQueryPort,
|
||||
SubscriptionRepository,
|
||||
)
|
||||
from app.application.transfer.execution import TransferExecutionRepository
|
||||
from app.runtime.tasks import TaskRegistry
|
||||
from app.startup.composition.context import (
|
||||
AgentChatRuntime,
|
||||
AuthenticationRuntime,
|
||||
HistoryRuntime,
|
||||
HostRuntime,
|
||||
MessagingRuntime,
|
||||
PersistenceRuntime,
|
||||
RepositoryFactory,
|
||||
SiteRuntime,
|
||||
SubscriptionRuntime,
|
||||
WorkflowRuntime,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.application.messaging.message import MessageHelper, MessageQueueManager
|
||||
from app.startup.composition.agent import AgentComposition
|
||||
from app.startup.composition.configuration import ConfigurationComposition
|
||||
from app.startup.composition.database import DatabaseComposition
|
||||
from app.startup.composition.security import SecurityComposition
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RuntimeDependencies:
|
||||
"""保存 HostRuntime、Agent 与 Chain 复用的同一批有状态依赖。"""
|
||||
|
||||
download_history: DownloadHistoryRepository
|
||||
transfer_history: TransferHistoryRepository
|
||||
site: SiteRepository
|
||||
subscription: SubscriptionRepository
|
||||
subscription_history: SubscriptionHistoryQueryPort
|
||||
transfer_execution: TransferExecutionRepository
|
||||
message_helper: MessageHelper
|
||||
message_queue: MessageQueueManager
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RuntimeInputs:
|
||||
"""描述构造 HostRuntime 所需的已完成组合输入。"""
|
||||
|
||||
configuration: ConfigurationComposition
|
||||
database: DatabaseComposition
|
||||
agent: AgentComposition
|
||||
authentication: SecurityComposition
|
||||
dependencies: RuntimeDependencies
|
||||
tasks: TaskRegistry
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RuntimeComposition:
|
||||
"""保存宿主运行时及其兼容与应用服务投影。"""
|
||||
|
||||
runtime: HostRuntime
|
||||
dependencies: RuntimeDependencies
|
||||
api_data: ApiDataPorts
|
||||
site_query: SiteQueryService
|
||||
site_health: SiteHealthService
|
||||
|
||||
|
||||
def compose_runtime_dependencies() -> RuntimeDependencies:
|
||||
"""构造一个 lifespan 内由 Agent、Chain 与 HostRuntime 共享的依赖。"""
|
||||
# 具体 DB adapter 延迟到执行期加载,导入组合 owner 不得启动或牵入数据库 worker。
|
||||
from app.application.messaging.message import MessageHelper, MessageQueueManager
|
||||
from app.db.adapters.history.download import TransactionalDownloadHistoryRepository
|
||||
from app.db.adapters.history.transfer import TransactionalTransferHistoryRepository
|
||||
from app.db.adapters.site import TransactionalSiteRepository
|
||||
from app.db.adapters.subscription import (
|
||||
TransactionalSubscriptionHistoryRepository,
|
||||
TransactionalSubscriptionRepository,
|
||||
)
|
||||
from app.db.adapters.transfer.execution import (
|
||||
TransactionalTransferExecutionRepository,
|
||||
)
|
||||
from app.db.session import SessionFactory, async_session_scope
|
||||
|
||||
message_helper_factory: Callable[[], MessageHelper] = MessageHelper
|
||||
return RuntimeDependencies(
|
||||
download_history=TransactionalDownloadHistoryRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
),
|
||||
transfer_history=TransactionalTransferHistoryRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
),
|
||||
site=TransactionalSiteRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
),
|
||||
subscription=TransactionalSubscriptionRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
),
|
||||
subscription_history=TransactionalSubscriptionHistoryRepository(
|
||||
async_session=async_session_scope,
|
||||
),
|
||||
transfer_execution=TransactionalTransferExecutionRepository(SessionFactory),
|
||||
message_helper=message_helper_factory(),
|
||||
message_queue=MessageQueueManager(auto_start=False),
|
||||
)
|
||||
|
||||
|
||||
def compose_runtime(inputs: RuntimeInputs) -> RuntimeComposition:
|
||||
"""从同一批组合输入构造 HostRuntime 及旧 API 数据投影。"""
|
||||
# 领域 Runtime 的具体持久化投影只在组合阶段解析,保持模块冷导入合同。
|
||||
from app.db.adapters.history.download import SessionDownloadHistoryRepository
|
||||
from app.db.adapters.history.transfer import SessionTransferHistoryRepository
|
||||
from app.db.adapters.outbox import (
|
||||
SqlAlchemyAsyncOutboxDispatchStore,
|
||||
SqlAlchemyAsyncOutboxStager,
|
||||
)
|
||||
from app.db.adapters.site import SessionSiteRepository
|
||||
from app.db.adapters.subscription import (
|
||||
SessionSubscriptionHistoryRepository,
|
||||
SessionSubscriptionRepository,
|
||||
)
|
||||
from app.db.oper.mediaserver import MediaServerOper
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.db.session import async_session_scope, get_async_db, get_db
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
from app.startup.composition.subscription import (
|
||||
async_rule_group_mutation_scope,
|
||||
build_subscription_batch_writer,
|
||||
rule_group_mutation_scope,
|
||||
site_reference_mutation_scope,
|
||||
)
|
||||
|
||||
dependencies = inputs.dependencies
|
||||
configuration = inputs.configuration
|
||||
authentication = AuthenticationRuntime(
|
||||
user_repository=inputs.authentication.user_repository,
|
||||
passkey_repository=inputs.authentication.passkey_repository,
|
||||
standalone_user=inputs.authentication.standalone_user,
|
||||
system_config=inputs.authentication.system_config,
|
||||
passkey=inputs.authentication.passkey,
|
||||
)
|
||||
host_runtime = HostRuntime(
|
||||
agent_chat=AgentChatRuntime(
|
||||
async_session=get_async_db,
|
||||
repository=inputs.agent.chat_repository,
|
||||
transaction=SqlAlchemyAsyncUnitOfWork,
|
||||
persistence=inputs.agent.persistence,
|
||||
),
|
||||
agent=inputs.agent.data,
|
||||
persistence=PersistenceRuntime(
|
||||
sync_session=get_db,
|
||||
async_session=get_async_db,
|
||||
sync_transaction=SqlAlchemyUnitOfWork,
|
||||
async_transaction=SqlAlchemyAsyncUnitOfWork,
|
||||
),
|
||||
authentication=authentication,
|
||||
messaging=MessagingRuntime(
|
||||
repository=cast(RepositoryFactory, MessageOper),
|
||||
helper=dependencies.message_helper,
|
||||
queue=dependencies.message_queue,
|
||||
),
|
||||
history=HistoryRuntime(
|
||||
download_repository=SessionDownloadHistoryRepository,
|
||||
transfer_repository=dependencies.transfer_history,
|
||||
transfer_mutation_repository=SessionTransferHistoryRepository,
|
||||
media_server_repository=cast(RepositoryFactory, MediaServerOper),
|
||||
transfer_execution_repository=dependencies.transfer_execution,
|
||||
),
|
||||
site=SiteRuntime(
|
||||
repository=SessionSiteRepository,
|
||||
standalone=dependencies.site,
|
||||
),
|
||||
subscription=SubscriptionRuntime(
|
||||
async_session=get_async_db,
|
||||
repository=SessionSubscriptionRepository,
|
||||
history_repository=SessionSubscriptionHistoryRepository,
|
||||
transaction=SqlAlchemyAsyncUnitOfWork,
|
||||
outbox=SqlAlchemyAsyncOutboxStager,
|
||||
dispatch_store=SqlAlchemyAsyncOutboxDispatchStore(async_session_scope),
|
||||
batch_writer=build_subscription_batch_writer,
|
||||
rule_group_mutation_scope=partial(
|
||||
rule_group_mutation_scope,
|
||||
configuration.system_config.publish_many,
|
||||
),
|
||||
async_rule_group_mutation_scope=partial(
|
||||
async_rule_group_mutation_scope,
|
||||
configuration.system_config.publish_many,
|
||||
),
|
||||
site_reference_mutation_scope=partial(
|
||||
site_reference_mutation_scope,
|
||||
configuration.system_config.publish_many,
|
||||
),
|
||||
),
|
||||
workflow=WorkflowRuntime(
|
||||
query=inputs.database.workflow_query,
|
||||
repository=cast(RepositoryFactory, WorkflowOper),
|
||||
system_config=lambda: configuration.system_service,
|
||||
),
|
||||
configuration=configuration.runtime,
|
||||
settings=configuration.settings,
|
||||
tasks=inputs.tasks,
|
||||
)
|
||||
api_data = ApiDataPorts(
|
||||
sync_session=host_runtime.persistence.sync_session,
|
||||
async_session=host_runtime.persistence.async_session,
|
||||
repositories={
|
||||
"download_history": host_runtime.history.download_repository,
|
||||
"media_server": host_runtime.history.media_server_repository,
|
||||
"message": host_runtime.messaging.repository,
|
||||
"passkey": host_runtime.authentication.passkey_repository,
|
||||
"site": host_runtime.site.repository,
|
||||
"subscribe": host_runtime.subscription.repository,
|
||||
"subscribe_history": host_runtime.subscription.history_repository,
|
||||
"user": host_runtime.authentication.user_repository,
|
||||
"workflow": host_runtime.workflow.repository,
|
||||
},
|
||||
standalone={
|
||||
"passkey": host_runtime.authentication.passkey,
|
||||
"system_config": host_runtime.authentication.system_config,
|
||||
"user": host_runtime.authentication.standalone_user,
|
||||
},
|
||||
unit_of_work={
|
||||
"async": host_runtime.persistence.async_transaction,
|
||||
"sync": host_runtime.persistence.sync_transaction,
|
||||
},
|
||||
)
|
||||
return RuntimeComposition(
|
||||
runtime=host_runtime,
|
||||
dependencies=dependencies,
|
||||
api_data=api_data,
|
||||
site_query=SiteQueryService(repository=dependencies.site),
|
||||
site_health=SiteHealthService(repository=dependencies.site),
|
||||
)
|
||||
|
||||
|
||||
def publish_runtime(composition: RuntimeComposition) -> None:
|
||||
"""发布 HostRuntime 派生的兼容端口与共享领域服务。"""
|
||||
configure_api_data_runtime(composition.api_data)
|
||||
configure_transfer_history_repository(lambda: composition.dependencies.transfer_history)
|
||||
configure_site_query_service(composition.site_query)
|
||||
configure_site_health_service(composition.site_health)
|
||||
|
||||
|
||||
def reset_runtime() -> None:
|
||||
"""撤销当前 lifespan 由运行时组合根发布的全部投影。"""
|
||||
reset_site_health_service()
|
||||
reset_site_query_service()
|
||||
reset_transfer_history_repository()
|
||||
reset_api_data_runtime()
|
||||
@@ -1,5 +1,8 @@
|
||||
"""认证、用户与 PassKey 服务的启动组合。"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from app.adapters.web.security.access import (
|
||||
reset_superuser_token_payload_provider,
|
||||
set_superuser_token_payload_provider,
|
||||
@@ -24,18 +27,30 @@ from app.db.adapters.user import SqlAlchemyUserRepository
|
||||
from app.db.oper.passkey import PassKeyOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.runtime.cache import TTLCache
|
||||
from app.startup.composition.context import AuthenticationRuntime
|
||||
from app.startup.composition.context import (
|
||||
RepositoryFactory,
|
||||
StandaloneRepositoryFactory,
|
||||
)
|
||||
from app.startup.composition.database import build_transactional_user_repository
|
||||
|
||||
|
||||
def configure_security_services() -> AuthenticationRuntime:
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SecurityComposition:
|
||||
"""保存认证领域运行时投影所需的持久化工厂。"""
|
||||
|
||||
user_repository: RepositoryFactory
|
||||
passkey_repository: RepositoryFactory
|
||||
standalone_user: StandaloneRepositoryFactory
|
||||
system_config: StandaloneRepositoryFactory
|
||||
passkey: StandaloneRepositoryFactory
|
||||
|
||||
|
||||
def configure_security_services() -> SecurityComposition:
|
||||
"""构造并登记认证、用户查询和 PassKey 服务。"""
|
||||
configure_user_lookups(
|
||||
by_id=lambda user_id: build_transactional_user_repository().get_by_id(user_id),
|
||||
by_name=lambda username: build_transactional_user_repository().get_by_name(username),
|
||||
by_channel=lambda **bindings: build_transactional_user_repository().find_name_by_bindings(
|
||||
bindings
|
||||
),
|
||||
by_channel=lambda **bindings: build_transactional_user_repository().find_name_by_bindings(bindings),
|
||||
)
|
||||
configure_auth_service(
|
||||
AuthService(
|
||||
@@ -52,8 +67,9 @@ def configure_security_services() -> AuthenticationRuntime:
|
||||
)
|
||||
)
|
||||
configure_passkey_service(PasskeyService(repository=PassKeyOper()))
|
||||
return AuthenticationRuntime(
|
||||
return SecurityComposition(
|
||||
user_repository=SqlAlchemyUserRepository,
|
||||
passkey_repository=cast(RepositoryFactory, PassKeyOper),
|
||||
standalone_user=build_transactional_user_repository,
|
||||
system_config=SystemConfigOper,
|
||||
passkey=PassKeyOper,
|
||||
|
||||
@@ -2,7 +2,6 @@ import asyncio
|
||||
import inspect
|
||||
import sys
|
||||
from collections.abc import Awaitable
|
||||
from functools import partial
|
||||
from typing import Callable, cast
|
||||
|
||||
from app.adapters.cache.redis import AsyncRedisHelper, RedisHelper
|
||||
@@ -28,7 +27,6 @@ from app.application.configuration import (
|
||||
configure_transfer_retry_config,
|
||||
get_configured_system_config,
|
||||
)
|
||||
from app.application.history import configure_transfer_history_repository
|
||||
from app.application.messaging.agent import (
|
||||
dispatch_web_agent_message_event,
|
||||
shutdown_web_agent_background_tasks,
|
||||
@@ -39,55 +37,20 @@ from app.application.messaging.chat import (
|
||||
)
|
||||
from app.application.messaging.message import (
|
||||
MessageHelper,
|
||||
MessageQueueManager,
|
||||
stop_message,
|
||||
)
|
||||
from app.application.module import configure_module_runtime
|
||||
from app.application.outbox import configure_outbox_dispatcher
|
||||
from app.application.security.url import close_image_proxy_block_log_coalescer
|
||||
from app.application.service import configure_service_directory
|
||||
from app.application.site.health import SiteHealthService, configure_site_health_service
|
||||
from app.application.site.query import SiteQueryService, configure_site_query_service
|
||||
from app.application.workflow import configure_workflow_execution
|
||||
from app.command import CommandChain
|
||||
from app.db.adapters.history.download import (
|
||||
SessionDownloadHistoryRepository,
|
||||
TransactionalDownloadHistoryRepository,
|
||||
)
|
||||
from app.db.adapters.history.transfer import (
|
||||
SessionTransferHistoryRepository,
|
||||
TransactionalTransferHistoryRepository,
|
||||
)
|
||||
from app.db.adapters.outbox import (
|
||||
SqlAlchemyAsyncOutboxDispatchStore,
|
||||
SqlAlchemyAsyncOutboxStager,
|
||||
)
|
||||
from app.db.adapters.site import SessionSiteRepository, TransactionalSiteRepository
|
||||
from app.db.adapters.subscription import (
|
||||
SessionSubscriptionHistoryRepository,
|
||||
SessionSubscriptionRepository,
|
||||
TransactionalSubscriptionHistoryRepository,
|
||||
TransactionalSubscriptionRepository,
|
||||
)
|
||||
from app.db.adapters.transfer.execution import (
|
||||
TransactionalTransferExecutionRepository,
|
||||
)
|
||||
from app.db.adapters.workflow import (
|
||||
TransactionalWorkflowExecutionService,
|
||||
)
|
||||
from app.db.oper.mediaserver import MediaServerOper
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.db.session import (
|
||||
SessionFactory,
|
||||
async_session_scope,
|
||||
close_database,
|
||||
get_async_db,
|
||||
get_db,
|
||||
)
|
||||
from app.db.uow import (
|
||||
SqlAlchemyAsyncUnitOfWork,
|
||||
SqlAlchemyUnitOfWork,
|
||||
)
|
||||
from app.runtime.config import settings as legacy_settings
|
||||
from app.runtime.events import EventHandlerBinding, EventManager
|
||||
@@ -119,15 +82,7 @@ from app.startup.composition.configuration import (
|
||||
publish_configuration,
|
||||
reset_configuration,
|
||||
)
|
||||
from app.startup.composition.context import (
|
||||
HistoryRuntime,
|
||||
HostRuntime,
|
||||
MessagingRuntime,
|
||||
PersistenceRuntime,
|
||||
SiteRuntime,
|
||||
SubscriptionRuntime,
|
||||
WorkflowRuntime,
|
||||
)
|
||||
from app.startup.composition.context import HostRuntime
|
||||
from app.startup.composition.database import (
|
||||
compose_database_services,
|
||||
configure_database,
|
||||
@@ -139,17 +94,17 @@ from app.startup.composition.database import (
|
||||
)
|
||||
from app.startup.composition.network import configure_application_network_ports
|
||||
from app.startup.composition.outbox import build_outbox_dispatcher
|
||||
from app.startup.composition.runtime import (
|
||||
RuntimeInputs,
|
||||
compose_runtime,
|
||||
compose_runtime_dependencies,
|
||||
publish_runtime,
|
||||
)
|
||||
from app.startup.composition.security import (
|
||||
configure_security_access,
|
||||
configure_security_services,
|
||||
)
|
||||
from app.startup.composition.server import configure_server_services
|
||||
from app.startup.composition.subscription import (
|
||||
async_rule_group_mutation_scope,
|
||||
build_subscription_batch_writer,
|
||||
rule_group_mutation_scope,
|
||||
site_reference_mutation_scope,
|
||||
)
|
||||
from app.startup.initializers.agent import configure_agent_data_context, init_agent
|
||||
from app.startup.initializers.resources import (
|
||||
init_managed_resources,
|
||||
@@ -457,98 +412,28 @@ async def _initialize_modules() -> HostRuntime:
|
||||
)
|
||||
system_config = configuration.system_config
|
||||
workflow_query = database_services.workflow_query
|
||||
download_history_repository = TransactionalDownloadHistoryRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
)
|
||||
transfer_history_repository = TransactionalTransferHistoryRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
)
|
||||
site_repository = TransactionalSiteRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
)
|
||||
subscription_repository = TransactionalSubscriptionRepository(
|
||||
sync_session=SessionFactory,
|
||||
async_session=async_session_scope,
|
||||
)
|
||||
subscription_history_repository = TransactionalSubscriptionHistoryRepository(
|
||||
async_session=async_session_scope,
|
||||
)
|
||||
transfer_execution_repository = TransactionalTransferExecutionRepository(SessionFactory)
|
||||
message_helper = MessageHelper()
|
||||
message_queue = MessageQueueManager(auto_start=False)
|
||||
runtime_dependencies = compose_runtime_dependencies()
|
||||
agent_composition = compose_agent(
|
||||
runtime=database_runtime,
|
||||
system_config=system_config,
|
||||
site=site_repository,
|
||||
subscription=subscription_repository,
|
||||
subscription_history=subscription_history_repository,
|
||||
transfer_history=transfer_history_repository,
|
||||
transfer_execution=transfer_execution_repository,
|
||||
download_history=download_history_repository,
|
||||
dependencies=runtime_dependencies,
|
||||
)
|
||||
authentication = configure_security_services()
|
||||
host_runtime = HostRuntime(
|
||||
agent_chat=agent_composition.chat,
|
||||
agent=agent_composition.data,
|
||||
persistence=PersistenceRuntime(
|
||||
sync_session=get_db,
|
||||
async_session=get_async_db,
|
||||
sync_transaction=SqlAlchemyUnitOfWork,
|
||||
async_transaction=SqlAlchemyAsyncUnitOfWork,
|
||||
),
|
||||
authentication=authentication,
|
||||
messaging=MessagingRuntime(
|
||||
repository=MessageOper,
|
||||
helper=message_helper,
|
||||
queue=message_queue,
|
||||
),
|
||||
history=HistoryRuntime(
|
||||
download_repository=SessionDownloadHistoryRepository,
|
||||
transfer_repository=transfer_history_repository,
|
||||
transfer_mutation_repository=SessionTransferHistoryRepository,
|
||||
media_server_repository=MediaServerOper,
|
||||
transfer_execution_repository=transfer_execution_repository,
|
||||
),
|
||||
site=SiteRuntime(
|
||||
repository=SessionSiteRepository,
|
||||
standalone=site_repository,
|
||||
),
|
||||
subscription=SubscriptionRuntime(
|
||||
async_session=get_async_db,
|
||||
repository=SessionSubscriptionRepository,
|
||||
history_repository=SessionSubscriptionHistoryRepository,
|
||||
transaction=SqlAlchemyAsyncUnitOfWork,
|
||||
outbox=SqlAlchemyAsyncOutboxStager,
|
||||
dispatch_store=SqlAlchemyAsyncOutboxDispatchStore(async_session_scope),
|
||||
batch_writer=build_subscription_batch_writer,
|
||||
rule_group_mutation_scope=partial(
|
||||
rule_group_mutation_scope,
|
||||
system_config.publish_many,
|
||||
),
|
||||
async_rule_group_mutation_scope=partial(
|
||||
async_rule_group_mutation_scope,
|
||||
system_config.publish_many,
|
||||
),
|
||||
site_reference_mutation_scope=partial(
|
||||
site_reference_mutation_scope,
|
||||
system_config.publish_many,
|
||||
),
|
||||
),
|
||||
workflow=WorkflowRuntime(
|
||||
query=workflow_query,
|
||||
repository=WorkflowOper,
|
||||
system_config=get_configured_system_config,
|
||||
),
|
||||
configuration=configuration.runtime,
|
||||
settings=configuration.settings,
|
||||
tasks=get_task_registry(),
|
||||
security_composition = configure_security_services()
|
||||
runtime_composition = compose_runtime(
|
||||
RuntimeInputs(
|
||||
configuration=configuration,
|
||||
database=database_services,
|
||||
agent=agent_composition,
|
||||
authentication=security_composition,
|
||||
dependencies=runtime_dependencies,
|
||||
tasks=get_task_registry(),
|
||||
)
|
||||
)
|
||||
host_runtime = runtime_composition.runtime
|
||||
publish_database_services(database_services)
|
||||
publish_runtime(runtime_composition)
|
||||
configure_runtime_data_providers()
|
||||
configure_server_services(workflow_query, subscription_repository)
|
||||
configure_server_services(workflow_query, runtime_dependencies.subscription)
|
||||
workflow_execution = TransactionalWorkflowExecutionService(SessionFactory)
|
||||
configure_workflow_execution(workflow_execution)
|
||||
configure_outbox_dispatcher(build_outbox_dispatcher)
|
||||
@@ -562,9 +447,6 @@ async def _initialize_modules() -> HostRuntime:
|
||||
agent_composition,
|
||||
data_context_registrar=configure_agent_data_context,
|
||||
)
|
||||
configure_transfer_history_repository(lambda: transfer_history_repository)
|
||||
configure_site_query_service(SiteQueryService(repository=site_repository))
|
||||
configure_site_health_service(SiteHealthService(repository=site_repository))
|
||||
from app.agent.tools.manager import moviepilot_tool_manager
|
||||
|
||||
moviepilot_tool_manager.set_data_context(agent_composition.data)
|
||||
@@ -575,14 +457,8 @@ async def _initialize_modules() -> HostRuntime:
|
||||
configure_wallpaper_services()
|
||||
# Chain 无参兼容入口由组合根明确提供依赖上下文;测试和新代码可直接注入替代上下文。
|
||||
configure_chain_runtime_context(
|
||||
message_helper=message_helper,
|
||||
message_queue=message_queue,
|
||||
dependencies=runtime_dependencies,
|
||||
system_config=system_config,
|
||||
site=site_repository,
|
||||
subscription=subscription_repository,
|
||||
download_history=download_history_repository,
|
||||
transfer_history=transfer_history_repository,
|
||||
transfer_execution=transfer_execution_repository,
|
||||
configuration=configuration.runtime.chain,
|
||||
)
|
||||
# 认证访问层不反向依赖数据库实现,由启动组合层注入载荷提供器。
|
||||
|
||||
@@ -62,7 +62,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
| `chain` | 多入口复用的用例编排与 Module 动态分发已稳定,包根 SCC 已清零 | God object、私有长方法、无参构造和资源物化过重 |
|
||||
| `db/oper` | Model/Oper 已不自建 Session、不自行提交 | 无 Session Facade 仍被宿主组合根注入,业务操作可能拆成多个事务 |
|
||||
| `db/adapters` | 已有显式 Session/UoW 的参考切片 | Port 返回类型不够稳定;Outbox 的 stage 与自提交 dispatcher store 混在一个类型 |
|
||||
| `startup` | 已是 HostRuntime 和生命周期组合根;Chain 兼容上下文由 `composition/chain.py` 独立装配 | `initializers/modules.py` 仍有其他领域高扇出和具体对象目录装配 |
|
||||
| `startup` | `composition/runtime.py` 已成为 HostRuntime、领域 Runtime 与旧 ApiData 投影唯一 owner;Chain/Agent 复用同一 RuntimeDependencies | `initializers/modules.py` 仍保留启动顺序与跨领域发布调用,高扇出需由生命周期验收继续约束 |
|
||||
| API/Command/Scheduler | 多数入口已转向 Chain/Application;Scheduler 单体已拆为同名职责包并由 startup 注入业务 callable | Agent/System/Plugin 等入口仍承担较多业务与 I/O 编排 |
|
||||
| `sdk`/`compat` | 精确映射、稳定插件 ABI 和宿主 canonical 路径已落地 | SDK 仍暴露部分可变全局对象/具体 Manager,只能渐进收窄,不能直接删除 |
|
||||
|
||||
@@ -70,7 +70,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
|
||||
| 指标 | 当前值 | 解释 |
|
||||
|---|---:|---|
|
||||
| 宿主 Python 模块 / 内部依赖边 | 971 / 8,306 | `dependency-baseline.json` 当前快照 |
|
||||
| 宿主 Python 模块 / 内部依赖边 | 972 / 8,301 | `dependency-baseline.json` 当前快照 |
|
||||
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
|
||||
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
|
||||
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
|
||||
@@ -78,8 +78,8 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement |
|
||||
| Python 源码量 | 约 298,700 行 | 60 个文件超过 1,000 行,10 个超过 2,000 行 |
|
||||
| 长方法 | 299 个超过 80 行 | 70 个超过 150 行,23 个超过 250 行;大量是私有方法 |
|
||||
| 全量 mypy 历史债务 | 10,335 / 593 文件 | 新 Chain composition owner 零类型债,`modules.py` 低水位下降 1;strict frontier 当前覆盖 41 个文件 |
|
||||
| Ruff 历史诊断 | 648 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| 全量 mypy 历史债务 | 10,320 / 592 文件 | Runtime 唯一组合 owner 零新增类型债,`modules.py` 低水位下降 4;strict frontier 当前覆盖 41 个文件 |
|
||||
| Ruff 历史诊断 | 644 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| 覆盖率低水位 | Application 81.00%,Domain 80.67% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
|
||||
|
||||
### 3.3 热点文件
|
||||
@@ -675,8 +675,9 @@ Pylint 10/10、Ruff、mypy/复杂度 ratchet、宿主与最新官方插件基线
|
||||
- [x] L4.2 已将 network、security、agent、server、outbox 与 Chain 装配迁入单词型
|
||||
`startup/composition/*` owner;Agent composition 在 Worker 启动后读取容量并共享同一数据和任务对象,
|
||||
Chain 通过 provider 保持插件无参构造兼容,旧 Transfer command 只在真实调用时延迟导入。
|
||||
- [ ] 继续将 HostRuntime 剩余构造移到 `startup/composition/runtime.py`,最终让 initializer
|
||||
只负责顺序、注册和是否重启的决策。
|
||||
- [x] L4.3 已将 HostRuntime、命名领域 Runtime 和旧 `ApiDataPorts` 投影统一移到单词型
|
||||
`startup/composition/runtime.py`;`RuntimeDependencies` 由 Agent、Chain、HostRuntime 复用,
|
||||
`HostRuntime.tasks` 显式绑定 lifecycle 的 TaskRegistry,Chain 不再二次构造 transfer execution。
|
||||
- [ ] 保留生命周期 manifest 和顺序快照;高扇出在组合根是允许的,但业务实现不能继续沉积其中。
|
||||
|
||||
### ARCH-204 收敛 sync/async 重复与测试存量
|
||||
|
||||
@@ -284,13 +284,18 @@ sequenceDiagram
|
||||
从 `HostRuntime.configuration` 获取 frozen 配置快照。系统设置管理 API 通过
|
||||
`HostRuntime.settings` 的窄服务读写可变部署设置,业务域不接触 Settings 实例;生产与测试组合根统一
|
||||
复用 `startup/composition/configuration.py` 的映射、快照加载与发布。`startup/composition/database.py`
|
||||
持有数据库 worker、兼容事务 runner、查询/插件持久化和旧 `ApiDataPorts` 的具体装配;initializer
|
||||
只保留顺序调用。`startup/composition/chain.py` 集中构造无参 Chain 兼容上下文及其持久化、事件、
|
||||
持有数据库 worker、兼容事务 runner、查询与插件持久化;`startup/composition/runtime.py` 是
|
||||
`HostRuntime`、全部命名领域 Runtime 和旧 `ApiDataPorts` 投影的唯一构造/发布 owner。它先构造
|
||||
一份 frozen `RuntimeDependencies`,Agent、Chain 与 HostRuntime 复用同一仓储、transfer execution
|
||||
ledger 和消息对象,导入阶段不启动数据库 worker 或消息队列;initializer 只保留顺序调用。
|
||||
`startup/composition/chain.py` 集中构造无参 Chain 兼容上下文及其持久化、事件、
|
||||
Outbox、消息和模块分发依赖,并延迟绑定旧 Transfer command 与壁纸 Chain,避免组合根导入环。
|
||||
`startup/composition/security.py` 统一装配认证、用户查询、PassKey 与 Web 访问
|
||||
provider,并将同一 `AuthenticationRuntime` 交给 HostRuntime;`startup/composition/network.py` 统一
|
||||
装配网络应用端口;`startup/composition/agent.py` 构造并发布唯一的 Agent 数据、会话和自主任务对象,
|
||||
`HostRuntime`、工具管理器与 Scheduler 复用同一对象身份。`ApiDataPorts` 仅保留旧导入 ABI,
|
||||
provider,并把持久化工厂交给 runtime owner 投影为 `AuthenticationRuntime`;
|
||||
`startup/composition/network.py` 统一装配网络应用端口;`startup/composition/agent.py` 构造并发布
|
||||
唯一的 Agent 数据、会话持久化和自主任务对象,由 runtime owner 投影 `AgentChatRuntime`,
|
||||
`HostRuntime`、工具管理器与 Scheduler 复用同一对象身份。`ApiDataPorts` 严格从同一 HostRuntime
|
||||
字段投影,仅保留旧导入 ABI,
|
||||
不参与正式请求链路。
|
||||
- **安全模式**:`MOVIEPILOT_SAFE_MODE` 会跳过插件、定时器、监控器、命令与工作流,用于故障自救。
|
||||
- **Scheduler 同名职责包**:旧 `app/scheduler.py` 单体已退役;`catalog.py` 负责作业目录和计划投影,
|
||||
@@ -749,8 +754,8 @@ flowchart LR
|
||||
|
||||
| 指标 | 当前值 |
|
||||
|---|---:|
|
||||
| Python 模块 | 971 |
|
||||
| 内部导入边 | 8,306 |
|
||||
| Python 模块 | 972 |
|
||||
| 内部导入边 | 8,301 |
|
||||
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
|
||||
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
|
||||
| Direct egress | 53(债务已清零,53 条精确 containment) |
|
||||
|
||||
@@ -139,15 +139,22 @@ not startup. Lower-level runtime modules must not import startup.
|
||||
`startup/composition/configuration.py` owns configuration snapshot loading, typed
|
||||
runtime/settings construction and publication. `startup/composition/database.py`
|
||||
owns the process database worker, compatibility transaction runner, query services,
|
||||
plugin persistence and legacy API data-port wiring. Initializers call these owners in
|
||||
plugin persistence and workflow query wiring. Initializers call these owners in
|
||||
startup order and must not recreate their concrete construction.
|
||||
`startup/composition/runtime.py` is the sole constructor of `HostRuntime`, its named
|
||||
domain Runtime projections and the legacy `ApiDataPorts` projection. It first creates
|
||||
one frozen `RuntimeDependencies` aggregate; Agent, Chain and HostRuntime must reuse the
|
||||
same repositories, transfer-execution ledger and message owners rather than constructing
|
||||
parallel instances. Importing this owner remains cold: concrete DB adapters are resolved
|
||||
only when composition functions run, and FastAPI `app.state` publication remains owned by
|
||||
the lifecycle layer.
|
||||
`startup/composition/chain.py` owns the no-argument Chain compatibility context,
|
||||
typed persistence/dispatch object graph, lazy legacy Transfer command binding and
|
||||
wallpaper-provider publication. `initializers/modules.py` only passes the already
|
||||
constructed repositories in startup order and must not recreate those dependencies.
|
||||
wallpaper-provider publication. `initializers/modules.py` passes the already constructed
|
||||
`RuntimeDependencies` in startup order and must not recreate those dependencies.
|
||||
`startup/composition/security.py` owns authentication, user lookup, PassKey and Web
|
||||
access provider assembly; `initializers/modules.py` only invokes that owner and places
|
||||
its returned `AuthenticationRuntime` into the host runtime.
|
||||
access provider assembly; it returns the persistence factories needed by the runtime
|
||||
owner, which alone constructs `AuthenticationRuntime`.
|
||||
`startup/composition/network.py` owns concrete network-test, image, internal-address
|
||||
and message-ingress Adapter wiring
|
||||
while retaining lazy RuntimeSettings reads. Initializers call these owners in
|
||||
@@ -160,8 +167,9 @@ startup order and must not recreate its concrete construction.
|
||||
construction and registration, including lazy local-data readers and external
|
||||
transport callbacks; `initializers/modules.py` retains only its startup-order call
|
||||
and the later explicit network warmup. Database runtime
|
||||
`startup/composition/agent.py` owns the single `AgentDataContext`, `AgentChatRuntime`,
|
||||
chat persistence and autonomous-task repository/execution service set. It may accept
|
||||
`startup/composition/agent.py` owns the single `AgentDataContext`, chat persistence and
|
||||
autonomous-task repository/execution service set. The runtime owner projects those same
|
||||
objects into `AgentChatRuntime`. Agent composition may accept
|
||||
the Agent initializer's data-context registrar as a callback, but must not import
|
||||
`startup.initializers`; tool-manager, scheduler and Agent activation order remains in
|
||||
the initializer. Database runtime ownership rejects overlapping lifespans; after a
|
||||
|
||||
@@ -1089,8 +1089,8 @@
|
||||
"runtime_only": true
|
||||
}
|
||||
},
|
||||
"edge_count": 8306,
|
||||
"edge_sha256": "2224fccf31c611f1ba2cf9e8afbdd33b9277ac2846101c85d57e0801b2a7b542",
|
||||
"edge_count": 8301,
|
||||
"edge_sha256": "a1148b9576004423eb291429dc3e9a0e99282f14fe9d3d416539584a28aec8d0",
|
||||
"edges": [
|
||||
"app -> app.foundation",
|
||||
"app -> app.foundation.environment",
|
||||
@@ -8629,15 +8629,8 @@
|
||||
"app.startup.composition.agent -> app.application",
|
||||
"app.startup.composition.agent -> app.application.agent",
|
||||
"app.startup.composition.agent -> app.application.agenttask",
|
||||
"app.startup.composition.agent -> app.application.history",
|
||||
"app.startup.composition.agent -> app.application.messaging",
|
||||
"app.startup.composition.agent -> app.application.messaging.chat",
|
||||
"app.startup.composition.agent -> app.application.site",
|
||||
"app.startup.composition.agent -> app.application.site.contract",
|
||||
"app.startup.composition.agent -> app.application.subscription",
|
||||
"app.startup.composition.agent -> app.application.subscription.contract",
|
||||
"app.startup.composition.agent -> app.application.transfer",
|
||||
"app.startup.composition.agent -> app.application.transfer.execution",
|
||||
"app.startup.composition.agent -> app.db",
|
||||
"app.startup.composition.agent -> app.db.adapters",
|
||||
"app.startup.composition.agent -> app.db.adapters.agent",
|
||||
@@ -8645,21 +8638,17 @@
|
||||
"app.startup.composition.agent -> app.db.oper.agentchat",
|
||||
"app.startup.composition.agent -> app.db.oper.systemconfig",
|
||||
"app.startup.composition.agent -> app.db.session",
|
||||
"app.startup.composition.agent -> app.db.uow",
|
||||
"app.startup.composition.agent -> app.startup",
|
||||
"app.startup.composition.agent -> app.startup.composition",
|
||||
"app.startup.composition.agent -> app.startup.composition.context",
|
||||
"app.startup.composition.agent -> app.startup.composition.database",
|
||||
"app.startup.composition.agent -> app.startup.composition.runtime",
|
||||
"app.startup.composition.agent -> app.startup.composition.subscription",
|
||||
"app.startup.composition.chain -> app.application",
|
||||
"app.startup.composition.chain -> app.application.chain",
|
||||
"app.startup.composition.chain -> app.application.chain.context",
|
||||
"app.startup.composition.chain -> app.application.configuration",
|
||||
"app.startup.composition.chain -> app.application.image",
|
||||
"app.startup.composition.chain -> app.application.messaging",
|
||||
"app.startup.composition.chain -> app.application.messaging.message",
|
||||
"app.startup.composition.chain -> app.application.transfer",
|
||||
"app.startup.composition.chain -> app.application.transfer.execution",
|
||||
"app.startup.composition.chain -> app.chain",
|
||||
"app.startup.composition.chain -> app.chain.mediaserver",
|
||||
"app.startup.composition.chain -> app.chain.tmdb",
|
||||
@@ -8669,12 +8658,7 @@
|
||||
"app.startup.composition.chain -> app.db.adapters",
|
||||
"app.startup.composition.chain -> app.db.adapters.chain",
|
||||
"app.startup.composition.chain -> app.db.adapters.download",
|
||||
"app.startup.composition.chain -> app.db.adapters.history",
|
||||
"app.startup.composition.chain -> app.db.adapters.history.download",
|
||||
"app.startup.composition.chain -> app.db.adapters.history.transfer",
|
||||
"app.startup.composition.chain -> app.db.adapters.mediaserver",
|
||||
"app.startup.composition.chain -> app.db.adapters.site",
|
||||
"app.startup.composition.chain -> app.db.adapters.subscription",
|
||||
"app.startup.composition.chain -> app.db.adapters.transfer",
|
||||
"app.startup.composition.chain -> app.db.adapters.transfer.admission",
|
||||
"app.startup.composition.chain -> app.db.oper",
|
||||
@@ -8693,6 +8677,7 @@
|
||||
"app.startup.composition.chain -> app.startup",
|
||||
"app.startup.composition.chain -> app.startup.composition",
|
||||
"app.startup.composition.chain -> app.startup.composition.database",
|
||||
"app.startup.composition.chain -> app.startup.composition.runtime",
|
||||
"app.startup.composition.chain -> app.startup.composition.subscription",
|
||||
"app.startup.composition.configuration -> app.application",
|
||||
"app.startup.composition.configuration -> app.application.configuration",
|
||||
@@ -8734,8 +8719,6 @@
|
||||
"app.startup.composition.database -> app.adapters.system.backup",
|
||||
"app.startup.composition.database -> app.adapters.system.backup.database",
|
||||
"app.startup.composition.database -> app.adapters.system.backup.files",
|
||||
"app.startup.composition.database -> app.api",
|
||||
"app.startup.composition.database -> app.api.data",
|
||||
"app.startup.composition.database -> app.application",
|
||||
"app.startup.composition.database -> app.application.backup",
|
||||
"app.startup.composition.database -> app.application.database",
|
||||
@@ -8746,13 +8729,9 @@
|
||||
"app.startup.composition.database -> app.application.workflow",
|
||||
"app.startup.composition.database -> app.db",
|
||||
"app.startup.composition.database -> app.db.adapters",
|
||||
"app.startup.composition.database -> app.db.adapters.history",
|
||||
"app.startup.composition.database -> app.db.adapters.history.download",
|
||||
"app.startup.composition.database -> app.db.adapters.pluginidentity",
|
||||
"app.startup.composition.database -> app.db.adapters.plugininstallation",
|
||||
"app.startup.composition.database -> app.db.adapters.query",
|
||||
"app.startup.composition.database -> app.db.adapters.site",
|
||||
"app.startup.composition.database -> app.db.adapters.subscription",
|
||||
"app.startup.composition.database -> app.db.adapters.transaction",
|
||||
"app.startup.composition.database -> app.db.adapters.user",
|
||||
"app.startup.composition.database -> app.db.adapters.workflow",
|
||||
@@ -8760,11 +8739,7 @@
|
||||
"app.startup.composition.database -> app.db.health",
|
||||
"app.startup.composition.database -> app.db.maintenance",
|
||||
"app.startup.composition.database -> app.db.oper",
|
||||
"app.startup.composition.database -> app.db.oper.mediaserver",
|
||||
"app.startup.composition.database -> app.db.oper.message",
|
||||
"app.startup.composition.database -> app.db.oper.passkey",
|
||||
"app.startup.composition.database -> app.db.oper.systemconfig",
|
||||
"app.startup.composition.database -> app.db.oper.workflow",
|
||||
"app.startup.composition.database -> app.db.session",
|
||||
"app.startup.composition.database -> app.db.uow",
|
||||
"app.startup.composition.database -> app.db.worker",
|
||||
@@ -8800,6 +8775,42 @@
|
||||
"app.startup.composition.outbox -> app.schemas",
|
||||
"app.startup.composition.outbox -> app.schemas.message",
|
||||
"app.startup.composition.outbox -> app.schemas.types",
|
||||
"app.startup.composition.runtime -> app.api",
|
||||
"app.startup.composition.runtime -> app.api.data",
|
||||
"app.startup.composition.runtime -> app.application",
|
||||
"app.startup.composition.runtime -> app.application.history",
|
||||
"app.startup.composition.runtime -> app.application.messaging",
|
||||
"app.startup.composition.runtime -> app.application.messaging.message",
|
||||
"app.startup.composition.runtime -> app.application.site",
|
||||
"app.startup.composition.runtime -> app.application.site.contract",
|
||||
"app.startup.composition.runtime -> app.application.site.health",
|
||||
"app.startup.composition.runtime -> app.application.site.query",
|
||||
"app.startup.composition.runtime -> app.application.subscription",
|
||||
"app.startup.composition.runtime -> app.application.subscription.contract",
|
||||
"app.startup.composition.runtime -> app.application.transfer",
|
||||
"app.startup.composition.runtime -> app.application.transfer.execution",
|
||||
"app.startup.composition.runtime -> app.db",
|
||||
"app.startup.composition.runtime -> app.db.adapters",
|
||||
"app.startup.composition.runtime -> app.db.adapters.history",
|
||||
"app.startup.composition.runtime -> app.db.adapters.history.download",
|
||||
"app.startup.composition.runtime -> app.db.adapters.history.transfer",
|
||||
"app.startup.composition.runtime -> app.db.adapters.outbox",
|
||||
"app.startup.composition.runtime -> app.db.adapters.site",
|
||||
"app.startup.composition.runtime -> app.db.adapters.subscription",
|
||||
"app.startup.composition.runtime -> app.db.adapters.transfer",
|
||||
"app.startup.composition.runtime -> app.db.adapters.transfer.execution",
|
||||
"app.startup.composition.runtime -> app.db.oper",
|
||||
"app.startup.composition.runtime -> app.db.oper.mediaserver",
|
||||
"app.startup.composition.runtime -> app.db.oper.message",
|
||||
"app.startup.composition.runtime -> app.db.oper.workflow",
|
||||
"app.startup.composition.runtime -> app.db.session",
|
||||
"app.startup.composition.runtime -> app.db.uow",
|
||||
"app.startup.composition.runtime -> app.runtime",
|
||||
"app.startup.composition.runtime -> app.runtime.tasks",
|
||||
"app.startup.composition.runtime -> app.startup",
|
||||
"app.startup.composition.runtime -> app.startup.composition",
|
||||
"app.startup.composition.runtime -> app.startup.composition.context",
|
||||
"app.startup.composition.runtime -> app.startup.composition.subscription",
|
||||
"app.startup.composition.security -> app.adapters",
|
||||
"app.startup.composition.security -> app.adapters.web",
|
||||
"app.startup.composition.security -> app.adapters.web.security",
|
||||
@@ -8965,7 +8976,6 @@
|
||||
"app.startup.initializers.modules -> app.agent.tools.manager",
|
||||
"app.startup.initializers.modules -> app.application",
|
||||
"app.startup.initializers.modules -> app.application.configuration",
|
||||
"app.startup.initializers.modules -> app.application.history",
|
||||
"app.startup.initializers.modules -> app.application.messaging",
|
||||
"app.startup.initializers.modules -> app.application.messaging.agent",
|
||||
"app.startup.initializers.modules -> app.application.messaging.chat",
|
||||
@@ -8976,8 +8986,6 @@
|
||||
"app.startup.initializers.modules -> app.application.security.url",
|
||||
"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.workflow",
|
||||
"app.startup.initializers.modules -> app.chain",
|
||||
"app.startup.initializers.modules -> app.chain.download",
|
||||
@@ -8990,21 +8998,8 @@
|
||||
"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.history",
|
||||
"app.startup.initializers.modules -> app.db.adapters.history.download",
|
||||
"app.startup.initializers.modules -> app.db.adapters.history.transfer",
|
||||
"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.transfer",
|
||||
"app.startup.initializers.modules -> app.db.adapters.transfer.execution",
|
||||
"app.startup.initializers.modules -> app.db.adapters.workflow",
|
||||
"app.startup.initializers.modules -> app.db.oper",
|
||||
"app.startup.initializers.modules -> app.db.oper.mediaserver",
|
||||
"app.startup.initializers.modules -> app.db.oper.message",
|
||||
"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.runtime",
|
||||
"app.startup.initializers.modules -> app.runtime.config",
|
||||
"app.startup.initializers.modules -> app.runtime.events",
|
||||
@@ -9031,9 +9026,9 @@
|
||||
"app.startup.initializers.modules -> app.startup.composition.database",
|
||||
"app.startup.initializers.modules -> app.startup.composition.network",
|
||||
"app.startup.initializers.modules -> app.startup.composition.outbox",
|
||||
"app.startup.initializers.modules -> app.startup.composition.runtime",
|
||||
"app.startup.initializers.modules -> app.startup.composition.security",
|
||||
"app.startup.initializers.modules -> app.startup.composition.server",
|
||||
"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.resources",
|
||||
@@ -9399,7 +9394,7 @@
|
||||
"app.workflow.actions.transfer_file -> app.workflow",
|
||||
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
||||
],
|
||||
"module_count": 971,
|
||||
"module_count": 972,
|
||||
"modules": [
|
||||
"app",
|
||||
"app.adapters",
|
||||
@@ -10329,6 +10324,7 @@
|
||||
"app.startup.composition.database",
|
||||
"app.startup.composition.network",
|
||||
"app.startup.composition.outbox",
|
||||
"app.startup.composition.runtime",
|
||||
"app.startup.composition.security",
|
||||
"app.startup.composition.server",
|
||||
"app.startup.composition.subscription",
|
||||
|
||||
@@ -3276,12 +3276,12 @@
|
||||
"no-untyped-def": 2
|
||||
},
|
||||
"app/startup/initializers/modules.py": {
|
||||
"arg-type": 5,
|
||||
"arg-type": 2,
|
||||
"assignment": 1,
|
||||
"attr-defined": 2,
|
||||
"misc": 1,
|
||||
"no-any-return": 2,
|
||||
"no-untyped-call": 15,
|
||||
"no-untyped-call": 14,
|
||||
"no-untyped-def": 6,
|
||||
"return-value": 2
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"repeat": 3,
|
||||
"targets": {
|
||||
"app.startup.lifecycle": {
|
||||
"loaded_app_module_count": 497,
|
||||
"loaded_app_module_count": 491,
|
||||
"max_ms": 1159.462,
|
||||
"median_ms": 1074.592,
|
||||
"min_ms": 1072.211,
|
||||
@@ -17,7 +17,7 @@
|
||||
]
|
||||
},
|
||||
"app.factory": {
|
||||
"loaded_app_module_count": 509,
|
||||
"loaded_app_module_count": 503,
|
||||
"max_ms": 1094.111,
|
||||
"median_ms": 1092.394,
|
||||
"min_ms": 1090.171,
|
||||
@@ -28,7 +28,7 @@
|
||||
]
|
||||
},
|
||||
"app.main": {
|
||||
"loaded_app_module_count": 511,
|
||||
"loaded_app_module_count": 505,
|
||||
"max_ms": 1165.991,
|
||||
"median_ms": 1159.713,
|
||||
"min_ms": 1143.531,
|
||||
|
||||
@@ -8,6 +8,7 @@ import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.startup.composition import agent as agent_composition
|
||||
from app.startup.composition.runtime import RuntimeDependencies
|
||||
|
||||
|
||||
class _StartedWorker:
|
||||
@@ -34,15 +35,20 @@ def _compose() -> tuple[agent_composition.AgentComposition, _StartedWorker]:
|
||||
transaction=SimpleNamespace(sync=lambda operation: operation(object())),
|
||||
)
|
||||
system_config = SimpleNamespace(publish_many=lambda _values: None)
|
||||
composition = agent_composition.compose_agent(
|
||||
runtime=runtime,
|
||||
system_config=system_config,
|
||||
dependencies = RuntimeDependencies(
|
||||
download_history=object(),
|
||||
transfer_history=object(),
|
||||
site=object(),
|
||||
subscription=object(),
|
||||
subscription_history=object(),
|
||||
transfer_history=object(),
|
||||
transfer_execution=object(),
|
||||
download_history=object(),
|
||||
message_helper=object(),
|
||||
message_queue=object(),
|
||||
)
|
||||
composition = agent_composition.compose_agent(
|
||||
runtime=runtime,
|
||||
system_config=system_config,
|
||||
dependencies=dependencies,
|
||||
)
|
||||
return composition, worker
|
||||
|
||||
@@ -52,7 +58,7 @@ def test_agent_composition_reuses_shared_objects_and_started_worker() -> None:
|
||||
composition, worker = _compose()
|
||||
|
||||
assert composition.data.tasks is composition.tasks
|
||||
assert composition.chat.persistence is composition.data.chat_persistence
|
||||
assert composition.persistence is composition.data.chat_persistence
|
||||
assert composition.data.chat_persistence._async_executor is worker
|
||||
assert composition.data.chat_persistence._capacity == 17
|
||||
assert composition.execution._async_executor is worker
|
||||
|
||||
@@ -296,24 +296,16 @@ def test_agent_manager_uses_focused_owner_modules_and_precise_facade():
|
||||
|
||||
manager_tree = ast.parse(owner_paths["manager"].read_text(encoding="utf-8-sig"))
|
||||
manager_class = next(
|
||||
node
|
||||
for node in manager_tree.body
|
||||
if isinstance(node, ast.ClassDef) and node.name == "AgentManager"
|
||||
node for node in manager_tree.body if isinstance(node, ast.ClassDef) and node.name == "AgentManager"
|
||||
)
|
||||
manager_methods = {
|
||||
node.name
|
||||
for node in manager_class.body
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
node.name for node in manager_class.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
}
|
||||
assert manager_methods == {"__init__"}
|
||||
assert [ast.unparse(base) for base in manager_class.bases] == ["AgentTaskOwner"]
|
||||
|
||||
orchestrator_tree = ast.parse(
|
||||
owner_paths["orchestrator"].read_text(encoding="utf-8-sig")
|
||||
)
|
||||
orchestrator_classes = {
|
||||
node.name for node in orchestrator_tree.body if isinstance(node, ast.ClassDef)
|
||||
}
|
||||
orchestrator_tree = ast.parse(owner_paths["orchestrator"].read_text(encoding="utf-8-sig"))
|
||||
orchestrator_classes = {node.name for node in orchestrator_tree.body if isinstance(node, ast.ClassDef)}
|
||||
orchestrator_assignments = {
|
||||
target.id
|
||||
for node in orchestrator_tree.body
|
||||
@@ -325,9 +317,7 @@ def test_agent_manager_uses_focused_owner_modules_and_precise_facade():
|
||||
assert "_MessageTask" not in orchestrator_classes
|
||||
assert "agent_manager" not in orchestrator_assignments
|
||||
|
||||
startup_source = (
|
||||
APP_ROOT / "startup" / "initializers" / "agent.py"
|
||||
).read_text(encoding="utf-8-sig")
|
||||
startup_source = (APP_ROOT / "startup" / "initializers" / "agent.py").read_text(encoding="utf-8-sig")
|
||||
assert "from app.agent.manager import AgentManager" in startup_source
|
||||
assert "from app.agent.orchestrator import AgentManager" not in startup_source
|
||||
|
||||
@@ -390,14 +380,10 @@ def test_domain_media_projection_uses_single_word_owner_package() -> None:
|
||||
filename=str(context_path),
|
||||
)
|
||||
media_class = next(
|
||||
node
|
||||
for node in context_tree.body
|
||||
if isinstance(node, ast.ClassDef) and node.name == "MediaInfo"
|
||||
node for node in context_tree.body if isinstance(node, ast.ClassDef) and node.name == "MediaInfo"
|
||||
)
|
||||
methods = {
|
||||
node.name: node
|
||||
for node in media_class.body
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
node.name: node for node in media_class.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
}
|
||||
for name in (
|
||||
"set_tmdb_info",
|
||||
@@ -419,9 +405,7 @@ def test_domain_media_projection_uses_single_word_owner_package() -> None:
|
||||
assert isinstance(executable[0], ast.Expr)
|
||||
assert isinstance(executable[0].value, ast.Call)
|
||||
|
||||
schema_source = (APP_ROOT / "schemas" / "context.py").read_text(
|
||||
encoding="utf-8-sig"
|
||||
)
|
||||
schema_source = (APP_ROOT / "schemas" / "context.py").read_text(encoding="utf-8-sig")
|
||||
assert "app.domain.projection" not in schema_source
|
||||
assert not any(name in schema_source for name in methods if name.startswith("set_") and name.endswith("_info"))
|
||||
|
||||
@@ -697,8 +681,11 @@ def test_download_history_ports_are_typed_detached_and_canonically_injected():
|
||||
assert "DownloadFiles = Any" not in source
|
||||
|
||||
startup_source = (APP_ROOT / "startup" / "initializers" / "modules.py").read_text(encoding="utf-8")
|
||||
assert "TransactionalDownloadHistoryRepository(" in startup_source
|
||||
assert "SessionDownloadHistoryRepository" in startup_source
|
||||
runtime_source = (APP_ROOT / "startup" / "composition" / "runtime.py").read_text(encoding="utf-8")
|
||||
assert "TransactionalDownloadHistoryRepository(" not in startup_source
|
||||
assert "TransactionalDownloadHistoryRepository(" in runtime_source
|
||||
assert "SessionDownloadHistoryRepository" not in startup_source
|
||||
assert "SessionDownloadHistoryRepository" in runtime_source
|
||||
assert "DownloadHistoryOper" not in startup_source
|
||||
|
||||
adapter_source = (APP_ROOT / "db" / "adapters" / "history" / "download.py").read_text(encoding="utf-8")
|
||||
@@ -734,9 +721,7 @@ def test_user_configuration_uses_typed_transactional_adapter():
|
||||
}
|
||||
assert "Any" not in application_source
|
||||
|
||||
composition_source = (
|
||||
APP_ROOT / "startup" / "composition" / "configuration.py"
|
||||
).read_text(encoding="utf-8")
|
||||
composition_source = (APP_ROOT / "startup" / "composition" / "configuration.py").read_text(encoding="utf-8")
|
||||
assert "TransactionalUserConfigurationRepository(SessionFactory)" in composition_source
|
||||
assert "UserConfigOper" not in composition_source
|
||||
|
||||
@@ -754,15 +739,10 @@ def test_user_configuration_uses_typed_transactional_adapter():
|
||||
|
||||
def test_modules_initializer_delegates_configuration_and_database_composition():
|
||||
"""模块初始化器只编排配置与数据库组合 API,不再内联其构造细节。"""
|
||||
initializer_source = (
|
||||
APP_ROOT / "startup" / "initializers" / "modules.py"
|
||||
).read_text(encoding="utf-8")
|
||||
configuration_source = (
|
||||
APP_ROOT / "startup" / "composition" / "configuration.py"
|
||||
).read_text(encoding="utf-8")
|
||||
database_source = (
|
||||
APP_ROOT / "startup" / "composition" / "database.py"
|
||||
).read_text(encoding="utf-8")
|
||||
initializer_source = (APP_ROOT / "startup" / "initializers" / "modules.py").read_text(encoding="utf-8")
|
||||
configuration_source = (APP_ROOT / "startup" / "composition" / "configuration.py").read_text(encoding="utf-8")
|
||||
database_source = (APP_ROOT / "startup" / "composition" / "database.py").read_text(encoding="utf-8")
|
||||
runtime_source = (APP_ROOT / "startup" / "composition" / "runtime.py").read_text(encoding="utf-8")
|
||||
|
||||
for call in (
|
||||
"start_database_runtime()",
|
||||
@@ -796,17 +776,14 @@ def test_modules_initializer_delegates_configuration_and_database_composition():
|
||||
assert "SqlAlchemyDataQueryAdapter(" in database_source
|
||||
assert "reset_transaction_runners()" in database_source
|
||||
assert "configure_api_data_runtime" not in initializer_source
|
||||
assert "configure_api_data_runtime" in database_source
|
||||
assert "configure_api_data_runtime" not in database_source
|
||||
assert "configure_api_data_runtime" in runtime_source
|
||||
|
||||
|
||||
def test_modules_initializer_delegates_network_composition():
|
||||
"""模块初始化器只调用网络组合 API,不再内联具体 Adapter 组装。"""
|
||||
initializer_source = (
|
||||
APP_ROOT / "startup" / "initializers" / "modules.py"
|
||||
).read_text(encoding="utf-8")
|
||||
network_source = (
|
||||
APP_ROOT / "startup" / "composition" / "network.py"
|
||||
).read_text(encoding="utf-8")
|
||||
initializer_source = (APP_ROOT / "startup" / "initializers" / "modules.py").read_text(encoding="utf-8")
|
||||
network_source = (APP_ROOT / "startup" / "composition" / "network.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "configure_application_network_ports()" in initializer_source
|
||||
for detail in (
|
||||
@@ -824,15 +801,9 @@ def test_modules_initializer_delegates_network_composition():
|
||||
|
||||
def test_modules_initializer_delegates_outbox_composition():
|
||||
"""模块初始化器只发布 Outbox dispatcher,不再内联 handler 与存储装配。"""
|
||||
initializer_source = (
|
||||
APP_ROOT / "startup" / "initializers" / "modules.py"
|
||||
).read_text(encoding="utf-8")
|
||||
composition_source = (
|
||||
APP_ROOT / "startup" / "composition" / "outbox.py"
|
||||
).read_text(encoding="utf-8")
|
||||
package_source = (
|
||||
APP_ROOT / "startup" / "composition" / "__init__.py"
|
||||
).read_text(encoding="utf-8")
|
||||
initializer_source = (APP_ROOT / "startup" / "initializers" / "modules.py").read_text(encoding="utf-8")
|
||||
composition_source = (APP_ROOT / "startup" / "composition" / "outbox.py").read_text(encoding="utf-8")
|
||||
package_source = (APP_ROOT / "startup" / "composition" / "__init__.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "configure_outbox_dispatcher(build_outbox_dispatcher)" in initializer_source
|
||||
for retired_name in ("_build_outbox_handlers", "_build_outbox_dispatcher"):
|
||||
@@ -850,31 +821,17 @@ def test_modules_initializer_delegates_outbox_composition():
|
||||
|
||||
def test_modules_initializer_delegates_server_service_composition():
|
||||
"""中心服务对象图只由单词型 composition owner 构造,initializer 仅保留调用顺序。"""
|
||||
initializer_source = (
|
||||
APP_ROOT / "startup" / "initializers" / "modules.py"
|
||||
).read_text(encoding="utf-8")
|
||||
initializer_source = (APP_ROOT / "startup" / "initializers" / "modules.py").read_text(encoding="utf-8")
|
||||
composition_path = APP_ROOT / "startup" / "composition" / "server.py"
|
||||
composition_source = composition_path.read_text(encoding="utf-8")
|
||||
architecture_source = (
|
||||
PROJECT_ROOT / "docs" / "rules" / "05-architecture.md"
|
||||
).read_text(encoding="utf-8")
|
||||
package_tree = ast.parse(
|
||||
(APP_ROOT / "startup" / "composition" / "__init__.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
architecture_source = (PROJECT_ROOT / "docs" / "rules" / "05-architecture.md").read_text(encoding="utf-8")
|
||||
package_tree = ast.parse((APP_ROOT / "startup" / "composition" / "__init__.py").read_text(encoding="utf-8"))
|
||||
|
||||
assert composition_path.is_file()
|
||||
assert not list(composition_path.parent.glob("server_*.py"))
|
||||
assert "from app.startup.composition.server import configure_server_services" in (
|
||||
initializer_source
|
||||
)
|
||||
assert "configure_server_services(workflow_query, subscription_repository)" in (
|
||||
initializer_source
|
||||
)
|
||||
assert "`startup/composition/server.py` owns MoviePilot Server" in (
|
||||
architecture_source
|
||||
)
|
||||
assert "from app.startup.composition.server import configure_server_services" in (initializer_source)
|
||||
assert "configure_server_services(workflow_query, runtime_dependencies.subscription)" in initializer_source
|
||||
assert "`startup/composition/server.py` owns MoviePilot Server" in (architecture_source)
|
||||
for detail in (
|
||||
"ServerReportService(",
|
||||
"ServerSharingService(",
|
||||
@@ -883,28 +840,21 @@ def test_modules_initializer_delegates_server_service_composition():
|
||||
assert detail not in initializer_source
|
||||
assert detail in composition_source
|
||||
assert not any(
|
||||
isinstance(node, (ast.Import, ast.ImportFrom, ast.FunctionDef, ast.ClassDef))
|
||||
for node in package_tree.body
|
||||
isinstance(node, (ast.Import, ast.ImportFrom, ast.FunctionDef, ast.ClassDef)) for node in package_tree.body
|
||||
)
|
||||
|
||||
|
||||
def test_modules_initializer_delegates_agent_composition():
|
||||
"""模块初始化器只编排 Agent composition,不再内联数据与任务构造。"""
|
||||
initializer_source = (
|
||||
APP_ROOT / "startup" / "initializers" / "modules.py"
|
||||
).read_text(encoding="utf-8")
|
||||
agent_source = (
|
||||
APP_ROOT / "startup" / "composition" / "agent.py"
|
||||
).read_text(encoding="utf-8")
|
||||
initializer_source = (APP_ROOT / "startup" / "initializers" / "modules.py").read_text(encoding="utf-8")
|
||||
agent_source = (APP_ROOT / "startup" / "composition" / "agent.py").read_text(encoding="utf-8")
|
||||
runtime_source = (APP_ROOT / "startup" / "composition" / "runtime.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "compose_agent(" in initializer_source
|
||||
assert "publish_agent_services(" in initializer_source
|
||||
assert initializer_source.index("await start_database_runtime()") < initializer_source.index(
|
||||
"compose_agent("
|
||||
)
|
||||
assert initializer_source.index("await start_database_runtime()") < initializer_source.index("compose_agent(")
|
||||
for detail in (
|
||||
"AgentDataContext(",
|
||||
"AgentChatRuntime(",
|
||||
"AgentChatPersistenceService(",
|
||||
"AgentChatService(",
|
||||
"TransactionalAgentTaskRepository(",
|
||||
@@ -915,26 +865,88 @@ def test_modules_initializer_delegates_agent_composition():
|
||||
):
|
||||
assert detail not in initializer_source
|
||||
assert detail in agent_source
|
||||
assert "AgentChatRuntime(" not in initializer_source
|
||||
assert "AgentChatRuntime(" not in agent_source
|
||||
assert "AgentChatRuntime(" in runtime_source
|
||||
assert "runtime.worker.snapshot().capacity" in agent_source
|
||||
assert "startup.initializers" not in agent_source
|
||||
|
||||
|
||||
def test_runtime_composition_is_the_sole_host_and_domain_runtime_owner():
|
||||
"""HostRuntime、领域 Runtime 与 ApiData 投影只能由单词型 runtime owner 构造。"""
|
||||
initializer_source = (APP_ROOT / "startup" / "initializers" / "modules.py").read_text(encoding="utf-8")
|
||||
runtime_path = APP_ROOT / "startup" / "composition" / "runtime.py"
|
||||
runtime_source = runtime_path.read_text(encoding="utf-8")
|
||||
database_source = (APP_ROOT / "startup" / "composition" / "database.py").read_text(encoding="utf-8")
|
||||
package_tree = ast.parse((APP_ROOT / "startup" / "composition" / "__init__.py").read_text(encoding="utf-8"))
|
||||
|
||||
assert runtime_path.is_file()
|
||||
assert not list(runtime_path.parent.glob("runtime_*.py"))
|
||||
assert "compose_runtime_dependencies()" in initializer_source
|
||||
assert "compose_runtime(" in initializer_source
|
||||
assert "RuntimeInputs(" in initializer_source
|
||||
assert "publish_runtime(runtime_composition)" in initializer_source
|
||||
assert "HostRuntime(" not in initializer_source
|
||||
assert "ApiDataPorts(" not in initializer_source
|
||||
assert "ApiDataPorts(" not in database_source
|
||||
for detail in (
|
||||
"class RuntimeDependencies:",
|
||||
"class RuntimeInputs:",
|
||||
"class RuntimeComposition:",
|
||||
"HostRuntime(",
|
||||
"AgentChatRuntime(",
|
||||
"AuthenticationRuntime(",
|
||||
"PersistenceRuntime(",
|
||||
"MessagingRuntime(",
|
||||
"HistoryRuntime(",
|
||||
"SiteRuntime(",
|
||||
"SubscriptionRuntime(",
|
||||
"WorkflowRuntime(",
|
||||
"ApiDataPorts(",
|
||||
):
|
||||
assert detail in runtime_source
|
||||
constructors = []
|
||||
for path in APP_ROOT.rglob("*.py"):
|
||||
if path.is_relative_to(APP_ROOT / "plugins"):
|
||||
continue
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
if any(
|
||||
isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "HostRuntime"
|
||||
for node in ast.walk(tree)
|
||||
):
|
||||
constructors.append(path.relative_to(APP_ROOT).as_posix())
|
||||
assert constructors == ["startup/composition/runtime.py"]
|
||||
context_tree = ast.parse((APP_ROOT / "startup" / "composition" / "context.py").read_text(encoding="utf-8"))
|
||||
host_runtime = next(
|
||||
node for node in context_tree.body if isinstance(node, ast.ClassDef) and node.name == "HostRuntime"
|
||||
)
|
||||
task_field = next(
|
||||
node
|
||||
for node in host_runtime.body
|
||||
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id == "tasks"
|
||||
)
|
||||
assert task_field.value is None
|
||||
assert (
|
||||
_class_annotations(
|
||||
APP_ROOT / "startup" / "composition" / "context.py",
|
||||
"AuthenticationRuntime",
|
||||
)["passkey_repository"]
|
||||
== "RepositoryFactory"
|
||||
)
|
||||
assert not any(
|
||||
isinstance(node, (ast.Import, ast.ImportFrom, ast.FunctionDef, ast.ClassDef)) for node in package_tree.body
|
||||
)
|
||||
|
||||
|
||||
def test_modules_initializer_delegates_chain_composition():
|
||||
"""Chain 上下文、壁纸与旧整理命令只由单词型 composition owner 装配。"""
|
||||
initializer_source = (
|
||||
APP_ROOT / "startup" / "initializers" / "modules.py"
|
||||
).read_text(encoding="utf-8")
|
||||
initializer_source = (APP_ROOT / "startup" / "initializers" / "modules.py").read_text(encoding="utf-8")
|
||||
composition_path = APP_ROOT / "startup" / "composition" / "chain.py"
|
||||
composition_source = composition_path.read_text(encoding="utf-8")
|
||||
composition_tree = ast.parse(composition_source, filename=str(composition_path))
|
||||
package_tree = ast.parse(
|
||||
(APP_ROOT / "startup" / "composition" / "__init__.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
architecture_source = (
|
||||
PROJECT_ROOT / "docs" / "rules" / "05-architecture.md"
|
||||
).read_text(encoding="utf-8")
|
||||
package_tree = ast.parse((APP_ROOT / "startup" / "composition" / "__init__.py").read_text(encoding="utf-8"))
|
||||
architecture_source = (PROJECT_ROOT / "docs" / "rules" / "05-architecture.md").read_text(encoding="utf-8")
|
||||
runtime_source = (APP_ROOT / "startup" / "composition" / "runtime.py").read_text(encoding="utf-8")
|
||||
|
||||
assert composition_path.is_file()
|
||||
assert not list(composition_path.parent.glob("chain_*.py"))
|
||||
@@ -958,32 +970,27 @@ def test_modules_initializer_delegates_chain_composition():
|
||||
):
|
||||
assert detail not in initializer_source
|
||||
assert detail in composition_source
|
||||
assert initializer_source.count(
|
||||
"TransactionalTransferExecutionRepository("
|
||||
) == 1
|
||||
assert "transfer_execution=transfer_execution_repository" in initializer_source
|
||||
assert "transfer_execution_repository=transfer_execution" in composition_source
|
||||
assert "TransactionalTransferExecutionRepository(" not in initializer_source
|
||||
assert runtime_source.count("TransactionalTransferExecutionRepository(") == 1
|
||||
assert "dependencies=runtime_dependencies" in initializer_source
|
||||
assert "transfer_execution_repository=dependencies.transfer_execution" in composition_source
|
||||
assert "TransactionalTransferExecutionRepository" not in composition_source
|
||||
transfer_function = next(
|
||||
node
|
||||
for node in composition_tree.body
|
||||
if isinstance(node, ast.FunctionDef)
|
||||
and node.name == "execute_legacy_transfer_command"
|
||||
if isinstance(node, ast.FunctionDef) and node.name == "execute_legacy_transfer_command"
|
||||
)
|
||||
assert any(
|
||||
isinstance(node, ast.ImportFrom)
|
||||
and node.module == "app.chain.transfer.facade"
|
||||
isinstance(node, ast.ImportFrom) and node.module == "app.chain.transfer.facade"
|
||||
for node in ast.walk(transfer_function)
|
||||
)
|
||||
assert not any(
|
||||
isinstance(node, ast.ImportFrom)
|
||||
and node.module == "app.chain.transfer.facade"
|
||||
isinstance(node, ast.ImportFrom) and node.module == "app.chain.transfer.facade"
|
||||
for node in composition_tree.body
|
||||
)
|
||||
assert "`startup/composition/chain.py` owns" in architecture_source
|
||||
assert not any(
|
||||
isinstance(node, (ast.Import, ast.ImportFrom, ast.FunctionDef, ast.ClassDef))
|
||||
for node in package_tree.body
|
||||
isinstance(node, (ast.Import, ast.ImportFrom, ast.FunctionDef, ast.ClassDef)) for node in package_tree.body
|
||||
)
|
||||
|
||||
|
||||
@@ -1181,16 +1188,12 @@ def test_user_chain_and_agent_ports_are_typed_and_orm_free():
|
||||
|
||||
def test_startup_injects_user_adapter_instead_of_raw_oper():
|
||||
"""启动组合根不得把无会话 UserOper 注入宿主查询调用面。"""
|
||||
initializer_source = (
|
||||
APP_ROOT / "startup" / "initializers" / "modules.py"
|
||||
).read_text(encoding="utf-8-sig")
|
||||
database_source = (
|
||||
APP_ROOT / "startup" / "composition" / "database.py"
|
||||
).read_text(encoding="utf-8-sig")
|
||||
chain_source = (
|
||||
APP_ROOT / "startup" / "composition" / "chain.py"
|
||||
).read_text(encoding="utf-8-sig")
|
||||
source = initializer_source + database_source + chain_source
|
||||
initializer_source = (APP_ROOT / "startup" / "initializers" / "modules.py").read_text(encoding="utf-8-sig")
|
||||
database_source = (APP_ROOT / "startup" / "composition" / "database.py").read_text(encoding="utf-8-sig")
|
||||
chain_source = (APP_ROOT / "startup" / "composition" / "chain.py").read_text(encoding="utf-8-sig")
|
||||
runtime_source = (APP_ROOT / "startup" / "composition" / "runtime.py").read_text(encoding="utf-8-sig")
|
||||
security_source = (APP_ROOT / "startup" / "composition" / "security.py").read_text(encoding="utf-8-sig")
|
||||
source = initializer_source + database_source + chain_source + runtime_source + security_source
|
||||
|
||||
assert "TransactionalUserRepository" in source
|
||||
assert "SqlAlchemyUserRepository" in source
|
||||
@@ -1953,8 +1956,7 @@ def test_host_code_imports_agent_and_llm_symbols_from_owner_modules():
|
||||
imported = {
|
||||
f"{node.module}.{alias.name}"
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom)
|
||||
and node.module in {"app.agent", "app.agent.llm"}
|
||||
if isinstance(node, ast.ImportFrom) and node.module in {"app.agent", "app.agent.llm"}
|
||||
for alias in node.names
|
||||
}
|
||||
if imported:
|
||||
@@ -2192,9 +2194,7 @@ def test_web_agent_non_transport_state_stays_in_application():
|
||||
"app.runtime.stop",
|
||||
}
|
||||
endpoint_imports = {
|
||||
node.module
|
||||
for node in ast.walk(endpoint_tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module
|
||||
node.module for node in ast.walk(endpoint_tree) if isinstance(node, ast.ImportFrom) and node.module
|
||||
}
|
||||
|
||||
assert required <= application_owners
|
||||
|
||||
@@ -11,6 +11,7 @@ from app.application.chain.context import ChainRuntimeContext
|
||||
from app.application.configuration import ChainRuntimeConfig
|
||||
from app.chain.base import ChainBase
|
||||
from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher
|
||||
from app.startup.composition.runtime import RuntimeDependencies
|
||||
|
||||
|
||||
def _context() -> ChainRuntimeContext:
|
||||
@@ -125,15 +126,19 @@ def test_chain_composition_registers_lazy_compatibility_provider(monkeypatch) ->
|
||||
context = _context()
|
||||
builder = Mock(return_value=context)
|
||||
register = Mock()
|
||||
dependencies = {
|
||||
"message_helper": Mock(),
|
||||
"message_queue": Mock(),
|
||||
runtime_dependencies = RuntimeDependencies(
|
||||
download_history=Mock(),
|
||||
transfer_history=Mock(),
|
||||
site=Mock(),
|
||||
subscription=Mock(),
|
||||
subscription_history=Mock(),
|
||||
transfer_execution=Mock(),
|
||||
message_helper=Mock(),
|
||||
message_queue=Mock(),
|
||||
)
|
||||
inputs = {
|
||||
"dependencies": runtime_dependencies,
|
||||
"system_config": Mock(),
|
||||
"site": Mock(),
|
||||
"subscription": Mock(),
|
||||
"download_history": Mock(),
|
||||
"transfer_history": Mock(),
|
||||
"transfer_execution": Mock(),
|
||||
"configuration": Mock(),
|
||||
}
|
||||
monkeypatch.setattr(chain_composition, "build_chain_runtime_context", builder)
|
||||
@@ -143,12 +148,12 @@ def test_chain_composition_registers_lazy_compatibility_provider(monkeypatch) ->
|
||||
register,
|
||||
)
|
||||
|
||||
chain_composition.configure_chain_runtime_context(**dependencies)
|
||||
chain_composition.configure_chain_runtime_context(**inputs)
|
||||
|
||||
builder.assert_not_called()
|
||||
provider = register.call_args.args[0]
|
||||
assert provider() is context
|
||||
builder.assert_called_once_with(**dependencies)
|
||||
builder.assert_called_once_with(**inputs)
|
||||
|
||||
|
||||
def test_chain_composition_keeps_legacy_transfer_import_lazy(monkeypatch) -> None:
|
||||
|
||||
@@ -286,7 +286,7 @@ async def test_modules_startup_failure_stops_database_runtime(monkeypatch) -> No
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_modules_startup_failure_preserves_original_error_when_cleanup_fails(
|
||||
monkeypatch,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
"""数据库任务清理失败时仍向上层保留原始启动异常。"""
|
||||
startup_error = RuntimeError("startup failed")
|
||||
@@ -343,7 +343,7 @@ async def test_database_worker_owner_is_retained_when_shutdown_fails(monkeypatch
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_database_runtime_rejects_parallel_start_and_allows_next_lifespan(
|
||||
monkeypatch,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
"""数据库 runtime 在启动中拒绝重入,成功关闭后才允许新 lifespan。"""
|
||||
started = asyncio.Event()
|
||||
@@ -401,7 +401,7 @@ async def test_database_runtime_rejects_parallel_start_and_allows_next_lifespan(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_database_runtime_start_failure_does_not_publish_transaction_runners(
|
||||
monkeypatch,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
"""Worker 启动失败时不得留下可调用的无会话事务入口。"""
|
||||
published = []
|
||||
@@ -461,7 +461,6 @@ async def test_database_service_reset_revokes_transaction_runners() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_startup_failure_revokes_published_database_services(monkeypatch) -> None:
|
||||
"""后续启动阶段失败并关闭 worker 后不得留下已失效的全局服务。"""
|
||||
from app.api.data import get_api_data_ports
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.application.plugin.transaction import get_plugin_persistence
|
||||
from app.application.query import get_configured_data_query_service
|
||||
@@ -475,7 +474,6 @@ async def test_startup_failure_revokes_published_database_services(monkeypatch)
|
||||
settings=SimpleNamespace(update=lambda _key, _value: (True, "")),
|
||||
)
|
||||
database = SimpleNamespace(
|
||||
api_data=object(),
|
||||
data_query=object(),
|
||||
plugin_persistence=object(),
|
||||
workflow_query=object(),
|
||||
@@ -503,7 +501,6 @@ async def test_startup_failure_revokes_published_database_services(monkeypatch)
|
||||
get_configured_data_query_service,
|
||||
get_plugin_persistence,
|
||||
get_configured_workflow_query,
|
||||
get_api_data_ports,
|
||||
):
|
||||
with pytest.raises(RuntimeError):
|
||||
getter()
|
||||
|
||||
@@ -23,6 +23,7 @@ from app.application.configuration import (
|
||||
RuntimeSettingsService,
|
||||
SchedulerRuntimeConfig,
|
||||
)
|
||||
from app.runtime.tasks import TaskRegistry
|
||||
from app.startup import lifecycle
|
||||
from app.startup.composition.context import (
|
||||
AgentChatRuntime,
|
||||
@@ -90,7 +91,6 @@ class _Outbox:
|
||||
"""模拟暂存 durable intent。"""
|
||||
|
||||
|
||||
|
||||
class _DispatchStore:
|
||||
"""提供订阅即时副作用所需的独立派发存储替身。"""
|
||||
|
||||
@@ -125,6 +125,7 @@ class _RuntimeSettings:
|
||||
|
||||
def _runtime() -> HostRuntime:
|
||||
"""构造不加载数据库引擎或 PluginManager 的假宿主运行时。"""
|
||||
|
||||
async def async_session():
|
||||
"""生成一个可被 FastAPI 依赖缓存的会话标记。"""
|
||||
yield object()
|
||||
@@ -165,6 +166,7 @@ def _runtime() -> HostRuntime:
|
||||
),
|
||||
authentication=AuthenticationRuntime(
|
||||
user_repository=_Repository,
|
||||
passkey_repository=_Repository,
|
||||
standalone_user=lambda: _Repository(object()),
|
||||
system_config=lambda: _Repository(object()),
|
||||
passkey=lambda: _Repository(object()),
|
||||
@@ -205,12 +207,29 @@ def _runtime() -> HostRuntime:
|
||||
configuration=RuntimeConfiguration(
|
||||
api=lambda: ApiRuntimeConfig(False, 60, False, True),
|
||||
scheduler=lambda: SchedulerRuntimeConfig(
|
||||
False, "Asia/Shanghai", 1, False, "", None, None,
|
||||
False, 24, "rss", 30, False, None, None, True, 1, False, None,
|
||||
False,
|
||||
"Asia/Shanghai",
|
||||
1,
|
||||
False,
|
||||
"",
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
24,
|
||||
"rss",
|
||||
30,
|
||||
False,
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
1,
|
||||
False,
|
||||
None,
|
||||
),
|
||||
chain=lambda: ChainRuntimeConfig(media_extensions=(".mkv",)),
|
||||
),
|
||||
settings=RuntimeSettingsService(_RuntimeSettings()),
|
||||
tasks=TaskRegistry(),
|
||||
)
|
||||
|
||||
|
||||
@@ -260,17 +279,13 @@ def test_string_api_data_locator_is_confined_to_compatibility_boundary() -> None
|
||||
if path.is_relative_to(PROJECT_ROOT / "app" / "plugins"):
|
||||
continue
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
imported_modules = {
|
||||
node.module
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module
|
||||
}
|
||||
imported_modules = {node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) and node.module}
|
||||
if imported_modules & {"app.api.data", "app.api.dependencies.data"}:
|
||||
importers.add(path.relative_to(PROJECT_ROOT).as_posix())
|
||||
|
||||
assert importers == {
|
||||
"app/api/dependencies/data.py",
|
||||
"app/startup/composition/database.py",
|
||||
"app/startup/composition/runtime.py",
|
||||
}
|
||||
|
||||
|
||||
@@ -293,7 +308,7 @@ async def test_lifecycle_component_attaches_init_modules_result(monkeypatch) ->
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifecycle_component_revokes_host_runtime_after_converged_stop(
|
||||
monkeypatch,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
"""模块 owner 完整关闭后 AppState 不得继续暴露上一 lifespan 的运行时。"""
|
||||
app = FastAPI()
|
||||
@@ -307,7 +322,7 @@ async def test_lifecycle_component_revokes_host_runtime_after_converged_stop(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifecycle_component_retains_host_runtime_after_failed_stop(
|
||||
monkeypatch,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
"""模块 owner 未收敛时保留 HostRuntime,供诊断与后续重试。"""
|
||||
app = FastAPI()
|
||||
|
||||
229
tests/test_runtime_composition.py
Normal file
229
tests/test_runtime_composition.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""宿主运行时组合根的对象身份、发布与冷导入测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
from app.runtime.tasks import TaskRegistry
|
||||
from app.startup.composition import runtime as runtime_composition
|
||||
|
||||
|
||||
def _inputs() -> runtime_composition.RuntimeInputs:
|
||||
"""构造不连接数据库、不启动线程的最小运行时输入。"""
|
||||
dependencies = runtime_composition.RuntimeDependencies(
|
||||
download_history=Mock(name="download_history"),
|
||||
transfer_history=Mock(name="transfer_history"),
|
||||
site=Mock(name="site"),
|
||||
subscription=Mock(name="subscription"),
|
||||
subscription_history=Mock(name="subscription_history"),
|
||||
transfer_execution=Mock(name="transfer_execution"),
|
||||
message_helper=Mock(name="message_helper"),
|
||||
message_queue=Mock(name="message_queue"),
|
||||
)
|
||||
system_config = Mock(name="system_config")
|
||||
configuration = SimpleNamespace(
|
||||
system_config=system_config,
|
||||
runtime=Mock(name="runtime_configuration"),
|
||||
settings=Mock(name="runtime_settings"),
|
||||
)
|
||||
database = SimpleNamespace(workflow_query=Mock(name="workflow_query"))
|
||||
agent_data = SimpleNamespace(
|
||||
sites=dependencies.site,
|
||||
subscriptions=dependencies.subscription,
|
||||
subscription_history=dependencies.subscription_history,
|
||||
transfer_history=dependencies.transfer_history,
|
||||
transfer_execution=dependencies.transfer_execution,
|
||||
download_history=dependencies.download_history,
|
||||
)
|
||||
agent = SimpleNamespace(
|
||||
data=agent_data,
|
||||
chat_repository=Mock(name="chat_repository"),
|
||||
persistence=Mock(name="chat_persistence"),
|
||||
)
|
||||
authentication = SimpleNamespace(
|
||||
user_repository=Mock(name="user_repository"),
|
||||
passkey_repository=Mock(name="passkey_repository"),
|
||||
standalone_user=Mock(name="standalone_user"),
|
||||
system_config=Mock(name="standalone_system_config"),
|
||||
passkey=Mock(name="standalone_passkey"),
|
||||
)
|
||||
return runtime_composition.RuntimeInputs(
|
||||
configuration=configuration,
|
||||
database=database,
|
||||
agent=agent,
|
||||
authentication=authentication,
|
||||
dependencies=dependencies,
|
||||
tasks=TaskRegistry(),
|
||||
)
|
||||
|
||||
|
||||
def test_runtime_composition_reuses_dependencies_and_projects_api_identity() -> None:
|
||||
"""HostRuntime、Agent、Chain 依赖与旧 API 投影必须保持同一对象身份。"""
|
||||
inputs = _inputs()
|
||||
|
||||
composition = runtime_composition.compose_runtime(inputs)
|
||||
runtime = composition.runtime
|
||||
ports = composition.api_data
|
||||
|
||||
assert composition.dependencies is inputs.dependencies
|
||||
assert runtime.agent is inputs.agent.data
|
||||
assert runtime.agent_chat.repository is inputs.agent.chat_repository
|
||||
assert runtime.agent_chat.persistence is inputs.agent.persistence
|
||||
assert runtime.authentication.user_repository is inputs.authentication.user_repository
|
||||
assert runtime.authentication.passkey_repository is inputs.authentication.passkey_repository
|
||||
assert runtime.configuration is inputs.configuration.runtime
|
||||
assert runtime.settings is inputs.configuration.settings
|
||||
assert runtime.tasks is inputs.tasks
|
||||
assert runtime.messaging.helper is inputs.dependencies.message_helper
|
||||
assert runtime.messaging.queue is inputs.dependencies.message_queue
|
||||
assert runtime.history.transfer_repository is inputs.dependencies.transfer_history
|
||||
assert runtime.history.transfer_execution_repository is inputs.dependencies.transfer_execution
|
||||
assert runtime.site.standalone is inputs.dependencies.site
|
||||
assert inputs.agent.data.transfer_execution is runtime.history.transfer_execution_repository
|
||||
assert inputs.agent.data.sites is runtime.site.standalone
|
||||
assert ports.sync_session is runtime.persistence.sync_session
|
||||
assert ports.async_session is runtime.persistence.async_session
|
||||
assert ports.repositories == {
|
||||
"download_history": runtime.history.download_repository,
|
||||
"media_server": runtime.history.media_server_repository,
|
||||
"message": runtime.messaging.repository,
|
||||
"passkey": runtime.authentication.passkey_repository,
|
||||
"site": runtime.site.repository,
|
||||
"subscribe": runtime.subscription.repository,
|
||||
"subscribe_history": runtime.subscription.history_repository,
|
||||
"user": runtime.authentication.user_repository,
|
||||
"workflow": runtime.workflow.repository,
|
||||
}
|
||||
assert ports.standalone == {
|
||||
"passkey": runtime.authentication.passkey,
|
||||
"system_config": runtime.authentication.system_config,
|
||||
"user": runtime.authentication.standalone_user,
|
||||
}
|
||||
assert ports.unit_of_work == {
|
||||
"async": runtime.persistence.async_transaction,
|
||||
"sync": runtime.persistence.sync_transaction,
|
||||
}
|
||||
assert composition.site_query._repository is inputs.dependencies.site
|
||||
assert composition.site_health._repository is inputs.dependencies.site
|
||||
|
||||
|
||||
def test_runtime_composition_publishes_and_resets_exact_projection(monkeypatch) -> None:
|
||||
"""运行时 owner 应发布并按对称顺序撤销其拥有的全部投影。"""
|
||||
composition = runtime_composition.compose_runtime(_inputs())
|
||||
published: dict[str, object] = {}
|
||||
reset_order: list[str] = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
runtime_composition,
|
||||
"configure_api_data_runtime",
|
||||
lambda value: published.update(api=value),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
runtime_composition,
|
||||
"configure_transfer_history_repository",
|
||||
lambda value: published.update(transfer=value),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
runtime_composition,
|
||||
"configure_site_query_service",
|
||||
lambda value: published.update(site_query=value),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
runtime_composition,
|
||||
"configure_site_health_service",
|
||||
lambda value: published.update(site_health=value),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
runtime_composition,
|
||||
"reset_site_health_service",
|
||||
lambda: reset_order.append("site_health"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
runtime_composition,
|
||||
"reset_site_query_service",
|
||||
lambda: reset_order.append("site_query"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
runtime_composition,
|
||||
"reset_transfer_history_repository",
|
||||
lambda: reset_order.append("transfer_history"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
runtime_composition,
|
||||
"reset_api_data_runtime",
|
||||
lambda: reset_order.append("api_data"),
|
||||
)
|
||||
|
||||
runtime_composition.publish_runtime(composition)
|
||||
runtime_composition.reset_runtime()
|
||||
|
||||
assert published["api"] is composition.api_data
|
||||
assert published["transfer"]() is composition.dependencies.transfer_history
|
||||
assert published["site_query"] is composition.site_query
|
||||
assert published["site_health"] is composition.site_health
|
||||
assert reset_order == [
|
||||
"site_health",
|
||||
"site_query",
|
||||
"transfer_history",
|
||||
"api_data",
|
||||
]
|
||||
|
||||
|
||||
def test_runtime_composition_import_is_cold() -> None:
|
||||
"""导入 runtime owner 不得加载 initializer、启动 worker/队列或发布端口。"""
|
||||
completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
r"""
|
||||
import json
|
||||
import sys
|
||||
|
||||
from app.api.data import get_api_data_ports
|
||||
from app.application.history import get_transfer_history_repository
|
||||
from app.application.messaging.message import MessageHelper, MessageQueueManager
|
||||
from app.application.site.health import get_configured_site_health_service
|
||||
from app.application.site.query import get_configured_site_query_service
|
||||
|
||||
import app.startup.composition.runtime
|
||||
|
||||
unconfigured = []
|
||||
for getter in (
|
||||
get_api_data_ports,
|
||||
get_transfer_history_repository,
|
||||
get_configured_site_health_service,
|
||||
get_configured_site_query_service,
|
||||
):
|
||||
try:
|
||||
getter()
|
||||
except RuntimeError:
|
||||
unconfigured.append(True)
|
||||
|
||||
print(json.dumps({
|
||||
"database_worker_loaded": "app.db.worker" in sys.modules,
|
||||
"initializer_loaded": any(
|
||||
name == "app.startup.initializers" or name.startswith("app.startup.initializers.")
|
||||
for name in sys.modules
|
||||
),
|
||||
"message_helper_created": MessageHelper.get_existing_instance() is not None,
|
||||
"message_queue_created": MessageQueueManager.get_existing_instance() is not None,
|
||||
"unconfigured": len(unconfigured),
|
||||
}))
|
||||
""",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert json.loads(completed.stdout) == {
|
||||
"database_worker_loaded": False,
|
||||
"initializer_loaded": False,
|
||||
"message_helper_created": False,
|
||||
"message_queue_created": False,
|
||||
"unconfigured": 4,
|
||||
}
|
||||
Reference in New Issue
Block a user