DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

wyk4447 /

wyk4447/dsh-diff-review

Verified

dsh的diff审核

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@03e5b522

dsh-diff-review

改动发生前先审查。模型要写、改、删文件时,右侧边栏弹出原文与新文的对比,你点「接受」它才真正落盘。

DeepSeek Harness 插件。零运行时依赖,不改宿主任何内置插件。

改动视图:统一 diff + 行内字符级高亮


它解决什么问题

模型直接改文件,你只能事后从对话里翻 diff,发现改错了还得自己回滚。这个插件把闸门挪到动手之前:

模型:我要把 greet.js 的第 4 行换掉
插件:↓ 暂停,不落盘
      右侧边栏显示 —— 第 4 行 红底删除、第 4~5 行 绿底新增
      改动到的那几个字再加一层深色
你:  [接受]  → 才真的写进去
      [拒绝]  → 文件一个字节都没动,模型收到拒绝原因

拒绝是真的没写,不是写完再回滚。

界面

改动视图(默认)

统一 diff,左右两列行号,+/- 标记,未改动处折叠成「⋯ 未改动的 N 行」。被改写的行除了整行底色,改动到的那几个字还会再加一层更深的底色。

改动视图,行内高亮

全文视图

原文 / 新文左右并排,行号跟文件真实行号走,改动行在左边距画彩色竖条(编辑器的 gutter 效果)。拖或滚任意一侧,另一侧的行与列同步跟动。

全文视图,双栏同步滚动

改成大段落的场景,左边删掉的整行和右边新增的整行在各自栏里对齐,一眼能看出哪几行是被替换掉的:

全文视图,整行替换时左右对照

命令级审查

命令要碰的文件或内容只有 shell 运行时才知道(iex $code、内容来自 $env:USERNAME)——面板给完整命令行,并明说为什么算不出 diff。

命令级审查

删除级审查

pwsh 里的 Remove-Item / rm / [IO.File]::Delete 这类删除命令,面板把将被删掉的整段原文逐行标红列出来,一并给出原始命令行——先看清楚要没的是什么,再决定。

删除级审查

文件越长,「要没的是什么」越该看全:整段原文逐行标红,滚到底才能点「接受」:

删除级审查,整段原文逐行标红

拦什么

操作 拦不拦 面板显示
write 新建 / 覆盖 拦 新增或修改,全文 +N -N
edit 改文件 拦 统一 diff / 全文双栏,标出改动行
pwsh 里的删除、覆盖命令 拦 命令原文 + 原文/新文 diff(能确定时)
read / glob / grep 等只读 不拦 —

pwsh 按「形状」识别,不靠命令名单

记住命令名单永远会漏。所以只要这条命令可能改写文件就先问你,判据是四类形状:

形状 例子
PowerShell 动词-名词约定 Set- New- Remove- Clear- Rename- Move- Copy- Add- Out- Tee- Write- Save- Export- Import- Compress- Expand- Restore-
.NET 成员调用 [System.IO.File]::Delete(...)、[IO.File]::WriteAllText(...)
把代码当字符串交给别的程序 cmd /c del、iex、powershell -EncodedCommand、node -e、python -c、bash -c
原地改文件的外部工具 git clean / git checkout -- / git reset / git stash、truncate、dd、shred、unlink

所以 Set-Item、New-Item、Tee-Object、Export-Csv、Rename-Item 这些没被逐个列出的命令同样会拦。只操作内存的 New-Object、Add-Type、Set-Variable、Clear-Host 显式放行。

另外覆盖:

  • 常见别名(ri/rm/del/erase/rd/rmdir/clc/cli/ren/rni/mi/mv/sc/ac/cpi/cp/tee/ni/si/sp/mkdir/md)与 > >> 2> 重定向
  • 脚本块里的命令同样算数:if (Test-Path $t) { Remove-Item $t -Force }、foreach (...) { ... }、& { ... } —— 藏在里面的删除不会漏
  • 带 -WhatIf 的干跑、只读管道、git status / git log / git diff 一律放行

