# 新增功能模块 / 模块重构 > 适用场景:新增模块、重大模块重构、核心架构能力演进。 > 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。 ## 基本信息 - 模块 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 会话 每次启用探测并打开日志文件形成一个会话: ```text 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`: ```text 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` | 观察瞬间的探测状态 | 修饰键状态可额外记录: ```text leftShift/rightShift leftCtrl/rightCtrl leftAlt/rightAlt capsLock/numLock/scrollLock ``` 必须注明这些值来自观察时的 `GetKeyState` 快照,不保证是消息生成瞬间的硬件状态。 ### 7. IME 字段 #### 7.1 `WM_IME_COMPOSITION` 解析并记录布尔 flags: ```text hasGcsCompStr hasGcsCompAttr hasGcsCompClause hasGcsCursorPos hasGcsResultStr hasGcsResultClause hasCsInsertChar hasCsNoMoveCaret hasAnyGcsFlag ``` 允许记录: ```text 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` 首次出现时记录一次窗口快照: ```text isWindowUnicode threadId processId windowClassDigest dpiForWindow dpiAwarenessContext ``` 窗口类名默认也使用会话摘要,不记录业务窗口标题。 `WM_INPUTLANGCHANGEREQUEST` 记录: ```text requestFlags requestedInputLanguageHandle requestedAnsiCodePage inputLanguageSysCharset inputLanguageForward inputLanguageBackward ``` - 该消息表示输入布局切换请求,不等于切换已经完成。 - 0A 只观察并原样转发,不因请求更新活动代码页,也不清理当前回声状态。 - `requestFlags` 与 `requestedInputLanguageHandle` 分别是通用 `wParamHex`、`lParamHex` 的具名副本,便于分析器在不反推参数语义的情况下直接使用。 - `requestedAnsiCodePage` 只描述 `lParam` 中目标 HKL 的只读解析结果。 `WM_INPUTLANGCHANGE` 记录: ```text 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 允许的文本元数据 ```text textSource utf16Length scalarCount isValidUtf16 sessionDigest correlationStatus correlationTargetSequence matchedUnitCount ``` #### 10.3 会话摘要 - 会话开始时生成随机密钥。 - 使用带密钥摘要,例如 HMAC-SHA-256。 - 日志只保存截断后的 128-bit 十六进制摘要。 - 密钥只驻留内存,不写日志、不跨会话复用。 - 摘要只用于同一会话内比较相等性,不能跨会话追踪用户输入。 #### 10.4 在线回声关联 探测器可以在内存中短暂保留本次 `GCS_RESULTSTR`,在线比较后续 `WM_IME_CHAR/WM_CHAR` 序列。日志只输出: ```text matched partial mismatched expired ``` 及匹配数量、目标 sequence 和摘要,不输出原文。 不默认记录“前 2–3 个码点”,因为短前缀仍可能泄露姓名、拼音或密码信息。 如果摘要和在线关联不足以定位问题,只允许在显式测试开关下启用明文模式,并满足: 1. UI 明确显示正在记录测试文本。 2. 只输入预先约定的非敏感测试串。 3. 明文模式不能进入正式构建默认配置。 4. 日志文件名明确包含 `PLAINTEXT-TEST`。 ### 11. 来源关联 WndProc 和 EasyX 记录的匹配只能是显式结论: ```text 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 建议默认: ```text 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 只能发生在: ```text 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. 日志文件 建议路径: ```text TestResults/InputProbe/ ``` 建议文件名: ```text input-probe-YYYYMMDD-HHMMSS-p-.jsonl ``` 格式: - UTF-8,无 BOM。 - JSON Lines,一行一条完整 JSON 对象。 - 换行使用 `\n`。 - schemaVersion 必须出现在每条记录中,便于截断文件恢复分析。 - 未知字段读取方忽略;语义变化必须提升 schemaVersion。 ### 15. JSONL 示例 以下示例只展示格式,不代表真实运行结果。 ```json {"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 至少定义: ```text 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`](./Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md) 承接,不硬塞进键盘消息 schema。 ## 落地信息 - 关联功能变更 ID:无 - 关联 BUG / Fix:[`BUG-20261004-0001`](../BUG/BUG-20261004-0001-EasyX原生窗口销毁后宿主事件循环未退出.md) / [`Fix-BUG-20261004-0001`](../Fix/Fix-BUG-20261004-0001-原生窗口销毁后的宿主循环退出修复.md) - Commit:`c218c25`(阶段 0A 诊断实现与线程归属复现);`c0eca1d`(探测期间发现并修复的原生窗口关闭缺陷) - PR:无 - 发布版本:未发布 - 相关文档: - [`Module-20261001-0001-通用输入焦点与键盘路由系统设计.md`](./Module-20261001-0001-通用输入焦点与键盘路由系统设计.md) - [`Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md`](./Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md) - [TranslateMessage](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-translatemessage) - [WM_UNICHAR](https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-unichar) - [WM_IME_COMPOSITION](https://learn.microsoft.com/en-us/windows/win32/intl/wm-ime-composition) - [WM_IME_CHAR](https://learn.microsoft.com/en-us/windows/win32/intl/wm-ime-char) - [ImmGetCompositionStringW](https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immgetcompositionstringw)