From 6e3f79dde9c69b38151ec01adcff7c87396b6f17 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Mon, 25 May 2026 11:13:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20=E6=8A=BD=E5=87=BA=20window=5Fwin3?= =?UTF-8?q?2=20=E5=85=B1=E4=BA=AB=E6=A8=A1=E5=9D=97=EF=BC=8Cclient=20?= =?UTF-8?q?=E6=BF=80=E6=B4=BB=E6=96=B9=E6=B3=95=E8=96=84=E5=A3=B3=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 _activate_and_maximize_windows 内嵌的 50+ 行 ctypes 实现抽到独立的 SDK 子模块 src/jiangchang_desktop_sdk/window_win32.py,暴露公开函数 activate_and_maximize_jiangchang_window,给 screencast 工具也复用(见后续 commit)。 client.py 的 _activate_and_maximize_windows 退化为 6 行薄壳,调用新模块。 设计: - window_win32 是 SDK 子模块(放 src/jiangchang_desktop_sdk/ 下),不放 tools/, 避免 SDK 反向依赖工具层 - ctypes EnumWindows + ShowWindow(SW_MAXIMIZE) + SetForegroundWindow 逻辑不变 - 跨平台行为不变:Windows 真执行,其他系统返回 False - bring_to_front 调用方完全不感知变化(向后兼容) Co-Authored-By: Claude Opus 4.7 Co-authored-by: Cursor --- src/jiangchang_desktop_sdk/client.py | 60 ++------------- src/jiangchang_desktop_sdk/window_win32.py | 85 ++++++++++++++++++++++ 2 files changed, 90 insertions(+), 55 deletions(-) create mode 100644 src/jiangchang_desktop_sdk/window_win32.py diff --git a/src/jiangchang_desktop_sdk/client.py b/src/jiangchang_desktop_sdk/client.py index b9803fb..2bdbb3f 100644 --- a/src/jiangchang_desktop_sdk/client.py +++ b/src/jiangchang_desktop_sdk/client.py @@ -219,63 +219,13 @@ class JiangchangDesktopClient: return False def _activate_and_maximize_windows(self) -> bool: - """Windows 专属:用 Win32 API 把主窗口拉到桌面最前并最大化。 + """Windows 专属:把匠厂主窗口拉到桌面最前并最大化。 - EnumWindows 遍历所有顶层窗口,按标题模糊匹配(含"匠厂/Jiangchang/ClawX/OpenClaw"), - 找到后 ShowWindow(SW_MAXIMIZE) + SetForegroundWindow,让录屏看到铺满桌面的匠厂, - 避免任务栏边角和其他应用露出来。 - - 为什么改用 ctypes(替代上一版 PowerShell AppActivate): - 1) AppActivate 只激活不最大化,依然能录到桌面边缘其他窗口; - 2) ctypes 不 fork powershell,启动更快(~0ms vs ~300-800ms); - 3) 直接拿到 hwnd 后续可扩展(如监控窗口尺寸)。 + 实现已抽到 jiangchang_desktop_sdk.window_win32 模块(SDK 子模块), + 给 screencast 工具也复用。本方法保留为类内入口(兼容旧调用)。 """ - 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 + from .window_win32 import activate_and_maximize_jiangchang_window + return activate_and_maximize_jiangchang_window() def _launch_via_executable(self, cdp_port: int) -> bool: """回退:直接 spawn 可执行文件,带 --remote-debugging-port。""" diff --git a/src/jiangchang_desktop_sdk/window_win32.py b/src/jiangchang_desktop_sdk/window_win32.py new file mode 100644 index 0000000..9423cba --- /dev/null +++ b/src/jiangchang_desktop_sdk/window_win32.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +""" +Win32 窗口工具:跨 SDK / screencast 共享的"激活并最大化匠厂主窗口"实现。 + +为什么单独抽出来: +- SDK 的 bring_to_front 需要 OS 级窗口前置(page.bring_to_front 只切 tab) +- screencast 的 run_screencast 录屏前也要把匠厂浮到前面并铺满屏幕 +- 两者用同一份 EnumWindows + ShowWindow(SW_MAXIMIZE) + SetForegroundWindow 实现, + 避免代码重复维护 + +跨平台行为: +- Windows: 真正执行 +- 其他系统: 直接返回 False(调用方按需 fallback 到协议激活) +""" +from __future__ import annotations + +import logging +import os +from typing import Optional, Tuple + +_logger = logging.getLogger("jiangchang_desktop_sdk.window_win32") + +_DEFAULT_KEYWORDS: Tuple[str, ...] = ("匠厂", "Jiangchang", "ClawX", "OpenClaw") +SW_MAXIMIZE = 3 + + +def activate_and_maximize_jiangchang_window( + keywords: Optional[Tuple[str, ...]] = None, +) -> bool: + """Windows: EnumWindows 找匠厂主窗口 → ShowWindow(SW_MAXIMIZE) + SetForegroundWindow。 + + Args: + keywords: 标题模糊匹配的关键词;不传则用默认 + ("匠厂", "Jiangchang", "ClawX", "OpenClaw") + + Returns: + True: 找到窗口并执行了 ShowWindow + SetForegroundWindow + False: 非 Windows / ctypes 不可用 / 找不到匹配窗口 / 任何异常 + """ + if os.name != "nt": + return False + + try: + import ctypes + from ctypes import wintypes + except Exception as exc: + _logger.debug("ctypes 不可用:%s", exc) + return False + + user32 = ctypes.windll.user32 + kw = keywords or _DEFAULT_KEYWORDS + 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(k in buf.value for k in kw): + matching.append((hwnd, buf.value)) + return True + + try: + user32.EnumWindows(_enum_proc, 0) + except Exception as exc: + _logger.debug("EnumWindows 失败:%s", exc) + return False + + if not matching: + _logger.debug("未找到匠厂主窗口(keywords=%s)", kw) + return False + + hwnd, title = matching[0] + try: + user32.ShowWindow(hwnd, SW_MAXIMIZE) + user32.SetForegroundWindow(hwnd) + _logger.debug("已激活+最大化 hwnd=%s title=%s", hwnd, title) + return True + except Exception as exc: + _logger.debug("ShowWindow/SetForegroundWindow 失败:%s", exc) + return False