Production Workflow
Обзор
На production интеграции работают как Cloudflare Workers.
Архитектура деплоя
┌─────────────────────────────────────┐
│ Cloudflare Edge │
│ │
External ──────> │ ┌─────────────────────────────┐ │
Request │ │ Gateway Worker │ │
│ │ │ │
│ │ integ.happ.tools/* │ │
│ │ • Rate limit + trace_id │ │
│ │ • Rate limiting │ │
│ │ • Routing │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ Service Binding │
│ │ │
│ ┌─────────────▼───────────────┐ │
│ │ Integration Worker │ │
│ │ (sofa) │ │
│ │ │ │
│ │ • Webhook handler │ │
│ │ • Handler execution │ │
│ │ • CRM API / LLM / DB │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────┘Полный Workflow вызова
Шаг 1: Внешний запрос → Gateway
POST https://integ.happ.tools/sofa/webhook/webinar-call-originate
Headers:
Content-Type: application/json
Body:
{
"phone_number": "+380501234567"
}Шаг 2: Gateway обработка
// workers/gateway/src/index.ts
// 1. Rate limiting (100 req/min per IP)
if (rateLimitExceeded) return 429;
// 3. Generate trace_id
const traceId = crypto.randomUUID();
c.header("x-trace-id", traceId);
// 4. Route via Service Binding
const response = await env.SOFA.fetch(request);Шаг 3: Integration Worker → Handler
// integrations/sofa/src/index.ts — используется createIntegration()
import { createIntegration } from "@happ-integ/core";
import * as handlers from "./handlers";
export default createIntegration({
name: "sofa",
handlers: Object.values(handlers),
migrations: SOFA_MIGRATIONS,
secrets: ["NETHUNT_EMAIL", "NETHUNT_API_KEY", "NETHUNT_FOLDER_ID", "PLATFORM_ASSISTANT_ID"],
scheduled: [{ cron: "0 * * * *", handlerName: "retry", payload: { triggered_by: "cron" } }],
});Шаг 4: Handler выполнение (defineHandler)
// integrations/sofa/src/handlers/webinar-call-originate.ts
export const webinarCallOriginateHandler = defineHandler({
name: "webinar-call-originate",
retries: 3,
endpoint: "POST /webhook/webinar-call-originate",
concurrency: { limit: 3 },
async handler({ payload, env, step, creds, integrationName }) {
const { phone_number } = payload;
// 1. Получить credentials из D1
const sofaCreds = await step.run("creds.get", () => creds.get<ISofaCredentials>(integrationName));
// 2. Инициировать звонок через Platform API
const platformClient = new PlatformApiClient(env.PLATFORM_API_URL, env.PLATFORM_ACCESS_TOKEN);
await step.run("platform.originate", () =>
platformClient.originateCall(sofaCreds.PLATFORM_ASSISTANT_ID, phone_number)
);
return { success: true };
},
});Environments
| Environment | URL |
|---|---|
| Production | integ.happ.tools |
| Dev | integ.dev.happ.tools |
| Reference | integ.happ.tools/reference |
| Docs | integ.docs.happ.tools |
| Local | localhost:8787 |
Примечание: Домены
integ.happ.toolsиinteg.dev.happ.toolsнастраиваются как Custom Domains в Cloudflare Dashboard (не через routes в wrangler.toml). См. SETUP.md → Custom Domains
Cutover и кроны
Интеграция с requireCutover: true выкатывается без единого cron-триггера: pnpm generate:env -- prod их не рендерит, и Cloudflare никогда её не будит. До этого спящие клиенты просыпались каждую минуту, читали KV и писали в integ-api прогон со skipped: cutover — у tusk ~600 таких в сутки, у globy 1440.
Следствие, которое надо помнить:
POST /<integration>/cutoverc{"scheduled": true}открывает HTTP-путь сразу, но расписание не запустит — триггеров в воркере нет. Ответ на этот случай содержитwarning.- Чтобы кроны заработали: убрать
requireCutoverизsrc/index.ts, задеплоить заново. GET /<integration>/metaпоказывает это вcutover.triggersRegistered.
Триггеры теперь регистрируются по одному на задачу, а не одним объединённым выражением. Объединённый триггер приходит одним событием, и все совпавшие задачи выполнялись в одной инвокации на общие 128 МБ: часовой retry у specprom делил изолят с разбором YML-каталога и падал с Worker exceeded memory limit. Потолок — 5 триггеров на воркер; сверх него генератор возвращается к объединению.
Деплой
Штатный путь — из тега через CI
Прод НИКОГДА не деплоится из ветки и не деплоится с ноутбука: feat/* → PR → main (авто-деплой dev) → release.yml (version=X.Y.Z → тег vX.Y.Z на HEAD main) → deploy-prod.yml (tag=vX.Y.Z, guard: тег ancestor main).
gh workflow run release.yml -f version=1.4.0
gh workflow run deploy-prod.yml -f tag=v1.4.0Подробности — CI_CD.md.
Локальные команды деплоя (escape hatch)
# Dev environment — норм для итераций по одной интеграции
pnpm --filter sofa deploy:dev
# Быстрый deploy (без sync secrets)
pnpm --filter sofa deploy:dev:fast
# Production — обходит тег, guard и CI. Не использовать, кроме разбора инцидента
pnpm --filter sofa deploy
pnpm deploy:prodЧто делает deploy
# package.json интеграции
"deploy": "pnpm build && pnpm sync-secrets:prod && wrangler deploy"pnpm build— сборка через tsuppnpm sync-secrets:prod— синхронизация секретов из Doppler в Cloudflarewrangler deploy— деплой на Cloudflare Workers
Sync Secrets
# Dev
pnpm --filter sofa sync-secrets:dev
# Production
pnpm --filter sofa sync-secrets:prodРучная установка секретов
# Через wrangler
wrangler secret put CRYPTO_KEY --env production
wrangler secret put CRYPTO_SALT --env productionD1 Миграции
Все миграции (глобальные, например creds, и integration-specific) являются code-based и применяются автоматически при вызове POST /{integration}/setup. SQL-файлы и ручные команды wrangler d1 execute / wrangler d1 migrations apply не используются.
Health Checks
# Gateway
curl https://integ.happ.tools/healthОтвет:
{
"status": "ok",
"timestamp": "2024-01-15T10:30:00.000Z",
"service": "integ-gateway",
"environment": "production"
}Мониторинг
Логирование
Все логи в JSON формате:
{
"level": "info",
"type": "gateway_request",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"method": "POST",
"path": "/sofa/webhook/webinar-call-originate",
"timestamp": "2024-01-15T10:30:00.000Z"
}Просмотр логов
# Real-time
wrangler tail integ-sofa --env production
# Cloudflare Dashboard
# → Workers & Pages → integ-sofa → LogsTracing
Каждый запрос имеет x-trace-id header для сквозного трейсинга.
Error Handling
LLM Fallback
Primary (OpenAI) → error → Secondary (Claude) → error → throwЛогируется warning при fallback:
Primary provider (openai) failed, falling back to claude: [error]Rate Limiting
- Gateway: 100 req/min per IP (in-memory)
Secrets Management
Структура
Doppler (global) D1 Creds (per-integration)
├── CRYPTO_KEY / CRYPTO_SALT ├── sofa/
├── GROQ_API_KEY │ ├── NETHUNT_EMAIL
├── PLATFORM_API_URL │ ├── NETHUNT_API_KEY
├── PLATFORM_ACCESS_TOKEN │ ├── NETHUNT_FOLDER_ID
└── ACCESS_TOKEN │ └── PLATFORM_ASSISTANT_ID
├── booking/
│ └── ...Добавление credentials
// Через CLI или admin endpoint
const creds = new Creds({ d1, masterKey });
await creds.set("sofa", "NEW_API_KEY", "secret-value");
// Просмотр ключей
const keys = await creds.listKeys("sofa");
// ["NETHUNT_EMAIL", "NETHUNT_API_KEY", "NEW_API_KEY"]Типичные сценарии
Outbound Call Flow
1. Запрос на /webhook/webinar-call-originate с номером телефона
2. Gateway проверяет auth, роутит на sofa worker
3. Handler инициирует звонок через Platform API Voice Assistant
4. Voice Assistant отправляет media_start → webinar-call-events создаёт запись
5. Voice Assistant отправляет media_end → webinar-call-events определяет результат:
- duration < 5s → pending_retry (retry по расписанию 10:40/13:40/16:40)
- duration >= 5s → waiting_postcall
6. Post-call webhook → webinar-agent-post-call:
- AI-анализ транскрипции через Claude
- Обновление CRM с квалификацией, резюме, этапом
7. Cron каждый час проверяет pending_retry, повторяет (max 3 попытки)Voice Call Flow
1. POST /webhook/voice-init
2. Handler получает данные клиента
3. LLM генерирует скрипт
4. Voice API инициирует звонок
5. При завершении → POST /webhook/voice-end
6. Результат записывается в CRMChecklist перед деплоем
- [ ] Все тесты проходят:
pnpm test - [ ] Нет ошибок линтера:
pnpm lint - [ ] Сборка успешна:
pnpm build - [ ] Секреты синхронизированы
- [ ] Миграции применены
Полезные ссылки
- Cloudflare Dashboard: https://dash.cloudflare.com
- Doppler: https://dashboard.doppler.com
Связанные документы
- ARCHITECTURE.md — архитектура системы
- DEVELOPMENT.md — локальная разработка
- MONITORING.md — подробнее о мониторинге
- ENV_CONFIG.md — переменные окружения