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.
328 lines
12 KiB
Bash
Executable File
328 lines
12 KiB
Bash
Executable File
#!/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)"
|