#!/usr/bin/env bash set -euo pipefail PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$PROJECT_DIR" MODE="${1:-dev}" stop_legacy_conflicts() { local pid cmd if [[ "${PMFLOW_KEEP_LEGACY:-0}" == "1" ]]; then echo "[PMFlow] 跳过旧链路冲突清理(PMFLOW_KEEP_LEGACY=1)" return fi while read -r pid; do [[ -z "${pid:-}" ]] && continue cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)" if [[ "$cmd" == *"uvicorn app.main:app"* ]]; then kill "$pid" 2>/dev/null || true echo "[PMFlow] 已停止旧链路后端 pid=$pid" fi done < <(lsof -tiTCP:8000 -sTCP:LISTEN 2>/dev/null || true) while read -r pid; do [[ -z "${pid:-}" ]] && continue cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)" if [[ "$cmd" == *"npm run dev -- --host 0.0.0.0 --port 5174"* ]] || [[ "$cmd" == *"vite --host 0.0.0.0 --port 5174"* ]]; then kill "$pid" 2>/dev/null || true echo "[PMFlow] 已停止旧链路前端 pid=$pid" fi done < <(lsof -tiTCP:5174 -sTCP:LISTEN 2>/dev/null || true) } extract_tunnel_url() { local logfile="$1" rg -o "https://[a-z0-9-]+\\.trycloudflare\\.com" "$logfile" 2>/dev/null \ | rg -v "^https://api\\.trycloudflare\\.com$" \ | tail -n 1 } probe_tunnel_url() { local url="$1" local health_path="$2" local code="" code="$(curl -s -m 8 -o /dev/null -w "%{http_code}" "${url}${health_path}" || true)" [[ "$code" =~ ^[0-9]{3}$ ]] || return 1 [[ "$code" -ge 200 && "$code" -lt 500 ]] } probe_tunnel_url_stable() { local url="$1" local health_path="$2" local checks="${3:-2}" local current=1 while [[ "$current" -le "$checks" ]]; do probe_tunnel_url "$url" "$health_path" || return 1 if [[ "$current" -lt "$checks" ]]; then sleep 1 fi current=$((current + 1)) done } probe_local_origin() { local port="$1" local path="${2:-/}" local code="" code="$(curl -s -m 3 -o /dev/null -w "%{http_code}" "http://127.0.0.1:${port}${path}" || true)" [[ "$code" =~ ^[0-9]{3}$ ]] || return 1 [[ "$code" -ge 200 && "$code" -lt 500 ]] } resolve_frontend_tunnel_port() { local preferred="${PMFLOW_FRONTEND_TUNNEL_PORT:-${FRONTEND_PORT:-5174}}" local candidates=("$preferred" 5174 5175 5176 5177 5178) local seen=" " local port="" for port in "${candidates[@]}"; do [[ "$seen" == *" ${port} "* ]] && continue seen="${seen}${port} " if probe_local_origin "$port" "/"; then echo "$port" return 0 fi done echo "$preferred" } start_or_reuse_tunnel() { local name="$1" local port="$2" local health_path="${3:-/}" local pidfile="logs/tunnel-${name}.pid" local logfile="logs/tunnel-${name}.log" local protocol="${PMFLOW_TUNNEL_PROTOCOL:-http2}" # default: reuse existing tunnel URL; set PMFLOW_FORCE_NEW_TUNNEL=1 to force rotate local force_new="${PMFLOW_FORCE_NEW_TUNNEL:-0}" # default: probe existing URL before reuse; set 0 only if you explicitly want fast/stale reuse local strict_check="${PMFLOW_STRICT_TUNNEL_CHECK:-1}" local url="" local max_attempts=2 local attempt=1 local new_pid="" if [[ "$force_new" != "1" ]]; then if [[ -f "$pidfile" ]]; then local old_pid old_pid="$(cat "$pidfile" 2>/dev/null || true)" if [[ -n "${old_pid:-}" ]] && ps -p "$old_pid" >/dev/null 2>&1; then url="$(extract_tunnel_url "$logfile")" if [[ -n "$url" ]]; then if [[ "$strict_check" != "1" ]] || probe_tunnel_url_stable "$url" "$health_path"; then echo "$url" return 0 fi fi fi fi fi while [[ "$attempt" -le "$max_attempts" ]]; do if [[ -f "$pidfile" ]]; then local old_pid old_pid="$(cat "$pidfile" 2>/dev/null || true)" if [[ -n "${old_pid:-}" ]] && ps -p "$old_pid" >/dev/null 2>&1; then kill "$old_pid" >/dev/null 2>&1 || true fi fi pkill -f "cloudflared tunnel .*http://127.0.0.1:${port}" >/dev/null 2>&1 || true rm -f "$pidfile" "$logfile" nohup env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \ NO_PROXY="*" \ cloudflared tunnel --protocol "$protocol" --url "http://127.0.0.1:${port}" >"$logfile" 2>&1 & new_pid="$!" echo "$new_pid" >"$pidfile" for _ in {1..35}; do url="$(extract_tunnel_url "$logfile")" if [[ -n "$url" ]]; then if probe_tunnel_url_stable "$url" "$health_path"; then echo "$url" return 0 fi fi if [[ -n "$new_pid" ]] && ! ps -p "$new_pid" >/dev/null 2>&1; then break fi sleep 1 done attempt=$((attempt + 1)) done echo "" } print_temp_public_urls() { if [[ "${PMFLOW_NO_TUNNEL:-0}" == "1" ]]; then echo "[PMFlow] 跳过临时公网隧道(PMFLOW_NO_TUNNEL=1)" return fi if ! command -v cloudflared >/dev/null 2>&1; then echo "[PMFlow] 未检测到 cloudflared,跳过临时公网地址生成" return fi mkdir -p logs local front_url back_url frontend_port local expose_backend="${PMFLOW_EXPOSE_BACKEND_TUNNEL:-0}" local protocol="${PMFLOW_TUNNEL_PROTOCOL:-http2}" local frontend_watch="${PMFLOW_FRONTEND_TUNNEL_WATCH:-1}" echo "[PMFlow] 隧道协议: ${protocol}" frontend_port="$(resolve_frontend_tunnel_port)" echo "[PMFlow] 隧道前端源站: http://127.0.0.1:${frontend_port}" front_url="" if [[ "$frontend_watch" == "1" ]]; then bash scripts/tunnel_frontend_watch.sh start >/dev/null 2>&1 || true for _ in {1..25}; do front_url="$(rg '^frontend=' logs/public_url.txt | tail -n 1 | sed 's/^frontend=//' || true)" if [[ -z "$front_url" ]]; then front_url="$(sed -n '1p' logs/public_url.txt | rg '^https?://' -N || true)" fi if [[ -n "$front_url" ]]; then break fi sleep 1 done else front_url="$(start_or_reuse_tunnel "frontend" "$frontend_port" "/")" fi back_url="" if [[ "$expose_backend" == "1" ]]; then back_url="$(start_or_reuse_tunnel "backend" "8000" "/healthz")" fi : > logs/public_url.txt if [[ -n "$front_url" ]]; then echo "frontend=$front_url" >> logs/public_url.txt echo "[PMFlow] 临时前端地址: $front_url" else echo "[PMFlow] 临时前端地址: (等待中,请查看 logs/tunnel-frontend.log)" fi if [[ "$expose_backend" != "1" ]]; then echo "[PMFlow] 临时后端地址: (已跳过,PMFLOW_EXPOSE_BACKEND_TUNNEL=0)" else if [[ -n "$back_url" ]]; then echo "backend=$back_url" >> logs/public_url.txt echo "[PMFlow] 临时后端地址: $back_url" else echo "[PMFlow] 临时后端地址: (等待中,请查看 logs/tunnel-backend.log)" fi fi echo "[PMFlow] 公网地址已写入: logs/public_url.txt" } case "$MODE" in dev|"") echo "[PMFlow] 启动模式: 开发环境(Docker 热重载)" stop_legacy_conflicts bash scripts/docker_stack.sh dev-start echo "[PMFlow] 前端地址: http://127.0.0.1:5174" echo "[PMFlow] 后端地址: http://127.0.0.1:8000" print_temp_public_urls ;; prod) echo "[PMFlow] 启动模式: 生产环境(Docker Compose)" stop_legacy_conflicts bash scripts/docker_stack.sh start echo "[PMFlow] 前端地址: http://127.0.0.1:5174" echo "[PMFlow] 后端地址: http://127.0.0.1:8000" print_temp_public_urls ;; urls) if [[ -s logs/public_url.txt ]]; then local_front_url="$(rg '^frontend=' logs/public_url.txt | tail -n 1 | sed 's/^frontend=//' || true)" local_back_url="$(rg '^backend=' logs/public_url.txt | tail -n 1 | sed 's/^backend=//' || true)" if [[ -z "$local_front_url" ]]; then local_front_url="$(sed -n '1p' logs/public_url.txt | rg '^https?://' -N || true)" fi if [[ -z "$local_back_url" ]]; then local_back_url="$(sed -n '2p' logs/public_url.txt | rg '^https?://' -N || true)" fi echo "[PMFlow] 当前公网地址:" echo "前端: ${local_front_url:-未就绪}" echo "后端: ${local_back_url:-未暴露}" else echo "[PMFlow] 未找到公网地址记录(logs/public_url.txt)" exit 1 fi ;; *) echo "用法: $0 [dev|prod|urls]" exit 1 ;; esac