Community生產力與協作github.com

green-dalii/dsh-plugin-dev-skill

一個技能包,讓任何 Agent 都能正確、高效、且符合規範地開發 DeepSeek Harness (DSH) 插件。

dsh-plugin-dev-skill 是什麼?

這個技能包是為 AI Agent(如 Claude Code, Cursor, Codex)設計的開發助手,專門用於 DeepSeek Harness (DSH) 插件的開發。DSH 是一個基於 Cordis 框架的插件化系統,本技能提供了完整的心智模型、鐵律、可複製的程式碼模板以及逐步操作流程。它涵蓋了工具開發、LLM 適配器、插件配置、服務與依賴管理、事件系統以及打包部署等所有關鍵面向。支援在 DSH 的原始碼檢出環境或已安裝 CLI 環境中使用,透過 TypeScript 模組遵循可逆效應、反應式共效應和 Fiber 生命週期等核心概念來開發插件。此技能特別強調透過 `ctx` 進行受管註冊、明確宣告依賴、可設定的配置以及純函數回調等關鍵開發紀律。

相容平台✓Claude Code✓Codex CLI~Cursor
npx skills add green-dalii/dsh-plugin-dev-skill

Installed? Explore more 生產力與協作 skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

DeepSeek Harness 插件開發技能

讓任何 Agent 都能正確、高效、符合規範地開發 DeepSeek Harness(DSH)插件。 本技能是 DSH 插件開發的操作手冊:先給出心智模型與鐵律,再給出可直接照抄的程式碼模板與分步流程,最後給出驗證清單。深度背景請見 References/ 目錄下的精簡提煉文件。


0. 何時使用本技能

