- Início
- Plugins
- Modelos e provedores
- dsh-rustvy
dsh-rustvy
koliy82/dsh-rustvy
Vendor marks in the model picker plus a model list kept in step with the provider: mirrors the public catalog and logos to loopback, and regenerates the pi-ai route's models from that route's own endpoint, showing the drift before anything is written.
Instalar
dsh plugin --profile web add github:koliy82/dsh-rustvyREADME
Регистрация на 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
Сверху вниз страница устроена так: выключатели и поля, затем блок «Аптайм моделей», «Модели провайдера», «Ключи на несколько чатов», «Сбор ошибок Rustvy» и в самом низу «Зеркало каталога» с его адресами и файлами. Аптайм первый, потому что это единственный блок, отвечающий на вопрос, с которым страницу открывают, — жива ли модель, которую сейчас выберут; зеркало последнее, рядом с предупреждением о неудачном чтении каталога, которого только оно и касается.
Значки:
| Параметр | По умолчанию | Смысл |
|---|---|---|
| Показывать иконки моделей | вкл | общий выключатель значков; в выключенном виде убирает все значки и не трогает список моделей |
| Иконка в кнопке выбранной модели | вкл | значок и в кнопке модели композера, а не только в строках меню; он же убирает с кнопки значок аптайма |
| Прятать логотипы других скинов | вкл | прятать собственную плашку скина в строке, где значок нарисован этим плагином |
| Показывать аптайм моделей | вкл | блок «Аптайм моделей» на этой странице и значок состояния в списке моделей и в кнопке выбранной модели; выключенный не читает зеркало коллектора вовсе |
| Размер значка, px | 16 | от 10 до 32 |
Список моделей:
| Параметр | По умолчанию | Смысл |
|---|---|---|
| Держать список моделей маршрута актуальным | вкл | общий выключатель; в выключенном виде YAML маршрута не трогается |
| Записывать список | при нажатии «Обновить» | только отдельной кнопкой — самостоятельно не пишет никогда · при нажатии «Обновить» — пишет по кнопке обновления · сам, при каждом обновлении — пишет и при фоновом суточном обновлении |
| Название модели | как на сайте провайдера | отображаемое имя провайдера либо сырой идентификатор |
| Предлагать уровень «без рассуждений» | вкл | добавлять уровень off рассуждающей модели, которая его не объявила |
| Оставлять модели, которые провайдер больше не отдаёт | выкл | сохранять установленную модель, которую эндпоинт больше не отдаёт, — на случай временно ошибочного списка, а не действительно снятой модели |
| Маршрут провайдера | rustvy | какой ключ внутри providers записывать |
| Строка профиля | llm-pi-ai | какая строка загрузчика его содержит |
| Сайт провайдера | https://rustvy.xyz | откуда читать каталог и логотипы |
Режим записи по умолчанию намеренно средний: нажатие кнопки — это решение человека, а фоновое истечение срока — нет, и переписывать конфигурацию загрузчика работающего профиля, когда никто не просил, — это то, на что стоит спросить разрешение.
Каждый адрес и файл, с которым работает плагин, виден на странице: сайт, страница
каталога и эндпоинт списка моделей — ссылками, открывающимися в новой вкладке, а
пути к зеркалу, настройкам, копии для отката и патчу профиля — текстом с кнопкой
копирования. Кнопка «Показать блок для ручной правки» выдаёт ровно тот YAML,
который получился бы при автоматической записи, так что желающий вставить его в
cordis.patch.yml сам не переписывает краткий пересказ.
У одного поля нет элемента управления, потому что оно принадлежит YAML профиля, а не
форме: modelZones (по умолчанию ["standard"]) — зоны каталога, которые
допускаются, когда эндпоинт недоступен и состав списка определяет зеркало.
Ключи:
| Параметр | По умолчанию | Смысл |
|---|---|---|
| Ключи на несколько чатов | вкл | общий выключатель кольца; в выключенном виде подмена маршрута не применяется и каждый запрос идёт маршрутом профиля |
Выключение ничего не удаляет: маршруты ключей остаются в конфигурации профиля, значения — в хранилище учётных данных, выбор чатов — в своём файле, поэтому включённая обратно возможность продолжает с того места, на котором её выключили. Переключатель живёт в разделе «API-ключи», а не рядом с выключателями значков и списка моделей: он принадлежит кольцу, и там же страница объясняет, почему добавление ключа недоступно, если хост не отдаёт нужный сервис.
Сбор ошибок:
| Параметр | По умолчанию | Смысл |
|---|---|---|
| Собирать ошибки rustvy | вкл | общий выключатель сбора: одна строка на инфраструктурный сбой провайдера уходит коллектору аптайма. Выключенный не отправляет ничего, и накопленная очередь остаётся на диске |
Выключатель живёт в собственном блоке «Сбор ошибок Rustvy», потому что это единственная
настройка страницы, которая что-то отправляет за пределы машины, — одна строка на сбой
провайдера уходит коллектору монитора. Сам блок — заголовок и выключатель, больше ничего:
ни пояснения под выключателем, ни счётчиков, ни кнопок отправки на странице нет. Что
именно уходит, что не уходит никогда и когда сбор срабатывает, описано в разделе
«Сбор ошибок и аптайм» ниже и в docs/error-report.md —
раскрытие не исчезло вместе с текстом на странице, оно переехало туда, где его можно
прочитать целиком. Состояние сбора — адрес коллектора, анонимный
идентификатор, счётчики отправленного, очереди и отброшенного, последний ответ, последняя
ошибка отправки и разбивка сбоев по видам — хост-половина по-прежнему отдаёт на своих
петлевых маршрутах /dsh-rustvy/state и /dsh-rustvy/errors, и смотреть его нужно там,
а не в интерфейсе. Адрес — uptimeEndpoint, по умолчанию
http://185.75.189.247:3789/api/report — поля ввода не имеет: его перенаправляют без
выпуска новой версии переменной окружения DSH_RUSTVY_UPTIME_ENDPOINT или правкой
<профиль>\.dsh-rustvy\config.json руками.
Настройки хранятся в <профиль>\.dsh-rustvy\config.json. Пять переменных окружения
перекрывают их — для развёртывания, которое направляет зеркало и сборщик в другое место
либо хочет смонтировать плагин неработающим, молчащим или без панели аптайма:
DSH_RUSTVY_ORIGIN и DSH_RUSTVY_DISABLED=1, DSH_RUSTVY_UPTIME_ENDPOINT,
DSH_RUSTVY_NO_ERRORS=1 и DSH_RUSTVY_NO_UPTIME=1.
Первая перенаправляет сайт провайдера, вторая выключает плагин целиком. Третья
перенаправляет коллектор аптайма и переопределяет и значение по умолчанию, и
config.json: принимается только адрес, разбирающийся как http/https, концевые
слеши обрезаются, а непригодное значение игнорируется, и тогда остаётся действующая
настройка. Четвёртая и пятая, каждая в значении 1 или true, выключают независимо от
настройки сбор ошибок и вид аптайма — тот же приём, которым DSH_RUSTVY_DISABLED гасит
значки.
Откуда берутся картинки
В сам пакет ничего не вшито. Хост-половина зеркалит два публичных объекта с сайта провайдера в профиль, а браузерная половина читает их с локального адреса:
- каталог моделей — сайт рисует свою таблицу моделей из 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 |
| Анонимный идентификатор установки | <профиль>\.dsh-rustvy\install.json — случайный UUID и дата создания; принадлежит сбору ошибок, а не кольцу, и лежит рядом с остальными файлами плагина |
| Неотправленные отчёты об ошибках | <профиль>\.dsh-rustvy\errors-queue.json — очередь отчётов, которые коллектор аптайма ещё не принял |
| Последний вид монитора | <профиль>\.dsh-rustvy\uptime.json — кешированный ответ коллектора, который панель показывает с пометкой stale, когда коллектор недоступен; принадлежит виду аптайма, а не кольцу |
В файлах плагина значения ключа нет никогда: ни в 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 — сколько из них были переименованы на
маршрут ключа. Ноль при живом трафике означает, что водопад не доходит до плагина,
и это видно сразу.
Сбор ошибок и аптайм
Монитор существует потому, что «сайт провайдера лежит» — это знание, которое сегодня
есть только у тех, кто в него упёрся, и у каждого из них по отдельности. Сбой запроса
выглядит как локальная проблема: человек повторяет шаг, переключает модель или уходит,
и никто — включая самого провайдера — не узнаёт, что в этот же момент upstream_502
ловили ещё четыреста установок.
Собирать это опросом нельзя. Проверочный запрос — это настоящий запрос к модели, он стоит токенов того, чьим ключом отправлен, и одна метка «живо» на каждой модели раз в минуту сжигает ровно то, за что пользователь платит. Поэтому аптайм здесь выводится из пассивных отчётов об ошибках: запрос, который и так состоялся, либо удался, либо сломался, хост-половине известен любой исход, но наружу уходит только сбой. Дополнительных запросов нет вовсе, а монитор судит о здоровье модели по отсутствию ошибок в окне — и знает то, чего опрос не покажет никогда: сколько людей задел сбой.
Что уходит в одном отчёте
Один отчёт — один запрос: плоский JSON-объект, без конверта и без массива событий.
Коллектор принимает его на POST /api/report; meta — единственный объект в нём, а всё
остальное лежит на верхнем уровне, потому что таких отчётов хранится много и читаются они
обратно сгруппированными по модели и часу.
Адрес по умолчанию — http://185.75.189.247:3789/api/report: публичный коллектор
монитора, пока намеренно на голом HTTP и по голому IP, — и он меняется без
выпуска новой версии, полем uptimeEndpoint в <профиль>\.dsh-rustvy\config.json или
переменной окружения DSH_RUSTVY_UPTIME_ENDPOINT.
| Поле запроса | Что несёт |
|---|---|
plugin | dsh-rustvy |
version | версия пакета |
provider | имя маршрута, которым шёл запрос |
model | идентификатор модели |
code | вид сбоя из собственного словаря плагина: upstream_502, connect_timeout, dns, … |
status | HTTP-статус, и только когда он есть: без соединения поля нет вовсе, а не 0 в нём |
message | собственный текст ошибки провайдера, очищенный и обрезанный до 2000 символов |
ts | момент сбоя в миллисекундах; коллектор в любом случае ставит и собственный serverTs |
meta | объект не больше 8 КБ: id — анонимный идентификатор установки, ms — длительность попытки, n — сколько событий свёрнуто (только когда больше одного), harnessCode — вердикт harness таким, каким он его назвал, и t |
Каждому понятию соответствует ровно один псевдоним из тех, что принимает коллектор:
code, а не type и не kind; message, а не error и не detail; ts, а не
timestamp. Анонимный идентификатор живёт в meta.id — отдельного поля для него у API
нет, а без него монитор не ответил бы на вопрос «у скольких людей».
Не уходит никогда: промпты, содержание сообщений и чатов, содержимое и пути файлов,
API-ключи, идентификатор сессии и вообще всё, что называет пользователя или его машину.
Это устройство, а не обещание: в отчёте нет ни одного поля, куда такое можно было бы
положить, а единственное свободное — message — проходит через очистку, стирающую ключи
вида sk-…, Bearer-токены, заголовки учётных данных, именованные секреты, пути Windows
(целиком, вместе с пробелами) и POSIX — они заменяются на <path> — и длинные
непрозрачные строки.
Какие сбои считаются аварией
Правило одно: сообщается о том, что сломалось у провайдера, и никогда о том, что произошло у пользователя. Разбор идёт по коду harness, по статусу и по тексту сообщения, и своя ситуация проверяется первой — текст про исчерпанную квоту не становится аварией, даже если в нём встретилось число, похожее на статус.
| Считается аварией | Не считается |
|---|---|
upstream_502, upstream_503, upstream_400, upstream_404 — любой upstream_*, кроме 401, 402, 403 и 429 | закончившаяся подписка, заблокированный счёт, исчерпанная квота или кредиты |
http_5xx, http_404, http_408 | ограничение частоты запросов, включая 429 |
dns, refused, unreachable, tls | неверный, отсутствующий или отозванный ключ |
connect_timeout, timeout, reset, server, transport | превышенное окно контекста, неизвестная модель, неверный запрос |
| остановка, нажатая самим пользователем |
upstream_400 и upstream_404 сообщаются, а голые 400 и 404 — нет: префикс
означает, что шлюз провайдера сам назвал ответ своего источника, то есть сломалось там,
а не в запросе, который составили вы.
Успехи никуда не уходят
Коллектор получает только сбои: отчёт об удачном запросе не отправляется никогда. Успехи
при этом наблюдаются локально, и нужны они для одного — немедленно отправить ещё
сворачивающийся сбой этой модели, как только запрос снова прошёл: отчёт попадает в
коллектор в момент восстановления, а не в конце окна сворачивания, и промежуток между
двумя метками времени и есть длина аварии. Счётчики, которые успехи двигают, хост-половина
по-прежнему отдаёт в errorReport, но страница настроек их не рисует — как и всю остальную
сводку сбора.
Следствие стоит назвать прямо: аптайм на мониторе — это отсутствие ошибок в окне, а не отношение двух видов отчётов. Модель, которую никто не спрашивал, выглядит там так же, как модель, которая работает.
Анонимный идентификатор и два файла
Идентификатор — случайный UUID и ничего больше: ни аппаратного идентификатора, ни имени
учётной записи, ни пути. Он нужен, чтобы считать людей, а не запросы: тысяча отчётов из
одной установки — это один человек, повторяющий попытку, а тысяча отчётов из трёхсот
установок — это авария. В отчёте он лежит в meta.id. Сбросить его — удалить
<профиль>\.dsh-rustvy\install.json: следующая отправка создаст новый, и установка
станет для монитора другой.
| Что | Где |
|---|---|
| Анонимный идентификатор | <профиль>\.dsh-rustvy\install.json |
| Неотправленные отчёты | <профиль>\.dsh-rustvy\errors-queue.json |
Очередь на диске — часть смысла, а не кеш: отчёт, который не удалось доставить, остаётся в файле со своей настоящей меткой времени и уходит, когда сайт отвечает, поэтому авария попадает в монитор с временами, в которые она произошла, а не с моментом восстановления связи. Записывается она не чаще раза в 5 секунд и ещё раз при выгрузке, поэтому перезапуск во время аварии ничего не теряет.
Сворачивание и пределы
Без них установка в циклическом повторе отправила бы коллектору тысячи отчётов об одной и той же аварии:
| Что | Значение |
|---|---|
| Окно сворачивания сбоев | 60 секунд на пару «модель + вид»: ts остаётся временем первого сбоя, meta.ms берётся от последнего, число событий растёт в meta.n |
| Отправка очереди | раз в 15 секунд, до 10 отчётов за одну отправку — и каждый своим запросом, потому что одного запроса на отчёт требует сам коллектор |
| Предел в минуту | 20 отчётов на установку — прямой ответ на лимит коллектора в 60 отчётов в минуту на IP |
| Предел на установку | 240 отчётов в час; сверх него они отбрасываются, а не копятся |
| Потолок очереди | 500 отчётов; при переполнении отбрасываются самые старые |
| Повтор после отказа | 15 секунд, удваивается до 10 минут |
| Срок одной отправки | 10 секунд |
| Запись очереди на диск | не чаще раза в 5 секунд и ещё раз при выгрузке |
Ответы коллектора разбираются по смыслу, а не одним правилом на все 4xx. 200 { ok: true }
— отчёт принят. 400 означает, что в отчёте не оказалось ни message, ни code: такой
отчёт отбрасывается как непригодный, потому что коллектор, отказавший ему один раз,
откажет и снова. 429 и любой 5xx — в том числе 503, когда недоступно хранилище, —
оставляют отчёт в очереди и откладывают следующую попытку; заголовок Retry-After в
ответе 429 принимается как нижняя граница отсрочки. Сетевая ошибка оставляет отчёт в
очереди точно так же — именно поэтому авария доходит до коллектора уже после того, как
сайт ответил, и доходит с исходными метками времени.
429 отделён от 400 сознательно: это «занято», а не «запись плохая», и отчёт,
отброшенный на чужом лимите, был бы потерянным настоящим сбоем. Лимит коллектора — 60
отчётов в минуту на IP, и наш поток ниже него по построению, а не по договорённости:
одинаковые сбои одной модели сворачиваются в один отчёт за 60 секунд, одна отправка
уносит не больше 10 отчётов, минутный предел на установку — 20 против их 60, почасовой —
240, а очередь выше 500 отчётов не растёт. То есть не этот плагин приводит кого-то к
429.
Адрес коллектора у всех установок один, поэтому минутный предел защищает ещё и соседей: авария на одной машине не съедает долю в 60 отчётов в минуту, которая у общего NAT одна на всех.
Как выключить
Выключателем «Собирать ошибки rustvy» в Настройки → Rustvy,
collectErrors: false в <профиль>\.dsh-rustvy\config.json либо переменной окружения
DSH_RUSTVY_NO_ERRORS=1, которая вынуждает молчание независимо от настройки: она для
развёртывания, которому нужен смонтированный, но молчащий плагин и которое не хочет
править файл профиля. Выключенный сбор не отправляет ничего и ничего нового в очередь не
ставит; на сами запросы к провайдеру он не влияет ни в каком состоянии. Форма отчёта,
словарь видов и контракт коллектора описаны в
docs/error-report.md.
Аптайм моделей
Тот же коллектор, который принимает отчёты об ошибках, отдаёт обратно вид монитора: какие модели отвечают, у скольких людей и как давно они не отвечали. Плагин показывает этот вид прямо в DSH — блоком на странице настроек и значком аптайма в списке моделей, — чтобы выбор модели не требовал открывать браузер и искать там строку, которая и так видна здесь.
Числа в блоке — собственные числа коллектора. Плагин ничего не пересчитывает: оценка, посчитанная локально, разошлась бы со страницей монитора, а два ответа на вопрос «модель жива?» хуже одного.
Процента больше нет
Плагин сообщает только об ошибках: ни удачных запросов, ни объёма трафика у него нет, поэтому у «процента доступности» нет знаменателя в принципе. Число, которое раньше рисовалось на значке, было долей корзин без ошибок, и у него три плохих свойства: оно менялось вместе с окном при одном и том же потоке событий, насыщалось — 47 ошибок в корзине и одна ошибка давали одинаковый вклад, — и не различало широту: сбой у одного человека и авария у провайдера выглядели одинаково.
Хорошо измеряется другое: сколько различных установок сообщили об ошибках, как давно пришла последняя и сколько их было вообще. Поэтому заголовок теперь состояние, а не процент. Единственная доля, которая осталась на экране, — «затронуто N% установок»: различные установки, сообщившие об ошибках в окне, к различным установкам, присылавшим что-либо за 30 дней. Знаменатель у неё настоящий, и означает она охват, а не доступность; аптаймом она не называется нигде.
Откуда читаются
Хост-половина зеркалит ответ коллектора на свой петлевой маршрут
GET /dsh-rustvy/uptime.json?range=1h|24h|7d|30d, и панель читает зеркало, а не коллектор
напрямую: запрос остаётся в пределах машины, а зеркало держит последний удачный ответ,
поэтому недоступный коллектор показывает кешированную картину с явной пометкой, а не
пустой блок. Обновляется она каждые 30 секунд, пока страница настроек открыта, — столько
же ждёт и своя страница монитора, — и перестаёт спрашивать, как только страница закрыта.
Четыре состояния
| Состояние | Правило коллектора | Слово | Цвет |
|---|---|---|---|
ok | в окне ошибок не было | Без ошибок | #22c55e |
single | ошибки есть, установка одна | Локальный сбой | #f59e0b |
issues | ошибки есть, установок две и больше | Сбои | #ef4444 |
down | установок три и больше, последняя ошибка не старше пяти минут | Лежит | #ef4444 |
Правило и его пороги живут в коллекторе, а не в плагине: плагин берёт готовое status и
переводит его в слово и цвет. single янтарный, а issues и down красные намеренно —
одна сообщившая установка чаще всего означает её собственную сеть, ключ или регион, и
красный цвет на таком вечере был бы криком о чужой беде. Слова и цвета взяты со страницы
монитора, чтобы плагин и сайт не рассказывали разное об одной и той же модели; поверхностями
при этом остаются обычные токены темы --dsw-alias-*, потому что у страницы монитора одна
тёмная тема, а у этой две.
Коллектор старше v2 отвечает словами up | slow | down. Они переводятся как up → ok,
slow → issues, down → down, а single от него просто не приходит: пока сервер не
обновлён, локальный сбой неотличим от аварии провайдера. Незнакомое слово принимается за
ok — модель, о которой в окне ничего не сообщили, это модель, на которую никто не
пожаловался, а не пятая категория, которой на проводе не было.
Окна и полосы
Переключатель окна — 1 час, 24 часа, 7 дней, 30 дней, по умолчанию сутки — это состояние
самой панели, а не настройка: в config.json он не пишется, и один взгляд на прошлый
месяц не становится предпочтением для всех остальных поверхностей. Корзин в ответе
столько, сколько прислал коллектор: 60 минутных на час и 48 на каждое окно длиннее,
поэтому ширина корзины меняется вместе с окном — минута на часе, полчаса на сутках, три с
половиной часа на неделе, пятнадцать на месяце. Полоса несёт подпись своей ширины
(«1 столбик = 30 мин»), потому что без подписи полоса на неделю читается как полоса на
час. Рисуются корзины в масштабе max(1, …counts), то есть одна-единственная ошибка уже
видна.
Строка модели
Слово состояния, «ошибок N от M установок», «последняя X назад», полоса корзин с подписью ширины и «затронуто N% установок», когда знаменатель есть; ниже — строка последней ошибки с её временем, кодом и собственным текстом провайдера.
Часть отчётов приходит без пригодного meta.id, и их не на кого посчитать. Тогда «ошибок
3 от 1 установки» молча обещает широту, которой на самом деле не измерили, — а это ровно
то число, по которому решают, доверять модели или нет. Поэтому рядом с ним появляется
оговорка: «(все анонимные)», когда анонимных столько же, сколько ошибок, и «(2 из 3
анонимных)», когда часть. Когда анонимных нет — а это обычный случай — не пишется ничего.
Ответ коллектора старше v2 не несёт ни installs, ни affectedPct, ни
minutesSinceLast. Строка показывает число ошибок без «от M установок», не показывает
долю вовсе и берёт «последняя X назад» из lastError; из шапки исчезает член про охват.
Панель становится беднее, но не неправильнее: ни «от 0 установок», ни «затронуто 0%» на
ней не появляется никогда.
Шапка блока: «моделей N · без ошибок N · локально N · сбои N · лежат N · ошибок за период
N · затронуто M из K установок». Процента в шапке нет ни на одном окне, и это не
упущение: на 30 днях окно и справочный период совпадают, числитель и знаменатель — одно и
то же множество установок, и «затронуто 100% установок» читалось бы как вердикт, означая
лишь, что долю измерили саму против себя. Коллектор отвечает на таком окне
summary.affectedPct: null, а шапка показывает сами счётчики — «затронуто 47 из 47
установок», — которые остаются честными числами и говорят то же самое без притворной
точности. Доля каждой отдельной модели при этом настоящая на любом окне, включая
тридцатидневное: её знаменатель — вся база установок, а числитель — установки одной
модели, поэтому «затронуто 4,3% установок» в строке модели остаётся осмысленным там, где
та же цифра в шапке была бы тавтологией.
Коллектор, который не знает часа
Значки читают окно 1h, а коллектор первой версии его не знает и отвечает на него
ошибкой. Плагин при этом обновляется раньше, чем разворачивается коллектор, поэтому такой
промежуток — обычное состояние, а не авария: хост-половина отдаёт 502 без единой
модели, значки исчезают все — и в списке моделей, и в кнопке выбранной модели, — значки
производителя остаются на месте, а панель на сутках продолжает работать как работала.
Пустой значок здесь честнее любого числа: окно, которое коллектор не может назвать, это
окно, в котором он не назвал ни одной модели. Если переключить панель на час, она скажет
об этом словами и назовёт окно — «Коллектор не ответил за период «1h»: …», — потому что
«коллектор не ответил» и «коллектор не знает этого окна» требуют разных действий.
Транспортная ошибка убирает значки точно так же: в отличие от панели, которая может
показать кешированную картину с пометкой о её моменте, значок не умеет сказать «это мы
знали некоторое время назад».
Пустой ответ — нормальный
Агрегация коллектора ещё пишется, и он отвечает total: 0, нулём ошибок и пустыми
корзинами на всех окнах. Панель рисует это как находку, а не как поломку: «ошибок нет» —
ответ, и он показан словами вместо пустых полос. Когда агрегация заработает, те же строки
начнут показывать настоящие аварии, и менять для этого страницу не придётся. Это свойство
коллектора, а не плагина.
Значки аптайма в списке моделей и в кнопке выбранной модели
У строк, которые коллектор назвал, в списке моделей появляется значок состояния — справа
в строке, там же, где harness рисует свою собственную плашку, и рядом с ней, а не поверх
неё. Процента на нём нет: ok — тихая зелёная точка без числа, single — янтарная плашка
с числом ошибок, issues и down — красная плашка с числом ошибок. Число ошибок и цвет
состояния читаются быстрее, чем 97,9%, которые ничего не значили.
Строка, которую коллектор не назвал, не получает ничего — и именно поэтому у ok точка, а
не пустое место: «про эту модель ничего не известно» и «у неё всё хорошо» обязаны
выглядеть по-разному. Подсказка несёт остальное — «3 ошибки от 2 установок за последний
час · последняя 4 мин назад», с той же оговоркой про анонимные отчёты, — а для ok
говорит «ошибок не было за последний час».
Тот же значок рисуется и в кнопке выбранной модели — между названием модели и индикатором рассуждений, если сборка его рисует, а если не рисует, сразу за названием. Кнопка единственное место, где модель меняется под значком, поэтому правило одно: при смене выбранной модели значок либо переписывается, либо исчезает — состояние прошлой модели в кнопке не остаётся. Состояния, цвета и окно у него ровно те же, что и у значков в списке, а выключатель «Иконка в кнопке выбранной модели» убирает с кнопки и значок производителя, и значок аптайма: это слово пользователя о том, писать ли в кнопку вообще.
Значок описывает последний час и только его: окно панели на него не влияет, иначе подсказка врала бы о числе, которое показывает. Рисует значки тот же проход, что и значки провайдера, поэтому общий выключатель значков убирает и их, а выключатель «Показывать аптайм моделей» — только их.
Это правило отрисовки, а не новое значение состояния: значок и панель рисуют одно и то же
status коллектора, просто значок говорит цветом и числом, а панель — словом, числом и
полосой.
Как выключить
Выключателем «Показывать аптайм моделей» в Настройки → Rustvy (по умолчанию
включён), uptimeView: false в <профиль>\.dsh-rustvy\config.json либо переменной
окружения DSH_RUSTVY_NO_UPTIME=1, которая прячет панель независимо от настройки.
Выключенный вид
не читает ничего: ни панели, ни значков аптайма в списке моделей нет, и запрос к зеркалу
не уходит вовсе. Форма
ответа коллектора, его поля и контракт зеркала описаны в
docs/uptime-view.md.
Маршруты
Одна регистрация префикса на собственном веб-сервере 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 | состояние зеркала: версия, число моделей, кешированные логотипы, маршрут, прокси (учётные данные маскируются), все пути к файлам — включая install, errorsQueue и uptime, — последняя ошибка, последнее вычисленное расхождение, сводка кольца ключей и блок errorReport: включён ли сбор, адрес коллектора, анонимный идентификатор, счётчики отправленного, очереди и отброшенного, последний ответ, последняя ошибка отправки и разбивка сбоев по видам. Собственных запросов не делает; сводка кольца синхронная и без чтения учётных данных, поэтому масок в ней нет — только счётчики и маршруты. Блок errorReport страница настроек не рисует вовсе — он для тех, кто читает маршрут руками; у хост-половины старше сбора ошибок его и двух новых путей нет |
/dsh-rustvy/refresh | POST | перечитать каталог, заново прогреть все названные им логотипы и сообщить — либо, когда режим записи позволяет, применить — список моделей |
/dsh-rustvy/apply-models | POST | записать сгенерированный список в профиль маршрута через ctx.configEditor |
/dsh-rustvy/revert-models | POST | вернуть сохранённый профиль маршрута |
/dsh-rustvy/config | POST | записать настройки; сети не касается |
/dsh-rustvy/errors | GET, POST | петлевая диагностика сбора ошибок, которой в интерфейсе больше нет: ни кнопки, ни строки состояния на странице настроек не ведут сюда. GET: сводка сбора — тот же блок errorReport, что и в /state. POST { action: 'test' } ставит в очередь одно синтетическое событие и немедленно его отправляет, { action: 'flush' } отправляет то, что накопилось; оба отвечают свежей сводкой. Отказ — HTTP 200 с { ok: false, error }, как у выключенной возможности на /apply-models. Маршрут остаётся рабочим: им проверяют доставку руками, когда смотреть на экран не хочется |
/dsh-rustvy/uptime.json | GET | зеркало вида монитора: ?range=1h|24h|7d|30d (по умолчанию 24h) и ответ коллектора как он есть — summary, installs, models с их status, errors, installs, anonymous, firstAt, lastAt, minutesSinceLast, affectedPct, buckets, events и lastError, byCode, generatedAt, storage и monitorUrl; устаревшие поля uptime и summary.slow приходят от старого коллектора и плагином не показываются. Когда коллектор недоступен, отдаёт последний удачный ответ с stale: true, fetchedAt и error; когда кеша нет вовсе — 502 с { ok: false, error }. При выключенной настройке — 200 с { ok: false, error }, как у всякой выключенной возможности |
/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/errors.js, lib/uptime.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' }) записывает маршруты кольца заново |
| коллектор аптайма | отчёты остаются очередью на диске и уходят, когда сайт отвечает, поэтому авария попадает в монитор с настоящими временами; на запросы к провайдеру это не влияет никак, а неотправленное и последнюю ошибку отправки показывают петлевые маршруты /dsh-rustvy/state и /dsh-rustvy/errors — на странице настроек этого нет |
| хост-половина старше сбора ошибок | /state приходит без блока errorReport и без двух новых путей: страница настроек рисует выключатель одинаково в обоих случаях, потому что сводку сбора она не рисует вовсе, а пути к файлам показываются только тогда, когда хост их назвал |
| вид монитора у коллектора | зеркало отдаёт последний удачный ответ с stale: true, и панель показывает кешированную картину с пометкой, от какого она момента; без кеша вовсе — 502, и панель называет причину вместо того, чтобы крутиться. Значки аптайма в списке моделей исчезают вместе с данными, а значки провайдера остаются: на запросы к провайдеру это не влияет никак |
коллектор старше v2, не знающий окна 1h | значки исчезают все — и в списке моделей, и в кнопке выбранной модели, — значки провайдера остаются, а панель на сутках работает как прежде и называет окно, которое не смогла прочитать, если переключить её на час. Это состояние между обновлением плагина и развёртыванием коллектора, и пустой значок в нём честнее числа |
| коллектор старше v2, без полей широты | панель переводит старые слова состояний, показывает число ошибок без «от M установок», не показывает долю вовсе и берёт давность из lastError; из шапки исчезает член про охват. Ни «от 0 установок», ни «затронуто 0%» на ней не появляется |
Структура
| Файл | Роль |
|---|---|
lib/index.js | стабильная точка входа загрузчика; владеет циклом перезагрузки |
lib/host.js | маршруты, зеркало каталога, кеш логотипов, синхронизация моделей, кольцо ключей и подмена маршрута на agent/request, настройки |
lib/models.js | чистое отображение: что публикует источник → одна запись профиля маршрута, расхождение двух списков и YAML для ручной правки |
lib/keys.js | чистая арифметика кольца: названия, имена учётных данных и маршрутов, документы кольца и выбора чатов, сверка карты маршрутов с кольцом |
lib/errors.js | классификация сбоя на инфраструктурный и собственный, очистка текста от секретов и путей, анонимный идентификатор установки, сворачивающая очередь отчётов и её файл на диске |
lib/uptime.js | чистая форма вида монитора: допустимые окна, приведение ответа коллектора к полям, которые рисует панель, обрезка строк и clamping процентов. Не читает сеть и не держит кеш — этим владеет lib/host.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 и lib/uptime.js намеренно чистые — без сети, файловой
системы и Cordis, — поэтому отображение можно проверить на реальном ответе источника,
арифметику кольца на выдуманном профиле, а форму вида монитора на ответе коллектора,
ничего не монтируя. Сети не касается и
lib/errors.js: отправитель внедряется извне, именно потому, что рассуждать об
очереди, чей транспорт импортирован, невозможно в тот момент, когда лежит сайт, о
котором она сообщает. Свои два файла этот модуль читает и пишет сам — идентификатор
установки, который можно было бы создать только внутри смонтированного хоста, не на
чем было бы строить. 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\error-report-host-test.mjs # сбор ошибок: классификация, сворачивание и доставка локальному коллектору
node tests\uptime-host-test.mjs # вид монитора: зеркало, кеш последнего ответа и пометка stale
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 нет секрета, выгрузка снимает и маршрут, и слушателя, а с выключенной
возможностью не переименовывается ничего.
error-report-host-test.mjs — хост-тест сбора ошибок: классификация сбоев на
инфраструктурные и собственные, сворачивание отчётов по окнам и их доставка коллектору,
поднятому на локальном адресе, — то есть та же host-половина, на которой
работают два предыдущих хост-теста, поэтому маршрут /dsh-rustvy/errors, блок
errorReport в /state и очередь на диске проверяются вместе с тем, что именно
попадает в отправленный отчёт.
uptime-host-test.mjs — хост-тест вида монитора: зеркало на
/dsh-rustvy/uptime.json, три окна, кеш последнего удачного ответа на диске и пометка
stale с fetchedAt и причиной, когда коллектор недоступен. Проверяется и то, что
зеркало ничего не пересчитывает: ответ коллектора отдаётся как пришёл, а выключенная
настройка отвечает { ok: false } и не делает запроса вовсе.
Браузерный тест рисует страницу настроек мини-реактом, чьи ячейки хуков привязаны к экземпляру компонента, и проверяет: раздел называется Rustvy; три внешних адреса — настоящие ссылки, открывающиеся вне приложения; каждый путь к файлу показан с кнопкой копирования; маршрут, строка профиля, сайт и размер значка редактируются и сохраняются по потере фокуса; оба выпадающих списка сохраняются по изменению; панель ручной правки читает и показывает блок; расхождение называет новые, снятые и изменённые модели; кнопка записи включена при наличии расхождения, выключена при актуальном списке и выключена, когда хост не отдаёт редактор; откат остаётся выключенным, пока копии нет; каждое изменение уходит на нужный маршрут и сообщает, что именно сдвинулось.
Тот же тест рисует и кольцо ключей: раздел «API-ключи» со списком, масками,
переименованием по потере фокуса, инлайн-формой «+», проверкой, назначением
ключом по умолчанию и удалением в два шага; объяснения на каждый отказ — нет
credentials, host-половина устарела, возможность выключена — вместе с
возвращением способности, когда причина устранена; переключатель «Ключи на
несколько чатов», который читает ответ хоста и пишет своё поле в /config; блок
«Маршруты ключей» со счётчиками подмены, числом чатов со своим выбором, путями к
обоим файлам, отсутствующим и отставшим маршрутами и кнопкой «Починить
маршруты»; предел в 12 ключей; переключатель композера, который не рисуется без
sessionId и при одном ключе, читает выбор своего чата, публикует его в
/session-key и предлагает добавить ключ; меню выбора моделей и окно /model,
которые не получают ни раздела ключей, ни его строк; стоимость обращений к хосту —
два взаимодействия подряд стоят одного чтения, открытие переключателя читает
всегда, а выбор обходится одним POST без дочитывания; и выгрузка, после которой в
меню не остаётся ничего своего. Отдельно проверяется, что значение ключа не
попадает ни в один URL и не остаётся на странице после добавления.
Он же рисует блок сбора ошибок: выключатель с его точной подписью — включённый по
умолчанию, в том числе когда фиктивный /state поля collectErrors не знает вовсе.
Отдельно проверяется отсутствие, и не только сводки: фикстура несёт полный блок
errorReport, и при этом на странице нет ни заголовка «Состояние сборщика», ни одной из
восьми строк сводки, ни их значений — ни анонимного идентификатора, ни разбивки по видам,
ни момента последней отправки, — ни одной из двух кнопок отправки, и в записанных
запросах нет ни одного обращения к /dsh-rustvy/errors. Ровно так же проверяется
снятый со страницы текст: девятнадцать строк, которые она раньше несла — вступление под
заголовком, обещание под выключателем сбора и семь абзацев в конце, — ищутся в дереве и
не находятся, а сами возможности при этом на месте. Удаление, которое никто не проверяет,
возвращается само. Единственная строка, пережившая снятие текста, проверена положительно:
предупреждение «Каталог не получен» рисуется, когда фиктивный /state несёт error, и
не рисуется, когда не несёт, — это не проза, а единственное место, где страница сообщает
о неудачном чтении каталога. Проверяется и хост-половина старше сбора ошибок: без блока
errorReport страница рисует ровно то же, что и с ним, и не бросает исключение.
Он же рисует блок аптайма — на четырёх фикстурах: всезелёный ответ, который коллектор
даёт чаще всего; наполненный, несущий все четыре состояния и оба крайних случая
(affectedPct: null и окно, где все отчёты анонимные); часовой ответ, из которого
значки берут свои числа, — а на любое окно длиннее зеркало в этом режиме отвечает
удвоенными счетчиками, поэтому значок, перерисованный не из своего окна, виден сразу; и
ответ коллектора старше v2, с тремя старыми состояниями, процентом и без единого поля
широты. Проверяются шапка, считающая состояния вместо процента, и строки моделей со
словами «Лежит», «Сбои», «Локальный сбой» и «Без ошибок»; окно: переключатель предлагает
все четыре окна, по умолчанию стоит 24h, спрашивает зеркало про ?range=1h и не пишет
ничего в /config, потому что это состояние панели, а не настройка; ширина корзины
подписана — 48 столбиков по 30 минут на сутках и 60 по минуте на часе; «ошибок 5 от 3
установок» и «затронуто 6,4% установок» — числа коллектора, а не пересчёт; доля без
знаменателя рисуется как «—», а не как 0%; последняя ошибка трёхчасовой давности читается
как «3 ч назад», а не как «только что»; оговорка про анонимные отчёты стоит рядом с
числом, которое она ограничивает; кешированный ответ показывает fetchedAt и
предупреждение с причиной; недоступный коллектор называет причину и оставляет кнопку
«Обновить» вместо вечного ожидания. Старый ответ рисуется беднее, но не неправильнее:
старые слова переведены, число ошибок показано без «от M установок», доля не показана
вовсе, и ни «от 0 установок», ни «затронуто 0%» на панели не появляется. Пятая фикстура —
тридцатидневное окно, на котором доля в шапке теряет смысл: шапка показывает счётчики
(«затронуто 47 из 47 установок») и не показывает процента, а доля отдельной модели на том
же окне остаётся настоящей («затронуто 6,4% установок»), и полоса говорит, что её
столбик — пятнадцать часов.
Отдельно проверяется промежуток между обновлением плагина и развёртыванием коллектора:
фикстура, в которой range=1h отвечает 502 без единой модели, а сутки работают как
прежде. Проверяется то, что иначе осталось бы догадкой, — путь значков не бросает
исключение и не зависает на незавершённом чтении: значки исчезают все, включая значок в
кнопке выбранной модели, значки предыдущего окна на экране не задерживаются, значки
провайдера остаются, панель называет окно, которое не смогла прочитать, а на сутках
продолжает рисовать строки. Транспортная ошибка — чтение, которое не ответило вовсе, —
проверяется тем же образом: значки уходят, а не застывают, потому что значок, в отличие от
панели, не умеет сказать, от какого он момента.
Отдельно проверяются значки аптайма в списке моделей — на часовой фикстуре, по одной
модели на каждое состояние: ok — тихая точка без числа, single — янтарная плашка с
числом ошибок, issues и down — красные плашки со своими числами, и все четыре цвета —
цвета монитора; точка при этом отличается от пустого места, поэтому «коллектор ничего не
сказал» и «ошибок не было» выглядят по-разному; подсказка называет окно («за последний
час»), число ошибок, число установок и давность; значок стоит в строке до её собственного
хвостового элемента и не теряет значок провайдера; второй проход не рисует второй значок;
строка, на которой уже есть плашка harness, сохраняет обе — наша оказывается соседом, а
не вложенным элементом; переключение панели на 30 дней не перерисовывает значки, у
которых в подсказке час; выключенный вид убирает значки и оставляет значки провайдера, а
выгрузка забирает их с собой. Отдельно проверяется значок в кнопке выбранной модели: он
несёт состояние выбранной модели и её цвет, стоит между названием и индикатором
рассуждений, а на кнопке без индикатора — сразу за названием; собственная обвязка кнопки
(индикатор, «Default», шеврон, кнопка остановки) и значок провайдера остаются
нетронутыми; при смене выбранной модели значок переписывается — точка тихой модели
становится красной плашкой сломанной, — а на модели, которую коллектор не назвал,
исчезает, и состояние прошлой модели в кнопке не задерживается; выключенная «Иконка в
кнопке выбранной модели» убирает с кнопки и значок аптайма, оставляя значки в списке
моделей. Выключатель аптайма проверяется так же, как остальные: постит своё поле в
/config, а в выключенном виде панель объясняет себя словами и не делает ни одного
запроса к зеркалу.
Половина, рисующая значки, автоматическим набором в этом репозитории не
покрыта — и это стоит знать заранее. Она работает с DOM уже открытой страницы,
поэтому проверяется глазами: какие строки получают значок и какой именно, что
строки уровней усилия, заголовки производителей и поле поиска остаются пустыми, что
вместо неполученного логотипа появляется плашка с монограммой, что добавленная
позже строка тоже получает значок, что повторный проход ничего не удваивает и что
выгрузка убирает добавленное. model-sync-client-test.mjs покрывает страницу
настроек и переключатель ключа в композере, но не рисование значков; скрытие
групп-клонов в меню — та же работа с DOM уже открытой страницы, и набор проверяет
там только то, что меню не получает ни раздела ключей, ни его строк.
Лицензия
MIT — см. LICENSE.
Plugins relacionados
dsh-our-free-model
ebony-vinyl/dsh-our-free-model
dsh-routing-suite
yjh051108/dsh-routing-suite
dsh-workbuddy-connect
corrinehu/dsh-workbuddy-connect
dsh-connect-workbuddy
dingminhua/dsh-connect-workbuddy