dsh-plugin-debug-mode
DSH(DeepSeek Harness)插件:Cursor Agent 式的 Debug 模式。 让 Agent 别猜——它自己在你的代码里埋探针,把现场取回来;你复现一次,然后二选一。
它解决什么问题
「点了保存之后列表偶尔不刷新。」
传统流程是 Agent 读代码 → 猜一个原因 → 改 → 你试 → 没好 → 再猜一轮。 Debug 模式把这件事反过来:先在代码里装仪表,再让现象自己说话。
你:现象描述
└─ Agent 分析 → 在可疑路径埋探针(每处都带可摘除标记)
└─ 探针把记录交给宿主,落进 <你的仓库>/.dsh/debug-mode/tasks/<hash>/<hash>.log
└─ 你复现一次
└─ Agent 通过「读取日志」拿到摘要(异常全量、分组计数、省略量显式)
└─ 面板弹出,你选:
[ 问题已解决 ] → 删日志 + 删掉本次全部探针代码
[ 问题复现,需要进一步修复 ] → 日志**保留**(Agent 还没读它)
整个流程不需要 Agent 运行任何命令。 它的动作只有两种:给调试服务发信号
(dsh_debug_* 工具)+ 在你的源代码里埋点/改代码。"跑起来复现"是你的事。
这不是洁癖——有一次真实调试里,工作区所在的 D:\ 归 SYSTEM 所有、当前账号缺
「更改权限」,于是所有 shell 命令在执行前的授权阶段就被系统拒绝了;
而同一时刻编辑文件完全正常。能挡住整个流程的,恰恰是流程本来就不需要的那一步,
所以现在这条被写成了硬约束。
怎么进入
只有一个入口:斜杠命令。
/debug 保存之后列表偶尔不刷新,十几分钟出现一次
敲完回车就开始了:这一条命令会连同你的描述直接开一轮对话,Agent 立刻开始分析, 不需要你再补一句「开始」。描述没写全也没关系——Agent 会先问你现象、期望与复现路径。
不识别对话意图、不做工具栏按钮。埋探针是在改你的代码库,这个动作必须由你显式发起,
不能由模型推断出来。dsh_debug_start 工具必须持有 /debug 签发的票据才放行——
无票调用一律被宿主拒绝(这是硬校验,不是提示词里的一句叮嘱)。
四个信号
| 信号 | 谁触发 | 做什么 |
|---|---|---|
| 启动调试 | /debug 之后由 Agent 调用 |
首次进入调试模式;生成唯一哈希,创建日志、探针桩与现成代码片段。同一任务的哈希此后不变 |
| 汇报埋点 | Agent | 交卷:记录分析结论与两份清单,然后把球交回给你复现 |
| 进一步调试 | Agent | 开下一轮:这时才清空日志(哈希不变)。注意顺序——用户点「复现」时日志是保留的,Agent 必须先将 dsh_debug_read 读它,再 continue |
| 读取日志 | Agent | 把日志内容以摘要形式输出给 Agent,无需 Agent 自己去翻文件 |
| 结束调试 | Agent(由用户「已解决」驱动) | 删除任务目录,并移除本次任务埋下的全部探针与临时块 |
调试期间的代码改动
调试不只是「埋点 → 读日志」——改代码本身也是调试手段,但轮次有讲究:
- 第 1 轮默认不改代码,只取证。没看到数据之前猜的修法一旦改掉原始行为,
后面的日志就再也证不出真相了。(闸门只在出现
dsh-debug:change标记时拒绝—— 埋探针本身不算改行为。) - 第 2 轮起可以「先按假设改,再埋点验证」,但改完必须再埋点,用日志证明 「改之前复现、改之后不复现」。
- 探针和修复分开记账:探针用
dsh-debug:probe <hash>包裹,临时代码用dsh-debug:temp,改动用dsh-debug:change <hash>标记。收尾时只删探针与临时块, 绝不回滚你的修复——整体git checkout回滚是被明令禁止的收尾方式。
埋点入口:任何能发 HTTP 的语言都一样
写日志这件事只有宿主一份实现。 探针不需要自己格式化时间戳、自己截断、自己数上限、
自己保证字段序——它只要把一条 JSON POST 过去:
# GET 是自检:先确认通道活着,再让用户去复现
curl -s 'http://127.0.0.1:<DSH 端口>/dsh-debug-mode/probe?h=<hash>'
# POST 写一条(只有 probe 是必填)
curl -s -X POST 'http://127.0.0.1:<DSH 端口>/dsh-debug-mode/probe?h=<hash>' \
-H 'content-type: text/plain' \
--data-binary '{"probe":"app.py:42:enter","data":{"order_id":"A1"}}'
路径是固定的:/dsh-debug-mode/probe,任务哈希走查询串 ?h=。
宿主地址与端口由插件自己算出来(DSH_WEB_URL,回退到 webServer.port),
所以你不需要告诉 Agent 地址——它拿到的就是一条可以直接用的完整 URL。
只有目标不在本机时(容器里、另一台机器上)才需要把 127.0.0.1 换成可达地址,
路径与哈希一个字都不要动。
dsh_debug_start 还会把端点与哈希已经填好的现成代码片段生成到
.dsh/debug-mode/tasks/<hash>/runtime/probe.http.md,覆盖 curl / Python / Node /
Ruby / Go / PHP / PowerShell / Java —— 复制粘贴就能用。
这条通道带来的两件事:
- 语言无关:字段序、带时区的时间戳、8 KiB 单行截断、16 MiB 总量上限、并发追加, 全部由宿主强制。所以不存在"某个语言的桩把协议实现错了"这种失败模式。
- 可自检:
GET一眼看出通道通不通。投递是尽力而为的(探针不许拖慢被调试的程序), 所以"通道是死的"与"埋点没执行到"的现象本来一模一样——现在它们可区分。
两条桩还会在加载时自己写一条 debug-mode:ready:
- 看得到
ready⇒ 桩是通的 ⇒ 日志里没有你的探针 = 那段代码没走到; - 看不到
ready⇒ 桩根本没被加载 ⇒ 去查 import 路径 /<script src>/ 进程重启过没有。
什么时候不走 HTTP
| 场合 | 用什么 |
|---|---|
| 热路径(每帧 / 每请求上千次) | HTTP + 调用方聚合(每 N 次或每 X ms 一条) |
| 进程可能硬崩,要求"崩溃前最后几条必须落盘" | Node 文件桩(同步追加,不缓冲) |
| 运行时没有网络栈 | 按指南 §六 自写文件桩 |
浏览器页面
如果被调试的是在浏览器里打开的 HTML 页面(尤其是 file:// 直接打开的静态页),
用宿主给的经典脚本桩,把记录 POST 回宿主(走 DSH 自己的 HTTP 载体,
所以你不需要为了调试去起一个本地服务器):
<script src=".dsh/debug-mode/tasks/<hash>/runtime/probe.browser.js"></script>
<script>
dshProbe('buggy-demo.html:42:onclick', { list: todos.length })
</script>
记录会按同一份行协议落进任务日志(task 由宿主写死,字段序只有一处真相)。
面板:输入框上方那张可折叠的卡片
从任务创建到任务结束,输入框正上方常驻一张小卡片。它在文档流里(不是浮层), 所以不会挤动输入框、也不可能挡住它;而且默认是收起来的—— 只有轮到你做决定时才自动展开:
▸ 调试模式 · #deadbeef · 第 2 轮 · Agent 正在埋点/排查 312 行 · 48 KB
轮到你了,它自己展开,并且只说两件事——结论一句话 + 你要做什么:
▾ 调试模式 · #deadbeef · 第 2 轮 · 轮到你复现并选择 312 行 · 48 KB
total 在 items 为空时算成了 null
请按这个顺序复现:
1. 打开保存对话框
2. 清空标题后点保存
3. 回到列表页
也可以不用按钮:直接在输入框里描述现象,同样算「还在」。
[ 问题已解决 ] [ 问题复现,需要进一步修复 ]
- 点头部那一行即可折叠/展开;折叠之后那一行仍然说清现在轮到谁。
- 那份复现清单是 Agent 交卷时给出来的——过去它不存在,你面对"请复现"只能凭记忆 猜一遍当初怎么触发的。
- 只有等你在面板上做过选择,Agent 才被允许收尾(
dsh_debug_end会校验这一点)。 - 输入框始终可用:想补充「只在切换标签页之后才复现」这种信息,直接打字就行。 输入框里有没发出去的内容时,两个按钮会自动禁用——免得同一条描述被送两遍。
- 宿主扫不动你的工作区时(只读沙箱等),面板会直接说**「残留检测未生效」**, 而不是假装还在保护你。
收尾会发生什么
选择「问题已解决」之后,宿主会:
- 按
dsh-debug:probe <hash>与dsh-debug:temp <hash>标记逐块摘除探针与 临时探索代码(摘之前先备份到任务目录的backups/,但绝不自动还原); - 删除任务目录(日志、状态、探针桩一起消失);
- 在回复里给出三类账:修复(保留)/ 探针(已删)/ 临时改动(已还原)。
两件它不会做的事:
- 不会回滚你的修复。
git checkout/git stash整体回滚是明令禁止的收尾方式。 - 不会静默保留。 如果扫到"既不是本次探针、也不能确认是修复"的残留 (陌生哈希、残缺标记、配对有歧义),收尾会暂停一次,把清单交回来, Agent 会逐条问你三选一:保留 / 删除(先看 diff)/ 我自己处理。
另外:Agent 可以不进调试
/debug 是你发起的,但它不是契约。如果 Agent 读完代码就看出根因(空指针、
明显拼写错误、缺一次 await……),它应当直接告诉你根因并改代码,
而不是为了走流程先埋一圈探针。票据会自己过期,你下次想调试时重新 /debug 即可。
状态
v0.0.1 · M0–M6 + §16 的 R1–R6 + HTTP 探针通道全部落地,77 个测试全绿:
/debug 与票据闸门、5 个信号工具、日志管道与摘要、唤醒闭环、交互面板、收尾对账、
防堆屎山机制(R1 工作区索引 + 窗口 diff、残留扫描与隔离区、停止规则、欠账看板)
全部就位;浏览器/远程运行时的 HTTP 探针通道也已补齐,整条流程不依赖任何命令。
唯一还没做的是人在真实 GUI 里点一次面板——那一步只有你能做。 详见 DESIGN.md §14 路线图与 §15 待验证清单(10 项里 9 项已闭合, 核实依据都写在那一节)。
先读 DESIGN.md(设计冻结稿)与 AGENTS.md(工程纪律)。
安装
# npm 安装(发布后)
dsh plugin --profile web add dsh-plugin-debug-mode
# 本地开发
dsh plugin --profile web add link:<本目录绝对路径>
安装后在任意会话里敲 /debug <现象描述> 即可——命令会自动出现在宿主的命令面板里。
开发
node scripts/build-client.mjs # 生成 lib/client.js(改了 src/client.js 必须重跑)
node --test test/*.test.mjs # 全部单测(零依赖,会先自动重建客户端产物)
零运行时依赖:插件本体只用 Node 内置模块,因为探针桩会被复制进你的项目里执行。
浏览器半也只有一份手写的 src/client.js,由一个 30 行的构建脚本包成 DSH 要求的
window.__ModuleLoader__.load({ id, factory }) 形式——没有打包器,也没有 node_modules。
设计要点
- 日志是 JSONL,字段序固定(
ts, task, probe, level, msg, data)—— 追加写天然多进程安全,崩溃最多丢半行,grep依然好使。 - 探针桩永不抛异常、不持有 fd:可观测性代码不许把被调试的程序搞挂。
单任务日志到 16 MiB 就停止追加(并写一条
debug-mode:overflow), 免得一个跑飞的循环把磁盘写满。 - 探针代码用
dsh-debug:probe <hash> start/end包裹,可被机器整块摘除; 临时探索用dsh-debug:temp,修复用dsh-debug:change。收尾时宿主会拿 「工作区 diff 扫到的东西」和「你自报的清单」对账并点名漏网项—— 以扫描为准,不靠 Agent 自诚实。 - 调试日志永不长期保存:任务关闭即删除,
.dsh/自动 gitignore。 - 面板常驻在输入框上方(
conversation.input.dock,文档流兄弟节点, 不会被浮层遮住输入框),按sessionId归属,不会串到别的会话去; 默认折叠,只在轮到你复现时自动展开。 - 调试期间输入框始终可用:按钮只能表达「还在 / 好了」,而「只在切换标签页之后才复现」 这种信息只有打字能说清。输入框里有没发出去的内容时,面板按钮会自动禁用, 免得同一条描述被送两遍。
- 闸门不会把人卡死:任何硬闸门连续拒绝同一原因 3 次后自动降级为警告并把降级 写进面板;闸门依赖的扫描能力不可用时,面板直接显示「残留检测未生效」。
License
MIT
No comments yet. Be the first to write one.