當任務涉及以下任一項時,必須載入本技能:

  • 為 DeepSeek Harness 編寫、修改或除錯插件(tool、LLM adapter、服務提供方、鉤子、UI、協定橋等)
  • 編寫或修改 cordis.yml / cordis.patch.yml / dsh.profile / dsh.bundle 配置
  • 用 dsh plugin ... 安裝、移除、打包插件
  • 理解 DSH 中 ctx.tools、ctx.llm、ctx.agents、ctx.sessions 等服務或 tools/*、agent/*、session/event 等事件
  • 研究 DSH 的插件模型(Cordis 框架)如何運作

先讀本 SKILL.md 全篇,再動手。動手前先讀專案裡已有的 cordis.yml / profile / 相關原始碼,確認現有約定。


1. 心智模型:DSH 是構建在 Cordis 之上的插件化系統

一句話概括:DeepSeek Harness 是一個 Agent Harness SDK,其中每一項能力——工具、LLM 適配器、檔案存取、agent loop 本身——都是一個插件(component),掛載到一個共享的上下文(context)上。

三個核心概念(來自論文《A Programming Paradigm for Spatiotemporal Composability》,見 References/10-spatiotemporal.md):

概念含義在程式碼中的體現
可逆效應(revertible effect)元件對共享環境的每次修改都攜帶一個逆操作,執行時期追蹤並在元件卸載時按 LIFO 順序還原ctx.effect(() => () => cleanup);ctx.on()、ctx.tools.register() 等都是 effect,卸載自動撤銷
反應式共效應(reactive coeffect)元件宣告它依賴哪些服務;服務出現/消失/換實現時,元件自動激活/停用/重載export const inject = ['tools'];依賴消失時插件自動 dispose,恢復時自動重載
Fiber(元件實例)每個已載入的插件實例是一個 fiber,有生命週期狀態機PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED(apply 拋錯則 FAILED)

由此得到的鐵律:

  1. 所有註冊都必須通過 ctx 完成(ctx.on / ctx.tools.register / ctx.effect / ctx.plugin / ctx.provide)。凡是經過 ctx 的註冊,卸載時自動清理,絕不手動 removeListener / clearInterval。
  2. 凡是不經過 ctx 管理的資源(網路連線、自建定時器、檔案句柄、watcher),必須包在 ctx.effect(() => { ...; return () => cleanup }) 裡。
  3. 依賴必須宣告(inject),不要用 ctx.get() 探測必需依賴,不要臆想載入順序——順序由依賴決定,不由 cordis.yml 行序決定。
  4. 配置必須可配置:任何不同部署取值可能不同的參數都必須成為 Config 欄位,絕無硬編碼(檢驗標準:能否在 cordis.yml 中改這個值而不改程式碼?)。
  5. 配置錯誤要響亮:用 Schemastery schema 在載入時校驗,無效配置直接載入失敗並給出明確錯誤,絕不帶病執行。
  6. 回呼可被回放/熱重載:展示類回呼(output.render、presentCall、presentResult)必須是純函數;插件隨時可能被 HMR 卸載重載,不要依賴跨重載的全域狀態。

2. 環境與工具鏈

  • 插件是 TypeScript 模組(也接受 JS),執行於 Node.js ESM 環境。
  • 核心依賴套件(都來自 @deepseek-ai 作用域):
    • @deepseek-ai/cordis — 插件框架(context、fiber、事件、註冊表)
    • @deepseek-ai/schemastery — 配置 schema 校驗(Standard Schema 實現)
    • @deepseek-ai/dsh-tools — defineTool 與工具註冊表服務
    • @deepseek-ai/dsh-llm — LlmAdapter、StreamChunk、GenerateOptions、CallId
    • 各能力 seam 套件:dsh-shell、dsh-session、dsh-agent、dsh-system-prompt、dsh-fs、dsh-jobs、dsh-credentials 等(完整清單見 References/08-capability-layering.md)
  • 兩個開發入口(任選):
    • 原始碼檢出(推薦,教學場景):克隆 deepseek-ai/deepseek-harness,pnpm install,用 pnpm dsh web --patch <你的cordis.yml> 啟動 Web UI,用 node --import tsx ../../vendor/cordis/bin.js 跑純 Cordis 教學範例。
    • 已安裝 CLI:dsh --profile <name> ... 啟動 profile,dsh plugin --profile <name> <pnpm args> 管理插件。

3. 插件的最小形態(先學會這個)

一個插件就是一個匯出 apply 函數的模組:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-plugin'   // 可選,僅用於診斷顯示

export function apply(ctx: Context) {
  // 在這裡註冊能力
}

透過 cordis.yml 或 patch 掛載:

- name: './src/my-plugin.ts'        # 相對路徑(原始碼/開發)或 npm 套件名(安裝後)
  id: my-plugin                     # 穩定標識,loader 按 id 做配置對帳

三種插件形態

  1. 函數形態(預設推薦):
    export const name = 'greet-tool'
    export const inject = ['tools']
    export function apply(ctx: Context) { /* ... */ }
    
  2. 物件形態:export default { name, inject, apply(ctx) {} }
  3. 類別形態(需要對外提供服務時):export default class MyService extends Service { constructor(ctx) { super(ctx, 'myService') } }

在需要提供服務之前一律用函數形態。


4. 開發一個 Tool(最高頻任務)

工具註冊到 ctx.tools,用 defineTool 定義。模板(可直接照抄):

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'my-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'my_tool',                                  // 模型看到的工具名(snake_case 慣例)
    description: 'One sentence about what it does.',  // 模型看到的功能描述,要具體、說清何時用
    parameters: {
      path: { type: 'string', required: true, description: 'What the param means' },
      limit: { type: 'number' },                      // 未標 required 即為可選
    },
    output: {
      schema: { type: 'string' },                     // 宣告 execute 返回的規範 JSON 值
      render: (_args, value) => [{ type: 'text', text: value }],  // 轉為模型可見內容
    },
    async execute(args, exec) {
      // args 已被 schema 校驗並推斷型別
      // exec.signal 是取消訊號,長任務必須回應它
      return `result for ${args.path}`
    },
  }))
}

