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
|
||||
|
||||
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。"""
|
||||
|
||||
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