`dsh plugin --profile web add <pkg>` 到底干了什么
0. 你的命令 — 一句话解读
dsh plugin --profile web add @studyzy/dsh-web-remote-access
意思是: 把 @studyzy/dsh-web-remote-access 这个 npm 包安装到名为 web 的 profile 里。
这跟 npm install 不一样 — 它把包装进 $DSH_HOME/profiles/web/ 这个独立目录,自动把它登记到 profile 的 layer 列表(如果它声明了 dsh.bundle)。
1. 真实的实现 — apps/cli/src/plugin.ts (158 行)
读 dsh 实际代码,流程是清楚的两步:
runPlugin(profile, args)
├─ 1. resolveProfileDir('web') → $DSH_HOME/profiles/web
├─ 2. 如果没有 package.json 就 initProfile(...)
├─ 3. spawnSync('pnpm', args, cwd=profileDir) ← 把 args 透传给 pnpm
└─ 4. pnpm 退出 0 → reconcilePlugins(before, profileDir) ← 同步 bundles 列表
reconcilePlugins 干的事 (真实代码第 59-91 行):
// 对每个安装在 profile/node_modules 下的包:
// - 如果它在 package.json 里有 dsh.bundle 声明 → 加进 dsh.profile.bundles
// - 如果没有 → 警告但保持为普通依赖
关键: dsh plugin add 本质是 pnpm forwarder + manifest 自动同步器。它不改 dsh 自己的代码,只改 profile 目录里的 package.json 和 cordis.patch.yml。
2. 它"装到"哪里 — 真实路径
$DSH_HOME/ ← 通常是 ~/.dsh (C:\Users\你\.dsh\ 在 Windows)
├── profiles/
│ ├── web/ ← `dsh --profile web` 用的
│ │ ├── package.json ← 在这里登记 bundles + dependencies
│ │ ├── cordis.patch.yml ← 你的用户自定义层
│ │ ├── pnpm-workspace.yaml ← pnpm 配置 (hoisted linker)
│ │ ├── node_modules/ ← pnpm 装的包在这
│ │ │ └── @studyzy/
│ │ │ └── dsh-web-remote-access/ ← 你的包就装在这里
│ │ └── ...
│ ├── headless/
│ └── my-custom/
└── ...
$DSH_HOME 怎么找:
- 先看
$DSH_HOME环境变量 - 没有就
~/.dsh(Windows 上是C:\Users\<you>\.dsh\)
dsh 第一次启动时如果没这个目录会自动创建。
3. bundle 是个什么东西 — 你必须给你的包加 dsh.bundle
dsh plugin add 装上你的包后会自动检查它有没有 dsh.bundle 声明。有,就激活;没有,只装没激活(还警告)。这是 reconcilePlugins 干的事。
@studyzy/dsh-web-remote-access 必须这样写 package.json 才会被 dsh plugin add 真正激活:
{
"name": "@studyzy/dsh-web-remote-access",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
}
关键字段:
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }← 必须,否则只是普通 npm 依赖,不进入 layer 列表"files"包含cordis.patch.yml← npm publish 不会自动带非main引用的文件,要明确列出
4. 完整的 cordis.patch.yml 长什么样
真实例子 (从 docs/user/develop/basic/publish.md 抄来):
# hello-plugin/cordis.patch.yml
- insert:
- id: hello
name: dsh-hello-plugin
更复杂的真实例子 (从 examples/web-cordis/cordis.yml 抄来):
- id: webserver
config:
host: 127.0.0.1
port: 3081
- insert:
- id: cordis-host-runner
name: '@deepseek-ai/dsh-cordis-host-runner'
- id: tool-cordis
name: '@deepseek-ai/dsh-tool-cordis'
对你的 @studyzy/dsh-web-remote-access 来说,大概长这样:
# @studyzy/dsh-web-remote-access/cordis.patch.yml
- insert:
- id: studyzy-web-remote-access
name: '@studyzy/dsh-web-remote-access'
5. 你的包里真正跑 JS 的入口
@studyzy/dsh-web-remote-access/index.js (或 dist/index.js):
export const name = 'studyzy-web-remote-access'
export function apply(ctx, config) {
// 你想做的事情 — 注册工具、监听事件、什么都可以
ctx.on('session/event', (session, event) => {
if (event.type === 'assistant/chunk') {
// ...
}
})
}
6. 完整目录结构 — 你打包时要长这样
@studyzy/dsh-web-remote-access/
├── package.json # dsh.bundle declaration
├── cordis.patch.yml # 你的 layer
├── index.js # 入口 (被 patch 引用)
├── README.md
└── LICENSE
就这么 4 个文件,不需要 src/lib/typescript。这是有意为之 — dsh bundle 故意的就是简单。
7. 完整分发流程
# 1. 在你的项目目录
mkdir studyzy-dsh-web-remote-access && cd studyzy-dsh-web-remote-access
git init
# 2. 写上面 4 个文件
# - package.json (带 dsh.bundle)
# - cordis.patch.yml
# - index.js
# - README.md
# 3. 发布到 npm
npm publish --access public
# 4. 用户装入 web profile
dsh plugin --profile web add @studyzy/dsh-web-remote-access
# 5. (可选) 删掉
dsh plugin --profile web remove @studyzy/dsh-web-remote-access
# 6. 更新
dsh plugin --profile web update @studyzy/dsh-web-remote-access
# 7. 跑 — 注意是 dsh --profile web,不是 dsh plugin
dsh --profile web
8. 从 GitHub 装 (你也可以这样)
dsh plugin add 实际上就是 pnpm 的透传,所以 pnpm 支持的所有源都支持:
dsh plugin --profile web add github:studyzy/dsh-web-remote-access
dsh plugin --profile web add github:studyzy/dsh-web-remote-access#v0.1.0 # pin 标签
dsh plugin --profile web add github:studyzy/dsh-web-remote-access#abc123 # pin commit
dsh plugin --profile web add ./本地路径
dsh plugin --profile web add ./hello-plugin-0.1.0.tgz # pnpm pack
两个坑 (从 publish.md 抄):
- GitHub 装的是源代码,不是构建好的产物 —
pnpm install不会跑build。如果你用 TypeScript,需要包prepare脚本:"scripts": { "prepare": "tsdown" } - pnpm ≥10 拒绝跑 git 依赖的
prepare— 用户必须显式允许:# $DSH_HOME/profiles/web/pnpm-workspace.yaml allowBuilds: "@studyzy/dsh-web-remote-access": true # ← 注意 import 的名字也要写对
9. 你会看到的实际效果
跑 dsh plugin --profile web add @studyzy/dsh-web-remote-access 之后:
$DSH_HOME/profiles/web/
├── package.json # ← dsh plugin 改了这个文件
│ {
│ "name": "dsh-profile-web",
│ "private": true,
│ "dependencies": {
│ "@studyzy/dsh-web-remote-access": "^0.1.0" ← pnpm 加的
│ },
│ "dsh": {
│ "profile": {
│ "bundles": [
│ "@deepseek-ai/dsh-base",
│ "@deepseek-ai/dsh-web-app",
│ "@studyzy/dsh-web-remote-access" ← dsh reconcilePlugins 加的
│ ]
│ }
│ }
│ }
├── node_modules/
│ └── @studyzy/
│ └── dsh-web-remote-access/ ← 装在这
├── cordis.patch.yml ← 你的用户层 (空的话就这样)
└── pnpm-workspace.yaml
Layer 顺序 (这个很重要):
dsh-base(默认)dsh-web-app(web profile 自带)@studyzy/dsh-web-remote-access(你的,最后装的)cordis.patch.yml(用户自己的,永远在最后)
后面的层可以覆盖前面的层 — 这就是为什么用户能在 cordis.patch.yml 改你的 config。
10. --profile web 之外的 profile
dsh 默认有这些 profile (从 pkg/boot/app-boot/src/profile.ts 实际代码):
| profile | bundle 层 |
|---|---|
web |
@deepseek-ai/dsh-base + @deepseek-ai/dsh-web-app |
headless |
@deepseek-ai/dsh-base + @deepseek-ai/dsh-web-app + @deepseek-ai/dsh-headless |
| 自定义名字 | 默认 @deepseek-ai/dsh-base (如果没有 PROFILE_TEMPLATES 命中) |
所以 dsh --profile web 跑的是 web UI,dsh --profile headless 跑的是无 UI 的命令行,自定义名要先 init。
11. 验证你的 @studyzy/dsh-web-remote-access 能跑
# 1. 装进去
dsh plugin --profile web add @studyzy/dsh-web-remote-access
# 2. 看实际加载的插件树
dsh --profile web --dump-config
# 3. 启动
dsh --profile web
# 4. 在 browser (http://127.0.0.1:3080) 里看是否生效
如果 --dump-config 输出的 YAML 里有 你的 @studyzy/dsh-web-remote-access 这条,说明 layer 注册成功。
12. 一个最小可发布的样例 (完整,粘贴即用)
@studyzy/dsh-web-remote-access/package.json:
{
"name": "@studyzy/dsh-web-remote-access",
"version": "0.1.0",
"description": "Web remote access for dsh",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"license": "MIT",
"keywords": ["dsh-plugin", "deepseek-harness", "cordis"],
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
}
@studyzy/dsh-web-remote-access/cordis.patch.yml:
- insert:
- id: studyzy-web-remote-access
name: '@studyzy/dsh-web-remote-access'
@studyzy/dsh-web-remote-access/index.js:
export const name = 'studyzy-web-remote-access'
export function apply(ctx, config) {
console.log('[studyzy-web-remote-access] loaded')
ctx.on('session/event', (session, event) => {
if (event.type === 'assistant/chunk') {
// 你的实现
}
})
}
@studyzy/dsh-web-remote-access/README.md (按 dsh 习惯):
# @studyzy/dsh-web-remote-access
[English](README.md) | [中文](README.zh.md)
为 dsh 增加远程访问能力。
## 安装
```sh
dsh plugin --profile web add @studyzy/dsh-web-remote-access
配置
无。
事件
监听 session/event。
License
MIT
```bash
npm publish --access public
完事。
13. 什么时候 不 用 dsh.bundle?
如果你的包只是给其他 dsh 插件用的库 (例如一个共享的工具函数集),不要加 dsh.bundle,否则 dsh plugin add 会警告 ("declares no dsh.bundle")。
// 纯库,不是 bundle
{
"name": "@studyzy/dsh-shared-utils",
"version": "0.1.0",
"main": "index.js",
"type": "module",
"files": ["index.js"]
// 没有 dsh.bundle
}
让别人 dsh plugin add @studyzy/dsh-shared-utils 后,只是把库文件装进 node_modules,不激活为 layer (但你的 bundle 包可以通过 import 拿到它)。
14. 一个隐藏的细节 (从源码第 156-167 行)
initProfile 给 profile 创建 package.json 时:
{
"name": "dsh-profile-web",
"private": true,
"dependencies": {},
"dsh": { "profile": { "bundles": [...] } }
}
"private": true 防止意外 npm publish。"name": "dsh-profile-<name>" 是 profile 标识,不会影响你的 bundle 名字。
pnpm-workspace.yaml 包含:
packages:
- .
nodeLinker: hoisted
autoInstallPeers: false
nodeLinker: hoisted 让所有包安装成扁平 node_modules (跟 npm 一样),不是 pnpm 默认的隔离安装 — 这是为了 dsh plugin 名字解析的兼容性。

浙公网安备 33010602011771号