Как создать плагин для 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 указано, что возможны изменения, нарушающие обратную совместимость. Все названия ключей ниже были взяты из официальной документации и репозитория на указанную дату. При повторной проверке руководства 12 октября 2026 года тег npm latest был 0.2.0-rc.2, и в разделах ниже указано, где этот релиз ведет себя иначе. Перепроверяйте названия ключей перед тем, как использовать их в своих проектах, так как 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).
Две точки расширения, с которыми сталкивается первый плагин
Полный список мест для интеграции обширен. Две из них охватывают почти каждый первый плагин.
События диалога (conversation events) представляют собой постоянный, логируемый поток. В версии 0.1.0-rc.7 использовались имена session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call и tool/result. В релизе 0.2.0-rc.2 отсутствует assistant/chunk. Потоковая передача в реальном времени была перенесена в agent/assistant-stream, кадры которого остаются в памяти процесса и не записываются в логи, а новое постоянное событие assistant/attempt фиксирует неудачные, повторные и отмененные попытки. Вы подключаете обычный слушатель (listener).
ctx.on('tool/call', (payload) => {
console.log('[my-plugin] tool/call', JSON.stringify(payload))
})Выведите полезную нагрузку (payload) один раз и изучите её. Не копируйте имена полей полезной нагрузки из руководств, включая это, так как структура данных является частью предварительного API, который меняется чаще всего.
Вторая точка расширения — это каскад (waterfall). События agent/pre-step, agent/request, agent/request-error, llm/stream и tools/* являются каскадными, и слушатель каскада имеет другую сигнатуру. Он принимает обратный вызов next, и цепочка продолжается только в том случае, если он будет вызван.
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 оборачивает сам вызов модели. Его полезная нагрузка содержит агента, выполняющего вызов, номер открытого хода (turn), шаг, к которому относится запрос, и сигнал прерывания для этого хода. Это делает данную точку подходящей для логгера запросов или ограничителя частоты (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, sdk, sdk-minimal и acp создаются из шаблонов при первом использовании. В версии 0.2.0-rc.2 любое другое имя создается одним из двух способов. dsh plugin запускает его только на основе базового бандла. dsh --profile <name> --from-default-profile <template> копирует список бандлов из поставляемого шаблона. Запуск пользовательского профиля, который еще не существует, завершится ошибкой с подсказкой выполнить 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) во время отладки, а не через менеджер служб. Так вывод загрузчика попадет прямо в ваш терминал, а не в журнал, который пришлось бы искать и читать отдельно.
Итерация без перезагрузки всей системы
В версии 0.1.0-rc.7 ответ для серверной части был прост: перезагрузка. В комплекте веб-приложения того релиза общая строка горячей перезагрузки модулей была отключена, а в файле содержалось примечание о том, что она будет включена после тестирования жизненного цикла перезагрузки. В 0.2.0-rc.2 это переопределение удалено. Базовый пакет монтирует @deepseek-ai/dsh-hmr в каждом профиле с root: [], с комментарием «Конфигурация профиля перезагружается по умолчанию; корневые модули требуют явного подключения». Корректные правки в файлах профиля и патчах home теперь применяются повторно без перезагрузки. Исходный код вашего плагина по-прежнему не отслеживается, если вы не добавите его директорию в root этой строки. Добавление, удаление или обновление пакета всё ещё требует перезагрузки. Цепочка перезагрузки на стороне клиента всегда смонтирована, но остаётся неактивной до тех пор, пока наблюдатель за пересборкой не перезапишет клиентские пакеты, поэтому для вашей части на Node она также ничего не делает.
Сделайте перезагрузку быстрой, вместо того чтобы полагаться на перезагрузку, которую нужно подключать отдельно. Держите плагин в одном файле. Загружайте его с помощью --patch, а не устанавливайте в профиль, чтобы между правкой и запуском не было этапов сборки или выполнения pnpm. Регистрируйте всё через ctx, чтобы перезагрузка не оставляла дубликаты инструментов или «зависшие» слушатели. Оборачивайте всё, что выделяете самостоятельно, в ctx.effect() с реальным механизмом очистки (disposer), так как типичный симптом отсутствия очистки — это сбой второго запуска из-за порта, который всё ещё занят первым процессом.
Если вы ведете разработку с использованием обвязки (harness), запущенной на сервере, а не на вашем ноутбуке, вышесказанное не меняется, но привязка Web UI имеет значение. Локальный запуск предоставляет Web UI по адресу http://127.0.0.1:3080 и открывает его в браузере по умолчанию, либо пропускает открытие браузера, если вы передадите --no-open. Запуск по SSH только выводит URL хоста, поэтому на вашей машине ничего не открывается. Привязка 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 это невозможно, так как в её веб-пакете строка для горячей перезагрузки модулей была отключена. В версии 0.2.0-rc.2 эта строка включена в каждый профиль, но по умолчанию перезагружает только конфигурацию профиля. Корневые модули подключаются через список root, поэтому код вашего плагина всё равно потребует перезапуска, если вы не добавите соответствующую логику. В любом случае, проектируйте систему с расчётом на быстрый перезапуск: один файл, загружаемый через --patch без этапа сборки, и каждая регистрация, выполненная через ctx, чтобы данные не перетекали из одного запуска в другой. Используйте ctx.effect() с функцией очистки (disposer) для ресурсов, которые Cordis не может освободить самостоятельно.