Zum Hauptinhalt springen
Z

dsh-approval-why

zhknb/dsh-approval-why

Adds a “why does this need escalation?” button to every DSH approval card: it opens a panel with the permission requested, the risk, what refusing means, the exact tool arguments and whether the named paths fall outside the session workspace; the card also keeps a one-line risk and refusal consequence visible under it, and the model’s original reason is split into labelled paragraphs instead of a run-on block.

Installation

dsh plugin --profile web add github:zhknb/dsh-approval-why

README

越权说明按钮(dsh-approval-why)

给 DSH 的工具审批卡片加上「可追问的中文说明」:卡片上方一个按钮、卡片下方两行常驻提示、底部一块未删改的原始理由。它不改变批准还是拒绝,只让「这次到底在要什么权限」在点按钮之前就说清楚。

它解决什么问题

审批卡上原先能读到的只有模型写的那段理由,而那段理由常常只有一句话——例如 create_skill 自带的理由就是「在 DSH_HOME/skills 目录下创建新技能」。为什么这算越权、出问题会怎样、拒绝之后会怎样,卡上没有;而审批期间模型正阻塞等你点按钮,也没法临时补一段解释。

插件因此不等模型,直接读这次调用自己的参数(会话事件里的 tool/call.arguments,一段 JSON),在浏览器里当场分析、当场渲染。

装上之后,你会看到三样东西

  1. 卡片上方:追问:为什么需要越权?(点开后变成「收起说明」)。它只在一张真的审批卡出现时出现,不影响批准/拒绝按钮,也不拦 Enter/Esc。
  2. 卡片下方:两行灰字——风险 … 与 不授权 …。这是给「理由只有一句话」的工具兜底的,不点开也能读到关键后果;如果模型理由里已经写了风险和拒绝后果,这两行会自动让位,不重复。
  3. 虚线框:模型理由的完整原文(不截断、保留换行、可滚动),按「需要的权限 / 风险 / 不授权」切段,每段一行灰色小标题。措辞一个字不改,只做分段和排版;末尾那句给纯文本界面用的「追问:回复『追问』…」会在这里被去掉。

点开按钮后,面板从上到下设四段:

  • 判断依据 —— 拿这次调用的真实参数和工作区根目录(event.data.meta.cwd)比对,说明「上面 N 个路径里有 M 个不在工作区内」;参数里每个路径后面都会标「(工作区外)/(工作区内)」。提权跟路径无关时(例如整条命令被 danger-full-access 放大),也会明说这一点。
  • 为什么需要越权 —— 按工具归类讲影响面,而不是套一句泛化的话。分类覆盖全部 44 个内置工具:本机命令、文件读写、插件安装、技能写入、定时任务、长期目标、子代理、联网、读取、上下文管理、运行时探查、界面、后台任务、Office 文件等;表里没有的新工具会明确标「未收录」并列出这次用到的参数字段。
  • 参数与动作 —— 真正要执行的工具名、参数原文(美化过的 JSON,转义的 \n 会还原成换行)、以及从参数里识别出的动作(删除、写入覆盖、启动外部程序、联网下载、改插件配置、改注册表)。
  • 理由只是自述 —— 一句提醒:卡片上的理由是模型写的(按固定三段式组织,难免像套话),参数才是系统真正要执行的指令;两者冲突时以参数为准。

它是怎么算出来的

全部在客户端本地完成,没有网络请求、没有额外进程:

  • resolveReason() 优先用宿主给的 displayReason(多语言),退回 reason,再退回空。
  • classifyTool() 先按精确表 TOOL_KINDS(44 个内置工具 → 命令/文件/技能/插件/定时/目标/子代理/联网/…)匹配,再按 office_* 前缀、参数里自报的 danger-full-access、动作里的联网特征兜底,最后落到 unknown(并显式说明「未收录」)。
  • inspectArguments() 用路径正则和动作正则从参数里抽「涉及哪些路径、做了什么」。
  • concreteWhy() 把抽出的路径与工作区根目录做小写前缀比较,得出这次越权的真实触发点。
  • readableArguments() 负责把 JSON 字符串里的 \\n、\\t 还原成真换行/制表符——参数里的理由是 JSON 转义过的,不还原就会显示成一整行。
  • parseReason() 负责把原文按小标题切段,并告诉渲染层「这份理由里有没有风险与拒绝后果」,据此决定卡下两行显示还是让位。

它明确不做什么

  • 不改判决:不自动允许、不自动拒绝,也不改写审批理由本身;按钮和面板都是只读的。
  • 不替换原生卡片:仍然调用 renderFactorySlot('conversation.approval.card', …) 画内置卡片,命令详情、按钮样式、快捷键都还是原生的。插件只在它上下加东西。
  • 不加新的审批门:Host 半(index.js)是空实现。create_skill 这类工具由它自己的 bundle 发起审批(@michengai/dsh-skills-manager 的 tools/pre-execute 守卫);试过在 tools/execute 上再补一道门,结果是同一张卡弹两遍,所以放弃了。
  • 不适用于纯文本界面:ACP(VS Code 等)没有客户端槽位,装了也不会显示。那种界面靠 AGENTS.md 里的文字规则,插件里的「追问」那句也正为它保留。

