From b4df19b2c1d93a6cfbb908e0b835540d3823a979 Mon Sep 17 00:00:00 2001 From: Marcos Date: Sat, 11 Jul 2026 21:34:18 +0000 Subject: [PATCH] =?UTF-8?q?fix(audit):=20integrando=20AbortSignal=20na=20r?= =?UTF-8?q?equisi=C3=A7=C3=A3o=20de=20rede=20para=20cancelamento=20instant?= =?UTF-8?q?=C3=A2neo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/components/AuditPanel.tsx | 10 ++++--- app/src/lib/audit/runner.ts | 45 ++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/app/src/components/AuditPanel.tsx b/app/src/components/AuditPanel.tsx index 8338049..9df90c7 100644 --- a/app/src/components/AuditPanel.tsx +++ b/app/src/components/AuditPanel.tsx @@ -66,7 +66,9 @@ export default function AuditPanel() { return; } - abortRef.current = false; + const controller = new AbortController(); + abortRef.current = controller as any; // Usando any pois não precisamos refatorar todos os tipos agora, apenas para passar o signal + setError(null); setReport(null); setProgress(0); @@ -84,13 +86,13 @@ export default function AuditPanel() { scenarios, (p) => { setProgress(p); - if (abortRef.current) throw new Error('Abortado pelo usuário'); }, + controller.signal ); setReport(reportResult); setStatus('done'); } catch (e) { - if (e instanceof Error && e.message === 'Abortado pelo usuário') { + if (e instanceof Error && e.message.includes('Abortado')) { setStatus('idle'); return; } @@ -179,7 +181,7 @@ export default function AuditPanel() { Executar Auditoria {(status === 'running' || status === 'generating') && ( - )} diff --git a/app/src/lib/audit/runner.ts b/app/src/lib/audit/runner.ts index af4a0cc..140b5ca 100644 --- a/app/src/lib/audit/runner.ts +++ b/app/src/lib/audit/runner.ts @@ -27,18 +27,19 @@ export async function runLLMAudit( systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, + signal?: AbortSignal, ): Promise { config = { ...config, apiKey: config.apiKey?.trim() || '' }; if (config.provider === 'openai') { - return callOpenAI(config, systemPrompt, userPrompt, onProgress); + return callOpenAI(config, systemPrompt, userPrompt, onProgress, signal); } else if (config.provider === 'anthropic') { - return callAnthropic(config, systemPrompt, userPrompt, onProgress); + return callAnthropic(config, systemPrompt, userPrompt, onProgress, signal); } else if (config.provider === 'minimax') { - return callMinimax(config, systemPrompt, userPrompt, onProgress); + return callMinimax(config, systemPrompt, userPrompt, onProgress, signal); } else if (config.provider === 'openrouter') { - return callOpenRouter(config, systemPrompt, userPrompt, onProgress); + return callOpenRouter(config, systemPrompt, userPrompt, onProgress, signal); } else { - return callOllama(config, systemPrompt, userPrompt, onProgress); + return callOllama(config, systemPrompt, userPrompt, onProgress, signal); } } @@ -47,6 +48,7 @@ async function callOpenAI( systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, + signal?: AbortSignal, ): Promise { onProgress?.('Enviando para OpenAI...'); const url = 'https://api.openai.com/v1/chat/completions'; @@ -66,7 +68,7 @@ async function callOpenAI( 'Authorization': `Bearer ${config.apiKey}`, }, body: JSON.stringify(body), - }, 180000); + }, 180000, signal); const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } }; if (data.error) throw new Error(`OpenAI error: ${data.error.message}`); const content = data.choices?.[0]?.message?.content; @@ -79,6 +81,7 @@ async function callAnthropic( systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, + signal?: AbortSignal, ): Promise { onProgress?.('Enviando para Anthropic...'); const url = 'https://api.anthropic.com/v1/messages'; @@ -100,7 +103,7 @@ async function callAnthropic( 'anthropic-dangerous-direct-browser-access': 'true', }, body: JSON.stringify(body), - }, 180000); + }, 180000, signal); const data = await response.json() as { content?: { text?: string }[]; error?: { type?: string; message?: string } }; if (data.error) throw new Error(`Anthropic error: ${data.error.message}`); const text = data.content?.[0]?.text; @@ -113,6 +116,7 @@ async function callMinimax( systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, + signal?: AbortSignal, ): Promise { onProgress?.('Enviando para MiniMax...'); const url = config.baseUrl || 'https://api.minimax.chat/v1/chat/completions'; @@ -132,7 +136,7 @@ async function callMinimax( 'Authorization': `Bearer ${config.apiKey}`, }, body: JSON.stringify(body), - }, 180000); + }, 180000, signal); const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } }; if (data.error) throw new Error(`MiniMax error: ${data.error.message}`); const content = data.choices?.[0]?.message?.content; @@ -145,6 +149,7 @@ async function callOpenRouter( systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, + signal?: AbortSignal, ): Promise { onProgress?.('Enviando para OpenRouter...'); const url = config.baseUrl || 'https://openrouter.ai/api/v1/chat/completions'; @@ -165,7 +170,7 @@ async function callOpenRouter( 'X-Title': 'BrainWind NBR 6123', }, body: JSON.stringify(body), - }, 180000); + }, 180000, signal); if (!response.ok) { const errText = await response.text(); throw new Error(`OpenRouter HTTP ${response.status}: ${errText}`); @@ -182,6 +187,7 @@ async function callOllama( systemPrompt: string, userPrompt: string, onProgress?: (msg: string) => void, + signal?: AbortSignal, ): Promise { onProgress?.('Enviando para Ollama (local)...'); const baseUrl = config.baseUrl || 'http://localhost:11434'; @@ -199,7 +205,7 @@ async function callOllama( method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), - }, 300000); + }, 300000, signal); const data = await response.json() as { message?: { content?: string }; error?: string }; if (data.error) throw new Error(`Ollama error: ${data.error}`); if (!data.message?.content) throw new Error('Ollama returned empty response'); @@ -210,6 +216,7 @@ export async function runAudit( config: AuditConfig, scenarios: AuditScenario[], onProgress?: (progress: number) => void, + signal?: AbortSignal, ): Promise { onProgress?.(0); const maxRetriesPerChunk = 2; @@ -220,20 +227,25 @@ export async function runAudit( let rawResponseFull = ''; for (let i = 0; i < scenarios.length; i += chunkSize) { + if (signal?.aborted) { + throw new Error('Abortado pelo usuário'); + } const chunk = scenarios.slice(i, i + chunkSize); let chunkSuccess = false; let chunkError: Error | null = null; for (let retry = 0; retry <= maxRetriesPerChunk; retry++) { + if (signal?.aborted) { + throw new Error('Abortado pelo usuário'); + } try { const prompt = buildPrompt(chunk); // Avisar a UI do progresso absoluto onProgress?.(completed); const rawResponse = await runLLMAudit(config, prompt.system, prompt.user, (msg) => { - // Chamamos o onProgress para disparar a verificação de abortar do AuditPanel - onProgress?.(completed); - }); + // opcional + }, signal); rawResponseFull += rawResponse + '\n\n'; const llmResults = parseLLMResponse(rawResponse, chunk.map(s => s.id)); @@ -262,9 +274,16 @@ async function fetchWithTimeout( url: string, init: RequestInit, timeoutMs: number, + parentSignal?: AbortSignal, ): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); + + if (parentSignal) { + parentSignal.addEventListener('abort', () => controller.abort()); + if (parentSignal.aborted) controller.abort(); + } + try { const response = await fetch(url, { ...init, signal: controller.signal }); return response;