关于 debug
debug 是面向开发者和编码 Agent 的软件排错技能,强调用运行时日志而非猜测定位问题。它按假设、插桩、复现、分析、修复和验证组织排错流程。
主要功能
- 01基于假设开展排错
- 02Node.js 日志服务与会话
- 03JavaScript 和 Python 插桩指引
- 04根据日志评估假设
- 05基于证据验证修复
- 06浏览器与扩展日志方案
适用场景
- 01追踪空值或类型错误
- 02调查条件分支与异步时序
- 03复现并分析交互流程故障
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vltansky/ debug-skill --skill debug安装后在 Claude Code 中输入/debug确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
package.jsonREADME.md.claude-plugin/marketplace.jsonscripts/debug_cleanup.jsdebug_server.js详细介绍
调试模式
用运行时证据修复 bug,而不是靠猜测。
Don't guess → Hypothesize → Instrument → Reproduce → Analyze → Fix → Verify
何时使用
触发信号(如果你正准备做以下任何一件事,请改用本 skill):
- “打开 DevTools Console 检查……”
- “复现这个 bug,然后告诉我你看到了什么”
- “加个 console.log,把输出告诉我”
- “点击 X,打开 Y,检查控制台里是否出现 Z”
应当触发本 skill 的示例场景:
❌ Without skill (manual, slow):
"I added debug logging. Please:
1. Open the app in browser
2. Open DevTools Console (F12)
3. Open the defect modal and select a defect
4. Check console for [DEBUG] logs
5. Tell me what you see"
✅ With skill (automated):
Logs are captured server-side → you read them directly → no user copy-paste needed
在调试以下问题时使用:
- 状态/取值问题(null、undefined、类型错误)
- 条件逻辑(走了哪个分支)
- 异步时序(竞态条件、加载顺序)
- 用户交互流程(弹窗、表单、点击)
参数
/debug /path/to/project
如果未提供路径,则使用当前工作目录。
工作流
阶段 1:启动日志服务
步骤 1:确保服务正在运行(需要时启动,已在运行则不操作):
node skills/debug/scripts/debug_server.js /path/to/project &
服务输出 JSON:
{"status":"started",...}- 新服务已启动{"status":"already_running",...}- 服务原本就在运行(这没问题!)
步骤 2:创建会话(服务根据你的描述生成唯一 ID):
curl -s -X POST http://localhost:8787/session -d '{"name":"fix-null-userid"}'
响应:
{"session_id":"fix-null-userid-a1b2c3","log_file":"/path/to/project/.debug/debug-fix-null-userid-a1b2c3.log"}
保存响应中的 session_id - 后续所有步骤都要用到它。
服务端点:
- POST
/session,请求体为{"name": "description"}→ 创建会话,返回{session_id, log_file} - POST
/log,请求体为{"sessionId": "...", "msg": "..."}→ 写入日志文件 - GET
/→ 返回状态和日志目录
如果端口 8787 被占用: lsof -ti :8787 | xargs kill -9,然后重启
──────────
阶段 2:生成假设
在插桩之前,先提出 3-5 条具体假设:
Hypothesis H1: userId is null when passed to calculateScore()
Expected: number (e.g., 5)
Actual: null
Test: Log userId at function entry
Hypothesis H2: score is string instead of number
Expected: 85 (number)
Actual: "85" (string)
Test: Log typeof score
每条假设必须:
- 具体(不能是“某个地方出错了”)
- 可验证(能通过日志确认或否定)
- 覆盖不同子系统(不要扎堆)
──────────
阶段 3:对代码插桩
添加日志调用,以验证所有假设。
JavaScript/TypeScript:
// #region debug
const SESSION_ID = 'REPLACE_WITH_SESSION_ID'; // e.g. 'fix-null-userid-a1b2c3'
const DEBUG_LOG_URL = 'http://localhost:8787/log';
const debugLog = (msg, data = {}, hypothesisId = null) => {
const payload = JSON.stringify({
sessionId: SESSION_ID,
msg,
data,
hypothesisId,
loc: new Error().stack?.split('\n')[2],
});
if (navigator.sendBeacon?.(DEBUG_LOG_URL, payload)) return;
fetch(DEBUG_LOG_URL, { method: 'POST', body: payload }).catch(() => {});
};
// #endregion
// Usage
debugLog('Function entry', { userId, score, typeScore: typeof score }, 'H1,H2');
Python:
# #region debug
import requests, traceback
SESSION_ID = 'REPLACE_WITH_SESSION_ID' # e.g. 'fix-null-userid-a1b2c3'
def debug_log(msg, data=None, hypothesis_id=None):
try:
requests.post('http://localhost:8787/log', json={
'sessionId': SESSION_ID, 'msg': msg, 'data': data,
'hypothesisId': hypothesis_id, 'loc': traceback.format_stack()[-2].strip()
}, timeout=0.5)
except: pass
# #endregion
# Usage
debug_log('Function entry', {'user_id': user_id, 'type': type(user_id)}, 'H1')
指引:
- 3-8 个插桩点
- 覆盖:入口/出口、关键操作的前/后、各分支路径
- 每条日志都打上
hypothesisId标签 - 用
// #region debug...// #endregion包裹 - 高频事件(mousemove、scroll):仅在状态变化时记录日志
- 同时记录意图和结果
──────────
阶段 4:清空并复现
-
清空日志:
: > /path/to/project/.debug/debug-$SESSION_ID.log -
提供复现步骤:
<reproduction_steps> 1. Start app: yarn dev 2. Navigate to /users 3. Click "Calculate Score" 4. Observe NaN displayed </reproduction_steps> -
用户复现 bug
──────────
阶段 5:分析日志
读取并评估:
cat /path/to/project/.debug/debug-$SESSION_ID.log
对每条假设:
Hypothesis H1: userId is null
Status: CONFIRMED
Evidence: {"msg":"Function entry","data":{"userId":null}}
Hypothesis H2: score is string
Status: REJECTED
Evidence: {"data":{"typeScore":"number"}}
状态选项:
- CONFIRMED:日志证实
- REJECTED:日志否定
- INCONCLUSIVE:需要更多插桩
如果全部为 INCONCLUSIVE/REJECTED:生成新假设,添加更多日志,迭代。
──────────
阶段 6:修复
只有在日志确认根因后才进行修复。
保持插桩处于启用状态(暂时不要移除)。
给验证日志打上 runId: "post-fix" 标签:
debugLog('Function entry', { userId, runId: 'post-fix' }, 'H1');
──────────
阶段 7:验证
- 清空日志
- 用户复现(bug 应该已消失)
- 对比修复前/后:
Before: {"data":{"userId":null},"runId":"run1"} After: {"data":{"userId":5},"runId":"post-fix"} - 用日志证据确认
如果仍然有问题:提出新假设,添加更多日志,迭代。
──────────
阶段 8:五问法(可选)
何时执行: 反复出现的 bug、生产事故、安全问题,或者“这问题一直出现”。
修复后,追问“这个 bug 为什么会存在?”,以找出系统性原因:
Bug: API returns NaN
Why 1: userId was null → Code fix: null check
Why 2: No input validation → Add validation
Why 3: No test for null case → Add test
Why 4: Review didn't catch → (one-off, acceptable)
分类:
| 类型 | 行动 |
|---|---|
| CODE | 立即修复 |
| TEST | 添加测试 |
| PROCESS | 更新检查清单/评审流程 |
| SYSTEMIC | 记录模式 |
跳过条件: 简单的一次性 bug、影响低、不会反复出现。
──────────
阶段 9:清理
仅在满足以下条件后移除插桩:
- 修复后的日志证明成功
- 用户确认已解决
搜索 #region debug 并移除所有调试代码。
日志格式
每行都是 NDJSON:
{"ts":"2024-01-03T12:00:00.000Z","msg":"Button clicked","data":{"id":5},"hypothesisId":"H1","loc":"app.js:42"}
关键规则
- 未经运行时证据,绝不修复 - 始终先收集日志
- 未经验证,绝不移除埋点 - 保留至修复确认
- 绝不猜测 - 如有不确定,添加更多日志
- 如果所有假设均被否定 - 从不同子系统生成新假设
故障排查
| 问题 | 解决方案 |
|---|---|
| 服务无法启动 | 检查端口 8787 是否被占用:lsof -i :8787 |
| 日志为空 | 检查浏览器拦截(混合内容/CSP/CORS)、防火墙 |
| 日志文件错误 | 验证 session ID 是否匹配 |
| 日志过多 | 按 hypothesisId 过滤,使用状态变更日志 |
| 无法复现 | 向用户询问确切步骤,检查环境 |
CORS / 混合内容变通方案
如果日志未能到达,通常是以下情况之一:
- 混合内容:HTTPS 应用 →
http://localhost:8787被阻止。使用开发服务器代理(同源)或通过 HTTPS 提供日志端点。 - CSP:
connect-src阻止了日志 URL。使用开发服务器代理或更新 CSP。 - CORS 预检:
Content-Type: application/json会触发OPTIONS。使用“简单”请求(text/plain)或sendBeacon。
1. sendBeacon(避免预检;即发即忘):
const DEBUG_LOG_URL = 'http://localhost:8787/log';
const debugLog = (msg, data = {}, hypothesisId = null) => {
const payload = JSON.stringify({ sessionId: SESSION_ID, msg, data, hypothesisId });
if (navigator.sendBeacon?.(DEBUG_LOG_URL, payload)) return;
fetch(DEBUG_LOG_URL, { method: 'POST', body: payload }).catch(() => {});
};
注意:仍会被混合内容 + CSP 阻止。
2. 开发服务器代理(Vite 示例) - 同源 /__log → http://localhost:8787/log:
// vite.config.js
export default {
server: {
proxy: {
'/__log': {
target: 'http://localhost:8787',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/__log/, '/log'),
},
},
},
};
// Then POST to /__log instead of localhost:8787/log
3. 最后手段(仅限本地) - 在浏览器设置中允许不安全内容 / 禁用混合内容阻止
Chrome 扩展调试
内容脚本运行在具有严格 CSP 的隔离世界中 - 它们无法直接 fetch 到 localhost:8787。解决方案是通过后台脚本(service worker)中继日志。
内容脚本(发送方):
// #region debug
const DEBUG_SESSION_ID = 'your-session-id-here';
const debugLog = (msg, data = {}, hypothesisId = null) => {
chrome.runtime.sendMessage({
type: 'DEBUG_LOG',
payload: {
sessionId: DEBUG_SESSION_ID,
msg,
data,
hypothesisId,
loc: new Error().stack?.split('\n')[2]?.trim(),
},
}).catch(() => {});
};
// #endregion
// Usage
debugLog('handleMouseMove', { target: target.tagName, rect }, 'H1');
后台脚本(中继):
// #region debug - relay logs to debug server
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'DEBUG_LOG') {
fetch('http://localhost:8787/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(message.payload),
}).catch(() => {});
sendResponse({ ok: true });
return true;
}
});
// #endregion
为什么这有效:
- 后台脚本(service worker)具有宽松的 CSP,可以 fetch 到 localhost
chrome.runtime.sendMessage是内容脚本与后台之间的桥梁- 保持两个调试区域都有标记,便于清理
注入脚本(MAIN 世界):
如果调试代码通过 <script> 注入到页面上下文中,使用 window.postMessage 中继到内容脚本,再由内容脚本中继到后台:
// In MAIN world (injected script)
window.postMessage({ type: 'DEBUG_LOG_RELAY', payload: { ... } }, '*');
// In content script
window.addEventListener('message', (e) => {
if (e.data?.type === 'DEBUG_LOG_RELAY') {
chrome.runtime.sendMessage({ type: 'DEBUG_LOG', payload: e.data.payload });
}
});
检查清单
- 服务正在运行(started 或 already_running)
- 通过
POST /session创建会话 - 保存返回的session_id - 生成了 3-5 个假设
- 添加了 3-8 条日志,并标记 hypothesisId
- 在复现前清空日志
- 提供了复现步骤
- 评估了每个假设(CONFIRMED/REJECTED/INCONCLUSIVE)
- 仅基于证据进行修复
- 完成了修复前/后对比
- 确认后移除了埋点