re-ITRT/dsh-skill-mcp-panel

dsh-skill-mcp-panel

A Skill list and MCP server manager for DeepSeek Harness, mounted as a sidebar panel and themed entirely from the host's own design tokens.

待分类agentcordisdeepseek

dsh-skill-mcp-panel · 技能与 MCP 面板

DeepSeek Harness 的技能列表与 MCP 服务器管理面板,挂在侧边栏,主题完全继承宿主自身的 设计 token。

English

侧边栏一个入口——技能与 MCP——打开一整页,里面是两个紧凑列表:当前部署对外提供的全部 Agent 技能,以及全部已配置的 MCP 服务器。每行只有一行:名字、一句说明、一个开关,以及原地 展开的 详情。MCP 行还能编辑、新建、删除;两个列表都能打开该对象所在的目录。

页面里没有自己的主题代码。所有颜色、圆角、字体、焦点环都取自宿主的 --dsw-* token, 字号也跟随宿主,所以它会自动跟着部署主题(浅色/深色/第三方主题)以及 设置 → 外观 → 字号大小 走。

排版

dsh-client-ui-layout 把正文字号作为内联变量写到 body 上,并由它派生出一个增量:

--dsh-content-font-size: 14px;                      /* 12–17,就是用户设置的那个值 */
--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px);

本页每一个尺寸都按会话区自己的写法表达——<基准>px + var(--smp-delta),其中 --smp-delta 是 --dsh-content-font-delta 的别名。没有任何写死的 px,并且有测试守着这一点,所以面板 会和旁边的对话正文一起长缩。

两个列表

技能

当前会话构成下可见的技能目录,所以项目级和 preset 级的技能会和内置技能并列出现。

控件行为
名字/名字 标识 + 发现来源;调用策略只在非默认时才加标签
说明截断到一行,完整文本留在 tooltip
开关启用/停用该技能
详情provider、来源、路径、用完整句子写清的调用策略、完整正文,以及 打开目录 / 删除

技能的调用策略由两个互相独立的标记决定,默认都开:

标记含义
modelInvocable技能在模型的技能目录里,Agent 可以自己决定加载
userInvocable技能在输入框的 / 菜单里,你可以打 /名字 手动调用

因为几乎所有技能两个都是开的,到处打标签等于没信息。所以行内只标非默认的情况,详情面板则用 一整句话说明,而不是丢两个词让你猜。

开关是怎么实现的

"停用"由本插件维护,不是去改文件。DSH 没有 per-skill 开关:技能存在是因为某个 provider 列出了它,而调用策略由 provider 自己决定——官方 Office provider 把两个标记硬编码了、 根本不读 frontmatter,所以改文件对它永远无效。

对所有 provider 都有效的杠杆是 registry 自己的去重规则:同一层内,rank 最小的候选赢下这个 名字。所以本插件注册一个 provider,把每个被停用的技能用 rank: 0、 invocation: { modelInvocable: false, userInvocable: false } 重新声明一遍。这样它会从模型目录 和 / 菜单里消失,但在本页面仍然可见——连同原来的说明、provider 和正文——随时可以开回来。

集合存在一个 JSON 文件里:<DSH 目录>/storages/skill-mcp-panel.json,每次变更都会让 registry 的目录缓存失效。

删除技能

删除是真的删文件,不是另一种"停用"。目录型技能删的是整个 <根>/<名字>/ 目录,所以它的 references/、scripts/ 一起走;其它情况只删那一个文件。确认框会在动手之前告诉你会走哪种 模式。

技能来源模式行为
你自己的——project-dsh、project-agents、user-dsh、user-agents、custom永久删除直接删掉,什么都不留
官方/包内的——bundled 及其它移入回收站文件离开原目录,但被移到回收站并列在 已删除 里,点 恢复 放回原位

这个区分依据 registry 自带的 source 字段——它本来就记录了谁拥有这个文件。你自己的技能是你的, 删就删。官方技能不一样:删了会毁掉升级自带的文件,而且重装时会被悄悄还原;何况开关对 Office 技能本来就无解。把这一类存起来,能让唯一有效的杠杆保持可逆。

