feat: 增加 systemd 定时执行与断点续跑

- backend/server.py: 信令 WebSocket 被远端关闭或视频流停滞超过阈值时自动拆除会话,
  由 finally 释放网易云端实例,避免会话空挂、后续 /start 一直拿到 503
- bin/maa-online-daily-run: 新增日常任务包装脚本,等待 API 就绪、启动云游戏、
  停滞看门狗(连续 ScreencapFailed 或 Disconnected)、失败即释放云会话并最多重试 3 次
- bin/maa-resume-tasks.py: 新增断点续跑规划器,重试时只运行未出现 Completed 的任务,
  全部完成则直接判定成功,不再开启新会话;StartUp 永不跳过
- systemd/: 新增 server/daily/update 服务与定时器模板,每天 04:00 起随机 0-2 小时执行
- bin/maa-online-install-systemd: 渲染 __ROOT__ 并安装、启用上述单元
- README.md, user_config/README.md: 补充自动定时执行、断点续跑与已知限制说明
- .gitignore: 忽略生成的 config/tasks/daily-resume.toml
This commit is contained in:
RRRosmontis
2026-09-19 16:45:48 +08:00
parent 73250c91b6
commit 0264053b08
12 changed files with 481 additions and 3 deletions
+20 -2
View File
@@ -35,6 +35,7 @@ WIDTH = int(os.getenv("MAA_ONLINE_WIDTH", "1280"))
HEIGHT = int(os.getenv("MAA_ONLINE_HEIGHT", "720"))
FPS = os.getenv("MAA_ONLINE_FPS", "30")
START_WAIT_SECONDS = float(os.getenv("MAA_ONLINE_START_WAIT_SECONDS", "30"))
STALE_LIMIT = float(os.getenv("MAA_ONLINE_STALE_LIMIT_SECONDS", "120"))
# --- 颜色定义 ---
class Colors:
@@ -439,9 +440,26 @@ async def run_cloud_game():
raise RuntimeError("failed to receive the first cloud-game video frame")
# Use a cancellable wait instead of Event().wait()
# This allows the task to be properly cancelled by signals
# This allows the task to be properly cancelled by signals.
# Also watch for a dead cloud session: if the remote signaling
# websocket closes or the video stream stalls for too long, tear
# the session down (finally-block releases the NetEase session and
# closes the peer connection) so the next /start reconnects cleanly
# instead of serving 503s forever.
while True:
await asyncio.sleep(1)
await asyncio.sleep(5)
sock = app_state.sock
if sock is not None and sock.close_code is not None:
raise RuntimeError(
f"cloud signaling websocket closed (code {sock.close_code}); tearing down session"
)
snapshotter = app_state.snapshotter
if snapshotter is not None:
last_recv = getattr(snapshotter, "_last_recv_ts", 0.0)
if last_recv and (time.monotonic() - last_recv) > STALE_LIMIT:
raise RuntimeError(
f"cloud video stream stale for over {STALE_LIMIT:.0f}s; tearing down session"
)
except asyncio.CancelledError:
print(f"\n{Colors.CYAN}[*] Cloud game task cancelled.{Colors.RESET}")