INDEX / modules /APIClient.js
akra35567's picture
Upload 18 files
7226ab4 verified
/**
* ═══════════════════════════════════════════════════════════════════════
* CLASSE: APIClient
* ═══════════════════════════════════════════════════════════════════════
* Cliente HTTP com retry automΓ‘tico, conformidade com api.py payload
* Gerencia todas as comunicaΓ§Γ΅es com o backend Python
* ═══════════════════════════════════════════════════════════════════════
*/
const axios = require('axios');
const ConfigManager = require('./ConfigManager');
class APIClient {
constructor(logger = null) {
this.config = ConfigManager.getInstance();
this.logger = logger || console;
this.requestCount = 0;
this.errorCount = 0;
}
/**
* Formata payload conforme esperado por api.py
*/
buildPayload(messageData) {
const {
usuario,
numero,
mensagem,
tipo_conversa = 'pv',
tipo_mensagem = 'texto',
mensagem_citada = '',
reply_metadata = {},
imagem_dados = null,
grupo_id = null,
grupo_nome = null,
forcar_pesquisa = false
} = messageData;
const payload = {
usuario: String(usuario || 'anonimo').substring(0, 50),
numero: String(numero || 'desconhecido').substring(0, 20),
mensagem: String(mensagem || '').substring(0, 2000),
tipo_conversa: ['pv', 'grupo'].includes(tipo_conversa) ? tipo_conversa : 'pv',
tipo_mensagem: ['texto', 'image', 'audio', 'video'].includes(tipo_mensagem) ? tipo_mensagem : 'texto',
historico: [],
forcar_busca: Boolean(forcar_pesquisa)
};
// Adiciona contexto de reply se existir
if (mensagem_citada) {
payload.mensagem_citada = String(mensagem_citada).substring(0, 500);
payload.reply_metadata = {
is_reply: true,
reply_to_bot: Boolean(reply_metadata.reply_to_bot),
quoted_author_name: String(reply_metadata.quoted_author_name || 'desconhecido').substring(0, 50),
quoted_author_numero: String(reply_metadata.quoted_author_numero || 'desconhecido'),
quoted_type: String(reply_metadata.quoted_type || 'texto'),
quoted_text_original: String(reply_metadata.quoted_text_original || '').substring(0, 200),
context_hint: String(reply_metadata.context_hint || '')
};
} else {
payload.reply_metadata = {
is_reply: false,
reply_to_bot: false
};
}
// Adiciona dados de imagem se existirem
if (imagem_dados && imagem_dados.dados) {
payload.imagem = {
dados: imagem_dados.dados,
mime_type: imagem_dados.mime_type || 'image/jpeg',
descricao: imagem_dados.descricao || 'Imagem enviada',
analise_visao: imagem_dados.analise_visao || {}
};
}
// Adiciona info de grupo se existir
if (grupo_id) {
payload.grupo_id = grupo_id;
payload.contexto_grupo = grupo_nome || 'Grupo';
}
return payload;
}
/**
* Realiza requisiΓ§Γ£o com retry exponencial
*/
async request(method, endpoint, data = null, options = {}) {
const url = `${this.config.API_URL}${endpoint}`;
const maxRetries = options.retries || this.config.API_RETRY_ATTEMPTS;
let lastError = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
this.requestCount++;
if (this.config.LOG_API_REQUESTS) {
this.logger.info(`[API] ${method.toUpperCase()} ${endpoint} (tentativa ${attempt}/${maxRetries})`);
}
const axiosConfig = {
method,
url,
timeout: this.config.API_TIMEOUT,
headers: {
'Content-Type': 'application/json',
'User-Agent': `AkiraBot/${this.config.BOT_VERSION}`
},
...options
};
if (data) {
axiosConfig.data = data;
}
const response = await axios(axiosConfig);
if (response.status >= 200 && response.status < 300) {
if (this.config.LOG_API_REQUESTS) {
this.logger.info(`[API] βœ… ${endpoint} (${response.status})`);
}
return { success: true, data: response.data, status: response.status };
}
} catch (error) {
lastError = error;
const statusCode = error.response?.status;
const errorMsg = error.response?.data?.error || error.message;
if (this.config.LOG_API_REQUESTS) {
this.logger.warn(`[API] ⚠️ Erro ${statusCode || 'NETWORK'}: ${errorMsg} (tentativa ${attempt}/${maxRetries})`);
}
// NΓ£o retry em erros 4xx (exceto timeout)
if (statusCode >= 400 && statusCode < 500 && statusCode !== 408) {
this.errorCount++;
return { success: false, error: errorMsg, status: statusCode };
}
// Retry com delay exponencial
if (attempt < maxRetries) {
const delayMs = this.config.API_RETRY_DELAY * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
this.errorCount++;
const errorMsg = lastError?.response?.data?.error || lastError?.message || 'Erro desconhecido';
if (this.config.LOG_API_REQUESTS) {
this.logger.error(`[API] ❌ Falhou após ${maxRetries} tentativas: ${errorMsg}`);
}
return { success: false, error: errorMsg, lastError };
}
/**
* Envia mensagem para processar na API
*/
async processMessage(messageData) {
try {
const payload = this.buildPayload(messageData);
const result = await this.request('POST', '/akira', payload);
if (result.success) {
return {
success: true,
resposta: result.data?.resposta || 'Sem resposta',
tipo_mensagem: result.data?.tipo_mensagem || 'texto',
pesquisa_feita: result.data?.pesquisa_feita || false,
metadata: result.data
};
} else {
return {
success: false,
resposta: 'Eita! Tive um problema aqui. Tenta de novo em um segundo?',
error: result.error
};
}
} catch (error) {
this.logger.error('[API] Erro ao processar mensagem:', error.message);
return {
success: false,
resposta: 'Deu um erro interno aqui. Tenta depois?',
error: error.message
};
}
}
/**
* Faz requisiΓ§Γ£o para anΓ‘lise de visΓ£o
*/
async analyzeImage(imageBase64, usuario = 'anonimo', numero = '') {
try {
const result = await this.request('POST', '/vision/analyze', {
imagem: imageBase64,
usuario,
numero,
include_ocr: true,
include_shapes: true,
include_objects: true
});
if (result.success) {
return {
success: true,
analise: result.data
};
} else {
return {
success: false,
error: result.error
};
}
} catch (error) {
this.logger.error('[VISION] Erro ao analisar imagem:', error.message);
return {
success: false,
error: error.message
};
}
}
/**
* Faz OCR em imagem
*/
async performOCR(imageBase64, numero = '') {
try {
const result = await this.request('POST', '/vision/ocr', {
imagem: imageBase64,
numero
});
if (result.success) {
return {
success: true,
text: result.data?.text || '',
confidence: result.data?.confidence || 0,
word_count: result.data?.word_count || 0
};
} else {
return {
success: false,
error: result.error
};
}
} catch (error) {
this.logger.error('[OCR] Erro ao fazer OCR:', error.message);
return {
success: false,
error: error.message
};
}
}
/**
* Requisita reset da API
*/
async reset(usuario = null) {
try {
const payload = usuario ? { usuario } : {};
const result = await this.request('POST', '/reset', payload);
return {
success: result.success,
status: result.data?.status || 'reset_attempted',
message: result.data?.message || 'Reset solicitado'
};
} catch (error) {
this.logger.error('[RESET] Erro ao fazer reset:', error.message);
return {
success: false,
error: error.message
};
}
}
/**
* Health check
*/
async healthCheck() {
try {
const result = await this.request('GET', '/health');
return {
success: result.success,
status: result.data?.status || 'unknown',
version: result.data?.version || 'unknown'
};
} catch (error) {
return {
success: false,
status: 'down',
error: error.message
};
}
}
/**
* Retorna estatΓ­sticas
*/
getStats() {
return {
totalRequests: this.requestCount,
totalErrors: this.errorCount,
errorRate: this.requestCount > 0 ? (this.errorCount / this.requestCount * 100).toFixed(2) + '%' : '0%'
};
}
}
module.exports = APIClient;