Releases: kuyueliuhun-ctrl/dsh-command-queue
Release list
v0.2.0 — 浏览器队列面板:复刻原生队列样式,排队中的斜杠命令看得见
Changelog
v0.2.0 — 2026-09-18
新增浏览器队列面板:排队中的斜杠命令现在看得见了。
背景
v0.1.0 的排队是 host 侧的延迟执行,功能正确但排队期间没有任何可见提示 ——
UI 的「执行中…」卡片由会话事件 command/run 驱动,而它在 handler 调用前才写入,
我们正是卡在它之前。
排查后确认不能用 DSH 原生队列达成这件事,这是结构性的:
- 原生队列(agent inbox)的条目只能是
UserMessage(模型可见内容),
没有「命令项」这个类型; - 队列项在下一个 step 边界就被
claim()取走,那一刻 agent 仍在
running,compactNow照样报 busy —— 只是把错误推迟几十秒; - 若用官方
agent/pre-stepwaterfall 把标记从messages里摘掉(让模型看不见),
它此时已经出队,原生队列 UI 上随之消失。
即「一直待在原生队列里显示」与「等到 idle 才执行」在 /compact 上互斥。
这正是 DSH 作者写下 "Queue the command itself. Rejected." 的原因。
做法:自建队列 + 复刻原生队列样式
原生消息队列本身就是槽位 conversation.input.dock 上的一个 list 贡献
(id: "queue"、order: 20)。list 槽允许第三方携带自己的 id 追加条目,
于是本插件注册 id: "command-queue"、order: 30,在原生队列正下方并排渲染
自己的命令队列:
- 样式:CSS 取自原生 QueueDock 的 CSS 模块原文
(dsh-client-ui-conversation/lib/client.js:14142),类名前缀改为dshcq_,
设计变量沿用全局主题,深浅色下与原生队列一致;DOM 结构同样复刻
(dock > panel > header(可折叠计数) / ul.list > li.row,每行一个移除按钮)。 - 数据通道:
ctx.connection.fetch.register()注册两条已鉴权路由
/api/command-queue/state(GET 快照)与/api/command-queue/drop(POST 移除)。
它们由/api前缀处理器分发,因此先过requestRejection()的 Host/Origin
围栏与浏览器签名 cookie 校验。刻意不用裸ctx.webServer.register():
那条路径完全绕过鉴权,把 exact 路由注册在/api/*下还会遮蔽内建鉴权。 - client 每 1.5s 轮询一次;空队列渲染
null,零视觉占用。
⚠️ 从 v0.1.0 升级的注意点
dsh-client-modules 的 resolveMeta() 有一层 pkgMeta 负缓存
(dsh-client-modules/lib/index.js:652):包在没有 dsh.client 声明的形态下
被加载过一次,null 就会被缓存,之后即使补上 dsh.client 也读不到,
client 半边不会注册。
- 重启 host 即可(缓存冷启动,最省事);
- 不想重启时,清掉该进程里的
clientModules.pkgMeta里对应条目再调
processOne(包名)+compose()+notifyGraphChanged()即可(本次升级就是这样处理的)。 - 全新安装不受影响。
另外别忘了:客户端插件是页面加载时装配的,升级后需要刷新页面。
其它变更
- 队列条目改为带 id(
cq1、cq2…),支持从面板/接口移除一条在队命令
(被移除的命令不会执行,RPC 返回removed from the command queue)。 - 新增配置
exposeState(默认true);设为false即回到纯 host 形态。 /cmdqueue与浏览器面板读同一份队列。
兼容性与风险控制
- host 侧仅用到公开面:
ctx.commands.execute/find/register、agent.status、
agent.ctx.on('agent/status')、ctx.connection.fetch.register。 - 客户端 bundle 抛错会让整个 Web 应用无法 mount(DSH 的 boot 审计没有
per-plugin 隔离),因此:apply整体 try/catch、组件内网络/DOM 操作全自吞、
空队列返回null、React.useId缺失时降级为常量。 - 新增
tests/client-bundle.test.mjs:用最小 React 运行时 + 假
window.__ModuleLoader__把 bundle 真正加载并渲染,覆盖 bundle 形态
(id/factory/无 default)、dock 条目 id 与 order、异常自吞、空队列渲染null、
有队列时渲染命令行并经 drop 路由移除。 - 客户端插件是页面加载时装配的 —— 安装/注入后需要刷新页面才会出现面板。
验证
- 单元测试:
npm test→ 29/29 通过(host 22 + client 7)。 - 真实端到端(v0.1.0 已验,逻辑未变):非命令行 2ms 返回 200;
已注册命令在 agent running 时被挂起排队(8s 无响应)。
v0.1.0 — 2026-09-18
首个版本:让 /compact 这类斜杠命令在 agent 忙碌时不再立刻报错,
而是进入按 agent 串行的命令队列,等 agent 空闲后自动执行。
背景
DSH 里普通消息与斜杠命令走两条完全不同的通道:
| 普通消息 | 斜杠命令 | |
|---|---|---|
| 客户端 | session.prompt({ mode }) |
remote.commands.execute() |
| host 落点 | agent.followup() / agent.steer() → agent inbox |
ctx.commands.execute() → 直接调 handler |
| 忙碌时 | 天然排队 | 立即执行,无排队语义 |
/compact 的 handler 调 compactNow() → agent.runMaintenance(),后者的同步
gate 是 if (this.phase.kind !== 'idle') throw,被包成
ManualCompactionError('busy')。所以 agent 忙碌时敲 /compact 只会立刻收到
"Compaction is unavailable because this process has an active compaction, or the
agent is not idle."——既没排队,也没有任何重试。
DSH 作者把「命令自己进队列」当作有意排除的方案记录在案
(Agent Note: "Queue the command itself. Rejected."),因此官方没有开关。
本插件在不改 DSH 源码的前提下补上这一段。
新增
- 包装 host 侧
ctx.commands.execute():合法 + 已注册 + 未豁免 + agent 正忙的
调用进入该 agent 的 FIFO 队列,等agent/status → idle后按原顺序执行。 - 命令仍由原注册表解析,
command/run+command/done生命周期日志仍由
CommandRuntime写入,只是执行时机推迟到空闲窗口。 - 新增只读命令
/cmdqueue,查看当前 agent 还排着哪些命令。 - 可选配置:
verbose、pollIntervalMs、immediateCommands、
alwaysImmediateCommands。
安全边界
- 非命令语法 / 注册表解析不到 / 控制动词(
stop、cancel、abort、interrupt、
halt)/ agent 已空闲 —— 一律照旧立即执行,零回归; - 排队逻辑自身抛错时回落原行为,绝不吞掉用户命令;
- 按 agent 隔离,一个 agent 空闲不会放行另一个在队的命令;
- 严格 FIFO 串行,多个
/compact不会挤进同一个空闲窗口; - 可随 UI 请求的
signal取消(浏览器断连是唯一实际取消源); - 插件卸载时唤醒所有等待者,已排队命令立刻按原顺序执行完,不留悬挂 RPC。
两个踩过的实现坑
ctx.commands是 cordis 的 traceable Proxy,get trap 对函数值每次返回新的
shadow method,所以service.execute === patched这种身份比较恒为 false
(用它做还原守卫会导致补丁永远摘不掉)。改为经
Symbol.for('cordis.original')取 raw 实例,用
Object.getOwnPropertyDescriptor记录原状、精确 patch、精确还原。AgentLoop.kick()的 finally 是「setPhase(idle)同步 emit
agent/status→wakeDriver()」。任何跨 promise 边界后再调用都会撞回
running,compactNow仍是 busy。因此把「检查agent.status === 'idle'」和
「调用run()」放在同一个同步块内,中间绝不await。
验证
-
单元测试:
node --test tests/*.test.mjs→ 17/17 通过
(含 traceable Proxy 的 patch 与精确还原两个回归用例)。 -
真实端到端(插件注入运行中的 host,经 typert gateway HTTP 直连):
T1 line="hello" → 200 {"ok":true} 0.002s ✅ 通路正常、非命令不排队 T2 line="/cmdqueue" → 8s 无响应(curl 28) ✅ 命令在 agent running 时被挂起排队
兼容性
针对运行中的 @deepseek-ai/dsh@0.1.6-alpha.1 实现,依据其编译产物逐行核对。
不依赖任何 DSH 内部私有结构,只用公开面:ctx.commands.execute/find/register、
agent.status、agent.ctx.on('agent/status')。
v0.1.0 — 斜杠命令排队执行(/compact 在 agent 忙碌时不再报错)
Changelog
v0.1.0 — 2026-09-18
首个版本:让 /compact 这类斜杠命令在 agent 忙碌时不再立刻报错,
而是进入按 agent 串行的命令队列,等 agent 空闲后自动执行。
背景
DSH 里普通消息与斜杠命令走两条完全不同的通道:
| 普通消息 | 斜杠命令 | |
|---|---|---|
| 客户端 | session.prompt({ mode }) |
remote.commands.execute() |
| host 落点 | agent.followup() / agent.steer() → agent inbox |
ctx.commands.execute() → 直接调 handler |
| 忙碌时 | 天然排队 | 立即执行,无排队语义 |
/compact 的 handler 调 compactNow() → agent.runMaintenance(),后者的同步
gate 是 if (this.phase.kind !== 'idle') throw,被包成
ManualCompactionError('busy')。所以 agent 忙碌时敲 /compact 只会立刻收到
"Compaction is unavailable because this process has an active compaction, or the
agent is not idle."——既没排队,也没有任何重试。
DSH 作者把「命令自己进队列」当作有意排除的方案记录在案
(Agent Note: "Queue the command itself. Rejected."),因此官方没有开关。
本插件在不改 DSH 源码的前提下补上这一段。
新增
- 包装 host 侧
ctx.commands.execute():合法 + 已注册 + 未豁免 + agent 正忙的
调用进入该 agent 的 FIFO 队列,等agent/status → idle后按原顺序执行。 - 命令仍由原注册表解析,
command/run+command/done生命周期日志仍由
CommandRuntime写入,只是执行时机推迟到空闲窗口。 - 新增只读命令
/cmdqueue,查看当前 agent 还排着哪些命令。 - 可选配置:
verbose、pollIntervalMs、immediateCommands、
alwaysImmediateCommands。
安全边界
- 非命令语法 / 注册表解析不到 / 控制动词(
stop、cancel、abort、interrupt、
halt)/ agent 已空闲 —— 一律照旧立即执行,零回归; - 排队逻辑自身抛错时回落原行为,绝不吞掉用户命令;
- 按 agent 隔离,一个 agent 空闲不会放行另一个在队的命令;
- 严格 FIFO 串行,多个
/compact不会挤进同一个空闲窗口; - 可随 UI 请求的
signal取消(浏览器断连是唯一实际取消源); - 插件卸载时唤醒所有等待者,已排队命令立刻按原顺序执行完,不留悬挂 RPC。
两个踩过的实现坑
ctx.commands是 cordis 的 traceable Proxy,get trap 对函数值每次返回新的
shadow method,所以service.execute === patched这种身份比较恒为 false
(用它做还原守卫会导致补丁永远摘不掉)。改为经
Symbol.for('cordis.original')取 raw 实例,用
Object.getOwnPropertyDescriptor记录原状、精确 patch、精确还原。AgentLoop.kick()的 finally 是「setPhase(idle)同步 emit
agent/status→wakeDriver()」。任何跨 promise 边界后再调用都会撞回
running,compactNow仍是 busy。因此把「检查agent.status === 'idle'」和
「调用run()」放在同一个同步块内,中间绝不await。
验证
-
单元测试:
node --test tests/*.test.mjs→ 17/17 通过
(含 traceable Proxy 的 patch 与精确还原两个回归用例)。 -
真实端到端(插件注入运行中的 host,经 typert gateway HTTP 直连):
T1 line="hello" → 200 {"ok":true} 0.002s ✅ 通路正常、非命令不排队 T2 line="/cmdqueue" → 8s 无响应(curl 28) ✅ 命令在 agent running 时被挂起排队
兼容性
针对运行中的 @deepseek-ai/dsh@0.1.6-alpha.1 实现,依据其编译产物逐行核对。
不依赖任何 DSH 内部私有结构,只用公开面:ctx.commands.execute/find/register、
agent.status、agent.ctx.on('agent/status')。