10/01 预检存档揭示了坏首启的真面目:游戏停在资源检查提示屏(轮换 Tips 图文)等待底部 START 按钮,没人点就无限期干等,体积 250-560KB 与登录/标题屏区间重叠。现在每轮点按都先点 (640,680) 的 START, 再按画面类别点对应位置;该坐标在其余已知画面上均无交互物。
222 lines
9.8 KiB
Bash
Executable File
222 lines
9.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# MAA Online daily runner, designed for systemd oneshot execution.
|
|
#
|
|
# Flow: wait for the cloud API -> plan remaining tasks from earlier attempts ->
|
|
# start a cloud session -> preflight (watch the screen, tap through title/login
|
|
# until the game is recognizable -- the first session of the day may process
|
|
# resources for ~10 min on a screen MAA cannot recognize) -> run the daily task
|
|
# file under a stall watchdog -> on failure release the cloud session and
|
|
# retry (up to MAA_ONLINE_MAX_ATTEMPTS).
|
|
#
|
|
# Retries use resume mode: bin/maa-resume-tasks.py inspects the earlier
|
|
# attempts' logs and only the tasks that never reported "<Type> Completed" are
|
|
# run again, so a retry does not re-burn cloud-game minutes on finished work.
|
|
#
|
|
# Manual catch-up run (for example after a failed night, using the remaining
|
|
# daily quota):
|
|
# MAA_ONLINE_RESUME_FROM="artifacts/maa-daily-<stamp>.log" bin/maa-online-daily-run
|
|
set -uo pipefail
|
|
|
|
ROOT="${MAA_ONLINE_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
API="${MAA_ONLINE_API:-http://127.0.0.1:22888}"
|
|
LOG_DIR="${MAA_ONLINE_LOG_DIR:-$ROOT/artifacts}"
|
|
MAA_ONLINE_BIN="${MAA_ONLINE_BIN:-$ROOT/bin/maa-online}"
|
|
PYTHON="${MAA_ONLINE_PYTHON:-$ROOT/runtime/venv/bin/python}"
|
|
RESUME_HELPER="$ROOT/bin/maa-resume-tasks.py"
|
|
DAILY_SOURCE="${MAA_ONLINE_DAILY_SOURCE:-$ROOT/config/tasks/daily.toml}"
|
|
RESUME_FILE="${MAA_ONLINE_RESUME_OUT:-$ROOT/config/tasks/daily-resume.toml}"
|
|
|
|
MAX_ATTEMPTS="${MAA_ONLINE_MAX_ATTEMPTS:-3}"
|
|
STALL_FAILS="${MAA_ONLINE_STALL_FAILS:-12}" # consecutive ScreencapFailed (~24s+)
|
|
ATTEMPT_MAX_SECONDS="${MAA_ONLINE_ATTEMPT_MAX_SECONDS:-3000}" # per-attempt ceiling
|
|
# Pre-flight: after /start, watch the game screen and tap through the boot flow
|
|
# (title -> login -> loading) until a recognizable in-game screen appears.
|
|
# The first session of the day can sit on an unrecognizable screen for ~10 min
|
|
# while the game does first-login resource processing; without this, MAA burns
|
|
# the whole attempt (and its quota) failing recognition on every task.
|
|
PREFLIGHT_MAX_SECONDS="${MAA_ONLINE_PREFLIGHT_MAX_SECONDS:-780}"
|
|
# Archive pre-flight screenshots (one every 30s) so a bad first boot can be
|
|
# diagnosed visually afterwards: the bad screen is static, so a handful of
|
|
# saved frames tell us exactly what the game was showing.
|
|
PREFLIGHT_SHOT_DIR="${MAA_ONLINE_PREFLIGHT_SHOT_DIR:-$LOG_DIR/preflight}"
|
|
rm -rf "$PREFLIGHT_SHOT_DIR"
|
|
mkdir -p "$PREFLIGHT_SHOT_DIR"
|
|
|
|
mkdir -p "$LOG_DIR"
|
|
|
|
api() { # api <METHOD> <PATH>
|
|
curl --noproxy '*' -fsS --max-time 180 -X "$1" "$API$2"
|
|
}
|
|
|
|
echo "[daily-run] $(date --iso-8601=seconds) waiting for cloud API at $API ..."
|
|
ready=""
|
|
for _ in $(seq 1 90); do
|
|
if api GET /info >/dev/null 2>&1; then ready=1; break; fi
|
|
sleep 2
|
|
done
|
|
if [ -z "$ready" ]; then
|
|
echo "[daily-run] ERROR: cloud API not ready after 180s" >&2
|
|
exit 2
|
|
fi
|
|
|
|
REASON=""
|
|
run_daily_watchdog() { # arg1: log file, arg2: task file name; sets REASON on abort
|
|
local log="$1"
|
|
local task="$2"
|
|
REASON=""
|
|
"$MAA_ONLINE_BIN" run "$task" --batch -v --log-file="$log" &
|
|
local maa_pid=$!
|
|
while kill -0 "$maa_pid" 2>/dev/null; do
|
|
sleep 5
|
|
if [ $(( $(date +%s) - attempt_epoch )) -gt "$ATTEMPT_MAX_SECONDS" ]; then
|
|
REASON="attempt exceeded ${ATTEMPT_MAX_SECONDS}s ceiling"
|
|
break
|
|
fi
|
|
local streak
|
|
streak=$(awk '/ScreencapFailed/{c++; next} {c=0} END{print c+0}' "$log" 2>/dev/null)
|
|
if [ "${streak:-0}" -ge "$STALL_FAILS" ]; then
|
|
REASON="screencap stalled (${streak} consecutive failures; cloud session likely dead)"
|
|
break
|
|
fi
|
|
if grep -qE '\] Disconnected$' "$log" 2>/dev/null; then
|
|
REASON="MAA reported Disconnected (cloud session lost)"
|
|
break
|
|
fi
|
|
if grep -qE '\] StartUp Error$' "$log" 2>/dev/null; then
|
|
# When StartUp fails, every later task fails too (observed on
|
|
# 2026-09-23/24/25): the game screen is not ready. Abort now
|
|
# instead of burning ~7 more minutes of quota on the remaining
|
|
# tasks; the retry's preflight will find a ready screen.
|
|
REASON="StartUp failed; game screen not ready (remaining tasks would fail too)"
|
|
break
|
|
fi
|
|
done
|
|
if [ -n "$REASON" ]; then
|
|
echo "[daily-run] watchdog: $REASON; aborting attempt" >&2
|
|
kill -INT "$maa_pid" 2>/dev/null
|
|
for _ in $(seq 1 6); do kill -0 "$maa_pid" 2>/dev/null || break; sleep 5; done
|
|
kill -0 "$maa_pid" 2>/dev/null && kill -9 "$maa_pid" 2>/dev/null
|
|
wait "$maa_pid" 2>/dev/null
|
|
return 1
|
|
fi
|
|
wait "$maa_pid" || { REASON="maa exited rc=$?"; return 1; }
|
|
return 0
|
|
}
|
|
|
|
attempt_logs=()
|
|
if [ -n "${MAA_ONLINE_RESUME_FROM:-}" ]; then
|
|
read -r -a seeded <<< "$MAA_ONLINE_RESUME_FROM"
|
|
attempt_logs+=("${seeded[@]}")
|
|
echo "[daily-run] seeded resume planning with: ${attempt_logs[*]}"
|
|
fi
|
|
|
|
# Screen classes by PNG size, calibrated on real captures (2026-09-23):
|
|
# black/loading <20KB | login 200-350KB (开始唤醒) | title ~480KB |
|
|
# announcement/menu >=650KB (hand off to MAA)
|
|
preflight() { # arg1: max seconds, arg2: attempt no.; returns 0 once the screen looks in-game
|
|
local max="$1" attempt_no="${2:-x}" elapsed=0 last_action=-20 ready_hits=0
|
|
local png=/tmp/maa-preflight.png
|
|
while [ "$elapsed" -lt "$max" ]; do
|
|
if api GET /screencap > "$png" 2>/dev/null; then
|
|
local size
|
|
size=$(stat -c %s "$png" 2>/dev/null || echo 0)
|
|
if [ $((elapsed % 30)) -eq 0 ]; then
|
|
cp "$png" "$PREFLIGHT_SHOT_DIR/a${attempt_no}-t$(printf '%04d' "$elapsed").png" 2>/dev/null
|
|
fi
|
|
if [ "$size" -ge 650000 ]; then
|
|
# Require the in-game-class screen to be stable: a single large
|
|
# frame can still be a loading/transition screen (2026-09-25
|
|
# attempt 1 handed a 679KB not-ready screen to MAA this way).
|
|
ready_hits=$((ready_hits + 1))
|
|
if [ "$ready_hits" -ge 2 ]; then
|
|
echo "[daily-run] preflight: ready after ${elapsed}s (screen ${size}B, in-game class, stable)"
|
|
return 0
|
|
fi
|
|
else
|
|
ready_hits=0
|
|
if [ $((elapsed - last_action)) -ge 12 ]; then
|
|
# The resource-check tips screen sits indefinitely until its
|
|
# bottom-center START button is tapped (2026-10-01 preflight
|
|
# captures). Its PNG size overlaps both the login and title
|
|
# bands, so tap START on every cycle, plus the branch-specific
|
|
# position. (640,680) is inert on every other known screen.
|
|
api POST /click '{"x":640,"y":680}' >/dev/null 2>&1
|
|
if [ "$size" -ge 200000 ] && [ "$size" -lt 350000 ]; then
|
|
api POST /click '{"x":640,"y":510}' >/dev/null 2>&1
|
|
echo "[daily-run] preflight: ${elapsed}s login-class screen (${size}B); tapping START + 开始唤醒"
|
|
else
|
|
api POST /click '{"x":640,"y":360}' >/dev/null 2>&1
|
|
echo "[daily-run] preflight: ${elapsed}s screen ${size}B; tapping START + center"
|
|
fi
|
|
last_action=$elapsed
|
|
fi
|
|
fi
|
|
fi
|
|
sleep 6
|
|
elapsed=$((elapsed + 6))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
plan_tasks() { # echoes the task file to run ("daily", "daily-resume" or "NONE")
|
|
if [ "${#attempt_logs[@]}" -eq 0 ]; then
|
|
echo daily
|
|
return
|
|
fi
|
|
local result
|
|
result=$("$PYTHON" "$RESUME_HELPER" "${attempt_logs[@]}" \
|
|
--source "$DAILY_SOURCE" --out "$RESUME_FILE" 2>&1 | tail -n1)
|
|
case "$result" in
|
|
NONE)
|
|
echo NONE
|
|
;;
|
|
RESUME_TASKS=*)
|
|
echo "[daily-run] resume plan: ${result#RESUME_TASKS=}" >&2
|
|
echo daily-resume
|
|
;;
|
|
*)
|
|
echo "[daily-run] WARN: resume planning failed (${result}); using full daily" >&2
|
|
echo daily
|
|
;;
|
|
esac
|
|
}
|
|
|
|
attempt=0
|
|
while [ "$attempt" -lt "$MAX_ATTEMPTS" ]; do
|
|
attempt=$((attempt + 1))
|
|
TASK_FILE=$(plan_tasks)
|
|
if [ "$TASK_FILE" = "NONE" ]; then
|
|
echo "[daily-run] $(date --iso-8601=seconds) every daily task already completed in an earlier attempt; nothing left to do"
|
|
exit 0
|
|
fi
|
|
echo "[daily-run] attempt $attempt/$MAX_ATTEMPTS: task file '$TASK_FILE'; starting cloud game ..."
|
|
if ! api POST /start; then
|
|
echo "[daily-run] attempt $attempt: /start failed (cloud service or token issue)" >&2
|
|
sleep 20
|
|
continue
|
|
fi
|
|
echo "[daily-run] attempt $attempt: preflight: waiting for a recognizable game screen (max ${PREFLIGHT_MAX_SECONDS}s) ..."
|
|
if ! preflight "$PREFLIGHT_MAX_SECONDS" "$attempt"; then
|
|
REASON="preflight timed out after ${PREFLIGHT_MAX_SECONDS}s; game screen never became ready"
|
|
echo "[daily-run] attempt $attempt failed: $REASON; releasing cloud session" >&2
|
|
api POST /exit >/dev/null 2>&1 || true
|
|
sleep 30
|
|
continue
|
|
fi
|
|
RUN_LOG="$LOG_DIR/maa-daily-$(date +%Y%m%d-%H%M%S).log"
|
|
echo "[daily-run] attempt $attempt: running '$TASK_FILE' (log: $RUN_LOG) ..."
|
|
attempt_epoch=$(date +%s)
|
|
if run_daily_watchdog "$RUN_LOG" "$TASK_FILE"; then
|
|
echo "[daily-run] $(date --iso-8601=seconds) daily completed successfully (attempt $attempt)"
|
|
exit 0
|
|
fi
|
|
attempt_logs+=("$RUN_LOG")
|
|
echo "[daily-run] attempt $attempt failed: ${REASON:-unknown}; releasing cloud session" >&2
|
|
api POST /exit >/dev/null 2>&1 || true
|
|
sleep 30
|
|
done
|
|
|
|
echo "[daily-run] ERROR: daily failed after $MAX_ATTEMPTS attempts (last: ${REASON:-unknown})" >&2
|
|
echo "[daily-run] if the failure mentions login/token, re-run: runtime/venv/bin/python backend/login.py request <phone>" >&2
|
|
exit 1
|