Files
terminal-lab/terminal/question-solved.md
douboer@gmail.com 3b7c1d558a first commit
2026-03-03 13:23:14 +08:00

156 lines
8.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 问题闭环记录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)
<a id="ime-dup"></a>
## 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`
- 覆盖文本键、功能键、组合键判定。
---
<a id="paste-invalid-union"></a>
## 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`
---
<a id="multiline-paste-enter"></a>
## 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 包裹序列正确。
---
<a id="regression-checklist"></a>
## 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 屏幕处。
- 收起键盘后,稳定跳掉之前的阅读点(快照复原准确)。
- 长按或者多次键盘输入不再跳动到屏幕中部。
---
<a id="enter-leading-dup"></a>
## 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. 多行粘贴不应被当作多次回车直接执行。