Skip to content

fix: 修复多实例跨服切换时游戏进程未关闭及不必要的强制退出登录#2273

Open
pumpkinperson996 wants to merge 7 commits into
OneDragon-Anything:mainfrom
pumpkinperson996:fix/cross-server-close-game-and-skip-relogin
Open

fix: 修复多实例跨服切换时游戏进程未关闭及不必要的强制退出登录#2273
pumpkinperson996 wants to merge 7 commits into
OneDragon-Anything:mainfrom
pumpkinperson996:fix/cross-server-close-game-and-skip-relogin

Conversation

@pumpkinperson996
Copy link
Copy Markdown

@pumpkinperson996 pumpkinperson996 commented May 18, 2026

背景

多账号一条龙中,国服与国际服账号各只有一个时,切换服务器后存在两个问题。


问题一:切换到不同路径实例时旧游戏客户端未关闭

现象:国服账号运行完毕后,切换到国际服实例时旧国服客户端没有被关闭,导致国际服客户端无法启动(实例冲突),一条龙卡住。

根本原因:\ctx.switch_instance()\ 内部的 \on_switch_instance()\ 会修改 \controller.game_win.win_title\,而 _last_controller\ 和 \ctx.controller\ 指向同一对象。导致 \close_game()\ 节点用新服务器的窗口标题查找旧游戏窗口,找不到后误判为已关闭,跳过了关闭步骤。

修复:切换前保存旧游戏路径,\close_game()\ 改为通过 \ askkill /F /IM \ 按进程名强制关闭,绕开窗口标题问题。对 exe 名做合法性校验防止注入。


问题二:未配置账号密码时仍强制退出当前登录

现象:多实例模式下 \ orce_login\ 固定为 \True\,每次进入游戏都会强制点击「切换账号」退出当前登录,再用存储的账号密码重新登录。对于未在一条龙中配置账号密码、依赖游戏自动保存登录状态的实例,这一操作是多余的,且在国际服会触发人机验证(验证码),导致自动化流程中断

根本原因:\EnterGame.init()\ 中 \ orce_login\ 只检查实例数量,未区分「同服多账号(需退出重登)」和「未配置密码(依赖游戏保存登录状态)」两种情况。

修复:若当前实例的账号、密码、B服用户名均未配置,则将 \ orce_login\ 置为 \False\,直接利用游戏保存的登录状态进入。不影响已配置账号密码的用户。

Summary by CodeRabbit

发布说明

  • Bug修复
    • 实例切换优化:当当前与目标游戏路径不一致时记录状态并延迟切换,降低误切换风险。
    • 关闭流程改进:基于上次控制器状态执行关闭或重试,遇未就绪窗口时等待后再确认成功,提高可靠性。
    • 登录流程优化:在未配置账号信息且不开启切换时优先使用游戏内保存的登录状态直接进入,提升进入速度与体验。

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8691f31a-f6ff-426e-b48e-2d6b6bdbf1a9

📥 Commits

Reviewing files that changed from the base of the PR and between 07c0362 and 4f1bea2.

📒 Files selected for processing (2)
  • src/one_dragon/base/config/game_account_config.py
  • src/one_dragon/base/operation/one_dragon_app.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/one_dragon/base/operation/one_dragon_app.py

📝 Walkthrough

Walkthrough

修改实例切换以先比较 game_path 并在路径不同情况下缓存上一个 controller;close_game 基于缓存的 _last_controller 进行关闭重试或等待;after_close_game 在重开前切回目标实例并记录日志;EnterGame 根据账号配置决定是否强制登录。

变更

实例切换与登录行为调整

