From 2527560772ce33aaee5c6a42a860fd91ad8b2850 Mon Sep 17 00:00:00 2001 From: li-chx Date: Sun, 1 Nov 2026 22:41:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E7=95=99=20Git=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=B9=B6=E7=AE=80=E5=8C=96=20MAA=20=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=B5=81=E7=A8=8B=EF=BC=8C=E5=8A=A0=E5=85=A5=20MaaEnd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- ArknightsMaaConfig/maa.py | 206 ++++++++++++-------------------- MaaEndConfig/maa.py | 52 ++++++++ MaaEndConfig/maa_pi_config.json | 6 + README.md | 13 +- run_all.sh | 35 ++++-- tests/test_maa.py | 41 +++++++ 7 files changed, 211 insertions(+), 145 deletions(-) create mode 100644 MaaEndConfig/maa.py create mode 100644 MaaEndConfig/maa_pi_config.json create mode 100644 tests/test_maa.py diff --git a/.gitignore b/.gitignore index bf0824e..852bc45 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*.log \ No newline at end of file +*.log +__pycache__/ diff --git a/ArknightsMaaConfig/maa.py b/ArknightsMaaConfig/maa.py index d754c83..b8824af 100755 --- a/ArknightsMaaConfig/maa.py +++ b/ArknightsMaaConfig/maa.py @@ -1,139 +1,85 @@ #!/usr/bin/env python3 import os +import subprocess import sys -import asyncio -import re import time from datetime import datetime -# time record -def fprint(s:str)->None: - print(s) - sys.stdout.flush() -fprint("-------------------------------------------------------") -fprint(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) -fprint("-------------------------------------------------------") -time.sleep(1) - -class CommandProcessor: - - def Command(self, s: str) -> int: - fprint("_> " + s) - if (t := os.system(s)) != 0: - fprint("Fatal Error: Command Failed:" + s) - fprint("Error Number: " + str(t)) - return t - return 0 - - def CommandWithNoProxy(self, s: str) -> int: - fprint("_> " + s) - # 保存并删除 - old_http = os.environ.pop('http_proxy', None) - old_https = os.environ.pop('https_proxy', None) - - t = os.system(s) - - # 立即恢复 - if old_http: - os.environ['http_proxy'] = old_http - if old_https: - os.environ['https_proxy'] = old_https - - if t != 0: - fprint("Fatal Error: Command Failed:" + s) - fprint("Error Number: " + str(t)) - return t - - def RetryCommand(self,s:str,retry:int=3) -> int: - result = 0 - while retry > 0: - result = cp.Command(s) - if result == 0: - break - retry-=1 - time.sleep(1) - return result - - def RetryCommandWithNoProxy(self,s:str,retry:int=3) -> int: - result = 0 - while retry > 0: - result = cp.CommandWithNoProxy(s) - if result == 0: - break - retry-=1 - time.sleep(1) - return result - - async def AsyncCommand(self,command:str,regex:str): - self._asyncCommand = command - fprint("_> await! " + command) - process = await asyncio.create_subprocess_shell(command,stderr=asyncio.subprocess.PIPE) - while True: - line = (await process.stderr.readline()).decode() - fprint(line) - if re.search(regex, line): - fprint("done") - break +ADDRESS = os.getenv("ADB_ADDRESS", "127.0.0.1:5555") +REDROID = os.getenv("REDROID_CONTAINER", "redroid") +TASKS = ["startup", "recruit", "infrast", "fight", "mall", "award"] +DIRECT = {k: v for k, v in os.environ.items() if k.lower() not in ("http_proxy", "https_proxy", "all_proxy")} - - -# cp.Command("command") -# cp.RetryCommand("command",retry_time:int) -# asyncio.run(cp.AsyncCommand("command","stop track regex")) -# script_dir = os.path.dirname((os.path.abspath(__file__))) -cp = CommandProcessor() -haveError = False -def TaskFailRetry(command:str)->int: - retry = 3 - result = 0 - while retry > 0: - retry -= 1 - result = cp.RetryCommand(command) - if result == 0: - return 0 - fprint(f"Task Command is {command}, ret code is {result}") - fprint("Task Failed, retrying...") - cp.RetryCommand("maa closedown") - cp.RetryCommand("maa run startup") - return result +def command(args, proxy=False): + print(f"[{datetime.now():%H:%M:%S}] {' '.join(args)}", flush=True) + return subprocess.run(args, env=os.environ if proxy else DIRECT).returncode -def TaskFailRetryWithNoProxy(command:str)->int: - retry = 3 - result = 0 - while retry > 0: - retry -= 1 - result = cp.RetryCommandWithNoProxy(command) - if result == 0: - return 0 - fprint(f"Task Command is {command}, ret code is {result}") - fprint("Task Failed, retrying...") - cp.RetryCommand("maa closedown") - cp.RetryCommand("maa run startup") - return result -# update -cp.Command("maa update") -cp.Command("maa self update") -# fire it up -cp.Command("docker stop redroid") -redroid_path = os.getenv('redroid_path', "/home/lichx/.config/redroid-rk3588/") -adb_address = os.getenv('adb_address', "localhost:5555") -log_dir = os.getenv('MAA_LOG', "/home/lichx/.config/redroid-rk3588/log/") -os.chdir(redroid_path) -cp.Command("docker compose up -d") -time.sleep(15) -cp.RetryCommand(f"adb connect {adb_address}") -cp.RetryCommand(f"adb -s {adb_address} shell am start --windowingMode 4 com.hypergryph.arknights/com.u8.sdk.U8UnityContext") -# run maa -haveError |= TaskFailRetryWithNoProxy("maa run startup") != 0 -haveError |= TaskFailRetryWithNoProxy("maa run recruit") != 0 -haveError |= TaskFailRetryWithNoProxy("maa run infrast") != 0 -haveError |= TaskFailRetryWithNoProxy("maa run fight") != 0 -haveError |= TaskFailRetryWithNoProxy("maa run mall") != 0 -haveError |= TaskFailRetryWithNoProxy("maa run award") != 0 -# storage error log -if haveError: - cp.Command(f"cp {log_dir}/maa_pyshell.log {log_dir}/maa_pyshell.{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log") -# shutdown -cp.RetryCommand("maa closedown") -cp.Command("docker stop redroid") + +def retry(args): + for attempt in range(3): + if command(args) == 0: + return True + print(f"命令失败,第 {attempt + 1}/3 次", flush=True) + time.sleep(1) + return False + + +def maa(*args): + return ["maa", *args, "--batch", "-a", ADDRESS] + + +def wait_android(): + print("等待 Android 启动……", flush=True) + deadline = time.monotonic() + 180 + while time.monotonic() < deadline: + try: + subprocess.run(["adb", "connect", ADDRESS], env=DIRECT, capture_output=True, timeout=10) + result = subprocess.run(["adb", "-s", ADDRESS, "shell", "getprop", "sys.boot_completed"], + env=DIRECT, capture_output=True, text=True, timeout=10) + if result.returncode == 0 and result.stdout.strip() == "1": + return True + except subprocess.TimeoutExpired: + pass + time.sleep(2) + print("Android 启动超时", flush=True) + return False + + +def task(name): + for _ in range(3): + if retry(maa("run", name)): + return True + print(f"任务 {name} 失败,重新打开游戏后重试", flush=True) + retry(maa("closedown")) + retry(maa("run", "startup")) + return False + + +def main(): + # 更新走代理;失败时仍尝试使用已有版本,与原脚本一致。 + command(["maa", "update"], proxy=True) + command(["maa", "self", "update"], proxy=True) + if command(["docker", "restart", REDROID]): + return 1 + failed = False + ready = False + try: + ready = wait_android() + if not ready: + return 1 + retry(["adb", "-s", ADDRESS, "shell", "am", "start", "--windowingMode", "4", + "com.hypergryph.arknights/com.u8.sdk.U8UnityContext"]) + for name in TASKS: + if not task(name): + failed = True + finally: + if ready and not retry(maa("closedown")): + failed = True + if command(["docker", "stop", REDROID]): + failed = True + return int(failed) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/MaaEndConfig/maa.py b/MaaEndConfig/maa.py new file mode 100644 index 0000000..8de7581 --- /dev/null +++ b/MaaEndConfig/maa.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""MaaEnd 只补齐当前官方终端包的路径和 ADB 界面适配。""" +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from ArknightsMaaConfig.maa import ADDRESS, REDROID, DIRECT, command, wait_android + + +def prepare(configure=False): + source = Path("/app/maaend") + version = json.loads((source / "interface.json").read_text())["version"] + target = Path("/state") / f"maaend-{version}" + if not target.exists(): + shutil.copytree(source, target, symlinks=True) + # 保留原文件;CLI 不支持 hotkey 设置,ADB 使用原始 pipeline 的默认值。 + original = target / "interface.upstream.json" + if not original.exists(): + shutil.copyfile(target / "interface.json", original) + interface = json.loads(original.read_text()) + interface["controller"] = [c for c in interface["controller"] if c["name"] == "ADB"] + interface["import"] = [p for p in interface["import"] if p != "tasks/setting/Keymap.json"] + (target / "interface.json").write_text(json.dumps(interface, ensure_ascii=False, indent=2)) + work = target / "maafw" + config = json.loads(Path(__file__).with_name("maa_pi_config.json").read_text()) + if not configure and not config.get("task"): + raise RuntimeError("请先配置 MaaEnd 任务并提交到 MAAsConfig,再启用 maaend。") + config["adb"].update(address=ADDRESS, adb_path="/usr/bin/adb") + (work / "config").mkdir(exist_ok=True) + (work / "config/maa_pi_config.json").write_text(json.dumps(config, ensure_ascii=False, indent=2)) + return work + + +def main(): + configure = sys.argv[1:] == ["configure"] + work = prepare(configure) + if command(["docker", "restart", REDROID]): + return 1 + try: + if not wait_android(): + return 1 + return subprocess.run([str(work / "MaaPiCli")] + ([] if configure else ["-d"]), cwd=work, env=DIRECT).returncode + finally: + command(["docker", "stop", REDROID]) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/MaaEndConfig/maa_pi_config.json b/MaaEndConfig/maa_pi_config.json new file mode 100644 index 0000000..1e26b36 --- /dev/null +++ b/MaaEndConfig/maa_pi_config.json @@ -0,0 +1,6 @@ +{ + "controller": {"name": "ADB"}, + "adb": {"name": "ReDroid", "adb_path": "/usr/bin/adb", "address": "127.0.0.1:5555"}, + "resource": "官服", + "task": [] +} diff --git a/README.md b/README.md index 3f0d926..25dea33 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -## Lichx's Maa Like software config -目前包括明日方舟(ArknightsMAA) +# MAAsConfig(个人使用) -计划加入重返未来·1999(M9A) \ No newline at end of file +镜像构建时克隆此仓库,容器每次启动由 run_all.sh 拉取更新,再运行游戏脚本。修改配置和脚本后提交、推送,下轮容器启动就会取得更新。 + +- `ArknightsMaaConfig/maa.py`:保留 MAA 更新、ReDroid 启停、六步日常任务、失败重试和重开游戏。更新走代理,ADB 和游戏任务不走代理。 +- `ArknightsMaaConfig/tasks/`、`infrast/`:沿用原任务和基建配置,包括旧活动关卡和理智药策略。 +- `MaaEndConfig/maa.py`:运行官方 MaaPiCli,补齐当前发行包的 ADB 路径和界面适配。 +- `MaaEndConfig/maa_pi_config.json`:MaaEnd 任务选择,目前为空。配置后再启用 `GAMES="arknights maaend"`。 +- `run_all.sh`:更新 Git、按 GAMES 依次执行脚本,用 tee 同时输出和保存日志。 + +`MAA_LOG=info` 控制基本进度;日志目录是 `LOG_DIR`,不再把路径放进 MAA_LOG。默认由 MAAsDocker 的 Compose 提供这些环境变量。 diff --git a/run_all.sh b/run_all.sh index 8233208..3579ecf 100755 --- a/run_all.sh +++ b/run_all.sh @@ -1,12 +1,25 @@ #!/usr/bin/env bash - -SCRIPT_DIR=$(dirname $(readlink -f "$0")) -MAA_PY="$SCRIPT_DIR/ArknightsMaaConfig/maa.py" -MAA_LOG_DIR="${MAA_LOG:-/var/log/maa.log}" -cd $SCRIPT_DIR -http_proxy="" https_proxy="" git fetch origin && http_proxy="" https_proxy="" git reset --hard origin/master && http_proxy="" https_proxy="" git pull -if [ -x "$MAA_PY" ]; then - "$MAA_PY" > "$MAA_LOG_DIR/maa_pyshell.log" 2>&1 -else - python3 "$MAA_PY" > "$MAA_LOG_DIR/maa_pyshell.log" 2>&1 -fi \ No newline at end of file +set -euo pipefail +cd -- "$(dirname -- "$(readlink -f -- "$0")")" +case "${1:-}" in + ""|maaend-config) ;; + *) echo "未知参数:$1(仅支持无参数运行或 maaend-config)"; exit 2 ;; +esac +mkdir -p "${LOG_DIR:-/state/logs}" /state/tmp +{ + date + http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= git pull --ff-only || exit 1 + if [[ "${1:-}" == maaend-config ]]; then + python3 -u MaaEndConfig/maa.py configure + else + result=0 + for game in ${GAMES-arknights}; do + case "$game" in + arknights) python3 -u ArknightsMaaConfig/maa.py || result=1 ;; + maaend) python3 -u MaaEndConfig/maa.py || result=1 ;; + *) echo "未知游戏:$game"; result=1 ;; + esac + done + exit "$result" + fi +} 2>&1 | tee -a "${LOG_DIR:-/state/logs}/maa-$(date +%Y%m%d-%H%M%S).log" diff --git a/tests/test_maa.py b/tests/test_maa.py new file mode 100644 index 0000000..012934a --- /dev/null +++ b/tests/test_maa.py @@ -0,0 +1,41 @@ +import importlib.util +from pathlib import Path +import unittest +from unittest.mock import call, patch + +spec = importlib.util.spec_from_file_location("maa_script", Path(__file__).resolve().parents[1] / "ArknightsMaaConfig/maa.py") +maa = importlib.util.module_from_spec(spec) +spec.loader.exec_module(maa) + + +class MaaTest(unittest.TestCase): + def test_update_uses_proxy_but_game_commands_do_not(self): + with patch.object(maa.os, "environ", {"http_proxy": "http://proxy:7890"}), \ + patch.object(maa, "DIRECT", {"MAA_LOG": "info"}), \ + patch.object(maa.subprocess, "run") as run: + run.return_value.returncode = 0 + maa.command(["maa", "update"], proxy=True) + self.assertEqual(run.call_args.kwargs["env"]["http_proxy"], "http://proxy:7890") + maa.command(["maa", "run", "startup"]) + self.assertNotIn("http_proxy", run.call_args.kwargs["env"]) + + def test_failed_task_does_not_skip_other_tasks_or_shutdown(self): + with patch.object(maa, "command", return_value=0) as command, \ + patch.object(maa, "wait_android", return_value=True), \ + patch.object(maa, "retry", return_value=True), \ + patch.object(maa, "task", side_effect=[True, False, True, True, True, True]) as task: + self.assertEqual(maa.main(), 1) + self.assertEqual(task.call_args_list, [call(name) for name in maa.TASKS]) + self.assertEqual(command.call_args.args[0], ["docker", "stop", maa.REDROID]) + + def test_task_failure_reopens_game_then_retries(self): + with patch.object(maa, "retry", side_effect=[False, True, True, True]) as retry: + self.assertTrue(maa.task("fight")) + self.assertEqual(retry.call_args_list, [ + call(maa.maa("run", "fight")), call(maa.maa("closedown")), + call(maa.maa("run", "startup")), call(maa.maa("run", "fight")), + ]) + + +if __name__ == "__main__": + unittest.main()