primocode 8.38.1 → 8.40.0

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/lib/referencia.js CHANGED
@@ -56,7 +56,7 @@ function hostBloqueado(hostname) {
56
56
  return false; // hostname comum (DNS) — não dá pra saber o IP sem resolver
57
57
  }
58
58
 
59
- const TIMEOUT_MS = 12000;
59
+ const TIMEOUT_MS = 8000; // por página; os buscadores têm o PRAZO_BUSCADOR, menor
60
60
  const MAX_POR_FONTE = 4;
61
61
  const MAX_TEXTO = 4500;
62
62
  // Quanto HTML baixar antes de parar. Só os primeiros MAX_TEXTO caracteres de
@@ -95,7 +95,12 @@ function viaProxy(alvo, { timeout }) {
95
95
  });
96
96
  }
97
97
 
98
- function pegar(url, { timeout = TIMEOUT_MS, saltos = 0 } = {}) {
98
+ // Bytes, não texto: a logo em PNG, a música em MP3. Mesmo caminho (proxy,
99
+ // redirect, prazo duro, SSRF), corpo em Buffer e teto maior — um mp3 de três
100
+ // minutos tem 5 MB, e 400 KB o cortariam no primeiro refrão.
101
+ const LIMITE_BYTES = 40 * 1024 * 1024;
102
+
103
+ function pegar(url, { timeout = TIMEOUT_MS, saltos = 0, binario = false } = {}) {
99
104
  return new Promise((resolve) => {
100
105
  let req;
101
106
  const alvo = (() => { try { return new URL(url); } catch { return null; } })();
@@ -128,6 +133,22 @@ function pegar(url, { timeout = TIMEOUT_MS, saltos = 0 } = {}) {
128
133
  })).catch(() => null)
129
134
  : Promise.resolve(opcoes);
130
135
 
136
+ // PRAZO DURO, do começo ao fim. O `timeout` do http é de OCIOSIDADE:
137
+ // um servidor que pinga um byte a cada segundo, ou um túnel que abre e
138
+ // trava no aperto de mão TLS, passava dele sem nunca disparar — medido:
139
+ // api.duckduckgo.com levava 11 s para devolver nada com timeout de 6 s.
140
+ // Aqui o relógio conta o tempo total; estourou, entrega o que veio.
141
+ let corpoParcial = '';
142
+ let encerrado = false;
143
+ const resolver = resolve;
144
+ const relogio = setTimeout(() => {
145
+ if (encerrado) return;
146
+ encerrado = true;
147
+ try { if (req) req.destroy(); } catch { /* já morto */ }
148
+ resolver(corpoParcial.length > 2000 ? corpoParcial : null);
149
+ }, timeout + 500);
150
+ resolve = (v) => { clearTimeout(relogio); if (!encerrado) { encerrado = true; resolver(v); } };
151
+
131
152
  abrir.then((cfg) => {
132
153
  if (!cfg) return resolve(null);
133
154
  try {
@@ -136,15 +157,29 @@ function pegar(url, { timeout = TIMEOUT_MS, saltos = 0 } = {}) {
136
157
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
137
158
  res.resume();
138
159
  const destino = new URL(res.headers.location, url).href;
139
- return pegar(destino, { timeout, saltos: saltos + 1 }).then(resolve);
160
+ return pegar(destino, { timeout, saltos: saltos + 1, binario }).then(resolve);
140
161
  }
141
162
  if (res.statusCode !== 200) { res.resume(); return resolve(null); }
163
+ if (binario) {
164
+ // Bytes inteiros ou nada: meia logo e meio mp3 não servem.
165
+ const pedacos = [];
166
+ let total = 0;
167
+ res.on('data', (c) => {
168
+ total += c.length;
169
+ if (total > LIMITE_BYTES) { res.destroy(); return resolve(null); }
170
+ pedacos.push(c);
171
+ });
172
+ res.on('end', () => resolve(Buffer.concat(pedacos)));
173
+ res.on('error', () => resolve(null));
174
+ return;
175
+ }
142
176
  let corpo = '';
143
177
  let pronto = false;
144
178
  const entregar = (valor) => { if (!pronto) { pronto = true; resolve(valor); } };
145
179
  res.setEncoding('utf8');
146
180
  res.on('data', (c) => {
147
181
  corpo += c;
182
+ corpoParcial = corpo;
148
183
  // Página grande: PARA de baixar e entrega o que já veio.
149
184
  //
150
185
  // Antes isto era `req.destroy()` sem entregar nada, e o
@@ -338,7 +373,7 @@ async function chutesVivos(chutes, limite = 2) {
338
373
  // centenas de KB. Com 6 s o site CERTO chegava atrasado, era descartado
339
374
  // como morto, e a pesquisa falhava com o site no ar — medido.
340
375
  const respostas = await Promise.all(chutes.slice(0, 12).map(async (c) => {
341
- const html = await pegar(c.url, { timeout: 12000 });
376
+ const html = await pegar(c.url, { timeout: 8000 });
342
377
  return html && html.length > 500 ? { cand: c, html } : null;
343
378
  }));
344
379
  const vivos = [];
@@ -373,9 +408,15 @@ function limparLinks(pares, limite) {
373
408
  return bons;
374
409
  }
375
410
 
411
+ // Prazo de UM buscador. Eles rodam em paralelo (ver `buscar`), então este é
412
+ // o teto da busca inteira — não a soma. Medido: o DuckDuckGo estava levando
413
+ // 11 s para devolver NADA, e a pesquisa esperava por ele antes de tentar o
414
+ // Bing, que respondia em meio segundo.
415
+ const PRAZO_BUSCADOR = 6000;
416
+
376
417
  /** DuckDuckGo (HTML). O destino vem embrulhado em /l/?uddg=<url>. */
377
418
  async function buscarDDG(consulta, limite) {
378
- const html = await pegar(`https://duckduckgo.com/html/?q=${encodeURIComponent(consulta)}`);
419
+ const html = await pegar(`https://duckduckgo.com/html/?q=${encodeURIComponent(consulta)}`, { timeout: PRAZO_BUSCADOR });
379
420
  if (!html) return [];
380
421
  const pares = [];
381
422
  const re = /<a[^>]+class="[^"]*result__a[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
@@ -442,29 +483,73 @@ async function buscarRespostaDDG(consulta, limite) {
442
483
  return limparLinks(achados, limite);
443
484
  }
444
485
 
445
- /** Bing (HTML) — a reserva quando o DDG não responde. */
486
+ /**
487
+ * O destino de um link do Bing.
488
+ *
489
+ * O Bing embrulha TODO resultado em `bing.com/ck/a?…&u=a1<base64url>`: o
490
+ * `a1` é prefixo, o resto é a URL em base64 (alfabeto de URL, sem "="). O
491
+ * parser antigo lia o `<cite>` — que mostra a URL com " › " no lugar das
492
+ * barras e reticências no meio — e por isso casava zero resultados numa
493
+ * página com dez. Medido no IP deste servidor: 117 KB de HTML, dez
494
+ * resultados, nenhum aproveitado. O buscador respondia; nós é que não líamos.
495
+ */
496
+ function destinoBing(href) {
497
+ const h = String(href || '').replace(/&amp;/g, '&');
498
+ const m = /[?&]u=a1([A-Za-z0-9_-]+)/.exec(h);
499
+ if (!m) return /^https?:\/\//i.test(h) && !/bing\.com\//i.test(h) ? h : null;
500
+ try {
501
+ const b64 = m[1].replace(/-/g, '+').replace(/_/g, '/');
502
+ const url = Buffer.from(b64 + '='.repeat((4 - (b64.length % 4)) % 4), 'base64').toString('utf8');
503
+ return /^https?:\/\//i.test(url) ? url : null; // "/images/search?…" é do próprio Bing
504
+ } catch { return null; }
505
+ }
506
+
507
+ /** Bing (HTML). Responde rápido e sem bloquear onde o DDG cala. */
446
508
  async function buscarBing(consulta, limite) {
447
- const html = await pegar(`https://www.bing.com/search?q=${encodeURIComponent(consulta)}&setlang=en`);
509
+ const html = await pegar(`https://www.bing.com/search?q=${encodeURIComponent(consulta)}&setlang=en`, { timeout: PRAZO_BUSCADOR });
448
510
  if (!html) return [];
449
511
  const pares = [];
450
- // O Bing largou o <h2><a href> e hoje mostra a URL num <cite>, com " › "
451
- // no lugar da barra. O parser antigo casava ZERO resultados numa página
452
- // que trazia dez — e a pesquisa caía na Wikipédia, que era o lixo.
453
- const cites = html.match(/<cite[^>]*>[\s\S]{0,200}?<\/cite>/gi) || [];
454
- for (const bruto of cites) {
455
- const mostrado = textoDe(bruto);
456
- if (!/^https?:\/\//i.test(mostrado) || /[…]/.test(mostrado)) continue;
457
- pares.push({ titulo: mostrado.replace(/\s*›\s*/g, ' / '), url: mostrado.replace(/\s*›\s*/g, '/') });
512
+ const re = /<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
513
+ let m;
514
+ while ((m = re.exec(html))) {
515
+ const url = destinoBing(m[1]);
516
+ if (url) pares.push({ titulo: textoDe(m[2]), url });
458
517
  }
459
- const re = /<h2>\s*<a[^>]+href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
518
+ return limparLinks(pares, limite);
519
+ }
520
+
521
+ /**
522
+ * Yahoo (HTML). Também embrulha: `r.search.yahoo.com/…/RU=<url codificada>/RK=…`.
523
+ * É a segunda porta que responde onde o DDG não responde — e os índices dos
524
+ * dois (Bing e Yahoo) são parecidos, então os dois juntos confirmam um ao
525
+ * outro mais do que ampliam.
526
+ */
527
+ function destinoYahoo(href) {
528
+ const h = String(href || '').replace(/&amp;/g, '&');
529
+ const m = /\/RU=([^/]+)/.exec(h);
530
+ if (!m) return /^https?:\/\//i.test(h) && !/yahoo\.com\//i.test(h) ? h : null;
531
+ try {
532
+ const url = decodeURIComponent(m[1]);
533
+ return /^https?:\/\//i.test(url) && !/yahoo\.com\//i.test(url) ? url : null;
534
+ } catch { return null; }
535
+ }
536
+
537
+ async function buscarYahoo(consulta, limite) {
538
+ const html = await pegar(`https://search.yahoo.com/search?p=${encodeURIComponent(consulta)}`, { timeout: PRAZO_BUSCADOR });
539
+ if (!html) return [];
540
+ const pares = [];
541
+ const re = /<h3[^>]*class="[^"]*\btitle\b[^"]*"[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
460
542
  let m;
461
- while ((m = re.exec(html))) pares.push({ titulo: textoDe(m[2]), url: m[1] });
543
+ while ((m = re.exec(html))) {
544
+ const url = destinoYahoo(m[1]);
545
+ if (url) pares.push({ titulo: textoDe(m[2]), url });
546
+ }
462
547
  return limparLinks(pares, limite);
463
548
  }
464
549
 
465
550
  /** SearXNG público, em JSON — a terceira porta. */
466
551
  async function buscarSearx(consulta, limite) {
467
- const corpo = await pegar(`https://searx.be/search?q=${encodeURIComponent(consulta)}&format=json`);
552
+ const corpo = await pegar(`https://searx.be/search?q=${encodeURIComponent(consulta)}&format=json`, { timeout: PRAZO_BUSCADOR });
468
553
  if (!corpo) return [];
469
554
  try {
470
555
  const j = JSON.parse(corpo);
@@ -473,29 +558,33 @@ async function buscarSearx(consulta, limite) {
473
558
  }
474
559
 
475
560
  /**
476
- * Busca na web. Tenta as portas em ordem e para na primeira que responder —
477
- * um buscador fora do ar (ou bloqueado pela rede) não pode desligar a
478
- * pesquisa inteira. Sem chave de API em nenhuma delas.
561
+ * Busca na web — TODAS as portas de uma vez, e fica com o que voltar.
562
+ *
563
+ * Em fila, o buscador que não responde segura os outros: o DDG gastava 11 s
564
+ * para calar, e só então o Bing era tentado. Em paralelo, a pesquisa leva o
565
+ * tempo do MAIS RÁPIDO que respondeu com algo (e no máximo PRAZO_BUSCADOR).
566
+ * A ordem da lista é a ordem de confiança na hora de juntar: o que vem
567
+ * primeiro entra primeiro. Sem chave de API em nenhuma delas.
479
568
  */
480
569
  async function buscar(consulta, limite = MAX_POR_FONTE) {
481
- // Ordem deliberada: primeiro os buscadores raspados (dão os links mais
482
- // frescos, quando respondem), depois as APIs que NUNCA bloqueiam. Antes a
483
- // fila acabava no Searx e, com os três calados, a pesquisa inteira voltava
484
- // vazia — mesmo havendo resposta boa na Wikipédia.
485
- const portas = [buscarDDG, buscarBing, buscarSearx, buscarRespostaDDG, buscarWikipedia];
570
+ const portas = [buscarBing, buscarYahoo, buscarDDG, buscarSearx, buscarRespostaDDG, buscarWikipedia];
571
+ const respostas = await Promise.all(portas.map((p) => p(consulta, limite).catch(() => [])));
486
572
  const juntos = [];
487
- for (const porta of portas) {
488
- try {
489
- const achados = await porta(consulta, limite);
490
- for (const a of achados) {
491
- if (!juntos.some((j) => j.url === a.url)) juntos.push(a);
492
- }
493
- // Os raspados, quando respondem, já bastam. As APIs entram para
494
- // completar quando eles vieram vazios.
495
- if (juntos.length >= limite) break;
496
- } catch {}
573
+ const dominios = new Map();
574
+ for (const achados of respostas) {
575
+ for (const a of achados) {
576
+ if (juntos.some((j) => j.url === a.url)) continue;
577
+ // No máximo dois links do mesmo domínio: uma página de resultados
578
+ // dominada por um site só (a Wikipédia, o próprio produto) não é
579
+ // variedade de fonte.
580
+ const dom = (a.url.match(/^https?:\/\/(?:www\.)?([^/]+)/i) || [])[1] || a.url;
581
+ const n = dominios.get(dom) || 0;
582
+ if (n >= 2) continue;
583
+ dominios.set(dom, n + 1);
584
+ juntos.push(a);
585
+ }
497
586
  }
498
- return juntos.slice(0, limite);
587
+ return juntos.slice(0, limite * 2);
499
588
  }
500
589
 
501
590
  /** Respostas do StackOverflow — a API pública, sem chave. */
@@ -515,24 +604,61 @@ async function stackoverflow(consulta, limite = 3) {
515
604
  } catch { return []; }
516
605
  }
517
606
 
518
- // Última porta: a documentação oficial dos assuntos mais comuns, direto. Não
519
- // depende de buscador nenhum — é o que salva a pesquisa em rede corporativa
520
- // que bloqueia mecanismo de busca mas libera site de documentação.
607
+ /**
608
+ * A documentação oficial, direto — não depende de buscador nenhum, e é o que
609
+ * salva a pesquisa em rede corporativa que bloqueia buscador e libera site de
610
+ * documentação.
611
+ *
612
+ * A MDN entra pela API dela, não pela página de busca: a página é uma casca
613
+ * que carrega os resultados por JavaScript, e o que chegava aqui eram 226
614
+ * caracteres de menu. A pesquisa dizia "1 fonte lida" e o modelo recebia
615
+ * nada — foi assim que "liquid glass css" voltou de mãos vazias com a
616
+ * resposta certa (backdrop-filter) a uma chamada de distância. A API devolve
617
+ * os artigos com resumo, e o artigo é a melhor fonte que existe sobre CSS.
618
+ * O mesmo vale para o python.org: a busca lá é JavaScript, e o índice dos
619
+ * módulos é o que se lê.
620
+ */
621
+ // Palavras que toda consulta técnica carrega e que não apontam artigo nenhum.
622
+ // "effect" casava vector-effect para "liquid glass css effect" — e o modelo
623
+ // recebia a doc de SVG como referência de vidro.
624
+ const TECNICAS_GENERICAS = new Set(['css', 'html', 'javascript', 'js', 'effect', 'efeito', 'effects', 'como', 'fazer',
625
+ 'how', 'make', 'create', 'criar', 'tutorial', 'example', 'exemplo', 'code', 'codigo', 'with', 'using', 'web',
626
+ 'style', 'estilo', 'property', 'propriedade', 'usar', 'use', 'best', 'melhor', 'simple', 'simples']);
627
+
628
+ async function buscarMDN(consulta, limite = 3) {
629
+ const palavras = palavrasDaConsulta(consulta).filter((p) => !TECNICAS_GENERICAS.has(p));
630
+ if (!palavras.length) return [];
631
+ const bruto = await pegar(`https://developer.mozilla.org/api/v1/search?q=${encodeURIComponent(palavras.join(' '))}&locale=en-US`, { timeout: PRAZO_BUSCADOR });
632
+ if (!bruto) return [];
633
+ try {
634
+ const j = JSON.parse(bruto);
635
+ return (j.documents || [])
636
+ // O índice da MDN casa por palavra, e "effect" casa vector-effect.
637
+ // Só entra o artigo cujo título ou caminho traz uma palavra que
638
+ // é DO ASSUNTO — as genéricas já saíram acima.
639
+ .filter((d) => {
640
+ const alvo = `${d.title} ${d.slug}`.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
641
+ return palavras.some((p) => alvo.includes(p));
642
+ })
643
+ .slice(0, limite).map((d) => ({
644
+ titulo: d.title,
645
+ url: 'https://developer.mozilla.org' + d.mdn_url,
646
+ nota: 'MDN' + (d.summary ? ' — ' + String(d.summary).slice(0, 160) : ''),
647
+ }));
648
+ } catch { return []; }
649
+ }
650
+
521
651
  const DOCS = [
522
- [/\b(js|javascript|array|string|promise|fetch|dom|css|html|regex)\b/i,
523
- (q) => `https://developer.mozilla.org/en-US/search?q=${encodeURIComponent(q)}`],
524
- [/\b(react|jsx|hook|usestate|useeffect)\b/i, () => 'https://react.dev/reference/react'],
525
- [/\b(node|npm|express|fs|require)\b/i, () => 'https://nodejs.org/docs/latest/api/'],
526
- [/\b(python|django|flask|pandas)\b/i,
527
- (q) => `https://docs.python.org/3/search.html?q=${encodeURIComponent(q)}`],
652
+ [/\b(js|javascript|array|string|promise|fetch|dom|css|html|regex|svg|canvas|webgl|animation|keyframes|backdrop|filter|grid|flex)\b/i, buscarMDN],
653
+ [/\b(react|jsx|hook|usestate|useeffect)\b/i, async () => [{ titulo: 'React — referência', url: 'https://react.dev/reference/react', nota: 'documentação oficial' }]],
654
+ [/\b(node|npm|express|fs|require)\b/i, async () => [{ titulo: 'Node.js — API', url: 'https://nodejs.org/docs/latest/api/', nota: 'documentação oficial' }]],
655
+ [/\b(python|django|flask|pandas)\b/i, async () => [{ titulo: 'Python — índice dos módulos', url: 'https://docs.python.org/3/py-modindex.html', nota: 'documentação oficial' }]],
528
656
  ];
529
657
 
530
- function docsConhecidas(consulta) {
531
- const achados = [];
532
- for (const [re, url] of DOCS) {
533
- if (re.test(consulta)) achados.push({ titulo: 'documentação oficial', url: url(consulta) });
534
- }
535
- return achados;
658
+ async function docsConhecidas(consulta) {
659
+ const casam = DOCS.filter(([re]) => re.test(consulta));
660
+ const listas = await Promise.all(casam.map(([, f]) => f(consulta).catch(() => [])));
661
+ return listas.flat();
536
662
  }
537
663
 
538
664
  /**
@@ -543,9 +669,15 @@ async function pesquisar({ consulta, paginas = 3 }) {
543
669
  const termo = String(consulta || '').trim();
544
670
  if (!termo) return { ok: false, error: 'diga o que pesquisar' };
545
671
 
546
- const [web, so] = await Promise.all([
672
+ // Tudo de uma vez: os buscadores, o StackOverflow, a documentação e a
673
+ // sonda dos sites prováveis da empresa. A pesquisa leva o tempo do mais
674
+ // lento que RESPONDEU, não a soma de todos.
675
+ const palpites = sitesProvaveis(termo);
676
+ const [web, so, docs, sondados] = await Promise.all([
547
677
  buscar(termo, MAX_POR_FONTE),
548
678
  stackoverflow(termo, 2),
679
+ docsConhecidas(termo),
680
+ chutesVivos(palpites),
549
681
  ]);
550
682
 
551
683
  // A documentação oficial entra SEMPRE na fila, não só quando as outras
@@ -559,20 +691,28 @@ async function pesquisar({ consulta, paginas = 3 }) {
559
691
  // respondem ocupam vaga. São dez palpites (5 TLDs x com/sem www) e a
560
692
  // maioria não existe — enfileirar todos expulsaria os resultados reais
561
693
  // do buscador. A sonda roda em paralelo e o HTML já volta aproveitado.
562
- const palpites = sitesProvaveis(termo);
563
- const { vivos, html: htmlChutes } = await chutesVivos(palpites);
694
+ const { vivos, html: htmlChutes } = sondados;
564
695
  // A sonda ORDENA, não faz porteiro. Ela é uma corrida contra o relógio
565
696
  // contra home pesada e rede lenta, e perdê-la não pode significar perder
566
697
  // o site certo: quando nada responde a tempo, os primeiros palpites vão
567
698
  // para a fila mesmo assim e o laço de leitura tenta de novo, sem pressa.
568
699
  const chutes = vivos.length ? vivos : palpites.slice(0, 3);
569
700
  const palavras = palavrasDaConsulta(termo);
570
- const brutos = [...chutes, ...web, ...so, ...docsConhecidas(termo)];
701
+ // A documentação oficial vai NA FRENTE dos links de buscador: quando a
702
+ // consulta é técnica, o artigo da MDN vale mais que o blog que o Bing
703
+ // pôs em primeiro.
704
+ const brutos = [...chutes, ...docs, ...web, ...so];
571
705
 
572
706
  // Fora o que não é sobre o assunto. Sem isto o modelo recebia "Milan" e
573
707
  // "History of autism" para uma pesquisa sobre uma empresa, e escrevia o
574
708
  // vídeo com aquilo.
575
- const filtrados = brutos.filter((cand) => cand.nota === 'site provável da empresa' || relevante(cand, palavras));
709
+ // A documentação oficial passa pelo nome: "backdrop-filter CSS property"
710
+ // não repete as palavras de "liquid glass css effect", e mesmo assim é a
711
+ // resposta. Quem a escolheu foi o índice da MDN, que entende sinônimo —
712
+ // o filtro de palavras é para link de buscador, que devolve página
713
+ // aleatória quando não conhece o assunto.
714
+ const filtrados = brutos.filter((cand) => cand.nota === 'site provável da empresa'
715
+ || /^(MDN|documentação oficial)/.test(String(cand.nota || '')) || relevante(cand, palavras));
576
716
  const candidatos = filtrados.slice(0, Math.max(2, Math.min(10, paginas + 6)));
577
717
  const descartados = brutos.length - filtrados.length;
578
718
  if (!candidatos.length) {
@@ -602,14 +742,20 @@ async function pesquisar({ consulta, paginas = 3 }) {
602
742
  // Os chutes de domínio ficam de lado até o fim: só então dá para saber
603
743
  // qual deles fala mais do assunto.
604
744
  const chutesLidos = [];
605
- for (const cand of candidatos) {
745
+ // As páginas abrem JUNTAS, não uma depois da outra: dez candidatos a 2 s
746
+ // cada eram 20 s de espera antes de ler a primeira linha. Quem não abriu
747
+ // vira null e é pulado na ordem — a ordem de confiança continua valendo.
748
+ const paginasAbertas = await Promise.all(candidatos.map((cand) =>
749
+ htmlChutes.has(cand.url) ? Promise.resolve(htmlChutes.get(cand.url)) : pegar(cand.url)));
750
+ for (let ic = 0; ic < candidatos.length; ic++) {
751
+ const cand = candidatos[ic];
606
752
  if (lidas.length >= paginas) break;
607
- // A sonda já baixou o HTML dos chutes: reaproveita em vez de pedir
608
- // a mesma página duas vezes.
609
- const html = htmlChutes.get(cand.url) || await pegar(cand.url);
753
+ const html = paginasAbertas[ic];
610
754
  if (!html) continue;
611
755
  const texto = textoDe(html);
612
- if (texto.length < 200) continue;
756
+ // 400, não 200: uma casca de página que carrega tudo por JavaScript
757
+ // deixa 200-300 caracteres de menu, e isso passava como "fonte lida".
758
+ if (texto.length < 400) continue;
613
759
  if (pareceCodigo(texto)) continue;
614
760
  // O chute de domínio precisa PROVAR que acertou. "Paris Group" tem
615
761
  // parisgroup.com (um grupo de varejo no exterior) e parisgroup.ai (a
@@ -675,4 +821,5 @@ async function pesquisar({ consulta, paginas = 3 }) {
675
821
  }
676
822
 
677
823
  module.exports = { pesquisar, buscar, stackoverflow, textoDe, buscarWikipedia, buscarRespostaDDG, pegar,
678
- relevante, quantoBate, palavrasDaConsulta, sitesProvaveis, pareceCodigo };
824
+ relevante, quantoBate, palavrasDaConsulta, sitesProvaveis, chutesVivos, pareceCodigo,
825
+ destinoBing, destinoYahoo, buscarBing, buscarYahoo, buscarMDN, docsConhecidas, PRAZO_BUSCADOR };