Files
StellarX-kaifa/开发记录/模块/Module-20261001-0002-阶段0A输入链路被动探测规格.md

42 KiB
Raw Permalink Blame History

新增功能模块 / 模块重构

适用场景:新增模块、重大模块重构、核心架构能力演进。 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。

基本信息

  • 模块 ID:Module-20261001-0002
  • 模块名称:阶段 0A 输入链路被动探测规格
  • 状态:已完成(0A 核心与微软拼音/搜狗人工验证已完成;0B 为后续独立阶段)
  • 类型:新增模块 / 诊断基础设施
  • 所属系统 / 子系统:StellarX Core / Window / Input Probe
  • 版本 / 分支:master,阶段 0A 实现基线 c218c25
  • 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX + IMM32
  • 负责人:Codex 协作设计

背景与目标

  • 背景:
    • 通用输入、焦点和键盘路由设计需要建立在 EasyX 真实消息链路之上。
    • 当前尚不确定 EasyX 是否以及何时调用 TranslateMessage,WndProc 与 EasyX EX_KEY 是否重复报告,普通循环和模态循环是否一致,以及 IME 结果消息是否形成回声。
    • 在没有证据前直接实现消费策略、去重状态机或 TextBox 输入链,会把推断写成框架行为。
  • 当前痛点:
    • 调试器逐条查看消息无法稳定覆盖完整输入批次和模态重入。
    • 直接记录文字原文存在隐私风险。
    • 在 WndProc 内同步写盘会扰动需要观察的消息时序。
    • “为了探测额外读取 EasyX 消息”本身会改变被测对象。
  • 目标:
    • 定义阶段 0A 的版本化日志 schema、观察点、字段、缓冲、flush 和隐私规则。
    • 被动回答 WndProc、EasyX、IMM32 和宿主事件循环之间的顺序与重复关系。
    • 为阶段 0B 的逐消息消费实验和阶段 1 InputCollector 提供共同输入。
    • 保证探测开启时业务语义透明,并把不可避免的时序扰动降到最低。
  • 非目标:
    • 第一切片不实现 IMM 结果串摘要、在线回声关联和真实中文 IME 结论;这些仍需后续切片与人工输入验证。
    • 0A 不消费任何消息,不修改任何 WndProc 参数或返回值。
    • 0A 不新增焦点、键盘路由、TextBox 编辑或 IME 控制行为。
    • 0A 不验证消费消息的影响;该问题属于阶段 0B。
    • 日志不作为用户输入内容审计或遥测系统。

模块边界

  • 职责:
    • 定义探测会话、消息记录、来源后端和关联字段。
    • 定义 Key lParam、IME flags、窗口属性和 EasyX 消息的解析结果。
    • 定义文本负载的隐私化摘要和在线回声关联结果。
    • 定义固定内存缓冲、丢失记录、逻辑 flush 与耐久 flush 边界。
    • 定义 JSON Lines 输出格式、文件生命周期和 schema 版本。
  • 不负责什么:
    • 不决定生产输入队列的数据结构。
    • 不决定最终消费白名单。
    • 不在 WndProc 中执行磁盘 I/O、JSON 序列化或阻塞等待。
    • 不为观察 EasyX 后端而额外调用 peekmessage。
    • 不使用日志内容还原用户输入原文。
  • 外部依赖:
    • 现有 Window WndProc 子类化链。
    • 现有 EasyX peekmessage / ExMessage 消费位置。
    • IMM32 只读查询。
    • Windows 高精度计时和安全随机数能力。
  • 对外能力 / API:
    • 本文只定义内部诊断格式,不新增框架公共 API。
    • 探测器必须由测试/诊断构建开关显式启用,正式默认关闭。
  • 关键数据 / 状态:
    • probeSessionId、schemaVersion、全会话 sequence。
    • hwndInstanceId、wndProcCallId、parentSequence、callDepth。
    • 固定容量记录缓冲、flush 请求原因和丢失范围。
    • 会话级随机摘要密钥,仅驻留内存,不写入日志。

设计说明

1. 语义透明原则

阶段 0A 的“被动”定义为业务语义透明,而不是宣称完全没有时间成本。

启用 0A 后必须保持:

  1. 传给旧窗口过程的 hwnd/message/wParam/lParam 原样不变。
  2. 每条原生消息调用旧窗口过程的次数不变。
  3. WndProc 对调用方返回的值不变。
  4. 原有 EasyX 消息读取次数、过滤条件和顺序不变。
  5. 不额外调用 TranslateMessage、DispatchMessage、PeekMessage 或 EasyX peekmessage。
  6. 不调用会改变 IME 状态的 API,包括 ImmNotifyIME、ImmSetCompositionWindow 和 ImmSetCandidateWindow。
  7. 只允许执行读取型窗口/IME 查询和内存记录。
  8. 日志失败不能改变窗口、输入或控件行为。

探测记录不可避免会产生少量 CPU、内存和时间开销,因此所有结论都要标注“探测构建观察结果”,必要时与关闭探测的基线行为对照。

2. 观察后端与观察点

sourceBackend 表示记录来自哪个观察后端,不等同于系统消息的真实起源。

sourceBackend 观察点 允许动作 禁止动作
Win32WndProc 现有 WndProc 入口、旧过程返回后 解析参数、配对进入/离开、记录返回值 修改参数、吞消息、改变返回值
EasyXExKey 现有主循环已经取出 EX_KEY 的位置 记录现有 ExMessage 为探测新增 peekmessage
Imm32Query 处理现有 IME 消息时的只读查询 查询长度、状态、组合/结果摘要 完成、取消、转换或移动 IME UI
HostEventLoop 一次宿主消息/输入批次返回后的安全点 flush、记录批次边界 在仍持有控件借用指针时写盘