defineTool 的完整契約(務必遵守)

  • parameters:參數 schema 映射,根是隱式開放物件;每屬性可 required: true。支援 string/number/integer/boolean/null/array/object/oneOf;顯式 object 節點必須宣告 additionalProperties: true | false。
  • output.schema:宣告 execute 返回的規範 JSON 值(物件/陣列/純量/null 皆可)。這是給程式化呼叫方(Code Mode 的 await tools.xxx())用的 API,要設計成直接返回控制代碼與欄位;面向人的解釋放 output.render。
  • execute(args, exec):
    • args 已經過執行時期校驗,但 schema DSL 表達不了的約束(非空字串、正數、跨欄位)要自己 throw new Error(...)。
    • 返回 output.schema 宣告的規範值。不要返回內容塊,不要逼呼叫方從自然語言裡解析 id。
    • 遵守 exec.signal:訊號觸發時取消進行中的工作。
    • 基礎設施故障(拋異常/無效返回值)→ 結果標記為 isError。成功但結果不理想(如非零退出碼)→ 仍返回規範值,在 render 裡解釋。
  • output.render(args, value):純函數,把規範值轉成 ContentBlock[](模型看到的文字)。絕不在此做 I/O、讀會話狀態。
  • 可選 output.presentationMeta(args, value):從規範值派生可回放的 UI 元資料(純函數)。
  • 可選 presentCall(args) / presentResult(args, result):宣告工具在 UI 中的卡片渲染意圖(card: 'generic' | 'terminal' | 'diff' | 'search' | 'web' | 'read')。必須純函數(會在即時串流和會話回放中執行),不做 I/O。
  • 不要把 timeoutMs、isConcurrencySafe、finalizeContent 等執行時期元資料當作模型可見 schema——它們不會洩漏給模型。
  • 參數不可變:把 args 當唯讀;註冊後不要改 schema 或替換回呼。

長時間執行的任務(後台任務)

若要支援 run_in_background:

const jobs = ctx.get('jobs')
if (!jobs) throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs')
return {
  kind: 'background',
  jobId: jobs.start({
    kind: 'my-work',
    label: args.command,
    ...exec.agent ? { owner: exec.agent } : {},
    run: () => { /* 返回 { cancel, done, readOutput? } */ },
  }),
}

要點:發布 jobId 之後用任務自己的取消訊號(歸 job_kill / owner dispose 管),不再用 exec.signal;成功輸出 schema 要能承載 { kind: 'background', jobId } 這種規範控制代碼。參考 References/03-tools.md 與 @deepseek-ai/dsh-tool-bash 實現。

工具的執行策略(擴充點,按需用)

擴充點模式用途
tools/pre-executewaterfall允許/拒絕/詢問策略(權限門禁);返回 {kind:'allow'} / {kind:'deny',reason} / {kind:'ask'}
ctx.tools.guard()單調守衛最終拒絕,後續監聽器無法撤銷(返回 reason 即拒絕)
tools/executewaterfall包裹分發:超時/重試/指標;可替換 exec.signal 但不可移除
tools/post-executewaterfall替換內容/值、阻止結果、附加模型上下文
tools/resultemit唯讀觀測不可變最終結果(日誌、稽核)

策略不要內建進工具;把部署策略放進鉤子插件(普通 Cordis 插件監聽這些事件即可,如權限門禁範例見 References/11-cookbook.md)。


5. 插件配置(Schemastery)

插件匯出 Config(同名的 TS 介面 + Schemastery schema),Cordis 在 apply 前校驗並填充預設值:

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'

export interface Config {
  greeting: string
  maxRetries: number
  verbose?: boolean
}

export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  maxRetries: Schema.number().default(3),
  verbose: Schema.boolean().default(false),
})

export function apply(ctx: Context, config: Config) {
  // config 一定是完整且經過校驗的
}

在 cordis.yml 中傳配置:

- name: './src/my-plugin.ts'
  config:
    greeting: 'Hi there'
    maxRetries: 5

