Files
StellarX-kaifa/开发记录/模块/Module-20261001-0004-阶段0B原生键盘消息隔离消费实验规格.md
T
2026-10-07 04:09:36 +08:00

33 KiB
Raw Blame History

阶段 0B 原生键盘消息隔离消费实验规格

基本信息

  • 模块 ID:Module-20261001-0004
  • 模块名称:阶段 0B 原生键盘消息隔离消费实验规格
  • 状态:0B 阶段 1 前置基线已关闭;微软拼音 Forward/Consume 有效,搜狗 Consume 请求按 ime_condition_not_met 运行时 fail-open 并形成 MustForward 结论;未建立生产消费白名单
  • 类型:新增模块 / 诊断基础设施
  • 所属系统 / 子系统:StellarX Core / Window / InputProbe / EasyX Event Loop
  • 版本 / 分支:master,0C 收口基线 e514a7d
  • 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX 25.9.10
  • 负责人:Codex 协作设计
  • 阶段定位:阶段 0 的消费行为隔离实验;执行顺序为 0A → 0C → 0B → 阶段 1。

背景与目标

背景

阶段 0A 已确认:

  • EasyX HWND/WndProc 与宿主 peekmessage 循环分属两个线程。
  • 固定键在 WndProc 与 EasyX EX_KEY 两个观察面均可出现。
  • 当前 EasyX 窗口为 ANSI,活动代码页为 CP936。
  • 0A 只观察并转发,没有验证 WndProc return 0 后 EasyX 事件面是否仍然产生相应 EX_KEY。

阶段 0C 已确认:

  • 宽字符 GDI 绘制链可用。
  • 当前 DPI-unaware 96 与 Per-Monitor-V2 144 坐标基线可用。
  • 微软拼音与搜狗候选框定位链通过当前环境人工验收。
  • 0B 可以引用可哈希的绘制/坐标基线判断环境是否可比。

阶段 1 的 InputCollector 必须决定每类消息采用 capture-and-forward 还是 capture-and-consume。如果跳过 0B,这个决定只能依赖推测;FocusManager 又依赖阶段 1 的队列和事件模型,因此 0B 是阶段 1 的硬前置,不与阶段 1 或 FocusManager 并行开发。

目标

0B 只回答一个问题:

对指定原生键盘消息,在现有 WndProc 层观察后不调用旧窗口过程、直接返回 0,是否安全;该动作是否阻断 EasyX 宿主侧对应的 EX_KEY,以及是否破坏 EasyX/IME 的既有行为。

具体目标:

  1. 对照 ObserveAndForward 与 ObserveAndConsume 两种且仅两种 WndProc 动作。
  2. 单次只改变消息类型、虚拟键、IME 状态和动作中的一个变量。
  3. 单独测量消费 WM_KEYDOWN 后真实宿主 peekmessage(EX_KEY) 是否仍返回对应事件。
  4. 分开记录 KeyDown/KeyUp,禁止把成对输入合并成一个模糊结论。
  5. 在微软拼音与搜狗的 Idle/Composition 场景中复验候选、提交、取消和定位行为。
  6. 输出阶段 1 可引用的消息级结论及证据,不直接实现生产 InputCollector 或下游路由抑制。

非目标

  • 不实现 InputCollector、NativeInputQueue、FocusManager、KeyboardRouter 或 TextBox。
  • 不决定 Button/Dialog 是否接收已转发消息。
  • 不把“转发给旧窗口过程但不再路由给控件”作为第三种 WndProc 动作。
  • 不在 0B 中新增 TranslateMessage、DispatchMessage 或额外 peekmessage。
  • 不修改 EasyX、输入法、系统键盘布局或用户输入配置。
  • 不把自动合成输入等同于真实键盘或真实 IME 验收。

分层边界

WndProc 动作:严格两值

enum class ProbeAction
{
    ObserveAndForward,
    ObserveAndConsume
};
  • ObserveAndForward:完成观察后调用旧窗口过程,原样返回旧过程的 LRESULT。
  • ObserveAndConsume:完成观察后不调用旧窗口过程,直接返回当前实验规定的消费值;首期仅允许异步键盘消息,消费值固定为 0。
  • 不允许 ForwardButSuppressRouting、ObserveOnly 等第三种动作进入枚举。

“消息必须转发给 IME,但阶段 1 不应把它路由给 Button/Dialog”属于阶段 1 的队列和路由决策,不属于 0B 的 WndProc 动作。

实验结论:三值

enum class ExperimentVerdict
{
    SafeToConsume,
    MustForward,
    Inconclusive
};
  • SafeToConsume:同环境对照有效,消费动作确实命中,自动与所需人工门槛均通过,且没有破坏 EasyX/IME 行为。
  • MustForward:消费导致输入丢失、IME/候选异常、系统行为异常,或无法达到阶段 1 所需语义。
  • Inconclusive:环境、规则、时序、证据或输入法状态存在任何不确定。

