SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-21

Как создать плагин для DeepSeek Harness

Руководство по разработке плагина dsh с нуля. Узнайте, какие поля в package.json обязательны, как настроить patch-файл для монтирования и какие два хука нужны для работы.

Что на самом деле представляет собой плагин dsh

Плагин dsh — это npm-пакет, который экспортирует функцию apply и содержит один небольшой YAML-файл, указывающий DeepSeek Harness загрузить его. Изучать отдельный SDK для плагинов не требуется. dsh — это приложение на базе Cordis, и принцип «всё есть плагин» здесь буквален: реестр инструментов, цикл агента, хранилище сессий и веб-сервер — это записи в том же дереве плагинов, к которому присоединяется ваш пакет.

Cordis — это универсальный фреймворк композиции, разработанный независимо и годами используемый в качестве основы для фреймворка чат-ботов Koishi. Он управляет загрузкой и выгрузкой, а также разрешает зависимости между плагинами. Он ничего не знает об агентах. Всё, что относится к агентам, поступает из пакетов harness, развёрнутых поверх него, поэтому структура плагина, описанная ниже, выглядит такой компактной. Большая часть функциональности наследуется.

Плагин состоит из двух частей. Серверная часть выполняется в Node, регистрирует инструменты и обработчики событий, а также может предоставлять собственные сервисы. Браузерная часть выполняется внутри Web UI и регистрирует слоты интерфейса. Первый плагин почти всегда является только серверным, поэтому считайте браузерную часть опциональной, пока она не понадобится.

Это руководство написано для версии @deepseek-ai/dsh 0.1.0-rc.7, тег npm latest от 19 августа 2026 года. dsh находится на стадии предварительного просмотра для разработчиков (developer preview), и в его README указано, что возможны изменения, нарушающие обратную совместимость. Все имена ключей ниже были взяты из официальной документации и репозитория на указанную дату. Перепроверяйте их перед использованием, так как API на стадии предварительного просмотра может менять названия полей между релиз-кандидатами. Если harness ещё не запущен, сначала настройте его с помощью DeepSeek Harness на VPS и API-ключа dsh и конфигурации модели, а затем вернитесь сюда.

Загрузка одного временного файла перед упаковкой

Сначала упаковывать — медленный способ обучения. Загрузите один файл, убедитесь, что среда выполнения вызывает ваш код, и только потом упаковывайте его.

Создайте папку вне checkout harness и поместите в неё один файл.

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

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  console.log('[hello-plugin] plugin loaded')
}

export const name — это метаданные, используемые для маркировки плагина в диагностике. apply — это весь контракт: Cordis вызывает его один раз и передает контекст, ограниченный областью действия вашего плагина. Всё, что вы зарегистрируете в этом контексте, будет автоматически отменено при удалении плагина.

Рядом создайте файл cordis.yml.

- insert:
    - id: hello
      name: '/absolute/path/to/scratch-plugin/hello.ts'

Теперь запустите профиль с наложенным поверх него файлом.

dsh web --patch ./scratch-plugin/cordis.yml

Если dsh отсутствует в вашем PATH, npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml выполняет ту же задачу. Этот путь через npx может выдать вам кэшированный старый release candidate вместо версии, описанной в данном руководстве. Поэтому, если harness категорически отвергает задокументированный флаг, ознакомьтесь с исправлениями ошибок установки и версий dsh, прежде чем сомневаться в собственном файле. Вы должны увидеть [hello-plugin] plugin loaded в терминале, который запустил dsh. Если ничего не появилось, строка не была разрешена.

Поле name принимает имя пакета npm или путь в файловой системе, при этом документация upstream указывает, что путь должен быть абсолютным. Относительный путь в ./hello.ts — это первое, что нужно проверить, если временный плагин не выводит данные. Второе — расширение файла. Задокументированный цикл выполняется как pnpm dsh web --patch ... из клона репозитория harness, где записи TypeScript загружаются через tsx. Если ваш dsh был установлен через npm, укажите в строке обычный JavaScript или сначала соберите файл.

