Веб-сервис для создания и редактирования JSON-шаблонов Modbus-устройств для драйвера wb-mqtt-serial (Wiren Board).
- Автоматическая генерация через LLM — загрузите документацию устройства (PDF, Excel, изображение с таблицей регистров), и LLM извлечёт все Modbus-регистры, определит типы каналов, параметры, единицы измерения и enum-значения
- Создание шаблона вручную — добавляйте регистры по одному через визуальный редактор: адрес, тип, формат, масштаб, группа, enum и другие свойства
- Импорт существующих шаблонов — загрузите готовый
.jsonили.json.jinjaшаблон wb-mqtt-serial для редактирования и доработки - Визуальный редактор — таблица регистров с inline-редактированием, drag-n-drop сортировкой, группировкой, CSV импортом/экспортом
- Превью шаблона — интерактивное превью справа показывает как устройство будет выглядеть в интерфейсе Wiren Board (каналы, параметры, переключатели, слайдеры)
- Мультиязычность — переводы названий каналов и enum-значений на любые языки, автоперевод через LLM
- Экспорт — скачивание готового
.jsonшаблона или.json.jinja(с автоматическим обнаружением повторяющихся паттернов и сворачиванием в{% for %}циклы)
cp env.example .env
# Отредактируйте .env — укажите LLM_API_KEY и LLM_API_URL
docker compose up --build -d
# Откройте http://localhost:8080- Загрузите PDF / Excel / изображение с таблицей Modbus-регистров
- Выберите тип шаблона (Small / Medium / Full)
- LLM проанализирует документ и извлечёт регистры
- Доработайте результат в визуальном редакторе
- Скачайте готовый
.jsonили.json.jinjaшаблон
| Тип | Описание |
|---|---|
| Small | 10-30 основных каналов для измерений и управления |
| Medium | Каналы + параметры конфигурации устройства |
| Full | Все регистры устройства без фильтрации |
┌─────────┐ ┌─────────┐ ┌─────────────┐
Браузер ──────>│ nginx │────>│ FastAPI │────>│ OpenAI API │
│ :8080 │ │ :8000 │ │ (любой LLM) │
└─────────┘ └─────────┘ └─────────────┘
frontend backend
- Frontend: React 18 + TypeScript + Vite + Zustand + Tailwind CSS v4
- Backend: Python 3.12 + FastAPI + uvicorn
- LLM: Любой OpenAI-совместимый API (OpenAI, Anthropic, локальный)
- PDF: уходит в модель файлом как есть
- Excel: openpyxl, изображения — Pillow
- Контейнеризация: Docker Compose (nginx + uvicorn)
backend/
main.py # FastAPI: эндпоинты, middleware, очереди, rate limiting
config.py # Настройки из .env (pydantic-settings)
models.py # Pydantic-модели (Register, BuildRequest и т.д.)
llm_service.py # LLM-интеграция: анализ документов, автофикс регистров
template_builder.py # Детерминированная сборка JSON-шаблона
template_importer.py # Импорт существующих .json/.json.jinja шаблонов
jinja_exporter.py # Экспорт в .json.jinja с детекцией for-паттернов
file_converter.py # Excel -> text, изображения -> base64 с потолками на разбор
prompts.py # Системные промпты для LLM
sse.py # SSE-события (progress, result, done, error)
request_context.py # ContextVar для request_id (трейсинг)
queue_manager.py # In-memory очередь на asyncio.Semaphore
pyproject.toml # Конфиг ruff, mypy, pytest
tests/ # pytest-тесты + фикстуры
frontend/src/
App.tsx # Главный компонент (редактор)
api.ts # API-клиент (SSE, REST)
store.ts # Zustand store — всё состояние приложения
types.ts # TypeScript типы
constants.ts # Форматы, единицы, языки
components/ # UI-компоненты редактора
.github/workflows/
ci.yml # CI: ruff, mypy, pytest, eslint, tsc
| Метод | Путь | Описание |
|---|---|---|
| POST | /api/analyze |
SSE — анализ документа через LLM |
| POST | /api/build |
Сборка JSON-шаблона из регистров |
| POST | /api/build-jinja |
Сборка Jinja-шаблона (.json.jinja) |
| POST | /api/import-template |
Импорт .json / .json.jinja |
| POST | /api/translate |
Перевод строк через LLM |
| POST | /api/models |
Список моделей LLM API |
| GET | /api/status |
Статус сервера (LLM, лимиты) |
| GET | /api/health |
Healthcheck (uptime, очереди) |
| GET | /api/queue-status |
Состояние очередей |
| GET | /api/metrics |
Метрики (счётчики, гистограммы) |
event: progress -> {stage, message, current?, total?, request_id, queue_position?, queue_eta?}
event: result -> {request_id, device_info, registers}
event: done -> {message, request_id}
event: error -> {message, request_id}
Стадии прогресса: queued -> uploading -> converting -> analyzing -> merging -> validating -> autofix? -> done/error.
Стадия slow не звено цепочки, а замена analyzing после мягкого таймаута (LLM_SOFT_TIMEOUT, по умолчанию 3 мин): анализ продолжается, но пользователю предлагается подождать или отменить. Стадия autofix появляется только если валидация нашла ошибки.
Все настройки через переменные окружения (.env). См. env.example.
| Переменная | По умолчанию | Описание |
|---|---|---|
LLM_API_URL |
(пусто) | URL OpenAI-совместимого API. Пусто = анализ недоступен, пока пользователь не укажет свой LLM в настройках |
LLM_API_KEY |
(пусто) | API-ключ |
LLM_MODEL |
gpt-5.6-luna |
Модель (gpt-5.4-mini — ещё дешевле, gpt-5.5 — дороже, качество то же) |
LLM_MAX_TOKENS |
0 |
0 = без ограничения, >0 = лимит токенов |
LLM_LEGACY_MAX_TOKENS |
false |
true = max_tokens (старые API), false = max_completion_tokens |
LLM_TIMEOUT |
600 |
Жёсткий таймаут HTTP-запроса к LLM (сек) |
LLM_SOFT_TIMEOUT |
180 |
Мягкий таймаут — предложить продлить (сек) |
LLM_TEMPERATURE |
(пусто) | Пусто = дефолт модели (нужно для gpt-5.x); 0 = детерминизм для gpt-4o/локальных |
LLM_PROXY |
(пусто) | HTTP/SOCKS5 прокси для запросов к LLM API |
LLM_ALLOW_PRIVATE_URLS |
false |
Разрешить пользовательский адрес LLM во внутренней сети |
MAX_REQUEST_SIZE_MB |
2 |
Потолок на запрос целиком (МБ), не на файл |
MAX_FILES |
10 |
Максимум файлов в одном запросе на анализ |
| Переменная | По умолчанию | Описание |
|---|---|---|
QUEUE_SERVER_MAX_CONCURRENT |
15 |
Параллельных запросов к серверному LLM |
QUEUE_CUSTOM_MAX_CONCURRENT |
15 |
Параллельных запросов с пользовательским LLM |
QUEUE_ACTIVATION_DELAY |
1.0 |
Задержка перед стартом того, кто ждал в очереди (сек) |
RATE_LIMIT_REQUESTS |
10 |
Запросов за окно |
RATE_LIMIT_WINDOW |
60 |
Окно rate limit (сек) |
| Переменная | По умолчанию | Описание |
|---|---|---|
CORS_ORIGINS |
(пусто) | Разрешённые origins через запятую. Пусто = кросс-доменные запросы запрещены; интерфейсу CORS не нужен, он ходит через тот же origin |
LOG_FORMAT |
text |
Формат логов: text (dev) или json (prod) |
- Request ID: каждый запрос получает 8-символьный hex ID, который пробрасывается через SSE, логи и заголовок
X-Request-Id - Очереди: in-memory на
asyncio.Semaphore, раздельные для серверного и пользовательского LLM, с задержкой перед стартом того, кто ждал - Rate limiter: sliding window по IP
- Изоляция LLM: при серверном LLM пользовательский system_prompt игнорируется
- Мягкий таймаут: через 3 мин анализа пользователю предлагается продолжить или отменить
- SSE keepalive: каждые 15 сек отправляется прогресс с таймером, чтобы nginx не убивал соединение
- Метрики: in-memory счётчики и гистограммы в
/api/metrics - Security headers:
X-Content-Type-Options,X-Frame-Options,Content-Security-Policy,Referrer-Policy(в prod nginx) - JSON-логи:
LOG_FORMAT=jsonвключает структурированные логи с таймингами операций - Лог запросов: пишет своё middleware — метод, путь и request_id, без строки запроса. Access-log uvicorn отключён флагом
--no-access-log - CORS: параметризован через
CORS_ORIGINS, по умолчанию пусто — кросс-доменные запросы не разрешены никому, куки и сессии не используются (allow_credentials=False) - Валидация файлов: проверка MIME-типа и расширения загружаемых файлов (pdf, xlsx, png, jpg, jpeg, webp)
- Потолки на входе: число файлов (
MAX_FILES), размер запроса целиком (MAX_REQUEST_SIZE_MB, он же у импорта шаблона и он же у nginx вclient_max_body_size), распакованный объём и текст xlsx, число пикселей изображения, длина строк и размер списков в теле запроса
# Копируйте .env и настройте для production
cp env.example .env
# В файле .env важно добавить ваш API ключ из личного кабинета OpenAI
# Запуск через prod-конфигурацию (bridge networking, restart: always)
docker compose -f docker-compose.prod.yml up --build -ddocker-compose.prod.yml отличается от dev: bridge-сеть вместо host, restart: always, healthcheck-зависимость frontend от backend, без volume-маунтов исходников.
# Dev-режим (host networking, hot reload)
docker compose up --build -d
# Frontend: http://localhost:9080
# Backend: http://localhost:9000
# Тесты
docker compose exec backend pytest tests/ -v
# Тесты с покрытием
docker compose exec backend pytest tests/ -v --cov=. --cov-report=term
# Линтинг
docker compose exec backend ruff check .
# Проверка типов
docker compose exec backend mypy models.py template_builder.py jinja_exporter.py
# Пересборка без кеша (при изменении зависимостей)
docker compose build --no-cache && docker compose up -d
# Логи
docker compose logs -f backendGitHub Actions (ci.yml) запускается на push/PR в main:
- Backend:
ruff check,mypy,pytest --cov(порог покрытия 70%) - Frontend:
npm ci,eslint,tsc -b
Генерируемые шаблоны соответствуют формату wb-mqtt-serial.
Допустимые значения полей:
- type: value, switch, pushbutton, range, text, rgb, wo-switch, temperature, voltage, current, power, ...
- format: u16, s16, u32, s32, u64, float, string
- reg_type: holding, input, coil, discrete
- units: V, A, deg C, %, RH, Ohm, bar, ppm, W, kWh, ...
При экспорте в .json.jinja автоматически обнаруживаются повторяющиеся структуры и сворачиваются в {% for %} циклы:
- Числовые паттерны — каналы с числом в имени ("Input 1"..."Input 8") и арифметической прогрессией адресов (только числовая запись адреса — hex и побитовые каналы остаются списком)
- Строковые паттерны — каналы с одинаковой структурой, различающиеся одним словом ("Button Single Press", "Button Long Press") →
{% for val in [...] %} - Вариантные каналы — каналы с одинаковым именем но разными sporadic/condition → вложенный
{% for %}по вариантам - Переводы — повторяющиеся ключи в секции translations сворачиваются в циклы
- Шаблонизируемые поля — group, condition с варьирующимся номером заменяются на
{{ i }}
Минимум 2 элемента для обнаружения паттерна. Числовые паттерны имеют приоритет над строковыми.


