fix(desktop-wake): multi-hwnd subclass + no maximize on search popup (1.2.8)
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 37s
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 37s
SearchResultWindow2 can be warmed after main is already warm; desktop-wake no longer maximizes the popup. 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.7"
|
||||
version = "1.2.8"
|
||||
description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK + 桌面 UIA 暖树"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# jiangchang_desktop_wake 验收(ensure 主路径,1.2.7+)
|
||||
# jiangchang_desktop_wake 验收(ensure 主路径,1.2.8+)
|
||||
|
||||
正式对外路径是 **`ensure`**(内部:`activate` → `desktop-wake --mode n2-wake` → `probe`)。
|
||||
`n2-wake`:**仅 OBJID_CLIENT** 走 DuiLib;不做进程内 FindAll、不同步 GETOBJECT、不 SPI 读屏;判暖只看进程外 probe。
|
||||
@@ -7,7 +7,7 @@
|
||||
| profile | 窗 | 说明 |
|
||||
|---|---|---|
|
||||
| `wecom` | `WeWorkWindow` | 主窗(原有) |
|
||||
| `wecom_search` | `SearchResultWindow2` | 搜索浮层;不 Maximize;**n2-wake 注入同 PID 主窗**(`native_hwnd_class=WeWorkWindow`),probe 仍对浮层 |
|
||||
| `wecom_search` | `SearchResultWindow2` | 搜索浮层;**不对浮层 Maximize**;n2-wake **子类化浮层本身**(DLL 支持多窗;主窗已暖后仍可再装浮层) |
|
||||
|
||||
## 0. 编译(企微先完全退出,含托盘)
|
||||
|
||||
@@ -77,10 +77,11 @@ python -m jiangchang_desktop_wake probe --profile wecom
|
||||
|
||||
---
|
||||
|
||||
## C. 搜索浮层 wecom_search(1.2.7+)
|
||||
## C. 搜索浮层 wecom_search(1.2.8+)
|
||||
|
||||
1. 打开企微主窗并登录;在搜索框输入联系人,**保持搜索结果浮层打开**(类名 `SearchResultWindow2`)。
|
||||
2. 验收:
|
||||
1. **建议先** `ensure --profile wecom` 暖主窗(可与浮层分步)。
|
||||
2. 在搜索框输入联系人,**保持搜索结果浮层打开**(类名 `SearchResultWindow2`)。
|
||||
3. 验收:
|
||||
|
||||
```powershell
|
||||
cd D:\OpenClaw\client-jiangchang\jiangchang-platform-kit
|
||||
@@ -94,7 +95,8 @@ python -m jiangchang_desktop_wake probe --profile wecom_search
|
||||
|---|---|
|
||||
| ensure 退出码 `0` | 浮层已暖 |
|
||||
| `warm=true` | markers 含 `search_result` / `user_item`(或 findall 足够) |
|
||||
| 企微 UI | **不白屏**、无红叹号错误页(1.2.6 对浮层 hwnd 注入曾导致) |
|
||||
| `%TEMP%\jiangchang_desktop_wake_subclass.txt` | 含浮层 hwnd,`ok=1`(主窗已暖后再 ensure 浮层也会再写一条) |
|
||||
| 企微 UI | 浮层**不**被 Maximize 撑满、**不白屏**、不无故关闭 |
|
||||
| `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;**n2-wake 注入同 PID 主窗**(1.2.7+) |
|
||||
| `wecom_search` | `SearchResultWindow2` | 搜索浮层;markers=`search_result`,`user_item`;不 Maximize;**n2-wake 对浮层子类化**(1.2.8+ 支持主窗+浮层多窗) |
|
||||
|
||||
| CLI | 退出码 |
|
||||
|---|---|
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -19,6 +19,7 @@ namespace JiangchangDesktopWake
|
||||
private const uint OBJID_CLIENT = 0xFFFFFFFC;
|
||||
private const uint OBJID_WINDOW = 0x00000000;
|
||||
private const int SW_RESTORE = 9;
|
||||
private const int SW_SHOW = 5;
|
||||
private const int SW_MAXIMIZE = 3;
|
||||
private const int WH_CALLWNDPROC = 4;
|
||||
private const uint PROCESS_CREATE_THREAD = 0x0002;
|
||||
@@ -42,6 +43,9 @@ namespace JiangchangDesktopWake
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool IsIconic(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
@@ -578,12 +582,32 @@ namespace JiangchangDesktopWake
|
||||
return new IntPtr(Convert.ToInt64(s, 16));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主窗可 Maximize;SearchResultWindow2 等浮层禁止 Maximize(会撑爆/关掉浮层)。
|
||||
/// </summary>
|
||||
private static bool ShouldMaximize(IntPtr hwnd)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder(256);
|
||||
if (GetClassName(hwnd, sb, sb.Capacity) <= 0) return true;
|
||||
string cls = sb.ToString();
|
||||
if (string.Equals(cls, "SearchResultWindow2", StringComparison.Ordinal))
|
||||
return false;
|
||||
if (cls.IndexOf("Shadow", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return false;
|
||||
}
|
||||
catch { }
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void Activate(IntPtr hwnd)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsIconic(hwnd)) ShowWindow(hwnd, SW_RESTORE);
|
||||
else ShowWindow(hwnd, SW_MAXIMIZE);
|
||||
else if (ShouldMaximize(hwnd)) ShowWindow(hwnd, SW_MAXIMIZE);
|
||||
else ShowWindow(hwnd, SW_SHOW);
|
||||
SetForegroundWindow(hwnd);
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* - UiaRootObjectId 等原样交给旧 WndProc(禁止用 MSAA Lresult 冒充 UIA root)
|
||||
* - 不做进程内 UIAutomation FindAll;不做同步 SendMessage(GETOBJECT)
|
||||
* - 重入守卫;判暖交给进程外 probe
|
||||
* 1.2.8:支持多 HWND 子类化(主窗 + SearchResultWindow2),禁止「全局只装一次」。
|
||||
*/
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
@@ -40,16 +41,25 @@
|
||||
#define WM_JC_WAKE (WM_APP + 0x51)
|
||||
#define WM_JC_QUIT (WM_APP + 0x52)
|
||||
#define JC_SUBCLASS_ID 0x4A435701u
|
||||
/* 主窗 + 搜索浮层等可能各装一次;禁止全局「只装一次」漏掉 SearchResultWindow2 */
|
||||
#define JC_MAX_SUBCLASS 8
|
||||
|
||||
static HMODULE g_self = NULL;
|
||||
static HWND g_hwnd = NULL;
|
||||
static WNDPROC g_oldProc = NULL;
|
||||
static WNDPROC g_duiOldProc = NULL;
|
||||
static volatile LONG g_getobjectHits = 0;
|
||||
static volatile LONG g_hookHits = 0;
|
||||
static volatile LONG g_duiHits = 0;
|
||||
static volatile LONG g_reenterSkip = 0;
|
||||
static volatile LONG g_subclassInstalled = 0;
|
||||
|
||||
typedef struct JcSubEntry {
|
||||
HWND hwnd;
|
||||
WNDPROC oldProc; /* SetWindowLongPtr 路径;SetWindowSubclass 时为 NULL */
|
||||
int useSubclassApi;
|
||||
} JcSubEntry;
|
||||
|
||||
static JcSubEntry g_subs[JC_MAX_SUBCLASS];
|
||||
static volatile LONG g_subCount = 0;
|
||||
|
||||
static HANDLE g_staThread = NULL;
|
||||
static HANDLE g_staReady = NULL;
|
||||
@@ -209,6 +219,16 @@ static LRESULT HandleGetObject(
|
||||
return r;
|
||||
}
|
||||
|
||||
static JcSubEntry* FindSubEntry(HWND hwnd)
|
||||
{
|
||||
LONG n = g_subCount;
|
||||
for (LONG i = 0; i < n && i < JC_MAX_SUBCLASS; i++) {
|
||||
if (g_subs[i].hwnd == hwnd)
|
||||
return &g_subs[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK DuiSubclassProc(
|
||||
HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
|
||||
UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
|
||||
@@ -222,27 +242,46 @@ static LRESULT CALLBACK DuiSubclassProc(
|
||||
|
||||
static LRESULT CALLBACK DuiWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
JcSubEntry* e = FindSubEntry(hwnd);
|
||||
WNDPROC old = e ? e->oldProc : NULL;
|
||||
if (msg == WM_GETOBJECT)
|
||||
return HandleGetObject(hwnd, wParam, lParam, g_duiOldProc, false);
|
||||
if (g_duiOldProc)
|
||||
return CallWindowProcW(g_duiOldProc, hwnd, msg, wParam, lParam);
|
||||
return HandleGetObject(hwnd, wParam, lParam, old, false);
|
||||
if (old)
|
||||
return CallWindowProcW(old, hwnd, msg, wParam, lParam);
|
||||
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
static bool InstallDuiSubclass(HWND hwnd)
|
||||
{
|
||||
if (!hwnd || !IsWindow(hwnd)) return false;
|
||||
if (InterlockedCompareExchange(&g_subclassInstalled, 1, 0) != 0)
|
||||
if (FindSubEntry(hwnd))
|
||||
return true;
|
||||
if (g_subCount >= JC_MAX_SUBCLASS) {
|
||||
WriteMarker("jiangchang_desktop_wake_subclass.txt",
|
||||
"err=subclass_slots_full\r\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
ResolveDuiLib();
|
||||
void* userdata = (void*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
|
||||
|
||||
/* 先占槽再换 WndProc,避免 GETOBJECT 抢在 Remember 之前 */
|
||||
LONG idx = InterlockedIncrement(&g_subCount) - 1;
|
||||
if (idx < 0 || idx >= JC_MAX_SUBCLASS) {
|
||||
InterlockedDecrement(&g_subCount);
|
||||
WriteMarker("jiangchang_desktop_wake_subclass.txt",
|
||||
"err=subclass_slots_full\r\n");
|
||||
return false;
|
||||
}
|
||||
g_subs[idx].hwnd = hwnd;
|
||||
g_subs[idx].oldProc = NULL;
|
||||
g_subs[idx].useSubclassApi = 0;
|
||||
|
||||
SetLastError(0);
|
||||
g_duiOldProc = (WNDPROC)SetWindowLongPtrW(
|
||||
WNDPROC oldProc = (WNDPROC)SetWindowLongPtrW(
|
||||
hwnd, GWLP_WNDPROC, (LONG_PTR)DuiWndProc);
|
||||
DWORD gle = GetLastError();
|
||||
BOOL ok = (g_duiOldProc != NULL);
|
||||
BOOL ok = (oldProc != NULL);
|
||||
const char* method = "SetWindowLongPtr";
|
||||
|
||||
if (!ok) {
|
||||
@@ -255,17 +294,22 @@ static bool InstallDuiSubclass(HWND hwnd)
|
||||
ok = SetWindowSubclass(hwnd, DuiSubclassProc, JC_SUBCLASS_ID, 0);
|
||||
gle = GetLastError();
|
||||
method = "SetWindowSubclass";
|
||||
g_subs[idx].useSubclassApi = 1;
|
||||
oldProc = NULL;
|
||||
} else {
|
||||
g_subs[idx].oldProc = oldProc;
|
||||
}
|
||||
|
||||
char line[512];
|
||||
sprintf_s(line,
|
||||
"hwnd=%p ok=%d gle=%lu method=%s userdata=%p "
|
||||
"duilib=%p getacc=%p onget=%p\r\n",
|
||||
"duilib=%p getacc=%p onget=%p slots=%ld\r\n",
|
||||
hwnd, (int)ok, gle, method, userdata,
|
||||
g_duilib, g_fnGetAccessible, g_fnOnGetObject);
|
||||
g_duilib, g_fnGetAccessible, g_fnOnGetObject, (long)g_subCount);
|
||||
WriteMarker("jiangchang_desktop_wake_subclass.txt", line);
|
||||
if (!ok) {
|
||||
InterlockedExchange(&g_subclassInstalled, 0);
|
||||
g_subs[idx].hwnd = NULL;
|
||||
InterlockedDecrement(&g_subCount);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -44,12 +44,11 @@ _BUILTIN = {
|
||||
},
|
||||
"wecom_search": {
|
||||
"id": "wecom_search",
|
||||
"description": "企业微信搜索结果浮层 SearchResultWindow2(n2-wake 注入主窗)",
|
||||
"description": "企业微信搜索结果浮层 SearchResultWindow2(n2-wake 对浮层子类化,1.2.8+)",
|
||||
"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,11 +1,10 @@
|
||||
{
|
||||
"id": "wecom_search",
|
||||
"description": "企业微信搜索结果浮层 SearchResultWindow2(联系人列表 user_item);n2-wake 注入同 PID 主窗 WeWorkWindow,避免对浮层 hwnd 注入导致白屏",
|
||||
"description": "企业微信搜索结果浮层 SearchResultWindow2(联系人列表 user_item);n2-wake 对浮层本身子类化(1.2.8+ 支持多窗),禁止 Maximize",
|
||||
"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,8 +29,8 @@ class Profile:
|
||||
native_args: list[str] = field(default_factory=list)
|
||||
# 浮层窗(如搜索结果)禁止 Maximize,避免打乱搜索态
|
||||
activate_maximize: bool = True
|
||||
# native_helper / n2-wake 注入目标窗 class;空则用 probe 窗本身。
|
||||
# 搜索浮层应对主窗 WeWorkWindow 注入,避免对 SearchResultWindow2 注入导致白屏。
|
||||
# native_helper / n2-wake 目标窗 class;空则用 probe 窗本身。
|
||||
# 1.2.8+ wecom_search 留空(对浮层子类化);若需改注其它窗可再设。
|
||||
native_hwnd_class: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -34,7 +34,8 @@ 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")
|
||||
# 1.2.8+:对浮层本身 n2-wake(多窗子类化);不再改注主窗
|
||||
self.assertFalse((p.native_hwnd_class or "").strip())
|
||||
self.assertIn("native_helper", p.wake_strategies)
|
||||
self.assertIn("n2-wake", p.native_args or [])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user