如果同一输入同时被 WndProc 和 EasyX 报告,它们是两条独立观察记录。只有存在可靠证据时才通过关联字段声明二者匹配,不能仅因消息名称相同就认定是同一事件。

3. 会话与序列

3.1 会话

每次启用探测并打开日志文件形成一个会话:

probeSessionId = 随机 UUID
schemaVersion = 1
startedAtUtc = ISO-8601 UTC 时间
processId
uiThreadId
qpcFrequency

会话开始时写入一条 session_start,正常关闭时写入 session_end。

3.2 sequence

  • sequence 是会话内跨所有后端严格递增的 uint64_t。
  • 0 保留为无记录/未关联值,第一条记录从 1 开始。
  • 嵌套 WndProc 调用仍按实际观察顺序分配 sequence。
  • sequence 只描述探测器观察顺序,不等同于 Windows 消息队列序号。
  • 溢出视为会话不可继续,停止探测但不能改变业务行为。

3.3 调用配对

每次 WndProc 进入分配 wndProcCallId:

WndProc enter:
    recordType = wndproc_enter
    wndProcCallId = 新 ID
    callDepth = 当前重入深度

旧窗口过程返回:
    recordType = wndproc_leave
    wndProcCallId = 对应 enter 的 ID
    forwardResult = 原样结果

如果旧窗口过程同步触发嵌套消息,内层记录会自然出现在外层 enter/leave 之间。

4. 通用记录字段

以下字段构成 schema v1 的公共部分。

字段 类型 必需 说明
schemaVersion integer 是 初始为 1
recordType string 是 记录类型
sessionId string 是 探测会话 UUID
sequence uint64 是 全会话严格递增序号
sourceBackend string 是 观察后端
observationPoint string 是 具体入口/离开/批次边界
qpcTicks uint64 是 QueryPerformanceCounter 原始值
threadId uint32 是 当前线程 ID
processId uint32 是 当前进程 ID
hwnd string/null 视情况 十六进制句柄,仅用于本次会话诊断
hwndInstanceId uint64/null 视情况 探测器为一次 HWND 生命周期分配的 ID
wndProcCallId uint64/null 否 WndProc enter/leave 配对
callDepth uint32 否 WndProc 重入深度
parentSequence uint64/null 否 IMM 查询等子记录关联的父消息
dispatchDepth uint32/null 否 当前框架分发深度,未知则 null
modalDepth uint32/null 否 当前模态深度,未知则 null
imeState string 否 unknown/idle/composing
compositionId uint64 是 0 表示无活动 IME 组合

hwnd 可能被系统重用,因此分析生命周期时必须使用 hwndInstanceId,不能只比较句柄值。

5. 原生消息字段

适用于 Win32WndProc 记录。

字段 类型 说明
messageId uint32 原始消息编号
messageName string 已知消息符号名,未知时使用 UNKNOWN_0xNNNN
messageCategory string key/text/ime/focus/window/mouse/other
wParamPolicy string raw/parsed/redacted/not_applicable
wParamHex string/null 仅允许非文本、非敏感消息记录
lParamPolicy string raw_and_parsed/parsed_only/redacted/not_applicable
lParamHex string/null 只允许无指针、无文本含义的消息记录
forwarded boolean 是否按原链调用旧窗口过程
forwardResultHex string/null leave 记录中的旧过程返回值
messageTime uint32/null 只有来源明确时记录
messageTimeSource string msg/easyx/stale_or_unknown/not_available

约束:

  • WM_CHAR、WM_UNICHAR、WM_IME_CHAR 等文本消息不得记录原始 wParamHex。
  • lParam 为指针的消息不得记录指针值或盲目解引用,只记录允许的解析结果和是否为空。
  • WndProc 本身没有 MSG.time;不得把 GetMessageTime() 的潜在陈旧值伪装成当前消息时间。
  • 对 SendMessage 产生的同步消息,messageTime 通常应为 null/unknown。

6. Key lParam 解析

对 WM_KEYDOWN/UP、WM_SYSKEYDOWN/UP 记录:

字段 来源
virtualKey wParam,作为键值而非文字记录
action 消息类型决定 down/up
repeatCount lParam bits 0–15
scanCode bits 16–23
extended bit 24
altContext bit 29
wasDown bit 30
transition bit 31
systemKey 是否为 WM_SYSKEY*
imeComposingAtObservation 观察瞬间的探测状态

修饰键状态可额外记录:

leftShift/rightShift
leftCtrl/rightCtrl
leftAlt/rightAlt
capsLock/numLock/scrollLock

必须注明这些值来自观察时的 GetKeyState 快照,不保证是消息生成瞬间的硬件状态。

7. IME 字段

7.1 WM_IME_COMPOSITION

解析并记录布尔 flags:

hasGcsCompStr
hasGcsCompAttr
hasGcsCompClause
hasGcsCursorPos
hasGcsResultStr
hasGcsResultClause
hasCsInsertChar
hasCsNoMoveCaret
hasAnyGcsFlag

允许记录:

compositionUtf16Length
compositionScalarCount
compositionDigest
resultUtf16Length
resultScalarCount
resultDigest
cursorPosition
immQueryStatus

禁止记录组合串、读取串、结果串和候选文字原文。

7.2 WM_IME_STARTCOMPOSITION

  • 记录进入前后的探测 IME 状态。
  • 记录新分配的探测 compositionId。
  • 该 ID 只用于关联日志,不代表阶段 1 最终实现已经落地。

7.3 缺失 START 的容错