动作枚举和结论枚举不可混用。Inconclusive 不是第三种动作;运行时仍必须 fail-open 到 ObserveAndForward。

不变量

  1. 生产默认路径永远没有消费规则。
  2. 0B 必须同时满足编译期开关、0A 运行开关和独立 0B 运行开关才可装载规则。
  3. 每个进程只允许一条规则;每轮实验只允许一个目标消息/虚拟键/IME 条件组合。
  4. 规则未命中时调用旧窗口过程并原样返回。
  5. 规则配置、环境、IME 状态或日志完整性存在不确定时调用旧窗口过程并原样返回。
  6. 同步窗口管理消息禁止进入消费规则表。
  7. WndProc 线程不写文件、不等待宿主线程、不调用新的消息泵 API。
  8. 0B 复用 0A 的跨线程序列、缓冲、flush 和隐私边界,不另建不可比较的日志格式。
  9. sequence 只描述探测器观察顺序,不伪装成 Windows 队列序号。
  10. 任何 records_lost 都使相关消费结论降级为 Inconclusive。

规则模型

struct ProbeConsumeRule
{
    std::string experimentId;
    std::string ruleId;
    UINT message;
    std::optional<UINT> virtualKey;
    std::optional<std::uint8_t> scanCode;
    ImeStateCondition imeCondition;  // Any / IdleOnly / ComposingOnly
    ProbeAction action;              // ObserveAndForward / ObserveAndConsume
    std::uint32_t maxMatches;
    std::string baselineId;
    InputSource inputSource;         // SendInput / Manual
};

首期规则只接受:

WM_KEYDOWN
WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP

原因是首个核心问题是 WndProc 物理键消息与 EasyX EX_KEY 的关系。WM_CHAR/WM_SYSCHAR/WM_IME_CHAR 涉及 ANSI/DBCS、隐私和 IME 回声状态机,在首期键消息结论稳定后再按同一 schema 扩展,不与物理键首轮混测。

下列消息永远不得加入 0B 消费规则:

WM_GETDLGCODE
WM_IME_SETCONTEXT
WM_UNICHAR + UNICODE_NOCHAR
WM_SETCURSOR
WM_MOUSEACTIVATE
WM_INPUTLANGCHANGEREQUEST
WM_INPUTLANGCHANGE
WM_CLOSE / WM_DESTROY / WM_NCDESTROY

规则配置

诊断构建通过以下环境字段装载唯一规则:

STELLARX_INPUT_PROBE=1
STELLARX_INPUT_PROBE_0B=1
STELLARX_INPUT_PROBE_0B_EXPERIMENT_ID=<安全标识>
STELLARX_INPUT_PROBE_0B_RULE_ID=<安全标识>
STELLARX_INPUT_PROBE_0B_ACTION=ObserveAndForward|ObserveAndConsume
STELLARX_INPUT_PROBE_0B_MESSAGE=WM_KEYDOWN|WM_KEYUP|WM_SYSKEYDOWN|WM_SYSKEYUP
STELLARX_INPUT_PROBE_0B_VIRTUAL_KEY=<十进制或 0x 十六进制>
STELLARX_INPUT_PROBE_0B_SCAN_CODE=<0..255;256 表示任意>
STELLARX_INPUT_PROBE_0B_IME_CONDITION=Any|IdleOnly|ComposingOnly
STELLARX_INPUT_PROBE_0B_MAX_MATCHES=<正整数,小上限>
STELLARX_INPUT_PROBE_0B_INPUT_SOURCE=SendInput|Manual
STELLARX_INPUT_PROBE_0B_BASELINE_ID=<0C combinedBaselineId>
STELLARX_INPUT_PROBE_0B_EXPECT_WINDOW_UNICODE=0|1
STELLARX_INPUT_PROBE_0B_EXPECT_ANSI_CODE_PAGE=<正整数>
STELLARX_INPUT_PROBE_0B_EXPECT_DPI_FOR_WINDOW=<正整数>
STELLARX_INPUT_PROBE_0B_EXPECT_DPI_AWARENESS=<枚举整数>

Composition 人工实验还使用固定、非文本的客户区锚点配置:

STELLARX_INPUT_PROBE_0B_IME_CARET_X/Y
STELLARX_INPUT_PROBE_0B_IME_LINE_LEFT/TOP/RIGHT/BOTTOM

当前夹具固定为 720×420,caret point 为 (225,282),排除矩形为 (200,255)-(680,284)。探测器在焦点/IME context 建立以及组合生命周期中重复调用 ImmSetCompositionWindow(CFS_POINT) 与 ImmSetCandidateWindow(CFS_EXCLUDE);两个 setter 至少一次同时成功,才允许把 Composition 消费实验视为位置基线可用。

experimentId、ruleId 只允许 ASCII 字母、数字、点、下划线和连字符;不得把用户输入或候选文字写入标识字段。

0C 基线引用契约

0B runner 必须读取 0C 的 baseline-set-v1.json,并在启动夹具前验证:

  1. baseline sidecar SHA-256 与文件一致。
  2. drawingBaselineManifest 和 coordinateBaselineManifest 存在。
  3. 两份 manifest 的 SHA-256 与 baseline 内记录一致。
  4. 用 drawingSha256 + ':' + coordinateSha256 重新计算的组合值等于 combinedBaselineId。
  5. automatedStatus=pass。
  6. DPI awareness、DPI、窗口 A/W 属性与本轮期望一致。

当前可用基线为:

drawingBaselineSha256    BCD0E3DD5D90C5D6F892F9127D81FD124C1DC7D619260B236DBCB8E53CBEC9CA
coordinateBaselineSha256 35D15BF2DD54D9A57B1F8470C8415025F1BAE6CE98D4628F13B64AB1EC381000
combinedBaselineId       sha256:5C544D9FEC9C1A2324A7AF3F593A1A34110100644BAF5071E777302387E3C6C5
dpiAwareness             unaware
dpiForWindow             96
windowUnicode            false
ansiCodePage             936(来自 0A 同环境证据)

0C 基线不自动证明实际 TSF 输入配置文件身份。Composition 人工实验还必须记录操作者确认的实际输入法名称和身份来源;脚本参数只能表示预期名称。

fail-open 硬规则

出现以下任一情况时,规则不得消费,实际动作必须退回 ObserveAndForward:

  1. 配置缺失、无法解析、超长或出现未知枚举值。
  2. 规则消息不在首期允许列表。
  3. 虚拟键越界、maxMatches 为 0 或超过上限。
  4. 同一进程出现多条规则或同一事件存在模糊匹配。
  5. baseline ID、DPI、DPI awareness、窗口 A/W 属性或代码页不匹配。
  6. Composition 实验的实际输入法身份未经操作者确认。
  7. IME 状态条件无法由当前证据可靠确认。
  8. Composition 消费实验尚未成功设置固定组合窗和候选窗位置。
  9. 已发生缓冲溢出、采集丢失或序列不完整。
  10. 规则命中次数已经达到 maxMatches。
  11. 目标 HWND、窗口实例或线程归属与会话 attach 记录不一致。
  12. 任意同步窗口管理消息或未识别消息。

fail-open 必须在 WndProc 决策前完成;不能先消费、事后再把结果标记为 Inconclusive。

日志 schema

0B 是 0A schema 的可加性 v2,不新建平行日志:

  • 被动 0A 会话仍可写 schema v1。
  • 启用 0B 时会话写 schema v2。
  • v2 保留 v1 全部公共字段和 record type,并新增可选实验字段。

新增 experiment_config 记录:

字段 说明
experimentId/ruleId 本轮实验与唯一规则标识
requestedAction 两值动作之一
targetMessage/targetVirtualKey/targetScanCode 唯一原生目标;scan code 可为 null 表示任意
imeCondition any/idle_only/composing_only
maxMatches 最大命中次数
inputSource SendInput/manual
baselineId 引用的 0C 组合基线
configurationValid 配置是否合法
baselineMatched 运行环境是否匹配
ruleEligible 是否允许进入匹配
failOpenReason 结构化原因;无问题为 none
imePositionConfigured/imeCaretPoint/imeLineRect Composition 人工实验的固定 IMM32 客户区锚点

目标 wndproc_enter 增加:

experimentId
ruleId
ruleMatched
matchOrdinal
requestedAction
actualAction
failOpenApplied
failOpenReason

对应 wndproc_leave 增加相同关联字段,并继续保留:

wndProcCallId
forwarded
forwardResultHex

easyx_key 增加 experimentId/ruleId 会话标签,但不伪造与 WndProc 的直接 ID 关联。跨线程关联由受控实验协议、目标消息、虚拟键、方向、scan code、QPC 区间和序列窗口共同完成。

IMM32 定位结果使用独立 ime_position 记录:

字段 说明
parentSequence 触发定位请求且已经入队的 wndproc_enter.sequence
wndProcCallId 触发定位的窗口过程调用
triggerMessage WM_SETFOCUS/WM_IME_SETCONTEXT/WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION
imePositionContextAvailable 是否取得当前 HWND 的 HIMC
compositionPositionSucceeded/candidatePositionSucceeded 两个 setter 的独立结果
imeCaretPoint/imeLineRect 本次使用的不可变客户区坐标快照

ime_position 必须在 IMM32 调用完成后再分配序号,禁止预留低序号后跨锁补写;setter 同步重入产生的消息应自然排在父 enter 与结果记录之间。分析器必须校验 parentSequence 存在且小于结果序号。

WndProc 执行语义

WndProc enter
  → 0A 解析并记录原始消息
  → 0B 校验规则、基线、IME 条件、丢失状态和命中上限
  → 实际动作 ObserveAndForward
       → CallWindowProc(oldWndProc)
       → leave(forwarded=true, 原样 result)
  → 实际动作 ObserveAndConsume
       → 不调用 oldWndProc
       → leave(forwarded=false, result=0)
       → return 0

只有成功写入目标 enter 记录且决策证据完整时才允许消费。采集器使用两个固定记录缓冲:安全点在锁内只交换 active buffer、清空新 active 的索引,文件序列化在锁外完成;WndProc 使用极短的阻塞内存临界区,不再因 try-lock 失败主动丢弃观察。缓冲覆盖等任何实际丢失仍必须写 records_lost,并使实验失效。

固定 IME 定位严格执行三阶段协议:锁内提交父 enter 并复制请求,锁外调用全部定位相关 IMM32 API,再次加锁校验 enabled + attachedHwnd + hwndInstanceId 后提交结果。禁止在状态锁内调用 ImmGetContext、两个 setter 或 ImmReleaseContext。定位 API 调用期间使用 WndProc 线程局部标志阻止重入消息递归发起定位;重入消息本身仍按正常路径观察和转发。

EX_KEY 核心测量

必答问题

在 WndProc 对目标 WM_KEYDOWN 执行 ObserveAndConsume 后,宿主线程现有的 peekmessage(&msg, EX_KEY, true) 是否仍返回同一虚拟键的 KeyDown?

该问题必须作为独立结果字段,而不是隐藏在总通过状态中:

easyxVisibility = present_once | present_multiple | absent | inconclusive

解释:

  • absent:WndProc 消费阻断了 EasyX 键盘面;阶段 1可以评估以采集队列作为唯一业务输入源。
  • present_once/present_multiple:return 0 不足以消除 EasyX 事件;阶段 1 必须显式排空或忽略 EasyX 键盘事件,不能依赖消费避免双触发。
  • inconclusive:对照缺失、额外输入污染、关联不唯一或日志丢失。

受控关联协议

每轮自动实验:

  1. 启动独立进程和唯一 experimentId。
  2. 等待 HWND、探测 attach 和空闲窗口稳定。
  3. 在启用目标输入前排空历史事件;探针不得为实验新增第二个 peekmessage。
  4. 只用一次 SendInput 产生固定虚拟键的 Down/Up。
  5. 规则只匹配其中一个方向,另一个方向作为普通转发事件。
  6. 等待宿主安全点 flush 后关闭窗口。
  7. 使用目标 message、vk、可选 scan code、方向和受控时间窗关联 WndProc 与 easyx_key;指定 scan code 时,WndProc 和 EasyX 两个观察面都必须匹配该值。
  8. 对照组和消费组分别使用新进程,禁止在同一进程热切换 action。

自动注入标记为 inputSource=SendInput。PostMessage 只可用于结构烟测,不能作为物理键/IME 行为结论。

实验矩阵

0B-1:核心自动 A/B

第一门槛固定为 Idle 状态的 F24:

组 消息 VK 动作 目标
Control WM_KEYDOWN VK_F24 Forward 证明对照能同时看到 WndProc 和 EX_KEY
Experiment WM_KEYDOWN VK_F24 Consume 判断消费后对应 EX_KEY 是否仍出现
Control WM_KEYUP VK_F24 Forward 建立 KeyUp 对照
Experiment WM_KEYUP VK_F24 Consume 判断 KeyUp 是否独立受影响

最初候选 F8 在当前中文 IME 环境中不适合作为自动门槛:SendInput 的 F8 KeyDown 没有到达目标 WndProc,仅观察到 KeyUp。当前证据没有确认它在哪一层被截断,也不据此推断 EasyX 消费关系;因此固定键改为当前环境中可稳定通过 WndProc 与 EX_KEY 两个观察面的 F24。

0B-1 当前自动结论(2026-10-06)

目标 Forward EX_KEY Consume EX_KEY 自动状态 当前结论
WM_KEYDOWN + VK_F24 + IdleOnly present_once absent pass WndProc return 0 会阻断对应 EasyX KeyDown
WM_KEYUP + VK_F24 + IdleOnly present_once absent pass WndProc return 0 会阻断对应 EasyX KeyUp
错误 baseline + 请求 Consume present_once(实际 Forward) 不适用 pass baseline_id_mismatch 正确 fail-open,规则零命中、零消费

