dsh-session-composer
用 GUI 组装一个会话的插件组合,存成 DSH 原生预设。Assemble a session plugin set in the GUI, saved as a native DSH agent preset.
- Stars
- 1
- Language
- JavaScript
- Created
- Oct 6, 2026
- Updated
- Oct 6, 2026
Introduction
DSH 的插件是全局的:装了什么,所有会话都一样。想给某个会话配一套专属的插件组合,
你只能手写 cordis.patch.yml。
这个插件把这件事变成点几下:
输入框旁的「技能框」按钮
├─ 把已装的插件点进技能框(顺序可调)
├─ 起个名,保存
└─ → 生成一份 DSH 原生预设
↓
DSH 自带的预设选择器里就能选到它
选中的会话用这套组合,别的会话一点不受影响
外加:在项目根目录放一份 .dsh-workflow.json,可以写一条「把关」规则 ——
你说的话不够清楚时,AI 会在动手之前先问清楚,而不是闷头写一堆你没要的东西。
为什么零依赖
| 别人 | 这个插件 | |
|---|---|---|
| 运行时依赖 | 常见需要 react-flow / zustand / js-yaml 等 | 0 个(只用 node: 内置模块) |
| 构建步骤 | 常见需要 tsc + 打包 | 不需要 —— 装完就能跑 |
这不是为了炫技:依赖和构建是"装不上"的两个主要原因。装一个要构建的插件,
pnpm 默认会拦住构建脚本(ERR_PNPM_IGNORED_BUILDS),你得手工加 allowBuilds 再重来一遍。
这个插件没有这一步。
安装
# npm(发布后)
dsh plugin --profile desktop add dsh-session-composer
# 或直接从 GitHub
dsh plugin --profile desktop add github:kkaporn/dsh-session-composer
兼容性怎么声明的
靠 peerDependencies,这是宿主唯一会校验的机制:
"peerDependencies": { "@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0-0" }
宿主在导入一个 bundle 前,会拿 peerDependencies 里所有 @deepseek-ai/dsh / @deepseek-ai/dsh-*
的范围跟运行时版本比对(官方原文:Missing DSH peers impose no constraint)。
范围不匹配时 bundle 会被跳过,所以这条声明是有效的拦截,不是装饰。
本插件不带
compatibility.json。宿主要在 profile 目录下读一个同名文件,schema 是{"包名@精确版本": ["精确DSH版本"]},和插件目录里放一份「给人看」的兼容声明完全不是一回事; 写一份宿主不读、字段名却像官方字段的文件,只会制造"已经声明过兼容范围"的错觉。
开发与实测环境是 Node 24;engines: ">=20" 的下限来自代码实际用到的最新 API(structuredClone,Node 17+),
20/21 未实测。
装完重启一次 DSH(装载进来的插件代码要一次完整启动),然后硬刷新浏览器(Cmd/Ctrl+Shift+R)。
之后保存一个组合就不需要重启了 —— 前提是这个 profile 的 HMR 是生效的。 HMR 由
dsh-base声明(disabled: !!js "!ctx.get('profileContext')"),而dsh [--profile] <name>启动路径总是会提供profileContext,所以正常启动的 profile 都有 HMR。如果界面提示「重启后生效」,说明这个 profile 的 HMR 没生效(hmr 那一行被关掉了, 或宿主没有提供
profileContext),重启一次即可。这不是插件偷懒:没有 HMR 时宿主的重载接口 会静默返回空,插件只能如实报告。
用法
一、组装一套插件组合
- 打开任意会话,点输入框左边的 「技能框」
- 点一行即可把那个插件放进技能框(不用去找小按钮)
- 用 ▲▼ 调整顺序;插件多时用搜索框过滤
- 起个名(可以留空,会自动起名),点 保存
- 新开会话时,用 DSH 自带的预设选择器选它
正常启动的 profile 里保存后立刻生效;如果界面提示需重启,说明该 profile 的 HMR 没生效。
┌ 组装插件组合 ───────────────────────────┐
│ 技能框 (2 个 · 顺序即预设里的顺序) │
│ 1 your-team-linter ▲▼ ✕ │
│ 2 your-docs-helper ▲▼ ✕ │
│ [ 清空技能框 ] │
│ ────────────────────────────────────── │
│ 可添加的插件 3 个 · 点一行即可加入 │
│ + your-db-client 1.2.0 │
│ + your-team-linter 全局 0.9 │
│ + your-docs-helper 全局 1.0 │
│ ────────────────────────────────────── │
│ 保存为新组合 │
│ [ 给我的项目用 ] [ 保存 ] │
│ ────────────────────────────────────── │
│ 全局开关 (勾上 = 每个会话都响应) │
│ · 宿主自带的插件已隐藏 │
│ ☑ your-db-client │
│ ☑ your-docs-helper │
│ ────────────────────────────────────── │
│ 已保存的组合 │
│ 给我的项目用 2 个 ⤓ ✕ │
└─────────────────────────────────────────┘
上面是界面的示意,包名是占位示例,不是你机器上会看到的东西 —— 实际列出的是你自己 profile 里已装的那些插件,以及它们真实的版本号。
「全局」标记:这个插件已经在
dsh.profile.bundles里,每个会话都有, 放进技能框不会产生额外变化 —— 插件会照实标出来,而不是让你以为它起作用了。
二、给项目加把关
在项目根目录建 .dsh-workflow.json(可以提交进 git,团队共享):
{
"version": 1,
"gate": "接到需求先复述一遍,检查有没有可执行信息;缺关键信息就提问,不许动手。"
}
存好,在那个目录新开一个会话。
之后你每发一条消息,AI 在动手之前会先收到这条规则。实测效果:
你:做个游戏
AI:(被拦住)先复述需求 —— 你要的是 2D 还是 3D?主角是什么?这一步做到什么程度算完?
这个文件是项目自己的,不进 ~/.dsh/。 换项目就换规则,也可以跟代码一起提交。
配置字段(.dsh-workflow.json)
| 字段 | 作用 |
|---|---|
version | 必填,只接受 1;不认识的值 → 整份配置无效并记一次 WARN |
gate | 每条用户消息的第一次模型调用前注入的指令。留空 = 不注入 |
notes | 保留字段,当前版本不生效 |
skills | 保留字段,当前版本不生效 |
这个插件不做什么
写在这里,免得误解:
- DSH 本体自带的插件永远不出现在面板里,也永远关不掉。 不是界面灰显 —— 每一次切换都会在宿主层重新核对,手工构造的请求返回 403。 理由很实际:不懂的用户一旦取消勾选本体条目,后果他无法理解、也修不回来。 面板只处理第三方插件。
- 不自己挂载插件。 生效路径全部是 DSH 自己的(原生预设声明 + 原生预设选择器)。 本插件只写一份声明。
- 不改 DSH 本体。 只写自己的两个文件:自己的状态 JSON,和自己的
cordis.patch.yml里 带标记的一小块区域。你的~/.dsh/profiles/*/cordis.patch.yml一个字都不动。 - 不编排执行顺序。 插件是能力容器,不按顺序执行。技能框的顺序只是预设里记录的顺序, 它表达优先级,不是执行序列 —— 插件里可能有个编排器,它自己决定这轮怎么走。
- 不给已开始的会话换组合。 这是 DSH 的设计:会话一开口,组合就固定。
- 不装任何东西。 技能框里只能选已经装好的插件,装插件是
dsh plugin add的事。
安全边界
| 项 | 做法 |
|---|---|
| 写文件 | 只写自己的;写前先备份(保留最近 5 份)、先做 YAML 校验;任一步失败 → 一个字都不写 |
| 标记区 | 只替换两个标记之间;标记丢了 → 拒绝写入,而不是猜文件结构 |
| 原子性 | 先写临时文件再改名,避免半截文件 |
| 失败退场 | gate 与 agent/created 全部包在 try/catch 里,只 fail-open、不阻断会话;上游监听器的异常照常抛出,不会被吞掉。每会话的状态在 session/disposed 时清理 |
| HTTP 面 | 五个路由都先过宿主自己的准入栅栏 ctx.connection.admit(req)(webServer 先匹配 exact 表,若不检查就会绕过 /api 前缀的认证),请求体上限 1 MB。宿主若没有 connection 服务,栅栏缺失时插件会放行并记一条警告 —— 那种宿主本来就没有栅栏可复用,拒绝所有请求只会让面板彻底不能用 |
| 全局开关的副作用(必须知道) | 你在面板里拨动开关时,是宿主自己把改动持久化进 ~/.dsh/profiles/<name>/cordis.patch.yml(新增或改一行 - id: <插件>)。这是宿主插件管理器的原生行为 —— DSH 自己的设置界面拨同一个开关也是同样效果。插件的代码从不写你的 profile patch,它只是调宿主的接口 |
| 候选范围 | 技能框里的包名来自磁盘扫描,浏览器改不了 |
卸载即归零:删掉插件目录 + 从 dsh.profile.bundles 移除,一切还原,不留痕迹。
数据落点
| 内容 | 位置 |
|---|---|
| 组装过的组合 | ~/.dsh/session-composer/presets.json |
| 预设声明 | 插件自己的 cordis.patch.yml 标记区内 |
| 备份 | ~/.dsh/session-composer/backups/,cordis.patch.yml.bak-<时间戳>,保留 5 份。刻意不放插件目录 —— 那里是代码,dsh plugin update 会整包替换 |
| 工作区规则 | 你的项目根目录 .dsh-workflow.json |
插件安装目录里不存用户数据。
常见问题
| 现象 | 解决 |
|---|---|
| 保存了但选不到 | 正常启动的 profile 会立刻生效。若提示「宿主重载没成功」,说明该 profile 的 HMR 没生效,重启一次 DSH 即可 |
| 按钮不出现 | 硬刷新浏览器;还不行就重启 |
| 提示「找不到标记区」 | 插件自己的 cordis.patch.yml 被改坏了。从同目录的 .bak-* 恢复 |
| 提示「拒绝写包外」 | 保险拦下了异常写入,插件没写任何东西。报给作者 |
| 技能框里没有我要的插件 | 那个插件还没装。先 dsh plugin add,回来刷新面板 |
| 关掉某个全局开关后界面少了东西 | 那个开关就是"每个会话是否响应"。宿主必需的条目(标「必需」)不给关 |
| 把关没生效 | 确认 .dsh-workflow.json 在项目根目录(会向上查找),且 gate 非空 |
和「场景」类插件的关系
生态里已有成熟插件(如 dsh-plugin-tool-management)用**场景(scene)**管理
MCP 服务器、技能、子智能体人设、记忆与提示词 —— 进入一个场景整套切换、退出还原。
它们和本插件是互补的:
| 场景类插件 | 本插件 | |
|---|---|---|
| 管什么 | MCP / 技能 / 人设 / 记忆 / 提示词 | 插件(bundle) |
| 生效方式 | 进入/退出场景,运行时切换 | 组装成预设,新会话按预设组成 |
"哪些插件全局响应"也是 DSH 原生能力,本插件面板里的「全局开关」直接调用宿主的
pluginManager.setPluginEnabled —— 不另造一套开关去和宿主打架。宿主必需的条目
(如 dsh-base)会被标成「必需」并禁止关闭。
开发
node lib/index.test.mjs # 80 项自测,无框架、无夹具
DSH_STRICT=1 node lib/index.test.mjs # 严格模式:任何一个 SKIP 都算失败(给 CI 用)
node scripts/assert-lf.mjs # 发布前字节闸门:查 BOM / CR / U+FFFD / 末尾换行
自测文件不随包发布(生态惯例,也符合 DSH 插件模板的发布契约)。 要跑自测就 clone 仓库:
git clone https://github.com/kkaporn/dsh-session-composer && cd dsh-session-composer && npm test。 成品包只有 10 个文件 / 98 KB —— 因为一旦发出去一个带 CR 的cordis.patch.yml, 所有用户的保存都会失败(插件写前硬拒 CR),所以发布体积换的是这道闸门跑在真路径上。
自测覆盖安全路径:标记区拼接、写前校验、备份轮转与目录归属、失败退场、工作区向上查找、注入消息形状,以及宿主开关调用的参数形状与返回值语义。
后面两项是这份自测里最贵的部分,因为它们挡住的是静默失败:宿主的 setPluginEnabled 用位置参数,写错形状它不会抛错、只会返回一个 application 字段;而它一共会返回五种取值,只有 applied 代表"真的生效了"。这两点都有测试钉住 —— 把调用改回错误的形状,自测会变红(变异测试验过),而不是继续全绿。
依赖真机 profile 的用例在没有该 profile 的机器上会明确打印 SKIP,绝不计入通过。
License
MIT