fix(worker): 修 iOS 三方输入法在 WebShell 无法输入 / 语音纠错重复 - #666
Conversation
951a0a5 to
218f3b4
Compare
Claude 首审 — 通过,无阻塞(1 个 P3 加固建议 + 1 个流程提示)已把补丁 checkout 到 改动逻辑(白话)WebShell 终端页用 xterm.js 5.x。iOS 第三方输入法(豆包每个字、微信空格/句尾单字)走一条 xterm 处理不了的事件序列,导致丢字;语音纠错时一次退格触发 textarea 多个 核实结论(均基于真实 xterm 5.5.0 bundle)
P3(加固,非阻塞):
|
|
To use Codex here, create a Codex account and connect to github. |
Codex 复审 — 通过,无阻塞;建议顺手收紧
|
Web 终端页(getTerminalHtml)用原生 xterm.js 5.x,iOS 三方输入法(豆包/微信等) 在隐藏 textarea 上的事件序列命中 xterm 两处缺陷,导致输入丢失或重复: 1) keyCode=229 + composed insertText 死路:字符走 keydown(229)→input(insertText, composed=true) 且无 composition 事件。xterm 的 _inputEvent 因 (!composed || !_keyDownSeen) 恒为假而不发;唯一兜底 _handleAnyTextareaChanges 用 setTimeout + String.replace(oldValue,'') 计算差量,连打时算错 → 丢字。豆包每个字/词/标点全走 此路(几乎打不上),微信空格与句尾单字走此路(延迟/丢)。 2) 语音纠错重复:一次物理退格,iOS 三方输入法在 textarea 里连发 N 个 deleteContentBackward(意图删整行),但 xterm 对一个 Backspace keydown 只发 1 个 \x7f → 终端删 1 字、textarea 删 N 字,残留 N-1 字;随后输入法重插整句 → 旧内容 没删干净 + 新整句叠加 = 整段重复。 修复(仅接管这两条死/损路,其余不碰): - attachCustomKeyEventHandler 认领 keydown 229(非 composing);以及 Backspace 但 仅当 textarea 非空(有 IME 编辑待定)时——空 textarea 是正常终端退格,放行给 xterm 发标准 \x7f,避免吞掉 shell 命令行退格这一回归。 - 认领后 return false,xterm 跳过其缺陷兜底 / 单发退格,杜绝双发。 - textarea 'input' 监听逐事件转发:insertText→原样 term.input(data,true);每个 delete*→一个 \x7f,使 N 次删除 1:1 映射到终端 N 次删除。 - composition 输入(微信中文本来就可靠)在 _composing 期间完全不接管。 - term.input(data,true) 走 onData(非 paste,不做括号粘贴包装、不清 textarea), 沿用既有 WS 发送与只读门控。逃生阀:URL 加 ?imefix=0 可临时关闭回到原生行为。 影响面:仅改 worker.ts 的 getTerminalHtml 浏览器端 JS(用户实际使用的交互终端页), 不碰服务端 / 其它 CLI 适配器 / 后端 / 其它终端页(debug-terminal、v3-terminal)。 纯浏览器行为,跨平台(daemon 运行的 Linux 不受影响)。 验证: - pnpm build 通过;运行期 JS 语法解析通过(new Function)。 - 基于两段真机 trace(豆包/微信 iOS 26.5.2)字节级离线模拟 5 场景全过:逐字输入、 连续空格转句号(删+插)、语音纠错整段删除、composition 期间零误发、 英文/Enter/方向键不误领。 - 真机复测(用户在 iOS 实测):豆包/微信打字正常、语音纠错重复消失、shell 命令行 退格正常。 - pnpm test:11167 passed,6 failed 均为本机沙箱预存环境性失败(v3-worker-fence / goal-cli / cancel-runtime,依赖 git 远程 ref / tmux server),git stash 后在纯净 分支复现同样失败,与本改动无关。 Co-Authored-By: Claude <noreply@anthropic.com>
回应 Codex review 的理论双发脆弱点:接管开关 _claim 原先只在 keyup 关闭,但 iOS 输入法合成键常不发 keyup,_claim 可能卡在开;若此时飘来一条 composed=false 的 insertText,xterm 的 _inputEvent 会发(其守卫在 !composed 时放行)、我方因 _claim 仍开也发 → 同字双发。真实豆包/微信输入均为 composed=true(39 条真机 trace 无一例外),故为理论脆弱点而非已触发的 bug,但加固代价极小、值得焊死。 加固(两条互斥保险 + 护栏): - input 转发仅认 composed=true:composed=false 的 insertText 交还 xterm 自己处理, 两条路径互斥,从根上杜绝双发。经 trace 验证零误伤(现有能用场景全 composed=true)。 - _claim 在每次 keydown 开头无条件重置(在认领判断之前),并新增 blur 监听关闭: 即便 keyup 缺失,新按键 / 失焦也会关上开关,消除“卡开”窗口。 - 新增 test/web-terminal-ime.test.ts:照 web-terminal-touch-scroll 的 source-level 断言范式,锁住 composed 门、keydown/blur 重置、退格非空判据、composition 不接管 等不变量——这段浏览器 JS 此前缺回归护栏,本测试补上。 不改变既有行为:逐字输入、连续空格转句号、语音纠错整段删除、微信 composition、 空 textarea 的普通终端退格均按原样工作(离线状态机模拟 8 场景全过)。 验证:pnpm build 通过;运行期 JS 语法解析通过;新测试 9 用例全过。 Co-Authored-By: Claude <noreply@anthropic.com>
218f3b4 to
dd6ef5a
Compare
追加:双发脆弱点加固(commit 2)回应 review 的理论双发脆弱点,追加 commit
离线状态机模拟 8 场景全过(逐字 / 空格转句号 / 语音纠错 / 缺 keyup+composed=false 不双发 / blur 关闭 / composition 零接管 / 空 textarea 退格放行); |
Claude 复核:P3 加固已在
|
Codex 增量复审:暂不通过 — 1 个 P2 阻塞(
|
…lacement) 回应 Codex 增量复审的 P2:上一版 `if(e.data&&e.composed)` 把 `e.composed` 当 "IME 输入"标志用是错的——`composed` 是事件能否穿越 shadow DOM 的标志,规范/WebKit 下每个受信任 InputEvent 都是 composed=true。于是 composed 门只与 xterm `_inputEvent` 的 insertText 分支互斥,却仍接管了其它输入路径: - insertFromPaste:xterm 的 handlePasteEvent 只 stopPropagation 不 preventDefault, 已发一次;默认粘贴又往 textarea 插入触发 insertFromPaste(composed=true)→ 补丁 再发一次 = 双 paste。 - insertReplacementText:WebKit 纠正序列 ㅎ→하→한 逐条 append → 终端变 "ㅎ하한" 而非 "한"。 修复:转发条件收窄为 `e.data && e.composed && it==='insertText'`——严格白名单, 只接管目标豆包/微信 trace 走的那条死路(keydown 229 → insertText)。仍保留 composed 门:composed=false 的 insertText 由 xterm _inputEvent 自己发(其守卫在 !composed 时放行),补丁不重复。白名单 + composed = 与 xterm 各输入路径互斥无缝。 测试:test/web-terminal-ime.test.ts 补一组可执行行为测试(抽取真实 IIFE 在 stub DOM 跑,建模 xterm 自身 _inputEvent + paste 发射),断言 exactly-once:豆包单字、 空格转句号、语音多删 1:1、微信 composition 让路、空退格不接管,以及 Codex 两条 失败序列(stuck+paste 不双发 / replacement 不 append)+ 原 P3(stuck+composed=false 不双发)+ keydown/blur 重置。之前 9 条 source-level 断言只锁语句形状锁不住行为, 新增行为测试补齐(mutation 还原 composed-only 写法 → 两条 REGRESSION 用例即 FAIL)。 验证:pnpm build 通过;新测 19/19;全量 11238 passed,唯一失败 group-join-shared-routing 系全量并发 beforeAll 超时(单文件隔离 5/5,与本 PR 无关); merge-tree vs master 0 冲突。 Co-Authored-By: Claude <noreply@anthropic.com>
Claude 代修 P2(Codex 增量复审):
|
|
To use Codex here, create a Codex account and connect to github. |
Codex 增量复审
|
|
🚀 Released in v3.8.0 |
背景 / 问题
WebShell 交互终端页(
worker.ts的getTerminalHtml)用原生 xterm.js 5.x(CDN 加载)。iOS 上第三方输入法(豆包、微信 等)在 xterm 隐藏textarea上产生的事件序列,命中 xterm 两处缺陷,导致:自带输入法正常,故此前一直没暴露。
根因(两处,均有 iOS 26.5.2 真机 trace 佐证)
1) keyCode=229 + composed insertText 死路
字符走
keydown(keyCode=229)→input(inputType=insertText, composed=true),无 composition 事件。_inputEvent守卫(!ev.composed || !_keyDownSeen):此处composed=true且 229 keydown 已置_keyDownSeen→ 恒为假 → 不发送。CompositionHelper._handleAnyTextareaChanges用setTimeout+String.replace(oldValue,'')算差量,连续快打时算错 → 丢字。豆包每个字/词/标点全走此路(几乎打不上);微信空格与句尾单字走此路(延迟/丢)。
2) 语音纠错重复
一次物理退格,iOS 三方输入法在
textarea里连发 N 个deleteContentBackward(意图删整行),但 xterm 对一个 Backspace keydown 只发 1 个\x7f→ 终端删 1 字、textarea 删 N 字,残留 N-1 字;随后输入法重插整句 → 旧内容没删干净 + 新整句叠加 = 整段重复。修复(仅接管这两条死/损路,其余一律不碰)
attachCustomKeyEventHandler认领:keydown229(非 composing);以及 Backspace 但仅当textarea非空(有 IME 编辑待定)时——空textarea是正常终端退格,放行给 xterm 发标准\x7f,避免吞掉 shell 命令行退格这一致命回归。return false,xterm 跳过其缺陷兜底 / 单发退格 → 杜绝双发。textarea'input'监听逐事件转发:insertText→ 原样term.input(data,true);每个delete*→ 一个\x7f,使 N 次删除 1:1 映射到终端 N 次删除。_composing期间完全不接管。term.input(data,true)(非paste:不做括号粘贴包装、不清空 textarea),走既有onData→ 沿用现有 WS 发送与只读门控。?imefix=0可临时关闭修复回到原生 xterm 行为,便于对比排查。影响面
worker.ts的getTerminalHtml浏览器端 JS(用户实际使用的交互终端页)。debug-terminal、v3-terminal)。_composing期间不接管,不受影响。测试与验证
pnpm build通过;运行期 JS 语法解析通过(new Function)。pnpm test:11167 passed,6 failed 均为本机沙箱预存环境性失败(v3-worker-fence/v3-goal-cli/v3-cancel-runtime,依赖 git 远程 ref / tmux server);git stash后在纯净分支复现同样失败,与本改动无关。上游
对应 xterm.js issue #5835 / PR #5836(同类根因)。本 PR 是 botmux 侧的 hook 层修复,与 CDN xterm 版本解耦,无需等上游发版。
🤖 Generated with Claude Code