三轮有效证据均满足:sequenceStrict=true、recordsLost=0、目标观察唯一、closeObserved=true、sessionEndObserved=true。证据如下(TestResults 为本地忽略目录,不进入源码提交):

KeyDown A/B
TestResults/InputProbe/0B/20261006-103302-713-f22cbc3c-4d54-4da6-a3da-9b32cd0aeb43/comparison-v1.json
SHA-256 55B2160CF84594D653B5BF4B71A8DA8E9E2FF9795EB88BC16BA0F61E94DCD440

KeyUp A/B
TestResults/InputProbe/0B/20261006-103644-778-f4086f15-fbd7-4328-ac4c-fda378ef4594/comparison-v1.json
SHA-256 7A1381E8E6B13B3808754863DCD88B38B07D8E79630614A8ACB75649DE780194

baseline mismatch fail-open
TestResults/InputProbe/0B/20261006-103714-598-d0f4b1f1-dc03-47da-a039-ca2cb7523f7b/comparison-v1.json
SHA-256 A69A3646ECBA1FCACE921B15DB6A82FCD27F4E9B1747767DA500310250D7D520

上述证据只回答 F24 的 WndProc/EX_KEY 关系,尚不足以把任何生产消息判为 SafeToConsume;普通键、导航键、快捷键和真实 IME 人工矩阵完成前,最终 verdict 仍为 Inconclusive。

随后再扩展到:

  • 普通字母键。
  • Tab、方向键、Enter、Escape、Space。
  • Ctrl 组合键。
  • WM_SYSKEYDOWN/UP,但不得用会改变系统状态或关闭窗口的组合。

0B-2:自动稳定性

每轮同时验证:

  • 进程、HWND 和宿主循环正常存活。
  • 鼠标、窗口关闭和 resize 路径未受规则影响。
  • enter/leave 配对符合实际动作。
  • sequence 严格递增。
  • 无原始文字泄露。
  • 无未记账丢失。
  • 对照与消费运行引用相同 0C baseline ID。
  • runner 必须通过 GetWindowThreadProcessId 跟踪真实 UiFixture 进程;cmd /c 仅是环境变量启动包装,不能作为退出或超时判断对象。
  • EasyX 旧窗口过程可能在 WM_CLOSE 内直接终止进程。探测构建须在转发 WM_CLOSE 前完成最后一次耐久 detach,写入 window_detach/session_end。
  • 宿主 flush 与 WndProc 关闭收口必须由独立文件写锁串行,统一采用“文件写锁 → 状态锁”的锁序,禁止跨线程快照乱序落盘。
  • 安全点不得在状态锁内复制或写出整批记录;双固定缓冲的交换必须为常数时间,避免 WndProc 因宿主 flush 占锁而丢失观察。
  • 不得在状态锁内调用可能同步重入窗口过程的 IMM32 setter;ime_position 父序号关联失败时实验必须判为 inconclusive_ime_position_link。

0B-3:真实 IME 人工 A/B

微软拼音和搜狗分别测试,Idle 与 Composition 分开;每次只启用一个规则。固定非敏感测试串沿用 ceshi。

至少覆盖:

  • 组合开始前的普通字母键。
  • 候选态方向键和数字选择键。
  • Enter 提交。
  • Escape 取消。
  • Space 选择候选。
  • Ctrl 快捷键。
  • 候选框位置是否仍符合 0C 三 anchor 基线。
  • 提交后切换 anchor 是否出现 stale position。

Composition 条件只在日志确实观察到 composing 状态时匹配;如果某 IME 已显示候选 UI 但没有向目标 HWND 提供足够生命周期证据,规则必须 fail-open,不能按 Idle 误消费。单次证据不足仍为 Inconclusive;如果受控人工运行已经同时证明候选 UI 存在、目标键到达且 Composition 条件不可可靠确认,则该消息/IME 组合形成 MustForward:生产路径必须转发给旧窗口过程,并在阶段 1 抑制其进入 Button、Dialog 和焦点导航等业务路由。

0B-3 首轮夹具发现(2026-10-06)

首轮微软拼音与搜狗测试不能作为正式 A/B 证据,原因不是两个输入法都定位失败,而是夹具本身存在三项诊断缺陷:

  1. input-consume-probe 未调用两个 IMM32 定位 setter,候选框落在桌面左下角;这与已通过的 0C 坐标链不一致。
  2. 操作者按 Escape 时,组合态 WndProc 实际看到的是 WM_KEYDOWN + VK_PROCESSKEY(229) + scanCode 1;WM_KEYUP 才恢复为 VK_ESCAPE(27)。因此原规则 WM_KEYDOWN + VK_ESCAPE 不可能命中。
  3. 运行出现 3 条 wndproc_lock_contention,recordsLost=3,按本规格必须判为 inconclusive_capture_loss。

