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;
|
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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user