Skip to content

fix(windows-ime): 避免 TSF 预检导致文本重复插入 - #1021

Open
tonyzaca7755963 wants to merge 1 commit into
Open-Less:betafrom
tonyzaca7755963:codex/fix-windows-ime-duplicate-insert
Open

fix(windows-ime): 避免 TSF 预检导致文本重复插入#1021
tonyzaca7755963 wants to merge 1 commit into
Open-Less:betafrom
tonyzaca7755963:codex/fix-windows-ime-duplicate-insert

Conversation

@tonyzaca7755963

Copy link
Copy Markdown

摘要

Related to #1014,但不自动关闭它:#1014 当前描述的是 PreviewConfirm / Ctrl+V 选区替换路径;本 PR 修复的是产生同类“完整文本嵌套重复”症状的 Windows 语音听写 TSF 路径。

Windows TSF edit session 现在只把最终文本提交给宿主一次,避免 Chromium / Electron 富文本宿主在 TF_IAS_QUERYONLY 预检与正式提交之间发生选区重算后,将同一全文嵌套插入两遍。

复现与根因

受影响会话具备以下特征:

  • OpenLess 历史记录中的 finalText 正常且只有一份;
  • 目标输入框中出现一份完整文本,同时另一份完整文本被嵌入原文本中间;
  • 会话只有一次热键开始/结束、一次 ASR 和一次 LLM 请求;
  • streaming_eligible=false,且没有进入 clipboard / SendInput fallback;
  • 目标是 Chromium 内核的富文本输入框。

OpenLessEditSession::InsertText 原先会把同一份全文传给宿主两次:

  1. InsertTextAtSelection(..., TF_IAS_QUERYONLY, full_text, ...)
  2. InsertTextAtSelection(..., 0, full_text, ...)

按照 Microsoft TSF 合同,TF_IAS_QUERYONLY 不应修改 context;但受影响宿主会在这两次调用之间暴露或重算编辑状态,最终表现为全文嵌套重复。这里不需要预检调用:带读写 edit cookie 的正式调用本身会返回 HRESULT 和 committed range。

参考:https://learn.microsoft.com/windows/win32/api/msctf/nf-msctf-itfinsertatselection-inserttextatselection

修复 / 新增 / 改进

  • 移除带完整文本的 TF_IAS_QUERYONLY 预检,改为一次正式 InsertTextAtSelection 调用;
  • 保留提交前取消检查、committed range、Word 光标折叠与 SetSelection 行为;
  • 增加契约测试,强制 edit session 只能出现一次 InsertTextAtSelection,且不得重新引入全文 QUERYONLY
  • 加强真实插入 smoke:对初始为空的 Notepad、browser、Win32 Edit 目标要求回读文本与 finalText 完全相等。旧的 Contains 断言会让嵌套重复误通过,因为内层仍包含一份完整 finalText

兼容

  • 不包含:PreviewConfirm 的 Ctrl+C 校验路径、clipboard fallback、SendInput fallback、ASR/LLM 逻辑;
  • 对现有用户 / 本地环境 / 构建流程的影响:仅改变 Windows TSF IME 的最终提交方式;异步 Word edit session 和提交后光标定位保持不变;
  • Microsoft 文档允许不带 TF_IAS_QUERYONLY 直接执行读写插入,并通过 ppRange 返回已插入范围。

测试计划

  • 命令:node scripts/windows-package-msvc.test.mjs
    • 修复前:失败,2 !== 1,证明同一 edit session 存在两次宿主调用;
    • 修复后:通过。
  • 命令:node scripts/windows-ime-lifecycle-contract.test.mjs
    • 结果:通过。
  • 命令:windows-ime-build.ps1 -Configuration Release -Platform x64
    • 结果:成功,0 errors。
  • 命令:windows-ime-build.ps1 -Configuration Release -Platform Win32
    • 结果:成功,0 errors。
  • 命令:npm test
    • 结果:production build 成功;发现并通过 61 个测试。
  • 命令:PowerShell parser + git diff --check
    • 结果:通过。
  • 安装态受影响宿主复测
    • 尚未替换用户当前系统注册的 OpenLess IME DLL,避免在提交 PR 时干扰正在运行的输入法环境。

证据路径:

  • openless-all/app/windows-ime/src/edit_session.cpp
  • openless-all/app/scripts/windows-package-msvc.test.mjs
  • openless-all/app/scripts/windows-real-asr-insertion-smoke.ps1

@Longado

Longado commented Sep 2, 2026

Copy link
Copy Markdown

方向认同,单次提交确实是对的。顺着失败路径读了一遍,有一个点想跟你确认。

被移除的 TF_IAS_QUERYONLY 预检,除了预检之外还兼了提交闸:旧代码是 if (SUCCEEDED(hr)) 才发起正式插入,所以宿主拒绝插入时,文档是没有被修改过的。现在这个闸没了,正式调用失败时文档可能已经被改动。

而下游对「插入失败」的处理是会补一次的:

  • windows_ime_ipc.rsclassify_native_submit_result 只把 NATIVE_TIMEOUT_HRESULT / NATIVE_CANCELLED_HRESULT 归入 OutcomeUnknown,其余失败 HRESULT 都当作「确定没插入」返回;
  • 因此 coordinator.rsshould_try_non_tsf_insertion_fallback(allow, Failed, outcome_known = true) 为真,而 allow_non_tsf_insertion_fallback 默认开启;
  • 于是走 insert_via_non_tsf_fallback,剪贴板 / SendInput 再插一份。

如果正式 InsertTextAtSelection 存在「已经改了文档但返回失败」的情况,这条链的结果正好是本 PR 要消除的那类重复。我不确定这个前提在实际宿主上有多大概率(TSF 插入在 edit session 内通常是原子的),所以这更像是残余风险,而不是本 PR 引入的回归 —— 预检那个闸本身就是 bug 的来源,删掉是对的。

想问的是:要不要顺手把「正式提交调用失败」这一类也映射进已有的 OutcomeUnknown?语义上它比「只读预检失败」更接近「不知道有没有插进去」,这样兜底就不会补刀。如果你判断这个场景不现实,忽略即可。

另外两个小点:

  1. windows-package-msvc.test.mjsSetSelection(edit_cookie, 1, &selection)TF_AE_END 这两行在 diff 里显示为改动,实际内容没变,只是行尾从 CRLF 变成了 LF(git diff --check 不检查行尾,所以自检不会报出来)。这个文件本来行尾就是混的,不影响功能,只是 diff 噪音,方便的话可以还原。

  2. windows-real-asr-insertion-smoke.ps1$targetStartsEmpty 用的是硬编码白名单 @("notepad", "browser", "win32edit"),以后新增 target 会默认落到 Contains 分支 —— 而这正是本 PR 指出的「嵌套重复会误通过」的那个弱断言。反过来写成默认严格、只对已知初始非空的 target 放宽,会更安全一点。

以上都不阻塞。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants