Регистрация на Rustvy по реферальной ссылке автора: https://rustvy.xyz/r/Z6LJC8FH При регистрации по этой ссылке вы получаете скидку 10% и бонусные токены. Подробные условия акции: https://rustvy.xyz/dashboard/referral
Rustvy (dsh-rustvy)
Русский | English
Значки производителей в списке моделей DeepSeek Harness, список моделей, который остаётся настолько же актуальным, насколько актуален сам провайдер, и несколько ключей провайдера в одном профиле — каждый чат отправляет запросы тем, который выбрал. Один плагин, одно зеркало, без перезапусков.
Что он делает
У плагина три задачи. Первые две работают от одного зеркала публичного каталога провайдера; третьей зеркало не нужно — ей нужен только маршрут профиля.
Значки. Один монохромный значок производителя перед каждой строкой модели в
списке выбора — нарисованный так же, как его рисует собственная панель провайдера:
CSS-маска, заливаемая currentColor, поэтому один файл на производителя
обслуживает обе темы оформления и все состояния строки — наведение, выбор,
отключение — без цветовых вариантов.
| Где | Что появляется |
|---|---|
| Меню моделей в композере | монохромный значок 16 px перед каждой строкой модели |
| Кнопка модели в композере | значок выбранной модели перед её названием (14 px) |
Всплывающее окно команды /model |
тот же значок на каждой строке |
Скины, перерисовывающие список (dsh-claude-style) |
тот же значок; собственная плашка скина в этой строке прячется, чтобы две плашки не встретились |
Список моделей. Профиль подключается к стороннему провайдеру через вручную
объявленный маршрут @deepseek-ai/dsh-llm-pi-ai, и именно список models этого
маршрута определяет, что вообще можно выбрать в интерфейсе: harness предлагает
модель только тогда, когда её отдаёт маршрут. Список, написанный вручную, устаревает
в тот день, когда провайдер выпускает новую модель — новая невидима, а снятая с
публикации остаётся выбираемой и падает уже на запросе. Плагин читает, что отдаёт
собственный эндпоинт маршрута, превращает это в точный формат записей адаптера,
показывает расхождение с установленным списком и по команде записывает его обратно
через ctx.configEditor.
Ключи. Несколько ключей провайдера в одном профиле и свой ключ для отдельного
чата — выбираемый в панели инструментов композера, сразу за списком режимов
доступа. Ключ добавляется в самом плагине, кнопкой «+»: ни .env, ни
cordis.patch.yml, ни страницу Models трогать не нужно, а выбор переживает
перезапуск. Учётная запись DeepSeek не затрагивается: подмена касается только
маршрутов этого плагина.
| Где | Что появляется |
|---|---|
| Настройки → Rustvy | раздел «API-ключи»: список ключей, кнопка «+», замена значения, переименование, проверка, назначение ключом по умолчанию, удаление |
| Композер, сразу за списком режимов доступа | компактный переключатель ключа этого чата, с меню и строкой «+ Добавить ключ» |
Раздела ключей в меню моделей и в окне /model нет: выбор ключа живёт в одном
месте. Меню моделей вместо этого не показывает те группы провайдеров, которые
оказались клонами маршрутов этого кольца, — иначе один и тот же список моделей
повторялся бы по разу на каждый ключ.
Зачем это нужно
Потому что обе поломки маршрута, который поддерживают вручную, — тихие. Модель, которую провайдер добавил, не появляется, и никто не говорит почему: ошибки нет, есть только отсутствие. Модель, которую провайдер снял, по-прежнему видна, и её выбор даёт сетевую ошибку, которая выглядит как проблема со связью, а не как устаревшая строка в YAML-файле.
И то и другое лечится чтением данных у провайдера вместо их переписывания. Список, который записывает этот плагин, несёт настоящие размер окна контекста, лимит вывода, входные модальности и уровни рассуждения, поэтому модель, получившая поддержку изображений или новый уровень мышления, описана верно в тот же день, а не наследует запасное значение, которое кто-то когда-то угадал.
А поскольку значки берутся из того же зеркала, совершенно новая модель приходит уже с кешированным значком своего производителя: зеркало перечитывает каталог в тот момент, когда эндпоинт называет модель, которой оно раньше не видело, — то есть ровно тогда, когда выходит релиз.
Третья задача — про другую тишину. Ключ провайдера — это отдельный лимит и отдельный учёт, а профиль один, поэтому «свой ключ на каждый чат» без плагина означает либо второй профиль, либо вторую строку в YAML, которую кто-то должен поддерживать вручную. Вручную поддержанный клон маршрута устаревает по тем же правилам, что и написанный вручную список моделей, — в день релиза. Кольцо ключей пишет клоны само и тем же проходом, которым записывается список моделей, поэтому маршрут ключа не отстаёт от маршрута профиля, а чат, отправляющий запросы другим ключом, видит ровно тот же выбор моделей, что и его сосед.
Ничего из этого не привязано к одному провайдеру. Сайт-источник, маршрут и строка
профиля — настройки; https://rustvy.xyz и rustvy являются значениями по
умолчанию.
Установка
dsh plugin --profile web add dsh-rustvy
Либо прямо с GitHub, если нужна самая свежая ревизия, а не опубликованная версия:
dsh plugin --profile web add github:koliy82/dsh-rustvy
Голый koliy82/dsh-rustvy без схемы не принимается: разбор источника установки
знает имя в реестре, git-спецификацию, tarball и абсолютный локальный путь, а
git-спецификации нужна схема либо сокращение github:.
Подтверждение сборки не запрашивается ни в том ни в другом случае: у пакета нет ни install-скрипта, ни зависимостей, поэтому pnpm нечего собирать.
Из локальной копии этого репозитория:
pwsh -File .\install.ps1
install.ps1 копирует пакет в <профиль>\node_modules\dsh-rustvy как настоящий
каталог и дописывает имя в dsh.profile.bundles манифеста профиля. Работающий
harness подхватывает строку по изменению манифеста.
После этого один раз перезапустите harness или обновите страницу: значки и страница настроек появляются на лету, а строка загрузчика читается на композиции.
Маршрут, в который пишется список моделей
Значки работают без всего остального. Половине со списком моделей нужен маршрут
@deepseek-ai/dsh-llm-pi-ai, в который можно писать. Добавьте в
cordis.patch.yml профиля — или отредактируйте в Настройки → Плагины →
Конфигурация плагинов:
- id: llm-pi-ai
name: "@deepseek-ai/dsh-llm-pi-ai"
config:
providers:
rustvy:
displayName: rustvy
apiKeyEnv: RUSTVY_API_KEY
api: openai-completions
baseURL: https://rustvy.xyz/v1
models:
- id: placeholder
name: placeholder
Список models обязан только разбираться: этот плагин его заменяет. Все остальные
поля при перегенерации переносятся без изменений, потому что переключатель
совместимости протокола или ссылка на учётные данные — это знание о вашем
развёртывании, которого нет ни в одном каталоге.
apiKeyEnv задаёт, откуда маршрут берёт ключ; он разрешается на каждый запрос через
механизм учётных данных harness — сначала унаследованное окружение процесса, затем
$DSH_HOME/.credentials.yaml, затем .env в каталоге запуска или в $DSH_HOME.
Сам ключ в конфигурационные файлы не попадает. Плагин при чтении списка моделей
подставляет ключ только тогда, когда окружение процесса уже содержит переменную с
таким именем; эндпоинт, под который он написан, отвечает и без ключа.
Чтобы сделать модель основной:
- id: agent-default-model
name: "@deepseek-ai/dsh-agent-default-model"
config:
provider: rustvy
model: claude-opus-5-5
reasoningEffort: high
Как пользоваться
Откройте Настройки → Rustvy. Страница показывает, что держит зеркало, что отдаёт провайдер и каково точное расхождение между ними. Кнопка «Обновить каталог и иконки» освежает обе половины, «Записать список моделей» записывает сгенерированный список. Без одного из этих нажатий не пишется ничего, если вы сами не попросили об этом в настройках.
Раздел «API-ключи» на той же странице держит кольцо: кнопка «+» открывает инлайн-форму «название + значение», у каждой строки ключа есть замена значения, переименование, назначение ключом по умолчанию, проверка и удаление. Ключом по умолчанию можно пользоваться, вообще не заходя в настройки: переключатель в панели инструментов композера делает то же самое для того чата, который открыт.
Настройки
Настройки → Rustvy
Значки:
| Параметр | По умолчанию | Смысл |
|---|---|---|
| Показывать иконки моделей | вкл | общий выключатель значков; в выключенном виде убирает все значки и не трогает список моделей |
| Иконка в кнопке выбранной модели | вкл | значок и в кнопке модели композера, а не только в строках меню |
| Прятать логотипы других скинов | вкл | прятать собственную плашку скина в строке, где значок нарисован этим плагином |
| Размер значка, px | 16 | от 10 до 32 |
Список моделей:
| Параметр | По умолчанию | Смысл |
|---|---|---|
| Держать список моделей маршрута актуальным | вкл | общий выключатель; в выключенном виде YAML маршрута не трогается |
| Записывать список | при нажатии «Обновить» | только отдельной кнопкой — самостоятельно не пишет никогда · при нажатии «Обновить» — пишет по кнопке обновления · сам, при каждом обновлении — пишет и при фоновом суточном обновлении |
| Название модели | как на сайте провайдера | отображаемое имя провайдера либо сырой идентификатор |
| Предлагать уровень «без рассуждений» | вкл | добавлять уровень off рассуждающей модели, которая его не объявила |
| Оставлять модели, которые провайдер больше не отдаёт | выкл | сохранять установленную модель, которую эндпоинт больше не отдаёт, — на случай временно ошибочного списка, а не действительно снятой модели |
| Маршрут провайдера | rustvy |
какой ключ внутри providers записывать |
| Строка профиля | llm-pi-ai |
какая строка загрузчика его содержит |
| Сайт провайдера | https://rustvy.xyz |
откуда читать каталог и логотипы |
Режим записи по умолчанию намеренно средний: нажатие кнопки — это решение человека, а фоновое истечение срока — нет, и переписывать конфигурацию загрузчика работающего профиля, когда никто не просил, — это то, на что стоит спросить разрешение.
Каждый адрес и файл, с которым работает плагин, виден на странице: сайт, страница
каталога и эндпоинт списка моделей — ссылками, открывающимися в новой вкладке, а
пути к зеркалу, настройкам, копии для отката и патчу профиля — текстом с кнопкой
копирования. Кнопка «Показать блок для ручной правки» выдаёт ровно тот YAML,
который получился бы при автоматической записи, так что желающий вставить его в
cordis.patch.yml сам не переписывает краткий пересказ.
У одного поля нет элемента управления, потому что оно принадлежит YAML профиля, а не
форме: modelZones (по умолчанию ["standard"]) — зоны каталога, которые
допускаются, когда эндпоинт недоступен и состав списка определяет зеркало.
Ключи:
| Параметр | По умолчанию | Смысл |
|---|---|---|
| Ключи на несколько чатов | вкл | общий выключатель кольца; в выключенном виде подмена маршрута не применяется и каждый запрос идёт маршрутом профиля |
Выключение ничего не удаляет: маршруты ключей остаются в конфигурации профиля, значения — в хранилище учётных данных, выбор чатов — в своём файле, поэтому включённая обратно возможность продолжает с того места, на котором её выключили. Переключатель живёт в разделе «API-ключи», а не рядом с выключателями значков и списка моделей: он принадлежит кольцу, и там же страница объясняет, почему добавление ключа недоступно, если хост не отдаёт нужный сервис.
Настройки хранятся в <профиль>\.dsh-rustvy\config.json. Две переменные окружения
перекрывают их — для развёртывания, которое направляет зеркало в другое место или
хочет смонтировать плагин неработающим: DSH_RUSTVY_ORIGIN и
DSH_RUSTVY_DISABLED=1.
Откуда берутся картинки
В сам пакет ничего не вшито. Хост-половина зеркалит два публичных объекта с сайта провайдера в профиль, а браузерная половина читает их с локального адреса:
- каталог моделей — сайт рисует свою таблицу моделей из JSON-острова в
собственной странице (
<script id="rustvy-catalog">), поэтому неаутентифицированный читатель получает ровно те данные, которые видит панель. Зеркалится в<профиль>\.dsh-rustvy\catalog.json. - файлы логотипов (
/logos/<имя>.svg) — копируются в<профиль>\.dsh-rustvy\logos\при первом обращении.
Оба читаются по собственному исходящему маршруту процесса (lib/net.js): сначала
живой глобальный диспетчер, чтобы прокси, установленный dsh-proxy-gui или любым
другим маршрутом процесса, соблюдался, затем соединитель, построенный здесь из
HTTPS_PROXY / ALL_PROXY / HTTP_PROXY, — на случай, когда диспетчер не
установлен. NO_PROXY учитывается в обоих случаях. Сама страница наружу не выходит:
браузер спрашивает значок у 127.0.0.1, и именно поэтому всё работает на машине,
чей браузер до источника не достаёт вовсе.
Каталог обновляется раз в 24 часа и по требованию; холодное зеркало ожидается до 6 секунд, а устаревшее обновляется в фоне, пока строки продолжают показывать имеющиеся значки. Когда источник недоступен, кеш на диске по-прежнему отдаёт всё, что в нём есть.
Откуда берётся список моделей
Два источника в таком порядке, потому что отвечают они на разные вопросы:
- собственный эндпоинт маршрута (
GET {baseURL}/models) — авторитетный список состава: ровно те модели, которые можно назвать в запросе, каждая со своимиcontext_k,max_output_tokens,modalitiesиreasoning_levels. Это тот же эндпоинт, который опрашивает собственное обнаружение адаптера. - зеркало острова каталога — названия и запасной вариант. Это единственный
источник, несущий отображаемое имя, производителя и имя файла логотипа, поэтому
он соединяется с отдаваемым списком по идентификатору. Когда эндпоинт не
отвечает, состав определяет сам остров по своим полям
enabledиzone, так что маршрут остаётся актуальным и во время сбоя, а не застывает на последнем удачном чтении. На здоровом источнике оба списка совпадают точно.
Модель, которую эндпоинт отдаёт, а зеркало ни разу не видело, — признак того, что остров отстал от боевого API, а это происходит ровно в момент релиза, поэтому тогда остров перечитывается; ожидание ограничено тем же коротким сроком, что и для холодного зеркала.
Что именно записывается
Каждая сгенерированная запись несёт только те поля, которые объявлены в схеме
конфигурации адаптера, потому что список models профиля маршрута заменяет
каталог маршрута, а неизвестное поле в лучшем случае будет отброшено:
| Поле | Откуда берётся |
|---|---|
id |
идентификатор отдаваемой модели — то, что нужно назвать в запросе |
name |
отображаемое имя из острова либо идентификатор, если имени нет |
contextWindow |
context_k × 1000 |
maxTokens |
max_output_tokens |
input |
modalities, отфильтрованные до text/image |
reasoningEfforts |
reasoning_levels, отображённые на уровни адаптера, либо false для нерассуждающей модели |
compat |
не генерируется никогда — существующее всегда переносится |
Поле, о котором никто не высказался, пропускается, а не заполняется значением по
умолчанию, чтобы запасное значение самого адаптера оставалось в силе и написанное
вручную не закреплялось молча. compat — единственное, что переносится без
изменений: переключатель совместимости протокола это знание о шлюзе, которого нет
ни в одном каталоге.
Уровни рассуждения отображаются на словарь адаптера (off, minimal, low,
medium, high, xhigh, max); неизвестный ему уровень отбрасывается и сообщается
примечанием, а не отказом всей записи. Необъявленные уровни адаптер не
поддерживает, а объявленный off без боевого значения означает «предлагать, ничего
не отправляя» — поэтому этот переключатель включён по умолчанию.
Что запись отклоняет
Пустой список не записывается никогда: единственный способ его получить — неудачное
чтение, а маршрут без моделей не обслуживает ничего. Актуальный список тоже не
записывается, потому что configEditor.edit() согласует весь загрузчик, и тратить
одно согласование на отсутствие изменений означало бы перезагрузить работающий
профиль впустую. Актуальным при этом считается не только маршрут профиля: запись
происходит и тогда, когда сам он в порядке, а список моделей одного из клонов
кольца ключей отстал, — иначе чат, отправляющий запросы другим ключом, видел бы
меньше моделей, чем его сосед. Каждая запись сохраняет предыдущий документ в
<профиль>\.dsh-rustvy\models-backup.json, а кнопка «Откатить к предыдущему»
возвращает его обратно; состояние, которое откат вытесняет, само становится
следующей копией, поэтому откат тоже можно откатить.
Как строка сопоставляется со значком
Строки сопоставляются по видимому названию в таком порядке:
- идентификатор модели из зеркального каталога (
claude-opus-5-5,abyss/glm-5.3, …); - её отображаемое имя (
OPUS 5.5); - идентификатор, с которого название начинается, — скин может оставить в том же
элементе собственную строку описания, поэтому название часто является префиксом,
а не всей строкой; побеждает самый длинный подходящий идентификатор, чтобы
claude-opus-5-5никогда не досталсяclaude-opus-5; - форма идентификатора — для модели, которой зеркало ни разу не видело:
claude-*→ Anthropic,gpt-*/o1…o9→ OpenAI,glm-*→ Zhipu,kimi-*/k3→ Moonshot,qwen-*→ Alibaba Qwen,deepseek-*→ DeepSeek,ahmia-*→ Ahmia,mimo-*→ Xiaomi,fable-*→ Anthropic,gemini-*→ Google,grok-*→ xAI.
Модель, не подошедшая ни под что, не получает значка вовсе: выдуманная иконка была бы неправильным ответом, а не отсутствующим. Модель, которая подошла, но чей логотип зеркало получить не смогло, получает вместо пустого квадрата плашку с монограммой — первыми буквами производителя: маска с неработающим URL закрасилась бы сплошным квадратом.
Строки уровней усилия, заголовки производителей, корневые ячейки меню, поле поиска и строки о сбое каталога не подходят ни под одно правило и остаются без значка.
Кольцо API-ключей
Несколько ключей провайдера в одном профиле, и каждый чат отправляет запросы тем, который выбрал его пользователь. Выбор живёт в панели инструментов композера — рядом со списком режимов доступа, а не в меню моделей, — действует на один чат и переживает перезапуск. Учётная запись DeepSeek не затрагивается ни в одном из мест этого раздела.
Почему один ключ — это один маршрут
@deepseek-ai/dsh-llm-pi-ai берёт ключ из profile.apiKeyEnv — это имя, а не
значение, — и разрешает его через механизм учётных данных harness на каждый запрос,
ничего не кешируя. Поля для ключа нет ни в конфигурации вызова (LlmCallConfig), ни
в параметрах генерации (GenerateOptions), а baseURL и заголовки запекаются в
снапшот профиля. Передать адаптеру ключ «в этом запросе» нечем, поэтому единственный
честный способ дать чату другой ключ — другой маршрут: клон с теми же api,
baseURL и models и своим apiKeyEnv.
Из этого следует всё устройство. Добавление ключа пишет значение в хранилище учётных
данных под новым именем, клонирует маршрут профиля через ctx.configEditor и
запоминает запись в кольце. Выбор чата применяется на водопаде agent/request
подменой имени маршрута в конфигурации вызова — тем же приёмом и на том же шве,
которым harness применяет выбор модели для отдельного чата, поэтому он работает
ровно там, где уже работает выбор модели, и не заводит собственного пути к запросу.
| Ключ | Имя учётных данных | Маршрут | Отображаемое имя маршрута |
|---|---|---|---|
| первый (основной) | RUSTVY_API_KEY — как объявлен в профиле |
rustvy — как объявлен в профиле |
rustvy |
| второй | RUSTVY_API_KEY_2 |
rustvy-k2 |
rustvy · <название ключа> |
| n-й | RUSTVY_API_KEY_<n> |
rustvy-k<n> |
rustvy · <название ключа> |
Имена строятся от маршрута из настроек, а не зашиты: при modelRoute: rustvy второй
ключ получает rustvy-k2, при другом имени маршрута — свои суффиксы. Клон копирует
профиль маршрута целиком, меняя только apiKeyEnv и отображаемое имя, поэтому
написанный вручную переключатель совместимости протокола и всё остальное знание о
своём шлюзе переходит в каждый ключ, а не теряется при первом же добавлении.
Предел — 12 ключей: это кольцо, а не реестр. Новый ключ берёт первый порядковый номер, у которого свободны и имя учётных данных, и маршрут, поэтому имя удалённого ключа не переиспользуется, а чужая строка, случайно оказавшаяся на таком имени, не перезаписывается.
Основной ключ
Первый ключ кольца — не запись, которую кто-то добавил, а маршрут, уже объявленный в
профиле: он наследует его apiKeyEnv и его имя и основным остаётся всегда. Профиль,
который работал до этой возможности, продолжает работать без изменений — кольцо с
одним ключом не переименовывает ничего и никуда не пишет. Удалить основной ключ
нельзя: он принадлежит маршруту профиля, и его удаление сломало бы провайдера для
каждого чата, включая те, которые кольцом ни разу не воспользовались. Отказ так и
объясняет и предлагает заменить значение — эта операция доступна любому ключу и
конфигурации не касается, потому что имя учётных данных при ней не меняется.
Где что лежит
| Что | Где |
|---|---|
| Значения ключей | $DSH_HOME\.credentials.yaml, раздел refs — то же хранилище, в которое пишет страница Models harness и из которого читает адаптер |
| Метаданные кольца | <профиль>\.dsh-rustvy\keys.json — id, название, имя учётных данных, маршрут, признак основного, порядковый номер, дата создания и defaultKeyId |
| Выбор чатов | <профиль>\.dsh-rustvy\session-keys.json — sessionId → keyId |
В файлах плагина значения ключа нет никогда: ни в keys.json, ни в
session-keys.json, ни в настройках, ни в копии профиля для отката. Loopback-маршруты
отвечают маской — первые три и последние два символа — и восьмисимвольным отпечатком
SHA-256; этого достаточно, чтобы узнать ключ в списке и заметить две записи с одним
значением, и недостаточно, чтобы им воспользоваться. Само значение приходит в теле
единственного запроса, который его добавляет или заменяет, и никогда в строке
запроса: URL пишется в журналы, попадает в кеш и в историю браузера, а секрет — не
дело ни того, ни другого, ни третьего.
Выбор хранится на диске, а не в журнале сессии: это предпочтение маршрутизации самого плагина, и плагин, пишущий собственные события в долговременный журнал, оставил бы их в каждой сессии, где когда-либо был смонтирован. В файле лежат только отклонения от ключа по умолчанию, поэтому чат, созданный до смены ключа по умолчанию, следует за новым, а не держится старого.
Что видно в интерфейсе
Две поверхности: одна управляет кольцом, другая выбирает ключ для своего чата. Переключатель появляется только тогда, когда выбирать есть из чего: при одном ключе его нет вовсе, потому что один ключ — это не выбор.
Настройки → Rustvy → «API-ключи». Список ключей: название, маска, имя учётных данных, маршрут, сколько моделей отдаёт этот маршрут и сколько чатов выбрали этот ключ. Кнопка «+» в заголовке раздела открывает инлайн-форму «название + значение» со скрытым полем и показом по желанию. У каждой строки — замена значения, переименование (сохраняется по потере фокуса), назначение ключом по умолчанию, проверка и удаление в два шага; у основного ключа кнопки удаления нет, и строка объясняет почему. Ниже — блок «Маршруты ключей»: строка профиля и пакет, адрес списка, счётчик «запросов через подмену», число чатов со своим выбором, списки отсутствующих и отставших маршрутов, пути к обоим файлам с кнопкой копирования и кнопка «Починить маршруты», которая появляется только тогда, когда маршрут какого-то ключа действительно пропал.
Композер. Компактная кнопка в панели инструментов, сразу за списком режимов
доступа, — регистрация в слоте conversation.input.left: название активного ключа
либо «ключ по умолчанию», если у чата нет собственного выбора. В меню — по строке на
ключ с галочкой на активном, строка «По умолчанию» и строка «+ Добавить ключ» с
той же инлайн-формой; ключ, добавленный из чата, сразу выбирается для этого чата,
потому что добавляют его из разговора именно затем, чтобы отправлять запросы им.
Кнопка не рисуется, пока слот не передал sessionId: выбор принадлежит чату, и
переключатель, который сохранит его не тому чату, хуже отсутствия переключателя.
Меню выбора модели и всплывающее окно /model. Раздела ключей здесь нет — ни
строк, ни заголовка: выбор живёт в композере, и второй такой же список в меню только
удваивал бы место, где можно ошибиться. Декоратор делает здесь обратное — прячет
группы провайдеров, которые хост назвал в hiddenGroups, то есть клоны маршрутов
этого кольца, чтобы один и тот же список моделей не показывался по разу на каждый
ключ. Не объявить их хост не может: клону нужен собственный список моделей, иначе
после подмены маршрута запрос не найдёт в нём свою модель. Скрытие структурное и
осторожное: заголовок группы опознаётся по форме, а не по имени класса, потому что
классы хозяйских примитивов этому плагину не видны и между версиями не стабильны, а
прячется либо наименьший содержащий группу элемент, либо заголовок вместе с
идущими за ним строками до следующего заголовка. Если уверенности нет, не прячется
ничего: по ошибке спрятанная строка модели — это отнятый выбор, а лишний раз
показанная группа — только повтор.
Что происходит при добавлении ключа
Порядок операций намеренный. Сначала значение пишется в хранилище учётных данных: маршрут, чьё имя ни во что не разрешается, роняет каждый запрос, который его назвал. Затем идёт запись конфигурации, и если она не удалась, значение забирается обратно, поэтому неудачное добавление не оставляет после себя ничего. Документ кольца пишется последним, когда существуют и значение, и маршрут.
Запись конфигурации — настоящая и поддерживаемая: она идёт через ctx.configEditor,
который согласует профиль, и ровно то же самое делает страница Models harness, когда
в профиль добавляют провайдер. configEditor.edit() согласует весь загрузчик,
поэтому записи кольца выстроены в очередь: два ключа, добавленные одновременно, —
это две последовательные правки, а не две правки против документа, который одна
другая только что заменила. И когда карта маршрутов уже соответствует кольцу,
запись не происходит вовсе — тратить согласование на отсутствие изменений этот
плагин отказывается и в списке моделей.
Ключ можно проверить, не отправляя им запрос: проверка делает GET {baseURL}/models
с этим ключом и сообщает статус, число отданных моделей и текст ошибки провайдера как
он пришёл.
Насколько узка подмена
Подмена меняет ровно одно поле конфигурации вызова — имя маршрута — и только когда выполнено всё перечисленное:
- возможность включена настройкой
apiKeys; - запрос пришёл с идентификатором сессии — без него выбирать не для кого;
- в кольце два ключа или больше;
- ключ этого чата — его собственный выбор, а при его отсутствии ключ по умолчанию — не основной;
- запрос и без того шёл одним из маршрутов этого кольца (
rustvy,rustvy-k*); - целевой маршрут установлен в профиле;
- целевой маршрут отдаёт запрашиваемую модель.
Во всех остальных случаях конфигурация возвращается как пришла. deepseek-account и
любой другой провайдер — второй шлюз, соседняя строка, чужая запись с ключом — не
переименовываются никогда: условие пятое проверяет принадлежность маршрута кольцу, а
не его похожесть.
Клон, который пропал или не отдаёт нужную модель, не роняет шаг: запрос уходит
маршрутом из профиля, а причина записывается. Она видна в поле fallback на
GET /keys — страница настроек рисует её предупреждением над списком ключей — и
строкой в журнале, которая не повторяется, пока причина не изменится. Логика здесь
такая: неработающий маршрут ключа — это проблема конфигурации, и правильный ответ на
неё — состоявшийся запрос плюс видимая причина, а не упавший шаг, о котором не
сказано почему. Исключение внутри самой подмены перехватывается, и запрос идёт как
шёл.
Список моделей пишется одним проходом на все маршруты кольца — базовый и каждый
клон, который кольцо видит в профиле, — поэтому клон не может отстать от базового
маршрута, а блок для ручной правки (/models.yaml) содержит клоны вместе с их
именами учётных данных, чтобы желающий вставить строку в cordis.patch.yml сам не
реконструировал их по описанию.
Два счётчика на GET /keys отвечают на вопрос, который иначе выглядел бы как
«ключи не работают»: routing.seen — сколько запросов harness вообще провёл через
водопад agent/request, routing.applied — сколько из них были переименованы на
маршрут ключа. Ноль при живом трафике означает, что водопад не доходит до плагина,
и это видно сразу.
Маршруты
Одна регистрация префикса на собственном веб-сервере harness. Маршруты плагинов не
видны фильтру запросов хоста, поэтому каждый из них проверяет собеседника сам:
только локальный адрес, только локальный Host, только same-origin
Sec-Fetch-Site и только совпадающий Origin.
| Маршрут | Метод | Ответ |
|---|---|---|
/dsh-rustvy/catalog.json |
GET | зеркальный каталог плюс настройки, с которыми браузерная половина рисует |
/dsh-rustvy/logo/<имя>.svg |
GET, HEAD | один кешированный логотип, при первом запросе забираемый с источника; 404, когда такого значка у источника нет, 400 для любого имени, не совпадающего с [A-Za-z0-9._-]+.svg |
/dsh-rustvy/models.json |
GET | сгенерированный список моделей и его расхождение с установленным; ?cached=1 отдаёт последний вычисленный план вместо повторного чтения эндпоинта |
/dsh-rustvy/models.yaml |
GET, HEAD | тот же список в виде готовой к вставке строки cordis.patch.yml; ?cached=1 как выше |
/dsh-rustvy/state |
GET | состояние зеркала: версия, число моделей, кешированные логотипы, маршрут, прокси (учётные данные маскируются), все пути к файлам, последняя ошибка, последнее вычисленное расхождение и сводка кольца ключей. Собственных запросов не делает; сводка кольца синхронная и без чтения учётных данных, поэтому масок в ней нет — только счётчики и маршруты |
/dsh-rustvy/refresh |
POST | перечитать каталог, заново прогреть все названные им логотипы и сообщить — либо, когда режим записи позволяет, применить — список моделей |
/dsh-rustvy/apply-models |
POST | записать сгенерированный список в профиль маршрута через ctx.configEditor |
/dsh-rustvy/revert-models |
POST | вернуть сохранённый профиль маршрута |
/dsh-rustvy/config |
POST | записать настройки; сети не касается |
/dsh-rustvy/keys |
GET, POST | GET: кольцо целиком — ключи с маской и отпечатком, ключ по умолчанию, скрытые группы провайдеров, отсутствующие и отставшие маршруты, счётчики подмены, последний отказ, пути к обоим файлам, доступность credentials и configEditor. POST: одно действие над кольцом (add, set, rename, default, remove, repair, test); успешный ответ — свежий GET плюс поле результата, поэтому один POST перерисовывает всё. Отказ — 409 с { ok: false, error }, неизвестное действие — 400. Значение ключа принимается только в теле запроса |
/dsh-rustvy/session-key |
GET, POST | GET с ?sessionId=: каким ключом этот чат отправляет запросы (keyId), есть ли у него собственный выбор (picked), и целиком тот же ответ, что у GET /keys; без идентификатора — 400. POST { sessionId, keyId } записывает выбор и отвечает тем же payload; keyId: null возвращает чат к ключу по умолчанию |
Без перезапусков
Хост-половина перезагружает себя сама, тем же способом, что и dsh-proxy-gui:
строка загрузчика называет этот пакет, чья точка входа импортирует реализацию через
запрос со сбрасывающим кеш параметром ?rev=, а опрос каталога lib раз в секунду
останавливает предыдущую редакцию и запускает следующую, как только изменившийся
отпечаток повторится. Поэтому lib/host.js, lib/models.js, lib/keys.js и
lib/net.js перезагружаются на лету, а перезапуск нужен только при правке самой
точки входа.
Браузерная половина приходит в открытую страницу через собственную живую композицию
плагинов harness; обновление страницы тоже работает.
Название, описание и значок в меню плагинов читаются из пакета на композиции,
поэтому изменение locale/*.json или assets/icon.svg требует перезапуска.
Требования и деградация
Проверено на dsh 0.2.1-rc.2. Пакет не объявляет ни engines.dsh, ни
peer-зависимостей, потому что не импортирует ни одного модуля @deepseek-ai/* — у
него ноль рантайм-зависимостей, а непроверенное требование к версии хоста дало бы в
маркете лишь ложные предупреждения о несовместимости.
Каждая возможность деградирует самостоятельно:
| Чего нет | Что происходит |
|---|---|
configEditor |
расхождение по-прежнему вычисляется и показывается; кнопки записи и отката объясняют, что хост не отдаёт редактор |
маршрут dsh-llm-pi-ai |
значки работают; половина со списком сообщает, что маршрут не найден |
| источник | кеш на диске отдаёт каждый значок и каждую модель, которые в нём есть |
| эндпоинт при живом каталоге | состав определяет остров, и страница об этом говорит |
credentials |
значение ключа некуда записать: кольцо остаётся с одним основным ключом, добавление недоступно, и раздел «API-ключи» называет причину вместо того, чтобы показать форму, которая откажет |
configEditor — для кольца |
значение существующего ключа заменить можно, маршрут для нового добавить нельзя: форма добавления выключена, и раздел объясняет почему |
маршрут ключа, потерянный после ручной правки cordis.patch.yml или отката списка моделей |
ключ остаётся в кольце, его маршрут числится в routes.missing, запросы этого чата идут маршрутом профиля с видимой причиной, а кнопка «Починить маршруты» ({ action: 'repair' }) записывает маршруты кольца заново |
Структура
| Файл | Роль |
|---|---|
lib/index.js |
стабильная точка входа загрузчика; владеет циклом перезагрузки |
lib/host.js |
маршруты, зеркало каталога, кеш логотипов, синхронизация моделей, кольцо ключей и подмена маршрута на agent/request, настройки |
lib/models.js |
чистое отображение: что публикует источник → одна запись профиля маршрута, расхождение двух списков и YAML для ручной правки |
lib/keys.js |
чистая арифметика кольца: названия, имена учётных данных и маршрутов, документы кольца и выбора чатов, сверка карты маршрутов с кольцом |
lib/net.js |
один исходящий GET по маршруту процесса, затем по прокси из окружения |
client/client.js |
браузерный набор, написанный вручную: декоратор, страница настроек, переключатель ключа в композере и скрытие групп-клонов в меню моделей |
locale/en.json, ru.json, zh.json |
название и описание для меню плагинов |
assets/icon.svg |
значок для меню плагинов |
cordis.patch.yml |
строка загрузчика, которую подтягивает список bundles профиля |
install.ps1 |
установка в профиль |
scripts/prepublish-check.mjs |
гейт публикации: всё, что может установиться и при этом быть молча неправильным |
tests/ |
проверки отображения, эмиттера YAML, кольца ключей, хост- и браузерной половин |
lib/models.js и lib/keys.js намеренно чистые — без сети, файловой системы и
Cordis, — поэтому отображение можно проверить на реальном ответе источника, а
арифметику кольца на выдуманном профиле, ничего не монтируя. lib/host.js владеет
всем, у чего есть время жизни, и именно его заменяет перезагрузка.
Тесты
node tests\models-check.mjs # чистое отображение: живой источник -> записи, расхождение, аудит схемы
node tests\yaml-roundtrip-test.mjs # эмиттер против двух настоящих YAML-парсеров
node tests\model-sync-host-test.mjs # хост: подставные webServer и configEditor, настоящий запрос к источнику
node tests\api-keys-host-test.mjs # кольцо ключей: подставные credentials, configEditor и шина событий, без сети
node tests\model-sync-client-test.mjs # браузер: мини-React и подставной DOM, фиктивный /state
node scripts\prepublish-check.mjs # гейт публикации
models-check.mjs читает эндпоинт и остров каталога источника, генерирует список и
проверяет каждую запись против схемы адаптера: только объявленные поля,
положительные целые размеры, известные модальности и словарь рассуждения, который
предлагает уровень помимо off, но даёт боевое значение каждому уровню, кроме
off. Он же проверяет, что запасной путь по острову выбирает ровно те модели,
которые отдаёт эндпоинт, — именно это делает запасной путь безопасным для записи.
yaml-roundtrip-test.mjs разбирает настоящую строку cordis.patch.yml профиля (или
синтетическую той же формы, если профиля на машине нет), заново выдаёт её эмиттером
этого пакета и проверяет, что два независимых парсера — js-yaml и yaml — читают
её обратно одинаково, что все поля подключения сохраняются, что подстановка строки
оставляет соседей байт в байт и что расстановка кавычек переживает обратный ход:
зарезервированные слова, числа, ведущие индикаторы, концевой пробел и кавычки
внутри строки.
Хост-тест прогоняет зарегистрированные маршруты на подставных парах запрос/ответ и
подставном configEditor, наполненном маршрутом в том виде, в каком он есть у
профиля, и проверяет: план читает эндпоинт; модель, которой зеркало не видело, всё
равно получает настоящее отображаемое имя, а не идентификатор; написанный вручную
compat переживает перегенерацию и не изобретается для другой модели;
/models.yaml выдаёт строку патча, сохраняющую поля подключения и отбрасывающую
заменяемый список; запись сохраняет displayName, apiKeyEnv, api и baseURL и
заменяет только models; копия ложится на диск; повторное применение ничего не
делает и конфигурацию не трогает; /state перестаёт сообщать расхождение, которое
запись только что устранила; откат возвращает снятые модели и их compat;
нелокальный собеседник получает 403, а неверный метод — 405.
api-keys-host-test.mjs — 84 проверки кольца ключей на той же host-половине, с
подставным хранилищем учётных данных, подставным configEditor и подставной шиной
событий, без сети вовсе: эндпоинт маршрута смотрит на закрытый порт, поэтому и
чтение списка моделей, и проверка ключа детерминированно заканчиваются отказом, а
состав списка определяет зеркальный остров. Проверяется подмена: выбранный ключ
переименовывает маршрут, а модель, уровень рассуждения и лимит вывода переживают её
без изменений; чат без собственного выбора идёт маршрутом профиля; deepseek-account
и соседний маршрут с чужим ключом не переименовываются никогда; запрос без сессии
остаётся как пришёл; модель, которой клон не отдаёт, оставляет запрос на базовом
маршруте и попадает в fallback; чат без выбора следует за ключом по умолчанию, а
keyId: null возвращает его обратно. Проверяются отказы: пустой ключ, ключ с
пробелом, неизвестное действие (400), удаление основного ключа (409).
Проверяется удаление ключа: маршрут клона исчезает, сосед и базовый маршрут
выживают, значение стирается из хранилища, выбор чатов с этого ключа сбрасывается, а
сам чат продолжает ходить базовым маршрутом. Проверяются записи и счётчики: одно
добавление — одна правка конфигурации, замена значения не правит её вовсе, запись
списка моделей накрывает клон и сохраняет его собственное имя учётных данных, блок
для ручной правки содержит клон, repair возвращает потерянный маршрут, в
keys.json нет секрета, выгрузка снимает и маршрут, и слушателя, а с выключенной
возможностью не переименовывается ничего.
Браузерный тест рисует страницу настроек мини-реактом, чьи ячейки хуков привязаны к экземпляру компонента, и проверяет: раздел называется Rustvy; три внешних адреса — настоящие ссылки, открывающиеся вне приложения; каждый путь к файлу показан с кнопкой копирования; маршрут, строка профиля, сайт и размер значка редактируются и сохраняются по потере фокуса; оба выпадающих списка сохраняются по изменению; панель ручной правки читает и показывает блок; расхождение называет новые, снятые и изменённые модели; кнопка записи включена при наличии расхождения, выключена при актуальном списке и выключена, когда хост не отдаёт редактор; откат остаётся выключенным, пока копии нет; каждое изменение уходит на нужный маршрут и сообщает, что именно сдвинулось.
Тот же тест рисует и кольцо ключей: раздел «API-ключи» со списком, масками,
переименованием по потере фокуса, инлайн-формой «+», проверкой, назначением
ключом по умолчанию и удалением в два шага; объяснения на каждый отказ — нет
credentials, host-половина устарела, возможность выключена — вместе с
возвращением способности, когда причина устранена; переключатель «Ключи на
несколько чатов», который читает ответ хоста и пишет своё поле в /config; блок
«Маршруты ключей» со счётчиками подмены, числом чатов со своим выбором, путями к
обоим файлам, отсутствующим и отставшим маршрутами и кнопкой «Починить
маршруты»; предел в 12 ключей; переключатель композера, который не рисуется без
sessionId и при одном ключе, читает выбор своего чата, публикует его в
/session-key и предлагает добавить ключ; меню выбора моделей и окно /model,
которые не получают ни раздела ключей, ни его строк; стоимость обращений к хосту —
два взаимодействия подряд стоят одного чтения, открытие переключателя читает
всегда, а выбор обходится одним POST без дочитывания; и выгрузка, после которой в
меню не остаётся ничего своего. Отдельно проверяется, что значение ключа не
попадает ни в один URL и не остаётся на странице после добавления.
Половина, рисующая значки, автоматическим набором в этом репозитории не
покрыта — и это стоит знать заранее. Она работает с DOM уже открытой страницы,
поэтому проверяется глазами: какие строки получают значок и какой именно, что
строки уровней усилия, заголовки производителей и поле поиска остаются пустыми, что
вместо неполученного логотипа появляется плашка с монограммой, что добавленная
позже строка тоже получает значок, что повторный проход ничего не удваивает и что
выгрузка убирает добавленное. model-sync-client-test.mjs покрывает страницу
настроек и переключатель ключа в композере, но не рисование значков; скрытие
групп-клонов в меню — та же работа с DOM уже открытой страницы, и набор проверяет
там только то, что меню не получает ни раздела ключей, ни его строк.
Лицензия
MIT — см. LICENSE.
No comments yet. Be the first to write one.