DSH 插件開發指南

官方文檔講清了概念,但從「讀懂」到「裝得上」之間有一段距離。 這篇是我們寫 插件測試平台 和 我們自己的插件 時的真實路徑 —— 包括踩的坑。

為什麼我們能寫這篇。 我們在沙箱裡跑了 122 個插件的安裝測試,自己寫了一個插件並把它測到四階段全過。 下面每一個錯誤都是我們真的遇到過的,不是想像的。

一、最小插件:三個文件

一個能裝進 DSH 的插件,最少需要三個文件:

my-plugin/
├── package.json        # 声明这是 dsh 插件,patch 指向哪
├── cordis.patch.yml    # 告诉 DSL 装载器插什么
└── lib/index.js        # 真正的代码

package.json

{
  "name": "dsh-my-plugin",
  "version": "0.1.0",
  "type": "module",
  "main": "lib/index.js",
  "files": ["lib", "cordis.patch.yml"],
  "dsh": {
    "bundle": {
      "patch": "./cordis.patch.yml"
    }
  },
  "engines": {
    "dsh": ">=0.2.0-rc.2"
  }
}
兩個關鍵欄位,少一個就裝不上: dsh.bundle.patch 告訴裝載器去哪找 patch 文件; engines.dsh 聲明兼容的 DSH 版本範圍。 我們測的 122 個插件裡,8 個因為 engines.dsh 寫死而裝不上 —— 見第五節。

cordis.patch.yml

- insert:
    - id: dsh-my-plugin
      name: dsh-my-plugin

只有三行。id 是這個插件在配置樹裡的名字(要唯一), name 是模塊名(npm 包名,或相對路徑)。

lib/index.js

export const name = "dsh-my-plugin";

export function apply(ctx) {
  console.log("hello from my plugin");
}

就這麼簡單。一個具名導出 apply 函數,Cordis 加載時會調用它, 傳入 ctx(上下文)—— 插件的所有能力都通過 ctx 註冊。

三種插件形狀(官方都支持): ① 具名導出 apply 的函數(最常用); ② 有 apply 方法的對象; ③ 繼承 Service 的類(要對外提供服務時才用)。 新手用第一種就行。

二、註冊一個工具

大部分插件做的事是給 AI 加工具。API 是 ctx.tools.register(defineTool({...}))。

import { defineTool } from "@deepseek-ai/dsh-tools";

export const inject = ["tools"];   // ← 见第三节,这行最容易漏

export function apply(ctx) {
  ctx.tools.register(defineTool({
    name: "my_lookup",
    description: "Look up something and return text.",

    parameters: {
      query: {
        type: "string",
        required: true,
        description: "What to look up.",
      },
      limit: {
        type: "number",
        description: "Max results. Defaults to 5.",
      },
    },

    output: {
      schema: {
        type: "object",
        additionalProperties: false,
        properties: {
          text: { type: "string", required: true },
        },
      },
      render: (_args, value) => [
        { type: "text", text: value.text || "" },
      ],
    },

    isConcurrencySafe: () => true,

    async execute(args, exec) {
      const res = await fetch("https://example.com/api?q=" + encodeURIComponent(args.query));
      const data = await res.json();
      return { text: JSON.stringify(data, null, 2) };
    },
  }));
}

四段結構

欄位作用
parameters聲明參數的 JSON Schema。模型靠這個知道怎麼調
output.schema聲明返回值的結構。execute 的返回值必須匹配
output.render把返回值變成給模型看的內容塊
execute真正幹活。exec.signal 是取消信號,要透傳給 fetch

三、inject:最容易踩的坑(我們真的踩了)

我們的真實報錯。 第一次測我們自己的插件,啟動日誌裡出現:
dsh-deepseekbase: Error: cannot get property "systemPrompt" without inject

原因:插件裡用了 ctx.systemPrompt.section(...), 但 inject 只聲明了 ["tools"]。
修法:改成 inject = ["tools", "systemPrompt"],重測即通過。

規則

你在 apply 裡用了 ctx.某某,就必须在 inject 裡聲明它。

export const inject = ["tools", "systemPrompt"];

export function apply(ctx) {
  ctx.systemPrompt.section({       // 需要 "systemPrompt"
    name: "tool:mine",
    order: 120,
    text: "Use my_lookup when asked about X.",
  });

  ctx.tools.register(defineTool({  // 需要 "tools"
    /* ... */
  }));
}