「清空文件」不算删除

Clear-Content / clc / Clear-Item 不算删除:文件还在,只是内容没了,面板显示成「所有行被删掉」的 diff,全部允许 也照样放行。

真正让文件消失或换位置的才算:Remove-Item、del、rd、Rename-Item、Move-Item、Clear-RecycleBin。

能不能给我看 diff?

能算出新内容 → 给完整 diff

写入内容在命令里就写死了,且目标文件能读。所谓「写死」包括:

  • 引号里的字面文本:Set-Content -Path 'a.txt' -Value 'hello'、'hello' > 'a.txt'
  • 同一命令里先赋值再写出的变量:$t = 'hello'; Set-Content -Path a.txt -Value $t
  • .NET 调用的前两个实参:[System.IO.File]::WriteAllText('a.txt', 'hello')
  • $"..." 里的插值(引用的变量也确定时):$dir = 'D:\w'; $js = "line one`nline two"; Out-File "$dir/probe.js" -InputObject $js → 解析成真实路径与真实三行文本(`n `t 一并还原)
  • here-string:@'...'@ 原样、@"..."@ 插值。正文里的 |、&、花括号、引号都不会被当成命令分隔符

Add-Content / Out-File -Append 按追加算,新文件按新增算。

算不出来 → 只确认命令本身

路径或内容只能由 shell 运行时决定时,只在会删东西、或同时碰多个文件时才弹:

情形 举例
通配符 * ? [ ]
没被赋值的变量 $env:TEMP\x、$(...)、${...}
运行期才算出来的值 $t = 'a' + 'b'、Get-Date | Out-File f
属性 / 索引访问 $a.Length

一个已知文件、内容看不见的写入(Set-Content -Value $x、$js > file)直接放行 —— 让人审批自己看不见的字节是问不出答案的噪音。

真要弹时,面板给完整命令行并明说「命令操作,无法预知最终内容」,不会假装列出被删的文件,也不会把这种情况说成「改动过大」。

两个例外:

  • $env:NAME 能查:Remove-Item "$env:TEMP\a.txt" 会还原成真实路径 —— 落在临时目录的照放行,落在别处的照常问
  • 逗号并列多目标(Remove-Item 'a.txt','b.txt')各自独立解析
  • 文件名里本来就有的 $(如 a$b.txt)不算展开,仍按真实路径审查

什么不会拦

  • 只读工具
  • 系统临时目录(%TEMP% / %TMP% / /tmp)
  • 工作区下的 .dsh/ 整个目录(一次性脚本、导出、截图、插件状态都落那儿)
  • 插件自己的源码(否则插件改坏了就再也修不回来)

除此之外,只要落盘就一定会先问你。

面板细节

顶栏:操作徽章(新增 / 修改 / 删除 / 命令)+ 文件名与目录分两行(像编辑器的标签页)+ +N -N + 视图切换 + 拒绝 + 接受。

只有命令级能确认的改动(iex $code、git clean -fdx)根本没有文件名,此时徽章写「命令」、下面直接给命令原文,不会把动词当成文件名。

  • 多个待审项在左侧列表纵向排列,逐条接受或拒绝
  • 审查属于它所在的对话:待审队列进程内共享,但每个标签页只看、只决定自己对话的改动;(N) 也只数自己这个对话。别的对话的记录既看不到、也读不到 diff、也接受不了(服务端按 sessionId 校验归属,不靠前端过滤)
  • 字体:代码区 13px/20px,等宽栈 JetBrains Mono → Fira Code → Cascadia Code → Consolas,关闭连字(连字会把 !=、-> 画变形);正文系统无衬线
  • 配色:全部走宿主的 --dsw-alias-file-diff-* token,跟随亮/暗主题自动切换,不写死颜色
  • 空态:显示提示,并把闸门自检信息收进一张卡片

