From 04cdb8f371009d2e5606fee1081fd9c2f18cabfd Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Wed, 9 Sep 2026 20:46:49 +0800 Subject: [PATCH] feat(scripts): add launch.sh to detect-and-restart dev stack (compose, backend, web) --- scripts/launch.sh | 194 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100755 scripts/launch.sh diff --git a/scripts/launch.sh b/scripts/launch.sh new file mode 100755 index 00000000..2db6b2f5 --- /dev/null +++ b/scripts/launch.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# launch.sh — dev 环境一键检测与启动(容器组 + Go 后端 + Web 前端) +# +# 行为(检测 → 按需更新 → 启动;无变化则完全不动): +# 1) 容器组 agent-dev:compose 配置 hash / 运行状态有变化 → `up -d --build` 重建过期容器;全部一致 → 跳过 +# 2) 后端 :8080:Go 源码比 bin/server 新(或服务未运行/不健康)→ 停止旧进程 → 重新构建 → 后台重启;否则跳过 +# 3) 前端 :3000: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=3000 +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 " 与旧版 "svc 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)" +