dsh-deferred-approvals(非阻塞审批)
审批不再阻塞模型:需要用户授权的工具调用会立即得到“已挂起、未执行”的结果并记为一个待办
(编号如 #A1),模型同一回合继续做其它工作;你随时回来在输入框位置(原生审批样式)的
「待处理审批」卡片里允许 / 拒绝。允许过的调用重试时由宿主自动放行(一次性授权),
也可以选择「本对话允许同类」不再反复问。会话列表里对应对话旁会出现原生审批那样的橙色小点。
权限边界(关键)
审批是宿主(sandbox / approval 层)的权限,不是对话内容:
- 模型在
workspace-write沙箱下拿不到更宽的权限,只能发起sandbox_permissions升级请求; - 插件只接管原生本来就会发生的审批:是否要审批完全由原生策略/沙箱决定。会话策略是
never或原生不需要审批时,插件不介入、不制造审批(forceAskPolicy默认false,绝不擅自改会话策略); - 插件拦截该请求并把它挂起,给工具返回“未执行”,同时记录待办;
- 唯一的授权来源是用户在界面上点击(允许一次 / 本对话允许同类)。宿主据此记录 “工具 + 参数指纹”的一次性授权;授权与那次调用绑定、用掉即销、默认 5 分钟过期;
- 模型收到的只是一条宿主事件通知(
user/message但source.kind不是user,在 Chat 里 渲染为上下文行,不是用户气泡)。它只是让模型知道“可以重试了”,不是授权凭据; - 模型自己声明已获批准、或直接重试时,宿主没有匹配的授权就仍然把它重新挂起——
对话内容无法授予权限。重试时参数指纹必须匹配(
command等实际参数一致;justification/description/timeoutMs不参与指纹)。
它解决的问题
- 以前:审批请求一出现,整个 turn 停住,模型必须等你点了才继续;你离开几分钟,任务就卡几分钟。
- 现在:审批变成后台待办,模型继续跑其它子任务,或改用不需要提权的做法;你回来再批。
- 你批过的操作:宿主事件通知模型重试原调用,宿主按一次性授权放行(不再弹框)。
工作方式
- 拦截:在
approval/request瀑布最外层(prepend)拿到请求;台账记下工具名、参数指纹、 权限级别与理由,立刻给这次调用返回rejected(工具层显示“未执行”,下面第 2 步改写文案)。 - 改写结果:
tools/post-execute把结果文案换成“已挂起为待办 #A1,不要等待也不要反复重试; 没有别的事就结束本回合等宿主事件;只有已用其它方式完成才调用pending(action="cancel")撤销”。 - 模型继续:同一个 assistant 回合继续下一步,做其它工作。
- 用户决定(输入框位置的原生式卡片,会话列表橙色小点提示):
- 允许一次:宿主记一次性授权(指纹匹配、5 分钟内有效、用掉即销);通知模型重试;
- 本对话允许同类:宿主记一条会话规则(工具 + 权限级别),之后同类请求直接放行,本次已排队的同类待办一并批准;
- 拒绝 / 忽略:关掉待办;拒绝会带备注告诉模型换方式,忽略静默作废。
- 送达(含中断):宿主事件消息(
source.kind = 'deferred-approval')。模型空闲就agent.followup(开新一轮);模型在跑时——若本会话没有工具正在执行(正在长考),先agent.cancel({keepInbox})中断当前思考再投递,决定一做出立刻可见;有工具在跑则退回agent.steer(下一步边界送达),避免杀掉运行中的工具。会话不在线则排队到下次恢复。 - 决定台账:每次批准/拒绝/忽略/过期/撤销都会记一条“最近决定”;
pending的cancel/list据此给出确定答复(“已被用户批准——不要撤销,直接重试”),不再返回含糊的“可能已批准”。 待办刚被撤销/过期时用户又点了卡片上的按钮(迟到点击),决定仍会被尊重(迟到放行/拒绝)。 面板同时展示「最近已处理」(/state的decided字段),你刚点的决定如何处置一目了然。 重复点击同一按钮是幂等的:第二次起直接返回 ok,不重复授权、不重复打扰模型。decide请求带sessionId时校验待办归属,跨会话的误点会被拒绝(旧客户端不带则放行,平滑过渡)。 - 过期:待办默认 15 分钟过期,静默作废、不唤醒模型;
pending工具可查待办、最近决定和已放行规则。
安装
在工作区目录 plugins/dsh-deferred-approvals 下,通过 plugin_manager 的 install_bundle
以该绝对目录为 target 安装。安装后 Client 半会自动加载;Host 半的源码修改需要重启 DSH 才生效
(进程内已 import 的模块不会热替换,除非开启 HMR 源码监听)。
依赖:无。只用 node:crypto 与 ctx 服务(tools / approval / agents / systemPrompt,
可选 connection、typertGateway);Client 用 slots / locale / uiSession。
配置(写在 profile patch 的这一行 config 里,全部可选)
| 键 | 默认 | 含义 |
|---|---|---|
ttlMinutes |
15 |
待办有效期(分钟),到期自动作废 |
grantMinutes |
5 |
「允许一次」的一次性授权有效分钟数 |
softCapAway |
20 |
没有在线客户端时的待办上限;有客户端时不设限 |
forceAskPolicy |
false |
是否把显式设为 never 的根会话策略切回 ask。默认不动会话策略:原生不要求审批时插件也不介入 |
requireLiveClient |
true |
没有在线 Web 客户端时交回原生审批,不做挂起 |
excludeTools |
[] |
不接管这些工具的审批。默认空:连 plugin_manager(装/禁插件)也走挂起,批准后重试一次即可 |
interruptOnDecision |
true |
用户做出决定时,若模型正在长考(本会话无工具在执行),中断当前思考立即送达通知;否则决定要等到当前这步 LLM 流结束才可见,慢模型会长时间以为“没人批准”并误撤销待办。有工具在跑时不中断(退回下一步边界送达),设 false 可整体关闭 |
sweepMs |
20000 |
过期清理间隔 |
- id: deferred-approvals
config:
ttlMinutes: 15
softCapAway: 20
未导出 Config schema,所以配置不做加载期校验;缺省值见上表。
已知限制
- 待办只在 Host 内存里:DSH 重启后丢失(审批本来就是一次性的;已执行的结果仍在会话日志里)。
- 需要
ask策略 + 在线 Web 客户端:策略为never时审批服务在瀑布之前就拒绝,插件收不到, 也就是原生不要求审批时插件不介入;插件默认不修改会话策略,不改子代理。 - 重试匹配依赖参数指纹:
command等实际参数一致才算同一次调用;模型改了命令会重新挂起(这正是 权限边界的体现,不是缺陷)。 - 批准后不自动执行:由模型核对后重试原调用(保证工具调用仍出现在会话日志与 UI 里);若模型已用其它 方式完成,它会按提示撤销授权、不重复执行。
- 中断只发生在“纯长考”时:本会话有工具在执行时不中断(避免杀掉运行中的命令),决定退回到 下一步边界送达;被中断的思考会以 interrupted 消息保留在历史里。
- 不接管的问题:
ask_user_question仍是原生的定时模式(见 DSH 的tool-ask-user配置)。 - 原生审批审计只为“挂起”记一条 rejected;真正的批准记录在插件内存(v1 未落盘)。
验证状态
- Host:语法、插件行激活、
pending工具注册、/api/deferred-approvals/*路由注册;挂起→批准→一次性放行 的链路用 Node mock 全覆盖测试通过(含“换 description 重试不会放行”的负例)。 - 回归测试在仓库里:
node test/run.mjs(零依赖,Node ≥18,退出码 0 = 全过)。当前 38 项断言: 挂起改写、决定送达三分支(中断/steer/followup)、指纹放行、决定台账确定答复、迟到放行与迟到拒绝、 双击幂等、跨会话 sessionId 拒绝、/state暴露decided、同类规则批量批准与自动放行。 - 2026-11-05 增量:长考时批准→中断思考立即送达(
interruptOnDecision,默认开)、有工具执行时不中断 退回 steer、pending cancel对已批准待办返回“不要撤销”、撤销/过期后的迟到批准与迟到拒绝(含撤销 既有放行)、idle 直接 followup、list 展示最近决定。 - Client:
conversation.composer插槽已注册(priority 2)、旧浮层已移除、会话状态点复用原生approval状态;视觉与交互以实际页面为准(本机无浏览器自动化)。 - 待重启验证:宿主事件通知渲染为上下文行(而非用户气泡)、侧边栏橙色小点、输入框位置卡片。
定时提问(原 dsh-timed-questions,已合并)
同一个 bundle 还把 standard 预设里的 ask_user_question 切成 timed 模式:
- 模型提问后最多等
timeout秒(默认 20);超时返回{pending:true, callId},模型继续干活; - 问题卡片保留,随时可答;回答作为用户消息送回模型(在跑就下一步收到,空闲就开新一轮);
- 单次调用可用
timeout覆盖默认值,-1表示这次必须等到回答; - 这是 DSH 内置能力(
@deepseek-ai/dsh-tool-ask-user的mode: timed),本包只负责配置到预设。
生效范围:新会话。已存在的会话沿用启动时的预设组合,需要新开一个对话。
升级 DSH 后
cordis.patch.yml 是对 standard 预设的整份覆盖(patch 的 config 是整块替换),DSH 升级后这份
副本可能过时。插件启动时会自动对比官方预设与本地副本,发现漂移就重新生成(保留 mode: timed
与 timeout),并提示需要重启后新会话才生效;也可手动:
node check-preset-drift.mjs --regenerate
卸载
plugin_manager 的 remove_bundle(target 为 dsh-deferred-approvals):审批与定时提问一起恢复原生行为。
No comments yet. Be the first to write one.