# 问题闭环记录(Question Solved) 本文档记录已确认问题的现象、根因、修复与回归要点,目标是避免重复踩坑。 ## 目录 1. [2026-03-02:第三方输入法导致输入重复](#ime-dup) 2. [2026-03-02:点击粘贴触发 BAD_REQUEST / invalid_union](#paste-invalid-union) 3. [2026-03-02:多行粘贴被当作回车执行](#multiline-paste-enter) 4. [2026-03-03:回车后首字母重复显示(`aasdf`)](#enter-leading-dup) 5. [通用回归清单](#regression-checklist) ## 2026-03-02:第三方输入法导致输入重复(`jk -> jkjk`,`测试 -> 测试测试`) ### 现象 - 第三方输入法下输入 `jk` 显示 `jkjk`,输入 `测试` 显示 `测试测试`。 - 原生输入法或英文输入模式正常。 ### 根因 - 同一文本同时走了 `keydown` 和 `input` 两条发送路径,导致双发。 - 第三方输入法会在 `keydown` 阶段给出整段文本(`key="测试"` / `key="jk"`),旧逻辑误判后直接发送。 ### 修复 - 文件:`apps/web/src/terminal/components/TerminalInputBar.vue` - 策略: 1. `keydown` 只处理控制键/功能键。 2. 文本输入统一走 `input` 通道,保证单通道发送。 3. `KEYDOWN_DIRECT_KEYS` 作为白名单,非白名单文本直接跳过。 ### 验证 - `ls`、`jk`、`测试` 均不重复。 - `Enter`、`Ctrl+C` 等控制键行为保持正常。 ### 自动化测试 - 文件:`apps/web/src/terminal/input/inputPolicy.test.ts` - 覆盖文本键、功能键、组合键判定。 --- ## 2026-03-02:点击粘贴触发 BAD_REQUEST / invalid_union ### 现象 - 点击工具栏“粘贴”后出现 error,连接异常或重连。 - 日志出现 `ws:error_frame code=BAD_REQUEST`,消息含 `invalid_union`。 ### 根因 - 网关 `stdin` 入参 schema 在现网存在差异;`paste` 携带 `meta` 会在部分环境触发 union 校验失败。 ### 修复 - 文件:`apps/web/src/terminal/stores/useTerminalStore.ts` - 策略: 1. `paste` 分片发送时不再携带 `meta`。 2. `keyboard/assist` 保持原有 `meta.source`。 3. 粘贴分片大小从 `512` 调整为 `256`,降低单帧风险。 ### 验证 - 粘贴日志出现 `wireMetaSource: "none"`。 - 不再出现 `BAD_REQUEST invalid_union`。 --- ## 2026-03-02:多行粘贴被当作回车执行 ### 现象 - 复制多行文本后粘贴进 shell,会被当作多次回车,导致命令提前执行。 ### 根因 - 旧粘贴逻辑将换行统一转为 `\r`(回车语义),不符合“多行粘贴应作为文本块输入”的预期。 ### 修复 - 文件: - `packages/terminal-core/src/input/inputBridge.ts` - `apps/web/src/terminal/components/TerminalInputBar.vue` - `apps/web/src/terminal/TerminalPage.vue` - 策略: 1. `mapPaste` 将换行统一归一为 `\n`,不再转成 `\r`。 2. Web 两条粘贴链路(原生 `paste`、工具栏“粘贴”)统一走 `InputBridge.mapPaste()`。 3. 开启 bracketed paste(`\x1b[200~... \x1b[201~`),避免被解释为逐行回车执行。 ### 验证 - 粘贴多行文本默认不自动逐行执行。 - 两条粘贴入口行为一致。 ### 自动化测试 - 文件:`packages/terminal-core/src/input/inputBridge.test.ts` - 覆盖: 1. `CRLF/CR/LF` 统一归一为 `LF`。 2. bracketed paste 包裹序列正确。 --- ## 2026-03-02:软键盘弹出后光标行定位不准与收回后页面跳动问题 ### 现象 1. 终端在 iOS 移动设备上点击输入区弹出软键盘时,期望将光标(提示符行)定位到屏幕的距顶 1/4 处,但实际经常定位到键盘顶部或在屏幕中部乱跳。 2. 软键盘收回时,期望恢复到键盘弹出前的状态,但页面内容经常回跳或者复位到屏幕底部。 3. 终端输入文字(`autoFollow=true`)时,滚动条经常突然跳到屏幕中部,无法锁定在最底部。 ### 根因 1. **行高计算不准确:** 在 `textareaRenderer` 的旧版计算方式中,利用 `scrollHeight / totalLines` 试图估算出单行的 `lineHeight`。但这在遇到换行文字(如超宽命令占据多行 HTML)或者终端末尾包含空白补齐的 Null Lines 时,按比例换算的 `offsetPx` 数值偏差严重。 2. **滚动到 scrollHeight 的陷阱:** `renderSnapshot` 及 `followBottom` 在自动跟随终端内容时,直接触发了 `this.outputEl.scrollTop = this.outputEl.scrollHeight`。但是 `xterm` 的 `snapshot.lines` 通常包含底部的很多未使用的纯空行(通常是为了填满预设高度而预留的。例如在你的日志中底部留有二十几行空行 Padding)。直接将 `scrollTop` 置为最大,会导致包含真实光标的行被视觉上挤到了屏幕上方,呈现出"跳到了屏幕中部"的怪异现象。 3. **iOS 异步布局时序缺陷:** `KeyboardAdjustController` 中,iOS 上软键盘收回(`visualViewport.height` 恢复)早于实际的 DOM `clientHeight` 恢复更新,用固定的时间延迟去读取和操作会导致布局错误。同时 iOS 原生的 `scroll` 行为是异步派发的,和程序的滚动重置产生了干扰覆盖(程序刚设回了 `snapshot` 高度,又被键盘底层收回的 `scroll` 冲成了底部)。 ### 修复 - 文件: - `apps/web/src/terminal/renderer/textareaRenderer.ts` - `apps/web/src/terminal/input/keyboardAdjustController.ts` - 策略: 1. **放弃比例估算,直接获取光标 DOM 节点:** 在 `textareaRenderer` 渲染的光标附带有专门的类名 `tc-cursor-cell`,此时我们设置外部包裹元素为 `position: relative`,并直接借助 `const cursorSpan = this.outputEl.querySelector('.tc-cursor-cell')` 拿到精确的 `offsetTop` 来计算光标此时刻在纵向的确切位置,确保毫无误差。 2. **将光标置于可视区底部,拒绝 scrollHeight:** 摒弃粗暴地拉向极限 `scrollHeight` 的做法,改为使用光标偏移量算出安全的底端视区:`let targetScrollTop = cursorBottom - clientH + 20` 并将其作为真实的自动跟随坐标,将跟随对象从"整个 Buffer 的底部"变更为"真正光标所处的区域"。 3. **Polling 等待基线恢复,长短结合阻塞 scroll 事件干扰:** 在 `keyboardAdjustController.ts` 中针对键盘关闭的 Layout 响应,引入 `requestAnimationFrame` 及定时检测(Polling),不断比对 `clientHeight` 是否已经长回键盘未弹出前的高度,只有确认恢复或者超市才恢复预存前的 `scrollTop` 快照,借此解决 iOS 下因为布局慢导致的跳闪。并通过定时锁去屏蔽由于视差调整造成的自动 `autofollow` 开关变动。 ### 验证 - 点击 iOS 输入区弹出软键盘,终端精确将当前焦点处于 1/4 屏幕处。 - 收起键盘后,稳定跳掉之前的阅读点(快照复原准确)。 - 长按或者多次键盘输入不再跳动到屏幕中部。 --- ## 2026-03-03:回车后首字母重复显示(`aasdf`) ### 现象 - 输入英文命令(如 `asdf`)后回车,终端提示符行显示 `aasdf`,但服务端实际执行的是 `asdf`(例如返回 `zsh: command not found: asdf`)。 - 问题具备偶发性,通常出现在回显分片较细时。 ### 根因 - 回显数据分帧到达,典型序列为:`"a"`、`"\b as"`、`"df..."`。 - 旧实现按帧即时渲染,第一帧字符已经落盘;第二帧开头的 `\b`(Backspace)无法回退上一帧已写入内容,导致视觉上保留多余首字母。 - 同时存在 `\r\r\n` 与 ANSI 控制序列交织,如果不按终端语义处理,也会放大错位和空行现象。 ### 修复 - 文件:`demo/main.js` - 策略: 1. 在 `renderAnsiToHtml` 中补齐控制字符语义:处理 `\x08`(退格)、`\r\n`(归一为换行)和裸 `\r`(回到当前行首覆盖)。 2. 增加短窗口流式合帧(`STREAM_BATCH_MS`):将连续 `stdout/stderr` 分片合并后再渲染,确保跨帧 `\b/\r` 能作用于同一文本缓冲。 3. 保留原有“本地回显清理”策略(`clearLast()`),避免与远端回显叠加。 ### 验证 - 复现输入 `asdf` 回车,不再出现 `aasdf`。 - 日志可见发送仍为 `asdf\n`,错误回显保持 `command not found: asdf`,与服务端行为一致。 - 其它输出路径(`stdout/stderr`、ANSI 颜色)不回归。 ## 通用回归清单(每次改输入/粘贴链路都执行) 1. 英文输入 `ls` 不重复。 2. 第三方输入法输入 `jk`、`测试` 不重复。 3. `Enter`、`Ctrl+C`、方向键功能正常。 4. 工具栏粘贴与原生粘贴行为一致。 5. 长文本粘贴不触发网关 `BAD_REQUEST invalid_union`。 6. 多行粘贴不应被当作多次回车直接执行。