微软文档说明,IME 处理后会把虚拟键改为 VK_PROCESSKEY;应用只有在自己的消息循环中、调用 TranslateMessage 之前才能用 ImmGetVirtualKey 恢复原 VK。StellarX 当前钩子位于 EasyX 的 WndProc,时机已经晚于该边界,因此 0B 不伪造“逻辑 Escape”,而是让规则匹配实际可观察的原生三元组;操作者指令仍写“按 Escape”。参考:https://learn.microsoft.com/windows/win32/api/immdev/nf-immdev-immgetvirtualkey。

无效运行保留为夹具缺陷证据:

TestResults/InputProbe/0B/20261006-140720-218-96b7392e-548a-44cf-9343-5d1449bf73fa
manual-evidence/composition-candidate-position-wrong.png
SHA-256 61E4A04CEE329B13BB22C919DC233A07ABF3EF6C298E64FB7B6E70F6D60DE0A3

该截图只证明候选框位于夹具窗口之外;运行混入多次组合且存在记录丢失,不用于区分微软拼音与搜狗,也不用于判断消费是否安全。

0B-3 首次修复复验发现(2026-10-06)

首次微软拼音 Forward 复验出现窗口未响应和无法输入。根因是定位实现持有不可重入的 SRW 状态锁时调用两个 IMM32 setter;setter 同步重入 WndProc 后再次获取同一把锁,形成线程自锁。失败运行保留于:

TestResults/InputProbe/0B/20261006-231604-636-4bc775a1-a3f8-41b9-b466-1cce740bc195/forward

该日志耐久保存 268 条严格递增、零丢失记录,但没有关闭收口;卡死发生在下一次定位进入且早于新记录入队是结合调用链得出的推断,不把它表述为日志直接证明的栈。修复拆成锁内请求、锁外 IMM32、锁内结果,并增加递归定位保护;详见 BUG-20261006-0003 / Fix-BUG-20261006-0003。

修复后的微软拼音 Forward 运行已经通过:

TestResults/InputProbe/0B/20261006-234544-007-603391f8-3a14-4c90-ab41-4d891ceb8f46/forward
comparison SHA-256 7CBEA9089E69BE921829BE49FC74B3F75E6BDAB8BB6DD252F4D4F1D6338AA456C
  • experimentValidity=valid、sequenceStrict=true、recordsLost=0。
  • Escape 原生规则唯一命中,ObserveAndForward leave 完整,对应 EasyX 键可见一次。
  • imePositionAttemptCount=18、imePositionSuccessCount=18、imePositionOrphanCount=0。
  • 组合开始/结束各 2 次,GCS 结果 1 次,关闭和 session_end 完整。
  • 人工截图 manual-evidence/microsoft-pinyin-forward-composition-position-pass.png 确认候选 UI 位于固定锚点附近;SHA-256 1447E9F1AAD2C90A787AADC2594C78AE596336FC6D569EA1A22C69B2FCA5C3D5。

该 Forward 运行关闭 BUG-20261006-0003;随后继续执行微软拼音 Consume 与搜狗运行,以形成消息消费 verdict。

微软拼音 Consume 第一次操作只完成了候选提交,没有按 Escape。日志中目标 scan code 1 计数为 0,runner 正确判为 unexpected_target_count;该运行只保留位置/响应证据,不计入 Consume 结论:

TestResults/InputProbe/0B/20261006-235044-603-9c996ff5-f85b-4127-aac0-68d9f0b89908/consume
manual-evidence/microsoft-pinyin-consume-position-only-no-escape.png
SHA-256 B7B218BEDD343A8E2023A6FE18FC678E5488CB17B1C857D46A254EC06F9095FB

按“候选显示时只按一次 Escape”重跑后的微软拼音 Consume 有效:

TestResults/InputProbe/0B/20261007-000152-550-4c83bcda-12e8-4f4e-b616-1d1fe1f56a27/consume
comparison SHA-256 BDB2E202EE7B050D74E6989208D7A63C2D74766292BC4410F5020213787CB698
  • experimentValidity=valid、915 条记录严格递增、recordsLost=0。
  • Escape 目标唯一命中,consumedMatchCount=1、forwardedMatchCount=0。
  • 对应 EasyX 键为 absent,结构关系为 easyx_suppressed_by_wndproc_consume。
  • imePosition=7/7、imePositionOrphanCount=0,关闭与 session_end 完整。
  • successfulResultCount=0,与本轮执行取消而非提交一致;操作者完成候选消失检查后关闭窗口,未报告位置或响应异常。