全部允许

懒得一条条点时按它:这个对话里后续写入不再询问,直到你再点一次关掉(重启 DSH 也恢复询问)。

  • 按对话记:这里打开不会替你放行别的对话,关掉也不会把别人的开关一起关掉
  • 按钮一直在顶栏,包括清空后的空面板(放行之后没待审项,那正是你会停留的页面,开关不能跟着消失)
  • 放行时变成「已全部允许 · 点此关闭」,随时能撤回
  • 按下去会顺手接受屏幕上那一条
  • 只有文件本身被拿走才照问:Remove-Item、del、rd、Rename-Item、Move-Item
  • 同一条命令里「写出临时文件 → 用掉 → 删掉」不算删除(那个文件本来就不存在,删它什么都没丢),整条直接过

安全阀

情形 行为 原因
侧边栏没打开 / 页面已关闭 放行 没人能审批时不能把 agent 卡死
你中断本轮(Esc) 取消当次调用 跟随 exec.signal
5 分钟没人理 放行 未答复不等于拒绝
插件自己的源码 放行 否则插件坏了就再也修不了(自锁)
临时目录 / 工作区 .dsh/ 放行 skipTemp / skipScratch(.dsh-tmp、.dshtmp 这类只是名字像的不在此列)
二进制 / 超大文件 降级为仅统计 读不出 UTF-8 时不假装能 diff
命令写入内容由 shell 决定 只碰一个已存在的文件就放行;删除、多目标、iex/node -e 仍照问 看不见的字节问不出答案;但删掉的东西拿不回来
一条命令同时碰多个文件 降级为仅确认命令 文件名本身就是这次审查要确认的东西
按了「全部允许」 本对话所有写入直接放行,直到手动关掉 文件被删除或改名仍照问;写出又删掉的临时文件不算

安装

dsh plugin add https://github.com/wyk4447/dsh-diff-review.git
# 或简写
dsh plugin add github:wyk4447/dsh-diff-review

装完在 profile 的 package.json 里是版本号 / 仓库形态,与其它插件一致。改代码后需重装一次。

Fork 了? 把上面两行的 wyk4447 换成你自己的用户名。

兼容的 DSH 版本:engines.dsh = >=0.2.1-rc.1 <0.3.0-0(整个 0.2.x)。DSH 导入前会比对这条范围(预发布版参与匹配),不匹配就拒绝加载整个插件、把它变成一条 disabled: true 的孤儿行。0.1.0 写的是 <0.2.2-0,升级到 DSH 0.2.2-rc.1 后就会被判不兼容;0.1.1 起放宽。

构建脚本:Git 托管的插件靠 prepare 脚本构建,pnpm 默认拦 allowBuilds。本插件零依赖、没有任何 prepare/install 脚本,正常不需要放行;只有你在这仓库里自己加了构建步骤时,才需要把 pnpm 打印的 key 写进 ~/.dsh/profiles/<profile>/pnpm-workspace.yaml 的 allowBuilds。

本地开发(软链)

// ~/.dsh/profiles/<profile>/package.json
// dependencies 里加:
"dsh-diff-review": "link:<你 clone 下来的绝对路径>"
// dsh.profile.bundles 里也加 "dsh-diff-review"