被移走的技能落在 <DSH 目录>/storages/skill-mcp-panel.trash/<id>/,带一个记着原路径的 manifest.json。恢复时会拒绝覆盖已经重新出现的路径;trash id 会被校验成单个路径段,所以回收站 之外的东西都寻址不到。

删除永远是两步:操作行被替换成一个写着技能名的确认块,只有确认块里的"删除"才会请求宿主。

MCP 服务器

每个已配置的服务器一行。

控件行为
命名空间mcp__<serverName>__*,附状态、传输、工具数量的标签
目标命令 …参数 那一行,或端点 URL
开关写 profile patch 里这一行的 disabled,和部署自己的插件列表完全一致
详情entry id、插件名、传输、脱敏后的配置、全部已注册工具,然后是 打开目录、编辑、删除
新建服务器用新建表单替换列表,所以空状态和表单永远不会同时出现
删除从 profile patch 移除这一行,然后等 Loader 真的卸载

新建/编辑表单

固定两列网格:服务器名和传输占第一行,然后是传输自己的字段——命令和工作目录,或者跨两列的 地址——参数和环境变量并排、各三行。常见情况下用不到的东西(超时、启动失败即报错)收在一个 更多选项 折叠里。

格式提示只放在控件的 placeholder 里,绝不写成控件下面的说明文字,所以没有字段会长出第二行, 列也始终对齐。表单打开时会替换列表,这样标题栏就不可能提供一个"它自己就是"的按钮。编辑时才按需 读取部署自己的表单投影,这是本页除技能正文之外唯一的单项读取。

打开目录

打开目录会揭开技能自己的目录,或 MCP 行所在的、装着 cordis.patch.yml 的 profile 目录。 它复用部署自身的能力,而不是重新实现平台启动:页面先问 GET /open-in-app/apps 这台宿主解析出 的是哪个文件管理器(explorer、finder、filemanager),再向 POST /open-in-app/open 发 {app, path}。当目录里解析不到文件管理器时——比如通过 SSH 启动——这个控件干脆不出现。

目录永远来自宿主返回的 payload,宿主用 dirname 从自己已经解析过的路径推导。页面从不自己编路径。

新鲜度

没有任何轮询,也没有刷新按钮。列表在挂载时读一次(每次打开面板都会挂载),窗口重新获得焦点时 再读一次(节流到每四秒最多一次),以及页面自己每次写入之后再读一次。所有会改动的控件都会自己 重读列表,所以页面永远不会展示一个部署已经离开的状态。

安装

dsh plugin --profile desktop add <本目录路径>

或者在 Agent 会话里用 plugin_manager,action: install_bundle,target 用本目录的绝对路径。

改宿主代码需要重启进程。 部署从 dsh.client 组合浏览器半边并热重载它的 bundle,所以 UI 改动刷新页面即可;而 Node 半边只被 import 进进程的 ESM 缓存一次,新的 lib/index.js 只有在应用 重启后才生效。

发布到 npm

本仓库是这个插件的家;npm 是可选的第二个分发渠道。发布用 npm run publish:npm,之所以要专门 写个脚本,是因为这个名字有两件事很容易搞错。

裸名字已经被占了。 npm 上的 dsh-skill-mcp-panel 属于另一个无关的 DSH 插件—— Fishquito7/dsh-skill-mcp-panel,它做的事 基本一样、功能更多,而且还在活跃维护。不带 scope 发布会被 403 拒掉;而另起一个相近到让人混淆的 名字,比不发还糟。所以脚本用当前登录者自己的 scope 发布,这样任何人都撞不到。

镜像不能接受发布。 ~/.npmrc 里的 registry= 常常是只读镜像,所以脚本永远显式打到 registry.npmjs.org。

npm login --registry=https://registry.npmjs.org/   # 只需一次;网站登录不算
npm run publish:npm

在 npmjs.com 网站登录和命令行登录不是一回事:npm 把浏览器会话和命令行分开,token 必须落到 ~/.npmrc 里。脚本发现没有 token 时会直接把这一点打印出来。

包名只在发布期间被替换,发布结束后会被还原——即使发布失败也还原——所以仓库始终保留自己的名字。 prepublishOnly 会跑测试作为门禁;再次发布前需要先升版本(npm version patch)。

目录结构

