Skip to content

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

Обзор

Платформа integ-core предназначена для интеграции голосовых и текстовых AI-ассистентов Happ с внешними сервисами (CRM, телефония и т.д.).

Основные эндпоинты интеграции

Каждая интеграция имеет стандартные built-in эндпоинты:

ЭндпоинтМетодТипНазначение
/setupPOSTBuilt-inИнфраструктура: миграции БД, секреты
/healthGETBuilt-inПроверка D1, KV, Creds
/referenceGETBuilt-inScalar UI - интерактивная API документация
/openapi.jsonGETBuilt-inOpenAPI 3.0 спецификация

Стандартные template handlers для голосовых интеграций:

ЭндпоинтМетодНазначение
/webhook/call-originatePOSTИнициация исходящего звонка через VA
/webhook/call-eventsPOSTПолучение событий call_start/call_end от VA
/webhook/agent-initPOSTКонтекст клиента для агента
/webhook/agent-postcallPOSTAI анализ + обновление CRM

Дополнительные эндпоинты определяются через endpoint в handler config.


Quick Start (5 минут)

1. Создать интеграцию

bash
pnpm generate:integration my-crm
cd integrations/my-crm

2. Структура проекта

integrations/my-crm/
├── src/
│   ├── index.ts              # Entry point (~20 строк)
│   ├── types.ts              # TypeScript типы
│   ├── config/
│   │   ├── types.ts          # Интерфейсы конфигов
│   │   ├── defaults.ts       # Дефолтные значения
│   │   └── index.ts          # Barrel export
│   ├── handlers/
│   │   ├── index.ts          # Barrel export всех handlers
│   │   ├── call-originate.ts # POST /webhook/call-originate
│   │   ├── call-events.ts    # POST /webhook/call-events
│   │   ├── agent-init.ts     # POST /webhook/agent-init
│   │   └── agent-postcall.ts # POST /webhook/agent-postcall
│   └── migrations/
│       └── index.ts          # SQL миграции
├── package.json
├── wrangler.toml
└── README.md

3. Entry point (index.ts)

typescript
import { createIntegration } from "@happ-integ/core";
import * as handlers from "./handlers";
import { MIGRATIONS } from "./migrations";
import { DEFAULT_CONFIGS } from "./config";

export default createIntegration({
  name: "my-crm",
  handlers: Object.values(handlers),
  migrations: MIGRATIONS,
  secrets: [
    "SAAS_ASSISTANT_ID",
    // Add your CRM credentials here, e.g.:
    // "NETHUNT_EMAIL",
    // "NETHUNT_API_KEY",
  ],
  defaultConfigs: DEFAULT_CONFIGS,
});

export { setEnv, getEnv, getDb, getCache, getCreds, getConfig } from "@happ-integ/core";

4. Создать конфигурацию (config/)

Все переменные бизнес-логики (названия полей CRM, пороги, лимиты, параметры LLM, тексты сообщений) обязательно выносятся в конфигурацию. Это позволяет менять поведение без редеплоя.

src/config/types.ts — интерфейсы конфигов:

typescript
export interface ICrmFieldsConfig {
  phone: string;
  name: string;
  comment: string;
}

export interface ICallHandlingConfig {
  concurrency_limit: number;
  failed_call_duration_threshold: number;
  max_comment_length: number;
}

export interface ILlmConfig {
  primary_provider: string;
  analysis_max_tokens: number;
}

src/config/defaults.ts — дефолтные значения:

typescript
import type { ICrmFieldsConfig, ICallHandlingConfig, ILlmConfig } from "./types";

export const DEFAULT_CRM_FIELDS: ICrmFieldsConfig = {
  phone: "Phone",
  name: "Name",
  comment: "Comment",
};

export const DEFAULT_CALL_HANDLING: ICallHandlingConfig = {
  concurrency_limit: 3,
  failed_call_duration_threshold: 5,
  max_comment_length: 2000,
};