--patch — это флаг запуска, и его наложение применяется в последнюю очередь, после всех бандлов и после вашего собственного патча профиля. Таким образом, временное наложение (scratch overlay) всегда имеет приоритет, что именно и требуется при итеративной разработке.

Напишите простейший инструмент, выполняющий полезную задачу

Строка в логе подтверждает загрузку плагина. Инструмент подтверждает, что плагин является частью агента.

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

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

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

export const inject = ['tools'] — это строка, которую часто забывают. Записи в конфигурации Cordis запускаются одновременно, поэтому положение строки в файле не гарантирует порядок загрузки. Очередность определяется объявленными зависимостями. inject указывает Cordis дождаться появления ctx.tools перед вызовом вашего apply; без этого ваш код может выполниться в момент, когда реестр еще недоступен для регистрации.

Остальная часть объекта представляет собой контракт, который видит модель. parameters — это схема аргументов, а execute получает аргументы, уже проверенные по этой схеме. output.schema описывает значение, которое возвращает execute, в то время как render преобразует это значение в блоки контента, считываемые моделью. Разделение этих двух элементов позволяет интерфейсу отображать одно, а модели — считывать другое.

Запустите профиль и попросите ассистента поприветствовать кого-либо по имени. Ответ поступит через ваш execute. Регистрация через ctx обратима, поэтому удаление плагина автоматически отменяет регистрацию инструмента. Для всего, о чем Cordis не может знать, например, для сокета или файлового дескриптора, вызовите ctx.effect() и передайте ему функцию очистки (disposer).

Две точки расширения, с которыми чаще всего работает первый плагин

Полный список мест для встраивания кода велик. Две из них охватывают почти все задачи при создании первого плагина.

События диалога — это постоянный, логируемый поток. Их названия: session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call и tool/result. К ним подключается обычный слушатель.

ctx.on('tool/call', (payload) => {
  console.log('[my-plugin] tool/call', JSON.stringify(payload))
})

Выведите содержимое payload один раз и изучите его. Не копируйте имена полей payload из каких-либо руководств, включая это, так как структура данных в preview API меняется чаще всего.

