DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

Ezisup /

Ezisup/dsh-stop-kills-jobs

Verified

在点击DSH停止时, 不只是停止会话输出而包含了停止后台终端运行

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

dsh-stop-kills-jobs

按下对话的停止按钮时,除了中断当前回复,还会结束这一轮发起的后台任务。

解决的问题

后台任务(run_in_background: true 的 bash、后台子代理)拿到 job id 之后, 就不再跟随对话的中断信号了。这是 DSH 的明确设计 —— dsh-tool-bash 自己的模块文档写着:

Background calls register process handles with ctx.jobs; their work uses job cancellation rather than the tool-call signal after an id is returned.

代码里这个对比很直白:

前台命令 后台命令
调用 ctx.shell.run({... signal: exec.signal}) jobs.start({... owner: exec.agent ...})
按停止 ✅ 传了 signal,会被杀掉 ❌ 没有传 signal

后果就是:你按了停止,任务还在跑;跑完之后它的完成通知会唤醒模型, 为一次你已经取消的对话多花一次请求 —— 而你还得再点一次停止。

这个插件把这个缺口补上:它监听 UI 同样在监听的 turn/end 事件, 认出「用户按了停止」这个原因,然后取消该会话自己的后台任务。

安装

dsh plugin --profile web add link:/xxx/dsh-stop-kills-jobs

装完重启 DSH 生效(客户端部分需要重新加载)。

设置

设置位置:设置 → 插件 → 插件配置,和「插件市场」「Shell」「Agent loop」等并列。

这些开关确定后基本不会再改,所以做成插件配置里的一张卡片, 而不是占用设置栏顶层 —— 顶层要留给经常调整的项。 卡片默认折叠,只显示标题和一行说明,和旁边的 Shell / Agent loop 等保持一致, 点标题才展开开关,不占版面。折叠箭头用的是同一套 IconChevronDownOutline14 图标。

总开关联动:关闭总开关后,「停止时结束什么」整段隐藏(因为此时插件不做任何事), 但已保存的设置原样保留,重新开启即恢复 —— 不会因为关一下就被重置。

正文分两段,因为它们不是一类东西:

分段 内容 与 Stop 的关系
停止时结束什么 后台命令 / 后台子代理 受总开关管辖
插件自身的输出 显示提示 / 输出日志 无关,任何时候都生效

平铺成一张列表时,「显示提示」「输出日志」会被误读成也会随停止按钮被终止 —— 这是分段的原因。

卡片标题为「停止时结束后台任务」(英文界面显示 "End background jobs on Stop"):

开关 默认 说明
启用 开 总开关。关掉即完全恢复 DSH 原行为
后台命令(bash) 开 是否结束 run_in_background 的 bash 任务
后台子代理(subagent) 开 是否结束后台子代理
在对话中显示提示 开 在设置页显示最近结束的任务
输出详细日志 关 把每次判断写进服务端日志,排查问题用

卡片里没有长篇说明

正文只有开关本身,没有"为什么需要它"那类解释 —— 会装这个插件的人看名字就知道用途, 展开后应该直接看到可调项。改这个插件时不要再把说明加回去。

卡片是怎么进到「插件配置」的

那个标签页会遍历宿主已注册的 settings namespace,逐个配对同 key 的卡片:

const served = new Set(mirrored.view?.namespaces.map((v) => v.ns) ?? [])
entries().flatMap((e) => served.has(e.options.key) ? [e.options.key] : [])

所以 settings.plugin.item 注册时的 key 必须等于插件注册的 settings namespace(stop-kills-jobs)。key 写错不会报错,只是卡片静默消失 —— 排查时先看这一点。

作用范围:只影响当前会话发起的任务,不会碰其他会话。 子代理任务注册时用的是 owner: parent,所以它的 ownerSession 就是父会话 id —— 一个过滤条件就同时覆盖了「本会话 + 它派生的子代理」,不用走任何树。

实测结果

在独立实例上、用真实鼠标/键盘事件(CDP Input.dispatchMouseEvent)驱动真实 GUI 测得:

场景 Stop 后存活的后台进程 结论
插件启用 0 ✅ 任务被结束
插件禁用 1 ✅ 任务存活(恢复原行为)

