# 问题闭环记录(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. 多行粘贴不应被当作多次回车直接执行。