实现要点(给改代码的人)

  • 审批卡由 @deepseek-ai/dsh-client-ui-approval 在 conversation.composer 这个 chain 槽位里接管:chain 按 priority 升序尝试,第一个返回非空的选择器胜出。原生面板是 1,本插件是 0.9,命中后复用原生卡片。
  • 组件拿到 pendingInteraction(PendingApproval:key / toolName / callId / reason / displayReason / answerable / answer()),用 key 作 React 的 key 重挂载,切换请求时展开状态自动复位。
  • 高度是三层有界的(面板 ≤ min(32vh, --dsh-composer-text-max-height),参数块 ≤ 120px,原文块 ≤ 150px),展开面板时原文块与卡下两行让位,避免把合成器容器撑破。
  • 样式只用宿主 CSS 变量(--dsw-alias-label-*、--dsw-alias-state-warn-secondary、--dsw-specific-input-major、--dsh-composer-side-clearance、--dsh-chat-content-width),跟随主题自动变色。

兼容性

  • 客户端平台:web(DSH 桌面端 / 网页端)。
  • 依赖(peer,由宿主提供,不随包安装):@deepseek-ai/dsh-client-ui-conversation、@deepseek-ai/dsh-client-ui-approval。
  • 无构建步骤:纯 ESM + React.createElement,React 18 由宿主提供。
  • 改完 client.js 必须刷新页面(F5)才生效;没有 HMR。

文件

文件作用
client.js浏览器半:文案字典、conversation.composer chain 条目、说明面板、卡下两行与原文分段
index.jsHost 半:空实现(create_skill 自带审批,重复加门会双卡),文件头注释保留了这段结论
cordis.patch.yml插入一行插件条目(id: approval-why)
locale/zh.json、locale/en.json插件管理页显示用的标题与描述
icon.svg插件图标

安装

三种方式任选,都用 plugin_manager 的 install_bundle(此操作会请求一次审批):

  1. tarball:target 填 dist/dsh-approval-why-1.0.8.tgz 的绝对路径(推荐,单文件)。
  2. zip:解压 dist/dsh-approval-why-1.0.8.zip,target 填解压出的目录绝对路径。
  3. 目录:target 填源码目录绝对路径(目录里要有 package.json)。

dist/安装说明.md 是给最终用户的中文快速上手(含 ACP 文字规则片段)。

安装后对该 profile 的所有会话生效,重启后仍在。装完刷新页面。

用目录(link)方式安装时注意:remove_bundle 可能顺着 junction 把源目录内容一起删掉。卸载前先备份,或直接用 tgz 安装(装的是副本,卸载不影响源码)。

卸载

plugin_manager 的 remove_bundle,target 填 dsh-approval-why(或插件列表里显示的包名)。也可以停用而不卸载:set_bundle 把 dsh-approval-why 设为 enabled: false。

排查

  • 按钮没出现:先确认页面已刷新;再确认当前确实是「越权审批」(卡片标题为「等待审批」,有拒绝 / 允许一次)。普通确认对话框不经过这个槽位。
  • 卡片显示成一坨带 \n\n 的原始 JSON:这是内置原生卡在渲染,说明插件此刻没加载(最常见的是「卸载 → 重装」之间那几秒,或页面还没刷新)。原生卡不还原理由里的 \n,也不加按钮。
  • 说明面板顶部被裁:面板高度是三层有界(见上),如果宿主主题改了合成器高度变量,适当下调 client.js 里的 maxHeight。
  • 文字是英文:跟随 DSH 当前界面语言,en 环境下显示英文文案。
  • DSH 启动失败、日志出现 duplicate factory registration / import failed:几乎都是同一个原因——client.js 顶部 window.__ModuleLoader__.load({ id: ... }) 的 id 与 package.json 的 name 不一致。客户端模块按包名查找,文件内声明的 id 才是真正的注册键;两者不同会注册失败并连带让 web boot 整体失败。改包名时必须同步改这里(1.0.0 踩过,1.0.1 已修正)。

版本记录

  • 1.0.8:重写 README 与插件页简介,按 1.0.7 的实际表现描述(无功能改动)。
  • 1.0.7:原文块按「需要的权限 / 风险 / 不授权」分段显示;模型理由里已经写了风险与拒绝后果时,不再重复渲染插件生成的那两行。
  • 1.0.6:对齐修复——按钮行、风险/不授权两行、原文块都与审批卡同宽居中(此前贴在窗口左缘)。
  • 1.0.5:卡片下方直接显示「风险」「不授权」两行短说明(不用点开面板),因为部分工具自带审批的理由只有一句话。
  • 1.0.4:撤掉 1.0.3 在 tools/execute 上加的审批门——create_skill 本来就会问一次,重复加门会让同一张卡弹两遍。Host 半回到空实现,功能全部由客户端半提供。
  • 1.0.3:(未单独分发)试过用 Host 半为 create_skill 兜底发起审批;风险说明补充「新建技能」的专门文案(该文案保留在 1.0.4 中)。
  • 1.0.2:风险说明覆盖全部内置工具(新增技能 / 定时任务 / 目标 / 子代理 / 联网 / 读取 / 上下文 / 探查 / 界面 / 后台任务 / Office 等类别),未收录工具显式标注并列出参数名。
  • 1.0.1:修复 client.js 的 loader id 与包名不一致导致的重启崩溃。
  • 1.0.0:首个可分发版本,改名 dsh-approval-why。

许可

MIT。

Ähnliche Plugins