要點:

  • Config 必須是一個 Standard Schema 物件(用 Schemastery 構建),不能匯出普通物件。
  • 常用構造:Schema.string().required() / .default(x)、Schema.number()、Schema.boolean()、Schema.array(String)、Schema.union(['a','b'])、Schema.object({...}) 巢狀。
  • 支援 !!js 運算式在載入時求值(如 greeting: !!js process.env.GREETING ?? 'Hello');!!js 僅對 config 與 disabled 欄位有效。
  • 配置變更會觸發 HMR:舊實例卸載(註冊自動清理)→ 新實例載入。不要在插件外部快取配置。

6. 服務與依賴(服務端開發)

使用服務(消費方)

export const inject = ['tools']        // 必需依賴:不滿足就不載入
export function apply(ctx: Context) {
  ctx.tools.register(/* ... */)        // apply 時保證已就緒
}

可選依賴:不寫 inject,使用時 ctx.get('service') 探測(返回 undefined 表示沒有提供方)。

提供服務(提供方)

用 Service 子類別(類別形態插件):

import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context {
    metrics: MetricsService     // 宣告合併:讓 ctx.metrics 有型別
  }
}

export default class MetricsService extends Service {
  static inject = ['llm']       // 服務也可以依賴其他服務
  constructor(ctx: Context) {
    super(ctx, 'metrics')       // 'metrics' 即服務名
  }
  record(event: string, value: number) { /* ... */ }
}

export const name = 'metrics'
export function apply(ctx: Context) {
  ctx.plugin(MetricsService)
}

低層 API(不常用但要知道):ctx.provide(name, value) 註冊服務實現(effect,卸載自動登出)、ctx.set/get 讀寫儲存、ctx.accessor 定義計算屬性、ctx.mixin 把服務成員掛到 ctx 上。

服務行為語義(理解而非背誦)

  • inject 不是一次性啟動檢查:執行期間必需服務消失 → 依賴插件自動 dispose;服務恢復 → 自動重載。這保證消費方永遠不會持有對已卸載服務的引用。
  • 服務名是扁平全域命名空間:tools、llm 等已被佔用;自有服務名要有辨識度前綴(如 myCap、metrics)。
  • 服務隔離:cordis.yml 中用 group + isolate 讓不同插件組看到同一服務名的不同實例(如兩組各自配置不同 timeout 的 shell 提供方)。

7. 事件系統(插件間通訊)

分發模式(事件名的一部分,必須選對)

模式呼叫語義
emitctx.emit(name, ...args)同步廣播,不等待、不收集返回值
parallelawait ctx.parallel(...)所有監聽器並發執行並等待
serialawait ctx.serial(...)按序執行等待,第一個非 null/false/undefined 返回值勝出並停止
bailctx.bail(...)serial 的同步版
waterfallawait ctx.waterfall(name, ...args, next)環繞中介軟體;監聽器收到 next,包裝下游返回值

waterfall 鐵律:只觀察/標註的監聽器必須呼叫 next();不呼叫 next() 直接返回 = 有意短路(否決/攔截)。 忘記呼叫 next() 會靜默吞掉下游預設行為。

型別安全的事件

declare module '@deepseek-ai/cordis' {
  interface Events {
    'my-plugin/ready': (payload: { id: string }) => void
    'my-plugin/check': (input: string) => boolean | undefined
  }
}

事件命名約定 namespace/action(如 agent/step、tools/result、session/event)。監聽器用 ctx.on() 註冊即自動隨插件卸載清理。

