@mars.liu/dsh-cybernetics 0.21.0 → 0.25.1

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 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
@@ -170,8 +170,10 @@ goal 一经存在不再原地修改。`revise` 是唯一的改目标动词(必
170
170
  **作用范围(用户裁定,最高优先)**:**session agent 不在本功能范围内**——session 模型由用户随时自行调整,因此选择方案与自动推荐只写三个子 agent 角色的配置:`src/modelPresets.ts` 的 `ALLOWED_OP_PATHS` 把可写字段结构性地钉死为六个扁平子 agent 字段(三个模型字段 + 三个思考强度字段);推荐 prompt 明令输出只允许 task/verify/triage 三个键、禁止 session 键(解析器对模型万一直出的 session 键丢弃并显式注明);各厂商的 session 推荐仅作参考数据保留在注册表(`variant.session`),卡片上常驻「session 由用户自行管理,方案不应用此项」提示,永远不会进入任何写入。
171
171
 
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
- - **自动推荐(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 工具一同保留。
173
+ - **自动推荐(v0.18.2 起 = session agent 交办,用户裁定)**:选择 auto 时插件**不发起任何独立 LLM 调用、不新起任何子 agent**(v0.17.2 及之前的「一次一次性 LLM 调用 + 推荐所用模型配置项」已删除——另起一次调用等于把上下文重复提交一遍,时间与经济性都不合理)。现在的流程:插件探测实时目录(`probeCatalogOptions`,与智能选择同源),把推荐交办单——内嵌目录(唯一可选来源)、跨厂商通用原则(task/verify/triage 三角色可灵活配置,允许同款模型或任意档位组合;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,69 @@
4
4
  > 当下的功能与使用方式以 [README.md](../README.md) 为准;协议细节见 [protocol.md](protocol.md)。
5
5
  > 约定:`src/` 改动需宿主重载生效;`lib/client.js` 改动需重启宿主(客户端模块图在宿主启动时构建)并刷新浏览器;`workflows/round.js` 改动热生效。
6
6
 
7
+ ## v0.25.1(2026-11-10):去除 verify≥task 且跨家族独立的硬约束,角色配置改为灵活权衡
8
+
9
+ **变更**:删除「verify 档位不得低于 task 且与 task 保持跨代际/跨家族独立(同家族 verify 升高一档补偿)」这条硬约束。保留的底线仅两条:triage 选便宜快速的模型;verify 不能过度自信(优先判断力强、能承认不确定性的模型)。task/verify/triage 三个角色改为可灵活配置——允许同款模型或任意档位组合,由使用者/推荐者按任务需要权衡审计增益与成本。同步修改:`src/modelPresets.ts` 的 `CROSS_VENDOR_PRINCIPLES` 与推荐 system prompt 规则第 2 条(reasoningEffort 规则第 5 条改为「verify 建议 high,可按需调整」)、绑定被删约束的厂商 rationale 措辞改为中性描述(预置方案本身不变)、`test/v17-model-presets.mjs` 断言更新(新原则存在、旧硬约束与「跨家族独立」不存在)、README 预置方案章节描述同步。
10
+
11
+ ## v0.25.0(2026-11-08):「输入 `/cybernetics use` 未回车即弹方案选择器」调研结论——协议内不可行;弹层文案与文档明确「按 Enter 唤起」
12
+
13
+ **背景(用户新诉求)**:v0.24.1 之后用户反馈:当前 `/cybernetics use` 必须**按 Enter** 才弹方案列表;希望输入到「/cybernetics use 」(以空格结尾、尚未回车)时就自动弹出。本轮做只读调研并尽力实现;若协议不支持则明确结论并在文档/弹层文案提示按 Enter。
14
+
15
+ - **调研结论:协议内不可行**(宿主源码证据,本轮全量复核):
16
+ 1. **唯一的草稿观察器在遇空白时自杀**:斜杠触发扫描 `detectTrigger`(`@deepseek-ai/dsh-client-ui-input-trigger/lib/client.js` 约 776–810 行)从光标向左扫,`if (WHITESPACE.test(ch)) return null;`——只要光标与 `/` 之间存在任何空白,触发即死、菜单关闭;「/cybernetics use␣」这个草稿态不存在任何活的 `/` 触发。
17
+ 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` 无任何回调。
18
+ 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 之外的草稿面。
19
+ 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 行)。
20
+ 5. **无轮询面**:草稿文本只存在于 composer 输入机私有状态(`this.projection.detectText` / `this.rev`,conversation `lib/client.js` 输入机区域),不在 session scope 的任何公开 face/projection 上——定时轮询 scope 无从读到草稿。
21
+ - **落地(零逻辑改动)**:`lib/client.js` 弹层搜索框占位文案(zh/en)加提示「输入 /cybernetics use 后按 Enter 唤起本列表」;README.md / README.zh-CN.md `use` 段落补全调研证据与显式指引;版本顺延 0.25.0(用户可见文案变更)。
22
+ - **生效手段**:`lib/client.js` + `package.json` 改动需重启宿主并刷新浏览器。
23
+ - **保留路径**:现状三条弹层触发(菜单点选=补全、裸回车=弹层、`use` 清单回执后弹层)全部不变;若宿主未来把 `matchEnter` 的 `line`(完整草稿)类观察能力或草稿变更事件开放为公开契约,可再评估。
24
+
25
+
26
+
27
+ ## v0.24.1(2026-11-08):修复菜单点选后草稿无 token——经 `slash/input-insert-text` 作用域事件补全 `/cybernetics ` 文本
28
+
29
+ **背景(v0.24.0 活体验证反馈)**:点选 `/` 菜单里的 `\cybernetics`(或列表选中后回车)后,编辑器**什么都没留下**——而点选 skills(如 `\browser-skill`)会留下高亮的完整 token。v0.24.0 的设计假设「菜单已把 token 放进草稿」被证伪。
30
+
31
+ - **根因(宿主源码证据,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 只是不弹层,草稿停留在用户敲入的部分查询上。
32
+ - **修复(`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` 抛错——一律保持弹层关闭的旧行为,不炸菜单。
33
+ - **不变项**:裸回车弹层(`via:'enter'` 透传)、`\cybernetics use` 清单回执弹层(`command/executed` 路径带 `via:'enter'` 合成段,绕过 menu 守卫)、键入路径与降级兜底全部原样。
34
+ - **测试**:`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` 通过。
35
+ - **生效手段**:`lib/client.js` + `package.json` 改动需重启宿主 + 刷新浏览器。
36
+ - **待活体验证**:真实 GUI 中点选菜单项后编辑器是否留下高亮 `/cybernetics` token(span CAS 依赖点选与 settle 同步完成,协议上应必然成立;若宿主版本行为有差,需以真实草稿复验)。
37
+
38
+ ## v0.24.0(2026-11-08):popupSelect 交互时机修正——菜单点选=补全文本、裸回车=弹层、`use` 无参回执后弹层
39
+
40
+ **背景(用户活体验证 v0.23 后的三处反馈)**:①/ 菜单点选 `\cybernetics` 时立即弹方案选择器——太早,用户连 `use` 都没输;②方案列表应在表达 `use` 意图后才出现;③手动键入 `\cybernetics use` 回车后没有可操作的方案选择路径。
41
+
42
+ - **契约复核(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,而它被装饰抢占。
43
+ - **实现(`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 已消亡则静默跳过,纯文本回执即兜底)。
44
+ - **交互形态(协议内可达的最优)**:菜单点选 → 编辑器补全 `cybernetics`(不弹层);键入 Space 或裸回车 → 弹层(裸回车路径不变);`\cybernetics use` 回车 → 服务端清单回执落地后弹层出现,点选即应用。草稿内 `use ` 之后的 inline 参数补全仍属协议不可达(v0.23 结论不变)。
45
+ - **测试**:`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` 通过。
46
+ - **生效手段**:`lib/client.js` + `package.json` 改动需重启宿主 + 刷新浏览器;`src/` 未改动。
47
+ - **待活体验证**:真实 GUI 中确认菜单点选后草稿保留 `/cybernetics` 文本(token 文本的精确拼写由宿主输入侧决定);`\cybernetics use` 回车后弹层在清单回执节点之上正常出现且点选可用。
48
+
49
+ ## v0.23.0(2026-11-08):`/cybernetics` 输入侧预置方案选择器——popupSelect 装饰(编辑器下拉选方案)
50
+
51
+ **背景(用户新需求)**:v0.22 的 `use` 子命令只能靠纯文本回执选方案。用户希望像 @file 那样在编辑器输入 `\cybernetics use` 时弹出可点选的下拉列表。本轮先做只读调研,再按调研结论实现。
52
+
53
+ - **调研结论(宿主源码证据)**:①斜杠触发扫描从光标向左**遇空白即止**(`@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。
54
+ - **实现(`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 纯文本清单兜底保留。
55
+ - **与需求的偏差(诚实记录)**:宿主协议下「输入 `use ` 即在草稿内弹参数补全」不可实现(证据 ①);本版实现的是宿主原生等价体验——输入 `/cybernetics` 触发弹层、搜索过滤、点选**直接应用**方案(而非仅补全草稿文本),效果上跳过了「补全后再回车」一步。
56
+ - **测试**:新增 `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 复验)。
57
+ - **生效手段**:`lib/client.js` + `package.json` 改动需重启宿主 + 刷新浏览器;`src/` 未改动。
58
+
59
+
60
+ ## v0.22.0(2026-11-08):`/cybernetics use` 子命令——命令通道直接应用预置方案(快速切换角色组合)
61
+
62
+ **背景(用户裁定:子命令名 `use`,早前方案的 `profile` 命名作废)**:预置方案此前只能经设置卡片下拉选择(volatile `presetPlan` + `loader/volatile-update` 一次性解析);要在会话里快速切换 task/verify/triage 的模型与思考强度,得先打开卡片。本版把同一应用管线开放到 `/cybernetics` 命令通道。
63
+
64
+ - **命令分支(`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 + 结果记录)。
65
+ - **守卫同步与重复应用**:handler 同步维护 `lastHandledPresetPlan`——volatile 一次性守卫保持一致(此后 loader 带同值不会重复应用),同时命令路径刻意绕过该守卫:重复输入同一条 `use <id>` 会再次应用,无需先切「(未选择)」。命令路径**不写** volatile `presetPlan` 字段。`auto` 分支同样经 `handlePresetSelection('auto')`(pending 记录 + session agent 交办,零独立 LLM 调用)。
66
+ - **命令引导**:动态 input hint 更新为 `[on|status|use]` / `[off|status|use]`(`profile` 命名全面移除);status 用法行与命令 description 补 `use` 说明。
67
+ - **测试**:新增 `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` 仅既有基线零新增。
68
+ - **生效手段**:`src/` 改动宿主重载生效。风险:命令应用的方案与卡片写入同路径——**profile 全局生效**,影响所有并发会话;`auto` 分支会向在线顶层 session agent 投递交办消息。
69
+
7
70
  ## v0.21.0(2026-11-07):轮次生效模型审计——round-cyb-*.json 记录 task/verify/triage 实际调用的 provider/model/effort,board 快照镜像 models 字段
8
71
 
9
72
  **背景(用户裁定,§7.12.1)**:历史全部 213 个 `round-cyb-*.json` 均无结构化 model/reasoningEffort 字段,board 快照只有 `{goals, savedAt, sessionId, version}`——「每个角色实际跑在什么模型上」跨轮不可审计。插件既有 taskAgentModel/verifyAgentModel/triageAgentModel 配置(v0.8.x 调用级回退、v0.15 思考强度),本轮把生效值落进轮审计与 board 快照。
@@ -137,7 +200,7 @@
137
200
  - **注册表填全 8 家厂商(全 `status: 'ready'`)**:OpenAI(gpt-6.1-sol task/verify + gpt-6-luna triage,effort 五档)、Claude(常规/高要求两变体,verify 恒高一档补偿单家族)、Gemini(家族 + 最新版本运行时解析,不硬编码旧版本号)、DeepSeek(仅本家/verify 外置两变体,verify thinking 档,「成本不应成为降级 verify 的理由」)、Kimi(长期组合/最高验收质量两变体,避开退役 K2)、Qwen(A/B/C 三变体,⚠️ verify 绝不用 Turbo——幻觉共振)、火山 Coding Plan(A/B 两变体,Seed 池 + GLM-5.3 备选边界)、Minimax(M3.1-Flash high/xhigh + M3 verify,缓存读 4 倍差价备注)。每厂商:多变体(`variants`,`roles` 恒为默认变体的同一对象引用)、逐角色 effort token/可升级档/fallback/rationale、厂商 note、session 参考(`variant.session`)+ 常量 `PRESET_SESSION_NOTE`。
138
201
  - **session 范围结构性钉死**:`ALLOWED_OP_PATHS` = 恰好六个扁平子 agent 字段(task/verify/triage × model/effort),所有 apply-ops 构建器经 `filterAllowedOps` 过滤;推荐 prompt 明令「输出 JSON 只允许 task/verify/triage 三个键,禁止 session 键」;`parseRecommendation` 对万一直出的 session 键丢弃并置 `ignoredSession`;厂商 session 数据只存 `variant.session`,卡片常驻「session 由用户自行管理」提示——任何代码路径都触不到 session 配置。
139
202
  - **effort 全链路**:`SettingsOp` 扩展 `unset`(角色换模型时清掉旧档位回模型默认);厂商方案按变体配置写入 effort token(异构:五档/thinking/默认),`recommendationApplyOps` 携带 AI 推荐的 effort;自动推荐的 effort 经 `validatePickEfforts` 软校验(`ctx.llm.resolveModelInfo`,不支持 → 丢档保模型 + 注明;调用面缺失/抛错 → 保留不阻断)。
140
- - **跨厂商通用原则注入推荐 prompt**(仅作用于 task/verify/triage):verify 档位不低于 task;跨代际/跨家族独立(同家族时 verify 升高一档补偿);verify 不能过度自信;省成本先压 task/triage、verify 最后动、成本不是降级 verify 的理由;triage 轻量。
203
+ - **跨厂商通用原则注入推荐 prompt**(仅作用于 task/verify/triage,v0.25.1 起为灵活权衡表述):task/verify/triage 三角色可灵活配置(允许同款模型或任意档位组合);verify 不能过度自信;省成本先压 task/triage、verify 最后动、成本不是降级 verify 的理由;triage 轻量。
141
204
  - **结果记录增润**:`PresetRecommendationRecord` 增加 `efforts`/`variant`/`otherVariants`/`notes`(session 范围声明 + 厂商 ⚠️ 备注 + effort 校验注记),卡片渲染逐角色「模型(effort X | 模型默认)」+ 变体 + 备注。
142
205
  - **顺手修复(本轮 typecheck 发现的既有缺口)**:Config 接口补齐 `presetPlan`/自动推荐请求路由字段/`presetRecommendation` 三个字段(schema 有、接口漏);`controller.ts` 的 `startRound`/`runRound` `parent: unknown` → `Agent`(`@deepseek-ai/dsh-workflow` 的 `WorkflowStartRequest.parent` 本就要求 Agent 类型,types 可解析时 strict 检查暴露);`tsc --noEmit` 现已零错误(仅存代码库级 TS5097 风格噪声)。
143
206
  - **测试**:`test/v17-model-presets.mjs` 全量重写(接替占位版)——注册表完整性(9 id、8 家全 ready、变体数对表、roles===variants[0].roles 同一性、session 仅存 variant.session)、请求构建(session 禁令/原则/effort 契约/目录 200 行上限)、解析(fenced/裸模型/`{"task":42}` 形状容忍/session 键忽略/effort 形状丢弃)、effort 软校验三态、apply-ops 六字段围栏 + `filterAllowedOps`、全注册表变体应用(显式/未知 variantId 回退默认、effort set/unset、session 数据零泄漏)、`recommendModels` 端到端(ok 带 notes/全降级带原因与请求)、插件处理器 7 场景(one-shot、记录增润、降级、未知 id、启动种子不重放、清除零写入、新选择触发)、设置卡片(9+1 选项镜像服务端 id 序、session 提示、记录渲染含 effort 后缀/变体/备注、volatile 写路径、zh/en 26 个 locale key 完整)。复跑 `node test/v17-model-presets.mjs` 全过;v03(75 checks)/v09–v16 全绿。
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.21.0",
3
+ "version": "0.25.1",
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
- const arg = (invocation.rawInput ?? '').trim().toLowerCase()
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
  })