Idle 状态直接观察到 WM_IME_COMPOSITION 时:

  • 惰性分配新的非零 compositionId。
  • 写入 compositionIdAllocation = lazy_missing_start。
  • 禁止沿用已 Commit/Cancel/End 的旧 ID。

7.4 WM_IME_ENDCOMPOSITION

  • 记录本次组合是否观察到 result/cancel。
  • 设置 flushRequested=true,原因为 ime_end。
  • 不在 WndProc 内执行实际写盘。

7.5 WM_IME_SETCONTEXT

  • 记录 activate 和 ISC_SHOWUI* flags。
  • 记录转发前后 flags 一致。
  • 记录旧窗口过程返回值。
  • 0A 不允许修改 flags。

8. 窗口和字符集字段

每个 hwndInstanceId 首次出现时记录一次窗口快照:

isWindowUnicode
threadId
processId
windowClassDigest
dpiForWindow
dpiAwarenessContext

窗口类名默认也使用会话摘要,不记录业务窗口标题。

WM_INPUTLANGCHANGEREQUEST 记录:

requestFlags
requestedInputLanguageHandle
requestedAnsiCodePage
inputLanguageSysCharset
inputLanguageForward
inputLanguageBackward
  • 该消息表示输入布局切换请求,不等于切换已经完成。
  • 0A 只观察并原样转发,不因请求更新活动代码页,也不清理当前回声状态。
  • requestFlags 与 requestedInputLanguageHandle 分别是通用 wParamHex、lParamHex 的具名副本,便于分析器在不反推参数语义的情况下直接使用。
  • requestedAnsiCodePage 只描述 lParam 中目标 HKL 的只读解析结果。

WM_INPUTLANGCHANGE 记录:

inputLanguageHandle
charset
oldAnsiCodePage
newAnsiCodePage
pendingLeadPresentBefore
pendingLeadCleared

0A 尚未接管 ANSI 解码时,pendingLead* 可以是 not_applicable;字段保留给阶段 1 共用 schema。

9. EasyX 后端字段

只在现有业务循环已经成功取出 EasyX 消息时记录,不允许为了探测额外读取队列。

字段 说明
easyxMessageType EasyX 消息类别
easyxVkCode 键盘虚拟键,不记录字符原文
easyxDown 按下/释放状态
easyxCtrl/Shift/Alt EasyX 提供的修饰状态
easyxMessageTime EasyX 提供时记录,否则 null
existingDispatchDecision 当前代码原本如何处理该消息

existingDispatchDecision 只描述观察到的旧行为,0A 不能把该消息改为“丢弃”或“进入新业务队列”。

10. 文本隐私与关联

10.1 默认禁止记录的内容

  • WM_CHAR/WM_UNICHAR/WM_IME_CHAR 原始字符值。
  • GCS_COMPSTR/GCS_RESULTSTR 原文。
  • 拼音串、候选词、剪贴板和 TextBox 内容。
  • 窗口标题或可能包含用户数据的控件文本。

10.2 允许的文本元数据

textSource
utf16Length
scalarCount
isValidUtf16
sessionDigest
correlationStatus
correlationTargetSequence
matchedUnitCount

10.3 会话摘要

  • 会话开始时生成随机密钥。
  • 使用带密钥摘要,例如 HMAC-SHA-256。
  • 日志只保存截断后的 128-bit 十六进制摘要。
  • 密钥只驻留内存,不写日志、不跨会话复用。
  • 摘要只用于同一会话内比较相等性,不能跨会话追踪用户输入。

10.4 在线回声关联

探测器可以在内存中短暂保留本次 GCS_RESULTSTR,在线比较后续 WM_IME_CHAR/WM_CHAR 序列。日志只输出:

matched
partial
mismatched
expired

及匹配数量、目标 sequence 和摘要,不输出原文。

不默认记录“前 2–3 个码点”,因为短前缀仍可能泄露姓名、拼音或密码信息。

如果摘要和在线关联不足以定位问题,只允许在显式测试开关下启用明文模式,并满足:

  1. UI 明确显示正在记录测试文本。
  2. 只输入预先约定的非敏感测试串。
  3. 明文模式不能进入正式构建默认配置。
  4. 日志文件名明确包含 PLAINTEXT-TEST。

11. 来源关联

WndProc 和 EasyX 记录的匹配只能是显式结论:

correlationStatus:
    none
    candidate
    matched
    ambiguous
    rejected

correlationConfidence:
    exact
    high
    low

匹配可参考:

  • HWND 实例。
  • 消息类别和虚拟键。
  • Down/Up 状态。
  • scan code。
  • 消息时间(仅来源可靠时)。
  • sequence 邻近关系。

如果同时存在多个候选,必须标为 ambiguous,不能强行一一配对。

12. 内存缓冲

12.1 捕获结构

  • WndProc 内只写紧凑的内部 ProbeRecord,不构造 JSON 字符串。
  • 缓冲预分配,避免在消息热路径频繁扩容。
  • 文本只计算长度、有效性、摘要和在线关联结果。
  • JSON 序列化和文件编码在安全点执行。

12.2 默认容量

schema v1 建议默认:

softLimit = 3072 records
hardLimit = 4096 records

容量必须可由测试配置覆盖,但不能在正式路径无限增长。

达到 softLimit:

  • 设置 flushRequested=true。
  • 原因为 soft_limit。
  • 继续记录到 hardLimit。

达到 hardLimit 且仍未到安全点:

  • 保留最近记录,覆盖最旧未写记录。
  • 维护 lostFirstSequence/lostLastSequence/lostCount。
  • 下一次实际 flush 时先输出一条 records_lost 元记录。
  • 禁止静默覆盖。

