#!/usr/bin/env bash # 生产部署脚本 —— openclaw-01(用户级 systemd,全程不需要 root) # # 遵守 AGENTS.md 的部署铁律: # 1. 生产只跑仓库 HEAD(git reset --hard origin/main,禁止服务器手工改) # 2. 前端产物只在服务器上由 HEAD 构建(npm ci && npm run build) # 3. 部署后校验 /version 的 commit == git rev-parse --short HEAD # # 用法(在 openclaw-01 上,openclaw 身份): # bash ~/app/dadaotask/deploy/prod/deploy.sh # 首次部署前准备好 ~/.config/dadaotask/env.production(见 env.production.example)。 set -euo pipefail # 这个脚本会 git reset 自己所在的仓库:bash 按字节偏移边读边执行,脚本被改写后 # 可能读到新旧混合的内容(实测踩过)——先把自己复制到临时文件再执行。 if [ -z "${DADAO_DEPLOY_SNAPSHOT:-}" ]; then _snap=$(mktemp /tmp/dadao-deploy.XXXXXX.sh) cp "$0" "$_snap" trap 'rm -f "$_snap"' EXIT DADAO_DEPLOY_SNAPSHOT=1 bash "$_snap" "$@" exit $? fi APP_DIR=${APP_DIR:-/home/openclaw/app/dadaotask} CONF_DIR=${CONF_DIR:-$HOME/.config/dadaotask} ENV_FILE=$CONF_DIR/env.production SERVICE=${SERVICE:-dadaotask-api} PORT=${PORT:-8200} REPO_URL=${REPO_URL:-https://gitea.tatta.cn/Tatta/DadaoTask.git} # 服务器直连 pypi/npm 官方源很慢(实测 npm 包几百 KB 就能卡住),默认走国内镜像 NPM_REGISTRY=${NPM_REGISTRY:-https://registry.npmmirror.com} step() { printf '\n== %s ==\n' "$*"; } die() { printf '!! %s\n' "$*" >&2; exit 1; } step "0. 前置检查" test -f "$ENV_FILE" || die "缺少 $ENV_FILE(参考 deploy/prod/env.production.example)" if ! command -v node >/dev/null; then die "node 未安装(前端必须在服务器上构建)"; fi node_major=$(node -v | sed 's/^v\([0-9]*\).*/\1/') [ "$node_major" -ge 20 ] || die "node 版本过低:$(node -v),需要 20+" step "1. 代码对齐仓库 HEAD" if [ ! -d "$APP_DIR/.git" ]; then git clone --branch main --single-branch "$REPO_URL" "$APP_DIR" fi cd "$APP_DIR" git fetch --prune origin git checkout -q main git reset --hard -q origin/main # 生产不允许领先/落后仓库,也不允许脏改动 COMMIT=$(git rev-parse HEAD) SHORT=$(git rev-parse --short HEAD) echo " HEAD=$COMMIT ($SHORT)" step "2. Python 依赖" # openclaw-01 是 Debian/Ubuntu 系但没装 python3-venv(ensurepip 缺失),也没有 sudo; # 所以先建 --without-pip 的 venv,再用系统 pip 把 pip 装进去。镜像可用 env 覆盖。 PIP_INDEX=${PIP_INDEX:-https://mirrors.aliyun.com/pypi/simple/} if ! [ -x .venv/bin/python ] || ! .venv/bin/python -m pip --version >/dev/null 2>&1; then rm -rf .venv python3 -m venv --without-pip .venv python3 -m pip --python .venv/bin/python install -q --index-url "$PIP_INDEX" pip setuptools wheel fi .venv/bin/python -m pip install -q --index-url "$PIP_INDEX" --upgrade -r backend/requirements.txt step "3. 数据库迁移(迁移角色,非应用角色)" set -a; . "$ENV_FILE"; set +a ( cd backend && ../.venv/bin/alembic upgrade head ) step "4. 前端构建(只在服务器上、只用 HEAD 的源码)" ( cd web && npm ci --no-audit --no-fund --registry="$NPM_REGISTRY" && npm run build ) test -f web/dist/index.html || die "前端构建产物缺失:web/dist/index.html" step "5. 写入构建标记、刷新 systemd 单元并重启服务" mkdir -p "$CONF_DIR" "$HOME/.config/systemd/user" backend/var/uploads chmod 700 "$CONF_DIR" cp deploy/prod/dadaotask-api.service "$HOME/.config/systemd/user/" printf 'BUILD_COMMIT=%s\nBUILD_TIME=%s\n' "$SHORT" "$(TZ=Asia/Shanghai date -Iseconds)" >"$CONF_DIR/env.build" chmod 600 "$CONF_DIR/env.build" "$ENV_FILE" systemctl --user daemon-reload systemctl --user enable "$SERVICE" >/dev/null 2>&1 || true systemctl --user restart "$SERVICE" step "6. 校验(/health + /version 的 commit)" for _ in $(seq 1 30); do curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1 && break sleep 1 done HEALTH=$(curl -fsS "http://127.0.0.1:$PORT/health") || die "健康检查失败:服务没起来(journalctl --user -u $SERVICE -n 100)" COMMIT_HTTP=$(curl -fsS "http://127.0.0.1:$PORT/version" | .venv/bin/python -c 'import json,sys; print(json.load(sys.stdin)["commit"])') \ || die "/version 读取失败" echo " health=$HEALTH" echo " version.commit=$COMMIT_HTTP" [ "$COMMIT_HTTP" = "$SHORT" ] || die "/version 的 commit($COMMIT_HTTP) 与 HEAD($SHORT) 不一致" printf '\n部署完成:http://%s:%s (commit %s)\n' "$(hostname -I | awk '{print $1}')" "$PORT" "$SHORT"