dsh-agent-control
DeepSeek Harness(DSH)插件:把两件破坏性、不可撤销的操作收进一个插件,用同一套确认与失败语义管理。
- 删除会话:把一个会话不留残留地移除——磁盘目录(两种 id 拼写)、工作区记账(含归档/置顶)、投影缓存记录,以及它派生的子代理会话(递归,同样清目录与缓存)。fork 出来的会话是独立对话,不受影响。子代理里只要有一个还驻留在内存里,整次删除都会拒绝、什么都不动
- 插件不碰的:别的插件自己存的数据(例如审批类插件的事件记录里可能提到这个会话 id)、按内容寻址共享的附件(
$DSH_HOME/attachments,可能被别的会话引用)
- 插件不碰的:别的插件自己存的数据(例如审批类插件的事件记录里可能提到这个会话 id)、按内容寻址共享的附件(
- 删除一轮对话:从模型可见的上下文里移除一轮已结束的对话(问题、回复与该轮的工具记录),会话本身、之后的轮次与日志都保留
- 做法与 DSH 自己的手动压缩同形:一笔独立压缩事务把那一轮的可见区间换成一句「这里原有一轮对话,已被用户删除」的提示,模型从此看不到原内容。只能在会话空闲时删(有一轮正在进行或正在压缩时会拒绝)。
- ⚠️ v1.0.0 的旧写法(空的
system/message墓碑)会让会话重启后打不开,已不再使用。
- 热重启:把「DSH 进程重启」这件事也收进同一个插件——模型可以调用
restart_harness(必须说明原因)让需要重启才生效的改动(例如宿主插件代码)落地,重启后这个会话自动继续;用户也可以在设置页点按钮重启。见下方「热重启」一节。
本插件只使用 DSH 的公开 API:不依赖内核的私有结构,也不照抄任何第三方插件的实现。这一点是刻意的——依赖私有结构的写法换一个 harness 版本就会静默失效(详见 AGENTS.md 第 4 节)。
两者的区别(最要紧的一条)
| 删除会话 | 删除一轮 | |
|---|---|---|
| 磁盘日志 | 永久删除 | 保留(日志是 append-only,不改写、不截断) |
| 模型上下文 | 会话整个消失 | 那一轮不再进入上下文 |
| 附件 | 随会话目录一起删 | 不会清理(见下方「已知限制」) |
| 可恢复 | ❌ 不可恢复 | ❌ 但内容仍在日志里 |
⚠️ 删除一轮不是安全删除。 它通过在日志末尾追加一笔压缩事务、把那一轮从模型可见面上替换掉来实现。被删内容仍然完整留在会话日志与附件存储里,只是不再进入模型上下文。任何「删了就没人能看到」的理解都是错的。
界面
- 会话行「…」菜单 → 「删除会话…」(侧栏)
- 每条已结束的回复旁 → 垃圾桶按钮(会话运行中时禁用)
- 两者都会弹出同一个确认框,必须勾选确认才能点「删除」
- 删掉的那一轮会在界面上被隐藏,让界面与模型看到的保持一致
- 设置 → Agent 控制 → 运行状态、阻塞提示、「重启 DSH」按钮与「最近一次重启」卡片,外加一个「关闭实例」按钮(见下)
热重启与关闭实例
模型可以在需要的时候重启宿主进程(例如它刚改了本插件的 host 端代码,而 host 模块不会热加载),重启在本轮结束后执行,这个会话随后自动继续——模型会收到一条「DSH 已热重启」的通知,里面带着它自己写下的原因与续作说明。
- 两个入口,同一套护栏:模型工具
restart_harness({ reason, resume_note? })与设置页的「重启 DSH」按钮走的是同一条路径(requestRestart)。 - 模型发起时默认要过审批:审批卡片里显示模型给的原因;被拒就什么都不做。可以在插件行配置里把
approval设成auto免审批(仍受限频与阻塞项约束)。 - 有别的会话在跑、或有后台任务在跑时会拒绝,并告诉模型「等哪个会话结束」。这是刻意的:重启会中断那些任务。
- ⚠️ 已知限制:宿主的作业列表按所有者隔离,插件看不到别的会话启动的后台任务,只能统计无主作业与调用者自己的作业。
- 限频:10 分钟内最多 3 次;同一个会话在 60 秒内再次请求会被拒绝(防模型死循环)。
- 停机时间约 6–10 秒,其中大部分是新进程启动的时间;重启期间页面会短暂不可用(不需要手动刷新,DSH 客户端会自己恢复)。
- 不做配置回滚:如果新进程因为配置或插件错误起不来,插件不会替你改回任何东西——它会如实报告失败,并在设置页给出新进程日志的文件名,剩下的要你自己处理。
- ⚠️ 如果你的实例是由桌面启动器(DSHL)拉起的:实测(v1.1.0)重启本身能成功,辅助进程也不会被杀,但启动器认不出重启后的新进程——它会从启动器的列表里消失,变成一个它管不到的孤儿进程(还活着、还在服务)。
- 原因:启动器按它自己 spawn 的那个子进程 PID 认实例(就绪判定读的是子进程 stdout 里的登录横幅),进程一退出它就记「已退出」;它只在自己启动时做一次「核验并恢复监控」,判据仍是那个进程的身份,所以新 PID 不会被认领。
- 后果与处置:启动器里看不到、也停不掉它(要从任务管理器结束进程);也别对同一个 profile 再点一次「启动」,那会变成两个实例争同一个端口。要恢复管理,只能先手动结束那个进程,再从启动器启动。
- 这条本插件修不了:需要启动器提供重启接口,而本插件按设计不依赖、也不改写启动器的私有状态(那个记录文件是不透明的加密格式,靠它等于把插件绑死在一个第三方实现上)。
- ⚠️ 端口暴露的风险同样适用:
POST /api/agent-control/restart走的是插件自建路由(没有 harness 鉴权),所以它额外做了 Origin/自定义头校验,避免被网页 CSRF。但如果你的webServer绑在0.0.0.0上,等于把「重启我的实例」暴露给整个网络——不要这么部署。
关闭实例
设置页同一页里还有一个「关闭 DSH」按钮:干净地停掉整个实例(ctx.appExit(0),与直接关窗口等价)。
- 它存在的理由:实例被启动器失去跟踪之后(见上面那条),启动器里既看不到也停不掉它——这个按钮就是那种情况下唯一的收尾入口。当然,任何时候都能用它停实例。
- 只有界面能触发:插件不提供对应的模型工具。关掉实例会让所有会话与后台任务一起中断,而且不会自动重启(不像热重启会续作),所以这个决定只能由人来做。
- 必须勾选确认,弹窗里会列出当前有几个会话 / 后台任务会被中断,并写明「不会自动重启」。
- 行为:接口先回
101(已经接受),然后进程退出;界面显示「DSH 正在关闭…」,随后是「实例已关闭,可以关掉这个页面」。要再用它,得从启动器或终端重新启动。 - 不会写坏日志:退出前会尽力把每个会话已经记录的事件刷到盘上(每个会话最多等 500 毫秒,刷不动也照常退出)。如果关的时候正好有一轮在跑,那一轮在日志里会是「未闭合」,打开会话时由 DSH 内核按「被中断」补齐——这正是关窗口/崩溃时的同一套恢复路径,不是损坏。
- 与热重启互斥:任一流程进行中时另一个按钮是禁用的。关闭不等正在跑的任务结束(它就是要立刻停),这一点与热重启刻意不同。
已知限制
- 不能删除仍处于活动状态的会话。DSH 没有公开的「把会话从内存 store 摘除」的 API(能摘除的 disposer 只交给会话的创建者),所以对一个活着的会话强删磁盘,只会留下「目录没了但会话还在列表里」的半删除状态。插件选择拒绝,而不是制造一个说不清的状态。
- ⚠️ 切换到别的会话并不会让它下线:Web 端打开过的会话会一直驻留在内存里,直到
dsh web退出。所以要删一个本次启动后打开过的会话,只能重启dsh web,重启后不要点开它,直接从侧栏「…」菜单删除。
- ⚠️ 切换到别的会话并不会让它下线:Web 端打开过的会话会一直驻留在内存里,直到
- 删除一轮不清理附件:内核当前没有公开的附件清理接口。被删轮次里的图片/文件仍留在
$DSH_HOME/attachments。 - 已压缩的轮次可能无法单独删除:如果那一轮与其它内容共用了一个压缩后的可见节点,或它的可见节点不再连续,插件会拒绝并说明原因,而不是删掉一半。
- HTTP 接口没有鉴权:插件自建的路由不走 harness 的鉴权链路——能访问到这个端口的人就能调用删除。默认只监听回环地址;如果你的
webServer配置成0.0.0.0,请自行评估。
安装
# 从已发布的版本分支安装
dsh plugin --profile <profile 名> add github:ventisyn/dsh-agent-control#0.1.1-alpha.1-v1.0.1
# 本地开发(改完重启即生效)
dsh plugin --profile <profile 名> add link:<本地 clone 路径>
⚠️ 如果 profile 里已经装了别的删除类插件,请先移除,否则界面上会出现两套删除按钮——同一批槽位只能有一个主人:
dsh plugin --profile <profile 名> remove <占用了同名槽位的插件>
安装或改动 host 端代码后需要重启 dsh web:已加载的 host 模块不会因为文件改动而重新导入(新增的路由会一直返回 401)。
排障
装好之后:
# 会话列表(同时也是「路由是否注册成功」的探针)
curl http://117.0.0.1:<端口>/api/agent-control/sessions
# 热重启的状态(也是辅助进程判断「新进程起来了没有」的探针)
curl http://117.0.0.1:<端口>/api/agent-control/restart/status
浏览器控制台里:
window.__dshAgentControl.usingNativePrimitives() // 用的是原生原语还是自带兜底
window.__dshAgentControl.id // 注册 id
接口一览:
GET /api/agent-control/sessions 会话列表
GET /api/agent-control/turns?sessionId= 某个会话里已被删除的轮次
POST /api/agent-control/session/delete { sessionId }
POST /api/agent-control/turn/delete { sessionId, assistantMessageId }
GET /api/agent-control/restart/status 热重启的状态(也是新进程的就绪探针)
POST /api/agent-control/restart { reason?, force? } → 101 { ok, restartId }
POST /api/agent-control/shutdown { }(可空体)→ 101 { ok } ← 停掉整个实例,只从界面触发
失败一律返回 { ok: false, error: { code, message } }。错误码含义:
| 码 | 含义 |
|---|---|
SESSION_LIVE |
会话本次启动后打开过、仍驻留在内存里:重启 dsh web 后不打开它直接删 |
TARGET_NOT_FOUND |
目标已经不存在 |
TURN_NOT_CLOSED |
那一轮还没结束 |
TURN_COMPACTED |
那一轮已被压缩或不再连续,无法单独删除 |
AGENT_BUSY |
会话的任务没有停下来,已放弃删除 |
DELETE_FAILED |
宿主拒绝了这次删除(消息里带原始原因) |
RESTART_BLOCKED |
别的会话或后台任务在忙(消息里列出是哪些) |
RESTART_IN_PROGRESS |
已经有一次重启在进行中 |
RESTART_RATE_LIMITED |
太频繁(10 分钟 3 次 / 同会话 60 秒冷却) |
RESTART_UNSUPPORTED |
这个部署做不到(取不到启动规格、没有 appExit、辅助脚本缺失…)——不会假装成功 |
RESTART_DENIED |
审批被拒,或这次请求没通过可信校验(Origin / 自定义头) |
RESTART_FORBIDDEN |
子代理发起重启:只有主会话可以重启宿主 |
SHUTDOWN_DENIED |
关闭请求没通过可信校验(Origin / 自定义头 / 浏览器鉴权) |
SHUTDOWN_UNSUPPORTED |
这个部署没有 appExit,插件无法请求退出(请从启动器或终端停止) |
SHUTDOWN_FAILED |
请求宿主退出时抛错(消息里带原始原因)——不会假装已经关掉 |
开发
npm test # 语法检查 + 117 项离线测试,不需要 DSH 进程,也不碰真实 profile
设计与取舍、与 DSH 版本的耦合点、改完的自检清单都在 AGENTS.md。
说明
- 非官方:这是第三方插件,与 DeepSeek 官方没有隶属或背书关系;文中出现的 DeepSeek、DeepSeek Harness、DSH 等名称仅用于说明它在什么环境里运行。
- 主要由 AI 编码代理编写:代码、测试与文档都是在与 AI 代理协作下产出的,提交历史里能直接看到。仓库里所有结论都尽量附上可复核的证据(日志、接口响应、诊断脚本输出),请按证据判断,不要按措辞判断。
- 本插件的实测记录(包括未验证项)都写在仓库内
docs/VERIFY-*.md里,请按那里的结论判断它的成熟度,不要只看本页的概览。 - 与 harness 版本的耦合点、改完的自检清单、以及踩过的坑都在 AGENTS.md。
许可
MIT
No comments yet. Be the first to write one.