dsh-approval-inbox
把「等你点头」变成「排队等你点头」:Agent 遇到要授权的步骤不再卡住,而是把请求丢进收件箱、自己继续做别的;你什么时候有空什么时候批,批完它自己接着跑。
English · 安装 · 快速上手 · 规则 · 已知问题 · 工作原理
背景
DeepSeek Harness 里有两件事会白白吃掉时间:
- 审批阻塞。Agent 跑到一个需要你确认的调用(删目录、强推、写系统路径),会停下来等你。你不在屏幕前,整条流水线就闲置——哪怕它手上有五个不需要授权的活可以干。
- 算力错峰。DeepSeek 按峰谷价计费(工作日 09:00–12:00、14:00–18:00 北京时间约为低谷价 2 倍,对应 UTC 01:00–04:00、06:00–10:00)。峰时停下来最省钱的假设只在"所有活都吃 token"时成立;本地测试、构建、数值验证这些占时间但不烧 token 的活,峰时照做才对。
这个插件同时处理这两件:
- 用
tools/pre-execute把待授权调用拦下入队并拒绝当前 step,模型收到「已排队、继续别的」后接着干下一个 step; - 把队列摊在左侧一个常驻面板里,按项目 / 会话分区,会话行上挂数字角标;
- 你批准后经
schedule.create投递重试消息,并留一张一次性凭据,它重试同一调用时放行一次; - 峰时批准会把投递排到谷时边界,吃算力的调用(
task/web_search/image_gen…)在峰时自动压后,谷时自己执行。
功能
不阻塞
- 🚦 命中规则的调用返回
deny而不是ask:当前 step 立刻结束,不产生任何等待中的 Promise - 📥 入队后立刻可见,模型在同一回合里继续做别的事,不需要你介入
- 🔁 批准 → 投递重试 → 凭据放行一次 → 再调再拦,闭环有测试守着
收件箱 UI
- 📌 左侧导航行「决策收件箱」:常驻、不可关闭,关掉右侧标签也不会失去入口
- 🗂 全局面板:项目 → 会话两级分区,逐条批准 / 拒绝 / 总是允许 / 批量批准,可批注
- 📄 右侧栏「待批阅」标签:只看当前会话,已注册到侧栏引导页
- 🔢 会话行角标:在转圈的同时显示黄色数字(
1–99,超过显示99+),颜色继承官方--dsw-alias-state-warn-* - 🈶 中英双语,跟随界面语言
规则
- 🎚 三种动作:
allow放行、defer入队、deny直接拒绝(并告诉模型别再试) - 🧩 十一个条件:
tool/kind/scope/project/session/contains/notContains/regex/notRegex/path/minRisk - 📦 规则集是普通 JSON:面板内导出下载、粘贴导入(合并或替换)、单条试跑告诉你哪条会赢
- 🛡 出厂 7 条只拦不可逆或离开本机的事,只读与本地构建一律放行
峰谷联动(配 dsh-peak-guard 或独立工作)
- 🌗 读 peak-guard 的阶段快照;没装也能用内置日历(
chinese-days+ 官方峰时窗口) - ⏳ 峰时批准 → 投递排到谷时边界,重试那一轮按低谷价
- 🤖 峰时对吃算力的工具族自动压后,谷时自动批准投递,不占你一次点击
外部接入
- 🧾
POST /raise与文件投递箱~/.dsh/dsh-approval-inbox/inbox/:任何进程都能登记一条待决 - 🤝 与
dsh-superagent联动:工单进入blocked或向人提问时,事件自动进收件箱
环境要求
- DeepSeek Harness >= 0.2.1-rc.2(桌面客户端或带宿主服务的 web 部署)
- Node.js
^22.19.0 || >=24.0.0 - 宿主进程保持运行(插件跑在宿主里;关掉 DSH 就没人拦、没人投递)
- 可选:
dsh-peak-guard>= 0.3.2(只为拿到"手动停过"这个状态;没有则自算)
安装
从 GitHub 安装(推荐)
dsh plugin --profile <profile> add "git+https://github.com/Aily-DSH/dsh-approval-inbox.git"
dsh 不在 PATH 时用客户端自带的 CLI:
cd "/d/Program Files/DeepSeek Harness/resources/runtime/cli/bin"
./dsh.cmd plugin --profile desktop add "git+https://github.com/Aily-DSH/dsh-approval-inbox.git"
desktop profile 的特殊情况
桌面客户端的 desktop profile 由 Electron 独占管理,CLI 会直接拒绝写入。两种可行方式:
- 客户端内安装:设置 → 插件 → 从 Git 地址安装(走 GUI 的同一套逻辑)。
- 手动装(本仓库开发时就是这么装的,已验证):
cd ~/.dsh/profiles/desktop
node -e "
const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));
p.dependencies['dsh-approval-inbox']='file:D:/DSH/plugins/dsh-approval-inbox';
const b=p.dsh.profile.bundles;
if(!b.includes('dsh-approval-inbox')){const i=b.indexOf('dsh-peak-guard');b.splice(i>=0?i+1:b.length,0,'dsh-approval-inbox');}
fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n');"
npx --yes pnpm@11 install
file:依赖在版本号没变时不会刷新副本。改过源码就把node_modules/dsh-approval-inbox删掉再 install,或者 bump 版本号。
本地开发安装
dsh plugin --profile <profile> add "file:D:/path/to/dsh-approval-inbox"
装完必须重启
宿主半(lib/index.js)只在启动时挂载,客户端半的注入清单也只在启动时计算。不重启不会生效。
快速上手
重启后看左侧导航:多了一行 决策收件箱。点开,面板顶部会显示当前是峰时还是谷时、下次切换的时间。
发一个会命中规则的请求:
请执行 rm -rf ./dsh-selftest-tmp,然后告诉我结果。期望:没有弹窗;面板里多出一条
bash待批;会话没有卡住,模型在拒绝理由之后继续说话。点 批准。模型收到一条「可以重试」的消息,重试同一个调用时放行一次;再跑一次又会重新排队。
想静音某一类操作:点 总是允许,它会提升成一条
allow规则(在「规则」区能看到并随时删掉)。
界面
| 位置 | 作用 | 会不会消失 |
|---|---|---|
| 左侧导航行「决策收件箱」 | 常驻入口,点击打开全局面板;有待批时图标带数字 | 不会,这是 sidebar.panellist 席位,没有关闭按钮 |
| 全局面板 | 项目 → 会话分区;批准 / 拒绝 / 总是允许 / 批量;规则编辑与导入导出 | 不会,由左侧行唤起 |
| 右侧栏「待批阅」标签 | 只看当前会话 | 会:关掉标签后从面板「去会话」或侧栏引导页重新打开 |
| 会话行角标 | 该会话的待批数量,转圈时也显示 | 跟随会话列表渲染 |
角标的数字含义:这个会话里有几件事在等你。它和「需要决策」的黄色标记同一套色板,但位置在行尾(官方 session.row.leading 席位只在 idle 时渲染,满足不了"转圈同时显示")。
规则
规则是一个有序列表,先匹配先生效。每条规则一个动作:
| 动作 | 结果 | 用在哪 |
|---|---|---|
allow |
直接放行 | 只读、本地构建、你信得过的常规操作 |
defer |
入队,当前 step 被拒绝,模型被要求继续别的 | 不可逆、离开本机、需要你判断的操作 |
deny |
直接拒绝并告诉模型不要重试 | 明确禁止的事(比如往生产库写数据) |
条件字段:
| 字段 | 类型 | 说明 |
|---|---|---|
tool |
string[] |
工具名,按 glob 匹配(* 不跨分隔符),如 bash、task* |
kind |
string[] |
tool / question / blocked |
scope |
string |
any(默认)/ main(你直接对话的会话)/ worker(子 Agent) |
project |
string |
项目(工作区)名,glob |
session |
string |
会话 id,glob |
contains |
string[] |
参数里出现任一子串才匹配(大小写不敏感) |
notContains |
string[] |
出现任一子串就排除(用来给 allow 规则开例外) |
regex |
string |
对「工具名 + 全部参数字符串」做正则匹配 |
notRegex |
string |
命中就排除 |
path |
string[] |
参数里的路径做 glob 匹配,* 不过分隔符、** 穿过;正反斜杠等价 |
minRisk |
number |
风险分下限(0–3,按命令内容估算) |
出厂规则:
| id | 名称 | 动作 | 拦什么 |
|---|---|---|---|
allow-workspace-writes |
工作区内读写放行 | allow | 读文件、写文件、列目录、glob、grep、改 todo |
allow-local-build |
本地构建与测试放行 | allow | pnpm / npm / node / tsc / vitest / cargo / go / python / git status,add,commit 等 |
defer-force-push |
强推与改写历史 | defer | git push --force、git reset --hard、git clean -f |
defer-destructive |
删除与格式化 | defer | rm -r、rmdir /s、del /s、format、mkfs、Remove-Item -Recurse |
defer-outside-workspace |
写系统目录或用户目录 | defer | C:\Windows\**、C:\Program Files*\**、**/AppData/Roaming/**、**/.dsh/** |
defer-publish |
发布与对外动作 | defer | npm publish、gh release、docker push、vercel --prod |
defer-questions |
需要人回答的问题 | defer | kind = question / blocked 的任何调用 |
导出与导入都是 JSON:
{ "format": 1, "rules": [ { "id": "my-rule", "name": "写生产库", "action": "deny", "order": 5,
"match": { "tool": ["bash"], "regex": "prod\\.db" }, "reason": "生产库禁止直接操作" } ] }
面板内「规则与导入导出」可以下载、粘贴导入(merge 按 id 覆盖 / replace 整体替换)、并对一条假想调用试跑,告诉你哪条规则会赢。
完整说明见 docs/RULES.md。
配置
配置文件 ~/.dsh/dsh-approval-inbox/config.json(首次启动自动生成;面板里能改大部分)。
| 字段 | 默认 | 说明 |
|---|---|---|
enabled |
true |
总开关;关掉后 UI 仍在,但不拦任何调用 |
nativeApproval |
rule |
原生审批怎么答:rule 按规则、allow 全放、defer 全入队 |
grantTtlMinutes |
30 |
一次性凭据的有效期(分钟) |
grantScope |
exact |
exact 要求参数完全相同;tool 放宽到同会话同工具 |
maxQueue |
400 |
队列容量(50–5000),超出先丢最旧的已关闭条目 |
expireHours |
168 |
待决条目超过多少小时自动过期 |
deferRiskAt |
0 |
没有规则命中但风险分达到此值时也入队;0 关闭 |
questionTools |
内置 6 个 | 哪些工具名算「Agent 在问你」 |
peak.hold |
true |
峰时是否把批准压到谷时 |
peak.deferTools |
task subagent web_search image_gen video_gen |
峰时自动压后的工具族 |
peak.maxHoldHours |
22 |
单次压后最多表达多少小时 |
rules |
7 条出厂规则 | 规则列表 |
HTTP 接口
宿主只监听回环地址。token 与 GUI 相同(?token=…)。
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /api/dsh-approval-inbox/state |
全量状态(配置、队列、计数、阶段) |
| GET | /api/dsh-approval-inbox/counts |
只取角标计数,供 UI 轮询 |
| POST | /api/dsh-approval-inbox/decide |
{ id, decision: approve|deny, note, always, now } |
| POST | /api/dsh-approval-inbox/decide-batch |
{ ids, decision } |
| POST | /api/dsh-approval-inbox/raise |
外部登记:{ sessionId, kind, title, detail } |
| GET | /api/dsh-approval-inbox/rules/export |
下载规则 JSON |
| POST | /api/dsh-approval-inbox/rules/import |
{ json, mode: merge|replace } |
| POST | /api/dsh-approval-inbox/rules/test |
试跑一条匹配 |
| POST | /api/dsh-approval-inbox/config |
局部配置更新 |
不想猜端口就往文件投递箱写:~/.dsh/dsh-approval-inbox/inbox/*.json,tick(30 秒)消费后删除。字段同名,另有 dedupeKey 去重。
工作原理
一次工具调用
│
├─ rules allow ───────────────────────────────▶ 正常执行
│
└─ rules defer
├─ 写入 queue.json(原子替换 + 上一代 .bak)
├─ 返回 { kind:'deny', reason:'已提交人工审批 #xxxx;先继续别的,稍后会通知' }
│ └─ 当前 step 结束 → 模型读完理由 → 下一 step 继续干别的
│
└─ 你点「批准」
├─ schedule.create(sessionId, { prompt:'可以重试…', after_seconds })
│ └─ 峰时:after_seconds = 距谷时边界的秒数
└─ 记一张一次性凭据(session + tool + argsHash,TTL 30 分钟)
└─ 模型重试同一调用 → 凭据命中 → 放行并消耗
每 30 秒一次 tick 负责:过期清理、凭据清扫、峰时压后条目在谷时自动批准、消费文件投递箱。
为什么是 deny 而不是 ask:内核的 approval/request 只在未结束的回合内有效,且只支持 allowed-once,跨回合挂起会被当成崩溃尾巴。也就是说"挂起一个审批等你两小时后点"在内核层不存在;ask 会把等待塞进工具调用里,整个 step 卡死——正是要消除的症状。详见 docs/ARCHITECTURE.md。
峰谷联动
装了 dsh-peak-guard 0.3.2+ 时它会写 ~/.dsh/dsh-peak-guard/state.json,本插件按这个优先级取阶段:
- 该文件(5 分钟内有效)——唯一知道「你手动停过」的来源;
- 自己的同名规则(官方峰时窗口 +
chinese-days节假日 + peak-guard 的按日覆盖); - 都没有 → 视为谷时,不干预。
峰时的两个动作:
- 压后批准:投递
after_seconds = 距谷时边界的秒数,重试那一轮按低谷价计费; - 压后算力调用:
task/subagent/web_search/image_gen/video_gen直接入队并标autoRelease,谷时 tick 自动批准投递,不用你点。
配套改动:dsh-peak-guard 0.3.2 起每次 tick 写那份快照(纯投影,写失败即吞);不装也不影响本插件。
状态文件
~/.dsh/dsh-approval-inbox/
config.json 配置 + 规则
queue.json 队列 + 一次性凭据
queue.json.bak 上一次写入的前一代(损坏时自动回退)
inbox/*.json 外部投递箱,消费后删除
DSH_HOME 优先,所以隔离实例有自己的一套状态,不会碰到真机。
已知问题
诚实分成两栏:缺陷(还没修) 和 限制(设计使然)。带复现路径、初步定位和修复方向的详版在 docs/KNOWN-ISSUES.md。
缺陷 — 还没修
| # | 问题 | 影响 | 现在怎么规避 |
|---|---|---|---|
| K1 | 多实例并发写会互相覆盖。写盘已经是「先备份、再临时文件、最后 rename」的原子替换(不会写出半个 JSON),但没有跨进程锁:两个宿主(desktop + dsh web)同时运行时,后写的一方会用自己内存里的队列覆盖对方新增的条目 |
同时开两个实例时,新入队的某一两条可能丢;不会损坏文件 | 同一时间只开一个宿主;需要并行就用不同的 DSH_HOME |
| K2 | 角标依赖官方 DOM 结构。它靠 [data-row-key^="session:"] 找会话行,官方改版或换属性名就失效 |
表现是角标不显示;拦截、面板、投递全都不受影响 | 无(等官方提供能显示在 running 状态上的席位) |
| K3 | 原生审批看不到参数。approval/request 只带工具名,没有参数 |
普通会话的原生审批只有「按工具名」的规则能生效;其余交还原生应答方,因此仍会弹窗 | 想全部接管就把 nativeApproval 设成 allow 或 defer |
| K4 | 被拒绝后模型可能不重试而直接报 blocked。拒绝理由写了「先继续别的、稍后会通知」,但模型行为不完全可控 | 任务可能提前停下等你 | 实测调规则粒度;必要时把 deferRiskAt 打开放宽命中 |
| K5 | grantScope: tool 会放宽。凭据按「会话 + 工具名」放行一次,不校验参数 |
同会话里另一个同工具的调用可能吃掉这张凭据 | 保持默认 exact |
| K6 | 客户端 UI 没有逐屏人工验收。已验的是:bundle 进了 application 阶段、假 DOM 下真实 apply() 的注册与角标逻辑、43 个自动化用例 |
布局细节、极端窗口宽度、主题切换下可能有瑕疵 | 按 docs/SELFTEST.md C1–C6 走一遍 |
| K7 | 会话被删后投递失败。条目会停在 approved,stats.failed 加一 |
那条决定不会真的到达任何会话 | 面板里能看到,手动再点一次或删掉 |
| K8 | dsh plugin --profile desktop add 会被拒。desktop profile 由 Electron 独占 |
命令行装不上 | 用客户端插件页,或按安装里的手动方式 |
限制 — 设计使然
- 只能排到调用粒度。一个回合里「先设计、再测试」的顺序由模型自己决定,插件插不进去。峰时能做的是「压后吃算力的调用 + 放行本地活」,做不到"这个任务的设计步骤放谷时、测试步骤放峰时"。
- 批准是一次性的。想长期放行就用「总是允许」生成规则,或者调
grantTtlMinutes。 - 峰时压后依赖那次投递还在。会话被清空、进程重启都可能让
after_seconds的那条消息落空。 - 队列容量 400。超出先丢最旧的已关闭条目,历史会变短;待批条目不受影响。
enabled=false不是卸载。UI 还在,只是不拦;要彻底移除见卸载。- 投递至少 1 秒延迟(
schedule.create的下限)。
卸载
dsh plugin --profile <profile> remove dsh-approval-inbox
再删状态目录(可选,会丢掉待批与规则):
rm -rf ~/.dsh/dsh-approval-inbox
配套的上游改动不回滚也不会坏:peak-guard 多写一个没人看的小文件,superagent 往一个没人读的目录写 JSON。回滚步骤见 docs/ROLLBACK-AND-UPSTREAM.md。
目录结构
lib/index.js 宿主半:规则评估、拦截、队列、HTTP 路由、投递、tick
lib/client.js 客户端半:左侧常驻行、全局面板、右侧标签、会话行角标
lib/core/rules.js 规则规格化、评估、导入导出
lib/core/match.js glob / 正则 / 参数摘要 / 风险分
lib/core/queue.js 队列与凭据(纯内存 + persist 回调)
lib/core/peak.js 峰谷日历(读 peak-guard 快照,退化为自算)
lib/core/sessions.js 会话目录(标题、项目、worker 判定)
lib/core/config.js 配置默认值、补丁校验
cordis.patch.yml 把插件行插入 profile 的 cordis 配置
docs/ 架构、规则、验收、回滚、已知问题
test/ 43 个用例(纯逻辑 16 / 宿主 19 / 客户端 8)
scripts/publish.ps1 版本校验 → 测试 → 打包 → 提交 → 打 tag
开发
node --test test/core.test.mjs test/host.test.mjs test/client.test.mjs
- 纯逻辑:规则、队列、日历、配置边界,零依赖。
- 宿主半:假 Cordis 上下文 + 真实
tools/pre-execute处理器 + 真实队列文件 + 假schedule。覆盖完整闭环:拦下 → 批准 → 投递 → 凭据命中一次 → 再次拦下;以及写盘回退(把queue.json写坏,重挂载后从.bak恢复)。 - 客户端半:假
__ModuleLoader__/ React / DOM,跑真实apply(),验证席位名、选项形状、角标增删。
隔离运行(不碰真机状态):
DSH_HOME=D:\DSH\test-home dsh web --port 14399 --no-open
发布:
pwsh -File scripts/publish.ps1 -Version 0.1.1 -Push
赞赏
如果这个插件帮你省了时间,欢迎扫码请作者喝一杯 ☕(纯属自愿,不影响使用和更新)
生态与收录
GitHub topic:
dsh-plugin(DSH 社区约定的插件标记),本仓库另带dsh/deepseek-harness/plugin等 topics。Aily-DSH 插件市场索引:本插件登记在 Aily-DSH/dsh-plugin-index(format 1,每个版本 pin 一个 commit)。把索引地址加进 profile 就能在客户端内看到:
# ~/.dsh/profiles/desktop/cordis.patch.yml - id: plugin-market name: "@deepseek-ai/dsh-plugin-market" config: indexes: - https://cdn.jsdelivr.net/gh/Aily-DSH/dsh-plugin-index@main/index.json分类
agents-workflows,关键词approval/inbox/permissions/queue/superagent/peak-hours。社区目录:cccakeee/awesome-dsh-plugins 收录后可被 deepseekharnessplugins.com 等站点搜到。
版本可溯源:每个 release 打
vX.Y.Ztag,tag 信息里记着当时依赖的上游版本;CHANGELOG.md按版本记录行为变化。查不到时:npm / GitHub 搜索有索引延迟(1–3 天),直接用
dsh plugin add "git+https://github.com/Aily-DSH/dsh-approval-inbox.git"装。
English
dsh-approval-inbox turns blocking approvals into a background queue for DeepSeek Harness.
When the agent reaches a call that needs your approval, most setups stop the whole turn and wait. This plugin intercepts the call at tools/pre-execute, answers it with a deny carrying "already queued, keep going", and parks the request in an inbox. The agent continues with everything that does not need you; you approve whenever you get back, and a one-shot grant lets the retried call through exactly once.
The inbox is a permanent row in the left rail (not closable), a panel grouped by project → conversation, a right-sidebar tab scoped to the current conversation, and a numeric badge on each conversation row that shows up even while that conversation is busy. Rules are an ordered list with allow / defer / deny and nine match conditions; rule sets export and import as plain JSON.
Paired with dsh-peak-guard, approvals made during DeepSeek's peak-priced hours are delivered at the off-peak boundary, and compute-heavy tool families (task, web_search, image_gen, …) are held automatically until the price drops.
dsh plugin --profile <profile> add "git+https://github.com/Aily-DSH/dsh-approval-inbox.git"
# restart the DeepSeek Harness client, then look for 决策收件箱 in the left rail
Requires DeepSeek Harness >= 0.2.1-rc.2 and Node ^22.19.0 || >=24.0.0.
Known issues are listed in 已知问题: no cross-process lock on the queue (run one host at a time), the row badge depends on the client's DOM shape, native approval/request carries no arguments, and the UI has not been reviewed screen by screen in a real client.
No comments yet. Be the first to write one.