feat(sdk): 抽出 window_win32 共享模块,client 激活方法薄壳化
把 _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 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -219,63 +219,13 @@ class JiangchangDesktopClient:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _activate_and_maximize_windows(self) -> bool:
|
def _activate_and_maximize_windows(self) -> bool:
|
||||||
"""Windows 专属:用 Win32 API 把主窗口拉到桌面最前并最大化。
|
"""Windows 专属:把匠厂主窗口拉到桌面最前并最大化。
|
||||||
|
|
||||||
EnumWindows 遍历所有顶层窗口,按标题模糊匹配(含"匠厂/Jiangchang/ClawX/OpenClaw"),
|
实现已抽到 jiangchang_desktop_sdk.window_win32 模块(SDK 子模块),
|
||||||
找到后 ShowWindow(SW_MAXIMIZE) + SetForegroundWindow,让录屏看到铺满桌面的匠厂,
|
给 screencast 工具也复用。本方法保留为类内入口(兼容旧调用)。
|
||||||
避免任务栏边角和其他应用露出来。
|
|
||||||
|
|
||||||
为什么改用 ctypes(替代上一版 PowerShell AppActivate):
|
|
||||||
1) AppActivate 只激活不最大化,依然能录到桌面边缘其他窗口;
|
|
||||||
2) ctypes 不 fork powershell,启动更快(~0ms vs ~300-800ms);
|
|
||||||
3) 直接拿到 hwnd 后续可扩展(如监控窗口尺寸)。
|
|
||||||
"""
|
"""
|
||||||
if os.name != "nt":
|
from .window_win32 import activate_and_maximize_jiangchang_window
|
||||||
return False
|
return activate_and_maximize_jiangchang_window()
|
||||||
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:
|
def _launch_via_executable(self, cdp_port: int) -> bool:
|
||||||
"""回退:直接 spawn 可执行文件,带 --remote-debugging-port。"""
|
"""回退:直接 spawn 可执行文件,带 --remote-debugging-port。"""
|
||||||
|
|||||||
85
src/jiangchang_desktop_sdk/window_win32.py
Normal file
85
src/jiangchang_desktop_sdk/window_win32.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user