Skip to content

desktop - #1887

Open
LGG686 wants to merge 8 commits into
runhey:devfrom
LGG686:feat/desktop-client
Open

desktop#1887
LGG686 wants to merge 8 commits into
runhey:devfrom
LGG686:feat/desktop-client

Conversation

@LGG686

@LGG686 LGG686 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

不确定是缩放还是渲染原因,游戏里部分模板偏移了1px左右,导致simalar骤降,所以对desktop模式做了模板的roi_back的扩大2px机制。

Sourcery 摘要

通过支持 DPI 容差的模板匹配、原生窗口控制,以及受管理的启动和登录生命周期,提升桌面客户端自动化的可靠性。

新功能:

  • 添加桌面客户端窗口绑定和配置支持,包括 PID 选择、自动启动、登录处理、调整窗口大小、截图以及窗口消息输入。

错误修复:

  • 通过扩大模板搜索区域,容忍细微的渲染或 DPI 相关偏移,提升桌面模板匹配的可靠性。

增强功能:

  • 将桌面客户端生命周期和 Windows 集成与模拟器处理分离,同时保留现有的模拟器行为。
  • 支持 DPI 感知的桌面截图、坐标转换、仿人指针移动以及可配置的桌面截图间隔。

杂项:

  • 添加针对桌面的配置项,用于设置游戏安装路径和窗口选择选项。
Original summary in English

Summary by Sourcery

Enable reliable desktop-client automation with DPI-tolerant template matching, native window control, and managed launch and login lifecycle support.

New Features:

  • Add desktop-client window binding and configuration support, including PID selection, automatic launching, login handling, resizing, screenshots, and window-message input.

Bug Fixes:

  • Improve desktop template matching reliability by expanding template search regions to tolerate small rendering or DPI-related offsets.

Enhancements:

  • Separate desktop-client lifecycle and Windows integration from emulator handling while preserving existing emulator behavior.
  • Support DPI-aware desktop screenshots, coordinate conversion, human-like pointer movement, and configurable desktop screenshot intervals.

Chores:

  • Add desktop-specific configuration for the game installation path and window selection options.

LGG686 added 8 commits October 9, 2026 09:41
- 新增 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.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

你好——我发现了 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 项


Sourcery 对开源项目免费——如果你喜欢我们的审查,请考虑分享 ✨
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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

logger.info(f'自动启动桌面客户端(第 {attempt} 轮): {exe}')
spawned = set()
try:
proc = subprocess.Popen([exe], cwd=root,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 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`.

Comment on lines +23 to +24
if pid is None or not self._desktop_pid_alive(pid):
return self.launch_desktop_client()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 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`.

Comment on lines +23 to +24
if pid is None or not self._desktop_pid_alive(pid):
return self.launch_desktop_client()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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`.

Comment on lines +704 to +707
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High · 并发启动会共享同一个窗口

当两个配置同时启动同一个桌面安装时,两个启动尝试都可能在任何新窗口出现之前获取 before_pids 快照;随后它们各自接受安装目录下枚举到的任意新窗口,而不只接受由自己启动的进程。两者可能绑定同一个候选 PID,因此停止一个客户端也会终止另一个配置的客户端。

请串行化桌面启动操作,或将候选窗口限制为已确认属于当前启动尝试的进程。

Original comment in English

🟠 High · 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.

Prompt for AI agents
In `module/device/method/desktop_impl.py` at lines 704-707:

**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.

else:
path = path.parent
candidate = path / 'bin' / 'onmyoji.exe'
if candidate.is_file():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Comment on lines +89 to +97
previous = user32.SetThreadDpiAwarenessContext(_PER_MONITOR_AWARE_V2)
except AttributeError:
# Windows 10 1607 以前没有该 API,退化为原有逻辑像素行为
pass
try:
yield
finally:
if previous:
user32.SetThreadDpiAwarenessContext(previous)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Comment on lines +690 to +691
proc = subprocess.Popen([exe], cwd=root,
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@runhey

runhey commented Oct 9, 2026

Copy link
Copy Markdown
Owner

桌面版本多开工具是什么,是否可以自动化配置

@LGG686

LGG686 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

桌面版本多开工具是什么,是否可以自动化配置

一般是v5多开器吧,我不用桌面版多开所以没配置过,脚本的话是直接启动onmyoji.exe,这个有个缺点就是每周三更新没法更新内容,需要手动用lanuch.exe更新一次

@runhey

runhey commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

真就往外边糊了一层。 我感觉从来就没有抽象过,oas的运行平台和跑游戏的目标平台,这两一直是混在一起的

@Carol-official

Copy link
Copy Markdown

卧槽,大佬真牛,我拉了你的仓库试了下真的能跑,太牛了👍👍👍👍👍👍

@Shuijing-Ye

Copy link
Copy Markdown

卧槽,大佬真牛,我拉了你的仓库试了下真的能跑,太牛了👍👍👍👍👍👍

请问可以教一下这个专版的如何适配OASX吗

@Carol-official

Copy link
Copy Markdown

卧槽,大佬真牛,我拉了你的仓库试了下真的能跑,太牛了👍👍👍👍👍👍

请问可以教一下这个专版的如何适配OASX吗

f80c3a6

@Shuijing-Ye

Copy link
Copy Markdown

卧槽,大佬真牛,我拉了你的仓库试了下真的能跑,太牛了👍👍👍👍👍👍

请问可以教一下这个专版的如何适配OASX吗

f80c3a6

好的谢谢,抱歉是我看的不够仔细

@Hurry2

Hurry2 commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

能自动重启吗,切号界面是不是不一样?

@runhey

runhey commented Oct 17, 2026

Copy link
Copy Markdown
Owner

@runhey

runhey commented Oct 17, 2026

Copy link
Copy Markdown
Owner

这个要大改

1. config

serial = "desktop" 太奇怪了,直接新开一个group, 里面第一个就是bool 表示是否是桌面模式
handle 和 pid 本质就不是一个东西,这个思路不太好,而且pid不能保存,为了这个引入了一堆补丁。
我之前做的 顶多是把handle 和 title 给混一起了。如果非要pid,必须要新开一个字段。

把desktop_game_path 搬到一个新的group, 这个时候handle填输入或者字符就是新开的游戏客户端的实例的标题。再加一个bool类型的(默认false) 用户点击后手动启动对应的客户端实例,但是我们内部不会把它变成 true,它就是类似一个按钮“restart.harvest_config.reset_task_datetime_enable” 重置所有任务的下一次时间。

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.

5 participants