选择保留最近记录是为了在异常退出前尽量留下最接近故障的消息,同时通过 loss 元记录明确标注因果链已不完整。

13. Flush 模型

13.1 两类 flush

区分:

  1. buffer_flush:把内存记录序列化并写入文件,由操作系统缓存。
  2. durable_flush:在 buffer_flush 后调用耐久化能力,降低进程崩溃后的日志丢失。

13.2 WndProc 内只请求

以下观察点只设置请求,不直接 I/O:

触发点 请求原因 建议级别
WM_IME_ENDCOMPOSITION ime_end durable
WM_KILLFOCUS / 窗口失活 window_deactivate durable
softLimit soft_limit buffer
最大滞留时间到期 max_latency buffer
WM_CLOSE/WM_DESTROY 观察 window_shutdown durable

13.3 实际安全点

实际 flush 只能发生在:

WndProc 已返回
&& 当前宿主消息/输入批次已经结束
&& 当前 mutation transaction 已提交
&& 当前没有跨 flush 保存的 Control* 借用视图
&& 当前循环是该 Window 的活动 drainer

现阶段尚未实现通用 drainer 时,0A 必须选择现有主循环已经完成一次消息/事件分发的位置,不能在控件回调、旧窗口过程调用或嵌套 WndProc 栈中写盘。

13.4 flush 时机

  • 每个宿主输入批次结束:如缓冲非空,执行 buffer_flush。
  • 收到 durable 请求:在下一个安全点执行 buffer_flush + durable_flush。
  • 最大滞留时间默认 500ms,到安全点后执行 buffer_flush。
  • 正常关闭:完成最终 durable_flush,并写 session_end。
  • 异常崩溃:不在异常处理器中尝试复杂 flush,允许最多丢失尚未到安全点的当前批次。

13.5 写入失败

写入失败时:

  • 保存错误码和失败计数到内存状态。
  • 禁止重试死循环或阻塞 WndProc。
  • 禁止退回记录明文的控制台输出。
  • 后续安全点可以进行有上限的重试。
  • 达到重试上限后禁用本次会话文件输出,但业务输入继续原样运行。

14. 日志文件

建议路径:

TestResults/InputProbe/

建议文件名:

input-probe-YYYYMMDD-HHMMSS-p<PID>-<sessionId>.jsonl

格式:

  • UTF-8,无 BOM。
  • JSON Lines,一行一条完整 JSON 对象。
  • 换行使用 \n。
  • schemaVersion 必须出现在每条记录中,便于截断文件恢复分析。
  • 未知字段读取方忽略;语义变化必须提升 schemaVersion。

15. JSONL 示例

以下示例只展示格式,不代表真实运行结果。

{"schemaVersion":1,"recordType":"session_start","sessionId":"7f472b6e-1d2a-4f8f-8df5-bdb72a8d52f7","sequence":1,"sourceBackend":"HostEventLoop","observationPoint":"probe_start","qpcTicks":518820000,"threadId":10420,"processId":18192,"hwnd":null,"hwndInstanceId":null,"compositionId":0,"startedAtUtc":"2026-10-01T08:00:00Z","qpcFrequency":10000000}
{"schemaVersion":1,"recordType":"wndproc_enter","sessionId":"7f472b6e-1d2a-4f8f-8df5-bdb72a8d52f7","sequence":42,"sourceBackend":"Win32WndProc","observationPoint":"enter","qpcTicks":518821240,"threadId":10420,"processId":18192,"hwnd":"0x00000000000A1042","hwndInstanceId":1,"wndProcCallId":19,"callDepth":1,"compositionId":3,"messageId":271,"messageName":"WM_IME_COMPOSITION","messageCategory":"ime","wParamPolicy":"redacted","wParamHex":null,"lParamPolicy":"raw_and_parsed","lParamHex":"0x00000800","hasGcsResultStr":true,"hasAnyGcsFlag":true}
{"schemaVersion":1,"recordType":"imm_query","sessionId":"7f472b6e-1d2a-4f8f-8df5-bdb72a8d52f7","sequence":43,"sourceBackend":"Imm32Query","observationPoint":"gcs_result","qpcTicks":518821260,"threadId":10420,"processId":18192,"hwnd":"0x00000000000A1042","hwndInstanceId":1,"parentSequence":42,"compositionId":3,"textSource":"GCS_RESULTSTR","utf16Length":2,"scalarCount":2,"isValidUtf16":true,"sessionDigest":"9e471d4d3a5732aa4fd27dba86aa4c11","correlationStatus":"none"}
{"schemaVersion":1,"recordType":"wndproc_leave","sessionId":"7f472b6e-1d2a-4f8f-8df5-bdb72a8d52f7","sequence":44,"sourceBackend":"Win32WndProc","observationPoint":"after_old_proc","qpcTicks":518821300,"threadId":10420,"processId":18192,"hwnd":"0x00000000000A1042","hwndInstanceId":1,"wndProcCallId":19,"callDepth":1,"compositionId":3,"messageId":271,"messageName":"WM_IME_COMPOSITION","forwarded":true,"forwardResultHex":"0x0000000000000000"}
{"schemaVersion":1,"recordType":"flush_boundary","sessionId":"7f472b6e-1d2a-4f8f-8df5-bdb72a8d52f7","sequence":45,"sourceBackend":"HostEventLoop","observationPoint":"input_batch_end","qpcTicks":518821400,"threadId":10420,"processId":18192,"hwnd":"0x00000000000A1042","hwndInstanceId":1,"compositionId":3,"flushReason":"input_batch_end","flushLevel":"buffer","bufferedRecordCount":44,"lostCount":0}

