mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 02:41: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
156 lines
6.5 KiB
Bash
Executable File
156 lines
6.5 KiB
Bash
Executable File
#!/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"
|