export const DEFAULT_LLM: ILlmConfig = {
  primary_provider: "groq",
  analysis_max_tokens: 512,
};

export const DEFAULT_CONFIGS: Record<string, unknown> = {
  crm_fields: DEFAULT_CRM_FIELDS,
  call_handling: DEFAULT_CALL_HANDLING,
  llm: DEFAULT_LLM,
};

src/config/index.ts — barrel export:

typescript
export * from "./types";
export * from "./defaults";

Конфиги автоматически записываются в KV при вызове /setup.

5. Определить credentials

Отредактируйте src/types.ts:

typescript
export type { ICloudflareBindings } from "@happ-integ/core";

export interface IMyCrmCredentials {
  // SaaS API
  SAAS_ASSISTANT_ID: string;

  // Add your CRM credentials here, e.g.:
  // NETHUNT_EMAIL: string;
  // NETHUNT_API_KEY: string;
}

6. Реализовать handler

Каждый handler использует defineHandler() с полной типизацией и OpenAPI документацией:

typescript
import { defineHandler } from "@happ-integ/core";
import type { IAgentInitPayload, IAgentInitResponse, IMyCrmCredentials } from "../types";

export const agentInitHandler = defineHandler<IAgentInitPayload, IAgentInitResponse>({
  name: "agent-init",
  retries: 0,
  endpoint: "POST /webhook/agent-init",
  openapi: {
    summary: "Initialize agent conversation",
    description: "Provides dynamic variables for Voice Assistant agent",
    requestBody: {
      schema: {
        type: "object",
        properties: {
          phone_number: { type: "string", example: "+380991234567" },
        },
      },
    },
  },
  async handler({ payload, step, cache, creds, integrationName }) {
    const { phone_number } = payload;

    if (!phone_number) {
      return getDefaultResponse();
    }

    // Проверяем кэш
    const cachedContext = await step.run(
      "cache.get",
      () => cache.get<IAgentInitResponse>(`context:${phone_number}`),
      { optional: true }
    );

    if (cachedContext) {
      return cachedContext;
    }

    // Получаем credentials
    const secrets = await step.run(
      "creds.get",
      () => creds.get<IMyCrmCredentials>(integrationName),
      { skipOutput: true }
    );

    // ... логика поиска контакта в CRM ...

    const response: IAgentInitResponse = {
      type: "conversation_initiation_client_data",
      dynamic_variables: {
        first_message: "Привіт! Чим можу допомогти?",
        client_name: "Client",
      },
    };

    // Кэшируем контекст
    await step.run(
      "cache.set",
      () => cache.set(`context:${phone_number}`, response, 3600),
      { optional: true, skipOutput: true }
    );

    return response;
  },
});

7. Barrel export

Создайте src/handlers/index.ts:

typescript
export { callOriginateHandler } from "./call-originate";
export { callEventsHandler } from "./call-events";
export { agentInitHandler } from "./agent-init";
export { agentPostcallHandler } from "./agent-postcall";

Исходящие звонки (Call Queue)

Готовая библиотека: Если интеграция делает исходящие звонки через Voice Assistant — используй @happ-integ/call-queue. Не пиши логику очереди, concurrency-контроля и retry вручную.

Пакет @happ-integ/call-queue предоставляет:

  • CallQueue — класс с методами enqueue, handleCallStarted, handleCallEnded, processQueue
  • generateCallsMigration(prefix) — генерация SQL-миграции для таблицы звонков
  • DEFAULT_RETRY, DEFAULT_QUEUE_CONFIG — дефолтные настройки retry и concurrency
  • getNextRetryWindow() — вычисление следующего временного окна для retry

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

1. Миграция:

typescript
// migrations/index.ts
import { generateCallsMigration } from "@happ-integ/call-queue";
export const MIGRATIONS = [generateCallsMigration("my-crm")];

2. Factory-утилита:

typescript
// utils/create-call-queue.util.ts
import { CallQueue, DEFAULT_RETRY } from "@happ-integ/call-queue";