层级 / 文件 概述
GameAccountConfig: game_path 比较方法与导入清理
src/one_dragon/base/config/game_account_config.py
移除 typing.Optional 导入;新增 @classmethod is_different_game_path(current_idx, next_idx) -> bool,比较两个实例的 game_path 是否同时存在且不相同。
移除旧的上次账号字段声明
src/one_dragon/base/operation/one_dragon_app.py
OneDragonApp.__init__ 中移除了 _last_account_config 实例字段。
实例切换:基于 game_path 的分支
src/one_dragon/base/operation/one_dragon_app.py
switch_instance 在切换前比较当前与下一实例的 game_path;若两者均存在且不同,缓存当前 ctx.controller_last_controller、更新 _instance_idx 并返回状态 游戏路径不同;若相同则执行 ctx.switch_instance(next_instance.idx) 并返回成功。
关闭流程:仅基于 _last_controller
src/one_dragon/base/operation/one_dragon_app.py
close_game 仅依据 _last_controller:为 None 时直接成功;窗口就绪时调用 _last_controller.close_game() 并返回重试结果;否则等待约 10 秒后返回成功。
重开前切换与日志
src/one_dragon/base/operation/one_dragon_app.py
after_close_game 在重新打开游戏前执行 ctx.switch_instance 到当前 _instance_idx 并记录日志;若回到起始索引则直接成功;否则在 op_to_enter_game 不为空时执行打开游戏操作(移除原有 else 分支)。
EnterGame: 根据配置调整 force_login
src/zzz_od/operation/enter_game/enter_game.py
EnterGame.__init__ 中,当 switchFalse 且未配置 accountpasswordbilibili_account_name 时,将 self.force_login 设为 False,以依赖游戏已保存的登录状态直接进入。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

可能相关的 PR

建议审核人

  • DoctorReid

小兔跳过旧账号,轻声来报晓,
路径先比对,控制器暂存好。
关闭再重开,日志轻轻敲,
若无凭证登录,游戏自会到。 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了本 PR 的核心修复内容:多实例跨服切换时游戏进程关闭问题和不必要的强制登录问题。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/one_dragon/base/operation/one_dragon_app.py (1)

121-130: 💤 Low value

建议记录 taskkill 的 stderr 以便调试。

returncode != 0 时,除了"进程未找到",还可能是"拒绝访问"等其他错误。记录 stderr 有助于排查问题。

♻️ 可选的改进方案
             result = subprocess.run(
                 ['taskkill', '/F', '/IM', exe_name],
                 capture_output=True,
             )
             if result.returncode == 0:
                 log.info('关闭游戏成功: %s', exe_name)
                 log.info('等待游戏占用文件释放(10s)...')
                 return self.round_success(wait=10)
-            log.info('未找到游戏进程 %s,可能已关闭', exe_name)
+            stderr_msg = result.stderr.decode('gbk', errors='ignore').strip()
+            if stderr_msg:
+                log.debug('taskkill stderr: %s', stderr_msg)
+            log.info('未找到游戏进程 %s,可能已关闭', exe_name)
             return self.round_success()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/one_dragon/base/operation/one_dragon_app.py` around lines 121 - 130, The
taskkill subprocess call in the subprocess.run block (variable result) doesn't
log stderr when returncode != 0, making failures like "access denied" hard to
debug; update the handling around the subprocess.run call in one_dragon_app.py
(the block that uses exe_name, result and calls self.round_success()) to capture
and log result.stderr (decode bytes safely or use text=True on subprocess.run)
and include it in the log (e.g., log.warning or log.error) when
result.returncode != 0 so you record the underlying error message while keeping
the existing "process not found" message path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/one_dragon/base/operation/one_dragon_app.py`:
- Around line 121-130: The taskkill subprocess call in the subprocess.run block
(variable result) doesn't log stderr when returncode != 0, making failures like
"access denied" hard to debug; update the handling around the subprocess.run
call in one_dragon_app.py (the block that uses exe_name, result and calls
self.round_success()) to capture and log result.stderr (decode bytes safely or
use text=True on subprocess.run) and include it in the log (e.g., log.warning or
log.error) when result.returncode != 0 so you record the underlying error
message while keeping the existing "process not found" message path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 84a0a0c3-0194-47c8-b1c5-03659f7c2342

📥 Commits