路径作用
package.json声明 dsh.bundle.patch 和 dsh.client 浏览器半边
cordis.patch.yml把 Node 半边插进 profile 的 bundle patch
lib/index.jsNode 半边:/skill-mcp 读写接口
lib/client.js浏览器半边:sidebar.panellist 入口和 main 页面
test/host.test.mjsNode 半边测试,跑在假宿主上,用真实 YAML 校验写入
test/client.test.mjs浏览器半边渲染测试,跑在一个极小的、懂 hook 的渲染器上

没有构建步骤。 lib/ 既是源代码也是发布产物;浏览器半边是用模块表格式 (window.__ModuleLoader__.load({ id, factory }))手写的,所以不需要打包器,pnpm 也不用跑任何 依赖构建脚本。运行时零依赖。

测试

npm test

Node 自带测试运行器,83 个测试,零依赖。宿主套件在假宿主上下文上跑真实的 route handler,而这个 假宿主的 config editor 用一个真实的 YAML 解析器去解析 profile patch——所以每个产出的文档都是 被"写它的代码之外"的解析器校验过的。渲染套件用一个极小的、懂 hook 的渲染器渲染浏览器半边,并对 渲染结果断言。

如果机器上没有任何 YAML 解析器,宿主套件照样能跑,只是不再用独立解析器校验产出文档。可以用 DSH_YAML_MODULE 指向一个来恢复这层校验。

宿主接口

所有接口都在一个前缀路由下。每个请求先过部署自己的信任闸门 ctx.connection.requestRejection()——和每一个官方宿主路由(包括 /open-in-app)用的是同一套 Host/Origin 加浏览器身份校验。没有 connection 服务的组合会退回到 Sec-Fetch-Site/Origin 检查。

路由方法用途
/skill-mcp/stateGET合并快照;?bodies=0 跳过正文预览,?session=<id> 指定技能读取的作用域
/skill-mcp/skillGET单个技能的完整定义,只用来补全正文
/skill-mcp/skill/enabledPOST{ name, enabled }——写启用状态 overlay
/skill-mcp/skill/deletePOST{ name }——永久删除自己的技能,或把官方技能移入回收站
/skill-mcp/skill/restorePOST{ trashId }——把移入回收站的技能放回去
/skill-mcp/mcp/serverGET单个服务器的表单投影,由编辑表单按需读取
/skill-mcp/mcp/savePOST{ entryId, config }——经 ctx.configEditor.edit() 持久化
/skill-mcp/mcp/createPOST{ config }——向 profile patch 追加一行
/skill-mcp/mcp/enabledPOST{ entryId, enabled }——写这一行的 disabled
/skill-mcp/mcp/deletePOST{ entryId }——从 profile patch 移除这一行

只有详情页的正文补全和编辑表单走单项路由;列表 payload 已经带了其它一切,所以展开一行永远不可能 出现空面板。

Node 半边要求 webServer、connection、skills、tools,并通过 ctx.get(...) 读取可选的 agents、agentPresets、configEditor、hmr。技能读取的作用域按以下顺序解析:?session= 指定 的会话、最新的存活根 Agent(带它的工作区 cwd)、默认 Agent preset 的常驻作用域、最后只有全局层; 解析结果由 skills.scopeSource 报告。

写入为什么是安全的

  • 配置编辑走 ctx.configEditor.edit():它会拿运行中的插件校验新值、调和 Loader,失败时回滚。
  • 新建行、disabled 标记、删除行都是对 profile 自己的 patch 文件做文本补丁,经 ctx.hmr.runExclusive 串行化并原子写入。然后轮询 Loader:如果这一行六秒内没有挂载、标记没有 生效、或者删掉的行仍然挂着,就把先前的文件内容还原,并带着原因失败。
  • 技能启用状态只写本插件自己的 overlay 文档,并让 registry 的目录缓存失效。不改任何技能文件。
  • 技能删除绝不跟随页面给的路径;由宿主自己解析技能、用自己的规则推导目标,并把 trash id 校验 成单个路径段。
  • 凭据永不进入浏览器。 列表和详情 payload 会把所有形如凭据的值替换成 keep 标记;表单把这些字段 显示为空,保存空标记会还原已存储的值。全新的服务器没有东西可还原,所以空的凭据字段会被直接丢掉。

许可

MIT