Skip to content

Локальная разработка

📋 Быстрый старт: См. QUICK_START.md для пошаговой инструкции

Требования

  • Node.js >= 20
  • pnpm (npm install -g pnpm)
  • Docker Desktop
  • Doppler CLI (brew install dopplerhq/cli/doppler)
  • GitHub CLI (brew install gh) — для авторизации в GHCR
  • cloudflared (опционально) — для публичного доступа к локальному gateway через tunnel

Архитектура локальной разработки

┌─────────────────────────────────────────────────────────────────┐
│                    Локальная разработка                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   ┌─────────────┐    ┌─────────────┐    ┌─────────────┐         │
│   │ integ-admin │───▶│  integ-api  │───▶│   Gateway   │         │
│   │             │    │  :3005      │    │   :3010     │         │
│   └─────────────┘    └──────┬──────┘    └──────┬──────┘         │
│                             │                   │                │
│                             │ SQLite            │ HTTP           │
│                             ▼                   ▼                │
│                      ┌─────────────┐    ┌─────────────┐         │
│                      │ data/       │◀───│   Sofa      │         │
│                      │ miniflare/  │    │   :8787     │         │
│                      │ (D1 + KV)   │    └─────────────┘         │
│                      └─────────────┘                            │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Локальная инфраструктура

D1 и KV (Miniflare)

D1 и KV эмулируются через wrangler dev (miniflare встроен внутри).

База данных:

  • DB (integ-db) — единая база данных (credentials + данные интеграций)

KV:

  • KV — KV хранилище

Persistence: Данные сохраняются в data/miniflare/ и сохраняются между перезагрузками.

Важно: Gateway и интеграции используют одну директорию data/miniflare/ для D1/KV, что позволяет им работать с общими данными.

Подключение integ-api

integ-api в локальном режиме работает с SQLite файлами напрямую (не через HTTP):

bash
# integ-api .env.local
ENVIRONMENT=local
INTEG_CORE_URL=http://localhost:3010
D1_LOCAL_PATH=/path/to/integ-core/data/miniflare

В production integ-api использует Cloudflare D1 REST API.

Быстрый старт

Полная пошаговая инструкция: QUICK_START.md

bash
# 1. Первоначальная настройка (один раз)
pnpm install && pnpm build
doppler login && doppler setup  # выбрать integ-core / local
gh auth token | docker login ghcr.io -u $(gh api user -q .login) --password-stdin
pnpm generate:env -- local
pnpm setup:local

# 2. Ежедневный запуск
pnpm docker:local               # Docker: postgres, integ-api, integ-admin
pnpm start gateway              # Терминал 1
pnpm start sofa                 # Терминал 2

# 3. Обновление Docker-образов
docker compose pull && docker compose up -d

Архитектура с Docker:

┌─────────────────── Docker Network ───────────────────┐
│                                                      │
│  ┌─────────────┐    ┌─────────────┐    ┌──────────┐ │
│  │integ-admin  │───▶│ integ-api   │───▶│ postgres │ │
│  │   :4205     │    │   :3005     │    │  :5435   │ │
│  └─────────────┘    └─────────────┘    └──────────┘ │
│                                                      │
└──────────────────────────────────────────────────────┘
                             │ host.docker.internal

┌──────────────────── Host Machine ────────────────────┐
│  Gateway :3010  ───▶  Sofa :8787  ───▶  D1/KV       │
│  (pnpm start)        (pnpm start)      (miniflare)   │
└──────────────────────────────────────────────────────┘

Доступные сервисы:

СервисURL
PostgreSQLlocalhost:5435
integ-apihttp://localhost:3005
integ-adminhttp://localhost:4205

Полезные команды:

bash
# Запустить только инфраструктуру (postgres)
docker compose up -d postgres

# Просмотр логов
docker compose logs -f happ-integ-api
docker compose logs -f happ-integ-admin

# Остановка всех сервисов
docker compose down

# Обновить образы до последней версии
docker compose pull
docker compose up -d

