diff --git a/AGENTS.md b/AGENTS.md index 5ccf9c954..faf4eea20 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,11 +61,11 @@ 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;`projection/` 只拥有外部媒体来源到 canonical `MediaInfo` 字段的纯映射 | Persistence, global settings reads, network/filesystem clients, Rust imports, service discovery, process lifecycle | `context.py`, `media.py`, `metainfo.py`, `scraper.py`, `meta/`, `projection/` | | `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/runtime/extensions/` | 模块、插件、配置化服务和托管资源实现的发现、注册与生命周期适配 | 通用反射机制、插件公开 API、无关业务流程 | `module/manager.py`, `plugin/manager.py`, `resource.py`, `service.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` | -| `app/adapters/external/` | CookieCloud、插件市场、OCR、IP 归属和 MoviePilot Server 等命名外部生态 | 通用 HTTP/DNS/文件机制或可复用领域语义 | `market.py`, `server.py`, `cookiecloud.py`, `ocr.py`, `location.py`, `wechat_crypt.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/security/` | 认证、授权、Cookie、Passkey、OTP/二次认证、路径/URL 安全、SSRF 和签名策略 | 通用 URL 解析、进程运行策略、普通业务校验 | `access.py`, `auth.py`, `cookie.py`, `passkey.py`, `otp.py`, `twofactor.py`, `url.py` | @@ -74,7 +74,7 @@ The legacy roots have no physical directories in the source tree. Current images | `app/db/adapters/` | 实现 Application 持久化 Port,创建短生命周期 Session/UoW,并适配 Oper | 用例规则、启动顺序、进程生命周期 | `subscription.py`, `site.py`, `outbox.py`, `workflow.py` | | `app/startup/` | Composition root: `composition/` 构造并注入跨层依赖,`initializers/` 按领域初始化,`lifecycle/` 编排启动关闭 | Reusable business rules or adapter implementation details | `composition/context.py`, `composition/database.py`, `initializers/modules.py`, `lifecycle/components.py` | | `app/sdk/` | Deliberately curated stable imports for new plugins | Canonical implementation logic or host-internal dependencies | `browser.py`, `cache.py`, `logging.py`, `media.py`, `network.py`, `services.py` | -| `app/runtime/compat/` | 仅依赖标准库的精确旧导入路由、资源前置扫描和 DEBUG 诊断 | 业务实现、通配猜测、目标模块的提前导入 | `manifest.py`, `imports.py`, `resource_imports.py`, `diagnostics.py` | +| `app/runtime/compat/` | 仅依赖标准库的精确旧导入路由、资源前置扫描和 DEBUG 诊断 | 业务实现、通配猜测、目标模块的提前导入 | `manifest.py`, `imports.py`, `resources.py`, `diagnostics.py` | 容易误分的三个边界必须按实际职责判断:`application/rss.py` 同时承担 Feed/种子语义、站点规则和浏览器回退,不是单纯 HTTP 传输;规范目标是由它拥有所需 Port、startup 注入 network/system Adapter。当前直接导入是 `S2-L6` 临时债务,不是允许的新模式。`application/site/sites.*` 及 `user.sites.v3.bin` 共同构成站点目录、认证和索引应用能力,只有下载安装机制留在 `adapters/system/resource.py`;`foundation/crypto.py` 只提供无状态 RSA/摘要/AES 算法,认证、签名、令牌和二次验证策略仍属于 `application/security/`。 diff --git a/app/adapters/external/wechat_crypt.py b/app/adapters/external/wechat.py similarity index 100% rename from app/adapters/external/wechat_crypt.py rename to app/adapters/external/wechat.py diff --git a/app/agent/llm/discovery.py b/app/agent/llm/discovery.py index 63adee299..f65847ac5 100644 --- a/app/agent/llm/discovery.py +++ b/app/agent/llm/discovery.py @@ -22,7 +22,7 @@ def attach_server_tool_capabilities( base_url: Optional[str] = None, ) -> list[dict[str, Any]]: """为 Provider 模型目录附加统一的服务端工具能力描述。""" - from app.agent.llm.server_tools import ServerToolRegistry + from app.agent.llm.tools import ServerToolRegistry result = [] for item in models: diff --git a/app/agent/llm/helper.py b/app/agent/llm/helper.py index 05a33bfb2..3fd0d1c18 100644 --- a/app/agent/llm/helper.py +++ b/app/agent/llm/helper.py @@ -15,7 +15,7 @@ from app.runtime.log import logger from app.runtime.settings import get_runtime_setting if TYPE_CHECKING: - from app.agent.llm.server_tools import ServerToolResolution + from app.agent.llm.tools import ServerToolResolution class LLMTestError(RuntimeError): @@ -1169,7 +1169,7 @@ class LLMHelper: use_proxy=use_proxy, ) model_name = runtime.get("model_id") or model_name - from app.agent.llm.server_tools import ( + from app.agent.llm.tools import ( ServerToolRegistry, ServerToolUnavailableError, ) diff --git a/app/agent/llm/server_tools.py b/app/agent/llm/tools.py similarity index 100% rename from app/agent/llm/server_tools.py rename to app/agent/llm/tools.py diff --git a/app/agent/runtime_loader.py b/app/agent/loader.py similarity index 100% rename from app/agent/runtime_loader.py rename to app/agent/loader.py diff --git a/app/agent/middleware/activity_log.py b/app/agent/middleware/activity.py similarity index 100% rename from app/agent/middleware/activity_log.py rename to app/agent/middleware/activity.py diff --git a/app/agent/middleware/runtime_config.py b/app/agent/middleware/config.py similarity index 100% rename from app/agent/middleware/runtime_config.py rename to app/agent/middleware/config.py diff --git a/app/agent/middleware/patch_tool_calls.py b/app/agent/middleware/patching.py similarity index 100% rename from app/agent/middleware/patch_tool_calls.py rename to app/agent/middleware/patching.py diff --git a/app/agent/middleware/tool_selection.py b/app/agent/middleware/selection.py similarity index 100% rename from app/agent/middleware/tool_selection.py rename to app/agent/middleware/selection.py diff --git a/app/agent/orchestrator.py b/app/agent/orchestrator.py index 2973a4aa6..0dfeb9290 100644 --- a/app/agent/orchestrator.py +++ b/app/agent/orchestrator.py @@ -22,10 +22,10 @@ from langgraph.checkpoint.memory import InMemorySaver from app.agent.callback import StreamingHandler from app.agent.contracts import ReplyMode, build_display_message from app.agent.llm.helper import LLMHelper -from app.agent.llm.server_tools import ServerToolRegistry +from app.agent.llm.tools import ServerToolRegistry from app.agent.mcp import agent_mcp_manager from app.agent.memory import MemoryManager, memory_manager -from app.agent.middleware.activity_log import ( +from app.agent.middleware.activity import ( QUERY_ACTIVITY_LOG_TOOL_NAME, ActivityLogMiddleware, ) @@ -33,9 +33,9 @@ from app.agent.middleware.jobs import ( JobsMiddleware, ) from app.agent.middleware.memory import MemoryMiddleware -from app.agent.middleware.patch_tool_calls import PatchToolCallsMiddleware +from app.agent.middleware.patching import PatchToolCallsMiddleware from app.agent.middleware.policy import AgentPolicyMiddleware -from app.agent.middleware.runtime_config import RuntimeConfigMiddleware +from app.agent.middleware.config import RuntimeConfigMiddleware from app.agent.middleware.skills import SKILL_TOOL_NAME, SkillsMiddleware from app.agent.middleware.subagents import ( SUBAGENT_CONTROL_TOOL_NAME, @@ -49,7 +49,7 @@ from app.agent.middleware.summarization import ( from app.agent.middleware.summarization import ( FinalRequestCompactionMiddleware, ) -from app.agent.middleware.tool_selection import ToolSelectorMiddleware +from app.agent.middleware.selection import ToolSelectorMiddleware from app.agent.middleware.usage import UsageMiddleware from app.agent.policy.contracts import ( AuthSource, @@ -1552,7 +1552,7 @@ class MoviePilotAgent: """ 初始化主 Agent 本地工具实例。 """ - from app.agent.runtime_loader import get_tool_factory + from app.agent.loader import get_tool_factory return get_tool_factory().create_tools( session_id=self.session_id, @@ -1570,7 +1570,7 @@ class MoviePilotAgent: self, ) -> tuple[ToolCatalogSnapshot, ToolCatalogSnapshot]: """在同一插件 revision 窗口内建立主图和子图工具目录。""" - from app.agent.runtime_loader import get_tool_factory + from app.agent.loader import get_tool_factory tool_factory = get_tool_factory() plugin_manager = get_plugin_manager() @@ -1677,7 +1677,7 @@ class MoviePilotAgent: @staticmethod def _tool_factory_revision() -> str: """在目录签名确实需要时解析工具工厂版本。""" - from app.agent.runtime_loader import get_tool_factory + from app.agent.loader import get_tool_factory return get_tool_factory().catalog_factory_revision() @@ -1807,7 +1807,7 @@ class MoviePilotAgent: """ 初始化子代理专用静默工具列表。 """ - from app.agent.runtime_loader import get_tool_factory + from app.agent.loader import get_tool_factory return get_tool_factory().create_tools( session_id=self.session_id, @@ -2005,7 +2005,7 @@ class MoviePilotAgent: logger.debug(f"复用会话内 Agent 图: session_id={self.session_id}") return cached_agent max_tools = get_runtime_setting('LLM_MAX_TOOLS') - from app.agent.runtime_loader import get_tool_factory + from app.agent.loader import get_tool_factory always_include_tools = ( get_tool_factory().get_tool_selector_always_include_names(tools) diff --git a/app/agent/policy/sanitizer.py b/app/agent/policy/sanitizer.py index 465441500..340b51f80 100644 --- a/app/agent/policy/sanitizer.py +++ b/app/agent/policy/sanitizer.py @@ -10,7 +10,7 @@ from typing import Any from pydantic import AliasChoices, AliasPath, BaseModel, ValidationError -from app.agent.policy.secret_fields import is_secret_setting_key +from app.agent.policy.secrets import is_secret_setting_key REDACTED_VALUE = "***" diff --git a/app/agent/policy/secret_fields.py b/app/agent/policy/secrets.py similarity index 100% rename from app/agent/policy/secret_fields.py rename to app/agent/policy/secrets.py diff --git a/app/agent/prompt/transfer_redo.py b/app/agent/prompt/transfer.py similarity index 100% rename from app/agent/prompt/transfer_redo.py rename to app/agent/prompt/transfer.py diff --git a/app/agent/tools/impl/_system_setting_utils.py b/app/agent/tools/impl/_system_setting_utils.py index bfb8fd787..fb9f7d66f 100644 --- a/app/agent/tools/impl/_system_setting_utils.py +++ b/app/agent/tools/impl/_system_setting_utils.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from typing import Any, Optional -from app.agent.policy.secret_fields import is_secret_setting_key +from app.agent.policy.secrets import is_secret_setting_key from app.runtime.config import Settings from app.schemas.types import SystemConfigKey diff --git a/app/agent/tools/manager.py b/app/agent/tools/manager.py index 3c08c3212..7fb6aca38 100644 --- a/app/agent/tools/manager.py +++ b/app/agent/tools/manager.py @@ -97,7 +97,7 @@ class MoviePilotToolsManager: 工厂负责插件 revision 前后稳定窗口;manager 只发布完整快照,避免 并发调用观察到一半刷新后的工具列表。 """ - from app.agent.runtime_loader import get_tool_factory + from app.agent.loader import get_tool_factory try: catalog = get_tool_factory().create_catalog( @@ -255,7 +255,7 @@ class MoviePilotToolsManager: if self.catalog is None or [ id(tool) for tool in self.catalog.tools ] != [id(tool) for tool in self.tools]: - from app.agent.runtime_loader import get_tool_factory + from app.agent.loader import get_tool_factory from app.agent.tools.catalog import ToolCatalogSnapshot self.catalog = ToolCatalogSnapshot.from_tools( diff --git a/app/agent/web.py b/app/agent/web.py index c4ab63cc8..a0aa541ec 100644 --- a/app/agent/web.py +++ b/app/agent/web.py @@ -3,7 +3,7 @@ from threading import Lock from typing import Any, Awaitable, Callable, Optional -from app.agent.runtime_loader import get_moviepilot_agent_type +from app.agent.loader import get_moviepilot_agent_type from app.application.security.user import get_configured_user_id_lookup from app.runtime.log import logger from app.schemas.message import Message diff --git a/app/api/apiv1.py b/app/api/apiv1.py index 089fcbe27..37be33794 100644 --- a/app/api/apiv1.py +++ b/app/api/apiv1.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.router_specs import API_V1_ROUTER_SPECS +from app.api.routers import API_V1_ROUTER_SPECS api_router = APIRouter() for spec in API_V1_ROUTER_SPECS: diff --git a/app/api/endpoints/anthropic.py b/app/api/endpoints/anthropic.py index eaba2d879..c0e904c49 100644 --- a/app/api/endpoints/anthropic.py +++ b/app/api/endpoints/anthropic.py @@ -16,7 +16,7 @@ from app.api.endpoints.openai import ( _is_manager_unavailable, _run_managed_agent, ) -from app.api.openai_utils import ( +from app.api.protocol import ( build_anthropic_messages, build_prompt, build_session_id, diff --git a/app/api/endpoints/history.py b/app/api/endpoints/history.py index 1491c2897..7b2c9e21e 100644 --- a/app/api/endpoints/history.py +++ b/app/api/endpoints/history.py @@ -7,7 +7,7 @@ from fastapi import Depends from app.adapters.web.security.access import verify_token from app.agent.contracts import ReplyMode -from app.agent.prompt.transfer_redo import ( +from app.agent.prompt.transfer import ( build_batch_manual_redo_prompt, build_manual_redo_prompt, ) diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index f9cfc49a5..9f648eb24 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -7,7 +7,7 @@ from typing import Annotated, Any, List, Optional, Protocol, Union from fastapi import Depends, Request from starlette.responses import PlainTextResponse -from app.adapters.external.wechat_crypt import WXBizMsgCrypt +from app.adapters.external.wechat import WXBizMsgCrypt from app.adapters.web.security.access import verify_apitoken, verify_token from app.api.context import get_background_task_registry, resolve_background_task_registry from app.api.dependencies.agent import get_message_query_service diff --git a/app/api/endpoints/openai.py b/app/api/endpoints/openai.py index 0dd3326d0..75765e17e 100644 --- a/app/api/endpoints/openai.py +++ b/app/api/endpoints/openai.py @@ -10,12 +10,12 @@ from fastapi.security import HTTPAuthorizationCredentials from app.adapters.web.security.access import openai_bearer_scheme from app.agent.contracts import ReplyMode -from app.agent.runtime_loader import get_moviepilot_agent_type +from app.agent.loader import get_moviepilot_agent_type from app.api.context import ( get_background_task_registry_compat, resolve_background_task_registry, ) -from app.api.openai_utils import ( +from app.api.protocol import ( build_completion_payload, build_prompt, build_responses_input, diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index b4d630cfa..6a51e152e 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -127,7 +127,7 @@ def _database_backup_artifact_data(artifact: Any) -> _SchemaDatabaseBackupArtifa def _validate_llm_server_tool_config(env: dict) -> Optional[str]: """校验强制服务端联网搜索配置,返回用户可读错误信息。""" - from app.agent.llm.server_tools import ( + from app.agent.llm.tools import ( ServerToolRegistry, ServerToolUnavailableError, ) diff --git a/app/api/openai_utils.py b/app/api/protocol.py similarity index 100% rename from app/api/openai_utils.py rename to app/api/protocol.py diff --git a/app/api/router_specs.py b/app/api/routers.py similarity index 100% rename from app/api/router_specs.py rename to app/api/routers.py diff --git a/app/modules/__init__.py b/app/modules/__init__.py index 513ef2657..53f63c008 100644 --- a/app/modules/__init__.py +++ b/app/modules/__init__.py @@ -3,7 +3,7 @@ from abc import abstractmethod, ABCMeta from typing import Generic, Tuple, Union, TypeVar, Type, Dict, Optional, Callable from pathlib import Path -from app.runtime.extensions.service_config import ServiceConfigHelper +from app.runtime.extensions.service import ServiceConfigHelper from app.runtime.log import logger from app.schemas.message import Message from app.schemas.system import NotificationConf diff --git a/app/modules/_base/media_auxiliary.py b/app/modules/_base/media.py similarity index 100% rename from app/modules/_base/media_auxiliary.py rename to app/modules/_base/media.py diff --git a/app/modules/anilist/__init__.py b/app/modules/anilist/__init__.py index 38e1f590d..a91a2ad86 100644 --- a/app/modules/anilist/__init__.py +++ b/app/modules/anilist/__init__.py @@ -7,7 +7,7 @@ from app.domain.meta.metabase import MetaBase from app.domain.projection.anilist import format_date as format_anilist_date from app.domain.scraper import MediaScraperHelper from app.modules import _ModuleBase -from app.modules._base.media_auxiliary import MediaAuxiliaryProviderMixin +from app.modules._base.media import MediaAuxiliaryProviderMixin from app.modules.anilist.anilist import AniListApi from app.runtime.log import logger from app.runtime.settings import get_runtime_setting diff --git a/app/modules/bangumi/__init__.py b/app/modules/bangumi/__init__.py index dbf1f5f0b..beae34967 100644 --- a/app/modules/bangumi/__init__.py +++ b/app/modules/bangumi/__init__.py @@ -8,7 +8,7 @@ from app.domain.meta.metabase import MetaBase from app.domain.projection.bangumi import resolve_media_type as resolve_bangumi_media_type from app.domain.scraper import MediaScraperHelper from app.modules import _ModuleBase -from app.modules._base.media_auxiliary import MediaAuxiliaryProviderMixin +from app.modules._base.media import MediaAuxiliaryProviderMixin from app.modules.bangumi.bangumi import BangumiApi from app.runtime.log import logger from app.runtime.settings import get_runtime_setting diff --git a/app/modules/douban/__init__.py b/app/modules/douban/__init__.py index 8c8568978..971e0117e 100644 --- a/app/modules/douban/__init__.py +++ b/app/modules/douban/__init__.py @@ -16,7 +16,7 @@ from app.domain.meta.metamusic import MetaMusic from app.domain.metainfo import MetaInfo from app.foundation.text import convert as zhconv_convert from app.modules import _ModuleBase -from app.modules._base.media_auxiliary import MediaAuxiliaryProviderMixin +from app.modules._base.media import MediaAuxiliaryProviderMixin from app.modules.douban.apiv2 import DoubanApi from app.modules.douban.scraper import DoubanScraper from app.runtime.execution import retry diff --git a/app/modules/imdb/__init__.py b/app/modules/imdb/__init__.py index 2835a4a7f..7d35deef7 100644 --- a/app/modules/imdb/__init__.py +++ b/app/modules/imdb/__init__.py @@ -11,7 +11,7 @@ from app.domain.meta.metabase import MetaBase from app.domain.scraper import MediaScraperHelper from app.foundation.text import convert as zhconv_convert from app.modules import _ModuleBase -from app.modules._base.media_auxiliary import MediaAuxiliaryProviderMixin +from app.modules._base.media import MediaAuxiliaryProviderMixin from app.runtime.log import logger from app.runtime.settings import get_runtime_setting from app.schemas.context import MediaCredit, MediaImageSet diff --git a/app/modules/indexer/parser/ipt_project.py b/app/modules/indexer/parser/ipt.py similarity index 100% rename from app/modules/indexer/parser/ipt_project.py rename to app/modules/indexer/parser/ipt.py diff --git a/app/modules/musicbrainz/__init__.py b/app/modules/musicbrainz/__init__.py index 13ebda5f6..999a41842 100644 --- a/app/modules/musicbrainz/__init__.py +++ b/app/modules/musicbrainz/__init__.py @@ -18,7 +18,7 @@ from app.domain.meta.metabase import MetaBase from app.domain.meta.metamusic import MetaMusic from app.foundation.text import convert as zhconv_convert from app.modules import _ModuleBase -from app.modules.musicbrainz.music_cache import MusicBrainzCache +from app.modules.musicbrainz.cache import MusicBrainzCache from app.runtime.cache import cached from app.runtime.log import logger from app.runtime.settings import get_runtime_setting diff --git a/app/modules/musicbrainz/music_cache.py b/app/modules/musicbrainz/cache.py similarity index 100% rename from app/modules/musicbrainz/music_cache.py rename to app/modules/musicbrainz/cache.py diff --git a/app/modules/themoviedb/__init__.py b/app/modules/themoviedb/__init__.py index 661eb2516..388ee452e 100644 --- a/app/modules/themoviedb/__init__.py +++ b/app/modules/themoviedb/__init__.py @@ -11,10 +11,10 @@ from app.domain.media import is_media_source_enabled, is_media_source_selected from app.domain.meta.metabase import MetaBase from app.foundation.text import convert as zhconv_convert from app.modules import _ModuleBase -from app.modules._base.media_auxiliary import MediaAuxiliaryProviderMixin +from app.modules._base.media import MediaAuxiliaryProviderMixin from app.modules.themoviedb.category import CategoryHelper from app.modules.themoviedb.scraper import TmdbScraper -from app.modules.themoviedb.tmdb_cache import TmdbCache +from app.modules.themoviedb.cache import TmdbCache from app.modules.themoviedb.tmdbapi import TmdbApi from app.modules.themoviedb.tmdbv3api.exceptions import TMDbConnectionError from app.runtime.log import logger diff --git a/app/modules/themoviedb/tmdb_cache.py b/app/modules/themoviedb/cache.py similarity index 100% rename from app/modules/themoviedb/tmdb_cache.py rename to app/modules/themoviedb/cache.py diff --git a/app/modules/thetvdb/__init__.py b/app/modules/thetvdb/__init__.py index 95c932351..734ac564e 100644 --- a/app/modules/thetvdb/__init__.py +++ b/app/modules/thetvdb/__init__.py @@ -8,7 +8,7 @@ from app.runtime.execution import run_in_threadpool from app.runtime.settings import get_runtime_setting from app.modules import _ModuleBase -from app.modules.thetvdb import tvdb_v4_official +from app.modules.thetvdb import client from app.runtime.log import logger from app.schemas.types import ( MediaRecognizeType, @@ -24,7 +24,7 @@ class TheTvDbModule(_ModuleBase): TVDB媒体信息匹配 """ __timeout: int = 15 - tvdb: Optional[tvdb_v4_official.TVDB] = None + tvdb: Optional[client.TVDB] = None __auth_lock = Lock() def init_module(self) -> None: @@ -40,7 +40,7 @@ class TheTvDbModule(_ModuleBase): try: if not get_runtime_setting('TVDB_V4_API_KEY'): raise ConnectionError("TVDB API Key 未配置,无法初始化会话。") - self.tvdb = tvdb_v4_official.TVDB(apikey=get_runtime_setting('TVDB_V4_API_KEY'), + self.tvdb = client.TVDB(apikey=get_runtime_setting('TVDB_V4_API_KEY'), pin=get_runtime_setting('TVDB_V4_API_PIN'), proxy=get_runtime_setting('PROXY'), timeout=self.__timeout) diff --git a/app/modules/thetvdb/tvdb_v4_official.py b/app/modules/thetvdb/client.py similarity index 100% rename from app/modules/thetvdb/tvdb_v4_official.py rename to app/modules/thetvdb/client.py diff --git a/app/modules/wechat/__init__.py b/app/modules/wechat/__init__.py index 1e4bf76ed..aa64748ba 100644 --- a/app/modules/wechat/__init__.py +++ b/app/modules/wechat/__init__.py @@ -12,7 +12,7 @@ from app.application.messaging.agent import ( ) from app.runtime.log import logger from app.modules._base.notification import _MessageChannelModuleBase -from app.adapters.external.wechat_crypt import WXBizMsgCrypt +from app.adapters.external.wechat import WXBizMsgCrypt from app.modules.wechat.wechat import WeChat from app.modules.wechat.wechatbot import WeChatBot from app.schemas.notification import NotificationChannel diff --git a/app/runtime/compat/manifest.py b/app/runtime/compat/manifest.py index 46070d847..b1354ab72 100644 --- a/app/runtime/compat/manifest.py +++ b/app/runtime/compat/manifest.py @@ -441,13 +441,13 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = { owner="domain", ), "app.core.module": ModuleAlias( - target="app.runtime.extensions.module_manager", + target="app.runtime.extensions.module.manager", replacement="app.sdk.plugins", introduced="v3.0.0", owner="runtime", ), "app.core.plugin": ModuleAlias( - target="app.runtime.extensions.plugin_manager", + target="app.runtime.extensions.plugin.manager", replacement="app.sdk.plugins", introduced="v3.0.0", owner="runtime", diff --git a/app/runtime/compat/resource_imports.py b/app/runtime/compat/resources.py similarity index 100% rename from app/runtime/compat/resource_imports.py rename to app/runtime/compat/resources.py diff --git a/app/runtime/extensions/host_module_adapter.py b/app/runtime/extensions/module/adapter.py similarity index 98% rename from app/runtime/extensions/host_module_adapter.py rename to app/runtime/extensions/module/adapter.py index 67daaf09c..378d5d8ec 100644 --- a/app/runtime/extensions/host_module_adapter.py +++ b/app/runtime/extensions/module/adapter.py @@ -14,7 +14,7 @@ from app.runtime.capabilities.model import ( ) from app.runtime.capabilities.registry import CapabilityRegistry from app.runtime.settings import get_runtime_setting, has_runtime_setting -from app.runtime.extensions.service_config import ServiceConfigHelper +from app.runtime.extensions.service import ServiceConfigHelper from app.schemas.types import ( DownloaderType, MediaRecognizeType, @@ -30,7 +30,7 @@ from app.schemas.types import ( HOST_MODULE_KIND = "host_module" _SETTING_SELECTOR = "setting_truthy" _SERVICE_SELECTOR = "system_config_item" -_MODULE_ROOT = Path(__file__).resolve().parents[2] / "modules" +_MODULE_ROOT = Path(__file__).resolve().parents[3] / "modules" _SERVICE_CONFIG_GETTERS = MappingProxyType({ SystemConfigKey.Downloaders.value: ServiceConfigHelper.get_downloader_configs, SystemConfigKey.MediaServers.value: ServiceConfigHelper.get_mediaserver_configs, diff --git a/app/runtime/extensions/module_manager.py b/app/runtime/extensions/module/manager.py similarity index 99% rename from app/runtime/extensions/module_manager.py rename to app/runtime/extensions/module/manager.py index d06bbc3c6..d2b4f7561 100644 --- a/app/runtime/extensions/module_manager.py +++ b/app/runtime/extensions/module/manager.py @@ -14,7 +14,7 @@ from app.runtime.capabilities.model import ( from app.runtime.capabilities.runtime import CapabilityRuntime from app.runtime.settings import get_runtime_setting from app.runtime.events import Event, EventHandlerBinding, eventmanager -from app.runtime.extensions.host_module_adapter import ( +from app.runtime.extensions.module.adapter import ( HOST_MODULE_KIND, HostModuleAdapter, build_host_module_registry, diff --git a/app/runtime/extensions/plugin_manager.py b/app/runtime/extensions/plugin/manager.py similarity index 100% rename from app/runtime/extensions/plugin_manager.py rename to app/runtime/extensions/plugin/manager.py diff --git a/app/runtime/extensions/service_config.py b/app/runtime/extensions/service.py similarity index 100% rename from app/runtime/extensions/service_config.py rename to app/runtime/extensions/service.py diff --git a/app/sdk/plugins.py b/app/sdk/plugins.py index e1f533424..c456d0656 100644 --- a/app/sdk/plugins.py +++ b/app/sdk/plugins.py @@ -1,7 +1,7 @@ """插件和模块运行时管理接口。""" -from app.runtime.extensions.module_manager import ModuleManager -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.module.manager import ModuleManager +from app.runtime.extensions.plugin.manager import PluginManager __all__ = ["ModuleManager", "PluginManager"] diff --git a/app/sdk/services.py b/app/sdk/services.py index d4b45feb1..1d6ad0a05 100644 --- a/app/sdk/services.py +++ b/app/sdk/services.py @@ -1,7 +1,7 @@ """插件可使用的宿主服务发现与运行时门面。""" from app.application.service import ServiceBaseHelper -from app.runtime.extensions.service_config import ServiceConfigHelper +from app.runtime.extensions.service import ServiceConfigHelper from app.runtime.state import SystemHelper from app.application.downloader import DownloaderHelper from app.application.rules import RuleHelper diff --git a/app/startup/composition/chain.py b/app/startup/composition/chain.py index 51d091b95..6ce403637 100644 --- a/app/startup/composition/chain.py +++ b/app/startup/composition/chain.py @@ -24,8 +24,8 @@ from app.db.session import SessionFactory from app.runtime.cache import AsyncFileCache, FileCache from app.runtime.events import EventManager 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.module.manager import ModuleManager +from app.runtime.extensions.plugin.manager import PluginManager from app.runtime.stop import runtime_stop_state from app.startup.composition.database import build_transactional_user_repository from app.startup.composition.runtime import RuntimeDependencies diff --git a/app/startup/initializers/agent.py b/app/startup/initializers/agent.py index 0b49e77fc..ca5bea557 100644 --- a/app/startup/initializers/agent.py +++ b/app/startup/initializers/agent.py @@ -1,17 +1,17 @@ from typing import Any from app.agent.llm.gateway import register_llm_provider_runtime -from app.agent.runtime_loader import ( +from app.agent.loader import ( activate_agent_service, begin_agent_shutdown, close_materialized_terminal_sessions, is_tool_factory_materialized, reconcile_agent_service, ) -from app.agent.runtime_loader import ( +from app.agent.loader import ( get_agent_manager as get_runtime_agent_manager, ) -from app.agent.runtime_loader import ( +from app.agent.loader import ( get_running_agent_manager as get_runtime_running_agent_manager, ) from app.application.agent import ( @@ -141,7 +141,7 @@ def _get_llm_helper() -> Any: def _get_manual_redo_prompt_builder() -> Any: """首个整理接管请求才导入对应提示词构建器。""" - from app.agent.prompt.transfer_redo import build_manual_redo_prompt + from app.agent.prompt.transfer import build_manual_redo_prompt return build_manual_redo_prompt diff --git a/app/startup/initializers/modules.py b/app/startup/initializers/modules.py index b5a3aeead..755214b93 100644 --- a/app/startup/initializers/modules.py +++ b/app/startup/initializers/modules.py @@ -56,8 +56,8 @@ from app.db.session import ( from app.runtime.config import settings as legacy_settings from app.runtime.events import EventHandlerBinding, EventManager from app.runtime.execution import run_in_threadpool_to_completion -from app.runtime.extensions.module_manager import ModuleManager -from app.runtime.extensions.service_config import ( +from app.runtime.extensions.module.manager import ModuleManager +from app.runtime.extensions.service import ( ServiceConfigHelper, configure_service_config_reader, reset_service_config_reader, diff --git a/app/startup/initializers/plugins.py b/app/startup/initializers/plugins.py index bb8ef281d..23a3cd896 100644 --- a/app/startup/initializers/plugins.py +++ b/app/startup/initializers/plugins.py @@ -88,7 +88,7 @@ from app.runtime.compat.diagnostics import ( configure_legacy_import_diagnostics, scan_plugin_legacy_imports, ) -from app.runtime.compat.resource_imports import scan_plugin_resource_imports +from app.runtime.compat.resources import scan_plugin_resource_imports from app.runtime.execution import run_in_threadpool_to_completion from app.runtime.extensions.plugin.dependency import PluginDependencyInstallResult from app.runtime.extensions.plugin.runtime import ( @@ -107,7 +107,7 @@ from app.runtime.extensions.plugin.system import ( configure_plugin_system, get_plugin_system, ) -from app.runtime.extensions.plugin_manager import ( +from app.runtime.extensions.plugin.manager import ( PluginManager, configure_plugin_catalog_factory, configure_plugin_legacy_import_services, diff --git a/app/startup/initializers/routers.py b/app/startup/initializers/routers.py index 07eb9f652..24c62caef 100644 --- a/app/startup/initializers/routers.py +++ b/app/startup/initializers/routers.py @@ -8,7 +8,7 @@ def init_routers(app: FastAPI, api_prefix: str = "/api/v1"): :param app: 需要挂载路由的 FastAPI 应用 :param api_prefix: v1 API 根路径,由启动组合根传入 """ - from app.api.router_specs import API_V1_ROUTER_SPECS + from app.api.routers import API_V1_ROUTER_SPECS from app.api.servarr import arr_router from app.api.servcookie import cookie_router # 直接聚合端点路由,避免先构建兼容路由器再克隆到最终应用。 diff --git a/app/testing/__init__.py b/app/testing/__init__.py index 58524dd12..bbe021a57 100644 --- a/app/testing/__init__.py +++ b/app/testing/__init__.py @@ -4,9 +4,9 @@ - :mod:`app.testing.stub`:测试期对 ``sys.modules`` 的临时打桩并自动还原,避免残留假模块相互污染; - :mod:`app.testing.bootstrap`:隔离 CONFIG_DIR、建表、插件目录注入与 v1/v2 marker 等引导逻辑; -- :mod:`app.testing.network_guard`:autouse 拦截测试期对非本地主机的真实出站。 +- :mod:`app.testing.network`:autouse 拦截测试期对非本地主机的真实出站。 -子模块各自按需 import(如 ``network_guard`` 依赖 pytest),故此处只 re-export 无第三方依赖的 +子模块各自按需 import(如 ``network`` 依赖 pytest),故此处只 re-export 无第三方依赖的 :func:`stub_modules`,保持 ``import app.testing`` 不引入 pytest 等测试期依赖。 """ from app.testing.stub import stub_modules diff --git a/app/testing/network_guard.py b/app/testing/network.py similarity index 98% rename from app/testing/network_guard.py rename to app/testing/network.py index 5e510cb7f..64a3298ed 100644 --- a/app/testing/network_guard.py +++ b/app/testing/network.py @@ -1,7 +1,7 @@ """测试网络守卫(主程序与插件仓共享)。 提供一个 autouse 的 pytest fixture,拦截测试期对非本地主机的真实出站网络。主程序 -``tests/conftest.py`` 与各插件仓 conftest 只需 ``from app.testing.network_guard import +``tests/conftest.py`` 与各插件仓 conftest 只需 ``from app.testing.network import block_real_network`` 即复用同一道守卫——pytest 会把 conftest 命名空间内(含 import 进来的) fixture 一并识别,autouse 自动作用于每个用例,无需逐用例改动。 diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 0a5c9131e..2b98b0d09 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -193,12 +193,12 @@ SDK/Compat。领域查询与写入继续使用 Application 所属的冻结 DTO/P | `app/foundation/` | 无状态、无配置、无 I/O 的底层原语:反射/动态导入、加密、DOM、单例、文本、URL、版本比较 | `reflection.py`、`crypto.py`、`singleton.py` | | `app/domain/` | 纯 MoviePilot 业务语义:媒体上下文、识别解析、站点状态解释、磁力语义、NFO 刮削 | `context.py`、`metainfo.py`、`meta/`、`scraper.py` | | `app/runtime/` | 进程级运行机制:配置、进程拓扑、事件、完整日志、缓存契约与内存后端、运行依赖 profile 与原生载荷激活检测、任务所有权、执行/关联上下文、并发、调度、限流、本地化、GC、重启状态 | `config.py`、`events.py`、`event/`、`dependencies/`、`tasks.py`、`execution.py`、`correlation.py`、`log.py`、`cache.py` | -| `app/runtime/extensions/` | 模块 / 插件 / 配置化服务 / 托管资源的发现、注册与生命周期适配;旧管理器文件保留稳定 ABI 门面,具体实现拆在主题子包 | `module_manager.py`、`plugin_manager.py`、`plugin/` | +| `app/runtime/extensions/` | 模块 / 插件 / 配置化服务 / 托管资源的发现、注册与生命周期适配;管理器归入对应主题包,旧插件路径只由 Compat 精确映射 | `module/manager.py`、`plugin/manager.py`、`service.py` | | `app/runtime/compat/` | 仅标准库的精确旧模块、包与符号导入路由;不是业务实现,也不是通用 re-export 层 | `manifest.py`、`imports.py` | | `app/adapters/network/` | 通用 HTTP、浏览器、DNS、Cloudflare、IP 传输机制 | `http.py`、`browser.py` | | `app/adapters/cache/` | Redis 与文件缓存的具体实现 | `backends.py`、`redis.py` | | `app/adapters/system/` | OS/文件/进程/stdio/显示/包安装/Rust 加速适配 | `host.py`、`resource.py`、`fsproxy.py` | -| `app/adapters/external/` | 命名外部生态:插件市场、CookieCloud、OCR、IP 归属、MP Server、微信加密 | `market.py`、`server.py`、`wechat_crypt.py` | +| `app/adapters/external/` | 命名外部生态:插件市场、CookieCloud、OCR、IP 归属、MP Server、微信加密 | `market.py`、`server.py`、`wechat.py` | | `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/` | @@ -340,7 +340,7 @@ flowchart LR MM --> M6["indexer / subtitle / filter ..."] ``` -- 模块由 `runtime/extensions/module_manager.py` 发现并管理生命周期; +- 模块由 `runtime/extensions/module/manager.py` 发现并管理生命周期; `app/modules/_base/` 承载各模块族的共享模板基类(下载器、媒体服务器、消息渠道)。 - 模块开关由 `init_setting()` 声明的配置项决定(如 `DOWNLOADER = "qbittorrent"`)。 - **模块之间、模块到 Chain 的直接依赖被禁止**,跨模块编排一律由 Chain 完成。 @@ -590,7 +590,7 @@ flowchart TB Entry["消息渠道 / API / MCP"] --> Facade["app/application/agent.py
编排门面(get_agent_manager 等)"] Reg["app/startup/initializers/agent.py
生命周期显式注册/重置 Provider"] Reg --> Facade - Facade -.能力启用或首次使用时物化.-> RT["app/agent/runtime_loader.py
能力发现与服务物化"] + Facade -.能力启用或首次使用时物化.-> RT["app/agent/loader.py
能力发现与服务物化"] RT --> Manager["app/agent/manager.py
稳定 AgentManager 门面"] Manager --> Session["session.py / lifecycle.py
会话队列与有界生命周期"] Manager --> Tasks["tasks.py
后台、调度与心跳任务"] @@ -663,9 +663,9 @@ flowchart TB 端点层禁止直接依赖 `factory`。 - 动态插件路由使用原生 `APIRoute`,插件自行决定返回结构;主程序的统一 `Response` 封装只适用于 `app/api/` 的宿主端点。插件若已经自行返回 `Response`、字典、列表或其它可序列化值,宿主不再二次包裹。 -- `app/runtime/extensions/plugin_manager.py` 是保留插件 ABI 的管理器门面,发现、加载、生命周期、 - 目录、同步等实现拆在 `app/runtime/extensions/plugin/`;这个“门面 + 实现包”是有意的兼容边界, - 不应为了目录整齐而让外部插件改用内部实现文件。 +- `app/runtime/extensions/plugin/manager.py` 是 canonical 管理器 owner,发现、加载、生命周期、 + 目录、同步等实现共同归入 `app/runtime/extensions/plugin/`。旧插件仍从 `app.core.plugin` 或 + `app.sdk.plugins` 进入,并由 Compat 精确路由到同一个 `PluginManager` 身份。 - 插件可参与 `run_module` 方法分发(同名方法优先响应)并注册事件处理器。 --- @@ -776,8 +776,8 @@ flowchart LR `app/sdk/_legacy/` 薄门面保留行为兼容。兼容清单是导入路由,不负责合并模块,也不负责把任意 新实现重新导出到旧模块。 - 已完成的插件边界:插件 API 的动态路由由 application 端口 + web adapter 组成,使用原生 - `APIRoute` 保留插件响应;插件管理器保留 `plugin_manager.py` 的稳定 ABI,内部实现拆在 - `runtime/extensions/plugin/`;`app/plugins/` 仅作为运行时插件副本/覆盖层处理。 + `APIRoute` 保留插件响应;插件管理器归入 `runtime/extensions/plugin/manager.py`,旧 ABI + 只由 SDK/Compat 路由;`app/plugins/` 仅作为运行时插件副本/覆盖层处理。 - 已完成的主题收口:订阅 DTO/Port 归入 `app/application/subscription/contract.py`,用例写入归入 `app/application/subscription/write.py`,SQLAlchemy 实现只在 `app/db/adapters/subscription.py`;插件动态路由与 文件夹操作归入 `app/application/plugin/routes.py`、`folders.py`。原 diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index 6f9c10dc4..0a17531b5 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -436,10 +436,13 @@ ownership. New production Python module filenames use one lowercase word. When one topic needs multiple modules, create a topic package and keep each child filename to one word, for example `runtime/event/{registry,binding,dispatch,errors}.py` or -`application/subscription/{contract,delete,identity}.py`. Established multiword -public import paths may remain as compatibility exceptions after plugin/import -scanning, but they are not templates for new modules. Test filenames continue -to follow pytest's descriptive `test_.py` convention. +`application/subscription/{contract,delete,identity}.py`. Multiword production +paths may remain only when the machine policy verifies a stable discovery +identifier or records a path-specific reason and consumer evidence; an +unstructured grandfathered filename list is forbidden. The same gate scans +every package root: new roots are documentation-only or precise lazy facades +unless a reviewed owner reason is recorded. Test filenames continue to follow +pytest's descriptive `test_.py` convention. Legacy module paths belong in `app/runtime/compat/manifest.py`. New implementation modules must not re-export old managers, helpers or Oper classes @@ -928,7 +931,7 @@ driven workflow registration. | `app/agent/lifecycle.py` | Agent manager admission, startup, idle collection and bounded shutdown owner | | `app/agent/tasks.py` | Background prompt, scheduled task and heartbeat execution owner | | `app/agent/orchestrator.py` | Per-session `MoviePilotAgent` execution and LLM/tool/middleware orchestration only | -| `app/agent/runtime_loader.py` | Agent-specific capability discovery and canonical entrypoint/service materialization; reuses the generic Capability Runtime while keeping Agent ownership under `app/agent/` | +| `app/agent/loader.py` | Agent-specific capability discovery and canonical entrypoint/service materialization; reuses the generic Capability Runtime while keeping Agent ownership under `app/agent/` | | `app/agent/__init__.py` | Implementation-free package root; exact historical Agent symbols are supplied by the Compat overlay only, while host callers import `orchestrator.py` or the relevant owner directly | | `app/agent/llm/__init__.py` | Implementation-free package root; only the verified historical `LLMHelper` symbol is supplied by exact Compat routing | | `app/agent/llm/helper.py` | Canonical `LLMHelper` owner and exact target of the historical `app.helper.llm` module path | @@ -981,8 +984,8 @@ driven workflow registration. | `app/runtime/extensions/module/contracts.py` | High-frequency method families and frozen legacy fallback contract | | `app/application/chain/context.py` | Injectable Chain dependencies, no-argument compatibility provider and legacy Transfer command Port | | `app/startup/lifecycle/components.py` | Declarative normal/safe-mode lifecycle manifest, ordering and timeout budgets | -| `app/runtime/extensions/module_manager.py` | Module discovery and lifecycle | -| `app/runtime/extensions/plugin_manager.py` | Stable PluginManager ABI facade plus plugin discovery/lifecycle entrypoints; its constructor only consumes the startup-injected Runtime factory and does not build an environment or individual owner | +| `app/runtime/extensions/module/manager.py` | Module discovery and lifecycle | +| `app/runtime/extensions/plugin/manager.py` | Stable PluginManager ABI facade plus plugin discovery/lifecycle entrypoints; its constructor only consumes the startup-injected Runtime factory and does not build an environment or individual owner | | `app/runtime/extensions/plugin/runtime.py` | Frozen typed aggregate and construction primitive for plugin registry, lifecycle, catalog, dependency, monitor, projection, sync and package-system owners; it does not choose concrete startup dependencies | | `app/runtime/extensions/plugin/system.py` | Startup-injected market/package/dependency system-port facade, including delegation to the unique physical package deletion owner; it does not construct concrete adapters | | `app/runtime/extensions/plugin/dependency.py` | Virtual-instance dependency classification and runtime-status persistence owner | @@ -1018,7 +1021,7 @@ driven workflow registration. | `app/adapters/cache/backends.py` | Redis and filesystem cache adapters | | `app/adapters/system/resource.py` | Runtime resource detection/download/installation | | `app/adapters/system/fsproxy.py` | Timeout-guarded local filesystem operations in a killable subprocess (with colocated `fsworker.py`) | -| `app/adapters/external/wechat_crypt.py` | WeChat enterprise-message XML encryption/decryption protocol | +| `app/adapters/external/wechat.py` | WeChat enterprise-message XML encryption/decryption protocol | | `app/application/rules.py` | Rule domain: user rule-group config access (`RuleHelper`), built-in torrent filter rule set and rule parser | | `app/adapters/external/market.py` | Exact legacy `PluginHelper` compatibility facade and install Gateway; canonical market behavior lives in `plugin/client.py` and host code must not import this facade | | `app/application/security/url.py` | URL/path validation, SSRF protection and signed image policy | diff --git a/docs/rules/06-code-styles.md b/docs/rules/06-code-styles.md index 3bd19bf18..22367c79c 100644 --- a/docs/rules/06-code-styles.md +++ b/docs/rules/06-code-styles.md @@ -110,7 +110,7 @@ except: - Private functions in the same file are preferable to extracting a new module for single-use logic. - Add code to the canonical capability package that owns it, and extend an existing domain file whenever that domain already exists. - Do not recreate generic `core`, `helper`, or `utils` buckets; see `05-architecture.md` for placement rules. -- New files should use a focused noun name; a role suffix is appropriate only when it distinguishes ownership, such as `plugin_manager.py`; otherwise prefer the package-owned noun, such as `adapters/system/package.py`. +- New files use one focused lowercase noun. When the capability already names the package, put the role in a single-word child such as `plugin/manager.py`; do not flatten it back into `plugin_manager.py`. - Keep files focused on one domain concern. --- diff --git a/scripts/architecture/service_locator.py b/scripts/architecture/service_locator.py index bf68327a9..69f80708f 100644 --- a/scripts/architecture/service_locator.py +++ b/scripts/architecture/service_locator.py @@ -43,7 +43,7 @@ RUNTIME_FACADE_POLICIES = ( ), RuntimeFacadePolicy( name="module", - dependency="app.runtime.extensions.module_manager", + dependency="app.runtime.extensions.module.manager", exact_consumers=frozenset( { "app.sdk.plugins", @@ -54,7 +54,7 @@ RUNTIME_FACADE_POLICIES = ( ), RuntimeFacadePolicy( name="plugin", - dependency="app.runtime.extensions.plugin_manager", + dependency="app.runtime.extensions.plugin.manager", exact_consumers=frozenset( { "app.sdk.plugins", diff --git a/scripts/perf/instrument/sitecustomize.py b/scripts/perf/instrument/sitecustomize.py index d6832db7e..62a207c74 100644 --- a/scripts/perf/instrument/sitecustomize.py +++ b/scripts/perf/instrument/sitecustomize.py @@ -170,7 +170,7 @@ def _agent_module_observation() -> dict[str, object]: def _read_agent_runtime() -> dict[str, object]: """读取轻量 Agent loader 的公开只读状态,不触发 capability 首用。""" try: - from app.agent.runtime_loader import is_tool_factory_materialized + from app.agent.loader import is_tool_factory_materialized return { "available": True, diff --git a/tests/conftest.py b/tests/conftest.py index af2fcbb62..48b24677e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ from app.testing.bootstrap import prepare_backend prepare_backend() # 复用共享 autouse 网络守卫;同一实现亦供各插件仓 conftest import 复用,避免逐仓维护 -from app.testing.network_guard import block_real_network # noqa: E402,F401 +from app.testing.network import block_real_network # noqa: E402,F401 TResult = TypeVar("TResult") @@ -207,20 +207,20 @@ def configure_plugin_system_services(): from app.application.plugin.runtime import configure_plugin_runtime from app.runtime.cache import AsyncFileCache, FileCache from app.runtime.events import EventManager - from app.runtime.extensions import plugin_manager as plugin_manager_module + from app.runtime.extensions.plugin import manager as plugin_manager_module from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher - from app.runtime.extensions.module_manager import ModuleManager + from app.runtime.extensions.module.manager import ModuleManager from app.runtime.extensions.plugin.runtime import ( PluginRuntimeEnvironment, build_plugin_runtime, ) from app.runtime.extensions.plugin.storage import get_plugin_storage from app.runtime.extensions.plugin.system import get_plugin_system - from app.runtime.extensions.plugin_manager import ( + from app.runtime.extensions.plugin.manager import ( PluginManager, reset_plugin_runtime_factory, ) - from app.runtime.extensions.service_config import ServiceConfigHelper + from app.runtime.extensions.service import ServiceConfigHelper configure_service_directory( configs=ServiceConfigHelper.get_configs, diff --git a/tests/fixtures/architecture/filename-policy.json b/tests/fixtures/architecture/filename-policy.json index 0b11474b3..6791cb984 100644 --- a/tests/fixtures/architecture/filename-policy.json +++ b/tests/fixtures/architecture/filename-policy.json @@ -1,152 +1,184 @@ { - "grandfathered_multiword_files": [ - "app/adapters/external/wechat_crypt.py", - "app/agent/llm/server_tools.py", - "app/agent/middleware/activity_log.py", - "app/agent/middleware/patch_tool_calls.py", - "app/agent/middleware/runtime_config.py", - "app/agent/middleware/tool_selection.py", - "app/agent/policy/secret_fields.py", - "app/agent/prompt/transfer_redo.py", - "app/agent/runtime_loader.py", - "app/agent/tools/impl/_command_safety.py", - "app/agent/tools/impl/_file_write_utils.py", - "app/agent/tools/impl/_filter_rule_utils.py", - "app/agent/tools/impl/_music_utils.py", - "app/agent/tools/impl/_plugin_tool_utils.py", - "app/agent/tools/impl/_system_setting_utils.py", - "app/agent/tools/impl/_terminal_session.py", - "app/agent/tools/impl/_torrent_search_utils.py", - "app/agent/tools/impl/add_custom_filter_rule.py", - "app/agent/tools/impl/add_download_tasks.py", - "app/agent/tools/impl/add_rule_group.py", - "app/agent/tools/impl/add_subscribe.py", - "app/agent/tools/impl/apply_patch.py", - "app/agent/tools/impl/ask_user_choice.py", - "app/agent/tools/impl/browse_webpage.py", - "app/agent/tools/impl/create_agent_task.py", - "app/agent/tools/impl/delete_agent_task.py", - "app/agent/tools/impl/delete_custom_filter_rule.py", - "app/agent/tools/impl/delete_download_history.py", - "app/agent/tools/impl/delete_download_tasks.py", - "app/agent/tools/impl/delete_rule_group.py", - "app/agent/tools/impl/delete_subscribe.py", - "app/agent/tools/impl/delete_transfer_history.py", - "app/agent/tools/impl/edit_file.py", - "app/agent/tools/impl/execute_command.py", - "app/agent/tools/impl/get_recommendations.py", - "app/agent/tools/impl/get_search_results.py", - "app/agent/tools/impl/install_plugin.py", - "app/agent/tools/impl/list_directory.py", - "app/agent/tools/impl/list_slash_commands.py", - "app/agent/tools/impl/query_agent_tasks.py", - "app/agent/tools/impl/query_builtin_filter_rules.py", - "app/agent/tools/impl/query_custom_filter_rules.py", - "app/agent/tools/impl/query_custom_identifiers.py", - "app/agent/tools/impl/query_directory_settings.py", - "app/agent/tools/impl/query_doctor_report.py", - "app/agent/tools/impl/query_download_tasks.py", - "app/agent/tools/impl/query_downloaders.py", - "app/agent/tools/impl/query_episode_schedule.py", - "app/agent/tools/impl/query_installed_plugins.py", - "app/agent/tools/impl/query_library_exists.py", - "app/agent/tools/impl/query_library_latest.py", - "app/agent/tools/impl/query_market_plugins.py", - "app/agent/tools/impl/query_media_detail.py", - "app/agent/tools/impl/query_personas.py", - "app/agent/tools/impl/query_plugin_capabilities.py", - "app/agent/tools/impl/query_plugin_config.py", - "app/agent/tools/impl/query_plugin_data.py", - "app/agent/tools/impl/query_popular_subscribes.py", - "app/agent/tools/impl/query_rule_groups.py", - "app/agent/tools/impl/query_schedulers.py", - "app/agent/tools/impl/query_site_userdata.py", - "app/agent/tools/impl/query_sites.py", - "app/agent/tools/impl/query_subscribe_history.py", - "app/agent/tools/impl/query_subscribe_shares.py", - "app/agent/tools/impl/query_subscribes.py", - "app/agent/tools/impl/query_system_settings.py", - "app/agent/tools/impl/query_transfer_history.py", - "app/agent/tools/impl/query_workflows.py", - "app/agent/tools/impl/read_file.py", - "app/agent/tools/impl/recognize_captcha.py", - "app/agent/tools/impl/recognize_media.py", - "app/agent/tools/impl/reload_plugin.py", - "app/agent/tools/impl/run_agent_task.py", - "app/agent/tools/impl/run_scheduler.py", - "app/agent/tools/impl/run_slash_command.py", - "app/agent/tools/impl/run_workflow.py", - "app/agent/tools/impl/scrape_metadata.py", - "app/agent/tools/impl/search_media.py", - "app/agent/tools/impl/search_person.py", - "app/agent/tools/impl/search_person_credits.py", - "app/agent/tools/impl/search_subscribe.py", - "app/agent/tools/impl/search_torrents.py", - "app/agent/tools/impl/search_web.py", - "app/agent/tools/impl/send_local_file.py", - "app/agent/tools/impl/send_message.py", - "app/agent/tools/impl/send_voice_message.py", - "app/agent/tools/impl/switch_persona.py", - "app/agent/tools/impl/test_site.py", - "app/agent/tools/impl/transfer_file.py", - "app/agent/tools/impl/uninstall_plugin.py", - "app/agent/tools/impl/update_agent_task.py", - "app/agent/tools/impl/update_custom_filter_rule.py", - "app/agent/tools/impl/update_custom_identifiers.py", - "app/agent/tools/impl/update_download_tasks.py", - "app/agent/tools/impl/update_persona_definition.py", - "app/agent/tools/impl/update_plugin_config.py", - "app/agent/tools/impl/update_rule_group.py", - "app/agent/tools/impl/update_site.py", - "app/agent/tools/impl/update_site_cookie.py", - "app/agent/tools/impl/update_subscribe.py", - "app/agent/tools/impl/update_system_settings.py", - "app/agent/tools/impl/write_file.py", - "app/api/openai_utils.py", - "app/api/router_specs.py", - "app/chain/_contracts.py", - "app/chain/_interaction.py", - "app/chain/_messaging.py", - "app/chain/_music.py", - "app/chain/_recognition.py", - "app/db/models/_constraints.py", - "app/db/models/_identity.py", - "app/modules/_base/media_auxiliary.py", - "app/modules/indexer/parser/file_list.py", - "app/modules/indexer/parser/ipt_project.py", - "app/modules/indexer/parser/nexus_audiences.py", - "app/modules/indexer/parser/nexus_hhanclub.py", - "app/modules/indexer/parser/nexus_php.py", - "app/modules/indexer/parser/nexus_project.py", - "app/modules/indexer/parser/nexus_rabbit.py", - "app/modules/indexer/parser/small_horse.py", - "app/modules/indexer/parser/torrent_leech.py", - "app/modules/musicbrainz/music_cache.py", - "app/modules/themoviedb/tmdb_cache.py", - "app/modules/themoviedb/tmdbv3api/as_obj.py", - "app/modules/thetvdb/tvdb_v4_official.py", - "app/runtime/compat/resource_imports.py", - "app/runtime/extensions/host_module_adapter.py", - "app/runtime/extensions/module_manager.py", - "app/runtime/extensions/plugin_manager.py", - "app/runtime/extensions/service_config.py", - "app/testing/network_guard.py", - "app/workflow/actions/add_download.py", - "app/workflow/actions/add_subscribe.py", - "app/workflow/actions/fetch_downloads.py", - "app/workflow/actions/fetch_medias.py", - "app/workflow/actions/fetch_rss.py", - "app/workflow/actions/fetch_torrents.py", - "app/workflow/actions/filter_medias.py", - "app/workflow/actions/filter_torrents.py", - "app/workflow/actions/invoke_plugin.py", - "app/workflow/actions/scan_file.py", - "app/workflow/actions/scrape_file.py", - "app/workflow/actions/send_event.py", - "app/workflow/actions/send_message.py", - "app/workflow/actions/transfer_file.py" + "package_root_exceptions": [ + { + "mode": "bootstrap", + "path": "app/__init__.py", + "reason": "进程入口在任何 app 子模块导入前安装平台警告和精确兼容导入钩子。" + }, + { + "mode": "public_facade", + "path": "app/adapters/system/display/__init__.py", + "reason": "DisplayHelper 是已验证插件 ABI,包根只保留延迟托管资源门面。" + }, + { + "mode": "public_facade", + "path": "app/db/__init__.py", + "reason": "数据库包根保留 Engine 特殊解析和生成的历史兼容导出表,宿主直接导入 owner 子模块。" + }, + { + "mode": "implementation", + "path": "app/agent/callback/__init__.py", + "reason": "StreamingHandler 仍是消息回调能力的既有 owner,后续拆分必须单独迁移状态和插件合同。" + }, + { + "mode": "implementation", + "path": "app/agent/capabilities/__init__.py", + "reason": "包根集中声明无实现副作用的 Agent capability 标识常量。" + }, + { + "mode": "implementation", + "path": "app/agent/memory/__init__.py", + "reason": "MemoryManager 是既有会话记忆 owner,当前包根不是对子模块的重复导出门面。" + }, + { + "mode": "public_facade", + "path": "app/agent/middleware/__init__.py", + "reason": "LangChain middleware 构建入口由包根集中公开,具体实现仍从单词子模块导入。" + }, + { + "mode": "public_facade", + "path": "app/agent/policy/__init__.py", + "reason": "策略包根按显式 __all__ 提供稳定懒加载 API。" + }, + { + "mode": "implementation", + "path": "app/agent/prompt/__init__.py", + "reason": "PromptManager 与系统任务定义仍共同拥有提示配置加载合同。" + }, + { + "mode": "public_facade", + "path": "app/agent/tools/__init__.py", + "reason": "工具包根只负责稳定 Tool 类型的延迟解析。" + }, + { + "mode": "implementation", + "path": "app/db/models/__init__.py", + "reason": "load_all_models 是数据库组合根显式加载全部 ORM 映射的唯一入口。" + }, + { + "mode": "public_facade", + "path": "app/db/oper/__init__.py", + "reason": "历史 Oper 包根符号由精确懒加载兼容表提供,宿主直接导入具体 oper。" + }, + { + "mode": "public_facade", + "path": "app/doctor/__init__.py", + "reason": "doctor 包根保留既有诊断 API 的精确懒加载表。" + }, + { + "mode": "implementation", + "path": "app/modules/__init__.py", + "reason": "ServiceBase 是模块发现框架的历史公共基类,具体模块子包另按 Module 入口合同校验。" + }, + { + "mode": "implementation", + "path": "app/modules/filemanager/storages/__init__.py", + "reason": "StorageBase 与 transfer_process 共同定义文件管理存储驱动合同。" + }, + { + "mode": "implementation", + "path": "app/modules/indexer/parser/__init__.py", + "reason": "SiteSchema 与 SiteParserBase 是按解析器文件名发现实现的框架合同。" + }, + { + "mode": "implementation", + "path": "app/modules/indexer/spider/__init__.py", + "reason": "SiteSpider 与分类投影函数共同定义站点 Spider 基础合同。" + }, + { + "mode": "contained_vendor", + "path": "app/modules/themoviedb/tmdbv3api/__init__.py", + "reason": "内含 tmdbv3api 兼容库维持其上游包根导出合同。" + }, + { + "mode": "contained_vendor", + "path": "app/modules/themoviedb/tmdbv3api/objs/__init__.py", + "reason": "内含 tmdbv3api 对象包维持上游结构,不作为宿主新模块模板。" + }, + { + "mode": "public_facade", + "path": "app/monitor/__init__.py", + "reason": "Monitor 包根保留稳定监控入口的显式懒加载表。" + }, + { + "mode": "public_facade", + "path": "app/runtime/capabilities/__init__.py", + "reason": "Capability Runtime 公共类型通过显式 __all__ 延迟解析。" + }, + { + "mode": "implementation", + "path": "app/runtime/observability/__init__.py", + "reason": "低基数观测合同与无操作实现共同组成一个轻量运行时 owner。" + }, + { + "mode": "public_facade", + "path": "app/schemas/__init__.py", + "reason": "Schemas 包根维持生成的插件兼容导出清单,宿主直接导入具体 schema。" + }, + { + "mode": "implementation", + "path": "app/startup/lifecycle/__init__.py", + "reason": "lifespan 与启动关闭步骤仍由同一组合根 owner 持有,并非子模块实现重导出。" + }, + { + "mode": "public_facade", + "path": "app/testing/__init__.py", + "reason": "测试支持包保留 stub_modules 的便捷公开入口,不参与生产运行时。" + }, + { + "mode": "implementation", + "path": "app/workflow/__init__.py", + "reason": "WorkflowManager 是历史工作流发现与执行 owner,拆分需保持运行时身份。" + }, + { + "mode": "implementation", + "path": "app/workflow/actions/__init__.py", + "reason": "BaseAction 与 ActionChain 定义按动作文件名发现的工作流扩展合同。" + } ], - "schema_version": 1, + "reviewed_multiword_files": [ + { + "evidence": "多条现有 Chain 直接导入该私有协议集合,迁移必须随 chain 同名包治理完成。", + "path": "app/chain/_contracts.py", + "reason": "跨既有 Chain 的私有运行协议尚未归入单一能力包。" + }, + { + "evidence": "MessageChain 与交互 Chain 共享该私有 mixin,当前没有公开或插件导入。", + "path": "app/chain/_interaction.py", + "reason": "历史 Chain 私有交互实现等待随 owner 包迁移。" + }, + { + "evidence": "多个消息 Chain 直接复用该私有 owner,不能仅改名制造另一个平级模块。", + "path": "app/chain/_messaging.py", + "reason": "历史 Chain 私有消息能力尚未完成同名包归属。" + }, + { + "evidence": "MusicChain 及搜索流程共享该私有实现,需在音乐 Chain 包治理时迁移。", + "path": "app/chain/_music.py", + "reason": "历史音乐 Chain 私有能力暂未形成独立 owner 包。" + }, + { + "evidence": "媒体识别同步异步状态机已收敛,但当前由多条 Chain 共用此私有模块。", + "path": "app/chain/_recognition.py", + "reason": "识别 Chain owner 迁移必须与完整调用边界一起完成。" + }, + { + "evidence": "所有 ORM 表约束从该私有模块集中引用,单独改名不能改善归属。", + "path": "app/db/models/_constraints.py", + "reason": "数据库模型包内共享约束集合是私有实现。" + }, + { + "evidence": "多个 ORM 模型共用媒体身份列定义,文件不构成平级业务能力。", + "path": "app/db/models/_identity.py", + "reason": "数据库模型包内共享身份列是私有实现。" + }, + { + "evidence": "路径属于内含 tmdbv3api 代码,包根已按 contained_vendor 单独登记。", + "path": "app/modules/themoviedb/tmdbv3api/as_obj.py", + "reason": "保留内含上游库的原始模块名以维持其内部导入。" + } + ], + "schema_version": 2, "scope": { "excluded": [ "app/plugins" @@ -155,5 +187,27 @@ "special": [ "__init__.py" ] - } + }, + "semantic_filename_contracts": [ + { + "kind": "agent_tool_contract", + "reason": "Agent 工具文件名就是向模型公开的稳定 tool name,必须与类内 name 字段逐项一致。", + "root": "app/agent/tools/impl" + }, + { + "kind": "agent_tool_private_helper", + "reason": "工具私有共享实现已归入 impl 能力包且以下划线隔离,不形成宿主公开模块。", + "root": "app/agent/tools/impl" + }, + { + "kind": "indexer_parser_contract", + "reason": "索引解析器文件名由 SiteUserInfo 类名和站点 schema 发现合同共同确定。", + "root": "app/modules/indexer/parser" + }, + { + "kind": "workflow_action_contract", + "reason": "工作流动作文件名是持久化动作类型的稳定发现标识,必须与 Action 类名逐项一致。", + "root": "app/workflow/actions" + } + ] } diff --git a/tests/test_agent_activity_log.py b/tests/test_agent_activity_log.py index c08f23088..0c7126db2 100644 --- a/tests/test_agent_activity_log.py +++ b/tests/test_agent_activity_log.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage -from app.agent.middleware.activity_log import ( +from app.agent.middleware.activity import ( ActivityLogMiddleware, QUERY_ACTIVITY_LOG_TOOL_DESCRIPTION, QUERY_ACTIVITY_LOG_TOOL_NAME, @@ -111,7 +111,7 @@ def test_activity_log_skips_trivial_greeting_without_llm(tmp_path): with ( patch( - "app.agent.middleware.activity_log._summarize_with_llm", + "app.agent.middleware.activity._summarize_with_llm", new=summarize_mock, ), patch.object(middleware, "_append_activity", new=append_mock), @@ -185,7 +185,7 @@ def test_activity_log_records_detailed_summary(tmp_path): async def _run_test(): middleware = ActivityLogMiddleware(activity_dir=str(tmp_path)) with patch( - "app.agent.middleware.activity_log._summarize_with_llm", + "app.agent.middleware.activity._summarize_with_llm", new=AsyncMock(return_value=summary), ): await middleware.aafter_agent( @@ -234,7 +234,7 @@ def test_activity_log_after_agent_does_not_wait_for_summary(tmp_path): append_mock = AsyncMock() with ( patch( - "app.agent.middleware.activity_log._summarize_with_llm", + "app.agent.middleware.activity._summarize_with_llm", side_effect=_slow_summarize, ) as summarize_mock, patch.object(middleware, "_append_activity", new=append_mock), @@ -479,7 +479,7 @@ def test_activity_log_middleware_sanitizes_its_own_logs(tmp_path): async def _failing_handler(_request): raise RuntimeError(f"Authorization: Bearer {secret_marker}") - with patch("app.agent.middleware.activity_log.logger", mock_logger): + with patch("app.agent.middleware.activity.logger", mock_logger): try: await middleware.awrap_tool_call(request, _failing_handler) except RuntimeError: @@ -503,10 +503,10 @@ def test_activity_log_provider_error_does_not_echo_secret(tmp_path): with ( patch( - "app.agent.middleware.activity_log.query_activity_logs", + "app.agent.middleware.activity.query_activity_logs", side_effect=RuntimeError(f"OPENAI_API_KEY={secret_marker}"), ), - patch("app.agent.middleware.activity_log.logger", mock_logger), + patch("app.agent.middleware.activity.logger", mock_logger), ): result = asyncio.run( middleware._tool_provider.query_activity_log(keyword="visible") diff --git a/tests/test_agent_background_output.py b/tests/test_agent_background_output.py index 87f4f1347..4031c88de 100644 --- a/tests/test_agent_background_output.py +++ b/tests/test_agent_background_output.py @@ -7,7 +7,7 @@ from langchain_core.messages import AIMessage, HumanMessage from app.agent.contracts import ReplyMode from app.agent.manager import AgentManager from app.agent.memory import memory_manager -from app.agent.middleware.activity_log import QUERY_ACTIVITY_LOG_TOOL_NAME +from app.agent.middleware.activity import QUERY_ACTIVITY_LOG_TOOL_NAME from app.agent.middleware.skills import SKILL_TOOL_NAME from app.agent.middleware.subagents import ( SUBAGENT_CONTROL_TOOL_NAME, diff --git a/tests/test_agent_lazy_runtime_boundary.py b/tests/test_agent_lazy_runtime_boundary.py index 7268087cc..1a27a4f30 100644 --- a/tests/test_agent_lazy_runtime_boundary.py +++ b/tests/test_agent_lazy_runtime_boundary.py @@ -145,7 +145,7 @@ from typing import get_args, get_type_hints from app.testing.bootstrap import ensure_sites_stub ensure_sites_stub() -from app.agent.runtime_loader import get_tool_factory +from app.agent.loader import get_tool_factory factory = get_tool_factory() catalog = factory.create_catalog(session_id="lazy", user_id="api") @@ -197,7 +197,7 @@ print(json.dumps({ def test_manager_first_catalog_use_is_single_flight(monkeypatch) -> None: """并发首次查询只能在 manager 锁内建立一次会话工具快照。""" - from app.agent import runtime_loader + from app.agent import loader as runtime_loader from app.agent.tools.manager import MoviePilotToolsManager calls: list[tuple[str, str]] = [] @@ -240,7 +240,7 @@ def test_legacy_explicit_tool_refresh_keeps_atomic_catalog_contract( monkeypatch, ) -> None: """插件显式刷新旧入口应继续发布同一次构造的完整目录快照。""" - from app.agent import runtime_loader + from app.agent import loader as runtime_loader from app.agent.tools.manager import MoviePilotToolsManager calls: list[int] = [] diff --git a/tests/test_agent_patch_tool_calls.py b/tests/test_agent_patch_tool_calls.py index 137681cb7..6da6bc562 100644 --- a/tests/test_agent_patch_tool_calls.py +++ b/tests/test_agent_patch_tool_calls.py @@ -3,7 +3,7 @@ import unittest from langchain_core.messages import AIMessage, HumanMessage, ToolMessage -from app.agent.middleware.patch_tool_calls import PatchToolCallsMiddleware +from app.agent.middleware.patching import PatchToolCallsMiddleware def _build_tool_call(tool_call_id: str = "call_1", name: str = "search") -> dict: diff --git a/tests/test_agent_request_budget.py b/tests/test_agent_request_budget.py index 47b5b1465..42af6c8a6 100644 --- a/tests/test_agent_request_budget.py +++ b/tests/test_agent_request_budget.py @@ -12,7 +12,7 @@ from langchain_core.messages import AIMessage, HumanMessage, SystemMessage from langchain_core.tools import tool from app.agent import MoviePilotAgent -from app.agent.middleware.tool_selection import ToolSelectorMiddleware +from app.agent.middleware.selection import ToolSelectorMiddleware from app.agent.middleware.usage import UsageMiddleware from app.chain.message import MessageChain diff --git a/tests/test_agent_runtime_loader.py b/tests/test_agent_runtime_loader.py index c2bc6ed35..7fff09a17 100644 --- a/tests/test_agent_runtime_loader.py +++ b/tests/test_agent_runtime_loader.py @@ -22,7 +22,7 @@ from app.runtime.capabilities.model import ( @pytest.fixture def runtime_loader(monkeypatch): """为每个用例提供未构建、未关闭的 Agent Capability Runtime。""" - from app.agent import runtime_loader as module + from app.agent import loader as module monkeypatch.setattr(module, "_agent_runtime", None) for implementation_module in ( diff --git a/tests/test_agent_scheduled_tasks.py b/tests/test_agent_scheduled_tasks.py index ece72fa78..d6413af4d 100644 --- a/tests/test_agent_scheduled_tasks.py +++ b/tests/test_agent_scheduled_tasks.py @@ -17,7 +17,7 @@ from langchain_core.messages import AIMessage from app.agent.contracts import ReplyMode from app.agent.manager import AgentManager -from app.agent.middleware.tool_selection import ToolSelectorMiddleware +from app.agent.middleware.selection import ToolSelectorMiddleware from app.agent.orchestrator import MoviePilotAgent from app.agent.session import _MessageTask from app.agent.tools.factory import MoviePilotToolFactory diff --git a/tests/test_agent_summarization_streaming.py b/tests/test_agent_summarization_streaming.py index a470f8796..c48c3b439 100644 --- a/tests/test_agent_summarization_streaming.py +++ b/tests/test_agent_summarization_streaming.py @@ -25,7 +25,7 @@ from pydantic import Field import app.agent.orchestrator as agent_module from app.agent.memory import MemoryManager -from app.agent.middleware.runtime_config import RuntimeConfigMiddleware +from app.agent.middleware.config import RuntimeConfigMiddleware from app.agent.middleware.summarization import ( ContextPreservingSummarizationMiddleware, ContextSummarizationError, diff --git a/tests/test_agent_tool_catalog.py b/tests/test_agent_tool_catalog.py index 1389b147d..f2ce5eccc 100644 --- a/tests/test_agent_tool_catalog.py +++ b/tests/test_agent_tool_catalog.py @@ -11,7 +11,7 @@ from app.agent.tools.catalog import ( ToolIdentityAmbiguousError, ) from app.agent.tools.factory import MoviePilotToolFactory -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.plugin.manager import PluginManager class _Arguments(BaseModel): diff --git a/tests/test_agent_tool_factory_cache.py b/tests/test_agent_tool_factory_cache.py index 12badc52c..69d100076 100644 --- a/tests/test_agent_tool_factory_cache.py +++ b/tests/test_agent_tool_factory_cache.py @@ -5,14 +5,14 @@ from unittest.mock import patch import pytest from pydantic import BaseModel -from app.agent.middleware.activity_log import QueryActivityLogInput +from app.agent.middleware.activity import QueryActivityLogInput from app.agent.middleware.skills import SkillToolInput from app.agent.tools.base import MoviePilotTool from app.agent.tools.factory import MoviePilotToolFactory from app.agent.tools.impl.ask_user_choice import AskUserChoiceInput, AskUserChoiceTool from app.agent.tools.impl.send_local_file import SendLocalFileTool from app.agent.tools.impl.send_voice_message import SendVoiceMessageTool -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.plugin.manager import PluginManager from app.foundation.singleton import Singleton diff --git a/tests/test_agent_tool_policy.py b/tests/test_agent_tool_policy.py index 86b01f53a..22396ead4 100644 --- a/tests/test_agent_tool_policy.py +++ b/tests/test_agent_tool_policy.py @@ -8,7 +8,7 @@ from langchain_core.messages import ToolMessage from pydantic import BaseModel, Field import app.agent.orchestrator as agent_module -from app.agent.middleware.activity_log import ActivityLogMiddleware +from app.agent.middleware.activity import ActivityLogMiddleware from app.agent.middleware.memory import MemoryMiddleware from app.agent.middleware.policy import AgentPolicyMiddleware from app.agent.middleware.summarization import FinalRequestCompactionMiddleware diff --git a/tests/test_agent_tool_selector_middleware.py b/tests/test_agent_tool_selector_middleware.py index c7718ee50..f0e9ac882 100644 --- a/tests/test_agent_tool_selector_middleware.py +++ b/tests/test_agent_tool_selector_middleware.py @@ -4,7 +4,7 @@ from unittest.mock import patch from langchain_core.messages import AIMessage, HumanMessage, SystemMessage -from app.agent.middleware import tool_selection as tool_selector_module +from app.agent.middleware import selection as tool_selector_module from app.agent.tools.tags import ToolTag diff --git a/tests/test_api_response.py b/tests/test_api_response.py index b5e08406b..285b97e38 100644 --- a/tests/test_api_response.py +++ b/tests/test_api_response.py @@ -51,7 +51,7 @@ class Item(BaseModel): def _v1_compat_routes() -> list[tuple[str, APIRoute]]: """返回兼容 v1 导出的公开路由,不依赖 FastAPI 内部 include 包装器。""" - from app.api.router_specs import API_V1_ROUTER_SPECS + from app.api.routers import API_V1_ROUTER_SPECS return [ (f"{spec.prefix}{route.path}", route) diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py index fe199f615..f115fccc4 100644 --- a/tests/test_architecture_dependencies.py +++ b/tests/test_architecture_dependencies.py @@ -79,6 +79,28 @@ RETIRED_CANONICAL_FILES = ( "app/runtime/runtime.py", "app/runtime/dependencies.py", "app/runtime/native_dependencies.py", + "app/agent/runtime_loader.py", + "app/agent/llm/server_tools.py", + "app/agent/middleware/activity_log.py", + "app/agent/middleware/patch_tool_calls.py", + "app/agent/middleware/runtime_config.py", + "app/agent/middleware/tool_selection.py", + "app/agent/policy/secret_fields.py", + "app/agent/prompt/transfer_redo.py", + "app/api/openai_utils.py", + "app/api/router_specs.py", + "app/adapters/external/wechat_crypt.py", + "app/modules/_base/media_auxiliary.py", + "app/modules/indexer/parser/ipt_project.py", + "app/modules/musicbrainz/music_cache.py", + "app/modules/themoviedb/tmdb_cache.py", + "app/modules/thetvdb/tvdb_v4_official.py", + "app/runtime/compat/resource_imports.py", + "app/runtime/extensions/host_module_adapter.py", + "app/runtime/extensions/module_manager.py", + "app/runtime/extensions/plugin_manager.py", + "app/runtime/extensions/service_config.py", + "app/testing/network_guard.py", "app/chain/media.py", "app/adapters/network/rss.py", "app/adapters/network/sites.pyi", @@ -169,22 +191,22 @@ FORBIDDEN_IMPORT_PREFIXES = { "app.sdk", ), "app.api": ( - "app.runtime.extensions.plugin_manager", - "app.runtime.extensions.module_manager", + "app.runtime.extensions.plugin.manager", + "app.runtime.extensions.module.manager", "app.scheduler", ), "app.agent": ( - "app.runtime.extensions.plugin_manager", - "app.runtime.extensions.module_manager", + "app.runtime.extensions.plugin.manager", + "app.runtime.extensions.module.manager", ), "app.chain": ( - "app.runtime.extensions.plugin_manager", - "app.runtime.extensions.module_manager", + "app.runtime.extensions.plugin.manager", + "app.runtime.extensions.module.manager", "app.runtime.extensions.module.dispatcher", ), "app.workflow": ( - "app.runtime.extensions.plugin_manager", - "app.runtime.extensions.module_manager", + "app.runtime.extensions.plugin.manager", + "app.runtime.extensions.module.manager", ), } @@ -1387,7 +1409,7 @@ def test_canonical_service_config_consumers_use_application_directory(): for path in paths: tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) for node in ast.walk(tree): - if isinstance(node, ast.ImportFrom) and node.module == "app.runtime.extensions.service_config": + if isinstance(node, ast.ImportFrom) and node.module == "app.runtime.extensions.service": violations.append(f"{path.relative_to(PROJECT_ROOT).as_posix()}:{node.lineno}") if ( path.name == "mediaserver.py" @@ -1401,8 +1423,9 @@ def test_canonical_service_config_consumers_use_application_directory(): def test_plugin_components_do_not_reexport_legacy_abi_names(): - """新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。""" + """插件组件只在 manager owner 定义 PluginManager,不得复制旧 ABI。""" violations: list[str] = [] + manager_path = APP_ROOT / "runtime" / "extensions" / "plugin" / "manager.py" for root in PLUGIN_COMPONENT_ROOTS: for path in (PROJECT_ROOT / root).rglob("*.py"): tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) @@ -1411,7 +1434,11 @@ def test_plugin_components_do_not_reexport_legacy_abi_names(): if node.name == "__getattr__" or node.name in PLUGIN_LEGACY_ABI_NAMES: violations.append(f"{path.relative_to(PROJECT_ROOT)}:{node.name}") elif isinstance(node, ast.ClassDef): - if node.name in PLUGIN_LEGACY_ABI_NAMES or node.name.endswith("Oper"): + is_canonical_manager = path == manager_path and node.name == "PluginManager" + if ( + not is_canonical_manager + and (node.name in PLUGIN_LEGACY_ABI_NAMES or node.name.endswith("Oper")) + ): violations.append(f"{path.relative_to(PROJECT_ROOT)}:{node.name}") elif isinstance(node, ast.ImportFrom): for alias in node.names: @@ -1990,7 +2017,7 @@ def test_host_consumers_get_agent_manager_through_application_facade(): imported = { alias.name for node in ast.walk(tree) - if isinstance(node, ast.ImportFrom) and node.module == "app.agent.runtime_loader" + if isinstance(node, ast.ImportFrom) and node.module == "app.agent.loader" for alias in node.names if alias.name in forbidden } diff --git a/tests/test_architecture_egress.py b/tests/test_architecture_egress.py index 4a2f21c64..0239b9c20 100644 --- a/tests/test_architecture_egress.py +++ b/tests/test_architecture_egress.py @@ -82,7 +82,7 @@ FROZEN_EGRESS_EDGES_BY_REASON = { ("app.runtime.state", "docker"), }, "test_network_guard": { - ("app.testing.network_guard", "socket.getaddrinfo"), + ("app.testing.network", "socket.getaddrinfo"), }, } FROZEN_EGRESS_REASON_BY_EDGE = { @@ -143,7 +143,7 @@ FROZEN_EGRESS_FINGERPRINT_BY_EDGE = { ("app.modules.zspace.zspace", "requests"): "9df3fd27b9696d45a72e7c8f67b5a9ad79a7371d1fe690bbaa17485bd1960d51", ("app.runtime.state", "docker"): "20a91ec521f7dfe6a0153dfd8ea49c4bac7f0a16b55f1c0655dfb33f54a01215", ("app.startup.lifecycle", "urllib3"): "cb6f0a314aeb1e2d3e76c240aa20460ac0c36d9f5c18c1a6ea3170f64dd3366b", - ("app.testing.network_guard", "socket.getaddrinfo"): "2518de211c9ba32ccbc004cf58fe9c98837fe8ac95fe8ffb6ea82897b28d753f", + ("app.testing.network", "socket.getaddrinfo"): "2518de211c9ba32ccbc004cf58fe9c98837fe8ac95fe8ffb6ea82897b28d753f", } diff --git a/tests/test_delete_transfer_history_tool.py b/tests/test_delete_transfer_history_tool.py index 0b6aa0164..3fb3570e7 100644 --- a/tests/test_delete_transfer_history_tool.py +++ b/tests/test_delete_transfer_history_tool.py @@ -2,7 +2,7 @@ import asyncio import threading from types import SimpleNamespace -from app.agent.prompt.transfer_redo import build_manual_redo_template_context +from app.agent.prompt.transfer import build_manual_redo_template_context from app.agent.tools.impl.delete_transfer_history import DeleteTransferHistoryTool diff --git a/tests/test_downloader_path_mapping.py b/tests/test_downloader_path_mapping.py index 45a4ad069..f2edf878b 100644 --- a/tests/test_downloader_path_mapping.py +++ b/tests/test_downloader_path_mapping.py @@ -17,7 +17,7 @@ def _load_downloader_base(): runtime_module.__path__ = [] runtime_extensions_module = types.ModuleType("app.runtime.extensions") runtime_extensions_module.__path__ = [] - service_module = types.ModuleType("app.runtime.extensions.service_config") + service_module = types.ModuleType("app.runtime.extensions.service") log_module = types.ModuleType("app.runtime.log") schemas_module = types.ModuleType("app.schemas") schemas_module.__path__ = [] @@ -100,7 +100,7 @@ def _load_downloader_base(): "app.helper": helper_module, "app.runtime": runtime_module, "app.runtime.extensions": runtime_extensions_module, - "app.runtime.extensions.service_config": service_module, + "app.runtime.extensions.service": service_module, "app.runtime.log": log_module, "app.schemas": schemas_module, "app.schemas.message": schema_message_module, diff --git a/tests/test_event_plugin_errors.py b/tests/test_event_plugin_errors.py index 6e56e5d73..476fabaad 100644 --- a/tests/test_event_plugin_errors.py +++ b/tests/test_event_plugin_errors.py @@ -3,7 +3,7 @@ import pytest from app.runtime.events import Event, EventManager -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.plugin.manager import PluginManager from app.schemas.types import ChainEventType diff --git a/tests/test_history_batch_ai_redo_prompt.py b/tests/test_history_batch_ai_redo_prompt.py index ccb15d409..b5b069307 100644 --- a/tests/test_history_batch_ai_redo_prompt.py +++ b/tests/test_history_batch_ai_redo_prompt.py @@ -1,7 +1,7 @@ from types import SimpleNamespace from app.agent.prompt import prompt_manager -from app.agent.prompt.transfer_redo import build_batch_manual_redo_prompt +from app.agent.prompt.transfer import build_batch_manual_redo_prompt def test_batch_manual_redo_prompt_requires_plain_text_result(): diff --git a/tests/test_legacy_plugin_resource_imports.py b/tests/test_legacy_plugin_resource_imports.py index 00d6e6595..6834bf0ee 100644 --- a/tests/test_legacy_plugin_resource_imports.py +++ b/tests/test_legacy_plugin_resource_imports.py @@ -9,8 +9,8 @@ from types import ModuleType import pytest -from app.runtime.compat import resource_imports -from app.runtime.compat.resource_imports import ( +from app.runtime.compat import resources as resource_imports +from app.runtime.compat.resources import ( RESOURCE_IMPORT_RULES, PluginResourceImportScanError, scan_plugin_resource_imports, diff --git a/tests/test_lifecycle_provider_resets.py b/tests/test_lifecycle_provider_resets.py index ff55850a8..a6c554cba 100644 --- a/tests/test_lifecycle_provider_resets.py +++ b/tests/test_lifecycle_provider_resets.py @@ -110,8 +110,8 @@ from app.application.workflow import ( get_configured_workflow_execution, reset_workflow_execution, ) -from app.runtime.extensions import service_config as service_config_module -from app.runtime.extensions.service_config import ( +from app.runtime.extensions import service as service_config_module +from app.runtime.extensions.service import ( configure_service_config_reader, reset_service_config_reader, ) diff --git a/tests/test_llm_server_tools.py b/tests/test_llm_server_tools.py index 7aad4ea37..a9862a211 100644 --- a/tests/test_llm_server_tools.py +++ b/tests/test_llm_server_tools.py @@ -7,7 +7,7 @@ import pytest from app.agent.llm import LLMHelper from app.agent.llm.provider import LLMProviderManager -from app.agent.llm.server_tools import ( +from app.agent.llm.tools import ( ServerToolRegistry, ServerToolUnavailableError, ) diff --git a/tests/test_mcp_plugin_tools.py b/tests/test_mcp_plugin_tools.py index 30c7579f1..7b67b9265 100644 --- a/tests/test_mcp_plugin_tools.py +++ b/tests/test_mcp_plugin_tools.py @@ -11,7 +11,7 @@ from app.agent.tools.factory import MoviePilotToolFactory from app.agent.tools.manager import MoviePilotToolsManager from app.agent.tools.catalog import ToolCatalogSnapshot from app.api.endpoints import mcp -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.plugin.manager import PluginManager from app.foundation.singleton import Singleton diff --git a/tests/test_media_auxiliary.py b/tests/test_media_auxiliary.py index 77cf85a1b..58dc0358d 100644 --- a/tests/test_media_auxiliary.py +++ b/tests/test_media_auxiliary.py @@ -6,7 +6,7 @@ from unittest.mock import patch from app.chain.search import SearchChain from app.domain.context import MediaInfo -from app.modules._base.media_auxiliary import MediaAuxiliaryProviderMixin +from app.modules._base.media import MediaAuxiliaryProviderMixin from app.schemas.types import MediaSource, MediaType diff --git a/tests/test_mediaserver_conf_sync_interval.py b/tests/test_mediaserver_conf_sync_interval.py index 9785f98c0..fbf7ba085 100644 --- a/tests/test_mediaserver_conf_sync_interval.py +++ b/tests/test_mediaserver_conf_sync_interval.py @@ -1,4 +1,4 @@ -from app.runtime.extensions.service_config import ServiceConfigHelper +from app.runtime.extensions.service import ServiceConfigHelper from app.schemas.system import MediaServerConf from app.schemas.types import SystemConfigKey @@ -16,7 +16,7 @@ def test_mediaserver_conf_tolerates_blank_sync_interval(): def test_get_configs_skips_invalid_entries(monkeypatch): """单条配置校验失败时应跳过该条,不影响其它服务配置的加载。""" monkeypatch.setattr( - "app.runtime.extensions.service_config._service_config_reader", + "app.runtime.extensions.service._service_config_reader", lambda key: [ {"name": "good", "type": "emby", "enabled": True}, "bad-format", diff --git a/tests/test_module_manager_capability_adapter.py b/tests/test_module_manager_capability_adapter.py index 056c67228..ad33fdb08 100644 --- a/tests/test_module_manager_capability_adapter.py +++ b/tests/test_module_manager_capability_adapter.py @@ -19,9 +19,9 @@ from app.runtime.capabilities.errors import CapabilityRuntimeClosedError from app.runtime.capabilities.model import CapabilityLifecycleState, SelectorSchema from app.runtime.capabilities.registry import CapabilityRegistry from app.runtime.events import Event, EventHandlerBinding, eventmanager -from app.runtime.extensions import module_manager as module_manager_extension -from app.runtime.extensions.module_manager import ModuleManager -from app.runtime.extensions.service_config import configure_service_config_reader +from app.runtime.extensions.module import manager as module_manager_extension +from app.runtime.extensions.module.manager import ModuleManager +from app.runtime.extensions.service import configure_service_config_reader from app.schemas.event import ConfigChangeEventData from app.schemas.types import EventType @@ -551,7 +551,7 @@ def test_all_real_host_modules_zero_arg_construct_without_starting_resources( ) -> None: """每份真实 manifest 都必须能解析 canonical class 并零参数构造且不启动资源。""" body = r""" -from app.runtime.extensions.host_module_adapter import ( +from app.runtime.extensions.module.adapter import ( HostModuleAdapter, build_host_module_registry, ) @@ -595,11 +595,11 @@ from app.db.oper.systemconfig import SystemConfigOper from app.runtime.capabilities.model import ActivationPolicy from app.runtime.config import settings from app.runtime.events import Event -from app.runtime.extensions.host_module_adapter import ( +from app.runtime.extensions.module.adapter import ( HostModuleAdapter, build_host_module_registry, ) -from app.runtime.extensions.service_config import configure_service_config_reader +from app.runtime.extensions.service import configure_service_config_reader from app.schemas.event import ConfigChangeEventData from app.schemas.types import EventType @@ -655,7 +655,7 @@ def get_config(_self, key=None): SystemConfigOper.get = get_config configure_service_config_reader(lambda key: SystemConfigOper().get(key)) -from app.runtime.extensions.module_manager import ModuleManager +from app.runtime.extensions.module.manager import ModuleManager manager = ModuleManager() bootstrap_ids = { @@ -765,7 +765,7 @@ def test_default_config_keeps_every_manifest_configured_entrypoint_unimported( from app.db.oper.systemconfig import SystemConfigOper from app.runtime.capabilities.model import ActivationPolicy from app.runtime.config import settings -from app.runtime.extensions.host_module_adapter import ( +from app.runtime.extensions.module.adapter import ( HostModuleAdapter, build_host_module_registry, ) @@ -802,7 +802,7 @@ for spec in specs: assert configured_modules.isdisjoint(sys.modules) -from app.runtime.extensions.module_manager import ModuleManager +from app.runtime.extensions.module.manager import ModuleManager manager = ModuleManager() assert manager.get_specs() == manager.list_specs() @@ -862,7 +862,7 @@ SystemConfigOper.get = empty_config settings.ACOUSTID_API_KEY = None settings.FANART_API_KEY = None -from app.runtime.extensions.module_manager import ModuleManager +from app.runtime.extensions.module.manager import ModuleManager from app.application.module import configure_module_runtime configure_module_runtime(lambda: ModuleManager()) @@ -978,7 +978,7 @@ from app.runtime.config import settings settings.ACOUSTID_API_KEY = None settings.FANART_API_KEY = None -from app.runtime.extensions.module_manager import ModuleManager +from app.runtime.extensions.module.manager import ModuleManager manager = ModuleManager() modules = manager.get_modules() diff --git a/tests/test_music_endpoint.py b/tests/test_music_endpoint.py index bfea1fbe9..0f52fee50 100644 --- a/tests/test_music_endpoint.py +++ b/tests/test_music_endpoint.py @@ -21,7 +21,7 @@ from app.schemas.types import MediaSource, MediaType def test_music_routes_are_registered(): """V1 API 应注册音乐详情识别、探索及艺术家专辑浏览路由。""" - from app.api.router_specs import API_V1_ROUTER_SPECS + from app.api.routers import API_V1_ROUTER_SPECS routes = { (f"{spec.prefix}{route.path}", tuple(route.methods or [])) diff --git a/tests/test_music_recognize_cache.py b/tests/test_music_recognize_cache.py index b86d13544..afc525fca 100644 --- a/tests/test_music_recognize_cache.py +++ b/tests/test_music_recognize_cache.py @@ -12,9 +12,9 @@ from app.api.endpoints import music as music_endpoint from app.domain.context import MusicInfo from app.domain.meta.metamusic import MetaMusic from app.api.deps import get_current_active_superuser_async -from app.modules.musicbrainz import music_cache as music_cache_module +from app.modules.musicbrainz import cache as music_cache_module from app.modules.musicbrainz import MusicBrainzModule -from app.modules.musicbrainz.music_cache import MusicBrainzCache +from app.modules.musicbrainz.cache import MusicBrainzCache class _MemoryCacheStub: diff --git a/tests/test_navidrome_module.py b/tests/test_navidrome_module.py index e6c0ea572..e6f31f8fe 100644 --- a/tests/test_navidrome_module.py +++ b/tests/test_navidrome_module.py @@ -2,7 +2,7 @@ from unittest.mock import Mock from app import schemas -from app.runtime.extensions.module_manager import ModuleManager +from app.runtime.extensions.module.manager import ModuleManager from app.domain.context import MusicInfo from app.modules.navidrome import NavidromeModule from app.modules.navidrome.navidrome import Navidrome diff --git a/tests/test_network_guard.py b/tests/test_network_guard.py index cc1ed7526..979f6503f 100644 --- a/tests/test_network_guard.py +++ b/tests/test_network_guard.py @@ -2,7 +2,7 @@ import socket import pytest -from app.testing.network_guard import block_real_network +from app.testing.network import block_real_network def test_network_guard_fails_when_blocked_attempt_is_swallowed(monkeypatch): diff --git a/tests/test_observability.py b/tests/test_observability.py index 72769e09b..73c6a899f 100644 --- a/tests/test_observability.py +++ b/tests/test_observability.py @@ -17,7 +17,7 @@ from app.adapters.external.server import MoviePilotServerHelper from app.adapters.web.metrics import HttpMetricsMiddleware from app.db.engine import _register_database_pool_metrics from app.runtime.extensions.plugin.lifecycle import observe_plugin_lifecycle -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.plugin.manager import PluginManager from app.schemas.plugin import PluginRuntimeStatus from app.runtime.observability import ( METRIC_SPECS, diff --git a/tests/test_openai_utils.py b/tests/test_openai_utils.py index b9730c78d..d17b99f51 100644 --- a/tests/test_openai_utils.py +++ b/tests/test_openai_utils.py @@ -1,6 +1,6 @@ from unittest import TestCase -from app.api.openai_utils import ( +from app.api.protocol import ( build_anthropic_messages, build_completion_payload, build_prompt, diff --git a/tests/test_plugin_compat_governance.py b/tests/test_plugin_compat_governance.py index 0b5b459cd..0ade44339 100644 --- a/tests/test_plugin_compat_governance.py +++ b/tests/test_plugin_compat_governance.py @@ -71,6 +71,90 @@ def _public_names(tree: ast.AST) -> set[str]: return {name for name in names if not name.startswith("_")} +def _camel_to_snake(value: str) -> str: + """把合同类名转换为对应的稳定模块标识。""" + first_pass = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", value) + return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", first_pass).lower() + + +def _matches_filename_contract(path: Path, rule: dict[str, str]) -> bool: + """按真实发现合同判断多词文件是否属于有理由的语义命名。""" + relative = path.relative_to(PROJECT_ROOT).as_posix() + root = rule["root"].rstrip("/") + if not relative.startswith(f"{root}/"): + return False + kind = rule["kind"] + if kind == "agent_tool_private_helper": + return path.stem.startswith("_") + + tree = _parse(path) + class_names = {node.name for node in tree.body if isinstance(node, ast.ClassDef)} + if kind == "agent_tool_contract": + expected_name = path.stem + for node in tree.body: + if not isinstance(node, ast.ClassDef): + continue + for statement in node.body: + if not isinstance(statement, ast.AnnAssign): + continue + if not isinstance(statement.target, ast.Name) or statement.target.id != "name": + continue + if isinstance(statement.value, ast.Constant) and statement.value.value == expected_name: + return True + return False + if kind == "workflow_action_contract": + return any( + name.endswith("Action") + and _camel_to_snake(name.removesuffix("Action")) == path.stem + for name in class_names + ) + if kind == "indexer_parser_contract": + if any( + name.endswith("SiteUserInfo") + and _camel_to_snake(name.removesuffix("SiteUserInfo")) == path.stem + for name in class_names + ): + return True + return any( + isinstance(statement, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "schema" for target in statement.targets) + and isinstance(statement.value, ast.Attribute) + and _camel_to_snake(statement.value.attr) == path.stem + for node in tree.body + if isinstance(node, ast.ClassDef) + for statement in node.body + ) + raise AssertionError(f"未知文件命名合同:{kind}") + + +def _is_documentation_only_root(tree: ast.Module) -> bool: + """判断包根是否只保留包说明。""" + return not tree.body or (ast.get_docstring(tree) is not None and len(tree.body) == 1) + + +def _is_curated_lazy_facade(tree: ast.Module) -> bool: + """判断包根是否为显式白名单驱动且不含公开实现的懒门面。""" + assignments = { + target.id: node.value + for node in tree.body + if isinstance(node, ast.Assign) + for target in node.targets + if isinstance(target, ast.Name) and target.id in {"_EXPORTS", "__all__"} + } + if set(assignments) != {"_EXPORTS", "__all__"}: + return False + if any( + isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) + and not node.name.startswith("_") + for node in tree.body + ): + return False + exports = ast.literal_eval(assignments["_EXPORTS"]) + public = ast.literal_eval(assignments["__all__"]) + export_names = set(exports) if isinstance(exports, dict) else set(exports) + return export_names == set(public) + + def test_official_plugin_legacy_imports_are_exactly_routed_or_retired() -> None: """官方插件旧导入必须有精确 Compat 路由,或保持一条已审查的可选退役路径。""" baseline = json.loads(OFFICIAL_PLUGIN_BASELINE.read_text(encoding="utf-8")) @@ -90,7 +174,7 @@ def test_official_plugin_legacy_imports_are_exactly_routed_or_retired() -> None: def test_plugin_manager_legacy_and_sdk_paths_share_canonical_identity() -> None: """V2 旧路径与 V3 SDK 必须复用同一个 PluginManager 身份及已验证成员。""" - canonical = importlib.import_module("app.runtime.extensions.plugin_manager") + canonical = importlib.import_module("app.runtime.extensions.plugin.manager") legacy = importlib.import_module("app.core.plugin") sdk = importlib.import_module("app.sdk.plugins") @@ -121,7 +205,7 @@ def test_plugin_entry_and_application_do_not_reimport_concrete_legacy_owners() - forbidden_modules = { "app.adapters.external.market", "app.adapters.external.server", - "app.runtime.extensions.plugin_manager", + "app.runtime.extensions.plugin.manager", } forbidden_names = {"MoviePilotServerHelper", "PluginHelper", "PluginManager"} paths = [APP_ROOT / "api" / "endpoints" / "plugin.py"] @@ -147,7 +231,7 @@ def test_plugin_entry_and_application_do_not_reimport_concrete_legacy_owners() - def test_plugin_manager_catalog_compat_methods_remain_thin_delegates() -> None: """PluginManager 的旧市场目录方法只能委托,不得重新吸收应用目录算法。""" - path = APP_ROOT / "runtime" / "extensions" / "plugin_manager.py" + path = APP_ROOT / "runtime" / "extensions" / "plugin" / "manager.py" tree = _parse(path) manager = next( node @@ -194,9 +278,9 @@ def test_plugin_capability_package_roots_do_not_duplicate_exports() -> None: def test_new_host_production_modules_use_single_word_filenames() -> None: - """宿主多词生产文件必须等于审查低水位,新增文件只能使用单个小写词。""" + """宿主多词文件必须满足稳定发现合同或带有可审查的逐项理由。""" policy = json.loads(FILENAME_POLICY.read_text(encoding="utf-8")) - assert policy["schema_version"] == 1 + assert policy["schema_version"] == 2 assert policy["scope"] == { "root": "app", "excluded": ["app/plugins"], @@ -204,11 +288,62 @@ def test_new_host_production_modules_use_single_word_filenames() -> None: } single_word = re.compile(r"[a-z][a-z0-9]*") actual = sorted( - path.relative_to(PROJECT_ROOT).as_posix() + path for path in APP_ROOT.rglob("*.py") if not path.is_relative_to(APP_ROOT / "plugins") and path.name != "__init__.py" and not single_word.fullmatch(path.stem) ) - assert actual == policy["grandfathered_multiword_files"] + contracts = policy["semantic_filename_contracts"] + reviewed = {item["path"]: item for item in policy["reviewed_multiword_files"]} + assert all(item["reason"].strip() for item in contracts) + assert all(item["reason"].strip() and item["evidence"].strip() for item in reviewed.values()) + + unmatched = [] + contract_paths = set() + for path in actual: + relative = path.relative_to(PROJECT_ROOT).as_posix() + if any(_matches_filename_contract(path, rule) for rule in contracts): + contract_paths.add(relative) + elif relative not in reviewed: + unmatched.append(relative) + + assert unmatched == [] + assert set(reviewed) == { + path.relative_to(PROJECT_ROOT).as_posix() for path in actual + } - contract_paths + + +def test_package_roots_are_documentation_facades_or_reasoned_owners() -> None: + """全局包根只能为空、说明、精确懒门面、模块入口或有理由的既有 owner。""" + policy = json.loads(FILENAME_POLICY.read_text(encoding="utf-8")) + exceptions = {item["path"]: item for item in policy["package_root_exceptions"]} + assert all( + item["mode"] in {"bootstrap", "implementation", "public_facade", "contained_vendor"} + and item["reason"].strip() + for item in exceptions.values() + ) + + used_exceptions: set[str] = set() + violations: list[str] = [] + modules_root = APP_ROOT / "modules" + for path in APP_ROOT.rglob("__init__.py"): + if path.is_relative_to(APP_ROOT / "plugins"): + continue + relative = path.relative_to(PROJECT_ROOT).as_posix() + if relative in exceptions: + used_exceptions.add(relative) + continue + tree = _parse(path) + if _is_documentation_only_root(tree) or _is_curated_lazy_facade(tree): + continue + if path.parent.parent == modules_root and any( + isinstance(node, ast.ClassDef) and node.name.endswith("Module") + for node in tree.body + ): + continue + violations.append(relative) + + assert violations == [] + assert used_exceptions == set(exceptions) diff --git a/tests/test_plugin_dashboard.py b/tests/test_plugin_dashboard.py index ccca0e675..3b23fe33d 100644 --- a/tests/test_plugin_dashboard.py +++ b/tests/test_plugin_dashboard.py @@ -2,7 +2,7 @@ from types import SimpleNamespace from typing import Any, Iterator import pytest -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.plugin.manager import PluginManager from app.runtime.extensions.plugin.contracts import PluginDashboardError from app.foundation.singleton import Singleton diff --git a/tests/test_plugin_endpoint.py b/tests/test_plugin_endpoint.py index 45b02c690..38ba85b15 100644 --- a/tests/test_plugin_endpoint.py +++ b/tests/test_plugin_endpoint.py @@ -31,7 +31,7 @@ from app.application.plugin.release import PluginReleaseService from app.foundation.singleton import Singleton from app.runtime.config import settings from app.runtime.extensions.plugin.admission import PluginMutationAdmission -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.plugin.manager import PluginManager from app.runtime.tasks import TaskRegistry from app.schemas.event import PluginDataResetEventData from app.schemas.plugin import PluginInstance, PluginRuntimeStatus diff --git a/tests/test_plugin_helper.py b/tests/test_plugin_helper.py index 416dcc9cb..bd465ea71 100644 --- a/tests/test_plugin_helper.py +++ b/tests/test_plugin_helper.py @@ -55,7 +55,7 @@ def _configure_plugin_catalog_factory(monkeypatch): from app.adapters.external.plugin.client import PluginMarketClient from app.application.plugin.catalog import PluginCatalogService from app.foundation.version import compare_version - from app.runtime.extensions import plugin_manager as manager_module + from app.runtime.extensions.plugin import manager as manager_module from app.runtime.extensions.plugin.storage import get_plugin_storage from app.schemas.types import SystemConfigKey original_runtime_setting = plugin_client_module.get_runtime_setting @@ -836,7 +836,7 @@ class TestPluginHelper: 插件市场 labels 为列表时应转换为字符串,避免响应模型序列化异常。 """ try: - from app.runtime.extensions.plugin_manager import PluginManager + from app.runtime.extensions.plugin.manager import PluginManager except ModuleNotFoundError as exc: pytest.skip(f"missing dependency: {exc}") @@ -858,7 +858,7 @@ class TestPluginHelper: SimpleNamespace(read=lambda _key: []), ) monkeypatch.setattr( - "app.runtime.extensions.plugin_manager._site_auth_level_provider", + "app.runtime.extensions.plugin.manager._site_auth_level_provider", lambda: 1, ) monkeypatch.setattr( @@ -879,7 +879,7 @@ class TestPluginHelper: package.v2.json 中的 v2 原生插件,并过滤掉未声明任何版本兼容的 v1 插件。 """ try: - from app.runtime.extensions.plugin_manager import PluginManager + from app.runtime.extensions.plugin.manager import PluginManager except ModuleNotFoundError as exc: pytest.skip(f"missing dependency: {exc}") @@ -930,7 +930,7 @@ class TestPluginHelper: SimpleNamespace(read=lambda _key: []), ) monkeypatch.setattr( - "app.runtime.extensions.plugin_manager._site_auth_level_provider", + "app.runtime.extensions.plugin.manager._site_auth_level_provider", lambda: 1, ) monkeypatch.setattr(PluginMarketTransport, "get_plugins", fake_get_plugins) @@ -1092,7 +1092,7 @@ class TestPluginHelper: 全市场刷新不清理 Release 缓存,Release 接口按请求仓库协调刷新两类数据。 """ try: - from app.runtime.extensions.plugin_manager import PluginManager + from app.runtime.extensions.plugin.manager import PluginManager except ModuleNotFoundError as exc: pytest.skip(f"missing dependency: {exc}") @@ -1111,7 +1111,7 @@ class TestPluginHelper: def test_async_get_online_plugins_force_keeps_release_cache_scoped(self, monkeypatch): """异步全市场刷新同样不得清理其他仓库的 Release 缓存。""" try: - from app.runtime.extensions.plugin_manager import PluginManager + from app.runtime.extensions.plugin.manager import PluginManager except ModuleNotFoundError as exc: pytest.skip(f"missing dependency: {exc}") @@ -1137,7 +1137,7 @@ class TestPluginHelper: def test_get_local_plugin_version_reads_only_requested_installed_plugin(self, monkeypatch): """单插件版本查询不构建全部本地插件信息。""" try: - from app.runtime.extensions.plugin_manager import PluginManager + from app.runtime.extensions.plugin.manager import PluginManager from app.schemas.types import SystemConfigKey except ModuleNotFoundError as exc: pytest.skip(f"missing dependency: {exc}") diff --git a/tests/test_plugin_local_sync.py b/tests/test_plugin_local_sync.py index 28aff5349..6f8a6b5c2 100644 --- a/tests/test_plugin_local_sync.py +++ b/tests/test_plugin_local_sync.py @@ -13,7 +13,7 @@ from app.foundation.singleton import Singleton from app.runtime.events import Event, eventmanager from app.runtime.extensions.plugin.paths import PluginPathResolver from app.runtime.extensions.plugin.system import get_plugin_system -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.plugin.manager import PluginManager from app.scheduler import reconcile as scheduler_reconcile from app.scheduler.facade import Scheduler from app.scheduler.registry import ExecutionRegistry @@ -102,7 +102,7 @@ def _build_local_plugin_repo(tmp_path: Path) -> tuple[Path, Path]: def _patch_plugin_runtime_settings(monkeypatch, settings) -> None: """以只读键值端口注入插件运行配置。""" for target in ( - "app.runtime.extensions.plugin_manager.get_runtime_setting", + "app.runtime.extensions.plugin.manager.get_runtime_setting", "app.adapters.external.plugin.client.get_runtime_setting", ): monkeypatch.setattr( @@ -143,7 +143,7 @@ def _configure_local_watcher( ) _patch_plugin_runtime_settings(monkeypatch, settings_stub) _patch_package_runtime_settings(monkeypatch, settings_stub) - monkeypatch.setattr("app.runtime.extensions.plugin_manager.watch", lambda *_args, **_kwargs: iter([changes])) + monkeypatch.setattr("app.runtime.extensions.plugin.manager.watch", lambda *_args, **_kwargs: iter([changes])) def _set_running_render_mode( @@ -427,7 +427,7 @@ def test_local_federated_asset_reads_running_render_mode_for_each_batch( ) _patch_plugin_runtime_settings(monkeypatch, settings_stub) monkeypatch.setattr( - "app.runtime.extensions.plugin_manager.watch", + "app.runtime.extensions.plugin.manager.watch", lambda *_args, **_kwargs: iter([ {(Change.modified, str(source_dir / "dist" / "assets" / "remoteEntry.js"))}, {(Change.modified, str(next_entry))}, @@ -533,7 +533,7 @@ def test_local_requirements_change_still_does_not_sync_or_reload( monkeypatch.setattr(plugin_manager, "_sync_local_plugin_if_installed", sync_spy) monkeypatch.setattr(plugin_manager, "reload_plugin", reload_spy) log = Mock() - monkeypatch.setattr("app.runtime.extensions.plugin_manager.logger", log) + monkeypatch.setattr("app.runtime.extensions.plugin.manager.logger", log) plugin_manager._run_file_watcher() @@ -566,7 +566,7 @@ def test_local_pyproject_change_prompts_reinstall_without_sync_or_reload( monkeypatch.setattr(plugin_manager, "_sync_local_plugin_if_installed", sync_spy) monkeypatch.setattr(plugin_manager, "reload_plugin", reload_spy) log = Mock() - monkeypatch.setattr("app.runtime.extensions.plugin_manager.logger", log) + monkeypatch.setattr("app.runtime.extensions.plugin.manager.logger", log) plugin_manager._run_file_watcher() @@ -600,7 +600,7 @@ def test_local_inactive_requirements_change_is_debug_only( monkeypatch.setattr(plugin_manager, "_sync_local_plugin_if_installed", sync_spy) monkeypatch.setattr(plugin_manager, "reload_plugin", reload_spy) log = Mock() - monkeypatch.setattr("app.runtime.extensions.plugin_manager.logger", log) + monkeypatch.setattr("app.runtime.extensions.plugin.manager.logger", log) plugin_manager._run_file_watcher() @@ -634,7 +634,7 @@ def test_deleting_active_pyproject_prompts_for_requirements_takeover( ) monkeypatch.setattr(PluginMarketTransport, "get_current_system_version", lambda: Version("2.13.11")) log = Mock() - monkeypatch.setattr("app.runtime.extensions.plugin_manager.logger", log) + monkeypatch.setattr("app.runtime.extensions.plugin.manager.logger", log) plugin_manager._run_file_watcher() @@ -660,7 +660,7 @@ def test_deleting_only_active_requirements_prompts_reinstall( ) monkeypatch.setattr(PluginMarketTransport, "get_current_system_version", lambda: Version("2.13.11")) log = Mock() - monkeypatch.setattr("app.runtime.extensions.plugin_manager.logger", log) + monkeypatch.setattr("app.runtime.extensions.plugin.manager.logger", log) plugin_manager._run_file_watcher() @@ -691,7 +691,7 @@ def test_deleting_inactive_requirements_is_debug_only( ) monkeypatch.setattr(PluginMarketTransport, "get_current_system_version", lambda: Version("2.13.11")) log = Mock() - monkeypatch.setattr("app.runtime.extensions.plugin_manager.logger", log) + monkeypatch.setattr("app.runtime.extensions.plugin.manager.logger", log) plugin_manager._run_file_watcher() diff --git a/tests/test_plugin_manager_async_methods.py b/tests/test_plugin_manager_async_methods.py index dcd8965f1..7de906605 100644 --- a/tests/test_plugin_manager_async_methods.py +++ b/tests/test_plugin_manager_async_methods.py @@ -36,7 +36,7 @@ async def test_async_run_plugin_method_offloads_sync_plugin_method( ) worker = AsyncMock(return_value=2) monkeypatch.setattr( - "app.runtime.extensions.plugin_manager.run_in_threadpool_to_completion", + "app.runtime.extensions.plugin.manager.run_in_threadpool_to_completion", worker, ) @@ -61,7 +61,7 @@ async def test_async_run_plugin_method_keeps_async_plugin_method_on_loop( ) worker = AsyncMock() monkeypatch.setattr( - "app.runtime.extensions.plugin_manager.run_in_threadpool_to_completion", + "app.runtime.extensions.plugin.manager.run_in_threadpool_to_completion", worker, ) diff --git a/tests/test_plugin_manager_governance.py b/tests/test_plugin_manager_governance.py index 5b6fe9d2d..a392c3273 100644 --- a/tests/test_plugin_manager_governance.py +++ b/tests/test_plugin_manager_governance.py @@ -6,7 +6,7 @@ from pathlib import Path from app.startup.initializers import plugins as plugins_initializer PROJECT_ROOT = Path(__file__).parents[1] -MANAGER_PATH = PROJECT_ROOT / "app" / "runtime" / "extensions" / "plugin_manager.py" +MANAGER_PATH = PROJECT_ROOT / "app" / "runtime" / "extensions" / "plugin" / "manager.py" RUNTIME_PATH = PROJECT_ROOT / "app" / "runtime" / "extensions" / "plugin" / "runtime.py" diff --git a/tests/test_plugin_monitor_lifecycle.py b/tests/test_plugin_monitor_lifecycle.py index fc8684a91..6ab2ef844 100644 --- a/tests/test_plugin_monitor_lifecycle.py +++ b/tests/test_plugin_monitor_lifecycle.py @@ -19,8 +19,8 @@ from app.runtime.extensions.plugin.monitor import ( ) from app.runtime.extensions.plugin.admission import PluginMutationAdmission from app.runtime.extensions.plugin.system import reset_plugin_system -from app.runtime.extensions import plugin_manager as plugin_manager_module -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.plugin import manager as plugin_manager_module +from app.runtime.extensions.plugin.manager import PluginManager from app.schemas.plugin import PluginRuntimeStatus from app.startup.initializers import plugins as plugins_initializer @@ -752,7 +752,7 @@ async def test_quiesce_timeout_retains_future_owner_until_worker_finishes( with ThreadPoolExecutor(max_workers=1) as executor: thread_helper = SimpleNamespace(submit=executor.submit) monkeypatch.setattr( - "app.runtime.extensions.plugin_manager.ThreadHelper", + "app.runtime.extensions.plugin.manager.ThreadHelper", lambda: thread_helper, ) diff --git a/tests/test_plugin_mutation_admission.py b/tests/test_plugin_mutation_admission.py index f1e06eb63..0964adf42 100644 --- a/tests/test_plugin_mutation_admission.py +++ b/tests/test_plugin_mutation_admission.py @@ -16,7 +16,7 @@ from app.runtime.extensions.plugin.admission import ( PluginMutationRejectedError, ) from app.runtime.extensions.plugin.system import reset_plugin_system -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.plugin.manager import PluginManager from app.schemas.plugin import PluginRuntimeStatus from app.schemas.types import EventType @@ -161,7 +161,7 @@ async def test_quiesce_timeout_retains_admitted_owner_and_nested_reload( with ThreadPoolExecutor(max_workers=1) as executor: monkeypatch.setattr( - "app.runtime.extensions.plugin_manager.ThreadHelper", + "app.runtime.extensions.plugin.manager.ThreadHelper", lambda: SimpleNamespace(submit=executor.submit), ) mutation_task = asyncio.create_task(mutate()) @@ -200,15 +200,15 @@ def test_reload_attributes_gil_transition_to_plugin( ) warning = MagicMock() monkeypatch.setattr( - "app.runtime.extensions.plugin_manager.is_free_threaded_runtime", + "app.runtime.extensions.plugin.manager.is_free_threaded_runtime", lambda: True, ) monkeypatch.setattr( - "app.runtime.extensions.plugin_manager.is_gil_enabled", + "app.runtime.extensions.plugin.manager.is_gil_enabled", lambda: next(states), ) monkeypatch.setattr( - "app.runtime.extensions.plugin_manager.logger.warning", + "app.runtime.extensions.plugin.manager.logger.warning", warning, ) diff --git a/tests/test_plugin_sdk.py b/tests/test_plugin_sdk.py index 24c09bd23..47e36c4b0 100644 --- a/tests/test_plugin_sdk.py +++ b/tests/test_plugin_sdk.py @@ -31,8 +31,8 @@ def test_sdk_exports_canonical_plugin_interfaces(): ).StringUtils from app.foundation.crypto import CryptoJsUtils from app.foundation.text import convert as canonical_convert - from app.runtime.extensions.module_manager import ModuleManager as CanonicalModuleManager - from app.runtime.extensions.plugin_manager import PluginManager as CanonicalPluginManager + from app.runtime.extensions.module.manager import ModuleManager as CanonicalModuleManager + from app.runtime.extensions.plugin.manager import PluginManager as CanonicalPluginManager from app.adapters.network.http import RequestUtils as CanonicalRequestUtils from app.application.rss import RssHelper as CanonicalRssHelper from app.application.site.sites import SitesHelper as CanonicalSitesHelper # pylint: disable=import-error,no-name-in-module @@ -65,7 +65,7 @@ def test_sdk_exports_canonical_plugin_interfaces(): assert convert is canonical_convert assert ModuleManager is CanonicalModuleManager assert PluginManager is CanonicalPluginManager - assert CanonicalPluginManager.__module__ == "app.runtime.extensions.plugin_manager" + assert CanonicalPluginManager.__module__ == "app.runtime.extensions.plugin.manager" def test_legacy_common_crypto_aliases_round_trip(): diff --git a/tests/test_router_aggregation.py b/tests/test_router_aggregation.py index ac20de226..032b81583 100644 --- a/tests/test_router_aggregation.py +++ b/tests/test_router_aggregation.py @@ -55,7 +55,7 @@ def _route_contract(route: APIRoute) -> tuple[Any, ...]: def test_init_routers_directly_includes_endpoint_router_specs(monkeypatch): """启动聚合应直接 include 原始端点路由器并一次性附加完整 v1 前缀。""" - from app.api.router_specs import API_V1_ROUTER_SPECS + from app.api.routers import API_V1_ROUTER_SPECS from app.startup.initializers.routers import init_routers app = FastAPI() @@ -135,14 +135,14 @@ async def test_direct_routes_honor_application_dependency_overrides(): ) as client: response = await client.get(f"{settings.API_V1_STR}/system/ping") - assert response.status_code == 200 + assert response.status_code == 200, response.text assert response.json() == {"success": True, "message": "", "data": None} def test_compatibility_api_router_keeps_public_contract(): """历史导出应继续提供无 v1 根前缀的标准 APIRouter 与固定路由集合。""" from app.api.apiv1 import api_router - from app.api.router_specs import API_V1_ROUTER_SPECS + from app.api.routers import API_V1_ROUTER_SPECS assert type(api_router) is APIRouter app = FastAPI() diff --git a/tests/test_runtime_manager_identity.py b/tests/test_runtime_manager_identity.py index 7ca48c6e1..faed869d1 100644 --- a/tests/test_runtime_manager_identity.py +++ b/tests/test_runtime_manager_identity.py @@ -1,7 +1,7 @@ from app.foundation.singleton import Singleton from app.runtime.events import EventManager, eventmanager -from app.runtime.extensions.module_manager import ModuleManager -from app.runtime.extensions.plugin_manager import PluginManager +from app.runtime.extensions.module.manager import ModuleManager +from app.runtime.extensions.plugin.manager import PluginManager from app.sdk.plugins import ModuleManager as SdkModuleManager from app.sdk.plugins import PluginManager as SdkPluginManager diff --git a/tests/test_service_locator_gate.py b/tests/test_service_locator_gate.py index 45ef0d08c..73a66e0ce 100644 --- a/tests/test_service_locator_gate.py +++ b/tests/test_service_locator_gate.py @@ -18,22 +18,22 @@ def test_service_locator_gate_detects_each_concrete_runtime_family() -> None: """单一扫描器必须覆盖五类运行时,并保留明确兼容边界。""" graph = { "app.api.scheduler_bypass": {"app.scheduler.facade"}, - "app.api.module_bypass": {"app.runtime.extensions.module_manager"}, - "app.api.plugin_bypass": {"app.runtime.extensions.plugin_manager"}, + "app.api.module_bypass": {"app.runtime.extensions.module.manager"}, + "app.api.plugin_bypass": {"app.runtime.extensions.plugin.manager"}, "app.api.command_bypass": {"app.command"}, "app.api.workflow_bypass": {"app.workflow"}, "app.sdk.plugins": { - "app.runtime.extensions.module_manager", - "app.runtime.extensions.plugin_manager", + "app.runtime.extensions.module.manager", + "app.runtime.extensions.plugin.manager", }, "app.startup.initializers.modules": { "app.command", - "app.runtime.extensions.module_manager", + "app.runtime.extensions.module.manager", "app.scheduler.facade", }, "app.startup.composition.chain": { - "app.runtime.extensions.module_manager", - "app.runtime.extensions.plugin_manager", + "app.runtime.extensions.module.manager", + "app.runtime.extensions.plugin.manager", }, "app.startup.composition.outbox": {"app.command"}, "app.workflow.manager": {"app.workflow"}, diff --git a/tests/test_system_nettest.py b/tests/test_system_nettest.py index 1c95409cd..7f7fc8780 100644 --- a/tests/test_system_nettest.py +++ b/tests/test_system_nettest.py @@ -1,5 +1,6 @@ import asyncio import ipaddress +import sys from types import ModuleType, SimpleNamespace from unittest.mock import AsyncMock, Mock, patch @@ -49,7 +50,7 @@ _STUB_MODULES = dict([ _stub("app.chain.system", SystemChain=_Dummy), _stub("app.runtime.events", eventmanager=_Dummy(), Event=_Dummy, EventManager=_Dummy), _stub("app.domain.metainfo", MetaInfo=_Dummy), - _stub("app.runtime.extensions.module_manager", ModuleManager=_Dummy), + _stub("app.runtime.extensions.module.manager", ModuleManager=_Dummy), _stub("app.adapters.web.security.access", verify_apitoken=_Dummy, verify_resource_token=_Dummy, verify_token=_Dummy), _stub("app.api.context", get_host_runtime=_Dummy), _stub("app.startup.composition.context", HostRuntime=_Dummy), @@ -74,12 +75,25 @@ _STUB_MODULES = dict([ ]) _APP_MODULES = snapshot_modules("app") +_ENDPOINTS_PACKAGE = sys.modules.get("app.api.endpoints") +_MISSING_SYSTEM_BINDING = object() +_SYSTEM_BINDING = ( + getattr(_ENDPOINTS_PACKAGE, "system", _MISSING_SYSTEM_BINDING) + if _ENDPOINTS_PACKAGE is not None + else _MISSING_SYSTEM_BINDING +) try: with stub_modules(_STUB_MODULES): from app.api.endpoints import system as system_endpoint from app.application.network import NetworkTestService finally: restore_modules(_APP_MODULES, "app") + if _ENDPOINTS_PACKAGE is not None: + if _SYSTEM_BINDING is _MISSING_SYSTEM_BINDING: + if hasattr(_ENDPOINTS_PACKAGE, "system"): + delattr(_ENDPOINTS_PACKAGE, "system") + else: + setattr(_ENDPOINTS_PACKAGE, "system", _SYSTEM_BINDING) def _network_test_service(transport, **settings) -> NetworkTestService: diff --git a/tests/test_tmdb_cache_management.py b/tests/test_tmdb_cache_management.py index 80f2c012f..173dc7dea 100644 --- a/tests/test_tmdb_cache_management.py +++ b/tests/test_tmdb_cache_management.py @@ -6,8 +6,8 @@ from unittest.mock import Mock from app.api.endpoints import tmdb as tmdb_endpoint from app.api.deps import get_current_active_superuser_async -from app.modules.themoviedb import tmdb_cache as tmdb_cache_module -from app.modules.themoviedb.tmdb_cache import TmdbCache +from app.modules.themoviedb import cache as tmdb_cache_module +from app.modules.themoviedb.cache import TmdbCache from app.schemas.types import MediaType, SystemConfigKey diff --git a/tests/test_tmdb_cache_type_guard.py b/tests/test_tmdb_cache_type_guard.py index e0d1cf01b..6e1834bb6 100644 --- a/tests/test_tmdb_cache_type_guard.py +++ b/tests/test_tmdb_cache_type_guard.py @@ -2,7 +2,7 @@ from time import time from types import SimpleNamespace from app.runtime.config import settings -from app.modules.themoviedb.tmdb_cache import TmdbCache +from app.modules.themoviedb.cache import TmdbCache from app.schemas.types import MediaSource, MediaType diff --git a/tests/test_tmdb_retry_and_errors.py b/tests/test_tmdb_retry_and_errors.py index d68f45b79..188ecf98e 100644 --- a/tests/test_tmdb_retry_and_errors.py +++ b/tests/test_tmdb_retry_and_errors.py @@ -23,7 +23,7 @@ import pytest import app.modules.themoviedb as themoviedb_module from app.domain.metainfo import MetaInfo from app.modules.themoviedb import TheMovieDbModule -from app.modules.themoviedb.tmdb_cache import TmdbCache +from app.modules.themoviedb.cache import TmdbCache from app.modules.themoviedb.tmdbapi import TmdbApi from app.modules.themoviedb.tmdbv3api import tmdb as tmdb_module from app.modules.themoviedb.tmdbv3api.exceptions import TMDbConnectionError, TMDbException diff --git a/tests/test_transfer_failed_retry_buttons.py b/tests/test_transfer_failed_retry_buttons.py index fec628efb..730d6c96e 100644 --- a/tests/test_transfer_failed_retry_buttons.py +++ b/tests/test_transfer_failed_retry_buttons.py @@ -298,7 +298,7 @@ def test_transfer_ai_retry_callback_uses_successful_move_dest_as_source( async def fake_async_post_message(*args, **kwargs): return None - from app.agent.prompt.transfer_redo import build_manual_redo_prompt + from app.agent.prompt.transfer import build_manual_redo_prompt manager = SimpleNamespace(run_background_prompt=fake_run_background_prompt) loop = Mock(**{"is_running.return_value": True, "is_closed.return_value": False})