feat(cron): add --app-domain/--worker-domain/--debug-domain CLI flags, auto-prepend https://

Bare domains like messageweave.example work without the https:// prefix.
Old --*-url flags and MW_*_URL env vars still work as aliases.
Flag values override env vars; defaults applied after parsing.
This commit is contained in:
anonsaber 2026-10-10 18:12:31 +08:00
parent 427c487923
commit d5403d8b5b
1 changed files with 109 additions and 65 deletions

View File

@ -14,48 +14,65 @@
# #
# 详设计说明见 docs/deployment.md 第 6.3 节与第 6.3.1 节。 # 详设计说明见 docs/deployment.md 第 6.3 节与第 6.3.1 节。
# #
# --- 凭据只从环境变量读 --- # --- 凭据只从环境变量或命令行 flag 读取 ---
# 不读 .env、不读配置文件、不写盘(唯一例外是可选的 MW_LOG_FILE 运行日志)。 # 不读 .env、不读配置文件、不写盘(唯一例外是可选的 --log-file / MW_LOG_FILE 运行日志)。
# 所有 *_TOKEN 一律不打印、不写日志。 # 所有 *_TOKEN 一律不打印、不写日志。命令行 flag 优先于同名 MW_* 环境变量。
# <<HELP>> # <<HELP>>
# --- 环境变量 --- # --- 配置项(flag 或环境变量,flag 优先) ---
# MW_APP_URL 必填。对外平台 URL(走 LB 的域名),用于 GET /api/status、 # --app-domain DOMAIN / MW_APP_DOMAIN
# GET /healthz、POST /reconcile。 # 必填。对外平台域名(走 LB 的域名),用于 GET /api/status、
# MW_WORKER_URL 可选,默认 = MW_APP_URL。POST /worker 的地址。/worker 已在 LB # GET /healthz、POST /reconcile。只写域名,无需 https:// 前缀
# 路由白名单内,默认直接走 MW_APP_URL 即可;要跳过 LB 直连源站 # (脚本自动补 https://)。旧名 --app-url / MW_APP_URL 仍可用。
# 时才需要覆盖(如想绕过 LB 的排空超时,或源站另有独立域名)。 # --worker-domain DOMAIN / MW_WORKER_DOMAIN
# MW_DEBUG_URL 可选,默认 = MW_WORKER_URL。GET /debug/* 与 POST /debug/notify # 可选,默认 = --app-domain。POST /worker 的地址。/worker 已在
# 的地址(需要后端以 --debug 启动且 DEBUG_TOKEN 非空, # LB 路由白名单内,默认直接走 --app-domain 即可;要跳过 LB 直连
# 且需直连源站)。 # 源站时才需要覆盖。旧名 --worker-url / MW_WORKER_URL 仍可用。
# MW_RECONCILE_TOKEN 可选。缺省时跳过 /reconcile(仅靠 Push 回调入队的部署可接受)。 # --debug-domain DOMAIN / MW_DEBUG_DOMAIN
# MW_WORKER_TOKEN 必填。/worker 的 Bearer token。 # 可选,默认 = --worker-domain。GET /debug/* 与 POST /debug/notify
# MW_DEBUG_TOKEN 可选,仅 --diagnose / --test-notify 用。 # 的地址(需要后端以 --debug 启动且 DEBUG_TOKEN 非空,且需直连源站)。
# MW_BATCH 可选,默认不发(服务端默认 10,且上限 10,超出静默截断)。 # 旧名 --debug-url / MW_DEBUG_URL 仍可用。
# MW_TIMEOUT 可选,默认 60。单次请求超时秒数。 # --reconcile-token TOKEN / MW_RECONCILE_TOKEN
# MW_INTERVAL 可选,默认 30。仅 --loop 用,两次触发之间的秒数。 # 可选。缺省时跳过 /reconcile(仅靠 Push 回调入队的部署可接受)。
# MW_LOG_FILE 可选。给定时每次触发追加一行摘要(不含任何凭据)。 # --worker-token TOKEN / MW_WORKER_TOKEN
# MW_TEST_TEXT 可选,仅 --test-notify 用。直发测试的正文。 # 必填。/worker 的 Bearer token。
# --debug-token TOKEN / MW_DEBUG_TOKEN
# 可选,仅 --diagnose / --test-notify 用。
# --batch N / MW_BATCH 可选,默认不发(服务端默认 10,且上限 10,超出静默截断)。
# --timeout N / MW_TIMEOUT
# 可选,默认 60。单次请求超时秒数。
# --interval N / MW_INTERVAL
# 可选,默认 30。仅 --loop 用,两次触发之间的秒数。
# --log-file PATH / MW_LOG_FILE
# 可选。给定时每次触发追加一行摘要(不含任何凭据)。
# --test-text TEXT / MW_TEST_TEXT
# 可选,仅 --test-notify 用。直发测试的正文。
# #
# --- 用法 --- # --- 用法 ---
# MW_APP_URL=https://lb.example \ # # flag 风格(推荐交互式使用,只写域名):
# MW_WORKER_URL=https://origin.example \ # bash scripts/cron-drain.sh --once \
# MW_RECONCILE_TOKEN=reconcile-... \ # --app-domain messageweave.example \
# MW_WORKER_TOKEN=worker-... \ # --reconcile-token reconcile-... --worker-token worker-...
#
# # 环境变量风格(cron 推荐,凭据不出现在 ps):
# MW_APP_DOMAIN=messageweave.example \
# MW_RECONCILE_TOKEN=reconcile-... MW_WORKER_TOKEN=worker-... \
# bash scripts/cron-drain.sh --once # bash scripts/cron-drain.sh --once
# #
# # cron(每分钟,日志落盘便于事后排查): # # cron(每分钟,日志落盘便于事后排查):
# # * * * * * env MW_APP_URL=https://lb.example \ # # * * * * * env MW_APP_DOMAIN=messageweave.example \
# # MW_WORKER_URL=https://origin.example MW_RECONCILE_TOKEN=*** MW_WORKER_TOKEN=*** \ # # MW_RECONCILE_TOKEN=*** MW_WORKER_TOKEN=*** \
# # bash /opt/messageweave/scripts/cron-drain.sh --once >> /var/log/mw-drain.log 2>&1 # # bash /opt/messageweave/scripts/cron-drain.sh --once >> /var/log/mw-drain.log 2>&1
# #
# # 常驻进程,不依赖 crontab: # # 常驻进程,不依赖 crontab:
# MW_INTERVAL=30 bash scripts/cron-drain.sh --loop # bash scripts/cron-drain.sh --loop --app-domain messageweave.example \
# --reconcile-token <token> --worker-token <token> --interval 30
# #
# # 收不到提醒时先跑这个,自动定位原因: # # 收不到提醒时先跑这个,自动定位原因:
# MW_APP_URL=... MW_DEBUG_URL=... MW_DEBUG_TOKEN=*** bash scripts/cron-drain.sh --diagnose # bash scripts/cron-drain.sh --diagnose --app-domain messageweave.example --debug-token ***
# #
# # 绕过队列、复用生产出站路径直发一条,验证 bot token 与 chat_id 本身是否可用: # # 绕过队列、复用生产出站路径直发一条,验证 bot token 与 chat_id 本身是否可用:
# MW_DEBUG_URL=... MW_DEBUG_TOKEN=*** bash scripts/cron-drain.sh --test-notify # bash scripts/cron-drain.sh --test-notify --app-domain messageweave.example \
# --debug-token *** --test-text "probe"
# #
# --- 退出码 --- # --- 退出码 ---
# 0 成功(两个端点都返回 204,或 /reconcile 被主动跳过) # 0 成功(两个端点都返回 204,或 /reconcile 被主动跳过)
@ -70,16 +87,17 @@ MODE="--once"
DO_DIAGNOSE=0 DO_DIAGNOSE=0
DO_TEST_NOTIFY=0 DO_TEST_NOTIFY=0
APP_URL="" APP_URL="${MW_APP_DOMAIN:-${MW_APP_URL:-}}"
WORKER_URL="" WORKER_URL="${MW_WORKER_DOMAIN:-${MW_WORKER_URL:-}}"
DEBUG_URL="" DEBUG_URL="${MW_DEBUG_DOMAIN:-${MW_DEBUG_URL:-}}"
RECONCILE_TOKEN="" RECONCILE_TOKEN="${MW_RECONCILE_TOKEN:-}"
WORKER_TOKEN="" WORKER_TOKEN="${MW_WORKER_TOKEN:-}"
DEBUG_TOKEN="" DEBUG_TOKEN="${MW_DEBUG_TOKEN:-}"
BATCH="" BATCH="${MW_BATCH:-}"
TIMEOUT="${MW_TIMEOUT:-60}" TIMEOUT="${MW_TIMEOUT:-60}"
INTERVAL="${MW_INTERVAL:-30}" INTERVAL="${MW_INTERVAL:-30}"
LOG_FILE="${MW_LOG_FILE:-}" LOG_FILE="${MW_LOG_FILE:-}"
TEST_TEXT="${MW_TEST_TEXT:-}"
FAILURES=0 FAILURES=0
# --- 基础输出 ----------------------------------------------------------------- # --- 基础输出 -----------------------------------------------------------------
@ -114,13 +132,14 @@ usage_die() {
usage 2 usage 2
} }
# 校验 URL 前缀;空值跳过(未设置的可选变量)。 # 规范化域名/URL:没有协议前缀时自动补 https://,返回完整 URL。
check_url() { normalize_url() {
[ -n "$1" ] || return 0 local v="$1"
case "$1" in [ -n "$v" ] || { printf '%s' "$v"; return; }
http://* | https://*) return 0 ;; case "$v" in
http://* | https://*) printf '%s' "$v" ;;
*) printf 'https://%s' "$v" ;;
esac esac
die "$2 需要 http:// 或 https:// 前缀"
} }
# --- JSON 取值(不依赖 jq):json_field '<json>' <key> → 首个匹配键的值 ----------- # --- JSON 取值(不依赖 jq):json_field '<json>' <key> → 首个匹配键的值 -----------
@ -214,7 +233,7 @@ run_reconcile() {
FAILURES=$((FAILURES + 1)) FAILURES=$((FAILURES + 1))
;; ;;
404) 404)
log "reconcile=404 $APP_URL/reconcile 不存在(MW_APP_URL 可能指错)" log "reconcile=404 $APP_URL/reconcile 不存在(MW_APP_DOMAIN 可能指错)"
FAILURES=$((FAILURES + 1)) FAILURES=$((FAILURES + 1))
;; ;;
*) *)
@ -274,9 +293,9 @@ run_worker() {
if printf '%s' "$BODY" | grep -q 'route not forwarded'; then if printf '%s' "$BODY" | grep -q 'route not forwarded'; then
log "worker=404 $WORKER_URL/worker 被 LB 拦截(route not forwarded)" log "worker=404 $WORKER_URL/worker 被 LB 拦截(route not forwarded)"
log " LB 版本太旧:/worker 需要 LB_VERSION >= 2026.10.3(见 /healthz-worker)" log " LB 版本太旧:/worker 需要 LB_VERSION >= 2026.10.3(见 /healthz-worker)"
log " 升级 LB,或把 MW_WORKER_URL 指向源站直连地址绕过 LB" log " 升级 LB,或把 MW_WORKER_DOMAIN 指向源站直连地址绕过 LB"
else else
log "worker=404 $WORKER_URL/worker 不存在(MW_WORKER_URL 可能指错)" log "worker=404 $WORKER_URL/worker 不存在(MW_WORKER_DOMAIN 可能指错)"
fi fi
FAILURES=$((FAILURES + 1)) FAILURES=$((FAILURES + 1))
;; ;;
@ -301,7 +320,7 @@ diagnose() {
# 1) 公开端点,不用 token:先确认后端活着。 # 1) 公开端点,不用 token:先确认后端活着。
if [ -z "$APP_URL" ]; then if [ -z "$APP_URL" ]; then
say " [1] 跳过(未设置 MW_APP_URL,本次诊断只用了 MW_DEBUG_URL)" say " [1] 跳过(未设置 MW_APP_DOMAIN,本次诊断只用了 MW_DEBUG_DOMAIN)"
else else
request GET "$APP_URL/api/status" request GET "$APP_URL/api/status"
code="$CODE" code="$CODE"
@ -455,7 +474,7 @@ diagnose() {
# --- --test-notify:绕过队列直发一条,验证 bot token 与 chat_id 本身是否可用 ------ # --- --test-notify:绕过队列直发一条,验证 bot token 与 chat_id 本身是否可用 ------
test_notify() { test_notify() {
local text="${MW_TEST_TEXT:-[messageweave] cron-drain direct-send test}" local text="${TEST_TEXT:-[messageweave] cron-drain direct-send test}"
local code rc body local code rc body
rc=0 rc=0
@ -474,7 +493,7 @@ test_notify() {
code=$(cat "$BODY_FILE.code" 2>/dev/null) code=$(cat "$BODY_FILE.code" 2>/dev/null)
body=$(cat "$BODY_FILE" 2>/dev/null) body=$(cat "$BODY_FILE" 2>/dev/null)
if [ "$rc" -ne 0 ] || [ -z "$code" ]; then if [ "$rc" -ne 0 ] || [ -z "$code" ]; then
say "--test-notify: 请求未发出(需要 MW_DEBUG_URL 与 MW_DEBUG_TOKEN)" say "--test-notify: 请求未发出(需要 MW_DEBUG_DOMAIN 与 MW_DEBUG_TOKEN)"
FAILURES=$((FAILURES + 1)) FAILURES=$((FAILURES + 1))
return 0 return 0
fi fi
@ -516,33 +535,58 @@ main() {
--diagnose) DO_DIAGNOSE=1 ;; --diagnose) DO_DIAGNOSE=1 ;;
--test-notify) DO_TEST_NOTIFY=1 ;; --test-notify) DO_TEST_NOTIFY=1 ;;
--help) usage 0 ;; --help) usage 0 ;;
--app-domain=*) APP_URL="${1#--app-domain=}" ;;
--app-domain) shift; APP_URL="${1:?--app-domain 需要值}" ;;
--app-url=*) APP_URL="${1#--app-url=}" ;;
--app-url) shift; APP_URL="${1:?--app-url 需要值}" ;;
--worker-domain=*) WORKER_URL="${1#--worker-domain=}" ;;
--worker-domain) shift; WORKER_URL="${1:?--worker-domain 需要值}" ;;
--worker-url=*) WORKER_URL="${1#--worker-url=}" ;;
--worker-url) shift; WORKER_URL="${1:?--worker-url 需要值}" ;;
--debug-domain=*) DEBUG_URL="${1#--debug-domain=}" ;;
--debug-domain) shift; DEBUG_URL="${1:?--debug-domain 需要值}" ;;
--debug-url=*) DEBUG_URL="${1#--debug-url=}" ;;
--debug-url) shift; DEBUG_URL="${1:?--debug-url 需要值}" ;;
--reconcile-token=*) RECONCILE_TOKEN="${1#--reconcile-token=}" ;;
--reconcile-token) shift; RECONCILE_TOKEN="${1:?--reconcile-token 需要值}" ;;
--worker-token=*) WORKER_TOKEN="${1#--worker-token=}" ;;
--worker-token) shift; WORKER_TOKEN="${1:?--worker-token 需要值}" ;;
--debug-token=*) DEBUG_TOKEN="${1#--debug-token=}" ;;
--debug-token) shift; DEBUG_TOKEN="${1:?--debug-token 需要值}" ;;
--batch=*) BATCH="${1#--batch=}" ;;
--batch) shift; BATCH="${1:?--batch 需要值}" ;;
--timeout=*) TIMEOUT="${1#--timeout=}" ;;
--timeout) shift; TIMEOUT="${1:?--timeout 需要值}" ;;
--interval=*) INTERVAL="${1#--interval=}" ;;
--interval) shift; INTERVAL="${1:?--interval 需要值}" ;;
--log-file=*) LOG_FILE="${1#--log-file=}" ;;
--log-file) shift; LOG_FILE="${1:?--log-file 需要值}" ;;
--test-text=*) TEST_TEXT="${1#--test-text=}" ;;
--test-text) shift; TEST_TEXT="${1:?--test-text 需要值}" ;;
*) die "未知参数:$1(--help 查看用法)" ;; *) die "未知参数:$1(--help 查看用法)" ;;
esac esac
shift shift
done done
APP_URL="${MW_APP_URL:-}" # 命令行 flag 优先于环境变量;未显式指定时回退到默认值。
WORKER_URL="${MW_WORKER_URL:-$APP_URL}" WORKER_URL="${WORKER_URL:-$APP_URL}"
DEBUG_URL="${MW_DEBUG_URL:-$WORKER_URL}" DEBUG_URL="${DEBUG_URL:-$WORKER_URL}"
RECONCILE_TOKEN="${MW_RECONCILE_TOKEN:-}" # 没有协议前缀的域名自动补 https://
WORKER_TOKEN="${MW_WORKER_TOKEN:-}" APP_URL="$(normalize_url "$APP_URL")"
DEBUG_TOKEN="${MW_DEBUG_TOKEN:-}" WORKER_URL="$(normalize_url "$WORKER_URL")"
BATCH="${MW_BATCH:-}" DEBUG_URL="$(normalize_url "$DEBUG_URL")"
[ -n "$APP_URL" ] || [ -n "$WORKER_URL" ] || [ -n "$DEBUG_URL" ] \ [ -n "$APP_URL" ] || [ -n "$WORKER_URL" ] || [ -n "$DEBUG_URL" ] \
|| usage_die "缺少 MW_APP_URL / MW_WORKER_URL / MW_DEBUG_URL 之一" || usage_die "缺少 --app-domain / --worker-domain / --debug-domain 之一(或对应 MW_* 环境变量)"
check_url "$APP_URL" MW_APP_URL
check_url "$WORKER_URL" MW_WORKER_URL
check_url "$DEBUG_URL" MW_DEBUG_URL
for pair in "TIMEOUT=$TIMEOUT" "INTERVAL=$INTERVAL" "BATCH=$BATCH"; do for pair in "TIMEOUT=$TIMEOUT" "INTERVAL=$INTERVAL" "BATCH=$BATCH"; do
[ -n "${pair#*=}" ] || continue [ -n "${pair#*=}" ] || continue
case "${pair#*=}" in case "${pair#*=}" in
*[!0-9]*) die "${pair%%=*} 必须是整数" ;; *[!0-9]*) die "${pair%%=*} 必须是整数" ;;
esac esac
done done
[ "$TIMEOUT" -ge 1 ] || die "MW_TIMEOUT 至少 1 秒" [ "$TIMEOUT" -ge 1 ] || die "--timeout / MW_TIMEOUT 至少 1 秒"
if [ -n "$BATCH" ]; then if [ -n "$BATCH" ]; then
[ "$BATCH" -ge 1 ] || die "MW_BATCH 至少 1" [ "$BATCH" -ge 1 ] || die "--batch / MW_BATCH 至少 1"
if [ "$BATCH" -gt 10 ]; then if [ "$BATCH" -gt 10 ]; then
log "MW_BATCH=$BATCH 超过服务端上限 10,实际按 10 处理" log "MW_BATCH=$BATCH 超过服务端上限 10,实际按 10 处理"
fi fi
@ -551,10 +595,10 @@ main() {
# 诊断 / 直发测试是独立模式,不跑真正的排空。诊断缺 token 时也能降级跑公开端点。 # 诊断 / 直发测试是独立模式,不跑真正的排空。诊断缺 token 时也能降级跑公开端点。
if [ "$DO_DIAGNOSE" -eq 1 ] || [ "$DO_TEST_NOTIFY" -eq 1 ]; then if [ "$DO_DIAGNOSE" -eq 1 ] || [ "$DO_TEST_NOTIFY" -eq 1 ]; then
do_drain=0 do_drain=0
[ "$DO_TEST_NOTIFY" -eq 1 ] && [ -z "$DEBUG_TOKEN" ] && die "直发测试需要 MW_DEBUG_TOKEN" [ "$DO_TEST_NOTIFY" -eq 1 ] && [ -z "$DEBUG_TOKEN" ] && die "直发测试需要 --debug-token / MW_DEBUG_TOKEN"
else else
[ -n "$WORKER_URL" ] || usage_die "缺少 MW_WORKER_URL(默认取 MW_APP_URL)" [ -n "$WORKER_URL" ] || usage_die "缺少 --worker-domain / MW_WORKER_DOMAIN(默认取 --app-domain)"
[ -n "$WORKER_TOKEN" ] || usage_die "缺少 MW_WORKER_TOKEN(/worker 是排空的唯一入口)" [ -n "$WORKER_TOKEN" ] || usage_die "缺少 --worker-token / MW_WORKER_TOKEN(/worker 是排空的唯一入口)"
fi fi
BODY_FILE="$(mktemp -d 2>/dev/null)/body" || die "无法创建临时目录" BODY_FILE="$(mktemp -d 2>/dev/null)/body" || die "无法创建临时目录"
@ -566,7 +610,7 @@ main() {
if [ "$do_drain" -eq 1 ]; then if [ "$do_drain" -eq 1 ]; then
if [ "$MODE" = "--loop" ]; then if [ "$MODE" = "--loop" ]; then
trap 'log "收到退出信号,循环结束"; exit 0' TERM INT trap 'log "收到退出信号,循环结束"; exit 0' TERM INT
log "--loop 启动 MW_INTERVAL=${INTERVAL}s MW_TIMEOUT=${TIMEOUT}s 按 Ctrl-C 退出" log "--loop 启动 --interval=${INTERVAL}s --timeout=${TIMEOUT}s 按 Ctrl-C 退出"
while :; do while :; do
run_reconcile run_reconcile
run_worker run_worker