@@ -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]
@@ -189,8 +191,7 @@ export const PRESET_SESSION_NOTE = 'session agent 由用户自行管理:方案
189
191
 
190
192
  /** The cross-vendor principles injected into the auto prompt (sub-agents only). */
191
193
  export const CROSS_VENDOR_PRINCIPLES: string[] = [
192
- 'verify 的模型档位不得低于 task(审计岗至少与执行岗同级)。',
193
- 'verify 与 task 保持跨代际/跨家族独立,避免同一模型自我确认偏误;确需同家族时 verify 升高一档补偿。',
194
+ 'verify 与 task 可灵活配置(允许同款模型或任意档位组合),由使用者按任务需要权衡审计增益与成本。',
194
195
  'verify 不能过度自信:优先判断力强、能承认不确定性的模型。',
195
196
  '需要省成本时先压缩 task/triage,verify 最后动;成本不应成为降级 verify 的理由。',
196
197
  'triage 是轻量收敛分拣,选便宜快速的模型即可。',
@@ -233,7 +234,7 @@ export const VENDOR_PRESETS: VendorPreset[] = [
233
234
  verify: {
234
235
  provider: 'openai', model: 'gpt-6.1-sol', reasoningEffort: 'high',
235
236
  upgradeTo: 'gpt-6.1-sol (xhigh)',
236
- rationale: 'verify 与 task 同模型但高一档 effort,以思考深度补偿同家族独立性损失。',
237
+ rationale: 'verify 与 task 同模型但高一档 effort,以更强思考深度增强审计判断力。',
237
238
  },
238
239
  triage: {
239
240
  provider: 'openai', model: 'gpt-6-luna', reasoningEffort: 'low',
@@ -246,7 +247,7 @@ export const VENDOR_PRESETS: VendorPreset[] = [
246
247
  label: '标准组合',
247
248
  roles: {
248
249
  task: { provider: 'openai', model: 'gpt-6.1-sol', reasoningEffort: 'medium', upgradeTo: 'gpt-6.1-sol (high/xhigh)', rationale: '主力执行模型,effort 五档可调。' },
249
- verify: { provider: 'openai', model: 'gpt-6.1-sol', reasoningEffort: 'high', upgradeTo: 'gpt-6.1-sol (xhigh)', rationale: 'verify 高一档 effort 补偿同家族。' },
250
+ verify: { provider: 'openai', model: 'gpt-6.1-sol', reasoningEffort: 'high', upgradeTo: 'gpt-6.1-sol (xhigh)', rationale: 'verify 高一档 effort 增强审计判断力。' },
250
251
  triage: { provider: 'openai', model: 'gpt-6-luna', reasoningEffort: 'low', upgradeTo: 'gpt-6-luna (medium)', rationale: '轻量分拣。' },
251
252
  },
252
253
  session: { provider: 'openai', model: 'gpt-6-astra', reasoningEffort: 'high', upgradeTo: 'gpt-6-astra (xhigh/max)', rationale: 'session 主脑用旗舰 astra(仅参考)。' },
@@ -262,7 +263,7 @@ export const VENDOR_PRESETS: VendorPreset[] = [
262
263
  status: 'ready',
263
264
  roles: {
264
265
  task: { provider: 'anthropic', model: 'claude-sonnet-5-5', rationale: '常规档 task 用 Sonnet 5.5(claude-sonnet-5-5):性价比主力。' },
265
- verify: { provider: 'anthropic', model: 'claude-opus-5-5', fallback: 'claude-fable-5-1', rationale: 'verify 高一档(Opus 5.5)补偿单家族独立性损失。' },
266
+ verify: { provider: 'anthropic', model: 'claude-opus-5-5', fallback: 'claude-fable-5-1', rationale: 'verify 配置更强的模型(Opus 5.5)以增强审计判断力。' },
266
267
  triage: { provider: 'anthropic', model: 'claude-haiku-5-5', rationale: '轻量分拣。' },
267
268
  },
268
269
  variants: [
@@ -271,7 +272,7 @@ export const VENDOR_PRESETS: VendorPreset[] = [
271
272
  label: '常规',
272
273
  roles: {
273
274
  task: { provider: 'anthropic', model: 'claude-sonnet-5-5', rationale: '性价比主力执行。' },
274
- verify: { provider: 'anthropic', model: 'claude-opus-5-5', fallback: 'claude-fable-5-1', rationale: 'verify 高一档补偿单家族。' },
275
+ verify: { provider: 'anthropic', model: 'claude-opus-5-5', fallback: 'claude-fable-5-1', rationale: 'verify 高一档增强审计判断力。' },
275
276
  triage: { provider: 'anthropic', model: 'claude-haiku-5-5', rationale: '轻量分拣。' },
276
277
  },
277
278
  session: { provider: 'anthropic', model: 'claude-opus-5-5', rationale: 'session 主脑用 Opus 5.5(仅参考)。' },
@@ -341,7 +342,7 @@ export const VENDOR_PRESETS: VendorPreset[] = [
341
342
  label: 'verify 外置',
342
343
  roles: {
343
344
  task: { provider: 'deepseek-official', model: 'deepseek-v4-pro', fallback: 'deepseek-v4-pro (thinking)', rationale: '本家 task 不变。' },
344
- verify: { provider: 'anthropic', model: 'claude-opus-5-5', fallback: 'gpt-6-astra / gemini-pro-latest', rationale: 'verify 优先外置旗舰(Claude Opus / GPT 旗舰 / Gemini Pro),跨家族独立性最大化。' },
345
+ verify: { provider: 'anthropic', model: 'claude-opus-5-5', fallback: 'gpt-6-astra / gemini-pro-latest', rationale: 'verify 优先外置旗舰(Claude Opus / GPT 旗舰 / Gemini Pro),配置更强的模型以增强审计判断力。' },
345
346
  triage: { provider: 'deepseek-official', model: 'deepseek-flash', rationale: '轻量分拣。' },
346
347
  },
347
348
  session: { provider: 'deepseek-official', model: 'deepseek-v4-pro (非 thinking)', fallback: 'deepseek-flash', rationale: 'session 用 deepseek-v4-pro 非思考档或 deepseek-flash(仅参考)。' },
@@ -358,7 +359,7 @@ export const VENDOR_PRESETS: VendorPreset[] = [
358
359
  status: 'ready',
359
360
  roles: {
360
361
  task: { provider: 'kimi', model: 'kimi-for-coding-highspeed', rationale: '高速执行主力(本部署目录内的高速 coding 档)。' },
361
- verify: { provider: 'kimi', model: 'k3', fallback: 'k3-256k', rationale: 'verify 用上一代 k3:代际差异保持审计独立性;最高验收质量可换 k3-256k。' },
362
+ verify: { provider: 'kimi', model: 'k3', fallback: 'k3-256k', rationale: 'verify 用上一代 k3:配置差异化模型增强审计判断力;最高验收质量可换 k3-256k。' },
362
363
  triage: { provider: 'kimi', model: 'kimi-for-coding-highspeed', rationale: '轻量分拣同执行主力(高速版成本低)。' },
363
364
  },
364
365
  variants: [
@@ -518,7 +519,7 @@ export const VENDOR_PRESETS: VendorPreset[] = [
518
519
  label: '重活命名角色(frontend 等)',
519
520
  roles: {
520
521
  task: { provider: 'zai-coding-cn', model: 'glm-5.3', reasoningEffort: 'max', rationale: '官方建议复杂 coding 用 max;按需触发,只在难 subgoal 上生效。' },
521
- verify: { provider: 'zai-coding-cn', model: 'glm-5.3', reasoningEffort: 'max', rationale: 'verify 与 task 同级 max,同家族下以最高档补偿独立性损失。' },
522
+ verify: { provider: 'zai-coding-cn', model: 'glm-5.3', reasoningEffort: 'max', rationale: 'verify 与 task 同级 max,以最高思考档增强审计判断力。' },
522
523
  triage: { provider: 'zai-coding-cn', model: 'glm-5.3-flash', reasoningEffort: 'low', rationale: '轻量分拣不变。' },
523
524
  },
524
525
  session: { provider: 'zai-coding-cn', model: 'glm-5.3', reasoningEffort: 'high', rationale: 'session 同标准组合(仅参考)。' },
@@ -527,6 +528,50 @@ export const VENDOR_PRESETS: VendorPreset[] = [
527
528
  note: '命名角色(frontend 等)的 glm-5.3 max effort 只按需生效(智能选择命中难 subgoal 时),避免全量 max;verify 恒 glm-5.3 max,成本不构成降级理由。',
528
529
  sessionNote: PRESET_SESSION_NOTE,
529
530
  },
531
+ {
532
+ id: 'builtin-glm-light',
533
+ label: '轻量级GLM',
534
+ 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 只做中档强化,最小化单轮成本。',
535
+ status: 'ready',
536
+ roles: {
537
+ task: { provider: 'zai-coding-cn', model: 'glm-5.3-flash', rationale: '量最大的角色用 Flash:成本低、速度快。' },
538
+ verify: { provider: 'zai-coding-cn', model: 'glm-5.3', reasoningEffort: 'medium', rationale: 'verify 升到 glm-5.3 中档 effort,配置更强的模型以增强审计判断力。' },
539
+ triage: { provider: 'zai-coding-cn', model: 'glm-5.3-flash', reasoningEffort: 'minimal', rationale: '轻量分拣走 minimal effort,压成本与 wall-clock。' },
540
+ },
541
+ variants: [{
542
+ id: 'standard',
543
+ label: '标准组合',
544
+ roles: {
545
+ task: { provider: 'zai-coding-cn', model: 'glm-5.3-flash', rationale: 'Flash 执行主力。' },
546
+ verify: { provider: 'zai-coding-cn', model: 'glm-5.3', reasoningEffort: 'medium', rationale: 'verify glm-5.3 medium:轻量场景下的审计底线。' },
547
+ triage: { provider: 'zai-coding-cn', model: 'glm-5.3-flash', reasoningEffort: 'minimal', rationale: '轻量分拣 minimal。' },
548
+ },
549
+ }],
550
+ note: '源自用户自定义方案「轻量级GLM」的内置化副本(roles 照抄,id 固定为 builtin-glm-light);provider zai-coding-cn 需部署侧具备对应渠道方可应用。',
551
+ sessionNote: PRESET_SESSION_NOTE,
552
+ },
553
+ {
554
+ id: 'builtin-deepseek-fast',
555
+ label: 'deepseek 快速',
556
+ 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 增强审计判断力。',
557
+ status: 'ready',
558
+ roles: {
559
+ task: { provider: 'deepseek-official', model: 'deepseek-flash', rationale: 'Flash 档执行主力:速度优先。' },
560
+ verify: { provider: 'deepseek-official', model: 'deepseek-flash', reasoningEffort: 'high', rationale: 'verify 同模型但开 high effort,以更强思考深度增强审计判断力。' },
561
+ triage: { provider: 'deepseek-account', model: 'deepseek-flash', reasoningEffort: 'low', rationale: '轻量分拣 low 档(deepseek-account 为同款另一计费渠道)。' },
562
+ },
563
+ variants: [{
564
+ id: 'standard',
565
+ label: '标准组合',
566
+ roles: {
567
+ task: { provider: 'deepseek-official', model: 'deepseek-flash', rationale: 'Flash 执行主力。' },
568
+ verify: { provider: 'deepseek-official', model: 'deepseek-flash', reasoningEffort: 'high', rationale: 'verify high effort 审计。' },
569
+ triage: { provider: 'deepseek-account', model: 'deepseek-flash', reasoningEffort: 'low', rationale: '轻量分拣。' },
570
+ },
571
+ }],
572
+ note: '源自用户自定义方案「deepseek 快速」的内置化副本(roles 照抄,id 固定为 builtin-deepseek-fast);triage 走 deepseek-account 渠道,需部署侧具备 deepseek-official / deepseek-account 两个 provider 方可应用。',
573
+ sessionNote: PRESET_SESSION_NOTE,
574
+ },
530
575
  ]
531
576
 
532
577
  // Normalize the mirror invariant: a READY preset's top-level `roles` IS its
@@ -635,10 +680,10 @@ export function buildRecommendationPrompt(input: RecommendationRequestInput): Re
635
680
  '范围(最高优先):session agent(主会话 agent)不在推荐范围内,其模型由用户自行管理——输出 JSON 只允许 task/verify/triage 三个键,禁止输出 session 键。',
636
681
  '规则:',
637
682
  '1. 只能从「可用模型列表」中选择,逐字使用其中的 provider/model;列表外的模型一律禁止推荐。',
638
- '2. 通用原则:verify 的模型档位不得低于 task;verify 与 task 保持跨代际/跨家族独立(同一模型时 verify 升高一档补偿);verify 不能过度自信;省成本先压 task/triage、verify 最后动;成本不是降级 verify 的理由;triage 选便宜快速的模型。',
683
+ '2. 通用原则:task/verify/triage 三个角色可灵活配置,允许同款模型或任意档位组合,按任务需要权衡;verify 不能过度自信;省成本先压 task/triage、verify 最后动;成本不是降级 verify 的理由;triage 选便宜快速的模型。',
639
684
  '3. 某个角色如维持现状更好(例如当前配置已是最优、或列表中没有更好的选择),输出 null 保持现状。',
640
685
  '4. 「厂商预置方案」只是参考资料(各家产品思路),不是可选项——目录里没有的模型组合不要采纳。',
641
- '5. reasoningEffort 可选:仅当你确知该模型提供该思考强度档位时输出(例如 low/medium/high/xhigh/max 等),否则省略该字段;task 建议 medium/high,verify 至少 high,triage 建议 low。',
686
+ '5. reasoningEffort 可选:仅当你确知该模型提供该思考强度档位时输出(例如 low/medium/high/xhigh/max 等),否则省略该字段;task 建议 medium/high,verify 建议 high、可按需调整,triage 建议 low。',
642
687
  '6. 只输出一个 JSON 对象,不要解释、不要 markdown 代码块之外的文字。格式:',
643
688
  '{"task":{"provider":"<provider>","model":"<model>","reasoningEffort":"<可选>"}|null,"verify":...,"triage":...}',
644
689
  ].join('\n')