Ваш первый модуль
Мы соберём минимальный модуль, который монтирует страницу в оболочке Building OS и читает данные через контекст платформы. Бюджет: 30 минут.
1. Создание каркаса
SDK создаёт весь модуль за вас — манифест, конфиг Vite с уже подключённым экспортом федерации, Shell.tsx и package.json:
npx @tv/extension-sdk init-module hello \
--name="Hello Module" \
--category=operations✓ Scaffolded module at ./tv-module-hello
Files written:
module-manifest.json
vite.config.ts
src/Shell.tsx
package.json--category обязателен и должен быть одним из: core, operations, engagement, infrastructure, analytics, ai. Если фронтенд у вас уже есть и нужен только манифест — используйте init-manifest.
Хотите сначала посмотреть на настоящий модуль?
modules/tv-module-example — каноническая эталонная реализация: федерация инструментов Copilot, проверка запросов по HMAC-подписи, отчёт о живости. В каталоге модулей перечислены 30 модулей, работающих на платформе сегодня.
2. Разберитесь с манифестом
Генератор пишет валидный стартовый манифест. Вот его форма с заполненными полями, которые вы реально будете править:
{
"$schema": "./node_modules/@tv/extension-sdk/manifest.schema.json",
"sdkVersion": "1.1.0",
"id": "@acme/module-hello",
"name": "Hello Module",
"description": "Приветствует оператора и перечисляет помещения активного здания.",
"version": "1.0.0",
"minCoreVersion": ">=2.0.0",
"category": "operations",
"buildingTypes": ["all"],
"capabilities": { "provides": [], "requires": [] },
"permissions": [
{
"subject": "building.spaces",
"actions": ["read"],
"reason": "Выводит список помещений на главной странице модуля."
}
],
"events": { "publishes": [], "subscribes": [] },
"mcpTools": [],
"ui": {
"remoteEntry": "./Shell",
"routes": [{ "path": "/hello" }],
"navigation": [
{ "label": "Hello", "icon": "Hand", "path": "/hello", "section": "operations" }
]
},
"lifecycle": {
"healthEndpoint": "/health",
"init": "on_demand",
"dependencies": []
}
}Несколько полей значат больше, чем кажется по их размеру:
id— ваша собственная npm-область, не@tv. Генератор по умолчанию ставит@tv/…; поменяйте.permissions[].reason— показывается дословно администратору, который устанавливает ваш модуль. Пишите для него, а не для себя.ui.remoteEntry— должно остаться./Shell. Оболочка ищет ровно это имя;tv-sdk check-exposesэто проверяет.lifecycle.init—on_demandзагружает модуль лениво, при обращении к его маршруту.on_bootиспользуйте, только если оболочка действительно не может стартовать без вас.
Все поля описаны в справочнике по манифесту.
3. Проверьте его
npx @tv/extension-sdk validate module-manifest.json✓ module-manifest.json is a valid module manifest.
id: @acme/module-hello
version: 1.0.0
core: >=2.0.0Если он неверен, валидатор укажет точное поле:
✗ permissions[0].subject: must be one of [building.spaces, building.elements, ...]Встройте обе проверки в CI, чтобы сломанный контракт никогда не покидал вашу машину:
# .github/workflows/manifest.yml
- run: npx @tv/extension-sdk validate module-manifest.json
- run: npx @tv/extension-sdk check-exposes module-manifest.json --config=./vite.config.tsАвтодополнение в редакторе
Начиная с SDK 1.1.0 публикуемый manifest.schema.json пригоден для проверки в редакторе: укажите $schema на установленную копию, и редактор будет подсказывать поля и подсвечивать ошибки прямо при вводе:
{
"$schema": "./node_modules/@tv/extension-sdk/manifest.schema.json",
"sdkVersion": "1.1.0",
...
}tv-sdk init-module добавляет эту строку за вас. В версиях 1.0.x схема генерировалась в режиме output и отклоняла манифесты, которые tv-sdk validate принимает, — если вы закреплены ниже 1.1.0, не указывайте $schema.
Авторитетной проверкой остаётся tv-sdk validate — именно её выполняют CI и реестр.
4. Соберите фронтенд
Ваш модуль экспортирует федеративный Shell. Внутри него используйте контекст платформы:
// src/Shell.tsx
import { usePlatformContext, useBuilding } from '@tv/extension-sdk/react';
import { useQuery } from '@tanstack/react-query';
export default function Shell() {
const { api } = usePlatformContext();
const building = useBuilding();
const { data: spaces } = useQuery({
queryKey: ['spaces', building.id],
queryFn: () => api.get<Space[]>(`/api/v1/buildings/${building.id}/spaces`),
});
return (
<div>
<h1>Hello from {building.name}</h1>
<p>{spaces?.length ?? 0} spaces</p>
</div>
);
}Никаких токенов, никаких URL, которые нужно собирать вручную. Клиент api предварительно аутентифицирован и ограничен областью активного арендатора.
5. (Опционально) Объявите возможность бэкенда
Если у вашего модуля есть бэкенд на NestJS:
import { ModuleCapability, RequiresLicense } from '@tv/extension-sdk/nestjs';
@ModuleCapability({ id: 'hello.greeting', version: '1.0.0' })
@Controller('api/v1/buildings/:buildingId/hello')
export class HelloController {
@RequiresLicense('@acme/module-hello')
@Get()
greet() {
return { message: 'hello' };
}
}6. Протестируйте на мок-контексте
import { createMockPlatformContext } from '@tv/extension-sdk/testing';
import { PlatformProvider } from '@tv/extension-sdk/react';
import { render, screen } from '@testing-library/react';
import Shell from './Shell';
const ctx = createMockPlatformContext({
building: {
id: 'b1',
slug: 'demo-mall',
name: 'Demo Mall',
type: 'mall',
timezone: 'Europe/Moscow',
},
});
render(
<PlatformProvider value={ctx}>
<Shell />
</PlatformProvider>,
);
expect(screen.getByText(/Demo Mall/)).toBeInTheDocument();Вы тестируете на точно той же форме контекста, что используется в продакшене — просто заполненной фейковыми данными. Никаких сюрпризов «работает у меня, ломается в проде».
7. Запустите его в песочнице
Перед публикацией прогоните модуль на настоящем (изолированном) здании:
npx @tv/extension-sdk sandbox create \
--name="hello-dev" --type=mall --storeys=3 --area-sqm=20000→ Создать песочницу — полный жизненный цикл.
Что вы только что узнали
tv-sdk init-moduleдаёт заведомо рабочую форму; не собирайте её вручную.- Манифест — это ваш контракт;
validateиcheck-exposesдолжны быть в CI. PlatformContext— ваша единственная дверь к состоянию платформы.- Мок-контекст делает тесты соответствующими продакшену.
Далее: разберитесь с PlatformContext и событиями.