name: node-inspect-debugger description: “Debug Node.js via –inspect + Chrome DevTools Protocol CLI.” version: 1.0.0 author: Hermes Agent license: MIT platforms: [linux, macos, windows] metadata: hermes: tags: [debugging, nodejs, node-inspect, cdp, breakpoints, ui-tui] related_skills: [systematic-debugging, python-debugpy, debugging-hermes-tui-commands]
Node.js 检查调试器
概述
当 console.log 已无法满足需求时,您可以通过终端以编程方式调用 Node 内置的 V8 检查工具。该工具支持真正的断点设置、单步执行/跳过/退出、调用栈遍历、局部变量/闭包作用域查看,以及在对当前帧进行暂停时评估任意表达式。
您可以从以下两种工具中选择一种:
node inspect—— 内置工具,无需安装,采用 CLI REPL 形式。非常适合快速调试。ndb/ 通过chrome-remote-interface的 CDP —— 可从 Node/Python 脚本中调用;当您需要设置大量断点、在多次运行中收集状态信息,或通过代理循环进行非交互式调试时,此选项更为适用。
建议优先使用 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 或 cont | 继续执行 |
n 或 next | 跳过当前行 |
s 或 step | 进入函数内部 |
o 或 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 &
驱动脚本(保存为 /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的特别说明:ui-tui/package.json文件中并未包含chrome-remote-interface。若您不想污染项目源码,可将其安装到临时目录中。
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 <node pid>
接着在 debug> 内部:
sb('dist/app.js', 220) # or wherever the suspect render is
cont
当程序暂停时,可执行 repl 命令来查看 props、状态引用、useInput 处理函数的值等信息。
调试正在运行的 hermes --tui 程序
TUI 会通过 Python CLI 启动 Node.js。最简便的调试方式如下:
# 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 /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 <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('/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(''));
常见问题
-
TypeScript 源文件中的行号错误。 断点实际上作用于生成的 JavaScript 文件,而非
.ts文件。解决方案为:(a) 在构建后的dist/*.js文件中设置断点;或 (b) 启用源映射功能(使用node --enable-source-maps),并通过sb('src/app.tsx', N)设置断点——但仅适用于支持源映射的 CDP 客户端,node inspectCLI 不支持此功能。 -
--inspect与--inspect-brk的区别。--inspect仅启动调试器而不暂停脚本执行,若在过晚时附加调试器,脚本将会直接跳过第一个断点。当需要在代码运行之前就设置断点时,请使用--inspect-brk。 -
端口冲突问题。 默认端口为
9229。如果有多个 Node 进程正在使用该端口进行调试,可指定--inspect=0以随机选择端口,并通过/json/list获取实际的端口号: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退出程序,目标进程仍会保持暂停状态。此时要么先使用cont命令继续执行,要么直接使用kill命令终止目标进程。 -
通过Agent终端运行
node inspect。该工具是一个兼容伪终端的REPL环境。在Hermes中,可通过terminal(pty=true)或background=true结合process(action='submit', data='...')来启动它。非伪终端的前台模式仅适用于一次性命令,而不适合交互式调试。 -
安全性。使用
--inspect=0.0.0.0:9229选项会暴露代码执行风险。除非处于隔离网络环境中,否则应始终将监听地址设置为默认值127.0.0.1。
验证清单
设置完调试会话后,请进行以下验证:
- 执行
curl -s http://127.0.0.1:9229/json/list命令后,返回的结果确实为预期的目标进程。 - 第一个断点能够成功触发(若无法触发,很可能是遗漏了
--inspect-brk选项,或是在进程执行完毕后才进行连接)。 - 暂停时显示的源代码列表为正确的文件(若不一致,则属于源码映射问题,详见注意事项1)。
- 在REPL环境中执行
exec process.pid命令后,返回的进程ID即为原本打算连接的进程ID。
一次性调试方案
“为什么在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