# Руководство по участию в разработке плагинов Система плагинов Voyager в первую очередь рассчитана на декларативные плагины: `plugin.json` описывает метаданные и DOM-операции, а CSS описывает визуальные изменения. Сам плагин не выполняет удаленный JavaScript; встроенный движок Voyager интерпретирует manifest и стили. Так плагины проще проверять и поддерживать. Если вы хотите предложить плагин, начните с этого подхода. ## Только для себя: локальные плагины Плагин, который нужен только вам в своём браузере, не проходит этот процесс. Создайте его как локальный плагин и импортируйте во всплывающем окне: без Issue, без PR и без клонирования репозитория. [Напишите свой плагин](./write-a-plugin.md) описывает три способа его создать, включая проверочную утилиту `create-voyager-plugin` и skill для агентов-программистов. ## Рекомендуемый путь 1. Сначала убедитесь, что идея подходит для плагина: ширина чтения, исправления верстки, настройки темы, скрытие или пометка элементов страницы, простая адаптация сайтов обычно подходят. 2. Сначала откройте Issue в основном репозитории Voyager. Опишите проблему, целевой сайт и отличие от существующих плагинов; дождитесь явного одобрения мейнтейнера до начала разработки или открытия PR. 3. Используйте `plugin.json` для метаданных, соответствий сайтам, настроек и вкладов. 4. Поместите стили в `style.css` в том же каталоге и подключите его через `contributes.styles`. 5. Протестируйте локально и приложите к PR тестовые страницы, скриншоты или короткую запись. Мейнтейнеры решат, готов ли плагин для официального catalog. ## Структура каталогов Официальные встроенные плагины лежат в `src/features/plugins/catalog/`, по одной директории на платформу: ``` src/features/plugins/catalog/ marketplace.json индекс для магазина плагинов в документации sites//site.json адаптер сайта в виде данных sites//plugins//plugin.json декларативный плагин sites//plugins//style.css его стили sites//plugins//README.md что он чинит и почему ``` Обнаружение автоматическое: `catalog/sites/index.ts` находит каждый `site.json` и каждый `plugin.json` через `import.meta.glob`, поэтому добавить сайт или плагин означает добавить файлы. Таблицы соответствий, которую нужно править, здесь нет. `marketplace.json` не является такой таблицей: это только индекс, который читает магазин плагинов в документации. Тест держит его в синхронизации с обнаружением, поэтому новому плагину нужна запись и там. Поле `source` содержит путь относительно catalog, например `sites/deepseek/plugins/reading-width/plugin.json`. Исключение — плагины в `sites/gemini/` и `sites/aistudio/`: они поставляются внутри Voyager и в `marketplace.json` не попадают. `matches` плагина должны оставаться внутри `matches` его сайта. Плагин, вышедший за эти границы, ломает сборку. ## Ключи семантических селекторов `site.json` сопоставляет фиксированный словарь семантических ключей с CSS-селекторами конкретного сайта. Словарь задан в `src/features/plugins/sites/semanticKeys.ts`; `site.json` может использовать только эти ключи, а неизвестный ключ отклоняется. - `userTurn`: контейнер сообщения пользователя. - `assistantTurn`: контейнер сообщения ассистента. - `thinkingBlock`: блок рассуждений внутри ответа ассистента. - `codeBlock`: отрисованный блок кода. - `composer`: поле ввода запроса. - `sidebar`: список диалогов или панель навигации. - `sidePanel`: вспомогательная панель, например artifacts или canvas. - `headerActions`: группа действий в правом верхнем углу диалога. - `scrollContainer`: элемент, который прокручивает диалог. - `composerRoot`: контейнер, в котором находятся поле ввода запроса и его кнопки. - `sendButton`: кнопка отправки запроса; на некоторых сайтах она появляется, только когда поле не пустое. Сайт объявляет только те ключи, которые действительно может предоставить. Плагины ссылаются на ключ, а не на селектор: в `target` DOM-операции пишется `{ "kind": "semantic", "key": "userTurn" }`, поэтому редизайн сайта чинится правкой одного `site.json`. `conversationIdPattern` является отдельным полем `site.json`, а не селектором: это регулярное выражение по пути URL, первая захватывающая группа которого и есть id диалога, например `^/chat/([^/?#]+)`. ## Добавление новой платформы Добавление нового сайта с чатом состоит из двух отдельных слоёв: 1. **Адаптер сайта**, `catalog/sites//site.json`, учит Voyager распознавать сайт и находить реплики пользователя, реплики ассистента, поле ввода, боковую панель и маркеры темы. 2. **Плагин** в `catalog/sites//plugins//` решает одну задачу пользователя на этом сайте, например ширину чтения или исправление отображения. `site.json` содержит `id` (совпадает с именем каталога), `label`, `matches`, `selectors`, `theme`, `brandColor`, `capabilities`, а также необязательные `conversationIdPattern` и `turnKeyAttributes`. `turnKeyAttributes` задаёт атрибуты, которые сохраняют идентичность сообщения пользователя при повторной отрисовке страницы; отслеживание отправки (его используют Activity папок и метки времени таймлайна) работает только на сайте, который задаёт его вместе с `composer`, `userTurn` и `conversationIdPattern`. `validateSiteAdapterData` в `src/features/plugins/sites/siteAdapterData.ts` проверяет этот файл, а `bun run catalog:build` проверяет каждый сайт и плагин перед публикацией. Каждая платформа плагинов задаётся данными. Сейчас платформы такие: ChatGPT, Claude и DeepSeek. Gemini и AI Studio остаются нативными поверхностями Voyager, и у них тоже есть свой `site.json` (`catalog/sites/gemini/` и `catalog/sites/aistudio/`), но он поставляется внутри пакета и никогда не публикуется в удалённом каталоге. Все пять адаптеров (`adapters/gemini.ts`, `aistudio.ts`, `claude.ts`, `chatgpt.ts` и `deepseek.ts`) — однострочные обёртки над своим `site.json`: правьте JSON, а не TypeScript. Публикуемый каталог по хостам несёт и данные сайта, поэтому исправление селекторов в `site.json` может дойти до пользователей без выпуска расширения. Новому сайту всё равно нужен адаптер, потому что ему также требуются разрешение на доступ к хосту и регистрация content script, которые поставляются внутри пакета. Для новой платформы, например DeepSeek, держите работу над адаптером в рамках контракта платформы. Поместите `site.json`, обязательные тесты селекторов и минимальную документацию, объясняющую поддержку, в одно законченное изменение. Реестр сайтов сам подхватывает новый адаптер из файла, поэтому список регистрации редактировать не нужно. Не добавляйте новый сайт под каждый плагин. После принятия адаптера последующие плагины можно отправлять отдельными сфокусированными изменениями. Если несколько изменений зависят друг от друга, полезна цепочка связанных PR (stacked PR): каждая следующая ветка основана на предыдущей, и каждый PR показывает только свою инкрементальную функцию. Цепочка всё равно должна следовать тому же правилу области: одна понятная цель пользователя на PR, с нужными тестами и очисткой. Поддержка сайта не требует интеграции с API модели. Не добавляйте статические content scripts или обязательные разрешения на хост для новой платформы; для сайтов, которым нужен только плагин, используйте существующий поток опциональных разрешений и динамической регистрации. Этот путь зависит от версии браузера. Chrome и Edge поддерживают поток, которым пользуется расширение. Firefox требует версии 128 или новее для опциональных разрешений на хост, а Safari — версии 16.4 или новее для динамической регистрации content script. В старых поддерживаемых версиях браузеров popup может правильно отказаться включать плагин для пользовательского сайта, потому что нужные API не могут обеспечить постоянное разрешение. Не обходите это, добавляя широкое статическое разрешение на хост без одобрения мейнтейнеров. ## Границы плагина Плагин должен ограничиваться пользовательской задачей, а не механически делиться по платформам. Плагин каталога лежит в папке одного сайта, и все его `matches` должны покрываться `site.json` этой папки (иначе `bun run plugin:check` завершится ошибкой), поэтому один плагин не может охватывать сразу Claude и ChatGPT. Если функция подходит нескольким платформам, например ширина чтения, навигация по страницам или оформление блоков кода, делайте по одному плагину в папке каждого сайта (например `sites/claude/plugins/reading-width` и `sites/chatgpt/plugins/reading-width`) и держите их названия, настройки и описания согласованными. Если для каждой платформы нужны совершенно разные настройки, DOM-логика или пользовательские тексты, отдельные плагины будут понятнее. Не помещайте несвязанные функции в один плагин только ради "один плагин для всего"; один плагин должен решать одну понятную задачу. Короткое правило: - Та же цель пользователя, те же настройки, отличаются только селекторы: по одному плагину в папке каждого сайта, с согласованными названиями и настройками. - Одна тема, но опыт сильно различается по платформам: можно разделить, сохранив связь в названиях и описаниях. - Разные цели: не объединяйте. ## Избегайте дубликатов Перед отправкой проверьте marketplace и существующие официальные плагины. Если хороший плагин уже есть, лучше улучшить его, чем создавать похожий. Дубликат стоит принимать только при явном улучшении, например: - Он поддерживает важную платформу, которую не покрывает исходный плагин. - Он исправляет проблему совместимости, которую исходный плагин не может решить. - Он заметно лучше по производительности, доступности или поддерживаемости. - Он предлагает действительно другой полезный пользовательский опыт, а не только новое имя или легкие изменения стилей. Так marketplace остается чище, а пользователям легче выбирать. ## Минимальный пример Не начинайте с пустого каталога: `bun run plugin:new --site ` создаст `catalog/sites//plugins//` с `plugin.json`, `style.css` и `README.md` и добавит соответствующую запись в `marketplace.json`. То, что он создаёт, — отправная точка, а не одобрение; манифест, который вы заполняете, выглядит так: ```json { "$schema": "https://voyager.nagi.fun/plugin.schema.json", "id": "your-name.example-plugin", "name": "Example Plugin", "version": "1.0.0", "description": "A short description of what this plugin improves.", "author": "your-name", "category": "readability", "license": "MIT", "engine": ">=1.0.0", "tier": "declarative", "matches": ["https://claude.ai/*"], "contributes": { "styles": [{ "file": "style.css" }], "domOps": [ { "op": "addClass", "target": "body", "className": "gv-plugin-example" } ] } } ``` Строка `$schema` необязательна и нужна только редактору: она указывает на JSON Schema манифеста, поэтому файл проверяется и дополняется прямо при вводе. У `site.json` своя схема — `https://voyager.nagi.fun/site.schema.json`. `style.css` можно писать как обычный CSS, но стили плагина лучше держать внутри собственной `gv-plugin-*`-класса: ```css .gv-plugin-example .some-target { max-width: 880px; } ``` ## Заметки по manifest - Для `id` используйте префикс автора или стиль обратного домена, например `your-name.reading-width`, чтобы избежать конфликтов. - Держите `matches` узкими. Указывайте только сайты, где плагин действительно должен работать. - Плагин может перечислять в `matches` несколько хостов, только если `site.json` его сайта покрывает их все (например chatgpt.com и chat.openai.com); чтобы охватить другой сайт, добавьте плагин в папку этого сайта. - Рекомендуемые значения `category`: `render-fix`, `theme`, `layout`, `readability`, `productivity`, `integration` или `other`. - В `engine` укажите требуемую версию движка плагинов. Официальные плагины можно использовать как пример. - По возможности добавьте `i18n` для китайского, английского и других распространенных языков. ## Ограничения CSS и ресурсов Декларативные плагины проверяются как недоверенный ввод, поэтому держите ресурсы самодостаточными: - Не используйте `@import`. - Не ссылайтесь на удаленные изображения, внешние шрифты или удаленный CSS. - Можно использовать обычный CSS, пользовательские свойства и подстановки значений настроек Voyager. - Используйте префикс `gv-plugin-` для классов, чтобы не загрязнять стили сайта или самого Voyager. Если плагину нужны настройки, по возможности начните с числовых значений. Например, плагин ширины чтения может записывать значение настройки в CSS-переменную, а CSS будет ее использовать. ## Границы DOM-операций Декларативные плагины сейчас поддерживают: - `addClass`: добавить класс целевым элементам. - `setAttribute`: установить атрибут. - `setStyle`: задать inline-стиль или CSS-переменную. Если значение заканчивается на `!important`, оно применяется с этим приоритетом; старые версии Voyager такое значение отбрасывают, поэтому, если плагин на это полагается, укажите `engine` как `>=1.9.0`. Сокращённые свойства, например `margin`, отслеживаются по каждому longhand-свойству, поэтому выключение одного плагина никогда не сбрасывает `margin-top` другого плагина. - `hide`: скрыть целевые элементы. Целью может быть CSS-селектор или один из перечисленных выше семантических ключей, записанный как `{ "kind": "semantic", "key": "userTurn" }`. Семантические ключи обычно стабильнее, но адаптер текущего сайта должен объявлять этот ключ. Декларативные операции должны быть обратимыми и безопасными при повторном выполнении. Не зависьте от одноразового состояния страницы и не предполагайте, что DOM никогда не меняется. ### Примитивы Некоторое поведение нельзя описать через CSS и обратимое изменение DOM. Примитив представляет собой собственный код, который поставляется внутри Voyager; манифест может вызвать его по имени через операцию `native`: ```json { "engine": ">=1.3.0", "requires": { "handlers": ["formulaCopy"] }, "contributes": { "domOps": [{ "op": "native", "handler": "formulaCopy", "params": {} }] } } ``` Манифест выбирает примитив и настраивает его, но никогда не задаёт логику, а `params` проверяет сам примитив перед запуском. Для любого плагина, который использует примитив, действуют два правила: - Каждый сайт, на который нацелен плагин, должен объявлять семантические ключи, которые читает примитив (например, `mermaid` читает `assistantTurn` и `codeBlock`). `requires.handlers` необязателен: операция `native` уже подразумевает свой примитив. - `engine` должен быть не ниже той версии движка, в которой примитив появился. `formulaCopy` появился в движке 1.3.0, поэтому использующий его плагин объявляет `">=1.3.0"`. Более низкий диапазон приводит к ошибке `bun run catalog:build`, потому что старая версия Voyager сообщила бы о недостающем handler вместо предложения обновиться. Примитивы только дополняются: новый параметр всегда необязательный, а несовместимое изменение выходит под новым именем. Плагин с изменившимся поведением может нести однострочный `changelog`, который popup показывает рядом с версией. Перевод пишется в `i18n..changelog` рядом с `name` и `description`. ## Когда обычный плагин не подходит Если функции нужно выполнять JavaScript, перехватывать запросы, читать или записывать внутренние данные Voyager, либо полагаться на сложную runtime-логику, она не подходит для обычного декларативного плагина. Сначала откройте Issue и опишите потребность. Если действительно нужна встроенная возможность, мы можем рассмотреть реализацию в репозитории Voyager как builtin/native-плагин, например Formula Copy. ## Перед открытием PR Запустите `bun run plugin:check <каталог плагина>` и вставьте вывод в PR. Он сразу показывает все проблемы: манифест и CSS, выход `matches` за пределы сайта, примитивы, семантические ключи, переводы на десять языков и README; CI выполняет ту же проверку для всех встроенных плагинов. - Плагин выключен по умолчанию, пользователь включает его сам. - Вы проверили, что почти такого же плагина нет; если есть, сначала улучшили существующий. - Вы протестировали целевой сайт в светлой и темной теме. - `matches` не охватывает несвязанные сайты. - Нет ссылок на удаленные ресурсы. - Каталог плагина содержит `plugin.json`, необходимые CSS-файлы и короткий README. - Для официального плагина директория находится в `catalog/sites//plugins//`, его `matches` не выходят за `matches` сайта, и он указан в `catalog/marketplace.json` (кроме плагинов Gemini и AI Studio, которые поставляются вместе с Voyager). - PR описывает тестовые страницы, скриншоты или записи, а также затронутые области страницы. Держите плагин простым, сфокусированным и обратимым. Плагин, решающий одну ясную задачу, гораздо проще принять и поддерживать.