# Пошаговое руководство: как развернуть self-hosted голосового агента Lampline и импортировать агента из Dialogflow CX

> AI Release · @ai_release1 · https://ai-release.org/guides/poshagovoe-rukovodstvo-kak-razvernut-self-hosted-goloso.html

## TL;DR

- Время выполнения: 60–90 минут
- Уровень сложности: Intermediate
- Понадобится:
  - Linux-сервер (Ubuntu 22.04 LTS или совместимый дистрибутив), root/sudo, статический IP
  - Docker Engine и плагин Docker Compose v2 (проверка: `docker compose version`)
  - `git`, `curl`, `jq`, `python3`, `zip`/`unzip`
  - Домен `<ваш-домен>` и TLS-сертификат (например, Let's Encrypt) для HTTPS
  - Проект Google Cloud с агентом Dialogflow CX и роль `Dialogflow API Admin` (или Owner)
  - Установленный `gcloud` CLI для вызова REST API Dialogflow
  - Экспорт агента из Dialogflow CX — архив `.blob`

## Требования и подготовка
![Иллюстрация к разделу «Требования и подготовка»](https://ai-release.org/guides/img/poshagovoe-rukovodstvo-kak-razvernut-self-hosted-goloso-ru-1.jpg)


- Проверьте, что сервер не занят другими сервисами на портах 80/443 и внутреннем порту приложения (в примерах — 8000; точный порт Lampline сверьте с документацией).
- Создайте DNS-запись `A`: `<ваш-домен>` → IP сервера. Без неё не выпустится сертификат.
- Заведите сервисный аккаунт Google Cloud, скачайте JSON-ключ и положите его на сервер в `./secrets/gcp-sa.json` с правами `600`.
- Проверьте свободное место: минимум 20 ГБ под образы и данные (`df -h /`).
- Термины: **Dialogflow CX** — облачный конструктор диалоговых агентов от Google; **`.blob`** — ZIP-архив с описанием агента (файл `agent.json`, интенты, флоу); **reverse proxy** — сервер-«приёмная», который принимает HTTPS на 443 и передаёт запрос внутрь на порт приложения.

Если сервер у вас домашний, а не облачный, сначала пройдите [как подключить ИИ к домашнему серверу](https://ai-release.org/guides/kak-podklyuchit-ii-k-domashnemu-serveru-poshagovoe-ruko.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide) — там разобраны проброс портов и базовая изоляция сервисов.

## Шаг 1. Подготовка хоста и секретов

**Что сделать:** установите Docker, утилиты и создайте каталог проекта с файлом переменных окружения.

**Зачем:** все компоненты Lampline распространяются как контейнеры; переменные окружения описывают домен, токен API и доступ к Google Cloud, чтобы не хардкодить секреты в конфиг.

```bash
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg git jq python3 unzip
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker

mkdir -p ~/lampline/secrets ~/lampline/data
cd ~/lampline
```

**Пример:** файл `~/lampline/.env`:

```bash
LAMPLINE_DOMAIN=<ваш-домен>
LAMPLINE_PUBLIC_URL=https://<ваш-домен>
LAMPLINE_API_TOKEN=<токен-из-панели-или-сгенерированный>
LAMPLINE_HTTP_PORT=8000
GOOGLE_APPLICATION_CREDENTIALS=/run/secrets/gcp-sa.json
DIALOGFLOW_PROJECT_ID=<project-id>
DIALOGFLOW_LOCATION=<region, например europe-west1>
```

**Как проверить:**

```bash
docker compose version
ss -ltnp | grep -E ':(80|443|8000)\b' || echo "порты свободны"
ls -l secrets/gcp-sa.json
```

Ожидаемый результат: строка вида `Docker Compose version v2.x`, сообщение `порты свободны` и файл ключа с правами `-rw-------`. Имена переменных `LAMPLINE_*` — плейсхолдеры: точный список сверьте с документацией Lampline, раздел конфигурации.

Почему это работает: Docker читает `.env` при каждом `docker compose up`, поэтому смена домена или токена не требует пересборки образа — достаточно перезапуска.

## Шаг 2. Развёртывание Lampline через Docker Compose

**Что сделать:** опишите сервис в `docker-compose.yml`, поднимите контейнер и заверните его в reverse proxy с TLS.

**Зачем:** Compose фиксирует порты, тома и монтирование ключа Google в одном файле — это воспроизводимо при переустановке и переносе на другой сервер.

```yaml
services:
  lampline:
    image: <registry>/lampline:<тег-из-документации>
    env_file: .env
    ports:
      - "8000:8000"
    volumes:
      - ./data:/var/lib/lampline
      - ./secrets/gcp-sa.json:/run/secrets/gcp-sa.json:ro
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8000/"]
      interval: 30s
      timeout: 5s
      retries: 3
```

```bash
cd ~/lampline
docker compose pull
docker compose up -d
docker compose ps
```

**Пример:** успешный `docker compose ps` показывает `lampline ... Up (healthy)` — статус `healthy` означает, что внутренний healthcheck прошёл и приложение отвечает на HTTP.

Дальше настройте Nginx как reverse proxy: `proxy_pass http://127.0.0.1:8000;` для `<ваш-домен>` и сертификат от Let's Encrypt (например, `certbot --nginx -d <ваш-домен>`). Имя образа, тег и внутренний порт сверьте с документацией Lampline — универсальных значений здесь нет.

**Как проверить:**

```bash
curl -sS -o /dev/null -w "%{http_code}\n" https://<ваш-домен>/
docker compose logs --tail=50 lampline | grep -i -E "error|listen"
```

Ожидаемый результат: код `200` или `401` (если включена авторизация — это нормально) и строка лога о старте сервера без `error`. Код `502` означает, что Nginx жив, а контейнер — нет.

## Шаг 3. Экспорт агента из Dialogflow CX и импорт в Lampline
![Иллюстрация к разделу «Шаг 3. Экспорт агента из Dialogflow CX и импорт в Lampline»](https://ai-release.org/guides/img/poshagovoe-rukovodstvo-kak-razvernut-self-hosted-goloso-ru-2.jpg)


**Что сделать:** выгрузите агента из Dialogflow CX в архив `.blob`, проверьте его локально и загрузите в Lampline.

**Зачем:** экспорт — единственный поддерживаемый способ перенести интенты, флоу и сущности без ручного пересоздания; локальная проверка отсекает битый архив до загрузки.

Экспорт в консоли: откройте агента → **Manage** → **Export agent** → выберите формат экспорта → скачайте файл. Точные названия пунктов меню сверьте с актуальной документацией Dialogflow CX — Google периодически их меняет.

Проверка архива перед импортом:

```python
import json, zipfile

with zipfile.ZipFile("agent.blob") as z:
    names = z.namelist()
    print(len(names), "файлов в архиве")
    agent = json.loads(z.read("agent.json"))
    print("Агент:", agent["displayName"])
    print("Язык:", agent.get("defaultLanguageCode"))
```

**Пример** вывода: `214 файлов в архиве`, `Агент: SupportBot`, `Язык: ru`. Если `agent.json` в архиве нет — распечатайте `names[:20]` и сверьте структуру с документацией формата экспорта.

Загрузка в Lampline (путь эндпоинта и имя поля возьмите из документации Lampline):

```bash
curl -sS -X POST "https://<ваш-домен>/api/agents/import" \
  -H "Authorization: Bearer ${LAMPLINE_API_TOKEN}" \
  -F "file=@agent.blob" | jq
```

Если Lampline ожидает не файл, а уже восстановленного агента через API Google, используйте штатный метод restore Dialogflow CX — он принимает содержимое архива в base64:

```bash
TOKEN=$(gcloud auth print-access-token)
curl -sS -X POST \
  "https://${DIALOGFLOW_LOCATION}-dialogflow.googleapis.com/v3/projects/${DIALOGFLOW_PROJECT_ID}/locations/${DIALOGFLOW_LOCATION}/agents/${DIALOGFLOW_AGENT_ID}:restore" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{\"agentContent\":\"$(base64 -w0 agent.blob)\"}"
```

Почему это работает: `:restore` — стандартный метод Dialogflow CX, который принимает тот же архив, что отдаёт экспорт, и разворачивает его в указанном агенте; `base64 -w0` даёт одну строку без переносов, иначе JSON сломается.

**Как проверить:**

```bash
curl -sS "https://<ваш-домен>/api/agents" \
  -H "Authorization: Bearer ${LAMPLINE_API_TOKEN}" | jq '.[].name'
```

Ожидаемый результат: в списке появилось имя вашего агента (например, `SupportBot`), а на странице агента в UI число интентов совпадает с числом файлов интентов в архиве. Расхождение — повод смотреть лог импорта.

## Шаг 4. Проверка работы на реальном диалоге
![Иллюстрация к разделу «Шаг 4. Проверка работы на реальном диалоге»](https://ai-release.org/guides/img/poshagovoe-rukovodstvo-kak-razvernut-self-hosted-goloso-ru-3.jpg)


**Что сделать:** прогоните тестовую реплику через голосовой канал и сверьте ответ агента с исходным сценарием в Dialogflow.

**Зачем:** успешный импорт не гарантирует, что маршруты (routes) и сущности доехали — проверяется только живым запросом.

```bash
curl -sS -X POST "https://<ваш-домен>/api/agents/SupportBot/query" \
  -H "Authorization: Bearer ${LAMPLINE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"text":"Как перевыпустить пропуск?"}' | jq
```

**Пример** ожидаемого ответа: `{"reply":"Уточните номер пропуска","intent":"pass_reissue","confidence":0.87}` — имя интента должно совпадать с тем, что вы видите в Dialogflow CX. Если в ответе `intent: "Default Fallback Intent"` — маршруты не перенеслись.

| Что проверяем | Где смотреть | Ожидаемый результат |
|---|---|---|
| Контейнер | `docker compose ps` | `Up (healthy)` |
| HTTPS | `curl -I https://<ваш-домен>` | `HTTP/2 200` или `401` |
| Агент в списке | `GET /api/agents` | имя агента из Dialogflow |
| Интенты | UI агента | число = числу файлов в архиве |
| Диалог | `POST /query` | ответ по нужному интенту |

Для постоянного наблюдения за сервером и контейнерами удобно поднять бота — инструкция в гайде [как настроить мониторинг VPS через Telegram-бота с Mini App](https://ai-release.org/guides/kak-nastroit-monitoring-vps-cherez-telegram-bota-s-mini.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide). Точные пути эндпоинтов `query` и `agents` сверьте с документацией Lampline.

## Возможные проблемы и решения

**Проблема. Импорт отклонён с отказом доступа к Dialogflow**

- Ошибка: `Permission 'dialogflow.agents.restore' denied on resource`
- Причина: у сервисного аккаунта нет роли `Dialogflow API Admin` в проекте `${DIALOGFLOW_PROJECT_ID}` или ключ от другого проекта.
- Решение: выдать роль и проверить, какой проект реально используется.

```bash
gcloud projects get-iam-policy ${DIALOGFLOW_PROJECT_ID} \
  --flatten="bindings[].members" \
  --filter="bindings.members:<sa-email>" --format="value(bindings.role)"
python3 -c "import json;print(json.load(open('secrets/gcp-sa.json'))['project_id'])"
```

Нужна строка с ролью `roles/dialogflow.admin`, а `project_id` в ключе должен совпадать с `DIALOGFLOW_PROJECT_ID`.

**Проблема. Архив не принимается как содержимое агента**

- Ошибка: `INVALID_ARGUMENT: Agent content is not a valid blob`
- Причина: загружен не тот файл (например, CSV интентов или JSON отдельного флоу) либо base64 с переносами строк.
- Решение: убедитесь, что это ZIP, и кодируйте без переносов.

```bash
unzip -l agent.blob | head
base64 -w0 agent.blob | wc -l   # должно быть 1 (одна строка)
```

**Проблема. Контейнер падает сразу после старта**

- Ошибка: `standard_init_linux.go:228: exec user process caused: exec format error`
- Причина: образ собран под другую архитектуру (amd64 на ARM-сервере или наоборот).
- Решение: подтянуть тег под архитектуру хоста или указать платформу явно.

```bash
uname -m
docker compose down && docker compose pull && docker compose up -d
```

Если `uname -m` вернул `aarch64`, а в реестре только amd64-образ, добавьте в сервис `platform: linux/amd64` — но это медленнее, чем нативный образ.

## Резюме и следующие шаги

Вы развернули Lampline в Docker, закрыли его HTTPS-прокси и перенесли агента из Dialogflow CX архивом `.blob`, проверив импорт тестовым запросом. Дальше настройте резервное копирование каталога `./data` и подключите голосовой канал (STT/TTS) — параметры сверьте с документацией Lampline.

## FAQ

**Нужна ли GPU для self-hosted голосового агента?**
Для контейнерного развёртывания Lampline GPU не требуется; она нужна только локальным моделям распознавания и синтеза речи. Требования к ресурсам под конкретный голосовой движок сверьте с документацией.

**Чем `.blob` отличается от обычного JSON-экспорта?**
`.blob` — это ZIP-архив: внутри `agent.json` с настройками агента и отдельные файлы интентов, флоу и сущностей. Именно его принимает метод `:restore`.

**Можно ли тем же способом перенести агента из Dialogflow ES?**
Нет. ES и CX — разные форматы и разные API; архив CX не развернётся в ES-агенте. Порядок миграции между версиями сверьте с документацией Google.

**Что будет, если импортировать агента повторно?**
Повторный импорт перезаписывает содержимое целевого агента. Безопаснее загружать в нового агента и переключать трафик после проверки.
