# Рецепт плагина для DeepSeek Harness Практическое руководство, собранное на опыте `dsh-locale-russian` — языкового пакета, который доведён до публикации в npm. Здесь только то, что реально проверено на DSH `0.1.5-rc.x` и `0.2.1-rc.2`, с граблями, на которые мы наступили. ## Что такое плагин в DSH DSH собран из плагинов Cordis: почти каждая строка в профиле — плагин, а профиль — это стек патч-слоёв над пустым деревом. Отсюда два практических следствия: расширять систему можно, не меняя её код, а «правильный» плагин — это обычный npm-пакет с манифестом и патч-файлом. Три уровня вмешательства: | Уровень | Что даёт | Где живёт | |---|---|---| | Патч-слой | включить/выключить строку, заменить её `config`, вставить свою | `cordis.patch.yml` профиля, домашний патч, `--patch` | | Плагин | новая функциональность: инструменты, сервисы, UI, языки | npm-пакет с хост- и/или клиентской половиной | | Динамический пакет | временный инструмент или UI на время сессии | `dsh-tool-cordis` + host runner, только в памяти | ## Скелет пакета ``` my-plugin/ package.json # манифест с dsh.bundle и dsh.client cordis.patch.yml # слой патча: какие строки вставляет плагин lib/index.js # хост-половина (Node) lib/client.js # клиентская половина (браузер), собирается в формат загрузчика icon.svg # для карточек в списке плагинов и в маркете locale/en.json # собственные метаданные (title/description) по языкам ``` Манифест объявляет **обе роли**: ```json { "name": "my-plugin", "type": "module", "main": "lib/index.js", "exports": { ".": "./lib/index.js", "./client": "./lib/client.js", "./icon": "./icon.svg", "./locale/*.json": "./locale/*.json", "./package.json": "./package.json" }, "files": ["lib", "locale/*.json", "cordis.patch.yml", "icon.svg", "README.md"], "dsh": { "bundle": { "patch": "./cordis.patch.yml" }, "client": { "platform": "web", "inject": ["@deepseek-ai/dsh-client-locale"] } }, "engines": { "dsh": ">=0.1.5-rc.1 <0.3.0" } } ``` `cordis.patch.yml` — одна строка, которая и подключает плагин: ```yaml - insert: - id: my-plugin name: my-plugin ``` ## Грабли, на которые мы наступили ### 1. `id` клиентского бандла обязан совпадать с именем пакета Клиентская половина регистрируется в загрузчике модулей: ```js window.__ModuleLoader__.load({ id: "my-plugin", // ← ровно имя пакета из package.json factory: (require) => { /* … */ return module.exports }, }) ``` Хост сканирует строки загрузчика, находит ближайший `package.json`, берёт из него имя пакета и **сверяет** его с тем, что зарегистрировал бандл. При расхождении приложение отвечает: ``` client-modules: could not load "my-plugin": loaded without registering "my-plugin" via __ModuleLoader__.load ``` Мы поймали это, переименовав пакет: имя сменилось, а `id` в бандле остался прежним. Вывод: **генерируйте `id` из манифеста** при сборке, не пишите руками. И помните, что метаданные пакета кэшируются до перезапуска — перезагрузка страницы не поможет. ### 2. `engines.dsh` и ловушка pre-release Обычный диапазон `>=0.1.5-rc.1 <0.3.0` **не совпадёт** с `0.2.1-rc.2`: по правилам semver pre-release версия удовлетворяет диапазону, только если в нём есть компаратор с той же тройкой чисел. Пока DSH живёт в pre-release, перечисляйте сборки явно: ```json "engines": { "dsh": "^0.1.5-rc.1 || ^0.2.1-rc.2" } ``` Если диапазон не совпадёт, DSH отклонит пакет по этому полю — и плагин просто не загрузится. ### 3. Данные хоста не переводятся и не переписываются Описания навыков (`SKILL.md`), имена пакетов, идентификаторы моделей и провайдеров приходят от хоста как данные. Китайская локаль ведёт себя так же — это осознанное правило, а не пробел. Локализуйте только собственную копию продукта. ## Что и как расширять ### Инструменты для модели ```js import { defineTool } from '@deepseek-ai/dsh-tools' export const name = 'my-tool' export const inject = ['tools'] export function apply(ctx) { ctx.effect(() => ctx.tools.register(defineTool({ /* схема и обработчик */ })), 'my-tool: регистрация') } ``` Правила платформы: каждый вклад идёт через `ctx.effect()` и возвращает disposer, конфигурация — через `Config` (schemastery), а не хардкод, а всё, что видит модель, должно быть восстановимо из журнала сессии. ### Клиентский интерфейс Клиентская половина регистрирует компоненты в слоты, объявленные другими плагинами, и подписывается на сервисы через `inject`. Вставлять произвольный HTML нельзя — только слоты; для глобальных стилей и скриптов есть инъекция в индекс страницы (`webserver/index-inject`, так работает штатная тема). ### Язык ```js export const inject = ['locale'] export function apply(ctx) { ctx.effect(() => ctx.locale.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' }), 'ru: язык') ctx.effect(() => ctx.locale.register('common', 'ru', { cancel: 'Отмена' }), 'ru: словарь') } ``` Недостающие ключи падают в английский, поэтому неполный перевод ничего не ломает. Встроенный набор языков закрыт (`zh`, `en`) — внешний языковой пакет и есть штатный путь. ### Склонения В рантайме локали нет правил множественного числа: компоненты сами выбирают форму по английскому правилу `count === 1 ? '.one' : '.other'`. Языкам с тремя формами (русский: 1 файл, 2 файла, 5 файлов) этого мало. Рабочий обход — обернуть метод перевода и хранить до трёх форм через `|`: ```js const prototype = Object.getPrototypeOf(ctx.locale) const original = prototype.translate if (typeof original === 'function') { prototype.translate = function (ns, key, params) { /* выбрать форму по Intl.PluralRules('ru') */ } } ``` Обязательно с проверкой типа и безопасной деградацией: если метод исчезнет, строки должны остаться, потеряв только согласование. Об этом стоит просить публичный хук — мы открыли тему в их Discussions (см. ниже). ### Метаданные плагинов Описания карточек в разделе «Встроенные плагины» приходят как `LocalizedText` и рисуются через `LocaleRuntime.resolveText`. Языковой пакет может подменить и этот метод, подставляя перевод по английскому тексту из своей таблицы; если пакет отдаёт свой русский вариант, он выигрывает. ## Проверки перед публикацией | Что | Зачем | |---|---| | Линтер переводов | плейсхолдеры, терминология, склонения, пустые значения | | Тесты бандла без браузера | язык и словари регистрируются, склонения работают, другие локали не тронуты | | Обход интерфейса в headless-браузере | обрезанный текст: **на Linux и Windows метрики шрифтов другие**, macOS этого не покажет | | Матрица CI по платформам | платформенные пакеты и строки различаются: Linux и Windows ставят свои варианты | | Детектор обновлений | недельный прогон против свежего DSH из npm: появилась дельта — открывается issue | ## Публикация 1. **Рынок плагинов.** Индекс — один JSON формата 1 (`format`, `title`, `plugins[]` с `versions[]`, `source` и `engines.dsh`). Каталог с индексом и иконкой достаточно выложить на GitHub Pages; пользователи добавляют адрес на странице **Плагины → Обзор**. Валидируйте индекс официальным `@deepseek-ai/dsh-plugin-market-index` перед публикацией. 2. **npm.** Публикуйте как обычный пакет. Если у аккаунта passkey вместо TOTP, `npm publish` из скрипта не пройдёт: npm печатает ссылку для браузера и требует интерактивный терминал. Для автоматизации включите **trusted publishing** (OIDC): на странице пакета → *Publishing access* → *Trusted publishing* укажите владельца, репозиторий и имя workflow, и публикуйте из GitHub Actions **без токенов** — npm выдаст аттестацию происхождения сам. Долгоживущие токены с обходом 2FA npm сворачивает. 3. **Учтите staged publishing.** npm 11 умеет сначала создавать служебную версию-заполнитель `0.0.0-stage`, а настоящую публиковать после подтверждения. Не пугайтесь её в списке версий. 4. **Выпуск по тегу.** `npm version patch && git push --follow-tags`: workflow берёт версию из тега, собирает, проверяет, публикует и создаёт релиз. ## Как общаться с проектом В репозитории DSH **отключены и issues, и внешние pull request'ы** — их `CONTRIBUTING.md` прямо просит писать в GitHub Discussions. Рабочие каналы: - **Ideas** — предложения по API (мы так попросили публичный хук склонений и локализацию оболочки); - **Show Your Plugins!** — витрина плагинов; - тема репозитория **`dsh-plugin`** — по ней плагины находят другие. ## Чек-лист нового плагина 1. `id` бандла = `name` пакета; и то и другое генерируется сборкой. 2. Объявлены `dsh.bundle.patch` и (если есть UI) `dsh.client` с `platform: 'web'`. 3. Строка патча вставлена в `cordis.patch.yml`, имя строки равно имени пакета. 4. `engines.dsh` перечисляет проверенные сборки, включая pre-release. 5. Все вклады — через `ctx.effect()` с осмысленной подписью; есть disposer. 6. Конфигурация — через `Config`, без хардкода развёртывания. 7. Есть иконка и `locale/*.json` с собственными `title`/`description`. 8. Проверки: линтер, тесты без браузера, обход интерфейса, CI на двух платформах. 9. Публикация: индекс маркета + npm через trusted publishing. 10. Зависимости от приватных методов рантайма — с проверкой типа, безопасной деградацией и обращением в Discussions. ## Ссылки - Архитектура и профили: `docs/architecture.md`, `packages/boot/app-boot/README.md` - Клиентские плагины и слоты: `packages/client/AGENTS.md`, `docs/subsystems/slots.md` - Формат индекса рынка: `.agents/notes/implemented/feature/2026-10-02-plugin-market-index-format.md` - Живой пример: [dsh-locale-russian](https://github.com/nemezida1337/dsh-locale-russian) — языковой пакет с полным пайплайном перевода и проверок