565 lines
17 KiB
TypeScript
565 lines
17 KiB
TypeScript
/**
|
|
* Kernel Adapter - Ponte entre Engine Room e Arcadia Kernel
|
|
*
|
|
* MODO NORMAL: Conecta ao Kernel local (porta 5001) para gerenciar serviços
|
|
* MODO DOCKER: Consulta diretamente os serviços externos via HTTP health check
|
|
*
|
|
* A detecção de modo Docker é automática via variável DOCKER_MODE
|
|
*/
|
|
|
|
const KERNEL_BASE_URL = 'http://localhost:5001/api/kernel';
|
|
|
|
// Detectar modo Docker - em produção os serviços rodam em containers separados
|
|
const IS_DOCKER_MODE = process.env.DOCKER_MODE === 'true';
|
|
|
|
// URLs dos serviços externos em modo Docker (vindas das env vars)
|
|
// Em produção (Coolify), os containers têm nomes prefixados como 'arcadia-prod-{nome}-1'
|
|
// IMPORTANTE: Esta função é chamada em RUNTIME, não em build time
|
|
function getServiceUrl(engineName: string): string {
|
|
switch (engineName) {
|
|
case 'contabil':
|
|
return process.env.CONTABIL_PYTHON_URL || 'http://contabil:8003';
|
|
case 'bi-engine':
|
|
return process.env.BI_PYTHON_URL || 'http://bi:8004';
|
|
case 'automation-engine':
|
|
return process.env.AUTOMATION_PYTHON_URL || 'http://automation:8005';
|
|
case 'fisco':
|
|
return process.env.FISCO_PYTHON_URL || 'http://fisco:8002';
|
|
case 'communication':
|
|
return process.env.MIROFLOW_HOST
|
|
? `http://${process.env.MIROFLOW_HOST}:${process.env.MIROFLOW_PORT || 8006}`
|
|
: 'http://miroflow:8006';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// Mapeamento: Nome do Engine Room -> ID do Kernel
|
|
// NOTA: metaset NÃO está aqui - é gerenciado externamente (Java)
|
|
const ENGINE_TO_KERNEL_ID: Record<string, string> = {
|
|
"contabil": "python-contabil",
|
|
"bi-engine": "python-bi",
|
|
"automation-engine": "python-automation",
|
|
"fisco": "python-fisco",
|
|
"communication": "node-communication",
|
|
};
|
|
|
|
// Mapeamento inverso
|
|
const KERNEL_TO_ENGINE_NAME: Record<string, string> = {
|
|
"python-contabil": "contabil",
|
|
"python-bi": "bi-engine",
|
|
"python-automation": "automation-engine",
|
|
"python-fisco": "fisco",
|
|
"node-communication": "communication",
|
|
};
|
|
|
|
// Mapeamento de categorias
|
|
const CATEGORY_MAP: Record<string, string> = {
|
|
"python-contabil": "fiscal",
|
|
"python-bi": "data",
|
|
"python-automation": "automation",
|
|
"python-fisco": "fiscal",
|
|
"node-communication": "intelligence",
|
|
};
|
|
|
|
// Mapeamento de display names
|
|
const DISPLAY_NAME_MAP: Record<string, string> = {
|
|
"python-contabil": "Motor Contabil",
|
|
"python-bi": "Motor BI",
|
|
"python-automation": "Motor Automacao",
|
|
"python-fisco": "Motor Fiscal",
|
|
"node-communication": "Motor Comunicacao",
|
|
};
|
|
|
|
// Configurações dos engines para health check em modo Docker
|
|
const ENGINE_CONFIG: Record<string, { port: number; type: string; healthPath: string }> = {
|
|
"contabil": { port: 8003, type: "python", healthPath: "/health" },
|
|
"bi-engine": { port: 8004, type: "python", healthPath: "/health" },
|
|
"automation-engine": { port: 8005, type: "python", healthPath: "/health" },
|
|
"fisco": { port: 8002, type: "python", healthPath: "/health" },
|
|
"communication": { port: 8006, type: "node", healthPath: "/health" },
|
|
};
|
|
|
|
interface KernelServiceState {
|
|
config: {
|
|
id: string;
|
|
name: string;
|
|
type: string;
|
|
port: number;
|
|
autoStart: boolean;
|
|
maxRestarts: number;
|
|
};
|
|
status: 'running' | 'stopped' | 'error' | 'restarting';
|
|
pid?: number;
|
|
startTime?: string;
|
|
restartCount: number;
|
|
health?: 'healthy' | 'unhealthy' | 'unknown';
|
|
}
|
|
|
|
interface EngineStatus {
|
|
name: string;
|
|
displayName: string;
|
|
type: string;
|
|
port: number;
|
|
category: string;
|
|
description: string;
|
|
status: "online" | "offline" | "error";
|
|
responseTime?: number;
|
|
details?: any;
|
|
error?: string;
|
|
// Registry integration
|
|
source?: 'registry' | 'static' | 'external';
|
|
registryId?: string;
|
|
registrySource?: string;
|
|
}
|
|
|
|
// Busca serviços do Registry do Kernel
|
|
async function fetchRegistryServices(): Promise<any[] | null> {
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
|
|
const response = await fetch('http://localhost:5001/api/registry/services', {
|
|
signal: controller.signal,
|
|
});
|
|
|
|
clearTimeout(timeout);
|
|
|
|
if (!response.ok) return null;
|
|
|
|
const data = await response.json();
|
|
return data.success && Array.isArray(data.data?.services) ? data.data.services : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Mapeia serviço do Registry para EngineStatus
|
|
function mapRegistryToEngine(service: any): EngineStatus {
|
|
const id = service.id || service.name;
|
|
|
|
// Mapear ID do Registry para nome do Engine Room
|
|
const nameMap: Record<string, string> = {
|
|
'python-contabil': 'contabil',
|
|
'python-bi': 'bi-engine',
|
|
'python-automation': 'automation-engine',
|
|
'python-fisco': 'fisco',
|
|
'python-fiscal': 'fisco',
|
|
'node-communication': 'communication',
|
|
'miroflow-communication': 'communication',
|
|
'metaset-bi': 'metaset',
|
|
'superset-bi': 'metaset',
|
|
};
|
|
|
|
const name = nameMap[id] || nameMap[service.name] || id;
|
|
|
|
// Mapear categoria
|
|
const catMap: Record<string, string> = {
|
|
'fiscal': 'fiscal',
|
|
'contabil': 'fiscal',
|
|
'bi': 'data',
|
|
'data': 'data',
|
|
'automation': 'automation',
|
|
'communication': 'intelligence',
|
|
'intelligence': 'intelligence',
|
|
'erp': 'erp',
|
|
};
|
|
|
|
const category = catMap[service.category] || service.category || 'data';
|
|
|
|
// Mapear health status
|
|
let status: 'online' | 'offline' | 'error' = 'offline';
|
|
if (service.healthStatus === 'healthy') status = 'online';
|
|
else if (service.healthStatus === 'unhealthy') status = 'error';
|
|
|
|
return {
|
|
name,
|
|
displayName: service.displayName || service.name,
|
|
type: service.type || 'unknown',
|
|
port: service.port || 0,
|
|
category,
|
|
description: service.description || `Serviço ${service.name}`,
|
|
status,
|
|
details: {
|
|
endpoint: service.endpoint,
|
|
health: service.healthStatus,
|
|
version: service.version,
|
|
capabilities: service.capabilities?.map((c: any) => c.name) || [],
|
|
},
|
|
source: 'registry',
|
|
registryId: service.id,
|
|
registrySource: service.metadata?.source,
|
|
};
|
|
}
|
|
|
|
// Health check direto de um serviço (modo Docker)
|
|
async function checkServiceHealth(engineName: string): Promise<EngineStatus> {
|
|
const config = ENGINE_CONFIG[engineName];
|
|
// IMPORTANTE: getServiceUrl é chamado em RUNTIME, não em build time
|
|
const serviceUrl = getServiceUrl(engineName);
|
|
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
|
|
|
// DEBUG: Log da URL sendo usada
|
|
// console.log(`[DEBUG] checkServiceHealth(${engineName}) => URL: ${serviceUrl}, env: ${process.env.CONTABIL_PYTHON_URL || 'N/A'}`);
|
|
|
|
if (!config || !serviceUrl) {
|
|
return {
|
|
name: engineName,
|
|
displayName: DISPLAY_NAME_MAP[kernelId || engineName] || engineName,
|
|
type: "unknown",
|
|
port: 0,
|
|
category: CATEGORY_MAP[kernelId || engineName] || "data",
|
|
description: `Serviço ${engineName}`,
|
|
status: "offline",
|
|
error: "Configuração não encontrada",
|
|
source: 'static' as const,
|
|
};
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
|
|
try {
|
|
const start = Date.now();
|
|
const response = await fetch(`${serviceUrl}${config.healthPath}`, {
|
|
signal: controller.signal,
|
|
});
|
|
clearTimeout(timeout);
|
|
const elapsed = Date.now() - start;
|
|
|
|
if (response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
return {
|
|
name: engineName,
|
|
displayName: DISPLAY_NAME_MAP[kernelId] || engineName,
|
|
type: config.type,
|
|
port: config.port,
|
|
category: CATEGORY_MAP[kernelId] || "data",
|
|
description: `Serviço ${engineName}`,
|
|
status: "online",
|
|
responseTime: elapsed,
|
|
details: {
|
|
url: serviceUrl,
|
|
health: "healthy",
|
|
...data,
|
|
},
|
|
};
|
|
} else {
|
|
return {
|
|
name: engineName,
|
|
displayName: DISPLAY_NAME_MAP[kernelId] || engineName,
|
|
type: config.type,
|
|
port: config.port,
|
|
category: CATEGORY_MAP[kernelId] || "data",
|
|
description: `Serviço ${engineName}`,
|
|
status: "error",
|
|
responseTime: elapsed,
|
|
details: {
|
|
url: serviceUrl,
|
|
httpStatus: response.status,
|
|
},
|
|
};
|
|
}
|
|
} catch (err: any) {
|
|
clearTimeout(timeout);
|
|
return {
|
|
name: engineName,
|
|
displayName: DISPLAY_NAME_MAP[kernelId] || engineName,
|
|
type: config.type,
|
|
port: config.port,
|
|
category: CATEGORY_MAP[kernelId] || "data",
|
|
description: `Serviço ${engineName}`,
|
|
status: "offline",
|
|
error: err.name === "AbortError" ? "timeout" : err.message,
|
|
details: {
|
|
url: serviceUrl,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
// Helper para fazer requests ao Kernel
|
|
async function kernelRequest(endpoint: string, method = 'GET', body?: any): Promise<any> {
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
|
|
const options: RequestInit = {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
signal: controller.signal,
|
|
};
|
|
|
|
if (body) {
|
|
options.body = JSON.stringify(body);
|
|
}
|
|
|
|
const response = await fetch(`${KERNEL_BASE_URL}${endpoint}`, options);
|
|
clearTimeout(timeout);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Kernel responded with ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error: any) {
|
|
if (error.name === 'AbortError') {
|
|
throw new Error('Kernel request timeout');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Converter status do Kernel para formato do Engine Room
|
|
function mapStatus(kernelStatus: string, health?: string): "online" | "offline" | "error" {
|
|
if (kernelStatus === 'running') {
|
|
return health === 'unhealthy' ? 'error' : 'online';
|
|
}
|
|
if (kernelStatus === 'error') return 'error';
|
|
return 'offline';
|
|
}
|
|
|
|
// Obter todos os serviços formatados para Engine Room
|
|
export async function getKernelEngines(): Promise<EngineStatus[]> {
|
|
// MODO DOCKER: Consulta diretamente os serviços externos
|
|
if (IS_DOCKER_MODE) {
|
|
console.log('[KernelAdapter] Modo Docker detectado - consultando serviços externos diretamente');
|
|
|
|
const engineNames = Object.keys(ENGINE_CONFIG);
|
|
const results = await Promise.allSettled(
|
|
engineNames.map(name => checkServiceHealth(name))
|
|
);
|
|
|
|
return results.map((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
return result.value;
|
|
} else {
|
|
const name = engineNames[index];
|
|
const kernelId = ENGINE_TO_KERNEL_ID[name];
|
|
return {
|
|
name,
|
|
displayName: DISPLAY_NAME_MAP[kernelId] || name,
|
|
type: ENGINE_CONFIG[name]?.type || "unknown",
|
|
port: ENGINE_CONFIG[name]?.port || 0,
|
|
category: CATEGORY_MAP[kernelId] || "data",
|
|
description: `Serviço ${name}`,
|
|
status: "offline" as const,
|
|
error: result.reason?.message || "Erro desconhecido",
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
// MODO NORMAL: Conecta ao Kernel local
|
|
try {
|
|
const response = await kernelRequest('/services');
|
|
const services: KernelServiceState[] = response.data || [];
|
|
|
|
return services.map((service): EngineStatus => {
|
|
const engineName = KERNEL_TO_ENGINE_NAME[service.config.id] || service.config.id;
|
|
|
|
return {
|
|
name: engineName,
|
|
displayName: DISPLAY_NAME_MAP[service.config.id] || service.config.name,
|
|
type: service.config.type === 'python' ? 'python' :
|
|
service.config.type === 'java' ? 'java' : 'node',
|
|
port: service.config.port,
|
|
category: CATEGORY_MAP[service.config.id] || 'data',
|
|
description: service.config.name,
|
|
status: mapStatus(service.status, service.health),
|
|
details: {
|
|
pid: service.pid,
|
|
restartCount: service.restartCount,
|
|
startTime: service.startTime,
|
|
health: service.health,
|
|
},
|
|
};
|
|
});
|
|
} catch (error) {
|
|
console.error('[KernelAdapter] Error fetching engines:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Obter info de um serviço específico (formato compatível com antigo)
|
|
export async function getKernelServiceInfo(engineName: string): Promise<any> {
|
|
// MODO DOCKER: Retorna info baseada no health check
|
|
if (IS_DOCKER_MODE) {
|
|
const health = await checkServiceHealth(engineName);
|
|
return {
|
|
name: engineName,
|
|
port: health.port,
|
|
status: health.status === 'online' ? 'running' : 'stopped',
|
|
startedAt: null,
|
|
restartCount: 0,
|
|
uptime: 0,
|
|
logLines: 0,
|
|
health: health.status === 'online' ? 'healthy' : 'unhealthy',
|
|
mode: 'docker-external',
|
|
};
|
|
}
|
|
|
|
// MODO NORMAL: Conecta ao Kernel
|
|
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
|
if (!kernelId) return null;
|
|
|
|
try {
|
|
const response = await kernelRequest(`/services/${kernelId}`);
|
|
const service: KernelServiceState = response.data;
|
|
|
|
if (!service) return null;
|
|
|
|
return {
|
|
name: engineName,
|
|
port: service.config.port,
|
|
status: service.status,
|
|
startedAt: service.startTime || null,
|
|
restartCount: service.restartCount,
|
|
uptime: service.startTime
|
|
? Math.round((Date.now() - new Date(service.startTime).getTime()) / 1000)
|
|
: 0,
|
|
logLines: 0,
|
|
health: service.health,
|
|
};
|
|
} catch (error) {
|
|
console.error(`[KernelAdapter] Error getting info for ${engineName}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Obter logs de um serviço (formato compatível)
|
|
export async function getKernelServiceLogs(engineName: string, lines = 50): Promise<string[]> {
|
|
// MODO DOCKER: Logs não disponíveis via HTTP
|
|
if (IS_DOCKER_MODE) {
|
|
return [`[Docker Mode] Logs não disponíveis para ${engineName}. Use 'docker logs' no container.`];
|
|
}
|
|
|
|
// MODO NORMAL: Conecta ao Kernel
|
|
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
|
if (!kernelId) return [];
|
|
|
|
try {
|
|
const response = await kernelRequest(`/services/${kernelId}/logs?lines=${lines}`);
|
|
const logs = response.data || [];
|
|
|
|
// Converter formato do Kernel (objetos) para formato antigo (strings)
|
|
return logs.map((log: any) => {
|
|
const time = new Date(log.timestamp).toISOString().split('T')[1].slice(0, 8);
|
|
return `[${time}] [${log.level.toUpperCase()}] ${log.message}`;
|
|
});
|
|
} catch (error) {
|
|
console.error(`[KernelAdapter] Error getting logs for ${engineName}:`, error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Iniciar serviço
|
|
export async function startKernelService(engineName: string): Promise<boolean> {
|
|
// MODO DOCKER: Não pode iniciar container externo
|
|
if (IS_DOCKER_MODE) {
|
|
console.log(`[KernelAdapter] Modo Docker: não é possível iniciar ${engineName} (container externo)`);
|
|
return false;
|
|
}
|
|
|
|
// MODO NORMAL: Conecta ao Kernel
|
|
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
|
if (!kernelId) return false;
|
|
|
|
try {
|
|
await kernelRequest(`/services/${kernelId}/start`, 'POST');
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`[KernelAdapter] Error starting ${engineName}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Parar serviço
|
|
export async function stopKernelService(engineName: string): Promise<boolean> {
|
|
// MODO DOCKER: Não pode parar container externo
|
|
if (IS_DOCKER_MODE) {
|
|
console.log(`[KernelAdapter] Modo Docker: não é possível parar ${engineName} (container externo)`);
|
|
return false;
|
|
}
|
|
|
|
// MODO NORMAL: Conecta ao Kernel
|
|
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
|
if (!kernelId) return false;
|
|
|
|
try {
|
|
await kernelRequest(`/services/${kernelId}/stop`, 'POST');
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`[KernelAdapter] Error stopping ${engineName}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Reiniciar serviço
|
|
export async function restartKernelService(engineName: string): Promise<boolean> {
|
|
// MODO DOCKER: Não pode reiniciar container externo
|
|
if (IS_DOCKER_MODE) {
|
|
console.log(`[KernelAdapter] Modo Docker: não é possível reiniciar ${engineName} (container externo)`);
|
|
return false;
|
|
}
|
|
|
|
// MODO NORMAL: Conecta ao Kernel
|
|
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
|
if (!kernelId) return false;
|
|
|
|
try {
|
|
await kernelRequest(`/services/${kernelId}/restart`, 'POST');
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`[KernelAdapter] Error restarting ${engineName}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Verificar health geral do Kernel
|
|
export async function getKernelHealth(): Promise<any> {
|
|
if (IS_DOCKER_MODE) {
|
|
return {
|
|
mode: 'docker',
|
|
message: 'Modo Docker - serviços gerenciados externamente',
|
|
services: Object.keys(ENGINE_CONFIG),
|
|
};
|
|
}
|
|
|
|
try {
|
|
const response = await kernelRequest('/health');
|
|
return response.data;
|
|
} catch (error) {
|
|
console.error('[KernelAdapter] Error getting health:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Verificar se Kernel está disponível (ou modo Docker está ativo)
|
|
export async function isKernelAvailable(): Promise<boolean> {
|
|
// MODO DOCKER: Sempre "disponível" (serviços externos)
|
|
if (IS_DOCKER_MODE) {
|
|
return true;
|
|
}
|
|
|
|
// MODO NORMAL: Verifica se Kernel responde
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
|
|
const response = await fetch('http://localhost:5001/health', {
|
|
signal: controller.signal,
|
|
});
|
|
|
|
clearTimeout(timeout);
|
|
return response.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
console.log(`[KernelAdapter] Adapter loaded - Modo: ${IS_DOCKER_MODE ? 'DOCKER (serviços externos)' : 'Kernel local (porta 5001)'}`);
|
|
// FORCE REBUILD: sex 03 abr 2026 16:13:36 -03
|