mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:31:59 +00:00
d8034ca71a
以前有两个「对外地址」:环境变量 AGENTLAB_PUBLIC_URL 只管 OIDC 回调和 Origin 核对,设置里 notify.public_base_url 只管 通知里的链接。只配了环境变量的部署,通知里不带链接;只在设置里填了的,回调又按请求的 Host 拼——违背「一种说法」。 - 新增 core/public_url.py:环境变量优先,其次设置(沿用 notify.public_base_url 这个键),都没有是 None。设置的值缓存在 进程里(中间件按请求核对来源,不能每次查库):启动时 refresh 一次,PUT /api/settings 写完库就 remember,不用重启; 通知、定时任务 Webhook 手里本来就有会话,用 refresh 取,顺手按库里的值对齐缓存 - 地址的形状检查和规范化只有一份(url_problem / normalize),设置接口的中文报错和环境变量的启动校验都用它;环境变量 写错了(没有 http:// 或 https://、带 ? 或 #)启动时就报错,不再拼出坏的回调地址 - GET /api/settings 另给只读的 public_url 一组 {value, env_locked},设置页据此显示「由环境变量指定」;环境变量设了时 PUT 把这一项改成别的值回 409(public_url_env_locked,field 指到这一项),原样写回不算改;public_url 一组写不进库 - notify/service 取地址的那一处、scheduler/webhook 链接里的地址、api/auth 的 OIDC 回调、auth/middleware 的 Origin 核对 都改走 public_base_url;deploy/agentlab.env 把这个变量的说法改成全站唯一的对外访问地址 测试 tests/test_public_url.py:先后顺序(环境变量 > 设置 > 无)、规范化、启动校验、PUT 刷新缓存、环境变量锁定(409、 不挡原样写回)、只配环境变量时通知带链接、OIDC 回调和 Origin 核对认设置里的地址。去掉修复回跑,只配环境变量的通知 链接、按设置核对 Origin 两条都会失败。设置页把它挪到「通用」在后面的前端提交里。
98 lines
4.5 KiB
Python
98 lines
4.5 KiB
Python
"""对外访问地址:别人打开 AgentLab 用的那个地址(https://agentlab.example.com),全站只有这一个说法(04 细案 B4)。
|
||
|
||
用到它的地方:通知和定时任务 Webhook 里的链接、单点登录的回调地址、非 GET 请求的来源核对(auth/middleware.py);
|
||
之后的分享链接、接口说明也从这里取。服务端不知道自己在反向代理后面叫什么,只能由人告诉它。
|
||
|
||
取值(public_base_url):
|
||
- 环境变量 AGENTLAB_PUBLIC_URL 优先:部署时定死,设置页上显示「由环境变量指定」、不能改(同登录方式的 env_locked);
|
||
- 没设时取设置里 notify 一组的 public_base_url:沿用这个键,界面上叫「对外访问地址」,在「设置 → 通用」里改;
|
||
- 都没有是 None:通知里不附链接,单点登录的回调和来源核对按请求自己的 Host 认。
|
||
|
||
以前两处各管各的:环境变量只管回调和来源核对、设置只管通知里的链接——只配了环境变量的部署,通知里不带链接;只在设置里
|
||
填了的,回调又按请求的 Host 拼。违背「一种说法」。
|
||
|
||
设置的值缓存在进程里:中间件按请求核对来源,不能每次查库。启动时读一次(main._platform_startup),改设置的接口写完库就
|
||
remember;手里本来就有会话的异步路径(通知、定时任务 Webhook)用 refresh 取,顺手按库里的值对齐缓存。和登录设置的缓存一样
|
||
以单进程部署为前提。
|
||
|
||
这个模块不在顶层导入 core/config:config 的校验器要用这里的 url_problem,顶层互相导入会在启动时绕成环。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
from urllib.parse import urlsplit
|
||
|
||
#: 设置里存它的那一组、那一项(api/settings.py 的 NOTIFY_GROUP 就是这一组)
|
||
SETTING_GROUP = "notify"
|
||
SETTING_KEY = "public_base_url"
|
||
#: 环境变量的名字(core/config.Settings.public_url 按 AGENTLAB_ 前缀读它)
|
||
ENV_NAME = "AGENTLAB_PUBLIC_URL"
|
||
|
||
_stored: str | None = None
|
||
|
||
|
||
def url_problem(value: str) -> str | None:
|
||
"""地址的形状有什么问题:None 是没问题;"form" 不是 http:// 或 https:// 开头的完整地址;"suffix" 带了 ? 或 #
|
||
(链接要接在地址后面)。设置接口按它出中文的说法,环境变量按它在启动时报错。可以带路径(反向代理挂在子路径下)。"""
|
||
text = value.strip()
|
||
try:
|
||
parts = urlsplit(text)
|
||
except ValueError:
|
||
return "form"
|
||
if parts.scheme not in ("http", "https") or not parts.hostname or " " in text:
|
||
return "form"
|
||
if parts.query or parts.fragment:
|
||
return "suffix"
|
||
return None
|
||
|
||
|
||
def normalize(value: Any) -> str | None:
|
||
"""存着的(或刚填的)值 → 去掉首尾空白和末尾的 /;空的、形状不对的当没填(None)。末尾带 / 的地址拼链接会多出一个斜杠。"""
|
||
if not isinstance(value, str) or not value.strip() or url_problem(value):
|
||
return None
|
||
return value.strip().rstrip("/")
|
||
|
||
|
||
def env_url() -> str | None:
|
||
"""环境变量给的地址;没设为 None。形状在启动时由 config 的校验器查过、末尾的 / 去掉了。"""
|
||
from app.core.config import settings
|
||
|
||
return settings.public_url or None
|
||
|
||
|
||
def env_locked() -> bool:
|
||
"""环境变量设了:设置里的那一项不生效、不能改。"""
|
||
return env_url() is not None
|
||
|
||
|
||
def public_base_url() -> str | None:
|
||
"""对外访问地址:环境变量优先,其次设置(进程内缓存),都没有是 None。不查库,中间件里也能调。"""
|
||
return env_url() or _stored
|
||
|
||
|
||
def remember(value: Any) -> None:
|
||
"""改设置的接口写完库之后调:缓存换成新存的值(形状不对、空的当没填)。"""
|
||
global _stored
|
||
_stored = normalize(value)
|
||
|
||
|
||
async def refresh(session: Any = None) -> str | None:
|
||
"""按库里存的设置对齐缓存,交回对外访问地址。启动时、手里有会话的异步路径用。"""
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import Setting
|
||
|
||
if session is None:
|
||
async with SessionLocal() as own:
|
||
row = await own.get(Setting, SETTING_GROUP)
|
||
else:
|
||
row = await session.get(Setting, SETTING_GROUP)
|
||
value = row.value if row is not None else None
|
||
remember(value.get(SETTING_KEY) if isinstance(value, dict) else None)
|
||
return public_base_url()
|
||
|
||
|
||
def origin_of(url: str) -> str:
|
||
"""地址里的「协议://主机[:端口]」:来源核对按它比。"""
|
||
parts = urlsplit(url)
|
||
return f"{parts.scheme}://{parts.netloc}"
|