Рецепт: живые формы конфигурации
Объявите в схеме 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 отдаёт это пространство имён.