Skip to content

[Bug] 工具菜单在 npm registry 不可达时键盘无响应(select prompt 死循环 abort) #17

Description

@S0ngRu1

问题描述

进入「配置编码工具 → 任一工具(Codex / Claude Code / CodeBuddy / WorkBuddy / OpenCode)」后,完成「模型选择」或其他子流程返回到该工具菜单时,上下方向键无响应,无法再选择菜单项,只能 Ctrl+C 强退。

复现步骤

  1. 运行:npx qiniu-coding-helper enter codex(或任意其他工具)
  2. 在工具菜单选择「配置模型」并完成(或选择「查看当前配置」按回车返回)
  3. 返回到工具菜单后,按上下键无任何响应

注意:必须在「无法访问 https://registry.npmjs.org」的网络环境(如国内未配代理 / 公司防火墙)才能稳定复现。npm registry 通畅的网络下表面看不到,但仍存在隐式重渲染。

预期行为

完成子流程返回菜单后,能继续用方向键选择菜单项。

实际行为

菜单画面看上去"卡住"或在快速闪烁刷新,键盘输入被丢弃。底层是 @inquirer/select 持续抛 AbortPromptErrorcontinue 立刻重渲染 → 立刻又被 abort,形成死循环。

根因分析(源码定位)

src/lib/wizard/menus/tool-menu.ts:24-49

let latestVersion: string | null = null;
let fetchPromise: Promise<string | null> | null = null;

if (tool.npmPackageName) {
  fetchPromise = fetchLatestVersion(tool.npmPackageName);
  fetchPromise.then(v => { latestVersion = v; }).catch(() => {});
}

while (true) {
  // ...
  const controller = new AbortController();
  if (latestVersion === null && fetchPromise) {
    fetchPromise.then(() => {
      if (!controller.signal.aborted) {
        controller.abort();
      }
    });
  }
  // select<ToolMenuAction>({...}, { signal: controller.signal })
}

机制设计意图:菜单首次渲染时若 npm 最新版本还未拿到,等 fetch 完成自动 abort 当前 prompt,重渲染时显示最新版本号。

Bug:src/lib/version-service.ts:4fetchLatestVersion 在网络失败/超时时返回 null(而非抛错)。一旦返回 null,latestVersion === null 永远成立,每次 while 循环都会给同一个已 resolved 的 fetchPromise 再挂一个 .then(() => controller.abort())

  • 已 resolved 的 Promise 的 .then 回调会立刻同步触发
  • controller.abort() 在 select prompt 还未开始等待用户输入时已被触发
  • select 抛 AbortPromptErrorcatchcontinue → 重新渲染 → 又挂 .then → 又立刻 abort

按键无响应是因为 select 每次都在 fast-path 抛错退出,连键盘事件循环都没进入。

复现条件

  • 任何 npm registry 不可达的网络(国内裸网、公司代理拦截 GitHub/npm、TLS 中间人等)
  • 已 fetch 一次失败后所有工具菜单(不只 Codex)都会触发
  • 第一次进入因为 fetchPromise 还在 pending 不会立即触发,但子流程返回后必然触发

修复建议

引入 fetchCompleted 标志,fetch 完成(无论成败)后跳过后续 abort 挂载:

let latestVersion: string | null = null;
let fetchCompleted = false;
let fetchPromise: Promise<string | null> | null = null;

if (tool.npmPackageName) {
  fetchPromise = fetchLatestVersion(tool.npmPackageName);
  fetchPromise
    .then(v => { latestVersion = v; fetchCompleted = true; })
    .catch(() => { fetchCompleted = true; });
}

while (true) {
  // ...
  const controller = new AbortController();
  if (!fetchCompleted && fetchPromise) {  // ← 改这里
    fetchPromise.then(() => {
      if (!controller.signal.aborted) controller.abort();
    });
  }
  // ...
}

这样不论 fetch 成功(返回版本号)还是失败(返回 null)后,第二次进入循环都不会再挂 abort 钩子。

环境信息

  • 操作系统:Windows 11 (10.0.26200)
  • Node.js 版本:v24.15.0
  • pnpm 版本:10.33.2
  • qiniu-coding-helper 版本:0.3.0
  • 使用的工具:Codex / Claude Code / OpenCode 均能复现
  • 工具版本:codex 已安装、claude-code 已安装、opencode-ai 1.15.6 已安装
  • 线路:国内线路 api.qnaigc.com

其他信息

  • 该 bug 不是某个具体工具引入的,属于 tool-menu.ts 的通用菜单逻辑问题
  • 用户感受:第一次进菜单选项→子流程能用,返回菜单后就"键盘失灵",体验上像程序冻结
  • 如需我提交 PR 修复,可以基于 feat(opencode): 新增 OpenCode 工具配置支持 #16 同分支补一次提交,或单独开一个 fix 分支

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions