Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 54 additions & 19 deletions app/MaaFW/ArknightWin32.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,38 @@

logger = get_logger("明日方舟PC工具")

# 定时任务每秒触发一次,连接失败后若不退避就会以秒级频率反复重试并刷屏;
# 上限取 60 秒,保证用户随时进入游戏后仍能在一分钟内自动接上。
CONNECT_RETRY_BASE_SECONDS = 2.0
CONNECT_RETRY_MAX_SECONDS = 60.0

class _ArknightWin32Toolkit:

def connect_retry_delay(failures: int) -> float:
"""
按连续失败次数计算下次重试前的等待秒数

Args:
failures: 已连续失败的次数, 首次失败传 1

Returns:
float: 等待秒数, 指数增长并封顶到 ``CONNECT_RETRY_MAX_SECONDS``
"""

return min(
CONNECT_RETRY_BASE_SECONDS * 2 ** max(failures - 1, 0),
CONNECT_RETRY_MAX_SECONDS,
)
Comment on lines +73 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 当连续失败次数达到 1025 次时,2 ** (failures - 1) 在与浮点数相乘时转换为 float,直接抛出 OverflowError,因此异常处理本身失败,second_task 终止且后续不再自动重连。60 秒封顶并不能避免这个问题,因为指数表达式是在 min 执行前计算的。

Triggers: 当明日方舟窗口持续无法连接约 17 小时以上,累计失败次数达到 1025 次。

Suggested fix: 在指数运算前先按封顶阈值截断失败次数,或在 failures 达到对应阈值时直接返回 CONNECT_RETRY_MAX_SECONDS

Suggested change
return min(
CONNECT_RETRY_BASE_SECONDS * 2 ** max(failures - 1, 0),
CONNECT_RETRY_MAX_SECONDS,
)
return min(
CONNECT_RETRY_BASE_SECONDS * 2 ** min(max(failures - 1, 0), 5),
CONNECT_RETRY_MAX_SECONDS,
)
Original comment in English

issue (bug_risk): 当连续失败次数达到 1025 次时,2 ** (failures - 1) 在与浮点数相乘时转换为 float,直接抛出 OverflowError,因此异常处理本身失败,second_task 终止且后续不再自动重连。60 秒封顶并不能避免这个问题,因为指数表达式是在 min 执行前计算的。

Triggers: 当明日方舟窗口持续无法连接约 17 小时以上,累计失败次数达到 1025 次。

Suggested fix: 在指数运算前先按封顶阈值截断失败次数,或在 failures 达到对应阈值时直接返回 CONNECT_RETRY_MAX_SECONDS

Suggested change
return min(
CONNECT_RETRY_BASE_SECONDS * 2 ** max(failures - 1, 0),
CONNECT_RETRY_MAX_SECONDS,
)
return min(
CONNECT_RETRY_BASE_SECONDS * 2 ** min(max(failures - 1, 0), 5),
CONNECT_RETRY_MAX_SECONDS,
)



class _ArknightWin32Toolkit:
def __init__(self):

self.arknights_hwnd = -1
self.arknights_window = None

self.connect_failures = 0
self.next_connect_attempt = 0.0

self.tasker = Tasker()
self.listener = keyboard.Listener()

Expand Down Expand Up @@ -101,8 +125,9 @@ async def scheduled_task(self) -> None:
new_hwnd = win32gui.FindWindow(None, "明日方舟")

if self.arknights_hwnd != new_hwnd:

self.arknights_hwnd = new_hwnd
# 窗口发生变化意味着新的连接机会,清掉上一轮的退避
self.reset_connect_backoff()

if new_hwnd == 0:
logger.warning("未检测到明日方舟窗口,暂停任务器")
Expand All @@ -112,9 +137,19 @@ async def scheduled_task(self) -> None:
else:
await self.connect_arknights()

if not self.get_connect_status() and self.arknights_hwnd > 0:
if (
not self.get_connect_status()
and self.arknights_hwnd > 0
and time.monotonic() >= self.next_connect_attempt
):
await self.connect_arknights()

def reset_connect_backoff(self) -> None:
"""清空连接失败计数与退避窗口"""

self.connect_failures = 0
self.next_connect_attempt = 0.0

def get_connect_status(self) -> bool:
"""获取连接状态"""

