mcpField
N
node-inspect-debugger

node-inspect-debugger

github.com/nousresearch开发258.1k397 次安装今天更新
代码仓库

关于 node-inspect-debugger

面向 Node.js 开发者的调试技能,使用内置 V8 检查器或可编程的 CDP 客户端。内容涵盖断点、作用域检查、连接运行中进程,以及 CPU 或堆分析。

主要功能

  1. 01使用内置 node inspect 命令行调试
  2. 02设置断点并检查作用域
  3. 03连接运行中的 Node.js 进程
  4. 04通过 CDP 自动化调试
  5. 05采集 CPU 分析和堆快照

适用场景

  1. 01检查失败的 Node 测试中的中间状态
  2. 02查看渲染前的 UI 状态
  3. 03调试运行中的开发服务器
  4. 04收集 CPU 或堆诊断数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nousresearch/hermes-agent --skill node-inspect-debugger

安装后在 Claude Code 中输入/node-inspect-debugger确认已加载。

详细介绍

Node.js 检查器调试器

概述

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

两种工具,任选其一:

  • node inspect — 内置,无需安装,带命令行 REPL。适合快速探查。
  • ndb / CDP via chrome-remote-interface — 可通过 Node/Python 编写脚本;适合自动化设置多个断点、收集多次运行中的状态,或在 Agent 循环中进行非交互式调试。

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

适用场景

  • Node 测试失败,需要查看中间状态
  • ui-tui 崩溃或行为异常,需要在渲染前检查 React/Ink 状态
  • tui_gateway 子进程(_SlashWorker、PTY 桥接工作进程)行为异常
  • 需要检查闭包中的值,但无法在不修改代码的情况下通过 console.log 访问
  • 性能分析:附加到正在运行的进程,以捕获 CPU 配置文件或堆快照

不适用于: 能在一分钟内通过 console.log 解决的问题。断点驱动的调试开销更大;只有确实值得时才使用。

快速参考:node inspect REPL

在第一行暂停启动:

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

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

命令操作
c or cont继续
n or next单步跳过
s or step单步进入
o or out单步跳出
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 <pid>
# Node prints: Debugger listening on ws://127.0.0.1:9229/<uuid>

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

从启动时就启用检查器来启动进程:

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

通过 tsx 运行 TypeScript:

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 &

驱动脚本(保存为 ~/.hermes/cache/scratch/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 ~/.hermes/cache/scratch/cdp-debug.js

Hermes 特别说明:chrome-remote-interface 不在 ui-tui/package.json 中。如果不想污染项目,可将其安装到临时位置:

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

调试 Hermes ui-tui

TUI 使用 Ink + tsx 构建。常见的两种场景:

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

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

cd <hermes-agent-repo>/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 <node pid>

然后在 debug> 中:

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

暂停后,输入 repl → 检查 props、状态引用、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/<uuid>

与 TUI 交互(在其窗口中输入)会继续推进执行;调试器可通过任意 sb(...) 在断点处暂停它。

调试 _SlashWorker / PTY 子进程

这些是 Python 进程,而不是 Node 进程——请使用 python-debugpy 技能。只有 Node 部分(Ink UI、tui_gateway 客户端、ui-tui/ 下通过 tsx-run 运行的测试)使用此技能。

在调试器下运行 Vitest 测试

cd <hermes-agent-repo>/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 <pid>,然后运行 sb('src/app/foo.tsx', 42)、cont。

使用 --no-file-parallelism(vitest)或 --runInBand(jest),确保只有一个工作进程——调试进程池很麻烦。

堆快照和 CPU 性能分析(非交互式)

使用上文中的 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('~/.hermes/cache/scratch/cpu.cpuprofile', JSON.stringify(profile));
// Open ~/.hermes/cache/scratch/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('~/.hermes/cache/scratch/heap.heapsnapshot', chunks.join(''));

常见问题

  1. TypeScript 源码中的行号不正确。 断点会命中生成的 JS,而不是 .ts。可以(a)在构建后的 dist/*.js 中设置断点;或(b)启用 sourcemaps(node --enable-source-maps),并使用 sb('src/app.tsx', N)——但这仅适用于会遵循 sourcemaps 的 CDP 客户端。node inspect CLI 不支持。

  2. --inspect 与 --inspect-brk。 --inspect 会启动检查器,但不会暂停;如果连接太晚,脚本会在第一个断点前继续执行。需要在代码运行前设置断点时,请使用 --inspect-brk。

  3. 端口冲突。 默认端口为 9229。如果有多个 Node 进程正在接受检查,请传入 --inspect=0(随机端口),并从 /json/list 读取实际 URL:

    curl -s http://127.0.0.1:9229/json/list   # lists all inspectable targets on the host
    
  4. 子进程。 父进程上的 --inspect 不会检查其子进程。使用 NODE_OPTIONS='--inspect-brk' node parent.js 将其传递给每个子进程;注意它们都需要唯一端口(继承 NODE_OPTIONS='--inspect' 时,Node 会自动递增端口)。

  5. 后台终止。 如果在目标暂停时对 node inspect 按 Ctrl+C,目标仍会保持暂停。请先运行 cont,或者显式终止目标进程。

  6. 通过 Agent 终端运行 node inspect。 这是一个适用于 PTY 的 REPL。在 Hermes 中,使用 terminal(pty=true) 启动,或者使用 background=true,再通过 process(action='submit', data='...') 操作。非 PTY 前台模式可用于一次性命令,但不适用于交互式单步调试。

  7. 安全性。 --inspect=0.0.0.0:9229 会暴露任意代码执行能力。除非使用隔离网络,否则始终绑定到 127.0.0.1(默认值)。

验证清单

设置调试会话后,请确认:

  • curl -s http://127.0.0.1:9229/json/list 返回的内容与预期目标完全一致
  • 第一个断点确实命中(如果没有,可能是漏用了 --inspect-brk,或是在执行完成后才连接)
  • 暂停时显示的源代码文件正确(不匹配表示 sourcemap 有问题,参见问题 1)
  • 在 repl 中执行 exec process.pid,返回的是你打算连接的 PID

单步操作示例

“为什么此变量在第 X 行未定义?”

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

评论

支持 Markdown