mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 08:26:53 +08:00
refactor: add configuration dependency ratchet
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user