export async function createMyCallQueue({ db, config, env, creds, log, integrationName }) {
  const retryConfig = (await config.get("retry")) ?? DEFAULT_RETRY;
  const sofaCreds = await creds.get(integrationName);
  const saasClient = new SaasApiClient(env.SAAS_API_URL, env.SAAS_ACCESS_TOKEN);

  return new CallQueue({
    db, tableName: "calls", integrationPrefix: integrationName,
    queueConfig: { concurrency_limit: 3, failed_call_duration_threshold: 5 },
    retryConfig,
    originate: async (phone) => {
      const result = await saasClient.originateCall(sofaCreds.SAAS_ASSISTANT_ID, phone);
      return { vaCallId: result.callId! };
    },
    log,
  });
}

3. Handlers:

typescript
// call-originate handler
const queue = await createMyCallQueue({ db, config, env, creds, log, integrationName });
const result = await step.run("queue.enqueue", () => queue.enqueue(phone_number));
return { success: true, queued: result.queued, callId: result.callId };

// call-events handler
const queue = await createMyCallQueue({ db, config, env, creds, log, integrationName });
if (eventType === "media_start") {
  await step.run("queue.handleCallStarted", () => queue.handleCallStarted(callId, metadata));
}
if (eventType === "media_end") {
  await step.run("queue.handleCallEnded", () => queue.handleCallEnded(callId, duration, clientPhone));
}

// retry handler (cron)
const queue = await createMyCallQueue({ db, config, env, creds, log, integrationName });
const result = await queue.processQueue();

Подробнее: packages/call-queue


Архитектура пакетов

Core пакет

  • @happ-integ/core - главный пакет с createIntegration() и defineHandler()
    • Автоматически создает Hono app с CORS
    • Автоматически регистрирует handlers
    • Автоматически создает built-in handlers (setup, health, docs)
    • Автоматически генерирует OpenAPI документацию
    • Автоматически создает POST /cron/trigger при наличии scheduled (через @happ-integ/cron)
    • Dependency injection для всех клиентов (db, cache, config, creds, log)

Низкоуровневые драйверы

  • @happ-integ/d1 - Cloudflare D1 клиент
  • @happ-integ/kv - Cloudflare KV клиент
  • @happ-integ/neon - PostgreSQL клиент

Бизнес-логика

  • @happ-integ/db - универсальная БД (d1|neon, default: d1)
  • @happ-integ/cache - кэш (обёртка над KV)
  • @happ-integ/config - KV-based конфигурация (get/set/delete/list)
  • @happ-integ/creds - управление credentials в D1
  • @happ-integ/call-queue - очередь исходящих звонков (concurrency, retry, queue)
  • @happ-integ/cron - scheduled tasks (auto-creates POST /cron/trigger endpoint)

CRM клиенты

  • @happ-integ/nethunt - NetHunt CRM
  • @happ-integ/keycrm - KeyCRM
  • @happ-integ/bitrix - Bitrix24
  • @happ-integ/salesdrive - SalesDrive

Handler Context (Dependency Injection)

Каждый handler получает контекст с готовыми клиентами:

typescript
interface IHandlerContext<TPayload> {
  payload: TPayload;           // Данные запроса
  ctx: ITraceContext;          // Контекст трейсинга
  db: DB;                      // Клиент БД (D1/Neon)
  cache: Cache;                // Клиент кэша (KV)
  config: Config;              // KV-based configuration storage
  creds: Creds;                // Клиент credentials
  env: ICloudflareBindings;    // Cloudflare bindings
  integrationName: string;     // Имя интеграции
}

Преимущества:

  • Не нужно вызывать getDb(), getCreds() вручную
  • Клиенты уже инициализированы с правильными параметрами
  • Типизация payload и response через generics

Handler Config

