mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-31 20:34:48 +08:00
fix(agent): route API calls to the local backend (#6875)
This commit is contained in:
@@ -64,13 +64,12 @@ class MoviePilotApiExecutor:
|
||||
|
||||
@staticmethod
|
||||
def _resolve_base_url() -> str:
|
||||
"""解析本机 API 基址,避免把任意用户输入当成请求目标。"""
|
||||
configured_domain = str(get_runtime_setting("APP_DOMAIN", "") or "").strip()
|
||||
if configured_domain.startswith(("http://", "https://")):
|
||||
return configured_domain.rstrip("/")
|
||||
host = str(get_runtime_setting("HOST", "127.0.0.1") or "127.0.0.1")
|
||||
if host in {"0.0.0.0", "::", "[::]"}:
|
||||
host = "127.0.0.1"
|
||||
"""按后端监听地址直连本机 API,不依赖对外域名和反向代理。"""
|
||||
host = str(get_runtime_setting("HOST", "127.0.0.1") or "127.0.0.1").strip("[]")
|
||||
# 通配监听地址按相同地址族回环,兼容只监听 IPv6 的部署。
|
||||
host = {"0.0.0.0": "127.0.0.1", "::": "::1"}.get(host, host)
|
||||
if ":" in host:
|
||||
host = f"[{host}]"
|
||||
port = int(get_runtime_setting("PORT", 3001))
|
||||
return f"http://{host}:{port}"
|
||||
|
||||
|
||||
@@ -922,6 +922,9 @@ SDK method。普通 MCP 客户端如需这些 provider 原生能力,应使用
|
||||
宿主按 `operation_id` 决定固定 method 与 path,使用真实持久化管理员身份为
|
||||
API KEY 集成签发短期本机令牌,并按 operation 执行权限、确认、结果脱敏和恢复策略。
|
||||
调用方不能注入 host、URL、认证头或 API Token。
|
||||
网关按后端监听配置 `HOST` / `PORT` 通过 HTTP 直连本机 API,并绕过环境代理。
|
||||
`0.0.0.0` / `::` 通配监听分别使用 `127.0.0.1` / `::1` 回环地址,显式 IPv6 地址按 URL 规范加方括号。
|
||||
`APP_DOMAIN` 用于 Passkey 等对外地址,不作为网关基址;反代域名在容器内不可达时无需清空该配置。
|
||||
通知渠道的 `subscription.add`、`subscription.update`、`subscription.delete` 使用渠道账号绑定的
|
||||
有效 MoviePilot 用户身份,渠道管理员也不会借用超级管理员身份。创建时订阅归属绑定用户,
|
||||
普通用户只能修改或删除自己的订阅;未绑定或绑定用户已停用时拒绝执行。三项操作仍保留确认机制。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: moviepilot-api
|
||||
version: 42
|
||||
version: 43
|
||||
description: >-
|
||||
Use this skill for MoviePilot product operations such as media search, torrent
|
||||
search, downloads, subscriptions, library checks, sites, storage, workflows,
|
||||
@@ -78,6 +78,11 @@ only `operation_id`, `path_params`, `query`, and `body`. The host chooses the
|
||||
fixed HTTP method and path, creates the current user's authentication token,
|
||||
applies authorization and confirmation policy, and returns the API response.
|
||||
|
||||
The gateway connects directly to the local backend listener using `HOST` and
|
||||
`PORT`, bypassing environment proxies. `APP_DOMAIN` is for public URLs such as
|
||||
Passkey origins and is not the gateway base URL. Keep it configured even when
|
||||
the public reverse proxy is unreachable from inside the container.
|
||||
|
||||
`subscription.add`, `subscription.update`, and `subscription.delete` require
|
||||
authorization for the action and scope and use the active MoviePilot user bound to the channel account,
|
||||
including for channel administrators. Creation belongs to that user; ordinary
|
||||
|
||||
@@ -4,12 +4,72 @@ from types import SimpleNamespace
|
||||
from typing import Optional
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.agent.api.executor import ApiExecutionContext, MoviePilotApiExecutor
|
||||
from app.agent.tools.base import format_tool_result_for_agent
|
||||
from app.agent.tools.manager import MoviePilotToolsManager
|
||||
from app.schemas.types import NotificationChannel
|
||||
|
||||
|
||||
@pytest.mark.parametrize("app_domain", ["", "https://custom.domain.example:5443/moviepilot/"])
|
||||
@pytest.mark.parametrize(
|
||||
("host", "port", "base_url"),
|
||||
[
|
||||
(None, 3001, "http://127.0.0.1:3001"),
|
||||
("", 3001, "http://127.0.0.1:3001"),
|
||||
("0.0.0.0", 13001, "http://127.0.0.1:13001"),
|
||||
("127.0.0.1", 3001, "http://127.0.0.1:3001"),
|
||||
("192.0.2.10", 13001, "http://192.0.2.10:13001"),
|
||||
("localhost", 13001, "http://localhost:13001"),
|
||||
("::", 13001, "http://[::1]:13001"),
|
||||
("[::]", 13001, "http://[::1]:13001"),
|
||||
("::1", 3001, "http://[::1]:3001"),
|
||||
("[::1]", 3001, "http://[::1]:3001"),
|
||||
("2001:db8::10", 13001, "http://[2001:db8::10]:13001"),
|
||||
("[2001:db8::10]", 13001, "http://[2001:db8::10]:13001"),
|
||||
],
|
||||
)
|
||||
def test_executor_requests_local_listener_independently_of_public_domain(
|
||||
app_domain: str,
|
||||
host: Optional[str],
|
||||
port: int,
|
||||
base_url: str,
|
||||
) -> None:
|
||||
"""配置对外反代域名后,API 仍须按本机监听地址和端口发送且绕过环境代理。"""
|
||||
runtime_settings = {"APP_DOMAIN": app_domain, "HOST": host, "PORT": port}
|
||||
response = SimpleNamespace(
|
||||
status_code=200,
|
||||
headers={},
|
||||
json=lambda: {"success": True, "data": []},
|
||||
aclose=AsyncMock(),
|
||||
)
|
||||
request = AsyncMock(return_value=response)
|
||||
request_factory = MagicMock(return_value=SimpleNamespace(request=request))
|
||||
executor = MoviePilotApiExecutor(
|
||||
context=ApiExecutionContext(user_id="1", username="admin", is_admin=True),
|
||||
request_factory=request_factory,
|
||||
)
|
||||
|
||||
with (
|
||||
patch("app.agent.api.executor.get_runtime_setting", side_effect=runtime_settings.get),
|
||||
patch("app.agent.api.executor.create_access_token", return_value="token"),
|
||||
):
|
||||
result = asyncio.run(executor.execute("subscription.list", query={"page": 1}))
|
||||
|
||||
assert json.loads(result) == {"success": True, "data": []}
|
||||
request.assert_awaited_once_with(
|
||||
method="GET",
|
||||
url=f"{base_url}/api/v1/subscribe/",
|
||||
params={"page": 1},
|
||||
json=None,
|
||||
raise_exception=True,
|
||||
)
|
||||
assert request_factory.call_args.kwargs["headers"]["Authorization"] == "Bearer token"
|
||||
assert request_factory.call_args.kwargs["trust_env"] is False
|
||||
response.aclose.assert_awaited_once()
|
||||
|
||||
|
||||
def _execute_with_headers(headers: dict[str, str]) -> tuple[dict, AsyncMock]:
|
||||
"""用内存 HTTP 响应执行一次固定 API operation。"""
|
||||
response = SimpleNamespace(
|
||||
|
||||
@@ -117,7 +117,7 @@ def test_modified_builtin_skills_have_incremented_versions() -> None:
|
||||
"command-dispatch": "2",
|
||||
"database-operation": "9",
|
||||
"feedback-issue": "11",
|
||||
"moviepilot-api": "42",
|
||||
"moviepilot-api": "43",
|
||||
"moviepilot-update": "5",
|
||||
"organize-files": "5",
|
||||
"transfer-failed-retry": "5",
|
||||
|
||||
Reference in New Issue
Block a user