Spaces:
Sleeping
Sleeping
| /** | |
| * deployTools.ts — G-05: Deploy Connectors (CF Pages diretta) | |
| * | |
| * Tool `cf_pages_status`: chiama la CF Pages API DIRETTAMENTE dal browser, | |
| * senza passare per il backend HF Space (che può essere freddo/down). | |
| * | |
| * Usa VITE_CF_API_TOKEN + VITE_CF_ACCOUNT_ID iniettati al build time | |
| * (già presenti in vite.config.ts come injection targets). | |
| * | |
| * Completa `check_deploy` (che richiede backend up) con un fallback robusto. | |
| * | |
| * @module deployTools | |
| */ | |
| import { makeTimedSignal } from "./networkConstants"; | |
| // GAP-C: import ErrorSignatureBuilder for deterministic error parsing in loop | |
| import { buildErrorSignature } from "./errorSignatureBuilder"; | |
| const CF_BASE = "https://api.cloudflare.com/client/v4"; | |
| function cfHeaders(): { Authorization: string; "Content-Type": string } | null { | |
| const token = (import.meta.env.VITE_CF_API_TOKEN ?? "") as string; | |
| const accountId = (import.meta.env.VITE_CF_ACCOUNT_ID ?? "") as string; | |
| if (!token || !accountId) return null; | |
| return { | |
| "Authorization": `Bearer ${token}`, | |
| "Content-Type": "application/json", | |
| }; | |
| } | |
| function cfAccountId(): string { | |
| return (import.meta.env.VITE_CF_ACCOUNT_ID ?? "") as string; | |
| } | |
| /** Emoji e label per lo stato CF Pages */ | |
| function stageIcon(status: string): string { | |
| if (status === "success") return "✅"; | |
| if (status === "failure" || status === "failed") return "❌"; | |
| if (status === "active") return "🔄"; | |
| if (status === "skipped") return "⏭️"; | |
| return "⏳"; | |
| } | |
| // ─── cf_pages_status ───────────────────────────────────────────────────────── | |
| export async function handleCfPagesStatus(args: { | |
| project_name?: string; | |
| show_logs?: boolean; | |
| }): Promise<string> { | |
| const headers = cfHeaders(); | |
| if (!headers) { | |
| return [ | |
| "❌ **cf_pages_status**: credenziali Cloudflare non configurate.", | |
| "Aggiungi `CF_API_TOKEN` e `CF_ACCOUNT_ID` nei secrets del progetto CF Pages.", | |
| "I valori vengono iniettati da `vite.config.ts` al momento del build.", | |
| ].join("\n"); | |
| } | |
| const accountId = cfAccountId(); | |
| const projectName = (args.project_name ?? "agente-ai").trim(); | |
| const BASE = `${CF_BASE}/accounts/${accountId}/pages/projects`; | |
| const sig = makeTimedSignal(12_000); | |
| try { | |
| // ── 1. Ottieni ultimi 5 deployment ─────────────────────────────────────── | |
| const deployRes = await fetch( | |
| `${BASE}/${projectName}/deployments?per_page=5`, | |
| { headers, signal: sig }, | |
| ); | |
| if (!deployRes.ok) { | |
| if (deployRes.status === 404) { | |
| // Prova a listare i progetti disponibili | |
| const projRes = await fetch(BASE, { headers, signal: makeTimedSignal(8_000) }); | |
| if (projRes.ok) { | |
| const proj = await projRes.json() as { result?: { name: string }[] }; | |
| const names = (proj.result ?? []).map(p => p.name).join(", "); | |
| return `❌ Progetto "${projectName}" non trovato su CF Pages.\n📋 Progetti disponibili: ${names || "(nessuno)"}`; | |
| } | |
| } | |
| const body = await deployRes.text().catch(() => ""); | |
| return `❌ CF Pages API HTTP ${deployRes.status}: ${body.slice(0, 200)}`; | |
| } | |
| const data = await deployRes.json() as { | |
| result?: Array<{ | |
| id: string; | |
| created_on: string; | |
| url?: string; | |
| aliases?: string[]; | |
| stages?: Array<{ name: string; status: string; started_on?: string; ended_on?: string }>; | |
| latest_stage?: { name: string; status: string }; | |
| build_config?: { build_command?: string }; | |
| deployment_trigger?: { type: string }; | |
| }>; | |
| }; | |
| const deployments = data.result ?? []; | |
| if (deployments.length === 0) { | |
| return `ℹ️ Nessun deployment trovato per "${projectName}".`; | |
| } | |
| const latest = deployments[0]; | |
| const status = latest.latest_stage?.status ?? latest.stages?.at(-1)?.status ?? "unknown"; | |
| const liveUrl = latest.aliases?.[0] ?? latest.url ?? ""; | |
| const createdAt = new Date(latest.created_on).toLocaleString("it-IT", { | |
| day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", | |
| }); | |
| const trigger = latest.deployment_trigger?.type === "github" ? "push GitHub" : | |
| latest.deployment_trigger?.type === "api" ? "API trigger" : "manuale"; | |
| const lines: string[] = [ | |
| `${stageIcon(status)} **CF Pages — \`${projectName}\`**`, | |
| `📅 ${createdAt} · trigger: ${trigger}`, | |
| `🏷️ Stato: \`${status}\``, | |
| liveUrl ? `🌐 ${liveUrl}` : "", | |
| "", | |
| ]; | |
| // Stadi build con durata | |
| const stages = latest.stages ?? []; | |
| if (stages.length > 0) { | |
| lines.push("**Stadi:**"); | |
| for (const s of stages) { | |
| const dur = s.started_on && s.ended_on | |
| ? ` (${Math.round((+new Date(s.ended_on) - +new Date(s.started_on)) / 1000)}s)` | |
| : ""; | |
| lines.push(` ${stageIcon(s.status)} \`${s.name}\`${dur}`); | |
| } | |
| lines.push(""); | |
| } | |
| // Cronologia recente | |
| if (deployments.length > 1) { | |
| lines.push("**Ultimi deployment:**"); | |
| for (const d of deployments.slice(0, 4)) { | |
| const st = d.latest_stage?.status ?? "?"; | |
| const dt = new Date(d.created_on).toLocaleString("it-IT", { day:"2-digit", month:"short", hour:"2-digit", minute:"2-digit" }); | |
| lines.push(` ${stageIcon(st)} ${dt} — \`${st}\` [${d.id.slice(0, 8)}]`); | |
| } | |
| } | |
| return lines.filter((l, i) => l !== "" || i !== lines.length - 1).join("\n"); | |
| } catch (e) { | |
| if (e instanceof Error && e.name === "TimeoutError") { | |
| return "⏱️ cf_pages_status: timeout (CF API >12s). Riprova tra qualche secondo."; | |
| } | |
| return `❌ cf_pages_status: ${String(e).slice(0, 250)}`; | |
| } | |
| } | |
| // ─── deploy_and_verify ───────────────────────────────────────────────────── | |
| /** | |
| * Fix B: Polls CF Pages build status until success/failure or timeout. | |
| * Chiamato DOPO un git_push che trigga un deployment CF Pages. | |
| * NON avvia un nuovo deploy — monitora quello già in corso. | |
| * | |
| * @param args.project_name CF Pages project (default: "agente-ai") | |
| * @param args.timeout_s Max attesa in secondi (default: 90, max: 180) | |
| */ | |
| export async function handleDeployAndVerify(args: { | |
| project_name?: string; | |
| timeout_s?: number; | |
| }): Promise<string> { | |
| const headers = cfHeaders(); | |
| if (!headers) { | |
| return [ | |
| "❌ **deploy_and_verify**: credenziali CF non configurate.", | |
| "Aggiungi `CF_API_TOKEN` e `CF_ACCOUNT_ID` nei secrets.", | |
| ].join("\n"); | |
| } | |
| const projectName = (args.project_name ?? "agente-ai").trim(); | |
| const maxMs = Math.min((args.timeout_s ?? 90), 180) * 1_000; | |
| const pollMs = 6_000; | |
| const start = Date.now(); | |
| let attempt = 0; | |
| // eslint-disable-next-line no-constant-condition | |
| while (true) { | |
| attempt++; | |
| const snap = await handleCfPagesStatus({ project_name: projectName }); | |
| const elapsed = Math.round((Date.now() - start) / 1_000); | |
| const firstLine = snap.split("\n")[0] ?? ""; | |
| if (firstLine.includes("✅")) { | |
| return `✅ **deploy_and_verify**: build completata in ${elapsed}s (${attempt} poll).\n\n${snap}`; | |
| } | |
| if (firstLine.includes("❌")) { | |
| return `❌ **deploy_and_verify**: build fallita dopo ${elapsed}s.\n\n${snap}`; | |
| } | |
| if (Date.now() - start + pollMs > maxMs) { | |
| return `⏱️ **deploy_and_verify**: timeout ${args.timeout_s ?? 90}s — build ancora in corso.\n\nUltimo stato:\n${snap.slice(0, 500)}`; | |
| } | |
| await new Promise<void>(r => setTimeout(r, pollMs)); | |
| } | |
| } | |
| // ─── auto_deploy_loop ────────────────────────────────────────────────────────── | |
| /** | |
| * GAP-C v2: Deploy loop corretto — single-shot, stateless. | |
| * | |
| * Una chiamata = un tentativo. Dopo un fallimento ritorna gli errori strutturati | |
| * + istruzioni esplicite all'agente LLM per applicare il fix e richiamare. | |
| * | |
| * Flusso corretto: | |
| * Chiamata 1 (attempt=1, monitor_only=false): | |
| * trigger deploy → poll → se fallisce → ritorna errori → STOP | |
| * Agente: legge errori → applica fix → git_push (trigga CF Pages da solo) | |
| * Chiamata 2 (attempt=2, monitor_only=true): | |
| * monitora nuovo deploy → se fallisce → ritorna errori → STOP | |
| * ...fino a attempt === max_attempts | |
| * | |
| * Bug rimosso: il for loop interno ritriggerava il deploy senza che l'agente | |
| * avesse applicato nessun fix — ogni tentativo riceveva lo stesso codice rotto. | |
| * | |
| * @param args.attempt Tentativo corrente 1-based (default 1). L'agente incrementa. | |
| * @param args.monitor_only true sui retry: il git_push ha già triggerato CF Pages. | |
| */ | |
| export async function handleAutoDeployLoop(args: { | |
| project_name?: string; | |
| timeout_s?: number; | |
| max_attempts?: number; | |
| monitor_only?: boolean; | |
| /** Tentativo corrente 1-based. L'agente lo incrementa ad ogni retry dopo un fix. */ | |
| attempt?: number; | |
| }): Promise<string> { | |
| const headers = cfHeaders(); | |
| if (!headers) { | |
| return [ | |
| "❌ **auto_deploy_loop**: credenziali CF non configurate.", | |
| "Aggiungi VITE_CF_API_TOKEN e VITE_CF_ACCOUNT_ID nei secrets CF Pages.", | |
| ].join("\n"); | |
| } | |
| const projectName = (args.project_name ?? "agente-ai").trim(); | |
| const maxAttempts = Math.min(args.max_attempts ?? 3, 5); | |
| const attempt = Math.max(args.attempt ?? 1, 1); | |
| const accountId = cfAccountId(); | |
| const BASE = `${CF_BASE}/accounts/${accountId}/pages/projects`; | |
| const lines: string[] = [ | |
| `🔁 **auto_deploy_loop** — \`${projectName}\` — tentativo ${attempt}/${maxAttempts}`, | |
| ]; | |
| // ── Step 1: Trigger (solo se !monitor_only) ────────────────────────────── | |
| // Sui retry NON si triggera: il git_push ha già avviato un nuovo CF deployment. | |
| if (!args.monitor_only) { | |
| try { | |
| const triggerRes = await fetch( | |
| `${BASE}/${projectName}/deployments`, | |
| { method: "POST", headers, signal: makeTimedSignal(10_000) }, | |
| ); | |
| if (triggerRes.ok) { | |
| const tData = await triggerRes.json() as { result?: { id?: string } }; | |
| lines.push(`⚡ Deploy triggerato: ID ${tData.result?.id ?? "?"}`); | |
| } else if (triggerRes.status === 400) { | |
| lines.push(`ℹ️ Nessun nuovo commit — monitoro deployment corrente`); | |
| } else { | |
| lines.push(`⚠️ Trigger HTTP ${triggerRes.status} — monitoro deployment corrente`); | |
| } | |
| } catch { | |
| lines.push(`⚠️ Trigger fallito — monitoro deployment corrente`); | |
| } | |
| } else { | |
| lines.push(`👁️ monitor_only — monitoro deployment avviato dal git_push`); | |
| } | |
| // ── Step 2: Poll fino a success/failure ───────────────────────────────── | |
| const pollResult = await handleDeployAndVerify({ | |
| project_name: projectName, | |
| timeout_s: args.timeout_s ?? 90, | |
| }); | |
| lines.push(pollResult); | |
| // ── Step 3a: Successo ──────────────────────────────────────────────────── | |
| if (pollResult.startsWith("✅")) { | |
| lines.push(`\n✅ **auto_deploy_loop**: deploy completato al tentativo ${attempt}/${maxAttempts}.`); | |
| return lines.join("\n"); | |
| } | |
| // ── Step 3b: Timeout — build ancora in corso ───────────────────────────── | |
| if (pollResult.startsWith("⏱️")) { | |
| lines.push([ | |
| `\n⏱️ **auto_deploy_loop**: build ancora in corso dopo ${args.timeout_s ?? 90}s.`, | |
| `📋 Richiama con \`monitor_only: true, attempt: ${attempt}, max_attempts: ${maxAttempts}\` per continuare il monitoring.`, | |
| ].join("\n")); | |
| return lines.join("\n"); | |
| } | |
| // ── Step 3c: Build fallita — estrai errori e RITORNA SENZA RETRY ───────── | |
| // Il loop NON riesegue qui. L'agente deve: | |
| // 1. Leggere gli errori sotto | |
| // 2. Applicare i fix (git_push con le correzioni) | |
| // 3. Richiamare auto_deploy_loop con monitor_only:true, attempt:N+1 | |
| let buildErrors = ""; | |
| let errorSig = ""; | |
| try { | |
| const depRes = await fetch( | |
| `${BASE}/${projectName}/deployments?per_page=1`, | |
| { headers, signal: makeTimedSignal(8_000) }, | |
| ); | |
| if (depRes.ok) { | |
| const depData = await depRes.json() as { | |
| result?: Array<{ id: string; stages?: Array<{ name: string; status: string }> }> | |
| }; | |
| const latestId = depData.result?.[0]?.id; | |
| if (latestId) { | |
| const logRes = await fetch( | |
| `${BASE}/${projectName}/deployments/${latestId}/history/logs?size=100`, | |
| { headers, signal: makeTimedSignal(8_000) }, | |
| ); | |
| if (logRes.ok) { | |
| const logData = await logRes.json() as { | |
| result?: { logs?: Array<{ message: string; level?: string }> } | |
| }; | |
| const allLogs = (logData.result?.logs ?? []).map(l => l.message).join("\n"); | |
| const errorLines = allLogs | |
| .split("\n") | |
| .filter(l => | |
| l.includes("error TS") || | |
| l.includes("ERROR") || | |
| l.includes("Build failed") || | |
| l.includes("Cannot find") || | |
| l.includes("Type error") || | |
| (/\(\d+,\d+\)/.test(l) && l.includes("error")), | |
| ) | |
| .slice(0, 20) | |
| .join("\n"); | |
| if (errorLines.trim()) { | |
| buildErrors = errorLines; | |
| errorSig = JSON.stringify(buildErrorSignature(errorLines, "deploy_monitor", {}, 0, 0)); | |
| } | |
| } | |
| } | |
| } | |
| } catch { /* non-fatal — continuiamo senza errori estratti */ } | |
| if (buildErrors) { | |
| lines.push(`\n🔍 **Build errors (tentativo ${attempt})**:\n\`\`\`\n${buildErrors}\n\`\`\``); | |
| if (errorSig) lines.push(`🔑 Error signature: \`${errorSig}\``); | |
| } else { | |
| lines.push(`\n🔍 Nessun errore TypeScript estratto — controlla la pipeline CF Pages manualmente.`); | |
| } | |
| // ── Step 4: Istruzioni precise per l'agente LLM ────────────────────────── | |
| if (attempt < maxAttempts) { | |
| lines.push([ | |
| `\n⚠️ **auto_deploy_loop**: build fallita — tentativo ${attempt}/${maxAttempts}.`, | |
| ``, | |
| `📋 **Prossimi passi (in ordine):**`, | |
| ` 1. Analizza e correggi gli errori sopra nei file indicati`, | |
| ` 2. Usa \`git_push\` per pushare le correzioni (trigga CF Pages automaticamente)`, | |
| ` 3. Richiama \`auto_deploy_loop\` con:`, | |
| ` \`monitor_only: true\` ← non ritriggerare, il push ha già avviato il deploy`, | |
| ` \`attempt: ${attempt + 1}\``, | |
| ` \`max_attempts: ${maxAttempts}\``, | |
| ].join("\n")); | |
| } else { | |
| lines.push([ | |
| `\n❌ **auto_deploy_loop**: max tentativi raggiunti (${attempt}/${maxAttempts}).`, | |
| `Intervento manuale necessario — controlla la pipeline CF Pages.`, | |
| ].join("\n")); | |
| } | |
| return lines.join("\n"); | |
| } | |