typescript
interface IHandlerConfig<TPayload, TResponse> {
  name: string;                              // Уникальное имя handler
  endpoint?: `${HttpMethod} /${string}` | false;  // HTTP endpoint или false для event-only
  concurrency?: { limit: number };           // Ограничение параллельности
  dedup?: { ttlSeconds: number; keyField?: string };        // Deduplication
  openapi?: IHandlerOpenAPIConfig;           // OpenAPI документация
  handler: (ctx: IHandlerContext<TPayload>) => Promise<TResponse>;
}

Примеры endpoint:

  • "POST /webhook/agent-init" - создает POST endpoint
  • "GET /status" - создает GET endpoint
  • false - handler доступен только через event (без HTTP endpoint)

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

Каждая интеграция автоматически получает интерактивную API документацию через Scalar UI.

Endpoints

EndpointОписание
GET /{integration}/referenceScalar UI - интерактивная документация
GET /{integration}/openapi.jsonOpenAPI 3.0 спецификация

Доступ

Все интеграции (через Gateway):

https://integ.happ.tools/reference
https://integ.dev.happ.tools/reference
http://localhost:3001/reference

Отдельная интеграция:

https://integ.happ.tools/sofa/reference
http://localhost:8787/sofa/reference

Настройка документации для handler

typescript
defineHandler({
  name: "create-order",
  endpoint: "POST /webhook/create-order",
  openapi: {
    summary: "Create new order",
    description: "Full description of what this endpoint does",
    requestBody: {
      schema: {
        type: "object",
        required: ["phone"],
        properties: {
          phone: { type: "string", example: "+380991234567" },
        },
      },
    },
    responses: {
      "200": {
        description: "Order created",
        schema: {
          type: "object",
          properties: {
            success: { type: "boolean" },
            orderId: { type: "string" },
          },
        },
      },
    },
  },
  handler: async (ctx) => { ... },
});

Отключение документации

typescript
createIntegration({
  name: "my-integration",
  openapi: false,  // Отключает /reference и /openapi.json
  handlers: [...],
});

Миграции

Миграции — code-based. Определяются как IMigration[] в src/migrations/index.ts, применяются через POST /{integration}/setup. Единый механизм для local / dev / prod.

Как это работает

При вызове POST /my-crm/setup автоматически применяются:

  1. Глобальные миграции (GLOBAL_MIGRATIONS из core) — таблица creds
  2. Миграции интеграции (массив migrations из createIntegration())

Уже применённые миграции пропускаются (трекинг по name в таблице _migrations).

Вариант 1: С исходящими звонками (call-queue)

Если интеграция делает исходящие звонки через VA — используй generateCallsMigration():

typescript
// src/migrations/index.ts
import type { IMigration } from "@happ-integ/core";
import { generateCallsMigration } from "@happ-integ/call-queue";

export const MIGRATIONS: IMigration[] = [generateCallsMigration("mycrm")];
// Создаст таблицу mycrm_calls с полным набором колонок и 6 индексами

Вариант 2: Кастомные таблицы

Для произвольных таблиц пиши миграции вручную:

typescript
// src/migrations/index.ts
import type { IMigration } from "@happ-integ/core";

export const MIGRATIONS: IMigration[] = [
  {
    name: "mycrm/0001_create_orders",
    sql: `
      CREATE TABLE IF NOT EXISTS mycrm_orders (
        id TEXT PRIMARY KEY,
        external_id TEXT,
        phone TEXT,
        status TEXT NOT NULL DEFAULT 'pending',
        data TEXT,
        created_at TEXT DEFAULT (datetime('now')),
        updated_at TEXT DEFAULT (datetime('now'))
      );
      CREATE INDEX IF NOT EXISTS idx_mycrm_orders_status ON mycrm_orders(status);
      CREATE INDEX IF NOT EXISTS idx_mycrm_orders_phone ON mycrm_orders(phone)
    `,
  },
];

Вариант 3: Комбинация (call-queue + свои таблицы)

typescript
import type { IMigration } from "@happ-integ/core";
import { generateCallsMigration } from "@happ-integ/call-queue";

