通过 --inspect + Chrome DevTools Protocol CLI 调试 Node.js
快速参考:
在第一行暂停启动:
在 调试运行中的
TUI 从 Python CLI 生成 Node。最简单路径:
调试
这些是 Python,不是 Node——用
概述
当console.log 不够用时,从终端以编程方式驱动 Node 内置的 V8 检查器。你可以获得真正的断点、步入/步过/步出、调用栈遍历、局部/闭包作用域转储,以及在暂停帧中执行任意表达式。
两种工具,选一种:
node inspect— 内置、零安装、CLI REPL。最适合快速探索。- ndb / CDP via chrome-remote-interface — 可从 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
# 或使用 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') | 每次暂停时求值表达式 |
watchers | 显示监视表达式 |
repl | 在当前作用域进入 REPL(Ctrl+C 退出 REPL) |
exec expr | 求值表达式一次 |
restart | 重启脚本 |
kill | 终止脚本 |
.exit | 退出调试器 |
repl 子模式:输入任意 JS 表达式,包括访问局部/闭包变量。Ctrl+C 退回 debug>。
附加到运行中的进程
当进程已经在运行(如长期运行的开发服务器或 TUI gateway):
# 1. 发送 SIGUSR1 在现有进程上启用检查器
kill -SIGUSR1
# Node 打印:Debugger listening on ws://127.0.0.1:9229/
# 2. 附加调试器 CLI
node inspect -p
# 或通过 URL
node inspect ws://127.0.0.1:9229/
从头启动带检查器的进程:
node --inspect script.js # 监听 127.0.0.1:9229,继续运行
node --inspect-brk script.js # 监听并在第一行暂停
node --inspect=0.0.0.0:9230 script.js # 自定义 host:port
通过 tsx 处理 TypeScript:
node --inspect-brk --import tsx script.ts
# 或旧版 tsx
node --inspect-brk -r tsx/cjs script.ts
编程式 CDP(从终端脚本化)
当需要自动化——设置多个断点、捕获作用域状态、脚本化复现——使用chrome-remote-interface:
npm i -g chrome-remote-interface
# 或项目本地安装
# 启动目标:
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}); // 遍历作用域获取局部变量 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); } } } // 在暂停帧中求值表达式 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(); // 通过 URL 正则 + 行号设置断点 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 # 生成 dist/,这样首次加载不需要转译
node --inspect-brk dist/entry.js
# 在另一个终端:
node inspect -p
然后在 debug> 内:
sb('dist/app.js', 220) # 或可疑渲染的位置
cont
暂停时,repl → 检查 props、state refs、useInput 处理器值等。
调试运行中的 hermes --tui
TUI 从 Python CLI 生成 Node。最简单路径:
# 1. 启动 TUI
hermes --tui &
TUI_PID=$(pgrep -f 'ui-tui/dist/entry' | head -1)
# 2. 在该 Node PID 上启用检查器
kill -SIGUSR1 "$TUI_PID"
# 3. 找到 WS URL
curl -s http://127.0.0.1:9229/json/list | jq -r '.[0].webSocketDebuggerUrl'
# 4. 附加
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
# 在入口暂停运行单个测试文件
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)确保只有一个工作进程——调试池很痛苦。
堆快照和 CPU 分析(非交互式)
在上面的 CDP 驱动中,将 Debugger 换成 HeapProfiler / Profiler:
// CPU 分析 5 秒
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));
// 在 Chrome DevTools → Performance 标签打开 /tmp/cpu.cpuprofile
// 堆快照
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(''));
常见陷阱
- TS 源码行号错误。断点命中的是生成的 JS,不是
.ts。要么 (a) 在构建的dist/*.js中断点,要么 (b) 启用 sourcemaps(node --enable-source-maps)并使用sb('src/app.tsx', N)——但只有支持 sourcemaps 的 CDP 客户端才行。node inspectCLI 不支持。
--inspectvs--inspect-brk。--inspect启动检查器但不暂停;如果附加太晚,脚本会跑过第一个断点。需要在任何代码运行前设置断点时用--inspect-brk。
- 端口冲突。默认是 9229。如果多个 Node 进程在调试,传
--inspect=0(随机端口)并从/json/list读取实际 URL:
curl -s http://127.0.0.1:9229/json/list
- 子进程。父进程的
--inspect不会调试子进程。使用NODE_OPTIONS='--inspect-brk' node parent.js传播到每个子进程;注意它们都需要唯一端口(当NODE_OPTIONS='--inspect'被继承时 Node 自动递增)。
- 后台终止。如果目标暂停时你 Ctrl+C 退出
node inspect,目标保持暂停。要么先cont,要么显式kill目标。
- 通过代理终端运行
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 行是 undefined?"
node --inspect-brk script.js &
node inspect -p $!
# debug> sb('script.js', X)
cont
# 暂停。现在:
repl
> myVariable
> Object.keys(this)
"进入这个函数的调用路径是什么?"
debug> sb('suspectFn')
debug> cont
# 在入口暂停
debug> bt
"这个异步链卡住了——在哪?"
# 用 --inspect(无 -brk)启动,让它运行到卡住,然后:
debug> pause
debug> bt
# 现在你看到卡住的帧
安装指南
复制下方命令,在终端运行即可安装:
# 安装到当前项目
npx skills add node-inspect-debugger
# 全局安装 — 所有项目可用
npx skills add node-inspect-debugger -g
使用指南
安装完成后,在对话框中直接使用此技能。