前言
DeepSeek Harness(下文简称 dsh)采用插件化的方式扩展功能。
第一次接触 dsh 插件开发,最容易产生困惑的是这些概念之间的关系:
- Plugin 到底是什么?
apply(ctx)为什么是插件入口?ctx到底能做什么?- Tool 和 Plugin 有什么区别?
- Config 和 Tool Parameters 为什么不是一回事?
cordis.yml、--patch、Profile、Bundle 又分别负责什么?- 本地开发插件和正式安装插件,底层到底是不是两套机制?
- Host 插件和 Web 客户端插件为什么要拆开?
因此本文以自己做的一个用量监控插件从“写出来”到“跑起来”再到“打包发布”的完整过程来讲。
从零开始写出并运行第一个插件
怎么写出一个最简单的插件(Plugin),并让 dsh 真正把它运行起来?
Plugin 是什么
在 DeepSeek Harness 中,插件本质上是一个 Cordis 插件。
它需要提供一个 Harness 可以调用的入口,最基本的结构是:
import type { Context } from "@deepseek-ai/cordis";
export const name = "hello-plugin";
export function apply(ctx: Context) {
console.log("[hello-plugin] plugin loaded!");
}
这里最重要的是两个东西:
name
└── 标识插件
apply(ctx)
└── 插件被加载时执行的入口
因此可以先把 Plugin 理解成:
一组由 Harness 负责加载和管理生命周期的扩展代码。
name 与 apply(ctx)
name
export const name = "hello-plugin";
name 是插件模块导出的名称。
它描述的是:
“这个模块是什么插件”
apply(ctx)
export function apply(ctx: Context) {
console.log("plugin loaded");
}
插件被 Harness 加载时,会调用:
apply(ctx)
所以:
Harness 加载模块
↓
找到 apply()
↓
调用 apply(ctx)
↓
插件开始运行
后面当插件拥有配置以后, apply(ctx) 会变成 apply(ctx,config) :
export function apply(ctx: Context, config: Config) {
// ...
}
创建一个插件
开发阶段最方便的方式,是直接写一个本地 TypeScript 插件。
可以使用类似目录:
deepseek-harness/
└── scratch-plugin/
├── cordis.yml
└── src/
└── hello.ts
hello.ts:
import type { Context } from "@deepseek-ai/cordis";
export const name = "hello-plugin";
export function apply(ctx: Context) {
console.log("[hello-plugin] plugin loaded!");
}
创建 cordis.yml
接下来创建:
scratch-plugin/cordis.yml
内容:
- insert:
- id: hello
name: "/absolute/path/to/deepseek-harness/scratch-plugin/src/hello.ts"
暂时只需要这样理解:
id
└── 这条插件配置叫什么
name
└── 插件源码文件在哪里
使用 --patch 启动
运行:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
或者:
npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml
如果插件被正确加载,就应该看到:
[hello-plugin] plugin loaded!
到这里就已经完成了插件的开发和调用:
写插件
↓
告诉 dsh 插件在哪里
↓
启动 dsh
↓
插件被加载
↓
apply(ctx) 被调用
插件加载之后能做什么
第一章中,我们写了:
export function apply(ctx: Context) {
// ...
}
那么这个 ctx 到底是什么呢,现在正式解释:
ctx是插件与 Harness 交互的入口。
插件被加载之后,大部分能力都从这里开始。
Context(ctx)是插件与 Harness 的入口
在正式插件中,经常会看到:
export function apply(ctx: Context, config: Config) {
ctx.on("llm/stream", (options, next) => {
// ...
});
ctx.inject(["settings"], (settingsCtx) => {
// ...
});
ctx.inject(["webServer"], (webCtx) => {
// ...
});
}
也就是说:
插件不是直接修改 Harness 内部代码,而是通过 Context 注册行为和使用系统能力。
可以这样理解:
Plugin
│
▼
apply(ctx, config)
│
┌───────────┼───────────┐
▼ ▼ ▼
监听事件 使用服务 注册资源
ctx.on ctx.inject ctx.effect
ctx.on():监听系统事件
例如:
ctx.on("llm/stream", (options, next) => {
// 在模型流式调用过程中执行逻辑
});
这类事件机制可以用于:
- 调用统计
- 日志
- 监控
- 钩子
- 在某类系统事件发生时执行额外逻辑
例如后面我自己做的 usage-monitor,就是通过模型调用相关事件统计 LLM API 用量。
可以这样理解:
Harness 发生事件
↓
ctx.on(...)
↓
插件执行自己的逻辑
ctx.effect():让资源跟随插件生命周期
插件有时会注册一些资源,例如:
- HTTP 路由
- 监听器
- 定时器
- 外部资源
它们需要在卸载时清理,生命周期机制的目标是:
插件加载
├── 注册资源
▼
插件运行
│
▼
插件卸载
│
└── 清理属于该插件的资源
因此 effect 更适合用来管理:
“插件运行期间注册,插件卸载时需要释放”的资源。
Service 与 inject
很多能力并不直接属于插件本身。
例如插件可能需要:
tools
settings
webServer
这些能力可以理解为由 Harness 核心或其他插件提供的 Service。
可以把 Service 理解成:
供其他插件通过 Context 使用的一组能力。
例如:
Service
├── tools
├── settings
├── webServer
└── ...
插件需要某个 Service 时,可以通过依赖注入来使用。
示例:
ctx.inject(["webServer"], (webCtx) => {
webCtx.effect(() =>
webCtx.webServer.register({
kind: "exact",
path: "/api/example",
handler: (req, res) => {
// ...
},
})
);
});
它表达的是:
Plugin
↓ 需要 webServer
ctx.inject(["webServer"])
↓
获得 webServer 能力
↓
注册 HTTP 路由
因此:
Context
│
└── inject
│
├── tools
├── settings
└── webServer
开发第一个 Tool
Plugin 是 Harness 的扩展单元;Tool 是暴露给 LLM 调用的具体动作。
所以两者不是同一个东西,它们的关系更像:
Plugin
│
├── 可以监听事件
├── 可以使用 Service
├── 可以注册 HTTP 路由
├── 可以注册 Settings
│
└── 可以注册 Tool
│
└── 给 LLM 调用
一个插件:
- 可以没有 Tool;
- 可以有一个 Tool;
- 也可以注册多个 Tool。
一个最小 Tool 插件
例如我们写一个:
greet-plugin
示意代码:
import type { Context } from "@deepseek-ai/cordis";
import { defineTool } from "@deepseek-ai/dsh-tools";
export const name = "greet-plugin";
export const inject = ["tools"];
export function apply(ctx: Context) {
ctx.tools.register(
defineTool({
name: "greet",
parameters: {
name: {
type: "string",
required: true,
},
},
output: {
schema: { type: "string" },
render: (_args, value) => [{ type: "text", text: String(value) }],
},
async execute(args) {
return `Hello, ${args.name}!`;
},
})
);
}
这里有两个不同层级的入口:
export function apply(ctx)
│
└── Plugin 被 Harness 加载时执行
async execute(args)
│
└── Tool 被 LLM 调用时执行
Tool 的调用过程
假设用户说:
请使用 greet 工具向 Ada 打招呼。
大致过程:
用户
↓
LLM
↓ 判断需要调用 greet
greet({
name: "Ada"
})
↓
execute(args)
↓
"Hello, Ada!"
↓
返回给 LLM
因此一个 Tool 最核心的四个部分通常是:
Tool
├── name
├── parameters
├── output
└── execute
分别代表:
name
└── 工具叫什么
parameters
└── LLM 调用工具时应该传什么
output
└── 工具返回什么(返回值的 JSON Schema + 渲染方式,必填)
execute
└── 工具真正执行什么(第二个参数 exec 携带 signal、agent 等执行上下文)
Tool 常见用途包括:
- 查询数据库
- 调用 API
- 搜索
- 读写文件
- 获取业务状态
- 执行项目内部动作
- 调用外部服务
最关键的一点是:
Tool 是给 LLM 调用的。
给 Plugin 增加 Config
现在我们的 Tool 有一个问题:
return `Hello, ${args.name}!`;
其中 Hello 是被写死的字符。
如果这里用户希望配置别的值,就不应该要求每个人修改源码。
这就是 Plugin Config 的用途。
定义 Config
例如,类型与运行时 Schema 成对定义(dsh 使用 @deepseek-ai/schemastery,惯例以 z 作为导入名;interface Config 是类型、const Config 是运行时 schema,二者同名共存是 TS 的惯用写法):
import z from "@deepseek-ai/schemastery";
export interface Config {
greeting: string;
}
export const Config: z<Config> = z.object({
greeting: z.string().default("Hello"),
});
然后(别忘了 inject 声明,否则 ctx.tools 不可用):
export const inject = ["tools"];
export function apply(ctx: Context, config: Config) {
ctx.tools.register(
defineTool({
name: "greet",
parameters: {
name: {
type: "string",
required: true,
},
},
output: {
schema: { type: "string" },
render: (_args, value) => [{ type: "text", text: String(value) }],
},
async execute(args) {
return `${config.greeting}, ${args.name}!`;
},
})
);
}
现在的 Hello 不再被写死,而是来自:
config.greeting
Config 与 Tool Parameters 的区别
这是非常容易混淆的一点。
现在我们的代码同时出现:
config.greeting
和:
args.name
它们来源完全不同:
插件用户 / 管理员
↓ config
Plugin
apply(ctx, config)
↓ 注册
Tool
↑ args
LLM
对比:
| 维度 | Plugin Config | Tool Parameters |
|---|---|---|
| 谁提供 | 插件用户 / 管理员 | LLM |
| 什么时候提供 | 插件加载或配置更新时 | 每次 Tool 调用时 |
| 生命周期 | 相对长期 | 单次调用 |
| 作用 | 决定插件怎么运行 | 决定这一次工具做什么 |
| 典型内容 | API URL、API Key、时区、开关 | 查询词、用户名、文件名、日期 |
| 代码位置 | apply(ctx, config) |
execute(args) |
Config 是插件怎么运行;Tool Parameters 是这一次 Tool 做什么。
dsh 到底如何加载插件
在前面章节中我们使用过:
dsh web --patch ./scratch-plugin/cordis.yml
那么 --patch 到底干了什么?
这会自然引出 dsh 文档里经常出现的三个词:
Profile
Patch
Bundle
Patch:对插件树的一层修改
先从最熟悉的yml文件说起:
- insert:
- id: hello
name: "/path/to/hello.ts"
准确地说,这不是“完整插件配置”。它表达的是:
在现有插件树基础上,再做一次修改。
因此它其实就是 Patch ,可以把 Patch 理解成:
原始插件树
↓
Patch
↓
修改后的插件树
Patch 可以用于:
- 插入插件条目
- 覆盖已有条目
- 调整配置
- 禁用条目
所以:
Patch 不是整棵插件树,而是对插件树的一层修改。
为什么 --patch 很适合开发
开发模式中:
dsh web --patch ./scratch-plugin/cordis.yml
表示:
系统原有配置
↓
额外叠加 cordis.yml
↓
本次启动最终配置
下一次启动如果不传:
--patch
这层临时配置就不会自动加入。
因此 --patch 很适合:
- scratch 插件
- 临时测试
- 调试
- 临时覆盖
Profile:一套具体运行环境
如果我希望某些依赖和插件配置长期保留,它们放在哪里?
这就是 Profile,可以把 Profile 理解成:
一套具体运行环境的依赖与插件配置集合。
例如 Web profile:
~/.dsh/profiles/web/
典型内容可能包括:
web/
├── package.json
├── cordis.patch.yml
└── node_modules/
因此一个 Profile 可以包含:
Profile
├── dependencies
│ └── 安装了哪些 npm 包
│
├── bundles
│ └── 哪些 Bundle 参与启动
│
└── cordis.patch.yml
└── 这个 Profile 自己的补丁层
可以把 web 理解成一套独立运行环境。
Profile 自己的 cordis.patch.yml
除了命令行临时传:
--patch ./xxx.yml
Profile 目录还可以有自己的:
cordis.patch.yml
它可以理解成:
这个 Profile 长期存在的一层用户级 Patch。
于是:
临时测试
└── --patch
长期保留
└── profile/cordis.patch.yml
Bundle:一个携带 Patch 的包
接下来问题来了:
第三方 npm 插件安装进 Profile 以后,怎么自动把自己的插件配置带进来?
这就是 Bundle。
一个可安装插件包可能长这样:
dsh-usage-monitor/
├── package.json
├── cordis.patch.yml
├── lib/
│ └── index.js
└── src/
└── index.ts
其中 package.json:
{
"name": "dsh-usage-monitor",
"main": "lib/index.js",
"files": ["lib/index.js", "cordis.patch.yml"],
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
}
关键是:
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
它表达的是:
这个包携带一份 dsh Patch。
因此可以把 Bundle 理解成:
一个能够给 Profile 提供 Patch 的包。
Bundle 和 Plugin 不是一回事
这是非常容易混淆的地方。
假设:
dsh-usage-monitor/
├── package.json
├── cordis.patch.yml
└── lib/index.js
其中:
Bundle 层
└── cordis.patch.yml
Plugin 模块
└── lib/index.js
Bundle 解决:
“要把哪些插件配置带进 Profile?”
Plugin 解决:
“插件真正被加载以后执行什么?”
完整过程:
Bundle
↓ 提供 Patch
Patch
↓ 插入 Plugin 条目
Plugin Loader
↓
lib/index.js
↓
apply(ctx, config)
所以:
Bundle 是包装 / 配置层;Plugin 是执行代码层。
一个 Bundle 可以包含多个插件条目
Bundle 并不等于一个 Plugin。
例如某个 Bundle 的 patch 完全可以写:
- insert:
- id: search
name: my-search-plugin
- id: memory
name: my-memory-plugin
- id: analytics
name: my-analytics-plugin
也就是:
1 Bundle
│
└── 1 Patch
│
├── Plugin A
├── Plugin B
└── Plugin C
因此 Bundle 更接近:
一组插件配置的分发单元。
Profile / Bundle / Patch 的完整关系
现在三个词可以放到一张图里:
Profile 一套运行环境
↓ 加载多个 Bundle
Bundle 携带 Patch 的包
↓ 提供 Patch
Patch 对插件树的一层修改
↓ 修改插件树
Plugin Tree
↓
Plugin
↓
apply(ctx, config)
开发模式与安装模式
现在再看“开发模式”和“安装模式”,就会发现它们不是两套完全不同的系统。
开发模式
本地 .ts
↓
自己写 Patch
↓
--patch
↓
最终插件树
↓
Plugin
适合:
- 学习
- 快速试验
- 源码仓库内开发
安装模式
npm package
↓
Bundle
↓
Bundle 自带 Patch
↓
Profile
↓
最终插件树
↓
Plugin
适合:
- 正式安装
- 分发
- 第三方插件
开发模式与安装模式对比
| 维度 | 开发模式(scratch) | 安装模式(npm 包) |
|---|---|---|
| 插件形态 | .ts 源文件 |
编译后的 npm 包 |
| Patch | 自己准备 | 包中携带 |
Patch 中的 name |
本地文件路径 | 裸包名 |
| 加载方式 | --patch / Profile Patch |
dsh plugin add |
| 持久性 | --patch 通常是临时 |
安装进入 Profile 后长期生效 |
| 适合场景 | 学习、试验、源码开发 | 正式安装、分发、第三方插件 |
最重要的一句话:
开发模式和安装模式底层仍然是在构建同一棵插件树。
区别主要是:
插件在哪里
+
Patch 从哪里来
安装插件
从 npm 安装:
npx @deepseek-ai/dsh plugin --profile web add <plugin-name>
从 GitHub 安装:
npx @deepseek-ai/dsh plugin --profile web add github:用户名/仓库名
安装本地插件:
npx @deepseek-ai/dsh plugin --profile web add <absolute_path_to_plugin>
查看依赖关系:
npx @deepseek-ai/dsh plugin --profile web why <plugin-name>
移除:
npx @deepseek-ai/dsh plugin --profile web remove <plugin-name>
Bundle 自动进入 Profile
安装完成后,dsh 会根据 Profile 的依赖,以及这些包是否声明:
dsh.bundle.patch
来维护:
dsh.profile.bundles
例如:
{
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"dsh-usage-monitor"
]
}
}
}
可以理解成:
Profile dependencies
↓ 哪些包声明 dsh.bundle.patch?
dsh.profile.bundles
因此:
安装一个声明了 Bundle Patch 的包后,它就可以自动参与 Profile 的启动加载。
最终 Patch Layer Stack
概念上,dsh 启动时可能形成:
Bundle 1 的 Patch
↓
Bundle 2 的 Patch
↓
第三方插件 Bundle 的 Patch
↓
Profile 自己的 cordis.patch.yml
↓
命令行 --patch
↓
最终插件树
越靠后的层,可以继续对前面的结果修改。
所以命令行 --patch 非常适合做最后一层临时覆盖。
Loader 最终做什么
经过 Patch 层叠加以后,最终得到的是一棵插件条目树。
然后 loader 根据每个条目的 name 去找到真正的插件模块,不管来源是哪一种,加载到的目标仍然是:
export const name = "...";
export const Config = ...; // 可选
export function apply(ctx, config) {
// ...
}
实例:我的API用量监控插件 usage-monitor
假设我们要开发:
dsh-usage-monitor
用于统计 LLM API 用量。
需求
这个插件希望完成:
- 监听模型调用;
- 计算或记录用量;
- 允许用户设置币种、时区;
- 提供 HTTP API 给前端读取报表;
- 最终可以在 Web UI 中展示数据。
可以先画成:
llm/stream
│
▼
usage-monitor
│
记录 / 计算用量
│
┌───────┴────────┐
▼ ▼
Settings HTTP API
│
▼
Web UI
Host 插件结构
Host 包可以是:
dsh-usage-monitor/
├── package.json
├── cordis.patch.yml
├── lib/
│ └── index.js
└── src/
└── index.ts
其中:
src/index.ts
└── TypeScript 源码
lib/index.js
└── 编译后的分发产物
Plugin Config
例如:
import z from "@deepseek-ai/schemastery";
export interface Config {
currency: string;
tz: string;
}
export const Config: z<Config> = z.object({
currency: z.string().default("CNY"),
tz: z.string().default("Asia/Shanghai"),
});
这里的currency和tz都是 Plugin Config。
因为它们描述的是:
这个 usage-monitor 插件实例应该怎么运行。
而不是 LLM 每次调用某个 Tool 时决定的参数。
监听 LLM 调用
示意:
export function apply(ctx: Context, config: Config) {
ctx.on("llm/stream", (options, next) => {
// 包装、记录或统计模型调用
});
}
这使用的是前面讲过的:
Plugin
↓
Context
↓
ctx.on(...)
↓
Event Hook
注册 Settings
如果希望配置可以进入设置系统:
ctx.inject(["settings"], (settingsCtx) => {
settingsCtx.settings.register(
settingsNamespace("usage-monitor"),
Config,
{ base: {} }
);
});
这里的结构:
usage-monitor Plugin
↓ 需要 settings
ctx.inject(["settings"])
↓
settings Service
↓
注册 Config
所以:
Config 定义“有哪些配置”;Settings Service 负责“把这些配置接入设置系统”。
注册 HTTP API
例如:
ctx.inject(["webServer"], (webCtx) => {
webCtx.effect(() =>
webCtx.webServer.register({
kind: "exact",
path: "/api/usage-monitor/report",
handler: (req, res) => {
// 返回报表
},
})
);
});
这里同时使用了两个前面讲过的概念:
inject
└── 获取 webServer Service
effect
└── 把路由注册与插件生命周期绑定
最终:
Browser
↓ HTTP
/api/usage-monitor/report
↓
usage-monitor
↓
返回统计数据
Host 插件的数据流
现在可以把 Host Plugin 看成:
Harness
│
│ llm/stream
▼
usage-monitor
│
├── 读取 config
│ ├── currency
│ └── tz
│
├── 记录模型调用
│
├── 注册 Settings
│
└── 注册 HTTP API
这就是一个真实插件在 apply(ctx, config) 中完成的工作。
打包为 Bundle
Host 包的 package.json:
{
"name": "dsh-usage-monitor",
"main": "lib/index.js",
"files": [
"lib/index.js",
"cordis.patch.yml"
],
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
}
它告诉 dsh:
这是一个可作为 Bundle 使用的包
│
└── Patch 在 ./cordis.patch.yml
Bundle 自带的 Patch
例如:
- insert:
- id: usage-monitor
name: dsh-usage-monitor
开发模式中,可能是:
name: /absolute/path/to/plugin.ts
安装模式中则可以写:
name: dsh-usage-monitor
后者由模块解析机制从 Profile 的依赖中定位到真正的编译产物。
安装以后发生什么
安装:
npx @deepseek-ai/dsh plugin --profile web add dsh-usage-monitor
大致可以理解成:
dsh-usage-monitor
↓
进入 web Profile 的 dependencies
↓
发现它声明 dsh.bundle.patch
↓
进入 Profile 的 bundles
↓
启动时加载它的 Patch
↓
Patch 插入 usage-monitor Plugin
↓
loader 找到 lib/index.js
↓
apply(ctx, config)
│
├── 监听 LLM
├── 注册 Settings
└── 注册 HTTP API
到这里,Host 插件已经完整。
接下来如果还需要 Web UI,就进入客户端插件。
给插件增加 Web 界面
前面讲的主要是 Host 插件。
也就是:
Node.js / Harness 侧
如果插件还需要在 Web UI 中展示:
- 会话页徽章
- 自定义面板
- 报表
- 前端交互组件
就需要客户端插件。
为什么 Host 和 Client 要拆开
Host 和 Browser 是两个不同运行环境。
Host / Node.js
├── 监听 LLM 事件
├── 使用 Harness Service
├── 提供 HTTP API
└── 处理业务逻辑
Browser
├── 渲染 UI
├── 响应用户操作
└── 调用 Host API
所以一个完整插件可能拆成:
dsh-usage-monitor
└── Host
dsh-usage-monitor-client
└── Browser Client
Client 包结构
例如:
dsh-usage-monitor-client/
├── package.json
├── lib/
│ ├── index.js
│ └── client.js
└── src/
└── ...
具体目录可以按项目构建方式调整。
dsh.client
客户端包可以在 package.json 声明:
{
"name": "dsh-usage-monitor-client",
"main": "lib/index.js",
"exports": {
"./client": "./lib/client.js"
},
"dsh": {
"client": {
"platform": "web",
"inject": [
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-runtime"
]
}
}
}
它表达的是:
Client Package
↓ 声明 dsh.client
Web 启动阶段发现客户端模块
↓
加载 exports["./client"]
↓
Browser 运行客户端代码
Client 如何访问 Host
Host 已经暴露:
/api/usage-monitor/report
客户端可以:
Browser UI
↓ HTTP
Host Plugin API
↓
usage-monitor 数据
↓
渲染报表
因此 Host / Client 的职责可以这样分:
Host
└── 数据和业务逻辑
Client
└── UI 和用户交互
纯客户端包不一定是 Bundle
如果一个客户端包只声明:
"dsh": {
"client": {
"platform": "web"
}
}
但没有:
"dsh": {
"bundle": {
"patch": "..."
}
}
那么它只是一个客户端依赖,并不一定进入:
dsh.profile.bundles
这也说明:
Bundle 与 Client 是两个不同维度的声明。
最终项目结构
一个完整的 usage-monitor 可以抽象成:
usage-monitor/
│
├── host/
│ ├── package.json
│ ├── cordis.patch.yml
│ ├── src/
│ │ └── index.ts
│ └── lib/
│ └── index.js
│
└── client/
├── package.json
├── src/
└── lib/
└── client.js
逻辑关系:
DeepSeek Harness
│
▼
Profile
│
▼
usage-monitor Bundle
│
▼
Patch
│
▼
usage-monitor Plugin
│
┌───────────┼───────────┐
▼ ▼ ▼
llm/stream Settings HTTP API
│
▼
usage-monitor-client
│
▼
Web UI
至此,一个从 Host 到 Browser 的完整插件结构就形成了。
附录 A:常用命令
使用临时 Patch 启动
pnpm dsh web --patch ./scratch-plugin/cordis.yml
或者:
npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml
安装 npm 插件
npx @deepseek-ai/dsh plugin --profile web add <plugin-name>
安装 GitHub 插件
npx @deepseek-ai/dsh plugin --profile web add github:用户名/仓库名
安装本地插件
npx @deepseek-ai/dsh plugin --profile web add <absolute_path_to_plugin>
查看依赖关系
npx @deepseek-ai/dsh plugin --profile web why <plugin-name>
移除插件
npx @deepseek-ai/dsh plugin --profile web remove <plugin-name>
附录 B:定义 Tool 速查
import { defineTool } from "@deepseek-ai/dsh-tools"
注册,注意使用 ctx.tools 前需要 export const inject = ["tools"](或 ctx.inject(["tools"], ...)):
ctx.tools.register(defineTool({ ... }))
name、parameters、output({ schema, render },presentationMeta 可选)、execute
{ name: { type: "string", required: true } }
execute(args, exec) 的第二个参数 exec 携带 signal(取消信号)、agent 等执行上下文
返回值的 output.schema 是 JSON Schema;
output.render(args, value) 返回展示内容,如 [{ type: "text", text: ... }] 。