export const MIGRATIONS: IMigration[] = [
  generateCallsMigration("mycrm"),
  {
    name: "mycrm/0002_create_contacts",
    sql: `
      CREATE TABLE IF NOT EXISTS mycrm_contacts (
        id TEXT PRIMARY KEY,
        phone TEXT NOT NULL UNIQUE,
        name TEXT,
        created_at TEXT DEFAULT (datetime('now'))
      );
      CREATE INDEX IF NOT EXISTS idx_mycrm_contacts_phone ON mycrm_contacts(phone)
    `,
  },
];

Подключение в entry point

typescript
// src/index.ts
import { createIntegration } from "@happ-integ/core";
import { MIGRATIONS } from "./migrations";

export default createIntegration({
  name: "mycrm",
  migrations: MIGRATIONS,
  // ...
});

Добавление миграций позже

Просто добавляй новые объекты в массив — уже применённые пропустятся:

typescript
export const MIGRATIONS: IMigration[] = [
  generateCallsMigration("mycrm"),              // уже применена
  { name: "mycrm/0002_create_contacts", sql: `...` },  // уже применена
  { name: "mycrm/0003_add_contacts_email", sql: `ALTER TABLE mycrm_contacts ADD COLUMN email TEXT` },  // новая
];

Затем вызови POST /mycrm/setup — применится только 0003.

Применение миграций

bash
# Локально
pnpm start my-crm
# POST http://localhost:8787/my-crm/setup

# Dev — после деплоя
# POST https://integ.dev.happ.tools/my-crm/setup

# Prod — после деплоя
# POST https://integ.happ.tools/my-crm/setup

Правила именования

ЧтоФорматПример
Имя миграции{integration}/{номер}_{описание}mycrm/0001_create_orders
Имя таблицы{integration}_{таблица}mycrm_orders
Имя индексаidx_{integration}_{таблица}_{колонка}idx_mycrm_orders_status

Важно

  • Все таблицы обязательно с префиксом интеграции (общая D1 база)
  • SQL должен быть идемпотентным: CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS
  • ALTER TABLE не идемпотентен — если миграция применена, она не выполнится повторно (трекинг по name)
  • Не удаляй и не переименовывай уже применённые миграции из массива

Подробнее: DATABASE.md


API Endpoints

POST /setup (built-in)

Инициализирует БД (миграции), создает placeholder'ы для секретов и записывает дефолтные конфиги в KV.

Request:

json
{ "force": false }

Response:

json
{
  "success": true,
  "database": { "migrated": true, "tables": ["0001_create_calls"] },
  "secrets": { "created": ["API_KEY"], "existing": [] },
  "configs": { "created": ["crm_fields", "call_handling", "llm"], "existing": [] },
  "errors": []
}

configs.created — ключи конфигов, записанные в KV при первом вызове. configs.existing — ключи, которые уже существовали (не перезаписываются без "force": true).

GET /health (built-in)

Проверяет доступность всех компонентов.

Response:

json
{
  "timestamp": "2024-01-15T10:00:00.000Z",
  "testId": "test_123",
  "success": true,
  "checks": {
    "d1": { "success": true, "details": { "tableExists": true } },
    "kv": { "success": true, "details": { "writeRead": true } },
    "creds": { "success": true, "details": { "keysFound": ["API_KEY"] } }
  }
}

Deployment

bash
# Локально
pnpm start my-crm

# На dev
pnpm deploy:dev --filter my-crm

# На production
pnpm deploy --filter my-crm

Troubleshooting

"D1Database is required"

  • Проверить wrangler.toml, binding должен быть INTEG_DB
  • Запустить pnpm setup:local для создания локальной базы

"KVNamespace is required"

  • Проверить wrangler.toml, binding должен быть INTEG_KV
  • Запустить pnpm setup:local

Credentials не находятся

  1. Вызвать /setup endpoint
  2. Заполнить секреты через integ-admin → Secrets
  3. Проверить через /health

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