dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH 插件:在输入框旁边给当前会话单独挑要挂载的插件 | Per-session plugin picker for DeepSeek Harness
DSH 的能力是全局的:装了什么插件,所有会话都一样。这个插件把选择权下放到单个会话—— 勾选即生效,取消即卸载,插件带来的工具、提示词和技能只在那个 agent 里存在。
一行命令(把 web 换成你要装进的那个 profile 名):
dsh plugin --profile web add git+https://github.com/kkaporn/dsh-workflow-plugins.git
装完重启一次应用——插件要重新加载才生效(之后改配置不用重启)。
用应用内的插件管理器,把仓库地址粘进去:
https://github.com/kkaporn/dsh-workflow-plugins
它走的就是 dsh plugin add,装完会提示重启。
desktop 这个 profile 名保留给 Electron 应用持有:官方 CLI 会拒绝针对它的启动与配置导出,
npm CLI 也会拒绝它的插件管理请求——所以 Desktop 用户的入口就是应用内那个插件管理器。
候选清单由包自带的 cordis.patch.yml 声明,装完就能用:
# cordis.patch.yml(随包分发)
- insert:
- id: session-plugin-picker
name: 'session-plugin-picker'
config:
candidates: ["dshmarket"]
想换成你自己的候选,就在你自己的 cordis.patch.yml 里按 id 覆盖它——
注意是整个 config 被替换,不是合并,所以要把完整的 candidates 都写出来:
# <DSH_HOME>/profiles/<profile>/cordis.patch.yml
- id: session-plugin-picker
config:
candidates: ["dshmarket", "mattpocock-skills-dsh-zh"]
| 字段 | 类型 | 作用 |
|---|---|---|
candidates | string[] | 唯一可被挂载的包名集合。留空则面板里没有可选条目 |
💡 一个包只有"不在
dsh.profile.bundles里"时才值得放进candidates: bundle 本来就会挂进每个会话,勾它等于什么都没做。
| 🎛️ 就在输入框旁边 | 挂在 conversation.input.left 这个槽位——一个发布时就是空列表的位置,所以不会顶掉任何内置控件 |
| ✅ 勾选即生效 | 每一次勾选立刻应用到那个会话活着的 agent,插件的工具和提示词随 agent 一起释放 |
| 🔒 白名单是构造性的 | config.candidates 就是全部可挂载集合。不在名单里的包,就算手工构造请求也到不了加载器 |
| 🚫 只认空会话 | DSH 的规则是"会话一开始,组合就固定"。所以只有还没发过消息的会话能改,发过消息的返回 409 并提示新开会话 |
| 🎨 不写死颜色 | 字体、圆角、阴影、配色全部读 DSH 主题变量,深浅色和任何主题覆盖自动跟随 |
| 📦 零依赖 | 宿主端只用 Node 自带能力;浏览器端只依赖 React 和 Cordis 客户端上下文 |
config.candidates 就是全部可挂载集合。请求里出现别的包名,会在到达加载器之前就被过滤掉——
所以那个 HTTP 端点是不是一个"浏览器要什么就挂什么"的口子。
DSH 自己的组合规则是 "fixed once the session starts"(预设选择器同样只重绑空 agent)。
去改一个已经开始的会话的工具面,等于跟这条规则对着干。所以发过消息的会话会拿到 409,
面板上直接告诉你新开一个。
| 选择本身 | $DSH_HOME/session-plugin-picker.json,按会话 id 存 |
| 文件坏了 / 不存在 | 当作空,不影响任何会话 |
| 写入失败 | 只丢这一次选择,不影响会话 |
| 内存里的挂载表 | 记录在 fiber 释放的那一刻就被丢掉;会话被销毁时整条忘掉——存储才是唯一的持久来源,会话回来了照样按存储重新挂上 |
node lib/index.test.mjs # 纯逻辑 23 项,不用真环境
node verify/client-contract.mjs # 浏览器端契约 17 项(替身 React)
$env:DSH_NM = "<DSH 安装目录>\node_modules"
node verify/host-mount.mjs # 真 Cordis 14 项:挂载 / 重挂 / 清理
$env:DSH_THEME = "<DSH 安装目录>\node_modules\@deepseek-ai\dsh-client-ui-theme\lib\client.js"
node verify/.preview.mjs # 生成上面的预览图
环境变量故意没有默认值:猜一个路径只会在某一台机器上凑巧能用,然后表现成"挂载坏了"。
在真实运行的 GUI 上验证(直接打 HTTP,不用重启):
| 检查 | 结果 |
|---|---|
| 插件条目加载且已启用 | fiberPhase: active |
GET /state 通 | {"candidates":[...],"live":true,"blank":true,...} |
缺 sessionId | 400 sessionId is required |
| 未知会话 | 404 unknown session |
| 发过消息的会话 | 409 …start a new session… |
| 真实挂载 / 卸载 | {"ok":true,"mounted":["…"],"failed":[]} → {"ok":true,"mounted":[]} |
| 白名单边界 | 请求一个不在 candidates 里的包 → selected: [],永不挂载 |
| 浏览器端真的到了页面里 | 实时 Slots 检查:占用者 session-plugin-picker,active: true |
组件真的渲染了、sessionId 真的传到了 | 临时探针读到 spp-probe-session-985c9090-…(探针已删,仓库里没有残留) |
像素本身。 控件被证明能注册、能渲染、能拿到 sessionId、行为正确——但 DSH 的页面只发给
持有启动口令的浏览器,外部截不到真实运行中的那张图。
能验证的是主题解析:verify/.preview.mjs 把组件自己的样式表叠在从 ui-theme 提取的
--dsw-* 变量上,浅深两套都渲染一遍,每个 token 都解析到了真值、没有退化成兜底色。
至于在你的主题里好不好看,这个还是得你自己看。
failed 里,会话本身不受影响。apply 里注册服务、定时器或 HTTP 路由的包不适合当候选——cordis.patch.yml 的注释里写明了为什么默认候选只有一个。/state 是本地 web 服务器上的普通 GET,没有按请求鉴权,所以任何能访问端口的页面都能读到哪些会话有选择。真正会改东西的 POST 需要 JSON 内容类型,跨站页面要发它就得过预检,而这个服务器不会放行。要收紧就是把端点按来源校验。lib/index.js 宿主端:状态文件、两个 HTTP 端点、挂载 / 卸载
lib/client.js 浏览器端:输入框旁那个控件和它的面板
cordis.patch.yml 包自己声明怎么被挂进 profile
lib/index.test.mjs 纯逻辑自检
verify/client-contract.mjs 订阅 / 渲染契约(替身 React)
verify/host-mount.mjs 真 Cordis:挂载、重挂、清理
verify/.preview.mjs 用真主题变量渲染预览图
bug 1:存储里的选择从来没被重新挂上。 选择存在文件里,但只在用户点击的那一刻应用过一次。
DSH 重启会重建会话的 agent,存储里的选择再也没人挂——/state 会报 selected 而 mounted 是空的。
bug 2:修了第一个之后,它其实还是坏的。 重挂的守卫问的是"这个会话有没有记录":
if ((mounted.get(agent.id) ?? []).length > 0) return; // 只数个数,从不问死活
会话的 agent 作用域拥有它挂载的一切、并跟着作用域一起释放,所以一个重建的 agent 会发现 上一个 agent 的记录还在表里。那些 fiber 已经死了,守卫照数不误,重挂被静默跳过。 原来的测试每次都开一个全新的根(也就是全新的表),所以看不到这个问题。
现在守卫问的是"还有活着的吗"(fiber.uid !== null,跟 Cordis 自己的 assertActive 读同一个字段),
顺手把死记录清掉。新增的测试段落专门复现这个场景,用一个独立的会话 id,
保证结论只依赖这一个场景。
踩坑经验:scope.dispose() 是异步的(dsh-scope 要等底层 fiber 收敛)。
不 await 就不会真正拆掉,旧 fiber 看起来还活着,重挂于是"合法地"什么都不做——
一个悄悄不再测试它所声称的东西的测试。
还有一条:Cordis 的配置是 ctx.plugin(plugin, config) 的第二个位置参数。
写成插件对象上的 config 属性,插件会收到空配置,然后被白名单过滤成空——
一个看起来和"挂载坏了"一模一样的问题。
MIT © the authors