diff --git a/pyproject.toml b/pyproject.toml index 2da8195..be12128 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/jiangchang_desktop_wake/ACCEPTANCE.md b/src/jiangchang_desktop_wake/ACCEPTANCE.md index 4386f9a..3f0ceea 100644 --- a/src/jiangchang_desktop_wake/ACCEPTANCE.md +++ b/src/jiangchang_desktop_wake/ACCEPTANCE.md @@ -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` | 浮层未打开——先展开搜索结果再测 | --- diff --git a/src/jiangchang_desktop_wake/README.md b/src/jiangchang_desktop_wake/README.md index d07b159..6e5c9d0 100644 --- a/src/jiangchang_desktop_wake/README.md +++ b/src/jiangchang_desktop_wake/README.md @@ -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 | 退出码 | |---|---| diff --git a/src/jiangchang_desktop_wake/native/desktop-wake.exe b/src/jiangchang_desktop_wake/native/desktop-wake.exe index 591b305..9754597 100644 Binary files a/src/jiangchang_desktop_wake/native/desktop-wake.exe and b/src/jiangchang_desktop_wake/native/desktop-wake.exe differ diff --git a/src/jiangchang_desktop_wake/native/wecom_wake.dll b/src/jiangchang_desktop_wake/native/wecom_wake.dll index 4b83986..21fa432 100644 Binary files a/src/jiangchang_desktop_wake/native/wecom_wake.dll and b/src/jiangchang_desktop_wake/native/wecom_wake.dll differ diff --git a/src/jiangchang_desktop_wake/native_src/DesktopWake/Program.cs b/src/jiangchang_desktop_wake/native_src/DesktopWake/Program.cs index 3f909a3..2c582c4 100644 --- a/src/jiangchang_desktop_wake/native_src/DesktopWake/Program.cs +++ b/src/jiangchang_desktop_wake/native_src/DesktopWake/Program.cs @@ -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)); } + /// + /// 主窗可 Maximize;SearchResultWindow2 等浮层禁止 Maximize(会撑爆/关掉浮层)。 + /// + 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); } diff --git a/src/jiangchang_desktop_wake/native_src/N2/payload/wecom_wake.cpp b/src/jiangchang_desktop_wake/native_src/N2/payload/wecom_wake.cpp index 3178c09..12f3ae3 100644 --- a/src/jiangchang_desktop_wake/native_src/N2/payload/wecom_wake.cpp +++ b/src/jiangchang_desktop_wake/native_src/N2/payload/wecom_wake.cpp @@ -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 @@ -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; diff --git a/src/jiangchang_desktop_wake/profiles.py b/src/jiangchang_desktop_wake/profiles.py index b2668b5..dd4144c 100644 --- a/src/jiangchang_desktop_wake/profiles.py +++ b/src/jiangchang_desktop_wake/profiles.py @@ -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"], diff --git a/src/jiangchang_desktop_wake/profiles/wecom_search.json b/src/jiangchang_desktop_wake/profiles/wecom_search.json index 67442e4..35dd482 100644 --- a/src/jiangchang_desktop_wake/profiles/wecom_search.json +++ b/src/jiangchang_desktop_wake/profiles/wecom_search.json @@ -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" diff --git a/src/jiangchang_desktop_wake/types.py b/src/jiangchang_desktop_wake/types.py index 0b66322..3b2b306 100644 --- a/src/jiangchang_desktop_wake/types.py +++ b/src/jiangchang_desktop_wake/types.py @@ -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 diff --git a/tests/desktop_wake/test_profiles_and_types.py b/tests/desktop_wake/test_profiles_and_types.py index cee9e4c..794a09d 100644 --- a/tests/desktop_wake/test_profiles_and_types.py +++ b/tests/desktop_wake/test_profiles_and_types.py @@ -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 [])