dsh-agent-preset-switcher
让 DSH 会话支持热切换工作模式(agent preset)——从「创建会话前选择」变成「会话中随时切换」。
它解决什么
DSH 的 agent preset(标准模式 / 极简模式 / PTC 模式 / 自定义预设)决定了一个会话 Agent 的工具目录、系统提示词、能力集。官方设计里 preset 在会话开始前选定,一旦产生过 turn/start 就禁止更换:
- 官方浏览器 RPC
agentPresets.select前检查sessionBlank(agent.session),非空白返回agent-preset-locked; - 理由:历史 transcript 是在旧工具集下生成的,中途换工具会导致「日志里记录的工具调用,新组合无法调用」的不一致。
本插件把这条通道扩展为任意阶段的会话热切换:切换只发生在 step 边界(一个模型请求完成后、下一请求组装前),并复用官方同一套机制 —— AgentPresets.recompose()(standing preset 单飞挂载 + roster 持有的 scope-parent binding 重链接)。
快速上手
从 GitHub 安装(其他用户推荐)
仓库已包含编译产物(lib/),无需本地构建,直接用 dsh plugin 安装:
# SSH 方式(需要已配置 GitHub SSH key)
dsh plugin --profile web add "git+ssh://git@github.com/aefuimn/dsh-agent-preset-switcher.git"
# 或 HTTPS 方式(无需 SSH key)
dsh plugin --profile web add "git+https://github.com/aefuimn/dsh-agent-preset-switcher.git"然后重启 dsh web 进程并刷新页面:
# 重启你的 dsh web 服务,再硬刷新浏览器标签页从本地目录安装(开发本插件时)
dsh plugin --profile web add link:/绝对路径/dsh-agent-preset-switcher
# 重启 dsh web 并刷新浏览器标签页验证
- 任意会话内可执行
/mode list与/mode <预设id>; dsh plugin --profile web ls应能看到dsh-agent-preset-switcher已安装。
组件
dsh-agent-preset-switcher/
├── package.json # dsh 插件 manifest(仅宿主面)
├── cordis.patch.yml # bundle patch:插入插件行
├── src/
│ ├── index.ts # 宿主面:服务注册 + /mode 命令 + 通告
│ └── switcher.ts # 热切换核心(armed → step 边界 recompose)
├── lib/
│ └── index.js / switcher.js # tsc 产物
└── test/
└── switcher.test.mjs # 单元测试热切换原理
触发
所有请求最终都调用 ctx.modeSwitcher.request(sessionId, presetId)。当前入口是 /mode 斜杠命令:
/mode list列出当前预设与全 roster;/mode <预设id>请求切换。
请求是「武装(armed)」而非立即动作:mode-switcher/requested 事件发出,目标预设先 resolve 校验。
应用(step 边界)
request(sessionId, targetId)
└─ armed(latest wins;同一会话重复请求后到覆盖先到)
│
▼ agent/pre-step(下一个模型请求组装前)
└─ pop armed
├─ 目标 == 当前 → no-op(不写日志、不重链接)
├─ agentPresets.recompose(agent.ctx, targetId)
│ └─ ensureStanding(target)(单飞) → 持有 binding.rebind(standing.key)
├─ agent.session.append('agent-preset/selected', …) # 官方日志事件
├─ mode-switcher/switched 事件
└─ 放行 step(新组合的 section/tool 从下一请求生效)关键点:
- 不是 unmount/remount:standing 组合永久、共享;切换只搬 agent scope 的父链接。
- 日志诚实:
agent-preset/selected与重链接一致写入日志,resume/fork 用resolveSessionPreset读最新事件重建组合,浏览器既有agent-preset/selected监听自动刷新。 - 失败语义:recompose 抛错 => 什么都不动,发出
mode-switcher/switch-failed,step 照常进行。 - 幂等与并发:同会话切换经 per-session promise 链串行化;与当前相同则为 no-op。
- 子代理:保持宿主规则,子代理会话拒绝切换(其组合跟随父会话)。
API(宿主)
declare module '@deepseek-ai/cordis' {
interface Context {
modeSwitcher: ModeSwitcherService
}
}modeSwitcher.request(sessionId, presetId)→{accepted:true,pending:true} | {accepted:false,reason}- 事件:
mode-switcher/requested、mode-switcher/switched、mode-switcher/switch-failed
与官方 recompose 的一致性
官方空白会话切换(agentPresets.select)的步骤是:
presets.recompose(agent.ctx, id) // ensureStanding + binding.rebind
agent.session.append('agent-preset/selected', { agentPreset: preset.id })本插件在非空白阶段做完全相同的两步,只是把调用时机从「请求到来时」移到 agent/pre-step 水瀑内部(模型请求之间)。recompose 的注释明确 *the CALLER owns the blank check*——官方 RPC 选择用 blank 检查,本插件选择用 step 边界,机制本身是同一套。
构建
npm install # 安装 devDependencies(宿主类型)
npm run build # tsc 编译宿主面限制与后续
- step 边界语义:切换不打断当前模型请求;最长等待 = 当前 step 完成。
- transcript 一致性:切换发生在模型请求之间,工具 schema/提示词在 step 边界更换;历史消息原样保留(这是热切换的价值,也是其边界)。
- 可扩展:给
src/index.ts的 service 增加 browser RPC(在 host-apiproxy 之外挂 Typert/远程面)即可让未来面直接遥调用,当前入口走/mode斜杠命令,宿主零新增 wire。
协议
[MIT](./LICENSE)