Repository navigation
desktop - #1887
desktop#1887
Conversation
- 新增 DesktopWindow mixin(module/device/method/desktop_impl.py),集中承载桌面 客户端的窗口枚举/绑定/截图/输入实现,Handle 与 Window 各继承一份 - 桌面分支按 PID 定位窗口;后台截图走 GetWindowDC+BitBlt,输入走 PostMessage/SendMessage - 新增 dev_tools/bind_desktop_window.py,一键绑定或按序号选择客户端窗口 PID - connection_attr 新增 is_desktop(serial == 'desktop'),桌面模式跳过 adb server 拉起 - cBezier 抽出 human_track:桌面与模拟器两条滑动路径共用同一套拟人轨迹, 顺带修掉纯垂直/零长度滑动原本会抛的 ValueError 与 ZeroDivisionError
- connection:跳过 detect_device(包名取配置),方向恒为 0,不走 adb dumpsys - screenshot:桌面模式放宽截图间隔;分支判断前移到夹取之前,避免按错误区间改写用户配置 - adb:swipe_adb 改走窗口消息 - platform_windows:emulator_stop 改关客户端 - app_control:app_is_alive / app_is_running 改判窗口存在性(pidof 在桌面模式必然失败)
- atom/image.py:ROI_MATCH_PAD / DESKTOP_ROI_MATCH_PAD 与 set_roi_match_padding() - 本仓库模板匹配在本地同步执行,外扩因此落在 RuleImage._pad_roi(corp 与 sift_match 共用同一条裁剪路径),不再经图像服务下发 - match / match_multi_scale / match_all / match_all_any 的命中回算改为以外扩后 的原点做基准;左上界夹 0,避免 numpy 负索引按“倒数”取值算错裁剪位置 - 模拟器模式保持 0,行为与改动前逐位一致
- 新增 DesktopDevice(与 DesktopWindow 对称):_desktop_ensure_launched 先看进程 再看窗口,避免把“窗口被最小化”当成“客户端未运行”而反复拉起并覆盖 handle - device.py 接入:__init__ 桌面分支、screenshot 窗口守卫、app_start/app_stop - 顺带修两处漂移:__init__ 重试次数还原基线语义;is_desktop 不再静默兜底成 False
- 登录循环每轮及截图后各查一次弹窗 HWND(防竞态),回车关闭并确认消失; 重弹超过 5 次抛 GameStuckError 交 Restart 重建客户端 - 本仓库登录实现在 tasks/Restart/login.py 的 LoginHandler,落点相应改到 _app_handle_login 主循环与 app_handle_login 外层 - 登录成功后标记登录态;close_unknown_pages 改调设备层 app_lost_login(), 任务层不再出现 is_desktop 与 MPay 字样
- app_start / app_restart 补桌面分支:不再强杀健康客户端 - 本仓库 ScriptTask 直接继承 LoginHandler,且 app_restart 末尾还有调度收尾, 故桌面分支只替换「启动」那一段、不提前 return - 登录流程自身的失败重试已由 app_handle_login 内部完成(它会关掉客户端再重拉), _desktop_start_and_login 只补「客户端窗口丢失」这一类重拉
- config_model:桌面模式下 handle 变“已开客户端窗口”下拉;回传展示串落盘前剥回纯 PID - 新增 desktop_game_path 配置项(留空自动发现客户端安装目录) - i18n 增加桌面模式说明(挂在 desktop_game_path 下)
Auto-launched desktop client stays at login page (_desktop_login_done=False), so force Restart to login first instead of running gameplay on a not-ready game.
There was a problem hiding this comment.
你好——我发现了 1 个安全问题,以及另外 10 个问题
安全问题:
- 检测到子进程函数
Popen使用了非静态字符串。如果此数据可由恶意行为者控制,则可能构成命令注入。请审查此调用的使用方式,确保其不可由外部资源控制。你可以考虑使用shlex.escape()。(链接)
面向 AI Agent 的提示
请处理此代码审查中的评论:
## 单独评论
### 评论 1
<location path="module/device/method/desktop_impl.py" line_range="690" />
<code_context>
+ logger.info(f'自动启动桌面客户端(第 {attempt} 轮): {exe}')
+ spawned = set()
+ try:
+ proc = subprocess.Popen([exe], cwd=root,
+ creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0))
+ spawned.add(proc.pid)
</code_context>
<issue_to_address>
**配置的路径会执行远程二进制文件**
当未经身份验证的网络行为者可以访问 Web 服务,将 `serial` 设置为 `desktop`,并将 `desktop_game_path` 设置为攻击者控制的、包含 `bin/onmyoji.exe` 的 UNC 共享,然后启动脚本时,`desktop_resolve_install_root` 会在配置目录包含 `bin/onmyoji.exe` 时接受该目录,而 `launch_desktop_client` 会将该可执行文件直接传递给 `subprocess.Popen`;因此,攻击者控制的 SMB 共享可以在启动桌面客户端时以 OAS 用户身份执行代码。
请将 `desktop_game_path` 限制为经过验证的本地客户端安装目录,并要求对配置更改和脚本启动操作进行身份验证。
另见 `tasks/Script/config_device.py:59`。
</issue_to_address>
### 评论 2
<location path="module/device/method/windows_impl.py" line_range="108" />
<code_context>
+ 'Desktop client window is minimized, screenshot unavailable. '
+ '请恢复游戏窗口(可被其他窗口遮挡,但不能最小化)'
+ )
+ hwndDc = GetDC(self.screenshot_handle_num)
+ mfcDc = CreateDCFromHandle(hwndDc)
+ saveDc = mfcDc.CreateCompatibleDC()
</code_context>
<issue_to_address>
**重复截图会耗尽 GDI 资源**
反复获取桌面截图时,`GetDC` 会获取窗口 DC,但 `mfcDc.DeleteDC()` 不会通过 `ReleaseDC` 释放它;当位图仍被选入 `saveDc` 时,`DeleteObject` 也会失败。未释放的句柄会不断累积,直到桌面捕获或其他窗口操作失败。
请恢复被选中的位图,使用 `ReleaseDC` 释放窗口 DC,并在每条截图路径上清理剩余的 GDI 对象。
另见 `module/device/method/windows_impl.py:119-121`。
</issue_to_address>
### 评论 3
<location path="module/device/method/desktop_impl.py" line_range="159" />
<code_context>
+ """返回进程 exe 完整路径;打开/查询失败(已退出、权限)返回空串。"""
+ kernel32 = ctypes.windll.kernel32
+ # PROCESS_QUERY_LIMITED_INFORMATION
+ handle = kernel32.OpenProcess(0x1000, False, int(pid))
+ if not handle:
+ return ''
</code_context>
<issue_to_address>
**进程句柄被截断**
当 64 位 Windows 返回高位非零的 `OpenProcess` 句柄时,`ctypes` 会将 `OpenProcess` 的返回值视为 `c_int`,从而截断指针大小的句柄;后续查询、关闭或终止调用会使用无效句柄,导致进程检测或清理失败。
请为 `OpenProcess` 以及使用其句柄的 API 声明指针大小的参数和返回类型。
另见 `module/device/method/desktop_impl.py:740`、`module/device/method/desktop_impl.py:811`、`module/device/method/desktop_impl.py:957`。
</issue_to_address>
### 评论 4
<location path="module/device/desktop_device.py" line_range="23-24" />
<code_context>
+ 原来那个。窗口被最小化不算没运行,截图方法会先还原再取帧。
+ """
+ pid = self.desktop_pid()
+ if pid is None or not self._desktop_pid_alive(pid):
+ return self.launch_desktop_client()
+ if self.desktop_window_exists():
+ return True
</code_context>
<issue_to_address>
**现有客户端会被启动两次**
当配置的客户端正在运行,但 OAS 没有权限查询其进程时,`OpenProcess` 被拒绝会使 `_desktop_pid_alive` 返回 `False`,而 `_desktop_ensure_launched` 在未先检查配置 PID 对应窗口的情况下启动另一个客户端。这样会产生重复客户端,并且配置的 PID 可能被替换。
请区分访问被拒绝和进程已退出的情况,并在启动另一个客户端前检查配置 PID 对应的窗口。
另见 `module/device/method/desktop_impl.py:811-813`。
</issue_to_address>
### 评论 5
<location path="module/device/desktop_device.py" line_range="23-24" />
<code_context>
+ 原来那个。窗口被最小化不算没运行,截图方法会先还原再取帧。
+ """
+ pid = self.desktop_pid()
+ if pid is None or not self._desktop_pid_alive(pid):
+ return self.launch_desktop_client()
+ if self.desktop_window_exists():
+ return True
</code_context>
<issue_to_address>
**复用的 PID 会阻止启动**
当 Windows 将保存的客户端 PID 重新分配给一个无关但仍在运行的进程时,`_desktop_ensure_launched` 会将该无关进程视为客户端,并等待其桌面窗口,而不是启动游戏,导致设备初始化超时。
请在将保存的 PID 视为存活之前,验证其确实属于桌面客户端。
另见 `module/device/method/desktop_impl.py:811-820`。
</issue_to_address>
### 评论 6
<location path="module/device/device.py" line_range="123" />
<code_context>
+ # 桌面模式:窗口缺失说明客户端当前不可用(启动期未出窗口,或客户端崩了)。
+ # 直接截图会抛 (1400, '无效的窗口句柄') 搞崩整个进程,必须先拦下;拉客户端
+ # 是 Restart 的职责。最小化不算窗口缺失——BitBlt 截图前会先还原窗口。
+ if self.is_desktop and not self.desktop_window_exists():
+ raise GameNotRunningError('Desktop client window not found')
+
</code_context>
<issue_to_address>
**窗口重建后会留下过期句柄**
当客户端在登录弹窗关闭后销毁并重新创建主窗口时,`desktop_window_exists` 会找到该 PID 的窗口,但不会刷新 `root_handle_num` 或缓存的截图句柄。随后 `screenshot_desktop_bitblt` 使用已销毁的 HWND 调用 `GetClientRect`,导致任务失败。
当客户端主窗口发生变化时,请重新绑定窗口,并使缓存的截图句柄失效或刷新该句柄。
另见 `module/device/method/windows_impl.py:99`。
</issue_to_address>
### 评论 7
<location path="module/device/method/desktop_impl.py" line_range="704-707" />
<code_context>
+ # 候选必须落在安装目录下的进程:客户端新旧版本窗口标题不同
+ # ("阴阳师-网易游戏" / "阴阳师-MuMu模拟器专版"),启动等待期内任何
+ # 匹配标题的新窗口都会成为候选,按 exe 归属收口,避免绑到别的进程
+ candidates = [w for w in list_desktop_windows()
+ if w['pid'] not in before_pids
+ and _desktop_pid_under_root(w['pid'], root)]
+ if candidates:
+ candidate = candidates[0]
+ if candidate['hwnd'] == stable_hwnd:
</code_context>
<issue_to_address>
**并发启动会共享同一个窗口**
当两个配置同时启动同一个桌面安装时,两个启动尝试都可能在任何新窗口出现之前获取 `before_pids` 快照;随后它们各自接受安装目录下枚举到的任意新窗口,而不只接受由自己启动的进程。两者可能绑定同一个候选 PID,因此停止一个客户端也会终止另一个配置的客户端。
请串行化桌面启动操作,或将候选窗口限制为已确认属于当前启动尝试的进程。
</issue_to_address>
### 评论 8
<location path="module/device/method/desktop_impl.py" line_range="586" />
<code_context>
+ else:
+ path = path.parent
+ candidate = path / 'bin' / 'onmyoji.exe'
+ if candidate.is_file():
+ return path.resolve()
+ return None
</code_context>
<issue_to_address>
**支持的安装路径会被拒绝**
当配置的安装目录包含 `Launch.exe` 但不包含 `bin/onmyoji.exe` 时,`_desktop_root_from_path` 因为只验证 `bin/onmyoji.exe` 而拒绝该安装目录,因此 `desktop_game_exe` 永远无法使用其 `Launch.exe` 回退逻辑,启动失败。
请接受包含任一受支持可执行文件的安装根目录。
</issue_to_address>
### 评论 9
<location path="module/device/method/desktop_impl.py" line_range="89-97" />
<code_context>
+ user32 = ctypes.windll.user32
+ previous = None
+ try:
+ previous = user32.SetThreadDpiAwarenessContext(_PER_MONITOR_AWARE_V2)
+ except AttributeError:
+ # Windows 10 1607 以前没有该 API,退化为原有逻辑像素行为
+ pass
+ try:
+ yield
+ finally:
+ if previous:
+ user32.SetThreadDpiAwarenessContext(previous)
+
+
</code_context>
<issue_to_address>
**DPI 上下文未恢复**
当代码运行在 64 位 Windows 上并恢复之前的 DPI 上下文时,`ctypes` 会将未声明的函数返回类型默认为 `c_int`,但 `SetThreadDpiAwarenessContext` 返回的是指针大小的上下文句柄。在 64 位 Windows 上,`previous` 会被截断,恢复调用接收到无效上下文,导致上下文管理器退出后工作线程仍处于 Per-Monitor V2 模式;后续 DPI 虚拟化操作可能使用错误的坐标。
请在调用前声明 `SetThreadDpiAwarenessContext.restype = wintypes.HANDLE`,并将其参数类型声明为 `wintypes.HANDLE`。
</issue_to_address>
### 评论 10
<location path="module/device/method/desktop_impl.py" line_range="127" />
<code_context>
+ user32 = ctypes.windll.user32
+ user32.keybd_event(0x7E, 0, 0, 0)
+ user32.keybd_event(0x7E, 0, 2, 0)
+ user32.SetCursorPos(400, 300)
+ except Exception as e:
+ logger.warning(f'wake input failed: {e}')
</code_context>
<issue_to_address>
**启动会移动用户的光标**
自动启动桌面客户端时,`desktop_keep_screen_on(True)` 会无条件将系统光标移动到 `(400, 300)`,且从不恢复之前的位置。由于该函数在每次自动启动成功后都会调用,即使显示器本来就是唤醒状态,用户的光标也会跳动。
请在唤醒输入前后保存并恢复光标位置,或使用不会重新定位用户光标的唤醒机制。
</issue_to_address>
### 评论 11
<location path="module/device/method/desktop_impl.py" line_range="690-691" />
<code_context>
proc = subprocess.Popen([exe], cwd=root,
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0))
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** 检测到子进程函数 `Popen` 使用了非静态字符串。如果此数据可由恶意行为者控制,则可能构成命令注入。请审查此调用的使用方式,确保其不可由外部资源控制。你可以考虑使用 `shlex.escape()`。
*来源:opengrep*
</issue_to_address>Sourcery 评估
需要人工审查。 有 11 个发现需要优先处理;此外,本次变更新增了桌面客户端生命周期管理、自动启动、PID/HWND 绑定、强制终止进程和后台输入,因此错误的窗口匹配可能控制或终止错误的进程,游戏操作也无法通过回退此 PR 撤销。持久化配置可以被更正,未来行为也可以通过回退停止,但已经执行的终止或操作将无法恢复。
阻塞性发现:module/device/method/desktop_impl.py:690、module/device/method/windows_impl.py:108、module/device/method/desktop_impl.py:159、module/device/desktop_device.py:24、module/device/desktop_device.py:24,以及另外 6 项
Original comment in English
Hey - I've found 1 security issue, and 10 other issues
Security issues:
- Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="module/device/method/desktop_impl.py" line_range="690" />
<code_context>
+ logger.info(f'自动启动桌面客户端(第 {attempt} 轮): {exe}')
+ spawned = set()
+ try:
+ proc = subprocess.Popen([exe], cwd=root,
+ creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0))
+ spawned.add(proc.pid)
</code_context>
<issue_to_address>
**Configured path executes remote binary**
When an unauthenticated network actor can reach the web service, sets `serial` to `desktop` and `desktop_game_path` to an attacker-controlled UNC share containing `bin/onmyoji.exe`, then starts the script, `desktop_resolve_install_root` accepts the configured directory when it contains `bin/onmyoji.exe`, and `launch_desktop_client` passes that executable directly to `subprocess.Popen`; an attacker-controlled SMB share can therefore execute code as the OAS user when the desktop client is started.
Restrict `desktop_game_path` to a verified local client installation and require authentication for configuration changes and script-start actions.
Also at `tasks/Script/config_device.py:59`.
</issue_to_address>
### Comment 2
<location path="module/device/method/windows_impl.py" line_range="108" />
<code_context>
+ 'Desktop client window is minimized, screenshot unavailable. '
+ '请恢复游戏窗口(可被其他窗口遮挡,但不能最小化)'
+ )
+ hwndDc = GetDC(self.screenshot_handle_num)
+ mfcDc = CreateDCFromHandle(hwndDc)
+ saveDc = mfcDc.CreateCompatibleDC()
</code_context>
<issue_to_address>
**Repeated captures exhaust GDI resources**
When desktop screenshots are taken repeatedly, `GetDC` obtains a window DC, but `mfcDc.DeleteDC()` does not release it with `ReleaseDC`; `DeleteObject` also fails while the bitmap remains selected into `saveDc`. The unreleased handles accumulate until desktop capture or other window operations fail.
Restore the selected bitmap, release the window DC with `ReleaseDC`, and clean up the remaining GDI objects on every capture path.
Also at `module/device/method/windows_impl.py:119-121`.
</issue_to_address>
### Comment 3
<location path="module/device/method/desktop_impl.py" line_range="159" />
<code_context>
+ """返回进程 exe 完整路径;打开/查询失败(已退出、权限)返回空串。"""
+ kernel32 = ctypes.windll.kernel32
+ # PROCESS_QUERY_LIMITED_INFORMATION
+ handle = kernel32.OpenProcess(0x1000, False, int(pid))
+ if not handle:
+ return ''
</code_context>
<issue_to_address>
**Process handles are truncated**
When 64-bit Windows returns an `OpenProcess` handle with nonzero upper bits, `ctypes` treats `OpenProcess` as returning `c_int`, truncating the pointer-sized handle; later query, close, or terminate calls then use an invalid handle, breaking process detection or cleanup.
Declare pointer-sized argument and return types for `OpenProcess` and the APIs that consume its handle.
Also at `module/device/method/desktop_impl.py:740`, `module/device/method/desktop_impl.py:811`, `module/device/method/desktop_impl.py:957`.
</issue_to_address>
### Comment 4
<location path="module/device/desktop_device.py" line_range="23-24" />
<code_context>
+ 原来那个。窗口被最小化不算没运行,截图方法会先还原再取帧。
+ """
+ pid = self.desktop_pid()
+ if pid is None or not self._desktop_pid_alive(pid):
+ return self.launch_desktop_client()
+ if self.desktop_window_exists():
+ return True
</code_context>
<issue_to_address>
**Existing client is launched twice**
When the configured client is running but OAS lacks permission to query its process, `_desktop_pid_alive` returns `False` when `OpenProcess` is denied, and `_desktop_ensure_launched` launches another client without first checking for the configured PID's window. The existing client is duplicated and the configured PID can be replaced.
Distinguish access-denied from an exited process, and check for the configured PID's window before launching another client.
Also at `module/device/method/desktop_impl.py:811-813`.
</issue_to_address>
### Comment 5
<location path="module/device/desktop_device.py" line_range="23-24" />
<code_context>
+ 原来那个。窗口被最小化不算没运行,截图方法会先还原再取帧。
+ """
+ pid = self.desktop_pid()
+ if pid is None or not self._desktop_pid_alive(pid):
+ return self.launch_desktop_client()
+ if self.desktop_window_exists():
+ return True
</code_context>
<issue_to_address>
**Reused PID blocks startup**
When windows reuses the saved client PID for an unrelated live process, `_desktop_ensure_launched` treats the unrelated process as the client and waits for its desktop window instead of launching the game, so device initialization times out.
Verify that a saved PID belongs to the desktop client before treating it as alive.
Also at `module/device/method/desktop_impl.py:811-820`.
</issue_to_address>
### Comment 6
<location path="module/device/device.py" line_range="123" />
<code_context>
+ # 桌面模式:窗口缺失说明客户端当前不可用(启动期未出窗口,或客户端崩了)。
+ # 直接截图会抛 (1400, '无效的窗口句柄') 搞崩整个进程,必须先拦下;拉客户端
+ # 是 Restart 的职责。最小化不算窗口缺失——BitBlt 截图前会先还原窗口。
+ if self.is_desktop and not self.desktop_window_exists():
+ raise GameNotRunningError('Desktop client window not found')
+
</code_context>
<issue_to_address>
**Recreated window leaves stale handle**
When the client destroys and recreates its main window after the login popup closes, `desktop_window_exists` finds a window for the PID but does not refresh `root_handle_num` or the cached screenshot handle. `screenshot_desktop_bitblt` then calls `GetClientRect` with the destroyed HWND and the task fails.
Rebind the window and invalidate or refresh the cached screenshot handle when the client's main window changes.
Also at `module/device/method/windows_impl.py:99`.
</issue_to_address>
### Comment 7
<location path="module/device/method/desktop_impl.py" line_range="704-707" />
<code_context>
+ # 候选必须落在安装目录下的进程:客户端新旧版本窗口标题不同
+ # ("阴阳师-网易游戏" / "阴阳师-MuMu模拟器专版"),启动等待期内任何
+ # 匹配标题的新窗口都会成为候选,按 exe 归属收口,避免绑到别的进程
+ candidates = [w for w in list_desktop_windows()
+ if w['pid'] not in before_pids
+ and _desktop_pid_under_root(w['pid'], root)]
+ if candidates:
+ candidate = candidates[0]
+ if candidate['hwnd'] == stable_hwnd:
</code_context>
<issue_to_address>
**Concurrent launches share one window**
When two configurations launch the same desktop installation concurrently, two launch attempts can both snapshot `before_pids` before either new window appears, then each accepts any newly enumerated window under the install root rather than only a process it launched. Both can bind the same candidate PID, so stopping one client also kills the other configuration’s client.
Serialize desktop launches or restrict candidate windows to processes proven to belong to the current launch attempt.
</issue_to_address>
### Comment 8
<location path="module/device/method/desktop_impl.py" line_range="586" />
<code_context>
+ else:
+ path = path.parent
+ candidate = path / 'bin' / 'onmyoji.exe'
+ if candidate.is_file():
+ return path.resolve()
+ return None
</code_context>
<issue_to_address>
**Supported install path is rejected**
When the configured installation contains `Launch.exe` but not `bin/onmyoji.exe`, `_desktop_root_from_path` rejects the installation because it validates only `bin/onmyoji.exe`, so `desktop_game_exe` never gets to use its `Launch.exe` fallback and startup fails.
Accept an installation root containing either supported executable.
</issue_to_address>
### Comment 9
<location path="module/device/method/desktop_impl.py" line_range="89-97" />
<code_context>
+ user32 = ctypes.windll.user32
+ previous = None
+ try:
+ previous = user32.SetThreadDpiAwarenessContext(_PER_MONITOR_AWARE_V2)
+ except AttributeError:
+ # Windows 10 1607 以前没有该 API,退化为原有逻辑像素行为
+ pass
+ try:
+ yield
+ finally:
+ if previous:
+ user32.SetThreadDpiAwarenessContext(previous)
+
+
</code_context>
<issue_to_address>
**DPI context is not restored**
When the code runs on 64-bit Windows and the previous DPI context is restored, `ctypes` defaults an undeclared function return type to `c_int`, but `SetThreadDpiAwarenessContext` returns a pointer-sized context handle. On 64-bit Windows, `previous` is truncated and the restore call receives an invalid context, leaving the worker thread in Per-Monitor V2 mode after the context manager exits; later DPI-virtualized operations can use the wrong coordinates.
Declare `SetThreadDpiAwarenessContext.restype = wintypes.HANDLE` and its argument type as `wintypes.HANDLE` before calling it.
</issue_to_address>
### Comment 10
<location path="module/device/method/desktop_impl.py" line_range="127" />
<code_context>
+ user32 = ctypes.windll.user32
+ user32.keybd_event(0x7E, 0, 0, 0)
+ user32.keybd_event(0x7E, 0, 2, 0)
+ user32.SetCursorPos(400, 300)
+ except Exception as e:
+ logger.warning(f'wake input failed: {e}')
</code_context>
<issue_to_address>
**Launching moves the user's cursor**
When the desktop client is automatically launched, `desktop_keep_screen_on(True)` unconditionally moves the system cursor to `(400, 300)` and never restores its previous position. Since the function is called after each successful automatic launch, the user's cursor jumps even when the monitor was already awake.
Preserve and restore the cursor position around the wake input, or use a wake mechanism that does not reposition the user's cursor.
</issue_to_address>
### Comment 11
<location path="module/device/method/desktop_impl.py" line_range="690-691" />
<code_context>
proc = subprocess.Popen([exe], cwd=root,
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0))
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>Sourcery assessment
Needs a human reviewer. 11 findings to address first, and this adds a new desktop-client lifecycle with automatic launching, PID/HWND binding, forced process termination, and background input, so a faulty window match could control or kill the wrong process and game actions cannot be undone by reverting the PR. The persisted configuration can be corrected and future behavior stopped by reverting, but any termination or action already performed would remain.
Blocking findings: module/device/method/desktop_impl.py:690, module/device/method/windows_impl.py:108, module/device/method/desktop_impl.py:159, module/device/desktop_device.py:24, module/device/desktop_device.py:24, and 6 more
| logger.info(f'自动启动桌面客户端(第 {attempt} 轮): {exe}') | ||
| spawned = set() | ||
| try: | ||
| proc = subprocess.Popen([exe], cwd=root, |
There was a problem hiding this comment.
🔴 Critical · 配置的路径会执行远程二进制文件
当未经身份验证的网络行为者可以访问 Web 服务,将 serial 设置为 desktop,并将 desktop_game_path 设置为攻击者控制的、包含 bin/onmyoji.exe 的 UNC 共享,然后启动脚本时,desktop_resolve_install_root 会在配置目录包含 bin/onmyoji.exe 时接受该目录,而 launch_desktop_client 会将该可执行文件直接传递给 subprocess.Popen;因此,攻击者控制的 SMB 共享可以在启动桌面客户端时以 OAS 用户身份执行代码。
请将 desktop_game_path 限制为经过验证的本地客户端安装目录,并要求对配置更改和脚本启动操作进行身份验证。
另见 tasks/Script/config_device.py:59。
面向 AI Agent 的提示
在 `module/device/method/desktop_impl.py` 第 690 行:
**配置的路径会执行远程二进制文件**
当未经身份验证的网络行为者可以访问 Web 服务,将 `serial` 设置为 `desktop`,并将 `desktop_game_path` 设置为攻击者控制的、包含 `bin/onmyoji.exe` 的 UNC 共享,然后启动脚本时,`desktop_resolve_install_root` 会在配置目录包含 `bin/onmyoji.exe` 时接受该目录,而 `launch_desktop_client` 会将该可执行文件直接传递给 `subprocess.Popen`;因此,攻击者控制的 SMB 共享可以在启动桌面客户端时以 OAS 用户身份执行代码。
请将 `desktop_game_path` 限制为经过验证的本地客户端安装目录,并要求对配置更改和脚本启动操作进行身份验证。
另见 `tasks/Script/config_device.py:59`。Original comment in English
🔴 Critical · Configured path executes remote binary
When an unauthenticated network actor can reach the web service, sets serial to desktop and desktop_game_path to an attacker-controlled UNC share containing bin/onmyoji.exe, then starts the script, desktop_resolve_install_root accepts the configured directory when it contains bin/onmyoji.exe, and launch_desktop_client passes that executable directly to subprocess.Popen; an attacker-controlled SMB share can therefore execute code as the OAS user when the desktop client is started.
Restrict desktop_game_path to a verified local client installation and require authentication for configuration changes and script-start actions.
Also at tasks/Script/config_device.py:59.
Prompt for AI agents
In `module/device/method/desktop_impl.py` at line 690:
**Configured path executes remote binary**
When an unauthenticated network actor can reach the web service, sets `serial` to `desktop` and `desktop_game_path` to an attacker-controlled UNC share containing `bin/onmyoji.exe`, then starts the script, `desktop_resolve_install_root` accepts the configured directory when it contains `bin/onmyoji.exe`, and `launch_desktop_client` passes that executable directly to `subprocess.Popen`; an attacker-controlled SMB share can therefore execute code as the OAS user when the desktop client is started.
Restrict `desktop_game_path` to a verified local client installation and require authentication for configuration changes and script-start actions.
Also at `tasks/Script/config_device.py:59`.| 'Desktop client window is minimized, screenshot unavailable. ' | ||
| '请恢复游戏窗口(可被其他窗口遮挡,但不能最小化)' | ||
| ) | ||
| hwndDc = GetDC(self.screenshot_handle_num) |
There was a problem hiding this comment.
🟠 High · 重复截图会耗尽 GDI 资源
反复获取桌面截图时,GetDC 会获取窗口 DC,但 mfcDc.DeleteDC() 不会通过 ReleaseDC 释放它;当位图仍被选入 saveDc 时,DeleteObject 也会失败。未释放的句柄会不断累积,直到桌面捕获或其他窗口操作失败。
请恢复被选中的位图,使用 ReleaseDC 释放窗口 DC,并在每条截图路径上清理剩余的 GDI 对象。
另见 module/device/method/windows_impl.py:119-121。
面向 AI Agent 的提示
在 `module/device/method/windows_impl.py` 第 108 行:
**重复截图会耗尽 GDI 资源**
反复获取桌面截图时,`GetDC` 会获取窗口 DC,但 `mfcDc.DeleteDC()` 不会通过 `ReleaseDC` 释放它;当位图仍被选入 `saveDc` 时,`DeleteObject` 也会失败。未释放的句柄会不断累积,直到桌面捕获或其他窗口操作失败。
请恢复被选中的位图,使用 `ReleaseDC` 释放窗口 DC,并在每条截图路径上清理剩余的 GDI 对象。
另见 `module/device/method/windows_impl.py:119-121`。Original comment in English
🟠 High · Repeated captures exhaust GDI resources
When desktop screenshots are taken repeatedly, GetDC obtains a window DC, but mfcDc.DeleteDC() does not release it with ReleaseDC; DeleteObject also fails while the bitmap remains selected into saveDc. The unreleased handles accumulate until desktop capture or other window operations fail.
Restore the selected bitmap, release the window DC with ReleaseDC, and clean up the remaining GDI objects on every capture path.
Also at module/device/method/windows_impl.py:119-121.
Prompt for AI agents
In `module/device/method/windows_impl.py` at line 108:
**Repeated captures exhaust GDI resources**
When desktop screenshots are taken repeatedly, `GetDC` obtains a window DC, but `mfcDc.DeleteDC()` does not release it with `ReleaseDC`; `DeleteObject` also fails while the bitmap remains selected into `saveDc`. The unreleased handles accumulate until desktop capture or other window operations fail.
Restore the selected bitmap, release the window DC with `ReleaseDC`, and clean up the remaining GDI objects on every capture path.
Also at `module/device/method/windows_impl.py:119-121`.| """返回进程 exe 完整路径;打开/查询失败(已退出、权限)返回空串。""" | ||
| kernel32 = ctypes.windll.kernel32 | ||
| # PROCESS_QUERY_LIMITED_INFORMATION | ||
| handle = kernel32.OpenProcess(0x1000, False, int(pid)) |
There was a problem hiding this comment.
🟠 High · 进程句柄被截断
当 64 位 Windows 返回高位非零的 OpenProcess 句柄时,ctypes 会将 OpenProcess 的返回值视为 c_int,从而截断指针大小的句柄;后续查询、关闭或终止调用会使用无效句柄,导致进程检测或清理失败。
请为 OpenProcess 以及使用其句柄的 API 声明指针大小的参数和返回类型。
另见 module/device/method/desktop_impl.py:740、module/device/method/desktop_impl.py:811、module/device/method/desktop_impl.py:957。
Original comment in English
🟠 High · Process handles are truncated
When 64-bit Windows returns an OpenProcess handle with nonzero upper bits, ctypes treats OpenProcess as returning c_int, truncating the pointer-sized handle; later query, close, or terminate calls then use an invalid handle, breaking process detection or cleanup.
Declare pointer-sized argument and return types for OpenProcess and the APIs that consume its handle.
Also at module/device/method/desktop_impl.py:740, module/device/method/desktop_impl.py:811, module/device/method/desktop_impl.py:957.
Prompt for AI agents
In `module/device/method/desktop_impl.py` at line 159:
**Process handles are truncated**
When 64-bit Windows returns an `OpenProcess` handle with nonzero upper bits, `ctypes` treats `OpenProcess` as returning `c_int`, truncating the pointer-sized handle; later query, close, or terminate calls then use an invalid handle, breaking process detection or cleanup.
Declare pointer-sized argument and return types for `OpenProcess` and the APIs that consume its handle.
Also at `module/device/method/desktop_impl.py:740`, `module/device/method/desktop_impl.py:811`, `module/device/method/desktop_impl.py:957`.| if pid is None or not self._desktop_pid_alive(pid): | ||
| return self.launch_desktop_client() |
There was a problem hiding this comment.
🟠 High · 现有客户端会被启动两次
当配置的客户端正在运行,但 OAS 没有权限查询其进程时,OpenProcess 被拒绝会使 _desktop_pid_alive 返回 False,而 _desktop_ensure_launched 在未先检查配置 PID 对应窗口的情况下启动另一个客户端。这样会产生重复客户端,并且配置的 PID 可能被替换。
请区分访问被拒绝和进程已退出的情况,并在启动另一个客户端前检查配置 PID 对应的窗口。
另见 module/device/method/desktop_impl.py:811-813。
Original comment in English
🟠 High · Existing client is launched twice
When the configured client is running but OAS lacks permission to query its process, _desktop_pid_alive returns False when OpenProcess is denied, and _desktop_ensure_launched launches another client without first checking for the configured PID's window. The existing client is duplicated and the configured PID can be replaced.
Distinguish access-denied from an exited process, and check for the configured PID's window before launching another client.
Also at module/device/method/desktop_impl.py:811-813.
Prompt for AI agents
In `module/device/desktop_device.py` at lines 23-24:
**Existing client is launched twice**
When the configured client is running but OAS lacks permission to query its process, `_desktop_pid_alive` returns `False` when `OpenProcess` is denied, and `_desktop_ensure_launched` launches another client without first checking for the configured PID's window. The existing client is duplicated and the configured PID can be replaced.
Distinguish access-denied from an exited process, and check for the configured PID's window before launching another client.
Also at `module/device/method/desktop_impl.py:811-813`.| if pid is None or not self._desktop_pid_alive(pid): | ||
| return self.launch_desktop_client() |
There was a problem hiding this comment.
🟡 Medium · 复用的 PID 会阻止启动
当 Windows 将保存的客户端 PID 重新分配给一个无关但仍在运行的进程时,_desktop_ensure_launched 会将该无关进程视为客户端,并等待其桌面窗口,而不是启动游戏,导致设备初始化超时。
请在将保存的 PID 视为存活之前,验证其确实属于桌面客户端。
另见 module/device/method/desktop_impl.py:811-820。
Original comment in English
🟡 Medium · Reused PID blocks startup
When windows reuses the saved client PID for an unrelated live process, _desktop_ensure_launched treats the unrelated process as the client and waits for its desktop window instead of launching the game, so device initialization times out.
Verify that a saved PID belongs to the desktop client before treating it as alive.
Also at module/device/method/desktop_impl.py:811-820.
Prompt for AI agents
In `module/device/desktop_device.py` at lines 23-24:
**Reused PID blocks startup**
When windows reuses the saved client PID for an unrelated live process, `_desktop_ensure_launched` treats the unrelated process as the client and waits for its desktop window instead of launching the game, so device initialization times out.
Verify that a saved PID belongs to the desktop client before treating it as alive.
Also at `module/device/method/desktop_impl.py:811-820`.| else: | ||
| path = path.parent | ||
| candidate = path / 'bin' / 'onmyoji.exe' | ||
| if candidate.is_file(): |
There was a problem hiding this comment.
🟡 Medium · 支持的安装路径会被拒绝
当配置的安装目录包含 Launch.exe 但不包含 bin/onmyoji.exe 时,_desktop_root_from_path 因为只验证 bin/onmyoji.exe 而拒绝该安装目录,因此 desktop_game_exe 永远无法使用其 Launch.exe 回退逻辑,启动失败。
请接受包含任一受支持可执行文件的安装根目录。
Original comment in English
🟡 Medium · Supported install path is rejected
When the configured installation contains Launch.exe but not bin/onmyoji.exe, _desktop_root_from_path rejects the installation because it validates only bin/onmyoji.exe, so desktop_game_exe never gets to use its Launch.exe fallback and startup fails.
Accept an installation root containing either supported executable.
Prompt for AI agents
In `module/device/method/desktop_impl.py` at line 586:
**Supported install path is rejected**
When the configured installation contains `Launch.exe` but not `bin/onmyoji.exe`, `_desktop_root_from_path` rejects the installation because it validates only `bin/onmyoji.exe`, so `desktop_game_exe` never gets to use its `Launch.exe` fallback and startup fails.
Accept an installation root containing either supported executable.| previous = user32.SetThreadDpiAwarenessContext(_PER_MONITOR_AWARE_V2) | ||
| except AttributeError: | ||
| # Windows 10 1607 以前没有该 API,退化为原有逻辑像素行为 | ||
| pass | ||
| try: | ||
| yield | ||
| finally: | ||
| if previous: | ||
| user32.SetThreadDpiAwarenessContext(previous) |
There was a problem hiding this comment.
🟠 High · DPI 上下文未恢复
当代码运行在 64 位 Windows 上并恢复之前的 DPI 上下文时,ctypes 会将未声明的函数返回类型默认为 c_int,但 SetThreadDpiAwarenessContext 返回的是指针大小的上下文句柄。在 64 位 Windows 上,previous 会被截断,恢复调用接收到无效上下文,导致上下文管理器退出后工作线程仍处于 Per-Monitor V2 模式;后续 DPI 虚拟化操作可能使用错误的坐标。
请在调用前声明 SetThreadDpiAwarenessContext.restype = wintypes.HANDLE,并将其参数类型声明为 wintypes.HANDLE。
Original comment in English
🟠 High · DPI context is not restored
When the code runs on 64-bit Windows and the previous DPI context is restored, ctypes defaults an undeclared function return type to c_int, but SetThreadDpiAwarenessContext returns a pointer-sized context handle. On 64-bit Windows, previous is truncated and the restore call receives an invalid context, leaving the worker thread in Per-Monitor V2 mode after the context manager exits; later DPI-virtualized operations can use the wrong coordinates.
Declare SetThreadDpiAwarenessContext.restype = wintypes.HANDLE and its argument type as wintypes.HANDLE before calling it.
Prompt for AI agents
In `module/device/method/desktop_impl.py` at lines 89-97:
**DPI context is not restored**
When the code runs on 64-bit Windows and the previous DPI context is restored, `ctypes` defaults an undeclared function return type to `c_int`, but `SetThreadDpiAwarenessContext` returns a pointer-sized context handle. On 64-bit Windows, `previous` is truncated and the restore call receives an invalid context, leaving the worker thread in Per-Monitor V2 mode after the context manager exits; later DPI-virtualized operations can use the wrong coordinates.
Declare `SetThreadDpiAwarenessContext.restype = wintypes.HANDLE` and its argument type as `wintypes.HANDLE` before calling it.| user32 = ctypes.windll.user32 | ||
| user32.keybd_event(0x7E, 0, 0, 0) | ||
| user32.keybd_event(0x7E, 0, 2, 0) | ||
| user32.SetCursorPos(400, 300) |
There was a problem hiding this comment.
🟡 Medium · 启动会移动用户的光标
自动启动桌面客户端时,desktop_keep_screen_on(True) 会无条件将系统光标移动到 (400, 300),且从不恢复之前的位置。由于该函数在每次自动启动成功后都会调用,即使显示器本来就是唤醒状态,用户的光标也会跳动。
请在唤醒输入前后保存并恢复光标位置,或使用不会重新定位用户光标的唤醒机制。
Original comment in English
🟡 Medium · Launching moves the user's cursor
When the desktop client is automatically launched, desktop_keep_screen_on(True) unconditionally moves the system cursor to (400, 300) and never restores its previous position. Since the function is called after each successful automatic launch, the user's cursor jumps even when the monitor was already awake.
Preserve and restore the cursor position around the wake input, or use a wake mechanism that does not reposition the user's cursor.
Prompt for AI agents
In `module/device/method/desktop_impl.py` at line 127:
**Launching moves the user's cursor**
When the desktop client is automatically launched, `desktop_keep_screen_on(True)` unconditionally moves the system cursor to `(400, 300)` and never restores its previous position. Since the function is called after each successful automatic launch, the user's cursor jumps even when the monitor was already awake.
Preserve and restore the cursor position around the wake input, or use a wake mechanism that does not reposition the user's cursor.| proc = subprocess.Popen([exe], cwd=root, | ||
| creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)) |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): 检测到子进程函数 Popen 使用了非静态字符串。如果此数据可由恶意行为者控制,则可能构成命令注入。请审查此调用的使用方式,确保其不可由外部资源控制。你可以考虑使用 shlex.escape()。
来源:opengrep
Original comment in English
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
|
桌面版本多开工具是什么,是否可以自动化配置 |
一般是v5多开器吧,我不用桌面版多开所以没配置过,脚本的话是直接启动onmyoji.exe,这个有个缺点就是每周三更新没法更新内容,需要手动用lanuch.exe更新一次 |
|
真就往外边糊了一层。 我感觉从来就没有抽象过,oas的运行平台和跑游戏的目标平台,这两一直是混在一起的 |
|
卧槽,大佬真牛,我拉了你的仓库试了下真的能跑,太牛了👍👍👍👍👍👍 |
请问可以教一下这个专版的如何适配OASX吗 |
|
好的谢谢,抱歉是我看的不够仔细 |
|
能自动重启吗,切号界面是不是不一样? |
|
这个要大改 1. configserial = "desktop" 太奇怪了,直接新开一个group, 里面第一个就是bool 表示是否是桌面模式 把desktop_game_path 搬到一个新的group, 这个时候handle填输入或者字符就是新开的游戏客户端的实例的标题。再加一个bool类型的(默认false) 用户点击后手动启动对应的客户端实例,但是我们内部不会把它变成 true,它就是类似一个按钮“restart.harvest_config.reset_task_datetime_enable” 重置所有任务的下一次时间。 |
- script.desktop 新组(enable/game_path/profile_dir/window_pos/window_title/ launch_instance)替代 serial="desktop" 魔法字符串,handle 语义变为窗口标题 - 客户端 PID 改为运行时状态:不写配置,档案内 oas_instance.json 供下次启动 认领;删除 handle 下拉注入与 PID 回退补丁 - 档案目录隔离启动:cwd 与 USERPROFILE 落在档案内(Documents 与 SDK 账号库 独立),大目录 junction 共享 - 「启动实例」一次性按钮:主进程直接拉起客户端,值自动弹回 false - 自定义窗口标题:绑定时改名,按标题找窗口的路径放行自定义标题 - 桌面模式强制 screenshot/control 方式为 window_background/window_message
1b3f0ff to
1d990fc
Compare
Why: - 上游桌面客户端 PR runhey#1887 依赖 roi_pad 吸收 DPI 重采样相位差,但本地模板匹配统一走 图像服务 RPC,上游客户端 `_pad_roi` 落点(`module/atom/image.py` 的同步匹配)在本地 不参与匹配;直接跳过会让 `desktop_device._init_desktop` 的 `set_roi_match_padding` 导入失败。 - 上游 MPay 登录弹窗补丁落在旧 `tasks/Restart/login.py`,本地登录已迁移到 `tasks/Component/Login/service.py`;Restart 也已改为 `LoginService` 架构。 What: - `module/atom/image.py`:新增 `ROI_MATCH_PAD` / `DESKTOP_ROI_MATCH_PAD` / `set_roi_match_padding()`,`to_service_payload()` 下发 `roi_pad`。 - `module/image/runtime.py`:`_normalize_rule` 按 `roi_pad` 四边外扩 ROI(左上夹 0), 命中坐标仍以原图 ROI 原点回算;默认 0 时行为不变。 - `tasks/Component/Login/service.py`:移植 MPay 弹窗处理(每轮 + 截图后各查一次, 5 次封顶抛 `GameStuckError`)与登录成功后的 `desktop_mark_logged_in()`。 - `tests/test_upstream_batch27_desktop.py`:桌面配置组、DesktopDevice 登录态、 roi_pad 端到端(1px 相位差命中/坐标换回、边缘夹取、payload 透传)共 13 项。 - `tests/test_upstream_batch27_fixes.py`:Delegation/WantedQuests/GeneralBattle/ 摸鱼弹窗清理/Restart 桌面重试/Login 接线共 13 项。 Verify: - `python -m unittest discover -s tests -p 'test_*.py'`(本机 3.14 + 外置 ppocronnx/ adbutils/uiautomator2 依赖 stub):136 项,仅 7 项既有 HeroTest 失败(与批次无关)。 - 本批新增 26 项全绿;`py_compile` 全部改动文件通过;i18n/template JSON 可解析; 桌面配置 `Config('batch27_smoke')` 可加载。
来源 runhey/dev / runhey/master(2026-10-15~18 新增): - `1d990fce`/`d0bc419a`/`cafe832b`/`0e7edcc8`/`f80c3a6d`/`251cb1cd` 桌面客户端 (PR runhey#1887):窗口后端、Device 层生命周期、独立配置组与多开账号隔离; - `3252b071` MPay 登录弹窗、`6ea67658` Restart 桌面就地重试(已移植到 `Component/Login` + 本地 Restart 架构); - `84366fc5` Delegation 勾玉确认/返回死循环(PR runhey#1940); - `9afe3d32` WantedQuests 邀请已选好友判定(PR runhey#1939); - `43c84c88` GeneralBattle 关 buff 面板后点准备; - `55b7d957` WeeklyTrifles 上周存储弹窗遮挡(PR runhey#1943)。 本地适配:roi_pad 服务端化(`module/image/runtime.py`),桌面登录/重启按本地 架构迁移;新增 26 项回归测试。 跳过:`7e506bc3`(本地已无 Task name mismatch 校验)、`911e996d`(本地无该 workflow 文件)、`60117f17`/`7f0728bf`(`battle_wait.py` 本地不存在;御魂溢出 提示本地 `_handle_reward`/`page_reward` 已覆盖)、`c2023476`+`d8677dc0`(净零)。
…oi_pad 服务端化) Why: - 记录本轮 2026-10-15~18 上游新增的采纳/跳过结论与本地适配,避免换对话重复分析。 - 说明 roi_pad 因本地 RPC 匹配而服务端化、MPay 登录补丁落到 Component/Login 的原因。 What: - §1 追加批次 27 总览;§2 追加 13 条落地明细(8 cherry-pick + 1 手工移植 + 1 测试提交); - §3 追加 5 组判定(BaseTask/workflow/御魂溢出/50%经验 revert); - §4 追加批次 27 影响面、回归测试数量、未做真机验证与待实机项,并更新 ignore 59 条。 Verify: - 与 `git log a074e59`、`dev_tools/upstream_ignored.json` 实测 total=59 一致。
本地与上游无共同祖先无法直接 merge,按 PR diff 逐文件移植,7 个冲突文件按本地结构适配: login.py 的 MPay 弹窗处理与登录态标记改落 tasks/Component/Login/service.py (本地登录实现已迁至此);script_task.py 的登录调用改走 LoginService, delay_pending_tasks 保留本地 reason 参数;image.py 的 match_all/match_all_any 保留本地远程匹配服务架构,仅融合 _pad_roi 回算;新增英文日志全部中文化。 功能:script.desktop.enable 开启后以桌面客户端替代模拟器——窗口绑定、自动拉起、 登录态跟踪、DPI 感知后台截图、窗口消息控制;桌面模式 roi_back 外扩 2px 容忍 1px 相位差,开关默认关闭。 验证状态:尚未真机验证。已通过全部 23 个文件编译、14 个模块导入、 Device/ScriptTask MRO 与 Desktop 模型构造检查;模拟器路径经 getattr 隔离无回归。


不确定是缩放还是渲染原因,游戏里部分模板偏移了1px左右,导致simalar骤降,所以对desktop模式做了模板的roi_back的扩大2px机制。
Sourcery 摘要
通过支持 DPI 容差的模板匹配、原生窗口控制,以及受管理的启动和登录生命周期,提升桌面客户端自动化的可靠性。
新功能:
错误修复:
增强功能:
杂项:
Original summary in English
Sourcery 摘要
在支持现有模拟器的同时,实现对 Windows 桌面客户端的可靠自动化。
新功能:
错误修复:
增强功能:
杂项:
Original summary in English
Sourcery 摘要
在现有模拟器工作流的基础上,支持可靠地自动化操作 Windows 桌面客户端。
新功能:
错误修复:
增强功能:
杂项:
Original summary in English
Summary by Sourcery
Enable reliable automation of the Windows desktop client alongside existing emulator workflows.
New Features:
Bug Fixes:
Enhancements:
Chores: