#!/usr/bin/env bash # 用 ngrok 暴露本机 8000 端口,并把公网 URL 写入 .env 的 CALLBACK_BASE_URL,便于 7006 回调调通 set -e cd "$(dirname "$0")" if ! command -v ngrok >/dev/null 2>&1; then echo "未检测到 ngrok。请先安装:" echo " brew install ngrok/ngrok/ngrok # macOS" echo " 或从 https://ngrok.com/download 下载" exit 1 fi NGROK_LOG="/tmp/ngrok-wechataiclaw.log" rm -f "$NGROK_LOG" # 避免重复启动导致多个 ngrok if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:4040/api/tunnels 2>/dev/null | grep -q 200; then echo "检测到 ngrok 已在运行(4040 可访问),直接读取 URL..." else echo "启动 ngrok http 8000(后端需在 8000 端口)..." nohup ngrok http 8000 --log=stdout > "$NGROK_LOG" 2>&1 & NGROK_PID=$! echo "等待 ngrok 就绪(最多 30 秒)..." for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:4040/api/tunnels 2>/dev/null | grep -q 200; then break fi sleep 1 done sleep 1 fi # 方式一:从 4040 API 获取 PAYLOAD="" for _ in 1 2 3 4 5; do PAYLOAD=$(curl -s http://127.0.0.1:4040/api/tunnels 2>/dev/null || true) if [ -n "$PAYLOAD" ] && [ "$PAYLOAD" != "null" ]; then break fi sleep 2 done # 仅接受隧道公网地址,排除 dashboard/signup/get-started/docs 等说明页 _is_tunnel_url() { case "$1" in *dashboard*|*signup*|*get-started*|*ngrok.com/docs*|*your-authtoken*) return 1 ;; *) return 0 ;; esac } # 方式二:4040 不可用时从 ngrok 启动日志中解析 https 公网地址(不依赖本地 API) if [ -z "$PAYLOAD" ] || [ "$PAYLOAD" = "null" ]; then echo "4040 API 不可用,尝试从 ngrok 日志解析 URL(约 20 秒)..." for _ in 1 2 3 4 5 6 7 8 9 10; do sleep 2 if [ -f "$NGROK_LOG" ] && [ -s "$NGROK_LOG" ]; then # 只匹配隧道域名:*.ngrok-free.app / *.ngrok.io(排除 dashboard.ngrok.com 等) PUBLIC_URL_FROM_LOG=$(grep -oE 'https://[a-zA-Z0-9][-a-zA-Z0-9.]*\.(ngrok-free\.app|ngrok\.io|ngrok-app\.com)([^"'\''<> /]*|$)' "$NGROK_LOG" 2>/dev/null | head -1) || true [ -z "$PUBLIC_URL_FROM_LOG" ] && PUBLIC_URL_FROM_LOG=$(grep -iE 'forwarding|Forwarding' "$NGROK_LOG" 2>/dev/null | grep -oE 'https://[a-zA-Z0-9][-a-zA-Z0-9.]*\.(ngrok-free\.app|ngrok\.io)[^ ]*' | head -1) || true if [ -n "$PUBLIC_URL_FROM_LOG" ] && _is_tunnel_url "$PUBLIC_URL_FROM_LOG"; then PUBLIC_URL="$PUBLIC_URL_FROM_LOG" echo "已从日志解析到: $PUBLIC_URL" break fi fi done fi # 若尚未从日志得到 URL,则从 API 的 PAYLOAD 解析 if [ -z "$PUBLIC_URL" ] && [ -n "$PAYLOAD" ] && [ "$PAYLOAD" != "null" ]; then PUBLIC_URL=$(echo "$PAYLOAD" | python3 -c " import sys, json try: d = json.load(sys.stdin) tunnels = d.get('tunnels') if isinstance(d, dict) else (d if isinstance(d, list) else []) if not isinstance(tunnels, list): tunnels = [] for t in tunnels: if not isinstance(t, dict): continue u = (t.get('public_url') or t.get('PublicURL') or '').strip() if u.startswith('https://'): print(u.rstrip('/')) break else: if tunnels: u = (tunnels[0].get('public_url') or tunnels[0].get('PublicURL') or '').strip() if u: print(u.rstrip('/')) except Exception: pass " 2>/dev/null) [ -z "$PUBLIC_URL" ] && PUBLIC_URL=$(echo "$PAYLOAD" | grep -oE 'https://[a-zA-Z0-9][-a-zA-Z0-9.]*\.(ngrok-free\.app|ngrok\.io|ngrok-app\.com)[^"]*' | head -1 | sed 's|"$||') [ -z "$PUBLIC_URL" ] && PUBLIC_URL=$(echo "$PAYLOAD" | grep -oE '"public_url"\s*:\s*"https://[^"]+' | sed 's/.*"https:/https:/' | sed 's/"$//' | head -1) fi if [ -z "$PUBLIC_URL" ]; then if [ -f "$NGROK_LOG" ] && grep -qE 'authentication failed|ERR_NGROK_4018|authtoken|requires a verified account' "$NGROK_LOG" 2>/dev/null; then echo "ngrok 需要先配置 authtoken(未登录或 token 未配置)。" echo "请到 https://dashboard.ngrok.com/get-started/your-authtoken 获取 token,然后执行:" echo " ngrok config add-authtoken <你的token>" echo "再重新运行: ./run-ngrok.sh" else echo "解析 ngrok URL 失败。请手动运行: ngrok http 8000" echo "把终端里显示的 https 隧道地址写入 .env: CALLBACK_BASE_URL=https://xxxx.ngrok-free.app" echo "或查看日志: cat $NGROK_LOG" fi exit 1 fi echo "ngrok 公网地址: $PUBLIC_URL" # 更新 .env:已有非空 CALLBACK_BASE_URL 时默认不覆盖,避免每次重启都要重新设定;传 --update 则强制更新 FORCE_UPDATE=0 for a in "$@"; do [ "$a" = "--update" ] && FORCE_UPDATE=1 done ENV_FILE=".env" CURRENT_CB="" [ -f "$ENV_FILE" ] && CURRENT_CB=$(grep -E '^CALLBACK_BASE_URL=' "$ENV_FILE" 2>/dev/null | sed 's/^CALLBACK_BASE_URL=//' | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') || true if [ "$FORCE_UPDATE" = "1" ] || [ -z "$CURRENT_CB" ]; then if [ ! -f "$ENV_FILE" ]; then echo "CALLBACK_BASE_URL=$PUBLIC_URL" >> "$ENV_FILE" echo "已写入 $ENV_FILE: CALLBACK_BASE_URL=$PUBLIC_URL" else if grep -q '^CALLBACK_BASE_URL=' "$ENV_FILE" 2>/dev/null; then if [[ "$(uname)" == "Darwin" ]]; then sed -i '' "s|^CALLBACK_BASE_URL=.*|CALLBACK_BASE_URL=$PUBLIC_URL|" "$ENV_FILE" else sed -i "s|^CALLBACK_BASE_URL=.*|CALLBACK_BASE_URL=$PUBLIC_URL|" "$ENV_FILE" fi echo "已更新 $ENV_FILE: CALLBACK_BASE_URL=$PUBLIC_URL" else echo "" >> "$ENV_FILE" echo "# 消息回调(ngrok 调通用,由 run-ngrok.sh 自动写入)" >> "$ENV_FILE" echo "CALLBACK_BASE_URL=$PUBLIC_URL" >> "$ENV_FILE" echo "已追加 $ENV_FILE: CALLBACK_BASE_URL=$PUBLIC_URL" fi fi else echo "已保留现有 CALLBACK_BASE_URL=$CURRENT_CB(不覆盖)。若 ngrok 已换新地址,请执行: ./run-ngrok.sh --update" fi echo "" echo "下一步:" echo " 1. 若尚未启动后端,请在新终端执行: ./run-dev.sh" echo " 2. 后端启动时会向 7006 注册 SetCallback,回调地址: $PUBLIC_URL/api/callback/wechat-message" echo " 3. 访问管理页 http://localhost:3000 登录后,新消息会由 7006 POST 到上述地址" echo "" echo "(本终端可保持 ngrok 运行;或先 Ctrl+C 结束 ngrok,再按上述步骤先 run-ngrok.sh 再 run-dev.sh)"