docs: 收口阶段 1 输入系统实现与验证记录

This commit is contained in:
Codex
2026-10-07 20:46:20 +08:00
parent c23b32bb40
commit 81341fab2c
3 changed files with 168 additions and 15 deletions
@@ -7,7 +7,7 @@
- 模块 ID:Module-20261001-0001
- 模块名称:通用输入采集、焦点与键盘路由系统
- 状态:设计中(v0.13;0C 当前可用环境基线通过,0B 阶段 1 前置基线已关闭;微软拼音 Forward/Consume 有效,搜狗 Consume 请求按 `ime_condition_not_met` fail-open 并形成 `MustForward`;阶段 1 尚未开始)
- 状态:实现中(v0.14;0C/0B 前置基线已关闭;阶段 1 输入采集、规范化与观察队列已落地并通过自动回归,待真实 IME/KEY1–6 人工回归;焦点与键盘路由尚未开始)
- 类型:新增模块 / 架构演进
- 所属系统 / 子系统:StellarX Core / Window / Control / Input / Focus / TextBox
- 版本 / 分支:`master`,阶段 0A 实现基线 `c218c25`
@@ -37,7 +37,7 @@
- v1 不自绘 IME 拼音组合串和候选列表,继续使用系统 IME UI。
- v1 不重写现有全部鼠标采集链;现有 EasyX 鼠标消息只在框架入口规范化为 `PointerEvent`。
- v1 不承诺完整 Unicode grapheme cluster 编辑,只保证不拆分 UTF-16 代理对。
- 本设计稿不代表框架代码已经实现,也不构成运行验证结果。
- 本稿中的阶段 1 已有实现记录和自动验证证据;阶段 2 至阶段 6 仍是设计目标,不应被误读为已经实现。
## 模块边界
@@ -162,8 +162,10 @@ Button / TextBox / Table / TabControl / Dialog
### 3. 关键不变量
1. NativeInputQueue 是键盘、文本和 IME 的唯一业务输入源。
2. EasyX `EX_KEY` 只排空和观察,不得再次触发业务路由。
以下是阶段 3 换轨后的完成态不变量;阶段 1–2 的兼容过渡例外仅限“旧 EX_KEY 继续业务下发、新队列只观察”,不得被扩展成长期双业务源:
1. 阶段 3 启用后,NativeInputQueue 是键盘、文本和 IME 的唯一业务输入源。
2. 阶段 3 启用后,EasyX `EX_KEY` 只排空和观察,不得再次触发业务路由。
3. 同步窗口协议不得进入异步输入队列。
4. 异步路由只能阻止框架默认动作,不能倒过来改变已经结束的 WndProc 返回值。
5. 队列、焦点、Tab 顺序、路由路径和待删除列表长期只保存 `ControlHandle`。
@@ -1034,13 +1036,15 @@ struct ProbeConsumeRule
#### 阶段 1:InputCollector
- 输入事件结构和 NativeInputQueue。
- 按已确认双线程模型实现 SPSC 语义的跨线程发布;正确性版本先用短临界区互斥队列。
- `sequence` 使用 atomic 分配,队列同步独立提供 happens-before。
- KeyEvent 完整解析。
- Unicode/ANSI 解码。
- IME 状态机。
- EasyX `EX_KEY` 排空但不业务路由。
- 已实现 `NativeInputEvent / KeyEvent / TextInputEvent / CompositionEvent / WindowActivationEvent` 和每 Window 的 `NativeInputContext`。
- 按已确认双线程模型使用 `mutex + deque` 实现跨线程 SPSC 语义;`sequence` 由 atomic 分配,队列互斥独立提供 happens-before。
- 已实现 Key lParam 完整解析、左右修饰键快照、Unicode/ANSI/CP936 状态解码、IME 生命周期、`compositionId` 与结果回声去重。
- WndProc 捕获时同时快照原始 VK 和 `imeComposingAtCapture`;二者是正交轴,只有原始 `VK_PROCESSKEY` 表示当前单键归 IME。
- 阶段 1 采用行为保持过渡:新队列只在宿主安全点观测排空,旧 `EX_KEY -> Control::handleEvent()` 暂时保留;阶段 3 才把 `EX_KEY` 改为纯排空,并让 NativeInputQueue 成为唯一键盘业务源。
- `Control::handleEvent()` 的 `EX_MOUSE` 永久保留;仅 `EX_KEY` 旧入口进入弃用期,派生控件不得新增对该路径的依赖。
- 队列分配失败或内部异常由无分配 POD 故障快照发布,WndProc 不抛异常;宿主安全点显式 fail-stop 并以 `-2` 退出,不允许静默丢输入后继续运行。
- 非法 UTF-16/ANSI 序列、IMM 查询失败和被边界清理的半截解码状态使用无原文累计计数,并在宿主安全点按变化输出警告。
- 阶段 1 不实现 FocusManager、KeyboardRouter、生产消费白名单、Button/TextBox 键盘行为或 TextBox 自绘。
#### 阶段 2:FocusManager
@@ -1108,17 +1112,23 @@ struct ProbeConsumeRule
28. IME 位置必须在焦点/caret/anchor 变化时立即预设,并在组合生命周期消息到达时再次校准;不得假定候选 UI 出现前一定会收到 `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION`。
29. 原始 `VK_PROCESSKEY` 一律视为 IME 所有:WndProc 层采集并转发,阶段 1 不向 Button、Dialog、快捷键和焦点导航路由;不得依赖 `imeComposingAtCapture` 单独判断搜狗候选态。
30. 0B 没有建立跨 IME 生产消费白名单;阶段 1 从默认转发基线开始,后续任何窄消费规则都必须有独立逐键证据。
31. 阶段 1 只建输入地基、不切换业务轨道:旧 `EX_KEY` 继续进入 `Control::handleEvent()`,新队列只观测排空;阶段 3 一次性切换,`EX_MOUSE` 不受影响。
32. 单键 IME 所有权只由采集时的原始 `VK_PROCESSKEY` 表达;`imeComposingAtCapture` 只记录标准生命周期,两者不得合并为一个持久“组合中”状态。
33. `mutex + deque` 在正常资源条件下不得丢输入;分配失败或队列异常必须 fail-stop,不能捕获后假装成功。
34. `WM_IME_ENDCOMPOSITION` 不得在转发旧窗口过程之前清空结果回声;END 处理中嵌套或紧随其后的匹配 `WM_IME_CHAR/WM_CHAR` 仍须抑制重复入队。
35. 无效文本序列、IMM 查询失败和残留半截解码状态必须以不含原文的计数显式诊断,不得写入替代文本或静默记为成功。
## 实现与影响
- 关键实现点:
- 阶段 0A 探测器已经进入框架;0C 独立视觉探测工程已实现;FocusManager、生产 NativeInputQueue 和 TextBox 改造尚未开始。
- 独立 EasyX 探测已经推翻 v0.4 的单线程假设,v0.5 据此补入跨线程队列约束;v0.6 根据微软拼音/搜狗真实日志锁定结果串与回声关系,并补入输入布局 request/change 分层;v0.7 根据专用短测区分 TSF 配置文件切换与跨语言 HKL,并增加重新获焦快照约束;v0.8 根据 0C 自动证据锁定宽字符绘制、批次进入顺序与当前 DPI 坐标规则;v0.9 根据搜狗真实失败与修复复验锁定 IME 位置双时机规则;v0.13 根据 0B 微软拼音 Consume 与搜狗运行时 fail-open 证据锁定默认转发、`VK_PROCESSKEY` 路由抑制和无生产消费白名单基线。
- 阶段 0A 探测器和 0C 独立视觉探测已进入仓库;阶段 1 的生产 NativeInputQueue、输入事件结构、采集规范化、IME 状态与观察排空已实现。FocusManager、KeyboardRouter 和 TextBox 改造尚未开始。
- 独立 EasyX 探测已经推翻 v0.4 的单线程假设,v0.5 据此补入跨线程队列约束;v0.6 根据微软拼音/搜狗真实日志锁定结果串与回声关系,并补入输入布局 request/change 分层;v0.7 根据专用短测区分 TSF 配置文件切换与跨语言 HKL,并增加重新获焦快照约束;v0.8 根据 0C 自动证据锁定宽字符绘制、批次进入顺序与当前 DPI 坐标规则;v0.9 根据搜狗真实失败与修复复验锁定 IME 位置双时机规则;v0.13 根据 0B 证据锁定默认转发与无生产消费白名单;v0.14 根据阶段 1 实现审查锁定 EX_KEY 兼容过渡、两轴快照、fail-stop、END 回声边界和显式异常计数。
- 涉及文件 / 类 / 函数:
- 预计涉及 `Window`、`Control`、`Canvas`、`Button`、`TextBox`、`Table`、`TabControl`、`Dialog`。
- 实际文件和接口必须在阶段 0 与实现前只读检查后确定。
- 兼容性影响:
- 设计目标是保留现有鼠标行为和公共文本 API 的兼容入口。
- 阶段 1 不改变现有 `EX_KEY` 业务交付;阶段 3 停止向 `Control::handleEvent()` 下发 `EX_KEY` 时属于明确的派生控件迁移点,`EX_MOUSE` 永久保留。
- 输入链路和 TextBox 内部模型将发生架构级变化,必须分阶段接入。
- 性能影响:
- 输入事件会增加规范化和句柄解析开销,但不允许在 WndProc 中执行阻塞 I/O。
@@ -1126,13 +1136,13 @@ struct ProbeConsumeRule
- 风险点:
- EasyX 双线程、ANSI 窗口和当前微软拼音/搜狗回声顺序已确认,但其他 IME、窗口 A/W 属性和系统版本仍可能产生不同顺序。
- 模态重入和控件删除若破坏单 drainer/句柄不变量,可能产生难复现的顺序或生命周期问题。
- 该文档是设计基线,不应被误读为已实现或已验证能力。
- 阶段 1 的自动验证与人工回归必须分开记账;尚未执行的真实 IME/KEY1–6 人工操作不能由单元测试替代。
## 测试与验证
- 测试范围:
- 设计评审覆盖输入采集、同步消息、Unicode、IME、焦点、复合控件、按键归属、TextBox 和模态重入。
- 独立线程归属探测、0A 构建、固定消息 smoke、微软拼音/搜狗真实输入链路测试、当前可用环境的 0C 基线,以及 0B 的 F24 KeyDown/KeyUp 自动 A/B、baseline fail-open、微软拼音 Forward/Consume 和搜狗运行时 fail-open 门槛均已完成;生产输入队列和焦点系统尚未开始。
- 独立线程归属探测、0A、0C、0B 门槛均已完成;阶段 1 已加入逻辑测试与真实 EasyX `input-shadow` 场景,焦点系统尚未开始。
- 验证步骤:
1. 先按关联规格实现阶段 0A 被动探测并取得消息链路证据。
@@ -1161,6 +1171,7 @@ struct ProbeConsumeRule
- 2026-10-07:微软拼音 Forward/Consume 有效复验均通过;Forward 对应 EasyX 键出现一次,Consume 后 EasyX 键消失,位置链分别为 18/18 与 7/7,均零丢失、无孤立定位记录。
- 2026-10-07:搜狗候选态没有向目标 HWND 暴露 START/COMPOSITION/END 生命周期;最终 Consume 请求以 `ime_condition_not_met` 正确 fail-open 为 Forward,规则零命中、零消费、EasyX 键出现一次,结构 verdict 为 `MustForward`。
- 2026-10-07:最终 Debug x64 单元测试 15/15、UI fixture 1 轮、F24 KeyDown A/B 新运行通过;既有 KeyUp 与 baseline mismatch 证据经当前分析器回放保持有效。阶段 1 以默认转发且无消费白名单为起点。
- 2026-10-07:阶段 1 自动回归通过:Debug x64、x64 ASan、Release x64、Debug Win32 均为 27/27,KEY1–6 全部编译/链接成功;完整 UI fixture 曾一轮通过,最终源码的独立 `input-shadow` 再次通过,旧 EX_KEY 与新队列均完整观察 4000/4000 条事件,新队列最终深度 0、顺序严格、内存增长 0、无 fault/规范化异常;PowerShell 5.1/7 AST、工程 XML 和 `git diff --check` 通过。最终完整 UI 的两次追加复跑分别在 `geometry/ownership` 鼠标点击检查点超时,均未进入输入场景,按既有桌面焦点抖动保留失败证据,不冒充最终全套连续通过。真实 IME 与 KEY1–6 交互仍待人工确认。
- 已知限制 / 遗留问题:
- §5.3 当前明确不建立生产消费白名单;若后续需要窄消费规则,仍必须补对应消息、虚拟键、IME 和环境的独立 0B 证据。
- §7.3 的微软拼音/搜狗回声状态机已有 0A 证据,其他 IME 仍需保持可诊断回退。
@@ -0,0 +1,116 @@
# 新增功能模块 / 模块重构
> 本记录描述通用输入系统阶段 1 的实际实现边界与验证证据。它不代表焦点、键盘路由或 TextBox 自绘已经完成。
## 基本信息
- 模块 ID:Module-20261007-0005
- 模块名称:阶段 1 输入采集、规范化与跨线程观察队列
- 状态:实现完成,自动回归与真实 IME / KEY1–6 人工兼容性回归通过
- 类型:新增模块 / 架构演进
- 所属系统 / 子系统:StellarX Core / Window / Native Input
- 版本 / 分支:`master`,未提交工作区
- 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX + MBCS 工程配置
- 负责人:Codex 协作实现
## 背景与目标
- 阶段 0 已确认 EasyX HWND/WndProc 和宿主 `peekmessage` 分属两个线程,当前窗口是 ANSI、中文布局为 CP936。
- 阶段 1 只建立“WndProc 采集与规范化 → 跨线程队列 → 宿主安全点观察排空”的地基,不实现 FocusManager、KeyboardRouter、消费白名单或控件键盘行为。
- 为保持现有行为,本阶段继续把 EasyX `EX_KEY` 交给旧 `Control::handleEvent()`;真正换轨留到阶段 3。
## 实现边界
### 已实现
- 公共事件模型:`KeyEvent`、`TextInputEvent`、`CompositionEvent`、`WindowActivationEvent`、`NativeInputEvent`。
- 每 Window `NativeInputContext` 和正确性优先的 `mutex + deque` 跨线程队列。
- 全局 atomic sequence、队列 pushed/popped/depth/maxDepth 统计和宿主观察统计。
- Key lParam 的 repeat、scan code、extended、alt context、previous state、system key 解析,以及左右 Ctrl/Shift/Alt 快照。
- 原始 `VK_PROCESSKEY` 单键所有权与 `imeComposingAtCapture` 生命周期快照的两轴模型。
- Unicode `WM_CHAR` 代理对、`WM_UNICHAR` UTF-32、ANSI/CP936 DBCS lead/trail 解码。
- `WM_IME_STARTCOMPOSITION/COMPOSITION/ENDCOMPOSITION` 状态机、缺失 START 时的惰性 `compositionId`。
- `GCS_RESULTSTR` 唯一正式提交,以及 `WM_IME_CHAR/WM_CHAR` 独立游标的邻接回声去重。
- `WM_SETFOCUS/WM_KILLFOCUS` 激活事件和重新获焦时的键盘布局快照。
- 主循环与模态循环在安全点观察排空;阶段 1 不调用控件路由。
- WndProc 分配失败/异常的无分配故障快照;宿主安全点 fail-stop,返回码为 `-2`。
- 非法文本序列、IMM 查询失败、残留半截解码状态的无原文累计诊断。
### 明确未实现
- FocusManager、FocusScope、Tab 顺序和焦点回退。
- KeyboardRouter、祖先冒泡、Dialog 默认动作和 `VK_PROCESSKEY` 下游抑制。
- 生产键盘消费白名单。
- Button、Table、TabControl、Dialog 的键盘接入。
- TextBox UTF-16 编辑内核、自绘、选区、剪贴板和 IME caret 定位。
- 将旧 `EX_KEY` 从 `Control::handleEvent()` 中移除。
## 关键契约
1. WndProc 线程只做状态快照、规范化和短临界区入队;不执行用户回调、控件 mutation 或文件 I/O。
2. 原始 `VK_PROCESSKEY` 只表示当前单键归 IME,不启动持久组合态,也不分配 `compositionId`。
3. 标准 IME 生命周期只由 START/COMPOSITION/END 维护;两轴均在采集时快照,延迟消费者不得重查全局输入状态。
4. `GCS_RESULTSTR` 只产生一次 `TextInputEvent`;紧邻且完整匹配的 IME/字符回声只观察,不重复入队。
5. END 转发旧窗口过程前不清回声,避免漏掉 END 处理中的嵌套或紧随回声。
6. 正常资源条件下队列不得丢输入;分配失败或队列异常必须显式 fail-stop。
7. 阶段 1 保留旧 EX_KEY 业务旁路;阶段 3 才把 EX_KEY 改为纯排空并启用新路由。EX_MOUSE 永久保留。
## 实现文件
- `include/StellarX/InputEvents.h`
- `src/StellarX/internal/SxNativeInputInternal.h`
- `src/StellarX/SxNativeInput.cpp`
- `include/StellarX/Window.h`
- `src/StellarX/Window.cpp`
- `src/StellarX/Dialog.cpp`
- `tests/StellarXTests.cpp`
- `tests/UiFixture.cpp`
- `scripts/run-ui-tests.ps1`
## 自动验证
### 逻辑测试
- Key lParam 和左右修饰键快照。
- `VK_PROCESSKEY` 不启动组合,且普通键在生命周期内不被改写为 IME 所有。
- IME 生命周期、Cancel/End 和 `compositionId`。
- CP936 lead/trail 合并。
- `WM_UNICHAR` 能力查询、UTF-32 代理对和非法 scalar 计数。
- GCS 结果回声双通道去重,以及 END 后紧随回声保持。
- CP936 不可表示结果只抑制实际替代字节回声,不误吞随后无物理键前导的程序化文本。
- 20,000 事件生产/消费跨线程严格顺序。
- 注入式分配失败 fail-stop。
- 宿主观察排空无积压。
### 真实 EasyX `input-shadow`
- 总计注入 4,000 条 F24 KeyDown/KeyUp,分为低于 EasyX 旧队列有效容量的持续小波次。
- 同时验证旧 `EX_KEY -> handleEvent()` 与新 NativeInputQueue;两面均必须完整看到 2,000 Down + 2,000 Up。
- 新队列必须 `pushed == popped`、depth 归零、sequence 严格、无 fault、无文本规范化异常。
- 两轮负载之间检查私有内存不出现线性增长,并验证窗口仍能响应鼠标点击。
- 脚本新增 `-OnlyScenario input-shadow`,默认不传参时仍执行完整 UI 套件。
## 当前证据
- Debug x64、x64 ASan、Release x64、Debug Win32 均为 27/27;KEY1–6 全部编译/链接成功。
- 完整 UI fixture 曾一轮通过;最终源码单独复跑 `input-shadow` 通过:旧面 4000/4000,新面 4000/4000,最大新队列深度 40,最终深度 0,内存增长 0,顺序严格且无故障。
- 最终完整 UI 的两次追加复跑分别在 `geometry`、`ownership` 的鼠标点击检查点超时,均未进入 `input-shadow`。该结果按既有桌面焦点/SendInput 抖动保留,不写成最终全套连续通过;阶段 1 核心输入场景已有最终源码独立证据。
- PowerShell 5.1 与 Codex 运行时 PowerShell 7.6.5 AST、`StellarX.Core.vcxproj` XML、`git diff --check` 均通过。
- 2026-10-07 微软拼音有效会话 `manual-20261007-192828-293-MicrosoftPinyin` 与搜狗有效会话 `manual-20261007-193100-289-Sogou` 均为 sequence 严格、零丢失、零原文泄漏、3 次正式结果与 `WM_IME_CHAR` 回声完整匹配,并观察到正常关闭和 `session_end`。
- 用户完成 KEY1–6 人工交互,当前快照未发现卡顿、重复提交、双触发或既有行为退化。人工用例中 KEY5/KEY6 的 `120 ms` Tooltip 覆盖已移除,恢复验证框架默认 `1000 ms`;KEY2 日志轮转阈值由 1 KiB 调整为 10 MiB。
- 上述用例调整后,正式 `NonUI` 再次确认四种单元测试配置均为 27/27,KEY1–6 全部编译链接成功。
- 输入探测夹具没有文本焦点叶子和 caret 锚点,候选框使用系统回退位置不属于阶段 1 失败;本记录不据此宣称 FocusManager、TextBox 或候选框定位已经集成。
## 风险与后续
- 当前微软拼音/搜狗 + ANSI EasyX + CP936 的结果/回声顺序有阶段 0 真实证据;其他 IME、窗口 A/W 属性和系统版本仍需保持可诊断回退。
- EasyX 旧 `EX_KEY` 队列在当前环境呈现约 63 条有效容量;压力测试必须使用持续小波次验证排空,不能把第三方瞬时溢出误判为新队列丢失。
- 阶段 3 是明确兼容性切换点:派生控件必须从 `handleEvent(EX_KEY)` 迁移到新的 routed key 接口;届时再补 `开发记录/MIGRATION.md` 条目。
- 阶段 1 的自动验证和用户人工兼容性回归已经收口;下一步可进入阶段 2 FocusManager 设计/实现,但仍不得提前混入阶段 3 KeyboardRouter 或阶段 5 TextBox 自绘。
## 关联记录
- [`Module-20261001-0001-通用输入焦点与键盘路由系统设计.md`](./Module-20261001-0001-通用输入焦点与键盘路由系统设计.md)
- [`Module-20261001-0002-阶段0A输入链路被动探测规格.md`](./Module-20261001-0002-阶段0A输入链路被动探测规格.md)
- [`Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md`](./Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md)
- [`Module-20261001-0004-阶段0B原生键盘消息隔离消费实验规格.md`](./Module-20261001-0004-阶段0B原生键盘消息隔离消费实验规格.md)