Files
Agent-System/scripts/launch.sh
T
weichen 3145550a59 feat(security): complete P0 release blocking remediation
Unify auth middleware and remove X-User-ID trust, add project_members
authorization, sandbox workspaces via resolver, and admin-only reconcile
by run_id. Introduce goose migrations with startup schema gate, fail closed
when DB unavailable outside explicit ALLOW_IN_MEMORY, bundle web/dist in
app Dockerfile, and remove frontend pseudo-login fallback.
2026-09-13 02:50:22 +00:00

195 lines
7.8 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
# launch.sh — dev 环境一键检测与启动(容器组 + Go 后端 + Web 前端)
#
# 行为(检测 → 按需更新 → 启动;无变化则完全不动):
# 1) 容器组 agent-dev:compose 配置 hash / 运行状态有变化 → `up -d --build` 重建过期容器;全部一致 → 跳过
# 2) 后端 :8080:Go 源码比 bin/server 新(或服务未运行/不健康)→ 停止旧进程 → 重新构建 → 后台重启;否则跳过
# 3) 前端 :3100:Vite 未运行 → npm install(如需)→ 后台启动;运行中 → 跳过(Vite 自带热更新)
#
# 说明:
# - 「更新到最新」= 按当前工作区源码重建,不做 git pull(避免影响未提交 WIP)
# - FORCE_BUILD=1 ./scripts/launch.sh 可跳过检测强制重建容器镜像与后端二进制
# - 日志归档:.tmp/launch/{server,web}.log
set -uo pipefail
# ---------- 常量 ----------
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="docker-compose.dev.yml"
COMPOSE_PROJECT="agent-dev"
# 本地后端模式:bin/server 占用 8080,容器化后端服务 app(container_name: agent-app-dev)
# 默认不启用(避免端口冲突)。若要改用容器化后端,请从 EXCLUDED_SERVICES 移除并停用本地 bin/server。
EXCLUDED_SERVICES="app"
LOG_DIR="$ROOT/.tmp/launch"
BACKEND_BIN="$ROOT/bin/server"
BACKEND_URL="http://127.0.0.1:8080/api/v1/health"
FRONTEND_PORT=3100
PI_AGENT_CONTAINER="shuimu-pi-agent-dev"
mkdir -p "$LOG_DIR"
# ---------- 输出与等待工具 ----------
info() { printf '\033[1;34m[launch]\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' "$*"; }
wait_http() { # $1=url $2=retries(默认30)
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
}
wait_port() { # $1=port $2=retries(默认30)
local port="$1" retries="${2:-30}" i
for ((i = 0; i < retries; i++)); do
lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0
sleep 1
done
return 1
}
# ---------- 1) 容器组 ----------
info "检测容器组 ($COMPOSE_PROJECT)..."
cd "$ROOT"
if ! docker info >/dev/null 2>&1; then
fail "Docker 未运行,请先启动 Docker Desktop"
exit 1
fi
SERVICES="$(docker compose -f "$COMPOSE_FILE" config --services 2>/dev/null || true)"
if [[ -z "$SERVICES" ]]; then
fail "无法解析 $COMPOSE_FILE (docker compose 版本过旧?)"
exit 1
fi
ENABLED_SERVICES=""
is_excluded() { case " $EXCLUDED_SERVICES " in *" $1 "*) return 0;; *) return 1;; esac; }
CONTAINERS_STALE=""
for svc in $SERVICES; do
if is_excluded "$svc"; then
# 排除服务:清理遗留的非运行容器,避免状态噪音
excluded_cid="$(docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT" ps -aq "$svc" 2>/dev/null | head -n1)"
if [[ -n "$excluded_cid" ]]; then
excluded_state="$(docker inspect --format '{{.State.Status}}' "$excluded_cid" 2>/dev/null || true)"
if [[ "$excluded_state" != "running" ]]; then
docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT" rm -f "$svc" >/dev/null 2>&1 || true
fi
fi
continue
fi
ENABLED_SERVICES="$ENABLED_SERVICES $svc"
# 期望的配置 hash:兼容 "svc <hash>" 与旧版 "svc hash=<hash>" 两种输出格式;解析失败则只看运行状态
want_hash="$(docker compose -f "$COMPOSE_FILE" config --hash "$svc" 2>/dev/null || true)"
want_hash="${want_hash##*hash=}"
want_hash="${want_hash##* }"
cid="$(docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT" ps -q "$svc" 2>/dev/null | head -n1)"
if [[ -z "$cid" ]]; then
CONTAINERS_STALE="$CONTAINERS_STALE $svc"
continue
fi
state="$(docker inspect --format '{{.State.Status}}' "$cid" 2>/dev/null || true)"
have_hash="$(docker inspect --format '{{ index .Config.Labels "com.docker.compose.config-hash" }}' "$cid" 2>/dev/null || true)"
if [[ "$state" != "running" || ( -n "$want_hash" && "$want_hash" != "$have_hash" ) ]]; then
CONTAINERS_STALE="$CONTAINERS_STALE $svc"
fi
done
if [[ -n "${FORCE_BUILD:-}" ]]; then
act "FORCE_BUILD=1:跳过检测,强制重建容器组 (排除: $EXCLUDED_SERVICES)"
docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT" up -d --build $ENABLED_SERVICES || { fail "容器组重建失败"; exit 1; }
elif [[ -n "$CONTAINERS_STALE" ]]; then
act "以下服务需要更新/启动:$CONTAINERS_STALE"
docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT" up -d --build $ENABLED_SERVICES || { fail "容器组更新失败"; exit 1; }
else
skip "容器组全部运行且配置一致,不处理"
fi
# 等待 pi-agent 健康(LLM 关键依赖;pg 类容器启动即用)
act "等待 $PI_AGENT_CONTAINER 健康..."
for ((i = 0; i < 60; i++)); do
hstate="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$PI_AGENT_CONTAINER" 2>/dev/null || true)"
if [[ "$hstate" == "healthy" || "$hstate" == "none" ]]; then break; fi
sleep 2
done
if [[ "$hstate" == "healthy" || "$hstate" == "none" ]]; then
ok "pi-agent 就绪 ($hstate)"
else
fail "pi-agent 健康检查超时 (当前 $hstate),可稍后重跑本脚本"
fi
# ---------- 2) 后端 ----------
info "检测后端(Go server :8080)..."
NEED_REBUILD=0
if [[ ! -f "$BACKEND_BIN" ]]; then
NEED_REBUILD=1
elif [[ -n "$(find "$ROOT/cmd" "$ROOT/internal" "$ROOT/go.mod" -name '*.go' -newer "$BACKEND_BIN" -print -quit 2>/dev/null)" ]]; then
NEED_REBUILD=1
act "检测到 Go 源码比 bin/server 新"
fi
[[ -n "${FORCE_BUILD:-}" ]] && NEED_REBUILD=1
BACKEND_PID="$(lsof -nP -tiTCP:8080 -sTCP:LISTEN 2>/dev/null | head -n1)"
BACKEND_RUNNING=0
if [[ -n "$BACKEND_PID" ]]; then
BACKEND_RUNNING=1
fi
if [[ "$NEED_REBUILD" -eq 0 && "$BACKEND_RUNNING" -eq 1 ]] && curl -sf -m 3 "$BACKEND_URL" >/dev/null 2>&1; then
skip "源码无变化且服务健康,保持运行"
else
if [[ "$BACKEND_RUNNING" -eq 1 ]]; then
act "停止运行中的后端进程 (PID $BACKEND_PID)..."
kill "$BACKEND_PID" 2>/dev/null || true
for ((i = 0; i < 10; i++)); do
lsof -nP -iTCP:8080 -sTCP:LISTEN >/dev/null 2>&1 || break
sleep 1
done
# 优雅退出超时则强杀兜底(仅针对仍占用 8080 的进程,避免误伤)
if lsof -nP -iTCP:8080 -sTCP:LISTEN >/dev/null 2>&1; then
lsof -nP -tiTCP:8080 -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true
sleep 1
fi
fi
if [[ "$NEED_REBUILD" -eq 1 || ! -f "$BACKEND_BIN" ]]; then
act "重新构建后端(go build ./cmd/server)..."
(cd "$ROOT" && go build -o "$BACKEND_BIN" ./cmd/server) || { fail "go build 失败"; exit 1; }
fi
act "后台启动后端(日志 $LOG_DIR/server.log)..."
(cd "$ROOT" && nohup "$BACKEND_BIN" >"$LOG_DIR/server.log" 2>&1 &)
if wait_http "$BACKEND_URL" 30; then
ok "后端已启动并通过健康检查"
else
fail "后端健康检查超时,请查看 $LOG_DIR/server.log"
fi
fi
# ---------- 3) 前端 ----------
info "检测前端 (Vite dev server :$FRONTEND_PORT)..."
if wait_port "$FRONTEND_PORT" 1; then
skip "Vite 已运行于 :$FRONTEND_PORT (热更新生效,源码改动无需重启)"
else
if [[ ! -d "$ROOT/web/node_modules" ]]; then
act "安装前端依赖(npm install)..."
(cd "$ROOT/web" && npm install) || { fail "npm install 失败"; exit 1; }
fi
act "后台启动前端(日志 $LOG_DIR/web.log)..."
(cd "$ROOT/web" && nohup npm run dev >"$LOG_DIR/web.log" 2>&1 &)
if wait_port "$FRONTEND_PORT" 30; then
ok "前端已启动"
else
fail "前端启动超时,请查看 $LOG_DIR/web.log"
fi
fi
# ---------- 汇总 ----------
info "完成。工作台入口:http://localhost:$FRONTEND_PORT (/api 代理 → 127.0.0.1:8080)"