fix(desktop-wake): n2-wake wecom_search via main WeWorkWindow hwnd (1.2.7)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jiangchang-platform-kit"
|
||||
version = "1.2.6"
|
||||
version = "1.2.7"
|
||||
description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK + 桌面 UIA 暖树"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# jiangchang_desktop_wake 验收(ensure 主路径,1.2.6+)
|
||||
# jiangchang_desktop_wake 验收(ensure 主路径,1.2.7+)
|
||||
|
||||
正式对外路径是 **`ensure`**(内部:`activate` → `desktop-wake --mode n2-wake` → `probe`)。
|
||||
`n2-wake`:**仅 OBJID_CLIENT** 走 DuiLib;不做进程内 FindAll、不同步 GETOBJECT、不 SPI 读屏;判暖只看进程外 probe。
|
||||
`c2` / `inject` 仅诊断用。
|
||||
|
||||
| profile | 窗 | 1.2.6 |
|
||||
| profile | 窗 | 说明 |
|
||||
|---|---|---|
|
||||
| `wecom` | `WeWorkWindow` | 主窗(原有) |
|
||||
| `wecom_search` | `SearchResultWindow2` | 搜索联系人浮层;不 Maximize;markers=`search_result`,`user_item` |
|
||||
| `wecom_search` | `SearchResultWindow2` | 搜索浮层;不 Maximize;**n2-wake 注入同 PID 主窗**(`native_hwnd_class=WeWorkWindow`),probe 仍对浮层 |
|
||||
|
||||
## 0. 编译(企微先完全退出,含托盘)
|
||||
|
||||
@@ -77,9 +77,9 @@ python -m jiangchang_desktop_wake probe --profile wecom
|
||||
|
||||
---
|
||||
|
||||
## C. 搜索浮层 wecom_search(1.2.6+)
|
||||
## C. 搜索浮层 wecom_search(1.2.7+)
|
||||
|
||||
1. 打开企微主窗并登录;在搜索框输入联系人,**保持搜索结果浮层打开**(标题/类名 `SearchResultWindow2`)。
|
||||
1. 打开企微主窗并登录;在搜索框输入联系人,**保持搜索结果浮层打开**(类名 `SearchResultWindow2`)。
|
||||
2. 验收:
|
||||
|
||||
```powershell
|
||||
@@ -94,6 +94,7 @@ python -m jiangchang_desktop_wake probe --profile wecom_search
|
||||
|---|---|
|
||||
| ensure 退出码 `0` | 浮层已暖 |
|
||||
| `warm=true` | markers 含 `search_result` / `user_item`(或 findall 足够) |
|
||||
| 企微 UI | **不白屏**、无红叹号错误页(1.2.6 对浮层 hwnd 注入曾导致) |
|
||||
| `WINDOW_NOT_FOUND` | 浮层未打开——先展开搜索结果再测 |
|
||||
|
||||
---
|
||||
|
||||
@@ -42,7 +42,7 @@ print(rs.warm, rs.probe_after.findall_count, rs.probe_after.markers_found)
|
||||
| profile | 窗 | 说明 |
|
||||
|---|---|---|
|
||||
| `wecom` | `WeWorkWindow` | 主窗;markers=`con_op`,`searchedit` |
|
||||
| `wecom_search` | `SearchResultWindow2` | 搜索联系人浮层;markers=`search_result`,`user_item`;不 Maximize |
|
||||
| `wecom_search` | `SearchResultWindow2` | 搜索浮层;markers=`search_result`,`user_item`;不 Maximize;**n2-wake 注入同 PID 主窗**(1.2.7+) |
|
||||
|
||||
| CLI | 退出码 |
|
||||
|---|---|
|
||||
|
||||
@@ -44,11 +44,12 @@ _BUILTIN = {
|
||||
},
|
||||
"wecom_search": {
|
||||
"id": "wecom_search",
|
||||
"description": "企业微信搜索结果浮层 SearchResultWindow2",
|
||||
"description": "企业微信搜索结果浮层 SearchResultWindow2(n2-wake 注入主窗)",
|
||||
"window_class": "SearchResultWindow2",
|
||||
"warm_min_findall": 20,
|
||||
"warm_markers": ["search_result", "user_item"],
|
||||
"activate_maximize": False,
|
||||
"native_hwnd_class": "WeWorkWindow",
|
||||
"wake_strategies": ["activate", "native_helper"],
|
||||
"native_helper": "desktop-wake.exe",
|
||||
"native_args": ["wecom", "0x{hwnd_hex}", "--mode", "n2-wake"],
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"id": "wecom_search",
|
||||
"description": "企业微信搜索结果浮层 SearchResultWindow2(联系人列表 user_item)",
|
||||
"description": "企业微信搜索结果浮层 SearchResultWindow2(联系人列表 user_item);n2-wake 注入同 PID 主窗 WeWorkWindow,避免对浮层 hwnd 注入导致白屏",
|
||||
"window_class": "SearchResultWindow2",
|
||||
"warm_min_findall": 20,
|
||||
"warm_markers": ["search_result", "user_item"],
|
||||
"activate_maximize": false,
|
||||
"native_hwnd_class": "WeWorkWindow",
|
||||
"wake_strategies": [
|
||||
"activate",
|
||||
"native_helper"
|
||||
|
||||
@@ -29,12 +29,16 @@ class Profile:
|
||||
native_args: list[str] = field(default_factory=list)
|
||||
# 浮层窗(如搜索结果)禁止 Maximize,避免打乱搜索态
|
||||
activate_maximize: bool = True
|
||||
# native_helper / n2-wake 注入目标窗 class;空则用 probe 窗本身。
|
||||
# 搜索浮层应对主窗 WeWorkWindow 注入,避免对 SearchResultWindow2 注入导致白屏。
|
||||
native_hwnd_class: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Profile":
|
||||
maximize = data.get("activate_maximize")
|
||||
if maximize is None:
|
||||
maximize = True
|
||||
native_cls = data.get("native_hwnd_class")
|
||||
return cls(
|
||||
id=str(data.get("id") or "unknown"),
|
||||
description=str(data.get("description") or ""),
|
||||
@@ -55,6 +59,7 @@ class Profile:
|
||||
native_helper=data.get("native_helper"),
|
||||
native_args=list(data.get("native_args") or []),
|
||||
activate_maximize=bool(maximize),
|
||||
native_hwnd_class=str(native_cls).strip() if native_cls else None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,13 @@ from .probe import probe
|
||||
from .profiles import load_profile
|
||||
from .types import Profile, WakeStepResult
|
||||
from . import uia_backend
|
||||
from .window import activate_hwnd, find_hwnd_for_profile, require_windows
|
||||
from .window import (
|
||||
activate_hwnd,
|
||||
find_hwnd_for_profile,
|
||||
find_same_pid_hwnd_by_class,
|
||||
get_window_pid,
|
||||
require_windows,
|
||||
)
|
||||
|
||||
_logger = logging.getLogger("jiangchang_desktop_wake.wake")
|
||||
|
||||
@@ -23,6 +29,48 @@ _logger = logging.getLogger("jiangchang_desktop_wake.wake")
|
||||
StrategyFn = Callable[[int, Profile], Tuple[str, str]]
|
||||
|
||||
|
||||
def resolve_native_hwnd(probe_hwnd: int, profile: Profile) -> int:
|
||||
"""native_helper 实际注入 hwnd。
|
||||
|
||||
若 profile.native_hwnd_class 已设:优先同 PID 下该 class 窗(如浮层 probe、主窗注入)。
|
||||
找不到则回退 probe_hwnd,并打 warning。
|
||||
"""
|
||||
cls = (getattr(profile, "native_hwnd_class", None) or "").strip()
|
||||
if not cls:
|
||||
return int(probe_hwnd)
|
||||
try:
|
||||
pid = get_window_pid(int(probe_hwnd))
|
||||
hit = find_same_pid_hwnd_by_class(pid=pid, class_name=cls)
|
||||
except Exception as exc:
|
||||
_logger.warning(
|
||||
"resolve_native_hwnd failed profile=%s probe=%s err=%s; fallback probe",
|
||||
profile.id,
|
||||
probe_hwnd,
|
||||
exc,
|
||||
)
|
||||
return int(probe_hwnd)
|
||||
if not hit:
|
||||
_logger.warning(
|
||||
"resolve_native_hwnd miss profile=%s native_hwnd_class=%s pid=%s "
|
||||
"probe=%s; fallback probe (may crash popup UI)",
|
||||
profile.id,
|
||||
cls,
|
||||
pid,
|
||||
probe_hwnd,
|
||||
)
|
||||
return int(probe_hwnd)
|
||||
if int(hit) != int(probe_hwnd):
|
||||
_logger.info(
|
||||
"native_hwnd redirect profile=%s probe=%s -> inject=%s class=%s pid=%s",
|
||||
profile.id,
|
||||
probe_hwnd,
|
||||
hit,
|
||||
cls,
|
||||
pid,
|
||||
)
|
||||
return int(hit)
|
||||
|
||||
|
||||
def _strategy_activate(hwnd: int, profile: Profile) -> Tuple[str, str]:
|
||||
maximize = bool(getattr(profile, "activate_maximize", True))
|
||||
activate_hwnd(hwnd, maximize=maximize)
|
||||
@@ -152,6 +200,7 @@ def wake(
|
||||
"""
|
||||
require_windows()
|
||||
prof = load_profile(profile) if isinstance(profile, str) else profile
|
||||
# probe / activate 目标(如 SearchResultWindow2)
|
||||
h = find_hwnd_for_profile(prof, hwnd=hwnd)
|
||||
steps: list[WakeStepResult] = []
|
||||
|
||||
@@ -163,7 +212,11 @@ def wake(
|
||||
)
|
||||
continue
|
||||
try:
|
||||
detail, status = fn(h, prof)
|
||||
# native_helper:可重定向到同 PID 主窗注入;其余策略仍作用在 probe 窗
|
||||
target = resolve_native_hwnd(h, prof) if name == "native_helper" else h
|
||||
detail, status = fn(target, prof)
|
||||
if name == "native_helper" and int(target) != int(h):
|
||||
detail = f"inject_hwnd={target} probe_hwnd={h} | {detail}"
|
||||
steps.append(
|
||||
WakeStepResult(
|
||||
strategy=name,
|
||||
@@ -184,6 +237,7 @@ def wake(
|
||||
time.sleep(pause_s)
|
||||
if stop_when_warm:
|
||||
try:
|
||||
# 始终对 profile 目标窗(浮层)判暖,不是 inject 窗
|
||||
pr = probe(prof, hwnd=h)
|
||||
if pr.warm:
|
||||
steps.append(
|
||||
|
||||
@@ -49,6 +49,26 @@ def window_area(hwnd: int) -> int:
|
||||
return max(0, rect.right - rect.left) * max(0, rect.bottom - rect.top)
|
||||
|
||||
|
||||
def get_window_pid(hwnd: int) -> int:
|
||||
require_windows()
|
||||
pid = wintypes.DWORD()
|
||||
_user32().GetWindowThreadProcessId(int(hwnd), ctypes.byref(pid))
|
||||
return int(pid.value)
|
||||
|
||||
|
||||
def find_same_pid_hwnd_by_class(
|
||||
*,
|
||||
pid: int,
|
||||
class_name: str,
|
||||
) -> Optional[int]:
|
||||
"""同进程下指定 class 的可见顶层窗(多窗取面积最大)。"""
|
||||
hwnds = enum_hwnds(class_name=class_name)
|
||||
same = [h for h in hwnds if get_window_pid(h) == int(pid)]
|
||||
if not same:
|
||||
return None
|
||||
return pick_largest(same) if len(same) > 1 else same[0]
|
||||
|
||||
|
||||
def enum_hwnds(
|
||||
*,
|
||||
class_name: Optional[str] = None,
|
||||
|
||||
@@ -34,9 +34,17 @@ class ProfileTests(unittest.TestCase):
|
||||
self.assertIn("search_result", p.warm_markers)
|
||||
self.assertIn("user_item", p.warm_markers)
|
||||
self.assertFalse(p.activate_maximize)
|
||||
self.assertEqual(p.native_hwnd_class, "WeWorkWindow")
|
||||
self.assertIn("native_helper", p.wake_strategies)
|
||||
self.assertIn("n2-wake", p.native_args or [])
|
||||
|
||||
def test_resolve_native_hwnd_fallback_without_class(self):
|
||||
from jiangchang_desktop_wake.wake import resolve_native_hwnd
|
||||
|
||||
p = Profile.from_dict({"id": "x", "window_class": "Foo"})
|
||||
self.assertIsNone(p.native_hwnd_class)
|
||||
self.assertEqual(resolve_native_hwnd(12345, p), 12345)
|
||||
|
||||
def test_unknown_profile(self):
|
||||
from jiangchang_desktop_wake.errors import DesktopWakeError
|
||||
|
||||
|
||||
Reference in New Issue
Block a user