Skip to content

Commit 3f7f84b

Browse files
committed
chore(scripts): probe_ime_mode.ps1 强化外部 IMM32 探测
明确脚本目标是"模拟 KBLSwitch 等第三方工具的 IMM32 视角", 而不是探测 TSF compartment(compartment 是进程内状态,跨进程 物理上不可读)。 改动: - 增加 NO-IMEWND 状态:ImmGetDefaultIMEWnd 返回 0 时识别为 TSF-only 客户端(Win11 新版记事本 / Edge / UWP),如实告知 外部 probe 此场景物理不可读 - 输出进程名 + PID,便于定位 - SendMessageTimeout 替代 SendMessage,避免目标线程阻塞时 主循环卡死 - 状态变化时才输出,不再每 200ms 刷屏 - 加 -IntervalMs 参数 AGENTS.md 同步更新描述、输出样例、验证方法(区分传统 IMM32 应用与 TSF-only 应用的不同期望)。
1 parent bab5aa2 commit 3f7f84b

2 files changed

Lines changed: 113 additions & 26 deletions

File tree

scripts/AGENTS.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
|------|-------------|
1414
| `bump-version.ps1` | 版本号管理脚本:读取 VERSION 文件,按 major/minor/patch/prerelease 规则递增版本号,同步更新所有版本号引用文件(VERSION、go.mod、CMakeLists.txt 等) |
1515
| `check_band.ps1` | DWM Window Band 诊断工具:枚举系统窗口并显示各窗口的 Band 等级,用于调试 Win11 开始菜单候选框 z-order 问题和验证 HostWindow 机制 |
16-
| `probe_ime_mode.ps1` | TSF 中/英文模式探针:`WM_IME_CONTROL/IMC_GETCONVERSIONMODE` 跨线程查询前台窗口当前 IME 状态,实时输出 `CN/EN`,用于验证 `GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION` 是否正确暴露给 KBLSwitch / Win11 任务栏等外部观察者 |
16+
| `probe_ime_mode.ps1` | IME 中/英文模式外部探针(IMM32 视角):模拟 KBLSwitch 等第三方工具的探测路径,`WM_IME_CONTROL/IMC_GETCONVERSIONMODE` 跨线程查询前台窗口的 IMM32 桥接状态。`NO-IMEWND` 表示前台是 TSF-only 客户端(Win11 新版记事本 / Edge / 部分 UWP),CUAS 未建 IMM HIMC,物理上无法外部读取,需要靠功能行为验证 |
1717

1818
## Usage
1919

@@ -52,20 +52,29 @@ scripts\check_band.ps1 -All
5252
### probe_ime_mode.ps1
5353

5454
```powershell
55-
# 在另一个终端运行;保持本脚本窗口不获焦,用鼠标点击想观察的应用(cmd / Notepad++ / WPS / 浏览器等)
55+
# 默认 200ms 轮询,状态变化时输出
5656
pwsh -File scripts\probe_ime_mode.ps1
57+
58+
# 自定义轮询间隔
59+
pwsh -File scripts\probe_ime_mode.ps1 -IntervalMs 100
5760
```
5861

59-
每 200ms 输出一行,例如
62+
输出形如
6063

6164
```
62-
13:45:01.234 CN open=1 conv=0x0001 imeWnd=0x000A0188 win=[xxx.txt - Notepad++]
63-
13:45:02.451 EN open=1 conv=0x0000 imeWnd=0x000A0188 win=[xxx.txt - Notepad++]
65+
13:45:01.234 CN open=1 conv=0x0001 imeWnd=0x000A0188 pid=12345 proc=notepad++ win=[xxx.txt - Notepad++]
66+
13:45:02.451 EN open=1 conv=0x0000 imeWnd=0x000A0188 pid=12345 proc=notepad++ win=[xxx.txt - Notepad++]
67+
13:45:05.012 NO-IMEWND open=0 conv=0x0000 imeWnd=0x0 pid=23456 proc=Notepad win=[文档 1 - 记事本]
6468
```
6569

66-
- `CN/EN``IME_CMODE_NATIVE` 位决定,即我们写入的 `GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION`
67-
- 模式切换瞬间应翻转;若超过 1 秒未变或始终为同一值,说明 compartment 未正确暴露。
68-
- 注意:Win11 新版 Notepad / 部分 WinUI 应用不使用 IMM 桥,会显示 `EN/conv=0x0000` 与实际状态无关;改用 cmd、Notepad++、Chrome 等传统 IMM 应用做验证窗口。
70+
- `Mode` 取值:
71+
- `CN`:IME_CMODE_NATIVE 置位(中文)
72+
- `EN`:NATIVE 清零(英文)
73+
- `OFF`:IME 未打开
74+
- `NO-IMEWND``ImmGetDefaultIMEWnd` 返回 0,前台是 TSF-only 客户端
75+
- 验证方法:
76+
- 传统 IMM32 应用(cmd / Notepad++ / WPS / Chrome):切换中英文时 `Mode` 应立即翻转,外部第三方工具(KBLSwitch)也能正确读到。
77+
- TSF-only 应用(Win11 新版记事本 / Edge / 部分 UWP):通常显示 `NO-IMEWND`**任何外部 probe 都读不到**(compartment 是进程内状态,CUAS 也没建 IMM HIMC),这种应用 KBLSwitch 的锁定功能受系统限制无法工作 —— 此时只能靠功能行为(实际锁定是否生效)验证。
6978