Альтернатива (без Docker): Можно запустить integ-api и integ-admin напрямую:

bash
# Терминал 3 - integ-api
cd ../integ-api
ENVIRONMENT=local npm start

# Терминал 4 - integ-admin
cd ../integ-admin
npm start

Документация (локально)

bash
pnpm docs:dev

Документация будет доступна на http://localhost:5173

Endpoints при локальном запуске

1. Gateway (Port 3010)

bash
pnpm start gateway

Endpoints:

MethodURLОписание
GET/healthПроверка здоровья сервиса
POST/sofa/webhook/webinar-call-originateИнициация звонка
POST/sofa/webhook/webinar-agent-initИнициализация агента
POST/sofa/webhook/webinar-call-eventsСобытия звонка
POST/sofa/webhook/webinar-agent-post-callPost-call обработка

Примеры:

bash
# Health check
curl http://localhost:3010/health

# Webhook (sofa) — инициация звонка
curl -X POST http://localhost:3010/sofa/webhook/webinar-call-originate \
  -H "Content-Type: application/json" \
  -d '{"phone_number":"+380991234567"}'

2. Integration Worker (Port 8787)

bash
pnpm start sofa

Endpoints:

MethodURLОписание
POST/sofa/webhook/webinar-call-originateИнициация звонка
POST/sofa/webhook/webinar-agent-initИнит. агента
POST/sofa/webhook/webinar-call-eventsСобытия звонка
POST/sofa/webhook/webinar-agent-post-callPost-call

Примеры:

bash
# Webhook (обход Gateway) — инициация звонка
curl -X POST http://localhost:8787/sofa/webhook/webinar-call-originate \
  -H "Content-Type: application/json" \
  -d '{"phone_number":"+380991234567"}'

Создание новой интеграции

1. Генерация из шаблона

bash
pnpm generate:integration my-integration

2. Структура созданной интеграции

integrations/my-integration/
├── src/
│   ├── index.ts           # Hono app
│   ├── types.ts           # TypeScript типы
│   └── handlers/
│       └── init.ts        # Начальный handler
├── package.json
├── tsconfig.json
└── wrangler.toml          # Генерируется автоматически!

3. Генерация wrangler.toml

wrangler.toml генерируется автоматически из единого шаблона templates/wrangler.integration.toml.tpl.

bash
# Генерация для локальной разработки
pnpm generate:env -- local

# Генерация для dev (CI/CD)
pnpm generate:env -- dev

# Генерация для prod (CI/CD)
pnpm generate:env -- prod

Что происходит:

  • Скрипт автоматически находит все интеграции в integrations/
  • Генерирует wrangler.toml для каждой из единого шаблона
  • Подставляет правильные ID (D1, KV) для выбранного окружения
  • Service bindings в Gateway генерируются автоматически

Пример сгенерированного wrangler.toml (local):

toml
name = "integ-my-integration"
main = "dist/index.js"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]

[vars]
ENVIRONMENT = "local"

[[d1_databases]]
binding = "INTEG_DB"
database_name = "integ-db"
database_id = "00000000-0000-0000-0000-000000000001"

[[kv_namespaces]]
binding = "INTEG_KV"
id = "00000000000000000000000000000002"

Важно: wrangler.toml в integrations/ и workers/ добавлены в .gitignore — они генерируются автоматически и не коммитятся.

4. Создание .dev.vars

bash
# integrations/my-integration/.dev.vars
ENVIRONMENT=local
DEBUG=1

# Encryption key (min 32 chars) and salt (min 16 chars)
CRYPTO_KEY=your-32-char-master-key-here!!!
CRYPTO_SALT=your-16-char-salt-here

# LLM (если нужен)
OPENAI_API_KEY=sk-...
CLAUDE_API_KEY=sk-ant-...
GEMINI_API_KEY=...
GROQ_API_KEY=gsk_...

Добавление handler-а

1. Создать файл

typescript
// integrations/my-integration/src/handlers/my-action.ts
import { defineHandler } from "@happ-integ/core";
import type { IMyActionPayload } from "../types";

