前言:最近在玩 DeepSeek Harness(DSH),发现一个结构性缺口------技能包装了等于没装。本文讲我怎么填这个坑,以及其中几个比较较真的设计选择。
目录
- [痛点:DSH 技能安装的结构性裂缝](#痛点:DSH 技能安装的结构性裂缝 "#%E7%97%9B%E7%82%B9dsh-%E6%8A%80%E8%83%BD%E5%AE%89%E8%A3%85%E7%9A%84%E7%BB%93%E6%9E%84%E6%80%A7%E8%A3%82%E7%BC%9D")
- [fusion 是什么](#fusion 是什么 "#fusion-%E6%98%AF%E4%BB%80%E4%B9%88")
- 四阶段生命周期设计
- 审计威胁模型:为什么扫指令而非扫脚本
- 五种来源适配器
- [一体四面架构:共享 lib](#一体四面架构:共享 lib "#%E4%B8%80%E4%BD%93%E5%9B%9B%E9%9D%A2%E6%9E%B6%E6%9E%84%E5%85%B1%E4%BA%AB-lib")
- 安全模型
- 零运行时依赖
- [与 dshmarket 的边界](#与 dshmarket 的边界 "#%E4%B8%8E-dshmarket-%E7%9A%84%E8%BE%B9%E7%95%8C")
- 快速上手
- 数据目录设计
- 写在最后
痛点:DSH 技能安装的结构性裂缝
DSH 是 DeepSeek 开源的 agent 框架,设计挺优雅------一切皆插件,Cordis 驱动。但用深了发现一个尴尬的事:它有"技能"和"插件"两条割裂的安装路径。
dsh plugin 路径:
- 本质是 pnpm 转发器,只调和
dsh.profile.bundles - 有
dsh.bundle声明的包进 profile 层 - 没有
dsh.bundle的就当普通依赖装,触发警告:declares no dsh.bundle - installed as a plain dependency, not a profile layer
技能发现路径:
- 只认 6 个固定根目录(
<project>/.dsh/skills、<project>/.agents/skills、~/.dsh/skills、~/.agents/skills、$DSH_BUNDLED_SKILL_DIR) - 一层深,frontmatter 需
name(kebab-case) +description - 由
dsh-skill-filesystem插件扫描
裂缝出现:
纯技能 npm 包(比如 adversarial-review 做代码审查的)发到 npm,skills/<name>/SKILL.md 是 ship 了的,但包没有 dsh.bundle 。dsh plugin add 把它装成普通依赖,SKILL.md 静躺在 node_modules/adversarial-review/skills/ → 不在任何被扫描根里,永不被发现。
用户被迫手动 cp 到 ~/.dsh/skills/。没版本管理、没安全检查、没回滚、没批量管理。
官方的 dsh-skill-manager 是只读 的------包描述明写 "Read-only browser",lib/index.js 仅一个 GET /api/skill-manager/list 路由,从不写文件,只扫 2 个根。连增删改都没有。
DSH 也没有 dsh skill 命令------CLI 只有 dsh plugin(→ pnpm)和 dsh web。技能安装无一等公民命令。
这个缺口真实且已机理性证实。所以我写了 dsh-skill-fusion(技能熔炉) 来填它。
fusion 是什么
一句话:任何来源的技能包,经统一四阶段(发现 → 审计 → 激活 → 固化)进入可用态。
与管插件的 dshmarket 严格互补,零功能重叠------fusion 只管技能,不管插件层。
四阶段生命周期设计
scss
Discover(只读) → Audit(只读+缓存) → Activate(写 ~/.dsh/skills + manifest) → [native 发现/HMR]
↓
Freeze(重审→切链→快照) / Export / Uninstall
Stage 1 · Discover(发现,只读)
扫源、列候选,零写盘。是"可装技能的货架",与"已激活"分开。
源适配器各自返回候选不写盘:
- npm :查 npm registry,拉
package.json,探测是否 ship 了skills/<name>/SKILL.md、是否声明dsh.bundle - github :拉 repo tree(GitHub API),递归找
**/SKILL.md(此源是唯一允许递归的,因为 GitHub repo 结构自由) - claude / codex / agents:读各自根目录(一层深)
- local:读本地路径
Stage 2 · Audit(审计,激活前必跑)
详见下一节审计威胁模型。
verdict 三档:
- pass:无 flag,直接 activate
- warn:有 injection 向量 flag,需用户显式确认
- block:结构性无效(frontmatter 坏/引用断/name 冲突),拒绝
审计结果按 SKILL.md 内容哈希缓存到 ~/.dsh/skill-fusion/audits/<hash>.json,下次同内容免审。
Stage 3 · Activate(激活,写盘)
智能选激活路径:
js
// lib/activate.js 简化逻辑
const strategy = canSymlink(source) ? 'symlink' : 'copy';
if (strategy === 'symlink') {
await fs.symlink(source, target); // 源更新即生效
} else {
await copyDir(source, target); // 回退
}
- 源是已装 npm 包路径 / 本地 / git → symlink(省空间、源更新即生效)
- symlink 失败(Windows 无 dev-mode / zip 拉取的 temp)→ copy 回退
- 有
dsh.bundle的包 → fusion 只 symlink 其 ship 的 skill(若该 skill 未被发现),不代行插件安装
激活后 DSH 原生 watcher 自动发现、HMR 热加载------零重造发现轮子。
orphan 清理 :fusion 启动时对账 manifest vs 文件系统------软链悬空(源被 pnpm remove)→ 标 orphan,UI 提示重链或移除。
Stage 4 · Freeze(固化)
- Pin :写
frozenVersion,更新检查跳过 pinned 项(除非--force) - Update :重新 fetch 源(npm 最新 version;github 最新 commit)→ 内容哈希变 → 重审 → pass/warn-confirmed → 切链/重拷;旧态先入
snapshots/ - Rollback :copy 模式换到
snapshots/<name>@<v>/;symlink 模式 git 源 checkout 旧 commit 重链 - Export :
skill-fusion-bundle-<ts>.json= manifest 子集 + 已激活技能内容 - Import:merge 语义(导出后新激活的保留)、写前校验、失败回滚
审计威胁模型:为什么扫指令而非扫脚本
这是整个项目我最较真的点。
DSH 技能是模型指令(body 进模型 context),不是可执行代码。恶意 SKILL.md 的真实攻击面是 prompt injection:
| 攻击向量 | 示例 |
|---|---|
| 调度劫持 | 恶意 whenToUse / description 触发短语让 skill 在不该触发时被自动调度 |
| 指令覆写 | body 含 disregard above instructions / ignore previous / you are now ... |
| 数据外泄 | body 指示模型 fetch 外部 URL 带上 ~/.dsh/.credentials / env / secrets |
| 越界写 | body 指示写 skill 资源目录外、删非 fusion 管理文件 |
"扫描脚本"的框架会给人虚假安全感------技能没有可执行脚本,执行的是模型读指令后用工具。所以审计的实质是扫指令向量,标 warn 为主、显式确认放行,而非假装能"杀毒"。
hard block 仅用于结构性无效:
js
// lib/audit.js 简化
const BLOCK_REASONS = [
'invalid-frontmatter', // name 非 kebab-case / 缺 description
'broken-references', // references/<file> 被提及但源 bundle 不存在
'name-conflict', // 与已存在 skill 同名
];
// injection 向量一律 warn,不 block
因为结构性无效激活了也 load 不起来或污染目录;injection 向量是用户的信任决定,fusion 只负责把风险显式化。
五种来源适配器
bash
# 本地文件夹
skill-fusion activate --local ./my-skills --name my-skill
# npm 包
skill-fusion activate --npm adversarial-review --name adversarial-review
# GitHub 仓库(可指定 ref)
skill-fusion activate --github owner/repo --ref main --name my-skill
# 从 Claude 的技能库(~/.claude/skills/)
skill-fusion activate --claude --name my-skill
# 从 Codex 的技能库(~/.codex/skills/)
skill-fusion activate --codex --name my-skill
每个源适配器(lib/sources/<name>.js)实现统一接口:
js
// lib/sources/npm.js 简化
export async function discover({ pkg }) {
const meta = await fetchJSON(`https://registry.npmjs.org/${pkg}`);
return extractSkillCandidates(meta); // 返回 [{name, description, source}]
}
export async function fetch({ pkg, version, dest }) {
const tarball = await downloadTarball(meta.dist.tarball);
await extractSkillDir(tarball, dest); // 只解压 skills/ 子目录
}
源适配器测试用 mock fetch,从不触网。
一体四面架构:共享 lib
一个包,四种使用方式,共享同一份 lib/* 核心:
| 面 | 文件 | 职责 | 调用者 |
|---|---|---|---|
| Host 插件 | lib/index.js + cordis.patch.yml |
dsh.bundle 激活进 profile 层;注册 same-origin JSON 路由 |
GUI(浏览器 fetch) |
| 浏览器设置页 | client/client.js |
window.__ModuleLoader__.load + ctx.slots.register("settings.section") |
人(点按钮) |
| CLI | bin/skill-fusion.js + lib/cli.js |
壳调用同一份 lib/* 的子命令 |
agent(bash) |
| 技能 | skills/skill-fusion/SKILL.md |
教 agent 用 CLI 驱动四阶段 | agent(模型) |
四种调用方式任选,逻辑同源,改一处四面同步。SKILL.md 是 agent 入口指向 CLI(避免 agent 直连 localhost JSON 路由的别扭)。
安全模型
这块我比较较真:
- 社区技能落沙箱根 :
~/.dsh/skills/(非trustedHost),body 经ctx.fs沙箱读取,不享受受信待遇 - 绝不滥用
DSH_BUNDLED_SKILL_DIR:那是给 DSH 自带受信技能的(trustedHost: true→ 裸 Node fs)。把未审计社区技能塞进受信根是安全降级 - POST 路由 same-origin 强制 :照搬 dsh-skill-manager 的
isSameOriginRequest(校sec-fetch-site/ origin / host) - 不重启:技能变更高 HMR(watcher),无需 dshmarket 那套 loopback 重启
- 不跑构建脚本 :npm 源走全局
fetch拉 tarball 解压到 fusion 缓存,不执行 scripts - 激活前审计:技能激活后 body 每 session 进模型 context,审计必须在写盘前跑完
零运行时依赖
核实后可达成且与 dsh-skill-manager 一致:
- HTTP(npm/GitHub 发现):全局
fetch(Node 18+) - fs:Node 内置
- frontmatter 解析:自写行级正则解析(照 dsh-skill-manager 的
parseFrontmatter,无 yaml 依赖) - zip 解压:Node 内置
zlib+ 手解 zip header
json
// package.json
{
"dependencies": {}, // 零 own deps
"peerDependencies": {
"react": ">=18",
"@deepseek-ai/cordis": ">=1"
}
}
不引入任何可信小依赖,连 yaml 解析都自己写------够用就行,复杂 YAML 交给发现层权威 dsh-skill-filesystem 去 skip,fusion 不重复其校验。安全模型对标 dsh-plugin-check。
与 dshmarket 的边界
fusion 与 dshmarket(管插件生命周期)严格互补,零功能重叠:
| dshmarket | dsh-skill-fusion | |
|---|---|---|
| 管 | 插件(install/update/uninstall/hot-disable/backup) | 技能(discover/audit/activate/freeze) |
触发 dsh.bundle |
是 | 否(只 symlink 其 ship 的 skill) |
| 重启机制 | loopback 重启 | HMR 热加载 |
fusion 不代行插件安装。有 dsh.bundle 的包 → fusion 只 symlink 其 ship 的 skill(若该 skill 未被发现);插件层(install/update/uninstall)归 dshmarket。
快速上手
1. 安装
bash
# 从 npm 装(发布后)
dsh plugin --profile web add dsh-skill-fusion
# 或从本地源码装
dsh plugin --profile web add /path/to/dsh-skill-fusion
2. CLI 一条命令激活
bash
# 从本地文件夹(最简单,先练这个)
skill-fusion discover --local ./my-skills
skill-fusion audit --local ./my-skills --name my-skill
skill-fusion activate --local ./my-skills --name my-skill
# 从 npm
skill-fusion activate --npm adversarial-review --name adversarial-review
# 从 GitHub
skill-fusion activate --github owner/repo --ref main --name my-skill
3. 浏览器 GUI
DSH Web UI → Settings → Skill Forge / 技能熔炉,两个 Tab:
- Discover:选源 → 搜索 → Audit 看结论(pass/warn/block)→ Activate
- Activated:Freeze / Update / Rollback / Uninstall / Export
4. 给 DSH Agent 说自然语言
fusion 自己的 SKILL.md 被 DSH 发现后,agent 就会自驱。直接说:
- "把
./my-skills/foo这个技能激活一下" - "从 GitHub
owner/repo装一下 my-skill 技能" - "先审计一下这个技能,告诉我有没有风险"
- "冻结 my-skill 在 1.0.0"
- "更新所有未冻结的技能"
agent 会自动跑 discover → audit(把 verdict 报给你)→ 等你确认 warn/block → activate。
5. 生命周期管理
bash
skill-fusion list # 查看所有管理的技能
skill-fusion freeze --name my-skill --version 1.0.0 # 锁版本
skill-fusion update --name my-skill # 更新(自动重审+快照)
skill-fusion rollback --name my-skill # 回滚
skill-fusion uninstall --name my-skill # 卸载
skill-fusion export --out backup.json # 导出清单
skill-fusion import --from backup.json # 导入清单
数据目录设计
perl
~/.dsh/skill-fusion/ # fusion 自有,不在任何被扫描根下
├── manifest.json # 所有权登记簿(旁路,不耦合激活态)
├── audits/ # 审计结果按内容哈希缓存
├── snapshots/<name>/ # 回滚快照(copy 模式)
└── cache/ # npm/github tarball 缓存
~/.dsh/skills/<name>/ # 被激活技能(原生发现、沙箱化,非 trustedHost)
~/.dsh/skill-fusion/ 是 ~/.dsh/skills/ 的兄弟,不在被扫描根内,所以 manifest / audits / snapshots 永不被 dsh-skill-filesystem 扫成 skill。
两条线不耦合:
- 删 manifest 不删 skill(只是 fusion 不再认领它)
- 删 skill 不删 manifest(fusion 标 orphan、下次启动清理)
项目状态
三阶段计划均已完成:
- ✅ Phase 1a --- 行走骨架(lib 核心 + CLI + 本地源)
- ✅ Phase 1b --- host 插件 + 设置页 + npm 源
- ✅ Phase 2 --- freeze 全套(pin/update/rollback/export)
19 个测试文件覆盖:纯函数单测、激活集成、源适配器(mock fetch 不触网)、平台矩阵、审计缓存、orphan 清理、e2e。
bash
# 跑测试
node --test
写在最后
DSH 是个好框架,"一切皆插件"的架构很优雅。但"技能"作为 agent 干活的核心载体,却连个一等公民命令都没有------这事儿不合理。
fusion 不是要替代 dshmarket,也不是要重写 dsh-skill-filesystem 的发现契约。它就是填"纯技能包的安装/激活/生命周期"这一个空缺,复用 DSH 已有的发现根、HMR、watcher,不重造轮子。
MIT 协议,三阶段交付完成,19 个测试文件覆盖。欢迎来 PR、提 issue、或者就是 star 一下让我知道有人在用。
相关阅读:
如果觉得有用,欢迎去 GitHub 给个 star ⭐
本文永久链接:github.com/UltimateJob...
作者:UltimateJob
License:MIT