7079
## For AI Agents
7180

scripts/probe_ime_mode.ps1

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,128 @@
1-
# 实时探测当前前台窗口的 IME 转换模式(IME_CMODE_NATIVE 位 = 中文)
2-
# 用法: pwsh scripts/probe_ime_mode.ps1
3-
# 然后用鼠标点击想观察的应用(记事本、浏览器、WPS 等)让其获得焦点,
4-
# 再用快捷键切中英文,看本脚本输出的 NATIVE 标志是否实时翻转。
1+
# 实时探测当前前台窗口的 IME 中英文模式(外部 / IMM32 视角)。
52
#
6-
# 与 KBLSwitch 同套路:用 ImmGetDefaultIMEWnd + WM_IME_CONTROL/IMC_GETCONVERSIONMODE
7-
# 跨线程查询,能在 Win11 新版 Notepad / TSF-only 应用里也读出来。
3+
# 目的:模拟 KBLSwitch 这类第三方工具的探测路径,验证我们 IME 的中英文状态
4+
# 是否能被外部进程读到。
5+
#
6+
# 用法:
7+
# pwsh scripts/probe_ime_mode.ps1 # 默认 200ms 轮询
8+
# pwsh scripts/probe_ime_mode.ps1 -IntervalMs 100
9+
#
10+
# 原理:
11+
# ImmGetDefaultIMEWnd(前台窗口) -> 拿到 IMM32 default IME 窗口句柄
12+
# SendMessageTimeout(WM_IME_CONTROL, IMC_GETCONVERSIONMODE) 跨线程查询
13+
# IME_CMODE_NATIVE 位 = 1 即中文模式
14+
#
15+
# 输出列含义:
16+
# Mode CN/EN/OFF/NO-IMEWND(NO-IMEWND 表示 IMM32 桥不可用,几乎可断定
17+
# 前台是 TSF-only 客户端,如 Win11 新版记事本 / Edge / 部分 UWP)
18+
# open IMC_GETOPENSTATUS 返回值(IME 是否打开)
19+
# conv IMC_GETCONVERSIONMODE 返回值(含 NATIVE/FULLSHAPE/SYMBOL 等位)
20+
# imeWnd IMM32 default IME window 句柄,0 = CUAS 没建窗口(TSF-only)
21+
# pid 前台窗口所在进程 ID
22+
# proc 前台进程名
23+
# win 窗口标题(截断 40 字符)
24+
#
25+
# 说明:
26+
# 1. 输出仅在状态变化时刷新,避免刷屏。
27+
# 2. TSF compartment 是进程内状态,**任何**外部 probe 都无法跨进程直接读取;
28+
# 对 TSF-only 应用,外部观察者只能依赖 CUAS 把 TSF 状态镜像到 IMM HIMC,
29+
# 若 CUAS 没建 HIMC(imeWnd=0),就是物理上不可读。这种情况下 KBLSwitch
30+
# 之类的工具也读不到,验证只能靠功能行为(实际锁定是否生效)。
31+
32+
[CmdletBinding()]
33+
param(
34+
[int]$IntervalMs = 200
35+
)
36+
837
Add-Type -Namespace W -Name Ime -MemberDefinition @'
938
[System.Runtime.InteropServices.DllImport("user32.dll")]
1039
public static extern System.IntPtr GetForegroundWindow();
40+
[System.Runtime.InteropServices.DllImport("user32.dll")]
41+
public static extern uint GetWindowThreadProcessId(System.IntPtr hWnd, out uint lpdwProcessId);
1142
[System.Runtime.InteropServices.DllImport("imm32.dll")]
1243
public static extern System.IntPtr ImmGetDefaultIMEWnd(System.IntPtr hWnd);
1344
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet=System.Runtime.InteropServices.CharSet.Auto)]
1445
public static extern int GetWindowText(System.IntPtr hWnd, System.Text.StringBuilder s, int n);
1546
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet=System.Runtime.InteropServices.CharSet.Auto)]
16-
public static extern System.IntPtr SendMessage(System.IntPtr hWnd, uint msg, System.IntPtr wp, System.IntPtr lp);
47+
public static extern System.IntPtr SendMessageTimeout(System.IntPtr hWnd, uint msg, System.IntPtr wp, System.IntPtr lp, uint flags, uint timeoutMs, out System.IntPtr result);
1748
'@
1849

19-
$WM_IME_CONTROL = 0x0283
50+
$WM_IME_CONTROL = 0x0283
2051
$IMC_GETCONVERSIONMODE = 0x0001
2152
$IMC_GETOPENSTATUS = 0x0005
2253
$IME_CMODE_NATIVE = 0x0001
23-
$IME_CMODE_FULLSHAPE = 0x0008
24-
$IME_CMODE_SYMBOL = 0x0400
54+
$SMTO_ABORTIFHUNG = 0x0002
2555