export const myActionHandler = defineHandler<IMyActionPayload>({
	name: "my-action",
	endpoint: "POST /webhook/my-action",
	async handler({ payload, step }) {
		const result = await step.run("do-something", () => doSomething(payload));
		return { success: true, data: result };
	},
});

2. Добавить типы

typescript
// integrations/my-integration/src/types.ts
export interface IMyActionPayload {
	recordId: string;
	phone?: string;
}

3. Создать handler

typescript
// integrations/my-integration/src/handlers/my-action.ts
import { defineHandler } from "@happ-integ/core";
import type { IMyActionPayload, IMyActionResponse } from "../types";

export const myActionHandler = defineHandler<IMyActionPayload, IMyActionResponse>({
	name: "my-action",
	retries: 3,
	endpoint: "POST /webhook/my-action",
	async handler({ payload, step, db, creds, integrationName }) {
		// Business logic with step.run() for durability
		return { success: true };
	},
});

Использование пакетов

CRM клиент

typescript
import { NethuntClient } from "@happ-integ/nethunt";

const nethunt = new NethuntClient(email, apiKey);
const record = await nethunt.getRecord(recordId);
await nethunt.updateRecord(recordId, { fields: { status: "done" } });

LLM с fallback

typescript
import { LLMService } from "@happ-integ/llm";

const llm = new LLMService({
	primary: "groq",
	secondary: "claude",
	env, // передаем env для автоматического получения ключей
});

const response = await llm.chat({
	messages: [{ role: "user", content: "Hello" }],
});
console.log(response.text);

Database и Cache

typescript
import { DB } from "@happ-integ/db";
import { Cache } from "@happ-integ/cache";

// Lazy init
let _db: DB | null = null;
export const getDb = (env) =>
	(_db ??= new DB({
		provider: "d1",
		d1: env.DB,
		project: "my-integration",
	}));

// Использование
const db = getDb(env);
await db.insert("calls", { id: "123", status: "pending" });
const calls = await db.select("calls", { status: "pending" });

Credentials

typescript
import { Creds } from "@happ-integ/creds";

const creds = new Creds({
	d1: env.DB,
	masterKey: env.CRYPTO_KEY,
	salt: env.CRYPTO_SALT,
});

interface MyCreds {
	API_KEY: string;
	SECRET: string;
}
const secrets = await creds.get<MyCreds>("my-integration");
console.log(secrets.API_KEY);

Тестирование

Запуск тестов

bash
# Все тесты
pnpm test

# Конкретная интеграция
pnpm --filter sofa test

# С coverage
pnpm test -- --coverage

# Watch mode
pnpm test -- --watch

Написание тестов

typescript
// src/handlers/my-action.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { handleMyAction } from "./my-action";

// Mock клиентов
vi.mock("../index", () => ({
	getDb: vi.fn(() => ({
		select: vi.fn().mockResolvedValue([]),
		insert: vi.fn().mockResolvedValue(undefined),
	})),
}));

describe("handleMyAction", () => {
	beforeEach(() => {
		vi.clearAllMocks();
	});

	it("should return success", async () => {
		const result = await handleMyAction({ recordId: "123" });
		expect(result.success).toBe(true);
	});
});

Полезные команды

Запуск (start / debug)

КомандаНазначение
pnpm start sofaЗапуск интеграции sofa (LOG_LEVEL=info)
pnpm start gatewayЗапуск Gateway Worker (LOG_LEVEL=info)
pnpm debug sofaЗапуск с verbose-логами (LOG_LEVEL=debug)
pnpm debug gatewayGateway с verbose-логами (LOG_LEVEL=debug)
pnpm start gateway --tunnelGateway + cloudflared quick tunnel
pnpm start gateway --tunnel=slavaGateway + named tunnel (integ.slava.happ.tools)

Docker

КомандаНазначение
pnpm docker:localЗапустить полный Docker-стек
docker compose --env-file .env.docker pullОбновить образы из GHCR
docker compose --env-file .env.docker up -dЗапустить Docker-сервисы
docker compose downОстановить все контейнеры
docker compose logs -f happ-integ-apiЛоги integ-api

