欢迎回来

登录 EAKE AI,继续您的智能之旅

忘记密码?
还没有账号?立即注册

调试器技能

2026-05-20 · Skills中心

Node.js Inspect 调试器

概述

console.log 不够用时,可以从终端以编程方式驱动 Node 内置的 V8 inspector。你可以获得真正的断点、单步跳入/跳过/跳出、调用栈遍历、局部/闭包作用域转储,以及在暂停帧中求值任意表达式的能力。

两种工具,任选其一:

  • node inspect —— 内置、零安装、CLI REPL。最适合快速探查。
  • ndb / 通过 chrome-remote-interface 使用 CDP —— 可从 Node/Python 脚本化驱动;当你想自动化设置大量断点、跨多次运行收集状态,或在代理循环中以非交互方式调试时最为合适。

优先使用 node inspect它始终可用,而且 REPL 响应快。

何时使用

  • 某个 Node 测试失败,需要查看中间状态
  • ui-tui 崩溃或行为异常,想在渲染前检查 React/Ink 状态
  • tui_gateway 子进程(_SlashWorker、PTY 桥接 worker)行为异常
  • 需要检查闭包中的某个值,而 console.log 不打补丁就够不到
  • 性能问题:附加到运行中的进程,采集 CPU profile 或堆快照

不要用于:console.log 一分钟内就能解决的问题。基于断点的调试更重;在确有回报时再使用它。

快速参考:`node inspect` REPL

启动并在第一行暂停:


node inspect path/to/script.js
# or with tsx
node --inspect-brk $(which tsx) path/to/script.ts

debug> 提示符可接受以下命令:

| 命令 | 作用 |

|---|---|

| ccont | 继续执行 |

| nnext | 单步跳过 |

| sstep | 单步跳入 |

| oout | 单步跳出 |

| pause | 暂停运行中的代码 |

| sb('file.js', 42) | 在 file.js 第 42 行设置断点 |

| sb(42) | 在当前文件第 42 行设置断点 |

| sb('functionName') | 在函数被调用时中断 |

| cb('file.js', 42) | 清除断点 |

| breakpoints | 列出所有断点 |

| bt | 回溯(调用栈) |

| list(5) | 显示当前位置周围 5 行源码 |

| watch('expr') | 每次暂停时求值 expr |

| watchers | 显示被观察的表达式 |

| repl | 在当前作用域进入 REPL(Ctrl+C 退出 REPL) |

| exec expr | 求值一次表达式 |

| restart | 重启脚本 |

| kill | 终止脚本 |

| .exit | 退出调试器 |

repl 子模式中:可以输入任意 JS 表达式,包括访问局部变量/闭包变量。Ctrl+C 退回 debug>

附加到运行中的进程

当进程已经在运行时(例如长期运行的开发服务器或 TUI 网关):


# 1. Send SIGUSR1 to enable the inspector on an existing process
kill -SIGUSR1 
# Node prints: Debugger listening on ws://127.0.0.1:9229/

# 2. Attach the debugger CLI
node inspect -p 
# or by URL
node inspect ws://127.0.0.1:9229/

要让进程从一开始就启用 inspector 启动:


node --inspect script.js           # listen on 127.0.0.1:9229, keep running
node --inspect-brk script.js       # listen AND pause on first line
node --inspect=0.0.0.0:9230 script.js   # custom host:port

TypeScript 通过 tsx 运行时:


node --inspect-brk --import tsx script.ts
# or older tsx
node --inspect-brk -r tsx/cjs script.ts

以编程方式使用 CDP(从终端脚本化)

当你想自动化操作 —— 设置大量断点、采集作用域状态、脚本化复现 —— 使用 chrome-remote-interface


npm i -g chrome-remote-interface        # or project-local
# Start your target:
node --inspect-brk=9229 target.js &

驱动脚本(保存为 /tmp/cdp-debug.js):


