skill-external-roots 插件开发与部署记录
August 15, 2026 · View on GitHub
目标:在不修改 DSH 主仓库源码的前提下,把本机 codex / claude / cursor / opencode 等外部 agent 工具目录里的既有技能,挂进运行中 dsh 的
ctx.skills注册表(source: 'external'、rank 350),让模型可直接调用。配套上层方案:dsh-launcher「技能」子界面(扫描/导入是启动器文件层的事;本插件提供"模型侧可调用"的运行时能力)。本文档 = 实现记录 + 实测结果;调研与设计底稿见
packages/skill-external-roots/docs/development-plan.md(已标注实现状态)。
1. 结论先行
| 问题 | 结论 |
|---|---|
| 需要改主仓库吗? | 不需要。纯 Provider 插件 + profile 用户补丁层激活 |
| 插件形态? | Provider 型函数插件(inject: ['skills']),向 ctx.skills.registerProvider(...) 注册 ExternalRootsProvider;不新增服务、不拥有 ctx.<key> |
| 挂哪些根? | codex ~/.codex/skills、claude ~/.claude/skills、cursor ~/.cursor/skills + ~/.cursor/skills-cursor(精确探测,不展开通配)、opencode ~/.config/opencode/skills;不存在的根静默跳过;customDirs 追加;agentsRoot 默认 false(~/.agents/skills 已被内置 skill-filesystem 覆盖) |
| 技能怎么来? | 复用官方本地提供方解析规则:<kebab>/SKILL.md 目录包 + 扁平 <kebab>.md,frontmatter 必填 name+description,多余键(Cursor environments、Codex license 等)忽略 |
谁拥有 ctx.skills? | @deepseek-ai/dsh-skill(Service Definition);本插件与 @deepseek-ai/dsh-skill-filesystem 一样只是注册进 registry |
| 与内置提供方如何协作? | 不同 provider 名(external-roots vs filesystem)、不同 source(external vs custom/user-*);rank 350 落在"项目之后、用户之前";同名冲突时项目 > 外部 > 用户 |
| 运行时刷新? | chokidar 监听各已存在根(沿用官方 stability/poll 参数思路)→ control.invalidate();watch: false 逃生口 |
| Invariant? | 注册 manifest 名 + 每次 skills/change 检查"扫描→候选"数据关系(候选路径必须在记录根内;缺失根不得产候选) |
2. 包结构与导出
packages/skill-external-roots/
├── src/index.ts name='skill-external-roots' / inject=['skills'] / Config / apply
├── src/provider.ts ExternalRootsProvider(list/get) + roots 探测 + frontmatter 解析 + chokidar watch + health
├── src/types.ts 仅 wire 类型(ExternalSkillLocator / ExternalRootProbe / ExternalRootKind)
├── src/invariant.ts manifest 名 + 数据关系检查(checkExternalRootsHealth)
├── tests/external-roots.spec.ts 单测(29 用例)
├── tests/registry.spec.ts 集成(cordis-plugin-loader 组装 skills + 本插件)
├── tests/external-roots.e2e.ts 真实机器根冒烟(无 codex/cursor 根时自跳过)
├── README.{md,zh.md} + README.i18n.yaml
└── package.json @dsh-plugins/skill-external-roots@0.1.0
- named-export
name/inject/Config/apply,无 default export; exports含"."、"./invariant"、"./src/*"、"./package.json";peerDependencies:@deepseek-ai/dsh-skill/dsh-invariants^0.1.0-rc.6、@deepseek-ai/cordis^4.0.1(公开 npm 线,无workspace:^);dependencies:@deepseek-ai/schemastery、chokidar(^5.0.0)、yaml;devDependencies另含@deepseek-ai/cordis-plugin-loader^1.0.2(集成测试用)。- Config 全字段可选,默认值显式写进 schema(
providerName/enabled{codex,claude,cursor,opencode}/customDirs/agentsRoot/rank/exclude/watch,与方案 §4.2 一致);无密钥字段。 - 方案 §5 草案里的
@deepseek-ai/cordis-plugin-loader: ^4.0.1按 npm 实际版本修正为^1.0.2(该包发布线是 1.x,不是 4.x)。
3. 关键实现决策
- 根解析是"精确候选 + 存在性探测":
defaultRootCandidates()$ 纯函数产出有序路径(\text{codex} → \text{claude} → \text{cursor} \times 2 → \text{opencode} → \text{agents}(可选) → \text{customDirs});\text{provider} 逐个 $stat探测,目录才挂载——不存在的根静默跳过,绝不展开通配。 - 解析规则与官方对齐、对未知键宽松:
parseExternalSkill()复用---frontmatter +isSkillNamekebab 校验 + 布尔文法(true/1/yes/on等);未知键忽略(不因 Cursorenvironments、Codexlicense拒载);与官方不同处:camelCase 遗留键不抛错(外部工具不受 dsh 约定约束),仅忽略。 .system桶跳过:Codex 的~/.codex/skills/.system/是其内部技能(review-agent、skill-creator…),不挂进 dsh 目录(list 与 watcher 事件过滤两处都跳过)。- watcher 简化版:已存在根用 chokidar(
depth: 1+awaitWriteFinishstability/poll +usePolling可注入);不存在的根用「最近存在祖先」watchFile轮询(根被创建时下一次读取失效);相关事件按官方isRelevantWatchEvent语义过滤(直属目录增删、扁平.md增删改、<skill>/SKILL.md增删改)。watch:false时不建任何 watcher。 - invariant 走模块级 health 单例:
apply创建ExternalRootsHealth并经registerExternalRootsHealth()注册;provider 每次list()记录{root, exists, candidates};invariant 伴侣在安装时 + 每次skills/change检查"候选 ∈ 根内"与"缺失根零候选"两条关系。 - 集成测试用真实 Loader:
Context + cordis-plugin-loader,loader.internal.import桩把@deepseek-ai/dsh-skill与@dsh-plugins/skill-external-roots映射到进程内模块(源文件,不经 lib/);loader.create返回 entry id,loader.remove(id)验证 dispose 后移除。
4. 实测结果(本机,2026-08-16)
4.1 构建 / 类型 / 测试
$ cd /Users/yoahoug/Desktop/dsh-plugins
$ pnpm install # 新 workspace 包 + 依赖落位
$ pnpm run build # tsc → lib/,3 包全绿
$ pnpm run typecheck # 全绿
$ pnpm run test # 9 文件 139 用例全绿(本包 29 单测 + 3 集成)
$ pnpm run test:e2e # 本包真实机器根冒烟通过(tavily/vision 无 key 自跳过)
4.2 安装与激活(web profile)
$ cd /Users/yoahoug/Desktop/deepseek-harness # 主仓库只读,仅运行 dsh CLI
$ pnpm dsh plugin --profile web add file:/Users/yoahoug/Desktop/dsh-plugins/packages/skill-external-roots
# ✓ 安装成功:+ @dsh-plugins/skill-external-roots file:...
# 提示:declares no dsh.bundle — installed as a plain dependency, not a profile layer
# (与 web-search-tavily / vision-bridge 同款:无 bundle manifest,走用户补丁层 insert 激活)
# 未出现 pnpm allowBuilds 提示(file: 安装不运行 build 脚本,无需授权)
~/.dsh/profiles/web/cordis.patch.yml 追加 insert 行(id: skill-external-roots,配置 enabled 四族全开)。
$ pnpm dsh --profile web --dump-config | grep -A8 skill-external-roots
- id: skill-external-roots
name: '@dsh-plugins/skill-external-roots'
config:
enabled:
codex: true
claude: true
cursor: true
opencode: true
4.3 运行中 dsh web 验证(新开实例,端口 3099,未动既有 3080 实例)
新开实例:pnpm dsh --profile web --port 3099。经 web 前端同一 RPC 通道(POST /api/session.create + POST /api/skill.list,即 web「/」技能面板的 ui-skill 数据源)验证:
session.create(cwd=dsh-plugins,preset=no-subagent)→skill.list返回 29 个技能,含外部工具技能:codex 的read-opencode-session/gh-private-exe-release/miniapp-request-study,cursor 的automate/canvas/create-skill/sdk等(另有 preset 侧skill-filesystem提供的server-ops/tavily-*/win-host);- watch 生效:向
~/.codex/skills新增zz-watch-probe技能 → 3 秒内skill.list出现zz-watch-probe(且本会话模型侧<available_skills>目录同步出现);删除后 3 秒内消失; - 模型侧可加载:外部技能进入模型
<available_skills>目录(只有 name+description),skill()加载正文路径 = registryget()→ provider 重读文件,由单测/e2e 覆盖(真实机器根get()返回正文断言通过); - 验证完毕停掉 3099 实例、删除探测 session;既有 3080 实例保持健康(HTTP 200)且同样加载了新组合(其会话目录已含外部技能)。
4.4 遗留事项
- OpenCode 根在本机不存在(
~/.config/opencode只有opencode.json等)→ 静默跳过,符合设计; - Claude 根全部是指向
~/.agents/skills/*的符号链接:~/.claude/skills名下技能与 agents 根内容相同,属用户既有布局;本插件按目录包规则跟随符号链接解析(与官方一致),不产生去重(同名单靠 rank/层裁决)。 invariants服务行未在 web profile 挂载(既有组合没有/invariant行);如部署方启用,按 README 加skill-external-roots-invariant行即可。
5. 相关文件
- 方案底稿(调研/设计/实施计划):
packages/skill-external-roots/docs/development-plan.md - 根 README 插件清单表与「相关文档」:
README.md - 主仓库契约:
deepseek-harness/docs/subsystems/skills.zh.md、deepseek-harness/packages/skill/skill-filesystem/src/index.ts - 模板:
packages/web-search-tavily/