[Bug已解决] CLI-信任提示因终端转义序列被误读为输入而退出-终端输入解析方案
[Bug已解决] CLI 信任提示因终端转义序列被误读为输入而退出终端输入解析解决方案一、现象长什么样你在一个分屏终端split terminal里用某个 CLI 工具它弹出一个「信任此操作吗[y/N]」的交互提示。你还没来得及输入提示就自己退出了当作你选了某个默认/取消。排查发现是终端的Device Attributes设备属性DA 转义序列\033[c的响应被 CLI 当成了用户输入触发了提示的「收到输入即处理」逻辑于是误判你回答了、直接退出。即 GitHub Issues (openai/codex) #31420CLI trust prompt exits when split terminal Device Attributes tail is read as input。本质是CLI 在读取用户交互输入时没有正确区分「真实键盘输入」与「终端自身发回的转义序列响应」把终端的 DA 响应当成了用户的按键导致提示被错误触发/退出。这是个通用的「终端 I/O 解析」问题。本文聚焦通用技术终端转义序列尤其是 DA 设备属性查询、为什么分屏终端会发 DA、如何在读取交互输入时过滤/正确解析转义序列用真实可运行代码说明不依赖具体产品。二、背景终端的 Device AttributesDA转义序列终端是「带协议的字符设备」。程序可以向终端发控制序列escape sequence查询信息终端会回发响应程序发\033[c或\033[0cDA1 查询→ 问「你是什么终端」终端回\033[?1;2c之类 → 报告能力这在分屏终端tmux/screen/某些终端模拟器的 split pane里很常见分屏创建/重绘 pane 时会主动向程序发 DA 查询或程序自己探测终端类型时发终端回的响应出现在 stdin 上——因为程序把终端设成了某种「原始/回显」模式来读交互输入于是这段转义响应被读进了输入缓冲区。问题就在这CLI 读信任提示的输入时把这段「终端回发的 DA 响应」当成了用户敲的键。信任提示通常是「读一个字符就处理」于是读到\033[?1;2c的第一个字节ESC或某字符被当作「用户按了某键」提示按默认逻辑退出/取消。三、为什么分屏终端会触发分屏 pane 重绘发 DAtmux/screen split 时新 pane 初始化会发终端属性查询终端响应落到 stdinRAW 模式读输入CLI 为了「读单个字符不回车」把终端设成 raw/cbreak 模式这时任何到达 stdin 的字节包括终端响应都被当输入读走没过滤转义CLI 的读取循环没识别「这是转义序列以 ESC 开头」并跳过而是逐字节喂给提示逻辑提示逻辑过于敏感信任提示「一收到任意输入就处理」没有「等用户真正按键」的判定。本质读交互输入时必须先把「终端协议字节转义序列」和「用户按键」区分开否则终端的自动响应会冒充用户输入。四、最小可运行复现「转义序列被当输入」并正确过滤下面用 Python 演示模拟 stdin 收到 DA 响应\033[?1;2c naive 读取会把它当输入正确做法是识别 ESC 开头的转义序列并跳过。import sys, os, tty, termios, select DA_RESPONSE b\x1b[?1;2c # 终端对 DA 查询的回应示例 def read_char_naive(): # ❌ 错误任何字节都当用户输入 ch sys.stdin.read(1) return ch def read_char_skip_escape(timeout0.5): # ✅ 正确以 ESC(\x1b) 开头的序列是终端协议跳过 if not select.select([sys.stdin], [], [], timeout)[0]: return None ch sys.stdin.read(1) if ch \x1b: # 转义序列开始 # 读完整转义序列并丢弃含 [ ? 1 ; 2 c seq ch while True: nxt sys.stdin.read(1) seq nxt if nxt in (~, c, m, A, B, C, D) or len(seq) 16: break return None # 这是终端协议不是用户输入 return ch # 演示把 DA 响应喂进 stdin 缓冲示意 if __name__ __main__: # 实际环境分屏终端的 DA 响应会自然出现在 stdin print(naive 读到 ESC 会误判为输入skip_escape 会识别并丢弃转义序列)要点以\x1b开头的字节串是转义序列必须在交互读取时识别并跳过不能当用户输入。五、解决方案一识别并跳过转义序列核心交互读取单字符时遇到 ESC 就「吃掉整个转义序列」import sys, select def get_key(timeout0.5): 读一个『用户按键』自动跳过终端转义序列。 if not select.select([sys.stdin], [], [], timeout)[0]: return None c sys.stdin.read(1) if c ! \x1b: return c # ESC 开头 转义序列完整读掉再递归等真实按键 buf c while True: if not select.select([sys.stdin], [], [], timeout)[0]: return None n sys.stdin.read(1) buf n # 转义序列以字母或 ~ 结束CSI 序列 if n.isalpha() or n ~: break if len(buf) 32: break # 转义序列读完继续等用户真实按键 return get_key(timeout) # 信任提示用它读输入就不会把 DA 响应当回答 ans get_key() print(用户真实输入:, ans)这是修复的核心终端协议字节永不进入「用户输入」语义。六、解决方案二不要依赖「读一个字符即处理」加明确判定信任提示的逻辑应更稳健def ask_trust(): print(信任此操作吗[y/N]) while True: k get_key() # 跳过转义后的真实按键 if k is None: continue # 超时/转义继续等不退出 if k.lower() y: return True if k.lower() n or k \r: # 显式 N 或回车 return False # 其它字符忽略继续问不误退出关键收到非预期字节转义/未知时「继续等」而不是「当默认处理」。这样 DA 响应被跳过或忽略不会让提示退出。七、解决方案三避免在提示期间发 DA 查询若 CLI 自己会发 DA 查询探测终端如sys.stdout.write(\033[c)再读响应应在发查询和读响应之间用独立通道或在提示激活期间不发查询def query_terminal_capabilities(): # 查询在『非交互提示』阶段做拿到响应后缓存提示期间不再查 saved termios.tcgetattr(sys.stdin) tty.setraw(sys.stdin.fileno()) try: sys.stdout.write(\033[c) # 发 DA 查询 sys.stdout.flush() resp read_escape_response() # 专门读响应不混入用户输入 finally: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, saved) return resp原则终端能力探测的响应要用「专门的读取函数」消费绝不能与「信任提示的用户输入读取」混在同一个缓冲区。八、解决方案四用成熟的 TTY 库而非手搓手搓 raw 模式 转义解析容易漏。生产用成熟库如prompt_toolkit、readline、click.prompt它们已正确处理转义序列# 用 click 的确认提示已处理终端协议 import click if click.confirm(信任此操作吗, defaultFalse): print(已信任)prompt_toolkit/click在底层已区分转义序列与用户输入避免这类坑。自定义 TUI 才需自己处理见方案一/二。九、排查清单CLI 提示被转义误触交互提示莫名自动退出且在分屏终端tmux/screen下 → 终端 DA 响应被当输入#31420 类。识别转义以\x1b开头的字节是终端协议读取时跳过整段。提示逻辑收到非预期/转义字节「继续等」不默认处理、不退。探测隔离终端能力 DA 查询的响应用专门函数消费不与用户输入混缓冲。用成熟库click/prompt_toolkit已正确处理转义。复现在 tmux split pane 下测试提示验证 DA 不触发误退出。十、小结CLI trust prompt exits when split terminal Device Attributes tail is read as inputopenai/codex #31420的本质是CLI 在 raw 模式下读信任提示的输入时没有区分「真实键盘输入」与「分屏终端发回的 DA 设备属性转义响应如\033[?1;2c」把终端协议字节当成了用户按键触发了「读到一个字符即处理」的提示逻辑导致提示被误判退出。通用解决方案适用于任何 TTY 交互程序跳过转义序列读取单字符时遇到\x1b开头的序列整段识别并丢弃绝不当用户输入稳健提示逻辑非预期/转义字节「继续等」不默认处理、不退出探测隔离终端能力 DA 查询的响应用专门函数消费不与用户输入混缓冲用成熟库click/prompt_toolkit已正确区分转义与输入。记住终端是「带协议的字符设备」它发回的转义响应DA 等不是用户输入。交互读取必须识别并跳过转义序列否则终端的自动响应会冒充你的按键。这是终端 I/O / TTY 编程里「转义序列解析 vs 用户输入」的经典课题。