Skip to content

Releases: kuyueliuhun-ctrl/dsh-command-queue

v0.2.0 — 浏览器队列面板:复刻原生队列样式,排队中的斜杠命令看得见

Choose a tag to compare

@kuyueliuhun-ctrl kuyueliuhun-ctrl released this 17 Sep 17:07

Changelog

v0.2.0 — 2026-09-18

新增浏览器队列面板:排队中的斜杠命令现在看得见了。

背景

v0.1.0 的排队是 host 侧的延迟执行,功能正确但排队期间没有任何可见提示 ——
UI 的「执行中…」卡片由会话事件 command/run 驱动,而它在 handler 调用前才写入,
我们正是卡在它之前。

排查后确认不能用 DSH 原生队列达成这件事,这是结构性的:

  1. 原生队列(agent inbox)的条目只能是 UserMessage(模型可见内容),
    没有「命令项」这个类型;
  2. 队列项在下一个 step 边界就被 claim() 取走,那一刻 agent 仍在
    running,compactNow 照样报 busy —— 只是把错误推迟几十秒;
  3. 若用官方 agent/pre-step waterfall 把标记从 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-modulesresolveMeta() 有一层 pkgMeta 负缓存
dsh-client-modules/lib/index.js:652):包在没有 dsh.client 声明的形态下
被加载过一次,null 就会被缓存,之后即使补上 dsh.client 也读不到,
client 半边不会注册。

  • 重启 host 即可(缓存冷启动,最省事);
  • 不想重启时,清掉该进程里的 clientModules.pkgMeta 里对应条目再调
    processOne(包名) + compose() + notifyGraphChanged() 即可(本次升级就是这样处理的)。
  • 全新安装不受影响。

另外别忘了:客户端插件是页面加载时装配的,升级后需要刷新页面

其它变更

  • 队列条目改为带 id(cq1cq2…),支持从面板/接口移除一条在队命令
    (被移除的命令不会执行,RPC 返回 removed from the command queue)。
  • 新增配置 exposeState(默认 true);设为 false 即回到纯 host 形态。
  • /cmdqueue 与浏览器面板读同一份队列。

兼容性与风险控制

  • host 侧仅用到公开面:ctx.commands.execute/find/registeragent.status
    agent.ctx.on('agent/status')ctx.connection.fetch.register
  • 客户端 bundle 抛错会让整个 Web 应用无法 mount(DSH 的 boot 审计没有
    per-plugin 隔离),因此:apply 整体 try/catch、组件内网络/DOM 操作全自吞、
    空队列返回 nullReact.useId 缺失时降级为常量。
  • 新增 tests/client-bundle.test.mjs:用最小 React 运行时 + 假
    window.__ModuleLoader__ 把 bundle 真正加载并渲染,覆盖 bundle 形态
    (id/factory/无 default)、dock 条目 id 与 order、异常自吞、空队列渲染 null
    有队列时渲染命令行并经 drop 路由移除。
  • 客户端插件是页面加载时装配的 —— 安装/注入后需要刷新页面才会出现面板。

验证

  • 单元测试:npm test29/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 还排着哪些命令。
  • 可选配置:verbosepollIntervalMsimmediateCommands
    alwaysImmediateCommands

安全边界

  • 非命令语法 / 注册表解析不到 / 控制动词(stopcancelabortinterrupt
    halt)/ agent 已空闲 —— 一律照旧立即执行,零回归;
  • 排队逻辑自身抛错时回落原行为,绝不吞掉用户命令
  • 按 agent 隔离,一个 agent 空闲不会放行另一个在队的命令;
  • 严格 FIFO 串行,多个 /compact 不会挤进同一个空闲窗口;
  • 可随 UI 请求的 signal 取消(浏览器断连是唯一实际取消源);
  • 插件卸载时唤醒所有等待者,已排队命令立刻按原顺序执行完,不留悬挂 RPC。

两个踩过的实现坑

  1. ctx.commands 是 cordis 的 traceable Proxy,get trap 对函数值每次返回新的
    shadow method,所以 service.execute === patched 这种身份比较恒为 false
    (用它做还原守卫会导致补丁永远摘不掉)。改为经
    Symbol.for('cordis.original') 取 raw 实例,用
    Object.getOwnPropertyDescriptor 记录原状、精确 patch、精确还原。
  2. AgentLoop.kick() 的 finally 是「setPhase(idle) 同步 emit
    agent/statuswakeDriver()」。任何跨 promise 边界后再调用都会撞回
    running,compactNow 仍是 busy。因此把「检查 agent.status === 'idle'」和
    「调用 run()」放在同一个同步块内,中间绝不 await

验证

  • 单元测试:node --test tests/*.test.mjs17/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.statusagent.ctx.on('agent/status')

v0.1.0 — 斜杠命令排队执行(/compact 在 agent 忙碌时不再报错)

Choose a tag to compare

@kuyueliuhun-ctrl kuyueliuhun-ctrl released this 17 Sep 16:40

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 还排着哪些命令。
  • 可选配置:verbosepollIntervalMsimmediateCommands
    alwaysImmediateCommands

安全边界

  • 非命令语法 / 注册表解析不到 / 控制动词(stopcancelabortinterrupt
    halt)/ agent 已空闲 —— 一律照旧立即执行,零回归;
  • 排队逻辑自身抛错时回落原行为,绝不吞掉用户命令
  • 按 agent 隔离,一个 agent 空闲不会放行另一个在队的命令;
  • 严格 FIFO 串行,多个 /compact 不会挤进同一个空闲窗口;
  • 可随 UI 请求的 signal 取消(浏览器断连是唯一实际取消源);
  • 插件卸载时唤醒所有等待者,已排队命令立刻按原顺序执行完,不留悬挂 RPC。

两个踩过的实现坑

  1. ctx.commands 是 cordis 的 traceable Proxy,get trap 对函数值每次返回新的
    shadow method,所以 service.execute === patched 这种身份比较恒为 false
    (用它做还原守卫会导致补丁永远摘不掉)。改为经
    Symbol.for('cordis.original') 取 raw 实例,用
    Object.getOwnPropertyDescriptor 记录原状、精确 patch、精确还原。
  2. AgentLoop.kick() 的 finally 是「setPhase(idle) 同步 emit
    agent/statuswakeDriver()」。任何跨 promise 边界后再调用都会撞回
    running,compactNow 仍是 busy。因此把「检查 agent.status === 'idle'」和
    「调用 run()」放在同一个同步块内,中间绝不 await

验证

  • 单元测试:node --test tests/*.test.mjs17/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.statusagent.ctx.on('agent/status')