feat(audit): adicionar módulo de auditoria + rebranding BrainWind

- AuditPanel, AuditDetailPopup, parser, runner, prompt-builder, serializer, scenarios, export-audit-pdf
- i18n: strings novas para auditoria
- Settings: integração com módulo de auditoria
- types: tipos compartilhados do módulo audit
This commit is contained in:
2026-07-10 11:38:12 +00:00
parent dc3721e12b
commit d40b3b0c9c
12 changed files with 1657 additions and 14 deletions
+247
View File
@@ -0,0 +1,247 @@
import type { AuditConfig, AuditScenario, AuditReport } from './types';
import { buildPrompt } from './prompt-builder';
import { parseLLMResponse, buildAuditReport } from './parser';
interface OpenAIMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
interface OpenAIRequest {
model: string;
messages: OpenAIMessage[];
temperature: number;
response_format: { type: 'json_object' };
}
interface AnthropicRequest {
model: string;
messages: { role: 'user' | 'assistant'; content: string }[];
system?: string;
temperature: number;
max_tokens: number;
}
export async function runLLMAudit(
config: AuditConfig,
systemPrompt: string,
userPrompt: string,
onProgress?: (msg: string) => void,
): Promise<string> {
if (config.provider === 'openai') {
return callOpenAI(config, systemPrompt, userPrompt, onProgress);
} else if (config.provider === 'anthropic') {
return callAnthropic(config, systemPrompt, userPrompt, onProgress);
} else if (config.provider === 'minimax') {
return callMinimax(config, systemPrompt, userPrompt, onProgress);
} else if (config.provider === 'openrouter') {
return callOpenRouter(config, systemPrompt, userPrompt, onProgress);
} else {
return callOllama(config, systemPrompt, userPrompt, onProgress);
}
}
async function callOpenAI(
config: AuditConfig,
systemPrompt: string,
userPrompt: string,
onProgress?: (msg: string) => void,
): Promise<string> {
onProgress?.('Enviando para OpenAI...');
const url = 'https://api.openai.com/v1/chat/completions';
const body: OpenAIRequest = {
model: config.model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
temperature: 0,
response_format: { type: 'json_object' },
};
const response = await fetchWithTimeout(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`,
},
body: JSON.stringify(body),
}, 180000);
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;
if (!content) throw new Error('OpenAI returned empty response');
return content;
}
async function callAnthropic(
config: AuditConfig,
systemPrompt: string,
userPrompt: string,
onProgress?: (msg: string) => void,
): Promise<string> {
onProgress?.('Enviando para Anthropic...');
const url = 'https://api.anthropic.com/v1/messages';
const body: AnthropicRequest = {
model: config.model,
messages: [
{ role: 'user', content: `${systemPrompt}\n\n${userPrompt}` },
],
system: systemPrompt,
temperature: 0,
max_tokens: 8192,
};
const response = await fetchWithTimeout(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': config.apiKey,
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true',
},
body: JSON.stringify(body),
}, 180000);
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;
if (!text) throw new Error('Anthropic returned empty response');
return text;
}
async function callMinimax(
config: AuditConfig,
systemPrompt: string,
userPrompt: string,
onProgress?: (msg: string) => void,
): Promise<string> {
onProgress?.('Enviando para MiniMax...');
const url = config.baseUrl ?? 'https://api.minimax.chat/v1/chat/completions';
const body = {
model: config.model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
temperature: 0,
response_format: { type: 'json_object' },
};
const response = await fetchWithTimeout(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`,
},
body: JSON.stringify(body),
}, 180000);
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;
if (!content) throw new Error('MiniMax returned empty response');
return content;
}
async function callOpenRouter(
config: AuditConfig,
systemPrompt: string,
userPrompt: string,
onProgress?: (msg: string) => void,
): Promise<string> {
onProgress?.('Enviando para OpenRouter...');
const url = config.baseUrl ?? 'https://openrouter.ai/api/v1/chat/completions';
const body: OpenAIRequest = {
model: config.model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
temperature: 0,
response_format: { type: 'json_object' },
};
const response = await fetchWithTimeout(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`,
},
body: JSON.stringify(body),
}, 180000);
const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } };
if (data.error) throw new Error(`OpenRouter error: ${data.error.message}`);
const content = data.choices?.[0]?.message?.content;
if (!content) throw new Error('OpenRouter returned empty response');
return content;
}
async function callOllama(
config: AuditConfig,
systemPrompt: string,
userPrompt: string,
onProgress?: (msg: string) => void,
): Promise<string> {
onProgress?.('Enviando para Ollama (local)...');
const baseUrl = config.baseUrl ?? 'http://localhost:11434';
const body = {
model: config.model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
stream: false,
format: 'json',
options: { temperature: 0 },
};
const response = await fetchWithTimeout(`${baseUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}, 300000);
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');
return data.message.content;
}
export async function runAudit(
config: AuditConfig,
scenarios: AuditScenario[],
onProgress?: (progress: number) => void,
): Promise<AuditReport> {
onProgress?.(0);
const maxRetries = 2;
const chunkSize = Math.ceil(scenarios.length / 1);
let lastError: Error | null = null;
for (let retry = 0; retry <= maxRetries; retry++) {
try {
const prompt = buildPrompt(scenarios);
const rawResponse = await runLLMAudit(config, prompt.system, prompt.user, () => {
onProgress?.(Math.round((retry * chunkSize) / maxRetries));
});
const llmResults = parseLLMResponse(rawResponse, scenarios.map(s => s.id));
const scenarioMap = new Map(scenarios.map(s => [s.id, { module: s.module, moduleLabel: s.moduleLabel }]));
const report = buildAuditReport(llmResults, scenarioMap, config.provider, config.model, rawResponse);
onProgress?.(scenarios.length);
return report;
} catch (e) {
lastError = e instanceof Error ? e : new Error(String(e));
if (retry < maxRetries) {
onProgress?.(0);
}
}
}
throw lastError ?? new Error('Audit failed after retries');
}
async function fetchWithTimeout(
url: string,
init: RequestInit,
timeoutMs: number,
): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { ...init, signal: controller.signal });
return response;
} finally {
clearTimeout(timeout);
}
}