Files
weichen b812d1d30f feat(dev): single-binary no-docker runtime with managed localpg
Embed web/dist, run events in-process, manage brew PG18+pgvector, add PI host sidecar and launch --no-docker; unify MiniMax-M2.7-highspeed pins and record dev acceptance.
2026-09-24 15:53:50 +00:00

328 lines
12 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
# launch.sh — dev 环境一键检测与启动
#
# 模式:
# ./scripts/launch.sh 容器组 (system-db + pi-agent) + 宿主 bin/server + Vite
# ./scripts/launch.sh --no-docker 非容器:localpg + PI sidecar + bin/agent-server + Vite
# (共享 KB 容器 shuimu-kb-shared 仍可保留)
#
# FORCE_BUILD=1 强制重建;日志 .tmp/launch/{server,web,pi-sidecar}.log
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="docker-compose.dev.yml"
COMPOSE_PROJECT="agent-dev"
EXCLUDED_SERVICES="app"
LOG_DIR="$ROOT/.tmp/launch"
BACKEND_BIN_LEGACY="$ROOT/bin/server"
BACKEND_BIN_SINGLE="$ROOT/bin/agent-server"
BACKEND_URL="http://127.0.0.1:8080/api/v1/health"
FRONTEND_PORT=3100
PI_AGENT_CONTAINER="shuimu-pi-agent-dev"
PI_SIDECAR_PORT="${PI_AGENT_RUNNER_PORT:-18865}"
NO_DOCKER=0
for arg in "$@"; do
case "$arg" in
--no-docker) NO_DOCKER=1 ;;
-h|--help)
echo "usage: $0 [--no-docker]"
exit 0
;;
esac
done
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() {
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() {
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
}
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
}
# ---------- --no-docker path ----------
launch_no_docker() {
info "模式: --no-docker(localpg + PI sidecar + agent-server + Vite)"
cd "$ROOT"
# Stop conflicting docker app/events/pi-agent (keep shared KB; system-db stopped after migrate)
if docker info >/dev/null 2>&1; then
act "停止 dev 容器 app / events / pi-agent(保留 shared KB)..."
docker compose -f "$COMPOSE_FILE" -p "$COMPOSE_PROJECT" stop app events pi-agent 2>/dev/null || true
fi
# Ensure env flags for managed PG
export LOCALPG_MODE="${LOCALPG_MODE:-managed}"
export PI_AGENT_RUNNER_URL="${PI_AGENT_RUNNER_URL:-http://127.0.0.1:${PI_SIDECAR_PORT}}"
export MINIMAX_MODEL_ID="${MINIMAX_MODEL_ID:-MiniMax-M2.7-highspeed}"
export PI_AGENT_MODEL="${PI_AGENT_MODEL:-MiniMax-M2.7-highspeed}"
export DATA_DIR="${DATA_DIR:-$ROOT/data/dev}"
export ENV="${ENV:-dev}"
# PI sidecar
info "检测 PI sidecar :$PI_SIDECAR_PORT ..."
if wait_http "http://127.0.0.1:${PI_SIDECAR_PORT}/health" 2; then
skip "PI sidecar 已健康"
else
stop_pid_on_port "$PI_SIDECAR_PORT"
act "启动 PI sidecar(日志 $LOG_DIR/pi-sidecar.log)..."
(
set -a
# shellcheck disable=SC1091
[[ -f "$ROOT/.env" ]] && . "$ROOT/.env"
set +a
export PORT="$PI_SIDECAR_PORT"
export DATA_DIR="$DATA_DIR/workspaces"
export PI_CODING_AGENT_DIR="$DATA_DIR/../pi-agent"
export MINIMAX_MODEL_ID PI_AGENT_MODEL
nohup bash "$ROOT/deploy/pi-agent-sidecar/start.sh" >"$LOG_DIR/pi-sidecar.log" 2>&1 &
)
if wait_http "http://127.0.0.1:${PI_SIDECAR_PORT}/health" 60; then
ok "PI sidecar 就绪"
else
fail "PI sidecar 健康检查超时,见 $LOG_DIR/pi-sidecar.log"
exit 1
fi
fi
# agent-server (embeds web; manages localpg when LOCALPG_MODE=managed)
info "检测后端 agent-server :8080 ..."
NEED_REBUILD=0
if [[ ! -f "$BACKEND_BIN_SINGLE" ]]; then
NEED_REBUILD=1
elif [[ -n "$(find "$ROOT/cmd" "$ROOT/internal" "$ROOT/web/embed.go" "$ROOT/go.mod" -newer "$BACKEND_BIN_SINGLE" -print -quit 2>/dev/null)" ]]; then
NEED_REBUILD=1
fi
[[ -n "${FORCE_BUILD:-}" ]] && NEED_REBUILD=1
if [[ "$NEED_REBUILD" -eq 1 ]]; then
act "构建单二进制(scripts/build-single-binary.sh)..."
# Prefer quick go build if dist already present
if [[ -f "$ROOT/web/dist/index.html" ]]; then
(cd "$ROOT" && CGO_ENABLED=0 go build -o "$BACKEND_BIN_SINGLE" ./cmd/server) || {
fail "go build 失败"
exit 1
}
else
bash "$ROOT/scripts/build-single-binary.sh" || { fail "build-single-binary 失败"; exit 1; }
fi
fi
stop_pid_on_port 8080
# Also stop legacy bin/server if different path
if [[ -f "$BACKEND_BIN_LEGACY" ]]; then
pgrep -f "$BACKEND_BIN_LEGACY" >/dev/null 2>&1 && pkill -f "$BACKEND_BIN_LEGACY" || true
fi
act "后台启动 agent-server(LOCALPG_MODE=$LOCALPG_MODE)..."
(
cd "$ROOT"
set -a
# shellcheck disable=SC1091
[[ -f "$ROOT/.env" ]] && . "$ROOT/.env"
set +a
export LOCALPG_MODE PI_AGENT_RUNNER_URL MINIMAX_MODEL_ID PI_AGENT_MODEL DATA_DIR ENV
nohup "$BACKEND_BIN_SINGLE" >"$LOG_DIR/server.log" 2>&1 &
)
if wait_http "$BACKEND_URL" 90; then
ok "后端已启动并通过健康检查"
else
fail "后端健康检查超时,见 $LOG_DIR/server.log"
exit 1
fi
# Vite
info "检测前端 :$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 "启动 Vite..."
(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"
exit 1
fi
fi
info "完成(--no-docker)。工作台: http://localhost:$FRONTEND_PORT"
info "API: http://127.0.0.1:8080 PI: http://127.0.0.1:$PI_SIDECAR_PORT"
info "若首次切换,请跑: ./scripts/migrate-dev-db.sh 然后 docker compose -p agent-dev stop system-db"
}
if [[ "$NO_DOCKER" -eq 1 ]]; then
launch_no_docker
exit 0
fi
# ---------- legacy docker-assisted path (unchanged below) ----------
BACKEND_BIN="$BACKEND_BIN_LEGACY"
info "检测容器组 ($COMPOSE_PROJECT)..."
cd "$ROOT"
if ! docker info >/dev/null 2>&1; then
fail "Docker 未运行,请先启动 Docker Desktop(或改用 --no-docker)"
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"
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
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
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
[[ -n "$BACKEND_PID" ]] && BACKEND_RUNNING=1
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
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
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)"