示例中的摘要不可反推出原始文字,会话密钥也不得写入文件。

16. 记录类型清单

schema v1 至少定义:

session_start
session_end
window_attach
window_detach
wndproc_enter
wndproc_leave
easyx_key
imm_query
host_batch_begin
host_batch_end
flush_boundary
records_lost
probe_error

不得用一个含义模糊的 event 类型承载所有数据。

17. 配置边界

0A 配置只允许控制:

  • 是否启用探测。
  • 输出目录。
  • soft/hard limit。
  • 最大滞留时间。
  • 是否启用显式明文测试模式。
  • 是否记录非输入类窗口消息。

0A 配置不得包含“消费消息”选项。任何消费规则都属于 0B,并使用独立配置结构和明显的实验标识。

18. 0A 需要回答的问题

  1. EasyX 主循环中的键盘消息是否经过当前 WndProc。
  2. WndProc 和 EasyX EX_KEY 是否重复报告同一输入。
  3. WndProc、EasyX、IMM 查询和宿主批次的真实顺序。
  4. WM_KEYDOWN 后是否出现 WM_CHAR/WM_SYSCHAR。
  5. 窗口实际为 ANSI 还是 Unicode。
  6. WM_UNICHAR 是否出现、从哪个观察链出现。
  7. WM_IME_STARTCOMPOSITION 是否总在 COMPOSITION 前出现。
  8. GCS_RESULTSTR、WM_IME_CHAR、WM_CHAR 是否重复以及如何分段。
  9. ANSI WM_CHAR 是否以 lead/trail 分开发送。
  10. WM_GETDLGCODE 是否到达当前窗口过程。
  11. 普通循环和 Dialog 模态循环是否经过同一链路。
  12. Window 激活/失活与 IME End/Cancel 的顺序。
  13. 不同输入法的消息序列差异。
  14. 是否存在无法通过现有观察点可靠关联的消息。

“消费某类消息是否影响 EasyX/IME”不属于 0A 结论,由 0B 单变量实验回答。

19. 0B 和阶段 1 的输入契约

0B 复用:

  • 同一 schema 和 sequence。
  • 同一来源后端。
  • 同一 Key/IME 解析结果。
  • 同一缓冲、隐私和 flush 规则。
  • 额外增加 experimentId/ruleId/action,不得改写 0A 字段语义。

阶段 1 复用:

  • 已验证的消息来源和顺序。
  • HWND A/W 属性。
  • Key lParam 解析。
  • IME result/echo 边界。
  • ANSI/DBCS 实际分段。
  • 需要保留或丢弃 EasyX EX_KEY 的证据。

20. 第一切片落地边界(2026-10-03)

第一切片已经实现以下能力:

  • 编译期开关:/p:StellarXInputProbe=true;默认值为 false。
  • 运行期开关:STELLARX_INPUT_PROBE=1;只有编译期和运行期同时开启才创建探测会话。
  • 输出目录:默认 TestResults/InputProbe/,可用 STELLARX_INPUT_PROBE_DIR 覆盖。
  • 不新增公共 API;关闭编译开关时内部调用全部内联为空操作。
  • 在现有 WndProc、普通事件循环和 Dialog 模态循环的原路径上被动观察,不额外读取 EasyX 队列,不改变消息参数、转发路径和返回值。
  • 记录 WndProc enter/leave、EasyX EX_KEY、宿主批次边界、Key lParam 解析、IME flags/组合状态、窗口 A/W 属性、DPI 和线程快照。
  • 使用固定容量环形缓冲;WndProc 只尝试获取 SRW 独占锁,竞争时不等待并登记丢失;宿主安全点在锁内复制快照,JSON 序列化和文件 I/O 在锁外执行。
  • 每个普通或模态宿主批次结束后尝试安全点 flush;WM_IME_ENDCOMPOSITION、失焦、失活和关闭消息只请求耐久 flush,不在 WndProc 内写盘。
  • 文本类消息不记录原始 wParam,当前第一切片也不读取或保存组合串、结果串原文。

第一切片当时尚未实现的后续项,当前进度如下:

  • ImmGetCompositionStringW 的长度、UTF-16 有效性、scalar count 和会话摘要记录。
  • GCS_RESULTSTR 与后续 WM_IME_CHAR/WM_CHAR 的在线回声关联。
  • 真实中文 IME 覆盖:微软拼音和搜狗均已完成人工执行。
  • 0B 的逐消息消费实验仍未开始;当前实现没有任何消费配置。

21. 第二切片落地边界(2026-10-03)

第二切片新增:

  • WM_IME_COMPOSITION + GCS_RESULTSTR 时调用 ImmGetCompositionStringW,记录字节长度、UTF-16 code unit 数、Unicode scalar count 和 UTF-16 合法性。
  • 会话启动时生成只驻留内存的随机 HMAC 密钥;结果串只写入截断至 128 bit 的 HMAC-SHA-256 摘要,不记录原文,也不记录密钥。
  • 对超过 64 KiB、奇数字节长度、无 IME context、空结果和读取失败分别记录明确状态。
  • 为每次结果建立短期内存回声状态;Unicode 窗口比较 UTF-16 units,ANSI 窗口按活动输入布局的 ANSI 代码页生成预期字节。
  • WM_IME_CHAR 与 WM_CHAR 使用独立 cursor,分别输出 candidate/matched/partial/mismatched/expired/unrepresentable,不预设两条通道必然都出现。
  • ANSI WM_IME_CHAR 按高字节 lead、低字节 trail 展开;ANSI WM_CHAR 按单字节连续比较。
  • 新结果、新组合、失焦、窗口失活、输入语言切换和关闭会结束当前回声窗口并写隐私安全的汇总记录;IME End 在旧窗口过程返回后的 leave 阶段收口,避免漏掉 END 处理期间嵌套产生的字符回声。
  • WM_INPUTLANGCHANGE 更新活动布局代码页;实测中文布局为 CP936。

