保留 Git 自动更新并简化 MAA 任务流程,加入 MaaEnd

This commit is contained in:
2026-11-01 22:41:35 +08:00
parent 8cf86bc408
commit 2527560772
7 changed files with 211 additions and 145 deletions
+1
View File
@@ -1 +1,2 @@
*.log *.log
__pycache__/
+74 -128
View File
@@ -1,139 +1,85 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import subprocess
import sys import sys
import asyncio
import re
import time import time
from datetime import datetime from datetime import datetime
# time record
def fprint(s:str)->None:
print(s)
sys.stdout.flush()
fprint("-------------------------------------------------------") ADDRESS = os.getenv("ADB_ADDRESS", "127.0.0.1:5555")
fprint(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) REDROID = os.getenv("REDROID_CONTAINER", "redroid")
fprint("-------------------------------------------------------") TASKS = ["startup", "recruit", "infrast", "fight", "mall", "award"]
time.sleep(1) DIRECT = {k: v for k, v in os.environ.items() if k.lower() not in ("http_proxy", "https_proxy", "all_proxy")}
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
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
# cp.Command("command") def retry(args):
# cp.RetryCommand("command",retry_time:int) for attempt in range(3):
# asyncio.run(cp.AsyncCommand("command","stop track regex")) if command(args) == 0:
# script_dir = os.path.dirname((os.path.abspath(__file__))) return True
cp = CommandProcessor() print(f"命令失败,第 {attempt + 1}/3 次", flush=True)
haveError = False time.sleep(1)
def TaskFailRetry(command:str)->int: return False
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 TaskFailRetryWithNoProxy(command:str)->int:
retry = 3 def maa(*args):
result = 0 return ["maa", *args, "--batch", "-a", ADDRESS]
while retry > 0:
retry -= 1
result = cp.RetryCommandWithNoProxy(command) def wait_android():
if result == 0: print("等待 Android 启动……", flush=True)
return 0 deadline = time.monotonic() + 180
fprint(f"Task Command is {command}, ret code is {result}") while time.monotonic() < deadline:
fprint("Task Failed, retrying...") try:
cp.RetryCommand("maa closedown") subprocess.run(["adb", "connect", ADDRESS], env=DIRECT, capture_output=True, timeout=10)
cp.RetryCommand("maa run startup") result = subprocess.run(["adb", "-s", ADDRESS, "shell", "getprop", "sys.boot_completed"],
return result env=DIRECT, capture_output=True, text=True, timeout=10)
# update if result.returncode == 0 and result.stdout.strip() == "1":
cp.Command("maa update") return True
cp.Command("maa self update") except subprocess.TimeoutExpired:
# fire it up pass
cp.Command("docker stop redroid") time.sleep(2)
redroid_path = os.getenv('redroid_path', "/home/lichx/.config/redroid-rk3588/") print("Android 启动超时", flush=True)
adb_address = os.getenv('adb_address', "localhost:5555") return False
log_dir = os.getenv('MAA_LOG', "/home/lichx/.config/redroid-rk3588/log/")
os.chdir(redroid_path)
cp.Command("docker compose up -d") def task(name):
time.sleep(15) for _ in range(3):
cp.RetryCommand(f"adb connect {adb_address}") if retry(maa("run", name)):
cp.RetryCommand(f"adb -s {adb_address} shell am start --windowingMode 4 com.hypergryph.arknights/com.u8.sdk.U8UnityContext") return True
# run maa print(f"任务 {name} 失败,重新打开游戏后重试", flush=True)
haveError |= TaskFailRetryWithNoProxy("maa run startup") != 0 retry(maa("closedown"))
haveError |= TaskFailRetryWithNoProxy("maa run recruit") != 0 retry(maa("run", "startup"))
haveError |= TaskFailRetryWithNoProxy("maa run infrast") != 0 return False
haveError |= TaskFailRetryWithNoProxy("maa run fight") != 0
haveError |= TaskFailRetryWithNoProxy("maa run mall") != 0
haveError |= TaskFailRetryWithNoProxy("maa run award") != 0 def main():
# storage error log # 更新走代理;失败时仍尝试使用已有版本,与原脚本一致。
if haveError: command(["maa", "update"], proxy=True)
cp.Command(f"cp {log_dir}/maa_pyshell.log {log_dir}/maa_pyshell.{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log") command(["maa", "self", "update"], proxy=True)
# shutdown if command(["docker", "restart", REDROID]):
cp.RetryCommand("maa closedown") return 1
cp.Command("docker stop redroid") 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())
+52
View File
@@ -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())
+6
View File
@@ -0,0 +1,6 @@
{
"controller": {"name": "ADB"},
"adb": {"name": "ReDroid", "adb_path": "/usr/bin/adb", "address": "127.0.0.1:5555"},
"resource": "官服",
"task": []
}
+10 -3
View File
@@ -1,4 +1,11 @@
## Lichx's Maa Like software config # MAAsConfig(个人使用)
目前包括明日方舟(ArknightsMAA)
计划加入重返未来·1999(M9A) 镜像构建时克隆此仓库,容器每次启动由 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 提供这些环境变量。
+24 -11
View File
@@ -1,12 +1,25 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(dirname $(readlink -f "$0")) cd -- "$(dirname -- "$(readlink -f -- "$0")")"
MAA_PY="$SCRIPT_DIR/ArknightsMaaConfig/maa.py" case "${1:-}" in
MAA_LOG_DIR="${MAA_LOG:-/var/log/maa.log}" ""|maaend-config) ;;
cd $SCRIPT_DIR *) echo "未知参数:$1(仅支持无参数运行或 maaend-config)"; exit 2 ;;
http_proxy="" https_proxy="" git fetch origin && http_proxy="" https_proxy="" git reset --hard origin/master && http_proxy="" https_proxy="" git pull esac
if [ -x "$MAA_PY" ]; then mkdir -p "${LOG_DIR:-/state/logs}" /state/tmp
"$MAA_PY" > "$MAA_LOG_DIR/maa_pyshell.log" 2>&1 {
else date
python3 "$MAA_PY" > "$MAA_LOG_DIR/maa_pyshell.log" 2>&1 http_proxy= https_proxy= HTTP_PROXY= HTTPS_PROXY= git pull --ff-only || exit 1
fi 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"
+41
View File
@@ -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()