微软拼音 A/B 当前对照为:Forward 时目标键进入 EasyX 一次,Consume 时目标键在 EasyX 面消失;两轮 IME 定位与窗口响应均正常。该结论仍限定于微软拼音和当前 ANSI/CP936、96 DPI 基线。

0B-3 搜狗生命周期缺失与 MustForward 结论(2026-10-07)

搜狗 Forward 受控运行中,候选 UI 实际可见且位置正常,但目标 HWND 没有收到 WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION/WM_IME_ENDCOMPOSITION。操作者按 Escape 时仍观察到唯一的 WM_KEYDOWN + VK_PROCESSKEY(229) + scanCode 1,探测状态为 idle,因此 ComposingOnly 规则没有命中:

TestResults/InputProbe/0B/20261007-000604-592-4ddb4314-769e-4c4a-b3e8-696bd0489956/forward
JSONL SHA-256 539BE9BC378EB86825A1040D2A39BF7A1FD3C7F7C07966B67BBC753EB7F83608
  • 1181 条记录严格递增、recordsLost=0、定位 6/6、无孤立定位结果、关闭收口完整。
  • experimentValidity=unexpected_match_count 准确表示“请求的 ComposingOnly 条件不可证明”,不表示候选 UI 或 Forward 行为异常。
  • 对应 EasyX 键为 present_once;该轮不得改写为 IdleOnly 后强行消费。

最终轮显式请求 ObserveAndConsume,并把预期运行时退让原因固定为 ime_condition_not_met:

TestResults/InputProbe/0B/20261007-033454-763-56a2630b-bf2a-4431-b501-38f6ff977dfe/consume
comparison SHA-256 0FD8E46493A6D84FD8C0BAD55D22CDB55B48F13BE94EB2831D39E14B7993FBEC
  • experimentValidity=runtime_fail_open,目标观察唯一、规则命中 0 次、消费 0 次。
  • enter/leave 的唯一原因均为 ime_condition_not_met,实际动作退回 ObserveAndForward,旧窗口过程被调用。
  • failOpenForwardedCount=1,对应 EasyX 键为 present_once,结构关系为 easyx_visible_after_fail_open_forward。
  • 582 条记录严格递增、recordsLost=0、定位 3/3、imePositionOrphanCount=0、关闭与 session_end 完整。
  • 最终 verdict 为 MustForward。这里的“有效”是指 fail-open 契约得到验证,不是搜狗真实执行过 Consume。

阶段 1 因此采用保守且跨 IME 一致的基线:原生键默认采集并转发;VK_PROCESSKEY 和已确认组合态键不得路由到 Button、Dialog、快捷键或焦点导航。微软拼音的单点 Consume 成功只证明 WndProc 消费会阻断对应 EasyX 键面,不足以建立生产消费白名单。

0B-4:结论回写

每个消息/虚拟键/IME 状态组合必须输出:

controlSessionId
consumeSessionId
baselineId
easyxVisibility
imeBehavior
windowBehavior
verdict
evidenceHashes
knownScope

只有完成所需自动和人工矩阵的组合才可判定 SafeToConsume;其余保持 Inconclusive 或因明确破坏判定 MustForward。

验收门槛

自动运行有效必须同时满足:

  1. experiment_config.ruleEligible=true。
  2. 正常 A/B 的目标规则恰好命中预期次数;预期运行时 fail-open 的目标观察次数必须符合 maxMatches、规则命中数必须为 0。
  3. Forward 组对应 leave 为 forwarded=true。
  4. 真正 Consume 组对应 leave 为 forwarded=false 且返回 0;预期 fail-open 组必须为 actualAction=ObserveAndForward、forwarded=true。
  5. Forward 对照组至少出现一次对应 easyx_key。
  6. Consume 或运行时 fail-open 组明确得到 present_once/present_multiple/absent 之一。
  7. sequence 严格递增,enter/leave 可配对。
  8. records_lost=0,无缓冲溢出或锁竞争丢失。
  9. 原生关闭与 session_end 完整。
  10. 两组 baseline ID 和关键环境字段一致。
  11. 运行时 fail-open 的 enter/leave 原因必须唯一且一致,分析结果必须为 runtime_fail_open;原因不明或半边错配时仍为 Inconclusive。

人工 IME 运行有效还要求:

  1. 操作者明确确认实际输入法身份。
  2. 使用固定非敏感输入。
  3. 记录候选、提交、取消是否正常。
  4. 位置判断引用相同环境的 0C 基线。
  5. 日志至少有一次 ImmGetContext、composition setter 与 candidate setter 同时成功,且 caret/line rect 等于固定夹具锚点。
  6. 自动失败不能被人工通过覆盖。