独立 EasyX.ThreadProbe 不链接 StellarX,已经确认:EasyX HWND/WndProc 属于内部窗口线程,peekmessage 属于 main/宿主线程,双线程是 EasyX 固有模型而非 UiFixture 假象。该结论已写回 v0.5 设计稿。

第二切片仍不负责:

  • 生产 NativeInputQueue、焦点系统或 TextBox 输入。
  • 消费、拦截或改写任何 IME/字符消息。
  • 用自动化伪造真实 IME 结果;真正的提交、候选、取消和输入法切换必须由人工运行微软拼音与搜狗。

22. 微软拼音实测与关闭链修复(2026-10-04)

微软拼音真实人工会话已经完成,结果目录为 TestResults/InputProbe/manual-20261004-194521-908-MicrosoftPinyin:

  • JSONL 共 4609 条,全部可解析,sequence 严格递增,文字原始 wParam 泄露为 0。
  • WndProc 线程 ID 为 27744,宿主线程 ID 为 33148;线程 ID 仅代表本次会话。
  • 观察到 6 次 WM_IME_STARTCOMPOSITION、35 次 WM_IME_COMPOSITION、6 次 WM_IME_ENDCOMPOSITION。
  • 3 次 GCS_RESULTSTR 查询成功;每次为 2 个 UTF-16 code units、2 个 Unicode scalar,当前 CP936 回声预期为 4 个字节且未使用默认替代字符。
  • 3 次 WM_IME_CHAR 回声均为 4/4 matched;对应 3 次 WM_CHAR 通道均为 0/4 expired。当前微软拼音 + ANSI EasyX 窗口组合下,应优先防止 GCS_RESULTSTR 与 WM_IME_CHAR 重复提交,不能假定还会出现 WM_CHAR 回声。
  • WM_INPUTLANGCHANGE 观察数为 0,因此“切换中英文一次”没有形成该消息;该会话不能据此验证输入布局切换清理路径。
  • 共有 5 条 records_lost,原因均为 wndproc_lock_contention;所有丢失均被显式记账,不能把本次日志称为零丢失。

本次真实关闭还暴露 BUG-20261004-0001:WndProc 已观察到 WM_CLOSE -> WM_DESTROY -> WM_NCDESTROY,但宿主 peekmessage 没有取得 WM_CLOSE,造成窗口消失后进程继续运行。修复后由 WndProc 在完成 WM_NCDESTROY 转发与探测 leave 后,以 release 发布原生窗口死亡状态;普通和模态宿主循环以 acquire 读取并在安全点退出。发布点不得提前,否则会造成仍在执行的 WndProc 访问已析构 Window。

自动关闭 smoke 在 Start-Process + SC_CLOSE + EX_SHOWCONSOLE 组合下还确认:EasyX 可能以 0xC000013A 直接结束,最后一批 WndProc 内存记录来不及由宿主安全点刷盘。探测脚本不得无条件忽略该退出码;只有日志可解析、sequence 严格递增、无文字原值泄露且已有 closeObserved=true 时才接受。0A 继续坚持 WndProc 不做磁盘 I/O,不为了补齐 session_end 改变被测时序。

23. 搜狗实测与输入布局请求补观测(2026-10-04)

搜狗真实提交会话目录为 TestResults/InputProbe/manual-20261004-204556-353-Sogou:

  • JSONL 共 2120 条,全部可解析,sequence 严格递增,文字原始 wParam 泄露为 0。
  • WndProc 与宿主线程分别为 12600 和 668;线程 ID 仅代表本次会话。
  • 观察到 3 次 START、3 次 COMPOSITION、3 次 END,3 次 GCS_RESULTSTR 均成功。
  • 三次结果分别为 7、2、6 个有效 Unicode scalar;对应 CP936 预期回声为 14、4、12 字节,均未使用默认替代字符。
  • WM_IME_CHAR 三次分别为 14/14、4/4、12/12 matched;WM_CHAR 三次均为 0/N expired。
  • 共有 3 条 records_lost,原因均为 wndproc_lock_contention;关闭链完整且写出 window_detach/session_end。

微软拼音与搜狗在当前 ANSI EasyX + CP936 环境下给出相同提交拓扑:GCS_RESULTSTR 是正式提交,随后出现完整匹配的 WM_IME_CHAR 回声,没有形成可匹配的 WM_CHAR 回声。阶段 1 必须以结果串只提交一次,并把匹配回声标记为观察/转发而不重复入队;不得按字符内容做全局去重。

短输入布局切换会话目录为 TestResults/InputProbe/manual-20261004-223833-893-Sogou:

  • JSONL 共 1048 条,全部可解析,sequence 严格递增,无文字原值泄露;4 条丢失均为 wndproc_lock_contention。
  • 日志观察到 VK_LWIN、焦点往返、Shift 和 A,但 WM_INPUTLANGCHANGE 为 0,关闭与 session_end 完整。
  • 该会话运行时的探测器尚未把 WM_INPUTLANGCHANGEREQUEST 列入默认观察集合,因此不能用“日志里没有 request”证明系统没有发送 request。
  • 这暴露了 0A schema/观察面的组织性缺口,而不是用户操作失败。现已把 request 纳入 message name、window category、原始参数和方向 flags 观察,并在分析摘要中分别统计 request/change。
  • IME 内部中英文模式、TSF 输入配置文件和 HKL/代码页是不同层次;只有实际 WM_INPUTLANGCHANGE 才先清 pendingLead、再更新活动代码页,不能把 Win+Space、Shift 或配置文件切换本身当作代码页已经变化。