两个基线都是 0,所以这个对照是可信的。

其他已验证项:

  • 配置接口返回正确的 schema 默认值
  • 设置页 5 个开关全部渲染,点击后持久化生效(verbose 往返 true→false→true)
  • 回归:正常跑完的回合不会误杀自己的后台任务
  • 回归:没有后台任务时按停止是干净的空操作,无报错

服务端日志(开启 verbose 时):

[stop-kills-jobs] watching for stopped turns
[stop-kills-jobs] kill bash-2 -> requested
[stop-kills-jobs] stopped 1 job(s) on user Stop: bash-2
[stop-kills-jobs] disabled; leaving jobs alone

实现要点(踩过的坑)

1. 作业访问是按「所有者身份」授权的

这是这个插件最容易写错的地方。第一版写的是 jobs.list(), 日志显示 total_jobs=0 —— 明明有任务在跑却看不见。

根因在 dsh-jobs-local 的隔离栅栏:

assertAccess(job, caller) {
    if (job.owner !== void 0 && job.owner.id !== caller?.id)
        throw new Error(`job ${job.id} belongs to another session`);
}

传 undefined 等于「无代理调用者」,永远匹配不上有主作业。 必须先用 ctx.agents.get(session.id) 拿到那个会话的 Agent 再传进去:

const caller = ctx.agents.get(sessionId)   // 这是授权凭据,不是查表便利
jobs.list(caller)
jobs.kill(job.id, caller, reason)

2. 识别「用户按停止」要精确匹配

reason?.kind === 'aborted' && reason?.reason?.kind === 'user'

只看 aborted 不够 —— 父代理中断、插件 hook 也都会 abort, 为那些情况取消用户的任务是意外行为。取消原因一共四种: 'user' / 'parent' / 'hook' / 'disposed',只有 'user' 是停止按钮。

3. ctx.logger 在这个部署里是黑洞

两个坑叠在一起:

  • ctx.logger 是可调用对象,要先传频道名:ctx.logger('name').warn(...)。 写成 ctx.logger.warn(...) 会拿到 undefined,被可选链静默吞掉。
  • 更关键的是:这个部署没有安装任何 logger exporter, 所以即使调用方式对了,ctx.logger 的输出也没有任何地方显示。

所以诊断信息走 process.stderr(带 [stop-kills-jobs] 前缀), 它能进服务端日志和启动它的终端。一个「verbose 开关写进虚空」的功能, 本身就和这个插件要消灭的静默失败是同一类问题。

4. 客户端插件必须声明 inject

client 半边如果用了 ctx.locale / ctx.slots 却没声明依赖:

failed to apply loader entry: cannot get property "locale" without inject

而且失败方式是整个界面渲染不出来(页面只有 "Failed to load plugins"), 不是只有这个插件坏掉。所以必须:

const inject = ["slots", "locale"];
exports.inject = inject;

5. 插件在工作区里,解析不到 @deepseek-ai/*

插件放在 ~/dsh_projects/ 下,在 $DSH_HOME/profiles 树之外, Node 向上找 node_modules 时永远走不到 DSH 在 ~/.dsh/profiles/node_modules 挂的 fallback。加载会直接失败:

Cannot find package '@deepseek-ai/schemastery'

运行 tools/link-runtime-deps.sh 只给实际用到的包建软链(不是整个 scope), 保持依赖集可见且最小。

pnpm install 之后需要重新跑这个脚本(如果软链被清掉)。

文件

路径 说明
lib/index.js host 半边:监听 turn/end、取消任务、配置路由
lib/client.js client 半边:设置页 UI
cordis.patch.yml 把插件插进 profile 层栈
tools/ 安装/自检脚本(WSL 用,避免 Windows PATH 破坏引号)
verify_plugin.py 端到端验证(配置 + 启用/禁用对照)
regression.py 回归验证
verify_result.json 验证结果记录

卸载

dsh plugin --profile web remove dsh-stop-kills-jobs

然后重启 DSH。

—/ 5

No ratings yet

Verified DSH bundle

Commit 3ccf336eab84

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