refactor(architecture): enforce module naming policy

This commit is contained in:
jxxghp
2026-08-29 22:17:17 +08:00
parent ed85cc6cf7
commit 0c05956814
111 changed files with 596 additions and 363 deletions
+3 -3
View File
@@ -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/`
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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,
)
+10 -10
View File
@@ -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)
+1 -1
View File
@@ -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 = "***"
@@ -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
+2 -2
View File
@@ -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(
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
)
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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",
@@ -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,
@@ -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,
+2 -2
View File
@@ -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"]
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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
# 直接聚合端点路由,避免先构建兼容路由器再克隆到最终应用。
+2 -2
View File
@@ -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
@@ -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 自动作用于每个用例无需逐用例改动
+9 -9
View File
@@ -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<br/>编排门面(get_agent_manager 等)"]
Reg["app/startup/initializers/agent.py<br/>生命周期显式注册/重置 Provider"]
Reg --> Facade
Facade -.能力启用或首次使用时物化.-> RT["app/agent/runtime_loader.py<br/>能力发现与服务物化"]
Facade -.能力启用或首次使用时物化.-> RT["app/agent/loader.py<br/>能力发现与服务物化"]
RT --> Manager["app/agent/manager.py<br/>稳定 AgentManager 门面"]
Manager --> Session["session.py / lifecycle.py<br/>会话队列与有界生命周期"]
Manager --> Tasks["tasks.py<br/>后台、调度与心跳任务"]
@@ -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`。原
+11 -8
View File
@@ -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_<behavior>.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_<behavior>.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 |
+1 -1
View File
@@ -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.
---
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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,
+5 -5
View File
@@ -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,
+201 -147
View File
@@ -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"
}
]
}
+7 -7
View File
@@ -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")
+1 -1
View File
@@ -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,
+3 -3
View File
@@ -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] = []
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 (
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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):
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+39 -12
View File
@@ -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
}
+2 -2
View File
@@ -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",
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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():
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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,
)
+1 -1
View File
@@ -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,
)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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",
+11 -11
View File
@@ -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()
+1 -1
View File
@@ -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 []))
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+142 -7
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+8 -8
View File
@@ -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}")
+10 -10
View File
@@ -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()
+2 -2
View File
@@ -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,
)
+1 -1
View File
@@ -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"

Some files were not shown because too many files have changed in this diff Show More