#!/usr/bin/env bash # launch-single.sh — 非容器单二进制启动入口(非 dev 环境;当前用于 test) # # 与 launch.sh --no-docker(dev 专用:含 Vite/热更)相对,本脚本服务长期部署形态: # - PI sidecar(环境专属端口/工作区)+ agent-server 单二进制(embed 静态,无 Vite) # - LOCALPG_MODE 默认 managed(PGDATA=$DATA_DIR/pg,端口沿用环境约定) # - 从 configs/.env. + 根 .env 分层加载配置;SYSTEM_DB_*(compose 约定) # 在此映射为宿主运行所需 CONTEXT_DB_* # # 用法: # ./scripts/launch-single.sh test [--bin /path/to/agent-server] [--build] # --bin 指定二进制路径(默认 bin/agent-server-) # --build 从当前工作树构建。test 部署应使用 rc tag 构建的制品,勿默认依赖 --build # # 日志:.tmp/launch/{server,pi-sidecar}-.log set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ENV_NAME="" if [[ $# -gt 0 && "${1:0:1}" != "-" ]]; then ENV_NAME="$1" shift fi BIN_OVERRIDE="" DO_BUILD=0 while [[ $# -gt 0 ]]; do case "$1" in --bin) BIN_OVERRIDE="${2:-}"; shift 2 ;; --build) DO_BUILD=1; shift ;; -h|--help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "unknown arg: $1" >&2; exit 1 ;; esac done if [[ -z "$ENV_NAME" || "$ENV_NAME" == "dev" ]]; then echo "error: dev 请使用 ./scripts/launch.sh --no-docker;本脚本用于非 dev 环境(如 test)" >&2 exit 1 fi LOG_DIR="$ROOT/.tmp/launch" mkdir -p "$LOG_DIR" info() { printf '\033[1;34m[launch-single]\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m ✅ %s\033[0m\n' "$*"; } skip() { printf '\033[1;32m ⏭️ %s\033[0m\n' "$*"; } act() { printf '\033[1;33m 🔄 %s\033[0m\n' "$*"; } fail() { printf '\033[1;31m ❌ %s\033[0m\n' "$*"; } # ---------- 分层加载配置:根 .env(公共密钥)→ configs/.env.(环境差异) ---------- # shellcheck disable=SC1091 set -a [[ -f "$ROOT/.env" ]] && . "$ROOT/.env" if [[ -f "$ROOT/configs/.env.$ENV_NAME" ]]; then . "$ROOT/configs/.env.$ENV_NAME" else fail "缺少 configs/.env.$ENV_NAME(参考 configs/.env.$ENV_NAME.example)" exit 1 fi set +a # ---------- 宿主运行派生(进程环境变量优先于 .env 文件) ---------- export ENV="$ENV_NAME" DATA_DIR="${DATA_DIR:-$ROOT/data/$ENV_NAME}" case "$DATA_DIR" in /*) ;; *) DATA_DIR="$ROOT/$DATA_DIR" ;; esac export DATA_DIR HTTP_PORT="${HTTP_PORT:-8081}" export HTTP_HOST="${HTTP_HOST:-${APP_PUBLISH_HOST:-0.0.0.0}}" export HTTP_PORT export SEED_DEMO_USERS="${SEED_DEMO_USERS:-false}" export LOCALPG_MODE="${LOCALPG_MODE:-managed}" # SYSTEM_DB_*(compose 约定)→ CONTEXT_DB_*(config.Load 宿主读取约定)。 # 注意:SYSTEM_DB_* 必须压过根 .env 的 dev 值 CONTEXT_DB_*(分层加载先读根 .env), # 因此这里不用 :- 回退而直接覆盖;attach 模式可用 APP_DB_DSN 显式指定。 export CONTEXT_DB_HOST="127.0.0.1" export CONTEXT_DB_PORT="${SYSTEM_DB_PUBLISH_PORT:-5434}" export CONTEXT_DB_USER="${SYSTEM_DB_USER:-app}" export CONTEXT_DB_PASSWORD="${SYSTEM_DB_PASSWORD:?SYSTEM_DB_PASSWORD required}" export CONTEXT_DB_NAME="${SYSTEM_DB_NAME:-agent_system_$ENV_NAME}" PI_PORT="${PI_AGENT_RUNNER_PORT:-18866}" export PI_AGENT_RUNNER_URL="${PI_AGENT_RUNNER_URL:-http://127.0.0.1:$PI_PORT}" # 与 compose 对齐的 vault 状态根(FAV 关闭时 inert;无 master key 时 health 同样报 disconnected) export VAULT_STATE_ROOT="${VAULT_STATE_ROOT:-$ROOT/data/vault/$ENV_NAME}" export VAULT_STORAGE_IDENTITY="${VAULT_STORAGE_IDENTITY:-${ENV_NAME}-vault}" BIN="${BIN_OVERRIDE:-$ROOT/bin/agent-server-$ENV_NAME}" wait_http() { local url="$1" retries="${2:-30}" i for ((i = 0; i < retries; i++)); do curl -sf -m 2 "$url" >/dev/null 2>&1 && return 0 sleep 1 done return 1 } stop_pid_on_port() { local port="$1" pid pid="$(lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -n1 || true)" if [[ -n "$pid" ]]; then act "停止占用 :$port 的进程 PID $pid" kill "$pid" 2>/dev/null || true for ((i = 0; i < 10; i++)); do lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 || return 0 sleep 1 done lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | xargs -r kill -9 2>/dev/null || true fi } info "环境: $ENV_NAME(HTTP :$HTTP_HOST:$HTTP_PORT / DB :$CONTEXT_DB_PORT/$CONTEXT_DB_NAME / PI :$PI_PORT / DATA_DIR=$DATA_DIR)" # ---------- PI sidecar ---------- info "检测 PI sidecar :$PI_PORT ..." if wait_http "http://127.0.0.1:$PI_PORT/health" 2; then skip "PI sidecar 已健康" else stop_pid_on_port "$PI_PORT" act "启动 PI sidecar(日志 $LOG_DIR/pi-sidecar-$ENV_NAME.log)..." ( export PORT="$PI_PORT" export DATA_DIR="$DATA_DIR/workspaces" export PI_CODING_AGENT_DIR="$DATA_DIR/pi-agent" nohup bash "$ROOT/deploy/pi-agent-sidecar/start.sh" >"$LOG_DIR/pi-sidecar-$ENV_NAME.log" 2>&1 & ) if wait_http "http://127.0.0.1:$PI_PORT/health" 60; then ok "PI sidecar 就绪" else fail "PI sidecar 健康检查超时,见 $LOG_DIR/pi-sidecar-$ENV_NAME.log" exit 1 fi fi # ---------- agent-server ---------- if [[ "$DO_BUILD" -eq 1 ]]; then act "从当前工作树构建 $BIN ..." OUT_BIN="$BIN" bash "$ROOT/scripts/build-single-binary.sh" || { fail "构建失败"; exit 1; } fi if [[ ! -x "$BIN" ]]; then fail "二进制不存在或不可执行: $BIN(用 --bin 指定 rc tag 构建的制品,或 --build)" exit 1 fi stop_pid_on_port "$HTTP_PORT" act "后台启动 agent-server(LOCALPG_MODE=$LOCALPG_MODE,日志 $LOG_DIR/server-$ENV_NAME.log)..." ( cd "$ROOT" nohup "$BIN" >"$LOG_DIR/server-$ENV_NAME.log" 2>&1 & ) if wait_http "http://127.0.0.1:$HTTP_PORT/api/v1/health" 120; then ok "后端已启动并通过健康检查" else fail "后端健康检查超时,见 $LOG_DIR/server-$ENV_NAME.log" exit 1 fi info "完成。入口: http://127.0.0.1:$HTTP_PORT PI: http://127.0.0.1:$PI_PORT" info "若首次从容器切换,请先跑: ./scripts/migrate-test-db.sh(test),容器 volume 保留可回滚"