#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BACKEND_DIR="$ROOT_DIR/backend" WORKER_DIR="$BACKEND_DIR/worker" WEB_DIR="$BACKEND_DIR/web" ENV_FILE="$ROOT_DIR/deploy/.env.dev" RUN_DIR="$ROOT_DIR/.run" PID_FILE="$RUN_DIR/dev-all.pids" mkdir -p "$RUN_DIR" : > "$PID_FILE" PIDS=() cleanup() { local pid for pid in "${PIDS[@]:-}"; do if kill -0 "$pid" 2>/dev/null; then # Each job was started via `setsid`, so its PID is also its process # group id; killing the negative pid takes down the whole subtree # (e.g. npm -> tsx -> node) instead of leaving orphaned children. kill -TERM -- "-$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true fi done wait 2>/dev/null || true rm -f "$PID_FILE" } trap cleanup EXIT INT TERM load_env() { if [[ ! -f "$ENV_FILE" ]]; then echo "[dev-all] 找不到 $ENV_FILE,請先:cp deploy/.env.dev.example deploy/.env.dev 並填值" >&2 exit 1 fi set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a : "${HAIXUN_MONGO_URI:?請在 $ENV_FILE 設定 HAIXUN_MONGO_URI}" : "${HAIXUN_REDIS_ADDR:?請在 $ENV_FILE 設定 HAIXUN_REDIS_ADDR}" : "${HAIXUN_REDIS_PASSWORD:?請在 $ENV_FILE 設定 HAIXUN_REDIS_PASSWORD}" : "${HAIXUN_JWT_ACCESS_SECRET:?請在 $ENV_FILE 設定 HAIXUN_JWT_ACCESS_SECRET}" : "${HAIXUN_JWT_REFRESH_SECRET:?請在 $ENV_FILE 設定 HAIXUN_JWT_REFRESH_SECRET}" : "${HAIXUN_WORKER_SECRET:?請在 $ENV_FILE 設定 HAIXUN_WORKER_SECRET}" : "${HAIXUN_MONGO_DB:=haixun}" : "${HAIXUN_SECRETS_KEY:=}" : "${HAIXUN_BACKEND_URL:=http://127.0.0.1:8890}" : "${HAIXUN_WORKER_POLL_MS:=3000}" : "${HAIXUN_8D_TARGET_SAMPLES:=4}" : "${PLAYWRIGHT_HEADLESS:=true}" export HAIXUN_MONGO_URI HAIXUN_MONGO_DB HAIXUN_REDIS_ADDR HAIXUN_REDIS_PASSWORD export HAIXUN_JWT_ACCESS_SECRET HAIXUN_JWT_REFRESH_SECRET HAIXUN_WORKER_SECRET export HAIXUN_SECRETS_KEY HAIXUN_BACKEND_URL export HAIXUN_WORKER_POLL_MS HAIXUN_8D_TARGET_SAMPLES export THREADS_APP_ID THREADS_APP_SECRET THREADS_REDIRECT_URI THREADS_OAUTH_SUCCESS_REDIRECT export PLAYWRIGHT_HEADLESS } start() { local name="$1" shift echo "[dev-all] starting $name" # setsid makes the launched process its own session/process-group leader, # so dev-all-stop.sh can reliably kill it *and* every child it spawns # (npm -> tsx -> node, etc.) with a single `kill -- -PID`, even if this # script itself gets killed abruptly and never runs its own cleanup trap. setsid "$@" & local pid="$!" PIDS+=("$pid") echo "$pid $name" >> "$PID_FILE" } port_in_use() { local port="$1" ss -tlnp 2>/dev/null | grep -qE ":${port} " } free_port() { local port="$1" if ! port_in_use "$port"; then return 0 fi echo "[dev-all] freeing port :$port" if command -v fuser >/dev/null 2>&1; then fuser -k "${port}/tcp" 2>/dev/null || true else local line pid while IFS= read -r line; do for pid in $(grep -oE 'pid=[0-9]+' <<<"$line" | cut -d= -f2); do kill "$pid" 2>/dev/null || true done done < <(ss -tlnp 2>/dev/null | grep -E ":${port} " || true) fi sleep 0.3 } ensure_http_service() { local name="$1" local port="$2" local health_url="$3" shift 3 if curl -fsS "$health_url" >/dev/null 2>&1; then echo "[dev-all] reusing $name (already healthy on :$port)" return 0 fi if port_in_use "$port"; then echo "[dev-all] port :$port occupied but $name unhealthy; restarting" free_port "$port" fi start "$name" "$@" wait_for_http "$name" "$health_url" } wait_for_http() { local name="$1" local url="$2" local i for i in {1..40}; do if curl -fsS "$url" >/dev/null 2>&1; then echo "[dev-all] ready $name" return 0 fi sleep 0.5 done echo "[dev-all] $name did not become ready: $url" >&2 return 1 } pick_web_port() { python3 - <<'PY' import socket for port in range(5173, 5200): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(0.1) if sock.connect_ex(('127.0.0.1', port)) != 0: print(port) raise SystemExit(0) raise SystemExit('no free Vite port found in 5173..5199') PY } resolve_web_port() { local requested="${WEB_PORT:-}" if [[ -n "$requested" && "$requested" -ge 1024 ]]; then echo "$requested" return fi if [[ -n "$requested" ]]; then echo "[dev-all] WEB_PORT=$requested is for prod/nginx; using a free dev port instead" >&2 fi pick_web_port } load_env cd "$BACKEND_DIR" mkdir -p bin go build -o bin/gateway gateway.go go build -o bin/worker ./cmd/worker cd "$WORKER_DIR" npm install cd "$WEB_DIR" npm install cd "$BACKEND_DIR" ensure_http_service "gateway" 8890 "http://127.0.0.1:8890/api/v1/health" \ ./bin/gateway -f etc/gateway.yaml # Only worker types that have registered step handlers. GO_WORKER_TYPES=( go-demo go-expand-graph go-placement-scan go-scan-viral go-generate-outreach-draft go-generate-topic-matrix go-generate-formula-draft go-generate-rewrite-draft go-refresh-threads-token go-publish-analytics ) for worker_type in "${GO_WORKER_TYPES[@]}"; do start "worker:$worker_type" env \ HAIXUN_JOB_WORKER_TYPE="$worker_type" \ HAIXUN_JOB_WORKER_ID="local-$worker_type" \ ./bin/worker -f etc/gateway.worker.yaml done cd "$WORKER_DIR" ensure_http_service "node-tool:keyword-search" 9101 "http://127.0.0.1:9101/health" \ env PORT=9101 npm run keyword-search-server ensure_http_service "node-tool:resolve-media" 9102 "http://127.0.0.1:9102/health" \ env PORT=9102 npm run resolve-media-server start "node-worker:style-8d" env \ HAIXUN_NODE_WORKER_TYPE=node-style-8d \ HAIXUN_NODE_WORKER_ID=local-node-style-8d \ npm run style-8d cd "$WEB_DIR" WEB_PORT="$(resolve_web_port)" start "web" npm run dev -- --host 0.0.0.0 --port "$WEB_PORT" --strictPort wait_for_http "web" "http://127.0.0.1:$WEB_PORT/" echo echo "[dev-all] all services are running" echo "[dev-all] API: http://127.0.0.1:8890/api/v1/health" echo "[dev-all] Web: http://127.0.0.1:$WEB_PORT/" echo "[dev-all] Press Ctrl+C to stop" wait -n