fix(audit): integrando AbortSignal na requisição de rede para cancelamento instantâneo
This commit is contained in:
@@ -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
|
||||
</Button>
|
||||
{(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
|
||||
</Button>
|
||||
)}
|
||||
|
||||
+32
-13
@@ -27,18 +27,19 @@ export async function runLLMAudit(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
onProgress?: (msg: string) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<AuditReport> {
|
||||
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<Response> {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user