Генерация (generate:*)

КомандаНазначение
pnpm generate:env -- localСгенерировать env из Doppler
pnpm generate:env -- devСгенерировать env для dev
pnpm generate:env -- prodСгенерировать env для prod
pnpm generate:integration <name>Создать новую интеграцию
pnpm generate:package <name>Создать новый пакет

Настройка (setup:*)

КомандаНазначение
pnpm setup:localСоздать локальные D1/KV базы
pnpm setup:resetСбросить и пересоздать локальные БД

Разработка

КомандаНазначение
pnpm buildСборка всех пакетов
pnpm devЗапуск всех в dev режиме
pnpm testЗапуск тестов
pnpm lintПроверка линтером
pnpm formatФорматирование кода
pnpm typecheckПроверка типов
pnpm clean:codeУдалить комментарии и return types

Документация

КомандаНазначение
pnpm docs:devЗапуск документации локально
pnpm docs:buildСборка документации
pnpm docs:deploy:devДеплой документации в dev
pnpm docs:deploy:prodДеплой документации в production

Troubleshooting

"pnpm: command not found"

bash
npm install -g pnpm

"Can't resolve @happ-integ/..."

bash
pnpm build
# или
rm pnpm-lock.yaml && pnpm install

D1 Database errors

bash
# Создать D1 базы
pnpm setup:local

# Сбросить и пересоздать D1
pnpm setup:reset

Как работает локальная D1

setup:local создаёт SQLite файлы в data/miniflare/v3/d1/miniflare-D1DatabaseObject/.

Алгоритм именования файлов (miniflare):

typescript
// Miniflare использует HMAC-based алгоритм для генерации имени файла
const uniqueKey = "miniflare-D1DatabaseObject";
const key = sha256(uniqueKey);
const nameHmac = HMAC-SHA256(key, database_id).slice(0, 16);
const hmac = HMAC-SHA256(key, nameHmac).slice(0, 16);
const filename = concat(nameHmac, hmac).hex() + ".sqlite";

ID баз данных берутся из .env файла (генерируется из Doppler):

  • CLOUDFLARE_INTEG_DB_ID — ID для D1
  • CLOUDFLARE_INTEG_KV_ID — ID для KV

"D1Database is required"

  • Проверить wrangler.toml — binding должен быть DB
  • Убедиться что D1 база создана

"KVNamespace is required"

bash
# Создать KV namespaces для dev и prod
wrangler kv namespace create integ-kv-dev
wrangler kv namespace create integ-kv-prod

Добавить ID в Doppler:

  • dev config: KV_ID=<UUID от integ-kv-dev>
  • prod config: KV_ID=<UUID от integ-kv-prod>

Затем перегенерировать wrangler.toml:

bash
pnpm generate:env -- dev   # или prod

Cloudflared Tunnel (публичный доступ)

Для тестирования вебхуков от реальных агентов можно поднять публичный tunnel до локального gateway через cloudflared.

Установка cloudflared

bash
# macOS
brew install cloudflared

# Linux
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
chmod +x cloudflared
sudo mv cloudflared /usr/local/bin/

Типы туннелей

ТипURLИспользование
Quick tunnelhttps://random-words.trycloudflare.comБыстрый тест, URL меняется при каждом запуске
Named tunnelhttps://integ.<name>.happ.toolsПостоянный URL для разработчика

Quick Tunnel (случайный URL)

bash
pnpm start gateway --tunnel

Вывод:

🌐 Starting cloudflared quick tunnel...

╔══════════════════════════════════════════════════════════════════╗
║  🚇 Tunnel URL: https://random-words-here.trycloudflare.com      ║
╚══════════════════════════════════════════════════════════════════╝

Named Tunnel (статический URL)

Для постоянного URL используйте named tunnel:

bash
pnpm start gateway --tunnel=slava

Вывод:

🌐 Starting cloudflared named tunnel "slava"...

