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