From 9924e5e7ff7fbbd4eff69402baf235c0aa61f6ce Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 9 Oct 2026 23:58:23 +0800 Subject: [PATCH] fix(workflow): satisfy architecture gates for issue 6931 --- app/workflow/actions/add_download.py | 65 ++++++++++--------- docs/architecture-overview.md | 6 +- docs/refactor/optimization-checklist.md | 2 +- .../architecture/dependency-baseline.json | 9 ++- 4 files changed, 44 insertions(+), 38 deletions(-) diff --git a/app/workflow/actions/add_download.py b/app/workflow/actions/add_download.py index 09a827800..5eb277b82 100644 --- a/app/workflow/actions/add_download.py +++ b/app/workflow/actions/add_download.py @@ -1,5 +1,5 @@ from dataclasses import fields -from typing import Optional +from typing import Any, Optional from pydantic import Field @@ -82,10 +82,11 @@ class AddDownloadAction(BaseAction): logger.warning(f"{t.torrent_info.title} 未识别到媒体信息,无法下载") continue download_context = self._to_domain_context(t) - if params.only_lack: - exists_info = DownloadChain().media_exists(download_context.media_info) + download_media_info = download_context.media_info + if params.only_lack and isinstance(download_media_info, DownloadMediaInfo): + exists_info = DownloadChain().media_exists(download_media_info) if exists_info: - if download_context.media_info.type == MediaType.MOVIE: + if download_media_info.type == MediaType.MOVIE: # 电影 logger.warning(f"{t.torrent_info.title} 媒体库中已存在,跳过") continue @@ -136,49 +137,49 @@ class AddDownloadAction(BaseAction): if context.media_info is None or context.torrent_info is None: raise ValueError("工作流下载上下文缺少媒体或种子信息") - media_info = context.media_info - if isinstance(media_info, (DownloadMediaInfo, DownloadMusicInfo)): - domain_media_info = media_info + context_data: dict[str, Any] = context.model_dump() + media_data = context_data.get("media_info") + if not isinstance(media_data, dict): + raise ValueError("工作流下载上下文缺少有效媒体信息") + domain_media_info: DownloadMediaInfo | DownloadMusicInfo + if media_data.get("type") == MediaType.MUSIC.value: + domain_media_info = DownloadMusicInfo.from_dict(media_data) else: - media_data = media_info.model_dump() - if media_data.get("type") == MediaType.MUSIC.value: - domain_media_info = DownloadMusicInfo.from_dict(media_data) - else: - domain_media_info = DownloadMediaInfo() - domain_media_info.from_dict(media_data) + domain_media_info = DownloadMediaInfo() + domain_media_info.from_dict(media_data) - torrent_info = context.torrent_info - if isinstance(torrent_info, DownloadTorrentInfo): - domain_torrent_info = torrent_info - else: - domain_torrent_info = DownloadTorrentInfo() - domain_torrent_info.from_dict(torrent_info.model_dump()) + torrent_data = context_data.get("torrent_info") + if not isinstance(torrent_data, dict): + raise ValueError("工作流下载上下文缺少有效种子信息") + domain_torrent_info = DownloadTorrentInfo() + domain_torrent_info.from_dict(torrent_data) - meta_info = context.meta_info - if isinstance(meta_info, MetaBase): - domain_meta_info = meta_info - elif meta_info is None: - domain_meta_info = None - else: - meta_data = meta_info.model_dump() + domain_meta_info: MetaBase | None = None + meta_data = context_data.get("meta_info") + if isinstance(meta_data, dict): meta_type = meta_data.get("type") if meta_type == MediaType.MUSIC.value: domain_meta_info = MetaMusic.from_dict(meta_data) else: - domain_meta_info = MetaInfo( - title=meta_data.get("org_string") or meta_data.get("title") or torrent_info.title or "", + title = meta_data.get("org_string") or meta_data.get("title") or torrent_data.get("title") + video_meta_info = MetaInfo( + title=title if isinstance(title, str) else "", subtitle=meta_data.get("subtitle"), mtype=MediaType(meta_type) if meta_type else None, ) - for item in fields(domain_meta_info): + for item in fields(video_meta_info): if item.name not in meta_data: continue value = meta_data[item.name] if item.name == "type" and value: value = MediaType(value) - setattr(domain_meta_info, item.name, value) - if meta_data.get("name"): - domain_meta_info.name = meta_data["name"] + if item.name == "title" and value is None: + continue + setattr(video_meta_info, item.name, value) + name = meta_data.get("name") + if isinstance(name, str) and name: + video_meta_info.name = name + domain_meta_info = video_meta_info return DownloadContext( meta_info=domain_meta_info, diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 2a1c7186a..b9db4a4e2 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -755,7 +755,7 @@ flowchart LR SDK 导出(若公开)、`docs/rules/05-architecture.md` 与上述架构测试。 - 延迟导入不被接受为隐藏循环依赖的手段。 -### 10.1 2026-09-11 当前收口状态与后续边界 +### 10.1 2026-10-09 当前收口状态与后续边界 当前宿主架构基线(排除 `app/plugins/**`)如下;数字来自 `tests/fixtures/architecture/`,更新基线前必须先审查语义变化: @@ -763,7 +763,7 @@ flowchart LR | 指标 | 当前值 | |---|---:| | Python 模块 | 1114 | -| 内部导入边 | 9,416 | +| 内部导入边 | 9,421 | | 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) | | Application / Chain 具体 Adapter 直连 | 0 / 0 | | Direct egress | 49(债务已清零,49 条精确 containment) | @@ -773,7 +773,7 @@ flowchart LR | Model/Oper 自动事务与自建 Session | 0 | | 组合根外 `SystemConfigOper()` | 0 | -整理失败反馈由 `app.application.transfer.feedback` 集中投影;已整理下载的站点字幕复用 `app.chain.transfer` 自动整理队列;网络连通性检测按 `app.application.nettest` 归档,并覆盖 HTTP 与 WebSocket 探测。未启用智能助手的消息音乐交互由 `app.chain.music_interaction` 包中的解析展示与订阅协调模块处理;音乐标签准入与批次共识由 `app.chain.transfer.music` 负责,预览证据由 `app.application.transfer.projection` 只读投影;宿主依赖边现为 9,416,音乐来源链与识别缓存复用领域身份核验,音乐交互包未新增 SCC,插件一次性任务 owner `app.scheduler.oncejob` 带来 1 个模块和 9 条依赖边。Agent 持久回执新增 Application 端口及 DB Model/Oper/Adapter 四个冷导入模块。缓存组合根只在启动时选用 Redis 缓存才导入 Redis 适配器,依赖 langchain_core 的对话记忆模型归属 `app.agent.memory`,文件缓存且未启用智能体的实例不再加载 redis 与 langchain_core。Telegram 渠道改用模块内 `app.modules.telegram.botapi` 精简 Bot API 客户端,经 `RequestUtils` 出站,宿主不再导入 pyTelegramBotAPI,启用 Telegram 的实例也不会因该 SDK 加载 redis;该依赖暂时保留给插件使用。Web Push 协议(aes128gcm 加密、VAPID 签名与经 `RequestUtils` 投递)由网络适配器 `app.adapters.network.webpush` 直接基于 http-ece 与 py-vapid 实现,消息端点与 `WebPushModule` 共用,宿主不再导入 pywebpush 及其连带的 aiohttp;该依赖同样暂时保留给插件使用。飞书渠道改用模块内 `app.modules.feishu.openapi` OpenAPI 客户端(经 `RequestUtils` 出站)与网络适配器 `app.adapters.network.feishu` 事件长连接(websocket-client 同步收发 pbbp2 帧),宿主不再导入 lark-oapi 及其上万个生成模块,长连接也不再依赖 SDK 的模块级事件循环;插件经 `app.sdk.feishu` 复用同一长连接,lark-oapi 暂时保留给尚未迁移的插件使用。API 文档路由由 `app.adapters.web.docs` 按 `API_DOCS_ENABLE` 逐请求开放,默认关闭,未开放时不生成文档。当前 `app.startup.lifecycle` 为 582、`app.factory` 为 595、`app.main` 为 597。音乐调用观察 `app.application.music.observation` 新增一个轻量模块,记录来源结果及请求预算;AcoustID及路径识别复用此上下文传递有界指纹候选,保留旧单ID模块合同。站点图片域名快照新增 `app.application.security.image`,各启动入口增加一个模块;性能基线只同步模块数量,原有耗时预算、历史采样和生命周期资源约束保持有效。 +整理失败反馈由 `app.application.transfer.feedback` 集中投影;已整理下载的站点字幕复用 `app.chain.transfer` 自动整理队列;网络连通性检测按 `app.application.nettest` 归档,并覆盖 HTTP 与 WebSocket 探测。未启用智能助手的消息音乐交互由 `app.chain.music_interaction` 包中的解析展示与订阅协调模块处理;音乐标签准入与批次共识由 `app.chain.transfer.music` 负责,预览证据由 `app.application.transfer.projection` 只读投影;宿主依赖边现为 9,421;工作流添加下载动作由 `app.workflow.actions.add_download` 转换工作流 Schema 为下载链领域上下文,新增 5 条模块导入边。音乐来源链与识别缓存复用领域身份核验,音乐交互包未新增 SCC,插件一次性任务 owner `app.scheduler.oncejob` 带来 1 个模块和 9 条依赖边。Agent 持久回执新增 Application 端口及 DB Model/Oper/Adapter 四个冷导入模块。缓存组合根只在启动时选用 Redis 缓存才导入 Redis 适配器,依赖 langchain_core 的对话记忆模型归属 `app.agent.memory`,文件缓存且未启用智能体的实例不再加载 redis 与 langchain_core。Telegram 渠道改用模块内 `app.modules.telegram.botapi` 精简 Bot API 客户端,经 `RequestUtils` 出站,宿主不再导入 pyTelegramBotAPI,启用 Telegram 的实例也不会因该 SDK 加载 redis;该依赖暂时保留给插件使用。Web Push 协议(aes128gcm 加密、VAPID 签名与经 `RequestUtils` 投递)由网络适配器 `app.adapters.network.webpush` 直接基于 http-ece 与 py-vapid 实现,消息端点与 `WebPushModule` 共用,宿主不再导入 pywebpush 及其连带的 aiohttp;该依赖同样暂时保留给插件使用。飞书渠道改用模块内 `app.modules.feishu.openapi` OpenAPI 客户端(经 `RequestUtils` 出站)与网络适配器 `app.adapters.network.feishu` 事件长连接(websocket-client 同步收发 pbbp2 帧),宿主不再导入 lark-oapi 及其上万个生成模块,长连接也不再依赖 SDK 的模块级事件循环;插件经 `app.sdk.feishu` 复用同一长连接,lark-oapi 暂时保留给尚未迁移的插件使用。API 文档路由由 `app.adapters.web.docs` 按 `API_DOCS_ENABLE` 逐请求开放,默认关闭,未开放时不生成文档。当前 `app.startup.lifecycle` 为 582、`app.factory` 为 595、`app.main` 为 597。音乐调用观察 `app.application.music.observation` 新增一个轻量模块,记录来源结果及请求预算;AcoustID及路径识别复用此上下文传递有界指纹候选,保留旧单ID模块合同。站点图片域名快照新增 `app.application.security.image`,各启动入口增加一个模块;性能基线只同步模块数量,原有耗时预算、历史采样和生命周期资源约束保持有效。 架构专项验证分为两个 CI 投影:`Check event semantic policy` 先运行依赖、Adapter、出口和 Event 语义门禁,`Check host architecture snapshot` 再执行快照测试及一次 diff --git a/docs/refactor/optimization-checklist.md b/docs/refactor/optimization-checklist.md index fafe69442..6b4caeb4f 100644 --- a/docs/refactor/optimization-checklist.md +++ b/docs/refactor/optimization-checklist.md @@ -98,7 +98,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁 | 指标 | 当前值 | 解释 | |---|---:|---| -| 宿主 Python 模块 / 内部依赖边 | 1114 / 9,416 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档;消息音乐交互由 `app.chain.music_interaction` 包中的音乐解析/展示与订阅协调模块处理;已整理下载的站点字幕经 `app.chain.transfer` 自动整理队列完成命名与归档;网络连通性检测按 `app.application.nettest` 归档;LLM 独占连接关闭复用宿主线程池;歌词字形转换复用 `app.foundation.text`;订阅创建使用 `app.domain.context.MediaInfo` 和运行时配置端口,将非 TMDB 影视身份投影为 TMDB 身份 | +| 宿主 Python 模块 / 内部依赖边 | 1114 / 9,421 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档;消息音乐交互由 `app.chain.music_interaction` 包中的音乐解析/展示与订阅协调模块处理;已整理下载的站点字幕经 `app.chain.transfer` 自动整理队列完成命名与归档;网络连通性检测按 `app.application.nettest` 归档;工作流添加下载动作由 `app.workflow.actions.add_download` 转换工作流 Schema 为下载链领域上下文,新增 5 条合法模块导入边;LLM 独占连接关闭复用宿主线程池;歌词字形转换复用 `app.foundation.text`;订阅创建使用 `app.domain.context.MediaInfo` 和运行时配置端口,将非 TMDB 影视身份投影为 TMDB 身份 | | 非平凡 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 a48251d5e..1e786d202 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -941,8 +941,8 @@ "runtime_only": true } }, - "edge_count": 9416, - "edge_sha256": "b1ea9dffbfa52f3cf110294c10113b24f592aca38a97d4025e3fa99f9bee7005", + "edge_count": 9421, + "edge_sha256": "605aad14dd551c25ca9d9bb87680f4ca05c68d00b613ce552abb19f0449c8669", "edges": [ "app -> app.foundation", "app -> app.foundation.environment", @@ -10205,11 +10205,16 @@ "app.workflow.actions.add_download -> app.chain.download", "app.workflow.actions.add_download -> app.chain.media", "app.workflow.actions.add_download -> app.domain", + "app.workflow.actions.add_download -> app.domain.context", + "app.workflow.actions.add_download -> app.domain.meta", + "app.workflow.actions.add_download -> app.domain.meta.metabase", + "app.workflow.actions.add_download -> app.domain.meta.metamusic", "app.workflow.actions.add_download -> app.domain.metainfo", "app.workflow.actions.add_download -> app.runtime", "app.workflow.actions.add_download -> app.runtime.log", "app.workflow.actions.add_download -> app.runtime.stop", "app.workflow.actions.add_download -> app.schemas", + "app.workflow.actions.add_download -> app.schemas.context", "app.workflow.actions.add_download -> app.schemas.types", "app.workflow.actions.add_download -> app.schemas.workflow", "app.workflow.actions.add_download -> app.workflow",