隐私与安全

  • 沿用 0A:文字只记来源、长度、结构和会话摘要,不记原文。
  • 0B 配置不允许携带用户输入文本。
  • 自动测试使用 F24 等非文字键作为第一门槛;当前环境不使用 KeyDown 未到达目标 WndProc 的 F8。
  • 人工 IME 只使用约定的固定非敏感测试串。
  • 截图只包含探测窗口和候选 UI,不包含其他应用。
  • 无论探测错误、日志错误还是环境错误,默认行为都是转发。

实现与影响

  • 已复用并实现:
    • src/StellarX/SxInputProbe.cpp
    • src/StellarX/internal/SxInputProbeInternal.h
    • src/StellarX/Window.cpp
    • tests/UiFixture.cpp
    • scripts/analyze-input-probe.ps1
  • 已新增:
    • scripts/run-input-consume-probe.ps1:自动 A/B、单规则人工模式、0C baseline 校验、真实进程跟踪和 comparison 输出。
    • scripts/run-input-consume-probe-manual.ps1:真实 IME 人工单规则入口和固定 ceshi checklist。
    • 分析摘要中的实验有效性、easyxVisibility、关闭完整性和 fail-open 证据字段。
    • 可选 scan code 精确规则、固定 IME caret/line rect、IMM32 定位调用及位置成功计数。
    • 双固定记录缓冲;安全点常数时间交换、锁外文件写入,取消 WndProc try-lock 丢记录路径。
    • IMM32 定位三阶段拆锁、线程内递归定位保护、独立 ime_position 结果记录与 parentSequence 校验。
  • 不修改公共 API。
  • 不新增生产焦点或键盘路由行为。
  • 探测关闭时不产生文件、线程、定时器或消息返回差异。

测试与验证

  • F24 WM_KEYDOWN/UP 的 Forward/Consume 自动 A/B 已完成,均直接报告消费后 EX_KEY=absent。
  • 错误 baseline 的请求 Consume 负向实验已完成,实际动作正确 fail-open 为 Forward。
  • 默认 Debug x64 构建与现有单元测试通过(15/15);探测开关构建已由三轮最终自动实验覆盖。
  • 三份 PowerShell 脚本在 Windows PowerShell 5.1.22000.2538 与 PowerShell 7.6.5 下均为零解析错误;git diff --check 无空白错误,仅有仓库换行规范提示。
  • 0B-3 重入死锁修复后的微软拼音 Forward/Consume 已通过;搜狗候选态未暴露可证明的 Composition 生命周期,最终 Consume 请求按 ime_condition_not_met 正确 fail-open 为 Forward,形成 MustForward 结论。
  • 2026-10-07 最终回归:Debug x64 单元测试 15/15、UI fixture 1 轮通过;F24 KeyDown A/B 新运行通过(comparison SHA-256 1C1E4EDA1251A460AC250AE57F4F8A5FC8AFE4A9C0EDA87C731BA3C6301FB6FF),既有 KeyUp 与 baseline mismatch 证据使用当前分析器回放仍分别为 valid 与 ineligible_fail_open。
  • 阶段 1 可以按“默认采集并转发、EasyX EX_KEY 只排空、VK_PROCESSKEY 与已确认组合态键不进入业务路由”的基线开始;任何生产消费白名单仍需新的逐键证据。
  • 构建至少覆盖 Debug x64 探测开关开启、默认关闭构建和现有 15 个单元测试。
  • PowerShell 脚本必须同时通过 Windows PowerShell 5.1 与可用的 PowerShell 7 解析检查。

已知限制

  • 自动 SendInput 不等同于物理键盘。
  • 当前中文 IME 环境下,自动 F8 KeyDown 未到达目标 WndProc;截断层级尚未确认,因此 F8 运行只作为“门槛键选择失败”记录,不作为消费行为证据。
  • 相同中文 HKL 下的 TSF 输入配置文件不能只靠 HKL 自动区分。
  • 当前 0C 基线没有覆盖 100%、125% 和跨屏;不同环境的 0B IME 位置结论必须为 Inconclusive。
  • 首期不消费文字和 IME 消息,只先回答物理键与 EasyX EX_KEY 的核心关系。
  • WndProc 层只能可靠记录 VK_PROCESSKEY 与 scan code,不能在 EasyX 已调用 TranslateMessage 后再依赖 ImmGetVirtualKey 恢复原始逻辑键。
  • 搜狗候选 UI 可见时可能不向目标 HWND 发送标准 Composition 生命周期;不得把 imeState=idle 推断为“当前键不属于 IME”。

落地信息