const CDP = require('chrome-remote-interface');

(async () => {
  const client = await CDP({ port: 9229 });
  const { Debugger, Runtime } = client;

  Debugger.paused(async ({ callFrames, reason }) => {
    const top = callFrames[0];
    console.log(`PAUSED: ${reason} @ ${top.url}:${top.location.lineNumber + 1}`);

    // Walk scopes for locals
    for (const scope of top.scopeChain) {
      if (scope.type === 'local' || scope.type === 'closure') {
        const { result } = await Runtime.getProperties({
          objectId: scope.object.objectId,
          ownProperties: true,
        });
        for (const p of result) {
          console.log(`  ${scope.type}.${p.name} =`, p.value?.value ?? p.value?.description);
        }
      }
    }

    // Evaluate an expression in the paused frame
    const { result } = await Debugger.evaluateOnCallFrame({
      callFrameId: top.callFrameId,
      expression: 'typeof state !== "undefined" ? JSON.stringify(state) : "n/a"',
    });
    console.log('state =', result.value ?? result.description);

    await Debugger.resume();
  });

  await Runtime.enable();
  await Debugger.enable();

  // Set a breakpoint by URL regex + line
  await Debugger.setBreakpointByUrl({
    urlRegex: '.*app.tsx$',
    lineNumber: 119,       // 0-indexed
    columnNumber: 0,
  });

  await Runtime.runIfWaitingForDebugger();
})();

运行它:


node /tmp/cdp-debug.js

Hermes 特别说明:chrome-remote-interface 不在 ui-tui/package.json 中。如果不想弄脏项目,可以安装到一个临时位置:


mkdir -p /tmp/cdp-tools && cd /tmp/cdp-tools && npm i chrome-remote-interface
NODE_PATH=/tmp/cdp-tools/node_modules node /tmp/cdp-debug.js

调试 Hermes ui-tui

TUI 基于 Ink + tsx 构建。两种常见场景:

在开发模式下调试单个 Ink 组件

ui-tui/package.json 中有 npm run dev(tsx --watch)。直接运行 tsx 并加上 --inspect-brk 即可:


cd /home/bb/hermes-agent/ui-tui
npm run build    # produce dist/ once so transpile isn't needed on first load
node --inspect-brk dist/entry.js
# In another terminal:
node inspect -p 

然后在 debug> 中:


sb('dist/app.js', 220)     # or wherever the suspect render is
cont

暂停后,使用 repl 检查 props、状态 ref、useInput 处理函数的值等。

调试运行中的 `hermes --tui`

TUI 由 Python CLI 启动 Node 进程。最简单的路径:


# 1. Launch TUI
hermes --tui &
TUI_PID=$(pgrep -f 'ui-tui/dist/entry' | head -1)

# 2. Enable inspector on that Node PID
kill -SIGUSR1 "$TUI_PID"

# 3. Find the WS URL
curl -s http://127.0.0.1:9229/json/list | jq -r '.[0].webSocketDebuggerUrl'

# 4. Attach
node inspect ws://127.0.0.1:9229/

与 TUI 交互(在其窗口中输入)会继续推进执行;你的调试器可以在任意 sb(...) 断点处随时将其暂停。

调试 `_SlashWorker` / PTY 子进程

那些是 Python 进程而非 Node —— 请对它们使用 python-debugpy 技能。只有 Node 部分(Ink UI、tui_gateway 客户端、ui-tui/ 下由 tsx 运行的测试)使用本技能。

在调试器下运行 Vitest 测试


cd /home/bb/hermes-agent/ui-tui
# Run a single test file paused on entry
node --inspect-brk ./node_modules/vitest/vitest.mjs run --no-file-parallelism src/app/foo.test.tsx

在另一个终端中:node inspect -p ,然后 sb('src/app/foo.tsx', 42)cont

使用 --no-file-parallelism(vitest)或 --runInBand(jest),保证只有一个 worker 存在 —— 调试一个进程池是很痛苦的。

