refactor: unify service configuration boundary

This commit is contained in:
jxxghp
2026-08-24 05:12:21 +08:00
parent 52c5c14f87
commit cb3c5f008f
19 changed files with 174 additions and 53 deletions
+2 -3
View File
@@ -12,6 +12,7 @@ from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from uuid import uuid4 from uuid import uuid4
from app.application.notification import get_notification_configs
from app.runtime.settings import RuntimeSettingsCompat from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
@@ -801,9 +802,7 @@ class AgentCapabilityManager:
if not source: if not source:
return False return False
from app.runtime.extensions.service_config import ServiceConfigHelper for config in get_notification_configs(include_disabled=True):
for config in ServiceConfigHelper.get_notification_configs():
if config.name != source: if config.name != source:
continue continue
return (config.config or {}).get("WECHAT_MODE", "app") != "bot" return (config.config or {}).get("WECHAT_MODE", "app") != "bot"
+2 -2
View File
@@ -23,7 +23,7 @@ from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.application.messaging.agent import matches_channel_admin from app.application.messaging.agent import matches_channel_admin
from app.runtime.extensions.service_config import ServiceConfigHelper from app.application.notification import get_notification_configs
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.types import NotificationChannel, MessageType from app.schemas.types import NotificationChannel, MessageType
@@ -719,7 +719,7 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta):
return False return False
try: try:
configs = ServiceConfigHelper.get_notification_configs() configs = get_notification_configs(include_disabled=True)
for config in configs: for config in configs:
if config.name == self._source and config.config: if config.name == self._source and config.config:
return matches_channel_admin( return matches_channel_admin(
+2 -3
View File
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field
from app.agent.tools.base import MoviePilotTool from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag from app.agent.tools.tags import ToolTag
from app.chain.mediaserver import MediaServerChain from app.chain.mediaserver import MediaServerChain
from app.runtime.extensions.service_config import ServiceConfigHelper from app.application.mediaserver import get_mediaserver_configs
from app.runtime.log import logger from app.runtime.log import logger
PAGE_SIZE = 20 PAGE_SIZE = 20
@@ -61,8 +61,7 @@ class QueryLibraryLatestTool(MoviePilotTool):
@staticmethod @staticmethod
def _get_enabled_servers() -> list[str]: def _get_enabled_servers() -> list[str]:
"""同步读取启用的媒体服务器列表。""" """同步读取启用的媒体服务器列表。"""
mediaservers = ServiceConfigHelper.get_mediaserver_configs() return [config.name for config in get_mediaserver_configs()]
return [ms.name for ms in mediaservers if ms.enabled]
@staticmethod @staticmethod
def _load_latest_items( def _load_latest_items(
+2 -2
View File
@@ -28,7 +28,7 @@ from app.application.configuration import (
from app.api.dependencies.agent import get_message_query_service from app.api.dependencies.agent import get_message_query_service
from app.api.dependencies.auth import get_current_active_superuser from app.api.dependencies.auth import get_current_active_superuser
from app.application.messaging.message import MessageQueryService from app.application.messaging.message import MessageQueryService
from app.runtime.extensions.service_config import ServiceConfigHelper from app.application.notification import get_notification_configs
from app.runtime.log import logger from app.runtime.log import logger
from app.adapters.external.wechat_crypt import WXBizMsgCrypt from app.adapters.external.wechat_crypt import WXBizMsgCrypt
from app.schemas.types import NotificationChannel, SystemConfigKey from app.schemas.types import NotificationChannel, SystemConfigKey
@@ -261,7 +261,7 @@ def wechat_verify(
微信验证响应 微信验证响应
""" """
# 获取服务配置 # 获取服务配置
client_configs = ServiceConfigHelper.get_notification_configs() client_configs = get_notification_configs(include_disabled=True)
if not client_configs: if not client_configs:
return "未找到对应的消息配置" return "未找到对应的消息配置"
client_config = next( client_config = next(
+9
View File
@@ -304,3 +304,12 @@ class MediaServerHelper(ServiceBaseHelper[MediaServerConf]):
"""判断给定服务或服务名称是否属于指定媒体服务器类型。""" """判断给定服务或服务名称是否属于指定媒体服务器类型。"""
service = service or self.get_service(name=name) service = service or self.get_service(name=name)
return bool(service and service.type == service_type) return bool(service and service.type == service_type)
def get_mediaserver_configs(
include_disabled: bool = False,
) -> list[MediaServerConf]:
"""返回媒体服务器配置列表,并按调用方需要决定是否包含禁用项。"""
return list(
MediaServerHelper().get_configs(include_disabled=include_disabled).values()
)
+23 -3
View File
@@ -1,9 +1,9 @@
from typing import Optional from typing import Optional
from app.application.service import ServiceBaseHelper from app.application.service import ServiceBaseHelper, get_service_configs
from app.schemas.system import NotificationConf from app.schemas.system import NotificationConf, NotificationSwitchConf
from app.schemas.system import ServiceInfo from app.schemas.system import ServiceInfo
from app.schemas.types import ModuleType, SystemConfigKey from app.schemas.types import MessageType, ModuleType, SystemConfigKey
class NotificationHelper(ServiceBaseHelper[NotificationConf]): class NotificationHelper(ServiceBaseHelper[NotificationConf]):
@@ -33,3 +33,23 @@ class NotificationHelper(ServiceBaseHelper[NotificationConf]):
""" """
service = service or self.get_service(name=name) service = service or self.get_service(name=name)
return bool(service and service.type == service_type) return bool(service and service.type == service_type)
def get_notification_configs(
include_disabled: bool = False,
) -> list[NotificationConf]:
"""返回通知配置列表,并按调用方需要决定是否包含禁用项。"""
return list(
NotificationHelper().get_configs(include_disabled=include_disabled).values()
)
def get_notification_switch(mtype: MessageType) -> Optional[str]:
"""返回指定通知场景的目标范围。"""
for switch in get_service_configs(
SystemConfigKey.NotificationSwitchs,
NotificationSwitchConf,
):
if switch.type == mtype.value:
return switch.action
return None
+9 -1
View File
@@ -41,6 +41,14 @@ def configure_service_directory(
_module_loader = modules _module_loader = modules
def get_service_configs(
config_key: SystemConfigKey,
conf_type: Type[TConf],
) -> list[TConf]:
"""通过组合根登记的读取器返回已校验服务配置。"""
return _config_loader(config_key, conf_type)
class ServiceBaseHelper(Generic[TConf]): class ServiceBaseHelper(Generic[TConf]):
"""通过应用端口查询服务配置和对应运行实例。""" """通过应用端口查询服务配置和对应运行实例。"""
@@ -57,7 +65,7 @@ class ServiceBaseHelper(Generic[TConf]):
def get_configs(self, include_disabled: bool = False) -> Dict[str, TConf]: def get_configs(self, include_disabled: bool = False) -> Dict[str, TConf]:
"""返回按名称索引的有效服务配置。""" """返回按名称索引的有效服务配置。"""
configs = _config_loader(self.config_key, self.conf_type) configs = get_service_configs(self.config_key, self.conf_type)
return { return {
config.name: config config.name: config
for config in configs for config in configs
+3 -3
View File
@@ -13,7 +13,7 @@ from app.domain.context import Context, MediaInfo, MusicInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase from app.domain.meta.metabase import MetaBase
from app.foundation.identity import normalize_internal_user_id from app.foundation.identity import normalize_internal_user_id
from app.application.messaging.message import MessageTemplateHelper from app.application.messaging.message import MessageTemplateHelper
from app.runtime.extensions.service_config import ServiceConfigHelper from app.application.notification import get_notification_switch
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.message import MessageResponse from app.schemas.message import MessageResponse
from app.schemas.message import Message from app.schemas.message import Message
@@ -159,7 +159,7 @@ class NotificationMixin:
# 发送消息按设置隔离 # 发送消息按设置隔离
if not dispatch_message.userid and dispatch_message.mtype: if not dispatch_message.userid and dispatch_message.mtype:
# 消息隔离设置 # 消息隔离设置
notify_action = ServiceConfigHelper.get_notification_switch( notify_action = get_notification_switch(
dispatch_message.mtype dispatch_message.mtype
) )
if notify_action: if notify_action:
@@ -277,7 +277,7 @@ class NotificationMixin:
# 发送消息按设置隔离 # 发送消息按设置隔离
if not dispatch_message.userid and dispatch_message.mtype: if not dispatch_message.userid and dispatch_message.mtype:
# 消息隔离设置 # 消息隔离设置
notify_action = ServiceConfigHelper.get_notification_switch( notify_action = get_notification_switch(
dispatch_message.mtype dispatch_message.mtype
) )
if notify_action: if notify_action:
+2 -2
View File
@@ -5,7 +5,7 @@ from typing import Callable, Dict, List, Union, Optional, Generator, Any, Tuple
from app.chain import ChainBase from app.chain import ChainBase
from app.runtime.config import global_vars from app.runtime.config import global_vars
from app.application.chain.data import get_chain_media_server_port from app.application.chain.data import get_chain_media_server_port
from app.runtime.extensions.service_config import ServiceConfigHelper from app.application.mediaserver import get_mediaserver_configs
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.mediaserver import MediaServerLibrary from app.schemas.mediaserver import MediaServerLibrary
from app.schemas.mediaserver import MediaServerItem from app.schemas.mediaserver import MediaServerItem
@@ -458,7 +458,7 @@ class MediaServerChain(ChainBase):
:param server: 指定媒体服务器名称,为空时同步全部已启用服务器 :param server: 指定媒体服务器名称,为空时同步全部已启用服务器
""" """
# 设置的媒体服务器 # 设置的媒体服务器
mediaservers = ServiceConfigHelper.get_mediaserver_configs() mediaservers = get_mediaserver_configs(include_disabled=True)
if not mediaservers: if not mediaservers:
if progress_callback: if progress_callback:
progress_callback(value=100, text="未配置媒体服务器,跳过同步") progress_callback(value=100, text="未配置媒体服务器,跳过同步")
+2 -2
View File
@@ -38,10 +38,10 @@ from app.application.configuration import (
get_scheduler_runtime_config, get_scheduler_runtime_config,
) )
from app.application.image import WallpaperHelper from app.application.image import WallpaperHelper
from app.application.mediaserver import get_mediaserver_configs
from app.application.messaging.message import MessageHelper from app.application.messaging.message import MessageHelper
from app.runtime.progress import AsyncProgressHelper, ProgressHelper from app.runtime.progress import AsyncProgressHelper, ProgressHelper
from app.adapters.external.server import MoviePilotServerHelper from app.adapters.external.server import MoviePilotServerHelper
from app.runtime.extensions.service_config import ServiceConfigHelper
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.message import Message from app.schemas.message import Message
@@ -603,7 +603,7 @@ class Scheduler(ConfigReloadMixin, metaclass=SingletonClass):
# 按媒体服务器分别注册自动同步任务 # 按媒体服务器分别注册自动同步任务
mediaserver_schedules = self._build_mediaserver_sync_schedules( mediaserver_schedules = self._build_mediaserver_sync_schedules(
mediaservers=ServiceConfigHelper.get_mediaserver_configs(), mediaservers=get_mediaserver_configs(include_disabled=True),
default_interval=config.mediaserver_sync_interval, default_interval=config.mediaserver_sync_interval,
) )
for mediaserver_schedule in mediaserver_schedules: for mediaserver_schedule in mediaserver_schedules:
@@ -6,7 +6,7 @@
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本 > 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
> 规范优先级:`AGENTS.md``docs/rules/` 高于本文 > 规范优先级:`AGENTS.md``docs/rules/` 高于本文
> 相关文档:`docs/architecture-overview.md``docs/refactor/backend-architecture-governance.md``docs/refactor/backend-module-refactor-compatibility.md` > 相关文档:`docs/architecture-overview.md``docs/refactor/backend-architecture-governance.md``docs/refactor/backend-module-refactor-compatibility.md`
> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口。 > 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口;阶段 22 已统一服务配置应用边界
## 当前复核结论(2026-08-24 ## 当前复核结论(2026-08-24
@@ -229,13 +229,24 @@
- 兼容边界不变:`TransferHistoryPort`、DB Oper、监控候选判定、历史查重、失败重试和整理触发语义均 - 兼容边界不变:`TransferHistoryPort`、DB Oper、监控候选判定、历史查重、失败重试和整理触发语义均
未修改。 未修改。
### 长期整改阶段 22:服务配置应用边界统一(2026-08-24)
- 启动组合根已注入应用层服务目录,但 Chain、消息 API、Scheduler 和 Agent 仍直接读取 runtime
`ServiceConfigHelper`,形成两个应用入口;现在统一通过通知与媒体服务器应用模块的命名函数读取。
- 命名函数显式区分仅启用配置和包含禁用配置,保持定向同步、定时注册、企业微信模式和渠道管理员判断
的原语义;架构门禁覆盖七个生产消费者。
- 依赖基线经语义诊断后从 `6544` 条边降为 `6539`:七个消费者移除 runtime service-config 边并改为
Application 边,12 组禁止边和唯一隔离 TMDB SCC 均未变化。
- `ServiceConfigHelper` 继续保留在 startup、runtime module adapter、模块实例初始化和 `app.sdk.services`
插件兼容出口;V2/V3 插件导入、配置 Schema 和热更新读取器均未修改。
### 总体判断 ### 总体判断
当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**: 当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**:
- 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。 - 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。
- `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。 - `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。
- 依赖图当前为 `806` 个 Python 模块、`6544` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。 - 依赖图当前为 `806` 个 Python 模块、`6539` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。
- 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。 - 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。
综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。 综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。
+5
View File
@@ -143,6 +143,11 @@ must not be reintroduced as Oper aliases in canonical Agent modules.
Monitor history checks use `get_transfer_history_port()` from Monitor history checks use `get_transfer_history_port()` from
`app/application/history.py`; the constructible `TransferHistoryPort` facade is `app/application/history.py`; the constructible `TransferHistoryPort` facade is
retained only for compatibility and is not a canonical Oper substitute. retained only for compatibility and is not a canonical Oper substitute.
Canonical Chain, API, Scheduler and Agent consumers read notification and media
server configuration through the named helpers in `app/application/notification.py`
and `app/application/mediaserver.py`. `ServiceConfigHelper` remains the parser at
the startup/runtime module boundary and a plugin SDK compatibility export; it is
not a second application-facing service directory.
### Adapter boundaries ### Adapter boundaries
+11 -16
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [], "runtime_to_db": [],
"workflow_to_db": [] "workflow_to_db": []
}, },
"edge_count": 6544, "edge_count": 6539,
"edge_sha256": "562471ed16680ca403b6cdb7661b20b1200c53b75da5e162efe4e7400cde174b", "edge_sha256": "28c7ab8544ede290e6dacf5f126ca1aad95e09627d874a0d23c6476108a88e1b",
"edges": [ "edges": [
"app -> app.runtime", "app -> app.runtime",
"app -> app.runtime.compat", "app -> app.runtime.compat",
@@ -202,9 +202,9 @@
"app.agent.llm.capability -> app.agent", "app.agent.llm.capability -> app.agent",
"app.agent.llm.capability -> app.agent.llm", "app.agent.llm.capability -> app.agent.llm",
"app.agent.llm.capability -> app.agent.llm.helper", "app.agent.llm.capability -> app.agent.llm.helper",
"app.agent.llm.capability -> app.application",
"app.agent.llm.capability -> app.application.notification",
"app.agent.llm.capability -> app.runtime", "app.agent.llm.capability -> app.runtime",
"app.agent.llm.capability -> app.runtime.extensions",
"app.agent.llm.capability -> app.runtime.extensions.service_config",
"app.agent.llm.capability -> app.runtime.log", "app.agent.llm.capability -> app.runtime.log",
"app.agent.llm.capability -> app.runtime.settings", "app.agent.llm.capability -> app.runtime.settings",
"app.agent.llm.capability -> app.schemas", "app.agent.llm.capability -> app.schemas",
@@ -432,10 +432,9 @@
"app.agent.tools.base -> app.application", "app.agent.tools.base -> app.application",
"app.agent.tools.base -> app.application.messaging", "app.agent.tools.base -> app.application.messaging",
"app.agent.tools.base -> app.application.messaging.agent", "app.agent.tools.base -> app.application.messaging.agent",
"app.agent.tools.base -> app.application.notification",
"app.agent.tools.base -> app.chain", "app.agent.tools.base -> app.chain",
"app.agent.tools.base -> app.runtime", "app.agent.tools.base -> app.runtime",
"app.agent.tools.base -> app.runtime.extensions",
"app.agent.tools.base -> app.runtime.extensions.service_config",
"app.agent.tools.base -> app.runtime.log", "app.agent.tools.base -> app.runtime.log",
"app.agent.tools.base -> app.runtime.settings", "app.agent.tools.base -> app.runtime.settings",
"app.agent.tools.base -> app.schemas", "app.agent.tools.base -> app.schemas",
@@ -952,11 +951,11 @@
"app.agent.tools.impl.query_library_latest -> app.agent.tools", "app.agent.tools.impl.query_library_latest -> app.agent.tools",
"app.agent.tools.impl.query_library_latest -> app.agent.tools.base", "app.agent.tools.impl.query_library_latest -> app.agent.tools.base",
"app.agent.tools.impl.query_library_latest -> app.agent.tools.tags", "app.agent.tools.impl.query_library_latest -> app.agent.tools.tags",
"app.agent.tools.impl.query_library_latest -> app.application",
"app.agent.tools.impl.query_library_latest -> app.application.mediaserver",
"app.agent.tools.impl.query_library_latest -> app.chain", "app.agent.tools.impl.query_library_latest -> app.chain",
"app.agent.tools.impl.query_library_latest -> app.chain.mediaserver", "app.agent.tools.impl.query_library_latest -> app.chain.mediaserver",
"app.agent.tools.impl.query_library_latest -> app.runtime", "app.agent.tools.impl.query_library_latest -> app.runtime",
"app.agent.tools.impl.query_library_latest -> app.runtime.extensions",
"app.agent.tools.impl.query_library_latest -> app.runtime.extensions.service_config",
"app.agent.tools.impl.query_library_latest -> app.runtime.log", "app.agent.tools.impl.query_library_latest -> app.runtime.log",
"app.agent.tools.impl.query_market_plugins -> app.agent", "app.agent.tools.impl.query_market_plugins -> app.agent",
"app.agent.tools.impl.query_market_plugins -> app.agent.tools", "app.agent.tools.impl.query_market_plugins -> app.agent.tools",
@@ -1983,12 +1982,11 @@
"app.api.endpoints.message -> app.application.configuration", "app.api.endpoints.message -> app.application.configuration",
"app.api.endpoints.message -> app.application.messaging", "app.api.endpoints.message -> app.application.messaging",
"app.api.endpoints.message -> app.application.messaging.message", "app.api.endpoints.message -> app.application.messaging.message",
"app.api.endpoints.message -> app.application.notification",
"app.api.endpoints.message -> app.chain", "app.api.endpoints.message -> app.chain",
"app.api.endpoints.message -> app.chain.message", "app.api.endpoints.message -> app.chain.message",
"app.api.endpoints.message -> app.runtime", "app.api.endpoints.message -> app.runtime",
"app.api.endpoints.message -> app.runtime.config", "app.api.endpoints.message -> app.runtime.config",
"app.api.endpoints.message -> app.runtime.extensions",
"app.api.endpoints.message -> app.runtime.extensions.service_config",
"app.api.endpoints.message -> app.runtime.log", "app.api.endpoints.message -> app.runtime.log",
"app.api.endpoints.message -> app.runtime.tasks", "app.api.endpoints.message -> app.runtime.tasks",
"app.api.endpoints.message -> app.schemas", "app.api.endpoints.message -> app.schemas",
@@ -2941,6 +2939,7 @@
"app.chain._messaging -> app.application.messaging", "app.chain._messaging -> app.application.messaging",
"app.chain._messaging -> app.application.messaging.agent", "app.chain._messaging -> app.application.messaging.agent",
"app.chain._messaging -> app.application.messaging.message", "app.chain._messaging -> app.application.messaging.message",
"app.chain._messaging -> app.application.notification",
"app.chain._messaging -> app.domain", "app.chain._messaging -> app.domain",
"app.chain._messaging -> app.domain.context", "app.chain._messaging -> app.domain.context",
"app.chain._messaging -> app.domain.meta", "app.chain._messaging -> app.domain.meta",
@@ -2948,8 +2947,6 @@
"app.chain._messaging -> app.foundation", "app.chain._messaging -> app.foundation",
"app.chain._messaging -> app.foundation.identity", "app.chain._messaging -> app.foundation.identity",
"app.chain._messaging -> app.runtime", "app.chain._messaging -> app.runtime",
"app.chain._messaging -> app.runtime.extensions",
"app.chain._messaging -> app.runtime.extensions.service_config",
"app.chain._messaging -> app.runtime.log", "app.chain._messaging -> app.runtime.log",
"app.chain._messaging -> app.schemas", "app.chain._messaging -> app.schemas",
"app.chain._messaging -> app.schemas.message", "app.chain._messaging -> app.schemas.message",
@@ -3166,13 +3163,12 @@
"app.chain.mediaserver -> app.application", "app.chain.mediaserver -> app.application",
"app.chain.mediaserver -> app.application.chain", "app.chain.mediaserver -> app.application.chain",
"app.chain.mediaserver -> app.application.chain.data", "app.chain.mediaserver -> app.application.chain.data",
"app.chain.mediaserver -> app.application.mediaserver",
"app.chain.mediaserver -> app.application.security", "app.chain.mediaserver -> app.application.security",
"app.chain.mediaserver -> app.application.security.url", "app.chain.mediaserver -> app.application.security.url",
"app.chain.mediaserver -> app.chain", "app.chain.mediaserver -> app.chain",
"app.chain.mediaserver -> app.runtime", "app.chain.mediaserver -> app.runtime",
"app.chain.mediaserver -> app.runtime.config", "app.chain.mediaserver -> app.runtime.config",
"app.chain.mediaserver -> app.runtime.extensions",
"app.chain.mediaserver -> app.runtime.extensions.service_config",
"app.chain.mediaserver -> app.runtime.log", "app.chain.mediaserver -> app.runtime.log",
"app.chain.mediaserver -> app.schemas", "app.chain.mediaserver -> app.schemas",
"app.chain.mediaserver -> app.schemas.mediaserver", "app.chain.mediaserver -> app.schemas.mediaserver",
@@ -5801,6 +5797,7 @@
"app.scheduler -> app.application.configuration", "app.scheduler -> app.application.configuration",
"app.scheduler -> app.application.database", "app.scheduler -> app.application.database",
"app.scheduler -> app.application.image", "app.scheduler -> app.application.image",
"app.scheduler -> app.application.mediaserver",
"app.scheduler -> app.application.messaging", "app.scheduler -> app.application.messaging",
"app.scheduler -> app.application.messaging.message", "app.scheduler -> app.application.messaging.message",
"app.scheduler -> app.application.outbox", "app.scheduler -> app.application.outbox",
@@ -5824,8 +5821,6 @@
"app.scheduler -> app.runtime.config", "app.scheduler -> app.runtime.config",
"app.scheduler -> app.runtime.correlation", "app.scheduler -> app.runtime.correlation",
"app.scheduler -> app.runtime.events", "app.scheduler -> app.runtime.events",
"app.scheduler -> app.runtime.extensions",
"app.scheduler -> app.runtime.extensions.service_config",
"app.scheduler -> app.runtime.gc", "app.scheduler -> app.runtime.gc",
"app.scheduler -> app.runtime.log", "app.scheduler -> app.runtime.log",
"app.scheduler -> app.runtime.observability", "app.scheduler -> app.runtime.observability",
+1 -1
View File
@@ -199,7 +199,7 @@ class AgentCapabilityManagerTest(unittest.TestCase):
] ]
with patch( with patch(
"app.runtime.extensions.service_config.ServiceConfigHelper.get_notification_configs", "app.agent.llm.capability.get_notification_configs",
return_value=configs, return_value=configs,
): ):
self.assertTrue( self.assertTrue(
@@ -454,7 +454,7 @@ def test_channel_primary_id_defaults_to_admin_without_admin_list():
) )
with patch( with patch(
"app.agent.tools.base.ServiceConfigHelper.get_notification_configs", "app.agent.tools.base.get_notification_configs",
return_value=[ return_value=[
SimpleNamespace( SimpleNamespace(
name="telegram-main", name="telegram-main",
@@ -477,7 +477,7 @@ def test_channel_primary_id_mismatch_remains_non_admin():
) )
with patch( with patch(
"app.agent.tools.base.ServiceConfigHelper.get_notification_configs", "app.agent.tools.base.get_notification_configs",
return_value=[ return_value=[
SimpleNamespace( SimpleNamespace(
name="telegram-main", name="telegram-main",
@@ -500,7 +500,7 @@ def test_feishu_primary_open_id_defaults_to_admin():
) )
with patch( with patch(
"app.agent.tools.base.ServiceConfigHelper.get_notification_configs", "app.agent.tools.base.get_notification_configs",
return_value=[ return_value=[
SimpleNamespace( SimpleNamespace(
name="feishu-main", name="feishu-main",
@@ -523,7 +523,7 @@ def test_channel_primary_id_still_prefers_admin_list():
) )
with patch( with patch(
"app.agent.tools.base.ServiceConfigHelper.get_notification_configs", "app.agent.tools.base.get_notification_configs",
return_value=[ return_value=[
SimpleNamespace( SimpleNamespace(
name="telegram-main", name="telegram-main",
@@ -0,0 +1,49 @@
"""应用层服务配置目录测试。"""
from app.application import service
from app.application.mediaserver import get_mediaserver_configs
from app.application.notification import (
get_notification_configs,
get_notification_switch,
)
from app.schemas.system import (
MediaServerConf,
NotificationConf,
NotificationSwitchConf,
)
from app.schemas.types import MessageType, SystemConfigKey
def test_named_service_config_helpers_preserve_enabled_policy(monkeypatch) -> None:
"""命名应用函数应复用同一配置目录,并显式控制禁用项可见性。"""
configs = {
SystemConfigKey.MediaServers: [
MediaServerConf(name="enabled-media", type="plex", enabled=True),
MediaServerConf(name="disabled-media", type="emby", enabled=False),
],
SystemConfigKey.Notifications: [
NotificationConf(name="enabled-channel", type="telegram", enabled=True),
NotificationConf(name="disabled-channel", type="wechat", enabled=False),
],
SystemConfigKey.NotificationSwitchs: [
NotificationSwitchConf(
type=MessageType.Download.value,
action="admin",
)
],
}
monkeypatch.setattr(
service,
"_config_loader",
lambda config_key, _conf_type: configs.get(config_key, []),
)
assert [item.name for item in get_mediaserver_configs()] == ["enabled-media"]
assert [
item.name for item in get_mediaserver_configs(include_disabled=True)
] == ["enabled-media", "disabled-media"]
assert [item.name for item in get_notification_configs()] == ["enabled-channel"]
assert [
item.name for item in get_notification_configs(include_disabled=True)
] == ["enabled-channel", "disabled-channel"]
assert get_notification_switch(MessageType.Download) == "admin"
+26
View File
@@ -614,6 +614,32 @@ def test_monitor_dispatcher_uses_explicit_history_port_getter():
assert violations == [] assert violations == []
def test_canonical_service_config_consumers_use_application_directory():
"""Chain、API、Scheduler 与 Agent 不得绕过应用目录读取运行时配置 Helper。"""
paths = [
APP_ROOT / "chain" / "_messaging.py",
APP_ROOT / "chain" / "mediaserver.py",
APP_ROOT / "api" / "endpoints" / "message.py",
APP_ROOT / "scheduler.py",
APP_ROOT / "agent" / "llm" / "capability.py",
APP_ROOT / "agent" / "tools" / "base.py",
APP_ROOT / "agent" / "tools" / "impl" / "query_library_latest.py",
]
violations: list[str] = []
for path in paths:
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
for node in ast.walk(tree):
if (
isinstance(node, ast.ImportFrom)
and node.module == "app.runtime.extensions.service_config"
):
violations.append(
f"{path.relative_to(PROJECT_ROOT).as_posix()}:{node.lineno}"
)
assert violations == []
def test_plugin_components_do_not_reexport_legacy_abi_names(): def test_plugin_components_do_not_reexport_legacy_abi_names():
"""新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。""" """新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。"""
violations: list[str] = [] violations: list[str] = []
+7 -7
View File
@@ -106,7 +106,7 @@ def test_sync_persists_music_without_querying_tv_episodes(database):
"get_chain_media_server_port", "get_chain_media_server_port",
lambda: MediaServerOper(session), lambda: MediaServerOper(session),
), patch.object( ), patch.object(
MEDIA_SERVER_CHAIN_MODULE.ServiceConfigHelper, MEDIA_SERVER_CHAIN_MODULE,
"get_mediaserver_configs", "get_mediaserver_configs",
return_value=[SimpleNamespace(name="navidrome", enabled=True, sync_libraries=["all"])], return_value=[SimpleNamespace(name="navidrome", enabled=True, sync_libraries=["all"])],
): ):
@@ -199,7 +199,7 @@ def test_sync_updates_rows_and_removes_stale_entries(database):
"get_chain_media_server_port", "get_chain_media_server_port",
lambda: MediaServerOper(session), lambda: MediaServerOper(session),
), patch.object( ), patch.object(
MEDIA_SERVER_CHAIN_MODULE.ServiceConfigHelper, MEDIA_SERVER_CHAIN_MODULE,
"get_mediaserver_configs", "get_mediaserver_configs",
return_value=[SimpleNamespace(name="plex", enabled=True, sync_libraries=["movies"])], return_value=[SimpleNamespace(name="plex", enabled=True, sync_libraries=["movies"])],
): ):
@@ -283,7 +283,7 @@ def test_sync_queries_counts_before_items_and_reports_media_progress(database):
"get_chain_media_server_port", "get_chain_media_server_port",
lambda: MediaServerOper(session), lambda: MediaServerOper(session),
), patch.object( ), patch.object(
MEDIA_SERVER_CHAIN_MODULE.ServiceConfigHelper, MEDIA_SERVER_CHAIN_MODULE,
"get_mediaserver_configs", "get_mediaserver_configs",
return_value=[ return_value=[
SimpleNamespace(name="plex-a", enabled=True, sync_libraries=["all"]), SimpleNamespace(name="plex-a", enabled=True, sync_libraries=["all"]),
@@ -340,9 +340,9 @@ def test_sync_targets_one_server_without_excluding_other_enabled_servers(monkeyp
FakeMediaServerOper, FakeMediaServerOper,
) )
monkeypatch.setattr( monkeypatch.setattr(
MEDIA_SERVER_CHAIN_MODULE.ServiceConfigHelper, MEDIA_SERVER_CHAIN_MODULE,
"get_mediaserver_configs", "get_mediaserver_configs",
lambda: [ lambda **_kwargs: [
SimpleNamespace(name="plex-a", enabled=True, sync_libraries=["all"]), SimpleNamespace(name="plex-a", enabled=True, sync_libraries=["all"]),
SimpleNamespace(name="plex-b", enabled=True, sync_libraries=["all"]), SimpleNamespace(name="plex-b", enabled=True, sync_libraries=["all"]),
], ],
@@ -383,9 +383,9 @@ def test_sync_stops_without_emitting_completion_after_stop_signal(monkeypatch):
) )
monkeypatch.setattr(chain, "_sync_server_libraries", stop_during_sync) monkeypatch.setattr(chain, "_sync_server_libraries", stop_during_sync)
monkeypatch.setattr( monkeypatch.setattr(
MEDIA_SERVER_CHAIN_MODULE.ServiceConfigHelper, MEDIA_SERVER_CHAIN_MODULE,
"get_mediaserver_configs", "get_mediaserver_configs",
lambda: [server], lambda **_kwargs: [server],
) )
global_vars.STOP_EVENT.clear() global_vars.STOP_EVENT.clear()
try: try:
+2 -2
View File
@@ -90,9 +90,9 @@ def test_clear_cache_is_manual_only(monkeypatch):
]: ]:
monkeypatch.setattr(scheduler_module, name, lambda: generic_chain) monkeypatch.setattr(scheduler_module, name, lambda: generic_chain)
monkeypatch.setattr( monkeypatch.setattr(
scheduler_module.ServiceConfigHelper, scheduler_module,
"get_mediaserver_configs", "get_mediaserver_configs",
lambda: [], lambda **_kwargs: [],
) )
monkeypatch.setattr( monkeypatch.setattr(
scheduler_module, scheduler_module,