补观测后的专用 Win+Space 短测目录为 TestResults/InputProbe/manual-20261004-231414-758-Sogou:

  • JSONL 共 1243 条,全部可解析,sequence 严格递增,无文字原值泄露;2 条丢失均为已显式记账的 wndproc_lock_contention,关闭链与 session_end 完整。
  • 新版观察面下 WM_INPUTLANGCHANGEREQUEST=0、WM_INPUTLANGCHANGE=0,同时也未出现 WM_IME_SELECT。
  • 日志观察到 VK_LWIN、窗口焦点往返;输入 A 时按键进入 VK_PROCESSKEY,随后产生 1 次 IME composition/result 和匹配 WM_IME_CHAR 回声,说明切换后的输入配置文件仍属于中文 IME 路径。
  • Get-WinUserLanguageList 只返回 zh-Hans-CN,但含多个输入配置文件。因此这次 Win+Space 只能验证同一中文 HKL/CP936 内的 TSF 配置文件切换,不具备跨语言 HKL 条件。
  • 结论:同 HKL 的 TSF 配置文件切换不保证产生 request/change;跨语言 HKL 的消息行为仍未实测。验证后未新增英文键盘布局,因为那会改变用户系统配置。

实现与影响

  • 关键实现点:
    • 已增加内部 InputProbe 第一、二切片,使用固定内存记录和安全点 JSONL 序列化。
    • 实测 EasyX 窗口过程与框架宿主循环位于不同线程,因此共享状态按双线程模型实现:WndProc 使用非阻塞 TryAcquireSRWLockExclusive,宿主安全点复制快照后在锁外写盘。
    • WndProc enter/leave 覆盖现有提前返回分支;调用旧窗口过程后原样返回其结果。
    • 0A 和 0B 必须共用 schema,消费实验不得另造不可比较的日志格式。
  • 涉及文件 / 类 / 函数:
    • src/StellarX/internal/SxInputProbeInternal.h:编译开关下的内部接口及默认关闭空实现。
    • src/StellarX/SxInputProbe.cpp:会话、记录、缓冲、序列化、flush 与窗口/键盘/IME 元数据采集。
    • src/StellarX/Window.cpp:WndProc、普通事件循环和安全点接入。
    • src/StellarX/Dialog.cpp:模态循环批次与安全点接入。
    • StellarX.Core.vcxproj:源文件登记及 StellarXInputProbe 编译期开关。
    • tests/EasyXThreadProbe.cpp、tests/EasyX.ThreadProbe.vcxproj:不链接 StellarX 的线程归属独立复现。
    • scripts/run-easyx-thread-probe.ps1:独立复现运行入口。
    • scripts/run-input-probe-manual.ps1、scripts/analyze-input-probe.ps1:微软拼音/搜狗人工测试与隐私安全汇总入口。
  • 兼容性影响:
    • 探测关闭时不产生文件、线程、定时器或公共 API 变化;实测即使设置运行环境变量也不会创建日志。
    • 探测开启时业务消息参数、转发次数和返回值保持不变。
  • 性能影响:
    • 开启探测会增加 QPC、IMM 查询、UTF-16 扫描、HMAC、字段解析和内存写入开销;全部仅存在于显式探测构建与运行开关下。
    • 通过预分配和安全点批量序列化避免在 WndProc 执行文件 I/O。
  • 风险点:
    • 探测本身仍会产生微小时序扰动,不能把日志视为零扰动的硬件级跟踪。
    • 缓冲达到 hardLimit 时因果链可能不完整,必须依赖 records_lost 判断。
    • 错误记录原始 wParam、组合串或明文测试日志可能泄露用户输入。
    • WndProc 消息时间来源不可靠时,错误填充 messageTime 会制造虚假顺序证据。