╔══════════════════════════════════════════════════════════════════╗
║  🚇 Tunnel URL: https://integ.slava.happ.tools                   ║
╚══════════════════════════════════════════════════════════════════╝

Создание нового named tunnel (для админа)

Для создания нового туннеля разработчику:

bash
# 1. Авторизация в Cloudflare (один раз)
cloudflared tunnel login

# 2. Создание туннеля
cloudflared tunnel create <name>

# 3. Настройка DNS
cloudflared tunnel route dns <name> integ.<name>.happ.tools

# 4. Проверка
cloudflared tunnel list

Существующие туннели:

TunnelURLCredentials файл
slavahttps://integ.slava.happ.tools1999280f-05fe-42c6-aa9d-446f06efe37b.json
sergeyhttps://integ.sergey.happ.toolsce47ba72-f6ec-41d6-b06a-43c58e734d63.json
nikitahttps://integ.nikita.happ.tools931660b1-c7c0-4175-8b60-20ae54740b67.json
vladhttps://integ.vlad.happ.toolse9be2f78-3e45-4721-bc9a-f50b80243846.json
artemhttps://integ.artem.happ.tools5d86c1ba-14f7-46a6-bb50-78a017508a2c.json
furmanhttps://integ.furman.happ.tools44d0e7e5-9e9b-4879-a4f2-da85c8f010a3.json

Получение credentials: Запросите у админа файл <tunnel-id>.json и положите его в ~/.cloudflared/

Использование

  1. Запустить pnpm start gateway --tunnel=slava (или --tunnel для quick tunnel)
  2. URL туннеля появится в консоли
  3. Настроить агента на отправку вебхуков на https://<tunnel-url>/sofa/webhook/<handler-name>
  4. Вебхуки будут приходить на локальный gateway

Проверка работы

bash
# Health check через tunnel
curl https://integ.slava.happ.tools/health

# Webhook через tunnel — инициация звонка
curl -X POST https://integ.slava.happ.tools/sofa/webhook/webinar-call-originate \
  -H "Content-Type: application/json" \
  -d '{"phone_number":"+380991234567"}'

Troubleshooting

"tunnel not found" — туннель не создан или неправильное имя. Проверьте cloudflared tunnel list.

"failed to connect" — нужна авторизация: cloudflared tunnel login

Debug Mode

Для verbose-логирования (видны debug-сообщения, step :input/:output) используйте pnpm debug:

bash
pnpm debug sofa       # LOG_LEVEL=debug — все логи включая debug
pnpm debug gateway    # LOG_LEVEL=debug — все логи gateway
pnpm start sofa       # LOG_LEVEL=info (по умолчанию) — только info+
pnpm start gateway    # LOG_LEVEL=info (по умолчанию) — только info+

Для отладки webhook-запросов с сохранением payload'ов в файлы:

bash
pnpm start sofa --debug

Что происходит при --debug:

  1. Устанавливается DEBUG_MODE=1 в .dev.vars
  2. Создается папка logs/ (добавлена в .gitignore)
  3. Все входящие webhook payload'ы сохраняются в JSON файлы

Что происходит при pnpm debug:

  1. Устанавливается LOG_LEVEL=debug через environment
  2. Trace-система показывает debug-логи (step :input/:output)
  3. Logger в packages выводит все уровни

Пример лога

📝 Saved debug log: logs/2026-02-03T12-47-33-170Z_webinar-call-events.json

Содержимое файла:

json
{
  "timestamp": "2026-02-03T12:47:33.170Z",
  "traceId": "17daaa44-0ad7-48d6-81a8-35c6d98f7e45",
  "handler": "webinar-call-events",
  "payload": {
    "callId": "123",
    "eventType": "media_start",
    "clientPhone": "+380123456789"
  }
}

Просмотр логов

bash
# Последние логи
ls -la logs/

# Просмотр конкретного файла
cat logs/2026-02-03T12-47-33-170Z_webinar-call-events.json | jq

Важно: Debug mode работает только локально. В dev/prod переменная DEBUG_MODE не устанавливается.


Связанные документы