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:
155
src/index.ts
Normal file
155
src/index.ts
Normal 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 });
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user