refactor(architecture): enforce module naming policy

This commit is contained in:
jxxghp
2026-08-29 22:17:17 +08:00
parent ed85cc6cf7
commit 0c05956814
111 changed files with 596 additions and 363 deletions
+3 -3
View File
@@ -61,11 +61,11 @@ The legacy roots have no physical directories in the source tree. Current images
| `app/foundation/` | 无状态、无配置和无 I/O 的底层机制:反射/动态导入、加密、DOM、身份、集合、单例、文本、URL 和版本比较 | `settings`、DB/SystemConfig、网络请求、运行日志、MoviePilot 业务规则、旧导入路径 | `reflection.py`, `crypto.py`, `collections.py`, `text.py`, `url.py` | | `app/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/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/` | 进程级运行机制和策略:配置、事件、完整日志、缓存契约/内存行为、托管资源门面、并发、调度、限流、本地化、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/network/` | HTTP、浏览器、DNS、Cloudflare 和 IP 等通用网络技术适配 | RSS/站点业务编排、身份认证策略、命名外部产品流程 | `http.py`, `browser.py`, `doh.py`, `ip.py` |
| `app/adapters/cache/` | Redis 与文件缓存等具体持久化实现 | 缓存协议、装饰器和进程内缓存策略 | `backends.py`, `redis.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/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/` | 聚焦应用服务、用例命令,以及由用例拥有的持久化/技术能力 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/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` | | `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/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/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/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/` 容易误分的三个边界必须按实际职责判断:`application/rss.py` 同时承担 Feed/种子语义、站点规则和浏览器回退,不是单纯 HTTP 传输;规范目标是由它拥有所需 Port、startup 注入 network/system Adapter。当前直接导入是 `S2-L6` 临时债务,不是允许的新模式。`application/site/sites.*``user.sites.v3.bin` 共同构成站点目录、认证和索引应用能力,只有下载安装机制留在 `adapters/system/resource.py``foundation/crypto.py` 只提供无状态 RSA/摘要/AES 算法,认证、签名、令牌和二次验证策略仍属于 `application/security/`
+1 -1
View File
@@ -22,7 +22,7 @@ def attach_server_tool_capabilities(
base_url: Optional[str] = None, base_url: Optional[str] = None,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""为 Provider 模型目录附加统一的服务端工具能力描述。""" """为 Provider 模型目录附加统一的服务端工具能力描述。"""
from app.agent.llm.server_tools import ServerToolRegistry from app.agent.llm.tools import ServerToolRegistry
result = [] result = []
for item in models: for item in models:
+2 -2
View File
@@ -15,7 +15,7 @@ from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting from app.runtime.settings import get_runtime_setting
if TYPE_CHECKING: if TYPE_CHECKING:
from app.agent.llm.server_tools import ServerToolResolution from app.agent.llm.tools import ServerToolResolution
class LLMTestError(RuntimeError): class LLMTestError(RuntimeError):
@@ -1169,7 +1169,7 @@ class LLMHelper:
use_proxy=use_proxy, use_proxy=use_proxy,
) )
model_name = runtime.get("model_id") or model_name model_name = runtime.get("model_id") or model_name
from app.agent.llm.server_tools import ( from app.agent.llm.tools import (
ServerToolRegistry, ServerToolRegistry,
ServerToolUnavailableError, ServerToolUnavailableError,
) )
+10 -10
View File
@@ -22,10 +22,10 @@ from langgraph.checkpoint.memory import InMemorySaver
from app.agent.callback import StreamingHandler from app.agent.callback import StreamingHandler
from app.agent.contracts import ReplyMode, build_display_message from app.agent.contracts import ReplyMode, build_display_message
from app.agent.llm.helper import LLMHelper 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.mcp import agent_mcp_manager
from app.agent.memory import MemoryManager, memory_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, QUERY_ACTIVITY_LOG_TOOL_NAME,
ActivityLogMiddleware, ActivityLogMiddleware,
) )
@@ -33,9 +33,9 @@ from app.agent.middleware.jobs import (
JobsMiddleware, JobsMiddleware,
) )
from app.agent.middleware.memory import MemoryMiddleware 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.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.skills import SKILL_TOOL_NAME, SkillsMiddleware
from app.agent.middleware.subagents import ( from app.agent.middleware.subagents import (
SUBAGENT_CONTROL_TOOL_NAME, SUBAGENT_CONTROL_TOOL_NAME,
@@ -49,7 +49,7 @@ from app.agent.middleware.summarization import (
from app.agent.middleware.summarization import ( from app.agent.middleware.summarization import (
FinalRequestCompactionMiddleware, 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.middleware.usage import UsageMiddleware
from app.agent.policy.contracts import ( from app.agent.policy.contracts import (
AuthSource, AuthSource,
@@ -1552,7 +1552,7 @@ class MoviePilotAgent:
""" """
初始化主 Agent 本地工具实例。 初始化主 Agent 本地工具实例。
""" """
from app.agent.runtime_loader import get_tool_factory from app.agent.loader import get_tool_factory
return get_tool_factory().create_tools( return get_tool_factory().create_tools(
session_id=self.session_id, session_id=self.session_id,
@@ -1570,7 +1570,7 @@ class MoviePilotAgent:
self, self,
) -> tuple[ToolCatalogSnapshot, ToolCatalogSnapshot]: ) -> tuple[ToolCatalogSnapshot, ToolCatalogSnapshot]:
"""在同一插件 revision 窗口内建立主图和子图工具目录。""" """在同一插件 revision 窗口内建立主图和子图工具目录。"""
from app.agent.runtime_loader import get_tool_factory from app.agent.loader import get_tool_factory
tool_factory = get_tool_factory() tool_factory = get_tool_factory()
plugin_manager = get_plugin_manager() plugin_manager = get_plugin_manager()
@@ -1677,7 +1677,7 @@ class MoviePilotAgent:
@staticmethod @staticmethod
def _tool_factory_revision() -> str: 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() 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( return get_tool_factory().create_tools(
session_id=self.session_id, session_id=self.session_id,
@@ -2005,7 +2005,7 @@ class MoviePilotAgent:
logger.debug(f"复用会话内 Agent 图: session_id={self.session_id}") logger.debug(f"复用会话内 Agent 图: session_id={self.session_id}")
return cached_agent return cached_agent
max_tools = get_runtime_setting('LLM_MAX_TOOLS') 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 = ( always_include_tools = (
get_tool_factory().get_tool_selector_always_include_names(tools) get_tool_factory().get_tool_selector_always_include_names(tools)
+1 -1
View File
@@ -10,7 +10,7 @@ from typing import Any
from pydantic import AliasChoices, AliasPath, BaseModel, ValidationError from 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 = "***" REDACTED_VALUE = "***"
@@ -3,7 +3,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Optional 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.runtime.config import Settings
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
+2 -2
View File
@@ -97,7 +97,7 @@ class MoviePilotToolsManager:
工厂负责插件 revision 前后稳定窗口manager 只发布完整快照避免 工厂负责插件 revision 前后稳定窗口manager 只发布完整快照避免
并发调用观察到一半刷新后的工具列表 并发调用观察到一半刷新后的工具列表
""" """
from app.agent.runtime_loader import get_tool_factory from app.agent.loader import get_tool_factory
try: try:
catalog = get_tool_factory().create_catalog( catalog = get_tool_factory().create_catalog(
@@ -255,7 +255,7 @@ class MoviePilotToolsManager:
if self.catalog is None or [ if self.catalog is None or [
id(tool) for tool in self.catalog.tools id(tool) for tool in self.catalog.tools
] != [id(tool) for tool in self.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 from app.agent.tools.catalog import ToolCatalogSnapshot
self.catalog = ToolCatalogSnapshot.from_tools( self.catalog = ToolCatalogSnapshot.from_tools(
+1 -1
View File
@@ -3,7 +3,7 @@
from threading import Lock from threading import Lock
from typing import Any, Awaitable, Callable, Optional 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.application.security.user import get_configured_user_id_lookup
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.message import Message from app.schemas.message import Message
+1 -1
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter 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() api_router = APIRouter()
for spec in API_V1_ROUTER_SPECS: for spec in API_V1_ROUTER_SPECS:
+1 -1
View File
@@ -16,7 +16,7 @@ from app.api.endpoints.openai import (
_is_manager_unavailable, _is_manager_unavailable,
_run_managed_agent, _run_managed_agent,
) )
from app.api.openai_utils import ( from app.api.protocol import (
build_anthropic_messages, build_anthropic_messages,
build_prompt, build_prompt,
build_session_id, build_session_id,
+1 -1
View File
@@ -7,7 +7,7 @@ from fastapi import Depends
from app.adapters.web.security.access import verify_token from app.adapters.web.security.access import verify_token
from app.agent.contracts import ReplyMode 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_batch_manual_redo_prompt,
build_manual_redo_prompt, build_manual_redo_prompt,
) )
+1 -1
View File
@@ -7,7 +7,7 @@ from typing import Annotated, Any, List, Optional, Protocol, Union
from fastapi import Depends, Request from fastapi import Depends, Request
from starlette.responses import PlainTextResponse 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.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.context import get_background_task_registry, resolve_background_task_registry
from app.api.dependencies.agent import get_message_query_service from app.api.dependencies.agent import get_message_query_service
+2 -2
View File
@@ -10,12 +10,12 @@ from fastapi.security import HTTPAuthorizationCredentials
from app.adapters.web.security.access import openai_bearer_scheme from app.adapters.web.security.access import openai_bearer_scheme
from app.agent.contracts import ReplyMode 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 ( from app.api.context import (
get_background_task_registry_compat, get_background_task_registry_compat,
resolve_background_task_registry, resolve_background_task_registry,
) )
from app.api.openai_utils import ( from app.api.protocol import (
build_completion_payload, build_completion_payload,
build_prompt, build_prompt,
build_responses_input, build_responses_input,
+1 -1
View File
@@ -127,7 +127,7 @@ def _database_backup_artifact_data(artifact: Any) -> _SchemaDatabaseBackupArtifa
def _validate_llm_server_tool_config(env: dict) -> Optional[str]: def _validate_llm_server_tool_config(env: dict) -> Optional[str]:
"""校验强制服务端联网搜索配置,返回用户可读错误信息。""" """校验强制服务端联网搜索配置,返回用户可读错误信息。"""
from app.agent.llm.server_tools import ( from app.agent.llm.tools import (
ServerToolRegistry, ServerToolRegistry,
ServerToolUnavailableError, ServerToolUnavailableError,
) )
+1 -1
View File
@@ -3,7 +3,7 @@ from abc import abstractmethod, ABCMeta
from typing import Generic, Tuple, Union, TypeVar, Type, Dict, Optional, Callable from typing import Generic, Tuple, Union, TypeVar, Type, Dict, Optional, Callable
from pathlib import Path 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.runtime.log import logger
from app.schemas.message import Message from app.schemas.message import Message
from app.schemas.system import NotificationConf from app.schemas.system import NotificationConf
+1 -1
View File
@@ -7,7 +7,7 @@ from app.domain.meta.metabase import MetaBase
from app.domain.projection.anilist import format_date as format_anilist_date from app.domain.projection.anilist import format_date as format_anilist_date
from app.domain.scraper import MediaScraperHelper from app.domain.scraper import MediaScraperHelper
from app.modules import _ModuleBase 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.modules.anilist.anilist import AniListApi
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting from app.runtime.settings import get_runtime_setting
+1 -1
View File
@@ -8,7 +8,7 @@ from app.domain.meta.metabase import MetaBase
from app.domain.projection.bangumi import resolve_media_type as resolve_bangumi_media_type from app.domain.projection.bangumi import resolve_media_type as resolve_bangumi_media_type
from app.domain.scraper import MediaScraperHelper from app.domain.scraper import MediaScraperHelper
from app.modules import _ModuleBase 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.modules.bangumi.bangumi import BangumiApi
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting from app.runtime.settings import get_runtime_setting
+1 -1
View File
@@ -16,7 +16,7 @@ from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo from app.domain.metainfo import MetaInfo
from app.foundation.text import convert as zhconv_convert from app.foundation.text import convert as zhconv_convert
from app.modules import _ModuleBase 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.apiv2 import DoubanApi
from app.modules.douban.scraper import DoubanScraper from app.modules.douban.scraper import DoubanScraper
from app.runtime.execution import retry from app.runtime.execution import retry
+1 -1
View File
@@ -11,7 +11,7 @@ from app.domain.meta.metabase import MetaBase
from app.domain.scraper import MediaScraperHelper from app.domain.scraper import MediaScraperHelper
from app.foundation.text import convert as zhconv_convert from app.foundation.text import convert as zhconv_convert
from app.modules import _ModuleBase 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.log import logger
from app.runtime.settings import get_runtime_setting from app.runtime.settings import get_runtime_setting
from app.schemas.context import MediaCredit, MediaImageSet from app.schemas.context import MediaCredit, MediaImageSet
+1 -1
View File
@@ -18,7 +18,7 @@ from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic from app.domain.meta.metamusic import MetaMusic
from app.foundation.text import convert as zhconv_convert from app.foundation.text import convert as zhconv_convert
from app.modules import _ModuleBase 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.cache import cached
from app.runtime.log import logger from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting from app.runtime.settings import get_runtime_setting
+2 -2
View File
@@ -11,10 +11,10 @@ from app.domain.media import is_media_source_enabled, is_media_source_selected
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.foundation.text import convert as zhconv_convert from app.foundation.text import convert as zhconv_convert
from app.modules import _ModuleBase 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.category import CategoryHelper
from app.modules.themoviedb.scraper import TmdbScraper 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.tmdbapi import TmdbApi
from app.modules.themoviedb.tmdbv3api.exceptions import TMDbConnectionError from app.modules.themoviedb.tmdbv3api.exceptions import TMDbConnectionError
from app.runtime.log import logger from app.runtime.log import logger
+3 -3
View File
@@ -8,7 +8,7 @@ from app.runtime.execution import run_in_threadpool
from app.runtime.settings import get_runtime_setting from app.runtime.settings import get_runtime_setting
from app.modules import _ModuleBase 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.runtime.log import logger
from app.schemas.types import ( from app.schemas.types import (
MediaRecognizeType, MediaRecognizeType,
@@ -24,7 +24,7 @@ class TheTvDbModule(_ModuleBase):
TVDB媒体信息匹配 TVDB媒体信息匹配
""" """
__timeout: int = 15 __timeout: int = 15
tvdb: Optional[tvdb_v4_official.TVDB] = None tvdb: Optional[client.TVDB] = None
__auth_lock = Lock() __auth_lock = Lock()
def init_module(self) -> None: def init_module(self) -> None:
@@ -40,7 +40,7 @@ class TheTvDbModule(_ModuleBase):
try: try:
if not get_runtime_setting('TVDB_V4_API_KEY'): if not get_runtime_setting('TVDB_V4_API_KEY'):
raise ConnectionError("TVDB 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'), pin=get_runtime_setting('TVDB_V4_API_PIN'),
proxy=get_runtime_setting('PROXY'), proxy=get_runtime_setting('PROXY'),
timeout=self.__timeout) timeout=self.__timeout)
+1 -1
View File
@@ -12,7 +12,7 @@ from app.application.messaging.agent import (
) )
from app.runtime.log import logger from app.runtime.log import logger
from app.modules._base.notification import _MessageChannelModuleBase 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.wechat import WeChat
from app.modules.wechat.wechatbot import WeChatBot from app.modules.wechat.wechatbot import WeChatBot
from app.schemas.notification import NotificationChannel from app.schemas.notification import NotificationChannel
+2 -2
View File
@@ -441,13 +441,13 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = {
owner="domain", owner="domain",
), ),
"app.core.module": ModuleAlias( "app.core.module": ModuleAlias(
target="app.runtime.extensions.module_manager", target="app.runtime.extensions.module.manager",
replacement="app.sdk.plugins", replacement="app.sdk.plugins",
introduced="v3.0.0", introduced="v3.0.0",
owner="runtime", owner="runtime",
), ),
"app.core.plugin": ModuleAlias( "app.core.plugin": ModuleAlias(
target="app.runtime.extensions.plugin_manager", target="app.runtime.extensions.plugin.manager",
replacement="app.sdk.plugins", replacement="app.sdk.plugins",
introduced="v3.0.0", introduced="v3.0.0",
owner="runtime", owner="runtime",
@@ -14,7 +14,7 @@ from app.runtime.capabilities.model import (
) )
from app.runtime.capabilities.registry import CapabilityRegistry from app.runtime.capabilities.registry import CapabilityRegistry
from app.runtime.settings import get_runtime_setting, has_runtime_setting 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 ( from app.schemas.types import (
DownloaderType, DownloaderType,
MediaRecognizeType, MediaRecognizeType,
@@ -30,7 +30,7 @@ from app.schemas.types import (
HOST_MODULE_KIND = "host_module" HOST_MODULE_KIND = "host_module"
_SETTING_SELECTOR = "setting_truthy" _SETTING_SELECTOR = "setting_truthy"
_SERVICE_SELECTOR = "system_config_item" _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({ _SERVICE_CONFIG_GETTERS = MappingProxyType({
SystemConfigKey.Downloaders.value: ServiceConfigHelper.get_downloader_configs, SystemConfigKey.Downloaders.value: ServiceConfigHelper.get_downloader_configs,
SystemConfigKey.MediaServers.value: ServiceConfigHelper.get_mediaserver_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.capabilities.runtime import CapabilityRuntime
from app.runtime.settings import get_runtime_setting from app.runtime.settings import get_runtime_setting
from app.runtime.events import Event, EventHandlerBinding, eventmanager 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, HOST_MODULE_KIND,
HostModuleAdapter, HostModuleAdapter,
build_host_module_registry, build_host_module_registry,
+2 -2
View File
@@ -1,7 +1,7 @@
"""插件和模块运行时管理接口。""" """插件和模块运行时管理接口。"""
from app.runtime.extensions.module_manager import ModuleManager from app.runtime.extensions.module.manager import ModuleManager
from app.runtime.extensions.plugin_manager import PluginManager from app.runtime.extensions.plugin.manager import PluginManager
__all__ = ["ModuleManager", "PluginManager"] __all__ = ["ModuleManager", "PluginManager"]
+1 -1
View File
@@ -1,7 +1,7 @@
"""插件可使用的宿主服务发现与运行时门面。""" """插件可使用的宿主服务发现与运行时门面。"""
from app.application.service import ServiceBaseHelper 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.runtime.state import SystemHelper
from app.application.downloader import DownloaderHelper from app.application.downloader import DownloaderHelper
from app.application.rules import RuleHelper from app.application.rules import RuleHelper
+2 -2
View File
@@ -24,8 +24,8 @@ from app.db.session import SessionFactory
from app.runtime.cache import AsyncFileCache, FileCache from app.runtime.cache import AsyncFileCache, FileCache
from app.runtime.events import EventManager from app.runtime.events import EventManager
from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher 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_manager import PluginManager from app.runtime.extensions.plugin.manager import PluginManager
from app.runtime.stop import runtime_stop_state from app.runtime.stop import runtime_stop_state
from app.startup.composition.database import build_transactional_user_repository from app.startup.composition.database import build_transactional_user_repository
from app.startup.composition.runtime import RuntimeDependencies from app.startup.composition.runtime import RuntimeDependencies
+4 -4
View File
@@ -1,17 +1,17 @@
from typing import Any from typing import Any
from app.agent.llm.gateway import register_llm_provider_runtime from app.agent.llm.gateway import register_llm_provider_runtime
from app.agent.runtime_loader import ( from app.agent.loader import (
activate_agent_service, activate_agent_service,
begin_agent_shutdown, begin_agent_shutdown,
close_materialized_terminal_sessions, close_materialized_terminal_sessions,
is_tool_factory_materialized, is_tool_factory_materialized,
reconcile_agent_service, reconcile_agent_service,
) )
from app.agent.runtime_loader import ( from app.agent.loader import (
get_agent_manager as get_runtime_agent_manager, 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, get_running_agent_manager as get_runtime_running_agent_manager,
) )
from app.application.agent import ( from app.application.agent import (
@@ -141,7 +141,7 @@ def _get_llm_helper() -> Any:
def _get_manual_redo_prompt_builder() -> 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 return build_manual_redo_prompt
+2 -2
View File
@@ -56,8 +56,8 @@ from app.db.session import (
from app.runtime.config import settings as legacy_settings from app.runtime.config import settings as legacy_settings
from app.runtime.events import EventHandlerBinding, EventManager from app.runtime.events import EventHandlerBinding, EventManager
from app.runtime.execution import run_in_threadpool_to_completion from app.runtime.execution import run_in_threadpool_to_completion
from app.runtime.extensions.module_manager import ModuleManager from app.runtime.extensions.module.manager import ModuleManager
from app.runtime.extensions.service_config import ( from app.runtime.extensions.service import (
ServiceConfigHelper, ServiceConfigHelper,
configure_service_config_reader, configure_service_config_reader,
reset_service_config_reader, reset_service_config_reader,
+2 -2
View File
@@ -88,7 +88,7 @@ from app.runtime.compat.diagnostics import (
configure_legacy_import_diagnostics, configure_legacy_import_diagnostics,
scan_plugin_legacy_imports, 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.execution import run_in_threadpool_to_completion
from app.runtime.extensions.plugin.dependency import PluginDependencyInstallResult from app.runtime.extensions.plugin.dependency import PluginDependencyInstallResult
from app.runtime.extensions.plugin.runtime import ( from app.runtime.extensions.plugin.runtime import (
@@ -107,7 +107,7 @@ from app.runtime.extensions.plugin.system import (
configure_plugin_system, configure_plugin_system,
get_plugin_system, get_plugin_system,
) )
from app.runtime.extensions.plugin_manager import ( from app.runtime.extensions.plugin.manager import (
PluginManager, PluginManager,
configure_plugin_catalog_factory, configure_plugin_catalog_factory,
configure_plugin_legacy_import_services, configure_plugin_legacy_import_services,
+1 -1
View File
@@ -8,7 +8,7 @@ def init_routers(app: FastAPI, api_prefix: str = "/api/v1"):
:param app: 需要挂载路由的 FastAPI 应用 :param app: 需要挂载路由的 FastAPI 应用
:param api_prefix: v1 API 根路径由启动组合根传入 :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.servarr import arr_router
from app.api.servcookie import cookie_router from app.api.servcookie import cookie_router
# 直接聚合端点路由,避免先构建兼容路由器再克隆到最终应用。 # 直接聚合端点路由,避免先构建兼容路由器再克隆到最终应用。
+2 -2
View File
@@ -4,9 +4,9 @@
- :mod:`app.testing.stub`测试期对 ``sys.modules`` 的临时打桩并自动还原避免残留假模块相互污染 - :mod:`app.testing.stub`测试期对 ``sys.modules`` 的临时打桩并自动还原避免残留假模块相互污染
- :mod:`app.testing.bootstrap`隔离 CONFIG_DIR建表插件目录注入与 v1/v2 marker 等引导逻辑 - :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 等测试期依赖 :func:`stub_modules`保持 ``import app.testing`` 不引入 pytest 等测试期依赖
""" """
from app.testing.stub import stub_modules from app.testing.stub import stub_modules
@@ -1,7 +1,7 @@
"""测试网络守卫(主程序与插件仓共享)。 """测试网络守卫(主程序与插件仓共享)。
提供一个 autouse pytest fixture拦截测试期对非本地主机的真实出站网络主程序 提供一个 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 进来的 block_real_network`` 即复用同一道守卫pytest 会把 conftest 命名空间内 import 进来的
fixture 一并识别autouse 自动作用于每个用例无需逐用例改动 fixture 一并识别autouse 自动作用于每个用例无需逐用例改动
+9 -9
View File
@@ -193,12 +193,12 @@ SDK/Compat。领域查询与写入继续使用 Application 所属的冻结 DTO/P
| `app/foundation/` | 无状态、无配置、无 I/O 的底层原语:反射/动态导入、加密、DOM、单例、文本、URL、版本比较 | `reflection.py``crypto.py``singleton.py` | | `app/foundation/` | 无状态、无配置、无 I/O 的底层原语:反射/动态导入、加密、DOM、单例、文本、URL、版本比较 | `reflection.py``crypto.py``singleton.py` |
| `app/domain/` | 纯 MoviePilot 业务语义:媒体上下文、识别解析、站点状态解释、磁力语义、NFO 刮削 | `context.py``metainfo.py``meta/``scraper.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/` | 进程级运行机制:配置、进程拓扑、事件、完整日志、缓存契约与内存后端、运行依赖 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/runtime/compat/` | 仅标准库的精确旧模块、包与符号导入路由;不是业务实现,也不是通用 re-export 层 | `manifest.py``imports.py` |
| `app/adapters/network/` | 通用 HTTP、浏览器、DNS、Cloudflare、IP 传输机制 | `http.py``browser.py` | | `app/adapters/network/` | 通用 HTTP、浏览器、DNS、Cloudflare、IP 传输机制 | `http.py``browser.py` |
| `app/adapters/cache/` | Redis 与文件缓存的具体实现 | `backends.py``redis.py` | | `app/adapters/cache/` | Redis 与文件缓存的具体实现 | `backends.py``redis.py` |
| `app/adapters/system/` | OS/文件/进程/stdio/显示/包安装/Rust 加速适配 | `host.py``resource.py``fsproxy.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/web/` | Web 技术适配:动态插件路由注册、认证依赖和 OpenAPI 重建;不承载插件路由用例 | `plugin/routes.py` |
| `app/adapters/observability/` | 可选观测技术适配;核心层只依赖 `runtime/observability` 定义的窄端口 | `otel.py` | | `app/adapters/observability/` | 可选观测技术适配;核心层只依赖 `runtime/observability` 定义的窄端口 | `otel.py` |
| `app/application/` | 读取配置/持久化状态的聚焦应用服务:识别、过滤、通知、RSS、站点、下载器、媒体服务器、存储、整理规则、可靠副作用等;同一主题拆成子包 | `recognition.py``rules.py``rss.py``outbox.py``site/``subscription/``plugin/` | | `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 ..."] MM --> M6["indexer / subtitle / filter ..."]
``` ```
- 模块由 `runtime/extensions/module_manager.py` 发现并管理生命周期; - 模块由 `runtime/extensions/module/manager.py` 发现并管理生命周期;
`app/modules/_base/` 承载各模块族的共享模板基类(下载器、媒体服务器、消息渠道)。 `app/modules/_base/` 承载各模块族的共享模板基类(下载器、媒体服务器、消息渠道)。
- 模块开关由 `init_setting()` 声明的配置项决定(如 `DOWNLOADER = "qbittorrent"`)。 - 模块开关由 `init_setting()` 声明的配置项决定(如 `DOWNLOADER = "qbittorrent"`)。
- **模块之间、模块到 Chain 的直接依赖被禁止**,跨模块编排一律由 Chain 完成。 - **模块之间、模块到 Chain 的直接依赖被禁止**,跨模块编排一律由 Chain 完成。
@@ -590,7 +590,7 @@ flowchart TB
Entry["消息渠道 / API / MCP"] --> Facade["app/application/agent.py<br/>编排门面(get_agent_manager 等)"] Entry["消息渠道 / API / MCP"] --> Facade["app/application/agent.py<br/>编排门面(get_agent_manager 等)"]
Reg["app/startup/initializers/agent.py<br/>生命周期显式注册/重置 Provider"] Reg["app/startup/initializers/agent.py<br/>生命周期显式注册/重置 Provider"]
Reg --> Facade 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 门面"] RT --> Manager["app/agent/manager.py<br/>稳定 AgentManager 门面"]
Manager --> Session["session.py / lifecycle.py<br/>会话队列与有界生命周期"] Manager --> Session["session.py / lifecycle.py<br/>会话队列与有界生命周期"]
Manager --> Tasks["tasks.py<br/>后台、调度与心跳任务"] Manager --> Tasks["tasks.py<br/>后台、调度与心跳任务"]
@@ -663,9 +663,9 @@ flowchart TB
端点层禁止直接依赖 `factory` 端点层禁止直接依赖 `factory`
- 动态插件路由使用原生 `APIRoute`,插件自行决定返回结构;主程序的统一 `Response` 封装只适用于 - 动态插件路由使用原生 `APIRoute`,插件自行决定返回结构;主程序的统一 `Response` 封装只适用于
`app/api/` 的宿主端点。插件若已经自行返回 `Response`、字典、列表或其它可序列化值,宿主不再二次包裹。 `app/api/` 的宿主端点。插件若已经自行返回 `Response`、字典、列表或其它可序列化值,宿主不再二次包裹。
- `app/runtime/extensions/plugin_manager.py`保留插件 ABI 的管理器门面,发现、加载、生命周期、 - `app/runtime/extensions/plugin/manager.py` canonical 管理器 owner,发现、加载、生命周期、
目录、同步等实现拆在 `app/runtime/extensions/plugin/`;这个“门面 + 实现包”是有意的兼容边界, 目录、同步等实现共同归入 `app/runtime/extensions/plugin/`。旧插件仍从 `app.core.plugin`
不应为了目录整齐而让外部插件改用内部实现文件 `app.sdk.plugins` 进入,并由 Compat 精确路由到同一个 `PluginManager` 身份
- 插件可参与 `run_module` 方法分发(同名方法优先响应)并注册事件处理器。 - 插件可参与 `run_module` 方法分发(同名方法优先响应)并注册事件处理器。
--- ---
@@ -776,8 +776,8 @@ flowchart LR
`app/sdk/_legacy/` 薄门面保留行为兼容。兼容清单是导入路由,不负责合并模块,也不负责把任意 `app/sdk/_legacy/` 薄门面保留行为兼容。兼容清单是导入路由,不负责合并模块,也不负责把任意
新实现重新导出到旧模块。 新实现重新导出到旧模块。
- 已完成的插件边界:插件 API 的动态路由由 application 端口 + web adapter 组成,使用原生 - 已完成的插件边界:插件 API 的动态路由由 application 端口 + web adapter 组成,使用原生
`APIRoute` 保留插件响应;插件管理器保留 `plugin_manager.py` 的稳定 ABI,内部实现拆在 `APIRoute` 保留插件响应;插件管理器归入 `runtime/extensions/plugin/manager.py`,旧 ABI
`runtime/extensions/plugin/``app/plugins/` 仅作为运行时插件副本/覆盖层处理。 只由 SDK/Compat 路由`app/plugins/` 仅作为运行时插件副本/覆盖层处理。
- 已完成的主题收口:订阅 DTO/Port 归入 `app/application/subscription/contract.py`,用例写入归入 - 已完成的主题收口:订阅 DTO/Port 归入 `app/application/subscription/contract.py`,用例写入归入
`app/application/subscription/write.py`SQLAlchemy 实现只在 `app/db/adapters/subscription.py`;插件动态路由与 `app/application/subscription/write.py`SQLAlchemy 实现只在 `app/db/adapters/subscription.py`;插件动态路由与
文件夹操作归入 `app/application/plugin/routes.py``folders.py`。原 文件夹操作归入 `app/application/plugin/routes.py``folders.py`。原
+11 -8
View File
@@ -436,10 +436,13 @@ ownership.
New production Python module filenames use one lowercase word. When one topic 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 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 one word, for example `runtime/event/{registry,binding,dispatch,errors}.py` or
`application/subscription/{contract,delete,identity}.py`. Established multiword `application/subscription/{contract,delete,identity}.py`. Multiword production
public import paths may remain as compatibility exceptions after plugin/import paths may remain only when the machine policy verifies a stable discovery
scanning, but they are not templates for new modules. Test filenames continue identifier or records a path-specific reason and consumer evidence; an
to follow pytest's descriptive `test_<behavior>.py` convention. 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 Legacy module paths belong in `app/runtime/compat/manifest.py`. New
implementation modules must not re-export old managers, helpers or Oper classes 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/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/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/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/__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/__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 | | `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/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/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/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/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/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/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/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 | | `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/cache/backends.py` | Redis and filesystem cache adapters |
| `app/adapters/system/resource.py` | Runtime resource detection/download/installation | | `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/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/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/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 | | `app/application/security/url.py` | URL/path validation, SSRF protection and signed image policy |
+1 -1
View File
@@ -110,7 +110,7 @@ except:
- Private functions in the same file are preferable to extracting a new module for single-use logic. - 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. - 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. - 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. - Keep files focused on one domain concern.
--- ---
+2 -2
View File
@@ -43,7 +43,7 @@ RUNTIME_FACADE_POLICIES = (
), ),
RuntimeFacadePolicy( RuntimeFacadePolicy(
name="module", name="module",
dependency="app.runtime.extensions.module_manager", dependency="app.runtime.extensions.module.manager",
exact_consumers=frozenset( exact_consumers=frozenset(
{ {
"app.sdk.plugins", "app.sdk.plugins",
@@ -54,7 +54,7 @@ RUNTIME_FACADE_POLICIES = (
), ),
RuntimeFacadePolicy( RuntimeFacadePolicy(
name="plugin", name="plugin",
dependency="app.runtime.extensions.plugin_manager", dependency="app.runtime.extensions.plugin.manager",
exact_consumers=frozenset( exact_consumers=frozenset(
{ {
"app.sdk.plugins", "app.sdk.plugins",
+1 -1
View File
@@ -170,7 +170,7 @@ def _agent_module_observation() -> dict[str, object]:
def _read_agent_runtime() -> dict[str, object]: def _read_agent_runtime() -> dict[str, object]:
"""读取轻量 Agent loader 的公开只读状态,不触发 capability 首用。""" """读取轻量 Agent loader 的公开只读状态,不触发 capability 首用。"""
try: try:
from app.agent.runtime_loader import is_tool_factory_materialized from app.agent.loader import is_tool_factory_materialized
return { return {
"available": True, "available": True,
+5 -5
View File
@@ -23,7 +23,7 @@ from app.testing.bootstrap import prepare_backend
prepare_backend() prepare_backend()
# 复用共享 autouse 网络守卫;同一实现亦供各插件仓 conftest import 复用,避免逐仓维护 # 复用共享 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") TResult = TypeVar("TResult")
@@ -207,20 +207,20 @@ def configure_plugin_system_services():
from app.application.plugin.runtime import configure_plugin_runtime from app.application.plugin.runtime import configure_plugin_runtime
from app.runtime.cache import AsyncFileCache, FileCache from app.runtime.cache import AsyncFileCache, FileCache
from app.runtime.events import EventManager 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.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 ( from app.runtime.extensions.plugin.runtime import (
PluginRuntimeEnvironment, PluginRuntimeEnvironment,
build_plugin_runtime, build_plugin_runtime,
) )
from app.runtime.extensions.plugin.storage import get_plugin_storage 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.system import get_plugin_system
from app.runtime.extensions.plugin_manager import ( from app.runtime.extensions.plugin.manager import (
PluginManager, PluginManager,
reset_plugin_runtime_factory, reset_plugin_runtime_factory,
) )
from app.runtime.extensions.service_config import ServiceConfigHelper from app.runtime.extensions.service import ServiceConfigHelper
configure_service_directory( configure_service_directory(
configs=ServiceConfigHelper.get_configs, configs=ServiceConfigHelper.get_configs,
+201 -147
View File
@@ -1,152 +1,184 @@
{ {
"grandfathered_multiword_files": [ "package_root_exceptions": [
"app/adapters/external/wechat_crypt.py", {
"app/agent/llm/server_tools.py", "mode": "bootstrap",
"app/agent/middleware/activity_log.py", "path": "app/__init__.py",
"app/agent/middleware/patch_tool_calls.py", "reason": "进程入口在任何 app 子模块导入前安装平台警告和精确兼容导入钩子。"
"app/agent/middleware/runtime_config.py", },
"app/agent/middleware/tool_selection.py", {
"app/agent/policy/secret_fields.py", "mode": "public_facade",
"app/agent/prompt/transfer_redo.py", "path": "app/adapters/system/display/__init__.py",
"app/agent/runtime_loader.py", "reason": "DisplayHelper 是已验证插件 ABI,包根只保留延迟托管资源门面。"
"app/agent/tools/impl/_command_safety.py", },
"app/agent/tools/impl/_file_write_utils.py", {
"app/agent/tools/impl/_filter_rule_utils.py", "mode": "public_facade",
"app/agent/tools/impl/_music_utils.py", "path": "app/db/__init__.py",
"app/agent/tools/impl/_plugin_tool_utils.py", "reason": "数据库包根保留 Engine 特殊解析和生成的历史兼容导出表,宿主直接导入 owner 子模块。"
"app/agent/tools/impl/_system_setting_utils.py", },
"app/agent/tools/impl/_terminal_session.py", {
"app/agent/tools/impl/_torrent_search_utils.py", "mode": "implementation",
"app/agent/tools/impl/add_custom_filter_rule.py", "path": "app/agent/callback/__init__.py",
"app/agent/tools/impl/add_download_tasks.py", "reason": "StreamingHandler 仍是消息回调能力的既有 owner,后续拆分必须单独迁移状态和插件合同。"
"app/agent/tools/impl/add_rule_group.py", },
"app/agent/tools/impl/add_subscribe.py", {
"app/agent/tools/impl/apply_patch.py", "mode": "implementation",
"app/agent/tools/impl/ask_user_choice.py", "path": "app/agent/capabilities/__init__.py",
"app/agent/tools/impl/browse_webpage.py", "reason": "包根集中声明无实现副作用的 Agent capability 标识常量。"
"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", "mode": "implementation",
"app/agent/tools/impl/delete_download_history.py", "path": "app/agent/memory/__init__.py",
"app/agent/tools/impl/delete_download_tasks.py", "reason": "MemoryManager 是既有会话记忆 owner,当前包根不是对子模块的重复导出门面。"
"app/agent/tools/impl/delete_rule_group.py", },
"app/agent/tools/impl/delete_subscribe.py", {
"app/agent/tools/impl/delete_transfer_history.py", "mode": "public_facade",
"app/agent/tools/impl/edit_file.py", "path": "app/agent/middleware/__init__.py",
"app/agent/tools/impl/execute_command.py", "reason": "LangChain middleware 构建入口由包根集中公开,具体实现仍从单词子模块导入。"
"app/agent/tools/impl/get_recommendations.py", },
"app/agent/tools/impl/get_search_results.py", {
"app/agent/tools/impl/install_plugin.py", "mode": "public_facade",
"app/agent/tools/impl/list_directory.py", "path": "app/agent/policy/__init__.py",
"app/agent/tools/impl/list_slash_commands.py", "reason": "策略包根按显式 __all__ 提供稳定懒加载 API。"
"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", "mode": "implementation",
"app/agent/tools/impl/query_custom_identifiers.py", "path": "app/agent/prompt/__init__.py",
"app/agent/tools/impl/query_directory_settings.py", "reason": "PromptManager 与系统任务定义仍共同拥有提示配置加载合同。"
"app/agent/tools/impl/query_doctor_report.py", },
"app/agent/tools/impl/query_download_tasks.py", {
"app/agent/tools/impl/query_downloaders.py", "mode": "public_facade",
"app/agent/tools/impl/query_episode_schedule.py", "path": "app/agent/tools/__init__.py",
"app/agent/tools/impl/query_installed_plugins.py", "reason": "工具包根只负责稳定 Tool 类型的延迟解析。"
"app/agent/tools/impl/query_library_exists.py", },
"app/agent/tools/impl/query_library_latest.py", {
"app/agent/tools/impl/query_market_plugins.py", "mode": "implementation",
"app/agent/tools/impl/query_media_detail.py", "path": "app/db/models/__init__.py",
"app/agent/tools/impl/query_personas.py", "reason": "load_all_models 是数据库组合根显式加载全部 ORM 映射的唯一入口。"
"app/agent/tools/impl/query_plugin_capabilities.py", },
"app/agent/tools/impl/query_plugin_config.py", {
"app/agent/tools/impl/query_plugin_data.py", "mode": "public_facade",
"app/agent/tools/impl/query_popular_subscribes.py", "path": "app/db/oper/__init__.py",
"app/agent/tools/impl/query_rule_groups.py", "reason": "历史 Oper 包根符号由精确懒加载兼容表提供,宿主直接导入具体 oper。"
"app/agent/tools/impl/query_schedulers.py", },
"app/agent/tools/impl/query_site_userdata.py", {
"app/agent/tools/impl/query_sites.py", "mode": "public_facade",
"app/agent/tools/impl/query_subscribe_history.py", "path": "app/doctor/__init__.py",
"app/agent/tools/impl/query_subscribe_shares.py", "reason": "doctor 包根保留既有诊断 API 的精确懒加载表。"
"app/agent/tools/impl/query_subscribes.py", },
"app/agent/tools/impl/query_system_settings.py", {
"app/agent/tools/impl/query_transfer_history.py", "mode": "implementation",
"app/agent/tools/impl/query_workflows.py", "path": "app/modules/__init__.py",
"app/agent/tools/impl/read_file.py", "reason": "ServiceBase 是模块发现框架的历史公共基类,具体模块子包另按 Module 入口合同校验。"
"app/agent/tools/impl/recognize_captcha.py", },
"app/agent/tools/impl/recognize_media.py", {
"app/agent/tools/impl/reload_plugin.py", "mode": "implementation",
"app/agent/tools/impl/run_agent_task.py", "path": "app/modules/filemanager/storages/__init__.py",
"app/agent/tools/impl/run_scheduler.py", "reason": "StorageBase 与 transfer_process 共同定义文件管理存储驱动合同。"
"app/agent/tools/impl/run_slash_command.py", },
"app/agent/tools/impl/run_workflow.py", {
"app/agent/tools/impl/scrape_metadata.py", "mode": "implementation",
"app/agent/tools/impl/search_media.py", "path": "app/modules/indexer/parser/__init__.py",
"app/agent/tools/impl/search_person.py", "reason": "SiteSchema 与 SiteParserBase 是按解析器文件名发现实现的框架合同。"
"app/agent/tools/impl/search_person_credits.py", },
"app/agent/tools/impl/search_subscribe.py", {
"app/agent/tools/impl/search_torrents.py", "mode": "implementation",
"app/agent/tools/impl/search_web.py", "path": "app/modules/indexer/spider/__init__.py",
"app/agent/tools/impl/send_local_file.py", "reason": "SiteSpider 与分类投影函数共同定义站点 Spider 基础合同。"
"app/agent/tools/impl/send_message.py", },
"app/agent/tools/impl/send_voice_message.py", {
"app/agent/tools/impl/switch_persona.py", "mode": "contained_vendor",
"app/agent/tools/impl/test_site.py", "path": "app/modules/themoviedb/tmdbv3api/__init__.py",
"app/agent/tools/impl/transfer_file.py", "reason": "内含 tmdbv3api 兼容库维持其上游包根导出合同。"
"app/agent/tools/impl/uninstall_plugin.py", },
"app/agent/tools/impl/update_agent_task.py", {
"app/agent/tools/impl/update_custom_filter_rule.py", "mode": "contained_vendor",
"app/agent/tools/impl/update_custom_identifiers.py", "path": "app/modules/themoviedb/tmdbv3api/objs/__init__.py",
"app/agent/tools/impl/update_download_tasks.py", "reason": "内含 tmdbv3api 对象包维持上游结构,不作为宿主新模块模板。"
"app/agent/tools/impl/update_persona_definition.py", },
"app/agent/tools/impl/update_plugin_config.py", {
"app/agent/tools/impl/update_rule_group.py", "mode": "public_facade",
"app/agent/tools/impl/update_site.py", "path": "app/monitor/__init__.py",
"app/agent/tools/impl/update_site_cookie.py", "reason": "Monitor 包根保留稳定监控入口的显式懒加载表。"
"app/agent/tools/impl/update_subscribe.py", },
"app/agent/tools/impl/update_system_settings.py", {
"app/agent/tools/impl/write_file.py", "mode": "public_facade",
"app/api/openai_utils.py", "path": "app/runtime/capabilities/__init__.py",
"app/api/router_specs.py", "reason": "Capability Runtime 公共类型通过显式 __all__ 延迟解析。"
"app/chain/_contracts.py", },
"app/chain/_interaction.py", {
"app/chain/_messaging.py", "mode": "implementation",
"app/chain/_music.py", "path": "app/runtime/observability/__init__.py",
"app/chain/_recognition.py", "reason": "低基数观测合同与无操作实现共同组成一个轻量运行时 owner。"
"app/db/models/_constraints.py", },
"app/db/models/_identity.py", {
"app/modules/_base/media_auxiliary.py", "mode": "public_facade",
"app/modules/indexer/parser/file_list.py", "path": "app/schemas/__init__.py",
"app/modules/indexer/parser/ipt_project.py", "reason": "Schemas 包根维持生成的插件兼容导出清单,宿主直接导入具体 schema。"
"app/modules/indexer/parser/nexus_audiences.py", },
"app/modules/indexer/parser/nexus_hhanclub.py", {
"app/modules/indexer/parser/nexus_php.py", "mode": "implementation",
"app/modules/indexer/parser/nexus_project.py", "path": "app/startup/lifecycle/__init__.py",
"app/modules/indexer/parser/nexus_rabbit.py", "reason": "lifespan 与启动关闭步骤仍由同一组合根 owner 持有,并非子模块实现重导出。"
"app/modules/indexer/parser/small_horse.py", },
"app/modules/indexer/parser/torrent_leech.py", {
"app/modules/musicbrainz/music_cache.py", "mode": "public_facade",
"app/modules/themoviedb/tmdb_cache.py", "path": "app/testing/__init__.py",
"app/modules/themoviedb/tmdbv3api/as_obj.py", "reason": "测试支持包保留 stub_modules 的便捷公开入口,不参与生产运行时。"
"app/modules/thetvdb/tvdb_v4_official.py", },
"app/runtime/compat/resource_imports.py", {
"app/runtime/extensions/host_module_adapter.py", "mode": "implementation",
"app/runtime/extensions/module_manager.py", "path": "app/workflow/__init__.py",
"app/runtime/extensions/plugin_manager.py", "reason": "WorkflowManager 是历史工作流发现与执行 owner,拆分需保持运行时身份。"
"app/runtime/extensions/service_config.py", },
"app/testing/network_guard.py", {
"app/workflow/actions/add_download.py", "mode": "implementation",
"app/workflow/actions/add_subscribe.py", "path": "app/workflow/actions/__init__.py",
"app/workflow/actions/fetch_downloads.py", "reason": "BaseAction 与 ActionChain 定义按动作文件名发现的工作流扩展合同。"
"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"
], ],
"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": { "scope": {
"excluded": [ "excluded": [
"app/plugins" "app/plugins"
@@ -155,5 +187,27 @@
"special": [ "special": [
"__init__.py" "__init__.py"
] ]
},
"semantic_filename_contracts": [
{
"kind": "agent_tool_contract",
"reason": "Agent 工具文件名就是向模型公开的稳定 tool name,必须与类内 name 字段逐项一致。",
"root": "app/agent/tools/impl"
},
{
"kind": "agent_tool_private_helper",
"reason": "工具私有共享实现已归入 impl 能力包且以下划线隔离,不形成宿主公开模块。",
"root": "app/agent/tools/impl"
},
{
"kind": "indexer_parser_contract",
"reason": "索引解析器文件名由 SiteUserInfo 类名和站点 schema 发现合同共同确定。",
"root": "app/modules/indexer/parser"
},
{
"kind": "workflow_action_contract",
"reason": "工作流动作文件名是持久化动作类型的稳定发现标识,必须与 Action 类名逐项一致。",
"root": "app/workflow/actions"
} }
]
} }
+7 -7
View File
@@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
from app.agent.middleware.activity_log import ( from app.agent.middleware.activity import (
ActivityLogMiddleware, ActivityLogMiddleware,
QUERY_ACTIVITY_LOG_TOOL_DESCRIPTION, QUERY_ACTIVITY_LOG_TOOL_DESCRIPTION,
QUERY_ACTIVITY_LOG_TOOL_NAME, QUERY_ACTIVITY_LOG_TOOL_NAME,
@@ -111,7 +111,7 @@ def test_activity_log_skips_trivial_greeting_without_llm(tmp_path):
with ( with (
patch( patch(
"app.agent.middleware.activity_log._summarize_with_llm", "app.agent.middleware.activity._summarize_with_llm",
new=summarize_mock, new=summarize_mock,
), ),
patch.object(middleware, "_append_activity", new=append_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(): async def _run_test():
middleware = ActivityLogMiddleware(activity_dir=str(tmp_path)) middleware = ActivityLogMiddleware(activity_dir=str(tmp_path))
with patch( with patch(
"app.agent.middleware.activity_log._summarize_with_llm", "app.agent.middleware.activity._summarize_with_llm",
new=AsyncMock(return_value=summary), new=AsyncMock(return_value=summary),
): ):
await middleware.aafter_agent( await middleware.aafter_agent(
@@ -234,7 +234,7 @@ def test_activity_log_after_agent_does_not_wait_for_summary(tmp_path):
append_mock = AsyncMock() append_mock = AsyncMock()
with ( with (
patch( patch(
"app.agent.middleware.activity_log._summarize_with_llm", "app.agent.middleware.activity._summarize_with_llm",
side_effect=_slow_summarize, side_effect=_slow_summarize,
) as summarize_mock, ) as summarize_mock,
patch.object(middleware, "_append_activity", new=append_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): async def _failing_handler(_request):
raise RuntimeError(f"Authorization: Bearer {secret_marker}") 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: try:
await middleware.awrap_tool_call(request, _failing_handler) await middleware.awrap_tool_call(request, _failing_handler)
except RuntimeError: except RuntimeError:
@@ -503,10 +503,10 @@ def test_activity_log_provider_error_does_not_echo_secret(tmp_path):
with ( with (
patch( 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}"), 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( result = asyncio.run(
middleware._tool_provider.query_activity_log(keyword="visible") middleware._tool_provider.query_activity_log(keyword="visible")
+1 -1
View File
@@ -7,7 +7,7 @@ from langchain_core.messages import AIMessage, HumanMessage
from app.agent.contracts import ReplyMode from app.agent.contracts import ReplyMode
from app.agent.manager import AgentManager from app.agent.manager import AgentManager
from app.agent.memory import memory_manager 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.skills import SKILL_TOOL_NAME
from app.agent.middleware.subagents import ( from app.agent.middleware.subagents import (
SUBAGENT_CONTROL_TOOL_NAME, SUBAGENT_CONTROL_TOOL_NAME,
+3 -3
View File
@@ -145,7 +145,7 @@ from typing import get_args, get_type_hints
from app.testing.bootstrap import ensure_sites_stub from app.testing.bootstrap import ensure_sites_stub
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() factory = get_tool_factory()
catalog = factory.create_catalog(session_id="lazy", user_id="api") 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: def test_manager_first_catalog_use_is_single_flight(monkeypatch) -> None:
"""并发首次查询只能在 manager 锁内建立一次会话工具快照。""" """并发首次查询只能在 manager 锁内建立一次会话工具快照。"""
from app.agent import runtime_loader from app.agent import loader as runtime_loader
from app.agent.tools.manager import MoviePilotToolsManager from app.agent.tools.manager import MoviePilotToolsManager
calls: list[tuple[str, str]] = [] calls: list[tuple[str, str]] = []
@@ -240,7 +240,7 @@ def test_legacy_explicit_tool_refresh_keeps_atomic_catalog_contract(
monkeypatch, monkeypatch,
) -> None: ) -> None:
"""插件显式刷新旧入口应继续发布同一次构造的完整目录快照。""" """插件显式刷新旧入口应继续发布同一次构造的完整目录快照。"""
from app.agent import runtime_loader from app.agent import loader as runtime_loader
from app.agent.tools.manager import MoviePilotToolsManager from app.agent.tools.manager import MoviePilotToolsManager
calls: list[int] = [] calls: list[int] = []
+1 -1
View File
@@ -3,7 +3,7 @@ import unittest
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage 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: def _build_tool_call(tool_call_id: str = "call_1", name: str = "search") -> dict:
+1 -1
View File
@@ -12,7 +12,7 @@ from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langchain_core.tools import tool from langchain_core.tools import tool
from app.agent import MoviePilotAgent 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.agent.middleware.usage import UsageMiddleware
from app.chain.message import MessageChain from app.chain.message import MessageChain
+1 -1
View File
@@ -22,7 +22,7 @@ from app.runtime.capabilities.model import (
@pytest.fixture @pytest.fixture
def runtime_loader(monkeypatch): def runtime_loader(monkeypatch):
"""为每个用例提供未构建、未关闭的 Agent Capability Runtime。""" """为每个用例提供未构建、未关闭的 Agent Capability Runtime。"""
from app.agent import runtime_loader as module from app.agent import loader as module
monkeypatch.setattr(module, "_agent_runtime", None) monkeypatch.setattr(module, "_agent_runtime", None)
for implementation_module in ( for implementation_module in (
+1 -1
View File
@@ -17,7 +17,7 @@ from langchain_core.messages import AIMessage
from app.agent.contracts import ReplyMode from app.agent.contracts import ReplyMode
from app.agent.manager import AgentManager 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.orchestrator import MoviePilotAgent
from app.agent.session import _MessageTask from app.agent.session import _MessageTask
from app.agent.tools.factory import MoviePilotToolFactory from app.agent.tools.factory import MoviePilotToolFactory
+1 -1
View File
@@ -25,7 +25,7 @@ from pydantic import Field
import app.agent.orchestrator as agent_module import app.agent.orchestrator as agent_module
from app.agent.memory import MemoryManager 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 ( from app.agent.middleware.summarization import (
ContextPreservingSummarizationMiddleware, ContextPreservingSummarizationMiddleware,
ContextSummarizationError, ContextSummarizationError,
+1 -1
View File
@@ -11,7 +11,7 @@ from app.agent.tools.catalog import (
ToolIdentityAmbiguousError, ToolIdentityAmbiguousError,
) )
from app.agent.tools.factory import MoviePilotToolFactory 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): class _Arguments(BaseModel):
+2 -2
View File
@@ -5,14 +5,14 @@ from unittest.mock import patch
import pytest import pytest
from pydantic import BaseModel 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.middleware.skills import SkillToolInput
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.factory import MoviePilotToolFactory from app.agent.tools.factory import MoviePilotToolFactory
from app.agent.tools.impl.ask_user_choice import AskUserChoiceInput, AskUserChoiceTool 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_local_file import SendLocalFileTool
from app.agent.tools.impl.send_voice_message import SendVoiceMessageTool 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 from app.foundation.singleton import Singleton
+1 -1
View File
@@ -8,7 +8,7 @@ from langchain_core.messages import ToolMessage
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import app.agent.orchestrator as agent_module 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.memory import MemoryMiddleware
from app.agent.middleware.policy import AgentPolicyMiddleware from app.agent.middleware.policy import AgentPolicyMiddleware
from app.agent.middleware.summarization import FinalRequestCompactionMiddleware from app.agent.middleware.summarization import FinalRequestCompactionMiddleware
+1 -1
View File
@@ -4,7 +4,7 @@ from unittest.mock import patch
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage from 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 from app.agent.tools.tags import ToolTag
+1 -1
View File
@@ -51,7 +51,7 @@ class Item(BaseModel):
def _v1_compat_routes() -> list[tuple[str, APIRoute]]: def _v1_compat_routes() -> list[tuple[str, APIRoute]]:
"""返回兼容 v1 导出的公开路由,不依赖 FastAPI 内部 include 包装器。""" """返回兼容 v1 导出的公开路由,不依赖 FastAPI 内部 include 包装器。"""
from app.api.router_specs import API_V1_ROUTER_SPECS from app.api.routers import API_V1_ROUTER_SPECS
return [ return [
(f"{spec.prefix}{route.path}", route) (f"{spec.prefix}{route.path}", route)
+39 -12
View File
@@ -79,6 +79,28 @@ RETIRED_CANONICAL_FILES = (
"app/runtime/runtime.py", "app/runtime/runtime.py",
"app/runtime/dependencies.py", "app/runtime/dependencies.py",
"app/runtime/native_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/chain/media.py",
"app/adapters/network/rss.py", "app/adapters/network/rss.py",
"app/adapters/network/sites.pyi", "app/adapters/network/sites.pyi",
@@ -169,22 +191,22 @@ FORBIDDEN_IMPORT_PREFIXES = {
"app.sdk", "app.sdk",
), ),
"app.api": ( "app.api": (
"app.runtime.extensions.plugin_manager", "app.runtime.extensions.plugin.manager",
"app.runtime.extensions.module_manager", "app.runtime.extensions.module.manager",
"app.scheduler", "app.scheduler",
), ),
"app.agent": ( "app.agent": (
"app.runtime.extensions.plugin_manager", "app.runtime.extensions.plugin.manager",
"app.runtime.extensions.module_manager", "app.runtime.extensions.module.manager",
), ),
"app.chain": ( "app.chain": (
"app.runtime.extensions.plugin_manager", "app.runtime.extensions.plugin.manager",
"app.runtime.extensions.module_manager", "app.runtime.extensions.module.manager",
"app.runtime.extensions.module.dispatcher", "app.runtime.extensions.module.dispatcher",
), ),
"app.workflow": ( "app.workflow": (
"app.runtime.extensions.plugin_manager", "app.runtime.extensions.plugin.manager",
"app.runtime.extensions.module_manager", "app.runtime.extensions.module.manager",
), ),
} }
@@ -1387,7 +1409,7 @@ def test_canonical_service_config_consumers_use_application_directory():
for path in paths: for path in paths:
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
for node in ast.walk(tree): 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}") violations.append(f"{path.relative_to(PROJECT_ROOT).as_posix()}:{node.lineno}")
if ( if (
path.name == "mediaserver.py" 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(): def test_plugin_components_do_not_reexport_legacy_abi_names():
"""插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出""" """插件组件只在 manager owner 定义 PluginManager,不得复制旧 ABI"""
violations: list[str] = [] violations: list[str] = []
manager_path = APP_ROOT / "runtime" / "extensions" / "plugin" / "manager.py"
for root in PLUGIN_COMPONENT_ROOTS: for root in PLUGIN_COMPONENT_ROOTS:
for path in (PROJECT_ROOT / root).rglob("*.py"): for path in (PROJECT_ROOT / root).rglob("*.py"):
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) 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: if node.name == "__getattr__" or node.name in PLUGIN_LEGACY_ABI_NAMES:
violations.append(f"{path.relative_to(PROJECT_ROOT)}:{node.name}") violations.append(f"{path.relative_to(PROJECT_ROOT)}:{node.name}")
elif isinstance(node, ast.ClassDef): 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}") violations.append(f"{path.relative_to(PROJECT_ROOT)}:{node.name}")
elif isinstance(node, ast.ImportFrom): elif isinstance(node, ast.ImportFrom):
for alias in node.names: for alias in node.names:
@@ -1990,7 +2017,7 @@ def test_host_consumers_get_agent_manager_through_application_facade():
imported = { imported = {
alias.name alias.name
for node in ast.walk(tree) 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 for alias in node.names
if alias.name in forbidden if alias.name in forbidden
} }
+2 -2
View File
@@ -82,7 +82,7 @@ FROZEN_EGRESS_EDGES_BY_REASON = {
("app.runtime.state", "docker"), ("app.runtime.state", "docker"),
}, },
"test_network_guard": { "test_network_guard": {
("app.testing.network_guard", "socket.getaddrinfo"), ("app.testing.network", "socket.getaddrinfo"),
}, },
} }
FROZEN_EGRESS_REASON_BY_EDGE = { FROZEN_EGRESS_REASON_BY_EDGE = {
@@ -143,7 +143,7 @@ FROZEN_EGRESS_FINGERPRINT_BY_EDGE = {
("app.modules.zspace.zspace", "requests"): "9df3fd27b9696d45a72e7c8f67b5a9ad79a7371d1fe690bbaa17485bd1960d51", ("app.modules.zspace.zspace", "requests"): "9df3fd27b9696d45a72e7c8f67b5a9ad79a7371d1fe690bbaa17485bd1960d51",
("app.runtime.state", "docker"): "20a91ec521f7dfe6a0153dfd8ea49c4bac7f0a16b55f1c0655dfb33f54a01215", ("app.runtime.state", "docker"): "20a91ec521f7dfe6a0153dfd8ea49c4bac7f0a16b55f1c0655dfb33f54a01215",
("app.startup.lifecycle", "urllib3"): "cb6f0a314aeb1e2d3e76c240aa20460ac0c36d9f5c18c1a6ea3170f64dd3366b", ("app.startup.lifecycle", "urllib3"): "cb6f0a314aeb1e2d3e76c240aa20460ac0c36d9f5c18c1a6ea3170f64dd3366b",
("app.testing.network_guard", "socket.getaddrinfo"): "2518de211c9ba32ccbc004cf58fe9c98837fe8ac95fe8ffb6ea82897b28d753f", ("app.testing.network", "socket.getaddrinfo"): "2518de211c9ba32ccbc004cf58fe9c98837fe8ac95fe8ffb6ea82897b28d753f",
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import asyncio
import threading import threading
from types import SimpleNamespace 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 from app.agent.tools.impl.delete_transfer_history import DeleteTransferHistoryTool
+2 -2
View File
@@ -17,7 +17,7 @@ def _load_downloader_base():
runtime_module.__path__ = [] runtime_module.__path__ = []
runtime_extensions_module = types.ModuleType("app.runtime.extensions") runtime_extensions_module = types.ModuleType("app.runtime.extensions")
runtime_extensions_module.__path__ = [] 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") log_module = types.ModuleType("app.runtime.log")
schemas_module = types.ModuleType("app.schemas") schemas_module = types.ModuleType("app.schemas")
schemas_module.__path__ = [] schemas_module.__path__ = []
@@ -100,7 +100,7 @@ def _load_downloader_base():
"app.helper": helper_module, "app.helper": helper_module,
"app.runtime": runtime_module, "app.runtime": runtime_module,
"app.runtime.extensions": runtime_extensions_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.runtime.log": log_module,
"app.schemas": schemas_module, "app.schemas": schemas_module,
"app.schemas.message": schema_message_module, "app.schemas.message": schema_message_module,
+1 -1
View File
@@ -3,7 +3,7 @@
import pytest import pytest
from app.runtime.events import Event, EventManager 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 from app.schemas.types import ChainEventType
+1 -1
View File
@@ -1,7 +1,7 @@
from types import SimpleNamespace from types import SimpleNamespace
from app.agent.prompt import prompt_manager 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(): def test_batch_manual_redo_prompt_requires_plain_text_result():
+2 -2
View File
@@ -9,8 +9,8 @@ from types import ModuleType
import pytest import pytest
from app.runtime.compat import resource_imports from app.runtime.compat import resources as resource_imports
from app.runtime.compat.resource_imports import ( from app.runtime.compat.resources import (
RESOURCE_IMPORT_RULES, RESOURCE_IMPORT_RULES,
PluginResourceImportScanError, PluginResourceImportScanError,
scan_plugin_resource_imports, scan_plugin_resource_imports,
+2 -2
View File
@@ -110,8 +110,8 @@ from app.application.workflow import (
get_configured_workflow_execution, get_configured_workflow_execution,
reset_workflow_execution, reset_workflow_execution,
) )
from app.runtime.extensions import service_config as service_config_module from app.runtime.extensions import service as service_config_module
from app.runtime.extensions.service_config import ( from app.runtime.extensions.service import (
configure_service_config_reader, configure_service_config_reader,
reset_service_config_reader, reset_service_config_reader,
) )
+1 -1
View File
@@ -7,7 +7,7 @@ import pytest
from app.agent.llm import LLMHelper from app.agent.llm import LLMHelper
from app.agent.llm.provider import LLMProviderManager from app.agent.llm.provider import LLMProviderManager
from app.agent.llm.server_tools import ( from app.agent.llm.tools import (
ServerToolRegistry, ServerToolRegistry,
ServerToolUnavailableError, ServerToolUnavailableError,
) )
+1 -1
View File
@@ -11,7 +11,7 @@ from app.agent.tools.factory import MoviePilotToolFactory
from app.agent.tools.manager import MoviePilotToolsManager from app.agent.tools.manager import MoviePilotToolsManager
from app.agent.tools.catalog import ToolCatalogSnapshot from app.agent.tools.catalog import ToolCatalogSnapshot
from app.api.endpoints import mcp 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 from app.foundation.singleton import Singleton
+1 -1
View File
@@ -6,7 +6,7 @@ from unittest.mock import patch
from app.chain.search import SearchChain from app.chain.search import SearchChain
from app.domain.context import MediaInfo 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 from app.schemas.types import MediaSource, MediaType
+2 -2
View File
@@ -1,4 +1,4 @@
from app.runtime.extensions.service_config import ServiceConfigHelper from app.runtime.extensions.service import ServiceConfigHelper
from app.schemas.system import MediaServerConf from app.schemas.system import MediaServerConf
from app.schemas.types import SystemConfigKey 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): def test_get_configs_skips_invalid_entries(monkeypatch):
"""单条配置校验失败时应跳过该条,不影响其它服务配置的加载。""" """单条配置校验失败时应跳过该条,不影响其它服务配置的加载。"""
monkeypatch.setattr( monkeypatch.setattr(
"app.runtime.extensions.service_config._service_config_reader", "app.runtime.extensions.service._service_config_reader",
lambda key: [ lambda key: [
{"name": "good", "type": "emby", "enabled": True}, {"name": "good", "type": "emby", "enabled": True},
"bad-format", "bad-format",
+11 -11
View File
@@ -19,9 +19,9 @@ from app.runtime.capabilities.errors import CapabilityRuntimeClosedError
from app.runtime.capabilities.model import CapabilityLifecycleState, SelectorSchema from app.runtime.capabilities.model import CapabilityLifecycleState, SelectorSchema
from app.runtime.capabilities.registry import CapabilityRegistry from app.runtime.capabilities.registry import CapabilityRegistry
from app.runtime.events import Event, EventHandlerBinding, eventmanager from app.runtime.events import Event, EventHandlerBinding, eventmanager
from app.runtime.extensions import module_manager as module_manager_extension from app.runtime.extensions.module import manager as module_manager_extension
from app.runtime.extensions.module_manager import ModuleManager from app.runtime.extensions.module.manager import ModuleManager
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.event import ConfigChangeEventData
from app.schemas.types import EventType from app.schemas.types import EventType
@@ -551,7 +551,7 @@ def test_all_real_host_modules_zero_arg_construct_without_starting_resources(
) -> None: ) -> None:
"""每份真实 manifest 都必须能解析 canonical class 并零参数构造且不启动资源。""" """每份真实 manifest 都必须能解析 canonical class 并零参数构造且不启动资源。"""
body = r""" body = r"""
from app.runtime.extensions.host_module_adapter import ( from app.runtime.extensions.module.adapter import (
HostModuleAdapter, HostModuleAdapter,
build_host_module_registry, 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.capabilities.model import ActivationPolicy
from app.runtime.config import settings from app.runtime.config import settings
from app.runtime.events import Event from app.runtime.events import Event
from app.runtime.extensions.host_module_adapter import ( from app.runtime.extensions.module.adapter import (
HostModuleAdapter, HostModuleAdapter,
build_host_module_registry, 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.event import ConfigChangeEventData
from app.schemas.types import EventType from app.schemas.types import EventType
@@ -655,7 +655,7 @@ def get_config(_self, key=None):
SystemConfigOper.get = get_config SystemConfigOper.get = get_config
configure_service_config_reader(lambda key: SystemConfigOper().get(key)) 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() manager = ModuleManager()
bootstrap_ids = { 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.db.oper.systemconfig import SystemConfigOper
from app.runtime.capabilities.model import ActivationPolicy from app.runtime.capabilities.model import ActivationPolicy
from app.runtime.config import settings from app.runtime.config import settings
from app.runtime.extensions.host_module_adapter import ( from app.runtime.extensions.module.adapter import (
HostModuleAdapter, HostModuleAdapter,
build_host_module_registry, build_host_module_registry,
) )
@@ -802,7 +802,7 @@ for spec in specs:
assert configured_modules.isdisjoint(sys.modules) assert configured_modules.isdisjoint(sys.modules)
from app.runtime.extensions.module_manager import ModuleManager from app.runtime.extensions.module.manager import ModuleManager
manager = ModuleManager() manager = ModuleManager()
assert manager.get_specs() == manager.list_specs() assert manager.get_specs() == manager.list_specs()
@@ -862,7 +862,7 @@ SystemConfigOper.get = empty_config
settings.ACOUSTID_API_KEY = None settings.ACOUSTID_API_KEY = None
settings.FANART_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 from app.application.module import configure_module_runtime
configure_module_runtime(lambda: ModuleManager()) configure_module_runtime(lambda: ModuleManager())
@@ -978,7 +978,7 @@ from app.runtime.config import settings
settings.ACOUSTID_API_KEY = None settings.ACOUSTID_API_KEY = None
settings.FANART_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() manager = ModuleManager()
modules = manager.get_modules() modules = manager.get_modules()
+1 -1
View File
@@ -21,7 +21,7 @@ from app.schemas.types import MediaSource, MediaType
def test_music_routes_are_registered(): def test_music_routes_are_registered():
"""V1 API 应注册音乐详情识别、探索及艺术家专辑浏览路由。""" """V1 API 应注册音乐详情识别、探索及艺术家专辑浏览路由。"""
from app.api.router_specs import API_V1_ROUTER_SPECS from app.api.routers import API_V1_ROUTER_SPECS
routes = { routes = {
(f"{spec.prefix}{route.path}", tuple(route.methods or [])) (f"{spec.prefix}{route.path}", tuple(route.methods or []))
+2 -2
View File
@@ -12,9 +12,9 @@ from app.api.endpoints import music as music_endpoint
from app.domain.context import MusicInfo from app.domain.context import MusicInfo
from app.domain.meta.metamusic import MetaMusic from app.domain.meta.metamusic import MetaMusic
from app.api.deps import get_current_active_superuser_async 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 import MusicBrainzModule
from app.modules.musicbrainz.music_cache import MusicBrainzCache from app.modules.musicbrainz.cache import MusicBrainzCache
class _MemoryCacheStub: class _MemoryCacheStub:
+1 -1
View File
@@ -2,7 +2,7 @@
from unittest.mock import Mock from unittest.mock import Mock
from app import schemas 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.domain.context import MusicInfo
from app.modules.navidrome import NavidromeModule from app.modules.navidrome import NavidromeModule
from app.modules.navidrome.navidrome import Navidrome from app.modules.navidrome.navidrome import Navidrome
+1 -1
View File
@@ -2,7 +2,7 @@ import socket
import pytest 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): def test_network_guard_fails_when_blocked_attempt_is_swallowed(monkeypatch):
+1 -1
View File
@@ -17,7 +17,7 @@ from app.adapters.external.server import MoviePilotServerHelper
from app.adapters.web.metrics import HttpMetricsMiddleware from app.adapters.web.metrics import HttpMetricsMiddleware
from app.db.engine import _register_database_pool_metrics from app.db.engine import _register_database_pool_metrics
from app.runtime.extensions.plugin.lifecycle import observe_plugin_lifecycle 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.schemas.plugin import PluginRuntimeStatus
from app.runtime.observability import ( from app.runtime.observability import (
METRIC_SPECS, METRIC_SPECS,
+1 -1
View File
@@ -1,6 +1,6 @@
from unittest import TestCase from unittest import TestCase
from app.api.openai_utils import ( from app.api.protocol import (
build_anthropic_messages, build_anthropic_messages,
build_completion_payload, build_completion_payload,
build_prompt, build_prompt,
+142 -7
View File
@@ -71,6 +71,90 @@ def _public_names(tree: ast.AST) -> set[str]:
return {name for name in names if not name.startswith("_")} 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: def test_official_plugin_legacy_imports_are_exactly_routed_or_retired() -> None:
"""官方插件旧导入必须有精确 Compat 路由,或保持一条已审查的可选退役路径。""" """官方插件旧导入必须有精确 Compat 路由,或保持一条已审查的可选退役路径。"""
baseline = json.loads(OFFICIAL_PLUGIN_BASELINE.read_text(encoding="utf-8")) 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: def test_plugin_manager_legacy_and_sdk_paths_share_canonical_identity() -> None:
"""V2 旧路径与 V3 SDK 必须复用同一个 PluginManager 身份及已验证成员。""" """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") legacy = importlib.import_module("app.core.plugin")
sdk = importlib.import_module("app.sdk.plugins") 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 = { forbidden_modules = {
"app.adapters.external.market", "app.adapters.external.market",
"app.adapters.external.server", "app.adapters.external.server",
"app.runtime.extensions.plugin_manager", "app.runtime.extensions.plugin.manager",
} }
forbidden_names = {"MoviePilotServerHelper", "PluginHelper", "PluginManager"} forbidden_names = {"MoviePilotServerHelper", "PluginHelper", "PluginManager"}
paths = [APP_ROOT / "api" / "endpoints" / "plugin.py"] 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: def test_plugin_manager_catalog_compat_methods_remain_thin_delegates() -> None:
"""PluginManager 的旧市场目录方法只能委托,不得重新吸收应用目录算法。""" """PluginManager 的旧市场目录方法只能委托,不得重新吸收应用目录算法。"""
path = APP_ROOT / "runtime" / "extensions" / "plugin_manager.py" path = APP_ROOT / "runtime" / "extensions" / "plugin" / "manager.py"
tree = _parse(path) tree = _parse(path)
manager = next( manager = next(
node 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: def test_new_host_production_modules_use_single_word_filenames() -> None:
"""宿主多词生产文件必须等于审查低水位,新增文件只能使用单个小写词""" """宿主多词文件必须满足稳定发现合同或带有可审查的逐项理由"""
policy = json.loads(FILENAME_POLICY.read_text(encoding="utf-8")) policy = json.loads(FILENAME_POLICY.read_text(encoding="utf-8"))
assert policy["schema_version"] == 1 assert policy["schema_version"] == 2
assert policy["scope"] == { assert policy["scope"] == {
"root": "app", "root": "app",
"excluded": ["app/plugins"], "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]*") single_word = re.compile(r"[a-z][a-z0-9]*")
actual = sorted( actual = sorted(
path.relative_to(PROJECT_ROOT).as_posix() path
for path in APP_ROOT.rglob("*.py") for path in APP_ROOT.rglob("*.py")
if not path.is_relative_to(APP_ROOT / "plugins") if not path.is_relative_to(APP_ROOT / "plugins")
and path.name != "__init__.py" and path.name != "__init__.py"
and not single_word.fullmatch(path.stem) and not single_word.fullmatch(path.stem)
) )
assert actual == policy["grandfathered_multiword_files"] contracts = policy["semantic_filename_contracts"]
reviewed = {item["path"]: item for item in policy["reviewed_multiword_files"]}
assert all(item["reason"].strip() for item in contracts)
assert all(item["reason"].strip() and item["evidence"].strip() for item in reviewed.values())
unmatched = []
contract_paths = set()
for path in actual:
relative = path.relative_to(PROJECT_ROOT).as_posix()
if any(_matches_filename_contract(path, rule) for rule in contracts):
contract_paths.add(relative)
elif relative not in reviewed:
unmatched.append(relative)
assert unmatched == []
assert set(reviewed) == {
path.relative_to(PROJECT_ROOT).as_posix() for path in actual
} - contract_paths
def test_package_roots_are_documentation_facades_or_reasoned_owners() -> None:
"""全局包根只能为空、说明、精确懒门面、模块入口或有理由的既有 owner。"""
policy = json.loads(FILENAME_POLICY.read_text(encoding="utf-8"))
exceptions = {item["path"]: item for item in policy["package_root_exceptions"]}
assert all(
item["mode"] in {"bootstrap", "implementation", "public_facade", "contained_vendor"}
and item["reason"].strip()
for item in exceptions.values()
)
used_exceptions: set[str] = set()
violations: list[str] = []
modules_root = APP_ROOT / "modules"
for path in APP_ROOT.rglob("__init__.py"):
if path.is_relative_to(APP_ROOT / "plugins"):
continue
relative = path.relative_to(PROJECT_ROOT).as_posix()
if relative in exceptions:
used_exceptions.add(relative)
continue
tree = _parse(path)
if _is_documentation_only_root(tree) or _is_curated_lazy_facade(tree):
continue
if path.parent.parent == modules_root and any(
isinstance(node, ast.ClassDef) and node.name.endswith("Module")
for node in tree.body
):
continue
violations.append(relative)
assert violations == []
assert used_exceptions == set(exceptions)
+1 -1
View File
@@ -2,7 +2,7 @@ from types import SimpleNamespace
from typing import Any, Iterator from typing import Any, Iterator
import pytest 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.runtime.extensions.plugin.contracts import PluginDashboardError
from app.foundation.singleton import Singleton from app.foundation.singleton import Singleton
+1 -1
View File
@@ -31,7 +31,7 @@ from app.application.plugin.release import PluginReleaseService
from app.foundation.singleton import Singleton from app.foundation.singleton import Singleton
from app.runtime.config import settings from app.runtime.config import settings
from app.runtime.extensions.plugin.admission import PluginMutationAdmission 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.runtime.tasks import TaskRegistry
from app.schemas.event import PluginDataResetEventData from app.schemas.event import PluginDataResetEventData
from app.schemas.plugin import PluginInstance, PluginRuntimeStatus from app.schemas.plugin import PluginInstance, PluginRuntimeStatus
+8 -8
View File
@@ -55,7 +55,7 @@ def _configure_plugin_catalog_factory(monkeypatch):
from app.adapters.external.plugin.client import PluginMarketClient from app.adapters.external.plugin.client import PluginMarketClient
from app.application.plugin.catalog import PluginCatalogService from app.application.plugin.catalog import PluginCatalogService
from app.foundation.version import compare_version 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.runtime.extensions.plugin.storage import get_plugin_storage
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
original_runtime_setting = plugin_client_module.get_runtime_setting original_runtime_setting = plugin_client_module.get_runtime_setting
@@ -836,7 +836,7 @@ class TestPluginHelper:
插件市场 labels 为列表时应转换为字符串避免响应模型序列化异常 插件市场 labels 为列表时应转换为字符串避免响应模型序列化异常
""" """
try: try:
from app.runtime.extensions.plugin_manager import PluginManager from app.runtime.extensions.plugin.manager import PluginManager
except ModuleNotFoundError as exc: except ModuleNotFoundError as exc:
pytest.skip(f"missing dependency: {exc}") pytest.skip(f"missing dependency: {exc}")
@@ -858,7 +858,7 @@ class TestPluginHelper:
SimpleNamespace(read=lambda _key: []), SimpleNamespace(read=lambda _key: []),
) )
monkeypatch.setattr( monkeypatch.setattr(
"app.runtime.extensions.plugin_manager._site_auth_level_provider", "app.runtime.extensions.plugin.manager._site_auth_level_provider",
lambda: 1, lambda: 1,
) )
monkeypatch.setattr( monkeypatch.setattr(
@@ -879,7 +879,7 @@ class TestPluginHelper:
package.v2.json 中的 v2 原生插件并过滤掉未声明任何版本兼容的 v1 插件 package.v2.json 中的 v2 原生插件并过滤掉未声明任何版本兼容的 v1 插件
""" """
try: try:
from app.runtime.extensions.plugin_manager import PluginManager from app.runtime.extensions.plugin.manager import PluginManager
except ModuleNotFoundError as exc: except ModuleNotFoundError as exc:
pytest.skip(f"missing dependency: {exc}") pytest.skip(f"missing dependency: {exc}")
@@ -930,7 +930,7 @@ class TestPluginHelper:
SimpleNamespace(read=lambda _key: []), SimpleNamespace(read=lambda _key: []),
) )
monkeypatch.setattr( monkeypatch.setattr(
"app.runtime.extensions.plugin_manager._site_auth_level_provider", "app.runtime.extensions.plugin.manager._site_auth_level_provider",
lambda: 1, lambda: 1,
) )
monkeypatch.setattr(PluginMarketTransport, "get_plugins", fake_get_plugins) monkeypatch.setattr(PluginMarketTransport, "get_plugins", fake_get_plugins)
@@ -1092,7 +1092,7 @@ class TestPluginHelper:
全市场刷新不清理 Release 缓存Release 接口按请求仓库协调刷新两类数据 全市场刷新不清理 Release 缓存Release 接口按请求仓库协调刷新两类数据
""" """
try: try:
from app.runtime.extensions.plugin_manager import PluginManager from app.runtime.extensions.plugin.manager import PluginManager
except ModuleNotFoundError as exc: except ModuleNotFoundError as exc:
pytest.skip(f"missing dependency: {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): def test_async_get_online_plugins_force_keeps_release_cache_scoped(self, monkeypatch):
"""异步全市场刷新同样不得清理其他仓库的 Release 缓存。""" """异步全市场刷新同样不得清理其他仓库的 Release 缓存。"""
try: try:
from app.runtime.extensions.plugin_manager import PluginManager from app.runtime.extensions.plugin.manager import PluginManager
except ModuleNotFoundError as exc: except ModuleNotFoundError as exc:
pytest.skip(f"missing dependency: {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): def test_get_local_plugin_version_reads_only_requested_installed_plugin(self, monkeypatch):
"""单插件版本查询不构建全部本地插件信息。""" """单插件版本查询不构建全部本地插件信息。"""
try: try:
from app.runtime.extensions.plugin_manager import PluginManager from app.runtime.extensions.plugin.manager import PluginManager
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
except ModuleNotFoundError as exc: except ModuleNotFoundError as exc:
pytest.skip(f"missing dependency: {exc}") pytest.skip(f"missing dependency: {exc}")
+10 -10
View File
@@ -13,7 +13,7 @@ from app.foundation.singleton import Singleton
from app.runtime.events import Event, eventmanager from app.runtime.events import Event, eventmanager
from app.runtime.extensions.plugin.paths import PluginPathResolver from app.runtime.extensions.plugin.paths import PluginPathResolver
from app.runtime.extensions.plugin.system import get_plugin_system 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 import reconcile as scheduler_reconcile
from app.scheduler.facade import Scheduler from app.scheduler.facade import Scheduler
from app.scheduler.registry import ExecutionRegistry 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: def _patch_plugin_runtime_settings(monkeypatch, settings) -> None:
"""以只读键值端口注入插件运行配置。""" """以只读键值端口注入插件运行配置。"""
for target in ( 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", "app.adapters.external.plugin.client.get_runtime_setting",
): ):
monkeypatch.setattr( monkeypatch.setattr(
@@ -143,7 +143,7 @@ def _configure_local_watcher(
) )
_patch_plugin_runtime_settings(monkeypatch, settings_stub) _patch_plugin_runtime_settings(monkeypatch, settings_stub)
_patch_package_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( 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) _patch_plugin_runtime_settings(monkeypatch, settings_stub)
monkeypatch.setattr( monkeypatch.setattr(
"app.runtime.extensions.plugin_manager.watch", "app.runtime.extensions.plugin.manager.watch",
lambda *_args, **_kwargs: iter([ lambda *_args, **_kwargs: iter([
{(Change.modified, str(source_dir / "dist" / "assets" / "remoteEntry.js"))}, {(Change.modified, str(source_dir / "dist" / "assets" / "remoteEntry.js"))},
{(Change.modified, str(next_entry))}, {(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, "_sync_local_plugin_if_installed", sync_spy)
monkeypatch.setattr(plugin_manager, "reload_plugin", reload_spy) monkeypatch.setattr(plugin_manager, "reload_plugin", reload_spy)
log = Mock() 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() 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, "_sync_local_plugin_if_installed", sync_spy)
monkeypatch.setattr(plugin_manager, "reload_plugin", reload_spy) monkeypatch.setattr(plugin_manager, "reload_plugin", reload_spy)
log = Mock() 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() 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, "_sync_local_plugin_if_installed", sync_spy)
monkeypatch.setattr(plugin_manager, "reload_plugin", reload_spy) monkeypatch.setattr(plugin_manager, "reload_plugin", reload_spy)
log = Mock() 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() 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")) monkeypatch.setattr(PluginMarketTransport, "get_current_system_version", lambda: Version("2.13.11"))
log = Mock() 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() 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")) monkeypatch.setattr(PluginMarketTransport, "get_current_system_version", lambda: Version("2.13.11"))
log = Mock() 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() 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")) monkeypatch.setattr(PluginMarketTransport, "get_current_system_version", lambda: Version("2.13.11"))
log = Mock() 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() plugin_manager._run_file_watcher()
+2 -2
View File
@@ -36,7 +36,7 @@ async def test_async_run_plugin_method_offloads_sync_plugin_method(
) )
worker = AsyncMock(return_value=2) worker = AsyncMock(return_value=2)
monkeypatch.setattr( monkeypatch.setattr(
"app.runtime.extensions.plugin_manager.run_in_threadpool_to_completion", "app.runtime.extensions.plugin.manager.run_in_threadpool_to_completion",
worker, worker,
) )
@@ -61,7 +61,7 @@ async def test_async_run_plugin_method_keeps_async_plugin_method_on_loop(
) )
worker = AsyncMock() worker = AsyncMock()
monkeypatch.setattr( monkeypatch.setattr(
"app.runtime.extensions.plugin_manager.run_in_threadpool_to_completion", "app.runtime.extensions.plugin.manager.run_in_threadpool_to_completion",
worker, worker,
) )
+1 -1
View File
@@ -6,7 +6,7 @@ from pathlib import Path
from app.startup.initializers import plugins as plugins_initializer from app.startup.initializers import plugins as plugins_initializer
PROJECT_ROOT = Path(__file__).parents[1] 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" 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