Files
AgentLab/backend/app/core/public_url.py
T
ThearchyHelios d8034ca71a 对外访问地址只留一个:core/public_url.py 的 public_base_url(),通知、Webhook 链接、OIDC 回调、来源核对都改用它
以前有两个「对外地址」:环境变量 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 两条都会失败。设置页把它挪到「通用」在后面的前端提交里。
2026-10-16 10:18:12 +08:00

98 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""对外访问地址:别人打开 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}"