【Bug已解决】Codex App crashes after typing@in the new-conversation composer 解决方案
原始报错线索:Codex App crashes after typing
@in the new-conversation composer(在新会话输入框里刚打出@这个字符,应用就崩溃了)。
一、现象长什么样
用户在新会话输入框敲下@:
- 应用立刻闪退 / 白屏 / 无响应;
- 崩溃栈指向「提及补全(mention completion)」相关代码;
- 有时只在「还没有任何会话 / 列表为空 / 网络未加载完」时打
@才崩,正常会话里没事; - 崩溃前往往没有任何错误提示,用户只是想 @ 个人/文件。 根因是触发字符的处理路径在「边界状态」下缺少防御,一旦
@触发时依赖的数据还没就绪或为空,就空指针/越界崩溃。
二、背景:提及补全是怎么工作的
现代编辑器/聊天框在输入@后,会:
- 检测触发:监听输入,发现
@且后面在「词起始位置」; - 收集候选项:从「会话成员 / 文件列表 / 命令表」拉取可 @ 的对象;
- 渲染浮层:把候选项画成下拉列表;
- 过滤:随用户输入实时过滤;
- 选择:上下键 + 回车插入。 崩溃几乎都发生在第 2、3 步:候选项源为空 / 未初始化 / 异步加载中。
三、为什么打 @ 会崩:根因
3.1 空数据解引用
候选列表None或空,代码却直接candidates[0]/candidates.length当非空处理 → 越界/空引用。
3.2 异步未就绪就渲染
候选来自异步加载(网络/数据库),@触发时future还没返回,代码直接读.result()阻塞或读空 → 崩。
3.3 触发检测误判
在「字符串中间」而非「词首」打@(如邮箱a@b.com)也触发补全,后续逻辑假设「@ 后是要补全的名字」,解析出错。
3.4 异常未捕获
补全逻辑任何一步抛异常都没被try/except包住,异常一路冒泡到主线程 → 应用崩。
四、最小可运行复现(空列表解引用崩溃)
下面演示「候选为空时直接取首项」如何崩溃:
def show_mention_popup(query, candidates): # 错误:假设 candidates 永远非空且已就绪 top = candidates[0] # candidates 为 None/[] -> 崩 return f"高亮: {top['name']}" if __name__ == "__main__": try: show_mention_popup("@", None) # 崩溃:NoneType 不可下标 except Exception as e: print("崩溃:", type(e).__name__, e)candidates=None时candidates[0]直接抛TypeError,若没捕获就崩应用。
五、解决方案一:触发检测要精确(避免误触发)
只在「词首 / 空白后」的@才触发,邮箱等场景不触发:
def should_trigger_mention(text, caret): """判断 caret 处的 @ 是否应触发提及补全。""" if caret <= 0 or text[caret - 1] != "@": return False # @ 前面必须是行首或空白/标点,避免匹配 a@b.com prev = text[caret - 2] if caret >= 2 else "" if prev and not prev.isspace() and prev not in "((【": return False return True if __name__ == "__main__": print(should_trigger_mention("你好 @", 3)) # True print(should_trigger_mention("a@b.com", 2)) # False(邮箱)精确触发避免「在错误上下文打 @ 也拉补全」导致的后续解析崩溃。
六、解决方案二:候选为空 / 未就绪的安全处理
补全逻辑必须接受「无数据」状态,绝不假设非空:
def show_mention_popup_safe(query, candidates): """安全渲染:候选为空/未就绪时,安静地不显示或显示『无匹配』。""" if not candidates: # None / [] 都安全处理 return {"visible": False, "items": []} items = [c for c in candidates if query.lower() in c.get("name", "").lower()] if not items: return {"visible": True, "items": [], "hint": "无匹配"} return {"visible": True, "items": items[:10], "highlight": items[0]["name"]} if __name__ == "__main__": print(show_mention_popup_safe("@", None)) # 不显示,不崩 print(show_mention_popup_safe("@a", [{"name": "Alice"}]))空/None 一律先判,绝不candidates[0],崩溃根因消除。
七、解决方案三:异步候选加载的异常安全
候选来自异步源时,用try/except包住加载,失败就降级为空列表而非崩溃:
def load_candidates_async(session_id): """异步加载可 @ 对象;任何失败都返回空列表而非抛异常。""" try: # 真实环境:从会话成员服务 / 文件索引拉取 data = fetch_members(session_id) # 可能网络失败 return data or [] except Exception as e: # 异常安全:记录日志,降级为空,绝不冒泡到 UI 线程 print(f"[mention] 加载候选失败,降级为空: {e}") return [] def fetch_members(session_id): # 示意:会话未就绪时返回 None return None if __name__ == "__main__": cands = load_candidates_async("no-session-yet") print("安全得到:", cands) # [] 而非崩溃异步失败被吞并降级,输入@时即便数据没加载完也不会崩(解决 3.2/3.4)。
八、解决方案四:整个触发路径包一层兜底
最外层用「防御性调用」,任何意外异常都就地捕获、记录,保证输入法字符永不导致应用退出:
def safe_on_text_changed(text, caret): """输入框文本变化的总入口:任何异常都不让应用崩。""" try: if should_trigger_mention(text, caret): cands = load_candidates_async(current_session()) return show_mention_popup_safe(text[:caret], cands) except Exception: # 兜底:宁可不出补全,也不能崩应用 import logging logging.exception("提及补全异常,已跳过") return {"visible": False} def current_session(): return None # 模拟未就绪 if __name__ == "__main__": # 即便内部全错,这里也返回安全结构,应用不崩 print(safe_on_text_changed("hi @", 4))try/except兜底保证:输入处理这条路径,异常只能被记录,不能被传播到让进程退出——这是输入触发崩溃的终极防线。
九、排查清单
「输入 @ 崩溃」,按下面排查:
- 触发检测是否精确?是否误匹配邮箱等
@(第五节); - 候选为空/None 是否安全?是否直接
candidates[0](第六节); - 异步加载是否异常安全?失败是否降级为空(第七节);
- 整条路径是否包兜底 try/except?异常能否冒泡到主线程(第八节);
- 崩溃栈指向哪一步?收集候选还是渲染浮层;
- 是否只在『空会话/未加载』时崩?说明是数据未就绪问题;
- UI 线程是否被阻塞?异步
.result()同步等待会卡; - 日志是否记录触发上下文。
十、小结
「输入 @ 触发应用崩溃」的根因是提及补全的触发路径在边界状态(空数据 / 异步未就绪)下缺少防御,异常冒泡到主线程致崩。通用修复:
- 精确触发:只在词首/
@后空白处触发,避开邮箱等误触发(第五节); - 空安全:候选为
None/空时安静不显示,绝不下标解引用(第六节); - 异步降级:候选加载失败返回空列表,不抛异常(第七节);
- 整路径兜底:
try/except包住触发处理逻辑,异常只记录不崩溃。 一句话:任何「输入字符 → 拉起 UI 浮层」的路径,都必须假设「依赖数据可能为空、可能未就绪、可能失败」,并把异常锁死在这一层。输入法是用户最高频的操作,绝不允许一个@把整个应用带走——这与第 115 篇「认证钩子异常不越界」、第 92 篇「终端转义不误读为输入」,共同体现「用户输入路径必须异常安全」的铁律。