Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24384d70ba | |||
| 17ac1b4e11 | |||
| 84157758e6 |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jiangchang-platform-kit"
|
name = "jiangchang-platform-kit"
|
||||||
version = "1.2.6"
|
version = "1.2.9"
|
||||||
description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK + 桌面 UIA 暖树"
|
description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK + 桌面 UIA 暖树"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# jiangchang_desktop_wake 验收(ensure 主路径,1.2.6+)
|
# jiangchang_desktop_wake 验收(ensure 主路径,1.2.9+)
|
||||||
|
|
||||||
正式对外路径是 **`ensure`**(内部:`activate` → `desktop-wake --mode n2-wake` → `probe`)。
|
正式对外路径是 **`ensure`**(内部:`activate` → `desktop-wake --mode n2-wake` → `probe`)。
|
||||||
`n2-wake`:**仅 OBJID_CLIENT** 走 DuiLib;不做进程内 FindAll、不同步 GETOBJECT、不 SPI 读屏;判暖只看进程外 probe。
|
`n2-wake`:**仅 OBJID_CLIENT** 走 DuiLib;不做进程内 FindAll、不同步 GETOBJECT、不 SPI 读屏;判暖只看进程外 probe。
|
||||||
`c2` / `inject` 仅诊断用。
|
`c2` / `inject` 仅诊断用。
|
||||||
|
|
||||||
| profile | 窗 | 1.2.6 |
|
| profile | 窗 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `wecom` | `WeWorkWindow` | 主窗(原有) |
|
| `wecom` | `WeWorkWindow` | 主窗(原有) |
|
||||||
| `wecom_search` | `SearchResultWindow2` | 搜索联系人浮层;不 Maximize;markers=`search_result`,`user_item` |
|
| `wecom_search` | `SearchResultWindow2` | 搜索浮层;**不对浮层 Maximize / 不注入**(1.2.9+ 仅软唤醒;注入会崩企微) |
|
||||||
|
|
||||||
## 0. 编译(企微先完全退出,含托盘)
|
## 0. 编译(企微先完全退出,含托盘)
|
||||||
|
|
||||||
@@ -77,10 +77,11 @@ python -m jiangchang_desktop_wake probe --profile wecom
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## C. 搜索浮层 wecom_search(1.2.6+)
|
## C. 搜索浮层 wecom_search(1.2.9+)
|
||||||
|
|
||||||
1. 打开企微主窗并登录;在搜索框输入联系人,**保持搜索结果浮层打开**(标题/类名 `SearchResultWindow2`)。
|
1. **建议先** `ensure --profile wecom` 暖主窗(可与浮层分步)。
|
||||||
2. 验收:
|
2. 在搜索框输入联系人,**保持搜索结果浮层打开**(类名 `SearchResultWindow2`)。
|
||||||
|
3. 验收(**禁止**对浮层 n2-wake/注入;profile 仅 `wm_getobject` + `msaa`):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd D:\OpenClaw\client-jiangchang\jiangchang-platform-kit
|
cd D:\OpenClaw\client-jiangchang\jiangchang-platform-kit
|
||||||
@@ -92,8 +93,10 @@ python -m jiangchang_desktop_wake probe --profile wecom_search
|
|||||||
|
|
||||||
| 期望 | 说明 |
|
| 期望 | 说明 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| ensure 退出码 `0` | 浮层已暖 |
|
| ensure **不**崩企微 | 故障模块不应再是 `wecom_wake.dll` |
|
||||||
| `warm=true` | markers 含 `search_result` / `user_item`(或 findall 足够) |
|
| `warm=true`(或 soft poke 后 findall≫2) | markers 含 `search_result` / `user_item` |
|
||||||
|
| 企微 UI | 浮层保持打开、**不** Maximize、不白屏 |
|
||||||
|
| 若手动对浮层跑 `desktop-wake … n2-wake` | exe/DLL 应走 `popup_soft_only` / `popup_nudge_only`,**不**子类化浮层 |
|
||||||
| `WINDOW_NOT_FOUND` | 浮层未打开——先展开搜索结果再测 |
|
| `WINDOW_NOT_FOUND` | 浮层未打开——先展开搜索结果再测 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ print(rs.warm, rs.probe_after.findall_count, rs.probe_after.markers_found)
|
|||||||
| profile | 窗 | 说明 |
|
| profile | 窗 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `wecom` | `WeWorkWindow` | 主窗;markers=`con_op`,`searchedit` |
|
| `wecom` | `WeWorkWindow` | 主窗;markers=`con_op`,`searchedit` |
|
||||||
| `wecom_search` | `SearchResultWindow2` | 搜索联系人浮层;markers=`search_result`,`user_item`;不 Maximize |
|
| `wecom_search` | `SearchResultWindow2` | 搜索浮层;markers=`search_result`,`user_item`;**禁止注入/n2-wake**(1.2.9+ 仅进程外软唤醒) |
|
||||||
|
|
||||||
| CLI | 退出码 |
|
| 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_CLIENT = 0xFFFFFFFC;
|
||||||
private const uint OBJID_WINDOW = 0x00000000;
|
private const uint OBJID_WINDOW = 0x00000000;
|
||||||
private const int SW_RESTORE = 9;
|
private const int SW_RESTORE = 9;
|
||||||
|
private const int SW_SHOW = 5;
|
||||||
private const int SW_MAXIMIZE = 3;
|
private const int SW_MAXIMIZE = 3;
|
||||||
private const int WH_CALLWNDPROC = 4;
|
private const int WH_CALLWNDPROC = 4;
|
||||||
private const uint PROCESS_CREATE_THREAD = 0x0002;
|
private const uint PROCESS_CREATE_THREAD = 0x0002;
|
||||||
@@ -42,6 +43,9 @@ namespace JiangchangDesktopWake
|
|||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
private static extern bool IsIconic(IntPtr hWnd);
|
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")]
|
[DllImport("user32.dll")]
|
||||||
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
@@ -165,6 +169,21 @@ namespace JiangchangDesktopWake
|
|||||||
if (!doWakeProvider)
|
if (!doWakeProvider)
|
||||||
EnableScreenReaderFlag();
|
EnableScreenReaderFlag();
|
||||||
|
|
||||||
|
// 1.2.9:SearchResultWindow2 禁止 n2 注入(wecom_wake.dll 子类化/DuiLib 会崩企微)
|
||||||
|
if (doN2 && IsSearchResultPopup(hwnd))
|
||||||
|
{
|
||||||
|
SendGetObject(hwnd);
|
||||||
|
TryMsaa(hwnd);
|
||||||
|
bool responsivePopup = CheckUiResponsive(hwnd);
|
||||||
|
method = "popup_soft_only";
|
||||||
|
WriteJson(
|
||||||
|
responsivePopup, false, responsivePopup,
|
||||||
|
profile, hwnd, 0, 0, new string[0],
|
||||||
|
"search_popup_no_inject|soft_getobject_msaa",
|
||||||
|
mode, method);
|
||||||
|
return responsivePopup ? 0 : 6;
|
||||||
|
}
|
||||||
|
|
||||||
if (doN2)
|
if (doN2)
|
||||||
{
|
{
|
||||||
string n2Detail;
|
string n2Detail;
|
||||||
@@ -578,12 +597,43 @@ namespace JiangchangDesktopWake
|
|||||||
return new IntPtr(Convert.ToInt64(s, 16));
|
return new IntPtr(Convert.ToInt64(s, 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsSearchResultPopup(IntPtr hwnd)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(256);
|
||||||
|
if (GetClassName(hwnd, sb, sb.Capacity) <= 0) return false;
|
||||||
|
return string.Equals(sb.ToString(), "SearchResultWindow2", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
private static void Activate(IntPtr hwnd)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (IsIconic(hwnd)) ShowWindow(hwnd, SW_RESTORE);
|
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);
|
SetForegroundWindow(hwnd);
|
||||||
Thread.Sleep(250);
|
Thread.Sleep(250);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
* - UiaRootObjectId 等原样交给旧 WndProc(禁止用 MSAA Lresult 冒充 UIA root)
|
* - UiaRootObjectId 等原样交给旧 WndProc(禁止用 MSAA Lresult 冒充 UIA root)
|
||||||
* - 不做进程内 UIAutomation FindAll;不做同步 SendMessage(GETOBJECT)
|
* - 不做进程内 UIAutomation FindAll;不做同步 SendMessage(GETOBJECT)
|
||||||
* - 重入守卫;判暖交给进程外 probe
|
* - 重入守卫;判暖交给进程外 probe
|
||||||
|
* 1.2.8:支持多 HWND 子类化(主窗 + SearchResultWindow2),禁止「全局只装一次」。
|
||||||
|
* 1.2.9:SearchResultWindow2 禁止子类化/DuiLib thiscall(会崩企微);浮层只异步 nudge。
|
||||||
*/
|
*/
|
||||||
#define WIN32_LEAN_AND_MEAN
|
#define WIN32_LEAN_AND_MEAN
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -40,16 +42,25 @@
|
|||||||
#define WM_JC_WAKE (WM_APP + 0x51)
|
#define WM_JC_WAKE (WM_APP + 0x51)
|
||||||
#define WM_JC_QUIT (WM_APP + 0x52)
|
#define WM_JC_QUIT (WM_APP + 0x52)
|
||||||
#define JC_SUBCLASS_ID 0x4A435701u
|
#define JC_SUBCLASS_ID 0x4A435701u
|
||||||
|
/* 主窗 + 搜索浮层等可能各装一次;禁止全局「只装一次」漏掉 SearchResultWindow2 */
|
||||||
|
#define JC_MAX_SUBCLASS 8
|
||||||
|
|
||||||
static HMODULE g_self = NULL;
|
static HMODULE g_self = NULL;
|
||||||
static HWND g_hwnd = NULL;
|
static HWND g_hwnd = NULL;
|
||||||
static WNDPROC g_oldProc = NULL;
|
static WNDPROC g_oldProc = NULL;
|
||||||
static WNDPROC g_duiOldProc = NULL;
|
|
||||||
static volatile LONG g_getobjectHits = 0;
|
static volatile LONG g_getobjectHits = 0;
|
||||||
static volatile LONG g_hookHits = 0;
|
static volatile LONG g_hookHits = 0;
|
||||||
static volatile LONG g_duiHits = 0;
|
static volatile LONG g_duiHits = 0;
|
||||||
static volatile LONG g_reenterSkip = 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_staThread = NULL;
|
||||||
static HANDLE g_staReady = NULL;
|
static HANDLE g_staReady = NULL;
|
||||||
@@ -129,6 +140,14 @@ static void NudgeWinEvents(HWND hwnd)
|
|||||||
PostMessageW(hwnd, WM_NULL, 0, 0);
|
PostMessageW(hwnd, WM_NULL, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool IsSearchResultPopup(HWND hwnd)
|
||||||
|
{
|
||||||
|
wchar_t cls[256];
|
||||||
|
if (!hwnd || !GetClassNameW(hwnd, cls, 256))
|
||||||
|
return false;
|
||||||
|
return _wcsicmp(cls, L"SearchResultWindow2") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
static bool ResolveDuiLib()
|
static bool ResolveDuiLib()
|
||||||
{
|
{
|
||||||
if (g_fnGetAccessible && g_fnOnGetObject) return true;
|
if (g_fnGetAccessible && g_fnOnGetObject) return true;
|
||||||
@@ -209,6 +228,16 @@ static LRESULT HandleGetObject(
|
|||||||
return r;
|
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(
|
static LRESULT CALLBACK DuiSubclassProc(
|
||||||
HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
|
HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
|
||||||
UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
|
UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
|
||||||
@@ -222,27 +251,46 @@ static LRESULT CALLBACK DuiSubclassProc(
|
|||||||
|
|
||||||
static LRESULT CALLBACK DuiWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
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)
|
if (msg == WM_GETOBJECT)
|
||||||
return HandleGetObject(hwnd, wParam, lParam, g_duiOldProc, false);
|
return HandleGetObject(hwnd, wParam, lParam, old, false);
|
||||||
if (g_duiOldProc)
|
if (old)
|
||||||
return CallWindowProcW(g_duiOldProc, hwnd, msg, wParam, lParam);
|
return CallWindowProcW(old, hwnd, msg, wParam, lParam);
|
||||||
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool InstallDuiSubclass(HWND hwnd)
|
static bool InstallDuiSubclass(HWND hwnd)
|
||||||
{
|
{
|
||||||
if (!hwnd || !IsWindow(hwnd)) return false;
|
if (!hwnd || !IsWindow(hwnd)) return false;
|
||||||
if (InterlockedCompareExchange(&g_subclassInstalled, 1, 0) != 0)
|
if (FindSubEntry(hwnd))
|
||||||
return true;
|
return true;
|
||||||
|
if (g_subCount >= JC_MAX_SUBCLASS) {
|
||||||
|
WriteMarker("jiangchang_desktop_wake_subclass.txt",
|
||||||
|
"err=subclass_slots_full\r\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ResolveDuiLib();
|
ResolveDuiLib();
|
||||||
void* userdata = (void*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
|
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);
|
SetLastError(0);
|
||||||
g_duiOldProc = (WNDPROC)SetWindowLongPtrW(
|
WNDPROC oldProc = (WNDPROC)SetWindowLongPtrW(
|
||||||
hwnd, GWLP_WNDPROC, (LONG_PTR)DuiWndProc);
|
hwnd, GWLP_WNDPROC, (LONG_PTR)DuiWndProc);
|
||||||
DWORD gle = GetLastError();
|
DWORD gle = GetLastError();
|
||||||
BOOL ok = (g_duiOldProc != NULL);
|
BOOL ok = (oldProc != NULL);
|
||||||
const char* method = "SetWindowLongPtr";
|
const char* method = "SetWindowLongPtr";
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@@ -255,17 +303,22 @@ static bool InstallDuiSubclass(HWND hwnd)
|
|||||||
ok = SetWindowSubclass(hwnd, DuiSubclassProc, JC_SUBCLASS_ID, 0);
|
ok = SetWindowSubclass(hwnd, DuiSubclassProc, JC_SUBCLASS_ID, 0);
|
||||||
gle = GetLastError();
|
gle = GetLastError();
|
||||||
method = "SetWindowSubclass";
|
method = "SetWindowSubclass";
|
||||||
|
g_subs[idx].useSubclassApi = 1;
|
||||||
|
oldProc = NULL;
|
||||||
|
} else {
|
||||||
|
g_subs[idx].oldProc = oldProc;
|
||||||
}
|
}
|
||||||
|
|
||||||
char line[512];
|
char line[512];
|
||||||
sprintf_s(line,
|
sprintf_s(line,
|
||||||
"hwnd=%p ok=%d gle=%lu method=%s userdata=%p "
|
"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,
|
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);
|
WriteMarker("jiangchang_desktop_wake_subclass.txt", line);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
InterlockedExchange(&g_subclassInstalled, 0);
|
g_subs[idx].hwnd = NULL;
|
||||||
|
InterlockedDecrement(&g_subCount);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -278,6 +331,19 @@ static void DoSafeWake(HWND hwnd)
|
|||||||
int subclass_ok = 0;
|
int subclass_ok = 0;
|
||||||
int dui_ok = 0;
|
int dui_ok = 0;
|
||||||
|
|
||||||
|
/* 浮层:DuiLib thiscall / 换 WndProc 在宿主环境会直接崩企微(fault=wecom_wake.dll) */
|
||||||
|
if (IsSearchResultPopup(hwnd)) {
|
||||||
|
NudgeWinEvents(hwnd);
|
||||||
|
NudgeWinEvents(hwnd);
|
||||||
|
sprintf_s(line,
|
||||||
|
"hwnd=%p sta_tid=%lu popup_nudge_only=1 subclass=0 dui=0 "
|
||||||
|
"dui_hits=%ld getobject_hits=%ld done=1\r\n",
|
||||||
|
hwnd, GetCurrentThreadId(),
|
||||||
|
(long)g_duiHits, (long)g_getobjectHits);
|
||||||
|
WriteMarker("jiangchang_desktop_wake_provider.txt", line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ResolveDuiLib();
|
ResolveDuiLib();
|
||||||
dui_ok = (g_fnGetAccessible || g_fnOnGetObject) ? 1 : 0;
|
dui_ok = (g_fnGetAccessible || g_fnOnGetObject) ? 1 : 0;
|
||||||
subclass_ok = InstallDuiSubclass(hwnd) ? 1 : 0;
|
subclass_ok = InstallDuiSubclass(hwnd) ? 1 : 0;
|
||||||
|
|||||||
@@ -44,14 +44,12 @@ _BUILTIN = {
|
|||||||
},
|
},
|
||||||
"wecom_search": {
|
"wecom_search": {
|
||||||
"id": "wecom_search",
|
"id": "wecom_search",
|
||||||
"description": "企业微信搜索结果浮层 SearchResultWindow2",
|
"description": "企业微信搜索结果浮层 SearchResultWindow2;仅进程外软唤醒(禁止注入,1.2.9+)",
|
||||||
"window_class": "SearchResultWindow2",
|
"window_class": "SearchResultWindow2",
|
||||||
"warm_min_findall": 20,
|
"warm_min_findall": 20,
|
||||||
"warm_markers": ["search_result", "user_item"],
|
"warm_markers": ["search_result", "user_item"],
|
||||||
"activate_maximize": False,
|
"activate_maximize": False,
|
||||||
"wake_strategies": ["activate", "native_helper"],
|
"wake_strategies": ["wm_getobject", "msaa_from_window"],
|
||||||
"native_helper": "desktop-wake.exe",
|
|
||||||
"native_args": ["wecom", "0x{hwnd_hex}", "--mode", "n2-wake"],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
{
|
{
|
||||||
"id": "wecom_search",
|
"id": "wecom_search",
|
||||||
"description": "企业微信搜索结果浮层 SearchResultWindow2(联系人列表 user_item)",
|
"description": "企业微信搜索结果浮层 SearchResultWindow2;仅进程外软唤醒(禁止 n2-wake/注入,1.2.9+)",
|
||||||
"window_class": "SearchResultWindow2",
|
"window_class": "SearchResultWindow2",
|
||||||
"warm_min_findall": 20,
|
"warm_min_findall": 20,
|
||||||
"warm_markers": ["search_result", "user_item"],
|
"warm_markers": ["search_result", "user_item"],
|
||||||
"activate_maximize": false,
|
"activate_maximize": false,
|
||||||
"wake_strategies": [
|
"wake_strategies": [
|
||||||
"activate",
|
"wm_getobject",
|
||||||
"native_helper"
|
"msaa_from_window"
|
||||||
],
|
]
|
||||||
"native_helper": "desktop-wake.exe",
|
|
||||||
"native_args": ["wecom", "0x{hwnd_hex}", "--mode", "n2-wake"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,16 @@ class Profile:
|
|||||||
native_args: list[str] = field(default_factory=list)
|
native_args: list[str] = field(default_factory=list)
|
||||||
# 浮层窗(如搜索结果)禁止 Maximize,避免打乱搜索态
|
# 浮层窗(如搜索结果)禁止 Maximize,避免打乱搜索态
|
||||||
activate_maximize: bool = True
|
activate_maximize: bool = True
|
||||||
|
# native_helper / n2-wake 目标窗 class;空则用 probe 窗本身。
|
||||||
|
# wecom_search 留空;1.2.9+ 浮层禁止 native 注入,勿再设为浮层自身。
|
||||||
|
native_hwnd_class: Optional[str] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> "Profile":
|
def from_dict(cls, data: dict[str, Any]) -> "Profile":
|
||||||
maximize = data.get("activate_maximize")
|
maximize = data.get("activate_maximize")
|
||||||
if maximize is None:
|
if maximize is None:
|
||||||
maximize = True
|
maximize = True
|
||||||
|
native_cls = data.get("native_hwnd_class")
|
||||||
return cls(
|
return cls(
|
||||||
id=str(data.get("id") or "unknown"),
|
id=str(data.get("id") or "unknown"),
|
||||||
description=str(data.get("description") or ""),
|
description=str(data.get("description") or ""),
|
||||||
@@ -55,6 +59,7 @@ class Profile:
|
|||||||
native_helper=data.get("native_helper"),
|
native_helper=data.get("native_helper"),
|
||||||
native_args=list(data.get("native_args") or []),
|
native_args=list(data.get("native_args") or []),
|
||||||
activate_maximize=bool(maximize),
|
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 .profiles import load_profile
|
||||||
from .types import Profile, WakeStepResult
|
from .types import Profile, WakeStepResult
|
||||||
from . import uia_backend
|
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")
|
_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]]
|
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]:
|
def _strategy_activate(hwnd: int, profile: Profile) -> Tuple[str, str]:
|
||||||
maximize = bool(getattr(profile, "activate_maximize", True))
|
maximize = bool(getattr(profile, "activate_maximize", True))
|
||||||
activate_hwnd(hwnd, maximize=maximize)
|
activate_hwnd(hwnd, maximize=maximize)
|
||||||
@@ -152,6 +200,7 @@ def wake(
|
|||||||
"""
|
"""
|
||||||
require_windows()
|
require_windows()
|
||||||
prof = load_profile(profile) if isinstance(profile, str) else profile
|
prof = load_profile(profile) if isinstance(profile, str) else profile
|
||||||
|
# probe / activate 目标(如 SearchResultWindow2)
|
||||||
h = find_hwnd_for_profile(prof, hwnd=hwnd)
|
h = find_hwnd_for_profile(prof, hwnd=hwnd)
|
||||||
steps: list[WakeStepResult] = []
|
steps: list[WakeStepResult] = []
|
||||||
|
|
||||||
@@ -163,7 +212,11 @@ def wake(
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
try:
|
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(
|
steps.append(
|
||||||
WakeStepResult(
|
WakeStepResult(
|
||||||
strategy=name,
|
strategy=name,
|
||||||
@@ -184,6 +237,7 @@ def wake(
|
|||||||
time.sleep(pause_s)
|
time.sleep(pause_s)
|
||||||
if stop_when_warm:
|
if stop_when_warm:
|
||||||
try:
|
try:
|
||||||
|
# 始终对 profile 目标窗(浮层)判暖,不是 inject 窗
|
||||||
pr = probe(prof, hwnd=h)
|
pr = probe(prof, hwnd=h)
|
||||||
if pr.warm:
|
if pr.warm:
|
||||||
steps.append(
|
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)
|
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(
|
def enum_hwnds(
|
||||||
*,
|
*,
|
||||||
class_name: Optional[str] = None,
|
class_name: Optional[str] = None,
|
||||||
|
|||||||
@@ -34,8 +34,18 @@ class ProfileTests(unittest.TestCase):
|
|||||||
self.assertIn("search_result", p.warm_markers)
|
self.assertIn("search_result", p.warm_markers)
|
||||||
self.assertIn("user_item", p.warm_markers)
|
self.assertIn("user_item", p.warm_markers)
|
||||||
self.assertFalse(p.activate_maximize)
|
self.assertFalse(p.activate_maximize)
|
||||||
self.assertIn("native_helper", p.wake_strategies)
|
# 1.2.9+:浮层禁止 n2-wake/注入(会崩企微);仅进程外软唤醒
|
||||||
self.assertIn("n2-wake", p.native_args or [])
|
self.assertFalse((p.native_hwnd_class or "").strip())
|
||||||
|
self.assertNotIn("native_helper", p.wake_strategies)
|
||||||
|
self.assertIn("wm_getobject", p.wake_strategies)
|
||||||
|
self.assertIn("msaa_from_window", p.wake_strategies)
|
||||||
|
|
||||||
|
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):
|
def test_unknown_profile(self):
|
||||||
from jiangchang_desktop_wake.errors import DesktopWakeError
|
from jiangchang_desktop_wake.errors import DesktopWakeError
|
||||||
|
|||||||
Reference in New Issue
Block a user