# 设计与实现细节 本文是 [dsh-command-queue](https://github.com/kuyueliuhun-ctrl/dsh-command-queue) 的 **完整技术细节**:实现原理、行为契约、浏览器半边、实测运行时行为、验证记录与代码出处。 面向想改这个插件、或想在 DSH 里做同类拦截的维护者。只想用它的话看 [README](../README.md) 就够了。 所有 `文件:行` 引用均以运行中的 `@deepseek-ai/dsh@0.1.7-alpha.2`(路径前缀 `…\npm\node_modules\@deepseek-ai\dsh\node_modules\@deepseek-ai\`)的**编译产物**为准, 并已回对 `0.1.7-rc.1` 抽样复核。 > **兼容性结论(0.1.7 复核)**:原始基线是 `0.1.6-alpha.1`。上游**未**原生支持 > 「忙时排队斜杠命令」——`dsh-command-compact/README.md:141,153` 明写 > *"the command itself is not queued"* 并把 *"Queued commands"* 列为 > *undecided / open direction*;`dsh-commands` 业务代码零排队逻辑;UI 层 > `plainMessageDraft` 显式把 `/` 草稿排除在 queue/steer 之外。本插件依赖的内部 API > 逐条仍在(`dsh-commands/lib/index.js` 与 0.1.6-alpha.1 **字节完全相同**), > 唯一需要跟进的是客户端 QueueDock 样式(已同步,见 §3)。 --- ## 1. 实现原理 在 host 侧包装 `ctx.commands.execute()`: ``` 浏览器 ──POST commands/execute──▶ typert gateway │ Reflect.get(receiver, 'execute') ← 每次调用动态取方法 ▼ ┌─ patched execute ─┐ │ 合法命令? │ │ 在 deferCommands?│ │ 未强制立即? │ │ 注册表能解析到? │ │ agent 正忙? │ └────────┬──────────┘ 是 ────┴────▶ 该 agent 的 FIFO 队列 │ 等 agent/status → idle ▼ original.execute(...) ← 原方法:原注册表解析、原生命周期日志 ``` **为什么打这个补丁有效**:typert gateway 在 `prepareInvocation()` 里每次调用都执行 `Reflect.get(receiver, descriptor.implementation ?? descriptor.method)` (`dsh-api-gateway/lib/index.js:977-978`);而 `commands/execute` 的 strict descriptor 只声明 `method: 'execute'`,**不携带方法引用**(`dsh-commands/lib/typert.host.js:54`)。 所以给服务对象加一个自有的 `execute` 会被后续所有请求采纳, `agentId` 参数解析、参数校验、结果 schema 全部不变。 > **两个必须知道的细节**(都踩过): > 1. `execute` 定义在 **`CommandRuntime.prototype`** 上,实例本身没有自有描述符 > (`Object.getOwnPropertyDescriptor(raw, 'execute') === undefined`)。插件用 > `Object.defineProperty(target, 'execute', …)` 建一个**遮蔽**原型方法的自有属性; > 还原时 `previous === undefined`,因此实际走 `delete target.execute` 把原型方法 > 重新露出来。若改成 `defineProperty(target, 'execute', previous)` 会直接抛错。 > 2. **不能**用 `service.execute === patched` 判断补丁是否生效 —— `ctx.commands` 是 > cordis 的 traceable Proxy,每次读函数属性都返回新的 shadow 方法,恒为 `false`。 > 要拿原始实例走 `ctx.commands[Symbol.for('cordis.original')]`(只在代理上有效)。 命令仍由**原注册表**解析(`ctx.commands.find()` / 层遮蔽规则都不动), `command/run` + `command/done` 生命周期日志仍由 `CommandRuntime` 自己写, 只是执行时机被推迟到空闲窗口。 --- ## 2. 行为契约:什么时候排队 **规则是白名单**(v0.2.2 起):只有名字明确列在 `deferCommands` 里的命令才可能被排队, 默认只有 `compact` —— 它是唯一「忙时必然失败」的命令(`compactNow` → `runMaintenance` 的 `phase.kind !== 'idle'` 同步闸门)。其余命令一律照旧立即执行,零回归。 判定顺序(**任一命中即立即执行,不进队列**): 1. **不是合法命令语法** —— 第 0 字节不是 `/`、名称含大写或非法字符(如 `/Compact`)、只有 `/`; 2. **命令名不在 `deferCommands` 里** —— 默认即绝大多数命令走这里; 3. **命令名在强制立即名单里** —— 内置 `stop` / `cancel` / `abort` / `interrupt` / `halt` 永远立即(这些控制动词排队会失去意义,甚至危险:忙碌时敲 `/stop` 本该立刻中断); 用户配置的 `immediateCommands` / `alwaysImmediateCommands` 与内置名单**取并集**, 所以无法把内置控制动词排除掉,某命令即使同时列在 `deferCommands` 里也立即执行; 4. **命令名在注册表里解析不到** —— `ctx.commands.find(agent, name)` 返回 `undefined` (客户端本来就会按「未命中」处理,没必要拖到空闲后); 5. **agent 当前就是 idle**。 只有「合法 + 在 `deferCommands` 里 + 未被强制立即 + 已注册 + agent 正忙」才入队。 此外,**排队逻辑自身抛错**时兜底回落成原行为,绝不吞掉用户命令。 配置语义(无 schema,容错读取): - `deferCommands`(命令名数组,接受带或不带前导 `/`)默认 `['compact']`;显式空数组 `[]` = 没有任何命令排队,等同于关闭排队功能; - `immediateCommands` / `alwaysImmediateCommands` 是**强制立即**名单,语义等价、只做「加」, 与内置名单取并集。注意 v0.2.1 及以前 `immediateCommands` 是**整体替换**内置名单, 这是 v0.2.2 的破坏性配置变更; - `pollIntervalMs`(默认 250,夹取 50–5000)、`verbose`(默认 false)、 `exposeState`(默认 true)语义不变。 其它保证: - **按 agent 隔离** —— 一个 agent 空闲不会放行另一个 agent 在队的命令; - **严格 FIFO 串行** —— 同一 agent 内第 N 个命令只在第 N-1 个 settle 之后才去等空闲, 多个 `/compact` 不会挤进同一个空闲窗口; - **可取消** —— UI 请求的 `signal` 中止时,命令返回 `{ kind: 'error', text: '/xxx was cancelled before it could run.' }` 且**不会执行**; - **卸载不丢命令** —— 插件卸载时唤醒所有等待者,已排队的命令立刻按原顺序执行完。 --- ## 3. 浏览器队列面板 **它挂在哪**:DSH 原生消息队列本身就是槽位 `conversation.input.dock` 上的一个 **list 贡献**(`dsh-client-ui-conversation/lib/client.js:15535-15537`,`id: "queue"`; 槽位声明见同文件 `:17789`)。list 槽允许第三方携带自己的 `id` **追加**条目, 所以本插件注册 `id: "command-queue"`、`order: 30`,两条队列上下并排: ```js ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({ name: 'conversation.input.dock', id: 'command-queue', order: 30, inject: (sessionId) => ({ sessionId }), // 返回值就是组件 props }, CommandQueueDock)) ``` 兼容边界:0.1.7 给 list 槽新增了两个**可选**字段 `label` / `priority` (槽位内核 `dsh-client-ui-slots/lib/index.js` 与 0.1.6-alpha.2 **字节相同**), 本插件不传即取默认值,向后兼容。 **样式来源**:CSS 取自原生 QueueDock 的 CSS 模块原文 (`dsh-client-ui-conversation/lib/client.js:15078` 的 `css$6`),类名统一改前缀为 `dshcq_` 避免冲突,设计变量(`--dsw-alias-*`、`--dsh-composer-*`)沿用全局主题, 因此深浅色主题下与原生队列表现一致。样式通过官方那套 `