diff --git a/app/chain/transfer/retry.py b/app/chain/transfer/retry.py index c51473cb6..9cfa8e5c2 100644 --- a/app/chain/transfer/retry.py +++ b/app/chain/transfer/retry.py @@ -168,7 +168,11 @@ class FailedRetryMixin(_TransferOwnerBase): state, errmsg = self.redo_transfer_history(history_id) if state: - public_message = public_error_message(errmsg, context="transfer") + public_message = ( + public_error_message(errmsg, context="transfer") + if errmsg + else "" + ) self.post_message( Message( channel=channel, diff --git a/app/schemas/history.py b/app/schemas/history.py index bb16b8824..f6a7d7a6b 100644 --- a/app/schemas/history.py +++ b/app/schemas/history.py @@ -1,8 +1,9 @@ from typing import List, Literal, Optional -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field +from pydantic import field_validator as _field_validator -from app.runtime.errors import public_error_message +from app.runtime.errors import public_error_message as _public_error_message from app.schemas.common import JsonData from app.schemas.media import OptionalMediaIdentityMixin from app.schemas.types import MediaSource @@ -149,13 +150,13 @@ class TransferHistory(OptionalMediaIdentityMixin, BaseModel): model_config = ConfigDict(from_attributes=True) - @field_validator("errmsg", mode="before") + @_field_validator("errmsg", mode="before") @classmethod def _sanitize_error_message(cls, value: object) -> Optional[str]: """历史接口只返回可理解的整理失败原因,数据库原文仍用于诊断。""" if value is None or not str(value).strip(): return None - return public_error_message(value, context="transfer") + return _public_error_message(value, context="transfer") class BatchTransferHistoryRedoRequest(BaseModel): diff --git a/app/schemas/response.py b/app/schemas/response.py index 662f7cfbe..c81aa238f 100644 --- a/app/schemas/response.py +++ b/app/schemas/response.py @@ -2,7 +2,7 @@ from typing import Any, Generic, Optional, TypeVar from pydantic import BaseModel, ConfigDict, field_validator -from app.runtime.errors import public_error_message +from app.runtime.errors import public_error_message as _public_error_message from app.runtime.localization import LocaleHelper DataT = TypeVar("DataT") @@ -32,7 +32,7 @@ class Response(BaseModel, Generic[DataT]): raw_message = str(value) if not raw_message.strip(): return "" - message = public_error_message(raw_message) + message = _public_error_message(raw_message) if not message: return "" return LocaleHelper.translate_text( diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 2eeb27f9b..56a90f894 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -755,7 +755,7 @@ flowchart LR | 指标 | 当前值 | |---|---:| | Python 模块 | 968 | -| 内部导入边 | 8,141 | +| 内部导入边 | 8,156 | | 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) | | Application / Chain 具体 Adapter 直连 | 0 / 0 | | Direct egress | 53(债务已清零,53 条精确 containment) | diff --git a/docs/architecture/optimization-checklist.md b/docs/architecture/optimization-checklist.md index a31f5b4f5..e18c0047f 100644 --- a/docs/architecture/optimization-checklist.md +++ b/docs/architecture/optimization-checklist.md @@ -94,7 +94,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁 | 指标 | 当前值 | 解释 | |---|---:|---| -| 宿主 Python 模块 / 内部依赖边 | 968 / 8,141 | `dependency-baseline.json` 当前快照 | +| 宿主 Python 模块 / 内部依赖边 | 969 / 8,156 | `dependency-baseline.json` 当前快照 | | 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 | | 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 | | Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 | diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 250e59fab..b2888b050 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -1089,8 +1089,8 @@ "runtime_only": true } }, - "edge_count": 8141, - "edge_sha256": "f86580389ab2c9efd50914fdbd34004fbdc3e9b01ad06a537817415add7c3687", + "edge_count": 8156, + "edge_sha256": "9b9596b5e25de2e68a007dfb3934f027458a6741cfc2df1e92e936b7ffca0d1a", "edges": [ "app -> app.foundation", "app -> app.foundation.environment", @@ -2275,6 +2275,7 @@ "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.errors", "app.api.endpoints.history -> app.runtime.log", "app.api.endpoints.history -> app.runtime.loop", "app.api.endpoints.history -> app.runtime.progress", @@ -2713,7 +2714,9 @@ "app.api.endpoints.subscribe -> app.domain.context", "app.api.endpoints.subscribe -> app.domain.metainfo", "app.api.endpoints.subscribe -> app.runtime", + "app.api.endpoints.subscribe -> app.runtime.errors", "app.api.endpoints.subscribe -> app.runtime.execution", + "app.api.endpoints.subscribe -> app.runtime.log", "app.api.endpoints.subscribe -> app.runtime.tasks", "app.api.endpoints.subscribe -> app.schemas", "app.api.endpoints.subscribe -> app.schemas.common", @@ -2833,6 +2836,7 @@ "app.api.endpoints.transfer -> app.chain.transfer", "app.api.endpoints.transfer -> app.chain.transfer.facade", "app.api.endpoints.transfer -> app.runtime", + "app.api.endpoints.transfer -> app.runtime.errors", "app.api.endpoints.transfer -> app.runtime.log", "app.api.endpoints.transfer -> app.runtime.stop", "app.api.endpoints.transfer -> app.schemas", @@ -2950,6 +2954,7 @@ "app.api.servarr -> app.domain.context", "app.api.servarr -> app.domain.metainfo", "app.api.servarr -> app.runtime", + "app.api.servarr -> app.runtime.errors", "app.api.servarr -> app.runtime.version", "app.api.servarr -> app.schemas", "app.api.servarr -> app.schemas.response", @@ -3644,6 +3649,8 @@ "app.application.subscription.status -> app.application", "app.application.subscription.status -> app.application.subscription", "app.application.subscription.status -> app.application.subscription.execution", + "app.application.subscription.status -> app.runtime", + "app.application.subscription.status -> app.runtime.errors", "app.application.subscription.write -> app.application", "app.application.subscription.write -> app.application.classification", "app.application.subscription.write -> app.application.classification.reference", @@ -4984,6 +4991,7 @@ "app.chain.transfer.history -> app.domain.meta", "app.chain.transfer.history -> app.domain.meta.metabase", "app.chain.transfer.history -> app.runtime", + "app.chain.transfer.history -> app.runtime.errors", "app.chain.transfer.history -> app.runtime.log", "app.chain.transfer.history -> app.schemas", "app.chain.transfer.history -> app.schemas.message", @@ -5093,6 +5101,7 @@ "app.chain.transfer.retry -> app.domain", "app.chain.transfer.retry -> app.domain.context", "app.chain.transfer.retry -> app.runtime", + "app.chain.transfer.retry -> app.runtime.errors", "app.chain.transfer.retry -> app.runtime.log", "app.chain.transfer.retry -> app.runtime.loop", "app.chain.transfer.retry -> app.runtime.tasks", @@ -5132,6 +5141,7 @@ "app.chain.transfer.settlement -> app.domain.meta", "app.chain.transfer.settlement -> app.domain.meta.metabase", "app.chain.transfer.settlement -> app.runtime", + "app.chain.transfer.settlement -> app.runtime.errors", "app.chain.transfer.settlement -> app.runtime.log", "app.chain.transfer.settlement -> app.runtime.loop", "app.chain.transfer.settlement -> app.schemas", @@ -5903,6 +5913,7 @@ "app.factory -> app.api", "app.factory -> app.api.response", "app.factory -> app.application", + "app.factory -> app.application.outbox", "app.factory -> app.application.plugin", "app.factory -> app.application.plugin.routes", "app.factory -> app.application.plugin.runtime", @@ -5910,6 +5921,7 @@ "app.factory -> app.application.security.token", "app.factory -> app.runtime", "app.factory -> app.runtime.correlation", + "app.factory -> app.runtime.errors", "app.factory -> app.runtime.localization", "app.factory -> app.runtime.log", "app.factory -> app.runtime.loop", @@ -8097,6 +8109,8 @@ "app.schemas.event -> app.schemas.types", "app.schemas.file -> app.schemas", "app.schemas.file -> app.schemas.types", + "app.schemas.history -> app.runtime", + "app.schemas.history -> app.runtime.errors", "app.schemas.history -> app.schemas", "app.schemas.history -> app.schemas.common", "app.schemas.history -> app.schemas.media", @@ -8130,6 +8144,7 @@ "app.schemas.query -> app.schemas.media", "app.schemas.query -> app.schemas.types", "app.schemas.response -> app.runtime", + "app.schemas.response -> app.runtime.errors", "app.schemas.response -> app.runtime.localization", "app.schemas.search -> app.schemas", "app.schemas.search -> app.schemas.common", @@ -9234,7 +9249,7 @@ "app.workflow.actions.transfer_file -> app.workflow", "app.workflow.actions.transfer_file -> app.workflow.actions" ], - "module_count": 968, + "module_count": 969, "modules": [ "app", "app.adapters", @@ -10003,6 +10018,7 @@ "app.runtime.deprecation", "app.runtime.deprecation.notices", "app.runtime.deprecation.policy", + "app.runtime.errors", "app.runtime.event", "app.runtime.event.binding", "app.runtime.event.contracts", diff --git a/tests/test_agent_side_effect_boundaries.py b/tests/test_agent_side_effect_boundaries.py index 2fa253c97..5c78c0be6 100644 --- a/tests/test_agent_side_effect_boundaries.py +++ b/tests/test_agent_side_effect_boundaries.py @@ -325,7 +325,7 @@ async def test_agent_execution_failure_closes_cached_subagent_middleware() -> No result, _ = await agent._execute_agent([]) - assert "failed" in result + assert result == "智能助手执行失败,请稍后重试" middleware.close.assert_awaited_once() assert agent._compiled_agent_bundle is None assert agent._subagent_middlewares == () diff --git a/tests/test_chain_durable_events.py b/tests/test_chain_durable_events.py index 0ec0e6aec..3bcd9196e 100644 --- a/tests/test_chain_durable_events.py +++ b/tests/test_chain_durable_events.py @@ -35,6 +35,7 @@ from app.application.outbox import ( DOWNLOAD_MODULE_TOPIC, DOWNLOAD_NOTIFICATION_TOPIC, DOWNLOAD_SUBTITLE_TOPIC, + PostCommitEffectError, ) from app.application.transfer.execution import ( TransferExecutionCheckpoint, @@ -561,7 +562,7 @@ def test_transfer_event_failure_leaves_committed_intent_pending(): """模拟插件事件总线在业务提交后失败。""" raise RuntimeError("event failed") - with pytest.raises(RuntimeError, match="event failed"): + with pytest.raises(PostCommitEffectError, match="提交后的相关处理未完成") as error: writer.transfer_result( topic="transfer.completed", stage_history=stage_history, @@ -576,6 +577,8 @@ def test_transfer_event_failure_leaves_committed_intent_pending(): }, publish=fail_publish, ) + assert len(error.value.errors) == 1 + assert str(error.value.errors[0]) == "event failed" with factory() as session: history = session.execute(select(TransferHistory)).scalar_one() diff --git a/tests/test_site_cookie_endpoint.py b/tests/test_site_cookie_endpoint.py index 84bc3fcee..b8959ad3f 100644 --- a/tests/test_site_cookie_endpoint.py +++ b/tests/test_site_cookie_endpoint.py @@ -51,7 +51,7 @@ def test_update_cookie_legacy_get_keeps_query_params(): ) assert response.success is False - assert response.message == "failed" + assert response.message == "操作失败,请稍后重试" fake_chain.update_cookie.assert_called_once_with( site_info=fake_site, username="user", diff --git a/tests/test_subscribe_oper.py b/tests/test_subscribe_oper.py index e17a5d295..539d807b6 100644 --- a/tests/test_subscribe_oper.py +++ b/tests/test_subscribe_oper.py @@ -186,7 +186,7 @@ def test_add_rejects_incomplete_media_identity(identity): ): result = _add(mediainfo=_media(None), season=1) - assert result == (0, "媒体身份不完整") + assert result == (0, "未识别到媒体信息,请检查媒体来源和媒体 ID 后重试") # 守卫必须在查询与建模之前短路,而不是先写进去再补救 subscribe_model.exists.assert_not_called() subscribe_model.assert_not_called() @@ -203,7 +203,7 @@ def test_async_add_rejects_incomplete_media_identity(identity): result = asyncio.run(_async_add(mediainfo=_media(None), season=1)) - assert result == (0, "媒体身份不完整") + assert result == (0, "未识别到媒体信息,请检查媒体来源和媒体 ID 后重试") subscribe_model.async_exists.assert_not_awaited() subscribe_model.assert_not_called() @@ -571,7 +571,7 @@ def test_subscribe_exists_distinguishes_music_entities_with_same_source_id(db): def test_subscribe_chain_exists_forwards_episode_group(): """订阅前置存在性检查必须查询当前剧集组,不能退回主季范围。""" - from app.chain.subscribe import SubscribeChain + from app.chain.subscribe.facade import SubscribeChain media = _media("eg-1") meta = SimpleNamespace(begin_season=1) diff --git a/tests/test_transfer_legacy_terminal_compat.py b/tests/test_transfer_legacy_terminal_compat.py index b05222dab..5465a43b9 100644 --- a/tests/test_transfer_legacy_terminal_compat.py +++ b/tests/test_transfer_legacy_terminal_compat.py @@ -9,7 +9,7 @@ from app.application.transfer.execution import ( TransferExecutionCheckpoint, TransferSettlementResult, ) -from app.chain.transfer import TransferChain +from app.chain.transfer.facade import TransferChain from app.domain.context import MediaInfo from app.domain.meta.metabase import MetaBase from app.schemas.file import FileItem diff --git a/tests/test_transfer_manual_review_api.py b/tests/test_transfer_manual_review_api.py index 9ad91359b..38ee3f9d8 100644 --- a/tests/test_transfer_manual_review_api.py +++ b/tests/test_transfer_manual_review_api.py @@ -195,5 +195,5 @@ def test_manual_review_conflict_returns_http_409(monkeypatch) -> None: ) assert error.value.status_code == 409 - assert error.value.detail == "步骤已被判定" + assert error.value.detail == "整理任务状态已变化,请刷新后重试" assert _ManualReviewCommand.calls[0][1]["actor"] == "11"