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