mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-10 18:06:48 +08:00
refactor(architecture): enforce module naming policy
This commit is contained in:
@@ -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/`。
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
+2
-2
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
# 直接聚合端点路由,避免先构建兼容路由器再克隆到最终应用。
|
||||
|
||||
@@ -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 自动作用于每个用例,无需逐用例改动。
|
||||
|
||||
@@ -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`。原
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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():
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 []))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user