Правила написания кода
Основные принципы
1. Типы функций
- НИКОГДА не пишем явный return type
- TypeScript выведет тип автоматически
- Return types удаляются скриптом
pnpm clean:codeперед деплоем
2. Early return pattern
- Вместо:typescript
if (state) { // logic } - Используем:typescript
if (!state) { return; } // logic
3. Утилиты
- Если утилита НЕ зависит от Cloudflare bindings или environment
- Выносим в отдельный файл:
/utils/util_name.util.ts
4. Параметры функций
Если функция принимает больше 2 параметров
Переделываем на объект с интерфейсом:
typescript// Плохо function create(name: string, age: number, email: string) {} // Хорошо function create(data: CreateUserData) {}
5. Типы и интерфейсы
- Все типы и интерфейсы в одном файле:
/types.ts - Все интерфейсы с префиксом
I:typescriptIUser; IChat; IMessage; ICreateUserData; - НИКОГДА не пишем типы напрямую в функциях
- ВСЕГДА выносим в
types.tsи импортируем
6. Именование переменных
- Используем краткие понятные версии
- ❌ Плохо:
mes,m,messag,messageEntity - ✅ Хорошо:
message,messageIntegration,callsDatabase,calls
Архитектура
7. Handler-based архитектура
Проект использует handler-based паттерн, а не NestJS DI:
// handlers/agent-init.ts - defineHandler
import { defineHandler } from "@happ-integ/core";
export const agentInitHandler = defineHandler<IAgentInitPayload, IAgentInitResponse>({
name: "agent-init",
retries: 0,
endpoint: "POST /webhook/agent-init",
async handler({ payload, step, cache, creds, integrationName }) {
return { type: "conversation_initiation_client_data", dynamic_variables: { ... } };
},
});- Handlers определяются через
defineHandler()из@happ-integ/core - Получают DI контекст:
payload,step,db,cache,creds,ctx,env - Регистрируются через
createIntegration()вindex.ts
8. Lazy initialization паттерн
Для инициализации клиентов используем lazy init (вместо DI):
let _db: DB | null = null;
export const getDb = (env: CloudflareBindings) =>
(_db ??= new DB({
provider: "d1",
d1: env.DB,
project: "sofa",
}));
// Использование
const db = getDb(env);
await db.insert("calls", { id: "123", status: "pending" });Преимущества:
- Нет DI framework overhead
- Работает в Cloudflare Workers
- Можно сбросить для тестов:
__resetClients()
9. Структура интеграции
integrations/sofa/
├── src/
│ ├── index.ts # createIntegration() entry point
│ ├── types.ts # Все типы и enums
│ ├── config/
│ │ ├── types.ts # Интерфейсы конфигов
│ │ ├── defaults.ts # Дефолтные значения
│ │ └── index.ts # Barrel export
│ ├── migrations/
│ │ └── index.ts # SQL миграции
│ ├── utils/
│ │ ├── create-call-queue.util.ts # Factory для CallQueue (@happ-integ/call-queue)
│ │ └── call-analysis.util.ts # AI-анализ звонков
│ └── handlers/ # defineHandler() функции
│ ├── webinar-call-originate.ts
│ ├── webinar-agent-init.ts
│ ├── webinar-call-events.ts
│ ├── webinar-agent-post-call.ts
│ └── retry.ts
├── package.json
├── wrangler.toml # Cloudflare конфиг
└── README.md10. Конфигурация интеграции (defaultConfigs)
Все переменные бизнес-логики обязательно выносятся в KV-based конфигурацию:
- Названия полей CRM (
"Телефон","Имя","AI Квалификация") - Пороги и лимиты (
concurrency_limit,failed_call_duration_threshold) - Параметры retry (
max_attempts,windows) - Настройки LLM (
primary_provider,max_tokens) - Тексты сообщений и локали (
first_message_template,locale) - Маппинги статусов (
outcome → CRM stage)
Структура config/:
// config/types.ts — интерфейсы
export interface ICrmFieldsConfig {
phone: string;
name: string;
// ...
}
// config/defaults.ts — дефолты + общий объект
export const DEFAULT_CRM_FIELDS: ICrmFieldsConfig = { phone: "Phone", name: "Name" };
export const DEFAULT_CONFIGS: Record<string, unknown> = {
crm_fields: DEFAULT_CRM_FIELDS,
// ...
};
// config/index.ts — реэкспорт
export * from "./types";
export * from "./defaults";Использование в handlers:
import type { ICrmFieldsConfig } from "../config";
import { DEFAULT_CRM_FIELDS } from "../config";
async handler({ config }) {
const crmFields = (await config.get<ICrmFieldsConfig>("crm_fields")) ?? DEFAULT_CRM_FIELDS;
// Используй crmFields.phone вместо хардкода "Телефон"
}Принцип: если значение может отличаться между клиентами/средами — оно должно быть в конфиге. ?? DEFAULT_* гарантирует работу даже если KV пуст.
11. Комментарии
- НЕ пишем комментарии к коду
- Код должен быть самодокументируемым
- Комментарии удаляются скриптом перед деплоем
12. Декомпозиция
- НЕ используем декомпозицию там, где она не нужна
- Если функция вызывается один раз - не выносим в отдельную функцию
- Исключение: утилитарные функции → выносим в
utils/
Возвращаемые значения Handler-ов
13. Handler return pattern
Все handlers ВСЕГДА возвращают консистентную структуру:
// Успех
return { success: true, data: result };
// Ошибка
return { success: false, error: "Error message" };Используется для обработки результатов.
14. Обработка ошибок в Handlers
export const myActionHandler = defineHandler<IPayload>({
name: "my-action",
endpoint: "POST /webhook/my-action",
async handler({ payload, step }) {
const result = await step.run("call-api", () => externalAPI.call());
return { success: true, data: result };
},
});Cloudflare Bindings и Environment
15. Типизация Bindings
Определяем интерфейс для всех Cloudflare bindings:
interface CloudflareBindings {
// Environment
ENVIRONMENT: "dev" | "production";
DEBUG?: string;
// D1 Database
DB: D1Database;
// KV
KV: KVNamespace;
// Секреты
CRYPTO_KEY: string;
CRYPTO_SALT: string;
}Используется в Hono типе:
const app = new Hono<{ Bindings: CloudflareBindings }>();16. Доступ к bindings
Handlers определяются через defineHandler() и регистрируются в createIntegration():
// Каждый handler — отдельный файл, экспортируемый через barrel export
// Webhook routing и обработка происходят автоматически через @happ-integ/corePackages и зависимости
17. Database пакет (@happ-integ/db)
Вместо TypeORM используем абстракцию @happ-integ/db:
const db = getDb(env);
// Select
const rows = await db.select("calls", { status: "pending" });
// Insert
await db.insert("calls", { id: "123", status: "pending" });
// Update
await db.update("calls", { status: "completed" }, { id: "123" });
// Delete
await db.delete("calls", { id: "123" });Поддерживает D1 и Neon.
18. Cache пакет (@happ-integ/cache)
const cache = getCache(env);
// Get
const value = await cache.get("key");
// Set
await cache.set("key", value, 3600); // TTL в секундах
// Cache-aside pattern
const data = await cache.cacheAside("key", 3600, async () => {
return await heavyOperation();
});Обёртка над KV.
19. Credentials пакет (@happ-integ/creds)
const creds = getCreds(env);
// Get credentials for integration
const secrets = await creds.get<{ API_KEY: string }>("sofa");
console.log(secrets.API_KEY);
// Set credentials
await creds.set("sofa", { API_KEY: "sk-..." });Автоматически шифрует значения в D1.
20. LLM пакет (@happ-integ/llm)
const llm = new LLMService({
primary: "groq",
secondary: "claude",
env,
});
const response = await llm.chat({
messages: [{ role: "user", content: "Hello" }],
});Автоматический fallback при ошибке провайдера.
21. CRM пакеты (NetHunt, KeyCRM, Bitrix и т.д.)
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" } });Именование
27. Сущности и переменные
- Переменные в единственном числе:typescript
message, call, user; - Сервисы/клиенты в множественном числе (если множество операций):typescript
ctx.db, ctx.cache, ctx.creds; - Приватные переменные с префиксом
_:typescriptlet _db: DB | null = null; const _logger = console;
28. Именование файлов
- Файлы называем в snake_case:
handle-init.ts voice-init.ts types.ts
29. Enums vs Constants
Enums в приоритете над константами для типобезопасных значений:
typescript// ПРАВИЛЬНО export enum CallStatusEnum { INITIATED = "initiated", IN_PROGRESS = "in_progress", COMPLETED = "completed", } // НЕПРАВИЛЬНО export const CALL_STATUS = { INITIATED: "initiated", IN_PROGRESS: "in_progress", };
30. Константы для endpoints (Hono маршруты)
Константы для endpoints должны содержать точно те пути, которые используются в декораторах:
export const INSTAGRAM_ENDPOINTS = {
AUTH: ":companyId/auth",
CALLBACK: "callback",
CHATS: ":companyId/chats",
};
// Использование в Hono
app.get(INSTAGRAM_ENDPOINTS.AUTH);
app.post(INSTAGRAM_ENDPOINTS.CALLBACK);31. Префиксы для констант
НИКОГДА не именуем константы просто
ENDPOINTSилиFIELD_NAMESВСЕГДА пишем с префиксом модуля/интеграции:
typescript// НЕПРАВИЛЬНО export const ENDPOINTS = { ... }; export const FIELD_NAMES = { ... }; // ПРАВИЛЬНО export const INSTAGRAM_ENDPOINTS = { ... }; export const INSTAGRAM_FIELD_NAMES = { ... };
Главное правило - Минимализм
32. Чем меньше кода - тем лучше
- Идеальный handler должен умещаться на один экран монитора
- Бизнес-логика должна быть минималистичной
- Только главная логика, без лишнего кода
- Объемный утилитарный код выносим в
/utils/ - Handlers должны быть крайне чистыми
Тестирование
33. Структура тестов
src/handlers/
├── init.ts
└── init.test.ts # Тест рядом с файломИспользуем Vitest с mocking:
import { describe, it, expect, vi } from "vitest";
import { agentInitHandler } from "./agent-init";
describe("agentInitHandler", () => {
it("should return default response for empty phone", async () => {
const result = await agentInitHandler.handler({
payload: { phone_number: "" },
// ... mock context
});
expect(result.type).toBe("conversation_initiation_client_data");
});
});Pre-commit и Linting
34. Husky + lint-staged Pipeline
Перед каждым коммитом запускается:
1. remove-comments.script.ts - удаляет все комментарии
2. remove-return-types.script.ts - удаляет явные return types
3. eslint --fix - исправляет lint ошибки
4. prettier --write - форматирует кодКонфигурация в package.json:
"lint-staged": {
"*.{ts,tsx}": [
"tsx scripts/remove-comments.script.ts --no-backup",
"tsx scripts/remove-return-types.script.ts --no-backup",
"eslint --fix",
"prettier --write"
]
}35. ESLint правила
Ключевые правила в eslint.config.js:
@typescript-eslint/explicit-function-return-type: "off"- нет явных return types@typescript-eslint/no-unused-vars: ["error", { argsIgnorePattern: "^_" }]- игнорируем_параметрыno-multiple-empty-lines- макс 1 пустая строкаsemi: "always"- всегда точка с запятой
Index.ts
36. Использование index.ts
В пакетах index.ts используется для реэкспорта публичного API:
// packages/llm/src/index.ts
export * from "./enums";
export * from "./types";
export * from "./services";Позволяет импортировать напрямую:
import { LLMService, CallStatusEnum } from "@happ-integ/llm";Вместо:
import { LLMService } from "@happ-integ/llm/services";
import { CallStatusEnum } from "@happ-integ/llm/enums";Логирование
37. Правила логирования
Проект использует централизованный Logger из @happ-integ/logger. Прямые вызовы console.error/console.warn запрещены в packages и workers — используйте Logger.
Два уровня логирования:
| Уровень | Где | Инструмент |
|---|---|---|
| Handlers | src/handlers/ | ctx.log, step.run(), ctx (trace-система @happ-integ/trace) |
| Packages и Gateway | packages/*/src/, workers/gateway/ | Logger из @happ-integ/logger |
LOG_LEVEL:
| Команда | LOG_LEVEL | Что видно |
|---|---|---|
pnpm start sofa | info | info, warn, error |
pnpm debug sofa | debug | debug, info, warn, error (включая step :input/:output) |
| CF prod | info | info, warn, error → CF Observability |
Логируем ТОЛЬКО:
- ❌ ОШИБКИ (в packages, handlers, services)
- ⚠️ ВАРНИНГИ (редко, только fallback-ситуации)
НЕ логируем:
- DEBUG сообщения (они видны только при
pnpm debug) - INFO сообщения в packages
- SUCCESS сообщения
- Progress сообщения
- Промежуточные операции
В Packages (src/client.ts, src/services/):
Используем Logger из @happ-integ/logger. Создаём один экземпляр на модуль:
import { Logger } from "@happ-integ/logger";
const logger = new Logger("redis");
// В catch блоках — logger.error(message, error)
catch (error) {
logger.error("set: Failed to set key", error);
throw error;
}
// Для fallback — logger.warn(message)
logger.warn(`chat: Primary (${primary}) failed, falling back to ${secondary}`);
// Плохо ❌
console.error("[ERROR] redis-set: Failed");
console.log("Connecting to Redis...");В Handlers (src/handlers/):
Используем step.run() и ctx.log из trace-системы (НЕ Logger):
// Хорошо — step.run() для структурированных операций
const record = await step.run("fetch-record", () => nethunt.getRecord(id));
// Хорошо — ctx.log для ошибок
catch (error) {
ctx.log.error("Handler failed", error);
return { success: false, error: error.message };
}
// Плохо ❌
console.log("Starting handler");
console.error("Failed:", error);В Scripts (scripts/*):
Единственное место где допустимы прямые console.error/console.log — CLI скрипты:
if (!target) {
console.error("❌ Error: Target is required");
process.exit(1);
}
console.log("✅ Setup completed");
// Плохо ❌
console.log("Starting environment generation...");
console.log("Processing file...");В Workers (workers/gateway/src/index.ts):
Используем Logger:
import { Logger } from "@happ-integ/logger";
const logger = new Logger("gateway");
catch (error) {
logger.error(`${name}: Service call failed`, error);
return c.json({ error: "Service unavailable" }, 503);
}
// Плохо ❌
console.error(`[ERROR] gateway-${name}: Service call failed`, error);