堆快照与 CPU Profile(非交互式)

在上面的 CDP 驱动脚本中,把 Debugger 换成 HeapProfiler / Profiler


// CPU profile for 5 seconds
await client.Profiler.enable();
await client.Profiler.start();
await new Promise(r => setTimeout(r, 5000));
const { profile } = await client.Profiler.stop();
require('fs').writeFileSync('/tmp/cpu.cpuprofile', JSON.stringify(profile));
// Open /tmp/cpu.cpuprofile in Chrome DevTools → Performance tab

// Heap snapshot
await client.HeapProfiler.enable();
const chunks = [];
client.HeapProfiler.addHeapSnapshotChunk(({ chunk }) => chunks.push(chunk));
await client.HeapProfiler.takeHeapSnapshot({ reportProgress: false });
require('fs').writeFileSync('/tmp/heap.heapsnapshot', chunks.join(''));

常见陷阱

  1. TS 源码中的行号不对。断点命中的是编译产出的 JS,而非 .ts。要么 (a) 在构建产物 dist/*.js 中打断点,要么 (b) 启用 sourcemap(node --enable-source-maps)并使用 sb('src/app.tsx', N) —— 但仅限支持 sourcemap 的 CDP 客户端。node inspect CLI 不支持。
    1. --inspect--inspect-brk 的区别。--inspect 启动 inspector 但不暂停;如果你附加得太晚,脚本会跑过第一个断点。需要在任何代码运行之前设置断点时,请使用 --inspect-brk
      1. 端口冲突。默认端口是 9229。如果有多个 Node 进程在调试,传入 --inspect=0(随机端口)并从 /json/list 读取实际 URL:
      2. 
           curl -s http://127.0.0.1:9229/json/list   # lists all inspectable targets on the host
        
        1. 子进程。对父进程使用 --inspect 不会调试其子进程。使用 NODE_OPTIONS='--inspect-brk' node parent.js 可传播到每个子进程;注意它们都需要唯一端口(当 NODE_OPTIONS='--inspect' 被继承时,Node 会自动递增端口号)。
          1. 后台终止问题。如果在目标暂停时用 Ctrl+C 退出 node inspect,目标会保持暂停状态。要么先 cont,要么显式 kill 目标。
            1. 通过代理终端运行 node inspect它是一个对 PTY 友好的 REPL。在 Hermes 中,用 terminal(pty=true)background=true + process(action='submit', data='...') 启动它。非 PTY 前台模式可以执行一次性命令,但无法进行交互式单步调试。
              1. 安全性。--inspect=0.0.0.0:9229 会暴露任意代码执行能力。除非处于隔离网络,否则始终绑定 127.0.0.1(默认值)。
              2. 验证清单

                建立调试会话后,验证:

                • [ ] curl -s http://127.0.0.1:9229/json/list 返回的正是你期望的目标
                • [ ] 第一个断点确实命中(如果没有,很可能是漏了 --inspect-brk,或在执行完成后才附加)
                • [ ] 暂停时的源码列表显示的是正确的文件(不匹配 = sourcemap 问题,见陷阱 1)
                • [ ] 在 repl 中执行 exec process.pid 返回的正是你想附加的 PID

                一键方案

                “为什么这个变量在第 X 行是 undefined?”

                
                node --inspect-brk script.js &
                node inspect -p $!
                # debug>
                sb('script.js', X)
                cont
                # paused. Now:
                repl
                > myVariable
                > Object.keys(this)
                

                “调用路径是怎么进入这个函数的?”

                
                debug> sb('suspectFn')
                debug> cont
                # paused on entry
                debug> bt
                

                “这条异步链卡住了 —— 卡在哪?”

                
                # Start with --inspect (no -brk), let it run to the hang, then:
                debug> pause
                debug> bt
                # Now you see the stuck frame
                

评论区

发表评论