← Back to home@kkaporn

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
GitHub repo

Introduction

🧩 dsh-session-composer

可视化组装一个会话的插件组合,存成 DSH 原生预设;并在第一步把关需求。

License Node DSH Dependencies Build


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 时宿主的重载接口 会静默返回空,插件只能如实报告。


用法

一、组装一套插件组合

  1. 打开任意会话,点输入框左边的 「技能框」
  2. 点一行即可把那个插件放进技能框(不用去找小按钮)
  3. 用 ▲▼ 调整顺序;插件多时用搜索框过滤
  4. 起个名(可以留空,会自动起名),点 保存
  5. 新开会话时,用 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保留字段,当前版本不生效

这个插件不做什么

写在这里,免得误解:

  1. DSH 本体自带的插件永远不出现在面板里,也永远关不掉。 不是界面灰显 —— 每一次切换都会在宿主层重新核对,手工构造的请求返回 403。 理由很实际:不懂的用户一旦取消勾选本体条目,后果他无法理解、也修不回来。 面板只处理第三方插件。
  2. 不自己挂载插件。 生效路径全部是 DSH 自己的(原生预设声明 + 原生预设选择器)。 本插件只写一份声明。
  3. 不改 DSH 本体。 只写自己的两个文件:自己的状态 JSON,和自己的 cordis.patch.yml 里 带标记的一小块区域。你的 ~/.dsh/profiles/*/cordis.patch.yml 一个字都不动。
  4. 不编排执行顺序。 插件是能力容器,不按顺序执行。技能框的顺序只是预设里记录的顺序, 它表达优先级,不是执行序列 —— 插件里可能有个编排器,它自己决定这轮怎么走。
  5. 不给已开始的会话换组合。 这是 DSH 的设计:会话一开口,组合就固定。
  6. 不装任何东西。 技能框里只能选已经装好的插件,装插件是 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