refactor: normalize capability module paths

This commit is contained in:
jxxghp
2026-08-27 22:20:38 +08:00
parent ea58c245dd
commit f134e068e7
85 changed files with 749 additions and 691 deletions
+2 -2
View File
@@ -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` |
+1 -1
View File
@@ -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,
)
+1 -2
View File
@@ -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"
@@ -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,
)
+1 -1
View File
@@ -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,
)
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+2 -3
View File
@@ -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]
@@ -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
),
+1
View File
@@ -0,0 +1 @@
"""整理应用能力包,分离任务工作流与持久执行状态机职责。"""
@@ -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,
# 不能复用 MediaInfoyear 为 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:
"""
+1 -1
View File
@@ -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,
+2 -8
View File
@@ -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
+16 -16
View File
@@ -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,
+2 -2
View File
@@ -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,
+1
View File
@@ -0,0 +1 @@
"""转移接纳与持久执行的数据库适配器包。"""
@@ -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,
@@ -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,
+2 -2
View File
@@ -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
+6 -6
View File
@@ -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
+29 -3
View File
@@ -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")
},
@@ -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}
@@ -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:
+6 -7
View File
@@ -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。
+3 -1
View File
@@ -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,
)
+4 -4
View File
@@ -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,
)
+1 -1
View File
@@ -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
@@ -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,
+4 -4
View File
@@ -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 热点文件
+5 -5
View File
@@ -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 可选装配 OTelHTTP、DB、Event、Module、Scheduler、插件生命周期和 Agent 只提交白名单标签 |
---
@@ -631,7 +631,7 @@ flowchart LR
RE["events.py<br/>事件总线"]
RL["log.py<br/>日志运行时(依赖叶子)"]
RCA["cache.py<br/>缓存协议 / 内存后端 / 装饰器"]
MR["managed_resources.py<br/>托管资源门面"]
MR["resources.py<br/>托管资源门面"]
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 | 6612 条待迁移债务,54 条精确 containment |
| Module Contract V2 spec | 217(其中 215 个进入 `run_module` 观察面) |
+4 -4
View File
@@ -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 | 高风险包纳入 coverageraw concurrency 分类清零;Module Quality 有真实 evidence test |
### S5Plugin、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 提交 checkpointOper 只负责带状态和指纹条件的
- `app/db/adapters/transfer/admission.py` 通过短 Session/UoW 提交 checkpointOper 只负责带状态和指纹条件的
stage3.0.14 migration 可升级、降级并在中断后重跑。
- cleanup intent 随准入输入冻结。宿主路径由 FileManager 在 `TransferIntercept` 放行后、任何文件写入前
执行;legacy provider 路径为保持旧 ABI 顺序,在全部冻结引用解析成功后、调用 provider 前执行。
+14 -13
View File
@@ -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 |
+5 -2
View File
@@ -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 `<capability>_<role>.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_<domain>.py` | `test_download_chain.py`, `test_subscribe_endpoint.py` |
| Alembic migrations | Auto-generated by Alembic; do not rename | `20240101_add_column.py` |
| Skill directories | `<kebab-case>/` | `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=...)` |
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
)
+2 -3
View File
@@ -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",
+39 -41
View File
@@ -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())
+3 -3
View File
@@ -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,
+115 -97
View File
@@ -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",
+1 -1
View File
@@ -142,7 +142,7 @@
"tracking": "S2-L6"
},
{
"source": "app.application.transfer",
"source": "app.application.transfer.workflow",
"target": "app.adapters.system.host",
"tracking": "S2-L6"
},
-20
View File
@@ -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
-53
View File
@@ -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
},
+38 -5
View File
@@ -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"
}
+113 -113
View File
@@ -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": [
"后台任务登记器",
+1 -1
View File
@@ -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",
+11 -3
View File
@@ -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
+2 -2
View File
@@ -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,
)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
)
+1 -1
View File
@@ -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
+31
View File
@@ -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"
+5 -6
View File
@@ -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(
+6 -10
View File
@@ -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),
+1 -1
View File
@@ -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
+1 -1
View File
@@ -2,7 +2,7 @@
from types import SimpleNamespace
from app.application.transfer_execution import (
from app.application.transfer.execution import (
TransferExecutionState,
TransferRetryRequestResult,
)
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
@@ -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)
@@ -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])
+1 -1
View File
@@ -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,
+6 -6
View File
@@ -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,
)
@@ -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,
)
+1 -1
View File
@@ -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,
@@ -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
@@ -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
+4 -3
View File
@@ -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(
+4 -4
View File
@@ -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),
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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,
)
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+7 -7
View File
@@ -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
+3 -3
View File
@@ -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
-42
View File
@@ -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),
+3 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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