Reviewing files that changed from the base of the PR and between fdc90b6 and a580d80.

📒 Files selected for processing (2)
  • src/one_dragon/base/operation/one_dragon_app.py
  • src/zzz_od/operation/enter_game/enter_game.py

@pumpkinperson996
Copy link
Copy Markdown
Author

自审 PASS

本地测试通过,场景:国服实例运行完毕后切换至国际服实例(两者游戏路径不同,均未在一条龙中配置账号密码)。

修复一验证:检测到路径不同后,旧国服进程被正确关闭,国际服客户端正常启动,无实例冲突。

修复二验证:进入国际服游戏时未触发退出登录操作,直接点击进入游戏,无账号密码输入步骤。

运行记录(关键片段):

[23:38:51] one_dragon_app.py  下一个实例 american
[23:38:51] operation.py       指令[ 一条龙 ] 节点 运行应用组 -> 切换实例配置 返回状态 游戏路径不同
[23:38:51] one_dragon_app.py  关闭游戏成功: ZenlessZoneZero.exe
[23:38:51] one_dragon_app.py  等待游戏占用文件释放(10s)...
[23:39:01] operation.py       指令[ 一条龙 ] 节点 切换实例配置 -> 关闭游戏 返回状态 成功
[23:39:02] open_game.py       尝试自动启动游戏 路径为 C:\Program Files\HoYoPlay\games\ZenlessZoneZero Game\ZenlessZoneZero.exe
[23:39:33] operation.py       指令[ 进入游戏 ] 节点 检测游戏窗口 -> 画面识别 返回状态 点击进入游戏
[23:39:49] operation.py       指令[ 进入游戏 ] 节点 画面识别 -> 进入游戏后操作 返回状态 大世界
[23:39:49] operation.py       指令[ 进入游戏 ] 执行成功 返回状态 大世界
[23:39:49] operation.py       指令[ 一条龙 ] 节点 关闭游戏 -> 切换账号重新打开游戏 返回状态 大世界

Copy link
Copy Markdown
Collaborator

@ShadowLemoon ShadowLemoon left a comment

Choose a reason for hiding this comment

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

建议不要在 OneDragonApp 中引入 subprocess/taskkill。#1380 的问题根源是关闭游戏发生在 ctx.switch_instance() 之后,controller 的窗口标题已经被 on_switch_instance() 改成新实例,导致 _last_controller 不能稳定代表旧窗口。

更合适的修复是调整流程:先计算 next_instance_idx 并比较路径;如果路径不同,在调用 ctx.switch_instance(next_idx) 之前使用当前 ctx.controller.close_game() 关闭旧游戏;确认关闭后再切换实例并重新打开游戏。这样 OneDragonApp 仍只做流程编排,关闭细节继续走 ControllerBase.close_game() 抽象,也避免 taskkill /IM 按进程名误杀同名客户端。

切换到不同游戏路径的实例前,先通过当前 controller(窗口标题尚未改变)
关闭旧游戏,确认关闭后再调用 ctx.switch_instance()。

移除 subprocess/taskkill 方案,改回走 ControllerBase.close_game() 抽象。
@pumpkinperson996
Copy link
Copy Markdown
Author

已按照建议重构:在 switch_instance 节点中提前读取下一个实例的游戏路径,路径不同时在调用 ctx.switch_instance() 之前先通过当前 controller.close_game() 关闭旧游戏,确认关闭后再在 after_close_game 节点中切换实例。移除了 subprocess/taskkill 方案。

@ShadowLemoon
Copy link
Copy Markdown
Collaborator

这里 force_login 的调整方向我理解是对的:PR 描述里的场景是跨服/不同路径重开游戏时,用户没有在一条龙里配置账号密码,希望直接复用游戏保存的登录状态,所以不应该因为多实例模式去点击「切换账号」。

不过这里建议不要覆盖 switch=True 的语义。switch=True 只在 SwitchAccount 里使用,前置流程已经明确执行了游戏内登出,属于“正在切换账号”的流程;这时如果因为账号密码为空把 force_login 改回 False,可能会变成已经登出后仍尝试直接点击进入游戏,导致进错保存账号或卡在登录页。

