diff --git a/AGENTS.md b/AGENTS.md
index ce72eb44a..cd5eb994e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -60,8 +60,8 @@ The legacy roots have no physical directories in the source tree. Current images
|---|---|---|---|
| `app/foundation/` | 无状态、无配置和无 I/O 的底层机制:反射/动态导入、加密、DOM、身份、集合、单例、文本、URL 和版本比较 | `settings`、DB/SystemConfig、网络请求、运行日志、MoviePilot 业务规则、旧导入路径 | `reflection.py`, `crypto.py`, `collections.py`, `text.py`, `url.py` |
| `app/domain/` | Pure MoviePilot business semantics and models for media, recognition, sites, and torrents | Persistence, global settings reads, network/filesystem clients, Rust imports, service discovery, process lifecycle | `context.py`, `media.py`, `metainfo.py`, `scraper.py`, `meta/` |
-| `app/runtime/` | 进程级运行机制和策略:配置、事件、完整日志、缓存契约/内存行为、托管资源门面、并发、调度、限流、本地化、GC 和重启状态 | 具体外部产品、业务流程、Redis/文件缓存实现 | `config.py`, `events.py`, `log.py`, `cache.py`, `managed_resources.py`, `thread.py`, `state.py` |
-| `app/runtime/extensions/` | 模块、插件、配置化服务和托管资源实现的发现、注册与生命周期适配 | 通用反射机制、插件公开 API、无关业务流程 | `module_manager.py`, `plugin_manager.py`, `managed_resource_adapter.py`, `service_registry.py` |
+| `app/runtime/` | 进程级运行机制和策略:配置、事件、完整日志、缓存契约/内存行为、托管资源门面、并发、调度、限流、本地化、GC 和重启状态 | 具体外部产品、业务流程、Redis/文件缓存实现 | `config.py`, `events.py`, `log.py`, `cache.py`, `resources.py`, `thread.py`, `state.py` |
+| `app/runtime/extensions/` | 模块、插件、配置化服务和托管资源实现的发现、注册与生命周期适配 | 通用反射机制、插件公开 API、无关业务流程 | `module_manager.py`, `plugin_manager.py`, `resource.py`, `service_registry.py` |
| `app/adapters/network/` | HTTP、浏览器、DNS、Cloudflare 和 IP 等通用网络技术适配 | RSS/站点业务编排、身份认证策略、命名外部产品流程 | `http.py`, `browser.py`, `doh.py`, `ip.py` |
| `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` |
diff --git a/app/adapters/network/browser.py b/app/adapters/network/browser.py
index 8d9e473df..aae633c65 100644
--- a/app/adapters/network/browser.py
+++ b/app/adapters/network/browser.py
@@ -10,7 +10,7 @@ from urllib.parse import urlparse
from app.adapters.network.http import RequestUtils, cookie_parse
from app.runtime.log import logger
-from app.runtime.managed_resources import (
+from app.runtime.resources import (
acquire_managed_resource,
acquire_managed_resource_async,
)
diff --git a/app/adapters/system/display/__init__.py b/app/adapters/system/display/__init__.py
index f64aa0797..cc27999d6 100644
--- a/app/adapters/system/display/__init__.py
+++ b/app/adapters/system/display/__init__.py
@@ -7,12 +7,11 @@ from typing import Any
from app.foundation.singleton import Singleton
from app.runtime.log import logger
-from app.runtime.managed_resources import (
+from app.runtime.resources import (
acquire_managed_resource,
stop_managed_resource,
)
-
DISPLAY_CAPABILITY_ID = "host.display"
diff --git a/app/agent/tools/impl/delete_transfer_history.py b/app/agent/tools/impl/delete_transfer_history.py
index 3e43128ac..b28d0213e 100644
--- a/app/agent/tools/impl/delete_transfer_history.py
+++ b/app/agent/tools/impl/delete_transfer_history.py
@@ -9,7 +9,7 @@ from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.application.agentdata import get_agent_transfer_history_port
from app.application.chain.data import get_chain_transfer_execution_port
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionCommand,
TransferRetryRequestResult,
)
diff --git a/app/api/endpoints/history.py b/app/api/endpoints/history.py
index 973feeaf4..8fa343014 100644
--- a/app/api/endpoints/history.py
+++ b/app/api/endpoints/history.py
@@ -35,7 +35,7 @@ from app.application.history import (
HistoryQueryService,
TransferHistoryMutationCommand,
)
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionCommand,
TransferRetryRequestResult,
)
diff --git a/app/api/endpoints/transfer.py b/app/api/endpoints/transfer.py
index 1caf9314c..ba6575e86 100644
--- a/app/api/endpoints/transfer.py
+++ b/app/api/endpoints/transfer.py
@@ -11,7 +11,7 @@ from app.application.chain.data import get_chain_transfer_execution_port
from app.application.configuration import get_api_runtime_config_snapshot
from app.application.directory import DirectoryHelper
from app.application.history import TransferHistoryLookupService
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionCommand,
TransferExecutionConflictError,
TransferExecutionState,
diff --git a/app/application/chain/context.py b/app/application/chain/context.py
index 3f8e45986..465e0dcc6 100644
--- a/app/application/chain/context.py
+++ b/app/application/chain/context.py
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
from typing import Any, Optional
from app.application.chain.data import ChainDataPorts
-from app.application.chain.durable_events import ChainDurableEventWriter
+from app.application.chain.events import ChainDurableEventWriter
from app.application.configuration import ChainRuntimeConfig
from app.runtime.stop import StopState, runtime_stop_state
diff --git a/app/application/chain/data.py b/app/application/chain/data.py
index f5fd5addf..92c92ea75 100644
--- a/app/application/chain/data.py
+++ b/app/application/chain/data.py
@@ -10,9 +10,8 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Optional
-from app.application.transfer import TransferAdmissionRepository
-from app.application.transfer_execution import TransferExecutionRepository
-
+from app.application.transfer.execution import TransferExecutionRepository
+from app.application.transfer.workflow import TransferAdmissionRepository
OperFactory = Callable[[], Any]
TransferAdmissionRepositoryFactory = Callable[[], TransferAdmissionRepository]
diff --git a/app/application/chain/durable_events.py b/app/application/chain/events.py
similarity index 96%
rename from app/application/chain/durable_events.py
rename to app/application/chain/events.py
index 848f7337e..21300e4f8 100644
--- a/app/application/chain/durable_events.py
+++ b/app/application/chain/events.py
@@ -1,4 +1,4 @@
-"""Chain durable 事件的事务写端口与可重放 payload 转换。"""
+"""Chain 持久事件的事务写端口与可重放 payload 转换。"""
from __future__ import annotations
@@ -11,7 +11,7 @@ from typing import Any, Protocol, cast
from uuid import uuid4
from app.application.history import TransferHistoryRecord, TransferHistoryWriter
-from app.application.transfer_execution import TransferSettlementResult
+from app.application.transfer.execution import TransferSettlementResult
from app.domain.context import Context, MediaInfo, MusicInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
@@ -108,7 +108,11 @@ def snapshot_download_added(payload: dict[str, Any]) -> dict[str, Any]:
context = payload.get("context")
return cast(dict[str, Any], _json_snapshot({
"hash": payload.get("hash"),
- "context": context.to_dict() if isinstance(context, Context) else context,
+ "context": (
+ cast(Callable[[], dict[str, Any]], context.to_dict)()
+ if isinstance(context, Context)
+ else context
+ ),
"username": payload.get("username"),
"downloader": payload.get("downloader"),
"episodes": list(payload.get("episodes") or []),
@@ -211,11 +215,11 @@ def _restore_context(payload: dict[str, Any]) -> Context:
media_info=(
_restore_media(media_payload) if isinstance(media_payload, dict) else None
),
- torrent_info=(
+ torrent_info=cast(TorrentInfo, (
_restore_torrent(torrent_payload)
if isinstance(torrent_payload, dict)
else None
- ),
+ )),
media_recognize_fail_count=int(
payload.get("media_recognize_fail_count") or 0
),
diff --git a/app/application/transfer/__init__.py b/app/application/transfer/__init__.py
new file mode 100644
index 000000000..fec0373bc
--- /dev/null
+++ b/app/application/transfer/__init__.py
@@ -0,0 +1 @@
+"""整理应用能力包,分离任务工作流与持久执行状态机职责。"""
diff --git a/app/application/transfer_execution.py b/app/application/transfer/execution.py
similarity index 100%
rename from app/application/transfer_execution.py
rename to app/application/transfer/execution.py
diff --git a/app/application/transfer.py b/app/application/transfer/workflow.py
similarity index 90%
rename from app/application/transfer.py
rename to app/application/transfer/workflow.py
index 8e165a174..ffa962592 100644
--- a/app/application/transfer.py
+++ b/app/application/transfer/workflow.py
@@ -22,6 +22,7 @@ from dataclasses import dataclass, field
from pathlib import Path
from time import monotonic
from typing import (
+ TYPE_CHECKING,
Any,
Callable,
Dict,
@@ -31,27 +32,29 @@ from typing import (
Tuple,
TypeAlias,
Union,
+ cast,
)
from pydantic import BaseModel, ConfigDict, PrivateAttr
from app.adapters.system.host import SystemUtils
from app.application.agent import get_prompt_manager, get_running_agent_manager
-from app.application.transfer_execution import TransferExecutionCheckpoint
+from app.application.transfer.execution import TransferExecutionCheckpoint
from app.domain.context import MediaInfo, MusicInfo
from app.domain.media import normalize_music_type
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.foundation import text as text_tools
from app.runtime.log import logger
+from app.schemas.context import MediaInfo as _SchemaMediaInfo
+from app.schemas.context import MetaInfo as _SchemaMetaInfo
from app.schemas.file import FileItem
from app.schemas.history import DownloadHistory
from app.schemas.media import OptionalMediaIdentityMixin, resolve_media_identity
+from app.schemas.music import MusicInfo as _SchemaMusicInfo
+from app.schemas.music import MusicMeta as _SchemaMusicMeta
from app.schemas.system import TransferDirectoryConf
from app.schemas.tmdb import TmdbEpisode
-from app.schemas.transfer import MetaInfo as _SchemaMetaInfo
-from app.schemas.transfer import MusicInfo as _SchemaMusicInfo
-from app.schemas.transfer import MusicMeta as _SchemaMusicMeta
from app.schemas.transfer import TransferInfo, TransferJob, TransferJobTask
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
@@ -60,9 +63,61 @@ from app.schemas.types import (
MediaType,
ReplyMode,
)
-from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
+
+if TYPE_CHECKING:
+ class _ApplicationModel:
+ """描述当前模块依赖的最小 Pydantic 模型类型形状。"""
+
+ def __init__(self, **data: Any) -> None:
+ """接受模型字段关键字参数。"""
+ raise NotImplementedError
+
+ def model_dump(self, **kwargs: Any) -> dict[str, Any]:
+ """返回模型字段字典。"""
+ raise NotImplementedError
+else:
+ _ApplicationModel = BaseModel
JSONValue: TypeAlias = Union[None, bool, int, float, str, list["JSONValue"], dict[str, "JSONValue"]]
+JobId: TypeAlias = tuple[object, ...]
+FileKey: TypeAlias = tuple[str, str]
+
+
+class _DictionarySerializable(Protocol):
+ """描述领域对象沿用的字典投影能力。"""
+
+ def to_dict(self) -> dict[str, Any]:
+ """返回领域对象的字典投影。"""
+
+
+def _domain_to_dict(value: object) -> dict[str, Any]:
+ """按领域对象既有 ``to_dict`` 合同生成字典投影。"""
+ return cast(_DictionarySerializable, value).to_dict()
+
+
+def _job_tasks(job: TransferJob) -> list[TransferJobTask]:
+ """声明进程内作业始终使用已初始化的任务列表。"""
+ return cast(list[TransferJobTask], job.tasks)
+
+
+def _job_task_fileitem(task: TransferJobTask) -> FileItem:
+ """声明进程内作业任务始终绑定源文件。"""
+ return cast(FileItem, task.fileitem)
+
+
+def _job_task_size(task: TransferJobTask) -> int:
+ """按既有本地目录回退规则返回已完成任务的文件大小。"""
+ fileitem = _job_task_fileitem(task)
+ if fileitem.size is not None:
+ return fileitem.size
+ if fileitem.storage == "local":
+ return SystemUtils.get_directory_size(Path(cast(str, fileitem.path)))
+ return 0
+
+
+def _transfer_task_meta(task: "TransferTask") -> MetaBase:
+ """声明进入作业管理器的整理任务已经完成元数据解析。"""
+ return cast(MetaBase, task.meta)
TRANSFER_ADMISSION_ACCEPTED = "accepted"
TRANSFER_ADMISSION_PROVIDER_PENDING = "provider_pending"
@@ -636,7 +691,7 @@ class TransferLeaseLostError(TransferPlanningStateError):
"""整理 worker 已失去持久租约、不得继续推进任务时抛出的错误。"""
-class TransferTask(OptionalMediaIdentityMixin, BaseModel):
+class TransferTask(OptionalMediaIdentityMixin, _ApplicationModel):
"""
文件整理任务。
"""
@@ -748,7 +803,7 @@ class TransferTask(OptionalMediaIdentityMixin, BaseModel):
self._lease_owner = owner_id
self._lease_token = lease_token
- def to_dict(self):
+ def to_dict(self) -> dict[str, Any]:
"""
返回字典。
@@ -758,13 +813,16 @@ class TransferTask(OptionalMediaIdentityMixin, BaseModel):
"""
dicts = vars(self).copy()
dicts["fileitem"] = self.fileitem.model_dump() if self.fileitem else None
- dicts["meta"] = self.meta.to_dict() if self.meta else None
- dicts["mediainfo"] = self.mediainfo.to_dict() if self.mediainfo else None
+ dicts["meta"] = _domain_to_dict(self.meta) if self.meta else None
+ dicts["mediainfo"] = _domain_to_dict(self.mediainfo) if self.mediainfo else None
dicts["target_directory"] = self.target_directory.model_dump() if self.target_directory else None
return dicts
-class TransferQueue(BaseModel):
+TransferCallback: TypeAlias = Callable[[TransferTask, TransferInfo], tuple[bool, str]]
+
+
+class TransferQueue(_ApplicationModel):
"""
异步整理队列信息。
@@ -774,7 +832,7 @@ class TransferQueue(BaseModel):
# 任务信息
task: Optional[TransferTask] = None
# 回调函数
- callback: Optional[Callable] = None
+ callback: Optional[TransferCallback] = None
# 整理结果
result: Optional[TransferInfo] = None
@@ -904,7 +962,7 @@ class TransferQueueService:
self._list_tasks = list_tasks
self._expire_tasks = expire_tasks
- def put(self, task: TransferTask, callback: Callable) -> bool:
+ def put(self, task: TransferTask, callback: TransferCallback) -> bool:
"""先持久化准入事实再入队;任何前置失败都撤销内存作业视图。"""
if not task or not self._register_task(task):
return False
@@ -1124,15 +1182,15 @@ class JobManager:
"""
# 整理中的作业
- _job_view: Dict[Tuple, TransferJob] = {}
+ _job_view: Dict[JobId, TransferJob] = {}
# 汇总季集清单
- _season_episodes: Dict[Tuple, List[int]] = {}
+ _season_episodes: Dict[JobId, List[int]] = {}
# 记录从 meta 作业迁移到 media 作业的关系,用于清理提前失败后残留的 media 作业
- _meta_to_media_ids: Dict[Tuple, set[Tuple]] = {}
+ _meta_to_media_ids: Dict[JobId, set[JobId]] = {}
# 记录任务最近一次状态心跳,供外部异步接管任务的失活检测使用
- _task_state_changed_at: Dict[Tuple[str, str], float] = {}
+ _task_state_changed_at: Dict[FileKey, float] = {}
# 记录仍由主程序整理线程直接执行的任务,避免把阻塞中的本地任务误判为失活
- _active_executions: set[Tuple[str, str]] = set()
+ _active_executions: set[FileKey] = set()
def __init__(self) -> None:
"""初始化当前进程内的整理作业状态。"""
@@ -1143,15 +1201,18 @@ class JobManager:
self._active_executions = set()
@staticmethod
- def __get_meta_id(meta: MetaBase = None, season: Optional[int] = None) -> Tuple:
+ def __get_meta_id(
+ meta: Optional[MetaBase] = None,
+ season: Optional[int] = None,
+ ) -> JobId:
"""
获取元数据ID
"""
- return meta.name, season
+ return cast(MetaBase, meta).name, season
@staticmethod
def __get_media_id(media: Optional[Union[MediaInfo, MusicInfo]] = None,
- season: Optional[int] = None) -> Tuple:
+ season: Optional[int] = None) -> JobId:
"""
获取媒体ID;音乐额外区分实体类型,并为无远端ID的曲目构造稳定身份。
"""
@@ -1204,18 +1265,19 @@ class JobManager:
)
return fileitem.storage or "local", normalized_path
- def __get_id(self, task: TransferTask = None) -> Tuple:
+ def __get_id(self, task: Optional[TransferTask] = None) -> JobId:
"""
获取作业ID
"""
- if task.mediainfo:
+ resolved_task = cast(TransferTask, task)
+ meta = _transfer_task_meta(resolved_task)
+ if resolved_task.mediainfo:
return self.__get_media_id(
- media=task.mediainfo, season=task.meta.begin_season
+ media=resolved_task.mediainfo, season=meta.begin_season
)
- else:
- return self.__get_meta_id(meta=task.meta, season=task.meta.begin_season)
+ return self.__get_meta_id(meta=meta, season=meta.begin_season)
- def get_job_id(self, task: TransferTask) -> Tuple:
+ def get_job_id(self, task: TransferTask) -> JobId:
"""返回任务当前所属的稳定作业身份,供作业级附加状态隔离使用。"""
return self.__get_id(task)
@@ -1229,11 +1291,11 @@ class JobManager:
mediainfo = deepcopy(task.mediainfo)
mediainfo.clear()
if isinstance(mediainfo, MusicInfo):
- return _SchemaMusicInfo(**mediainfo.to_dict())
- return _SchemaMediaInfo(**mediainfo.to_dict())
+ return _SchemaMusicInfo(**_domain_to_dict(mediainfo))
+ return _SchemaMediaInfo(**_domain_to_dict(mediainfo))
else:
# 没有媒体信息
- meta: MetaBase = task.meta
+ meta = _transfer_task_meta(task)
if isinstance(meta, MetaMusic):
# 未识别的音乐按已解析元数据兜底展示;音乐年份为 int,
# 不能复用 MediaInfo(year 为 str),否则触发 pydantic 校验异常
@@ -1262,7 +1324,7 @@ class JobManager:
"""
if isinstance(task.meta, MetaMusic):
return _SchemaMusicMeta(**task.meta.to_dict())
- return _SchemaMetaInfo(**task.meta.to_dict())
+ return _SchemaMetaInfo(**_domain_to_dict(_transfer_task_meta(task)))
def add_task(self, task: TransferTask, state: Optional[str] = "waiting") -> bool:
"""
@@ -1278,16 +1340,16 @@ class JobManager:
__mediaid__ = self.__get_id(task)
# 同一个源文件可能在识别前后落入不同作业,必须跨作业去重。
if any(
- self.__get_file_key(t.fileitem) == file_key
+ self.__get_file_key(_job_task_fileitem(t)) == file_key
for job in self._job_view.values()
- for t in job.tasks
+ for t in _job_tasks(job)
):
logger.debug(f"任务 {task.fileitem.name} 已存在,跳过重复添加")
return False
if __mediaid__ not in self._job_view:
self._job_view[__mediaid__] = TransferJob(
media=self.__get_media(task),
- season=task.meta.begin_season,
+ season=_transfer_task_meta(task).begin_season,
tasks=[
TransferJobTask(
fileitem=task.fileitem,
@@ -1302,13 +1364,13 @@ class JobManager:
# 不重复添加任务
if any(
[
- self.__get_file_key(t.fileitem) == file_key
- for t in self._job_view[__mediaid__].tasks
+ self.__get_file_key(_job_task_fileitem(t)) == file_key
+ for t in _job_tasks(self._job_view[__mediaid__])
]
):
logger.debug(f"任务 {task.fileitem.name} 已存在,跳过重复添加")
return False
- self._job_view[__mediaid__].tasks.append(
+ _job_tasks(self._job_view[__mediaid__]).append(
TransferJobTask(
fileitem=task.fileitem,
meta=self.__get_meta(task),
@@ -1320,12 +1382,14 @@ class JobManager:
self._task_state_changed_at[file_key] = monotonic()
# 添加季集信息
if self._season_episodes.get(__mediaid__):
- self._season_episodes[__mediaid__].extend(task.meta.episode_list)
+ self._season_episodes[__mediaid__].extend(
+ _transfer_task_meta(task).episode_list
+ )
self._season_episodes[__mediaid__] = list(
set(self._season_episodes[__mediaid__])
)
else:
- self._season_episodes[__mediaid__] = task.meta.episode_list
+ self._season_episodes[__mediaid__] = _transfer_task_meta(task).episode_list
return True
def migrate_task(self, task: TransferTask) -> bool:
@@ -1338,16 +1402,15 @@ class JobManager:
if not self.add_task(task, state=curr_task.state if curr_task else "waiting"):
return False
if curr_task and task.mediainfo:
- metaid = self.__get_meta_id(
- meta=task.meta, season=task.meta.begin_season
- )
+ meta = _transfer_task_meta(task)
+ metaid = self.__get_meta_id(meta=meta, season=meta.begin_season)
mediaid = self.__get_id(task)
if source_job_id == metaid and mediaid != metaid:
with job_lock:
self._meta_to_media_ids.setdefault(metaid, set()).add(mediaid)
return True
- def __is_job_done(self, job_id: Tuple) -> bool:
+ def __is_job_done(self, job_id: JobId) -> bool:
"""
检查指定作业是否已完成
"""
@@ -1355,10 +1418,10 @@ class JobManager:
return True
return all(
task.state in ["completed", "failed"]
- for task in self._job_view[job_id].tasks
+ for task in _job_tasks(self._job_view[job_id])
)
- def __pop_job(self, job_id: Tuple):
+ def __pop_job(self, job_id: JobId) -> None:
"""
移除指定作业和对应季集缓存
"""
@@ -1366,13 +1429,13 @@ class JobManager:
self._season_episodes.pop(job_id, None)
if not job:
return
- for task in job.tasks:
- file_key = self.__get_file_key(task.fileitem)
+ for task in _job_tasks(job):
+ file_key = self.__get_file_key(_job_task_fileitem(task))
if file_key:
self._task_state_changed_at.pop(file_key, None)
self._active_executions.discard(file_key)
- def __remove_done_job_groups(self, job_ids: set[Tuple]):
+ def __remove_done_job_groups(self, job_ids: set[JobId]) -> None:
"""
清理已进入终态的独立作业或关联作业组。
"""
@@ -1396,7 +1459,7 @@ class JobManager:
if self.__is_job_done(job_id):
self.__pop_job(job_id)
- def start_execution(self, task: TransferTask):
+ def start_execution(self, task: TransferTask) -> None:
"""
标记任务仍由主程序整理线程直接执行。
@@ -1410,7 +1473,7 @@ class JobManager:
with job_lock:
self._active_executions.add(file_key)
- def finish_execution(self, task: TransferTask):
+ def finish_execution(self, task: TransferTask) -> None:
"""
结束主程序整理线程对任务的直接执行标记。
@@ -1426,7 +1489,7 @@ class JobManager:
def expire_stale_running_tasks(
self, timeout_seconds: int
- ) -> List[Tuple[FileItem, int]]:
+ ) -> List[tuple[FileItem, int]]:
"""
将外部接管后长期无心跳的运行中任务标记失败并清理作业视图。
@@ -1440,12 +1503,13 @@ class JobManager:
return []
current_time = monotonic()
- expired: List[Tuple[FileItem, int]] = []
- affected_job_ids: set[Tuple] = set()
+ expired: List[tuple[FileItem, int]] = []
+ affected_job_ids: set[JobId] = set()
with job_lock:
for mediaid, job in self._job_view.items():
- for task in job.tasks:
- file_key = self.__get_file_key(task.fileitem)
+ for task in _job_tasks(job):
+ fileitem = _job_task_fileitem(task)
+ file_key = self.__get_file_key(fileitem)
if (
not file_key
or task.state != "running"
@@ -1463,13 +1527,13 @@ class JobManager:
self._season_episodes[mediaid] = list(
set(self._season_episodes[mediaid]) - set(episodes)
)
- expired.append((task.fileitem, int(inactive_seconds)))
+ expired.append((fileitem, int(inactive_seconds)))
affected_job_ids.add(mediaid)
self.__remove_done_job_groups(affected_job_ids)
return expired
- def running_task(self, task: TransferTask):
+ def running_task(self, task: TransferTask) -> None:
"""
设置任务为运行中,并刷新外部异步任务的状态心跳。
"""
@@ -1478,15 +1542,15 @@ class JobManager:
if __mediaid__ not in self._job_view:
return
# 更新状态
- for t in self._job_view[__mediaid__].tasks:
+ for t in _job_tasks(self._job_view[__mediaid__]):
if t.fileitem == task.fileitem:
t.state = "running"
- file_key = self.__get_file_key(t.fileitem)
+ file_key = self.__get_file_key(_job_task_fileitem(t))
if file_key:
self._task_state_changed_at[file_key] = monotonic()
break
- def finish_task(self, task: TransferTask):
+ def finish_task(self, task: TransferTask) -> None:
"""
设置任务为完成/成功
"""
@@ -1495,15 +1559,15 @@ class JobManager:
if __mediaid__ not in self._job_view:
return
# 更新状态
- for t in self._job_view[__mediaid__].tasks:
+ for t in _job_tasks(self._job_view[__mediaid__]):
if t.fileitem == task.fileitem:
t.state = "completed"
- file_key = self.__get_file_key(t.fileitem)
+ file_key = self.__get_file_key(_job_task_fileitem(t))
if file_key:
self._task_state_changed_at[file_key] = monotonic()
break
- def fail_task(self, task: TransferTask):
+ def fail_task(self, task: TransferTask) -> None:
"""
设置任务为失败
"""
@@ -1512,10 +1576,10 @@ class JobManager:
if __mediaid__ not in self._job_view:
return
# 更新状态
- for t in self._job_view[__mediaid__].tasks:
+ for t in _job_tasks(self._job_view[__mediaid__]):
if t.fileitem == task.fileitem:
t.state = "failed"
- file_key = self.__get_file_key(t.fileitem)
+ file_key = self.__get_file_key(_job_task_fileitem(t))
if file_key:
self._task_state_changed_at[file_key] = monotonic()
break
@@ -1523,10 +1587,10 @@ class JobManager:
if __mediaid__ in self._season_episodes:
self._season_episodes[__mediaid__] = list(
set(self._season_episodes[__mediaid__])
- - set(task.meta.episode_list)
+ - set(_transfer_task_meta(task).episode_list)
)
- def fail_unfinished_task(self, task: TransferTask):
+ def fail_unfinished_task(self, task: TransferTask) -> None:
"""
将指定任务视图中的非终态任务标记为失败
"""
@@ -1537,8 +1601,8 @@ class JobManager:
return
with job_lock:
for mediaid, job in self._job_view.items():
- for job_task in job.tasks:
- if self.__get_file_key(job_task.fileitem) != file_key:
+ for job_task in _job_tasks(job):
+ if self.__get_file_key(_job_task_fileitem(job_task)) != file_key:
continue
if job_task.state not in ["completed", "failed"]:
job_task.state = "failed"
@@ -1546,7 +1610,7 @@ class JobManager:
if mediaid in self._season_episodes:
self._season_episodes[mediaid] = list(
set(self._season_episodes[mediaid])
- - set(task.meta.episode_list)
+ - set(_transfer_task_meta(task).episode_list)
)
return
@@ -1561,7 +1625,7 @@ class JobManager:
self,
fileitem: FileItem,
preserve_execution: bool = False,
- ) -> Tuple[Optional[TransferJobTask], Optional[Tuple]]:
+ ) -> tuple[Optional[TransferJobTask], Optional[JobId]]:
"""
根据文件项移除任务,并返回任务所在的作业ID
"""
@@ -1571,14 +1635,14 @@ class JobManager:
with job_lock:
for mediaid in list(self._job_view):
job = self._job_view[mediaid]
- for task in job.tasks:
- if self.__get_file_key(task.fileitem) == file_key:
- job.tasks.remove(task)
+ for task in _job_tasks(job):
+ if self.__get_file_key(_job_task_fileitem(task)) == file_key:
+ _job_tasks(job).remove(task)
self._task_state_changed_at.pop(file_key, None)
if not preserve_execution:
self._active_executions.discard(file_key)
# 如果没有作业了,则移除作业
- if not job.tasks:
+ if not _job_tasks(job):
self._job_view.pop(mediaid)
# 移除季集信息
if mediaid in self._season_episodes:
@@ -1602,16 +1666,15 @@ class JobManager:
return job
return None
- def try_remove_job(self, task: TransferTask):
+ def try_remove_job(self, task: TransferTask) -> None:
"""
尝试移除任务对应的作业(严格检查未完成作业,线程安全)
"""
with job_lock:
- __metaid__ = self.__get_meta_id(
- meta=task.meta, season=task.meta.begin_season
- )
+ meta = _transfer_task_meta(task)
+ __metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season)
__mediaid__ = self.__get_media_id(
- media=task.mediainfo, season=task.meta.begin_season
+ media=task.mediainfo, season=meta.begin_season
)
related_media_ids = set(self._meta_to_media_ids.get(__metaid__, set()))
@@ -1634,23 +1697,22 @@ class JobManager:
检查任务对应的作业是否整理完成(不管成功还是失败)
"""
with job_lock:
- __metaid__ = self.__get_meta_id(
- meta=task.meta, season=task.meta.begin_season
- )
+ meta = _transfer_task_meta(task)
+ __metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season)
__mediaid__ = self.__get_media_id(
- media=task.mediainfo, season=task.meta.begin_season
+ media=task.mediainfo, season=meta.begin_season
)
if __metaid__ in self._job_view:
meta_done = all(
task.state in ["completed", "failed"]
- for task in self._job_view[__metaid__].tasks
+ for task in _job_tasks(self._job_view[__metaid__])
)
else:
meta_done = True
if __mediaid__ in self._job_view:
media_done = all(
task.state in ["completed", "failed"]
- for task in self._job_view[__mediaid__].tasks
+ for task in _job_tasks(self._job_view[__mediaid__])
)
else:
media_done = True
@@ -1661,21 +1723,20 @@ class JobManager:
检查任务对应的作业是否已完成且有成功的记录
"""
with job_lock:
- __metaid__ = self.__get_meta_id(
- meta=task.meta, season=task.meta.begin_season
- )
+ meta = _transfer_task_meta(task)
+ __metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season)
__mediaid__ = self.__get_media_id(
- media=task.mediainfo, season=task.meta.begin_season
+ media=task.mediainfo, season=meta.begin_season
)
if __metaid__ in self._job_view:
meta_finished = all(
task.state in ["completed", "failed"]
- for task in self._job_view[__metaid__].tasks
+ for task in _job_tasks(self._job_view[__metaid__])
)
else:
meta_finished = True
if __mediaid__ in self._job_view:
- tasks = self._job_view[__mediaid__].tasks
+ tasks = _job_tasks(self._job_view[__mediaid__])
media_finished = all(
task.state in ["completed", "failed"] for task in tasks
) and any(task.state == "completed" for task in tasks)
@@ -1688,23 +1749,22 @@ class JobManager:
检查任务对应的作业是否全部成功
"""
with job_lock:
- __metaid__ = self.__get_meta_id(
- meta=task.meta, season=task.meta.begin_season
- )
+ meta = _transfer_task_meta(task)
+ __metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season)
__mediaid__ = self.__get_media_id(
- media=task.mediainfo, season=task.meta.begin_season
+ media=task.mediainfo, season=meta.begin_season
)
if __metaid__ in self._job_view:
meta_success = all(
task.state in ["completed"]
- for task in self._job_view[__metaid__].tasks
+ for task in _job_tasks(self._job_view[__metaid__])
)
else:
meta_success = True
if __mediaid__ in self._job_view:
media_success = all(
task.state in ["completed"]
- for task in self._job_view[__mediaid__].tasks
+ for task in _job_tasks(self._job_view[__mediaid__])
)
else:
media_success = True
@@ -1716,9 +1776,9 @@ class JobManager:
"""
with job_lock:
return {
- task.download_hash
+ cast(str, task.download_hash)
for job in self._job_view.values()
- for task in job.tasks
+ for task in _job_tasks(job)
}
def is_torrent_done(self, download_hash: str) -> bool:
@@ -1729,7 +1789,7 @@ class JobManager:
if any(
task.state not in {"completed", "failed"}
for job in self._job_view.values()
- for task in job.tasks
+ for task in _job_tasks(job)
if task.download_hash == download_hash
):
return False
@@ -1743,7 +1803,7 @@ class JobManager:
if any(
task.state != "completed"
for job in self._job_view.values()
- for task in job.tasks
+ for task in _job_tasks(job)
if task.download_hash == download_hash
):
return False
@@ -1767,7 +1827,7 @@ class JobManager:
__metaid__ = self.__get_meta_id(meta=meta, season=season)
return (
__metaid__ in self._job_view
- and len(self._job_view[__metaid__].tasks) > 0
+ and len(_job_tasks(self._job_view[__metaid__])) > 0
)
def success_tasks(
@@ -1782,7 +1842,7 @@ class JobManager:
return []
return [
task
- for task in self._job_view[__mediaid__].tasks
+ for task in _job_tasks(self._job_view[__mediaid__])
if task.state == "completed"
]
@@ -1796,7 +1856,7 @@ class JobManager:
__mediaid__ = self.__get_media_id(media=media, season=season)
if __mediaid__ not in self._job_view:
return []
- return self._job_view[__mediaid__].tasks
+ return _job_tasks(self._job_view[__mediaid__])
def count(self, media: Union[MediaInfo, MusicInfo], season: Optional[int] = None) -> int:
"""
@@ -1809,7 +1869,7 @@ class JobManager:
return len(
[
task
- for task in self._job_view[__mediaid__].tasks
+ for task in _job_tasks(self._job_view[__mediaid__])
if task.state == "completed"
]
)
@@ -1824,14 +1884,8 @@ class JobManager:
return 0
return sum(
[
- task.fileitem.size
- if task.fileitem.size is not None
- else (
- SystemUtils.get_directory_size(Path(task.fileitem.path))
- if task.fileitem.storage == "local"
- else 0
- )
- for task in self._job_view[__mediaid__].tasks
+ _job_task_size(task)
+ for task in _job_tasks(self._job_view[__mediaid__])
if task.state == "completed"
]
)
@@ -1841,7 +1895,7 @@ class JobManager:
获取所有任务总数
"""
with job_lock:
- return sum([len(job.tasks) for job in self._job_view.values()])
+ return sum([len(_job_tasks(job)) for job in self._job_view.values()])
def pending_total(self) -> int:
"""
@@ -1855,7 +1909,7 @@ class JobManager:
return sum(
1
for job in self._job_view.values()
- for task in job.tasks
+ for task in _job_tasks(job)
if task.state not in ("completed", "failed")
)
@@ -1960,10 +2014,10 @@ class FailedRetryScheduler:
def _build_retry_transfer_prompt(self, history_ids: list[int]) -> str:
"""根据失败记录数量构建统一的重试整理后台任务提示词。"""
task_type, template_context = self._build_retry_transfer_template_context(history_ids)
- return get_prompt_manager().render_system_task_message(
+ return cast(str, get_prompt_manager().render_system_task_message(
task_type,
template_context=template_context,
- )
+ ))
async def schedule_retry(self, history_id: int, group_key: str = "") -> None:
"""
diff --git a/app/chain/__init__.py b/app/chain/__init__.py
index b2161c652..b6ccd5315 100644
--- a/app/chain/__init__.py
+++ b/app/chain/__init__.py
@@ -36,7 +36,7 @@ from app.schemas.types import (
from app.schemas.workflow import FileItem
if TYPE_CHECKING:
- from app.application.transfer import TransferPlanCheckpoint, TransferPlanningInput
+ from app.application.transfer.workflow import TransferPlanCheckpoint, TransferPlanningInput
class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin,
diff --git a/app/chain/_transfer.py b/app/chain/_transfer.py
index c6a8eb695..546a54345 100644
--- a/app/chain/_transfer.py
+++ b/app/chain/_transfer.py
@@ -14,7 +14,6 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
from app.adapters.system.host import SystemUtils
from app.application.agent import build_manual_redo_prompt, get_running_agent_manager
from app.application.chain.data import (
- get_chain_download_history_port,
get_chain_transfer_execution_port,
get_chain_transfer_history_port,
)
@@ -24,17 +23,12 @@ from app.application.configuration import (
)
from app.application.formatting import EpisodeFormatRuleHelper
from app.application.history import clear_transfer_failures, resolve_history
-from app.application.transfer import TransferTask, job_lock
-from app.application.transfer_execution import TransferExecutionCommand
+from app.application.transfer.execution import TransferExecutionCommand
+from app.application.transfer.workflow import TransferTask, job_lock
from app.chain._contracts import TransferMixinHost
from app.chain.media import MediaChain
from app.chain.storage import StorageChain
from app.chain.subscribe import SubscribeChain
-
-# 旧测试与插件补丁入口;正式依赖通过宿主工厂逐步收敛。
-MediaChain = MediaChain
-StorageChain = StorageChain
-SubscribeChain = SubscribeChain
from app.domain.context import MediaInfo, MusicInfo
from app.domain.media import normalize_music_type
from app.domain.meta.metabase import MetaBase
diff --git a/app/chain/transfer.py b/app/chain/transfer.py
index 636a9a56e..9621e3b40 100755
--- a/app/chain/transfer.py
+++ b/app/chain/transfer.py
@@ -24,7 +24,7 @@ from app.application.chain.data import (
get_chain_transfer_history_port,
get_chain_transfer_pending_port,
)
-from app.application.chain.durable_events import TransferResultSettlement
+from app.application.chain.events import TransferResultSettlement
from app.application.configuration import get_configured_system_config
from app.application.directory import DirectoryHelper
from app.application.formatting import FormatParser
@@ -46,7 +46,21 @@ from app.application.outbox import (
TRANSFER_COMPLETED_TOPIC,
TRANSFER_FAILED_TOPIC,
)
-from app.application.transfer import (
+from app.application.transfer.execution import (
+ TransferExecutionCheckpoint,
+ TransferExecutionCommand,
+ TransferExecutionConflictError,
+ TransferExecutionRepository,
+ TransferExecutionSnapshot,
+ TransferExecutionState,
+ TransferOperationObservation,
+ TransferOperationObservationState,
+ TransferSettlementResult,
+ TransferStepIntent,
+ TransferStepResult,
+ TransferStepState,
+)
+from app.application.transfer.workflow import (
FailedRetryScheduler,
JobManager,
TransferAdmission,
@@ -64,20 +78,6 @@ from app.application.transfer import (
build_transfer_failure_group_key,
job_lock,
)
-from app.application.transfer_execution import (
- TransferExecutionCheckpoint,
- TransferExecutionCommand,
- TransferExecutionConflictError,
- TransferExecutionRepository,
- TransferExecutionSnapshot,
- TransferExecutionState,
- TransferOperationObservation,
- TransferOperationObservationState,
- TransferSettlementResult,
- TransferStepIntent,
- TransferStepResult,
- TransferStepState,
-)
from app.chain import ChainBase
from app.chain._transfer import (
EpisodeFormatMixin,
diff --git a/app/db/adapters/chain.py b/app/db/adapters/chain.py
index ee422b13d..639b8a2d0 100644
--- a/app/db/adapters/chain.py
+++ b/app/db/adapters/chain.py
@@ -10,7 +10,7 @@ from typing import Any
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
-from app.application.chain.durable_events import (
+from app.application.chain.events import (
ChainDurableEventWriter,
TransferHistoryRef,
TransferResultSettlement,
@@ -25,7 +25,7 @@ from app.application.outbox import (
DurableEventCommand,
OutboxIntent,
)
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionConflictError,
TransferExecutionLeaseLostError,
TransferExecutionState,
diff --git a/app/db/adapters/transfer/__init__.py b/app/db/adapters/transfer/__init__.py
new file mode 100644
index 000000000..70ed86a95
--- /dev/null
+++ b/app/db/adapters/transfer/__init__.py
@@ -0,0 +1 @@
+"""转移接纳与持久执行的数据库适配器包。"""
diff --git a/app/db/adapters/transfer.py b/app/db/adapters/transfer/admission.py
similarity index 99%
rename from app/db/adapters/transfer.py
rename to app/db/adapters/transfer/admission.py
index 5cac4dd46..61fb5c854 100644
--- a/app/db/adapters/transfer.py
+++ b/app/db/adapters/transfer/admission.py
@@ -10,7 +10,7 @@ from uuid import uuid4
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
-from app.application.transfer import (
+from app.application.transfer.workflow import (
TRANSFER_ADMISSION_ACCEPTED,
TRANSFER_ADMISSION_PLANNED,
TRANSFER_ADMISSION_PROVIDER_PENDING,
diff --git a/app/db/adapters/transfer_execution.py b/app/db/adapters/transfer/execution.py
similarity index 99%
rename from app/db/adapters/transfer_execution.py
rename to app/db/adapters/transfer/execution.py
index f8a34793c..c81ff9d2e 100644
--- a/app/db/adapters/transfer_execution.py
+++ b/app/db/adapters/transfer/execution.py
@@ -10,7 +10,7 @@ from sqlalchemy import and_, func, or_, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionCheckpoint,
TransferExecutionConflictError,
TransferExecutionLeaseLostError,
diff --git a/app/modules/filemanager/module.py b/app/modules/filemanager/module.py
index 2d3585563..fd9c5f05b 100644
--- a/app/modules/filemanager/module.py
+++ b/app/modules/filemanager/module.py
@@ -4,8 +4,8 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from app.adapters.system.host import SystemUtils
from app.application.directory import DirectoryHelper
from app.application.messaging.message import MessageHelper
-from app.application.transfer import TransferPlanCheckpoint, TransferPlanningInput
-from app.application.transfer_execution import TransferStepRunner
+from app.application.transfer.execution import TransferStepRunner
+from app.application.transfer.workflow import TransferPlanCheckpoint, TransferPlanningInput
from app.domain.context import MediaInfo, MusicInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
diff --git a/app/modules/filemanager/transhandler.py b/app/modules/filemanager/transhandler.py
index 61a41c5d9..579c13fc6 100644
--- a/app/modules/filemanager/transhandler.py
+++ b/app/modules/filemanager/transhandler.py
@@ -10,17 +10,17 @@ from app.adapters.system.host import SystemUtils
from app.application.audio import AudioMetadataHelper
from app.application.directory import DirectoryHelper
from app.application.messaging.message import TemplateHelper
-from app.application.transfer import (
- TransferPlanCheckpoint,
- TransferPlanItem,
- TransferPlanningInput,
-)
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferOperationObservation,
TransferOperationObservationState,
TransferStepResult,
TransferStepRunner,
)
+from app.application.transfer.workflow import (
+ TransferPlanCheckpoint,
+ TransferPlanItem,
+ TransferPlanningInput,
+)
from app.domain.context import MediaInfo, MusicInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
diff --git a/app/runtime/compat/manifest.py b/app/runtime/compat/manifest.py
index 535439b51..2d05c3d79 100644
--- a/app/runtime/compat/manifest.py
+++ b/app/runtime/compat/manifest.py
@@ -56,6 +56,24 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = {
introduced="v3.0.0",
owner="application",
),
+ "app.application.chain.durable_events": ModuleAlias(
+ target="app.application.chain.events",
+ replacement="app.application.chain.events",
+ introduced="v3.0.0",
+ owner="application",
+ ),
+ "app.application.transfer_execution": ModuleAlias(
+ target="app.application.transfer.execution",
+ replacement="app.application.transfer.execution",
+ introduced="v3.0.0",
+ owner="application",
+ ),
+ "app.runtime.managed_resources": ModuleAlias(
+ target="app.runtime.resources",
+ replacement="app.runtime.resources",
+ introduced="v3.0.0",
+ owner="runtime",
+ ),
"app.db.agentchat_oper": ModuleAlias(
target="app.db.oper.agentchat",
replacement="app.db.oper.agentchat",
@@ -136,7 +154,7 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = {
),
"app.db.transferpending_oper": ModuleAlias(
target="app.sdk._legacy.transferpending",
- replacement="app.application.transfer",
+ replacement="app.application.transfer.workflow",
introduced="v3.0.0",
owner="sdk",
),
@@ -756,6 +774,14 @@ _MESSAGE_NOTIFICATION_SYMBOL_ALIASES: Dict[str, SymbolAlias] = {
}
SYMBOL_ALIASES: Dict[str, Dict[str, SymbolAlias]] = {
+ "app.application.transfer": {
+ name: SymbolAlias(
+ target_module="app.sdk._legacy.transfer",
+ target_name=name,
+ replacement=f"app.application.transfer.workflow.{name}",
+ )
+ for name in ("TransferTask", "TransferQueue")
+ },
"app.agent.orchestrator": {
"AgentChain": SymbolAlias(
target_module="app.chain.agent",
@@ -806,7 +832,7 @@ SYMBOL_ALIASES: Dict[str, Dict[str, SymbolAlias]] = {
name: SymbolAlias(
target_module="app.sdk._legacy.transfer",
target_name=name,
- replacement=f"app.application.transfer.{name}",
+ replacement=f"app.application.transfer.workflow.{name}",
)
for name in ("TransferTask", "TransferQueue")
},
@@ -817,7 +843,7 @@ SYMBOL_ALIASES: Dict[str, Dict[str, SymbolAlias]] = {
name: SymbolAlias(
target_module="app.sdk._legacy.transfer",
target_name=name,
- replacement=f"app.application.transfer.{name}",
+ replacement=f"app.application.transfer.workflow.{name}",
)
for name in ("TransferTask", "TransferQueue")
},
diff --git a/app/runtime/extensions/managed_resource_adapter.py b/app/runtime/extensions/resource.py
similarity index 99%
rename from app/runtime/extensions/managed_resource_adapter.py
rename to app/runtime/extensions/resource.py
index 86c9e5d47..20d5d0458 100644
--- a/app/runtime/extensions/managed_resource_adapter.py
+++ b/app/runtime/extensions/resource.py
@@ -15,12 +15,11 @@ from app.runtime.capabilities.model import (
CapabilitySpec,
)
from app.runtime.capabilities.registry import CapabilityRegistry
-from app.runtime.managed_resources import (
+from app.runtime.resources import (
MANAGED_RESOURCE_ASYNC_KIND,
MANAGED_RESOURCE_SYNC_KIND,
)
-
_DEFAULT_RESOURCE_ROOT = Path(__file__).resolve().parents[2] / "adapters"
_RESOURCE_KINDS = {MANAGED_RESOURCE_SYNC_KIND, MANAGED_RESOURCE_ASYNC_KIND}
diff --git a/app/runtime/managed_resources.py b/app/runtime/resources.py
similarity index 96%
rename from app/runtime/managed_resources.py
rename to app/runtime/resources.py
index 2f3dcab93..73c95aa5a 100644
--- a/app/runtime/managed_resources.py
+++ b/app/runtime/resources.py
@@ -4,8 +4,7 @@ from __future__ import annotations
import asyncio
import threading
-from typing import Any, Optional, Protocol
-
+from typing import Any, Literal, Optional, Protocol, overload
MANAGED_RESOURCE_SYNC_KIND = "managed_resource.sync"
MANAGED_RESOURCE_ASYNC_KIND = "managed_resource.async"
@@ -65,6 +64,14 @@ def configure_managed_resource_runtime(runtime: ManagedResourceRuntime) -> None:
_managed_resource_runtime = runtime
+@overload
+def _runtime(*, required: Literal[True]) -> ManagedResourceRuntime: ...
+
+
+@overload
+def _runtime(*, required: Literal[False]) -> Optional[ManagedResourceRuntime]: ...
+
+
def _runtime(*, required: bool) -> Optional[ManagedResourceRuntime]:
"""读取当前 Runtime;资源使用路径要求启动组合已经完成装配。"""
with _runtime_lock:
diff --git a/app/schemas/transfer.py b/app/schemas/transfer.py
index 5065dca3c..581048633 100644
--- a/app/schemas/transfer.py
+++ b/app/schemas/transfer.py
@@ -1,15 +1,14 @@
from pathlib import Path
-from typing import Literal, List, Optional, Union
+from typing import List, Literal, Optional, Union
from pydantic import BaseModel, ConfigDict, Field, model_validator
from app.schemas.common import JsonData
-from app.schemas.media import OptionalMediaIdentityMixin
-from app.schemas.types import MediaSource, MusicTargetEntityType
-
-from app.schemas.context import MetaInfo, MediaInfo
-from app.schemas.music import MusicInfo, MusicMeta
+from app.schemas.context import MediaInfo, MetaInfo
from app.schemas.file import FileItem
+from app.schemas.media import OptionalMediaIdentityMixin
+from app.schemas.music import MusicInfo, MusicMeta
+from app.schemas.types import MediaSource, MusicTargetEntityType
class DownloaderTorrent(BaseModel):
@@ -88,7 +87,7 @@ class DownloadingTorrent(DownloaderTorrent):
"""
-# TransferTask 已迁至 app/application/transfer.py:它是整理链的进程内工作项,装的是
+# TransferTask 已迁至 app/application/transfer/workflow.py:它是整理链的进程内工作项,装的是
# 领域对象而非 DTO,留在这里只能把两个字段标成 Any——app.schemas 命名领域类型会让
# app.schemas -> app.schemas.transfer -> app.domain.* -> app.schemas.types -> app.schemas
# 闭环。下面的 TransferJob / TransferJobTask 才是它面向前端的投影,用本包的同名 DTO。
diff --git a/app/sdk/_legacy/transfer.py b/app/sdk/_legacy/transfer.py
index e617209d5..24992e560 100644
--- a/app/sdk/_legacy/transfer.py
+++ b/app/sdk/_legacy/transfer.py
@@ -2,8 +2,10 @@
from typing import Any, Optional
-from app.application.transfer import (
+from app.application.transfer.workflow import (
TransferQueue as CanonicalTransferQueue,
+)
+from app.application.transfer.workflow import (
TransferTask as CanonicalTransferTask,
)
diff --git a/app/startup/initializers/modules.py b/app/startup/initializers/modules.py
index eee8d49e4..9ebe1256b 100644
--- a/app/startup/initializers/modules.py
+++ b/app/startup/initializers/modules.py
@@ -44,7 +44,7 @@ from app.application.chain.context import (
configure_chain_runtime_context_provider,
)
from app.application.chain.data import configure_chain_data_ports, get_chain_data_ports
-from app.application.chain.durable_events import (
+from app.application.chain.events import (
restore_download_added,
restore_transfer_result,
)
@@ -110,8 +110,8 @@ from app.db.adapters.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutbox
from app.db.adapters.site import TransactionalSiteRepository
from app.db.adapters.subscription import TransactionalSubscribeWriter
from app.db.adapters.transaction import TransactionalWriteRunner
-from app.db.adapters.transfer import TransactionalTransferAdmissionRepository
-from app.db.adapters.transfer_execution import (
+from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
+from app.db.adapters.transfer.execution import (
TransactionalTransferExecutionRepository,
)
from app.db.adapters.workflow import TransactionalWorkflowExecutionService
@@ -189,7 +189,7 @@ from app.startup.composition.subscription import (
configure_transactional_subscription_scopes,
)
from app.startup.initializers.agent import init_agent
-from app.startup.initializers.managed_resources import (
+from app.startup.initializers.resources import (
init_managed_resources,
stop_managed_resources,
)
diff --git a/app/startup/initializers/plugins.py b/app/startup/initializers/plugins.py
index 8d7e6e9dc..dae08cc35 100644
--- a/app/startup/initializers/plugins.py
+++ b/app/startup/initializers/plugins.py
@@ -79,7 +79,7 @@ from app.runtime.extensions.plugin_manager import (
configure_site_auth_level_provider,
)
from app.runtime.log import logger
-from app.runtime.managed_resources import acquire_managed_resource
+from app.runtime.resources import acquire_managed_resource
from app.runtime.settings import get_runtime_setting
from app.schemas.exception import PluginMutationRejectedError
from app.schemas.plugin import PluginRuntimeStatus
diff --git a/app/startup/initializers/managed_resources.py b/app/startup/initializers/resources.py
similarity index 93%
rename from app/startup/initializers/managed_resources.py
rename to app/startup/initializers/resources.py
index 1bb33d6e5..7902ee42d 100644
--- a/app/startup/initializers/managed_resources.py
+++ b/app/startup/initializers/resources.py
@@ -6,12 +6,12 @@ import threading
from typing import Optional
from app.runtime.capabilities.runtime import CapabilityRuntime
-from app.runtime.extensions.managed_resource_adapter import (
+from app.runtime.extensions.resource import (
AsyncManagedResourceAdapter,
SyncManagedResourceAdapter,
build_managed_resource_registry,
)
-from app.runtime.managed_resources import (
+from app.runtime.resources import (
MANAGED_RESOURCE_ASYNC_KIND,
MANAGED_RESOURCE_SYNC_KIND,
configure_managed_resource_runtime,
diff --git a/docs/architecture-optimization-checklist.md b/docs/architecture-optimization-checklist.md
index 41b26d173..edce89cbe 100644
--- a/docs/architecture-optimization-checklist.md
+++ b/docs/architecture-optimization-checklist.md
@@ -69,7 +69,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
| 指标 | 当前值 | 解释 |
|---|---:|---|
-| 宿主 Python 模块 / 内部依赖边 | 842 / 6,882 | `dependency-baseline.json` 当前快照 |
+| 宿主 Python 模块 / 内部依赖边 | 844 / 6,898 | `dependency-baseline.json` 当前快照 |
| 非平凡 SCC | 2 | 新增 Chain 包根环;另一个是隔离的 29 模块 TMDB 移植包环 |
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
@@ -77,9 +77,9 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement |
| Python 源码量 | 约 271,400 行 | 60 个文件超过 1,000 行,14 个超过 2,000 行 |
| 长方法 | 281 个超过 80 行 | 67 个超过 150 行,23 个超过 250 行;大量是私有方法 |
-| 全量 mypy 历史债务 | 11,983 / 601 文件 | strict frontier 当前只覆盖 41 个文件,且 ratchet 已新增 2 个错误 |
-| Ruff 历史诊断 | 929 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
-| 覆盖率低水位 | Application 78.24%,Domain 79.29% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
+| 全量 mypy 历史债务 | 11,827 / 596 文件 | strict frontier 当前覆盖 41 个文件,本批迁移路径的类型债务已清零 |
+| Ruff 历史诊断 | 889 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
+| 覆盖率低水位 | Application 78.63%,Domain 79.29% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
### 3.3 热点文件
diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md
index d63fe97a0..3a9265edd 100644
--- a/docs/architecture-overview.md
+++ b/docs/architecture-overview.md
@@ -200,7 +200,7 @@ Chain/Agent 的 `Any` factory 和裸 Oper 仍需迁移为类型化 Port/DTO,
| `app/adapters/web/` | Web 技术适配:动态插件路由注册、认证依赖和 OpenAPI 重建;不承载插件路由用例 | `plugin/routes.py` |
| `app/adapters/observability/` | 可选观测技术适配;核心层只依赖 `runtime/observability` 定义的窄端口 | `otel.py` |
| `app/application/` | 读取配置/持久化状态的聚焦应用服务:识别、过滤、通知、RSS、站点、下载器、媒体服务器、存储、整理规则、可靠副作用等;同一主题拆成子包 | `recognition.py`、`rules.py`、`rss.py`、`outbox.py`、`site/`、`subscription/`、`plugin/` |
-| `app/application/chain/` | Chain 运行时上下文、跨领域数据端口和 durable event 命令;将组合根注入的能力以命名 getter 暴露给 Chain | `context.py`、`data.py`、`durable_events.py` |
+| `app/application/chain/` | Chain 运行时上下文、跨领域数据端口和 durable event 命令;将组合根注入的能力以命名 getter 暴露给 Chain | `context.py`、`data.py`、`events.py` |
| `app/application/subscription/` | 订阅新增、查询、变更、删除、媒体身份与搜索契约 | `write.py`、`contract.py`、`mutation.py`、`delete.py`、`identity.py`、`search.py` |
| `app/application/plugin/` | 插件市场、安装、运行时端口、文件夹操作和动态路由用例;具体 FastAPI 路由适配器在 adapters 层 | `catalog.py`、`install.py`、`runtime.py`、`folders.py`、`routes.py` |
| `app/application/messaging/` | 渠道回环入口、消息渲染/路由、命令交互会话、插件按钮回调、Agent 消息桥接 | `ingress.py`、`message.py`、`router.py`、`agent.py` |
@@ -458,7 +458,7 @@ flowchart LR
|---|---|
| **Config Reload** | 继承 `ConfigReloadMixin` 并声明 `CONFIG_WATCH`,配置变更时自动重建长生命周期对象(如下载器客户端重连) |
| **Singleton** | `EventManager`、`ModuleManager`、`PluginManager` 等全局共享管理器继承 `foundation/singleton.py` 的 `Singleton` |
-| **Managed Resource** | 可选进程级技术资源(浏览器、虚拟显示等)以 data-only `capability.toml` 声明,`runtime/extensions` 解释生命周期,`startup` 构建 Runtime,消费者经 `runtime/managed_resources.py` 显式获取;插件使用浏览器走 `app.sdk.browser` |
+| **Managed Resource** | 可选进程级技术资源(浏览器、虚拟显示等)以 data-only `capability.toml` 声明,`runtime/extensions` 解释生命周期,`startup` 构建 Runtime,消费者经 `runtime/resources.py` 显式获取;插件使用浏览器走 `app.sdk.browser` |
| **Observability** | `runtime/observability` 定义低基数指标和默认 no-op 端口,Startup 可选装配 OTel;HTTP、DB、Event、Module、Scheduler、插件生命周期和 Agent 只提交白名单标签 |
---
@@ -631,7 +631,7 @@ flowchart LR
RE["events.py
事件总线"]
RL["log.py
日志运行时(依赖叶子)"]
RCA["cache.py
缓存协议 / 内存后端 / 装饰器"]
- MR["managed_resources.py
托管资源门面"]
+ MR["resources.py
托管资源门面"]
end
subgraph adapters["app/adapters(具体 I/O)"]
@@ -704,8 +704,8 @@ flowchart LR
| 指标 | 当前值 |
|---|---:|
-| Python 模块 | 842 |
-| 内部导入边 | 6,882 |
+| Python 模块 | 844 |
+| 内部导入边 | 6,898 |
| 非平凡 SCC | 2(`ARCH-107` 临时 Chain 包根环;精确 containment 的 TMDB 移植包环) |
| Direct egress | 66(12 条待迁移债务,54 条精确 containment) |
| Module Contract V2 spec | 217(其中 215 个进入 `run_module` 观察面) |
diff --git a/docs/architecture-refactor-roadmap.md b/docs/architecture-refactor-roadmap.md
index 03b312009..dbef163da 100644
--- a/docs/architecture-refactor-roadmap.md
+++ b/docs/architecture-refactor-roadmap.md
@@ -144,7 +144,7 @@ G-ARCH 只有在以下条件全部满足后才可完成:
| S4-L2 Event strict contract | `PLANNED` | S0-L2.6,S1-L6 | 宿主事件输入/输出按风险 strict,诊断例外只属于第三方插件兼容 |
| S4-L3 Complexity v2 | `PLANNED` | S3 | 私有方法、class/file、圈复杂度进入门禁;所有超限通过职责拆分归零 |
| S4-L4 全量 mypy 清零 | `PLANNED` | S3,S4-L1,S4-L2 | `mypy-baseline.json` 归零并删除债务接受路径,全宿主 strict 类型通过 |
-| S4-L5 Ruff 治理债务清零 | `PLANNED` | S3 | 当前受控 929 条诊断归零,规则集扩展经过独立审查且新增诊断为零 |
+| S4-L5 Ruff 治理债务清零 | `PLANNED` | S3 | 当前受控 889 条诊断归零,规则集扩展经过独立审查且新增诊断为零 |
| S4-L6 Coverage/并发/质量证据 | `PLANNED` | S3,S4-L1,S4-L2 | 高风险包纳入 coverage;raw concurrency 分类清零;Module Quality 有真实 evidence test |
### S5:Plugin、Agent、Domain、Startup 与最终收口
@@ -175,7 +175,7 @@ planning、lease、幂等执行或终态恢复已经完成。
**Ownership**
-- `app/application/transfer.py` 拥有 admission DTO、Protocol、结果语义与 persist-before-enqueue 编排。
+- `app/application/transfer/workflow.py` 拥有 admission DTO、Protocol、结果语义与 persist-before-enqueue 编排。
- `app/db/adapters/` 提供短 Session/UoW 的 Transfer pending 持久化实现;`app/db/oper/` 只接收
adapter 拥有的 Session 并 stage/flush。
- `app/startup/` 负责构造并注入 adapter,宿主 Chain 不再取得 raw/`Any` `TransferPendingOper`。
@@ -242,11 +242,11 @@ git diff --check
**Ownership and compatibility**
-- `app/application/transfer.py` 拥有 planning input、plan item、checkpoint 和状态错误合同;JSON 版本、
+- `app/application/transfer/workflow.py` 拥有 planning input、plan item、checkpoint 和状态错误合同;JSON 版本、
指纹及 resolved 上下文均可跨进程 round-trip。
- `app/modules/filemanager/transhandler.py` 是唯一目标规划与文件执行实现;`FileManagerModule.transfer`
与 `TransHandler.transfer_media` 已删除,不保留第二套重命名、覆盖或目录递归逻辑。
-- `app/db/adapters/transfer.py` 通过短 Session/UoW 提交 checkpoint;Oper 只负责带状态和指纹条件的
+- `app/db/adapters/transfer/admission.py` 通过短 Session/UoW 提交 checkpoint;Oper 只负责带状态和指纹条件的
stage,3.0.14 migration 可升级、降级并在中断后重跑。
- cleanup intent 随准入输入冻结。宿主路径由 FileManager 在 `TransferIntercept` 放行后、任何文件写入前
执行;legacy provider 路径为保持旧 ABI 顺序,在全部冻结引用解析成功后、调用 provider 前执行。
diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md
index a30323c1c..c76f7e21d 100644
--- a/docs/rules/05-architecture.md
+++ b/docs/rules/05-architecture.md
@@ -65,10 +65,10 @@ to make the directory tree look symmetrical.
| `app/application/search/` | Search state and later search-plan use cases |
| `app/application/download/` | Download task querying/control and later submission use cases |
| `app/application/music/` | Multi-source music catalog orchestration |
-| `app/application/chain/` | Injectable Chain runtime context and compatibility provider |
+| `app/application/chain/` | Injectable Chain runtime capabilities: `context.py` owns the runtime dependency aggregate, `data.py` owns named persistence ports, and `events.py` owns durable event write contracts plus replayable payload conversion |
| `app/application/agentdata.py` | Named Agent data ports; canonical Agent consumers use `get_agent_*_port()` and do not alias legacy proxies to Oper classes |
| `app/application/outbox.py` | Durable intent and Outbox repository/dispatcher contracts for post-commit side effects |
-| `app/application/transfer_execution.py` | Durable transfer execution contracts: stable operation identity, step/checkpoint state, retry/manual-review commands and terminal-settlement DTOs; contains no SQLAlchemy or external I/O |
+| `app/application/transfer/` | Durable transfer use cases: `workflow.py` owns admission/planning/queue behavior; `execution.py` owns stable operation identity, step/checkpoint state, retry/manual-review commands and terminal-settlement DTOs |
| `app/application/plugin/` | Plugin market catalog, installation command, installed-plugin identity contract and startup migration, runtime port, folder operations and dynamic-route use cases; filenames remain single words (`catalog.py`, `identity.py`, `migration.py`, `install.py`, `runtime.py`, `folders.py`, `routes.py`) |
| `app/application/server/` | MoviePilot Server reporting and sharing use cases; local data readers and transport callbacks are injected by startup |
| `app/application/site/` | Configured site catalog, authentication level and index-resource capability; the generated extension and its data bundle stay together here |
@@ -94,7 +94,7 @@ directory categories.
| `app/runtime/observability/` | Low-cardinality metric contracts and no-op-capable observation facade |
| `app/runtime/log.py` | Complete console/plugin/file logging runtime and shutdown |
| `app/runtime/cache.py` | Cache protocols, memory implementations, decorators and proxies |
-| `app/runtime/managed_resources.py` | Provider-neutral acquisition, observation and shutdown facade for process-owned optional resources |
+| `app/runtime/resources.py` | Provider-neutral acquisition, observation and shutdown facade for process-owned optional resources |
| `app/runtime/tasks.py` | Lifespan-scoped ownership, cancellation and bounded shutdown waiting for in-process background tasks |
| `app/runtime/execution.py` | Shared sync/async execution and cross-thread submission boundary with correlation propagation |
| `app/runtime/correlation.py` | Request/cross-thread correlation context and safe propagation into logs and child work |
@@ -152,7 +152,7 @@ retained only for compatibility and is not a canonical Oper substitute.
Durable transfer execution follows one explicit boundary. The Chain freezes each
external file operation into the Application-owned contract in
-`app/application/transfer_execution.py`; `app/db/adapters/transfer_execution.py`
+`app/application/transfer/execution.py`; `app/db/adapters/transfer/execution.py`
uses short transactions to persist the task ledger and fences every state change
with the current lease and attempt token. `app/db/oper/transferexecutionstep.py`
remains table-oriented and never owns retry or recovery policy. External file I/O
@@ -206,7 +206,7 @@ mechanism remains in `app/adapters/system/resource.py`.
可选的进程级技术资源使用 Managed Resource 合同:实现及其 data-only
`capability.toml` 与适配器同目录,`runtime/extensions` 只解释通用的同步/异步
`start`、`stop` 生命周期,`startup` 负责构建 Capability Runtime。声明必须使用
-`on_first_use`,普通启动只发现声明;消费者通过 `app/runtime/managed_resources.py`
+`on_first_use`,普通启动只发现声明;消费者通过 `app/runtime/resources.py`
显式获取资源。关闭路径先释放消费者,再关闭已初始化 Runtime,未使用的资源不得因关闭而物化。
应用级启动顺序使用 `app/startup/lifecycle/components.py` 的组件描述声明依赖、
normal/safe-mode 范围、start/stop 顺序、超时预算和失败策略。新增进程级资源不得只在
@@ -484,9 +484,9 @@ Durable post-commit side effects have a separate boundary:
must not replace an Outbox or persistent task table.
Transfer durable admission follows the same ownership direction without using
-the Outbox as an execution queue: `app/application/transfer.py` owns the typed
+the Outbox as an execution queue: `app/application/transfer/workflow.py` owns the typed
admission and versioned planning-checkpoint contracts, while
-`app/db/adapters/transfer.py` commits admission and the
+`app/db/adapters/transfer/admission.py` commits admission and the
`accepted -> provider_pending -> planned` compare-and-set transitions in short
Session/UoW scopes. `app/modules/filemanager/` owns the
single pure-plan and checkpoint-execution implementation: all file writes occur
@@ -504,8 +504,8 @@ command; `FileManagerModule.transfer` and `TransHandler.transfer_media` must not
be recreated.
Transfer execution ownership is orthogonal to those planning phases.
-`app/application/transfer.py` defines the claim, heartbeat, release and fenced
-mutation Port; `app/db/adapters/transfer.py` implements each operation in a
+`app/application/transfer/workflow.py` defines the claim, heartbeat, release and fenced
+mutation Port; `app/db/adapters/transfer/admission.py` implements each operation in a
short UoW with a unique lease token. Any active lease rejects another claim,
including one from the same process owner. Expired leases may be taken over with
a new token and incremented attempt count, while the stale token cannot renew,
@@ -659,8 +659,9 @@ driven workflow registration.
| `app/application/subscription/write.py` | Subscription media translation and sync/async write-port orchestration |
| `app/application/outbox.py` | Durable intent, topic handler and Outbox repository contracts |
| `app/db/adapters/outbox.py` | SQLAlchemy Outbox persistence, claim/lease and retry state adapter |
-| `app/application/transfer.py` | Transfer task, durable admission, versioned planning input/checkpoint contracts and queue use case |
-| `app/db/adapters/transfer.py` | SQLAlchemy admission/checkpoint persistence, CAS state transition and detached snapshot adapter |
+| `app/application/chain/events.py` | Chain durable-event write port, settlement projection and replayable payload conversion |
+| `app/application/transfer/workflow.py` | Transfer task, durable admission, versioned planning input/checkpoint contracts and queue use case |
+| `app/db/adapters/transfer/admission.py` | SQLAlchemy admission/checkpoint persistence, CAS state transition and detached snapshot adapter |
| `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/application/workflow.py` | Workflow use cases plus the runtime port consumed by API and Chain; `WorkFlowManager` is registered by `app/startup/initializers/workflow.py` |
@@ -700,8 +701,8 @@ driven workflow registration.
| `app/adapters/external/plugin/client.py` | Plugin-market read adapter and cache-refresh boundary |
| `app/adapters/system/plugin/package.py` | Plugin package installation adapter |
| `app/adapters/system/plugin/dependency.py` | Plugin dependency inspection and installation adapter |
-| `app/runtime/extensions/managed_resource_adapter.py` | Data-only managed-resource registry and sync/async lifecycle adapters |
-| `app/runtime/managed_resources.py` | Lightweight acquisition, state observation and shutdown facade |
+| `app/runtime/extensions/resource.py` | Data-only managed-resource registry and sync/async lifecycle adapters |
+| `app/runtime/resources.py` | Lightweight acquisition, state observation and shutdown facade |
| `app/foundation/reflection.py` | Generic reflection and Python module discovery |
| `app/adapters/network/http.py` | Shared synchronous and asynchronous HTTP clients |
| `app/adapters/network/browser.py` | Browser launch facade and browser session implementation |
diff --git a/docs/rules/07-naming-conventions.md b/docs/rules/07-naming-conventions.md
index 340f6b298..838e78b52 100644
--- a/docs/rules/07-naming-conventions.md
+++ b/docs/rules/07-naming-conventions.md
@@ -9,8 +9,9 @@ All new code must follow these conventions. Consistent naming is how the codebas
| Context | Convention | Examples |
|---|---|---|
| Python source files | `snake_case.py` | `download.py`, `qbittorrent.py`, `package.py` |
-| New files in canonical capability packages | Focused `snake_case.py`; prefer a package-owned noun and an existing owned domain file before adding one | `torrent.py`, `plugin_manager.py`, `package.py` |
-| Module package directories | `snake_case/` | `qbittorrent/`, `synologychat/` |
+| New files in canonical capability packages | Prefer one lowercase responsibility noun; extend an existing owner before adding a sibling file | `torrent.py`, `package.py`, `resources.py` |
+| Multi-file capabilities | Create a same-named package and use focused single-word child files; do not flatten related `_.py` siblings | `transfer/workflow.py`, `transfer/execution.py` |
+| Module package directories | `snake_case/`; package roots do not duplicate-export host implementations | `qbittorrent/`, `synologychat/`, `transfer/` |
| Test files | `test_.py` | `test_download_chain.py`, `test_subscribe_endpoint.py` |
| Alembic migrations | Auto-generated by Alembic; do not rename | `20240101_add_column.py` |
| Skill directories | `/` | `transfer-failed-retry/`, `moviepilot-cli/` |
@@ -118,6 +119,8 @@ All new code must follow these conventions. Consistent naming is how the codebas
| `def handleConfigChanged():` | `def on_config_changed():` or `def handle_config_changed():` |
| `configuration.get("RssUrls")` | `configuration.get(SystemConfigKey.RssUrls)` |
| `class subscribe_oper:` | `class SubscribeOper:` |
+| `transfer.py` + `transfer_execution.py` | `transfer/workflow.py` + `transfer/execution.py` |
+| Package-root host re-exports for an old path | Exact SDK/Compat mapping; host code imports the owning child module |
| `MessageChannel.Telegram`(新代码) | `NotificationChannel.Telegram` |
| `Notification(title=...)`(新代码) | `Message(title=...)` |
diff --git a/mypy.ini b/mypy.ini
index 7ecbd4cf9..751b76ab9 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -28,7 +28,7 @@ files =
app/application/scheduling.py,
app/application/workflow.py,
app/application/chain/context.py,
- app/application/chain/durable_events.py,
+ app/application/chain/events.py,
app/application/messaging/ingress.py,
app/application/subscription/delete.py,
app/application/subscription/identity.py,
diff --git a/scripts/perf/instrument/sitecustomize.py b/scripts/perf/instrument/sitecustomize.py
index 3fa40a18e..d6832db7e 100644
--- a/scripts/perf/instrument/sitecustomize.py
+++ b/scripts/perf/instrument/sitecustomize.py
@@ -367,7 +367,7 @@ def _activate_agent_scenario(
def _read_display_runtime() -> dict[str, object]:
"""读取 host.display 的只读状态和观测,不触发资源激活。"""
try:
- from app.runtime.managed_resources import (
+ from app.runtime.resources import (
managed_resource_observations,
managed_resource_snapshot,
)
diff --git a/scripts/perf/test_scenarios.py b/scripts/perf/test_scenarios.py
index f111bcc40..d4c85568b 100644
--- a/scripts/perf/test_scenarios.py
+++ b/scripts/perf/test_scenarios.py
@@ -14,7 +14,6 @@ from types import ModuleType, SimpleNamespace
import pytest
-
PERF_DIR = Path(__file__).resolve().parent
@@ -651,7 +650,7 @@ def test_sitecustomize_serializes_managed_resource_facade(monkeypatch) -> None:
error=None,
)
- facade = ModuleType("app.runtime.managed_resources")
+ facade = ModuleType("app.runtime.resources")
def managed_resource_snapshot(capability_id: str):
assert capability_id == "host.display"
@@ -663,7 +662,7 @@ def test_sitecustomize_serializes_managed_resource_facade(monkeypatch) -> None:
facade.managed_resource_snapshot = managed_resource_snapshot
facade.managed_resource_observations = managed_resource_observations
- monkeypatch.setitem(sys.modules, "app.runtime.managed_resources", facade)
+ monkeypatch.setitem(sys.modules, "app.runtime.resources", facade)
probe = load_module(
"moviepilot_perf_sitecustomize_observation",
PERF_DIR / "instrument" / "sitecustomize.py",
diff --git a/tests/conftest.py b/tests/conftest.py
index 770df6085..81e3ea798 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -23,7 +23,6 @@ prepare_backend()
# 复用共享 autouse 网络守卫;同一实现亦供各插件仓 conftest import 复用,避免逐仓维护
from app.testing.network_guard import block_real_network # noqa: E402,F401
-
TResult = TypeVar("TResult")
@@ -104,31 +103,29 @@ def pytest_runtest_call(item):
def configure_plugin_system_services():
"""为绕过完整启动流程的单元测试装配真实插件系统适配器。"""
from app.adapters.web.security.access import configure_token_codec
- from app.application.security.token import (
- create_access_token,
- decode_access_token,
- )
from app.api.data import configure_api_data_ports
from app.application.configuration import (
RuntimeConfiguration,
RuntimeSettingsService,
SystemConfigService,
TransferRetryConfig,
- configure_token_runtime_config,
configure_runtime_configuration,
configure_runtime_settings,
configure_system_config,
+ configure_token_runtime_config,
configure_transfer_retry_config,
)
- from app.runtime.config import settings
- from app.runtime.settings import configure_runtime_setting_provider
- from app.startup.composition.configuration import (
- build_api_runtime_config,
- build_chain_runtime_config,
- build_scheduler_runtime_config,
- build_token_runtime_config,
+ from app.application.security.token import (
+ create_access_token,
+ decode_access_token,
+ )
+ from app.application.security.userconfig import (
+ UserConfigurationService,
+ configure_user_configuration,
)
from app.application.service import configure_service_directory
+ from app.db.oper.systemconfig import SystemConfigOper
+ from app.db.oper.userconfig import UserConfigOper
from app.db.session import (
SessionFactory,
async_session_scope,
@@ -140,11 +137,13 @@ def configure_plugin_system_services():
SqlAlchemyUnitOfWork,
configure_transaction_runners,
)
- from app.db.oper.systemconfig import SystemConfigOper
- from app.db.oper.userconfig import UserConfigOper
- from app.application.security.userconfig import (
- UserConfigurationService,
- configure_user_configuration,
+ from app.runtime.config import settings
+ from app.runtime.settings import configure_runtime_setting_provider
+ from app.startup.composition.configuration import (
+ build_api_runtime_config,
+ build_chain_runtime_config,
+ build_scheduler_runtime_config,
+ build_token_runtime_config,
)
configure_token_codec(create_access_token, decode_access_token)
@@ -181,25 +180,25 @@ def configure_plugin_system_services():
max_failed_retries=settings.TRANSFER_MAX_FAILED_RETRIES,
)
)
- from app.application.chain.data import configure_chain_data_ports
- from app.application.subscription.write import configure_subscribe_writer
- from app.application.plugin.runtime import configure_plugin_runtime
- from app.application.module import configure_module_runtime
from app.application.chain.context import (
ChainRuntimeContext,
configure_chain_runtime_context_provider,
)
- from app.application.messaging.message import MessageHelper, MessageQueueManager
+ from app.application.chain.data import configure_chain_data_ports
from app.application.messaging.chat import (
- AgentChatService,
AgentChatPersistenceService,
- configure_agent_chat_service,
+ AgentChatService,
configure_agent_chat_persistence,
+ configure_agent_chat_service,
)
+ from app.application.messaging.message import MessageHelper, MessageQueueManager
+ from app.application.module import configure_module_runtime
+ from app.application.plugin.runtime import configure_plugin_runtime
+ from app.application.subscription.write import configure_subscribe_writer
from app.runtime.cache import AsyncFileCache, FileCache
from app.runtime.events import EventManager
- from app.runtime.extensions.module_manager import ModuleManager
from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher
+ from app.runtime.extensions.module_manager import ModuleManager
from app.runtime.extensions.plugin_manager import PluginManager
from app.runtime.extensions.service_config import ServiceConfigHelper
configure_service_directory(
@@ -208,8 +207,8 @@ def configure_plugin_system_services():
)
configure_plugin_runtime(lambda: PluginManager())
configure_module_runtime(lambda: ModuleManager())
- 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.site.query import SiteQueryService, configure_site_query_service
from app.application.workflow import (
WorkflowQueryService,
configure_workflow_query,
@@ -222,27 +221,26 @@ def configure_plugin_system_services():
AgentTaskExecutionService,
configure_agent_task_execution,
)
+ from app.db.adapters.download import TransactionalDownloadFailureRepository
+ from app.db.adapters.site import TransactionalSiteRepository
+ from app.db.adapters.subscription import TransactionalSubscribeWriter
+ from app.db.adapters.transaction import TransactionalWriteRunner
+ from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
+ from app.db.adapters.transfer.execution import (
+ TransactionalTransferExecutionRepository,
+ )
+ from app.db.adapters.workflow import TransactionalWorkflowExecutionService
from app.db.oper.agentchat import AgentChatOper
- from app.db.oper.downloadfailure import DownloadFailureOper
from app.db.oper.downloadhistory import DownloadHistoryOper
from app.db.oper.mediaserver import MediaServerOper
+ from app.db.oper.message import MessageOper
+ from app.db.oper.passkey import PassKeyOper
from app.db.oper.site import SiteOper
from app.db.oper.subscribe import SubscribeOper
from app.db.oper.subscribehistory import SubscribeHistoryOper
from app.db.oper.transferhistory import TransferHistoryOper
- from app.db.adapters.transfer import TransactionalTransferAdmissionRepository
- from app.db.adapters.transfer_execution import (
- TransactionalTransferExecutionRepository,
- )
from app.db.oper.user import UserOper
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.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 create_sync_session() -> Session:
"""为无显式会话的 Oper 测试入口创建独占同步 Session。"""
@@ -363,8 +361,8 @@ def configure_plugin_system_services():
)
configure_agent_chat_service(AgentChatService(repository=AgentChatOper()))
from app.adapters.external.market import (
- PluginHelper,
VERSION_BACKWARD_COMPATIBLE_FLAGS,
+ PluginHelper,
)
from app.adapters.external.plugin.client import PluginMarketClient
from app.adapters.system.plugin.dependency import PluginDependencyInstaller
@@ -389,9 +387,9 @@ def configure_plugin_system_services():
frozen=lambda: False,
install=lambda **_kwargs: (False, "测试环境未装配插件安装 Gateway"),
))
- from app.agent.skills.registry import SkillHelper
from app.agent.llm.gateway import register_llm_provider_runtime
from app.agent.llm.provider import LLMProviderManager
+ from app.agent.skills.registry import SkillHelper
from app.application.messaging.skill import register_skill_catalog_provider
register_skill_catalog_provider(lambda: SkillHelper())
diff --git a/tests/fixtures/architecture/coverage-baseline.json b/tests/fixtures/architecture/coverage-baseline.json
index a41d68151..43e601d8f 100644
--- a/tests/fixtures/architecture/coverage-baseline.json
+++ b/tests/fixtures/architecture/coverage-baseline.json
@@ -1,8 +1,8 @@
{
"application": {
- "covered_lines": 9966,
- "percent": 78.61,
- "statements": 12678
+ "covered_lines": 9992,
+ "percent": 78.63,
+ "statements": 12707
},
"domain": {
"covered_lines": 3392,
diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json
index 50c467a19..15337674d 100644
--- a/tests/fixtures/architecture/dependency-baseline.json
+++ b/tests/fixtures/architecture/dependency-baseline.json
@@ -77,7 +77,7 @@
"target": "app.adapters.network.http"
},
{
- "source": "app.application.transfer",
+ "source": "app.application.transfer.workflow",
"target": "app.adapters.system.host"
},
{
@@ -154,7 +154,7 @@
"app.application.security.cookie",
"app.application.security.passkey",
"app.application.torrent",
- "app.application.transfer",
+ "app.application.transfer.workflow",
"app.chain._recognition",
"app.chain._transfer",
"app.chain.download",
@@ -1441,8 +1441,8 @@
"runtime_only": true
}
},
- "edge_count": 6882,
- "edge_sha256": "602a73df30503fec4f4f01e28020222497cdadcad731dfedca40a3e4b1133c0d",
+ "edge_count": 6898,
+ "edge_sha256": "c73faf7e29ee12a862cf2332f1803fd4eb8a3f4d2994ed81075ff198e896f8df",
"edges": [
"app -> app.runtime",
"app -> app.runtime.compat",
@@ -1532,7 +1532,7 @@
"app.adapters.network.browser -> app.adapters.network.http",
"app.adapters.network.browser -> app.runtime",
"app.adapters.network.browser -> app.runtime.log",
- "app.adapters.network.browser -> app.runtime.managed_resources",
+ "app.adapters.network.browser -> app.runtime.resources",
"app.adapters.network.browser -> app.runtime.settings",
"app.adapters.network.cloudflare -> app.runtime",
"app.adapters.network.cloudflare -> app.runtime.log",
@@ -1553,7 +1553,7 @@
"app.adapters.system.display -> app.foundation.singleton",
"app.adapters.system.display -> app.runtime",
"app.adapters.system.display -> app.runtime.log",
- "app.adapters.system.display -> app.runtime.managed_resources",
+ "app.adapters.system.display -> app.runtime.resources",
"app.adapters.system.display.resource -> app.adapters",
"app.adapters.system.display.resource -> app.adapters.system",
"app.adapters.system.display.resource -> app.adapters.system.host",
@@ -2219,7 +2219,8 @@
"app.agent.tools.impl.delete_transfer_history -> app.application.agentdata",
"app.agent.tools.impl.delete_transfer_history -> app.application.chain",
"app.agent.tools.impl.delete_transfer_history -> app.application.chain.data",
- "app.agent.tools.impl.delete_transfer_history -> app.application.transfer_execution",
+ "app.agent.tools.impl.delete_transfer_history -> app.application.transfer",
+ "app.agent.tools.impl.delete_transfer_history -> app.application.transfer.execution",
"app.agent.tools.impl.delete_transfer_history -> app.chain",
"app.agent.tools.impl.delete_transfer_history -> app.chain.storage",
"app.agent.tools.impl.delete_transfer_history -> app.runtime",
@@ -3313,7 +3314,8 @@
"app.api.endpoints.history -> app.application.chain.data",
"app.api.endpoints.history -> app.application.configuration",
"app.api.endpoints.history -> app.application.history",
- "app.api.endpoints.history -> app.application.transfer_execution",
+ "app.api.endpoints.history -> app.application.transfer",
+ "app.api.endpoints.history -> app.application.transfer.execution",
"app.api.endpoints.history -> app.runtime",
"app.api.endpoints.history -> app.runtime.config",
"app.api.endpoints.history -> app.runtime.log",
@@ -3824,7 +3826,8 @@
"app.api.endpoints.transfer -> app.application.configuration",
"app.api.endpoints.transfer -> app.application.directory",
"app.api.endpoints.transfer -> app.application.history",
- "app.api.endpoints.transfer -> app.application.transfer_execution",
+ "app.api.endpoints.transfer -> app.application.transfer",
+ "app.api.endpoints.transfer -> app.application.transfer.execution",
"app.api.endpoints.transfer -> app.chain",
"app.api.endpoints.transfer -> app.chain.media",
"app.api.endpoints.transfer -> app.chain.transfer",
@@ -3977,26 +3980,28 @@
"app.application.chain.context -> app.application",
"app.application.chain.context -> app.application.chain",
"app.application.chain.context -> app.application.chain.data",
- "app.application.chain.context -> app.application.chain.durable_events",
+ "app.application.chain.context -> app.application.chain.events",
"app.application.chain.context -> app.application.configuration",
"app.application.chain.context -> app.runtime",
"app.application.chain.context -> app.runtime.stop",
"app.application.chain.data -> app.application",
"app.application.chain.data -> app.application.transfer",
- "app.application.chain.data -> app.application.transfer_execution",
- "app.application.chain.durable_events -> app.application",
- "app.application.chain.durable_events -> app.application.history",
- "app.application.chain.durable_events -> app.application.transfer_execution",
- "app.application.chain.durable_events -> app.domain",
- "app.application.chain.durable_events -> app.domain.context",
- "app.application.chain.durable_events -> app.domain.meta",
- "app.application.chain.durable_events -> app.domain.meta.metabase",
- "app.application.chain.durable_events -> app.domain.meta.metamusic",
- "app.application.chain.durable_events -> app.domain.metainfo",
- "app.application.chain.durable_events -> app.schemas",
- "app.application.chain.durable_events -> app.schemas.file",
- "app.application.chain.durable_events -> app.schemas.transfer",
- "app.application.chain.durable_events -> app.schemas.types",
+ "app.application.chain.data -> app.application.transfer.execution",
+ "app.application.chain.data -> app.application.transfer.workflow",
+ "app.application.chain.events -> app.application",
+ "app.application.chain.events -> app.application.history",
+ "app.application.chain.events -> app.application.transfer",
+ "app.application.chain.events -> app.application.transfer.execution",
+ "app.application.chain.events -> app.domain",
+ "app.application.chain.events -> app.domain.context",
+ "app.application.chain.events -> app.domain.meta",
+ "app.application.chain.events -> app.domain.meta.metabase",
+ "app.application.chain.events -> app.domain.meta.metamusic",
+ "app.application.chain.events -> app.domain.metainfo",
+ "app.application.chain.events -> app.schemas",
+ "app.application.chain.events -> app.schemas.file",
+ "app.application.chain.events -> app.schemas.transfer",
+ "app.application.chain.events -> app.schemas.types",
"app.application.configuration -> app.application",
"app.application.configuration -> app.application.database",
"app.application.configuration -> app.schemas",
@@ -4441,31 +4446,33 @@
"app.application.torrent_cache -> app.foundation.crypto",
"app.application.torrent_cache -> app.schemas",
"app.application.torrent_cache -> app.schemas.types",
- "app.application.transfer -> app.adapters",
- "app.application.transfer -> app.adapters.system",
- "app.application.transfer -> app.adapters.system.host",
- "app.application.transfer -> app.application",
- "app.application.transfer -> app.application.agent",
- "app.application.transfer -> app.application.transfer_execution",
- "app.application.transfer -> app.domain",
- "app.application.transfer -> app.domain.context",
- "app.application.transfer -> app.domain.media",
- "app.application.transfer -> app.domain.meta",
- "app.application.transfer -> app.domain.meta.metabase",
- "app.application.transfer -> app.domain.meta.metamusic",
- "app.application.transfer -> app.foundation",
- "app.application.transfer -> app.foundation.text",
- "app.application.transfer -> app.runtime",
- "app.application.transfer -> app.runtime.log",
- "app.application.transfer -> app.schemas",
- "app.application.transfer -> app.schemas.file",
- "app.application.transfer -> app.schemas.history",
- "app.application.transfer -> app.schemas.media",
- "app.application.transfer -> app.schemas.system",
- "app.application.transfer -> app.schemas.tmdb",
- "app.application.transfer -> app.schemas.transfer",
- "app.application.transfer -> app.schemas.types",
- "app.application.transfer -> app.schemas.workflow",
+ "app.application.transfer.workflow -> app.adapters",
+ "app.application.transfer.workflow -> app.adapters.system",
+ "app.application.transfer.workflow -> app.adapters.system.host",
+ "app.application.transfer.workflow -> app.application",
+ "app.application.transfer.workflow -> app.application.agent",
+ "app.application.transfer.workflow -> app.application.transfer",
+ "app.application.transfer.workflow -> app.application.transfer.execution",
+ "app.application.transfer.workflow -> app.domain",
+ "app.application.transfer.workflow -> app.domain.context",
+ "app.application.transfer.workflow -> app.domain.media",
+ "app.application.transfer.workflow -> app.domain.meta",
+ "app.application.transfer.workflow -> app.domain.meta.metabase",
+ "app.application.transfer.workflow -> app.domain.meta.metamusic",
+ "app.application.transfer.workflow -> app.foundation",
+ "app.application.transfer.workflow -> app.foundation.text",
+ "app.application.transfer.workflow -> app.runtime",
+ "app.application.transfer.workflow -> app.runtime.log",
+ "app.application.transfer.workflow -> app.schemas",
+ "app.application.transfer.workflow -> app.schemas.context",
+ "app.application.transfer.workflow -> app.schemas.file",
+ "app.application.transfer.workflow -> app.schemas.history",
+ "app.application.transfer.workflow -> app.schemas.media",
+ "app.application.transfer.workflow -> app.schemas.music",
+ "app.application.transfer.workflow -> app.schemas.system",
+ "app.application.transfer.workflow -> app.schemas.tmdb",
+ "app.application.transfer.workflow -> app.schemas.transfer",
+ "app.application.transfer.workflow -> app.schemas.types",
"app.chain -> app.application",
"app.chain -> app.application.chain",
"app.chain -> app.application.chain.context",
@@ -4568,7 +4575,8 @@
"app.chain._transfer -> app.application.formatting",
"app.chain._transfer -> app.application.history",
"app.chain._transfer -> app.application.transfer",
- "app.chain._transfer -> app.application.transfer_execution",
+ "app.chain._transfer -> app.application.transfer.execution",
+ "app.chain._transfer -> app.application.transfer.workflow",
"app.chain._transfer -> app.chain",
"app.chain._transfer -> app.chain._contracts",
"app.chain._transfer -> app.chain.media",
@@ -4994,14 +5002,15 @@
"app.chain.transfer -> app.application",
"app.chain.transfer -> app.application.chain",
"app.chain.transfer -> app.application.chain.data",
- "app.chain.transfer -> app.application.chain.durable_events",
+ "app.chain.transfer -> app.application.chain.events",
"app.chain.transfer -> app.application.configuration",
"app.chain.transfer -> app.application.directory",
"app.chain.transfer -> app.application.formatting",
"app.chain.transfer -> app.application.history",
"app.chain.transfer -> app.application.outbox",
"app.chain.transfer -> app.application.transfer",
- "app.chain.transfer -> app.application.transfer_execution",
+ "app.chain.transfer -> app.application.transfer.execution",
+ "app.chain.transfer -> app.application.transfer.workflow",
"app.chain.transfer -> app.chain",
"app.chain.transfer -> app.chain._transfer",
"app.chain.transfer -> app.chain.media",
@@ -5105,10 +5114,11 @@
"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.chain.events",
"app.db.adapters.chain -> app.application.history",
"app.db.adapters.chain -> app.application.outbox",
- "app.db.adapters.chain -> app.application.transfer_execution",
+ "app.db.adapters.chain -> app.application.transfer",
+ "app.db.adapters.chain -> app.application.transfer.execution",
"app.db.adapters.chain -> app.db",
"app.db.adapters.chain -> app.db.adapters",
"app.db.adapters.chain -> app.db.adapters.outbox",
@@ -5165,24 +5175,26 @@
"app.db.adapters.subscription -> app.db.uow",
"app.db.adapters.transaction -> app.db",
"app.db.adapters.transaction -> app.db.uow",
- "app.db.adapters.transfer -> app.application",
- "app.db.adapters.transfer -> app.application.transfer",
- "app.db.adapters.transfer -> app.db",
- "app.db.adapters.transfer -> app.db.models",
- "app.db.adapters.transfer -> app.db.models.transferpending",
- "app.db.adapters.transfer -> app.db.oper",
- "app.db.adapters.transfer -> app.db.oper.transferpending",
- "app.db.adapters.transfer -> app.db.uow",
- "app.db.adapters.transfer_execution -> app.application",
- "app.db.adapters.transfer_execution -> app.application.transfer_execution",
- "app.db.adapters.transfer_execution -> app.db",
- "app.db.adapters.transfer_execution -> app.db.models",
- "app.db.adapters.transfer_execution -> app.db.models.transferexecutionstep",
- "app.db.adapters.transfer_execution -> app.db.models.transferpending",
- "app.db.adapters.transfer_execution -> app.db.oper",
- "app.db.adapters.transfer_execution -> app.db.oper.transferexecutionstep",
- "app.db.adapters.transfer_execution -> app.db.oper.transferpending",
- "app.db.adapters.transfer_execution -> app.db.uow",
+ "app.db.adapters.transfer.admission -> app.application",
+ "app.db.adapters.transfer.admission -> app.application.transfer",
+ "app.db.adapters.transfer.admission -> app.application.transfer.workflow",
+ "app.db.adapters.transfer.admission -> app.db",
+ "app.db.adapters.transfer.admission -> app.db.models",
+ "app.db.adapters.transfer.admission -> app.db.models.transferpending",
+ "app.db.adapters.transfer.admission -> app.db.oper",
+ "app.db.adapters.transfer.admission -> app.db.oper.transferpending",
+ "app.db.adapters.transfer.admission -> app.db.uow",
+ "app.db.adapters.transfer.execution -> app.application",
+ "app.db.adapters.transfer.execution -> app.application.transfer",
+ "app.db.adapters.transfer.execution -> app.application.transfer.execution",
+ "app.db.adapters.transfer.execution -> app.db",
+ "app.db.adapters.transfer.execution -> app.db.models",
+ "app.db.adapters.transfer.execution -> app.db.models.transferexecutionstep",
+ "app.db.adapters.transfer.execution -> app.db.models.transferpending",
+ "app.db.adapters.transfer.execution -> app.db.oper",
+ "app.db.adapters.transfer.execution -> app.db.oper.transferexecutionstep",
+ "app.db.adapters.transfer.execution -> app.db.oper.transferpending",
+ "app.db.adapters.transfer.execution -> app.db.uow",
"app.db.adapters.workflow -> app.application",
"app.db.adapters.workflow -> app.application.workflow",
"app.db.adapters.workflow -> app.db",
@@ -5852,7 +5864,8 @@
"app.modules.filemanager.module -> app.application.messaging",
"app.modules.filemanager.module -> app.application.messaging.message",
"app.modules.filemanager.module -> app.application.transfer",
- "app.modules.filemanager.module -> app.application.transfer_execution",
+ "app.modules.filemanager.module -> app.application.transfer.execution",
+ "app.modules.filemanager.module -> app.application.transfer.workflow",
"app.modules.filemanager.module -> app.domain",
"app.modules.filemanager.module -> app.domain.context",
"app.modules.filemanager.module -> app.domain.meta",
@@ -6005,7 +6018,8 @@
"app.modules.filemanager.transhandler -> app.application.messaging",
"app.modules.filemanager.transhandler -> app.application.messaging.message",
"app.modules.filemanager.transhandler -> app.application.transfer",
- "app.modules.filemanager.transhandler -> app.application.transfer_execution",
+ "app.modules.filemanager.transhandler -> app.application.transfer.execution",
+ "app.modules.filemanager.transhandler -> app.application.transfer.workflow",
"app.modules.filemanager.transhandler -> app.domain",
"app.modules.filemanager.transhandler -> app.domain.context",
"app.modules.filemanager.transhandler -> app.domain.meta",
@@ -7351,12 +7365,6 @@
"app.runtime.extensions.host_module_adapter -> app.runtime.settings",
"app.runtime.extensions.host_module_adapter -> app.schemas",
"app.runtime.extensions.host_module_adapter -> app.schemas.types",
- "app.runtime.extensions.managed_resource_adapter -> app.runtime",
- "app.runtime.extensions.managed_resource_adapter -> app.runtime.capabilities",
- "app.runtime.extensions.managed_resource_adapter -> app.runtime.capabilities.errors",
- "app.runtime.extensions.managed_resource_adapter -> app.runtime.capabilities.model",
- "app.runtime.extensions.managed_resource_adapter -> app.runtime.capabilities.registry",
- "app.runtime.extensions.managed_resource_adapter -> app.runtime.managed_resources",
"app.runtime.extensions.module.dispatcher -> app.foundation",
"app.runtime.extensions.module.dispatcher -> app.foundation.reflection",
"app.runtime.extensions.module.dispatcher -> app.runtime",
@@ -7479,6 +7487,12 @@
"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.resource -> app.runtime",
+ "app.runtime.extensions.resource -> app.runtime.capabilities",
+ "app.runtime.extensions.resource -> app.runtime.capabilities.errors",
+ "app.runtime.extensions.resource -> app.runtime.capabilities.model",
+ "app.runtime.extensions.resource -> app.runtime.capabilities.registry",
+ "app.runtime.extensions.resource -> app.runtime.resources",
"app.runtime.extensions.service_config -> app.runtime",
"app.runtime.extensions.service_config -> app.runtime.log",
"app.runtime.extensions.service_config -> app.schemas",
@@ -7670,6 +7684,7 @@
"app.sdk._legacy.subscribe -> app.domain.context",
"app.sdk._legacy.transfer -> app.application",
"app.sdk._legacy.transfer -> app.application.transfer",
+ "app.sdk._legacy.transfer -> app.application.transfer.workflow",
"app.sdk._legacy.transferpending -> app.db",
"app.sdk._legacy.transferpending -> app.db.base",
"app.sdk._legacy.transferpending -> app.db.models",
@@ -7890,12 +7905,6 @@
"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",
@@ -7918,7 +7927,7 @@
"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.chain.events",
"app.startup.initializers.modules -> app.application.configuration",
"app.startup.initializers.modules -> app.application.database",
"app.startup.initializers.modules -> app.application.history",
@@ -7970,7 +7979,8 @@
"app.startup.initializers.modules -> app.db.adapters.subscription",
"app.startup.initializers.modules -> app.db.adapters.transaction",
"app.startup.initializers.modules -> app.db.adapters.transfer",
- "app.startup.initializers.modules -> app.db.adapters.transfer_execution",
+ "app.startup.initializers.modules -> app.db.adapters.transfer.admission",
+ "app.startup.initializers.modules -> app.db.adapters.transfer.execution",
"app.startup.initializers.modules -> app.db.adapters.workflow",
"app.startup.initializers.modules -> app.db.oper",
"app.startup.initializers.modules -> app.db.oper.agentchat",
@@ -8021,7 +8031,7 @@
"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.modules -> app.startup.initializers.resources",
"app.startup.initializers.monitor -> app.monitor",
"app.startup.initializers.monitor -> app.runtime",
"app.startup.initializers.monitor -> app.runtime.execution",
@@ -8075,12 +8085,18 @@
"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.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.resources -> app.runtime",
+ "app.startup.initializers.resources -> app.runtime.capabilities",
+ "app.startup.initializers.resources -> app.runtime.capabilities.runtime",
+ "app.startup.initializers.resources -> app.runtime.extensions",
+ "app.startup.initializers.resources -> app.runtime.extensions.resource",
+ "app.startup.initializers.resources -> app.runtime.resources",
"app.startup.initializers.routers -> app.api",
"app.startup.initializers.routers -> app.api.router_specs",
"app.startup.initializers.routers -> app.api.servarr",
@@ -8327,7 +8343,7 @@
"app.workflow.actions.transfer_file -> app.workflow",
"app.workflow.actions.transfer_file -> app.workflow.actions"
],
- "module_count": 842,
+ "module_count": 844,
"modules": [
"app",
"app.adapters",
@@ -8583,7 +8599,7 @@
"app.application.chain",
"app.application.chain.context",
"app.application.chain.data",
- "app.application.chain.durable_events",
+ "app.application.chain.events",
"app.application.commands",
"app.application.configuration",
"app.application.dashboard",
@@ -8674,7 +8690,8 @@
"app.application.torrent",
"app.application.torrent_cache",
"app.application.transfer",
- "app.application.transfer_execution",
+ "app.application.transfer.execution",
+ "app.application.transfer.workflow",
"app.application.workflow",
"app.chain",
"app.chain._contracts",
@@ -8726,7 +8743,8 @@
"app.db.adapters.subscription",
"app.db.adapters.transaction",
"app.db.adapters.transfer",
- "app.db.adapters.transfer_execution",
+ "app.db.adapters.transfer.admission",
+ "app.db.adapters.transfer.execution",
"app.db.adapters.workflow",
"app.db.base",
"app.db.decorators",
@@ -9025,7 +9043,6 @@
"app.runtime.execution",
"app.runtime.extensions",
"app.runtime.extensions.host_module_adapter",
- "app.runtime.extensions.managed_resource_adapter",
"app.runtime.extensions.module",
"app.runtime.extensions.module.contracts",
"app.runtime.extensions.module.dispatcher",
@@ -9050,16 +9067,17 @@
"app.runtime.extensions.plugin.system",
"app.runtime.extensions.plugin.tools",
"app.runtime.extensions.plugin_manager",
+ "app.runtime.extensions.resource",
"app.runtime.extensions.service_config",
"app.runtime.gc",
"app.runtime.health",
"app.runtime.localization",
"app.runtime.log",
- "app.runtime.managed_resources",
"app.runtime.observability",
"app.runtime.progress",
"app.runtime.rate",
"app.runtime.reload",
+ "app.runtime.resources",
"app.runtime.scheduling",
"app.runtime.settings",
"app.runtime.state",
@@ -9140,10 +9158,10 @@
"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.resources",
"app.startup.initializers.routers",
"app.startup.initializers.scheduler",
"app.startup.initializers.transfer",
diff --git a/tests/fixtures/architecture/dependency-policy.json b/tests/fixtures/architecture/dependency-policy.json
index 69452db64..82a27296f 100644
--- a/tests/fixtures/architecture/dependency-policy.json
+++ b/tests/fixtures/architecture/dependency-policy.json
@@ -142,7 +142,7 @@
"tracking": "S2-L6"
},
{
- "source": "app.application.transfer",
+ "source": "app.application.transfer.workflow",
"target": "app.adapters.system.host",
"tracking": "S2-L6"
},
diff --git a/tests/fixtures/architecture/mypy-baseline.json b/tests/fixtures/architecture/mypy-baseline.json
index 81ad20cc1..7c2a61cdc 100644
--- a/tests/fixtures/architecture/mypy-baseline.json
+++ b/tests/fixtures/architecture/mypy-baseline.json
@@ -1112,10 +1112,6 @@
"app/application/agenttask.py": {
"type-arg": 1
},
- "app/application/chain/durable_events.py": {
- "arg-type": 1,
- "no-untyped-call": 1
- },
"app/application/commands.py": {
"no-any-return": 1
},
@@ -1356,18 +1352,6 @@
"app/application/torrent_cache.py": {
"type-arg": 1
},
- "app/application/transfer.py": {
- "arg-type": 16,
- "assignment": 3,
- "attr-defined": 4,
- "misc": 6,
- "no-any-return": 1,
- "no-untyped-call": 2,
- "no-untyped-def": 10,
- "return-value": 1,
- "type-arg": 14,
- "union-attr": 47
- },
"app/chain/__init__.py": {
"assignment": 13,
"attr-defined": 1,
@@ -3210,10 +3194,6 @@
"app/runtime/localization.py": {
"no-any-return": 1
},
- "app/runtime/managed_resources.py": {
- "arg-type": 2,
- "union-attr": 3
- },
"app/runtime/progress.py": {
"no-any-return": 4,
"type-arg": 6
diff --git a/tests/fixtures/architecture/ruff-baseline.json b/tests/fixtures/architecture/ruff-baseline.json
index e0a61008d..8a6fa8018 100644
--- a/tests/fixtures/architecture/ruff-baseline.json
+++ b/tests/fixtures/architecture/ruff-baseline.json
@@ -15,9 +15,6 @@
"F841": 2,
"I001": 1
},
- "app/adapters/system/display/__init__.py": {
- "I001": 1
- },
"app/adapters/system/host.py": {
"I001": 2
},
@@ -300,9 +297,6 @@
"app/application/agenttask.py": {
"I001": 1
},
- "app/application/chain/data.py": {
- "I001": 1
- },
"app/application/directory.py": {
"I001": 1
},
@@ -392,10 +386,6 @@
"app/chain/_music.py": {
"E402": 5
},
- "app/chain/_transfer.py": {
- "E402": 15,
- "F401": 1
- },
"app/chain/media.py": {
"E731": 2
},
@@ -962,9 +952,6 @@
"app/runtime/extensions/host_module_adapter.py": {
"I001": 1
},
- "app/runtime/extensions/managed_resource_adapter.py": {
- "I001": 1
- },
"app/runtime/extensions/module_manager.py": {
"I001": 1
},
@@ -990,9 +977,6 @@
"E731": 1,
"I001": 1
},
- "app/runtime/managed_resources.py": {
- "I001": 1
- },
"app/runtime/observability/__init__.py": {
"I001": 1
},
@@ -1038,18 +1022,12 @@
"app/schemas/system.py": {
"I001": 1
},
- "app/schemas/transfer.py": {
- "I001": 1
- },
"app/schemas/types.py": {
"I001": 1
},
"app/schemas/user.py": {
"I001": 1
},
- "app/sdk/_legacy/transfer.py": {
- "I001": 1
- },
"app/sdk/_legacy/user.py": {
"I001": 1
},
@@ -1125,9 +1103,6 @@
"scripts/perf/task_registry_ab.py": {
"E402": 1
},
- "scripts/perf/test_scenarios.py": {
- "I001": 1
- },
"scripts/schema/exports.py": {
"I001": 1
},
@@ -1137,10 +1112,6 @@
"scripts/startup/performance.py": {
"I001": 1
},
- "tests/conftest.py": {
- "F401": 1,
- "I001": 7
- },
"tests/test_acoustid_module.py": {
"I001": 1
},
@@ -1440,12 +1411,6 @@
"tests/test_main_direct_execution.py": {
"I001": 1
},
- "tests/test_managed_resources.py": {
- "I001": 1
- },
- "tests/test_manual_transfer_history.py": {
- "I001": 1
- },
"tests/test_mcp_plugin_tools.py": {
"I001": 1
},
@@ -1797,9 +1762,6 @@
"E402": 6,
"I001": 2
},
- "tests/test_transfer_failure_notification_aggregation.py": {
- "I001": 1
- },
"tests/test_transfer_history_gate.py": {
"I001": 1
},
@@ -1809,27 +1771,12 @@
"tests/test_transfer_history_write_path.py": {
"I001": 1
},
- "tests/test_transfer_mark_torrent_completed.py": {
- "I001": 1
- },
- "tests/test_transfer_mounted_disk_cleanup.py": {
- "I001": 1
- },
- "tests/test_transfer_movie_collection.py": {
- "I001": 1
- },
"tests/test_transfer_queue_count.py": {
"I001": 1
},
"tests/test_transfer_rename_build_event.py": {
"I001": 1
},
- "tests/test_transfer_stale_tasks.py": {
- "I001": 1
- },
- "tests/test_transfer_tmdb_category.py": {
- "I001": 1
- },
"tests/test_transferhistory_media_source_migration.py": {
"I001": 1
},
diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json
index 5a69ca4a6..14942cc72 100644
--- a/tests/fixtures/architecture/runtime-contract-baseline.json
+++ b/tests/fixtures/architecture/runtime-contract-baseline.json
@@ -1,6 +1,13 @@
{
"compat_manifest": {
"module_aliases": {
+ "app.application.chain.durable_events": {
+ "introduced": "v3.0.0",
+ "is_package": false,
+ "owner": "application",
+ "replacement": "app.application.chain.events",
+ "target": "app.application.chain.events"
+ },
"app.application.filter": {
"introduced": "v3.0.0",
"is_package": false,
@@ -15,6 +22,13 @@
"replacement": "app.application.rules",
"target": "app.application.rules"
},
+ "app.application.transfer_execution": {
+ "introduced": "v3.0.0",
+ "is_package": false,
+ "owner": "application",
+ "replacement": "app.application.transfer.execution",
+ "target": "app.application.transfer.execution"
+ },
"app.chain.media_interaction": {
"introduced": "v3.0.0",
"is_package": false,
@@ -250,7 +264,7 @@
"introduced": "v3.0.0",
"is_package": false,
"owner": "sdk",
- "replacement": "app.application.transfer",
+ "replacement": "app.application.transfer.workflow",
"target": "app.sdk._legacy.transferpending"
},
"app.db.user_oper": {
@@ -596,6 +610,13 @@
"replacement": "app.sdk.services",
"target": "app.sdk.services"
},
+ "app.runtime.managed_resources": {
+ "introduced": "v3.0.0",
+ "is_package": false,
+ "owner": "runtime",
+ "replacement": "app.runtime.resources",
+ "target": "app.runtime.resources"
+ },
"app.utils.coalesce": {
"introduced": "v3.0.0",
"is_package": false,
@@ -869,6 +890,18 @@
"target_name": "ReplyMode"
}
},
+ "app.application.transfer": {
+ "TransferQueue": {
+ "replacement": "app.application.transfer.workflow.TransferQueue",
+ "target_module": "app.sdk._legacy.transfer",
+ "target_name": "TransferQueue"
+ },
+ "TransferTask": {
+ "replacement": "app.application.transfer.workflow.TransferTask",
+ "target_module": "app.sdk._legacy.transfer",
+ "target_name": "TransferTask"
+ }
+ },
"app.chain.media": {
"ScrapingChain": {
"replacement": "app.chain.scraping.ScrapingChain",
@@ -987,12 +1020,12 @@
"target_name": "MessageType"
},
"TransferQueue": {
- "replacement": "app.application.transfer.TransferQueue",
+ "replacement": "app.application.transfer.workflow.TransferQueue",
"target_module": "app.sdk._legacy.transfer",
"target_name": "TransferQueue"
},
"TransferTask": {
- "replacement": "app.application.transfer.TransferTask",
+ "replacement": "app.application.transfer.workflow.TransferTask",
"target_module": "app.sdk._legacy.transfer",
"target_name": "TransferTask"
}
@@ -1083,12 +1116,12 @@
"target_name": "TransferDirectoryConf"
},
"TransferQueue": {
- "replacement": "app.application.transfer.TransferQueue",
+ "replacement": "app.application.transfer.workflow.TransferQueue",
"target_module": "app.sdk._legacy.transfer",
"target_name": "TransferQueue"
},
"TransferTask": {
- "replacement": "app.application.transfer.TransferTask",
+ "replacement": "app.application.transfer.workflow.TransferTask",
"target_module": "app.sdk._legacy.transfer",
"target_name": "TransferTask"
}
diff --git a/tests/fixtures/architecture/startup-performance-baseline.json b/tests/fixtures/architecture/startup-performance-baseline.json
index 9ef42c5c6..7d8e7c78b 100644
--- a/tests/fixtures/architecture/startup-performance-baseline.json
+++ b/tests/fixtures/architecture/startup-performance-baseline.json
@@ -1,41 +1,41 @@
{
"schema_version": 2,
- "generated_at": "2026-08-27T12:39:02.223653+00:00",
+ "generated_at": "2026-08-27T13:33:52.081319+00:00",
"platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O",
"python": "3.14.3",
"repeat": 3,
"targets": {
"app.startup.lifecycle": {
- "loaded_app_module_count": 386,
- "max_ms": 943.849,
- "median_ms": 933.725,
- "min_ms": 933.213,
+ "loaded_app_module_count": 388,
+ "max_ms": 1035.341,
+ "median_ms": 993.245,
+ "min_ms": 991.998,
"samples_ms": [
- 943.849,
- 933.213,
- 933.725
+ 1035.341,
+ 991.998,
+ 993.245
]
},
"app.factory": {
- "loaded_app_module_count": 398,
- "max_ms": 959.917,
- "median_ms": 955.782,
- "min_ms": 950.477,
+ "loaded_app_module_count": 400,
+ "max_ms": 1006.172,
+ "median_ms": 1005.88,
+ "min_ms": 1003.841,
"samples_ms": [
- 950.477,
- 955.782,
- 959.917
+ 1005.88,
+ 1006.172,
+ 1003.841
]
},
"app.main": {
- "loaded_app_module_count": 400,
- "max_ms": 1027.215,
- "median_ms": 1012.512,
- "min_ms": 996.609,
+ "loaded_app_module_count": 402,
+ "max_ms": 1085.652,
+ "median_ms": 1055.994,
+ "min_ms": 1043.212,
"samples_ms": [
- 1027.215,
- 1012.512,
- 996.609
+ 1055.994,
+ 1043.212,
+ 1085.652
]
}
},
@@ -47,24 +47,24 @@
{
"mode": "normal",
"enabled_component_count": 25,
- "startup_ms": 0.668,
- "full_lifespan_ms": 1.475,
+ "startup_ms": 0.702,
+ "full_lifespan_ms": 1.54,
"stage_ms": {
- "后台任务登记器": 0.081,
- "数据库准备": 0.039,
- "HTTP 基础能力": 0.032,
- "领域依赖装配": 0.029,
- "数据库引擎预热": 0.026,
- "数据库连接预算": 0.025,
- "路由": 0.023,
- "模块服务": 0.026,
- "插件备份恢复": 0.023,
+ "后台任务登记器": 0.09,
+ "数据库准备": 0.048,
+ "HTTP 基础能力": 0.03,
+ "领域依赖装配": 0.033,
+ "数据库引擎预热": 0.031,
+ "数据库连接预算": 0.031,
+ "路由": 0.028,
+ "模块服务": 0.022,
+ "插件备份恢复": 0.021,
"插件": 0.025,
- "定时器": 0.022,
+ "定时器": 0.023,
"监控器": 0.021,
- "待处理整理回放": 0.024,
- "命令服务": 0.022,
- "工作流": 0.02,
+ "待处理整理回放": 0.025,
+ "命令服务": 0.021,
+ "工作流": 0.023,
"插件同步与启动收尾": 0.024
},
"threads_before": 2,
@@ -78,25 +78,25 @@
{
"mode": "normal",
"enabled_component_count": 25,
- "startup_ms": 0.644,
- "full_lifespan_ms": 1.465,
+ "startup_ms": 0.667,
+ "full_lifespan_ms": 1.559,
"stage_ms": {
- "后台任务登记器": 0.073,
- "数据库准备": 0.035,
- "HTTP 基础能力": 0.032,
- "领域依赖装配": 0.029,
- "数据库引擎预热": 0.025,
- "数据库连接预算": 0.025,
- "路由": 0.025,
- "模块服务": 0.024,
- "插件备份恢复": 0.023,
- "插件": 0.021,
- "定时器": 0.023,
- "监控器": 0.025,
- "待处理整理回放": 0.024,
- "命令服务": 0.023,
+ "后台任务登记器": 0.078,
+ "数据库准备": 0.037,
+ "HTTP 基础能力": 0.027,
+ "领域依赖装配": 0.03,
+ "数据库引擎预热": 0.027,
+ "数据库连接预算": 0.024,
+ "路由": 0.026,
+ "模块服务": 0.026,
+ "插件备份恢复": 0.026,
+ "插件": 0.025,
+ "定时器": 0.024,
+ "监控器": 0.024,
+ "待处理整理回放": 0.027,
+ "命令服务": 0.026,
"工作流": 0.023,
- "插件同步与启动收尾": 0.02
+ "插件同步与启动收尾": 0.021
},
"threads_before": 2,
"threads_started": 2,
@@ -109,25 +109,25 @@
{
"mode": "normal",
"enabled_component_count": 25,
- "startup_ms": 0.661,
- "full_lifespan_ms": 1.501,
+ "startup_ms": 0.735,
+ "full_lifespan_ms": 1.57,
"stage_ms": {
- "后台任务登记器": 0.073,
- "数据库准备": 0.049,
- "HTTP 基础能力": 0.032,
- "领域依赖装配": 0.029,
- "数据库引擎预热": 0.025,
+ "后台任务登记器": 0.103,
+ "数据库准备": 0.041,
+ "HTTP 基础能力": 0.031,
+ "领域依赖装配": 0.032,
+ "数据库引擎预热": 0.028,
"数据库连接预算": 0.026,
- "路由": 0.025,
- "模块服务": 0.025,
- "插件备份恢复": 0.023,
+ "路由": 0.027,
+ "模块服务": 0.026,
+ "插件备份恢复": 0.026,
"插件": 0.025,
- "定时器": 0.026,
+ "定时器": 0.024,
"监控器": 0.025,
- "待处理整理回放": 0.021,
- "命令服务": 0.022,
+ "待处理整理回放": 0.024,
+ "命令服务": 0.024,
"工作流": 0.024,
- "插件同步与启动收尾": 0.021
+ "插件同步与启动收尾": 0.024
},
"threads_before": 2,
"threads_started": 2,
@@ -138,8 +138,8 @@
"database_connections_started": 0
}
],
- "median_startup_ms": 0.661,
- "median_full_lifespan_ms": 1.475,
+ "median_startup_ms": 0.702,
+ "median_full_lifespan_ms": 1.559,
"enabled_component_count": 25,
"enabled_components": [
"后台任务登记器",
@@ -174,41 +174,17 @@
{
"mode": "safe",
"enabled_component_count": 13,
- "startup_ms": 0.48,
- "full_lifespan_ms": 0.905,
+ "startup_ms": 0.527,
+ "full_lifespan_ms": 0.956,
"stage_ms": {
- "后台任务登记器": 0.073,
- "数据库准备": 0.043,
+ "后台任务登记器": 0.091,
+ "数据库准备": 0.042,
"HTTP 基础能力": 0.03,
- "领域依赖装配": 0.027,
- "数据库引擎预热": 0.026,
- "数据库连接预算": 0.025,
- "路由": 0.025,
- "模块服务": 0.025,
- "插件同步与启动收尾": 0.022
- },
- "threads_before": 2,
- "threads_started": 2,
- "threads_after": 2,
- "tasks_before": 1,
- "tasks_started": 1,
- "tasks_after": 1,
- "database_connections_started": 0
- },
- {
- "mode": "safe",
- "enabled_component_count": 13,
- "startup_ms": 0.51,
- "full_lifespan_ms": 0.935,
- "stage_ms": {
- "后台任务登记器": 0.082,
- "数据库准备": 0.039,
- "HTTP 基础能力": 0.036,
- "领域依赖装配": 0.032,
- "数据库引擎预热": 0.024,
- "数据库连接预算": 0.026,
+ "领域依赖装配": 0.03,
+ "数据库引擎预热": 0.028,
+ "数据库连接预算": 0.027,
"路由": 0.026,
- "模块服务": 0.025,
+ "模块服务": 0.026,
"插件同步与启动收尾": 0.025
},
"threads_before": 2,
@@ -222,17 +198,41 @@
{
"mode": "safe",
"enabled_component_count": 13,
- "startup_ms": 0.564,
- "full_lifespan_ms": 0.97,
+ "startup_ms": 0.523,
+ "full_lifespan_ms": 0.954,
"stage_ms": {
- "后台任务登记器": 0.092,
- "数据库准备": 0.039,
+ "后台任务登记器": 0.082,
+ "数据库准备": 0.045,
"HTTP 基础能力": 0.036,
- "领域依赖装配": 0.031,
- "数据库引擎预热": 0.03,
- "数据库连接预算": 0.026,
- "路由": 0.027,
- "模块服务": 0.027,
+ "领域依赖装配": 0.033,
+ "数据库引擎预热": 0.026,
+ "数据库连接预算": 0.024,
+ "路由": 0.023,
+ "模块服务": 0.024,
+ "插件同步与启动收尾": 0.023
+ },
+ "threads_before": 2,
+ "threads_started": 2,
+ "threads_after": 2,
+ "tasks_before": 1,
+ "tasks_started": 1,
+ "tasks_after": 1,
+ "database_connections_started": 0
+ },
+ {
+ "mode": "safe",
+ "enabled_component_count": 13,
+ "startup_ms": 0.515,
+ "full_lifespan_ms": 0.934,
+ "stage_ms": {
+ "后台任务登记器": 0.084,
+ "数据库准备": 0.044,
+ "HTTP 基础能力": 0.029,
+ "领域依赖装配": 0.027,
+ "数据库引擎预热": 0.031,
+ "数据库连接预算": 0.028,
+ "路由": 0.023,
+ "模块服务": 0.022,
"插件同步与启动收尾": 0.025
},
"threads_before": 2,
@@ -244,8 +244,8 @@
"database_connections_started": 0
}
],
- "median_startup_ms": 0.51,
- "median_full_lifespan_ms": 0.935,
+ "median_startup_ms": 0.523,
+ "median_full_lifespan_ms": 0.954,
"enabled_component_count": 13,
"enabled_components": [
"后台任务登记器",
diff --git a/tests/test_architecture_adapter_imports.py b/tests/test_architecture_adapter_imports.py
index 6dc128d69..ae92da7e5 100644
--- a/tests/test_architecture_adapter_imports.py
+++ b/tests/test_architecture_adapter_imports.py
@@ -37,7 +37,7 @@ FROZEN_DIRECT_ADAPTER_IMPORTS = {
("app.application.security.cookie", "app.adapters.network.http"): "S2-L6",
("app.application.security.passkey", "app.adapters.cache.redis"): "S2-L4",
("app.application.torrent", "app.adapters.network.http"): "S2-L6",
- ("app.application.transfer", "app.adapters.system.host"): "S2-L6",
+ ("app.application.transfer.workflow", "app.adapters.system.host"): "S2-L6",
("app.chain._recognition", "app.adapters.external.server"): "S2-L7",
("app.chain._transfer", "app.adapters.system.host"): "S2-L7",
("app.chain.download", "app.adapters.network.http"): "S2-L7",
diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py
index d9396c3a7..ff914f626 100644
--- a/tests/test_architecture_dependencies.py
+++ b/tests/test_architecture_dependencies.py
@@ -82,6 +82,13 @@ RETIRED_CANONICAL_FILES = (
"app/adapters/network/sites.pyi",
"app/application/plugins.py",
"app/application/subscribe.py",
+ "app/application/chain/durable_events.py",
+ "app/application/transfer.py",
+ "app/application/transfer_execution.py",
+ "app/db/adapters/transfer.py",
+ "app/db/adapters/transfer_execution.py",
+ "app/runtime/extensions/managed_resource_adapter.py",
+ "app/runtime/managed_resources.py",
"app/startup/agent_initializer.py",
"app/startup/cache_initializer.py",
"app/startup/chain_events.py",
@@ -93,6 +100,7 @@ RETIRED_CANONICAL_FILES = (
"app/startup/domain_initializer.py",
"app/startup/download_failure.py",
"app/startup/managed_resources_initializer.py",
+ "app/startup/initializers/managed_resources.py",
"app/startup/modules_initializer.py",
"app/startup/monitor_initializer.py",
"app/startup/outbox.py",
@@ -481,9 +489,9 @@ def test_transfer_chains_use_explicit_data_port_getters():
def test_transfer_pending_oper_import_is_confined_to_database_boundary():
"""宿主仅允许事务适配器和兼容导出直接导入整理待处理 Oper。"""
allowed_paths = {
- "app/db/adapters/transfer.py",
+ "app/db/adapters/transfer/admission.py",
"app/db/adapters/chain.py",
- "app/db/adapters/transfer_execution.py",
+ "app/db/adapters/transfer/execution.py",
"app/db/oper/__init__.py",
}
violations: list[str] = []
@@ -520,7 +528,7 @@ def test_startup_injects_transactional_transfer_admission_repository():
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
imports_repository = any(
isinstance(node, ast.ImportFrom)
- and node.module == "app.db.adapters.transfer"
+ and node.module == "app.db.adapters.transfer.admission"
and any(
alias.name == "TransactionalTransferAdmissionRepository"
for alias in node.names
diff --git a/tests/test_chain_durable_events.py b/tests/test_chain_durable_events.py
index a516f3f43..4d540b8a4 100644
--- a/tests/test_chain_durable_events.py
+++ b/tests/test_chain_durable_events.py
@@ -11,7 +11,7 @@ from sqlalchemy import create_engine, delete, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker
-from app.application.chain.durable_events import (
+from app.application.chain.events import (
TransferResultSettlement,
download_added_event_key,
restore_download_added,
@@ -21,7 +21,7 @@ from app.application.chain.durable_events import (
transfer_result_event_key,
)
from app.application.history import TransferHistoryMutationCommand
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionLeaseLostError,
TransferSettlementResult,
)
diff --git a/tests/test_db_transferpending_queries.py b/tests/test_db_transferpending_queries.py
index 942896378..2535b50d6 100644
--- a/tests/test_db_transferpending_queries.py
+++ b/tests/test_db_transferpending_queries.py
@@ -12,7 +12,7 @@ from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
from app.db import base as db_base
-from app.db.adapters.transfer import TransactionalTransferAdmissionRepository
+from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
from app.db.models.transferhistory import TransferHistory
from app.db.models.transferpending import TransferPending
from app.db.oper.transferpending import TransferPendingOper
diff --git a/tests/test_filemanager_planning.py b/tests/test_filemanager_planning.py
index bdd610281..9785be525 100644
--- a/tests/test_filemanager_planning.py
+++ b/tests/test_filemanager_planning.py
@@ -3,7 +3,7 @@ from pathlib import Path
import pytest
-from app.application.transfer import TransferPlanCheckpoint, TransferPlanningInput
+from app.application.transfer.workflow import TransferPlanCheckpoint, TransferPlanningInput
from app.domain.context import MediaInfo
from app.domain.meta.metabase import MetaBase
from app.modules.filemanager import transhandler as transhandler_module
diff --git a/tests/test_history_ai_retry_gate.py b/tests/test_history_ai_retry_gate.py
index fdcac2bf3..c858ca903 100644
--- a/tests/test_history_ai_retry_gate.py
+++ b/tests/test_history_ai_retry_gate.py
@@ -8,7 +8,7 @@ from types import SimpleNamespace
from app.agent.tools.impl.delete_transfer_history import DeleteTransferHistoryTool
from app.api.endpoints import history as history_endpoint
from app.application.configuration import ApiRuntimeConfig
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionState,
TransferRetryRequestResult,
)
diff --git a/tests/test_legacy_db_behavior_compat.py b/tests/test_legacy_db_behavior_compat.py
index 938c930e7..ccbedf474 100644
--- a/tests/test_legacy_db_behavior_compat.py
+++ b/tests/test_legacy_db_behavior_compat.py
@@ -2,7 +2,7 @@ import importlib
import pytest
-from app.application.transfer import TransferTask as CanonicalTransferTask
+from app.application.transfer.workflow import TransferTask as CanonicalTransferTask
from app.db.models.transferhistory import TransferHistory
from app.schemas.file import FileItem
diff --git a/tests/test_legacy_import_compat.py b/tests/test_legacy_import_compat.py
index 2dc7eee88..9cf25d2e4 100644
--- a/tests/test_legacy_import_compat.py
+++ b/tests/test_legacy_import_compat.py
@@ -189,6 +189,37 @@ for legacy_name, alias in MODULE_ALIASES.items():
)
+def test_renamed_modules_use_exact_compat_routes() -> None:
+ """旧模块路径只经 manifest 复用新的 canonical 模块。"""
+ expected = {
+ "app.application.chain.durable_events": "app.application.chain.events",
+ "app.application.transfer_execution": "app.application.transfer.execution",
+ "app.runtime.managed_resources": "app.runtime.resources",
+ }
+ configure_legacy_import_diagnostics(enabled=False, emitter=lambda _: None)
+ try:
+ for legacy_name, target_name in expected.items():
+ assert MODULE_ALIASES[legacy_name].target == target_name
+ assert importlib.import_module(legacy_name) is importlib.import_module(target_name)
+ finally:
+ reset_legacy_import_diagnostics()
+
+
+def test_transfer_package_exposes_plugin_symbols_only_through_overlay() -> None:
+ """整理包不重导出宿主实现,只按兼容清单惰性提供插件旧符号。"""
+ configure_legacy_import_diagnostics(enabled=False, emitter=lambda _: None)
+ try:
+ package = importlib.import_module("app.application.transfer")
+ legacy = importlib.import_module("app.sdk._legacy.transfer")
+
+ assert package.TransferTask is legacy.TransferTask
+ assert package.TransferQueue is legacy.TransferQueue
+ assert "TransferTask" not in package.__all__
+ assert "TransferQueue" not in package.__all__
+ finally:
+ reset_legacy_import_diagnostics()
+
+
def test_virtual_package_exports_resolve_exact_manifest_symbols():
"""合成旧包仅公开 manifest 声明的符号,并记录 DEBUG 兼容警告。"""
legacy_package = "app.core.meta"
diff --git a/tests/test_managed_resources.py b/tests/test_managed_resources.py
index 80694fab8..d976b2620 100644
--- a/tests/test_managed_resources.py
+++ b/tests/test_managed_resources.py
@@ -13,18 +13,18 @@ from unittest.mock import MagicMock
import pytest
+from app.runtime import resources as managed_resource_facade
from app.runtime.capabilities.errors import (
CapabilityOperationError,
CapabilityRuntimeClosedError,
)
from app.runtime.capabilities.runtime import CapabilityRuntime
-from app.runtime.extensions.managed_resource_adapter import (
+from app.runtime.extensions.resource import (
AsyncManagedResourceAdapter,
SyncManagedResourceAdapter,
build_managed_resource_registry,
)
-from app.runtime import managed_resources as managed_resource_facade
-from app.runtime.managed_resources import (
+from app.runtime.resources import (
MANAGED_RESOURCE_ASYNC_KIND,
MANAGED_RESOURCE_SYNC_KIND,
acquire_managed_resource,
@@ -35,7 +35,6 @@ from app.runtime.managed_resources import (
shutdown_managed_resource_runtime,
)
-
PROJECT_ROOT = Path(__file__).parents[1]
@@ -346,7 +345,7 @@ def test_startup_initializer_discovers_manifest_without_importing_resource() ->
script = """
import asyncio
import sys
-from app.startup.initializers.managed_resources import (
+from app.startup.initializers.resources import (
init_managed_resources,
stop_managed_resources,
)
@@ -372,7 +371,7 @@ assert "pyvirtualdisplay" not in sys.modules
def test_startup_shutdown_without_init_does_not_build_registry(monkeypatch) -> None:
"""未执行启动装配时,关闭入口不得通过发现声明反向初始化 Runtime。"""
- from app.startup.initializers import managed_resources as managed_resources_initializer
+ from app.startup.initializers import resources as managed_resources_initializer
build_registry = MagicMock(side_effect=AssertionError("must not discover"))
monkeypatch.setattr(
diff --git a/tests/test_manual_transfer_history.py b/tests/test_manual_transfer_history.py
index a97986c8b..a30621a72 100644
--- a/tests/test_manual_transfer_history.py
+++ b/tests/test_manual_transfer_history.py
@@ -2,18 +2,20 @@ from types import SimpleNamespace
from app.api.endpoints.transfer import (
manual_transfer as manual_transfer_endpoint,
+)
+from app.api.endpoints.transfer import (
query_manual_transfer_history,
)
-from app.chain.transfer import TransferChain
-from app.runtime.config import settings
-from app.db.oper.transferhistory import TransferHistoryOper
from app.application.history import (
clear_transfer_failures,
failed_retry_count,
max_failed_retries,
record_transfer_failure,
)
-from app.schemas import ManualTransferItem
+from app.chain.transfer import TransferChain
+from app.db.oper.transferhistory import TransferHistoryOper
+from app.runtime.config import settings
+from app.schemas.transfer import ManualTransferItem
from tests.test_transfer_sync_extra_files import (
FakeMeta,
make_fileitem,
@@ -72,12 +74,6 @@ def _patch_transfer_planning(monkeypatch, chain, fileitem, history, planned, del
get_by_path=lambda path: None,
),
)
- monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace(
- get_by_hash=lambda download_hash: None,
- get_file_by_fullpath=lambda fullpath: None,
- get_files_by_savepath=lambda savepath: [],
- get_by_path=lambda path: None,
- ))
monkeypatch.setattr(
"app.chain.transfer.get_configured_system_config",
lambda: SimpleNamespace(get=lambda key: None),
diff --git a/tests/test_music_transfer.py b/tests/test_music_transfer.py
index 98fafde71..36649e6c9 100644
--- a/tests/test_music_transfer.py
+++ b/tests/test_music_transfer.py
@@ -5,7 +5,7 @@ from unittest.mock import Mock
from jinja2 import Template
from app.application.messaging.message import TemplateHelper
-from app.application.transfer import TransferTask
+from app.application.transfer.workflow import TransferTask
from app.chain.media import MediaChain
from app.chain.transfer import JobManager, TransferChain
from app.domain.context import MusicInfo
diff --git a/tests/test_transfer_durable_retry_owner.py b/tests/test_transfer_durable_retry_owner.py
index e5a33000d..5d10e02cb 100644
--- a/tests/test_transfer_durable_retry_owner.py
+++ b/tests/test_transfer_durable_retry_owner.py
@@ -2,7 +2,7 @@
from types import SimpleNamespace
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionState,
TransferRetryRequestResult,
)
diff --git a/tests/test_transfer_execution_migration.py b/tests/test_transfer_execution_migration.py
index a45d5466e..1a7deb3df 100644
--- a/tests/test_transfer_execution_migration.py
+++ b/tests/test_transfer_execution_migration.py
@@ -11,7 +11,7 @@ from alembic.migration import MigrationContext
from alembic.operations import Operations
from sqlalchemy.orm import sessionmaker
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionCommand,
TransferExecutionConflictError,
TransferExecutionState,
@@ -20,7 +20,7 @@ from app.application.transfer_execution import (
TransferStepIntent,
TransferStepResult,
)
-from app.db.adapters.transfer_execution import (
+from app.db.adapters.transfer.execution import (
TransactionalTransferExecutionRepository,
)
from app.db.models.transferpending import TransferPending
diff --git a/tests/test_transfer_execution_persistence.py b/tests/test_transfer_execution_persistence.py
index 1249ee570..1247dd910 100644
--- a/tests/test_transfer_execution_persistence.py
+++ b/tests/test_transfer_execution_persistence.py
@@ -6,7 +6,7 @@ import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionCheckpoint,
TransferExecutionCommand,
TransferExecutionConflictError,
@@ -20,7 +20,7 @@ from app.application.transfer_execution import (
build_transfer_checkpoint_fingerprint,
build_transfer_operation_id,
)
-from app.db.adapters.transfer_execution import (
+from app.db.adapters.transfer.execution import (
TransactionalTransferExecutionRepository,
)
from app.db.base import Base
diff --git a/tests/test_transfer_execution_runner.py b/tests/test_transfer_execution_runner.py
index e9f8637d6..74bfc37f0 100644
--- a/tests/test_transfer_execution_runner.py
+++ b/tests/test_transfer_execution_runner.py
@@ -8,7 +8,7 @@ import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionCommand,
TransferExecutionState,
TransferOperationObservation,
@@ -17,7 +17,7 @@ from app.application.transfer_execution import (
TransferStepResult,
)
from app.chain import transfer as transfer_chain_module
-from app.db.adapters.transfer_execution import (
+from app.db.adapters.transfer.execution import (
TransactionalTransferExecutionRepository,
)
from app.db.base import Base
diff --git a/tests/test_transfer_failed_retry_scheduler.py b/tests/test_transfer_failed_retry_scheduler.py
index a273cda0c..4fb794fd2 100644
--- a/tests/test_transfer_failed_retry_scheduler.py
+++ b/tests/test_transfer_failed_retry_scheduler.py
@@ -5,7 +5,7 @@ from unittest.mock import Mock, patch
import pytest
-from app.application.transfer import FailedRetryScheduler
+from app.application.transfer.workflow import FailedRetryScheduler
def test_retry_scheduler_close_cancels_buffered_timer_and_rejects_new_work():
@@ -76,7 +76,7 @@ def test_retry_scheduler_observes_unexpected_background_task_error():
raise RuntimeError("flush failed")
scheduler._flush_retry_transfer = failing_flush
- with patch("app.application.transfer.logger.error", Mock()) as log_error:
+ with patch("app.application.transfer.workflow.logger.error", Mock()) as log_error:
await scheduler.schedule_retry(11, group_key="media:test")
for _ in range(5):
await asyncio.sleep(0)
diff --git a/tests/test_transfer_failure_notification_aggregation.py b/tests/test_transfer_failure_notification_aggregation.py
index c19415389..0449372b6 100644
--- a/tests/test_transfer_failure_notification_aggregation.py
+++ b/tests/test_transfer_failure_notification_aggregation.py
@@ -1,16 +1,16 @@
-from unittest.mock import Mock, patch
from types import SimpleNamespace
+from unittest.mock import Mock, patch
import pytest
-from app.chain import transfer as transfer_module
-from app.chain.transfer import TransferChain
-from app.application.transfer import (
+from app.application.transfer.workflow import (
TransferFailureNotification,
TransferFailureNotificationAggregator,
TransferTask,
build_transfer_failure_group_key,
)
+from app.chain import transfer as transfer_module
+from app.chain.transfer import TransferChain
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
from app.runtime.config import ConfigModel
@@ -247,7 +247,7 @@ def test_aggregator_close_observes_flush_callback_error():
loop=loop,
)
- with patch("app.application.transfer.logger.error") as log_error:
+ with patch("app.application.transfer.workflow.logger.error") as log_error:
aggregator.close()
callback.assert_called_once_with([notice])
diff --git a/tests/test_transfer_job_manager.py b/tests/test_transfer_job_manager.py
index 9c973ad0e..0b2b142e3 100644
--- a/tests/test_transfer_job_manager.py
+++ b/tests/test_transfer_job_manager.py
@@ -9,7 +9,7 @@ from app.application.history import (
failed_retry_count,
record_transfer_failure,
)
-from app.application.transfer import (
+from app.application.transfer.workflow import (
TransferAdmission,
TransferPlanningInput,
TransferTask,
diff --git a/tests/test_transfer_lease_persistence.py b/tests/test_transfer_lease_persistence.py
index f45c73396..b63c99a38 100644
--- a/tests/test_transfer_lease_persistence.py
+++ b/tests/test_transfer_lease_persistence.py
@@ -9,7 +9,7 @@ import pytest
from sqlalchemy import create_engine, select, text
from sqlalchemy.orm import sessionmaker
-from app.application.transfer import (
+from app.application.transfer.workflow import (
TRANSFER_ADMISSION_PLANNED,
TransferAdmission,
TransferAdmissionProjectionError,
@@ -19,7 +19,7 @@ from app.application.transfer import (
TransferProviderInvocationSnapshot,
TransferProviderReference,
)
-from app.db.adapters.transfer import TransactionalTransferAdmissionRepository
+from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
from app.db.models.transferhistory import TransferHistory
from app.db.models.transferpending import TransferPending
from app.db.oper.transferpending import TransferPendingOper
@@ -321,7 +321,7 @@ def test_claim_recoverable_skips_corrupt_projection_and_claims_later_tasks(
repository = repository_factory()
messages: list[str] = []
monkeypatch.setattr(
- "app.db.adapters.transfer._diagnostic_logger.error",
+ "app.db.adapters.transfer.admission._diagnostic_logger.error",
messages.append,
)
corrupt = _admit(repository, "/downloads/a-corrupt.mkv")
@@ -389,7 +389,7 @@ def test_projection_diagnostic_changes_are_recorded_once_each(
repository = repository_factory()
messages: list[str] = []
monkeypatch.setattr(
- "app.db.adapters.transfer._diagnostic_logger.error",
+ "app.db.adapters.transfer.admission._diagnostic_logger.error",
messages.append,
)
admitted = _admit(repository, "/downloads/changing-corrupt.mkv")
@@ -459,7 +459,7 @@ def test_projection_diagnostic_cas_is_concurrency_safe(
messages.append(message)
monkeypatch.setattr(
- "app.db.adapters.transfer._diagnostic_logger.error",
+ "app.db.adapters.transfer.admission._diagnostic_logger.error",
capture,
)
barrier = Barrier(2)
@@ -498,7 +498,7 @@ def test_projection_diagnostic_does_not_overwrite_active_lease(
before = _pending_snapshot(repository, admitted.task_id)
messages: list[str] = []
monkeypatch.setattr(
- "app.db.adapters.transfer._diagnostic_logger.error",
+ "app.db.adapters.transfer.admission._diagnostic_logger.error",
messages.append,
)
diff --git a/tests/test_transfer_legacy_terminal_compat.py b/tests/test_transfer_legacy_terminal_compat.py
index cf90f8e57..8a0c3488f 100644
--- a/tests/test_transfer_legacy_terminal_compat.py
+++ b/tests/test_transfer_legacy_terminal_compat.py
@@ -4,7 +4,7 @@ import threading
from types import SimpleNamespace
from unittest.mock import Mock, patch
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionCheckpoint,
TransferSettlementResult,
)
diff --git a/tests/test_transfer_manual_review_api.py b/tests/test_transfer_manual_review_api.py
index ed2342f98..111c5b5d1 100644
--- a/tests/test_transfer_manual_review_api.py
+++ b/tests/test_transfer_manual_review_api.py
@@ -11,7 +11,7 @@ from pydantic import ValidationError
from app.api.dependencies.auth import get_current_active_manage_user
from app.api.endpoints import transfer as transfer_endpoint
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionConflictError,
TransferExecutionState,
TransferManualReviewDecision,
diff --git a/tests/test_transfer_manual_review_discovery.py b/tests/test_transfer_manual_review_discovery.py
index 6d9011602..00a722ff3 100644
--- a/tests/test_transfer_manual_review_discovery.py
+++ b/tests/test_transfer_manual_review_discovery.py
@@ -8,14 +8,14 @@ from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
from app.api.endpoints import transfer as transfer_endpoint
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionCommand,
TransferExecutionState,
TransferManualReviewQuery,
TransferStepIntent,
TransferStepResult,
)
-from app.db.adapters.transfer_execution import (
+from app.db.adapters.transfer.execution import (
TransactionalTransferExecutionRepository,
)
from app.db.base import Base
diff --git a/tests/test_transfer_mark_torrent_completed.py b/tests/test_transfer_mark_torrent_completed.py
index cff7ca63b..ecc7731e4 100644
--- a/tests/test_transfer_mark_torrent_completed.py
+++ b/tests/test_transfer_mark_torrent_completed.py
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
from types import SimpleNamespace
+from app.application.transfer.workflow import TransferTask
from app.chain.transfer import JobManager, TransferChain
from app.domain.meta.metabase import MetaBase
from app.runtime.config import settings
-from app.schemas import FileItem
-from app.application.transfer import TransferTask
+from app.schemas.file import FileItem
from app.schemas.types import MediaType
diff --git a/tests/test_transfer_mounted_disk_cleanup.py b/tests/test_transfer_mounted_disk_cleanup.py
index ad11c0629..ff96a9a43 100644
--- a/tests/test_transfer_mounted_disk_cleanup.py
+++ b/tests/test_transfer_mounted_disk_cleanup.py
@@ -2,10 +2,11 @@ from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
-from app.chain.transfer import TransferChain
-from app.schemas import FileItem, TransferDirectoryConf
-from app.application.transfer import TransferTask
from app.adapters.system.host import SystemUtils
+from app.application.transfer.workflow import TransferTask
+from app.chain.transfer import TransferChain
+from app.schemas.file import FileItem
+from app.schemas.system import TransferDirectoryConf
def _make_task(
diff --git a/tests/test_transfer_movie_collection.py b/tests/test_transfer_movie_collection.py
index 523cccfa7..7c1ed1a3a 100644
--- a/tests/test_transfer_movie_collection.py
+++ b/tests/test_transfer_movie_collection.py
@@ -2,12 +2,13 @@ from types import SimpleNamespace
import pytest
+from app.application.transfer.workflow import TransferTask
from app.chain.transfer import TransferChain
-from app.runtime.config import settings
from app.domain.context import MediaInfo
from app.domain.meta.metabase import MetaBase
-from app.schemas import DownloadHistory, FileItem
-from app.application.transfer import TransferTask
+from app.runtime.config import settings
+from app.schemas.file import FileItem
+from app.schemas.history import DownloadHistory
from app.schemas.types import MediaType
@@ -192,7 +193,6 @@ def test_movie_collection_conflict_only_drops_automatic_media(
)
monkeypatch.setattr("app.chain._transfer.get_chain_transfer_history_port", lambda: SimpleNamespace(get_by_src=lambda src, storage=None: None))
monkeypatch.setattr("app.chain.transfer.get_chain_download_history_port", lambda: history_oper)
- monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: history_oper)
monkeypatch.setattr(
"app.chain.transfer.get_configured_system_config",
lambda: SimpleNamespace(get=lambda key: None),
diff --git a/tests/test_transfer_music_job_media.py b/tests/test_transfer_music_job_media.py
index c15f3eb54..676a06b98 100644
--- a/tests/test_transfer_music_job_media.py
+++ b/tests/test_transfer_music_job_media.py
@@ -1,8 +1,8 @@
from app import schemas
-from app.application.transfer import TransferTask
+from app.application.transfer.workflow import TransferTask
from app.chain.transfer import JobManager
from app.domain.meta.metamusic import MetaMusic
-from app.schemas import FileItem
+from app.schemas.file import FileItem
def _music_task() -> TransferTask:
diff --git a/tests/test_transfer_overwrite_declined.py b/tests/test_transfer_overwrite_declined.py
index c69cba4bc..9f3a28aa0 100644
--- a/tests/test_transfer_overwrite_declined.py
+++ b/tests/test_transfer_overwrite_declined.py
@@ -11,7 +11,7 @@ from unittest.mock import MagicMock, patch
import pytest
-from app.application.transfer_execution import (
+from app.application.transfer.execution import (
TransferExecutionCheckpoint,
TransferSettlementResult,
)
diff --git a/tests/test_transfer_pending_legacy_compat.py b/tests/test_transfer_pending_legacy_compat.py
index 7a5de43d7..cc5037f04 100644
--- a/tests/test_transfer_pending_legacy_compat.py
+++ b/tests/test_transfer_pending_legacy_compat.py
@@ -50,7 +50,7 @@ def test_legacy_import_targets_private_sdk_facade() -> None:
assert alias.target == "app.sdk._legacy.transferpending"
assert alias.owner == "sdk"
- assert alias.replacement == "app.application.transfer"
+ assert alias.replacement == "app.application.transfer.workflow"
assert legacy is importlib.import_module(alias.target)
assert legacy.__all__ == ["TransferPendingOper"]
assert not hasattr(legacy, "TransferPending")
diff --git a/tests/test_transfer_pending_replay.py b/tests/test_transfer_pending_replay.py
index 6108e5dde..1b55e50ef 100644
--- a/tests/test_transfer_pending_replay.py
+++ b/tests/test_transfer_pending_replay.py
@@ -13,7 +13,7 @@ from dataclasses import replace
from pathlib import Path
from unittest.mock import MagicMock
-from app.application.transfer import TransferAdmission, TransferPlanningInput, TransferTask
+from app.application.transfer.workflow import TransferAdmission, TransferPlanningInput, TransferTask
from app.chain.transfer import TransferChain
from app.schemas.file import FileItem
diff --git a/tests/test_transfer_planning_checkpoint.py b/tests/test_transfer_planning_checkpoint.py
index 83948261a..c25873d3c 100644
--- a/tests/test_transfer_planning_checkpoint.py
+++ b/tests/test_transfer_planning_checkpoint.py
@@ -12,14 +12,14 @@ import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
-from app.application import transfer as transfer_application
-from app.application.transfer import TransferTask
-from app.application.transfer_execution import (
+from app.application.transfer import workflow as transfer_application
+from app.application.transfer.execution import (
TransferExecutionCheckpoint,
TransferSettlementResult,
)
+from app.application.transfer.workflow import TransferTask
from app.chain.transfer import TransferChain
-from app.db.adapters.transfer import TransactionalTransferAdmissionRepository
+from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
from app.db.models.transferhistory import TransferHistory
from app.db.models.transferpending import TransferPending
from app.domain.context import MediaInfo
diff --git a/tests/test_transfer_planning_compat.py b/tests/test_transfer_planning_compat.py
index 7b653dcee..86f5571d9 100644
--- a/tests/test_transfer_planning_compat.py
+++ b/tests/test_transfer_planning_compat.py
@@ -6,7 +6,7 @@ from unittest.mock import Mock
import pytest
-from app.application.transfer import TransferTask
+from app.application.transfer.workflow import TransferTask
from app.chain import ChainBase
from app.chain.transfer import TransferChain
from app.modules.filemanager.module import FileManagerModule
diff --git a/tests/test_transfer_planning_migration.py b/tests/test_transfer_planning_migration.py
index f37a1e5cd..3997db772 100644
--- a/tests/test_transfer_planning_migration.py
+++ b/tests/test_transfer_planning_migration.py
@@ -10,7 +10,7 @@ import sqlalchemy as sa
from alembic.migration import MigrationContext
from alembic.operations import Operations
-from app.application.transfer import TransferPlanningInput
+from app.application.transfer.workflow import TransferPlanningInput
from app.db.models.transferpending import TransferPending
try:
diff --git a/tests/test_transfer_planning_persistence.py b/tests/test_transfer_planning_persistence.py
index 6df131c74..8bdecb4da 100644
--- a/tests/test_transfer_planning_persistence.py
+++ b/tests/test_transfer_planning_persistence.py
@@ -6,7 +6,7 @@ import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
-from app.application.transfer import (
+from app.application.transfer.workflow import (
TRANSFER_ADMISSION_ACCEPTED,
TRANSFER_ADMISSION_PLANNED,
TRANSFER_ADMISSION_PROVIDER_PENDING,
@@ -19,7 +19,7 @@ from app.application.transfer import (
TransferProviderInvocationSnapshot,
TransferProviderReference,
)
-from app.db.adapters.transfer import TransactionalTransferAdmissionRepository
+from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
from app.db.models.transferhistory import TransferHistory
from app.db.models.transferpending import TransferPending
diff --git a/tests/test_transfer_queue_service.py b/tests/test_transfer_queue_service.py
index cdc8bb547..0c9fdbcfe 100644
--- a/tests/test_transfer_queue_service.py
+++ b/tests/test_transfer_queue_service.py
@@ -5,8 +5,8 @@ import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
-from app.application.transfer import TransferAdmission, TransferQueueService
-from app.db.adapters.transfer import TransactionalTransferAdmissionRepository
+from app.application.transfer.workflow import TransferAdmission, TransferQueueService
+from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
from app.db.models.transferhistory import TransferHistory
from app.db.models.transferpending import TransferPending
from app.schemas.file import FileItem
diff --git a/tests/test_transfer_settling_recovery.py b/tests/test_transfer_settling_recovery.py
index a29669d36..33f7d219d 100644
--- a/tests/test_transfer_settling_recovery.py
+++ b/tests/test_transfer_settling_recovery.py
@@ -9,19 +9,19 @@ import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
-from app.application.transfer import (
+from app.application.transfer.execution import (
+ TransferExecutionCheckpoint,
+ TransferExecutionSnapshot,
+ TransferExecutionState,
+)
+from app.application.transfer.workflow import (
TransferAdmission,
TransferPlanCheckpoint,
TransferPlanningInput,
TransferTask,
)
-from app.application.transfer_execution import (
- TransferExecutionCheckpoint,
- TransferExecutionSnapshot,
- TransferExecutionState,
-)
from app.chain.transfer import TransferChain
-from app.db.adapters.transfer import TransactionalTransferAdmissionRepository
+from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository
from app.db.models.transferhistory import TransferHistory
from app.db.models.transferpending import TransferPending
from app.schemas.file import FileItem
diff --git a/tests/test_transfer_stale_tasks.py b/tests/test_transfer_stale_tasks.py
index 90210bb0d..6da22a3e5 100644
--- a/tests/test_transfer_stale_tasks.py
+++ b/tests/test_transfer_stale_tasks.py
@@ -1,10 +1,10 @@
"""整理任务失活收敛行为测试。"""
+from app.application.transfer import workflow as app_transfer
+from app.application.transfer.workflow import TransferTask
from app.chain.transfer import JobManager
from app.domain.meta.metabase import MetaBase
-from app.schemas import FileItem
-from app.application.transfer import TransferTask
-from app.application import transfer as app_transfer
+from app.schemas.file import FileItem
from app.schemas.types import MediaType
diff --git a/tests/test_transfer_sync_extra_files.py b/tests/test_transfer_sync_extra_files.py
index 3f1de01c5..0c9947066 100644
--- a/tests/test_transfer_sync_extra_files.py
+++ b/tests/test_transfer_sync_extra_files.py
@@ -148,12 +148,6 @@ def test_sync_extra_subtitle_inherits_matching_video_episode(monkeypatch):
get_by_path=lambda path: None,
),
)
- monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace(
- get_by_hash=lambda download_hash: None,
- get_file_by_fullpath=lambda fullpath: None,
- get_files_by_savepath=lambda savepath: [],
- get_by_path=lambda path: None,
- ))
monkeypatch.setattr(
"app.chain.transfer.get_configured_system_config",
lambda: SimpleNamespace(get=lambda key: None),
@@ -245,12 +239,6 @@ def test_single_subtitle_transfer_reuses_same_name_video_episode(monkeypatch):
get_by_path=lambda path: None,
),
)
- monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace(
- get_by_hash=lambda download_hash: None,
- get_file_by_fullpath=lambda fullpath: None,
- get_files_by_savepath=lambda savepath: [],
- get_by_path=lambda path: None,
- ))
monkeypatch.setattr(
"app.chain.transfer.get_configured_system_config",
lambda: SimpleNamespace(get=lambda key: None),
@@ -360,12 +348,6 @@ def test_single_video_transfer_lists_parent_once_for_same_name_extra(monkeypatch
get_by_path=lambda path: None,
),
)
- monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace(
- get_by_hash=lambda download_hash: None,
- get_file_by_fullpath=lambda fullpath: None,
- get_files_by_savepath=lambda savepath: [],
- get_by_path=lambda path: None,
- ))
monkeypatch.setattr(
"app.chain.transfer.get_configured_system_config",
lambda: SimpleNamespace(get=lambda key: None),
@@ -458,12 +440,6 @@ def test_episode_format_filters_extra_files_before_sync_planning(monkeypatch):
get_by_path=lambda path: None,
),
)
- monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace(
- get_by_hash=lambda download_hash: None,
- get_file_by_fullpath=lambda fullpath: None,
- get_files_by_savepath=lambda savepath: [],
- get_by_path=lambda path: None,
- ))
monkeypatch.setattr(
"app.chain.transfer.get_configured_system_config",
lambda: SimpleNamespace(get=lambda key: None),
@@ -545,12 +521,6 @@ def test_episode_format_keeps_matching_extra_files_following_main(monkeypatch):
get_by_path=lambda path: None,
),
)
- monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace(
- get_by_hash=lambda download_hash: None,
- get_file_by_fullpath=lambda fullpath: None,
- get_files_by_savepath=lambda savepath: [],
- get_by_path=lambda path: None,
- ))
monkeypatch.setattr(
"app.chain.transfer.get_configured_system_config",
lambda: SimpleNamespace(get=lambda key: None),
@@ -641,12 +611,6 @@ def test_single_matching_subtitle_uses_unmatched_video_only_as_context(monkeypat
get_by_path=lambda path: None,
),
)
- monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace(
- get_by_hash=lambda download_hash: None,
- get_file_by_fullpath=lambda fullpath: None,
- get_files_by_savepath=lambda savepath: [],
- get_by_path=lambda path: None,
- ))
monkeypatch.setattr(
"app.chain.transfer.get_configured_system_config",
lambda: SimpleNamespace(get=lambda key: None),
@@ -741,12 +705,6 @@ def test_cleanup_dest_fileitem_is_checkpointed_only_after_allowed_items_exist(mo
get_by_path=lambda path: None,
),
)
- monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace(
- get_by_hash=lambda download_hash: None,
- get_file_by_fullpath=lambda fullpath: None,
- get_files_by_savepath=lambda savepath: [],
- get_by_path=lambda path: None,
- ))
monkeypatch.setattr(
"app.chain.transfer.get_configured_system_config",
lambda: SimpleNamespace(get=lambda key: None),
diff --git a/tests/test_transfer_tmdb_category.py b/tests/test_transfer_tmdb_category.py
index 1d74228f0..045caa7d4 100644
--- a/tests/test_transfer_tmdb_category.py
+++ b/tests/test_transfer_tmdb_category.py
@@ -1,10 +1,11 @@
from types import SimpleNamespace
+from app.application.transfer.workflow import TransferTask
from app.chain.transfer import TransferChain
from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
-from app.schemas import FileItem, TransferDirectoryConf
-from app.application.transfer import TransferTask
+from app.schemas.file import FileItem
+from app.schemas.system import TransferDirectoryConf
from app.schemas.types import MediaSource, MediaType
diff --git a/tests/test_transfer_worker_lifecycle.py b/tests/test_transfer_worker_lifecycle.py
index 8ddc6af22..c89cb52a0 100644
--- a/tests/test_transfer_worker_lifecycle.py
+++ b/tests/test_transfer_worker_lifecycle.py
@@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
-from app.application.transfer import TransferAdmission, TransferQueue, TransferTask
+from app.application.transfer.workflow import TransferAdmission, TransferQueue, TransferTask
from app.chain.transfer import TransferChain
from app.foundation.singleton import Singleton
from app.runtime.config import global_vars
diff --git a/tests/test_transhandler_special_extra.py b/tests/test_transhandler_special_extra.py
index d74faf6b3..f2bbef999 100644
--- a/tests/test_transhandler_special_extra.py
+++ b/tests/test_transhandler_special_extra.py
@@ -3,7 +3,7 @@ from unittest.mock import MagicMock
import pytest
-from app.application.transfer import TransferPlanningInput
+from app.application.transfer.workflow import TransferPlanningInput
from app.domain.context import MediaInfo
from app.domain.meta.metavideo import MetaVideo
from app.modules.filemanager.transhandler import TransHandler