测试与验证

  • 测试范围:
    • 自动验证默认关闭与探测开启两种 Debug x64 构建、现有单元测试和 JSONL 结构。
    • 固定消息 smoke 验证 WndProc/EasyX 双观察链、线程模型、enter/leave 配对、隐私字段和丢失记录。
    • 合成 IME smoke 只验证 imm_query schema、空结果错误路径和隐私边界,不冒充真实 IME 提交验证。
    • 已执行微软拼音与搜狗真实人工测试;结论只覆盖当前 Windows/EasyX/CP936 环境,不能外推到所有 IME。
  • 验证步骤:
  1. 实现前只读定位 WndProc、EasyX EX_KEY、普通循环和模态循环的现有接入点。
  2. 实现 0A 后比较探测关闭/开启时旧窗口过程参数、调用次数、返回值和现有业务行为。
  3. 使用预定非敏感测试串验证 WndProc/EasyX/IME 记录、在线回声关联、缓冲溢出和各类 flush 边界。
  • 验证结果:
    • 2026-10-03:默认关闭的 StellarX.Core Debug x64 编译通过;探测开启的 StellarX.Core 与 StellarX.UiFixture Debug x64 编译、链接通过。
    • 2026-10-03:scripts/test.ps1 -Suite Unit -Configuration Debug -Platform x64 通过,15/15。
    • 2026-10-03 固定消息 smoke:91 行 JSONL 全部可解析,sequence 严格递增;固定 A 键的 Down/Up 在 WndProc 与 EasyX 两条链均可观察,所有键消息 enter/leave 配对,无文本原始 wParam 泄露,无 records_lost。
    • 同次 smoke 实测 WndProc 线程 ID 为 11940、宿主循环线程 ID 为 15220,证明二者不是同一线程;线程 ID 仅代表本次会话,不是稳定常量。
    • 同次 smoke 实测 isWindowUnicode=false,即当前 EasyX 窗口为 ANSI;dpiForWindow=96。
    • 测试窗口短暂取得前台期间还观察到环境产生的其他键盘消息,因此固定消息结论按 virtual key、scan code 和 lParam flags 精确匹配,不以本次键消息总数作为稳定断言。
    • WM_CLOSE enter 之后旧窗口过程没有返回到当前 thunk,进程正常结束,因而该消息没有 leave,析构路径也没有写出 window_detach/session_end;不得伪造配对或会话结束记录。
    • 默认关闭构建在设置 STELLARX_INPUT_PROBE=1 后运行,指定输出目录文件数仍为 0。
    • 独立 EasyX 复现实测:main/peekmessage 线程 18860;HWND 属主、裸 WndProc 和固定键 WndProc 线程 29472;固定键在 WndProc/EasyX 两面各观察 2 条;窗口为 ANSI。线程 ID 只代表该次会话。
    • 第二切片合成 IME smoke:24 行 JSONL 全部可解析、sequence 严格递增、产生 1 条 imm_query,无真实结果时状态为 empty,无原始文本泄露且无 records_lost。
    • 2026-10-04 微软拼音真实会话:4609 行 JSONL 全部可解析、sequence 严格递增;START/END 为 6/6,3 次结果均为 2 个 scalar,3 次 WM_IME_CHAR 均 4/4 matched,3 次 WM_CHAR 均 0/4 expired;无文字原值泄露,存在 5 条显式 wndproc_lock_contention 丢失。
    • 同次真实关闭发现并修复 BUG-20261004-0001;普通主循环和真实模态循环的原生关闭回归均在 5 秒内自行退出。
    • 探测开启的最终自动关闭 smoke 产生 8 份可解析 JSONL,全部 sequence 严格递增、closeObserved=true、无文字原值泄露、无 records_lost。较早一轮中两条 Start-Process + SC_CLOSE 用例曾以 0xC000013A 结束且未刷出最后的 WM_CLOSE,说明该尾批落盘存在宿主时序差异;最终成功轮次不抹去该已知限制。
    • 2026-10-04 搜狗真实提交会话:2120 行 JSONL;3 次结果分别为 7、2、6 个 scalar,WM_IME_CHAR 分别 14/14、4/4、12/12 matched,WM_CHAR 均 expired;无文字原值泄露,3 条丢失均为 wndproc_lock_contention,正常写出 session_end。
    • 2026-10-04 输入布局短会话:1048 行 JSONL,观察到 LWin/Shift/A 与焦点往返,未观察到 WM_INPUTLANGCHANGE;旧探测构建未采集 WM_INPUTLANGCHANGEREQUEST,因此该会话对 request 的存在性不作结论。
    • 2026-10-04 request 补观测最终实现后:探测开启的 StellarX.Core 与 StellarX.UiFixture Debug x64 重新编译、链接通过;UI 回归 1 轮通过,产生 8 份 JSONL,全部可解析、sequence 严格递增且无文字原值泄露;其中 1 份有 1 条已显式记账的 wndproc_lock_contention,其余 7 份无丢失。自动场景没有执行输入布局切换,因此 request/change 均为 0,不冒充人工切换证据。
    • 新版 scripts/analyze-input-probe.ps1 已重新读取微软拼音与两份搜狗旧日志;新增 request 统计不破坏旧 schema,但旧日志中的 0 仅表示当时没有采集该消息。
    • 同一最终源码重新执行 Debug x64 单元测试,15/15 通过;编译输出仅包含 EasyX 匿名联合和仓库原有成员遮蔽/未使用项警告。
    • 2026-10-04 专用 Win+Space 短测:1243 条 JSONL 可解析且 sequence 严格递增,request/change 均为 0;当前系统仅有 zh-Hans-CN,该结果只证明同 HKL 的 TSF 输入配置文件切换不产生这两类消息。
    • 2026-10-05 提交前复验:独立线程探针再次得到 separateWindowThread=true、wndProcMatchesOwner=true、keyCrossedBothBackends=true、windowUnicode=false;探测开启 UI 回归 1 轮产生 8 份 JSONL,全部可解析、sequence 严格递增、无文字原值泄露、无 records_lost,自动原生关闭均保留 closeObserved=true 且不伪造 session_end;探测关闭构建的 VSTest 15/15 通过。
  • 已知限制 / 遗留问题:
    • 微软拼音和搜狗的结果摘要与回声顺序已经确认;其他 IME、其他 EasyX/Windows 版本仍可能不同。
  • WM_INPUTLANGCHANGEREQUEST 补观测是在两次搜狗会话之后加入,既有日志无法追溯恢复当时被过滤的 request;补观测后的短测已完成,但当前机器没有第二种语言/HKL,仍无法覆盖真实 request/change 分支。
  • 后续短测使用 scripts/run-input-probe-manual.ps1 -ImeName Sogou -InputLanguageOnly;该开关只缩短人工提示,不改变构建、探测、日志或摘要路径。
    • 默认缓冲容量和最大滞留时间仍需在真实 IME 日志量下校准。
    • WndProc 与 EasyX 记录能否精确匹配取决于实际可见字段,允许得出 ambiguous 结论。
    • 旧窗口过程处理 WM_CLOSE 时可能不返回;异常或直接终止场景只保证保留此前安全点已写出的记录,不保证存在 session_end。
    • 宽字符 GDI 和 DPI 探测由 Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md 承接,不硬塞进键盘消息 schema。

落地信息