建议改成只在普通进入游戏流程里跳过强制切号:

if not switch and not cfg.account and not cfg.password and not cfg.bilibili_account_name:
    self.force_login = False

@pumpkinperson996
Copy link
Copy Markdown
Author

已修复:加上 not switch 条件,switch=True 时不再覆盖 force_login,避免前置流程已执行登出后仍尝试直接进入游戏。

@ShadowLemoon
Copy link
Copy Markdown
Collaborator

还有一个边界场景需要处理:最后一个实例跑完后,next_idx 会回到 _instance_start_idx,切回起始实例配置是合理的,但不应该再把起始实例的游戏重新打开。

按当前流程,如果最后一个实例切回第一个实例且两者 game_path 不同,会走:

switch_instance: _instance_idx = 0,返回 游戏路径不同
close_game: 关闭最后一个实例的游戏
after_close_game: ctx.switch_instance(第一个实例) 后继续执行 op_to_enter_game
after_switch_account: 才判断 _instance_idx == _instance_start_idx,返回 全部结束

这样会导致流程结束前额外打开第一个实例的游戏。建议在 after_close_game() 中切回实例后,先判断是否已经回到起始实例;如果是,直接返回成功,让后续 after_switch_account() 返回 全部结束,不要再执行 op_to_enter_game

@node_from(from_name='关闭游戏')
@operation_node(name='切换账号重新打开游戏', screenshot_before_round=False)
def after_close_game(self) -> OperationRoundResult:
    self.ctx.switch_instance(self._instance_list[self._instance_idx].idx)
    log.info('下一个实例 %s', self.ctx.one_dragon_config.current_active_instance.name)

    if self._instance_idx == self._instance_start_idx:
        return self.round_success()

    if self.op_to_enter_game is None:
        return self.round_fail('未提供打开游戏方式')
    return self.round_by_op_result(self.op_to_enter_game.execute())

这样最后一个实例结束时仍会恢复到起始实例配置,但不会多启动一次游戏。

@pumpkinperson996
Copy link
Copy Markdown
Author

已修复:在 after_close_game 切换实例后加一行判断,_instance_idx == _instance_start_idx 时直接 return self.round_success(),跳过 op_to_enter_game,让 after_switch_account 正常返回全部结束。

self.ctx.switch_instance(self._instance_list[self._instance_idx].idx)
log.info('下一个实例 %s', self.ctx.one_dragon_config.current_active_instance.name)
current_game_path = self.ctx.game_account_config.game_path
next_game_path = GameAccountConfig(next_instance.idx).game_path
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这里不应该实例化,走上下文调用

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

好像调用也不太合适

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

补充一下:更建议不要把这个判断放到 ctx,也不要做成 GameAccountConfig 实例方法里再实例化另一个 GameAccountConfig。可以在 GameAccountConfig 上提供类方法,比如 GameAccountConfig.is_different_game_path(current_idx, next_idx),由配置类集中读取两个实例的 game_path 并比较。这样 OneDragonApp 只做流程编排,不直接实例化目标实例配置,也不需要提前 ctx.switch_instance() 触发 reload/on_switch_instance 的副作用。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

已按补充建议调整并推送到本 PR:

  • GameAccountConfig 上新增 is_different_game_path(current_idx, next_idx) 类方法,由配置类集中读取两个实例的 game_path 并比较。
  • OneDragonApp.switch_instance() 现在只传当前/下一个实例 idx 并调用该类方法,不再直接实例化目标实例配置,也不需要提前 ctx.switch_instance()

对应提交:4f1bea2

current_instance = self._instance_list[self._instance_idx]
next_instance = self._instance_list[next_idx]

if GameAccountConfig.is_different_game_path(current_instance.idx, next_instance.idx):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

走上下文调用已有实例 不需要导入用类了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants