mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 03:51:58 +00:00
6af4e396b9
- 服务端可以托管前端构建产物(AGENTLAB_WEB_DIST):页面路由回退到 index.html,/api 下不存在的路径照旧是 JSON 404, 挡住路径穿越;index.html 不缓存,assets/ 带哈希的文件长缓存;没设时行为完全不变 - scripts/prod.sh:不用 Docker 的生产运行。按需构建前端,单进程、不热重载,默认只听本机,--host 开放到局域网并提示没有登录 - Dockerfile(多阶段构建,Linux 镜像):前端在构建机架构上构建,运行阶段用 python:3.12-slim,依赖装进独立的 venv, 以非 root 用户(uid 10001)运行,数据在 /data,带健康检查;.dockerignore 排除 .env、data/、各种库文件,镜像里实测没有密钥和数据库 - scripts/package-docker.sh:构建指定平台的镜像(默认按本机架构,可选 linux/amd64),打成 release/agentlab-<版本>-linux-<架构>.tar,里面有 image.tar、deploy.sh、agentlab.env、README.md、SHA256SUMS - deploy/deploy.sh:目标机上只依赖 Docker,离线可用。install / start / stop / restart / status / logs / upgrade / uninstall; 检查架构和配置,自动导入镜像,替换容器时保留旧容器以便回退,数据永远保留,日志轮转 - 修正 bubblewrap 的可用性判断:以前只看有没有装 bwrap,容器里和 Ubuntu 24.04 默认配置下装了却建不了命名空间, 每次执行代码都失败;现在真起一次试,不能用就退回下一个后端,并写明原因 - README 增加「生产部署」一节,部署细节见 deploy/README.md
97 lines
3.8 KiB
Python
97 lines
3.8 KiB
Python
"""生产部署:由服务端直接托管前端构建产物(AGENTLAB_WEB_DIST)。
|
||
|
||
开发时页面由 Vite 开发服务器提供、/api 由它转发,用不到这里。生产部署只起一个进程,
|
||
页面和接口同源,省掉 CORS,也不用另配静态服务器。
|
||
|
||
规则:
|
||
- 构建产物里有这个文件就返回它;
|
||
- 其余非 /api 的 GET 回退到 index.html:前端是单页应用,/studio 这类地址由前端路由处理;
|
||
- /api 下不存在的路径照旧是 JSON 404。排除 /api 写在路由的路径规则里,不在处理函数里判断——
|
||
否则 POST /api/不存在 会因为「路径对上了、方法不对」变成 405;
|
||
- 只注册 GET / HEAD 的 HTTP 路由,WebSocket 不经过这里;
|
||
- index.html 不缓存,发版后刷新即生效;assets/ 下的文件名带内容哈希,长缓存。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import mimetypes
|
||
from pathlib import Path
|
||
|
||
from fastapi import FastAPI, HTTPException
|
||
from fastapi.responses import FileResponse
|
||
from starlette.convertors import Convertor, register_url_convertor
|
||
|
||
_NO_CACHE = "no-cache"
|
||
_IMMUTABLE = "public, max-age=31536000, immutable"
|
||
|
||
# 精简的 Linux 镜像里没有 /etc/mime.types,mimetypes 的内置表又缺几种(woff2 之类)。
|
||
# 构建产物里出现的类型写死在这里,别的再交给 mimetypes 猜
|
||
_TYPES = {
|
||
".html": "text/html; charset=utf-8",
|
||
".js": "text/javascript; charset=utf-8",
|
||
".mjs": "text/javascript; charset=utf-8",
|
||
".css": "text/css; charset=utf-8",
|
||
".json": "application/json",
|
||
".map": "application/json",
|
||
".svg": "image/svg+xml",
|
||
".png": "image/png",
|
||
".ico": "image/x-icon",
|
||
".woff": "font/woff",
|
||
".woff2": "font/woff2",
|
||
".wasm": "application/wasm",
|
||
".txt": "text/plain; charset=utf-8",
|
||
}
|
||
|
||
|
||
class _WebPath(Convertor[str]):
|
||
"""任意路径,但不以 api 这一段开头:/api、/api/… 留给接口路由和它们自己的 404。"""
|
||
|
||
regex = r"(?!api(?:/|$)).*"
|
||
|
||
def convert(self, value: str) -> str:
|
||
return value
|
||
|
||
def to_string(self, value: str) -> str:
|
||
return value
|
||
|
||
|
||
register_url_convertor("agentlab_web", _WebPath())
|
||
|
||
|
||
def _media_type(path: Path) -> str | None:
|
||
return _TYPES.get(path.suffix.lower()) or mimetypes.guess_type(path.name)[0]
|
||
|
||
|
||
def mount_web(app: FastAPI, dist: Path) -> None:
|
||
"""在 app 上挂页面托管。必须在所有 /api 路由注册之后调用。"""
|
||
root = dist.resolve()
|
||
index = root / "index.html"
|
||
if not index.is_file():
|
||
# 配了却没有产物:启动时就报出来,别等打开页面才看到一个 404
|
||
raise RuntimeError(
|
||
f"AGENTLAB_WEB_DIST 指向的 {root} 下没有 index.html。"
|
||
"先构建前端(cd frontend && pnpm build),或者去掉这个设置"
|
||
)
|
||
|
||
def respond(target: Path, *, immutable: bool = False) -> FileResponse:
|
||
return FileResponse(
|
||
target,
|
||
media_type=_media_type(target),
|
||
headers={"Cache-Control": _IMMUTABLE if immutable else _NO_CACHE},
|
||
)
|
||
|
||
async def serve(path: str) -> FileResponse:
|
||
if path:
|
||
target = (root / path).resolve()
|
||
# resolve 之后还得在 dist 里面:挡住 %2e%2e 这类穿越和指向外面的软链
|
||
if target.is_file() and target.is_relative_to(root):
|
||
return respond(target, immutable=path.startswith("assets/"))
|
||
if path.startswith("assets/"):
|
||
# 发版后旧页面还在请求旧哈希的文件。回退成 index.html 的话浏览器会把它当脚本
|
||
# 解析、报一个看不懂的 MIME 错误,所以明确给 404
|
||
raise HTTPException(status_code=404)
|
||
return respond(index)
|
||
|
||
app.add_api_route(
|
||
"/{path:agentlab_web}", serve, methods=["GET", "HEAD"], include_in_schema=False,
|
||
)
|