常用 Harness 事件(開發時經常用到)

  • tools/pre-execute / tools/execute / tools/post-execute / tools/result — 工具管線(見上表)
  • agent/*(agent/request、agent/pre-step、agent/session-start、agent/turn-stopping 等)— agent 生命週期與請求建構
  • session/event — 持久化的會話事件流(turn/*、step/*、tool/call、tool/result、assistant/chunk 都是這裡的 event.type,不是同名 Cordis 事件)
  • approval/request — 審批 waterfall
  • system-prompt/assemble — 系統提示詞整體裝配(權威返回,監聽者有責任保留既有貢獻)

完整簽名/觸發模式見各子系統頁面生成的 cordis-surface 區塊(References/08-capability-layering.md 有指引)。


8. LLM 適配器(接入新模型提供方)

適配器 = 繼承 LlmAdapter、實現 stream() 的類別,把 Harness 提供方無關請求轉成具體 API 呼叫並轉回 StreamChunk 分片。

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'

class MyAdapter extends LlmAdapter {
  async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
    // 1. options.messages → 提供方格式
    // 2. 調流式 API(必須傳 options.signal)
    // 3. 響應 → StreamChunk 序列
  }
}

export const name = 'my-llm-adapter'
export const inject = ['llm']
export const Config = Schema.object({
  apiKey: Schema.string().required(),
  providers: Schema.array(String).required(),
})

export function apply(ctx: Context, config: Config) {
  ctx.llm.registerAdapter(config.providers, new MyAdapter(config.apiKey))
}

StreamChunk 協議(嚴格遵守)

async function* chunks(): AsyncIterable<StreamChunk> {
  yield { type: 'block-start', index: 0, blockType: 'text' }
  yield { type: 'text-delta', index: 0, text: 'Hello' }
  yield { type: 'block-end', index: 0, block: { type: 'text', text: 'Hello world' } }
  // 工具呼叫塊:
  yield { type: 'block-start', index: 1, blockType: 'tool-call' }
  yield { type: 'tool-call-delta', index: 1, id: CallId('c1'), name: 'bash', argumentsDelta: '{"command":"ls"}' }
  yield { type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{"command":"ls"}' } }
  yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }   // 必須在 finish 前
  yield { type: 'finish', reason: { kind: 'stop' } }                        // 必須最後
}

規則:每個 block-start 必有對應 block-end;index 從 0 遞增;tool-call 的 arguments 全程是原始 JSON 字串;finish 是最後一塊;usage 在 finish 前。

錯誤與元資料

  • 傳輸/協定故障:throw new LlmError(msg, 'STABLE_CODE')(帶穩定 code),不要依賴普通 Error 自動轉換。
  • 提供方不支援 GenerateOptions 中的某個欄位:throw new LlmError(..., 'UNSUPPORTED'),絕不靜默丟棄。
  • 每個 HTTP 請求合併 attributionHeaders(),並傳遞 options.signal。
  • 可選:覆寫 resolveModel()(返回提供方/模型身份 + 可選 context/reasoning 元資料)與 listModels()(公佈模型選項)。

完整教學見 References/05-llm-adapter.md;參考實現:packages/llm/llm-deepseek(OpenAI 相容 SSE)與 packages/llm/llm-pi-ai。


9. 能力分層(三個角色,可替換能力的設計模式)

當一個能力需要可替換的提供方(如 Bash 執行),拆成三個角色、放入不同套件:

  1. Service Definition(如 dsh-shell):定義 Cordis 服務介面 + Request/Result 型別。abstract class MyCapService extends Service { super(ctx,'myCap'); abstract execute(req): Promise<res> }
  2. Service Provider(如 dsh-bash-local):實現介面。class MyCapLocal extends MyCapService { ... };apply(ctx){ ctx.plugin(MyCapLocal) }
  3. Consumer(如 dsh-tool-bash):把能力暴露為模型工具。inject: ['tools','myCap']

依賴方向:Provider 依賴 Definition;Consumer 依賴 Definition;Provider 與 Consumer 互不依賴。更換提供方只改 cordis.yml 一行,Definition 與 Consumer 不動。

不要預防性拆分:只有角色需要獨立演進才拆包。簡單工具插件一個包就夠。


10. 打包與安裝(把插件交付給使用者)

兩個概念

  • 組合包(bundle):附帶一個配置層的 npm 套件,manifest 宣告 dsh.bundle。是你編寫並分發的東西。
  • profile:位於 $DSH_HOME/profiles/<name> 的可啟動組合,manifest 宣告 dsh.profile(含有序 bundles 列表)。是使用者啟動的東西。

打包一個組合包

hello-plugin/
├── package.json       # 宣告 dsh.bundle
├── cordis.patch.yml   # 層內容
└── index.js           # 插件模組
// package.json
{
  "name": "dsh-hello-plugin",
  "version": "0.1.0",
  "type": "module",
  "main": "index.js",
  "files": ["index.js", "cordis.patch.yml"],
  "dsh": { "bundle": { "patch": "cordis.patch.yml" } }
}
# cordis.patch.yml
plugins:
  - name: dsh-hello-plugin

安裝組合包:dsh plugin install --profile <name> dsh-hello-plugin(或 pnpm add + 手動改 cordis.yml)。

建立一個 profile

如果整套配置要作為可重用的啟動設定,就寫一個 profile:

# $DSH_HOME/profiles/my-work/dsh.profile
version: "1"
summary: "My Work Harness"
needs:
  bundles:
    - dsh-hello-plugin@^0.1
    - @deepseek-ai/dsh-core
  plugins:
    - name: dsh-hello-plugin      # 套件內插件
      disabled: false
      config:
        greeting: Bonjour

然後 dsh --profile my-work 啟動。

原生支援打包(已整合的 CLI)

如果使用 DSH 的整合 CLI(dsh 命令):

# 開發中快速測試
$ dsh plugin install --profile dev ./path/to/my-plugin

# 打包為 tarball 並安裝
$ cd my-plugin && npm pack
$ dsh plugin install --profile dev ./dsh-my-plugin-0.1.0.tgz

# 發佈 npm 後
$ dsh plugin install --profile dev dsh-my-plugin

推薦:開發階段用 dsh plugin install --profile dev ./src/my-plugin.ts 直接載入原始碼,結合 HMR 快速迭代。發佈前再打包。


11. 驗證清單(動手前 / PR 前檢查)

#檢查項通過條件
1所有註冊透過 ctx無 addListener、setInterval、fs.watch 等裸調用
2非 ctx 資源包在 ctx.effect每個手動資源都有 ctx.effect(() => { create; return () => destroy })
3inject 宣告正確必需依賴在 inject 中列出;可選依賴用 ctx.get() 探測
4Config 有 Schemastery schema所有可配置參數有 schema 欄位 + 預設值
5工具使用 defineTool工具定義符合 name/description/parameters/output/execute 契約
6execute 返回規範值非 isError 時返回 output.schema 宣告的型別,不返回內容塊
7render 是純函數不做 I/O、不讀 ctx、不產生副作用
8服務提供方用 extends Service註冊透過 ctx.plugin(ServiceSubclass)
9長任務回應 exec.signal耗時操作檢查 signal,收到後取消
10schema 約束不足處有顯式檢查非空、正數等附加約束在 execute 頂部 throw
11無硬編碼可配置參數所有部署差異參數都在 Config 中
12使用 defineTool 註冊無遺漏的 ctx.tools.register

12. 參考文件索引

References/ 目錄下的簡潔提煉(均為單頁 Macchiato Markdown):

檔案內容
01-cordis.mdCordis 核心概念:context、fiber、effect、coeffect、服務、事件模式
02-schemastery.mdSchemastery schema 構造速查
03-tools.md工具定義、執行管道、擴充點詳解
04-ui-card.md工具卡片渲染(presentCall/presentResult)規範
05-llm-adapter.mdLLM 適配器完整教程
06-events.mdHarness 事件總表
07-packaging.md打包、bundle、profile 詳解
08-capability-layering.md能力分層架構與 seam 設計模式
09-sandboxing.md工具沙箱與安全模型
10-spatiotemporal.md時空可組合性論文摘要
11-cookbook.md常見模式與食譜:權限門禁、配置熱重載、自訂事件

本技能參考了 @deepseek-ai/cordis 3.x 與 @deepseek-ai/dsh-tools 0.x 的 API。

相關技能