Рецепт: живые формы конфигурации

Объявите в схеме Config плагина поля, изменяемые на лету, и выведите их через карточку настроек, которой владеет продукт. Экспортируемый интерфейс Config описывает значения, которые получает плагин, включая каждую ссылку Volatile<T>.

1. Объявите изменяемые на лету поля

import type { Context, Volatile } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'

export interface Config {
  endpoint: Volatile<string | undefined>
  retries: Volatile<number>
}

export const Config = z.object({
  endpoint: z.string().volatile(),
  retries: z.number().step(1).min(0).default(3).volatile(),
})

export function apply(ctx: Context, config: Config): void {
  ctx.on('loader/volatile-update', () => {
    ctx.logger.info('Retry limit: %d', config.retries.get())
  })
}

Читайте .get() в начале операции. Запрос, которому нужен согласованный снимок, забирает свои значения один раз. Для проверок Config, затрагивающих несколько полей, используйте .check(): такие проверки выполняются на Host до сохранения и в сериализованные схемы форм не попадают.

2. Соберите плагин

Дайте каждому экземпляру уникальный id записи в профиле. Базовый бандл монтирует settings и config-editor. Для собственных профилей прочитайте README этих пакетов, прежде чем монтировать те же сервисы.

role('secret') не пускает значение в ответы формы. Для значений, которыми управляет домен учётных данных, используйте ссылки на учётные данные. Обычные поля в схему настроек не попадают.

3. Проверьте редактирование

Измените поле в Plugins и сохраните. Проверьте патч профиля, следующую операцию потребителя, неизменную идентичность экземпляра плагина и восстановление после перезапуска. Отклоните некорректное значение и убедитесь, что не изменились ни файл, ни живое значение.

Собственные страницы плагинов получают form.state и form.mutate(operations, expectedRevision) от владельца страницы Plugins. Host проверяет Config целиком и применяет правки через ConfigEditor и volatile-HMR. Готовые примеры карточек лежат в пакете настроек плагинов.

4. Дополните страницу другого плагина

Плагин, которому есть что сказать о бандле, строке или официальном плагине, которым он не владеет, регистрируется в plugins.detail.actions (элемент управления в шапке страницы), plugins.detail.badge (метка рядом с заголовком) или plugins.detail.section (раздел под собственным содержимым страницы). Каждая запись рендерится с subject страницы — { kind: 'bundle', pkg }, { kind: 'row', pkg, row } или { kind: 'item', id } — и возвращает null, если про этот субъект ей сказать нечего:

ctx.slots.inject('plugins.detail.badge', () => ctx.slots.register({
  name: 'plugins.detail.badge',
  id: 'acme-update',
  locale: 'acmeUpdate',
}, ({ t, subject }) => subject.kind === 'bundle' && hasUpdate(subject.pkg) ? <Tag tone="info">{t('update')}</Tag> : null))

5. К чему крепится браузерная половина

Браузерную половину отдаёт странице система клиентских модулей: она сканирует включённые записи Loader в поисках пакетов, объявивших dsh.client, и отдаёт собранный экспорт ./client каждого из них. Но половину пакета она прикрепляет к той строке Loader, чей спецификатор — голое имя пакета. Строка, смонтированная из экспорта по подпути, половину не несёт никогда. Поэтому бандл, который разбивает один пакет на несколько строк, держит половину на корневой строке, и все страницы, которые она регистрирует, пропадают, когда эту строку выключают. Субплагин, чья страница должна пережить остальные строки, поставляется отдельным пакетом.

Собранный файл ./client должен быть в формате ленивой CJS-фабрики системы клиентских модулей: один скрипт, который регистрирует в загрузчике модулей страницы имя пакета и factory(require); формат описан в README системы клиентских модулей. Пресет tsdown clientBundle, который его выпускает, лежит в packages/client/tsdown.client.ts, а не в опубликованном пакете, так что пакету вне этого репозитория придётся воспроизвести эту сборку самостоятельно.

{
  "exports": {
    ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
    "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" }
  },
  "dsh": { "client": { "platform": "web", "inject": ["@deepseek-ai/dsh-client-ui-settings"] } }
}

Пакет-компаньон для встроенного пространства имён — это та же браузерная половина в пакете только для клиента с пустым apply на Host. Он указан в списке плагинов веб-композиции (packages/bundle/web-app/cordis.patch.yml) и регистрируется в plugins.item через ctx.configForms.whileServed, так что страница существует ровно пока Host отдаёт это пространство имён.

перевод и редактура:

обновлено

⌕ ESC