关于 node-inspect-debugger
面向 Node.js 开发者的调试技能,使用内置 V8 检查器或可编程的 CDP 客户端。内容涵盖断点、作用域检查、连接运行中进程,以及 CPU 或堆分析。
主要功能
- 01使用内置 node inspect 命令行调试
- 02设置断点并检查作用域
- 03连接运行中的 Node.js 进程
- 04通过 CDP 自动化调试
- 05采集 CPU 分析和堆快照
适用场景
- 01检查失败的 Node 测试中的中间状态
- 02查看渲染前的 UI 状态
- 03调试运行中的开发服务器
- 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 viachrome-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(''));
常见问题
-
TypeScript 源码中的行号不正确。 断点会命中生成的 JS,而不是
.ts。可以(a)在构建后的dist/*.js中设置断点;或(b)启用 sourcemaps(node --enable-source-maps),并使用sb('src/app.tsx', N)——但这仅适用于会遵循 sourcemaps 的 CDP 客户端。node inspectCLI 不支持。 -
--inspect与--inspect-brk。--inspect会启动检查器,但不会暂停;如果连接太晚,脚本会在第一个断点前继续执行。需要在代码运行前设置断点时,请使用--inspect-brk。 -
端口冲突。 默认端口为
9229。如果有多个 Node 进程正在接受检查,请传入--inspect=0(随机端口),并从/json/list读取实际 URL:curl -s http://127.0.0.1:9229/json/list # lists all inspectable targets on the host -
子进程。 父进程上的
--inspect不会检查其子进程。使用NODE_OPTIONS='--inspect-brk' node parent.js将其传递给每个子进程;注意它们都需要唯一端口(继承NODE_OPTIONS='--inspect'时,Node 会自动递增端口)。 -
后台终止。 如果在目标暂停时对
node inspect按Ctrl+C,目标仍会保持暂停。请先运行cont,或者显式终止目标进程。 -
通过 Agent 终端运行
node inspect。 这是一个适用于 PTY 的 REPL。在 Hermes 中,使用terminal(pty=true)启动,或者使用background=true,再通过process(action='submit', data='...')操作。非 PTY 前台模式可用于一次性命令,但不适用于交互式单步调试。 -
安全性。
--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