@mars.liu/dsh-cybernetics 0.21.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/docs/CHANGELOG.md +59 -0
- package/lib/client.js +288 -3
- package/package.json +3 -2
- package/src/index.ts +62 -5
- package/src/modelPresets.ts +46 -0
package/README.md
CHANGED
|
@@ -172,6 +172,8 @@ The settings card's「预置方案」dropdown — ten options: **auto-recommend*
|
|
|
172
172
|
- **Vendor entries (data-driven, `status: 'ready'`)**: each vendor carries per-role model + reasoning-effort token (heterogeneous: OpenAI/Minimax five-level low/medium/high/xhigh/max, DeepSeek thinking switch, others model default), upgrade tier, availability fallback, per-role rationale, per-vendor note (⚠️ caveats — e.g. Qwen's 「verify 绝不用 Turbo」幻觉共振 warning, Kimi's K2-retirement avoidance, DeepSeek's 「成本不应成为降级 verify 的理由」) and VARIANTS (Claude 常规/高要求, Qwen A/B/C, DeepSeek 仅本家/verify 外置, Kimi 长期/最高验收质量, 火山 A/B, GLM 标准组合/重活命名角色). Selecting a preset applies its **default variant** (the first one); the outcome record discloses the variant applied and lists the remaining ones. Vendor-canonical model ids are applied verbatim — an id absent from this deployment's catalog shows the card's model-unavailable warning and the round's availability fallback (v0.8) keeps rounds alive.
|
|
173
173
|
- **Auto-recommend (since v0.18.2 = a session-agent handoff, user ruling)**: selecting auto runs **NO independent LLM call and spawns NO agent** (the previous one guarded one-shot LLM call plus its route config field were removed — re-submitting the whole context to a second model call is a waste of both time and money). The plugin now probes the live catalog (`probeCatalogOptions`, the same directory smart selection reads), builds the handoff brief — embedding the catalog (the only selectable source), the cross-vendor principles (verify ≥ task; verify/task kept cross-generation/cross-family independent; verify must not be over-confident; cost cuts hit task/triage first and never verify; triage light), the current role config and every vendor preset as advisory reference — writes a `pending` record to the card, and hands the brief to the session agent through the plugin-registered `preset_recommend` tool (a steer/followup reminder is delivered to live session agents). The SESSION AGENT reads the catalog in its OWN context, answers for task/verify/triage, and calls the same tool again to submit; the plugin only validates (catalog-matched models only; per-role `reasoningEffort` validated against `ctx.llm.resolveModelInfo` and dropped to the model default when unsupported) and writes. Until the session agent answers, the card shows the pending note; an unusable answer (off-catalog / unparseable / zero picks) degrades to a written `degraded` record with the reason and is reported back to the session agent for a corrected re-submission — nothing is applied and no round is blocked. The tool survives the hard-gate allowlist alongside the cybernetics tool itself.
|
|
174
174
|
- **Selecting is one-shot**: re-selecting the same entry does not re-fire (the handler guards on the last handled value; a value persisted across a restart is not re-applied at boot). Switch to「(未选择)」first to re-apply an entry.
|
|
175
|
+
- **`/cybernetics use [id]` (v0.22.0, command channel)**: switch the role composition to any preset plan without opening the settings card. With no argument the command replies with a plain-text list of every applicable plan — all built-in registry ids plus the configured custom presets (`custom:<id>`) — and asks the user to confirm with a second `/cybernetics use <id>`. With an id it reuses the SAME apply pipeline as the dropdown (`handlePresetSelection`, six-field-bounded ops + the outcome record), so the reply and the card stay consistent; preset ids are case-preserved (custom ids are matched verbatim). Unlike the dropdown, a repeated `use <id>` DOES re-apply the same plan (the command keeps the one-shot volatile guard in sync and deliberately bypasses it), so no「(未选择)」detour is needed. The preset applied this way is profile-global — it affects every concurrent session.
|
|
176
|
+
- **Editor-side preset picker (v0.24.1, popupSelect)**: the picker appears only after you express the `use` intent. Picking `/cybernetics` in the `/` menu completes the composer text to `/cybernetics` (no popup — keep typing `use ...`); pressing Enter on the bare `/cybernetics` token opens a searchable popup listing every applicable plan (the 12 built-in ids and the configured `custom:<id>` entries, grouped, fuzzy-filtered as you type, current plan highlighted); typing `/cybernetics use` and pressing Enter executes the plain-text listing and then opens the same popup over it. Picking a row runs `/cybernetics use <id>` through the same command transport (validation/apply/receipt stay server-authoritative; errors surface in the popup). This rides the host's own mechanism for commands with pickable values (`commandUi.decorate` + the public `popupFor`/`command/executed` faces, the same pattern as the official `/model` picker); hosts without the `commandUi` service keep the plain-text v0.22 behavior, and the typed `/cybernetics use <id>` path is untouched. Note: inline argument completion *inside the draft* (a dropdown right after typing `use `) is not possible with the host protocol — the slash trigger scan stops at the first whitespace (re-verified v0.25.0: the host composer publishes no draft-change event, the four `slash/input-*` scope events are plugin→shell bail-request channels only, and trigger-source outcomes are limited to `{claim}/{text}/{insert}` — nothing can observe the `/cybernetics use` draft before Enter). **Type `/cybernetics use`, then press Enter to open the preset picker** (the bare-enter path lands the plain-text listing first and the popup opens over it).
|
|
175
177
|
|
|
176
178
|
## Installation and Usage
|
|
177
179
|
|
package/README.zh-CN.md
CHANGED
|
@@ -172,6 +172,8 @@ goal 一经存在不再原地修改。`revise` 是唯一的改目标动词(必
|
|
|
172
172
|
- **厂商方案(数据驱动,`status: 'ready'`)**:每厂商含逐角色模型 + 思考强度 token(异构:OpenAI/Minimax 五档 low/medium/high/xhigh/max、DeepSeek thinking 开关、其余模型默认)、可升级档、可用性 fallback、逐角色 rationale、厂商备注(⚠️ 注意事项——如 Qwen 的「verify 绝不用 Turbo(幻觉共振)」、Kimi 的避开退役 K2、DeepSeek 的「成本不应成为降级 verify 的理由」)与**多变体**(Claude 常规/高要求,Qwen A/B/C,DeepSeek 仅本家/verify 外置,Kimi 长期组合/最高验收质量,火山 A/B,GLM 标准组合/重活命名角色)。选择方案应用其**默认变体**(第一个);结果记录披露所应用变体并列出其余变体。厂商原生模型 id 逐字应用——部署目录中不存在的 id 会显示卡片不可用警告,轮内可用性 fallback(v0.8)保证轮次存活。
|
|
173
173
|
- **自动推荐(v0.18.2 起 = session agent 交办,用户裁定)**:选择 auto 时插件**不发起任何独立 LLM 调用、不新起任何子 agent**(v0.17.2 及之前的「一次一次性 LLM 调用 + 推荐所用模型配置项」已删除——另起一次调用等于把上下文重复提交一遍,时间与经济性都不合理)。现在的流程:插件探测实时目录(`probeCatalogOptions`,与智能选择同源),把推荐交办单——内嵌目录(唯一可选来源)、跨厂商通用原则(verify 档位不低于 task;verify/task 跨代际跨家族保持独立;verify 不能过度自信;省成本先压 task/triage、verify 最后动且成本不是降级 verify 的理由;triage 轻量)、当前角色配置与全部厂商方案(仅作参考)——经插件注册的 `preset_recommend` 工具交给 session agent,并在卡片写入 `pending` 记录、向在线 session agent 投递一条 steer/followup 提醒。session agent 在**自己的上下文中**读取目录、给出 task/verify/triage 的答案,再次调用该工具提交;插件只负责校验(逐字命中目录;逐角色 `reasoningEffort` 经 `ctx.llm.resolveModelInfo` 校验,不支持时降为模型默认并注明)与写入。session agent 未作答时卡片保持 pending;答案不可用(未命中目录/不可解析/零选择)时降级为带原因的 `degraded` 记录并回执给 session agent 修正重交——不应用任何更改、不阻塞任何轮次。该工具在硬门禁 allowlist 下与 cybernetics 工具一同保留。
|
|
174
174
|
- **选择是一次性的**:重复选择同一项不会再次触发(处理器按最后处理值守卫;跨重启持久化的值不会在启动时重放)。要重复应用某项,先切「(未选择)」再选。
|
|
175
|
+
- **`/cybernetics use [方案id]`(v0.22.0,命令通道)**:不开设置卡片,直接把角色组合切换为任意预置方案。无参数时命令回执纯文本清单——全部内置方案 id + 已配置的自定义方案(`custom:<id>`)——并提示用户再次输入 `/cybernetics use <id>` 确认;带 id 时复用与下拉框完全相同的应用管线(`handlePresetSelection`:六字段白名单 ops + 结果记录),回执与卡片保持一致;方案 id 大小写保留(自定义 id 逐字匹配)。与下拉框不同,重复输入同一条 `use <id>` 会**再次应用**(命令路径同步维护一次性守卫 `lastHandledPresetPlan` 并刻意绕过它),无需先切「(未选择)」。此方式应用的方案为 profile 全局生效——影响所有并发会话。
|
|
176
|
+
- **编辑器侧方案选择器(v0.24.1,popupSelect)**:选择器只在你表达 `use` 意图之后出现。在 / 菜单点选 `\cybernetics` 只把编辑器文本补全为 `/cybernetics`(不弹层——继续输入 `use ...` 即可);对裸 `/cybernetics` 回车弹出可搜索的方案选择器(12 个内置 id 与已配置的 `custom:<id>` 条目分组列出,输入即模糊过滤,当前方案高亮);键入 `\cybernetics use` 回车则先落地纯文本清单,随后同一弹层在其上弹出。点选一项即经同一条命令通道提交 `/cybernetics use <id>`(校验/应用/回执仍以服务端为唯一权威,错误就地显示在弹层)。实现走宿主为「带可选值的命令」提供的原生机制(`commandUi.decorate` + 公开的 `popupFor`/`command/executed` 面,与官方 `/model` 选择器同一模式);宿主无 commandUi 服务时保持 v0.22 纯文本行为,键入路径 `/cybernetics use <id>` 完全不动。说明:草稿内的参数级补全(输入 `use ` 后就地弹下拉)在宿主协议下不可实现——斜杠触发扫描遇空格即止(v0.25.0 复核:宿主 composer 不对外发射任何草稿变更事件,四个 `slash/input-*` 作用域事件均为插件→shell 的 bail 请求通道,触发源 outcome 仅 `{claim}/{text}/{insert}` 三种,均无法在回车前感知 `/cybernetics use` 草稿)。**请输入 `/cybernetics use` 后按 Enter 唤起方案选择器**(无参回车先落地纯文本清单,弹层随即在其上出现)。
|
|
175
177
|
|
|
176
178
|
## 安装与使用
|
|
177
179
|
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,65 @@
|
|
|
4
4
|
> 当下的功能与使用方式以 [README.md](../README.md) 为准;协议细节见 [protocol.md](protocol.md)。
|
|
5
5
|
> 约定:`src/` 改动需宿主重载生效;`lib/client.js` 改动需重启宿主(客户端模块图在宿主启动时构建)并刷新浏览器;`workflows/round.js` 改动热生效。
|
|
6
6
|
|
|
7
|
+
## v0.25.0(2026-11-08):「输入 `/cybernetics use` 未回车即弹方案选择器」调研结论——协议内不可行;弹层文案与文档明确「按 Enter 唤起」
|
|
8
|
+
|
|
9
|
+
**背景(用户新诉求)**:v0.24.1 之后用户反馈:当前 `/cybernetics use` 必须**按 Enter** 才弹方案列表;希望输入到「/cybernetics use 」(以空格结尾、尚未回车)时就自动弹出。本轮做只读调研并尽力实现;若协议不支持则明确结论并在文档/弹层文案提示按 Enter。
|
|
10
|
+
|
|
11
|
+
- **调研结论:协议内不可行**(宿主源码证据,本轮全量复核):
|
|
12
|
+
1. **唯一的草稿观察器在遇空白时自杀**:斜杠触发扫描 `detectTrigger`(`@deepseek-ai/dsh-client-ui-input-trigger/lib/client.js` 约 776–810 行)从光标向左扫,`if (WHITESPACE.test(ch)) return null;`——只要光标与 `/` 之间存在任何空白,触发即死、菜单关闭;「/cybernetics use␣」这个草稿态不存在任何活的 `/` 触发。
|
|
13
|
+
2. **触发管线的回调全部依赖活触发**:`candidates(session, req)` / `header?` / `matchSpace?` 只有在 `track()`(同文件约 1038 行;由 conversation shell 在每次草稿变更时调用,`dsh-client-ui-conversation/lib/client.js` 约 15444 / 15642 行)检出活 hit 时才会被调用——草稿为「token 后带空白」时全部静默。`matchSpace` 更是只在「前导 token 敲完空格」这一瞬间被轮询一次(token = `/cybernetics`),此后继续输入 `use` 无任何回调。
|
|
14
|
+
3. **会话作用域事件通道是单向请求,不是通知**:`@deepseek-ai/dsh-client-ui-conversation/lib/types/client/contract/input.d.ts` 约 129–154 行——`slash/input-begin-command` / `slash/input-insert-reference` / `slash/input-consume-token` / `slash/input-insert-text` 四个事件全部 `@mode bail`,语义是「插件→shell 的草稿编辑请求」(shell 侧监听实现见 conversation `lib/client.js` 约 16200–16203 行 `actx.on(...)`);用户打字**不发布**任何事件。`ctx.sessions.scope(sessionId)` 返回的 ClientContext(cordis Context)只暴露这些已声明 bail 事件的订阅,没有 on/subscribe/bus 之外的草稿面。
|
|
15
|
+
4. **触发源 outcome 无法开弹层**:`PickOutcome`(input.d.ts 约 53–60 行)仅 `{claim} | {insert} | {text} | 'handled' | undefined`——即使借 `registerSource` 造一个触发源,也只能在菜单活着时补全文本,无法经公开契约打开 popupSelect(`popupFor(actx)` 只被 `dispatch()/invoke()` 在真实开层时解析,`dsh-client-ui-commands/lib/types/client/contract.d.ts` 约 135–139 行)。
|
|
16
|
+
5. **无轮询面**:草稿文本只存在于 composer 输入机私有状态(`this.projection.detectText` / `this.rev`,conversation `lib/client.js` 输入机区域),不在 session scope 的任何公开 face/projection 上——定时轮询 scope 无从读到草稿。
|
|
17
|
+
- **落地(零逻辑改动)**:`lib/client.js` 弹层搜索框占位文案(zh/en)加提示「输入 /cybernetics use 后按 Enter 唤起本列表」;README.md / README.zh-CN.md `use` 段落补全调研证据与显式指引;版本顺延 0.25.0(用户可见文案变更)。
|
|
18
|
+
- **生效手段**:`lib/client.js` + `package.json` 改动需重启宿主并刷新浏览器。
|
|
19
|
+
- **保留路径**:现状三条弹层触发(菜单点选=补全、裸回车=弹层、`use` 清单回执后弹层)全部不变;若宿主未来把 `matchEnter` 的 `line`(完整草稿)类观察能力或草稿变更事件开放为公开契约,可再评估。
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## v0.24.1(2026-11-08):修复菜单点选后草稿无 token——经 `slash/input-insert-text` 作用域事件补全 `/cybernetics ` 文本
|
|
24
|
+
|
|
25
|
+
**背景(v0.24.0 活体验证反馈)**:点选 `/` 菜单里的 `\cybernetics`(或列表选中后回车)后,编辑器**什么都没留下**——而点选 skills(如 `\browser-skill`)会留下高亮的完整 token。v0.24.0 的设计假设「菜单已把 token 放进草稿」被证伪。
|
|
26
|
+
|
|
27
|
+
- **根因(宿主源码证据,dsh-client-ui-commands `lib/client.js`)**:菜单列的 `dispatch()` 对**装饰命令**走 `invoke()` → `popupFor().open()` 并返回 `"handled"`(约 L1523–1528)——宿主在这条路上**从不写 token**;token 的草稿写入只发生在 `dispatch`/`matchEnter` 返回 `{claim}` 时(input-trigger `settle()` → `execute()` → `slash/input-begin-command` → conversation shell `insertCommand`,约 L1364–1374 / L15703)。skills 无装饰,其源返回的是 `{text}`/`{insert}` 类 outcome,走 `slash/input-insert-text`/`insert-reference` 通道,所以留下高亮 token。装饰契约(contract.d.ts `CommandDecoration`)无 onComplete/取消回调,公开面上也没有任何草稿写回 API——v0.24.0 的 skip-open 只是不弹层,草稿停留在用户敲入的部分查询上。
|
|
28
|
+
- **修复(`lib/client.js` menu 守卫)**:skip open 的同时,在插件已持有的会话 scope(`popupFor(actx)` 接受的同一个 actx)上发宿主自己的纯文本插入事件 `slash/input-insert-text`,用菜单段携带的 pick-time token span 做 CAS,替换为完整 `'/cybernetics '` 文本——与 skills 留 token 走的是同一条宿主通道。良性降级:shell 未挂载(监听缺失)、span CAS 落空(点选后草稿被编辑,shell 校验 `span.draftRev`)、`bail` 抛错——一律保持弹层关闭的旧行为,不炸菜单。
|
|
29
|
+
- **不变项**:裸回车弹层(`via:'enter'` 透传)、`\cybernetics use` 清单回执弹层(`command/executed` 路径带 `via:'enter'` 合成段,绕过 menu 守卫)、键入路径与降级兜底全部原样。
|
|
30
|
+
- **测试**:`test/v24-use-popup.mjs` (d3) 更新:menu 守卫断言「不弹层 **且** 恰发出一条 `slash/input-insert-text`(`/cybernetics ` + pick span)」;新增 bail-less scope 降级分支。全仓 16 套件 + commands-hint-probe 全绿;`relay-host-probe` 的 `dsh-user-approval stateOf` 失败仍为**改动前既有**(git stash 复验)。`node --check lib/client.js` 通过。
|
|
31
|
+
- **生效手段**:`lib/client.js` + `package.json` 改动需重启宿主 + 刷新浏览器。
|
|
32
|
+
- **待活体验证**:真实 GUI 中点选菜单项后编辑器是否留下高亮 `/cybernetics` token(span CAS 依赖点选与 settle 同步完成,协议上应必然成立;若宿主版本行为有差,需以真实草稿复验)。
|
|
33
|
+
|
|
34
|
+
## v0.24.0(2026-11-08):popupSelect 交互时机修正——菜单点选=补全文本、裸回车=弹层、`use` 无参回执后弹层
|
|
35
|
+
|
|
36
|
+
**背景(用户活体验证 v0.23 后的三处反馈)**:①/ 菜单点选 `\cybernetics` 时立即弹方案选择器——太早,用户连 `use` 都没输;②方案列表应在表达 `use` 意图后才出现;③手动键入 `\cybernetics use` 回车后没有可操作的方案选择路径。
|
|
37
|
+
|
|
38
|
+
- **契约复核(dsh-client-ui-commands,contract.d.ts + lib/client.js)**:①`CommandDecoration` 无法区分 menu-pick 与 bare-enter 两条触发——`dispatch()`(菜单列)与 `matchEnter()`(裸回车列)都汇入 `invoke()` 用同一份 ui spec,只有 TokenSegment 携带 `via:'menu'|'enter'`,且永不到达业务 spec(contract.d.ts L106–122);②装饰只在**裸**调用触发——带空格的 `/cybernetics use` 行落回宿主 leadingInput claim(contract.d.ts L107–110;matchEnter 的 `if (bare)` 守卫);③**没有**草稿写回 API——唯一能把文本留在输入框的宿主机制就是 claim,而它被装饰抢占。
|
|
39
|
+
- **实现(`lib/client.js`)**:装饰保留(裸回车弹层为 v0.23 原生行为不变),在其上加两道公开契约边界的钩子:①`commandUi.popupFor(actx)` 包装——`invoke()` 正是经同一实例属性解析控制器,包装层对 `via:'menu'` 且命令为 `cybernetics` 的 open **不弹**(菜单点选已放进草稿的 token 文本 `/cybernetics` 原样保留 = 补全;同控制器的其它命令如 `/model` 的 popup 与一切 enter 路径原样透传;安装幂等);②`ctx.on('command/executed')` 监听(官方 dsh-session-log-export 同款公开事件;仅提交方客户端收到)——收到 `cybernetics` 的**无参 `use` 清单回执**(精确前缀 `cybernetics 可用预置方案:`,服务端 `src/index.ts` use 分支)时,经公开的 `sessions.scope(sessionId)` + `popupFor(actx).open('cybernetics', spec, {sessionId}, {via:'enter', token:'/cybernetics use'})` 弹出同一选择器(选后 consume 的 CAS 落空属契约明示的良性路径;应用/状态/未知 id/其它命令回执一律不触发;scope 已消亡则静默跳过,纯文本回执即兜底)。
|
|
40
|
+
- **交互形态(协议内可达的最优)**:菜单点选 → 编辑器补全 `cybernetics`(不弹层);键入 Space 或裸回车 → 弹层(裸回车路径不变);`\cybernetics use` 回车 → 服务端清单回执落地后弹层出现,点选即应用。草稿内 `use ` 之后的 inline 参数补全仍属协议不可达(v0.23 结论不变)。
|
|
41
|
+
- **测试**:`test/v24-use-popup.mjs` 更新至 v0.24(新增 d3/d4/d4b 三组:menu 守卫只拦 `cybernetics`+`via:'menu'`、enter 与其它命令透传、重复 apply 不双重包装;清单回执触发弹层且应用/状态/错误/其它命令/畸形回执全部不触发、scope 消亡静默、子会话门禁;原有 a–f 全保留)。全仓 16 套件 + commands-hint-probe 全绿;`node --check lib/client.js` 通过。
|
|
42
|
+
- **生效手段**:`lib/client.js` + `package.json` 改动需重启宿主 + 刷新浏览器;`src/` 未改动。
|
|
43
|
+
- **待活体验证**:真实 GUI 中确认菜单点选后草稿保留 `/cybernetics` 文本(token 文本的精确拼写由宿主输入侧决定);`\cybernetics use` 回车后弹层在清单回执节点之上正常出现且点选可用。
|
|
44
|
+
|
|
45
|
+
## v0.23.0(2026-11-08):`/cybernetics` 输入侧预置方案选择器——popupSelect 装饰(编辑器下拉选方案)
|
|
46
|
+
|
|
47
|
+
**背景(用户新需求)**:v0.22 的 `use` 子命令只能靠纯文本回执选方案。用户希望像 @file 那样在编辑器输入 `\cybernetics use` 时弹出可点选的下拉列表。本轮先做只读调研,再按调研结论实现。
|
|
48
|
+
|
|
49
|
+
- **调研结论(宿主源码证据)**:①斜杠触发扫描从光标向左**遇空白即止**(`@deepseek-ai/dsh-client-ui-input-trigger/lib/client.js` `detectTrigger`,约 763–810 行),因此 `use `(含空格)之后**不可能**由任何 `/` 触发源给出参数级补全——草稿内 inline 参数补全在宿主协议下不存在;②宿主自己为「带可选值的命令」提供的机制是挂在宿主命令上的 **popupSelect 装饰**(`@deepseek-ai/dsh-client-ui-commands/lib/types/client/contract.d.ts` `CommandDecoration`:「the decoration replaces only the bare menu-pick/enter with a popup whose onSelect typically submits a completed line back through command.execute」),`/model` 即官方先例(`dsh-client-ui-model-selection/lib/client.js` 约 2025–2045 行 `command.register` + `ui:{kind:'popupSelect'…}`);③静态 `input.hint` 只是命令描述符上的一个字符串(`dsh-commands/lib/index.js` 157–166 行仅校验 `hint` 与 `attachments`),无 completion provider。
|
|
50
|
+
- **实现(`lib/client.js` + `package.json`)**:客户端插件经 `ctx.inject(['commandUi'])` 对宿主命令 `/cybernetics` 做 `commandUi.decorate({name:'cybernetics', ui:{kind:'popupSelect'}})`:在 `/` 菜单点选(或裸回车)`/cybernetics` 时弹出可搜索方案选择器(fuzzy-label 搜索、内置/自定义分组、当前方案高亮「当前」角标);`options` 复用与设置卡片同源的 12 个内置 id + ConfigForm 快照里 `customPresets` 解析出的 `custom:<id>` 行(`usePopupRows` 导出供离线测试);`onSelect` 经与模式药丸同一条 `remote.commands.execute` 通道提交 `/cybernetics use <id>`——校验/应用管线/回执仍以服务端 `use` 分支为唯一权威,弹层内错误就地展示。`package.json dsh.client.inject` 补 `@deepseek-ai/dsh-client-ui-commands`。**守卫**:宿主无 commandUi 服务 / 无命令通道 / 子会话(available=false)时逐级降级;**键入路径完全不动**——`/cybernetics use <id>` 照旧 claim+submit,v0.22 纯文本清单兜底保留。
|
|
51
|
+
- **与需求的偏差(诚实记录)**:宿主协议下「输入 `use ` 即在草稿内弹参数补全」不可实现(证据 ①);本版实现的是宿主原生等价体验——输入 `/cybernetics` 触发弹层、搜索过滤、点选**直接应用**方案(而非仅补全草稿文本),效果上跳过了「补全后再回车」一步。
|
|
52
|
+
- **测试**:新增 `test/v24-use-popup.mjs`(6 组:usePopupRows 12 内置 + custom 行 + active 标记 + 坏快照降级;apply 恰注册一个 popupSelect 装饰且 fuzzy-label + 本地化搜索文案;options 实时行;onSelect 经通道提交 `/cybernetics use <id>`、error/失败结果抛出给弹层;三类守卫降级;inject/导出回归)。`test/v12-mode-pill-running.mjs`、`test/v21-hint-location.mjs` inject 清单断言更新为含 `commandUi`(10 项)。全仓离线套件全绿(relay-host-probe 的 `dsh-user-approval stateOf` 失败为**改动前既有**,与本版无关,已用 git stash 复验)。
|
|
53
|
+
- **生效手段**:`lib/client.js` + `package.json` 改动需重启宿主 + 刷新浏览器;`src/` 未改动。
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## v0.22.0(2026-11-08):`/cybernetics use` 子命令——命令通道直接应用预置方案(快速切换角色组合)
|
|
57
|
+
|
|
58
|
+
**背景(用户裁定:子命令名 `use`,早前方案的 `profile` 命名作废)**:预置方案此前只能经设置卡片下拉选择(volatile `presetPlan` + `loader/volatile-update` 一次性解析);要在会话里快速切换 task/verify/triage 的模型与思考强度,得先打开卡片。本版把同一应用管线开放到 `/cybernetics` 命令通道。
|
|
59
|
+
|
|
60
|
+
- **命令分支(`src/index.ts` 命令注册块)**:handler 新增 `use` 分支(子命令大小写不敏感,方案 id 大小写保留——自定义 id 逐字匹配)。**无参数**:回执纯文本清单(`PRESET_IDS` 全部 12 个内置 id 的 `presetChoiceLabel` + `customPresets` 已配置的自定义方案 `custom:<id>:名称`),并提示再次输入 `/cybernetics use <id>` 确认——命令通道只有 `{kind:'success'|'error', text}` 纯文本回执,无交互选择器。**带参数**:先同步校验(内置注册表 / `auto` / 自定义条目存在性,未知 id 直接 `error` 回执并列出全部可用 id,不写任何东西),随后以 `void handlePresetSelection(id)` fire-and-forget 走与下拉框完全相同的应用管线(六字段白名单 ops + 结果记录)。
|
|
61
|
+
- **守卫同步与重复应用**:handler 同步维护 `lastHandledPresetPlan`——volatile 一次性守卫保持一致(此后 loader 带同值不会重复应用),同时命令路径刻意绕过该守卫:重复输入同一条 `use <id>` 会再次应用,无需先切「(未选择)」。命令路径**不写** volatile `presetPlan` 字段。`auto` 分支同样经 `handlePresetSelection('auto')`(pending 记录 + session agent 交办,零独立 LLM 调用)。
|
|
62
|
+
- **命令引导**:动态 input hint 更新为 `[on|status|use]` / `[off|status|use]`(`profile` 命名全面移除);status 用法行与命令 description 补 `use` 说明。
|
|
63
|
+
- **测试**:新增 `test/v23-use-subcommand.mjs`(7 组:hint/用法文本只含 `use` 无 `profile`;无参数列出全部内置+自定义、零写入、确认提示;`use qwen`/`USE glm` 走 handlePresetSelection——恰 6 条角色字段 ops 且全部落在 ALLOWED_OP_PATHS 内 + ok 记录含变体;`custom:` 条目应用保存的组合并携带条目名;未知内置 id / 不存在的自定义 id → error + 清单 + 零写入;重复 `use` 再次应用且 volatile 字段未被命令路径触碰;on/status/未知文本回退不变)。`test/v09-session-mode.mjs` hint 断言更新为 `[on|status|use]` ↔ `[off|status|use]`。全仓 15 个离线套件全绿;`tsc --noEmit` 仅既有基线零新增。
|
|
64
|
+
- **生效手段**:`src/` 改动宿主重载生效。风险:命令应用的方案与卡片写入同路径——**profile 全局生效**,影响所有并发会话;`auto` 分支会向在线顶层 session agent 投递交办消息。
|
|
65
|
+
|
|
7
66
|
## v0.21.0(2026-11-07):轮次生效模型审计——round-cyb-*.json 记录 task/verify/triage 实际调用的 provider/model/effort,board 快照镜像 models 字段
|
|
8
67
|
|
|
9
68
|
**背景(用户裁定,§7.12.1)**:历史全部 213 个 `round-cyb-*.json` 均无结构化 model/reasoningEffort 字段,board 快照只有 `{goals, savedAt, sessionId, version}`——「每个角色实际跑在什么模型上」跨轮不可审计。插件既有 taskAgentModel/verifyAgentModel/triageAgentModel 配置(v0.8.x 调用级回退、v0.15 思考强度),本轮把生效值落进轮审计与 board 快照。
|
package/lib/client.js
CHANGED
|
@@ -177,6 +177,18 @@ window.__ModuleLoader__.load({
|
|
|
177
177
|
'settings.preset.optVolcanoCodingPlan': '火山 Coding Plan',
|
|
178
178
|
'settings.preset.optMinimax': 'Minimax',
|
|
179
179
|
'settings.preset.optGlm': 'GLM',
|
|
180
|
+
'settings.preset.optBuiltinGlmLight': '轻量级GLM',
|
|
181
|
+
'settings.preset.optBuiltinDeepseekFast': 'deepseek 快速',
|
|
182
|
+
// v0.23.0: the /cybernetics popupSelect decoration (slash menu →
|
|
183
|
+
// preset picker; selecting one runs `/cybernetics use <id>`).
|
|
184
|
+
'popup.searchPlaceholder': '搜索预置方案…(输入 /cybernetics use 后按 Enter 唤起本列表)',
|
|
185
|
+
'popup.empty': '暂无预置方案。',
|
|
186
|
+
'popup.noResults': '没有匹配的预置方案。',
|
|
187
|
+
'popup.group.builtin': '内置方案',
|
|
188
|
+
'popup.group.custom': '自定义方案',
|
|
189
|
+
'popup.badge.current': '当前',
|
|
190
|
+
'popup.error.transport': '命令通道不可用,无法应用方案;请改用文本命令 /cybernetics use <id>。',
|
|
191
|
+
'popup.error.result': '应用失败:{message}',
|
|
180
192
|
// v0.17.1: session 范围提示(用户裁定:session agent 不在本功能范围内)。
|
|
181
193
|
'settings.preset.sessionScope': 'session agent 由用户自行管理:预置方案与自动推荐只应用 task/verify/triage 三个子 agent 角色,厂商方案中的 session 推荐仅作参考展示,不会写入任何配置。',
|
|
182
194
|
'settings.preset.applied': '已应用({label}):{picks}',
|
|
@@ -354,6 +366,17 @@ window.__ModuleLoader__.load({
|
|
|
354
366
|
'settings.preset.optVolcanoCodingPlan': 'Volcano Coding Plan',
|
|
355
367
|
'settings.preset.optMinimax': 'Minimax',
|
|
356
368
|
'settings.preset.optGlm': 'GLM',
|
|
369
|
+
'settings.preset.optBuiltinGlmLight': 'GLM (light)',
|
|
370
|
+
'settings.preset.optBuiltinDeepseekFast': 'deepseek (fast)',
|
|
371
|
+
// v0.23.0: /cybernetics popupSelect decoration copy (en mirror).
|
|
372
|
+
'popup.searchPlaceholder': 'Search preset plans… (type /cybernetics use, then press Enter)',
|
|
373
|
+
'popup.empty': 'No preset plans available.',
|
|
374
|
+
'popup.noResults': 'No matching preset plan.',
|
|
375
|
+
'popup.group.builtin': 'Built-in plans',
|
|
376
|
+
'popup.group.custom': 'Custom plans',
|
|
377
|
+
'popup.badge.current': 'current',
|
|
378
|
+
'popup.error.transport': 'Command transport unavailable — use the plain /cybernetics use <id> command instead.',
|
|
379
|
+
'popup.error.result': 'Applying failed: {message}',
|
|
357
380
|
// v0.17.1: session-scope callout (user ruling: the session agent is out of scope).
|
|
358
381
|
'settings.preset.sessionScope': 'The session agent is user-managed: preset plans and auto-recommend apply only the task/verify/triage sub-agent roles; a vendor\'s session suggestion is displayed for reference only and is never written to any config.',
|
|
359
382
|
'settings.preset.applied': 'Applied ({label}): {picks}',
|
|
@@ -1381,8 +1404,7 @@ window.__ModuleLoader__.load({
|
|
|
1381
1404
|
// event; the labels live in the locale dicts. The leading "(none)"
|
|
1382
1405
|
// option is the select's neutral value ('' = unset op).
|
|
1383
1406
|
const PRESET_CHOICES = [
|
|
1384
|
-
{ id: 'auto', labelKey: 'settings.preset.optAuto' },
|
|
1385
|
-
{ id: 'openai', labelKey: 'settings.preset.optOpenai' },
|
|
1407
|
+
{ id: 'auto', labelKey: 'settings.preset.optAuto' }, { id: 'openai', labelKey: 'settings.preset.optOpenai' },
|
|
1386
1408
|
{ id: 'claude', labelKey: 'settings.preset.optClaude' },
|
|
1387
1409
|
{ id: 'gemini', labelKey: 'settings.preset.optGemini' },
|
|
1388
1410
|
{ id: 'deepseek', labelKey: 'settings.preset.optDeepseek' },
|
|
@@ -1391,7 +1413,68 @@ window.__ModuleLoader__.load({
|
|
|
1391
1413
|
{ id: 'volcano-coding-plan', labelKey: 'settings.preset.optVolcanoCodingPlan' },
|
|
1392
1414
|
{ id: 'minimax', labelKey: 'settings.preset.optMinimax' },
|
|
1393
1415
|
{ id: 'glm', labelKey: 'settings.preset.optGlm' },
|
|
1416
|
+
{ id: 'builtin-glm-light', labelKey: 'settings.preset.optBuiltinGlmLight' },
|
|
1417
|
+
{ id: 'builtin-deepseek-fast', labelKey: 'settings.preset.optBuiltinDeepseekFast' },
|
|
1394
1418
|
];
|
|
1419
|
+
/**
|
|
1420
|
+
* v0.23.0: popupSelect rows for the /cybernetics decoration — the
|
|
1421
|
+
* built-in registry ids (PRESET_CHOICES, same order as the settings
|
|
1422
|
+
* card dropdown) plus the user-saved custom presets parsed from the
|
|
1423
|
+
* live ConfigForm snapshot (`{status:'ready', value:{customPresets}}`
|
|
1424
|
+
* shape, identical to presetRow's reader). Custom ids get the
|
|
1425
|
+
* `custom:` selector prefix the server's use handler resolves. An
|
|
1426
|
+
* entry whose id equals the current `presetPlan` value is marked
|
|
1427
|
+
* `active` (the popup parks its highlight on it).
|
|
1428
|
+
*
|
|
1429
|
+
* Exported for the offline harness (test/v24-use-popup.mjs); the real
|
|
1430
|
+
* host path wraps it in the decoration's async options() provider.
|
|
1431
|
+
* @param {(key: string, params?: object) => string} t bound translator
|
|
1432
|
+
* @param {unknown} cfgSnap ConfigForm snapshot (or undefined)
|
|
1433
|
+
* @returns {Array<{id,label,detail?,group,active?,badge?}>}
|
|
1434
|
+
*/
|
|
1435
|
+
function usePopupRows(t, cfgSnap) {
|
|
1436
|
+
const cfgReady = cfgSnap !== undefined && cfgSnap !== null && typeof cfgSnap === 'object'
|
|
1437
|
+
&& cfgSnap.status === 'ready' && cfgSnap.value !== null && typeof cfgSnap.value === 'object';
|
|
1438
|
+
const cfg = cfgReady ? cfgSnap.value : {};
|
|
1439
|
+
const currentPlan = typeof cfg.presetPlan === 'string' ? cfg.presetPlan : '';
|
|
1440
|
+
const groups = {
|
|
1441
|
+
builtin: { name: 'builtin', label: t('popup.group.builtin') },
|
|
1442
|
+
custom: { name: 'custom', label: t('popup.group.custom') },
|
|
1443
|
+
};
|
|
1444
|
+
const rows = PRESET_CHOICES.map((opt) => {
|
|
1445
|
+
const id = opt.id;
|
|
1446
|
+
return {
|
|
1447
|
+
id,
|
|
1448
|
+
label: t(opt.labelKey),
|
|
1449
|
+
detail: '/' + 'cybernetics use ' + id,
|
|
1450
|
+
group: groups.builtin,
|
|
1451
|
+
...(currentPlan === id ? { active: true, badge: t('popup.badge.current') } : {}),
|
|
1452
|
+
};
|
|
1453
|
+
});
|
|
1454
|
+
// Defensive parse of the customPresets field (SettingsCard's
|
|
1455
|
+
// parseCustomPresetsList lives inside the card component body, so
|
|
1456
|
+
// this mirrors the same degrade-to-empty semantics inline).
|
|
1457
|
+
let list = [];
|
|
1458
|
+
try {
|
|
1459
|
+
const parsed = JSON.parse(cfg.customPresets);
|
|
1460
|
+
if (Array.isArray(parsed)) {
|
|
1461
|
+
list = parsed.filter((p) => p !== null && typeof p === 'object'
|
|
1462
|
+
&& typeof p.id === 'string' && p.id !== ''
|
|
1463
|
+
&& typeof p.name === 'string' && p.name.trim() !== '');
|
|
1464
|
+
}
|
|
1465
|
+
} catch { /* corrupt payload → built-ins only */ }
|
|
1466
|
+
for (const entry of list) {
|
|
1467
|
+
const id = 'custom:' + entry.id;
|
|
1468
|
+
rows.push({
|
|
1469
|
+
id,
|
|
1470
|
+
label: entry.name,
|
|
1471
|
+
detail: '/cybernetics use ' + id,
|
|
1472
|
+
group: groups.custom,
|
|
1473
|
+
...(currentPlan === id ? { active: true, badge: t('popup.badge.current') } : {}),
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
return rows;
|
|
1477
|
+
}
|
|
1395
1478
|
/**
|
|
1396
1479
|
* v0.17: parse the volatile presetRecommendation record the server
|
|
1397
1480
|
* writes after a selection is resolved. Shape-tolerant: anything that
|
|
@@ -2598,7 +2681,15 @@ window.__ModuleLoader__.load({
|
|
|
2598
2681
|
// (remote.commands.execute(sessionId, line, attachments)). Guarded
|
|
2599
2682
|
// at runtime: absent → the pill renders disabled with a hint
|
|
2600
2683
|
// instead of throwing.
|
|
2601
|
-
'remote.commands'
|
|
2684
|
+
'remote.commands',
|
|
2685
|
+
// v0.23.0: the /cybernetics popupSelect decoration hangs off the
|
|
2686
|
+
// client command surface (dsh-client-ui-commands). Guarded at
|
|
2687
|
+
// runtime: a host without the service keeps the pre-v0.23 behavior
|
|
2688
|
+
// (bare invocation = leading claim + typed arguments; the
|
|
2689
|
+
// plain-text listing fallback). The namespace must ALSO appear in
|
|
2690
|
+
// package.json dsh.client.inject, or the client runner refuses the
|
|
2691
|
+
// read (inject-declared access guard).
|
|
2692
|
+
'commandUi'];
|
|
2602
2693
|
|
|
2603
2694
|
function apply(ctx) {
|
|
2604
2695
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-cybernetics: dictionaries');
|
|
@@ -2816,6 +2907,199 @@ window.__ModuleLoader__.load({
|
|
|
2816
2907
|
} catch { /* namespace absent → undefined */ }
|
|
2817
2908
|
return undefined;
|
|
2818
2909
|
})();
|
|
2910
|
+
// v0.24.0: the /cybernetics popupSelect interaction, retimed per user
|
|
2911
|
+
// feedback on the live v0.23 GUI (three issues: the popup opened the
|
|
2912
|
+
// instant the menu row was picked — before the user could even type
|
|
2913
|
+
// "use"; and typing `/cybernetics use` + Enter gave no picker at all).
|
|
2914
|
+
// Contract findings (dsh-client-ui-commands, contract.d.ts +
|
|
2915
|
+
// lib/client.js):
|
|
2916
|
+
// - a CommandDecoration cannot distinguish its two trigger paths:
|
|
2917
|
+
// dispatch() (menu column) and matchEnter() (bare-enter column)
|
|
2918
|
+
// both route into invoke() with the same ui spec; only the
|
|
2919
|
+
// TokenSegment carries via:'menu'|'enter', and it never reaches
|
|
2920
|
+
// the business spec (contract.d.ts L106-122);
|
|
2921
|
+
// - a decoration fires on the BARE invocation only — a line like
|
|
2922
|
+
// `/cybernetics use` has a space, so matchEnter falls through to
|
|
2923
|
+
// the host leadingInput claim (contract.d.ts L107-110;
|
|
2924
|
+
// matchEnter `if (bare)` guard);
|
|
2925
|
+
// - there is NO draft-writeback API: the only host mechanism that
|
|
2926
|
+
// leaves text in the composer is the leadingInput claim, which a
|
|
2927
|
+
// decoration preempts.
|
|
2928
|
+
// Design within those bounds:
|
|
2929
|
+
// 1. menu-pick → completion, not popup: the decoration stays (so
|
|
2930
|
+
// bare Enter keeps the native popup), but we intercept at the
|
|
2931
|
+
// PUBLIC contract boundary commandUi.popupFor(actx) — the same
|
|
2932
|
+
// call invoke() uses — and wrap the per-session controller's
|
|
2933
|
+
// open(): a {via:'menu'} segment for 'cybernetics' opens
|
|
2934
|
+
// nothing, so the token text the menu already put into the
|
|
2935
|
+
// draft ("/cybernetics") stays as the completion, exactly like
|
|
2936
|
+
// the claim path leaves it. Other commands' popups (e.g. /model
|
|
2937
|
+
// contributions share the same per-session controller) pass
|
|
2938
|
+
// through untouched.
|
|
2939
|
+
// 2. bare Enter → native popup (unchanged from v0.23): the picker
|
|
2940
|
+
// appears only after the user deliberately submitted the bare
|
|
2941
|
+
// command.
|
|
2942
|
+
// 3. `/cybernetics use` + Enter → the host claim executes the line,
|
|
2943
|
+
// the server answers its plain-text listing receipt, and the
|
|
2944
|
+
// public `command/executed` event (same listener face the
|
|
2945
|
+
// official dsh-session-log-export uses) opens the SAME popup
|
|
2946
|
+
// through popupFor(actx).open() with a synthetic enter segment.
|
|
2947
|
+
// The consume CAS afterwards misses benignly (the draft is empty
|
|
2948
|
+
// — contract: a false CAS answer is benign and never retried).
|
|
2949
|
+
// Gated on the exact listing receipt, so `use <id>` applications,
|
|
2950
|
+
// status reports and unknown-id errors never open the picker.
|
|
2951
|
+
// Guarded: a host without the commandUi service (or a failed
|
|
2952
|
+
// registration) logs nothing and degrades to pre-v0.23 behavior;
|
|
2953
|
+
// without popupFor or sessions.scope the listener is a no-op.
|
|
2954
|
+
const popupSpec = {
|
|
2955
|
+
kind: 'popupSelect',
|
|
2956
|
+
searchMode: 'fuzzy-label',
|
|
2957
|
+
searchLabels: () => ({
|
|
2958
|
+
placeholder: t('popup.searchPlaceholder'),
|
|
2959
|
+
empty: t('popup.empty'),
|
|
2960
|
+
noResults: t('popup.noResults'),
|
|
2961
|
+
}),
|
|
2962
|
+
options: async () => usePopupRows(t, settingsForm && typeof settingsForm.getSnapshot === 'function' ? settingsForm.getSnapshot() : undefined),
|
|
2963
|
+
onSelect: async (option, session) => {
|
|
2964
|
+
if (typeof runCommand !== 'function') throw new Error(t('popup.error.transport'));
|
|
2965
|
+
const outcome = await runCommand(session.sessionId, '/cybernetics use ' + option.id);
|
|
2966
|
+
if (outcome && outcome.ok === false) {
|
|
2967
|
+
throw new Error(t('popup.error.result', { message: outcome.error && outcome.error.message ? outcome.error.message : String(outcome.error ?? 'transport error') }));
|
|
2968
|
+
}
|
|
2969
|
+
const result = outcome && outcome.value ? outcome.value.result : undefined;
|
|
2970
|
+
if (result && result.kind === 'error') {
|
|
2971
|
+
throw new Error(t('popup.error.result', { message: result.text ?? 'command error' }));
|
|
2972
|
+
}
|
|
2973
|
+
},
|
|
2974
|
+
};
|
|
2975
|
+
// v0.24.0: marker prefix the server's no-id `use` listing receipt
|
|
2976
|
+
// starts with (src/index.ts use branch). Matching the receipt — not
|
|
2977
|
+
// the submitted line, which the event does not carry — keeps the
|
|
2978
|
+
// gate exact and host-authoritative.
|
|
2979
|
+
const USE_LISTING_PREFIX = 'cybernetics 可用预置方案:';
|
|
2980
|
+
let commandUiRef;
|
|
2981
|
+
// Open the preset picker over one session (issue-3 path). Best
|
|
2982
|
+
// effort: a gone session scope, a missing sessions.scope face or a
|
|
2983
|
+
// popupFor refusal just leaves the plain-text listing as-is.
|
|
2984
|
+
const openPlanPicker = (sessionId) => {
|
|
2985
|
+
const ui = commandUiRef;
|
|
2986
|
+
if (!ui || typeof ui.popupFor !== 'function') return;
|
|
2987
|
+
try {
|
|
2988
|
+
if (ctx.sessions && typeof ctx.sessions.subagentAddress === 'function'
|
|
2989
|
+
&& ctx.sessions.subagentAddress(sessionId) !== undefined) return;
|
|
2990
|
+
} catch { /* catalog absent → stay open */ }
|
|
2991
|
+
try {
|
|
2992
|
+
const actx = ctx.sessions && typeof ctx.sessions.scope === 'function' ? ctx.sessions.scope(sessionId) : undefined;
|
|
2993
|
+
if (!actx) return;
|
|
2994
|
+
const controller = ui.popupFor(actx);
|
|
2995
|
+
if (!controller || typeof controller.open !== 'function') return;
|
|
2996
|
+
controller.open('cybernetics', popupSpec, { sessionId }, { via: 'enter', token: '/cybernetics use' });
|
|
2997
|
+
} catch { /* scope gone → the text receipt stays the fallback */ }
|
|
2998
|
+
};
|
|
2999
|
+
try {
|
|
3000
|
+
if (typeof ctx.inject === 'function') {
|
|
3001
|
+
ctx.inject(['commandUi'], (scope) => {
|
|
3002
|
+
const commandUi = scope.get('commandUi');
|
|
3003
|
+
if (!commandUi || typeof commandUi.decorate !== 'function') return;
|
|
3004
|
+
commandUiRef = commandUi;
|
|
3005
|
+
scope.effect(() => commandUi.decorate({
|
|
3006
|
+
name: 'cybernetics',
|
|
3007
|
+
available: (session) => {
|
|
3008
|
+
try {
|
|
3009
|
+
if (ctx.sessions && typeof ctx.sessions.subagentAddress === 'function'
|
|
3010
|
+
&& ctx.sessions.subagentAddress(session.sessionId) !== undefined) return false;
|
|
3011
|
+
} catch { /* catalog absent → stay available */ }
|
|
3012
|
+
return true;
|
|
3013
|
+
},
|
|
3014
|
+
ui: popupSpec,
|
|
3015
|
+
}), 'dsh-cybernetics: /cybernetics use popup decoration');
|
|
3016
|
+
// v0.24.0 issue 1: intercept the menu-pick popup at the
|
|
3017
|
+
// public popupFor boundary. invoke() resolves the
|
|
3018
|
+
// controller through this same instance property, so the
|
|
3019
|
+
// wrapper sees every open with its TokenSegment. Only
|
|
3020
|
+
// 'cybernetics' + {via:'menu'} is suppressed — every
|
|
3021
|
+
// other command's popup (the controller is shared per
|
|
3022
|
+
// session) and every enter-path open pass through.
|
|
3023
|
+
try {
|
|
3024
|
+
if (typeof commandUi.popupFor === 'function' && !commandUi.__cybMenuGuardInstalled) {
|
|
3025
|
+
commandUi.__cybMenuGuardInstalled = true;
|
|
3026
|
+
const popupForOriginal = commandUi.popupFor;
|
|
3027
|
+
commandUi.popupFor = function (actx) {
|
|
3028
|
+
const controller = popupForOriginal.call(commandUi, actx);
|
|
3029
|
+
if (controller && typeof controller.open === 'function' && !controller.__cybMenuGuard) {
|
|
3030
|
+
controller.__cybMenuGuard = true;
|
|
3031
|
+
const openOriginal = controller.open;
|
|
3032
|
+
controller.open = function (command, spec, context, segment) {
|
|
3033
|
+
if (command === 'cybernetics' && segment && segment.via === 'menu') {
|
|
3034
|
+
// v0.24.1 fix: a menu pick = completion
|
|
3035
|
+
// intent, so open nothing — but DO write
|
|
3036
|
+
// the token. Source evidence
|
|
3037
|
+
// (dsh-client-ui-commands lib/client.js):
|
|
3038
|
+
// dispatch() routes a decorated menu
|
|
3039
|
+
// pick into invoke() and returns
|
|
3040
|
+
// "handled" — the host NEVER inserts
|
|
3041
|
+
// the token on this path (the
|
|
3042
|
+
// leadingClaim writeback only happens
|
|
3043
|
+
// when dispatch returns {claim}, which
|
|
3044
|
+
// decorations preempt). The draft's
|
|
3045
|
+
// only in-protocol writeback is the
|
|
3046
|
+
// scoped `slash/input-insert-text`
|
|
3047
|
+
// event on the session scope — the
|
|
3048
|
+
// same channel the input-trigger
|
|
3049
|
+
// pipeline uses for plain-text
|
|
3050
|
+
// insert outcomes (the path skills
|
|
3051
|
+
// like /browser-skill ride, which is
|
|
3052
|
+
// why they leave a highlighted token).
|
|
3053
|
+
// We own the session actx (popupFor
|
|
3054
|
+
// accepted it), and the menu segment
|
|
3055
|
+
// carries the pick-time token span,
|
|
3056
|
+
// so replace that span with the full
|
|
3057
|
+
// "/cybernetics " text, exactly like
|
|
3058
|
+
// a text-insert pick outcome would.
|
|
3059
|
+
// Benign degrades: the listener is
|
|
3060
|
+
// absent (shell not mounted), the
|
|
3061
|
+
// span CAS misses (draft edited since
|
|
3062
|
+
// pick — the shell checks
|
|
3063
|
+
// span.draftRev), or bail throws — in
|
|
3064
|
+
// all cases nothing opens and we keep
|
|
3065
|
+
// the pre-v0.24.1 behavior (empty
|
|
3066
|
+
// draft) instead of crashing the menu.
|
|
3067
|
+
try {
|
|
3068
|
+
const span = segment.span;
|
|
3069
|
+
if (actx && typeof actx.bail === 'function'
|
|
3070
|
+
&& span && typeof span === 'object') {
|
|
3071
|
+
actx.bail(actx, 'slash/input-insert-text', {
|
|
3072
|
+
text: '/cybernetics ',
|
|
3073
|
+
span,
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
} catch { /* insertion refused → no token, popup stays closed */ }
|
|
3077
|
+
return undefined;
|
|
3078
|
+
}
|
|
3079
|
+
return openOriginal.call(controller, command, spec, context, segment);
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
return controller;
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
} catch { /* wrap failed → v0.23 menu behavior stays */ }
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
} catch { /* no commandUi → plain-text fallback stays */ }
|
|
3089
|
+
// v0.24.0 issue 3: after `/cybernetics use` (no id) executes, the
|
|
3090
|
+
// server's listing receipt IS the use intent — open the picker over
|
|
3091
|
+
// it. The event fires on the submitting client only (host contract:
|
|
3092
|
+
// other clients get the durable nodes, never this acknowledgment).
|
|
3093
|
+
try {
|
|
3094
|
+
if (typeof ctx.on === 'function') {
|
|
3095
|
+
ctx.effect(() => ctx.on('command/executed', (sessionId, commandName, result) => {
|
|
3096
|
+
if (commandName !== 'cybernetics') return;
|
|
3097
|
+
if (!result || result.kind !== 'success') return;
|
|
3098
|
+
if (typeof result.text !== 'string' || !result.text.startsWith(USE_LISTING_PREFIX)) return;
|
|
3099
|
+
openPlanPicker(sessionId);
|
|
3100
|
+
}), 'dsh-cybernetics: use-receipt preset picker');
|
|
3101
|
+
}
|
|
3102
|
+
} catch { /* no event face → the text receipt stays the fallback */ }
|
|
2819
3103
|
const modeIndicatorInject = (sessionId) => {
|
|
2820
3104
|
let mode = NO_MODE_FACE;
|
|
2821
3105
|
let goals = NO_MODE_FACE;
|
|
@@ -2861,6 +3145,7 @@ window.__ModuleLoader__.load({
|
|
|
2861
3145
|
// singleton) exported for the offline harness (test/v20-home-entry.mjs).
|
|
2862
3146
|
module.exports = {
|
|
2863
3147
|
name, inject, apply, countRunningGoals, countGoals, readHintLocation,
|
|
3148
|
+
usePopupRows,
|
|
2864
3149
|
homeSessionOf, requestHome, consumeHomeRequest,
|
|
2865
3150
|
};
|
|
2866
3151
|
return module.exports;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mars.liu/dsh-cybernetics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
36
36
|
"@deepseek-ai/dsh-client-ui-renderer",
|
|
37
37
|
"@deepseek-ai/dsh-client-ui-session",
|
|
38
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-commands"
|
|
39
40
|
],
|
|
40
41
|
"platform": "web",
|
|
41
42
|
"immediately": true
|
package/src/index.ts
CHANGED
|
@@ -36,7 +36,7 @@ import { startTeamRound, teamExecutorAvailable, teamExecutorUnavailableError } f
|
|
|
36
36
|
import { computeSmartSelection, probeCatalogOptions, sessionRouteOf } from './modelSelection.ts'
|
|
37
37
|
import type { SmartSelectionAudit } from './modelSelection.ts'
|
|
38
38
|
import {
|
|
39
|
-
PRESET_AUTO_ID, PRESET_RECOMMEND_TOOL_NAME, applyRecommendationAnswer, buildRecommendationBrief,
|
|
39
|
+
PRESET_AUTO_ID, PRESET_IDS, PRESET_RECOMMEND_TOOL_NAME, applyRecommendationAnswer, buildRecommendationBrief,
|
|
40
40
|
customPresetAsVendor, customPresetEntryId, formatRecommendationRecord, isCustomPresetId, parseCustomPresets,
|
|
41
41
|
presetById, presetChoiceLabel, vendorPresetApplyOps,
|
|
42
42
|
} from './modelPresets.ts'
|
|
@@ -1807,7 +1807,7 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1807
1807
|
// recent mode change; a multi-session host may briefly show the other
|
|
1808
1808
|
// session's hint until that session's own next /cybernetics invocation.
|
|
1809
1809
|
const commandHint = (session: unknown): string =>
|
|
1810
|
-
enabledFor(session) ? '[off|status]' : '[on|status]'
|
|
1810
|
+
enabledFor(session) ? '[off|status|use]' : '[on|status|use]'
|
|
1811
1811
|
let commandDisposer: (() => void) | undefined
|
|
1812
1812
|
const handler = (invocation: any) => {
|
|
1813
1813
|
const agent = invocation?.agent
|
|
@@ -1815,7 +1815,11 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1815
1815
|
if (session === undefined || typeof session.append !== 'function') {
|
|
1816
1816
|
return { kind: 'error' as const, text: 'cybernetics: 会话不可用,无法写入模式事件。' }
|
|
1817
1817
|
}
|
|
1818
|
-
|
|
1818
|
+
// Case-insensitive subcommand dispatch (v0.9.0); the `use` preset id
|
|
1819
|
+
// keeps its original case — custom preset ids are user-chosen and
|
|
1820
|
+
// case-sensitive (`custom:<id>` matches the customPresets entry verbatim).
|
|
1821
|
+
const rawInput = (invocation.rawInput ?? '').trim()
|
|
1822
|
+
const arg = rawInput.toLowerCase()
|
|
1819
1823
|
const override = modeOverrideOf(ctx, session)
|
|
1820
1824
|
const globalOn = allGoalsMode()
|
|
1821
1825
|
if (arg === 'on' || arg === 'off') {
|
|
@@ -1840,6 +1844,59 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1840
1844
|
: 'cybernetics 控制已对本会话关闭:不再注入控制 guidance,硬门禁放行,新输入回归 DSH 原生行为。/cybernetics on 可恢复。',
|
|
1841
1845
|
}
|
|
1842
1846
|
}
|
|
1847
|
+
// --- `use` subcommand (v0.22.0): switch the role composition (task/
|
|
1848
|
+
// verify/triage model + reasoning effort) to any preset plan — the
|
|
1849
|
+
// built-in registry ids (PRESET_IDS) or a user-saved custom preset
|
|
1850
|
+
// (`custom:<id>`). Reuses the SAME apply pipeline as the settings-card
|
|
1851
|
+
// dropdown (handlePresetSelection, same closure) as fire-and-forget;
|
|
1852
|
+
// lastHandledPresetPlan is kept in sync here so the one-shot volatile
|
|
1853
|
+
// guard stays coherent (a later loader update with the same value is a
|
|
1854
|
+
// no-op) while repeated invocations can re-apply the same plan.
|
|
1855
|
+
if (arg === 'use' || arg.startsWith('use ')) {
|
|
1856
|
+
const planArg = rawInput.slice(3).trim()
|
|
1857
|
+
const listPlans = (): string[] => {
|
|
1858
|
+
const lines = PRESET_IDS.map((id) => `- ${id}:${presetChoiceLabel(id)}`)
|
|
1859
|
+
try {
|
|
1860
|
+
for (const p of parseCustomPresets(readConfigField(config.customPresets, ''))) {
|
|
1861
|
+
lines.push(`- custom:${p.id}:${p.name}(自定义方案)`)
|
|
1862
|
+
}
|
|
1863
|
+
} catch { /* readConfigField face absent → built-ins only */ }
|
|
1864
|
+
return lines
|
|
1865
|
+
}
|
|
1866
|
+
if (planArg === '') {
|
|
1867
|
+
// No argument: plain-text listing (the command channel has no
|
|
1868
|
+
// picker UI); the user confirms with a second invocation.
|
|
1869
|
+
return {
|
|
1870
|
+
kind: 'success' as const,
|
|
1871
|
+
text:
|
|
1872
|
+
'cybernetics 可用预置方案:\n' + listPlans().join('\n') +
|
|
1873
|
+
'\n再次输入 /cybernetics use <id> 应用其中一个方案。',
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
const isCustom = isCustomPresetId(planArg)
|
|
1877
|
+
const customEntryExists = isCustom && (() => {
|
|
1878
|
+
try {
|
|
1879
|
+
return parseCustomPresets(readConfigField(config.customPresets, '')).some((p) => p.id === customPresetEntryId(planArg))
|
|
1880
|
+
} catch { return false }
|
|
1881
|
+
})()
|
|
1882
|
+
const known = isCustom ? customEntryExists : (planArg === PRESET_AUTO_ID || presetById(planArg) !== undefined)
|
|
1883
|
+
if (!known) {
|
|
1884
|
+
return {
|
|
1885
|
+
kind: 'error' as const,
|
|
1886
|
+
text:
|
|
1887
|
+
`cybernetics: 未知预置方案 "${planArg}"。可用 id:\n` + listPlans().join('\n') +
|
|
1888
|
+
'\n输入 /cybernetics use <id> 应用其中一个方案。',
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
lastHandledPresetPlan = planArg
|
|
1892
|
+
void handlePresetSelection(planArg)
|
|
1893
|
+
return {
|
|
1894
|
+
kind: 'success' as const,
|
|
1895
|
+
text:
|
|
1896
|
+
`cybernetics: 正在应用预置方案 "${planArg}"(${presetChoiceLabel(planArg)})——` +
|
|
1897
|
+
'task/verify/triage 的模型与思考强度将写入插件配置(全局生效,影响所有并发会话)。结果见设置卡片的结果记录。',
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1843
1900
|
// No argument (or unknown text) = status report.
|
|
1844
1901
|
const effective = override === null ? globalOn : override
|
|
1845
1902
|
const overrideText = override === null ? '未设置(遵循全局配置)' : override ? 'on' : 'off'
|
|
@@ -1852,7 +1909,7 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1852
1909
|
`- 硬门禁 hardGate:${hardGate() ? (resolveGateMode(config, true) === 'allowlist' ? 'on(allowlist)' : 'on(denylist)') : 'off'}\n` +
|
|
1853
1910
|
`- 新输入策略 inputPolicy:${readConfigField(config.inputPolicy, 'insert')}\n` +
|
|
1854
1911
|
`- 轮完成通知策略 noticePolicy:${readConfigField(config.noticePolicy, 'insert')}\n` +
|
|
1855
|
-
`用法:/cybernetics on | /cybernetics off | 无参数或 status =
|
|
1912
|
+
`用法:/cybernetics on | /cybernetics off | 无参数或 status = 显示状态 | /cybernetics use [方案id] = 应用预置方案(无参数列出全部可用方案)`,
|
|
1856
1913
|
}
|
|
1857
1914
|
}
|
|
1858
1915
|
const registerCommand = (session: unknown): void => {
|
|
@@ -1861,7 +1918,7 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1861
1918
|
} catch { /* a failed unregister must not block the re-register below */ }
|
|
1862
1919
|
commandDisposer = commandCtx.commands.register({
|
|
1863
1920
|
name: 'cybernetics',
|
|
1864
|
-
description: 'Toggle cybernetics control loop (ALL-GOALS routing + hard gate) for THIS session; no argument shows status',
|
|
1921
|
+
description: 'Toggle cybernetics control loop (ALL-GOALS routing + hard gate) for THIS session; no argument shows status; `use [id]` applies a preset plan (no id lists all)',
|
|
1865
1922
|
input: { hint: commandHint(session) },
|
|
1866
1923
|
handler,
|
|
1867
1924
|
})
|
package/src/modelPresets.ts
CHANGED
|
@@ -113,6 +113,8 @@ export const PRESET_IDS = [
|
|
|
113
113
|
'volcano-coding-plan',
|
|
114
114
|
'minimax',
|
|
115
115
|
'glm',
|
|
116
|
+
'builtin-glm-light',
|
|
117
|
+
'builtin-deepseek-fast',
|
|
116
118
|
] as const
|
|
117
119
|
|
|
118
120
|
export type PresetId = (typeof PRESET_IDS)[number]
|
|
@@ -527,6 +529,50 @@ export const VENDOR_PRESETS: VendorPreset[] = [
|
|
|
527
529
|
note: '命名角色(frontend 等)的 glm-5.3 max effort 只按需生效(智能选择命中难 subgoal 时),避免全量 max;verify 恒 glm-5.3 max,成本不构成降级理由。',
|
|
528
530
|
sessionNote: PRESET_SESSION_NOTE,
|
|
529
531
|
},
|
|
532
|
+
{
|
|
533
|
+
id: 'builtin-glm-light',
|
|
534
|
+
label: '轻量级GLM',
|
|
535
|
+
description: '用户自定义轻量 GLM 组合(provider 一律 zai-coding-cn):task glm-5.3-flash / verify glm-5.3(effort medium) / triage glm-5.3-flash(effort minimal)。任务以 flash 为主、verify 只做中档强化,最小化单轮成本。',
|
|
536
|
+
status: 'ready',
|
|
537
|
+
roles: {
|
|
538
|
+
task: { provider: 'zai-coding-cn', model: 'glm-5.3-flash', rationale: '量最大的角色用 Flash:成本低、速度快。' },
|
|
539
|
+
verify: { provider: 'zai-coding-cn', model: 'glm-5.3', reasoningEffort: 'medium', rationale: 'verify 升到 glm-5.3 中档 effort,以档位差补偿同家族独立性。' },
|
|
540
|
+
triage: { provider: 'zai-coding-cn', model: 'glm-5.3-flash', reasoningEffort: 'minimal', rationale: '轻量分拣走 minimal effort,压成本与 wall-clock。' },
|
|
541
|
+
},
|
|
542
|
+
variants: [{
|
|
543
|
+
id: 'standard',
|
|
544
|
+
label: '标准组合',
|
|
545
|
+
roles: {
|
|
546
|
+
task: { provider: 'zai-coding-cn', model: 'glm-5.3-flash', rationale: 'Flash 执行主力。' },
|
|
547
|
+
verify: { provider: 'zai-coding-cn', model: 'glm-5.3', reasoningEffort: 'medium', rationale: 'verify glm-5.3 medium:轻量场景下的审计底线。' },
|
|
548
|
+
triage: { provider: 'zai-coding-cn', model: 'glm-5.3-flash', reasoningEffort: 'minimal', rationale: '轻量分拣 minimal。' },
|
|
549
|
+
},
|
|
550
|
+
}],
|
|
551
|
+
note: '源自用户自定义方案「轻量级GLM」的内置化副本(roles 照抄,id 固定为 builtin-glm-light);provider zai-coding-cn 需部署侧具备对应渠道方可应用。',
|
|
552
|
+
sessionNote: PRESET_SESSION_NOTE,
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
id: 'builtin-deepseek-fast',
|
|
556
|
+
label: 'deepseek 快速',
|
|
557
|
+
description: '用户自定义 DeepSeek 快速组合:task deepseek-official/deepseek-flash / verify deepseek-official/deepseek-flash(effort high) / triage deepseek-account/deepseek-flash(effort low)。全 Flash 档拉满速度,verify 以 high effort 补偿同模型独立性。',
|
|
558
|
+
status: 'ready',
|
|
559
|
+
roles: {
|
|
560
|
+
task: { provider: 'deepseek-official', model: 'deepseek-flash', rationale: 'Flash 档执行主力:速度优先。' },
|
|
561
|
+
verify: { provider: 'deepseek-official', model: 'deepseek-flash', reasoningEffort: 'high', rationale: 'verify 同模型但开 high effort,以思考深度补偿独立性损失。' },
|
|
562
|
+
triage: { provider: 'deepseek-account', model: 'deepseek-flash', reasoningEffort: 'low', rationale: '轻量分拣 low 档(deepseek-account 为同款另一计费渠道)。' },
|
|
563
|
+
},
|
|
564
|
+
variants: [{
|
|
565
|
+
id: 'standard',
|
|
566
|
+
label: '标准组合',
|
|
567
|
+
roles: {
|
|
568
|
+
task: { provider: 'deepseek-official', model: 'deepseek-flash', rationale: 'Flash 执行主力。' },
|
|
569
|
+
verify: { provider: 'deepseek-official', model: 'deepseek-flash', reasoningEffort: 'high', rationale: 'verify high effort 审计。' },
|
|
570
|
+
triage: { provider: 'deepseek-account', model: 'deepseek-flash', reasoningEffort: 'low', rationale: '轻量分拣。' },
|
|
571
|
+
},
|
|
572
|
+
}],
|
|
573
|
+
note: '源自用户自定义方案「deepseek 快速」的内置化副本(roles 照抄,id 固定为 builtin-deepseek-fast);triage 走 deepseek-account 渠道,需部署侧具备 deepseek-official / deepseek-account 两个 provider 方可应用。',
|
|
574
|
+
sessionNote: PRESET_SESSION_NOTE,
|
|
575
|
+
},
|
|
530
576
|
]
|
|
531
577
|
|
|
532
578
|
// Normalize the mirror invariant: a READY preset's top-level `roles` IS its
|