Initial implementation of Telegram AI customer support bot

Cloudflare Workers + Hono + D1 + KV + R2 stack with 4 specialized AI agents
(onboarding, troubleshoot, asset, billing), OpenAI function calling with
7 tool definitions, human escalation, pending action approval workflow,
feedback collection, audit logging, i18n (ko/en), and Workers AI fallback.

43 source files, 45 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-11 13:21:38 +09:00
commit 1d6b64c9e4
58 changed files with 12857 additions and 0 deletions

155
src/index.ts Normal file
View File

@@ -0,0 +1,155 @@
import { Hono } from 'hono';
import type { Env } from './types';
import { webhookRouter } from './routes/webhook';
import { apiRouter } from './routes/api';
import { healthRouter } from './routes/health';
import { setWebhook, getWebhookInfo } from './telegram';
import { timingSafeEqual } from './security';
import { validateEnv } from './utils/env-validation';
import { createLogger } from './utils/logger';
const logger = createLogger('worker');
let envValidated = false;
const app = new Hono<{ Bindings: Env }>();
// Environment validation middleware (runs once per worker instance)
app.use('*', async (c, next) => {
if (!envValidated) {
const result = validateEnv(c.env as unknown as Record<string, unknown>);
if (!result.success) {
logger.error('Environment validation failed', new Error('Invalid configuration'), {
errors: result.errors,
});
return c.json({
error: 'Configuration error',
message: 'The worker is not properly configured.',
}, 500);
}
if (result.warnings.length > 0) {
logger.warn('Environment configuration warnings', { warnings: result.warnings });
}
logger.info('Environment validation passed', {
environment: c.env.ENVIRONMENT || 'production',
warnings: result.warnings.length,
});
envValidated = true;
}
return await next();
});
// Health check
app.route('/health', healthRouter);
// Setup webhook
app.get('/setup-webhook', async (c) => {
const env = c.env;
if (!env.BOT_TOKEN || !env.WEBHOOK_SECRET) {
return c.json({ error: 'Server configuration error' }, 500);
}
const token = c.req.query('token');
const secret = c.req.query('secret');
if (!token || !timingSafeEqual(token, env.BOT_TOKEN)) {
return c.text('Unauthorized', 401);
}
if (!secret || !timingSafeEqual(secret, env.WEBHOOK_SECRET)) {
return c.text('Unauthorized', 401);
}
const webhookUrl = `${new URL(c.req.url).origin}/webhook`;
const result = await setWebhook(env.BOT_TOKEN, webhookUrl, env.WEBHOOK_SECRET);
return c.json(result);
});
// Webhook info
app.get('/webhook-info', async (c) => {
const env = c.env;
if (!env.BOT_TOKEN || !env.WEBHOOK_SECRET) {
return c.json({ error: 'Server configuration error' }, 500);
}
const token = c.req.query('token');
const secret = c.req.query('secret');
if (!token || !timingSafeEqual(token, env.BOT_TOKEN)) {
return c.text('Unauthorized', 401);
}
if (!secret || !timingSafeEqual(secret, env.WEBHOOK_SECRET)) {
return c.text('Unauthorized', 401);
}
const result = await getWebhookInfo(env.BOT_TOKEN);
return c.json(result);
});
// API routes
app.route('/api', apiRouter);
// Telegram Webhook
app.route('/webhook', webhookRouter);
// Root
app.get('/', (c) => {
return c.text(
`Telegram AI Support Bot
Endpoints:
GET /health - Health check
GET /webhook-info - Webhook status
GET /setup-webhook - Configure webhook
POST /webhook - Telegram webhook (authenticated)
GET /api/* - Admin API (authenticated)`,
200
);
});
// 404
app.notFound((c) => c.text('Not Found', 404));
export default {
fetch: app.fetch,
async scheduled(event: ScheduledEvent, env: Env, _ctx: ExecutionContext): Promise<void> {
const cronSchedule = event.cron;
logger.info('Cron job started', { schedule: cronSchedule });
const {
cleanupExpiredSessions,
sendExpiryNotifications,
archiveOldConversations,
cleanupStaleOrders,
monitoringCheck,
} = await import('./services/cron-jobs');
try {
switch (cronSchedule) {
// Midnight KST (15:00 UTC): expiry notifications, archiving, session cleanup
case '0 15 * * *':
await sendExpiryNotifications(env);
await archiveOldConversations(env);
await cleanupExpiredSessions(env);
break;
// Every 5 minutes: stale session/order cleanup
case '*/5 * * * *':
await cleanupStaleOrders(env);
break;
// Every hour: monitoring checks
case '0 * * * *':
await monitoringCheck(env);
break;
default:
logger.warn('Unknown cron schedule', { schedule: cronSchedule });
}
} catch (error) {
logger.error('Cron job failed', error as Error, { schedule: cronSchedule });
}
},
};