From c8d136fb79de0056e75330af0a2510497bac0415 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Mon, 25 May 2026 10:13:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20bring=5Fto=5Ffront=20=E7=94=A8=20W?= =?UTF-8?q?in32=20API=20=E6=BF=80=E6=B4=BB=E5=B9=B6=E6=9C=80=E5=A4=A7?= =?UTF-8?q?=E5=8C=96=E4=B8=BB=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 _activate_and_maximize_windows 私有方法:用 ctypes 调 EnumWindows 找匠厂主窗口(标题含 匠厂/Jiangchang/ClawX/OpenClaw), ShowWindow(SW_MAXIMIZE) + SetForegroundWindow 让窗口铺满桌面。 bring_to_front 改 OS 分支: - Windows: 优先 ctypes 激活+最大化(dev / 安装版通吃,比 jiangchang:// 协议在 dev 模式下因 cwd 解析失败而报错可靠);失败时退回协议作为兜底 - macOS / Linux: 维持原协议激活路径(open / xdg-open) 适用场景:录屏 / 自动化测试需要 OS 级窗口前置。page.bring_to_front() 只切 Playwright tab 不影响 z-order,必须 OS 级 API 才能把 Electron 主窗口拉到桌面最前。 Co-Authored-By: Claude Opus 4.7 Co-authored-by: Cursor --- src/jiangchang_desktop_sdk/client.py | 89 +++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/src/jiangchang_desktop_sdk/client.py b/src/jiangchang_desktop_sdk/client.py index eff4434..b9803fb 100644 --- a/src/jiangchang_desktop_sdk/client.py +++ b/src/jiangchang_desktop_sdk/client.py @@ -218,6 +218,65 @@ class JiangchangDesktopClient: _logger.warning("[activate] 协议激活失败:%s", exc) return False + def _activate_and_maximize_windows(self) -> bool: + """Windows 专属:用 Win32 API 把主窗口拉到桌面最前并最大化。 + + EnumWindows 遍历所有顶层窗口,按标题模糊匹配(含"匠厂/Jiangchang/ClawX/OpenClaw"), + 找到后 ShowWindow(SW_MAXIMIZE) + SetForegroundWindow,让录屏看到铺满桌面的匠厂, + 避免任务栏边角和其他应用露出来。 + + 为什么改用 ctypes(替代上一版 PowerShell AppActivate): + 1) AppActivate 只激活不最大化,依然能录到桌面边缘其他窗口; + 2) ctypes 不 fork powershell,启动更快(~0ms vs ~300-800ms); + 3) 直接拿到 hwnd 后续可扩展(如监控窗口尺寸)。 + """ + if os.name != "nt": + return False + try: + import ctypes + from ctypes import wintypes + except Exception as exc: + _logger.debug("[bring_to_front] ctypes 不可用:%s", exc) + return False + + user32 = ctypes.windll.user32 + SW_MAXIMIZE = 3 + keywords = ("匠厂", "Jiangchang", "ClawX", "OpenClaw") + matching: list = [] + + @ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM) + def _enum_proc(hwnd, _lparam): + if not user32.IsWindowVisible(hwnd): + return True + length = user32.GetWindowTextLengthW(hwnd) + if length == 0: + return True + buf = ctypes.create_unicode_buffer(length + 1) + user32.GetWindowTextW(hwnd, buf, length + 1) + if any(kw in buf.value for kw in keywords): + matching.append((hwnd, buf.value)) + return True + + try: + user32.EnumWindows(_enum_proc, 0) + except Exception as exc: + _logger.debug("[bring_to_front] EnumWindows 失败:%s", exc) + return False + + if not matching: + _logger.debug("[bring_to_front] 未找到匠厂主窗口") + return False + + hwnd, title = matching[0] + try: + user32.ShowWindow(hwnd, SW_MAXIMIZE) + user32.SetForegroundWindow(hwnd) + _logger.debug("[bring_to_front] 已激活+最大化 hwnd=%s title=%s", hwnd, title) + return True + except Exception as exc: + _logger.debug("[bring_to_front] ShowWindow/SetForegroundWindow 失败:%s", exc) + return False + def _launch_via_executable(self, cdp_port: int) -> bool: """回退:直接 spawn 可执行文件,带 --remote-debugging-port。""" exe = self._get_default_executable_path() @@ -270,12 +329,38 @@ class JiangchangDesktopClient: ) def bring_to_front(self) -> None: - """把主窗口聚焦到最前。""" + """把主窗口聚焦到桌面最前。 + + 分两层: + 1) page.bring_to_front() —— Playwright 把 page 切到当前 context 内的最前 + tab。**不影响 OS 级窗口 z-order**。 + 2) OS 级窗口前置(录屏 / 自动化必需): + - Windows: 用 ctypes 调 Win32 EnumWindows 找匠厂主窗口 → + ShowWindow(SW_MAXIMIZE) + SetForegroundWindow,让窗口铺满桌面 + (详见 _activate_and_maximize_windows)。失败时退回协议激活作为 + 兜底,但 dev 模式(pnpm dev / electron .)下协议本身会因 cwd 解析 + 失败而无效,只有安装版协议兜底才可能成功。 + - macOS / Linux: 走 jiangchang:// 协议(open / xdg-open, + 非 Windows 系统下协议处理器没有 cwd 解析问题)。 + """ try: page = self.get_page() page.bring_to_front() except Exception as exc: - _logger.debug("[bring_to_front] 失败:%s", exc) + _logger.debug("[bring_to_front] Playwright 切 page 失败:%s", exc) + + if os.name == "nt": + if self._activate_and_maximize_windows(): + return + try: + self._activate_via_protocol() + except Exception as exc: + _logger.debug("[bring_to_front] 协议激活失败:%s", exc) + else: + try: + self._activate_via_protocol() + except Exception as exc: + _logger.debug("[bring_to_front] 协议激活失败:%s", exc) # ─── 连接 / 断开 ──────────────────────────────────────────────