From b99b4fe2c30a2cfbb543c4ca4402b568d6590ddb Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 16 Sep 2026 00:12:16 +0800 Subject: [PATCH] =?UTF-8?q?chore(architecture):=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E8=B7=AF=E5=BE=84=E4=B8=8E=E8=B4=A8=E9=87=8F?= =?UTF-8?q?=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 4 +- app/application/history/__init__.py | 38 +++++++++++++++++ app/application/messaging/webagent/events.py | 2 +- app/application/subscription/write.py | 2 +- app/application/transfer/jobs.py | 6 +-- app/application/transfer/notifications.py | 2 +- app/application/transfer/projection.py | 2 +- app/application/transfer/workflow.py | 41 +++++++++++++++++-- app/chain/message.py | 5 +-- app/chain/transfer/workflow.py | 2 +- app/domain/classification/conditions.py | 5 ++- app/monitor/dispatcher.py | 2 +- docs/refactor/optimization-checklist.md | 2 +- docs/rules/05-architecture.md | 23 +++++------ scripts/architecture/mypy_ratchet.py | 24 +++++++++++ .../architecture/complexity-v2-baseline.json | 3 +- .../fixtures/architecture/mypy-baseline.json | 16 ++------ tests/test_transfer_failed_retry_budget.py | 2 +- tests/test_transfer_history_gate.py | 2 +- tests/test_transfer_history_write_path.py | 2 +- 20 files changed, 135 insertions(+), 50 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f518d6951..af23d6f9f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,8 +76,8 @@ The legacy roots have no physical directories in the source tree. Current images | `app/adapters/cache/` | Redis 与文件缓存等具体持久化实现 | 缓存协议、装饰器和进程内缓存策略 | `backends.py`, `redis.py` | | `app/adapters/system/` | 操作系统、文件、进程、标准流、包/资源安装、显示和 Rust 加速适配 | 业务规则、进程重启决策 | `host.py`, `display/`, `stdio.py`, `package.py`, `resource.py`, `rust.py`, `fsproxy.py` | | `app/adapters/external/` | CookieCloud、插件市场、OCR、IP 归属和 MoviePilot Server 等命名外部生态 | 通用 HTTP/DNS/文件机制或可复用领域语义 | `market.py`, `server.py`, `cookiecloud.py`, `ocr.py`, `location.py`, `wechat.py` | -| `app/application/` | 聚焦应用服务、用例命令,以及由用例拥有的持久化/技术能力 Port/Protocol | SQLAlchemy、Session、Oper 等具体 DB 实现,具体 Adapter 静态依赖,多领域 Chain 编排、底层通用机制、通用传输协议 | `recognition.py`, `filter.py`, `outbox.py`, `subscription/write.py`, `workflow.py` | -| `app/application/messaging/` | 消息渲染/路由、交互和 Agent 到消息桥接:`ingress.py` 统一渠道回环入口;`interaction.py` 通用交互契约和视图工具;`router.py` 统一交互优先级和回调分发;`site.py`/`subscribe.py`/`skill.py` 对应命令的会话、输入解析和视图;`media.py` 媒体交互状态(业务工作流仍由 `MediaInteractionChain` 执行);`plugin.py` 插件输入接管和插件按钮回调;`agent.py` Agent 选择状态、回调协议和 WebAgent 消息桥接;`message.py` 通知渲染、模板和队列。不作为推荐给插件直接使用的公开 SDK | 认证策略、通用 HTTP、服务发现、仅端点使用的 Web Push 行为 | `ingress.py`, `message.py`, `interaction.py`, `router.py`, `agent.py` | +| `app/application/` | 聚焦应用服务、用例命令,以及由用例拥有的持久化/技术能力 Port/Protocol;同一模块的多文件实现进入单词命名子目录 | SQLAlchemy、Session、Oper 等具体 DB 实现,具体 Adapter 静态依赖,多领域 Chain 编排、底层通用机制、通用传输协议 | `recognition.py`, `filter.py`, `outbox.py`, `subscription/write.py`, `workflow.py` | +| `app/application/messaging/` | 消息渲染/路由、交互和 Agent 到消息桥接:`ingress.py` 统一渠道回环入口;`interaction/` 承载通用状态和 Agent 选择契约;`channel/admin.py` 承载渠道管理员解析;`router.py` 统一交互优先级和回调分发;`site.py`/`subscribe.py`/`skill.py` 对应命令的会话、输入解析和视图;`media.py` 媒体交互状态(业务工作流仍由 `MediaInteractionChain` 执行);`plugin.py` 插件输入接管和插件按钮回调;`webagent/` 承载 WebAgent 事件和流编排;`agent.py` 保留 WebAgent 应用编排。不作为推荐给插件直接使用的公开 SDK | 认证策略、通用 HTTP、服务发现、仅端点使用的 Web Push 行为 | `ingress.py`, `message.py`, `interaction/`, `channel/`, `webagent/`, `router.py`, `agent.py` | | `app/application/security/` | 认证、授权、Cookie、Passkey、OTP/二次认证、路径/URL 安全、SSRF 和签名策略 | 通用 URL 解析、进程运行策略、普通业务校验 | `access.py`, `auth.py`, `cookie.py`, `passkey.py`, `otp.py`, `twofactor.py`, `url.py` | | `app/chain/` | Reusable use-case orchestration across modules, Application services, injected ports, events, and caches; chains reach modules only through `run_module` dispatch on method-name contracts | Transport schemas, backend-specific protocol details, concrete Adapter imports, generic primitives, direct Oper/DB imports, direct imports of module internals (classes, exceptions, constants) | `media/`, `download/`, `subscribe/`, `transfer/` | | `app/db/oper/` | 面向表和持久化值的 SQLAlchemy 数据访问;接收调用方 Session,只查询、暂存或 flush | Application 业务规则、隐式事务所有权、外部副作用 | `subscribe.py`, `site.py`, `workflow.py` | diff --git a/app/application/history/__init__.py b/app/application/history/__init__.py index 460e838dd..fc846ed85 100644 --- a/app/application/history/__init__.py +++ b/app/application/history/__init__.py @@ -54,6 +54,44 @@ from app.schemas.transfer import TransferInfo from app.schemas.types import MediaSource +__all__ = [ + "DownloadFileMutationRepository", + "DownloadFileSnapshot", + "DownloadFileWrite", + "DownloadHistory", + "DownloadHistoryMutationCommand", + "DownloadHistoryMutationRepository", + "DownloadHistoryQueryPort", + "DownloadHistoryRepository", + "DownloadHistorySnapshot", + "DownloadHistoryWrite", + "DownloadHistoryWritePort", + "HistoryMutationResult", + "HistoryQueryService", + "HistoryUnitOfWork", + "ManualTransferHistory", + "TransferHistory", + "TransferHistoryLookupRepository", + "TransferHistoryLookupService", + "TransferHistoryMonthlyStatistics", + "TransferHistoryMutationCommand", + "TransferHistoryPage", + "TransferHistoryQueryPort", + "TransferHistoryReplacePort", + "TransferHistoryRepository", + "TransferHistorySnapshot", + "TransferHistoryStagingPort", + "TransferHistoryStatisticSnapshot", + "TransferHistoryWrite", + "TransferHistoryWritePort", + "add_transfer_fail", + "add_transfer_success", + "configure_transfer_history_repository", + "get_transfer_history_repository", + "reset_transfer_history_repository", +] + + @dataclass(frozen=True, slots=True) class DownloadHistorySnapshot: """脱离数据库会话后供宿主下载、订阅和整理用例读取的历史快照。""" diff --git a/app/application/messaging/webagent/events.py b/app/application/messaging/webagent/events.py index 8a7f684b5..6b60db53b 100644 --- a/app/application/messaging/webagent/events.py +++ b/app/application/messaging/webagent/events.py @@ -66,7 +66,7 @@ def build_web_agent_message_update_event( """构造可应用到 WebAgent 原消息的 SSE 更新事件。""" button_rows = normalize_web_agent_button_rows(buttons) content_parts = [part for part in (title, text) if part] - target_message = { + target_message: dict[str, Any] = { "id": str(message_id), "content": "" if button_rows else "\n\n".join(content_parts), "choices": [], diff --git a/app/application/subscription/write.py b/app/application/subscription/write.py index 6873221ff..b8f4bbb71 100644 --- a/app/application/subscription/write.py +++ b/app/application/subscription/write.py @@ -5,7 +5,7 @@ 长在 SubscribeOper.add 上,但取标题、选海报尺寸、判音乐实体、决定哪几个字段构成一条 订阅的身份,都是订阅业务的规则而非数据访问——Oper 只该收敛查询,领域对象不该出现在 它的入参里。搬上来之后 SubscribeOper 收到的是纯粹的持久化字典,与 -app/application/history.py 里整理历史的写入路径同构。 +app/application/history/ 里整理历史的写入路径同构。 留在 Oper 的是列类型强转与建库时间戳:那几步是为 PostgreSQL 的严格类型检查和订阅表 自己的列类型而存在的,跟着列走比跟着调用方走更不容易漂。 diff --git a/app/application/transfer/jobs.py b/app/application/transfer/jobs.py index c299564d8..5ed8998be 100644 --- a/app/application/transfer/jobs.py +++ b/app/application/transfer/jobs.py @@ -29,7 +29,7 @@ from app.schemas.transfer import TransferJob, TransferJobTask from app.schemas.types import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MediaType if TYPE_CHECKING: - from app.application.transfer.workflow import TransferTask + from app.application.transfer.models import TransferTask def monotonic() -> float: @@ -37,7 +37,7 @@ def monotonic() -> float: workflow = sys.modules.get("app.application.transfer.workflow") legacy_clock = getattr(workflow, "monotonic", None) if callable(legacy_clock) and legacy_clock is not monotonic: - return legacy_clock() + return cast(float, legacy_clock()) return _system_monotonic() JobId = tuple[object, ...] @@ -176,7 +176,7 @@ class JobManager: ) return fileitem.storage or "local", normalized_path - def __get_id(self, task: Optional[TransferTask] = None) -> JobId: + def __get_id(self, task: TransferTask) -> JobId: """ 获取作业ID """ diff --git a/app/application/transfer/notifications.py b/app/application/transfer/notifications.py index 268ebbd5e..db9e1d9ea 100644 --- a/app/application/transfer/notifications.py +++ b/app/application/transfer/notifications.py @@ -12,7 +12,7 @@ from app.runtime.log import logger from app.schemas.media import resolve_media_identity if TYPE_CHECKING: - from app.application.transfer.workflow import TransferTask + from app.application.transfer.models import TransferTask def build_transfer_failure_group_key(task: TransferTask) -> str: diff --git a/app/application/transfer/projection.py b/app/application/transfer/projection.py index 3749435e7..53e99a133 100644 --- a/app/application/transfer/projection.py +++ b/app/application/transfer/projection.py @@ -15,7 +15,7 @@ class _DictionarySerializable(Protocol): class _TransferTaskMetaSource(Protocol): """描述整理任务提供已解析领域元数据的最小形状。""" - meta: object + meta: MetaBase | None def domain_to_dict(value: object) -> dict[str, Any]: diff --git a/app/application/transfer/workflow.py b/app/application/transfer/workflow.py index df97f3a72..45542204d 100644 --- a/app/application/transfer/workflow.py +++ b/app/application/transfer/workflow.py @@ -23,8 +23,6 @@ from typing import ( from app.application.transfer.jobs import ( DirectorySize, JobManager, - _domain_to_dict, - _transfer_task_meta, configure_directory_size, job_lock, ) @@ -52,14 +50,51 @@ from app.application.transfer.models import ( TransferTask, ) from app.application.transfer.notifications import ( - TransferFailureNotification, TransferFailureNotificationAggregator, build_transfer_failure_group_key, ) +from app.application.transfer.feedback import TransferFailureNotification +from app.application.transfer.projection import ( + domain_to_dict as _domain_to_dict, + transfer_task_meta as _transfer_task_meta, +) from app.schemas.file import FileItem from app.schemas.transfer import TransferJob +__all__ = [ + "DirectorySize", + "JobManager", + "configure_directory_size", + "job_lock", + "TRANSFER_ADMISSION_ACCEPTED", + "TRANSFER_ADMISSION_PLANNED", + "TRANSFER_ADMISSION_PROVIDER_PENDING", + "TRANSFER_PLAN_CHECKPOINT_LEGACY_VERSION", + "TRANSFER_PLAN_CHECKPOINT_VERSION", + "TRANSFER_PLANNING_INPUT_VERSION", + "TRANSFER_PROVIDER_INVOCATION_VERSION", + "TransferAdmission", + "TransferAdmissionConflictError", + "TransferAdmissionProjectionError", + "TransferAdmissionRepository", + "TransferCallback", + "TransferFailureNotification", + "TransferFailureNotificationAggregator", + "TransferLeaseLostError", + "TransferPlanCheckpoint", + "TransferPlanItem", + "TransferPlanningInput", + "TransferPlanningStateError", + "TransferProviderInvocationSnapshot", + "TransferProviderReference", + "TransferQueue", + "TransferTask", + "build_transfer_failure_group_key", + "TransferQueueService", +] + + class TransferQueueService: """协调整理任务登记、入队、移除和队列视图查询。""" diff --git a/app/chain/message.py b/app/chain/message.py index 6530cdec4..ac4929761 100644 --- a/app/chain/message.py +++ b/app/chain/message.py @@ -19,10 +19,7 @@ from app.application.agent import ( transcribe_audio, ) from app.application.messaging import router as interaction_router -from app.application.messaging.interaction.agent import ( - agent_interaction_manager, - parse_agent_choice_callback, -) +from app.application.messaging.interaction.agent import agent_interaction_manager, parse_agent_choice_callback from app.application.messaging.interaction import InteractionContext, InteractionDispatch from app.application.messaging.media import media_interaction_manager from app.application.messaging.plugin import PluginInputInteractionHandler diff --git a/app/chain/transfer/workflow.py b/app/chain/transfer/workflow.py index 21b07f6e5..532e10157 100644 --- a/app/chain/transfer/workflow.py +++ b/app/chain/transfer/workflow.py @@ -713,7 +713,7 @@ class TransferWorkflowOwner(_TransferOwnerBase): raise OperationInterrupted() file_path = Path(file_item.path) - # 自动整理按 app/application/history.py 的统一判定去重(失败记录放行重试、 + # 自动整理按 app/application/history/ 的统一判定去重(失败记录放行重试、 # 成功但源文件已变化放行交 overwrite_mode 决断);手动整理可清理失败记录, # 或按用户确认清理成功记录;手动显式指定媒体身份时,先解除旧失败任务再重新规划。 if ( diff --git a/app/domain/classification/conditions.py b/app/domain/classification/conditions.py index 5db5e9169..8677602a3 100644 --- a/app/domain/classification/conditions.py +++ b/app/domain/classification/conditions.py @@ -15,10 +15,11 @@ def condition_field_ids(node: ClassificationConditionNode) -> tuple[str, ...]: return (node.field,) if not isinstance(node, ClassificationConditionGroup): return () + children: tuple[ClassificationConditionNode, ...] if node.all is not None: - children = node.all + children = tuple(node.all) elif node.any is not None: - children = node.any + children = tuple(node.any) elif node.not_ is not None: children = (node.not_,) else: diff --git a/app/monitor/dispatcher.py b/app/monitor/dispatcher.py index 15bb6a0fd..f6b76fe43 100644 --- a/app/monitor/dispatcher.py +++ b/app/monitor/dispatcher.py @@ -128,7 +128,7 @@ class TransferDispatcher: """ 依据整理历史判断本次是否跳过整理。 - 判定策略由 app/application/history.py 统一提供,整理链的计划整理段使用 + 判定策略由 app/application/history/ 统一提供,整理链的计划整理段使用 同一套判定,避免此处放行的文件在下游被另一套「存在记录即拦」的策略收回。 :param storage: 存储 :param src_path: 整理记录使用的源路径 diff --git a/docs/refactor/optimization-checklist.md b/docs/refactor/optimization-checklist.md index 6ce9c5b35..fc9b2cab8 100644 --- a/docs/refactor/optimization-checklist.md +++ b/docs/refactor/optimization-checklist.md @@ -102,7 +102,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁 | Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement | | Python 源码量 | 305,884 行 | 排除 `app/plugins/**`;61 个文件超过 1,000 行,11 个超过 2,000 行 | | 长方法 | 290 个超过 80 行 | AST 统计排除 `app/plugins/**`;65 个超过 150 行,21 个超过 250 行 | -| 全量 mypy 历史债务 | 9,341 / 508 文件 | Agent API 重构后的现状基线;canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降 | +| 全量 mypy 历史债务 | 9,329 / 507 文件 | Application 模块目录整理后的现状基线;canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降 | | Ruff 历史诊断 | 517 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | | 覆盖率固定基线 | Application 80.00%,Domain 80.00% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 | diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index 9b0747da0..5d65c0afd 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -60,22 +60,22 @@ to make the directory tree look symmetrical. | Path | Ownership | |---|---| -| `app/application/*.py` | Established single-module application services and compatibility facades | +| `app/application/*.py` | Established single-word application services; same-domain implementations use subpackages, while old paths are routed only by the exact Compat/SDK boundaries | | `app/application/subscription/` | Subscription use cases: `write.py` owns media-to-row translation and the write port; `contract.py` owns shared metadata/media-key projection; query, mutation, deletion, identity and search stay in their single-word modules | | `app/application/search/` | Search state and later search-plan use cases | | `app/application/download/` | Download task querying/control and selection use cases; `failures.py` owns the frozen failure-cooldown write/query DTOs and persistence Port | -| `app/application/history.py` | History use cases and persistence contracts; DownloadHistory and TransferHistory own deeply frozen DTOs plus typed query/write/staging ports | +| `app/application/history/` | History use cases and persistence contracts; DownloadHistory and TransferHistory own deeply frozen DTOs plus typed query/write/staging ports | | `app/application/music/` | Multi-source music catalog orchestration | | `app/application/chain/` | Injectable Chain runtime capabilities: `context.py` owns the typed runtime and persistence dependency aggregate, and `events.py` owns durable event write contracts plus replayable payload conversion | | `app/application/agent.py` | Agent orchestration facade and typed `AgentDataContext`; startup injects one explicit data context into the manager, memory, tool and scheduler owners without a process-wide persistence locator | | `app/application/invocation.py` | Frozen Agent write-call identity, claim and receipt contracts; the injected repository provides atomic claim, fenced settlement and unresolved-state reads, while `db/adapters/invocation.py` owns short transactions and cold-start recovery is invoked by startup | | `app/application/network.py` | System network-test target catalog, immutable public/private projections, URL and redirect admission, response validation and the injected transport Port; startup owns concrete HTTP Adapter assembly | | `app/application/outbox.py` | Durable intent, transaction-only stager, short-transaction dispatch store, claim fencing and structured post-commit result contracts | -| `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; `recovery.py` owns failed/corrupt task cleanup and history detachment through the execution repository; `history.py` projects history write fields and file fingerprints; `feedback.py` owns failure stages, notification snapshots and message text, while Chain owns notification delivery and cleanup side effects | +| `app/application/transfer/` | Durable transfer use cases: `workflow.py` owns queue service orchestration; `models.py` owns admission/planning/task contracts; `jobs.py` owns in-process task views; `notifications.py` owns failure aggregation; `projection.py` owns domain projections; `execution.py` owns stable operation identity, step/checkpoint state, retry/manual-review commands and terminal-settlement DTOs; `recovery.py` owns failed/corrupt task cleanup and history detachment through the execution repository; `history.py` projects history write fields and file fingerprints; `feedback.py` owns failure stages, notification snapshots and message text, while Chain owns notification delivery and cleanup side effects | | `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 | -| `app/application/messaging/` | Message rendering/routing, interactions and the Agent-to-message bridge: `ingress.py` owns the single channel-to-host loopback boundary; `interaction.py` shared interaction contracts and view helpers; `router.py` unified interaction priority and callback dispatch; `site.py`/`subscribe.py`/`skill.py` per-command sessions, input parsing and views; `media.py` media interaction state while the business workflow stays in `MediaInteractionChain`; `plugin.py` plugin input capture and plugin button callbacks; `agent.py` owns Agent choice state, callback protocol, bounded WebAgent event publication, display projection, session identity/persistence coordination, temporary attachment registry and audio preparation/transcription; `message.py` notification rendering, templates and queue. Not a public SDK recommended for direct plugin use | +| `app/application/messaging/` | Message rendering/routing, interactions and the Agent-to-message bridge: `ingress.py` owns the single channel-to-host loopback boundary; `interaction/` owns generic interaction state plus Agent choice contracts; `channel/admin.py` owns channel administrator resolution; `router.py` unified interaction priority and callback dispatch; `site.py`/`subscribe.py`/`skill.py` per-command sessions, input parsing and views; `media.py` media interaction state while the business workflow stays in `MediaInteractionChain`; `plugin.py` plugin input capture and plugin button callbacks; `webagent/` owns WebAgent notification events and stream transport; `agent.py` owns WebAgent application orchestration, display projection, session identity/persistence coordination, temporary attachment registry and audio preparation/transcription; `message.py` notification rendering, templates and queue. Not a public SDK recommended for direct plugin use | | `app/application/security/` | Authentication, authorization, frozen user/auth projections, atomic user aggregate commands, per-user configuration publication, cookies, passkeys, OTP/two-factor, path/URL safety, SSRF and signing policy | ### Agent runtime boundaries @@ -122,10 +122,9 @@ directory categories. `app/api/endpoints/agent.py` must not recreate WebAgent file registries, audio conversion/transcription policy, traditional-message dispatch, Agent execution lifecycle, event-to-display projection or AgentChat snapshot writes. Those reusable -state transitions and the transport-neutral event-stream use case belong to -`app/application/messaging/agent.py`; the endpoint maps FastAPI principals and DTOs, -translates upload/domain failures to HTTP responses, and frames Application events -as SSE. +state transitions belong to `app/application/messaging/agent.py` and its +`webagent/` subpackage; the endpoint maps FastAPI principals and DTOs, translates +upload/domain failures to HTTP responses, and frames Application events as SSE. ### Runtime boundaries @@ -293,7 +292,7 @@ were host-internal migration scaffolding, never plugin ABI, and must not gain SD Legacy public Agent imports remain exact SDK/Compat boundaries and must not be reintroduced as Oper aliases in canonical Agent modules. Monitor history checks use `get_transfer_history_repository()` from -`app/application/history.py`; old constructible Oper-style facades are available +`app/application/history/`; old constructible Oper-style facades are available only through the exact SDK Legacy/Compat mapping. Durable transfer execution follows one explicit boundary. The Chain freezes each @@ -837,7 +836,7 @@ The configured user-configuration repository publishes its in-memory snapshot only after the user transaction commits, and reloads from the database if publication fails. -History is a verified typed boundary: `app/application/history.py` owns deeply +History is a verified typed boundary: `app/application/history/` owns deeply frozen DownloadHistory and TransferHistory snapshots plus typed query/write and staging ports. `app/db/adapters/history/download.py` and `app/db/adapters/history/transfer.py` perform ORM projection and mutations inside @@ -1110,7 +1109,7 @@ driven workflow registration. | `app/application/download/failures.py` | Frozen download-failure cooldown write/query DTOs and Chain persistence Port | | `app/db/adapters/download.py` | Short-session download-failure snapshot and mutation adapter | | `app/db/adapters/mediaserver.py` | Per-operation media-server cache query/upsert/cleanup transaction adapter | -| `app/application/history.py` | History use cases; deeply frozen DownloadHistory/TransferHistory DTOs and typed query/write/staging ports | +| `app/application/history/` | History use cases; deeply frozen DownloadHistory/TransferHistory DTOs and typed query/write/staging ports | | `app/db/adapters/history/download.py` | DownloadHistory short-session snapshot, query and mutation adapter | | `app/chain/download/` | Stable DownloadChain facade plus single-owner selection, submission, batch, existence, failure, history, post-processing, subtitle, task and technical-port modules | | `app/chain/search/` | Stable SearchChain facade plus shared execution, plan, provider, pagination, result, cache, title, media, music policy, subtitle, site and recommendation owners | @@ -1122,7 +1121,7 @@ driven workflow registration. | `app/application/outbox.py` | Durable intent, stager/store, claim fencing, topic handler and structured post-commit contracts | | `app/db/adapters/outbox.py` | SQLAlchemy Outbox transaction-only stagers and short-transaction claim/settlement stores | | `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/application/transfer/` | `models.py` owns transfer task, durable admission and versioned planning contracts; `jobs.py` owns task views; `workflow.py` owns the queue use case; execution, recovery, notification and projection policies remain in their single-word modules | | `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/scheduler/` | Scheduler implementation package: stable facade plus catalog, execution/bridge/progress, registry, reconcile, lifecycle and maintenance owners; no business Chain construction | diff --git a/scripts/architecture/mypy_ratchet.py b/scripts/architecture/mypy_ratchet.py index 270d3b314..e1cf2def5 100644 --- a/scripts/architecture/mypy_ratchet.py +++ b/scripts/architecture/mypy_ratchet.py @@ -50,6 +50,30 @@ MYPY_PATH_MIGRATIONS = { ("app/agent/tools/impl/_plugin_tool_utils.py",), ("app/application/plugin/management.py",), ), + "history-package": ( + ( + "app/application/history.py", + "app/application/history_contracts.py", + "app/application/history_retry.py", + "app/application/historymutation.py", + ), + ("app/application/history/",), + ), + "messaging-interaction-package": ( + ( + "app/application/messaging/interaction.py", + "app/application/messaging/agent_interaction.py", + ), + ("app/application/messaging/interaction/",), + ), + "messaging-channel-package": ( + ("app/application/messaging/channel_admin.py",), + ("app/application/messaging/channel/",), + ), + "messaging-webagent-package": ( + ("app/application/messaging/webagentstream.py",), + ("app/application/messaging/webagent/",), + ), } # 形如 app/foo.py:12: error: 消息说明 [error-code];个别错误可能缺代码。 diff --git a/tests/fixtures/architecture/complexity-v2-baseline.json b/tests/fixtures/architecture/complexity-v2-baseline.json index bed375d07..ad2bf0da1 100644 --- a/tests/fixtures/architecture/complexity-v2-baseline.json +++ b/tests/fixtures/architecture/complexity-v2-baseline.json @@ -8,7 +8,7 @@ "app/application/rss.py:RssHelper": 531, "app/application/security/url.py:SecurityUtils": 771, "app/application/torrent/download.py:TorrentHelper": 738, - "app/application/transfer/workflow.py:JobManager": 754, + "app/application/transfer/jobs.py:JobManager": 754, "app/chain/_recognition.py:RecognitionMixin": 570, "app/chain/base.py:ChainBase": 1161, "app/chain/download/batch.py:DownloadBatchOwner": 515, @@ -35,7 +35,6 @@ "file": { "app/api/endpoints/system.py": 1152, "app/application/formatting.py": 1523, - "app/application/history.py": 1348, "app/application/messaging/agent.py": 2195, "app/application/messaging/message.py": 1314, "app/application/messaging/skill.py": 1300, diff --git a/tests/fixtures/architecture/mypy-baseline.json b/tests/fixtures/architecture/mypy-baseline.json index 78bfe181f..b4e4cf217 100644 --- a/tests/fixtures/architecture/mypy-baseline.json +++ b/tests/fixtures/architecture/mypy-baseline.json @@ -634,7 +634,7 @@ "operator": 3, "type-arg": 2 }, - "app/application/history.py": { + "app/application/history/__init__.py": { "no-any-return": 1 }, "app/application/image.py": { @@ -652,22 +652,15 @@ "operator": 1 }, "app/application/messaging/agent.py": { - "arg-type": 1, - "attr-defined": 1, - "no-untyped-call": 1, - "no-untyped-def": 1 + "arg-type": 1 }, "app/application/messaging/chat.py": { "type-arg": 8 }, - "app/application/messaging/interaction.py": { + "app/application/messaging/interaction/__init__.py": { "no-untyped-def": 1, "type-arg": 2 }, - "app/application/messaging/media.py": { - "no-untyped-call": 1, - "no-untyped-def": 1 - }, "app/application/messaging/message.py": { "arg-type": 5, "assignment": 9, @@ -692,8 +685,7 @@ "no-untyped-def": 2 }, "app/application/messaging/skill.py": { - "no-untyped-call": 4, - "no-untyped-def": 4, + "no-untyped-def": 1, "type-arg": 6 }, "app/application/messaging/subscribe.py": { diff --git a/tests/test_transfer_failed_retry_budget.py b/tests/test_transfer_failed_retry_budget.py index 1f57fb191..e2d18679c 100644 --- a/tests/test_transfer_failed_retry_budget.py +++ b/tests/test_transfer_failed_retry_budget.py @@ -1,7 +1,7 @@ """ 有界重试预算的端到端行为验证。 -app/application/history.py 的查重闸真值表与计数器 API 已在 +app/application/history/ 的查重闸真值表与计数器 API 已在 tests/test_transfer_history_gate.py 逐项覆盖,本文件换一个角度:把「同一源路径 连续多个监控事件」串成一条时间线,验证瞬时故障能在预算内自愈、耗尽预算后被拦、 以及删除整理记录会让预算重新满额,贴近真实使用场景。 diff --git a/tests/test_transfer_history_gate.py b/tests/test_transfer_history_gate.py index 41a3a6d85..03b0f72c2 100644 --- a/tests/test_transfer_history_gate.py +++ b/tests/test_transfer_history_gate.py @@ -1,5 +1,5 @@ """ -覆盖 app/application/history.py 的整理历史查重闸。 +覆盖 app/application/history/ 的整理历史查重闸。 监控分发(app/monitor/dispatcher.py)与整理链计划整理 owner(app/chain/transfer/plan.py) 共用这套判定,本文件只测判定本身的真值表与查询辅助函数,不涉及调用方。 diff --git a/tests/test_transfer_history_write_path.py b/tests/test_transfer_history_write_path.py index a9c81c585..1fbc4496a 100644 --- a/tests/test_transfer_history_write_path.py +++ b/tests/test_transfer_history_write_path.py @@ -9,7 +9,7 @@ 因此这里断言的是「落库后每个字段的实际值」,不是「调用了什么」。 它们与同一张表的读侧规则(查重闸,见 test_transfer_history_gate.py)同住 -app/application/history.py;此前长在 TransferHistoryOper 上,故本文件旧名为 +app/application/history/;此前长在 TransferHistoryOper 上,故本文件旧名为 test_db_transferhistory_write_path.py。 """ import pytest