Files
AgentLab/scripts/prod.sh
ThearchyHelios 2714890d0d 文档重新梳理:README 精简成介绍和索引,细节按主题拆进 docs/zh-CN 与 docs/en(17 篇一一对应),英文文档不含中文
README.zh-CN.md / README.md 只留:这是什么、为什么用它、主要功能(按领域一行一条,链到文档)、快速开始、部署、文档索引、
开发与测试、许可(原文不动)。原来的每一节按读者和任务分到 17 篇主题文档:ask-data、workflows、evidence、
data-sources(新增:接入数据库、只读保证、上传表格、跨库合并、遮罩列)、data-catalog、tools(新增:工具、审批档位、
能力登记、端点、网络规则——它们管所有工作流,不只应用)、apps、evaluation-and-ledger、schedules(含通知)、deployment、
configuration、postgresql-and-scaling、identity-and-security、architecture、development、upgrade-notes(按时间倒序,
每波先写要做的事)、faq。删掉的只有模块名(api/copilot_gen.py、chat/drive.py、data/schema_view.py)、重复的话和
「以前…现在…」的叙述;操作步骤、命令、配置名、限制、权限边界都有归宿。中文是原文,英文按新的中文写;英文里界面文字
只写英文名,开头注明界面目前只有简体中文,中文占位符换成英文占位符,中文数字的例子改成不依赖中文的说法。
数据剖析可以同时剖析 2–3 张表(baf23998)收进 data-catalog。
新增 scripts/check-docs.py:英文文档(README.md、docs/en/**)不许有中日韩字符和全角符号(代码块也算),
README、docs/**、deploy/README.md 的相对链接和锚点都要通,docs/zh-CN 与 docs/en 文件一一对应、标题层级相同。
中文提示里指向 README 章节的地方改指新文档:cluster/guard.py、cluster/version.py、db/base.py 两处报错,
data/schema_view.py 文档串,.env.example 三处,scripts/prod.sh、scripts/test-postgres.sh,terms.ts 两处、
DataSourcesTab.tsx、check-manage.mjs 注释,deploy/README.md 三处链接。deploy/agentlab.env 未动。
验收:check-docs 0 问题(英文文档 18 个 0 处中文,相对链接 369 条 0 断链,18 对标题层级一致);相关后端测试
117 通过、7 跳过;tsc -b、check-copy 通过。
2026-10-21 10:23:26 +08:00

180 lines
8.1 KiB
Bash
Executable File
Raw Permalink 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 <端口>] [--workers <进程数>] [--build | --no-build]
--host 监听 0.0.0.0(同一网络里的设备都能打开)。默认只听 127.0.0.1,只有本机能打开。
注意:AgentLab 没有登录,同一网络里打得开页面的人都能用它——跑工作流、花你配的
模型额度、读数据源、执行沙箱代码。只在信得过的网络里开,并配好防火墙。
--port <端口> 监听端口。也可以用环境变量 AGENTLAB_PORT,默认 8000。
--workers <N> 起几个工作进程(uvicorn --workers),也可以用环境变量 AGENTLAB_WORKERS,默认 1。
大于 1 要先把应用库换成 PostgreSQL(AGENTLAB_DATABASE_URL):SQLite 只支持单进程。
--build 不管前端产物新旧,强制重新构建。
--no-build 跳过构建,直接用现有的 frontend/dist。
-h, --help 显示这段说明。
默认只在 frontend/dist 不存在、或者比前端源码旧的时候才构建。
数据目录照后端配置:默认是仓库下的 data/,用环境变量 AGENTLAB_DATA_DIR 改。
应用库是 SQLite(缺省)时只能单进程;换成 PostgreSQL 之后可以多进程(docs/zh-CN/postgresql-and-scaling.md「多进程」)。
USAGE
}
# 参数先认完再干活,认不出的直接报错,免得写错的参数被悄悄忽略
HOST="127.0.0.1"
PORT="${AGENTLAB_PORT:-8000}"
WORKERS="${AGENTLAB_WORKERS:-1}"
WORKERS_CLI=""
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=}" ;;
--workers)
[ $# -ge 2 ] || { echo "--workers 后面要跟进程数"; echo; usage; exit 2; }
WORKERS="$2"; WORKERS_CLI=1; shift ;;
--workers=*) WORKERS="${1#--workers=}"; WORKERS_CLI=1 ;;
--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
case "$WORKERS" in
''|*[!0-9]*) echo "进程数必须是正整数:$WORKERS"; exit 2 ;;
esac
if [ "$WORKERS" -lt 1 ]; then
echo "进程数至少是 1:$WORKERS"; exit 2
fi
# 命令行给了 --workers 才写进环境变量(见下面问后端的那一句);没给的话照后端自己的配置(环境变量和 .env)
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"
# 起几个进程问后端(python -m app.cluster workers):它照环境变量和 .env 一起读应用库、进程数——写在 .env 里的 AGENTLAB_DATABASE_URL、
# AGENTLAB_WORKERS 这里也算数;应用库是 SQLite 而进程数大于 1 时它照实报错(SQLite 只支持单进程),这里不起
[ -n "$WORKERS_CLI" ] && export AGENTLAB_WORKERS="$WORKERS"
WORKERS="$("$PY_BIN" -m app.cluster workers)" || exit 2
# exec:让 uvicorn 直接收 Ctrl-C / SIGTERM,一个信号就走完整的优雅退出(收尾运行、关沙箱)。
# 要是包一层 shell 再转发,终端的 Ctrl-C 会让 uvicorn 收到两次信号,第二次直接强退、跳过收尾
# 多进程(--workers 大于 1,PostgreSQL):uvicorn 起几个工作进程共用这个端口,各自在库里登记、续租约(app/cluster)
if [ "$WORKERS" -gt 1 ]; then
echo "==> $WORKERS 个工作进程(应用库 PostgreSQL)"
exec "$PY_BIN" -m uvicorn app.main:app --host "$HOST" --port "$PORT" --workers "$WORKERS"
fi
exec "$PY_BIN" -m uvicorn app.main:app --host "$HOST" --port "$PORT"