refactor: expand api and chain config snapshots

This commit is contained in:
jxxghp
2026-08-22 13:22:33 +08:00
parent 7bc5e01afd
commit 996a1a0705
17 changed files with 197 additions and 107 deletions
+5 -4
View File
@@ -44,7 +44,7 @@ from app.agent.runtime_loader import (
)
from app.chain.message import MessageChain
from app.command import Command
from app.runtime.config import global_vars, settings
from app.runtime.config import global_vars
from app.runtime.events import Event, EventManager
from app.api.principal import ApiPrincipal
from app.api.dependencies.agent import get_agent_chat_service
@@ -55,6 +55,7 @@ from app.application.messaging.chat import (
get_configured_agent_chat_service,
)
from app.application.security.user import get_configured_user_id_lookup
from app.application.configuration import get_api_runtime_config_snapshot
from app.application.messaging.agent import attach_web_agent_edit_queue, detach_web_agent_edit_queue
from app.application.messaging.agent import agent_interaction_manager
from app.application.messaging.agent import (
@@ -744,7 +745,7 @@ def _get_web_agent_upload_dir(user: ApiPrincipal, session_id: Optional[str]) ->
"""
server_session_id = _build_web_agent_session_id(user, session_id)
safe_session_id = server_session_id.replace(":", "_")
upload_dir = settings.TEMP_PATH / "agent_uploads" / safe_session_id
upload_dir = get_api_runtime_config_snapshot().temp_path / "agent_uploads" / safe_session_id
upload_dir.mkdir(parents=True, exist_ok=True)
return upload_dir
@@ -972,7 +973,7 @@ def _prepare_web_agent_audio_attachment_path(voice_path: str) -> Path:
logger.warning("WebAgent 语音转 WAV 跳过:ffmpeg 不可用,path=%s", source_path)
return source_path
voice_dir = settings.TEMP_PATH / "voice"
voice_dir = get_api_runtime_config_snapshot().temp_path / "voice"
voice_dir.mkdir(parents=True, exist_ok=True)
output_path = voice_dir / f"{source_path.stem}_web_{uuid.uuid4().hex[:8]}.wav"
cmd = [
@@ -2113,7 +2114,7 @@ async def web_agent_stream(
return build_sse_response(traditional_event_generator())
if not settings.AI_AGENT_ENABLE:
if not get_api_runtime_config_snapshot().ai_agent_enable:
return _build_web_agent_error_response(
"智能助手未启用,请先在系统设置中开启。",
locale=locale,
+3 -3
View File
@@ -23,7 +23,7 @@ from app.api.openai_utils import (
)
from app.api.presentation.sse import build_sse_response, encode_named_event
from app.agent.runtime_loader import get_running_agent_manager
from app.runtime.config import settings
from app.application.configuration import get_api_runtime_config_snapshot
from app.adapters.web.security.access import anthropic_api_key_header
ANTHROPIC_ERROR_RESPONSES = {
@@ -56,7 +56,7 @@ def _check_auth(api_key: Optional[str]) -> Optional[JSONResponse]:
"""
Anthropic 兼容接口以 API_TOKEN 认证受信客户端,认证通过即按管理员级 Agent 集成处理。
"""
if not api_key or api_key != settings.API_TOKEN:
if not api_key or api_key != get_api_runtime_config_snapshot().api_token:
return _anthropic_error_response(
"invalid x-api-key",
401,
@@ -212,7 +212,7 @@ async def messages(
if auth_error:
return auth_error
if not settings.AI_AGENT_ENABLE:
if not get_api_runtime_config_snapshot().ai_agent_enable:
return _anthropic_error_response(
"MoviePilot AI agent is disabled.",
503,
+4 -4
View File
@@ -31,7 +31,7 @@ from app.agent.runtime_loader import (
get_running_agent_manager,
)
from app.agent.contracts import ReplyMode
from app.runtime.config import settings
from app.application.configuration import get_api_runtime_config_snapshot
from app.adapters.web.security.access import openai_bearer_scheme
from app.schemas.types import NotificationChannel
@@ -453,7 +453,7 @@ def _check_auth(
error_type="authentication_error",
code="invalid_api_key",
)
if credentials.credentials != settings.API_TOKEN:
if credentials.credentials != get_api_runtime_config_snapshot().api_token:
return _error_response(
"Invalid bearer token.",
401,
@@ -506,7 +506,7 @@ async def chat_completions(
if auth_error:
return auth_error
if not settings.AI_AGENT_ENABLE:
if not get_api_runtime_config_snapshot().ai_agent_enable:
return _error_response(
"MoviePilot AI agent is disabled.",
503,
@@ -607,7 +607,7 @@ async def responses(
if auth_error:
return auth_error
if not settings.AI_AGENT_ENABLE:
if not get_api_runtime_config_snapshot().ai_agent_enable:
return _error_response(
"MoviePilot AI agent is disabled.",
503,
+2 -2
View File
@@ -11,7 +11,7 @@ from app.schemas.tmdb import TmdbEpisode as _SchemaTmdbEpisode
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
from app.api.response import ResponseAPIRouter
from app.chain.tmdb import TmdbChain
from app.runtime.config import settings
from app.application.configuration import get_api_runtime_config_snapshot
from app.adapters.web.security.access import verify_token
from app.application.configuration import get_configured_system_config
from app.api.dependencies.auth import get_current_active_superuser_async
@@ -40,7 +40,7 @@ async def tmdb_recognition_cache(
"shared_recognized": get_configured_system_config().get(
SystemConfigKey.MediaRecognizeShareCount
) or 0,
"shared_recognize_enabled": settings.MEDIA_RECOGNIZE_SHARE,
"shared_recognize_enabled": get_api_runtime_config_snapshot().media_recognize_share,
"data": cache_items,
},
)
+2 -2
View File
@@ -8,7 +8,7 @@ from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter
from app.chain.media import MediaChain
from app.chain.torrents import TorrentsChain
from app.runtime.config import settings
from app.application.configuration import get_api_runtime_config_snapshot
from app.domain.context import MediaInfo, MusicInfo
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo
@@ -41,7 +41,7 @@ async def torrents_cache(_: object = Depends(get_current_active_superuser_async)
torrents_chain = TorrentsChain()
# 获取spider和rss两种缓存
if settings.SUBSCRIBE_MODE == "rss":
if get_api_runtime_config_snapshot().subscribe_mode == "rss":
cache_info = await torrents_chain.async_get_torrents("rss")
else:
cache_info = await torrents_chain.async_get_torrents("spider")
+15
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional, Protocol
@@ -49,6 +50,10 @@ class ApiRuntimeConfig:
access_token_expire_minutes: int
btrfs_fsid_dedup: bool
ai_agent_enable: bool
api_token: str | None = None
temp_path: Path = Path(".")
media_recognize_share: bool = False
subscribe_mode: str = "spider"
@dataclass(frozen=True, slots=True)
@@ -86,6 +91,16 @@ class ChainRuntimeConfig:
global_image_cache: bool = False
auto_download_user: Optional[str] = None
resource_url: Optional[str] = None
user_agent: str = ""
proxy: Any = None
proxy_server: Any = None
proxy_host: Optional[str] = None
cookiecloud_blacklist: Any = None
subscribe_mode: str = "spider"
no_cache_site_key: str = ""
refresh_batch_size: int = 50
torrent_cache_size: int = 1000
site_url: Optional[str] = None
@dataclass(frozen=True, slots=True)
+33 -41
View File
@@ -9,7 +9,7 @@ from lxml import etree
from app.chain import ChainBase
from app.chain._interaction import InteractionChainMixin
from app.runtime.config import global_vars, settings
from app.runtime.config import global_vars
from app.runtime.events import Event, eventmanager
from app.application.chain.data import SitePortProxy as SiteOper
from app.application.configuration import get_configured_system_config
@@ -175,18 +175,17 @@ class SiteChain(InteractionChainMixin, ChainBase):
"""
return domain in self.special_site_test
@staticmethod
def __zhuque_test(site: Site) -> Tuple[bool, str]:
def __zhuque_test(self, site: Site) -> Tuple[bool, str]:
"""
判断站点是否已经登陆:zhuique
"""
# 获取token
token = None
user_agent = site.ua or settings.USER_AGENT
user_agent = site.ua or self.runtime_config.user_agent
res = RequestUtils(
ua=user_agent,
cookies=site.cookie,
proxies=settings.PROXY if site.proxy else None,
proxies=self.runtime_config.proxy if site.proxy else None,
timeout=site.timeout or 15
).get_res(url=site.url)
if res is None:
@@ -207,7 +206,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
"User-Agent": f"{user_agent}"
},
cookies=site.cookie,
proxies=settings.PROXY if site.proxy else None,
proxies=self.runtime_config.proxy if site.proxy else None,
timeout=site.timeout or 15
).get_res(url=f"{site.url}api/user/getInfo")
if user_res is None:
@@ -220,12 +219,11 @@ class SiteChain(InteractionChainMixin, ChainBase):
else:
return False, f"错误:{user_res.status_code} {user_res.reason}"
@staticmethod
def __mteam_test(site: Site) -> Tuple[bool, str]:
def __mteam_test(self, site: Site) -> Tuple[bool, str]:
"""
判断站点是否已经登陆:m-team
"""
user_agent = site.ua or settings.USER_AGENT
user_agent = site.ua or self.runtime_config.user_agent
domain = site_rules.extract_domain(site.url)
url = f"https://api.{domain}/api/member/profile"
headers = {
@@ -235,7 +233,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
}
res = RequestUtils(
headers=headers,
proxies=settings.PROXY if site.proxy else None,
proxies=self.runtime_config.proxy if site.proxy else None,
timeout=site.timeout or 15
).post_res(url=url)
if res is None:
@@ -248,8 +246,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
else:
return False, f"错误:{res.status_code} {res.reason}"
@staticmethod
def __sunnypt_test(site: Site) -> Tuple[bool, str]:
def __sunnypt_test(self, site: Site) -> Tuple[bool, str]:
"""
通过 profile 接口测试 SunnyPT API Key 和下载权限
@@ -263,10 +260,10 @@ class SiteChain(InteractionChainMixin, ChainBase):
res = RequestUtils(
headers={
"Accept": "application/json",
"User-Agent": site.ua or settings.USER_AGENT,
"User-Agent": site.ua or self.runtime_config.user_agent,
"X-API-Key": site.apikey,
},
proxies=settings.PROXY if site.proxy else None,
proxies=self.runtime_config.proxy if site.proxy else None,
timeout=site.timeout or 15,
).get_res(url=f"{api_url}/profile")
if res is None:
@@ -283,12 +280,11 @@ class SiteChain(InteractionChainMixin, ChainBase):
return False, "当前账号没有下载权限"
return True, "连接成功"
@staticmethod
def __yema_test(site: Site) -> Tuple[bool, str]:
def __yema_test(self, site: Site) -> Tuple[bool, str]:
"""
判断站点是否已经登陆:yemapt
"""
user_agent = site.ua or settings.USER_AGENT
user_agent = site.ua or self.runtime_config.user_agent
url = f"{site.url}api/consumer/fetchSelfDetail"
headers = {
"User-Agent": user_agent,
@@ -298,7 +294,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
res = RequestUtils(
headers=headers,
cookies=site.cookie,
proxies=settings.PROXY if site.proxy else None,
proxies=self.runtime_config.proxy if site.proxy else None,
timeout=site.timeout or 15
).get_res(url=url)
if res is None:
@@ -318,8 +314,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
site.url = f"{site.url}index.php"
return self.__test(site)
@staticmethod
def __hddolby_test(site: Site) -> Tuple[bool, str]:
def __hddolby_test(self, site: Site) -> Tuple[bool, str]:
"""
判断站点是否已经登陆:hddolby
"""
@@ -331,7 +326,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
}
res = RequestUtils(
headers=headers,
proxies=settings.PROXY if site.proxy else None,
proxies=self.runtime_config.proxy if site.proxy else None,
timeout=site.timeout or 15
).get_res(url=url)
if res is None:
@@ -344,8 +339,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
else:
return False, f"错误:{res.status_code} {res.reason}"
@staticmethod
def __rousi_test(site: Site) -> Tuple[bool, str]:
def __rousi_test(self, site: Site) -> Tuple[bool, str]:
"""
判断站点是否已经登陆:rousi
"""
@@ -357,7 +351,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
}
res = RequestUtils(
headers=headers,
proxies=settings.PROXY if site.proxy else None,
proxies=self.runtime_config.proxy if site.proxy else None,
timeout=site.timeout or 15
).get_res(url=url)
if res is None:
@@ -477,7 +471,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
rss_url, errmsg = rsshelper.get_rss_link(
url=site_info.url,
cookie=cookie,
ua=site_info.ua or settings.USER_AGENT,
ua=site_info.ua or self.runtime_config.user_agent,
proxy=True if site_info.proxy else False,
timeout=site_info.timeout or 15
)
@@ -492,16 +486,16 @@ class SiteChain(InteractionChainMixin, ChainBase):
siteoper.update_cookie(domain=domain, cookies=cookie)
_update_count += 1
elif indexer:
if settings.COOKIECLOUD_BLACKLIST and any(
if self.runtime_config.cookiecloud_blacklist and any(
site_rules.extract_domain(domain) == site_rules.extract_domain(black_domain) for black_domain
in str(settings.COOKIECLOUD_BLACKLIST).split(",")):
in str(self.runtime_config.cookiecloud_blacklist).split(",")):
logger.warn(f"站点 {domain} 已在黑名单中,不添加站点")
continue
# 新增站点
domain_url = __indexer_domain(inx=indexer, sub_domain=domain)
proxy = False
res = RequestUtils(cookies=cookie,
ua=settings.USER_AGENT
ua=self.runtime_config.user_agent
).get_res(url=domain_url)
if res and res.status_code in [200, 500, 403]:
content = res.text
@@ -518,7 +512,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
logger.warn(f"站点 {indexer.get('name')} 连接状态码:{res.status_code},无法添加站点")
continue
else:
if not settings.PROXY_HOST:
if not self.runtime_config.proxy_host:
_fail_count += 1
logger.warn(f"站点 {indexer.get('name')} 连接失败,无法添加站点")
continue
@@ -527,8 +521,8 @@ class SiteChain(InteractionChainMixin, ChainBase):
logger.info(f"站点 {indexer.get('name')} 初次连接失败,尝试通过代理重试...")
proxy = True
res = RequestUtils(cookies=cookie,
ua=settings.USER_AGENT,
proxies=settings.PROXY
ua=self.runtime_config.user_agent,
proxies=self.runtime_config.proxy
).get_res(url=domain_url)
if res and res.status_code in [200, 500, 403]:
if not indexer.get("public") and not SiteUtils.is_logged_in(res.text):
@@ -547,7 +541,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
# 自动生成rss地址
rss_url, errmsg = rsshelper.get_rss_link(url=domain_url,
cookie=cookie,
ua=settings.USER_AGENT,
ua=self.runtime_config.user_agent,
proxy=proxy)
if errmsg:
logger.warn(errmsg)
@@ -616,7 +610,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
logger.info(f"开始缓存站点 {indexer.get('name')} 图标 ...")
icon_url, icon_base64 = self.__parse_favicon(url=indexer.get("domain"),
cookie=cookie,
ua=settings.USER_AGENT)
ua=self.runtime_config.user_agent)
if icon_url:
siteoper.update_icon(name=indexer.get("name"),
domain=domain,
@@ -702,18 +696,17 @@ class SiteChain(InteractionChainMixin, ChainBase):
except Exception as e:
return False, f"{str(e)}"
@staticmethod
def __test(site_info: Site) -> Tuple[bool, str]:
def __test(self, site_info: Site) -> Tuple[bool, str]:
"""
通用站点测试
"""
site_url = site_info.url
site_cookie = site_info.cookie
ua = site_info.ua or settings.USER_AGENT
ua = site_info.ua or self.runtime_config.user_agent
render = site_info.render
public = site_info.public
proxies = settings.PROXY if site_info.proxy else None
proxy_server = settings.PROXY_SERVER if site_info.proxy else None
proxies = self.runtime_config.proxy if site_info.proxy else None
proxy_server = self.runtime_config.proxy_server if site_info.proxy else None
timeout = site_info.timeout or 60
# 访问链接
@@ -815,8 +808,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
# 重新发送消息
self.remote_list(channel=channel, userid=userid, source=source)
@staticmethod
def update_cookie(site_info: Site,
def update_cookie(self, site_info: Site,
username: str, password: str, two_step_code: Optional[str] = None) -> Tuple[bool, str]:
"""
根据用户名密码更新站点Cookie
@@ -832,7 +824,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
username=username,
password=password,
two_step_code=two_step_code,
proxies=settings.PROXY_SERVER if site_info.proxy else None,
proxies=self.runtime_config.proxy_server if site_info.proxy else None,
timeout=site_info.timeout or 60
)
if result:
+19 -17
View File
@@ -7,7 +7,7 @@ from app.application.site.sites import SitesHelper # pylint: disable=import-err
from app.chain import ChainBase
from app.chain.media import MediaChain
from app.runtime.config import settings, global_vars
from app.runtime.config import global_vars
from app.domain.context import TorrentInfo, Context, MediaInfo
from app.domain.context import MusicInfo
from app.domain.meta.metamusic import MetaMusic
@@ -40,7 +40,7 @@ class TorrentsChain(ChainBase):
"""
返回缓存文件列表
"""
if settings.SUBSCRIBE_MODE == 'spider':
if self.runtime_config.subscribe_mode == 'spider':
return self._spider_file
return self._rss_file
@@ -67,7 +67,7 @@ class TorrentsChain(ChainBase):
"""
if not stype:
stype = settings.SUBSCRIBE_MODE
stype = self.runtime_config.subscribe_mode
# 读取缓存
if stype == 'spider':
@@ -92,7 +92,7 @@ class TorrentsChain(ChainBase):
:param stype: 强制指定缓存类型spider:爬虫缓存rss:rss缓存
"""
if not stype:
stype = settings.SUBSCRIBE_MODE
stype = self.runtime_config.subscribe_mode
music_file = self._music_spider_file if stype == 'spider' else self._music_rss_file
music_cache = self.load_cache(music_file) or {}
# 兼容性处理:为旧版本的Context对象补齐新增候选识别字段
@@ -105,7 +105,7 @@ class TorrentsChain(ChainBase):
:param stype: 强制指定缓存类型spider:爬虫缓存rss:rss缓存
"""
if not stype:
stype = settings.SUBSCRIBE_MODE
stype = self.runtime_config.subscribe_mode
if stype == 'spider':
return self._spider_file, self._music_spider_file
return self._rss_file, self._music_rss_file
@@ -135,7 +135,7 @@ class TorrentsChain(ChainBase):
"""
if not stype:
stype = settings.SUBSCRIBE_MODE
stype = self.runtime_config.subscribe_mode
# 异步读取缓存
if stype == 'spider':
@@ -456,7 +456,7 @@ class TorrentsChain(ChainBase):
site=site.get("id"),
site_name=site.get("name"),
site_cookie=site.get("cookie"),
site_ua=site.get("ua") or settings.USER_AGENT,
site_ua=site.get("ua") or self.runtime_config.user_agent,
site_proxy=site.get("proxy"),
site_order=site.get("pri"),
site_downloader=site.get("downloader"),
@@ -545,14 +545,14 @@ class TorrentsChain(ChainBase):
"""
判断站点是否不需要缓存
"""
for url_key in settings.NO_CACHE_SITE_KEY.split(','):
for url_key in self.runtime_config.no_cache_site_key.split(','):
if url_key in _domain:
return True
return False
# 刷新类型
if not stype:
stype = settings.SUBSCRIBE_MODE
stype = self.runtime_config.subscribe_mode
# 刷新站点
if not sites:
@@ -636,10 +636,10 @@ class TorrentsChain(ChainBase):
# 音乐与影视按同一公共参数独立计算刷新配额,并分别写入各自缓存,音乐不会被影视资源挤出
music_torrents = [
t for t in torrents if t.category == MediaType.MUSIC.value
][:settings.CONF.refresh]
][:self.runtime_config.refresh_batch_size]
torrents = [
t for t in torrents if t.category != MediaType.MUSIC.value
][:settings.CONF.refresh]
][:self.runtime_config.refresh_batch_size]
if torrents or music_torrents:
if __is_no_cache_site(domain):
# 不需要缓存的站点,直接处理
@@ -724,8 +724,10 @@ class TorrentsChain(ChainBase):
else:
target_cache[domain].append(context)
# 如果超过了限制条数则移除掉前面的,音乐与影视各自独立计算配额
if len(target_cache[domain]) > settings.CONF.torrents:
target_cache[domain] = target_cache[domain][-settings.CONF.torrents:]
if len(target_cache[domain]) > self.runtime_config.torrent_cache_size:
target_cache[domain] = target_cache[domain][
-self.runtime_config.torrent_cache_size:
]
finally:
torrents.clear()
music_torrents.clear()
@@ -814,7 +816,7 @@ class TorrentsChain(ChainBase):
rss_url, errmsg = RssHelper().get_rss_link(
url=site.get("url"),
cookie=site.get("cookie"),
ua=site.get("ua") or settings.USER_AGENT,
ua=site.get("ua") or self.runtime_config.user_agent,
proxy=True if site.get("proxy") else False,
timeout=site.get("timeout"),
)
@@ -831,13 +833,13 @@ class TorrentsChain(ChainBase):
# 发送消息
self.post_message(
Message(mtype=MessageType.SiteMessage, title=f"站点 {domain} RSS链接已过期",
link=settings.MP_DOMAIN('#/site'))
link=self.runtime_config.site_url)
)
else:
self.post_message(
Message(mtype=MessageType.SiteMessage, title=f"站点 {domain} RSS链接已过期",
link=settings.MP_DOMAIN('#/site')))
link=self.runtime_config.site_url))
except Exception as e:
logger.error(f"站点 {domain} RSS链接自动获取失败:{str(e)} - {traceback.format_exc()}")
self.post_message(Message(mtype=MessageType.SiteMessage, title=f"站点 {domain} RSS链接已过期",
link=settings.MP_DOMAIN('#/site')))
link=self.runtime_config.site_url))
+14
View File
@@ -178,6 +178,10 @@ def _build_api_runtime_config() -> ApiRuntimeConfig:
access_token_expire_minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES,
btrfs_fsid_dedup=settings.BTRFS_FSID_DEDUP,
ai_agent_enable=settings.AI_AGENT_ENABLE,
api_token=settings.API_TOKEN,
temp_path=settings.TEMP_PATH,
media_recognize_share=settings.MEDIA_RECOGNIZE_SHARE,
subscribe_mode=settings.SUBSCRIBE_MODE,
)
@@ -222,6 +226,16 @@ def _build_chain_runtime_config() -> ChainRuntimeConfig:
global_image_cache=settings.GLOBAL_IMAGE_CACHE,
auto_download_user=settings.AUTO_DOWNLOAD_USER,
resource_url=settings.MP_DOMAIN("#/resource"),
user_agent=settings.USER_AGENT,
proxy=settings.PROXY,
proxy_server=settings.PROXY_SERVER,
proxy_host=settings.PROXY_HOST,
cookiecloud_blacklist=settings.COOKIECLOUD_BLACKLIST,
subscribe_mode=settings.SUBSCRIBE_MODE,
no_cache_site_key=settings.NO_CACHE_SITE_KEY,
refresh_batch_size=settings.CONF.refresh,
torrent_cache_size=settings.CONF.torrents,
site_url=settings.MP_DOMAIN("#/site"),
)
@@ -570,6 +570,10 @@ app/api/dependencies/ # 按领域拆分依赖工厂
- Chain snapshot 继续覆盖超级用户、共享识别、辅助认证、全局图片缓存、自动下载用户和资源页链接;
消息、识别、交互、推荐和用户链的 5 个直接 `settings` 导入被移除,当前低水位进一步降到
161 个 settings import 文件,插件 SDK 与兼容入口未改。
- API snapshot 继续覆盖 API token、临时目录、识别共享与订阅模式;Chain snapshot 覆盖站点请求、
代理、CookieCloud 黑名单和种子缓存配额。Agent/OpenAI/Anthropic/TMDB/种子缓存 API 以及 Site、
Torrents Chain 共 7 个直接 `settings` 导入被移除,canonical 低水位降到 154 个文件;独立协议
测试显式注入快照,不再依赖 endpoint 模块中的全局配置别名。
- 直接调用 endpoint 和显式构造 `ChainRuntimeContext` 的旧测试/兼容入口仍有 fallback;正式 FastAPI 与
Startup 路径始终使用 HostRuntime 注入。插件 SDK 的 `app.sdk.config.settings`、动态 API 返回和事件字段未改。
@@ -9,7 +9,7 @@
"root": "app"
},
"settings_imports": {
"count": 161,
"count": 154,
"files": [
"app/adapters/cache/backends.py",
"app/adapters/cache/redis.py",
@@ -47,17 +47,12 @@
"app/agent/tools/impl/send_voice_message.py",
"app/agent/tools/impl/update_agent_task.py",
"app/agent/tools/impl/update_system_settings.py",
"app/api/endpoints/agent.py",
"app/api/endpoints/anthropic.py",
"app/api/endpoints/media.py",
"app/api/endpoints/message.py",
"app/api/endpoints/openai.py",
"app/api/endpoints/plugin.py",
"app/api/endpoints/storage.py",
"app/api/endpoints/subscribe.py",
"app/api/endpoints/system.py",
"app/api/endpoints/tmdb.py",
"app/api/endpoints/torrent.py",
"app/api/endpoints/transfer.py",
"app/api/servcookie.py",
"app/application/formatting.py",
@@ -75,10 +70,8 @@
"app/chain/message.py",
"app/chain/scraping.py",
"app/chain/search.py",
"app/chain/site.py",
"app/chain/subscribe.py",
"app/chain/system.py",
"app/chain/torrents.py",
"app/chain/transfer.py",
"app/cli.py",
"app/db/base.py",
+9 -10
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [],
"workflow_to_db": []
},
"edge_count": 6369,
"edge_sha256": "62638f6e334e7deb05902ca1e50cdcd03245a85c207dd2b4efaa03306cd28187",
"edge_count": 6368,
"edge_sha256": "03022352d218ecfb93295438f8264140fa7f6be2f7e55734e094989e454062ae",
"edges": [
"app -> app.runtime",
"app -> app.runtime.compat",
@@ -1632,6 +1632,7 @@
"app.api.endpoints.agent -> app.api.principal",
"app.api.endpoints.agent -> app.api.response",
"app.api.endpoints.agent -> app.application",
"app.api.endpoints.agent -> app.application.configuration",
"app.api.endpoints.agent -> app.application.messaging",
"app.api.endpoints.agent -> app.application.messaging.agent",
"app.api.endpoints.agent -> app.application.messaging.chat",
@@ -1677,8 +1678,8 @@
"app.api.endpoints.anthropic -> app.api.openai_utils",
"app.api.endpoints.anthropic -> app.api.presentation",
"app.api.endpoints.anthropic -> app.api.presentation.sse",
"app.api.endpoints.anthropic -> app.runtime",
"app.api.endpoints.anthropic -> app.runtime.config",
"app.api.endpoints.anthropic -> app.application",
"app.api.endpoints.anthropic -> app.application.configuration",
"app.api.endpoints.anthropic -> app.schemas",
"app.api.endpoints.anthropic -> app.schemas.openai",
"app.api.endpoints.auth -> app.api",
@@ -2023,8 +2024,8 @@
"app.api.endpoints.openai -> app.api.openai_utils",
"app.api.endpoints.openai -> app.api.presentation",
"app.api.endpoints.openai -> app.api.presentation.sse",
"app.api.endpoints.openai -> app.runtime",
"app.api.endpoints.openai -> app.runtime.config",
"app.api.endpoints.openai -> app.application",
"app.api.endpoints.openai -> app.application.configuration",
"app.api.endpoints.openai -> app.schemas",
"app.api.endpoints.openai -> app.schemas.openai",
"app.api.endpoints.openai -> app.schemas.types",
@@ -2268,8 +2269,6 @@
"app.api.endpoints.tmdb -> app.application.configuration",
"app.api.endpoints.tmdb -> app.chain",
"app.api.endpoints.tmdb -> app.chain.tmdb",
"app.api.endpoints.tmdb -> app.runtime",
"app.api.endpoints.tmdb -> app.runtime.config",
"app.api.endpoints.tmdb -> app.schemas",
"app.api.endpoints.tmdb -> app.schemas.context",
"app.api.endpoints.tmdb -> app.schemas.response",
@@ -2281,6 +2280,8 @@
"app.api.endpoints.torrent -> app.api.dependencies",
"app.api.endpoints.torrent -> app.api.dependencies.auth",
"app.api.endpoints.torrent -> app.api.response",
"app.api.endpoints.torrent -> app.application",
"app.api.endpoints.torrent -> app.application.configuration",
"app.api.endpoints.torrent -> app.chain",
"app.api.endpoints.torrent -> app.chain.media",
"app.api.endpoints.torrent -> app.chain.torrents",
@@ -2292,8 +2293,6 @@
"app.api.endpoints.torrent -> app.domain.metainfo",
"app.api.endpoints.torrent -> app.foundation",
"app.api.endpoints.torrent -> app.foundation.crypto",
"app.api.endpoints.torrent -> app.runtime",
"app.api.endpoints.torrent -> app.runtime.config",
"app.api.endpoints.torrent -> app.schemas",
"app.api.endpoints.torrent -> app.schemas.cache",
"app.api.endpoints.torrent -> app.schemas.media",
+16
View File
@@ -132,10 +132,19 @@ sys.modules["app.application.site.sites"] = sites
from fastapi.security import HTTPAuthorizationCredentials
from app import schemas
from app.api.endpoints import anthropic, openai
from app.api.endpoints.anthropic import messages as anthropic_messages
from app.api.endpoints.openai import chat_completions, responses
from app.application.configuration import ApiRuntimeConfig
from app.runtime.config import settings
runtime_config = ApiRuntimeConfig(
False, 60, False, settings.AI_AGENT_ENABLE,
api_token=settings.API_TOKEN,
)
anthropic.get_api_runtime_config_snapshot = lambda: runtime_config
openai.get_api_runtime_config_snapshot = lambda: runtime_config
credentials = HTTPAuthorizationCredentials(
scheme="Bearer",
credentials=settings.API_TOKEN,
@@ -367,9 +376,16 @@ sys.modules["app.application.site.sites"] = sites
from fastapi.security import HTTPAuthorizationCredentials
from app import schemas
from app.api.endpoints import anthropic, openai
from app.application.configuration import ApiRuntimeConfig
from app.runtime.config import settings
settings.AI_AGENT_ENABLE = True
runtime_config = ApiRuntimeConfig(
False, 60, False, True,
api_token=settings.API_TOKEN,
)
anthropic.get_api_runtime_config_snapshot = lambda: runtime_config
openai.get_api_runtime_config_snapshot = lambda: runtime_config
credentials = HTTPAuthorizationCredentials(
scheme="Bearer",
credentials=settings.API_TOKEN,
+9 -1
View File
@@ -387,7 +387,15 @@ def test_music_cache_not_evicted_by_video_torrents():
fake_settings.NO_CACHE_SITE_KEY = "no-cache-site.invalid"
with (
patch("app.chain.torrents.settings", fake_settings),
patch.object(
chain,
"runtime_config",
SimpleNamespace(
torrent_cache_size=fake_settings.CONF.torrents,
refresh_batch_size=fake_settings.CONF.refresh,
no_cache_site_key=fake_settings.NO_CACHE_SITE_KEY,
),
),
patch.object(chain, "load_cache", side_effect=_fake_load),
patch.object(chain, "browse", side_effect=_fake_browse),
patch.object(chain, "save_cache", save_cache),
+3 -1
View File
@@ -426,7 +426,9 @@ def test_sunnypt_site_test_uses_profile_api(monkeypatch):
timeout=15,
)
state, message = SiteChain._SiteChain__sunnypt_test(site)
chain = object.__new__(SiteChain)
chain.runtime_config = SimpleNamespace(proxy=None, user_agent="MoviePilot-Test")
state, message = chain._SiteChain__sunnypt_test(site)
assert state
assert message == "连接成功"
+6 -1
View File
@@ -1,6 +1,7 @@
import asyncio
import inspect
import pickle
from types import SimpleNamespace
from unittest.mock import Mock
from app.api.endpoints import tmdb as tmdb_endpoint
@@ -282,7 +283,11 @@ def test_tmdb_cache_endpoint_returns_management_statistics(monkeypatch):
"get_configured_system_config",
lambda: type("SystemConfigStub", (), {"get": get_system_config})(),
)
monkeypatch.setattr(tmdb_endpoint.settings, "MEDIA_RECOGNIZE_SHARE", True)
monkeypatch.setattr(
tmdb_endpoint,
"get_api_runtime_config_snapshot",
lambda: SimpleNamespace(media_recognize_share=True),
)
response = asyncio.run(tmdb_endpoint.tmdb_recognition_cache(None))
+52 -13
View File
@@ -633,7 +633,10 @@ def test_prepare_web_agent_audio_attachment_converts_unsupported_audio(tmp_path)
return SimpleNamespace(returncode=0, stderr="")
run.side_effect = write_converted_file
with patch("app.api.endpoints.agent.settings", SimpleNamespace(TEMP_PATH=tmp_path)):
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(temp_path=tmp_path),
):
output_path = _prepare_web_agent_audio_attachment_path(str(source_path))
assert output_path == converted_path
@@ -684,7 +687,10 @@ def test_web_agent_stream_returns_error_when_voice_transcription_fails():
request = SimpleNamespace()
user = SimpleNamespace(id=1, name="admin")
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch(
"app.api.endpoints.agent._transcribe_web_agent_audio_files",
return_value=None,
) as transcribe_audio:
@@ -722,7 +728,10 @@ def test_web_agent_stream_does_not_block_event_loop_during_transcription():
return await stream_task
try:
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch(
"app.api.endpoints.agent._resolve_web_agent_audio_refs",
return_value=[Mock()],
), patch(
@@ -788,7 +797,10 @@ def test_web_agent_stream_binds_session_to_agent_manager():
return "".join(await _collect_streaming_response(response))
try:
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch(
"app.api.endpoints.agent._get_web_agent_type",
return_value=FakeWebAgent,
):
@@ -884,7 +896,10 @@ def test_web_agent_stream_emits_secret_result_only_as_protected_event():
return "".join(await _collect_streaming_response(response))
try:
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch(
"app.api.endpoints.agent._get_web_agent_type",
return_value=FakeProtectedAgent,
), patch(
@@ -942,7 +957,10 @@ def test_web_agent_cancel_keeps_existing_display_history():
return "".join(await _collect_streaming_response(response))
try:
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch.object(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch.object(
agent_manager,
"matches_secret_confirmation",
return_value=True,
@@ -982,7 +1000,10 @@ def test_web_agent_stream_rejects_confirmation_without_protected_capability():
response = await web_agent_stream(payload, request, user)
return "".join(await _collect_streaming_response(response))
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch.object(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch.object(
agent_manager,
"matches_secret_confirmation",
return_value=True,
@@ -1008,7 +1029,10 @@ def test_web_agent_stream_keeps_confirmation_without_pending_on_normal_path():
body = "".join(await _collect_streaming_response(response))
return response, body
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch.object(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch.object(
agent_manager,
"process_message",
new=AsyncMock(return_value="普通回复"),
@@ -1076,7 +1100,10 @@ def test_web_agent_stream_drops_secret_result_after_disconnect():
return body
try:
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch.object(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch.object(
agent_manager,
"matches_secret_confirmation",
return_value=True,
@@ -1116,7 +1143,10 @@ def test_web_agent_stream_emits_heartbeat_during_idle_tool_wait():
response = await web_agent_stream(payload, request, user)
return "".join(await _collect_streaming_response(response))
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch(
"app.api.endpoints.agent.WEB_AGENT_STREAM_HEARTBEAT_SECONDS",
0.01,
), patch(
@@ -1188,7 +1218,10 @@ def test_web_agent_stop_finishes_stream_without_error():
return "".join(received)
try:
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch(
"app.api.endpoints.agent._is_web_agent_traditional_message",
return_value=False,
), patch(
@@ -1230,7 +1263,10 @@ def test_web_agent_stream_rechecks_running_service_before_enqueue():
response = await web_agent_stream(payload, request, user)
return "".join(await _collect_streaming_response(response))
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch(
"app.api.endpoints.agent._is_web_agent_traditional_message",
return_value=False,
), patch(
@@ -1366,7 +1402,10 @@ def test_web_agent_stream_sends_done_before_snapshot_persistence_finishes():
return "".join(received)
try:
with patch("app.api.endpoints.agent.settings.AI_AGENT_ENABLE", True), patch(
with patch(
"app.api.endpoints.agent.get_api_runtime_config_snapshot",
return_value=SimpleNamespace(ai_agent_enable=True),
), patch(
"app.api.endpoints.agent._is_web_agent_traditional_message",
return_value=False,
), patch(