docs: 记录阶段 0C 基线与 IME 双时机定位结论

This commit is contained in:
Codex
2026-10-06 02:50:09 +08:00
parent 43cf4ac5f7
commit e514a7dbb9
4 changed files with 322 additions and 19 deletions
@@ -0,0 +1,56 @@
# BUG-20261006-0001
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20261006-0001
- 标题: 搜狗未发送 IME 组合生命周期消息时,候选框未按当前锚点预定位
- 状态:已验证
- 严重性:S3
- 优先级:P1
- 模块: 阶段 0C / IME 坐标探测 / IMM32
- 版本 / 分支: `master`;发现基线为 `225bc35` 加开发中的 0C 探测实现,最终收口于 `43cf4ac`
- 环境: Windows 11 `10.0.22000`、EasyX `25.9.10`、DPI-unaware 虚拟化视图 96 DPI、搜狗拼音
- 发现人: 用户真实搜狗输入法人工探测
- 关联 Fix ID: Fix-BUG-20261006-0001
## 问题描述
- 现象: EasyX 探测窗口刚打开时,在 F1/F2 锚点直接输入 `ceshi`,搜狗候选框出现在窗口外的默认位置;首次提交后,候选框只在 F3 附近获得定位,之后切换 F1/F2/F3 仍可能沿用 F3 的旧位置。
- 影响范围: 阶段 0C-3 无法正确验证搜狗候选框定位;若生产输入系统只在 `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION` 中设置位置,会复现同类 stale-position 问题。
- 期望结果: F1/F2/F3 任一锚点被选中后,在下一次输入开始前就把组合窗口和候选窗口位置写入当前 HWND 的 IME context;IME 生命周期消息到达时再校准一次。
- 实际结果: 非组合态切换锚点只更新内部索引和画面,不调用 `ImmSetCompositionWindow/ImmSetCandidateWindow`。
## 复现信息
- 前置条件: 使用搜狗拼音运行 `scripts/run-visual-probe.ps1 -Mode ime-position -ImeName Sogou`。
- 复现步骤:
1. 启动探测窗口并确认搜狗拼音处于活动状态。
2. 按 F1,输入固定串 `ceshi`,观察候选框位于窗口外的默认位置。
3. 按 Escape 取消,按 F2 重复,仍观察到默认位置。
4. 在候选框中按 Space 提交一次,再切换锚点输入;候选框停留在 F3 附近。
5. 关闭窗口,检查 manifest 与 raw event sequence。
- 复现概率:本次人工测试稳定复现
- 最小复现 Demo:`EasyX.VisualProbe.exe --mode ime-position --ime-name Sogou`
- 证据:`TestResults/VisualProbe/20261005-235150-704-4e01df99-019b-41bd-b8ea-b2ff508626e0/ime-position`。
## 初步分析
- 疑似位置: `tests/visual-probe/ImePositionProbe.cpp` 的 F1/F2/F3 `WM_KEYDOWN` 分支。
- 触发条件: 输入法已经显示候选 UI,但没有向被子类化的 EasyX HWND 发送探测器所依赖的 `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION`。
- 关键证据:
- F1 `apiAttemptCount=0`,F2 `apiAttemptCount=0`,F3 `apiAttemptCount=2` 且两次成功。
- 原始事件序列中,F1/F2 只有 `anchor_change`;直到 F3 才出现 `ime_start -> ime_composition -> ime_end`。
- F1/F2 截图显示候选框在窗口外,F3 截图显示候选框位于最后一次成功设置的位置附近。
- 根因边界: 这不是 DPI 换算错误;F3 的 IMM 调用和坐标值成功。问题是位置设置触发得太晚,并错误假定所有 IME 都会先向目标 HWND 发送同一组生命周期消息。
## 跟踪信息
- 首次发现时间: 2026-10-05 23:51(人工运行),2026-10-06 完成归档与定位
- 最后更新时间: 2026-10-06
- 修复版本: `43cf4ac`(阶段 0C 视觉与 IME 坐标探测)
- 验证版本: 三平台构建与单元测试通过;搜狗真实输入复验自动与人工状态均为 `pass`
- 备注: `-ImeName` 只记录操作者预期,当前 HKL/IMM32 身份字段不能自动证明 TSF 输入配置文件;成功复验的实际搜狗身份由操作者确认并写入人工评审旁车。
@@ -0,0 +1,77 @@
# Fix-BUG-20261006-0001
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20261006-0001
- 关联 BUG ID: BUG-20261006-0001
- 修复目标: 不依赖 IME 必须先发送组合生命周期消息,在锚点变化时立即预设候选框和组合窗口位置
- 状态:已验证
- 负责人: Codex
- 分支 / 版本: `master`,修复实现提交 `43cf4ac`(起点 `225bc35`)
## 根因分析
- 根因: 探测器只在 `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION` 到达时调用 `ImmSetCompositionWindow/ImmSetCandidateWindow`。非组合态按 F1/F2/F3 时仅记录 `anchor_change`,导致下一次组合开始前 IME context 仍保留默认位置或上一个锚点位置。
- 触发条件: 搜狗候选 UI 已出现,但目标 EasyX HWND 没有收到探测器预期的组合生命周期消息;或者生命周期消息晚于候选 UI 首次定位。
- 为什么微软拼音测试未暴露: 微软拼音在此前人工测试中会为三个锚点产生可观察的组合生命周期消息,恰好触发原有设置路径;这不能证明其他 TSF 输入配置文件也遵循相同时序。
- 关键证据: 失败运行中 F1/F2 的 IMM API 尝试次数均为 0,F3 为 2/2;截图与事件序列一致。
## 修复方案
- 修复思路: 采用“双时机定位”。锚点变化时立即向当前 HWND 的 IME context 写入位置,生命周期消息到达时再次校准。
- 关键改动:
- F1/F2/F3 `WM_KEYDOWN` 立即调用 IMM 定位,并把该次调用标记为 `positionTrigger=anchor_change`、`stage=anchor_preconfigure`。
- 探测专用 F1/F2/F3 在完成锚点切换后返回 0,避免旧窗口过程或输入法把功能键解释成其他行为。
- `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION` 保留二次定位,并标记为 `positionTrigger=ime_lifecycle`。
- raw event 增加 `compositionActiveAtAttempt`,分析器分别统计预设调用和生命周期调用。
- 自动完成条件除三个锚点均至少有一次成功定位外,还要求本轮至少有一个真实 IME 生命周期定位事件完成 context 获取与两个 setter;仅按三次功能键或只观察到失败的生命周期调用不能伪造完整通过。
- 涉及文件 / 类 / 函数: `tests/visual-probe/ImePositionProbe.cpp`、`scripts/analyze-visual-probe.ps1`。
- 影响的 API / 行为: 仅改变诊断探测器;不修改当前生产框架的 WndProc 或控件行为。
- 关键约束 / 不变量:
- 坐标仍使用目标 HWND 客户区坐标,不新增 DPI 缩放。
- `CFS_POINT` 与 `CFS_EXCLUDE + caretLineRect` 组合保持不变。
- API 成功只证明位置请求被接受;最终候选框位置仍必须由人工截图验收。
- 回滚点 / 开关: 可单独恢复 F1/F2/F3 的只记录分支,但会恢复搜狗首次定位和 stale-position 问题。
## 影响评估
- 影响范围: `EasyX.VisualProbe` 的 IME position 模式及 manifest 字段。
- 兼容性影响:新增字段为向后兼容扩展;分析器对旧 raw event 继续通过 `stage` 推断生命周期来源。
- 行为变化:F1/F2/F3 成为探测器内部专用且被消费;每次锚点变化都会立即产生一条位置设置事件。
- 性能影响:每次人工切换锚点增加一次短生命周期 `ImmGetContext`、两个 IMM setter 和 `ImmReleaseContext`,只存在于诊断程序。
- 回归风险:
- 某些 IME 可能接受预设调用但随后主动调整候选框;必须保留截图和 `positionDisposition` 人工判断。
- 若只看 API 成功次数,可能把未实际输入的运行误判为完成;因此额外保留至少一个生命周期事件门槛。
## 验证结果
- 验证步骤:
1. 构建 `EasyX.VisualProbe` 的 Debug x64、Release x64 和 Debug Win32。
2. 检查 Windows PowerShell 5.1 与 PowerShell 7 对分析脚本的解析结果。
3. 用历史失败 raw 重放 analyzer,确认 F1/F2 仍失败、F3 仍通过,并纳入三张人工失败截图。
4. 执行仓库单元测试和 `git diff --check`。
5. 使用搜狗重新执行 F1/F2/F3 真实输入,确认三处候选 UI 均跟随当前锚点,提交一次后再次切换也不沿用旧坐标。
- 当前验证结果:
- `EasyX.VisualProbe` Debug x64、Release x64、Debug Win32 均构建成功;仅保留 EasyX 头文件既有的 C4201 匿名联合警告。
- Windows PowerShell 5.1 与 PowerShell 7.6.5 对 `analyze-visual-probe.ps1`、`run-visual-probe.ps1`、`test.ps1` 的 AST 解析均为 0 错误。
- Debug x64 单元测试 15/15 通过。
- 历史失败 raw 重放后仍保持 `automatedStatus=fail`:F1/F2 为 0 次定位尝试,F3 为 2/2;三张失败截图已纳入 manifest,`evidenceCount=6`,manifest SHA-256 为 `0AE37CB4D1B9B2D91A7625E0A3ADA5C4FA9B44C1671CA055EA84965D03F2D1E8`。
- 人工失败截图哈希分别为:F1 `5D54508C93385C07D10E9A347CA3443D750F81F4A1EF31E6D09C24692666D852`,F2 `7FE71CBB96DFD04301D1E7BD13ACB90CF6DFB2DBEB5F1E0DAEAD8AB1648D5B8D`,F3 stale-position `55DF55E86D7F4DE83AFB1901CEA23FBE825A04D6C2D857447530D0F39646480A`。
- 搜狗修复后真实运行 `20261006-010231-219-3fc5186a-f494-4d88-8988-67d80b25ed7e` 自动状态与人工状态均为 `pass`;F1 预设 2/2、F2 预设 1/1、F3 预设 1/1 且生命周期校准 2/2。
- F1 初始截图与“F3 提交后重新回到 F1”截图裁剪结果字节一致,SHA-256 均为 `230C2C260174AD4B45F95E97AF27CC4C0DF37105F9DB960EFBC6C1CFE6A2EC11`,确认不再沿用 F3 stale position。
- 成功运行 manifest SHA-256 为 `4908B1F9B5C29555216EF62BE64F2F6780F7EF213D8E1ECE16817B5915FF9AED`;人工评审旁车 SHA-256 为 `94459F19A3F74E46C23322E6276C03B3AF054D6293DA4D090220FF06470A32A3`,sidecar 校验匹配。
- analyzer 已验证人工 review 覆盖三个用例、逐用例证据归属和总体状态推导;使用临时副本把 F2 review 指向不存在的截图时,analyzer 按预期拒绝生成 manifest。
- 人工评审机制落地后再次执行 Debug x64 单元测试,15/15 通过。
- 回归检查: 微软拼音定位行为需在最终收口前确认未退化;阶段 6 仍须使用真实 TextBox anchor 集成复验。
- 验证证据: 修复前失败运行 `20261005-235150-704-4e01df99-019b-41bd-b8ea-b2ff508626e0`;修复后成功运行 `20261006-010231-219-3fc5186a-f494-4d88-8988-67d80b25ed7e`。
## 落地信息
- Commit: `43cf4ac`
- PR:无
- 发布版本:未发布
- 备注: 失败截图位于忽略目录 `TestResults`,文档只记录相对路径、事实与证据边界。
@@ -7,7 +7,7 @@
- 模块 ID:Module-20261001-0001 - 模块 ID:Module-20261001-0001
- 模块名称:通用输入采集、焦点与键盘路由系统 - 模块名称:通用输入采集、焦点与键盘路由系统
- 状态:设计中(v0.7 TSF/HKL 证据修订;阶段 0A 已实施,阶段 1 尚未开始) - 状态:设计中(v0.9 IME 位置双时机修订;0C 当前可用环境基线通过,下一步进入 0B,阶段 1 尚未开始)
- 类型:新增模块 / 架构演进 - 类型:新增模块 / 架构演进
- 所属系统 / 子系统:StellarX Core / Window / Control / Input / Focus / TextBox - 所属系统 / 子系统:StellarX Core / Window / Control / Input / Focus / TextBox
- 版本 / 分支:`master`,阶段 0A 实现基线 `c218c25` - 版本 / 分支:`master`,阶段 0A 实现基线 `c218c25`
@@ -19,7 +19,7 @@
- 背景: - 背景:
- 当前框架已经具备 Window、Control、Canvas、Button、TextBox、Table、TabControl、Dialog 等基础能力,但尚未形成统一的输入采集、逻辑焦点和键盘定向路由体系。 - 当前框架已经具备 Window、Control、Canvas、Button、TextBox、Table、TabControl、Dialog 等基础能力,但尚未形成统一的输入采集、逻辑焦点和键盘定向路由体系。
- 当前 TextBox 仍依赖 EasyX 输入能力,焦点也不能继续作为 TextBox 独有状态;Button、Table 分页按钮、TabControl 页签按钮和 Dialog 都需要进入同一套键盘交互模型。 - 当前 TextBox 仍依赖 EasyX 输入能力,焦点也不能继续作为 TextBox 独有状态;Button、Table 分页按钮、TabControl 页签按钮和 Dialog 都需要进入同一套键盘交互模型。
- 阶段 0 已确认 EasyX HWND/WndProc 与宿主 `peekmessage` 分属两个线程,当前窗口为 ANSI;微软拼音与搜狗的提交/回声顺序已经取得真实输入法证据。 - 阶段 0 已确认 EasyX HWND/WndProc 与宿主 `peekmessage` 分属两个线程,当前窗口为 ANSI;微软拼音与搜狗的提交/回声顺序已经取得真实输入法证据;宽字符 GDI 绘制链与当前 96/144 DPI 坐标基线也已取得自动证据。
- 当前痛点: - 当前痛点:
- 缺少“哪个控件接收键盘”的统一规则。 - 缺少“哪个控件接收键盘”的统一规则。
- 键盘物理按键、字符输入和 IME 提交结果没有明确分层。 - 键盘物理按键、字符输入和 IME 提交结果没有明确分层。
@@ -93,7 +93,7 @@
- [`Module-20261001-0002-阶段0A输入链路被动探测规格.md`](./Module-20261001-0002-阶段0A输入链路被动探测规格.md) - [`Module-20261001-0002-阶段0A输入链路被动探测规格.md`](./Module-20261001-0002-阶段0A输入链路被动探测规格.md)
#### 1.1 阶段 0 已确认事实(更新至 2026-10-04) #### 1.1 阶段 0 已确认事实(更新至 2026-10-06)
独立 `EasyX.ThreadProbe` 不链接 StellarX,只执行裸 `initgraph`、WndProc 子类化、固定键投递和 `peekmessage`。实测: 独立 `EasyX.ThreadProbe` 不链接 StellarX,只执行裸 `initgraph`、WndProc 子类化、固定键投递和 `peekmessage`。实测:
@@ -115,6 +115,17 @@
- 原探测器遗漏了 `WM_INPUTLANGCHANGEREQUEST` 默认观察,v0.6 已补入 request/change 分层:request 只观察和转发,change 才更新活动代码页。 - 原探测器遗漏了 `WM_INPUTLANGCHANGEREQUEST` 默认观察,v0.6 已补入 request/change 分层:request 只观察和转发,change 才更新活动代码页。
- v0.7 补观测后的 Win+Space 专用短测仍未出现 request/change;当前系统仅安装 `zh-Hans-CN` 语言标签,实际切换发生在同一中文 HKL/CP936 下的 TSF 输入配置文件之间,因此该结果确认“TSF 配置文件切换不保证产生 HKL 消息”,但不代表跨语言 HKL 切换也不会产生。 - v0.7 补观测后的 Win+Space 专用短测仍未出现 request/change;当前系统仅安装 `zh-Hans-CN` 语言标签,实际切换发生在同一中文 HKL/CP936 下的 TSF 输入配置文件之间,因此该结果确认“TSF 配置文件切换不保证产生 HKL 消息”,但不代表跨语言 HKL 切换也不会产生。
2026-10-05 的独立 `EasyX.VisualProbe` 进一步确认:
- EasyX 25.9.10 的 `TextOutW`、`DrawTextW`、`GetTextExtentPoint32W` 可直接写入主画面与 `SetWorkingImage` 工作图像,覆盖批次内外、BMP 中文、代理对、NFD 组合字符和混排;无需因当前 MBCS 工程默认降级 CP936 绘制。
- 主画面批次内绘制在 Flush 前不出现在客户区,Flush/End 后出现;工作图像不会自行上屏,必须显式 `putimage` 合成。
- 批次必须在主画面进入/退出;`SetWorkingImage(&working)` 后直接调用 `BeginBatchDraw()` 会触发 `0xC0000005`。安全顺序是“主画面 Begin → 批次内切工作图像 → 回主画面 → Flush/End”。
- 当前应用继承模式为 DPI-unaware,窗口 DPI 为 96;同一台 150% 显示器上的独立 Per-Monitor-V2 进程窗口 DPI 为 144。两种模式下 EasyX、原生 GDI 与 HWND 客户区使用相同数值坐标,未观察到 EasyX 额外隐式缩放,客户区/屏幕往返误差为 0。
- 上述坐标结论只覆盖当前显示器的 96 虚拟化视图和 144 物理视图;100%/125%、跨屏及真实候选框视觉位置仍不得宣称完成。
- 微软拼音三个合成 anchor 的真实截图未观察到相对 caret 的整体偏移;两轮人工操作实际均为微软拼音,脚本中的预期名称不能代替输入法身份事实。
- 搜狗真实运行证明候选 UI 可能已经出现,但 EasyX HWND 尚未收到探测器原先依赖的 `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION`。因此位置设置不能只挂在 IME 生命周期消息上;必须在焦点/caret/anchor 变化时预设,并在生命周期消息到达时再校准。
- 修复后的搜狗运行中,F1/F2/F3 的 anchor 预设全部成功,F3 生命周期二次校准成功;F3 提交后回到 F1 的截图与初始 F1 裁剪字节一致。当前 96 DPI 虚拟化视图下,微软拼音与搜狗 0C-3 均通过人工视觉验收。
### 2. 分层架构 ### 2. 分层架构
```text ```text
@@ -531,6 +542,15 @@ DPI 规则:
4. 只有实测发现坐标空间不同才做明确转换。 4. 只有实测发现坐标空间不同才做明确转换。
5. 验证 100%、125%、150% 和跨显示器移动。 5. 验证 100%、125%、150% 和跨显示器移动。
当前 0C-2 证据允许把第 3 条落实为当前默认:在已验证的 DPI-unaware 96 视图和 Per-Monitor-V2 144 视图中,`caretBottom` 与 `caretLineRect` 直接使用目标 HWND 客户区坐标,不再手工乘 DPI 比例。该默认必须在 0C-3 真实 IME 和尚缺的 100%/125%/跨屏矩阵中继续验证;发现反例时只能增加带 awareness 适用范围的显式换算,不能全局补乘比例。
位置刷新规则:
1. TextBox 获得焦点、caret 移动、选择变化导致 caret 改变、滚动改变可视 caret,或输入法/输入配置发生可观察变化时,立即把最新 anchor 写入当前 IME context。
2. `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION` 到达时再次写入,校准 IME 在组合开始时重置位置的实现差异。
3. 两类写入使用同一份 `TextInputAnchor` 和 HWND 客户区坐标,不建立两套换算公式。
4. 不把“未收到 IME 生命周期消息”等同于“没有候选 UI”;搜狗实测已经否定该假设。
### 10. 通用焦点系统 ### 10. 通用焦点系统
#### 10.1 焦点能力 #### 10.1 焦点能力
@@ -944,7 +964,7 @@ struct DispatchState
14. 跨显示器后的 DPI 和候选框坐标。(归属阶段 0C) 14. 跨显示器后的 DPI 和候选框坐标。(归属阶段 0C)
15. 接管后 EasyX 原有鼠标、缩放和退出行为是否正常。 15. 接管后 EasyX 原有鼠标、缩放和退出行为是否正常。
其中第 5、6、9 项在当前微软拼音/搜狗 + ANSI EasyX + CP936 环境下已有结论:正式提交来自 `GCS_RESULTSTR`,`WM_IME_CHAR` 是完整匹配回声,未观察到匹配的 `WM_CHAR` lead/trail 回声。第 7 项仍属于 0B 消费实验;第 12–14 项仍属于 0C。 其中第 5、6、9 项在当前微软拼音/搜狗 + ANSI EasyX + CP936 环境下已有结论:正式提交来自 `GCS_RESULTSTR`,`WM_IME_CHAR` 是完整匹配回声,未观察到匹配的 `WM_CHAR` lead/trail 回声。第 12 项的自动绘制门槛已通过;第 13 项已取得当前 150% 显示器上的 DPI-unaware 96 与 Per-Monitor-V2 144 坐标基线,并完成微软拼音/搜狗真实候选框视觉验收;100%/125% 仍受实机条件限制。第 14 项仍待跨屏环境。第 7 项仍属于 0B 消费实验。
阶段 0A 仅被动观察;阶段 0C 采集宽字符绘制与 IME 坐标证据;阶段 0B 才允许在测试开关下按消息类型、虚拟键和 IME 状态执行隔离消费实验。三者是能力分类,编号不代表执行顺序,建议顺序为 0A → 0C → 0B。 阶段 0A 仅被动观察;阶段 0C 采集宽字符绘制与 IME 坐标证据;阶段 0B 才允许在测试开关下按消息类型、虚拟键和 IME 状态执行隔离消费实验。三者是能力分类,编号不代表执行顺序,建议顺序为 0A → 0C → 0B。
@@ -967,6 +987,7 @@ struct DispatchState
- 覆盖 100%、125%、150% 和跨屏不同 DPI。 - 覆盖 100%、125%、150% 和跨屏不同 DPI。
- 采集 `COMPOSITIONFORM` / `CANDIDATEFORM` 定位证据。 - 采集 `COMPOSITIONFORM` / `CANDIDATEFORM` 定位证据。
- 详细规格见 [`Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md`](./Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md)。 - 详细规格见 [`Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md`](./Module-20261001-0003-阶段0C宽字符绘制与IME坐标探测规格.md)。
- 当前状态:0C-1 自动门槛通过;0C-2 当前 96/144 DPI 自动门槛通过;微软拼音与搜狗三个 anchor 已取得真实视觉证据,搜狗提交后 stale-position 回归通过。当前可用环境的 0C 基线关闭,100%/125%/跨屏作为实机能力缺口继续记录,下一步进入 0B。
#### 阶段 0B:隔离消费实验 #### 阶段 0B:隔离消费实验
@@ -1057,12 +1078,16 @@ struct ProbeConsumeRule
22. `WM_INPUTLANGCHANGEREQUEST` 只表示请求,不能提前更新代码页;只有 `WM_INPUTLANGCHANGE`,或挂接/重新获焦时确认 HKL 快照确已变化,才执行“清 pending lead → 更新代码页”。 22. `WM_INPUTLANGCHANGEREQUEST` 只表示请求,不能提前更新代码页;只有 `WM_INPUTLANGCHANGE`,或挂接/重新获焦时确认 HKL 快照确已变化,才执行“清 pending lead → 更新代码页”。
23. IME 内部中英文模式或 TSF 输入配置文件切换不保证产生 `WM_INPUTLANGCHANGE`,不得把该消息当作所有输入模式变化的统一通知。 23. IME 内部中英文模式或 TSF 输入配置文件切换不保证产生 `WM_INPUTLANGCHANGE`,不得把该消息当作所有输入模式变化的统一通知。
24. 当前环境只有 `zh-Hans-CN`,无法实测跨语言 HKL;阶段 1 在窗口挂接和重新获焦时必须用 HWND 属主线程的 `GetKeyboardLayout` 快照补偿潜在消息缺口。 24. 当前环境只有 `zh-Hans-CN`,无法实测跨语言 HKL;阶段 1 在窗口挂接和重新获焦时必须用 HWND 属主线程的 `GetKeyboardLayout` 快照补偿潜在消息缺口。
25. 当前 EasyX 宽字符 GDI 绘制链可用;TextBox 自绘优先保留 UTF-16 并使用 W API,不启用 CP936 显示降级。
26. EasyX 批次必须从主画面进入和退出;工作图像只在批次内部切换,并通过显式合成上屏。
27. 在已验证的 DPI-unaware 96 与 Per-Monitor-V2 144 环境中,EasyX 绘制坐标可直接作为 HWND 客户区坐标;不得额外乘 DPI 比例。
28. IME 位置必须在焦点/caret/anchor 变化时立即预设,并在组合生命周期消息到达时再次校准;不得假定候选 UI 出现前一定会收到 `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION`。
## 实现与影响 ## 实现与影响
- 关键实现点: - 关键实现点:
- 阶段 0A 探测器已经进入框架,FocusManager、生产 NativeInputQueue 和 TextBox 改造尚未开始。 - 阶段 0A 探测器已经进入框架;0C 独立视觉探测工程已实现;FocusManager、生产 NativeInputQueue 和 TextBox 改造尚未开始。
- 独立 EasyX 探测已经推翻 v0.4 的单线程假设,v0.5 据此补入跨线程队列约束;v0.6 根据微软拼音/搜狗真实日志锁定结果串与回声关系,并补入输入布局 request/change 分层;v0.7 根据专用短测区分 TSF 配置文件切换与跨语言 HKL,并增加重新获焦快照约束。 - 独立 EasyX 探测已经推翻 v0.4 的单线程假设,v0.5 据此补入跨线程队列约束;v0.6 根据微软拼音/搜狗真实日志锁定结果串与回声关系,并补入输入布局 request/change 分层;v0.7 根据专用短测区分 TSF 配置文件切换与跨语言 HKL,并增加重新获焦快照约束;v0.8 根据 0C 自动证据锁定宽字符绘制、批次进入顺序与当前 DPI 坐标规则;v0.9 根据搜狗真实失败与修复复验锁定 IME 位置双时机规则。
- 涉及文件 / 类 / 函数: - 涉及文件 / 类 / 函数:
- 预计涉及 `Window`、`Control`、`Canvas`、`Button`、`TextBox`、`Table`、`TabControl`、`Dialog`。 - 预计涉及 `Window`、`Control`、`Canvas`、`Button`、`TextBox`、`Table`、`TabControl`、`Dialog`。
- 实际文件和接口必须在阶段 0 与实现前只读检查后确定。 - 实际文件和接口必须在阶段 0 与实现前只读检查后确定。
@@ -1081,7 +1106,7 @@ struct ProbeConsumeRule
- 测试范围: - 测试范围:
- 设计评审覆盖输入采集、同步消息、Unicode、IME、焦点、复合控件、按键归属、TextBox 和模态重入。 - 设计评审覆盖输入采集、同步消息、Unicode、IME、焦点、复合控件、按键归属、TextBox 和模态重入。
- 独立线程归属探测、0A 构建、固定消息 smoke 及微软拼音/搜狗真实输入法测试已完成;生产输入队列和焦点系统尚未开始。 - 独立线程归属探测、0A 构建、固定消息 smoke、微软拼音/搜狗真实输入链路测试以及当前可用环境的 0C 基线均已完成;0B 消费实验、生产输入队列和焦点系统尚未开始。
- 验证步骤: - 验证步骤:
1. 先按关联规格实现阶段 0A 被动探测并取得消息链路证据。 1. 先按关联规格实现阶段 0A 被动探测并取得消息链路证据。
@@ -1098,16 +1123,23 @@ struct ProbeConsumeRule
- 2026-10-04:专用 Win+Space 短测产生 1243 条可解析记录,request/change 仍为 0;日志观察到 Win 键、焦点往返及 `VK_PROCESSKEY -> IME composition`,系统语言列表仅有 `zh-Hans-CN`,故本次结论限定为同 HKL 的 TSF 配置文件切换,跨语言 HKL 分支仍缺少实机样本。 - 2026-10-04:专用 Win+Space 短测产生 1243 条可解析记录,request/change 仍为 0;日志观察到 Win 键、焦点往返及 `VK_PROCESSKEY -> IME composition`,系统语言列表仅有 `zh-Hans-CN`,故本次结论限定为同 HKL 的 TSF 配置文件切换,跨语言 HKL 分支仍缺少实机样本。
- 2026-10-04:最终源码的 Debug x64 单元测试 15/15 通过。 - 2026-10-04:最终源码的 Debug x64 单元测试 15/15 通过。
- 2026-10-05:提交前重新执行独立线程探针、探测开启 UI 回归和探测关闭单元测试;再次确认 EasyX 双线程与 ANSI/CP936,8 份自动日志均严格递增、零文字原值泄露、零丢失,VSTest 15/15 通过。 - 2026-10-05:提交前重新执行独立线程探针、探测开启 UI 回归和探测关闭单元测试;再次确认 EasyX 双线程与 ANSI/CP936,8 份自动日志均严格递增、零文字原值泄露、零丢失,VSTest 15/15 通过。
- 2026-10-05:0C-1 在主画面/工作图像、批次内外的 40 个结构化绘制用例全部通过;30 张证据图带 SHA-256,W API、表面隔离、GDI 状态恢复、Flush/End 和后续 EasyX 绘制兼容性均有自动证据。
- 2026-10-05:0C-2 在 DPI-unaware 96 和独立 Per-Monitor-V2 144 两种视图中均未观察到 EasyX/GDI/HWND 坐标缩放差异;当前组合基线 ID 与完整哈希记录在 0003 规格。
- 2026-10-05:0C-3 三 anchor 探针与优雅退出/失败门槛烟测通过,但真实微软拼音/搜狗定位尚未执行,不计入阶段门槛通过。
- 2026-10-05:微软拼音三个 anchor 自动 IMM 调用和人工位置观察通过;第二轮虽然脚本参数为 `Sogou`,操作者确认实际未切换输入法,因此没有把它误记为搜狗证据。
- 2026-10-05 23:51:搜狗首次真实运行按预期门槛失败,F1/F2 未发生 IMM 定位调用,F3 为 2/2;截图确认候选框先位于窗口外,之后沿用 F3 旧位置。
- 2026-10-06:v0.9 将 IME 位置刷新改为“焦点/caret/anchor 变化立即预设 + IME 生命周期再次校准”;探测事件新增触发来源与分项计数,并完成三平台构建及真实搜狗回归。
- 2026-10-06:搜狗修复后运行自动与人工状态均为 `pass`;三个 anchor 均按请求定位,F3 提交后回到 F1 的截图与初始 F1 裁剪字节一致。当前可用环境 0C-3 关闭,下一步进入 0B。
- 已知限制 / 遗留问题: - 已知限制 / 遗留问题:
- §5.3 的最终消费白名单必须等待阶段 0B 数据。 - §5.3 的最终消费白名单必须等待阶段 0B 数据。
- §7.3 的微软拼音/搜狗回声状态机已有 0A 证据,其他 IME 仍需保持可诊断回退。 - §7.3 的微软拼音/搜狗回声状态机已有 0A 证据,其他 IME 仍需保持可诊断回退。
- 宽字符 GDI 与 DPI 坐标仍需实机验证,由阶段 0C 规格承接。 - 宽字符 GDI 与当前微软拼音/搜狗定位门槛已通过;100%/125% 与跨屏 DPI 仍由阶段 0C 规格作为实机能力缺口承接。
## 落地信息 ## 落地信息
- 关联功能变更 ID:无 - 关联功能变更 ID:无
- 关联 BUG / Fix:无 - 关联 BUG / Fix:无
- Commit:`c218c25`(阶段 0A 诊断实现与线程归属复现);`c0eca1d`(探测暴露的原生窗口关闭修复) - Commit:`c218c25`(阶段 0A 诊断实现与线程归属复现);`c0eca1d`(探测暴露的原生窗口关闭修复);`43cf4ac`(阶段 0C 视觉与 IME 坐标探测)
- PR:无 - PR:无
- 发布版本:未发布 - 发布版本:未发布
- 相关文档: - 相关文档:
@@ -7,10 +7,10 @@
- 模块 ID:Module-20261001-0003 - 模块 ID:Module-20261001-0003
- 模块名称:阶段 0C 宽字符绘制与 IME 坐标探测规格 - 模块名称:阶段 0C 宽字符绘制与 IME 坐标探测规格
- 状态:设计中(评审意见已并入,探测器尚未实现) - 状态:当前可用环境基线通过(0C-1/0C-2 自动基线通过;0C-3 微软拼音与搜狗真实定位通过;100%/125%/跨屏矩阵仍受实机条件限制)
- 类型:新增模块 / 诊断基础设施 - 类型:新增模块 / 诊断基础设施
- 所属系统 / 子系统:StellarX Core / Window / GDI / IMM32 / DPI - 所属系统 / 子系统:StellarX Core / Window / GDI / IMM32 / DPI
- 版本 / 分支:`master`,前置阶段 0A 基线 `c218c25` - 版本 / 分支:`master`,本轮实现起点 `225bc35`,前置阶段 0A 基线 `c218c25`
- 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX + GDI + IMM32 - 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX + GDI + IMM32
- 负责人:Codex 协作设计 - 负责人:Codex 协作设计
- 阶段定位:阶段 0 探测的能力分类之一(0A 消息链路 / 0B 消费实验 / 0C 绘制与坐标)。编号不代表严格执行顺序,建议顺序为 0A → 0C → 0B。 - 阶段定位:阶段 0 探测的能力分类之一(0A 消息链路 / 0B 消费实验 / 0C 绘制与坐标)。编号不代表严格执行顺序,建议顺序为 0A → 0C → 0B。
@@ -109,6 +109,26 @@ GetBkMode / GetTextColor / GetBkColor / GetTextAlign
GetTextCharacterExtra GetTextCharacterExtra
``` ```
`BeginBatchDraw` 前后即使 HDC 值相同,其内选入的 bitmap 也可能变化,因此表面身份必须在“实际绘制发生的那一刻”采集。绘制矩阵必须覆盖批次状态:
```text
默认画面 / SetWorkingImage
×
批次外 / BeginBatchDraw 批次内
×
绘制后 / FlushBatchDraw 后 / EndBatchDraw 后
×
EasyX 绘制 / 宽字符 GDI 绘制
```
这样才能区分五种不同结果:
- API 返回成功但画到了错误 bitmap。
- 已进入离屏缓冲但尚未合成上屏。
- `FlushBatchDraw` 后才可见(合成链正确)。
- `EndBatchDraw` 后被后续 EasyX 绘制覆盖。
- 工作图像正确但屏幕合成失败。
主画面和工作图像分别使用不同的固定哨兵底色,并按以下步骤验证: 主画面和工作图像分别使用不同的固定哨兵底色,并按以下步骤验证:
1. `SetWorkingImage` 切换前后分别采集 HDC、selected bitmap 和 GDI 状态。 1. `SetWorkingImage` 切换前后分别采集 HDC、selected bitmap 和 GDI 状态。
@@ -118,6 +138,21 @@ GetTextCharacterExtra
5. 从目标表面回读像素,验证目标发生预期变化。 5. 从目标表面回读像素,验证目标发生预期变化。
6. 从非目标表面回读相同区域,验证没有串画。 6. 从非目标表面回读相同区域,验证没有串画。
7. 调用 `RestoreDC`,重新采集状态并验证未污染 EasyX 后续绘制。 7. 调用 `RestoreDC`,重新采集状态并验证未污染 EasyX 后续绘制。
8. 记录 `FlushBatchDraw` 前、后以及 `EndBatchDraw` 后的屏幕证据,区分“已进离屏未合成”与“合成后可见”。
9. 在 `EndBatchDraw` 后对同一区域用 EasyX 再绘制一次并回读,确认宽字符 GDI 结果是否被后续 EasyX 绘制覆盖。
实测还确认了一个 EasyX 特有的调用顺序约束:不得在 `SetWorkingImage(&working)` 保持为当前目标时直接进入 `BeginBatchDraw()`;本轮隔离运行中该顺序稳定触发 `0xC0000005`。探测器和后续框架应使用:
```text
SetWorkingImage(nullptr)
BeginBatchDraw()
SetWorkingImage(&working) // 只在批次内部切换工作图像
...绘制...
SetWorkingImage(nullptr) // Flush/End 前回到主画面
FlushBatchDraw()/EndBatchDraw()
```
这不是“工作图像不能在批次内绘制”,而是“批次必须从主画面安全进入和退出”。工作图像上的宽字符结果仍需通过显式 `putimage` 合成到主画面。
宽字符能力与字体覆盖分开判定: 宽字符能力与字体覆盖分开判定:
@@ -136,9 +171,23 @@ GetTextCharacterExtra
- 验证 EasyX 逻辑坐标是否等于 HWND 目标坐标,还是需要显式换算。 - 验证 EasyX 逻辑坐标是否等于 HWND 目标坐标,还是需要显式换算。
- IMM 输入链只定义为:合成控件局部坐标 → EasyX 绘制坐标 → 目标 HWND 相对坐标 → `COMPOSITIONFORM/CANDIDATEFORM`。 - IMM 输入链只定义为:合成控件局部坐标 → EasyX 绘制坐标 → 目标 HWND 相对坐标 → `COMPOSITIONFORM/CANDIDATEFORM`。
- 同时把目标 HWND 相对坐标换算为屏幕坐标,作为截图标注和诊断证据;屏幕坐标不是预设的 IMM 输入中间态。 - 同时把目标 HWND 相对坐标换算为屏幕坐标,作为截图标注和诊断证据;屏幕坐标不是预设的 IMM 输入中间态。
- 额外记录:`GetMapMode`、viewport/window origin 与 extent、当前 graphics mode / world transform、`GetClientRect`、`GetWindowRect`,以及 `ClientToScreen → ScreenToClient` 的往返误差。
验证 EasyX 是否隐式缩放时,不只画文字,而是在同一组数值坐标上分别绘制:
```text
EasyX 十字和矩形
原生 GDI 十字和矩形
EasyX 文字基线
TextOutW 文字基线
```
然后通过像素 bounding box 比较位置和尺寸,区分“坐标相同但字体度量不同”与“整个绘图坐标系发生缩放”。
规则:只有实测发现 EasyX 坐标空间与目标 HWND 坐标空间不一致时,才引入显式换算;否则直接使用,避免双重缩放。 规则:只有实测发现 EasyX 坐标空间与目标 HWND 坐标空间不一致时,才引入显式换算;否则直接使用,避免双重缩放。
DPI awareness 不得在窗口创建后临时切换;需要比较不同 awareness 模式时各自启动独立进程,否则结论易受 Windows 虚拟化状态污染。
验收预期必须服从实际 DPI awareness: 验收预期必须服从实际 DPI awareness:
| Awareness | `GetDpiForWindow` 预期 | 跨屏预期 | | Awareness | `GetDpiForWindow` 预期 | 跨屏预期 |
@@ -174,6 +223,13 @@ COMPOSITIONFORM 输入值与 API 返回值
CANDIDATEFORM 输入值与 API 返回值 CANDIDATEFORM 输入值与 API 返回值
``` ```
IME 位置采用“双时机写入”,不得假定所有输入法都会先向目标 HWND 发送相同的组合生命周期消息:
1. 焦点、caret 或合成 anchor 变化时,立即对当前 IME context 调用 `ImmSetCompositionWindow/ImmSetCandidateWindow`,为下一次组合预设位置。
2. 收到 `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION` 时使用最新 anchor 再校准一次。
3. 两类调用必须分别记录 `positionTrigger`、调用次数与成功次数;预设调用成功不能代替真实输入和视觉验收。
4. 诊断程序至少还要有一次真实 IME 生命周期定位事件完成 context 获取与两个 setter,才允许自动运行整体完成;仅切换三个 anchor 或只观察到失败的生命周期调用不得伪造通过。
测试至少覆盖微软拼音和搜狗输入法,并记录输入法标识、版本(可取得时)、HKL 和测试时间。不同 IME 可能主动调整位置或不显示独立组合窗口: 测试至少覆盖微软拼音和搜狗输入法,并记录输入法标识、版本(可取得时)、HKL 和测试时间。不同 IME 可能主动调整位置或不显示独立组合窗口:
- API 返回成功不能代替视觉验收。 - API 返回成功不能代替视觉验收。
@@ -213,6 +269,8 @@ anchor:top_left_safe / center / bottom_right_safe
```text ```text
TestResults/VisualProbe/<sessionId>/manifest-v1.json TestResults/VisualProbe/<sessionId>/manifest-v1.json
TestResults/VisualProbe/<sessionId>/evidence/*.png TestResults/VisualProbe/<sessionId>/evidence/*.png
TestResults/VisualProbe/<sessionId>/manual-evidence/*.png
TestResults/VisualProbe/<sessionId>/manual-review-v1.json
``` ```
`visualProbeSchemaVersion` 初始为 1。manifest 必须包含: `visualProbeSchemaVersion` 初始为 1。manifest 必须包含:
@@ -227,7 +285,9 @@ TestResults/VisualProbe/<sessionId>/evidence/*.png
| `testCaseId` | 是 | 矩阵中唯一用例 ID | | `testCaseId` | 是 | 矩阵中唯一用例 ID |
| `testStringId/codePoints/utf16Units` | 绘制用例 | 固定测试串身份与实际构造结果 | | `testStringId/codePoints/utf16Units` | 绘制用例 | 固定测试串身份与实际构造结果 |
| `surfaceId` | 绘制用例 | `main` 或指定 working image | | `surfaceId` | 绘制用例 | `main` 或指定 working image |
| `hdc/hbitmap/hfont/logFont` | 绘制用例 | 表面和字体指纹 | | `batchState` | 绘制用例 | `outside / inside`,描述绘制发生时是否位于批次括号内 |
| `observationPhase` | 绘制用例 | `after_draw / after_flush / after_end`;不适用的组合写 `not_applicable` |
| `hdc/hbitmap/bitmapWidth/bitmapHeight/planes/bitsPerPixel/stride/dibSection/hfont/logFont` | 绘制用例 | 表面和字体指纹;无法从 DDB 稳定取得的字段允许写 `not_available` |
| `gdiStateBefore/gdiStateAfter` | 绘制用例 | mapping、clip、颜色、对齐等 | | `gdiStateBefore/gdiStateAfter` | 绘制用例 | mapping、clip、颜色、对齐等 |
| `apiResults` | 绘制用例 | 各 GDI API 的函数特定返回语义 | | `apiResults` | 绘制用例 | 各 GDI API 的函数特定返回语义 |
| `logicalExtent` | 绘制用例 | 相同 HDC/字体/状态下的测量结果 | | `logicalExtent` | 绘制用例 | 相同 HDC/字体/状态下的测量结果 |
@@ -239,20 +299,27 @@ TestResults/VisualProbe/<sessionId>/evidence/*.png
| `anchorCoordinates` | 坐标用例 | local/surface/HWND-relative/screen 四组坐标 | | `anchorCoordinates` | 坐标用例 | local/surface/HWND-relative/screen 四组坐标 |
| `compositionForm/candidateForm` | IME 用例 | 输入值、style、API 结果 | | `compositionForm/candidateForm` | IME 用例 | 输入值、style、API 结果 |
| `imeId/imeVersion/hkl` | IME 用例 | 输入法身份 | | `imeId/imeVersion/hkl` | IME 用例 | 输入法身份 |
| `positionTrigger/compositionActiveAtAttempt` | IME 用例 | 区分 anchor 预设与 IME 生命周期校准,并记录调用时是否处于组合态 |
| `preconfigureApiAttemptCount/lifecycleApiAttemptCount` | IME 用例 | 两类定位调用及其成功次数,避免把不同触发来源混为一类 |
| `positionDisposition` | IME 用例 | `as_requested/system_adjusted/not_observable` | | `positionDisposition` | IME 用例 | `as_requested/system_adjusted/not_observable` |
| `automatedStatus` | 是 | `pass/fail/inconclusive/not_available` | | `automatedStatus` | 是 | `pass/fail/inconclusive/not_available` |
| `manualStatus` | 是 | `pass/fail/inconclusive/not_reviewed/not_available` | | `manualStatus` | 是 | `pass/fail/inconclusive/not_reviewed/not_available` |
| `sourceManualReview/manualReview` | 有人工评审时 | 评审旁车路径与哈希、实际输入法身份来源、评审时间和摘要 |
| `failureCategory` | 否 | 失败分类,不得只写自由文本 | | `failureCategory` | 否 | 失败分类,不得只写自由文本 |
| `evidenceFiles` | 是 | 相对路径、用途、SHA-256、裁剪区域 | | `evidenceFiles` | 是 | 相对路径、用途、SHA-256、裁剪区域 |
| `notes` | 否 | 补充说明,不能替代结构化结论 | | `notes` | 否 | 补充说明,不能替代结构化结论 |
截图只能包含探测窗口和固定非敏感测试内容;不得截取其他应用或真实用户输入。每个 evidence 文件记录 SHA-256,防止结果文件和图片错配。 截图只能包含探测窗口和固定非敏感测试内容;不得截取其他应用或真实用户输入。每个 evidence 文件记录 SHA-256,防止结果文件和图片错配。
人工结论通过独立 `manual-review-v1.json` 输入 analyzer,不直接手改生成的 manifest。analyzer 必须验证 review 覆盖所有用例、状态和位置枚举合法、`pass` 用例具有明确的 `manual-evidence` 文件、人工通过不覆盖自动失败,并校验总体状态可由逐用例状态推导;任一条件不满足即拒绝生成 manifest。
`failureCategory` 只允许以下初始枚举;新增类别必须提升或兼容扩展 schema: `failureCategory` 只允许以下初始枚举;新增类别必须提升或兼容扩展 schema:
```text ```text
surface_unreachable surface_unreachable
surface_cross_write surface_cross_write
surface_not_composited
screen_composite_failed
gdi_state_not_restored gdi_state_not_restored
measurement_relation_invalid measurement_relation_invalid
font_coverage_missing font_coverage_missing
@@ -316,15 +383,60 @@ evidence_missing
CP936 只允许作为已证实宽字符绘制链无法使用后的显式显示降级;测量差异、字体缺字或单个 IME 定位异常都不是 CP936 降级依据。任何降级必须带证据并保留 UTF-16 内部原文。 CP936 只允许作为已证实宽字符绘制链无法使用后的显式显示降级;测量差异、字体缺字或单个 IME 定位异常都不是 CP936 降级依据。任何降级必须带证据并保留 UTF-16 内部原文。
### 10. 运行模式与进程隔离
0C-1 与 0C-2 可以并行开发和独立运行,但不得在同一进程内同时探测:EasyX 工作图像、批次状态和 DPI awareness 都带有进程级或全局状态,混在一次运行中会互相污染。
同一诊断工程提供独立模式,共享 manifest、截图和固定测试串基础设施,但每种模式独立启动进程:
```text
--mode wide-text
--mode dpi-coordinate
--mode ime-position
```
`--auto-close-ms` 仅用于不输入文字的 0C-3 生命周期烟测,正式人工运行不得启用;否则三个 anchor 不会产生真实 IME 位置证据,结果应为失败而不是伪通过。
### 11. 0C 基线产物与 0B 引用契约
0C 完成后必须生成可哈希的基线 manifest(绘制基线 + 坐标基线),并计算其 SHA-256。0B 的每轮实验结果必须强制记录:
```text
drawingBaselineManifest
coordinateBaselineManifest
drawingBaselineSha256
coordinateBaselineSha256
combinedBaselineId
dpiAwareness
dpiForWindow
imeIdentity
hkl
```
如果 0B 运行环境与 0C 基线不一致,该轮 0B 的候选框结论只能标记为 `inconclusive`,不得判定消费策略通过或失败。
## 实现与影响 ## 实现与影响
- 关键实现点: - 关键实现点:
- 当前只形成规格清单,尚未增加任何探测代码。 - 新增不链接 StellarX Core 的 `EasyX.VisualProbe` 独立诊断工程,避免框架 WndProc、控件或事件循环污染 EasyX 原始行为。
- `wide-text` 覆盖主画面/工作图像 × 批次外/批次内,构造五类固定 Unicode scalar 测试串,验证 W API、目标/非目标表面、GDI 状态恢复、Flush/End 合成和后续 EasyX 绘制兼容性。
- `dpi-coordinate` 在同一数值坐标上绘制 EasyX/GDI 矩形、十字和文本,记录 mapping、DPI awareness、像素 bounds 与客户区/屏幕往返误差。
- `ime-position` 提供三个合成 caret anchor;按总设计 v1 契约使用 `COMPOSITIONFORM/CFS_POINT` 与 `CANDIDATEFORM/CFS_EXCLUDE + caretLineRect`;anchor 变化时先预设位置,IME 生命周期消息到达时再校准;子类化分支只记录消息阶段、触发来源、anchor、HKL/IME 身份与 IMM API 返回值,不记录组合串或候选文字。
- 客户区截图优先使用 `GetDC(hwnd) + BitBlt`,只有失败才回退 `PrintWindow`,并把实际捕获后端写入 manifest,避免 `WM_PRINT` 扰动 Flush 前观察。
- PowerShell runner 为每个模式启动独立进程;analyzer 计算原始结果、manifest 和每张 PNG 的 SHA-256,并生成 0B 可引用的组合基线 ID。
- 执行顺序:0C-1(宽字符/批次绘制)与 0C-2(DPI/坐标)可并行 → 0C-3(真实 IME 定位)→ 回写带哈希的 0C 基线 → 0B 单消息消费实验 → 阶段 1 输入采集与规范化。
- 实现时根据 Unicode scalar 列表构造固定测试串,使用合成 anchor、预定义采样区域和主/工作图像双表面哨兵,不依赖用户输入或真实 TextBox。 - 实现时根据 Unicode scalar 列表构造固定测试串,使用合成 anchor、预定义采样区域和主/工作图像双表面哨兵,不依赖用户输入或真实 TextBox。
- 实现结果由版本化 manifest 和独立 PNG 证据组成,不复用 0A 键盘 JSONL schema。 - 实现结果由版本化 manifest 和独立 PNG 证据组成,不复用 0A 键盘 JSONL schema。
- 涉及文件 / 类 / 函数: - 涉及文件 / 类 / 函数:
- 预计观察现有 `Window`、EasyX 工作图像和 GDI 调用链。 - `tests/EasyX.VisualProbe.vcxproj`
- 具体文件名必须在实现前只读检查后确定,本规格不授权修改框架代码。 - `tests/visual-probe/VisualProbeCommon.h/.cpp`
- `tests/visual-probe/WideTextProbe.cpp`
- `tests/visual-probe/DpiCoordinateProbe.cpp`
- `tests/visual-probe/ImePositionProbe.cpp`
- `tests/visual-probe/VisualProbeMain.cpp`
- `scripts/run-visual-probe.ps1`
- `scripts/analyze-visual-probe.ps1`
- `imGui-easyX.sln`
- 兼容性影响: - 兼容性影响:
- 探测关闭时不应产生文件、线程、定时器或公共 API 变化。 - 探测关闭时不应产生文件、线程、定时器或公共 API 变化。
- 性能影响: - 性能影响:
@@ -338,7 +450,10 @@ CP936 只允许作为已证实宽字符绘制链无法使用后的显式显示
## 测试与验证 ## 测试与验证
- 测试范围: - 测试范围:
- 当前仅验证规格完整性,探测器未实现。 - Debug x64 独立工程构建。
- 0C-1 四组表面/批次矩阵、五类固定测试串、30 张 PNG 证据与 40 个结构化 manifest 用例。
- 0C-2 当前生产 awareness(inherit/unaware)和独立 Per-Monitor-V2 进程下的坐标、像素与 DPI 证据。
- 0C-3 自动关闭失败门槛烟测,以及微软拼音/搜狗真实候选框定位与搜狗提交后 stale-position 回归。
- 验证步骤: - 验证步骤:
1. 实现前只读定位 EasyX `SetWorkingImage`、主画面/工作图像 HDC、selected bitmap 和 GDI 调用位置。 1. 实现前只读定位 EasyX `SetWorkingImage`、主画面/工作图像 HDC、selected bitmap 和 GDI 调用位置。
@@ -347,10 +462,33 @@ CP936 只允许作为已证实宽字符绘制链无法使用后的显式显示
4. 依据 §9 分别判定宽字符链、测量链、字体覆盖、坐标换算和 IME 定位;禁止把不同失败类型合并成 CP936 降级。 4. 依据 §9 分别判定宽字符链、测量链、字体覆盖、坐标换算和 IME 定位;禁止把不同失败类型合并成 CP936 降级。
5. 阶段 6 使用真实 TextBox anchor 重跑坐标与 IME 定位集成验证。 5. 阶段 6 使用真实 TextBox anchor 重跑坐标与 IME 定位集成验证。
当前机器没有 `pwsh`,人工 0C-3 使用 Windows PowerShell,分别执行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-visual-probe.ps1 -Mode ime-position -ImeName MicrosoftPinyin
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-visual-probe.ps1 -Mode ime-position -ImeName Sogou
```
每轮依次用 F1/F2/F3 选择三个 anchor,只输入固定非敏感串 `ceshi`,观察候选框后按 Escape 取消。随后在 F3 提交一次固定候选,再切回 F1 输入 `ceshi`,确认没有沿用 F3 旧位置。要关闭 0C-3,必须在脚本打印的 `manual-evidence` 目录保存三个 anchor 的紧裁剪截图,并为提交后回到 F1 的回归场景再保存一张截图;建议文件名分别以 `anchor_top_left_safe_`、`anchor_center_`、`anchor_bottom_right_safe_` 开头。没有截图时即使 IMM API 自动状态通过,人工状态仍保持 `not_reviewed`。
- 验证结果: - 验证结果:
- 截至 2026-10-02,仅完成规格评审意见并入与文档归档,未执行任何绘制或坐标探测。 - 2026-10-05:`EasyX.VisualProbe` Debug x64、Release x64、Debug Win32 构建通过;仅有 EasyX 25.9.10 头文件自身的 C4201 匿名联合警告。解决宿主同时存在 `PATH/Path` 时,构建必须沿用干净子进程隔离,否则会触发环境导致的 MSB6001。
- 2026-10-05:最终自动组合基线位于忽略目录 `TestResults/VisualProbe/20261005-025951-856-06d08ac1-58fa-426f-b4c8-803d246b47dd`,`sessionId=06d08ac1-58fa-426f-b4c8-803d246b47dd`,自动状态为 `pass`,人工状态为 `not_reviewed`。
- 绘制 manifest SHA-256:`BCD0E3DD5D90C5D6F892F9127D81FD124C1DC7D619260B236DBCB8E53CBEC9CA`;坐标 manifest SHA-256:`35D15BF2DD54D9A57B1F8470C8415025F1BAE6CE98D4628F13B64AB1EC381000`;组合基线 ID:`sha256:5C544D9FEC9C1A2324A7AF3F593A1A34110100644BAF5071E777302387E3C6C5`。
- 0C-1:`TextOutW`、`DrawTextW`、`GetTextExtentPoint32W` 在主画面和工作图像、批次内外全部成功;目标表面变化、非目标表面不变、GDI 状态恢复;主画面批次内在 Flush 前不可见、Flush 后可见;工作图像不会自行上屏,经 `putimage + FlushBatchDraw` 后正确合成;End 后追加 EasyX 绘制未改变受保护文字区域的像素摘要。
- 0C-1 截图复核可见 BMP 中文、代理对 emoji 的单色 glyph、NFD 组合字符和混排结果;这属于模型辅助复核,不能替代规格要求的人类视觉签字,因此 manifest 仍保持 `manualStatus=not_reviewed`。
- 0C-2:当前程序继承模式为 DPI-unaware,`GetDpiForWindow=96`;在同一台实际 150% 显示器上,以独立 Per-Monitor-V2 进程运行得到 `GetDpiForWindow=144`。两种 awareness 下 EasyX 与原生 GDI 的像素 bounds 均与客户区一致,四个 `ClientToScreen -> ScreenToClient` 往返误差均为 0,未观察到 EasyX 自身额外隐式缩放。
- Per-Monitor-V2 补充 manifest SHA-256:`091E446F77BD997428452979A0AF122883129497104035F2688A3535C1A191E1`;该结果只覆盖当前 144 DPI 显示器,不代表 100%/125% 或跨屏矩阵已完成。
- 0C-3 无输入自动关闭烟测按预期生成三个 anchor 客户区证据与可解析 manifest,并因没有真实 IME API 事件返回失败;这证明失败门槛没有被烟测绕过,不构成候选框定位通过证据。
- 2026-10-05:微软拼音在三个 anchor 上的 IMM 调用均成功,人工截图未观察到相对 caret 的整体偏移;两轮实际都使用微软拼音,第二轮脚本参数 `Sogou` 仅为预期标签,不得当作搜狗证据。
- 2026-10-05 23:51:首次真实搜狗定位运行 `20261005-235150-704-4e01df99-019b-41bd-b8ea-b2ff508626e0` 按门槛正确失败。F1/F2 的 IMM API 尝试均为 0,F3 为 2/2;人工截图显示 F1/F2 候选框位于窗口外,首次获得 F3 生命周期定位后又沿用 F3 旧位置。
- 2026-10-06:确认根因是探测器只在 `WM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION` 到达后设置位置。已改为 anchor 变化立即预设、生命周期消息再次校准,并增加触发来源及分项计数;三平台构建与搜狗真实复验均已完成。
- 2026-10-06:修复后搜狗运行 `20261006-010231-219-3fc5186a-f494-4d88-8988-67d80b25ed7e` 自动与人工状态均为 `pass`。F1 预设 2/2、F2 预设 1/1、F3 预设 1/1 且生命周期校准 2/2;提交后回到 F1 的截图与初始 F1 裁剪字节一致,证明没有沿用 F3 stale position。
- 成功搜狗 manifest SHA-256:`4908B1F9B5C29555216EF62BE64F2F6780F7EF213D8E1ECE16817B5915FF9AED`;人工评审旁车 SHA-256:`94459F19A3F74E46C23322E6276C03B3AF054D6293DA4D090220FF06470A32A3`;四张人工证据和三张自动 anchor 图均已进入 manifest,`evidenceCount=7`。
- 已知限制 / 遗留问题: - 已知限制 / 遗留问题:
- 跨屏不同 DPI 结论依赖实机环境。 - 当前可用环境下微软拼音与搜狗的三个 anchor 已完成真实视觉验收,搜狗“提交一次后再次切换”回归也已通过;0C-3 当前环境门槛关闭。
- 当前 `-ImeName` 只是操作者预期标签;同一中文 HKL 下的 TSF 配置文件切换不能由现有 HKL/IMM32 身份字段自动证明,最终身份需保留操作者确认边界。
- 当前只覆盖同一台 150% 显示器上的 DPI-unaware 虚拟化视图(96 DPI)和 Per-Monitor-V2 物理视图(144 DPI);100%、125% 与跨屏不同 DPI 仍依赖实机环境。
- 字体回退行为与 EasyX 字体设置的耦合需要实测确认。 - 字体回退行为与 EasyX 字体设置的耦合需要实测确认。
- 0C 使用合成 anchor,只验证坐标转换和 IMM 调用链;真实 TextBox anchor 必须在阶段 6 复验。 - 0C 使用合成 anchor,只验证坐标转换和 IMM 调用链;真实 TextBox anchor 必须在阶段 6 复验。
- 某些 IME 不显示独立组合窗口或会主动调整候选框,允许用例得到 `not_available`、`system_adjusted` 或 `inconclusive`,但必须保留证据。 - 某些 IME 不显示独立组合窗口或会主动调整候选框,允许用例得到 `not_available`、`system_adjusted` 或 `inconclusive`,但必须保留证据。
@@ -359,7 +497,7 @@ CP936 只允许作为已证实宽字符绘制链无法使用后的显式显示
- 关联功能变更 ID:无 - 关联功能变更 ID:无
- 关联 BUG / Fix:无 - 关联 BUG / Fix:无
- Commit:无实现提交(本文件仅归档 0C 规格;0C 探测器尚未实现) - Commit:`43cf4ac`(实现起点 `225bc35`)
- PR:无 - PR:无
- 发布版本:未发布 - 发布版本:未发布
- 相关文档: - 相关文档: