Создание интеграций в integ-core
Обзор
Платформа integ-core предназначена для интеграции голосовых и текстовых AI-ассистентов Happ с внешними сервисами (CRM, телефония и т.д.).
Основные эндпоинты интеграции
Каждая интеграция имеет стандартные built-in эндпоинты:
| Эндпоинт | Метод | Тип | Назначение |
|---|---|---|---|
/setup | POST | Built-in | Инфраструктура: миграции БД, секреты |
/health | GET | Built-in | Проверка D1, KV, Creds |
/reference | GET | Built-in | Scalar UI - интерактивная API документация |
/openapi.json | GET | Built-in | OpenAPI 3.0 спецификация |
Стандартные template handlers для голосовых интеграций:
| Эндпоинт | Метод | Назначение |
|---|---|---|
/webhook/call-originate | POST | Инициация исходящего звонка через VA |
/webhook/call-events | POST | Получение событий call_start/call_end от VA |
/webhook/agent-init | POST | Контекст клиента для агента |
/webhook/agent-postcall | POST | AI анализ + обновление CRM |
Дополнительные эндпоинты определяются через endpoint в handler config.
Quick Start (5 минут)
1. Создать интеграцию
pnpm generate:integration my-crm
cd integrations/my-crm2. Структура проекта
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.md3. Entry point (index.ts)
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 — интерфейсы конфигов:
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 — дефолтные значения:
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:
export * from "./types";
export * from "./defaults";Конфиги автоматически записываются в KV при вызове /setup.
5. Определить credentials
Отредактируйте src/types.ts:
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 документацией:
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:
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,processQueuegenerateCallsMigration(prefix)— генерация SQL-миграции для таблицы звонковDEFAULT_RETRY,DEFAULT_QUEUE_CONFIG— дефолтные настройки retry и concurrencygetNextRetryWindow()— вычисление следующего временного окна для retry
Быстрый старт
1. Миграция:
// migrations/index.ts
import { generateCallsMigration } from "@happ-integ/call-queue";
export const MIGRATIONS = [generateCallsMigration("my-crm")];2. Factory-утилита:
// 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:
// 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-createsPOST /cron/triggerendpoint)
CRM клиенты
@happ-integ/nethunt- NetHunt CRM@happ-integ/keycrm- KeyCRM@happ-integ/bitrix- Bitrix24@happ-integ/salesdrive- SalesDrive
Handler Context (Dependency Injection)
Каждый handler получает контекст с готовыми клиентами:
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
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 endpointfalse- handler доступен только через event (без HTTP endpoint)
OpenAPI Документация
Каждая интеграция автоматически получает интерактивную API документацию через Scalar UI.
Endpoints
| Endpoint | Описание |
|---|---|
GET /{integration}/reference | Scalar UI - интерактивная документация |
GET /{integration}/openapi.json | OpenAPI 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
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) => { ... },
});Отключение документации
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 автоматически применяются:
- Глобальные миграции (
GLOBAL_MIGRATIONSиз core) — таблицаcreds - Миграции интеграции (массив
migrationsизcreateIntegration())
Уже применённые миграции пропускаются (трекинг по name в таблице _migrations).
Вариант 1: С исходящими звонками (call-queue)
Если интеграция делает исходящие звонки через VA — используй generateCallsMigration():
// 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: Кастомные таблицы
Для произвольных таблиц пиши миграции вручную:
// 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 + свои таблицы)
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
// src/index.ts
import { createIntegration } from "@happ-integ/core";
import { MIGRATIONS } from "./migrations";
export default createIntegration({
name: "mycrm",
migrations: MIGRATIONS,
// ...
});Добавление миграций позже
Просто добавляй новые объекты в массив — уже применённые пропустятся:
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.
Применение миграций
# Локально
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:
{ "force": false }Response:
{
"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:
{
"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
# Локально
pnpm start my-crm
# На dev
pnpm deploy:dev --filter my-crm
# На production
pnpm deploy --filter my-crmTroubleshooting
"D1Database is required"
- Проверить
wrangler.toml, binding должен бытьINTEG_DB - Запустить
pnpm setup:localдля создания локальной базы
"KVNamespace is required"
- Проверить
wrangler.toml, binding должен бытьINTEG_KV - Запустить
pnpm setup:local
Credentials не находятся
- Вызвать
/setupendpoint - Заполнить секреты через integ-admin → Secrets
- Проверить через
/health
Связанные документы
- INTEGRATION_CHECKLIST.md — полный чеклист
- HANDLERS_REFERENCE.md — справочник handlers
- CRM_CLIENTS.md — работа с CRM
- CODE_RULES.md — правила кода