26-
while ($true) {
56+
function Get-ImmView {
2757
$hwnd = [W.Ime]::GetForegroundWindow()
58+
if ($hwnd -eq [IntPtr]::Zero) {
59+
return [pscustomobject]@{ Mode='?'; Open=0; Conv=0; ImeWnd=[IntPtr]::Zero; Pid=0; Proc=''; Title='' }
60+
}
61+
2862
$sb = New-Object System.Text.StringBuilder 256
2963
[W.Ime]::GetWindowText($hwnd, $sb, 256) | Out-Null
64+
$title = $sb.ToString()
65+
if ($title.Length -gt 40) { $title = $title.Substring(0, 40) }
66+
67+
$procId = 0
68+
[W.Ime]::GetWindowThreadProcessId($hwnd, [ref]$procId) | Out-Null
69+
$procName = ''
70+
try {
71+
$procName = (Get-Process -Id $procId -ErrorAction Stop).ProcessName
72+
} catch {}
3073

3174
$imeWnd = [W.Ime]::ImmGetDefaultIMEWnd($hwnd)
3275
$open = 0; $conv = 0
3376
if ($imeWnd -ne [IntPtr]::Zero) {
34-
$open = [W.Ime]::SendMessage($imeWnd, $WM_IME_CONTROL, [IntPtr]$IMC_GETOPENSTATUS, [IntPtr]::Zero).ToInt32()
35-
$conv = [W.Ime]::SendMessage($imeWnd, $WM_IME_CONTROL, [IntPtr]$IMC_GETCONVERSIONMODE, [IntPtr]::Zero).ToInt32()
77+
# SendMessageTimeout 避免目标线程阻塞时主循环卡死。
78+
$result = [IntPtr]::Zero
79+
if ([W.Ime]::SendMessageTimeout($imeWnd, $WM_IME_CONTROL, [IntPtr]$IMC_GETOPENSTATUS, [IntPtr]::Zero, $SMTO_ABORTIFHUNG, 200, [ref]$result) -ne [IntPtr]::Zero) {
80+
$open = $result.ToInt32()
81+
}
82+
if ([W.Ime]::SendMessageTimeout($imeWnd, $WM_IME_CONTROL, [IntPtr]$IMC_GETCONVERSIONMODE, [IntPtr]::Zero, $SMTO_ABORTIFHUNG, 200, [ref]$result) -ne [IntPtr]::Zero) {
83+
$conv = $result.ToInt32()
84+
}
3685
}
3786

38-
$mode = if (-not $open) {
87+
$mode = if ($imeWnd -eq [IntPtr]::Zero) {
88+
'NO-IMEWND'
89+
} elseif (-not $open) {
3990
'OFF'
4091
} elseif ($conv -band $IME_CMODE_NATIVE) {
4192
'CN'
4293
} else {
4394
'EN'
4495
}
96+
97+
return [pscustomobject]@{
98+
Mode = $mode; Open = $open; Conv = $conv; ImeWnd = $imeWnd
99+
Pid = $procId; Proc = $procName; Title = $title
100+
}
101+
}
102+
103+
Write-Host ("Probing IME mode via IMM32 path (interval={0}ms)" -f $IntervalMs) -ForegroundColor Cyan
104+
Write-Host "Ctrl+C 退出。状态变化时才输出,不刷屏。" -ForegroundColor Cyan
105+
Write-Host ""
106+
107+
$lastSignature = $null
108+
while ($true) {
45109
$stamp = (Get-Date).ToString('HH:mm:ss.fff')
46-
$title = $sb.ToString()
47-
if ($title.Length -gt 40) { $title = $title.Substring(0, 40) }
48-
"$stamp $mode open=$open conv=0x{0:X4} imeWnd=0x{1:X} win=[{2}]" -f $conv, $imeWnd.ToInt64(), $title
49-
Start-Sleep -Milliseconds 200
110+
$v = Get-ImmView
111+
112+
$signature = "{0}|{1}|{2}|{3}|{4}" -f $v.Mode, $v.Conv, $v.Open, $v.Pid, $v.Title
113+
if ($signature -ne $lastSignature) {
114+
$color = switch ($v.Mode) {
115+
'CN' { 'Green' }
116+
'EN' { 'Yellow' }
117+
'OFF' { 'DarkGray' }
118+
'NO-IMEWND' { 'Magenta' }
119+
default { 'White' }
120+
}
121+
$line = "{0} {1,-9} open={2} conv=0x{3:X4} imeWnd=0x{4:X} pid={5,-6} proc={6,-20} win=[{7}]" -f `
122+
$stamp, $v.Mode, $v.Open, $v.Conv, $v.ImeWnd.ToInt64(), $v.Pid, $v.Proc, $v.Title
123+
Write-Host $line -ForegroundColor $color
124+
$lastSignature = $signature
125+
}
126+
127+
Start-Sleep -Milliseconds $IntervalMs
50128
}

0 commit comments

Comments
 (0)