DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

kkaporn /

kkaporn/dsh-workflow-plugins

Verified

DSH 插件:在输入框旁边给当前会话单独挑要挂载的插件 | Per-session plugin picker for DeepSeek Harness

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHubProject homepage
READMESource: main@7ccfcadb

🧩 session-plugin-picker

输入框旁边一个「插件」按钮,给当前这个会话单独挑要挂哪些插件。

License Node DSH Dependencies


DSH 的能力是全局的:装了什么插件,所有会话都一样。这个插件把选择权下放到单个会话—— 勾选即生效,取消即卸载,插件带来的工具、提示词和技能只在那个 agent 里存在。

浅色下的控件和面板 深色下的控件和面板
深浅色自动跟随,因为没有一个写死的颜色

🚀 安装

一行命令(把 web 换成你要装进的那个 profile 名):

dsh plugin --profile web add git+https://github.com/kkaporn/dsh-workflow-plugins.git

装完重启一次应用——插件要重新加载才生效(之后改配置不用重启)。

Desktop 应用怎么装(不想敲命令)

用应用内的插件管理器,把仓库地址粘进去:

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 里,会话本身不受影响。
  • 往单个 agent 作用域里挂任意第三方插件,这件事超出 DSH 的保证范围。白名单是让这个风险的边界明确且可控的东西。
  • 一个自身不是单例的包,会在每个挂它的会话里多出一个实例。在 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 和一条踩坑经验(给要读源码的人)

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 属性,插件会收到空配置,然后被白名单过滤成空—— 一个看起来和"挂载坏了"一模一样的问题。

📄 License

MIT © the authors

—/ 5

No ratings yet

Verified DSH bundle

Commit 7ccfcadb8f30

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout