Локальная разработка
📋 Быстрый старт: См. 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):
# 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
# 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 |
|---|---|
| PostgreSQL | localhost:5435 |
| integ-api | http://localhost:3005 |
| integ-admin | http://localhost:4205 |
Полезные команды:
# Запустить только инфраструктуру (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
Документация (локально)
pnpm docs:devДокументация будет доступна на http://localhost:5173
Endpoints при локальном запуске
1. Gateway (Port 3010)
pnpm start gatewayEndpoints:
| Method | URL | Описание |
|---|---|---|
| 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-call | Post-call обработка |
Примеры:
# 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)
pnpm start sofaEndpoints:
| Method | URL | Описание |
|---|---|---|
| POST | /sofa/webhook/webinar-call-originate | Инициация звонка |
| POST | /sofa/webhook/webinar-agent-init | Инит. агента |
| POST | /sofa/webhook/webinar-call-events | События звонка |
| POST | /sofa/webhook/webinar-agent-post-call | Post-call |
Примеры:
# Webhook (обход Gateway) — инициация звонка
curl -X POST http://localhost:8787/sofa/webhook/webinar-call-originate \
-H "Content-Type: application/json" \
-d '{"phone_number":"+380991234567"}'Создание новой интеграции
1. Генерация из шаблона
pnpm generate:integration my-integration2. Структура созданной интеграции
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.
# Генерация для локальной разработки
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):
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
# 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. Создать файл
// 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. Добавить типы
// integrations/my-integration/src/types.ts
export interface IMyActionPayload {
recordId: string;
phone?: string;
}3. Создать handler
// 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 клиент
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
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
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
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);Тестирование
Запуск тестов
# Все тесты
pnpm test
# Конкретная интеграция
pnpm --filter sofa test
# С coverage
pnpm test -- --coverage
# Watch mode
pnpm test -- --watchНаписание тестов
// 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 gateway | Gateway с verbose-логами (LOG_LEVEL=debug) |
pnpm start gateway --tunnel | Gateway + cloudflared quick tunnel |
pnpm start gateway --tunnel=slava | Gateway + 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"
npm install -g pnpm"Can't resolve @happ-integ/..."
pnpm build
# или
rm pnpm-lock.yaml && pnpm installD1 Database errors
# Создать D1 базы
pnpm setup:local
# Сбросить и пересоздать D1
pnpm setup:resetКак работает локальная D1
setup:local создаёт SQLite файлы в data/miniflare/v3/d1/miniflare-D1DatabaseObject/.
Алгоритм именования файлов (miniflare):
// 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 для D1CLOUDFLARE_INTEG_KV_ID— ID для KV
"D1Database is required"
- Проверить
wrangler.toml— binding должен бытьDB - Убедиться что D1 база создана
"KVNamespace is required"
# Создать KV namespaces для dev и prod
wrangler kv namespace create integ-kv-dev
wrangler kv namespace create integ-kv-prodДобавить ID в Doppler:
devconfig:KV_ID=<UUID от integ-kv-dev>prodconfig:KV_ID=<UUID от integ-kv-prod>
Затем перегенерировать wrangler.toml:
pnpm generate:env -- dev # или prodCloudflared Tunnel (публичный доступ)
Для тестирования вебхуков от реальных агентов можно поднять публичный tunnel до локального gateway через cloudflared.
Установка cloudflared
# 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 tunnel | https://random-words.trycloudflare.com | Быстрый тест, URL меняется при каждом запуске |
| Named tunnel | https://integ.<name>.happ.tools | Постоянный URL для разработчика |
Quick Tunnel (случайный URL)
pnpm start gateway --tunnelВывод:
🌐 Starting cloudflared quick tunnel...
╔══════════════════════════════════════════════════════════════════╗
║ 🚇 Tunnel URL: https://random-words-here.trycloudflare.com ║
╚══════════════════════════════════════════════════════════════════╝Named Tunnel (статический URL)
Для постоянного URL используйте named tunnel:
pnpm start gateway --tunnel=slavaВывод:
🌐 Starting cloudflared named tunnel "slava"...
╔══════════════════════════════════════════════════════════════════╗
║ 🚇 Tunnel URL: https://integ.slava.happ.tools ║
╚══════════════════════════════════════════════════════════════════╝Создание нового named tunnel (для админа)
Для создания нового туннеля разработчику:
# 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Существующие туннели:
| Tunnel | URL | Credentials файл |
|---|---|---|
| slava | https://integ.slava.happ.tools | 1999280f-05fe-42c6-aa9d-446f06efe37b.json |
| sergey | https://integ.sergey.happ.tools | ce47ba72-f6ec-41d6-b06a-43c58e734d63.json |
| nikita | https://integ.nikita.happ.tools | 931660b1-c7c0-4175-8b60-20ae54740b67.json |
| vlad | https://integ.vlad.happ.tools | e9be2f78-3e45-4721-bc9a-f50b80243846.json |
| artem | https://integ.artem.happ.tools | 5d86c1ba-14f7-46a6-bb50-78a017508a2c.json |
| furman | https://integ.furman.happ.tools | 44d0e7e5-9e9b-4879-a4f2-da85c8f010a3.json |
Получение credentials: Запросите у админа файл
<tunnel-id>.jsonи положите его в~/.cloudflared/
Использование
- Запустить
pnpm start gateway --tunnel=slava(или--tunnelдля quick tunnel) - URL туннеля появится в консоли
- Настроить агента на отправку вебхуков на
https://<tunnel-url>/sofa/webhook/<handler-name> - Вебхуки будут приходить на локальный gateway
Проверка работы
# 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:
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'ов в файлы:
pnpm start sofa --debugЧто происходит при --debug:
- Устанавливается
DEBUG_MODE=1в.dev.vars - Создается папка
logs/(добавлена в.gitignore) - Все входящие webhook payload'ы сохраняются в JSON файлы
Что происходит при pnpm debug:
- Устанавливается
LOG_LEVEL=debugчерез environment - Trace-система показывает debug-логи (step :input/:output)
- Logger в packages выводит все уровни
Пример лога
📝 Saved debug log: logs/2026-02-03T12-47-33-170Z_webinar-call-events.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"
}
}Просмотр логов
# Последние логи
ls -la logs/
# Просмотр конкретного файла
cat logs/2026-02-03T12-47-33-170Z_webinar-call-events.json | jqВажно: Debug mode работает только локально. В dev/prod переменная DEBUG_MODE не устанавливается.
Связанные документы
- ARCHITECTURE.md — архитектура системы
- PRODUCTION.md — деплой на production
- CODE_RULES.md — правила написания кода
- CI_CD.md — GitHub Actions workflows и оптимизация
- SCRIPTS.md — утилитные скрипты для разработки
- ai/TESTING_GUIDE.md — подробнее о тестах