refactor: add configuration dependency ratchet

This commit is contained in:
jxxghp
2026-08-21 21:20:56 +08:00
parent 9df599f502
commit ff0ce8d1c1
16 changed files with 671 additions and 34 deletions
+23 -4
View File
@@ -16,7 +16,10 @@ from app.agent.tools.impl._system_setting_utils import (
should_redact_setting,
)
from app.runtime.config import settings
from app.application.configuration import get_configured_system_config as SystemConfigOper
from app.application.configuration import (
SystemConfigReader,
get_configured_system_config as SystemConfigOper,
)
from app.runtime.log import logger
@@ -84,6 +87,23 @@ class QuerySystemSettingsTool(MoviePilotTool):
require_admin: bool = True
args_schema: Type[BaseModel] = QuerySystemSettingsInput
_secret_read_confirmed: bool = PrivateAttr(default=False)
_system_config: Optional[SystemConfigReader] = PrivateAttr(default=None)
def __init__(
self,
session_id: str,
user_id: str,
*,
system_config: Optional[SystemConfigReader] = None,
**kwargs,
) -> None:
"""注入授权范围内的配置读取端口,并兼容组合根默认服务。"""
super().__init__(session_id=session_id, user_id=user_id, **kwargs)
self._system_config = system_config
def _get_system_config(self) -> SystemConfigReader:
"""返回显式注入端口,旧构造形态则延迟读取组合根服务。"""
return self._system_config or SystemConfigOper()
async def _run_confirmed(self, **kwargs) -> str:
"""仅供宿主在消费有效确认后执行一次未脱敏读取。"""
@@ -105,12 +125,11 @@ class QuerySystemSettingsTool(MoviePilotTool):
return f"筛选系统设置: {group} / {keyword}"
return f"查询系统设置分组: {group}"
@staticmethod
def _load_setting_value(spec: SettingSpec):
def _load_setting_value(self, spec: SettingSpec):
"""读取指定设置项的当前值。"""
if spec.source == "settings":
return getattr(settings, spec.key)
return SystemConfigOper().get(spec.systemconfig_key)
return self._get_system_config().get(spec.systemconfig_key)
@staticmethod
def _summarize_value(value, *, redacted: bool = False) -> dict:
+27 -6
View File
@@ -4,7 +4,7 @@ import copy
import json
from typing import Any, Literal, Optional, Type, Union
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, PrivateAttr
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
@@ -18,7 +18,10 @@ from app.agent.tools.impl._system_setting_utils import (
)
from app.runtime.config import settings
from app.runtime.events import eventmanager
from app.application.configuration import get_configured_system_config as SystemConfigOper
from app.application.configuration import (
SystemConfigService,
get_configured_system_config as SystemConfigOper,
)
from app.runtime.log import logger
from app.schemas.event import ConfigChangeEventData
from app.schemas.types import EventType
@@ -74,6 +77,8 @@ class UpdateSystemSettingsInput(BaseModel):
class UpdateSystemSettingsTool(MoviePilotTool):
"""通过授权配置服务修改可登记系统设置。"""
name: str = "update_system_settings"
tags: list[str] = [
ToolTag.Write,
@@ -87,6 +92,23 @@ class UpdateSystemSettingsTool(MoviePilotTool):
)
require_admin: bool = True
args_schema: Type[BaseModel] = UpdateSystemSettingsInput
_system_config: Optional[SystemConfigService] = PrivateAttr(default=None)
def __init__(
self,
session_id: str,
user_id: str,
*,
system_config: Optional[SystemConfigService] = None,
**kwargs,
) -> None:
"""注入配置读写服务,并兼容组合根默认装配。"""
super().__init__(session_id=session_id, user_id=user_id, **kwargs)
self._system_config = system_config
def _get_system_config(self) -> SystemConfigService:
"""返回显式注入服务,旧构造形态则延迟读取组合根服务。"""
return self._system_config or SystemConfigOper()
def get_tool_message(self, **kwargs) -> Optional[str]:
"""根据更新参数生成友好的提示消息。"""
@@ -101,12 +123,11 @@ class UpdateSystemSettingsTool(MoviePilotTool):
}
return f"{action_map.get(operation, '更新系统设置')}: {setting_key}"
@staticmethod
def _load_setting_value(spec: SettingSpec):
def _load_setting_value(self, spec: SettingSpec):
"""读取指定设置项的当前值。"""
if spec.source == "settings":
return getattr(settings, spec.key)
return SystemConfigOper().get(spec.systemconfig_key)
return self._get_system_config().get(spec.systemconfig_key)
@staticmethod
def _normalize_systemconfig_value(value: Any):
@@ -266,7 +287,7 @@ class UpdateSystemSettingsTool(MoviePilotTool):
else:
normalized_value = self._normalize_systemconfig_value(next_value)
event_value = normalized_value
success = await SystemConfigOper().async_set(
success = await self._get_system_config().async_set(
spec.systemconfig_key,
normalized_value,
)
+57 -13
View File
@@ -2,21 +2,27 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Protocol
class ConfigurationRepository(Protocol):
"""配置服务所需的最小持久化端口。"""
class SystemConfigReader(Protocol):
"""持久化用户配置的最小只读端口。"""
def get(self, key: Any = None) -> Any:
"""读取配置。"""
def set(self, key: Any, value: Any) -> bool | None:
"""写入配置。"""
async def async_get(self, key: Any = None) -> Any:
"""异步读取配置。"""
class SystemConfigWriter(Protocol):
"""持久化用户配置的最小写入端口。"""
def set(self, key: Any, value: Any) -> bool | None:
"""写入配置。"""
async def async_set(self, key: Any, value: Any) -> bool | None:
"""异步写入配置。"""
@@ -24,35 +30,58 @@ class ConfigurationRepository(Protocol):
"""删除配置。"""
class ConfigurationRepository(SystemConfigReader, SystemConfigWriter, Protocol):
"""兼容同时提供读写能力的旧配置仓储。"""
@dataclass(frozen=True, slots=True)
class TransferRetryConfig:
"""整理失败重试用例在一次调用中使用的稳定配置快照。"""
max_failed_retries: Any
class SystemConfigService:
"""系统配置读写应用服务。"""
def __init__(self, repository: ConfigurationRepository) -> None:
"""注入配置数据端口。"""
self._repository = repository
def __init__(
self,
repository: ConfigurationRepository | None = None,
*,
reader: SystemConfigReader | None = None,
writer: SystemConfigWriter | None = None,
) -> None:
"""注入可分离的读写端口,并兼容旧的单仓储装配参数。"""
resolved_reader = reader or repository
resolved_writer = writer or repository
if resolved_reader is None or resolved_writer is None:
raise ValueError("系统配置服务必须同时提供 reader 与 writer")
self._reader = resolved_reader
self._writer = resolved_writer
def get(self, key: Any = None) -> Any:
"""读取配置。"""
return self._repository.get(key)
return self._reader.get(key)
def set(self, key: Any, value: Any) -> bool | None:
"""写入配置。"""
return self._repository.set(key, value)
return self._writer.set(key, value)
async def async_get(self, key: Any = None) -> Any:
"""异步读取配置。"""
return await self._repository.async_get(key)
return await self._reader.async_get(key)
async def async_set(self, key: Any, value: Any) -> bool | None:
"""异步写入配置。"""
return await self._repository.async_set(key, value)
return await self._writer.async_set(key, value)
def delete(self, key: Any) -> Any:
"""删除配置。"""
return self._repository.delete(key)
return self._writer.delete(key)
_configured_system_config: SystemConfigService | None = None
_transfer_retry_config_provider: Callable[[], TransferRetryConfig] | None = None
def configure_system_config(service: SystemConfigService) -> None:
@@ -66,3 +95,18 @@ def get_configured_system_config() -> SystemConfigService:
if _configured_system_config is None:
raise RuntimeError("系统配置服务尚未配置")
return _configured_system_config
def configure_transfer_retry_config(
provider: Callable[[], TransferRetryConfig],
) -> None:
"""由组合根登记整理失败重试快照工厂。"""
global _transfer_retry_config_provider
_transfer_retry_config_provider = provider
def get_transfer_retry_config() -> TransferRetryConfig:
"""为一次整理历史判定创建不可变配置快照。"""
if _transfer_retry_config_provider is None:
raise RuntimeError("整理失败重试配置尚未装配")
return _transfer_retry_config_provider()
+3 -3
View File
@@ -2,13 +2,13 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Protocol, Union
from app.application.configuration import TransferRetryConfig, get_transfer_retry_config
from app.domain.context import MediaInfo, MusicInfo
from app.schemas.media import resolve_media_identity
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.foundation.text import cut as jieba_cut
from app.runtime.cache import TTLCache
from app.runtime.config import settings
from app.runtime.log import logger
from app.schemas.history import (
DownloadHistory as DownloadHistoryView,
@@ -480,7 +480,7 @@ def is_skip_action(action: str) -> bool:
return action in (HistoryGateAction.SKIP, HistoryGateAction.SKIP_RETRY_EXHAUSTED)
def max_failed_retries() -> int:
def max_failed_retries(config: TransferRetryConfig | None = None) -> int:
"""
读取失败重试上限并钳制到合法区间。
@@ -488,7 +488,7 @@ def max_failed_retries() -> int:
永久漏件,无限重试会让永久失败的文件反复刷通知,两端都不接受。
:return: 合法的最大重试次数
"""
raw = settings.TRANSFER_MAX_FAILED_RETRIES
raw = (config or get_transfer_retry_config()).max_failed_retries
try:
value = int(raw)
except (TypeError, ValueError):
+12 -2
View File
@@ -1,4 +1,5 @@
from typing import List, Optional, Tuple, Union
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple, Union
from app.schemas.context import MediaPerson as _SchemaMediaPerson
from app.runtime.config import settings
@@ -19,6 +20,13 @@ from app.adapters.network.http import RequestUtils
from app.domain.media import is_media_source_enabled
@dataclass(frozen=True, slots=True)
class BangumiConfigSnapshot:
"""Bangumi 模块一次配置 generation 使用的稳定网络快照。"""
proxy: Any
class BangumiModule(_ModuleBase):
"""
Bangumi媒体信息匹配
@@ -27,11 +35,13 @@ class BangumiModule(_ModuleBase):
bangumiapi: BangumiApi = None
scraper: MediaScraperHelper = None
_config: BangumiConfigSnapshot = BangumiConfigSnapshot(proxy=None)
def init_module(self) -> None:
"""
初始化Bangumi客户端
"""
self._config = BangumiConfigSnapshot(proxy=settings.PROXY)
self.bangumiapi = BangumiApi()
self.scraper = MediaScraperHelper()
@@ -46,7 +56,7 @@ class BangumiModule(_ModuleBase):
"""
测试模块连接性
"""
ret = RequestUtils(proxies=settings.PROXY).get_res("https://api.bgm.tv/")
ret = RequestUtils(proxies=self._config.proxy).get_res("https://api.bgm.tv/")
if ret and ret.status_code == 200:
return True, ""
elif ret:
+11 -1
View File
@@ -35,7 +35,12 @@ from app.application.messaging.message import (
MessageQueueManager,
stop_message,
)
from app.application.configuration import SystemConfigService, configure_system_config
from app.application.configuration import (
SystemConfigService,
TransferRetryConfig,
configure_system_config,
configure_transfer_retry_config,
)
from app.application.database import configure_database_governance
from app.application.service import configure_service_directory
from app.application.plugin.runtime import configure_plugin_runtime
@@ -449,6 +454,11 @@ async def init_modules() -> HostRuntime:
user=lambda: UserOper(),
)
configure_system_config(SystemConfigService(repository=SystemConfigOper()))
configure_transfer_retry_config(
lambda: TransferRetryConfig(
max_failed_retries=settings.TRANSFER_MAX_FAILED_RETRIES,
)
)
configure_database_governance(build_database_governance())
configure_agent_chat_service(AgentChatService(repository=AgentChatOper()))
configure_user_lookups(