@danhachuel/leadmaster 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/backend/__pycache__/main.cpython-311.pyc +0 -0
- package/backend/main.py +47 -0
- package/backend/services/__pycache__/__init__.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/agents.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/crm_agent.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/extractors.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/flags.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/llm.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/storage.cpython-311.pyc +0 -0
- package/backend/services/extractors.py +68 -1
- package/backend/tests/__pycache__/conftest.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_cnpj_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_crm_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_instagram_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_launcher_protections.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_maps_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_process_flags.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/test_cnpj_extractor.py +64 -0
- package/backend/tests/test_launcher_protections.py +16 -3
- package/frontend/dist/assets/{index-tRUSD15G.css → index-Bf28IekR.css} +1 -1
- package/frontend/dist/assets/{index-CPrWAFdw.js → index-CAvEEMWg.js} +18 -18
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/cli.mjs +139 -24
package/frontend/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>LeadMaster — Painel</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CAvEEMWg.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bf28IekR.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-gray-100 min-h-screen">
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* LEADMASTER_PORT Porta inicial da UI/API (mesmo efeito de --port).
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
25
|
+
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
26
26
|
import { existsSync, mkdirSync, readFileSync, appendFileSync } from 'node:fs';
|
|
27
27
|
import { homedir, platform } from 'node:os';
|
|
28
28
|
import net from 'node:net';
|
|
@@ -267,23 +267,20 @@ function portaLivre(port) {
|
|
|
267
267
|
|
|
268
268
|
async function resolverPortaUI(portaInicial) {
|
|
269
269
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
270
|
+
* Devolve a primeira porta livre a partir de portaInicial (padrão 5055).
|
|
271
|
+
* Instâncias anteriores do LeadMaster já foram encerradas pelo kill switch
|
|
272
|
+
* (`encerrarInstanciaAnterior`) antes desta chamada; se uma porta seguir
|
|
273
|
+
* ocupada, é de outro projeto — o launcher avança (5056, 5057, ...).
|
|
274
|
+
* Se nenhuma porta livre no intervalo, devolve {porta: null}.
|
|
274
275
|
*/
|
|
275
276
|
for (let i = 0; i < UI_PORT_MAX_TENTATIVAS; i++) {
|
|
276
277
|
const porta = portaInicial + i;
|
|
277
|
-
const saude = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 800);
|
|
278
|
-
if (saude.ok && saude.body && (saude.body.versao || saude.body.storage)) {
|
|
279
|
-
return { jaRodando: true, porta };
|
|
280
|
-
}
|
|
281
278
|
if (await portaLivre(porta)) {
|
|
282
|
-
return {
|
|
279
|
+
return { porta };
|
|
283
280
|
}
|
|
284
281
|
console.log(`ℹ Porta ${porta} ocupada — tentando ${porta + 1}...`);
|
|
285
282
|
}
|
|
286
|
-
return {
|
|
283
|
+
return { porta: null };
|
|
287
284
|
}
|
|
288
285
|
|
|
289
286
|
async function procurarInstanciaAtiva(portaInicial, maxTentativas = 10) {
|
|
@@ -297,6 +294,114 @@ async function procurarInstanciaAtiva(portaInicial, maxTentativas = 10) {
|
|
|
297
294
|
return null;
|
|
298
295
|
}
|
|
299
296
|
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Kill switch (regra inviolável do AGENTS.md)
|
|
299
|
+
//
|
|
300
|
+
// O launcher NUNCA deve ignorar uma porta ocupada por outra instância do
|
|
301
|
+
// LeadMaster: avisar que já existe instância no ar é PROIBIDO — isso impede
|
|
302
|
+
// atualizações (a versão antiga continua rodando, travando arquivos e
|
|
303
|
+
// causando EBUSY no npx). Antes de subir a nova versão, encerramos qualquer
|
|
304
|
+
// instância anterior confirmada.
|
|
305
|
+
//
|
|
306
|
+
// Segurança (higiene de processos do AGENTS.md): só mata processos
|
|
307
|
+
// CONFIRMADOS como LeadMaster — /api/health respondendo com o payload próprio
|
|
308
|
+
// (versao/home/storage) e o PID dono da porta. Processos de outros projetos
|
|
309
|
+
// NUNCA são tocados: nesse caso o launcher apenas avança para a próxima porta
|
|
310
|
+
// livre, como sempre.
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
function saudeEhLeadmaster(body) {
|
|
314
|
+
return Boolean(body && (body.versao || body.storage));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function pidsOuvindoPortaWindows(porta) {
|
|
318
|
+
try {
|
|
319
|
+
const out = execSync('netstat -ano', { windowsHide: true, encoding: 'utf8', timeout: 8000 });
|
|
320
|
+
const pids = new Set();
|
|
321
|
+
for (const linha of out.split(/\r?\n/)) {
|
|
322
|
+
if (!linha.includes('LISTENING')) continue;
|
|
323
|
+
const colunas = linha.trim().split(/\s+/);
|
|
324
|
+
if (colunas.length < 5) continue;
|
|
325
|
+
if (!colunas[1].endsWith(`:${porta}`)) continue;
|
|
326
|
+
const pid = Number(colunas[colunas.length - 1]);
|
|
327
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
328
|
+
}
|
|
329
|
+
return [...pids];
|
|
330
|
+
} catch {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function pidsOuvindoPortaUnix(porta) {
|
|
336
|
+
try {
|
|
337
|
+
const out = execSync(`lsof -ti:${porta} -sTCP:LISTEN`, { encoding: 'utf8', timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
338
|
+
const pids = out.split(/\s+/).map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n > 0);
|
|
339
|
+
return [...new Set(pids)];
|
|
340
|
+
} catch {
|
|
341
|
+
// Sem lsof: fallback amplo — processos do LeadMaster na lista de processos.
|
|
342
|
+
try {
|
|
343
|
+
const out = execSync('ps -eo pid=,args=', { encoding: 'utf8', timeout: 8000 });
|
|
344
|
+
const pids = new Set();
|
|
345
|
+
for (const linha of out.split('\n')) {
|
|
346
|
+
const m = linha.trim().match(/^(\d+)\s+(.*)$/);
|
|
347
|
+
if (!m) continue;
|
|
348
|
+
const pid = Number(m[1]);
|
|
349
|
+
const args = m[2];
|
|
350
|
+
const ehLeadmaster = args.includes('leadmaster') && (args.includes('cli.mjs') || args.includes('uvicorn main:app'));
|
|
351
|
+
if (pid > 0 && pid !== process.pid && ehLeadmaster) pids.add(pid);
|
|
352
|
+
}
|
|
353
|
+
return [...pids];
|
|
354
|
+
} catch {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function matarArvoreProcesso(pid) {
|
|
361
|
+
try {
|
|
362
|
+
if (platform() === 'win32') {
|
|
363
|
+
const r = spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
364
|
+
return r.status === 0;
|
|
365
|
+
}
|
|
366
|
+
process.kill(pid, 'SIGKILL');
|
|
367
|
+
return true;
|
|
368
|
+
} catch {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Kill switch: encerra toda instância anterior do LeadMaster (varre as portas
|
|
375
|
+
* candidatas) antes de subir a nova versão. Portas ocupadas por outros
|
|
376
|
+
* projetos são deixadas em paz (o resolvedor avançará para a próxima porta).
|
|
377
|
+
*/
|
|
378
|
+
async function encerrarInstanciaAnterior(portaInicial) {
|
|
379
|
+
let encerrouAlguma = false;
|
|
380
|
+
for (let i = 0; i < UI_PORT_MAX_TENTATIVAS; i++) {
|
|
381
|
+
const porta = portaInicial + i;
|
|
382
|
+
const saude = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 600);
|
|
383
|
+
if (!saude.ok || !saudeEhLeadmaster(saude.body)) continue; // não é LeadMaster — respeitar
|
|
384
|
+
const pids = platform() === 'win32' ? pidsOuvindoPortaWindows(porta) : pidsOuvindoPortaUnix(porta);
|
|
385
|
+
if (pids.length === 0) continue;
|
|
386
|
+
console.log(`ℹ Instância anterior do LeadMaster detectada na porta ${porta} — encerrando (PID ${pids.join(', ')})...`);
|
|
387
|
+
for (const pid of pids) {
|
|
388
|
+
if (!matarArvoreProcesso(pid)) {
|
|
389
|
+
console.warn(`⚠ Não foi possível encerrar o PID ${pid} (permissão?). A nova instância usará outra porta.`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
encerrouAlguma = true;
|
|
393
|
+
// Aguarda o sistema operacional liberar a porta (até ~5s).
|
|
394
|
+
for (let t = 0; t < 10; t++) {
|
|
395
|
+
await sleep(500);
|
|
396
|
+
const ainda = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 600);
|
|
397
|
+
if (!ainda.ok) break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (encerrouAlguma) {
|
|
401
|
+
console.log('✓ Instância anterior encerrada — a versão nova assume o controle.');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
300
405
|
/**
|
|
301
406
|
* Guarda da consola (anti-EPIPE): se o terminal for fechado (MobaXterm, VS
|
|
302
407
|
* Code), escrever em stdout/stderr gera EPIPE — que o Node trataria como
|
|
@@ -331,10 +436,17 @@ function guardConsoleStream(stream, logFile) {
|
|
|
331
436
|
// Comandos
|
|
332
437
|
// ---------------------------------------------------------------------------
|
|
333
438
|
|
|
334
|
-
function cmdInstall(args) {
|
|
439
|
+
async function cmdInstall(args) {
|
|
335
440
|
const home = resolveHome(args);
|
|
441
|
+
const portaInicial = Number.isFinite(args.port) && args.port > 0 ? args.port : UI_PORT;
|
|
336
442
|
console.log(`LeadMaster v${VERSION} — instalação local`);
|
|
337
443
|
console.log('');
|
|
444
|
+
|
|
445
|
+
// Kill switch (regra inviolável do AGENTS.md): encerra instâncias anteriores
|
|
446
|
+
// ANTES de instalar — libera os arquivos travados no Windows (erro EBUSY do
|
|
447
|
+
// npx ao atualizar por cima de uma instância rodando).
|
|
448
|
+
await encerrarInstanciaAnterior(portaInicial);
|
|
449
|
+
|
|
338
450
|
ensureHome(home);
|
|
339
451
|
console.log(`✓ Pasta de dados pronta: ${home}`);
|
|
340
452
|
console.log(` (leads, campanhas e configuração ficam em "${path.join(home, 'storage')}")`);
|
|
@@ -377,23 +489,19 @@ function cmdStart(args) {
|
|
|
377
489
|
console.log(`✓ Pasta de dados: ${home}`);
|
|
378
490
|
|
|
379
491
|
(async () => {
|
|
380
|
-
//
|
|
381
|
-
|
|
492
|
+
// Kill switch (regra inviolável do AGENTS.md): encerra qualquer instância
|
|
493
|
+
// anterior do LeadMaster (Node + Python) antes de subir a versão nova —
|
|
494
|
+
// nunca deixar a versão antiga no ar nem exibir aviso de reuso.
|
|
495
|
+
await encerrarInstanciaAnterior(portaInicial);
|
|
496
|
+
|
|
497
|
+
// Porta: primeira livre a partir de 5055 (→ 5056 → 5057 → ...).
|
|
498
|
+
const { porta } = await resolverPortaUI(portaInicial);
|
|
382
499
|
if (porta === null) {
|
|
383
500
|
console.error(`✗ Nenhuma porta livre entre ${portaInicial} e ${portaInicial + UI_PORT_MAX_TENTATIVAS - 1}.`);
|
|
384
501
|
console.error(' Libere uma porta ou inicie com --port=<numero>.');
|
|
385
502
|
process.exit(1);
|
|
386
503
|
}
|
|
387
504
|
const url = `http://127.0.0.1:${porta}`;
|
|
388
|
-
if (jaRodando) {
|
|
389
|
-
console.log(`ℹ LeadMaster já está rodando em ${url} — abra o endereço no navegador.`);
|
|
390
|
-
// Saída INFORMATIVA (antes de qualquer inicialização própria): não há
|
|
391
|
-
// servidor filho para supervisionar nem terminal para prender — o
|
|
392
|
-
// launcher não deve chamar process.exit(0) no caminho de SUCESSO da
|
|
393
|
-
// inicialização (regra inviolável do AGENTS.md).
|
|
394
|
-
process.exit(0);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
505
|
if (porta !== portaInicial) {
|
|
398
506
|
console.log(`ℹ Porta ${portaInicial} ocupada — usando a porta ${porta}.`);
|
|
399
507
|
}
|
|
@@ -614,7 +722,14 @@ const CONSOLE_LOG = path.join(LOG_DIR, 'launcher_console.log');
|
|
|
614
722
|
[process.stdout, process.stderr].forEach((stream) => guardConsoleStream(stream, CONSOLE_LOG));
|
|
615
723
|
|
|
616
724
|
switch (command) {
|
|
617
|
-
case 'install':
|
|
725
|
+
case 'install':
|
|
726
|
+
// cmdInstall é async (kill switch): a promise mantém o evento vivo até o
|
|
727
|
+
// fim; rejeições são logadas pelos handlers globais e encerram com código 1.
|
|
728
|
+
cmdInstall(args).catch((err) => {
|
|
729
|
+
console.error('✗ Erro inesperado na instalação:', err && err.message ? err.message : err);
|
|
730
|
+
process.exit(1);
|
|
731
|
+
});
|
|
732
|
+
break;
|
|
618
733
|
case 'start': cmdStart(args); break;
|
|
619
734
|
case 'doctor': cmdDoctor(args); break;
|
|
620
735
|
case 'version': console.log(VERSION); break;
|