Expand Down Expand Up @@ -146,15 +181,24 @@ async def connect_arknights(self) -> None:
keyboard_method=MaaWin32InputMethodEnum.Seize,
)
logger.success("已连接到明日方舟")
self.reset_connect_backoff()
except Exception as e:
logger.error(f"连接明日方舟失败: {e}")
await Publisher.send(
id=protocol.ID_ARKNIGHTS_PC_TOOLKIT,
type=protocol.TOOLKIT_NOTICE,
data=WSTaskNoticeData(
level="error", message=f"无法连接明日方舟: {str(e)}"
),
self.connect_failures += 1
delay = connect_retry_delay(self.connect_failures)
self.next_connect_attempt = time.monotonic() + delay
logger.error(
f"连接明日方舟失败(第 {self.connect_failures} 次,"
f"{delay:.0f} 秒后重试): {e}"
)
# 仅首次失败提示用户,避免重试期间反复弹出同一条通知
if self.connect_failures == 1:
await Publisher.send(
id=protocol.ID_ARKNIGHTS_PC_TOOLKIT,
type=protocol.TOOLKIT_NOTICE,
data=WSTaskNoticeData(
level="error", message=f"无法连接明日方舟: {str(e)}"
),
)

def on_key_release(self, key: keyboard.Key | keyboard.KeyCode | None) -> None:
"""pynput 回调"""
Expand Down Expand Up @@ -232,7 +276,6 @@ def get_pause_position(self):

@MaaFWManager.resource.custom_action("PlaySelectDeployed[ArknightsPC]")
class PlaySelectDeployed(CustomAction):

def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

logger.info("开始执行战斗时选中已部署干员动作")
Expand All @@ -257,7 +300,6 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

@MaaFWManager.resource.custom_action("PauseSelectDeployed[ArknightsPC]")
class PauseSelectDeployed(CustomAction):

def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

logger.info("开始执行暂停时选中已部署干员动作")
Expand All @@ -284,7 +326,6 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

@MaaFWManager.resource.custom_action("PlaySkill[ArknightsPC]")
class PlaySkill(CustomAction):

def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

logger.info("开始执行战斗时释放技能动作")
Expand All @@ -306,7 +347,6 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

@MaaFWManager.resource.custom_action("PauseSkill[ArknightsPC]")
class PauseSkill(CustomAction):

def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

logger.info("开始执行暂停时释放技能动作")
Expand All @@ -332,7 +372,6 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

@MaaFWManager.resource.custom_action("PlayRetreat[ArknightsPC]")
class PlayRetreat(CustomAction):

def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

logger.info("开始执行战斗时撤退干员动作")
Expand All @@ -355,7 +394,6 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

@MaaFWManager.resource.custom_action("PauseRetreat[ArknightsPC]")
class PauseRetreat(CustomAction):

def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

logger.info("开始执行暂停时撤退干员动作")
Expand All @@ -381,7 +419,6 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

@MaaFWManager.resource.custom_action("NextFrame-0.2x[ArknightsPC]")
class NextFrame_0_2x(CustomAction):

def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

logger.info("开始执行0.2倍速下一帧动作")
Expand All @@ -406,7 +443,6 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

@MaaFWManager.resource.custom_action("NextFrame-1x[ArknightsPC]")
class NextFrame_1x(CustomAction):

def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

logger.info("开始执行1倍速下一帧动作")
Expand All @@ -431,7 +467,6 @@ def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

@MaaFWManager.resource.custom_action("NextFrame-2x[ArknightsPC]")
class NextFrame_2x(CustomAction):

def run(self, context: Context, argv: CustomAction.RunArg) -> bool:

logger.info("开始执行2倍速下一帧动作")
Expand Down
3 changes: 2 additions & 1 deletion res/version.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
],
"开发流程": [],
"修复BUG": [
"修复 Mirror 酱一次性下载地址被版本检查缓存复用导致更新包下载失败的问题 by [@qiyinxi](https://github.com/qiyinxi)"
"修复 Mirror 酱一次性下载地址被版本检查缓存复用导致更新包下载失败的问题 by [@qiyinxi](https://github.com/qiyinxi)",
"修复明日方舟PC工具连接失败后每秒重试并反复弹出错误提示的问题"
]
},
"v5.5.0-beta.1": {
Expand Down
29 changes: 29 additions & 0 deletions tests/task/test_arknights_connect_backoff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import unittest

from app.MaaFW.ArknightWin32 import (
CONNECT_RETRY_BASE_SECONDS,
CONNECT_RETRY_MAX_SECONDS,
connect_retry_delay,
)


class ConnectRetryDelayTest(unittest.TestCase):
def test_first_failure_waits_base_interval(self):
self.assertEqual(connect_retry_delay(1), CONNECT_RETRY_BASE_SECONDS)

def test_delay_grows_exponentially(self):
self.assertEqual(connect_retry_delay(2), CONNECT_RETRY_BASE_SECONDS * 2)
self.assertEqual(connect_retry_delay(3), CONNECT_RETRY_BASE_SECONDS * 4)

def test_delay_is_capped(self):
self.assertEqual(connect_retry_delay(100), CONNECT_RETRY_MAX_SECONDS)

def test_delay_never_below_base(self):
"""失败次数异常传 0 或负数时不应退化成 0 秒间隔。"""

self.assertEqual(connect_retry_delay(0), CONNECT_RETRY_BASE_SECONDS)
self.assertEqual(connect_retry_delay(-5), CONNECT_RETRY_BASE_SECONDS)


if __name__ == "__main__":
unittest.main()
Loading