dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
在 DeepSeek Harness 的设置面板里加一个「Skill 管理」分区(左栏导航,排在「Agent 预设」下方), 用来查看和打理本机的 agent skill。
设置
├── 通用设置
├── 模型
├── 内置插件
├── Agent 预设
└── Skill 管理 ← 本插件,order 30
页面本身:
.dsh/skills 与 .agents/skills 合成一组),
标题用缩短后的项目路径(~\Desktop\agent-test)——两个同名项目也能分辨;.dsh → 用户 .agents → 随包内置 → 运行时;SKILL.md 路径(主目录显示成 ~,悬停看全路径),
以及按可得性出现的操作按钮。| 按钮 | 出现条件 | 干什么 |
|---|---|---|
| 启用 / 禁用 | 文件系统上可写的 skill | 往 SKILL.md 的 frontmatter 写 disable-model-invocation / user-invocable(不改文件名;没有 frontmatter 的裸文件才退回改名) |
| 打开目录 | 有路径的 skill(磁盘上的,或注册表 resourceBase 报出目录的) | 资源管理器定位到它(平铺文件用 /select, 选中,不是打开) |
| 查看 / 收起 | 有路径的 skill | 就地预览 SKILL.md 正文 |
| 删除 | 文件系统上可写的 skill | 删掉整个 skill(内联二次确认,不可恢复) |
一屏只留一个"实体按钮"(启用/禁用,描边),定位、预览、删除都做成轻量文字动作。
随包内置的(bundled)与运行时 provider 提供的 skill 是只读的:能定位、能预览, 但没有启用/禁用与删除按钮(宿主侧也照样 403)。
DSH 的 skill 是文件系统资源,插件的扫描规则与 @deepseek-ai/dsh-skill-filesystem 对齐:
| 根目录 | source | 显示权重 |
|---|---|---|
<项目>/.dsh/skills | project-dsh | 100 |
<项目>/.agents/skills | project-agents | 200 |
$DSH_HOME/skills(默认 ~/.dsh/skills) | user-dsh | 400 |
$DSH_AGENTS_HOME/skills(默认 ~/.agents/skills) | user-agents | 500 |
$DSH_BUNDLED_SKILL_DIR | bundled | 600 |
这几个数字只是本插件排显示顺序用的权重。公共契约里的
SkillSummary并不带 rank (rank / locator 只属于 provider 层的SkillCandidate,注册表不往外给),所以它跟 provider 内部的名次没有可比性。
项目根取 DSH 进程的工作目录,外加工作区账本(ctx.workspaceRegistry)里登记过的每个路径。
每个根下认两种形态:目录包 <name>/SKILL.md,以及平铺文件 <name>.md。
以 . 开头的条目(含 .system)一律跳过。
清单来自两处合并:
ctx.skills.list() —— DSH 实际认得的 skill,含随包内置与运行时 provider 注册的;两者缺一不可:被禁用的 skill 只存在于磁盘上(注册表看不到它),而运行时 provider 的 skill 只存在于注册表里(没有文件)。页面会标出每个条目的来源;注册表整个不可用时,页面会明确提示 "现在只有磁盘扫描结果"。
合并只按路径认领:注册表条目要么带顶层 path,要么带
resourceBase: { kind: 'directory', path }(bundled provider 实测就是后者)。
认领不到就各自成条 —— 以前还按"条目名"兜底,那会把"注册表里活着的 foo"错挂到
"磁盘上已禁用的 foo":页面显示已禁用,按钮却去改另一个文件。宁可多一条只读条目,
也不张冠李戴。
注册表报出来的目录(bundled 等)不在任何扫描根里,所以另开一条只读通道:
read / reveal 放行,toggle / delete 依旧 403,而且只认注册表当次报出来的路径。
实测结论(0.2.0-rc.2):宿主插件上下文里的
ctx.skills.list()只返回随包内置与运行时 provider 的 skill,看不到skill-filesystem提供的那批 —— DSH 的 skill 注册表可以由 agent preset 或 host composition 挂载,而文件系统 provider 挂在前者。所以页面上的文件系统 skill 一律来自磁盘扫描,这不是降级。GET /skill-manager/api/registry能把各 cwd 的原始返回摊开看。
禁用不改文件名。 DSH 的 @deepseek-ai/dsh-skill-filesystem 本来就认两个 frontmatter
开关,插件只是把它们写进去:
---
name: my-skill
description: ...
disable-model-invocation: true # 模型目录与加载路径都够不到它
user-invocable: false # 人用的斜杠命令也够不到
---
provider 的 parseInvocationPolicy 把它解析成 invocation 策略
(modelInvocable: disableModelInvocation !== true、userInvocable: userInvocable !== false),
于是:
dsh-tool-skill 渲染目录时 snapshot.skills.filter(isModelInvocable) —— 模型看不见它;skill 工具,也会拿到
skill "x" is not available for model invocation(加载路径同样拦);isUserInvocable 过滤。文件、正文、名字、DSH 注册表里的条目全都原样保留 —— 它只是"不生效"。启用则把两个键
写回放行值(false / true,语义等同不写,但显式、幂等、不删用户自己写的键)。
写入是"同目录临时文件 + rename",BOM 与 CRLF/LF 原样保留,只动这两行:
SKILL.md → 抛 no-frontmatter,退回改名(唯一保留改名的场景)。改名的老办法仍然认得出来:SKILL.md.disabled / <name>.md.disabled 会照旧显示为"已禁用",
点启用会把文件名改回来;两种禁用叠加时先摘标记、再还原文件名。
为什么不干脆只改名?改名确实能让 provider 彻底看不见它,但文件名会变 —— 用户看到
SKILL.md.disabled会以为"文件被动了",而 DSH 自己的语义是文件不动、策略不允许。 另外两个键的值 provider 认不出会整条抛错(skill 反而从目录里消失), 所以插件只写干干净净的true/false。
所有文件动作的目标都不是从请求体里信任的,而是重新扫描后核对过的:
bundled 根下的一律拒绝(403);resolve() 规范化,.. 穿越会被前缀检查挡掉;read / reveal;写接口另有一道跨站防线:content-type 不是 application/json、且 Origin 又对不上
Host 的请求直接 403。浏览器发起的跨站"简单请求"只能带 text/plain 这类 content-type,
正好被挡住;带 JSON 的跨站请求会先触发预检,而本服务没有 CORS 应答,浏览器自己就挡了。
本页面永远发 JSON,本机脚本通常不带 Origin,都不受影响。
全部挂在 /skill-manager/api:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /health | 自检:home、agentsHome、bundledDir、注册表是否可用 |
| GET | /list | 完整清单:projects / roots / skills / registryAvailable |
| GET | /registry | 诊断:把 ctx.skills.list() 的原始返回按 cwd 逐个摊开 |
| POST | /toggle | { target } → 启停(写 frontmatter 标记,或还原历史改名) |
| POST | /delete | { target } → 删除 |
| POST | /read | { target } → 返回 SKILL.md 正文(>512 KB 拒绝) |
| POST | /reveal | { target } → 资源管理器定位(仅 Windows) |
/list 里每个 cwd 的注册表查询是并发发起的,整体共用一个 8 秒 deadline:
provider 卡住也不会把一次 /list 挂成分钟级(以前是串行 N × 8 秒)。
先克隆到本地(路径里别带空格),再用 dsh plugin 以 file: 依赖把它装进 profile:
git clone https://github.com/Nay-1/dsh-skill-manager.git C:/dsh-plugins/dsh-skill-manager
dsh plugin --profile desktop add "file:C:/dsh-plugins/dsh-skill-manager"
desktop 是 profile 名,按你自己的改。装完确认 profile 的 package.json 里
dsh.profile.bundles 有 dsh-skill-manager —— 只有进了 bundles,cordis.patch.yml
那行插入才会生效(dsh plugin add 通常会自动加,加不上就手动补一行)。
pnpm 的 file: 依赖是拷贝而不是软链,而且它只看 lockfile:直接再 add 一次只会得到
Already up to date,磁盘上的拷贝不会更新。必须先 remove 再 add:
dsh plugin --profile desktop remove dsh-skill-manager
dsh plugin --profile desktop add "file:C:/dsh-plugins/dsh-skill-manager"
另外 host 半和 client 半都只在 DSH 启动时加载一次(没有热重载),改完必须重启桌面端。
卸载:
dsh plugin --profile desktop remove dsh-skill-manager
然后从 dsh.profile.bundles 里删掉同名条目。
改源码后必须重装一次再重启 DSH:pnpm 的
file:依赖是拷贝而不是软链。
dsh-skill-manager/
├── package.json 插件清单;dsh.bundle.patch 指到 cordis.patch.yml
├── cordis.patch.yml bundle 层:插入本插件
├── lib/index.js host 半:扫描 + HTTP API + 文件动作
├── lib/client.js client 半:settings.section 页面
├── test-host.mjs host 半自测(90 项)
└── test-client.mjs client 半自测(67 项)
node test-host.mjs # 临时目录里造真实 skill 树,打 API
node test-client.mjs # mock Module Loader / React / fetch,真渲染一遍页面
SKILL.md 必须可写(bundled 一律 403)。rm -rf 掉 skill 目录,没有回收站。SKILL.md 原文(不渲染 Markdown),够用来核对内容。revealed: false。readdir 的 Dirent 对 symlink / junction 既不报目录也不报文件,
所以链接形式的 skill 条目会被跳过(provider 是否跟随未验证,故不擅自扩大范围)。<name>.md、.system 跳过、rank 数值)是与
@deepseek-ai/dsh-skill-filesystem 对齐的约定,但该 provider 不在公共契约里,
升级 DSH 后值得重新核对一次。disable-model-invocation: true + user-invocable: false)—— 这是 provider 自己解析的
invocation 策略,模型目录(filter(isModelInvocable))与加载路径
(skill 工具抛 "not available for model invocation")双双拦得住,而文件、正文、名字、
注册表条目全部原样保留。启用写回 false / true(显式、幂等、不删用户写的键)。
改名只留给没有 frontmatter 的裸文件兜底,以及识别历史遗留的 .disabled 改名。/list 每个 skill 新增 disableMode("flagged" / "renamed" / null),
页面上的"已禁用"徽章悬停能看出文件到底动没动。SKILL.md 会丢掉 frontmatter 最后一行的字段。切出来的 body 末尾留着
一个 \r,而 JS 正则的 . 不匹配 \r,(.*)$ 于是整行匹配失败 —— description
约定俗成就写在最后一行,所以受伤的几乎总是它(实测本机 5 个用户 skill 里 3 个描述为空)。
顺带兼容带 BOM 的文件(startsWith("---") 曾经直接失败)。解析前统一归一化行尾。path 或 resourceBase)。
以前同名条目会被错挂:注册表里活着的 foo 显示成"已禁用",按钮却指向磁盘上另一个 foo。resourceBase: { kind: 'directory', path }(bundled provider 实测用这个,
顶层 path 反而没有),于是随包内置的 skill 在页面上有了路径,可以定位与预览;
启停 / 删除仍然 403(新增一条只读通道,只认注册表当次报出来的路径)。/list 的注册表查询改成并发 + 单一 8 秒 deadline(原来串行 N × 8 秒)。rank 挑同名注册表条目的死代码 —— SkillSummary 根本没有 rank,那行比较恒为假。explorer.exe 定位平铺技能时改用 /select,<路径>(原来会把 .md 直接打开),
并且等到真的 spawn 成功才报 revealed: true。content-type + Origin)。load 不再依赖 t 的身份 —— 宿主若每次渲染换一个新的 t,
原来会"拉列表 → setState → 重渲染 → 再拉列表"死循环(测试里已能复现:2 → 6 次)。resourceBase /
只读通道 / 跨站请求 / 单 cwd 失败降级 / t 身份稳定性 / 预览清理等用例。