fix(audit): integrando AbortSignal na requisição de rede para cancelamento instantâneo

This commit is contained in:
2026-07-11 21:34:18 +00:00
parent b5b5b94595
commit b4df19b2c1
2 changed files with 38 additions and 17 deletions
+6 -4
View File
@@ -66,7 +66,9 @@ export default function AuditPanel() {
return; 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); setError(null);
setReport(null); setReport(null);
setProgress(0); setProgress(0);
@@ -84,13 +86,13 @@ export default function AuditPanel() {
scenarios, scenarios,
(p) => { (p) => {
setProgress(p); setProgress(p);
if (abortRef.current) throw new Error('Abortado pelo usuário');
}, },
controller.signal
); );
setReport(reportResult); setReport(reportResult);
setStatus('done'); setStatus('done');
} catch (e) { } catch (e) {
if (e instanceof Error && e.message === 'Abortado pelo usuário') { if (e instanceof Error && e.message.includes('Abortado')) {
setStatus('idle'); setStatus('idle');
return; return;
} }
@@ -179,7 +181,7 @@ export default function AuditPanel() {
Executar Auditoria Executar Auditoria
</Button> </Button>
{(status === 'running' || status === 'generating') && ( {(status === 'running' || status === 'generating') && (
<Button variant="outline" size="sm" onClick={() => { abortRef.current = true; }}> <Button variant="outline" size="sm" onClick={() => { if (abortRef.current && typeof (abortRef.current as any).abort === 'function') (abortRef.current as any).abort(); }}>
Cancelar Cancelar
</Button> </Button>
)} )}
+32 -13
View File
@@ -27,18 +27,19 @@ export async function runLLMAudit(
systemPrompt: string, systemPrompt: string,
userPrompt: string, userPrompt: string,
onProgress?: (msg: string) => void, onProgress?: (msg: string) => void,
signal?: AbortSignal,
): Promise<string> { ): Promise<string> {
config = { ...config, apiKey: config.apiKey?.trim() || '' }; config = { ...config, apiKey: config.apiKey?.trim() || '' };
if (config.provider === 'openai') { if (config.provider === 'openai') {
return callOpenAI(config, systemPrompt, userPrompt, onProgress); return callOpenAI(config, systemPrompt, userPrompt, onProgress, signal);
} else if (config.provider === 'anthropic') { } else if (config.provider === 'anthropic') {
return callAnthropic(config, systemPrompt, userPrompt, onProgress); return callAnthropic(config, systemPrompt, userPrompt, onProgress, signal);
} else if (config.provider === 'minimax') { } else if (config.provider === 'minimax') {
return callMinimax(config, systemPrompt, userPrompt, onProgress); return callMinimax(config, systemPrompt, userPrompt, onProgress, signal);
} else if (config.provider === 'openrouter') { } else if (config.provider === 'openrouter') {
return callOpenRouter(config, systemPrompt, userPrompt, onProgress); return callOpenRouter(config, systemPrompt, userPrompt, onProgress, signal);
} else { } else {
return callOllama(config, systemPrompt, userPrompt, onProgress); return callOllama(config, systemPrompt, userPrompt, onProgress, signal);
} }
} }
@@ -47,6 +48,7 @@ async function callOpenAI(
systemPrompt: string, systemPrompt: string,
userPrompt: string, userPrompt: string,
onProgress?: (msg: string) => void, onProgress?: (msg: string) => void,
signal?: AbortSignal,
): Promise<string> { ): Promise<string> {
onProgress?.('Enviando para OpenAI...'); onProgress?.('Enviando para OpenAI...');
const url = 'https://api.openai.com/v1/chat/completions'; const url = 'https://api.openai.com/v1/chat/completions';
@@ -66,7 +68,7 @@ async function callOpenAI(
'Authorization': `Bearer ${config.apiKey}`, 'Authorization': `Bearer ${config.apiKey}`,
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}, 180000); }, 180000, signal);
const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } }; const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } };
if (data.error) throw new Error(`OpenAI error: ${data.error.message}`); if (data.error) throw new Error(`OpenAI error: ${data.error.message}`);
const content = data.choices?.[0]?.message?.content; const content = data.choices?.[0]?.message?.content;
@@ -79,6 +81,7 @@ async function callAnthropic(
systemPrompt: string, systemPrompt: string,
userPrompt: string, userPrompt: string,
onProgress?: (msg: string) => void, onProgress?: (msg: string) => void,
signal?: AbortSignal,
): Promise<string> { ): Promise<string> {
onProgress?.('Enviando para Anthropic...'); onProgress?.('Enviando para Anthropic...');
const url = 'https://api.anthropic.com/v1/messages'; const url = 'https://api.anthropic.com/v1/messages';
@@ -100,7 +103,7 @@ async function callAnthropic(
'anthropic-dangerous-direct-browser-access': 'true', 'anthropic-dangerous-direct-browser-access': 'true',
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}, 180000); }, 180000, signal);
const data = await response.json() as { content?: { text?: string }[]; error?: { type?: string; message?: string } }; 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}`); if (data.error) throw new Error(`Anthropic error: ${data.error.message}`);
const text = data.content?.[0]?.text; const text = data.content?.[0]?.text;
@@ -113,6 +116,7 @@ async function callMinimax(
systemPrompt: string, systemPrompt: string,
userPrompt: string, userPrompt: string,
onProgress?: (msg: string) => void, onProgress?: (msg: string) => void,
signal?: AbortSignal,
): Promise<string> { ): Promise<string> {
onProgress?.('Enviando para MiniMax...'); onProgress?.('Enviando para MiniMax...');
const url = config.baseUrl || 'https://api.minimax.chat/v1/chat/completions'; const url = config.baseUrl || 'https://api.minimax.chat/v1/chat/completions';
@@ -132,7 +136,7 @@ async function callMinimax(
'Authorization': `Bearer ${config.apiKey}`, 'Authorization': `Bearer ${config.apiKey}`,
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}, 180000); }, 180000, signal);
const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } }; const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } };
if (data.error) throw new Error(`MiniMax error: ${data.error.message}`); if (data.error) throw new Error(`MiniMax error: ${data.error.message}`);
const content = data.choices?.[0]?.message?.content; const content = data.choices?.[0]?.message?.content;
@@ -145,6 +149,7 @@ async function callOpenRouter(
systemPrompt: string, systemPrompt: string,
userPrompt: string, userPrompt: string,
onProgress?: (msg: string) => void, onProgress?: (msg: string) => void,
signal?: AbortSignal,
): Promise<string> { ): Promise<string> {
onProgress?.('Enviando para OpenRouter...'); onProgress?.('Enviando para OpenRouter...');
const url = config.baseUrl || 'https://openrouter.ai/api/v1/chat/completions'; const url = config.baseUrl || 'https://openrouter.ai/api/v1/chat/completions';
@@ -165,7 +170,7 @@ async function callOpenRouter(
'X-Title': 'BrainWind NBR 6123', 'X-Title': 'BrainWind NBR 6123',
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}, 180000); }, 180000, signal);
if (!response.ok) { if (!response.ok) {
const errText = await response.text(); const errText = await response.text();
throw new Error(`OpenRouter HTTP ${response.status}: ${errText}`); throw new Error(`OpenRouter HTTP ${response.status}: ${errText}`);
@@ -182,6 +187,7 @@ async function callOllama(
systemPrompt: string, systemPrompt: string,
userPrompt: string, userPrompt: string,
onProgress?: (msg: string) => void, onProgress?: (msg: string) => void,
signal?: AbortSignal,
): Promise<string> { ): Promise<string> {
onProgress?.('Enviando para Ollama (local)...'); onProgress?.('Enviando para Ollama (local)...');
const baseUrl = config.baseUrl || 'http://localhost:11434'; const baseUrl = config.baseUrl || 'http://localhost:11434';
@@ -199,7 +205,7 @@ async function callOllama(
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}, 300000); }, 300000, signal);
const data = await response.json() as { message?: { content?: string }; error?: string }; const data = await response.json() as { message?: { content?: string }; error?: string };
if (data.error) throw new Error(`Ollama error: ${data.error}`); if (data.error) throw new Error(`Ollama error: ${data.error}`);
if (!data.message?.content) throw new Error('Ollama returned empty response'); if (!data.message?.content) throw new Error('Ollama returned empty response');
@@ -210,6 +216,7 @@ export async function runAudit(
config: AuditConfig, config: AuditConfig,
scenarios: AuditScenario[], scenarios: AuditScenario[],
onProgress?: (progress: number) => void, onProgress?: (progress: number) => void,
signal?: AbortSignal,
): Promise<AuditReport> { ): Promise<AuditReport> {
onProgress?.(0); onProgress?.(0);
const maxRetriesPerChunk = 2; const maxRetriesPerChunk = 2;
@@ -220,20 +227,25 @@ export async function runAudit(
let rawResponseFull = ''; let rawResponseFull = '';
for (let i = 0; i < scenarios.length; i += chunkSize) { 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); const chunk = scenarios.slice(i, i + chunkSize);
let chunkSuccess = false; let chunkSuccess = false;
let chunkError: Error | null = null; let chunkError: Error | null = null;
for (let retry = 0; retry <= maxRetriesPerChunk; retry++) { for (let retry = 0; retry <= maxRetriesPerChunk; retry++) {
if (signal?.aborted) {
throw new Error('Abortado pelo usuário');
}
try { try {
const prompt = buildPrompt(chunk); const prompt = buildPrompt(chunk);
// Avisar a UI do progresso absoluto // Avisar a UI do progresso absoluto
onProgress?.(completed); onProgress?.(completed);
const rawResponse = await runLLMAudit(config, prompt.system, prompt.user, (msg) => { const rawResponse = await runLLMAudit(config, prompt.system, prompt.user, (msg) => {
// Chamamos o onProgress para disparar a verificação de abortar do AuditPanel // opcional
onProgress?.(completed); }, signal);
});
rawResponseFull += rawResponse + '\n\n'; rawResponseFull += rawResponse + '\n\n';
const llmResults = parseLLMResponse(rawResponse, chunk.map(s => s.id)); const llmResults = parseLLMResponse(rawResponse, chunk.map(s => s.id));
@@ -262,9 +274,16 @@ async function fetchWithTimeout(
url: string, url: string,
init: RequestInit, init: RequestInit,
timeoutMs: number, timeoutMs: number,
parentSignal?: AbortSignal,
): Promise<Response> { ): Promise<Response> {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs); const timeout = setTimeout(() => controller.abort(), timeoutMs);
if (parentSignal) {
parentSignal.addEventListener('abort', () => controller.abort());
if (parentSignal.aborted) controller.abort();
}
try { try {
const response = await fetch(url, { ...init, signal: controller.signal }); const response = await fetch(url, { ...init, signal: controller.signal });
return response; return response;