AUDIT / functions /api /webhook /telegram.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
25.1 kB
/**
* 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 &lt;obiettivo&gt; — 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) {}
}