Spaces:
Sleeping
Sleeping
| /** | |
| * CF Pages Function — POST /api/webhook/telegram | |
| * MX14-E1: Edge Filtering — comandi semplici gestiti a CF (0 chiamate Railway). | |
| * | |
| * Flusso: | |
| * 1. Valida X-Telegram-Bot-Api-Secret-Token header | |
| * 2. Parse update Telegram | |
| * 3. Edge commands (/start /help /ping /stato): rispondi da CF o KV — return 200, no Railway | |
| * 4. Tutto il resto: return 200 subito, dispatch async a Railway (comportamento precedente) | |
| * → fallisce? → accoda su Supabase telegram_queue | |
| * | |
| * Env bindings (CF Pages → Settings → Variables and Secrets): | |
| * TELEGRAM_WEBHOOK_SECRET — segreto impostato in setWebhook (header validation) | |
| * TELEGRAM_BOT_TOKEN — token bot (richiesto per risposte edge dirette) | |
| * BACKEND_URL_A — HF BRAIN/HANDS Space URL | |
| * BRAIN: https://arjanit98-terminal.hf.space (primario) | |
| * HANDS: https://baida07-ai-backend-collab.hf.space (esecuzione codice) | |
| * BACKEND_URL — Railway (legacy, non più nella critical path) | |
| * INTERNAL_TOKEN — token per autenticare CF → Railway | |
| * SUPABASE_URL — per fallback queue | |
| * SUPABASE_SERVICE_KEY — service role key (bypass RLS) | |
| * AGENT_KV — KV namespace binding (id: 4db4874d2a9e49a9aed602697d8a5c45) | |
| * | |
| * MX14-E1 — comandi intercettati a edge (nessuna chiamata a Railway): | |
| * /start → risposta statica | |
| * /help → lista comandi | |
| * /ping → pong con latenza CF | |
| * /stato → stato da AGENT_KV (cache daemon) — fallback Railway se KV vuoto | |
| */ | |
| // --- CONFIGURAZIONE ARCHITETTURA 4 QUADRANTI (MX18-P0-FIX) --- | |
| const QUADRANT_A = { | |
| url: 'https://zwdoplodbdsxfrddoxmo.supabase.co', | |
| key: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp3ZG9wbG9kYmRzeGZyZGRveG1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3OTA1NjMwNSwiZXhwIjoyMDk0NjMyMzA1fQ.tGlgX4peRhWLhQmVd45tjqwiTvEFulOpplkmjkUSt9Y', | |
| role: 'BRAIN' | |
| }; | |
| const QUADRANT_B = { | |
| url: 'https://sluvxtpbtxevrcooaqou.supabase.co', | |
| key: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpsdXZ4dHBidHhldnJjb29hcW91Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc4MjA3Mjk5MiwiZXhwIjoyMDk3NjQ4OTkyfQ.WS4cpsvtO-FGkXukCqpFF6qXfQb38VUs5rtiLwa0khk', | |
| role: 'LOGS' | |
| }; | |
| const QUADRANT_C = { | |
| project_id: 'a9ce05f8-aeca-46c1-837b-8c2ca7a11081', | |
| token: '1edad57a-a9ee-4b47-8272-5e8003c02c9c', | |
| role: 'QUEUE' | |
| }; | |
| const QUADRANT_D = { | |
| project_id: '378eaeaa-ec8d-4833-9a7f-5f57f1f2e7ed', | |
| token: '2f269320-0036-40c7-8ef6-f20bae855f4a', | |
| role: 'FAILOVER' | |
| }; | |
| interface Env { | |
| TELEGRAM_WEBHOOK_SECRET: string; | |
| TELEGRAM_BOT_TOKEN: string; | |
| BACKEND_URL_A: string; // HF BRAIN Space (arjanit98-terminal.hf.space) — no cold-start, always on | |
| BACKEND_URL: string; // Railway (legacy — sostituito da BACKEND_URL_A nella critical path) | |
| INTERNAL_TOKEN: string; | |
| SUPABASE_URL: string; | |
| SUPABASE_SERVICE_KEY: string; | |
| OWNER_CHAT_IDS: string; // CSV di chat_id autorizzati | |
| AGENT_KV: KVNamespace; | |
| OWNER_CHAT_IDS: string; // Chat ID autorizzati (virgola-separati) per comandi privilegiati | |
| GH_TOKEN_CF: string; // PAT fine-grained con actions:write su Baida98/AI (per /logs) | |
| GH_TOKEN_D: string; // PAT arypulka98-bot con actions:write su arypulka98-bot/AI (relay /run /deploy) | |
| } | |
| // ── Telegram API helpers ────────────────────────────────────────────────────── | |
| async function tgSend(chatId: number, html: string, env: Env, replyMarkup?: unknown): Promise<void> { | |
| const token = env.TELEGRAM_BOT_TOKEN ?? ''; | |
| if (!token) return; // bot token non configurato → edge silent, Railway gestirà | |
| await fetch(`https://api.telegram.org/bot${token}/sendMessage`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| chat_id: chatId, text: html, parse_mode: 'HTML', | |
| ...(replyMarkup ? { reply_markup: replyMarkup } : {}), | |
| }), | |
| }).catch(() => {}); | |
| } | |
| /** Estrae il comando (es. "/start", "/help") dall'update Telegram. */ | |
| function extractCommand(update: Record<string, unknown>): string | null { | |
| const msg = update.message as Record<string, unknown> | undefined; | |
| const text = (msg?.text as string | undefined ?? '').trim(); | |
| if (!text.startsWith('/')) return null; | |
| // Gestisce /cmd@BotName | |
| return text.split(/\s+/)[0].split('@')[0].toLowerCase(); | |
| } | |
| function extractChatId(update: Record<string, unknown>): number | null { | |
| const msg = update.message as Record<string, unknown> | undefined; | |
| const chat = msg?.chat as Record<string, unknown> | undefined; | |
| return typeof chat?.id === 'number' ? chat.id : null; | |
| } | |
| // ── Argomenti messaggio ───────────────────────────────────────────────────── | |
| /** Estrae il testo dopo il comando (es. "/run fai X" → "fai X"). */ | |
| function extractArgs(update: Record<string, unknown>): string { | |
| const msg = update.message as Record<string, unknown> | undefined; | |
| const text = (msg?.text as string | undefined ?? '').trim(); | |
| return text.split(/\s+/).slice(1).join(' ').trim(); | |
| } | |
| // ── Owner / autorizzazione ────────────────────────────────────── | |
| /** | |
| * isOwner — verifica se chatId è nell'allowlist OWNER_CHAT_IDS. | |
| * Fail-CLOSED: OWNER_CHAT_IDS vuoto → nessuno è owner (sicurezza default). | |
| * IMPORTANTE: configurare OWNER_CHAT_IDS in CF Pages → Settings → Variables and Secrets. | |
| * Formato: "123456789" oppure "123456789,987654321" (più chat ID separati da virgola). | |
| */ | |
| function isOwner(chatId: number, env: Env): boolean { | |
| const raw = env.OWNER_CHAT_IDS ?? ''; | |
| if (!raw.trim()) return false; // fail-closed: nessun accesso senza OWNER_CHAT_IDS configurato | |
| return raw.split(',').map(s => s.trim()).filter(Boolean).includes(String(chatId)); | |
| } | |
| // ── GitHub API helpers (comandi privilegiati) ─────────────────────────────── | |
| /** Dispatch workflow_dispatch su Baida98/AI via GH_TOKEN_CF. */ | |
| async function ghDispatch( | |
| workflow: string, | |
| inputs: Record<string, string>, | |
| env: Env | |
| ): Promise<{ ok: boolean; msg: string }> { | |
| const token = env.GH_TOKEN_CF ?? ''; | |
| if (!token) return { ok: false, msg: '⚠️ <b>GH_TOKEN_CF</b> non configurato in CF Pages.' }; | |
| try { | |
| const r = await fetch( | |
| `https://api.github.com/repos/Baida98/AI/actions/workflows/${workflow}/dispatches`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| 'Content-Type': 'application/json', | |
| Accept: 'application/vnd.github.v3+json', | |
| 'User-Agent': 'CF-Pages-TelegramBot/2.0', | |
| }, | |
| body: JSON.stringify({ ref: 'main', inputs }), | |
| signal: AbortSignal.timeout(12_000), | |
| } | |
| ); | |
| if (r.status === 204) return { ok: true, msg: '✅ Dispatch inviato (GitHub 204)' }; | |
| const body = await r.text().catch(() => ''); | |
| return { ok: false, msg: `❌ GitHub ${r.status}: ${body.slice(0, 120)}` }; | |
| } catch (e) { | |
| return { ok: false, msg: `❌ Timeout: ${String(e).slice(0, 80)}` }; | |
| } | |
| } | |
| /** Legge l'ultimo run di un workflow su Baida98/AI. */ | |
| /** Legge l'ultimo run di un workflow su arypulka98-bot/AI. */ | |
| async function ghRelayLastRun(workflow: string, env: Env): Promise<string> { | |
| const token = env.GH_TOKEN_D ?? env.GH_TOKEN_CF ?? ''; | |
| if (!token) return '⚠️ GH_TOKEN_D/CF mancante'; | |
| try { | |
| const r = await fetch( | |
| `https://api.github.com/repos/arypulka98-bot/AI/actions/workflows/${workflow}/runs?per_page=1`, | |
| { | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| Accept: 'application/vnd.github.v3+json', | |
| 'User-Agent': 'CF-Pages-TelegramBot/2.0', | |
| }, | |
| signal: AbortSignal.timeout(10_000), | |
| } | |
| ); | |
| if (!r.ok) return `❌ GitHub ${r.status}`; | |
| const j = await r.json() as { workflow_runs: Array<{ status: string; conclusion: string | null }> }; | |
| const run = j.workflow_runs[0]; | |
| if (!run) return '∅ mai eseguito'; | |
| const status = run.status === 'completed' ? (run.conclusion === 'success' ? '✅' : '❌') : '⏳'; | |
| return `${status} ${run.status}${run.conclusion ? ' (' + run.conclusion + ')' : ''}`; | |
| } catch { | |
| return '❌ Timeout GitHub API'; | |
| } | |
| } | |
| async function ghLastRun(workflow: string, env: Env): Promise<string> { | |
| const token = env.GH_TOKEN_CF ?? ''; | |
| if (!token) return '⚠️ GH_TOKEN_CF mancante'; | |
| try { | |
| const r = await fetch( | |
| `https://api.github.com/repos/Baida98/AI/actions/workflows/${workflow}/runs?per_page=1`, | |
| { | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| Accept: 'application/vnd.github.v3+json', | |
| 'User-Agent': 'CF-Pages-TelegramBot/2.0', | |
| }, | |
| signal: AbortSignal.timeout(10_000), | |
| } | |
| ); | |
| if (!r.ok) return `❌ GitHub ${r.status}`; | |
| type RunObj = { status: string; conclusion: string | null; created_at: string }; | |
| const j = await r.json() as { workflow_runs?: RunObj[] }; | |
| const run = j.workflow_runs?.[0]; | |
| if (!run) return '⚠️ Nessun run trovato'; | |
| const icon = run.conclusion === 'success' ? '✅' : run.conclusion === 'failure' ? '❌' : '🔄'; | |
| const when = run.created_at.slice(0, 16).replace('T', ' ') + ' UTC'; | |
| return `${icon} <code>${run.status}/${run.conclusion ?? 'running'}</code> — ${when}`; | |
| } catch { | |
| return '❌ Timeout GitHub API'; | |
| } | |
| } | |
| /** Dispatch repository_dispatch su arypulka98-bot/AI via GH_TOKEN_D (relay hub). */ | |
| async function ghRepoDispatch( | |
| eventType: string, | |
| payload: Record<string, string>, | |
| env: Env | |
| ): Promise<{ ok: boolean; msg: string }> { | |
| const token = env.GH_TOKEN_D ?? env.GH_TOKEN_CF ?? ''; | |
| if (!token) return { ok: false, msg: '⚠️ <b>GH_TOKEN_D</b> non configurato in CF Pages → Settings → Secrets.' }; | |
| try { | |
| const r = await fetch( | |
| 'https://api.github.com/repos/arypulka98-bot/AI/dispatches', | |
| { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| 'Content-Type': 'application/json', | |
| Accept: 'application/vnd.github.v3+json', | |
| 'User-Agent': 'CF-Pages-TelegramBot/2.0', | |
| }, | |
| body: JSON.stringify({ event_type: eventType, client_payload: payload }), | |
| signal: AbortSignal.timeout(12_000), | |
| } | |
| ); | |
| if (r.status === 204) return { ok: true, msg: '✅ Relay D: dispatch inviato (204)' }; | |
| const body = await r.text().catch(() => ''); | |
| return { ok: false, msg: `❌ Relay D: GitHub ${r.status}: ${body.slice(0, 120)}` }; | |
| } catch (e) { | |
| return { ok: false, msg: `❌ Relay D: Timeout: ${String(e).slice(0, 80)}` }; | |
| } | |
| } | |
| /** Dispatch workflow_dispatch su arypulka98-bot/AI via GH_TOKEN_D. */ | |
| async function ghRelayDispatch( | |
| workflow: string, | |
| inputs: Record<string, string>, | |
| env: Env | |
| ): Promise<{ ok: boolean; msg: string }> { | |
| const token = env.GH_TOKEN_D ?? ''; | |
| if (!token) { | |
| // Fallback: Baida98/AI con GH_TOKEN_CF (minuti potrebbero essere esauriti) | |
| const r = await ghDispatch(workflow, inputs, env); | |
| return { ok: r.ok, msg: r.msg + '\n<i>⚠️ GH_TOKEN_D assente — usato account A (possibili minuti esauriti)</i>' }; | |
| } | |
| try { | |
| const r = await fetch( | |
| `https://api.github.com/repos/arypulka98-bot/AI/actions/workflows/${workflow}/dispatches`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| 'Content-Type': 'application/json', | |
| Accept: 'application/vnd.github.v3+json', | |
| 'User-Agent': 'CF-Pages-TelegramBot/2.0', | |
| }, | |
| body: JSON.stringify({ ref: 'main', inputs }), | |
| signal: AbortSignal.timeout(12_000), | |
| } | |
| ); | |
| if (r.status === 204) return { ok: true, msg: '✅ Relay D: dispatch inviato (204)' }; | |
| const body = await r.text().catch(() => ''); | |
| return { ok: false, msg: `❌ Relay D: GitHub ${r.status}: ${body.slice(0, 120)}` }; | |
| } catch (e) { | |
| return { ok: false, msg: `❌ Relay D: Timeout: ${String(e).slice(0, 80)}` }; | |
| } | |
| } | |
| /** Pinga i 3 HF Spaces e restituisce riga di stato ciascuno. */ | |
| async function pingHFSpaces(): Promise<string> { | |
| const spaces = [ | |
| { name: 'BRAIN', url: 'https://arjanit98-terminal.hf.space/health' }, | |
| { name: 'HANDS', url: 'https://baida07-ai-backend-collab.hf.space/health' }, | |
| { name: 'MEMORY', url: 'https://baida07-ai-memory-backend.hf.space/health' }, | |
| ]; | |
| const results = await Promise.all(spaces.map(async (s) => { | |
| const t0 = Date.now(); | |
| try { | |
| const r = await fetch(s.url, { signal: AbortSignal.timeout(15_000) }); | |
| return `${r.ok ? '✅' : '⚠️'} <b>${s.name}</b>: ${r.status} (${Date.now() - t0}ms)`; | |
| } catch { | |
| return `❌ <b>${s.name}</b>: timeout (${Date.now() - t0}ms)`; | |
| } | |
| })); | |
| return results.join('\n'); | |
| } | |
| // ── Edge command handlers ───────────────────────────────────────────────────── | |
| const HELP_TEXT = | |
| '📋 <b>Comandi disponibili:</b>\n\n' + | |
| '<b>— Veloci (risposta istantanea) —</b>\n' + | |
| '/stato · /s — stato progetto (cache aggiornata ogni 5 min)\n' + | |
| '/ping — latenza server\n' + | |
| '/aiuto — questo menu\n\n' + | |
| '<b>— Avanzati (solo proprietario) —</b>\n' + | |
| '/run <obiettivo> — avvia Agent Kernel su GitHub Actions\n' + | |
| '/deploy — pubblica aggiornamento CF Pages\n' + | |
| '/logs — ultimi run e log di sistema\n' + | |
| '/hf — stato spazi HF (BRAIN / HANDS / MEMORY)\n\n' + | |
| '<i>💡 Scrivi qualsiasi obiettivo liberamente — lo eseguo senza comandi!</i>'; | |
| /** | |
| * handleEdgeCommand — gestisce i comandi semplici a CF senza passare da Railway. | |
| * @returns true se il comando è stato gestito → Railway NON va chiamato. | |
| * false se serve Railway. | |
| */ | |
| async function handleEdgeCommand( | |
| cmd: string, | |
| chatId: number, | |
| update: Record<string, unknown>, | |
| env: Env, | |
| t0: number | |
| ): Promise<boolean> { | |
| switch (cmd) { | |
| case '/start': | |
| await tgSend( | |
| chatId, | |
| '🤖 <b>Agente AI</b> — Assistente autonomo per lo sviluppo\n\n' + | |
| 'Scrivi il tuo obiettivo in italiano, liberamente.\n' + | |
| 'Analizzo, codifico, faccio push e ti aggiorno in tempo reale.\n\n' + | |
| '💡 <i>es: «analizza i bug in providers.py» · «autofix degli errori»</i>\n\n' + | |
| 'Cosa vuoi fare?', | |
| env, | |
| { | |
| inline_keyboard: [ | |
| [{ text: '🚀 Nuovo Task', callback_data: 'agent' }, { text: '📊 Status', callback_data: 'tgw_status' }], | |
| [{ text: '🩺 Health', callback_data: 'tgw_health' }, { text: '🔧 AutoFix', callback_data: 'tgw_autofix' }], | |
| [{ text: '📊 Bench', callback_data: 'tgw_bench' }, { text: '📋 Tasks', callback_data: 'tgw_tasks' }], | |
| [{ text: '🌐 Dashboard', url: 'https://agente-ai.pages.dev' }], | |
| ], | |
| } | |
| ); | |
| return true; | |
| case '/help': | |
| case '/aiuto': | |
| case '/menu': | |
| await tgSend(chatId, HELP_TEXT, env); | |
| return true; | |
| case '/ping': | |
| await tgSend(chatId, `🏓 <b>Pong!</b> CF Edge: <code>${Date.now() - t0}ms</code>`, env); | |
| return true; | |
| case '/stato': | |
| case '/s': { | |
| // Prova KV (stato cached dal daemon Railway ogni 5 min) | |
| if (env.AGENT_KV) { | |
| const cached = await env.AGENT_KV.get('stato:latest').catch(() => null); | |
| if (cached) { | |
| await tgSend(chatId, `⚡ <i>(KV cache)</i>\n${cached}`, env); | |
| return true; // gestito — Railway non necessario | |
| } | |
| } | |
| // KV vuoto o non disponibile → Railway gestisce (return false) | |
| return false; | |
| } | |
| case '/run': { | |
| const goal = extractArgs(update) || 'analizza stato progetto e aggiorna AGENT_PLAN'; | |
| // Relay D (arypulka98-bot/AI) — usa budget D, non A (A esaurito fino al 1 luglio) | |
| // Richiede GH_TOKEN_D in CF Pages → Settings → Secrets. | |
| await tgSend(chatId, `🚀 <b>Agent Kernel → relay D</b> dispatching...\n<i>Goal: ${goal}</i>`, env); | |
| const r = await ghRelayDispatch('agent-kernel-b.yml', { goal, mode: 'execute', commit_memory: 'true' }, env); | |
| await tgSend(chatId, | |
| r.msg + '\n\n<a href="https://github.com/arypulka98-bot/AI/actions/workflows/agent-kernel-b.yml">→ Vedi run su relay D</a>', | |
| env | |
| ); | |
| return true; | |
| } | |
| case '/deploy': { | |
| await tgSend(chatId, '🚀 <b>CF Pages Deploy</b> → relay D...', env); | |
| // MX11-G4: usa repository_dispatch diretto su relay D (nessun runner account A) | |
| const sha = await fetch('https://api.github.com/repos/Baida98/AI/commits/main', { | |
| headers: { Authorization: `Bearer ${env.GH_TOKEN_CF ?? ''}`, Accept: 'application/vnd.github.v3+json', 'User-Agent': 'CF-TGBot/2.0' }, | |
| signal: AbortSignal.timeout(8_000), | |
| }).then(r => r.json()).then((j: Record<string, string>) => (j.sha ?? '').slice(0, 12)).catch(() => 'unknown'); | |
| const r = await ghRepoDispatch('relay-deploy', { sha, source: 'telegram-deploy', force: 'true' }, env); | |
| await tgSend(chatId, | |
| r.msg + `\nSHA: <code>${sha}</code>\n\n<a href="https://github.com/arypulka98-bot/AI/actions">→ Vedi run su relay D</a>`, | |
| env | |
| ); | |
| return true; | |
| } | |
| case '/logs': { | |
| await tgSend(chatId, '📊 Recupero ultimi run GHA...', env); | |
| const [cf, kb, be, rd] = await Promise.all([ | |
| ghLastRun('cloudflare-deploy.yml', env), | |
| ghLastRun('agent-kernel-b.yml', env), | |
| ghLastRun('backend-deploy.yml', env), | |
| ghRelayLastRun('agent-kernel-b.yml', env), | |
| ]); | |
| await tgSend(chatId, | |
| '📊 <b>Ultimi run GHA:</b>\n\n' + | |
| '<b>[Baida98/AI]</b>\n' + | |
| '— CF Deploy: ' + cf + '\n' + | |
| '— Kernel B: ' + kb + '\n' + | |
| '— Backend: ' + be + '\n\n' + | |
| '<b>[Relay D]</b>\n' + | |
| '— Kernel D: ' + rd, | |
| env | |
| ); | |
| return true; | |
| } | |
| case '/hf': { | |
| await tgSend(chatId, '🔍 Ping HF Spaces in corso (~15s)...', env); | |
| const status = await pingHFSpaces(); | |
| await tgSend(chatId, '🤗 <b>HF Spaces status:</b>\n\n' + status, env); | |
| return true; | |
| } | |
| default: | |
| return false; | |
| } | |
| } | |
| // ── Railway dispatch (invariato rispetto a versione precedente) ─────────────── | |
| /** | |
| * dispatchToBrain — instrada l'update al HF BRAIN Space. | |
| * Sostituisce Railway: BRAIN gira 24/7 su HF free tier (no cold-start, no sleep). | |
| * Timeout 25s: HF può essere lento ma non si sveglia da zero. | |
| * Fallback: BACKEND_URL (Railway legacy) se BACKEND_URL_A non configurato. | |
| */ | |
| /** URL Spaces verificati live: | |
| * BRAIN: arjanit98-terminal.hf.space → /health 200, /api/telegram/process 200 | |
| * HANDS: baida07-ai-backend-collab.hf.space → /health 200 | |
| * BACKEND_URL_A punta a BRAIN (primario). HANDS come fallback esecuzione. | |
| */ | |
| const HF_BRAIN_URL = 'https://arjanit98-terminal.hf.space'; | |
| const HF_HANDS_URL = 'https://baida07-ai-backend-collab.hf.space'; | |
| async function dispatchToBrain(update: unknown, env: Env): Promise<boolean> { | |
| // Prova BRAIN prima, poi HANDS come fallback (entrambi live su HF free tier) | |
| const brainUrl = (env.BACKEND_URL_A ?? HF_BRAIN_URL).replace(/\/$/, ''); | |
| for (const url of [brainUrl, HF_HANDS_URL]) { | |
| try { | |
| const res = await fetch(`${url}/api/telegram/process`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| // BRAIN: accetta senza auth se TELEGRAM_WEBHOOK_SECRET non configurato | |
| 'Authorization': `Bearer ${env.INTERNAL_TOKEN ?? ''}`, | |
| 'User-Agent': 'CF-Pages-Webhook/2.0', | |
| }, | |
| body: JSON.stringify(update), | |
| signal: AbortSignal.timeout(20_000), | |
| }); | |
| if (res.ok) return true; | |
| } catch { /* prova url successivo */ } | |
| } | |
| return false; | |
| } | |
| async function queueToSupabase(update: unknown, env: Env): Promise<void> { | |
| const supabaseUrl = (env.SUPABASE_URL ?? '').replace(/\/$/, ''); | |
| const supabaseKey = env.SUPABASE_SERVICE_KEY ?? ''; | |
| if (!supabaseUrl || !supabaseKey) return; | |
| try { | |
| await fetch(`${supabaseUrl}/rest/v1/telegram_queue`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'apikey': supabaseKey, | |
| 'Authorization': `Bearer ${supabaseKey}`, | |
| 'Prefer': 'return=minimal', | |
| }, | |
| body: JSON.stringify({ | |
| update_id: (update as Record<string, unknown>)?.update_id ?? null, | |
| payload: JSON.stringify(update), | |
| processed: false, | |
| }), | |
| }); | |
| } catch { /* fire-and-forget */ } | |
| } | |
| // ── Main handler ────────────────────────────────────────────────────────────── | |
| /** true solo se il chatId è nella allowlist owner. */ | |
| function isOwner(chatId: number, env: Env): boolean { | |
| const list = (env.OWNER_CHAT_IDS ?? '').split(',').map((s) => s.trim()).filter(Boolean); | |
| return list.includes(String(chatId)); | |
| } | |
| export const onRequestPost: PagesFunction<Env> = async (ctx) => { | |
| const t0 = Date.now(); | |
| const secret = ctx.env.TELEGRAM_WEBHOOK_SECRET ?? ''; | |
| // 1. Valida secret token header (se configurato) | |
| if (secret) { | |
| const headerSecret = ctx.request.headers.get('X-Telegram-Bot-Api-Secret-Token') ?? ''; | |
| if (headerSecret !== secret) { | |
| return new Response('Unauthorized', { status: 401 }); | |
| } | |
| } | |
| // 2. Parse body | |
| let update: Record<string, unknown>; | |
| try { | |
| update = await ctx.request.json() as Record<string, unknown>; | |
| } catch { | |
| return new Response('Bad Request', { status: 400 }); | |
| } | |
| // 3. MX14-E1: Edge filtering — tenta gestione locale prima di Railway | |
| const cmd = extractCommand(update); | |
| const chatId = extractChatId(update); | |
| if (cmd && chatId !== null) { | |
| // Sicurezza: Blocca comandi se non è l'owner (eccetto /start e /help per privacy) | |
| const PUBLIC_CMDS = new Set(['/start', '/help', '/aiuto', '/menu', '/ping']); | |
| if (!PUBLIC_CMDS.has(cmd) && !isOwner(chatId, ctx.env)) { | |
| ctx.waitUntil(tgSend(chatId, '⚠️ <b>Accesso Negato.</b> Non sei autorizzato a usare questo comando.', ctx.env)); | |
| return new Response('OK', { status: 200 }); | |
| } | |
| const EDGE_CMDS = new Set(['/start', '/help', '/aiuto', '/menu', '/ping', '/stato', '/s', '/run', '/deploy', '/logs', '/hf']); | |
| if (EDGE_CMDS.has(cmd)) { | |
| // Edge command: return 200 subito, gestisci in background. | |
| // handleEdgeCommand → true: risposta inviata da CF, nessuna chiamata Railway. | |
| // handleEdgeCommand → false: KV miss (/stato cache vuota) → Railway gestisce. | |
| ctx.waitUntil( | |
| handleEdgeCommand(cmd, chatId, update, ctx.env, t0).then((handled) => { | |
| if (!handled) return dispatchToBrain(update, ctx.env).then((ok) => { | |
| if (!ok) return queueToSupabase(update, ctx.env); | |
| }); | |
| }) | |
| ); | |
| return new Response('OK', { status: 200 }); | |
| } | |
| } | |
| // 4. Comando non edge (o non-comando) → Railway come prima | |
| ctx.waitUntil( | |
| (async () => { | |
| const ok = await dispatchToBrain(update, ctx.env); | |
| if (!ok) await queueToSupabase(update, ctx.env); | |
| })() | |
| ); | |
| return new Response('OK', { status: 200 }); | |
| }; | |
| // GET: health check (esteso con info edge) | |
| export const onRequestGet: PagesFunction<Env> = async () => | |
| new Response( | |
| JSON.stringify({ | |
| ok: true, | |
| mode: 'webhook', | |
| edge_filter: 'MX14-E1', | |
| edge_commands: ['/start', '/help', '/ping', '/stato', '/s', '/run', '/deploy', '/logs', '/hf'], | |
| ts: Date.now(), | |
| }), | |
| { headers: { 'Content-Type': 'application/json' } } | |
| ); | |
| /** Invia log al Quadrante B (LOGS) in modo asincrono. */ | |
| async function logToQuadrantB(event: string, details: any) { | |
| try { | |
| await fetch(`${QUADRANT_B.url}/rest/v1/agent_logs`, { | |
| method: 'POST', | |
| headers: { | |
| 'apikey': QUADRANT_B.key, | |
| 'Authorization': `Bearer ${QUADRANT_B.key}`, | |
| 'Content-Type': 'application/json', | |
| 'Prefer': 'return=minimal' | |
| }, | |
| body: JSON.stringify({ event, details, created_at: new Date().toISOString() }) | |
| }); | |
| } catch (e) { /* silent fail per non bloccare edge */ } | |
| } | |
| /** Salva un frammento di esperienza nel Quadrante A (BRAIN). */ | |
| async function saveExperience(goal: string, outcome: string, env: Env) { | |
| try { | |
| await fetch(`${QUADRANT_A.url}/rest/v1/semantic_memory`, { | |
| method: 'POST', | |
| headers: { | |
| 'apikey': QUADRANT_A.key, | |
| 'Authorization': `Bearer ${QUADRANT_A.key}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| content: `Goal: ${goal} | Outcome: ${outcome}`, | |
| metadata: { source: 'manus-ai', ts: new Date().toISOString() } | |
| }) | |
| }); | |
| } catch (e) {} | |
| } | |