# dsh-plugin-model-dispatch [English](README.md) | 简体中文 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![DeepSeek Harness](https://img.shields.io/badge/DSH-0.2.0--rc.2-blueviolet.svg)](https://github.com/deepseek-ai/deepseek-harness) [![Platform: Web](https://img.shields.io/badge/Platform-Web-orange.svg)]() [![Tests](https://img.shields.io/badge/tests-69%20passing-brightgreen.svg)](tests) **给主模型、子代理和计划模式各配一个模型与思考强度——就在聊天栏里,一个菜单搞定。** ![输入框上方的模型菜单:模型列表、思考强度滑杆,以及输入框里的两个芯片](docs/images/menu-light.png) DeepSeek Harness 自带的是**主模型**选择器。本插件保留这个控件,补上它没覆盖的两条路由——**子代理**跑在哪个模型上、**计划模式**用哪个模型思考——并把三者放进同一个「看起来就是原生控件」的菜单里。 | 路由 | 设置入口 | 作用范围 | |---|---|---| | **主模型** | 聊天栏主芯片 · 设置页第 1 行 | 当前会话,走 DSH 原生切换接口 | | **子代理模型** | 聊天栏子代理芯片 · 设置页第 2 行 | 本会话**之后**新启动的子代理 | | **制定计划模型** | 设置页第 3 行 | 仅计划模式;退出计划后恢复进入前的主模型 | --- ## 目录 - [功能](#功能) - [安装](#安装) - [使用](#使用) - [聊天栏](#聊天栏) - [设置页](#设置页) - [计划模式](#计划模式) - [子代理](#子代理) - [路由规则](#路由规则) - [持久化](#持久化) - [兼容性](#兼容性) - [开发](#开发) - [故障排查](#故障排查) - [已知限制](#已知限制) - [致谢](#致谢) - [许可](#许可) ## 功能 - **两个芯片,穿的是宿主自己的材质。** 输入框里一个主模型芯片(宿主原生模型图标)、一个子代理芯片(分支图标),**只靠图标区分**,悬停与无障碍标签仍是完整的 `主模型 · 模型 · 强度`。两个都是无边框 28px 芯片,只在悬停时浮起底色,和旁边的原生控件完全一致。子代理处于继承状态时显示 **跟随主模型**,不重复主模型的名称。 ![输入框里的两个芯片](docs/images/composer-light.png) - **所有入口共用一个菜单。** 模型/提供方搜索、提供方分组、收藏、最近使用、思考强度——聊天栏和设置页用的是同一个菜单。它采用宿主菜单的材质(半透明模糊面板、抬升阴影、4px 内边距、34px 行高、13px 字号、l2 发丝分隔),看起来是一方控件而不是插件浮层。 - **思考强度用离散滑杆。** 每个档位一个刻度,最左额外多一档 **提供方默认**,让「不指定强度」成为看得见的状态而不是隐藏行为。刻度画在轨道**下方**(不会把蓝色填充切成一段段),当前档位名显示在标题行,支持拖拽与方向键。 ![思考强度滑杆](docs/images/effort-light.png) - **即选即生效,没有确认行。** 选模型、拖滑杆、恢复默认都是当场写入:拖动滑杆会在停手后合并成一次写入,写入串行化,失败会回滚该行并给出真实错误。菜单保持打开,选完模型可以直接接着调强度;`Esc` 或点击菜单外只是关掉菜单,不会撤销已经生效的选择。 - **计划模式自动切换。** 进入计划模式切到「制定计划」模型;批准计划或 `/plan off` 后,在**第一条执行请求组装之前**恢复进入计划前的主模型**与推理档位**。审批面板会显示将要恢复的模型。 - **按会话的子代理覆盖。** 聊天栏的子代理选择作为该会话的持久覆盖保存,绝不改动设置默认;一键即可恢复为设置默认。 - **全部模型都能委派。** 子代理路由读取宿主**实时**模型目录:即使 rc.2 在会话创建时捕获了旧的白名单,显式选模与 `list_subagent_models` 在已有会话里依然可用。之后新增的模型在下一次委派即可用;被移除的路由与不支持的强度会在保存前就被拒绝。 - **保存前校验路由。** 覆盖与创建子代理前,通过宿主 `resolveCallConfig` 校验 provider/model/effort 的真实组合;无效组合当场报出真实原因,而不是等子代理运行到一半才失败。 ## 安装 要求 **DeepSeek Harness 0.2.0-rc.2**(其他 0.2.x 版本需先做兼容性验证,见[兼容性](#兼容性))。 ```bash dsh plugin --profile web add git@github.com:kolawong/dsh-plugin-model-dispatch.git ``` 把 `web` 换成你实际使用的 profile(`desktop` 等)。安装后**完全退出并重新打开 DSH**:宿主半边需要重组合,只刷新页面只会加载新的客户端半边。 仓库里的 `lib/` 是已构建产物(宿主 + 客户端),安装无需本地构建。
手动安装(链接本地目录) ```jsonc // /profiles//package.json { "dependencies": { "dsh-plugin-model-dispatch": "link:/path/to/dsh-plugin-model-dispatch" }, "dsh": { "profile": { "bundles": ["…", "dsh-plugin-model-dispatch"] } } } ``` 在 profile 目录下建立对应的 `node_modules` 链接(profile 目录是解析锚点),然后重启 DSH。
## 使用 ### 聊天栏 输入框右侧有两个芯片,紧邻权限与预设控件: | 芯片 | 作用 | |---|---| | **主模型**(模型图标) | 走原生接口切换当前会话的模型;悬停提示里给出实际路由与强度。 | | **子代理**(分支图标) | 选择本会话之后新启动的子代理用哪个模型,存为会话覆盖;没有覆盖时显示 **跟随主模型**。 | 点开任意一个都是同一个菜单:搜索、提供方分组、收藏(★,悬停浮现,按浏览器记忆)、最近使用、思考强度滑杆,以及子代理路由专属的 **恢复为设置默认** 一行。 ![深色模式下的同一个菜单](docs/images/menu-dark.png) ### 设置页 **Web 设置 → 模型切换** 里有三行,每行一句作用范围说明: | 行 | 含义 | |---|---| | 新会话默认模型 | 写入原生 `agent-default-model` 项;在聊天里切换模型**不会**改动它。 | | 子代理默认模型 | 供没有会话覆盖的会话中的子代理使用。 | | 制定计划模型 | 计划模式激活期间使用。 | ### 计划模式 1. 进入计划模式时,原子保存当前会话的 `provider` / `model` / `reasoningEffort`,并切到制定计划模型。 2. 批准计划(或 `/plan off`)后,在**第一条执行请求组装前**恢复刚才保存的路由。 3. 恢复失败可重试,不会把会话卡在计划模型上;审批面板会写明将要恢复的模型。 ### 子代理 - 工具调用里显式指定了模型的委派保持原样——插件只补齐原生调用留空的那部分。 - 子代理芯片与设置默认只影响**新启动**的子代理;已在运行的保持原模型。 - 子代理会话本身不参与计划切换与子代理覆盖。 ## 路由规则 | 决策 | 优先级(从高到低) | |---|---| | 子代理模型 | 本会话覆盖 → 设置默认 → 原生父模型继承 → 工具调用里显式指定的启动参数 | | 计划模型 | 计划模式激活期间用制定计划模型 → 之后恢复为进入前的会话路由 | | 主模型 | 会话内选择(原生接口) → 设置页的新会话默认 | 默认与会话相互独立:设置页保存的是新会话默认,聊天栏选择与计划切换只改当前会话。宿主适配层用 `AsyncLocalStorage` 隔离原生 `selectModel` 的「写部署默认」副作用,只拦这一条调用链。 ## 持久化 | 数据 | 位置 | |---|---| | 设置默认(子代理默认 / 制定计划模型) | DSH profile 配置(volatile 字段——设置页或配置文件均可改) | | 新会话主模型默认 | 原生 `agent-default-model` 项 | | 会话子代理覆盖 | `$DSH_HOME/storages/model-switch/session-overrides.json` | | 计划前主模型(恢复成功后清除) | `$DSH_HOME/storages/model-switch/plan-main-models.json` | | 菜单收藏 / 最近使用 | 浏览器 `localStorage`(按浏览器,不同步) | ## 兼容性 - **宿主:** DSH `0.2.0-rc.2`。插件只走 0.2.x 的扩展面(volatile `Config` + `configForms`、会话投影、会话选模接口、`planMode`),没有 patch DSH 核心,所以上游一键升级不受影响。 - **更新的宿主需要 ≥1.1.0。** 自 1.1.0 起子代理路由同时包装三条原生启动路径——`start` / `startContinuable`(rc.2)与 `startActivation`(更新宿主)。在只暴露 `startActivation` 的宿主上,1.0.x 会被静默旁路为父模型继承。 - **客户端:** 浏览器半边以 DSH 客户端 bundle(`lib/client.js`)形式发布,依赖宿主半边已加载;首次安装后只刷新页面是不够的。 ## 开发 ```bash pnpm install pnpm test # vitest —— 69 个用例 pnpm build # tsc(宿主 + 客户端两个工程)+ tsdown 客户端 bundle ``` 只改 UI 时可以只重建客户端: ```bash ./node_modules/.bin/tsc -p tsconfig.client.json && ./node_modules/.bin/tsdown ``` 测试覆盖:路由合并优先级、会话覆盖存取与并发写入、计划边界的进入 / 恢复 / 重启恢复 / 拒绝回退、会话选模与默认写入隔离、路由校验拒绝无效组合、客户端保存竞态、旧会话目录适配、并发委派隔离。 > `tsc -p tsconfig.json`(宿主工程)按 **profile** 解析到的 `@deepseek-ai/*` 做类型检查。如果本包自己的 `node_modules` 里恰好存在这些包的第二份拷贝,两份拷贝的 branded 类型会不一致并报错;客户端工程、bundle 构建与测试不受影响。 ## 故障排查 | 现象 | 原因 / 处理 | |---|---| | 安装后看不到芯片或设置项 | 宿主半边没有重组合——完全退出并重开 DSH(只刷新页面只会加载客户端半边)。 | | 子代理始终用父模型 | 比 rc.2 更新的宿主需要 ≥1.1.0(见[兼容性](#兼容性));另外确认没有停留在只暴露 `start` 的旧宿主上。 | | 菜单里少某个模型 | 该提供方的目录没加载成功——菜单顶部会提示部分目录加载失败,提供方恢复后模型即出现。 | | 保存时提示路由无效 | 该组合不在提供方的实时目录里;从它所属的提供方分组里重新选择再试。 | | 改了设置但聊天没变 | 属正常:聊天栏选择与计划切换是会话级,设置页保存的是新会话默认。 | ## 已知限制 - 与 DSH 0.2.0-rc.2 内部实现耦合较紧(`configForms`、会话投影、会话控制器、计划模式)——DSH 升级后请重跑测试。 - 计划边界的识别依赖原生 `exit_plan_mode` 的返回措辞;DSH 改措辞会延后恢复(`plan/mode` 事件是兜底)。 - 子代理会话有意不参与计划切换与子代理覆盖。 - 菜单收藏与最近使用按浏览器(`localStorage`)保存,不属于会话或 profile。 ## 致谢 - 原始设计与 0.1.x 实现:[lincong1987/dsh-model-switch](https://github.com/lincong1987/dsh-model-switch)(MIT)——本插件在其基础上构建。 - 0.2.x 适配、计划模式切换、会话覆盖、路由校验与当前 UI 均在本仓库重写/扩展。 ## 许可 [MIT](LICENSE)