改 lib/*.js 需重启 DSH 才生效(profile bundle 随 Host 启动加载,没有热重载)。

配置

在 ~/.dsh/profiles/<profile>/cordis.patch.yml 里给对应条目加 config::

- id: diff-review
  name: dsh-diff-review
  config:
    files: true          # 拦 write/edit
    shell: true          # 拦 pwsh 里的破坏性命令
    skipTemp: true       # 跳过系统临时目录
    skipScratch: true    # 跳过工作区 .dsh 整个目录
    skipSelf: true       # 跳过插件自身(强烈建议保持 true)
    trace: true          # 写诊断日志到 ~/.dsh/logs/diff-review.log
    timeoutMs: 300000    # 文件审查超时(毫秒)
    shellTimeoutMs: 120000

所有键都可省略,缺省用上面的值。类型不符的键会被忽略(不做强制校验,插件零依赖)。

卸载

  1. 删 cordis.patch.yml 里 - id: diff-review 那段(若你手动加过)
  2. 从 profile package.json 的 dsh.profile.bundles 与 dependencies 里移除
  3. 重启 DSH

本插件没有关掉任何内置插件,也没有改写宿主的 disabled 列表,卸载不留副作用。

自测

两套离线测试,不连 GUI、不写真文件:

node test\gate.test.mjs     # 闸门:拦截 / 放行 / 接受 / 拒绝 / 中断 / 自我豁免 / 兜底
node test\render.test.cjs   # 面板:空态 / 改动视图 / 全文视图 / 滚动同步 / 命令级 / 全部允许 / 侧栏图标

别用 node --run test / npm test:Windows 上 Node 在含非 ASCII 字符的路径下会算成乱码路径而报「Can't find package.json」。package.json 里仍留了 scripts.test,放在纯 ASCII 路径下可用。

闸门测试覆盖的 237 项断言(点开)

not-armed 放行、布防后挂起、decide accept 放行、decide reject 拒绝且 reason 带路径、abort 取消、edit diff 两侧、pwsh 删除挂起与接受、pwsh 字面写入出真 diff(覆盖 / 追加 / 重定向 / 新建四种)、同一命令里赋值后写出的变量($t = 'one'; Set-Content -Value $t、反引号转义还原、$p = 'D:/w/a.txt'; Remove-Item $p 解析成真实文件、$dir 参与路径拼接、"hello $n" 插值)、运行期才算出来的值不再弹($t = 'a' + 'b'、$env:USERNAME、$js > a.txt)、按形状识别的改写命令(Set-Item/Rename-Item/New-Item/Tee-Object/Export-Csv/[IO.File]::Delete/cmd /c del/iex/Invoke-Expression/node -e/python -c/powershell -EncodedCommand/git clean/git checkout --/管道右端无目标)且参数是字面量时仍出真 diff、形状命中但没有文件名的条目(iex/git clean/管道右端)标记为命令级且不带路径(op === 'shell'、displayPath 为空,避免把动词当成文件名)、只碰一个已知文件且内容看不见的写入放行、-WhatIf/Get-ChildItem/Write-Host/New-Object/Add-Type/Clear-Host/git status/node --version 放行、通配与变量目标的命令级确认($p/"${target}"/$out)且文件名里的字面 $ 不被误判、脚本块里的删除(if (...) { ... }、foreach (...) { ... }、& { ... })、同一命令内赋值参与路径解析、逗号并列多个目标、同一文件被写两次时顶栏只出现一次而两个不同文件按顺序都出现、$env:TEMP 目标按临时目录放行、自我豁免、临时目录豁免、工作区 .dsh/ 整个目录豁免且不误豁免 .dsh-tmp/.dshtmp、header.cwd 缺失时用 workingDirectory 兜底、ctx.get 抛异常时仍能挂起、全部允许(同一轮里后续写入直接放行且诊断记为 allow-all、文件被删除或改名仍照问而 Clear-Content 直接过、turn/end 与新一轮提问都不动这个开关、关掉开关不需要有待审项、关掉之后的下一次写入重新挂起、按对话记——开在一个对话上不会放行另一个、关掉一个不会动另一个、不带对话的开关请求被拒绝 missing)、同一条命令里写出来又删掉的临时文件直接放行而删掉一个本来就存在的文件仍照问、一个对话的改动不会被另一个对话看到或决定(list 只回本会话、detail/decide 对别的会话返回 missing 且记录仍挂着)。

面板测试的 8 组覆盖(点开)

用 node:vm 跑真实的 lib/client.js,桩掉 React/DOM/fetch,共 8 组:empty diag、unified + full、scroll sync、command-level review、allow-all-switch、session-scoped title、guide artwork、client render smoke。

具体验证:空态六行诊断(最后一行「轮次」,宿主一次都没报过轮次时显示 -)、改动视图有 +/- 标记、改写行只有真正变化的那几个字被加深高亮(前缀不被误涂)、全文视图有双栏与 gutter 竖条、拖动一侧另一侧同步且不回弹、删除态第二栏显示「文件将被删除」、命令级审查不出空代码区且用「无法预知最终内容」而不是「改动过大」、命令级条目不再编造文件名(iex $code 的面板不显示任何文件名)、「全部允许」开关始终在顶栏、放行之后队列清空的那一屏也仍然在、一个对话的开关状态不会串到另一个对话、关掉后仍能找到入口、标签页计数只算本对话、侧栏「开始」页的画走 guide 条目自己的 icon(不是清单里的 icon.svg)。

排错

没被拦住时,先看日志,再看面板。

日志(推荐,不依赖打开侧栏)

trace: true(默认)时,每次闸门调用、挂起、裁决、路由拒绝都会追加一行到 ~/.dsh/logs/diff-review.log(Windows:C:\Users\<你>\.dsh\logs\diff-review.log)。

Get-Content ~\.dsh\logs\diff-review.log -Tail 30
# 只看某个工具
Select-String -Path ~\.dsh\logs\diff-review.log -Pattern '"tool":"write"'

日志只记状态变化:轮询心跳首次出现或间断后恢复各记一次(poller),不会每 1.2s 刷屏。没有 poller 行 = 浏览器半从没连上,闸门必然一直「未布防」。

典型序列:

... poller {"first":true,"gapMs":-1}                  ← 侧栏连上,闸门开始布防
... park {"tool":"write","path":"...","pending":1}    ← 拦下了,等你点按钮
... verdict {"id":"...","outcome":"accept"}           ← 你点了接受,随后工具真的执行

面板

打开右侧「改动审查」Tab,空态直接列诊断:

  • 闸门已检查 N 次 · 最近工具 X · 结果 Y —— Y 是放行原因,见下表
  • 布防 no · 心跳龄 -1ms —— 页面从没轮询过,即 Tab 没打开
  • 最近调用:write:not-armed → read:not-gated —— 按时间顺序看最近 12 次
结果 含义
not-armed 侧边栏没在轮询(Tab 没开 / bundle 没加载),正常放行
not-gated 工具不在拦截范围(如 read)
no-session 拿不到会话工作目录
temp-skipped / scratch-skipped / self-skipped 命中临时目录 / 工作区 .dsh/ / 插件自身豁免
identical / edit-no-match / edit-ambiguous 改动本身无效或歧义,交给宿主自己报错
shell-not-destructive pwsh 命令不含破坏性操作

实现要点

  • 闸门:tools/pre-execute 的 async waterfall。挂起时只 await 一个 Promise,同时监听 exec.signal,宿主等多久都不会 abandon 我们的 promise。
  • 通道:自建同源 HTTP 路由 /diff-review/api/{list,detail,decide},前面挂 ctx.connection.requestRejection 做浏览器信任校验。Electron 壳把 dsh-app://app/ 的请求转发到 Host 并自动带认证 cookie,所以浏览器侧同源可达;从 Node 直接探测会 403,属正常。
  • diff:自己算 LCS(Int32Array DP,超 400 万格降级为全删全增),零外部依赖。
  • 布防:侧边栏每 1.2s 轮询 list,该调用同时刷新心跳;心跳 180s 内有效才算「布防」,否则一律放行。窗口不可见时 Chromium 把 setInterval 压到约 60s 一跳,窗口必须留得比这个大,否则切走窗口就等于放行。
—/ 5

No ratings yet

Verified DSH bundle

Commit 03e5b5225172

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout