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})