- Home
- Plugins
- Models & Providers
- dsh-plan-act-models
dsh-plan-act-models
cynthiayx/dsh-plan-act-models
A simplified version of the Plan/Act feature in the RooCode and Cline VS Code extensions, binding one provider, model and reasoning effort to plan mode and another to act mode, with per-workspace overrides.
Install
dsh plugin --profile web add github:cynthiayx/dsh-plan-act-modelsREADME
模式与模型(dsh-plan-act-models)
给 DeepSeek Harness 的对话框加一个模式下拉框,并按模式自动切换模型:
- 对话框底部、原生模型选择器左侧是一个真正的下拉框:
Plan、Act,以及你自己加的任意个 自定义模式,每一项都是模式名:<模型>。选它就等于用宿主原生的方式换模型 —— 调的是session.selectModel,所以下拉框和原生选择器永远显示同一个结果,重启后也还在 (选择存在model/selection投影里)。 - 自定义模式由你命名,绑定自己的 provider / model / reasoningEffort,能力和 Plan / Act 完全一致 (同样的目录校验、同样的提交路径、同样进菜单)。区别只有一条:它只在你手动选择时生效, 插件永远不会自动切到自定义模式。
- plan 模式与模型的强绑定已经解除:
/plan、对话框上的 Plan 芯片、审批都不再隐式决定模型。 进入 plan 只是软切换到 Plan 那一项,你可以立刻改回 Act 或某个自定义模式,插件不会跟你抢。 - 唯一保留的硬规则:批准计划进入「实施」那一刻,强制切到 Act(忽略之前选的是什么)。
判定只认
exit_plan_mode返回approved: true。这条规则和上面的软切换一样,会盖过 当前选中的自定义模式 —— 那是「进入 plan / 批准计划」的既定语义,不想被盖过就别让这两个事件发生。 - 设置页那几组路由就是下拉框里的各项。
enabled只是自动切换的总开关; 关掉它,下拉框照样能手动切。 - 插件不存 API Key,全部复用宿主凭据;不接管 subagent / 后台委派任务。
安装
分发包是 dsh-plan-act-models-0.5.0.tgz(npm pack 产物,无运行期依赖、可离线安装)。
完整步骤、验证清单与排错见 INSTALL.md,安装路线摘录:
-
桌面版:应用内 设置 → 插件 → 从本地文件安装 选 tarball,然后重启应用。 桌面 profile 由应用独占管理,外部
dsh --profile desktop …会报managed exclusively by the Electron application。 -
CLI / Web 普通 profile:
dsh plugin --profile <profile> add "<绝对路径>\dsh-plan-act-models-0.5.0.tgz", Launcher 会自动把 bundle 写进dsh.profile.bundles,然后重启。 -
直接从本仓库装(不需要本地有 tarball):装最新 Release 附件,或按 git 引用装源码——
dsh plugin --profile <profile> add "https://github.com/CynthiaYX/dsh-plan-act-models/releases/latest/download/dsh-plan-act-models.tgz" # 或锁定到某个 tag 的源码 dsh plugin --profile <profile> add "github:CynthiaYX/dsh-plan-act-models#v0.5.0"两者都指向同一套文件;Release 附件是
npm pack产物,github:走整仓 tarball, 仓库根就是包根,没有需要裁剪的子目录。当前最新 Release:v0.5.0(附件
dsh-plan-act-models.tgz与dsh-plan-act-models-0.5.0.tgz,两者 sha256 相同)。更新到新版只需重跑一次add, 把latest换成新 tag 可以锁定版本。
配置入口:设置 → 模式与模型(首次默认关闭自动切换)。
本机开发时的链接安装
源码目录本身就是包(dsh-plan-act-models),开发时按 @local/dsh-restart 的先例手写 profile 层:
- profile
package.json的dependencies加"dsh-plan-act-models": "link:<克隆目录>/dsh-plan-act-models"(Windows 上写成link:D:/private/DeepSeek-harness-plugin/dsh-plan-act-models这种正斜杠形式),dsh.profile.bundles里追加"dsh-plan-act-models" node_modules\dsh-plan-act-models目录联接指向本仓库(等价于 pnpmlink:)- 本仓库自带的
cordis.patch.yml由 bundle 机制自动插入 Loader 行;profile 的cordis.patch.yml只用来覆盖那一行的 config(同 id),不要再写第二条- insert: - 本包的
exports必须显式导出./package.json与./cordis.patch.yml:宿主@deepseek-ai/dsh-client-modules会用createRequire().resolve('<pkg>/package.json')读dsh.client声明,不导出就会ERR_PACKAGE_PATH_NOT_EXPORTED
发布流程
仓库 CynthiaYX/dsh-plan-act-models 就是分发仓库:仓库根 = npm 包根,tag 触发发布,
Release 附件同时提供 dsh-plan-act-models-<version>.tgz 与稳定名 dsh-plan-act-models.tgz
(后者让 releases/latest/download/dsh-plan-act-models.tgz 这种"永远最新"的安装/更新地址可用)。
-
改
package.json的version,提交并推送。 -
打 tag 并推送(注意:Windows PowerShell 5.1 不支持
&&,要用;或分成两行):git tag v<version> git push origin main --tags -
.github/workflows/release.yml会先校验 tag 与package.json一致,再npm test、npm pack, 最后把两个 tgz 挂到该 tag 的 Release 上。 -
装/更新的人用 设置 → 插件 或
dsh plugin add <Release 附件地址>即可,不需要本地构建。
上传前清单
本包尚未发布到 npm,GitHub Release 才是当前的发布通道。发布前依次确认:
node --test全绿(176 个用例)。npm pack --dry-run的文件列表里没有lib/*.js(该 glob 曾指向不存在的文件)。package.json无private,license与repository齐全。cordis.patch.yml的id与client.js的ENTRY逐字相同 ——client.test.mjs做静态比对,client-bundle.test.mjs真的执行apply()并断言传进configForms.get()的 namespace 就是 patch 的id。改一处必须改另一处。- 若要同时发 npm,上传后
npm view dsh-plan-act-models version应回显对应版本。
配置
| 字段 | 含义 |
|---|---|
enabled | 自动切换总开关。关闭后不再自动切模型;下拉框仍可手选。 |
plan | 下拉框里 Plan 那一项:provider、model、reasoningEffort(留空 = 跟随模型默认)。 |
act | 下拉框里 Act 那一项,同上。 |
modes | 自定义模式表:键是稳定 id(页面按 m1、m2… 生成),值是 name + 与上面同样的三项路由。 |
projects | 按工作区根目录(session.header.cwd)的覆盖表:inherit / override / off。 |
项目覆盖是完整的一对,不做半合并;项目设为 off 时该项目内所有模式都不可选。
它只覆盖 Plan / Act 两项 —— 自定义模式是全局的,不参与项目覆盖(页面上也暂时不提供这个编辑器,
见「已知限制」)。
自定义模式
modes 里每个条目就是菜单里多出来的一项,路由能力和 Plan / Act 完全一致:
modes:
m1:
name: 审查
provider: cline-pass
model: deepseek-v4.1-flash-1017
reasoningEffort: low
- 名字只用于显示。会话里存的是切到的 provider/model/effort,不是模式 id —— 所以改名、删掉、重新排一遍都不会影响已经在跑(或已经存盘)的会话。
name留空时按位置退化成模式 1、模式 2…;设置页保存时也会写入同样的默认名, 所以页面和宿主对同一个条目的叫法一致。- 设置页最多 12 个(
MODE_LIMIT);这是页面的上限,宿主按配置里写多少就渲染多少。 - 一个条目 provider/model 不全时,它照样出现在菜单里、标成
未配置且不可点 —— 不静默消失,你能看见自己配错了哪一项。 - 顺序 = 配置里的书写顺序,永远排在 Plan、Act 之后。
对话框里的模式下拉框
composer 底部、原生模型选择器紧左边,样式是一枚胶囊(圆角 + 模式圆点 + 着色模式词 + 箭头),
点开后向上弹出宿主风格的卡片菜单。Plan 是蓝色、Act 是绿色、自定义模式是琥珀色、自定义 占位是虚线灰,
模型名本身用次级文本色 —— 所以扫一眼就知道当前在哪条轨上。
它不是 <select>:原生弹出层无法着色,这正是旧版看起来像一颗跑错片场的系统控件的原因。
改成「按钮 + 手写 listbox」后,配色跟着宿主 --dsw-* 主题变量走,深/浅色都对。
| 情况 | 下拉框显示 |
|---|---|
会话当前选择 = Plan 那一项 | Plan:DeepSeek V4.1 Flash |
会话当前选择 = Act 那一项 | Act:DeepSeek V4.1 Flash |
| 会话当前选择 = 某个自定义模式 | 审查:DeepSeek V4.1 Flash(琥珀色) |
| 当前选择哪一项都不是(在原生选择器里手动换过模型) | 自定义:<模型>(灰、虚线边框、不可选,只说明现状) |
当前工作区在 projects 里被设为 off | 所有项都灰掉并标 此项目已停用 |
| 某项 provider/model 不完整 | 该项灰掉并标 未配置 |
宿主没有 session.selectModel | 所有项都灰掉,悬停提示 宿主不支持切换模型(菜单仍可打开,那里写着原因) |
- 显示哪一项只看会话已提交的模型选择,不看 plan 模式。所以在 plan 模式下选 Act 是允许的,
下拉框会如实显示
Act。plan投影只用来在几项配置完全相同时(provider/model/effort 一样) 决定先选中哪一个 —— 并列时内建的 Plan / Act 优先,不会被同路由的自定义模式顶掉。 - 选项文字里的模型名优先用模型目录的
name,查不到退回 model id;思考复杂度不进正文,只在悬停提示里:Plan:DeepSeek V4.1 Flash — cline-pass/deepseek-v4.1-flash-1017 (max) · 全局配置(来自项目覆盖时末尾是· 项目覆盖:<工作区>;自定义模式永远显示· 全局配置,因为它不参与覆盖)。 - 切一项 = 调宿主的
session.selectModel。它的三个副作用都是宿主原生的: 校验路由 → 更新model/selection投影(所有客户端立刻同步)→ 写进会话日志。 同时会把该选择存成 profile 的全局默认模型,与手动改原生选择器完全等价。 - 键盘全程可用:
Enter/Space/↓打开,↑/↓/Home/End移动,Enter/Space选中,Esc关闭并把焦点还给触发器,Tab关闭。鼠标:点空白处关闭。 - 切换进行中控件禁用(连菜单也打不开,避免连点两次提交),失败时把
错误码: 原因显示在右侧小字里(也写进悬停提示),不会静默失败,也不会卡住下一次选择。 - 不显示的两种情况:subagent 会话(宿主从不为它们做路由)、配置镜像尚未就绪
(
loading/unavailable/error)或没有绑定会话。
两条自动规则
两条都受 enabled 与项目 off 约束,都不完整则跳过并各告警一次。
两条都不会切到自定义模式 —— 它们只认内建的 Plan / Act,那是这两个事件本来的语义。
- 进入 plan → 切到 Plan 那一项(软切换)。判定依据是宿主
plan/mode事件里active: true, 不是消息里的/plan文本。如果当前选择已经算 Plan 那一项就什么都不做;如果 Plan 那项没写reasoningEffort,则保留你手选的 effort(视为"跟随默认")。切换完成后你随时可以改回 Act 或某个自定义模式。 - 批准计划 → 切到 Act 那一项(硬规则)。判定依据只有一条:
exit_plan_mode的工具结果value.approved === true且不是错误结果。keep planning、失败、其它任何工具都绝不触发。
以上两条都会盖过你当前选中的自定义模式(选的是自定义模式时,它们照常把模型切回 Plan / Act)。
离开 plan 模式(/plan off、计划被取消)不碰模型。这就是本次改动脱钩的地方。
状态说明
设置页顶部会直接反映读取结果:
正在读取配置…:客户端的 settings 镜像还在拉取。宿主没有提供配置项「plan-act-models」:Loader 行没启用/没加载,此时页面不会给你一个 写了也存不进去的表单。读取配置失败:…:镜像报错,原样展示原因。
已知限制
- 几项配置都不写
reasoningEffort时,下拉框无法区分它们:provider 与 model 都一样, "当前在哪一项"没有可比较的差异,此时按 plan 模式决出显示项(tie-break),内建的 Plan / Act 在并列时优先;选另一个同路由的条目效果只是把 effort 改回默认。 - 手动在原生选择器里换到的模型若恰好等于某一项的 route,会显示成那一项(语义等价,不算错)。
enabled关闭只停自动切换;它不会锁住下拉框,也不会阻止你手选一个和 plan 模式不匹配的模型, 或手选一个自定义模式。- 若被链接安装(目录 junction /
link:),lib/schema.mjs会依次尝试裸导入宿主安装里的@deepseek-ai/schemastery、DSH_SCHEMASTER_ROOT指定的副本、从process.execPath与启动参数 往上回溯找到的resources/app.asar/app.asar.unpacked副本(回溯 8 层,也接受相邻的普通node_modules),最后才是 profile 里的副本。每一份都会先校验能力:必须带.volatile()(schemastery ≥ 3.18.5)。profile 的上级node_modules里常残留旧副本(例如 3.18.1 的 陈旧 junction),旧副本能 resolve 但没有.volatile(),直接用会让插件在声明 Config 时就崩, 所以现在被跳过并在报错里点名版本。全都不可用时插件整体不加载——设置页显示「宿主没有提供配置项」, 报错列出每条试过的路径,可用DSH_SCHEMASTER_ROOT指向副本目录。 - 自动切换失败只降级,不拖垮会话:
selectModel被拒绝(模型不在目录里、连接不可用)时 插件只告警一次并跳过;agent/created里挂载监听器抛出的异常同样就地吞掉, 否则那个 Agent 永远 publish 不出来。 - 挂载点只有两个事件:每个 root Agent 的
tools/result(审批)与根平面的session/event(plan/mode)。不再有请求期改写,所以没有「比宿主更早装监听器」的优先级问题。 - 下拉框沿用硬编码中文:与设置页一致,没有走 Client locale 服务;
client.js里甚至不能出现locale字样(test/client.test.mjs有既有断言)。 conversation.input.right目前无人占用,所以我们的条目稳定落在模型选择器左侧;若将来有别的 包也注册到同一个座位,顺序由order决定(本插件是 100),届时可能需要重新排序。- 项目覆盖编辑器暂时从设置页隐藏(
client.js里SHOW_PROJECT_OVERRIDES = false)。 宿主仍然解析projects、测试仍然覆盖它、手写的覆盖照旧生效;只是页面上不再提供入口 —— 一个"含义随文件夹变"的下拉框比它解决的问题更让人困惑。代码一行没删,把那个常量改成true就恢复。隐藏期间页面不会因为一条手写的、不完整的覆盖而拒绝保存,也不会在保存时抹掉它。 - 自定义模式是全局的:它不参与
projects覆盖,也不会被两条自动规则选中, 只在你手动点选时生效。
开发
# 在仓库根执行;`npm test` 也会跑同样八个文件
npm test
$node 指向安装目录里的 Node(D:\DeepSeek Harness\resources\runtime\primary-runtime\dependencies\node\bin\node.exe),
或直接用系统 node(需 ≥ 20,仓库在 Node 24 上验证)。
- 八个用例文件都只依赖
node:test与包内源码,不需要运行中的 Host:routing.test.mjs跑纯决策表(resolveTarget矩阵、sameTrack/sessionTrack、customTracks/customName/tracksOf、两个 switch 的真值表),host.test.mjs直接 importlib/host.mjs验证模型目录容错、committedSelection与createSwitcher,index.test.mjs真正加载index.js并驱动两条事件路径(含"approved:false 不切"、 "subagent 不挂"、"开关关闭两条都不动"、"任何事件都不会自动切到自定义模式"),client.test.mjs对client.js做静态回归断言(<select>、selectModel、镜像哨兵),client-view.test.mjs从client.js里抽出#region plan-act-view那块镜像实现,用 输入矩阵与lib/routing.mjs的resolveTarget/sessionTrack/customTracks逐项比对, 并断言选项文字是模式:模型,client-bundle.test.mjs真的执行 bundle:伪造window.__ModuleLoader__与configForms/slots/remote三个 Service,调用 factory 拿到 module 再跑apply(), 断言注册的 settings 页 id 与传进configForms.get()的 namespace 都等于 patch 的id, 并带一个最小 hooks 渲染器真的把下拉框和设置页渲染出来(选项文字与顺序、modelSelection决定选中项、自定义占位、自定义模式的增删/命名/上限/保存 payload、onChange 的 payload、 pending 禁用、四种降级、失败小字、开关关闭仍可选、项目覆盖编辑器已隐藏),schema.test.mjs校验 schemastery 解析(含「旧副本缺.volatile()必须被拒绝」)。commit.test.mjs校验提交信息格式。 client.js无法 importlib/routing.mjs(它是发给页面的 classic script),所以路由判定在#region plan-act-view … #endregion之间有一份手写镜像。client-view.test.mjs靠这对哨兵 把镜像抽出来做等价断言:改resolveTarget/sessionTrack就必须同步改镜像,否则测试会红; 哨兵被删会直接失败,不会静默空跑。client-bundle.test.mjs用?case=N查询串绕开 ESM 模块缓存:bundle 的全部效果就是那一次load()调用,模块被缓存后第二次 import 不会重新执行。它自带的 hooks 渲染器会重渲染 直到 state 稳定,因为"模型目录到达"和"切换失败"都只会在第二遍才可见。index.test.mjs用DSH_SCHEMASTER_ROOT在系统临时目录里造一个假的 schemastery(版本号写成 3.18.5,并把所有 helper 挂在 default export 上——打包版schemastery只有export { Schema as default },没有任何具名导出),因此在没有宿主的机器上也能加载入口。 这个文件是必需的:曾经index.js引用了未定义的rootOf,而当时全绿, 因为没有任何测试 import 入口,结果发出去的包在宿主里fiberPhase: failed。lib/host.mjs把「枚举模型目录」与「用宿主的 selectModel 提交选择」从index.js拆了出来, 便于无宿主测试。lib/routing.mjs是纯逻辑(不依赖宿主):resolveTarget解析一项,sessionTrack判断 选择落在哪一项,switchPlan/switchActOnApproval回答"这次事件该不该切",tracksOf/customTracks/customName把自定义模式纳入同一张决策表。tools/read-header.mjs从session.v4.jsonl.zstd里解出每次request/header与plan/mode, 用来确认「Plan 模式下真的走了 Plan 路由」:
注意该文件是多帧 zstd,一次& $node "$pkg\tools\read-header.mjs" "$env:USERPROFILE\.dsh\sessions\<dir>\session-<id>\session.v4.jsonl.zstd"zstdDecompressSync只能解出第一帧(244 字节的会话头)。
Related plugins
dsh-our-free-model
ebony-vinyl/dsh-our-free-model
dsh-routing-suite
yjh051108/dsh-routing-suite
dsh-workbuddy-connect
corrinehu/dsh-workbuddy-connect
dsh-connect-workbuddy
dingminhua/dsh-connect-workbuddy