Files
AgentLab/scripts/prod.sh
T
ThearchyHelios 6af4e396b9 生产部署:prod.sh 直接运行,package-docker.sh 打 Docker 离线包(镜像 + 一键部署脚本 + 配置 + 说明)
- 服务端可以托管前端构建产物(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
2026-09-30 09:48:16 +08:00

156 lines
6.5 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# 不用 Docker 的生产运行:构建前端,由后端一并托管页面,单进程、不热重载。Ctrl-C 退出。
set -euo pipefail
usage() {
cat <<'USAGE'
用法:./scripts/prod.sh [--host] [--port <端口>] [--build | --no-build]
--host 监听 0.0.0.0(同一网络里的设备都能打开)。默认只听 127.0.0.1,只有本机能打开。
注意:AgentLab 没有登录,同一网络里打得开页面的人都能用它——跑工作流、花你配的
模型额度、读数据源、执行沙箱代码。只在信得过的网络里开,并配好防火墙。
--port <端口> 监听端口。也可以用环境变量 AGENTLAB_PORT,默认 8000。
--build 不管前端产物新旧,强制重新构建。
--no-build 跳过构建,直接用现有的 frontend/dist。
-h, --help 显示这段说明。
默认只在 frontend/dist 不存在、或者比前端源码旧的时候才构建。
数据目录照后端配置:默认是仓库下的 data/,用环境变量 AGENTLAB_DATA_DIR 改。
只能单进程运行:运行管理器在进程内、数据库是 SQLite,多个 worker 会互相踩。
USAGE
}
# 参数先认完再干活,认不出的直接报错,免得写错的参数被悄悄忽略
HOST="127.0.0.1"
PORT="${AGENTLAB_PORT:-8000}"
BUILD="auto"
while [ $# -gt 0 ]; do
case "$1" in
--host) HOST="0.0.0.0" ;;
--port)
[ $# -ge 2 ] || { echo "--port 后面要跟端口号"; echo; usage; exit 2; }
PORT="$2"; shift ;;
--port=*) PORT="${1#--port=}" ;;
--build) BUILD="always" ;;
--no-build) BUILD="never" ;;
-h|--help) usage; exit 0 ;;
*) echo "认不出的参数:$1"; echo; usage; exit 2 ;;
esac
shift
done
case "$PORT" in
''|*[!0-9]*) echo "端口必须是数字:$PORT"; exit 2 ;;
esac
if [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
echo "端口超出范围:$PORT"; exit 2
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST="$ROOT/frontend/dist"
CONDA_ENV="${AGENTLAB_CONDA_ENV:-agentlab}"
# ---- 后端解释器:和 dev.sh 一样找 conda 环境,没有就按 environment.yml 建 ----
command -v conda >/dev/null || { echo "缺少 conda:https://conda-forge.org/miniforge/"; exit 1; }
if ! conda env list | awk '{print $1}' | grep -qx "$CONDA_ENV"; then
echo "==> 创建 conda 环境 $CONDA_ENV"
conda env create -f "$ROOT/environment.yml"
fi
PY_BIN="$(conda run -n "$CONDA_ENV" python -c 'import sys; print(sys.executable)' 2>/dev/null)"
[ -x "$PY_BIN" ] || { echo "conda 环境 $CONDA_ENV 不可用,试试:conda env create -f environment.yml"; exit 1; }
echo "==> 后端解释器 $PY_BIN"
# ---- 前端构建 ----
# dist 比源码旧:源码、入口页、依赖清单、构建配置里有任何一个比 dist/index.html 新
dist_stale() {
[ -f "$DIST/index.html" ] || return 0
local newer
newer="$(find "$ROOT/frontend/src" "$ROOT/frontend/public" \
"$ROOT/frontend/index.html" "$ROOT/frontend/package.json" "$ROOT/frontend/pnpm-lock.yaml" \
"$ROOT/frontend/vite.config.ts" "$ROOT/frontend/tsconfig.json" \
-newer "$DIST/index.html" -print 2>/dev/null | head -n 1)"
[ -n "$newer" ]
}
need_build=""
case "$BUILD" in
always) need_build=1 ;;
never)
[ -f "$DIST/index.html" ] || { echo "用了 --no-build,但 $DIST 下没有 index.html。先去掉 --no-build 构建一次"; exit 1; }
;;
auto)
if [ ! -f "$DIST/index.html" ]; then
echo "==> 前端还没构建过"; need_build=1
elif dist_stale; then
echo "==> 前端源码比构建产物新"; need_build=1
else
echo "==> 前端构建产物是最新的,跳过构建(--build 可强制重建)"
fi
;;
esac
if [ -n "$need_build" ]; then
command -v pnpm >/dev/null || { echo "缺少 pnpm:npm i -g pnpm"; exit 1; }
[ -d "$ROOT/frontend/node_modules" ] || (cd "$ROOT/frontend" && pnpm install --frozen-lockfile)
echo "==> 构建前端(pnpm build)"
(cd "$ROOT/frontend" && pnpm build)
[ -f "$DIST/index.html" ] || { echo "构建完了却没有 $DIST/index.html,看上面的构建输出"; exit 1; }
fi
# ---- 端口 ----
# 生产服务要固定地址,端口被占就报错,不像 dev.sh 那样自动往后挪
port_taken() {
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1
else
# 没有 lsof(精简的 Linux)就试着绑一下:绑得上说明没人占
if "$PY_BIN" -c 'import socket,sys; s=socket.socket(); s.bind((sys.argv[1], int(sys.argv[2])))' "$HOST" "$1" 2>/dev/null; then
return 1
fi
return 0
fi
}
if port_taken "$PORT"; then
echo "端口 $PORT 已被占用,换一个(--port 或 AGENTLAB_PORT)或停掉占用方"
command -v lsof >/dev/null 2>&1 && { lsof -nP -iTCP:"$PORT" -sTCP:LISTEN | tail -n +2 | head -3; }
exit 1
fi
export AGENTLAB_WEB_DIST="$DIST"
DATA_SHOW="${AGENTLAB_DATA_DIR:-$ROOT/data}"
echo "==> 数据目录 $DATA_SHOW"
if [ "$HOST" = "0.0.0.0" ]; then
# 本机在局域网里的地址:macOS 取 en0 / en1,Linux 取 hostname -I 的第一个
LAN_IP="$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || true)"
URLS="http://127.0.0.1:$PORT · http://${LAN_IP:-<本机 IP>}:$PORT"
echo "==> 监听所有网卡。注意:AgentLab 没有登录,同一网络里打得开页面的人都能用它——"
echo " 跑工作流、花你配的模型额度、读数据源、执行沙箱代码。只在信得过的网络里开。"
else
URLS="http://127.0.0.1:$PORT"
fi
# 就绪后再报一次地址:启动日志一长,前面打的那行容易被冲掉。
# 确认答话的是 agentlab 自己(/api/health 里带 "agentlab"),不是碰巧占着端口的别的服务
if command -v curl >/dev/null 2>&1; then
(
# $$ 在子 shell 里仍是主脚本的进程号,exec 之后就是 uvicorn:它退了就别再等
for _ in $(seq 1 120); do
kill -0 $$ 2>/dev/null || exit 0
if curl -sf "http://127.0.0.1:$PORT/api/health" 2>/dev/null | grep -q '"agentlab"'; then
echo
echo " AgentLab 已就绪:$URLS"
echo " 按 Ctrl-C 停止"
echo
exit 0
fi
sleep 0.5
done
) &
fi
echo "==> 启动 $URLS"
cd "$ROOT/backend"
# exec:让 uvicorn 直接收 Ctrl-C / SIGTERM,一个信号就走完整的优雅退出(收尾运行、关沙箱)。
# 要是包一层 shell 再转发,终端的 Ctrl-C 会让 uvicorn 收到两次信号,第二次直接强退、跳过收尾
exec "$PY_BIN" -m uvicorn app.main:app --host "$HOST" --port "$PORT"