為什麼要聲明

Cordis 會等到所有 inject 列出的服務都存在,才調用 apply。 這解決了加載順序問題 —— 你不用管誰先誰後,依賴關係決定啟動時機。

但也意味著:inject 漏寫的服務,在 apply 裡訪問會直接報錯。

常用服務

服務用途
tools註冊 AI 可以調用的工具
systemPrompt往系統提示裡加一段說明
fs讀寫文件(需要權限)
llm調用模型
shell執行命令(高風險權限)

四、調試三招

① 看配置樹

dsh --profile web --dump-config | grep -A2 my-plugin

如果這裡沒有你的插件,說明 cordis.patch.yml 寫錯了,或者包名解析不到。

② 看是不是「沒激活」

啟動時如果出現 dsh: warning: 1 entry did not activate, 說明插件被掛上了但 apply 執行時報錯了。往上翻日誌找 Error。

③ 乾淨環境重裝

dsh plugin --profile web remove dsh-my-plugin
dsh plugin --profile web add ./my-plugin

很多「為什麼沒生效」是舊版本還在緩存裡。

五、engines.dsh:第二容易踩的坑

我們測出的真實案例。 dsh-ops@0.4.4 聲明 "engines.dsh": ">=0.2.0-rc.2 <0.2.2-0"。
這排除了 0.2.2-rc.1 —— 於是在最新版上直接拒絕安裝:
dsh: installation rejected: Plugin dsh-ops@0.4.4 is incompatible with dsh 0.2.2-rc.1

它的 CI 是綠的、單元測試也過了,但用戶裝不上。 122 個插件裡有 8 個栽在這。

建議寫法

"engines": {
  "dsh": ">=0.2.0"
}

只寫下界,不寫上界。除非你確知未來版本會破壞兼容,否則不要封頂。

六、配置

插件可以從 patch 文件接收配置:

- insert:
    - id: dsh-my-plugin
      name: dsh-my-plugin
      config:
        endpoint: "https://example.com/api"
        limit: 5
export function apply(ctx, config) {
  const endpoint = (config && config.endpoint) || "https://default.example.com";
  const limit = (config && config.limit) || 5;
  // ...
}

永遠給默認值。用戶可能不寫 config 就裝上。

七、權限與安全

DSH 插件跑在宿主進程裡,能力很大。所以權限要寫清楚。

權限能做什麼審核建議
fs-read讀文件會看到你的代碼和密鑰文件
fs-write寫文件能改你的項目
network發 HTTP 請求能外傳數據
shell執行命令等於完全控制
credentials讀憑證紅線
dynamic-code運行時生成代碼紅線
寫 README 時明確說明你做什麼、不做什麼。 我們自己的插件是這樣寫的:
「出站 HTTPS 只到 deepseekbase.com;不讀寫文件;不讀憑證;不執行命令。 整個實現是一個文件、零依賴,五分鐘可以審完。」

八、完整範例:我們的插件

我們的 dsh-deepseekbase 就是按上面寫的,而且完全開源:

文件內容
package.json34 行,含 dsh.bundle.patch 和 engines.dsh
cordis.patch.yml3 行
lib/index.js約 250 行,4 個工具,零運行時依賴
.github/workflows/test.ymlCI:push 時自動跑四階段測試

它做的四件事:

它用我們自己的 MCP Server 作為數據源 —— 一個插件背後接一層服務,是最省事的架構。

九、寫完之後:讓別人驗證

我們做了一個免費的插件測試服務。 你把插件寫完,會遇到一個問題:在我機器上能跑,別人機器上呢?

我們在隔離沙箱裡對插件做四階段真實測試:
① 裝進乾淨容器 → ② 確認出現在 plugin list → ③ 確認掛載進配置樹 → ④ 啟動 profile,確認進程存活

後三步完全無網絡(--network none),插件即使有惡意行為也無處可去。 看已測的 122 個結果 →

十、官方文檔

本文是實戰補充,概念和完整 API 請看官方:

文檔內容
cordis-primerCordis 五個核心概念
01 first plugin最小插件與三種形狀
03 services服務與 inject
tool catalog內置工具的完整 schema
config catalog所有配置項
版權說明:本頁是我們讀官方文檔後的獨立總結, 代碼範例是我們自己寫的(MIT 開源),不複製官方原文。官方文檔以其倉庫為準。

相關

插件測試結果 · 插件目錄 · MCP Server