Вторая точка расширения — это каскад (waterfall). События agent/pre-step, agent/request, agent/request-error, llm/stream и tools/* являются каскадными, и слушатель каскада имеет другую сигнатуру. Он принимает callback next, и цепочка продолжается только в том случае, если этот callback будет вызван.

ctx.on('agent/request', async (payload, next) => {
  const startedAt = Date.now()
  const downstream = await next()
  console.log('[my-plugin] model request took', Date.now() - startedAt, 'ms')
  return downstream
})

Если вы забудете про await next(), вы не добавите хук. Вы замените вызов модели «пустотой», и агент остановится на этом месте, так как принудительное прерывание — это заложенное поведение для плагина-шлюза, который намеренно отклоняет запрос. Именно эта разница вызывает больше всего путаницы при написании первых плагинов. Сначала напишите вызов next(), а затем добавляйте всё остальное вокруг него.

agent/request оборачивает сам вызов модели. В его payload передаются данные об агенте, совершающем вызов, номер текущего хода, шаг, к которому относится запрос, и сигнал отмены для этого хода. Это делает его подходящим местом для логирования запросов или ограничения частоты (rate limiter). Каскады tools/* имеют такую же структуру на уровень ниже. tools/pre-execute позволяет разрешить, отклонить или запросить подтверждение перед отправкой. tools/execute оборачивает процесс отправки. tools/post-execute может заменить или заблокировать нормализованный результат. tools/result только наблюдает за уже сформированным итогом.

Упаковка в виде бандла для установки другими пользователями

Бандл — это npm-пакет, в файле package.json которого объявлено поле dsh.bundle, указывающее на файл патча. Это объявление — единственное различие между обычным файлом и устанавливаемым пакетом.

{
  "name": "dsh-plugin-hello",
  "version": "0.1.0",
  "type": "module",
  "main": "lib/index.js",
  "files": ["lib", "cordis.patch.yml", "README.md", "LICENSE"],
  "engines": { "node": "^22.19 || >=24", "dsh": ">=0.1.0-rc.6" },
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
  "keywords": ["dsh-plugin", "deepseek-harness"],
  "scripts": { "build": "tsdown", "prepare": "pnpm run build" },
  "exports": {
    ".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
    "./cordis.patch.yml": "./cordis.patch.yml",
    "./package.json": "./package.json"
  }
}

Файл cordis.patch.yml, расположенный рядом, содержит немного строк.

- insert:
    - id: dsh-plugin-hello
      name: dsh-plugin-hello

Строка name — это имя пакета, поэтому эти две строки должны совпадать. Строка id — это идентификатор, на который ссылается последующий слой, когда пользователь переопределяет вашу конфигурацию. Выбирайте стабильное значение и никогда не используйте его повторно для другого плагина.

В files обязательно должен быть указан cordis.patch.yml. Если его пропустить, в опубликованный архив попадет dsh.bundle.patch, указывающий на файл, который не был включен в пакет. В результате пакет установится, но не внесет никаких изменений в дерево.

Установите его в профиль из директории, содержащей папку вашего плагина.

dsh plugin --profile demo add ./dsh-plugin-hello
dsh --profile demo --dump-config
dsh --profile demo

Команда dsh plugin --profile <name> передает остальные аргументы утилите pnpm внутри этой директории профиля, поэтому add и remove работают так же, как в pnpm. Для удаления используйте dsh plugin --profile demo remove dsh-plugin-hello. Профили web и headless создаются автоматически из поставляемых шаблонов при первом использовании, а любое другое имя профиля необходимо создавать с помощью dsh plugin.

Почему ваша строка отсутствует в скомпонованном дереве

Компоновка начинается с пустого списка записей, после чего слои накладываются друг на друга в фиксированном порядке. Сначала идут все пакеты, указанные в dsh.profile.bundles профиля, в том порядке, в котором они перечислены. Затем следуют собственные cordis.patch.yml профиля. Далее $DSH_HOME/cordis.patch.yml. В конце добавляются любые наложения --patch из командной строки. Последующие слои замещают предыдущие строки по их id.

Профили находятся в $DSH_HOME/profiles/<name>. Каталог профиля содержит package.json с манифестом dsh.profile, в котором указан упорядоченный список bundles, а также собственный файл патчей пользователя. Имена пакетов разрешаются сначала из установки dsh, а затем из node_modules профиля, куда pnpm помещает плагин, находящийся вне дерева.

dsh --profile demo --dump-config выводит полностью скомпонованное дерево без запуска каких-либо процессов, и этот вывод является отправной точкой для отладки. Если идентификатор вашей строки отсутствует, проблема заключается в компоновке: имя не разрешается или файл патча не был упакован. Если строка присутствует, но ничего не происходит, проблема в вашем коде. Ответьте на этот вопрос в первую очередь, чтобы исключить большинство догадок.

Где на самом деле возникают ошибки загрузки

Ошибка, возникшая внутри apply, проявляется явно. Процесс завершается с этим исключением, и вы получаете трассировку стека, указывающую на вашу строку кода.

Ошибки разрешения модулей проходят незаметно. Загрузчик сообщает о модуле, который он не может разрешить, через логгер Cordis, вместо того чтобы аварийно завершить работу. В официальном руководстве предупреждается, что эти сообщения могут теряться при запуске, так как они выводятся до подключения консольных экспортеров. Поэтому опечатка в пути выглядит точно так же, как плагин, который загрузился и ничего не сделал. Именно поэтому проверку --dump-config, описанную выше, стоит выполнить до того, как вы начнете читать код.

Держите console.log в качестве первой инструкции в apply на этапе разработки. Ее отсутствие покажет вам, с какой частью проблемы вы столкнулись, а удалить ее позже не составит труда. На сервере запускайте обвязку в интерактивном режиме (foreground) во время отладки, а не через менеджер служб. Так вывод загрузчика попадет прямо в ваш терминал, а не в журнал, который пришлось бы искать и читать отдельно.

Итерация без перезапуска всей системы

Честный ответ на сегодняшний день для серверной части заключается в том, что вам придётся выполнять перезапуск. Пакет веб-приложения поставляется с отключенной функцией горячей перезагрузки модулей (hot module reload), а в файле содержится примечание о том, что она будет включена повторно после тестирования жизненного цикла перезагрузки. Цепочка перезагрузки на стороне клиента всегда смонтирована, но остаётся неактивной, пока наблюдатель за сборкой не перепишет клиентские пакеты, поэтому для вашей части на Node она также ничего не делает.

Сделайте перезапуск дешёвым, вместо того чтобы пытаться добиться перезагрузки, которой пока не существует. Держите плагин в одном файле. Загружайте его с помощью --patch, а не устанавливайте в профиль, чтобы между правкой кода и запуском не было этапов сборки или выполнения pnpm. Регистрируйте всё через ctx, чтобы перезапуск не приводил к появлению дубликатов инструментов или зависших слушателей. Оборачивайте всё, что выделяете самостоятельно, в ctx.effect() с использованием реального метода очистки (disposer), так как типичный симптом отсутствия очистки — это сбой при втором запуске из-за порта, который всё ещё занят первым процессом.

Если вы ведёте разработку с использованием обвязки (harness), запущенной на сервере, а не на вашем ноутбуке, ничего из вышеперечисленного не меняется, но привязка веб-интерфейса имеет значение. Привязка к loopback на порту 3080 объясняет, почему страница не открывается сама по себе и что с этим делать.

Браузерная часть и уровень доверия к ней

Добавляйте этот раздел только в том случае, если вашему плагину требуется собственный интерфейс. Он объявляется в том же поле dsh, что и бандл.

{
  "dsh": {
    "client": {
      "platform": "web",
      "inject": [],
      "external": [],
      "immediately": false
    }
  },
  "exports": {
    ".": "./src/index.ts",
    "./client": "./src/client/apply.ts",
    "./package.json": "./package.json"
  }
}

Поле "platform": "web" является обязательным, и сканер выдаст ошибку, если в пакете отсутствует экспорт ./client, поэтому карта экспорта является частью манифеста, а не просто удобным дополнением. Клиентская точка входа получает Cordis Context, расширенный типом клиентской среды выполнения, а каждая регистрация происходит внутри apply через ctx.slots.register. Побочные эффекты на уровне модулей там запрещены.

import type { Context } from 'cordis'
import type { DshClientContext } from '@deepseek-ai/dsh-client-runtime'

export async function apply(ctx: Context & DshClientContext) {
  ctx.slots.register({ name: 'domain.entry.slot' }, MyComponent)
}

Перед началом работы стоит учесть две детали. Поле inject в клиентском манифесте носит документальный, а не планирующий характер: оно фиксирует связи зависимостей на уровне пакета и не управляет порядком активации. В external вы объявляете запросы модулей, выходящие за рамки базового набора, чтобы они были материализованы до того, как ваш плагин их запросит. Это наиболее динамично развивающаяся часть предварительной версии, поэтому читайте packages/client/AGENTS.md в репозитории harness в тот день, когда пишете код, а не в день прочтения руководства.

Публикация и описание того, к чему обращается ваш плагин

Добавление темы dsh-plugin в репозиторий на GitHub помещает его в список, который просматривают пользователи в поисках плагинов. Это заявка на доверие со стороны незнакомых людей, которая накладывает определенные обязательства. Эти обязательства зеркально отражают то, что наше руководство по проверке плагинов dsh перед установкой рекомендует пользователям изучить, поэтому написание документации в соответствии с этим чек-листом — самый простой способ пройти проверку.

  • Фиксируйте версии зависимостей. Использование диапазона с символом «крышка» (caret) для транзитивных зависимостей приводит к тому, что пакет, который был безопасным на прошлой неделе, на этой неделе выполняет другой код. Это именно тот механизм, который лежит в основе атак на цепочку поставок npm на сервере.
  • Указывайте в манифесте, к чему вы обращаетесь. Ваш список inject — это честное, машиночитаемое резюме того, какие сервисы окружения вы используете. Проверяющий прочтет его за секунды и составит свое мнение.
  • Никаких скрытых сетевых вызовов. Если инструмент обращается к API, укажите хост в README и сделайте эндпоинт настраиваемым. Плагин, который связывается с сервером, не упомянутым в документации, будет исключен из списка людьми, которые занимаются аудитом подобных решений.
  • Держите files в строгих рамках. Публикация всей рабочей папки приводит к тому, что случайный файл с учетными данными попадает в реестр.
  • Предоставьте пользователям, устанавливающим плагин через git, скрипт prepare, который выполняет сборку без предположений о наличии инструментов разработки, и укажите в README, что они должны добавить эту сборку в белый список в файле pnpm-workspace.yaml своего профиля.
  • Укажите в README дату выпуска и версию релиз-кандидата, на которой вы проводили сборку и тестирование. Пользователям предварительного API необходимо знать, с какой версией вы работали.

Чтобы увидеть, как готовый плагин выглядит со стороны, ознакомьтесь с разделом плагины dsh, заслуживающие установки и обратите внимание на то, что сообщает каждый README перед установкой. Если вы уже писали расширения для другого агента, информация о том, как устроены плагины Claude Code, послужит полезным материалом для сравнения. Окружение предоставляет вам «живой» граф объектов и возможность обратимой регистрации — это дает больше возможностей, чем простой манифест файлов, но и накладывает больше ответственности.

FAQ

Нужно ли публиковать пакет в npm, чтобы написать плагин для dsh?

Нет. Достаточно указать путь к файловой системе в cordis.yml overlay, загрузив его через dsh web --patch ./scratch-plugin/cordis.yml, чтобы запустить собственный код внутри окружения. Путь должен быть абсолютным. Упаковка важна только тогда, когда плагин устанавливает кто-то другой, и даже в этом случае вы можете установить локальную папку с помощью dsh plugin --profile demo add ./my-plugin, чтобы протестировать упакованную форму без обращения к реестру.

Почему мой плагин загружается, но инструмент не появляется?

Сначала выполните dsh --profile demo --dump-config. Если идентификатор вашей строки отсутствует в выводе, значит, плагин не был смонтирован, и причина кроется в композиции, а не в коде. Если строка присутствует, проверьте export const inject = ['tools']. Записи в конфигурации Cordis запускаются параллельно, поэтому порядок файлов не определяет порядок загрузки. Без этого объявления Cordis не дожидается реестра инструментов, и ваш apply может выполниться в момент, когда ctx.tools ещё недоступен для регистрации.

В чем разница между cordis.yml и cordis.patch.yml?

cordis.yml — это полный список записей. cordis.patch.yml — это слой, применяемый поверх основного списка; он нацелен на строки по их идентификаторам для вставки новых или замены существующих конфигураций. Пакет указывает на свой файл патча через dsh.bundle.patch в package.json. Слои применяются в фиксированном порядке: каждый пакет в порядке, указанном в профиле, затем файл патча профиля, затем $DSH_HOME/cordis.patch.yml, и в конце любой --patch overlay. Последующие слои имеют приоритет.

Можно ли выполнить горячую перезагрузку плагина dsh во время работы агента?

Начиная с версии 0.1.0-rc.7, для хост-части в веб-профиле это невозможно. В этом пакете общая строка горячей перезагрузки модулей отключена, а в файле есть примечание о том, что она вернётся после тестирования жизненного цикла перезагрузки. Вместо этого проектируйте систему с расчётом на быструю перезагрузку: один файл, загружаемый через --patch без этапа сборки, и каждая регистрация выполняется через ctx, чтобы данные не перетекали из одного запуска в другой. Используйте ctx.effect() с функцией очистки (disposer) для ресурсов, которые Cordis не может освободить самостоятельно.