jarvis-ai-framework 2.0.0 → 2.1.1
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/AGENTS.md +12 -9
- package/agents/AGENTS.md +9 -1
- package/agents/README.md +2 -2
- package/agents/engineering/data/eng.data-engineer.agent.md +2 -2
- package/agents/engineering/eng.agent.md +4 -6
- package/agents/engineering/eng.bug-hunter.md +10 -5
- package/agents/engineering/eng.cybersecurity.agent.md +2 -4
- package/agents/engineering/eng.dev-code-reviewer.md +4 -0
- package/agents/engineering/eng.docs-writer.md +3 -3
- package/agents/engineering/eng.frontend.agent.md +17 -8
- package/agents/engineering/eng.rpa.agent.md +3 -5
- package/agents/engineering/eng.ux-designer.agent.md +4 -0
- package/agents/engineering/qa/eng.qa.cypress-specialist.md +8 -5
- package/agents/engineering/qa/eng.qa.quality-champion-task-agent.md +4 -0
- package/agents/engineering/qa/eng.qa.quality-strategist.md +18 -8
- package/agents/engineering/qa/eng.qa.test-architect.md +17 -6
- package/agents/engineering/qa/eng.qa.test-planner.md +17 -8
- package/agents/engineering/qa/eng.qa.testing-engineer.md +26 -9
- package/agents/product/prod.discovery-interviewer.md +22 -3
- package/agents/product/prod.pm-checker.md +4 -0
- package/bin/commands/map.js +43 -0
- package/bin/commands/tokens.js +93 -0
- package/bin/jarvis.js +10 -0
- package/bin/lib/config/constants.js +11 -0
- package/bin/lib/core/profile-filter.js +113 -0
- package/bin/lib/core/scanner.js +2 -2
- package/bin/lib/core/token-report.js +90 -0
- package/bin/lib/flow-map/index.js +65 -0
- package/bin/lib/flow-map/parse.js +217 -0
- package/bin/lib/flow-map/render.js +86 -0
- package/bin/lib/flow-map/tree.js +199 -0
- package/members.md +1 -1
- package/package.json +4 -2
- package/rules/AGENTS.md +18 -7
- package/rules/engineering/eng-rules.md +2 -12
- package/rules/engineering/eng-security-rules.md +1 -2
- package/rules/engineering/eng.integrations-rules.md +0 -4
- package/rules/engineering/eng.skills-rules.md +0 -4
- package/rules/engineering/eng.specializations-rules.md +0 -4
- package/rules/product/README.md +1 -0
- package/rules/product/prod-rules.md +1 -4
- package/rules/product/prod.spec-rules.md +0 -4
- package/rules/rtk-rules.md +0 -1
- package/{rules → rules-on-demand}/engineering/eng.breakdown-subtasks-rules.md +0 -4
- package/{rules → rules-on-demand}/engineering/eng.plan-rules.md +0 -4
- package/{rules → rules-on-demand}/engineering/eng.pr-rules.md +0 -4
- package/{rules → rules-on-demand}/engineering/eng.pre-pr-rules.md +0 -4
- package/{rules → rules-on-demand}/engineering/eng.start-rules.md +0 -4
- package/{rules → rules-on-demand}/engineering/eng.tech-spec-rules.md +1 -2
- package/{rules → rules-on-demand}/engineering/eng.work-rules.md +0 -4
- package/skills/AGENTS.md +22 -21
- package/skills/SKILLS-ROADMAP.md +0 -4
- package/skills/eng-ai-engineer/SKILL.md +6 -0
- package/skills/eng-backend/SKILL.md +6 -0
- package/skills/eng-backend-arch-c4/SKILL.md +10 -4
- package/skills/eng-backend-microservices-trace/SKILL.md +6 -0
- package/skills/eng-backend-nestjs/SKILL.md +16 -461
- package/skills/eng-backend-nestjs/references/1-arquitetura-modulos.md +54 -0
- package/skills/eng-backend-nestjs/references/2-guards-autenticacao.md +118 -0
- package/skills/eng-backend-nestjs/references/3-interceptors-pipes-filters.md +113 -0
- package/skills/eng-backend-nestjs/references/4-config-logging.md +61 -0
- package/skills/eng-backend-nestjs/references/5-testes.md +83 -0
- package/skills/eng-backend-nestjs/references/6-problemas-comuns.md +53 -0
- package/skills/eng-backend-rabbitmq/SKILL.md +15 -298
- package/skills/eng-backend-rabbitmq/references/1-referencia-endpoints.md +28 -0
- package/skills/eng-backend-rabbitmq/references/2-codigo-consumer-producer.md +90 -0
- package/skills/eng-backend-rabbitmq/references/3-arquiteturas-comuns.md +101 -0
- package/skills/eng-backend-rabbitmq/references/4-troubleshooting.md +93 -0
- package/skills/eng-data-bi/SKILL.md +6 -0
- package/skills/eng-data-debug/SKILL.md +6 -0
- package/skills/eng-data-engineer/SKILL.md +16 -7
- package/skills/eng-data-onboard/SKILL.md +6 -0
- package/skills/eng-data-orchestrator/SKILL.md +6 -0
- package/skills/eng-devops-performance-engineer/SKILL.md +6 -0
- package/skills/eng-frontend/SKILL.md +6 -0
- package/skills/eng-frontend-design-system/SKILL.md +13 -479
- package/skills/eng-frontend-design-system/references/1-estrutura-pacote.md +57 -0
- package/skills/eng-frontend-design-system/references/2-tokens.md +116 -0
- package/skills/eng-frontend-design-system/references/3-componentes.md +165 -0
- package/skills/eng-frontend-design-system/references/4-storybook.md +85 -0
- package/skills/eng-frontend-design-system/references/5-auditoria-versionamento.md +76 -0
- package/skills/eng-frontend-microfrontend/SKILL.md +17 -457
- package/skills/eng-frontend-microfrontend/references/1-criando-remote.md +109 -0
- package/skills/eng-frontend-microfrontend/references/2-configurando-shell.md +70 -0
- package/skills/eng-frontend-microfrontend/references/3-contratos-interface.md +73 -0
- package/skills/eng-frontend-microfrontend/references/4-shared-dependencies.md +48 -0
- package/skills/eng-frontend-microfrontend/references/5-event-bus.md +59 -0
- package/skills/eng-frontend-microfrontend/references/6-monorepo-deploy.md +77 -0
- package/skills/eng-frontend-microfrontend/references/7-troubleshooting.md +45 -0
- package/skills/eng-global-browser-extension-builder/SKILL.md +6 -0
- package/skills/eng-global-docs-write/SKILL.md +10 -5
- package/skills/eng-global-jira-comment/SKILL.md +6 -0
- package/skills/eng-global-pr/SKILL.md +7 -1
- package/skills/eng-global-task-comment/SKILL.md +6 -0
- package/skills/eng-qa-a11y-audit/SKILL.md +10 -4
- package/skills/eng-qa-bug-report/SKILL.md +18 -230
- package/skills/eng-qa-bug-report/references/1-integracao-task-managers.md +148 -0
- package/skills/eng-qa-bug-report/references/2-exemplos-de-uso.md +46 -0
- package/skills/eng-qa-bug-report/references/3-bug-to-test.md +45 -0
- package/skills/eng-qa-cypress-e2e/SKILL.md +6 -0
- package/skills/eng-qa-dev-guide/SKILL.md +9 -1
- package/skills/eng-qa-e2e/SKILL.md +9 -3
- package/skills/eng-qa-e2e-spec-writer/SKILL.md +15 -9
- package/skills/eng-qa-exploratory/SKILL.md +16 -9
- package/skills/eng-qa-gate/SKILL.md +11 -5
- package/skills/eng-qa-gate/assets/checklist-validacao.md +1 -1
- package/skills/eng-qa-graphql-contract/SKILL.md +9 -3
- package/skills/eng-qa-quality-report/SKILL.md +9 -3
- package/skills/eng-qa-test-plan/SKILL.md +6 -0
- package/skills/eng-qa-testsprite/SKILL.md +10 -4
- package/skills/eng-qa-unit-test/SKILL.md +7 -1
- package/skills/eng-scraper/SKILL.md +12 -436
- package/skills/eng-scraper/references/1-puppeteer.md +140 -0
- package/skills/eng-scraper/references/2-playwright-cheerio-pdf.md +82 -0
- package/skills/eng-scraper/references/3-anti-bot.md +94 -0
- package/skills/eng-scraper/references/4-resiliencia-etl-monitoramento.md +137 -0
- package/skills/eng-scraper-robot-builder/SKILL.md +6 -0
- package/skills/eng-security-cybersecurity/SKILL.md +13 -409
- package/skills/eng-security-cybersecurity/references/1-owasp-top-10.md +331 -0
- package/skills/eng-security-cybersecurity/references/2-secrets-management.md +31 -0
- package/skills/eng-security-cybersecurity/references/3-input-validation.md +43 -0
- package/skills/eng-security-cybersecurity/references/4-compliance.md +20 -0
- package/skills/eng-security-patch/SKILL.md +6 -0
- package/skills/eng-security-threat-model/SKILL.md +6 -0
- package/skills/eng-security-triage/SKILL.md +6 -0
- package/skills/jarvis-context-detect/SKILL.md +6 -0
- package/skills/jarvis-create-specialization/SKILL.md +6 -0
- package/skills/jarvis-docs-central/SKILL.md +6 -0
- package/skills/jarvis-docs-index/SKILL.md +10 -2
- package/skills/jarvis-evolution-architect/SKILL.md +439 -0
- package/skills/jarvis-evolution-architect/references/exemplos.md +297 -0
- package/skills/jarvis-evolution-architect/references/guia-de-uso.md +355 -0
- package/skills/jarvis-evolution-architect/references/lentes-e-conceitos.md +292 -0
- package/skills/jarvis-evolution-architect/references/visao-geral.md +272 -0
- package/skills/jarvis-init/SKILL.md +14 -7
- package/skills/jarvis-list-specializations/SKILL.md +6 -0
- package/skills/jarvis-report-issue/SKILL.md +6 -0
- package/skills/product-roadmap-report/SKILL.md +8 -0
- package/skills/product-specs/SKILL.md +9 -0
- package/skills/product-specs-update/SKILL.md +6 -0
- package/taxonomy.md +12 -12
- package/templates/AGENTS.md +2 -0
- package/templates/engineering/tech-spec-delivery-template.md +179 -0
- package/workflows/AGENTS.md +12 -2
- package/workflows/README.md +18 -16
- package/workflows/all-tools.md +7 -1
- package/workflows/engineering/data/data.contract.md +8 -0
- package/workflows/engineering/data/data.new-pipeline.md +10 -0
- package/workflows/engineering/eng.breakdown-subtasks.md +9 -0
- package/workflows/engineering/eng.bug-audit.md +9 -2
- package/workflows/engineering/eng.build-tech-spec.md +17 -146
- package/workflows/engineering/eng.create-ard-from-code.md +8 -0
- package/workflows/engineering/eng.create-ard.md +8 -0
- package/workflows/engineering/eng.create-rfc.md +7 -0
- package/workflows/engineering/eng.debug.md +3 -3
- package/workflows/engineering/eng.docs.md +9 -0
- package/workflows/engineering/eng.light-arch.md +7 -0
- package/workflows/engineering/eng.plan.md +15 -2
- package/workflows/engineering/eng.pr.md +19 -7
- package/workflows/engineering/eng.pre-pr.md +27 -1
- package/workflows/engineering/eng.review.md +13 -0
- package/workflows/engineering/eng.rpa.robot.md +16 -0
- package/workflows/engineering/eng.security-audit.md +7 -0
- package/workflows/engineering/eng.security-incident.md +6 -0
- package/workflows/engineering/eng.security-pipeline.md +9 -0
- package/workflows/engineering/eng.security-review.md +6 -0
- package/workflows/engineering/eng.start.md +22 -6
- package/workflows/engineering/eng.work.md +18 -2
- package/workflows/engineering/frontend/eng.frontend-component.md +7 -0
- package/workflows/engineering/frontend/eng.frontend-perf-audit.md +8 -0
- package/workflows/engineering/frontend/eng.frontend-review.md +8 -0
- package/workflows/engineering/qa/eng.qa-dev-quality-guide.md +6 -0
- package/workflows/engineering/qa/eng.qa-e2e-test-generation.md +6 -0
- package/workflows/engineering/qa/eng.qa-exploratory-session.md +8 -0
- package/workflows/engineering/qa/eng.qa-quality-gate-validation.md +7 -0
- package/workflows/engineering/qa/eng.qa-quality-report.md +7 -0
- package/workflows/engineering/qa/eng.qa-refinement-entry.md +7 -0
- package/workflows/engineering/qa/eng.qa-release-signoff.md +6 -0
- package/workflows/engineering/qa/eng.qa-sprint-planning.md +6 -0
- package/workflows/product/prod.roadmap.preview.md +9 -0
- package/workflows/product/prod.spec.ard.md +97 -0
- package/workflows/product/prod.spec.breakdown.md +10 -0
- package/workflows/product/prod.spec.clarify.md +8 -2
- package/workflows/product/prod.spec.discovery.md +10 -0
- package/workflows/product/prod.spec.epic.md +8 -0
- package/workflows/product/prod.spec.frd.md +7 -0
- package/workflows/product/prod.spec.issue.md +6 -0
- package/workflows/product/prod.spec.md +15 -1
- package/workflows/product/prod.spec.prd.md +7 -0
- package/workflows/taxonomy.md +58 -6
- package/workflows/warm-up.md +24 -9
- package/rules/engineering/eng.downstream-flow-rules.md +0 -297
|
@@ -145,446 +145,22 @@ curl -s "https://exemplo.com/robots.txt"
|
|
|
145
145
|
|
|
146
146
|
> Se houver restrições legais ou éticas significativas, comunicar ao usuário antes de prosseguir.
|
|
147
147
|
|
|
148
|
-
###
|
|
149
|
-
|
|
150
|
-
Puppeteer é a ferramenta padrão para scraping com browser headless. Todo código de scraping deve ser em TypeScript e, quando integrado ao sistema, implementado como `@Injectable()` NestJS.
|
|
151
|
-
|
|
152
|
-
#### Setup básico
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
import puppeteer, { Browser, Page } from 'puppeteer'
|
|
156
|
-
|
|
157
|
-
async function createBrowser(): Promise<Browser> {
|
|
158
|
-
return puppeteer.launch({
|
|
159
|
-
headless: true,
|
|
160
|
-
args: [
|
|
161
|
-
'--no-sandbox',
|
|
162
|
-
'--disable-setuid-sandbox',
|
|
163
|
-
'--disable-blink-features=AutomationControlled',
|
|
164
|
-
],
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ✅ Sempre fechar browser após uso
|
|
169
|
-
async function withBrowser<T>(fn: (browser: Browser) => Promise<T>): Promise<T> {
|
|
170
|
-
const browser = await createBrowser()
|
|
171
|
-
try {
|
|
172
|
-
return await fn(browser)
|
|
173
|
-
} finally {
|
|
174
|
-
await browser.close()
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
#### NestJS — ScraperService como @Injectable
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
import { Injectable, Logger } from '@nestjs/common'
|
|
183
|
-
import puppeteer, { Browser, Page } from 'puppeteer'
|
|
184
|
-
|
|
185
|
-
@Injectable()
|
|
186
|
-
export class ScraperService {
|
|
187
|
-
private readonly logger = new Logger(ScraperService.name)
|
|
188
|
-
|
|
189
|
-
async scrapeProducts(url: string): Promise<Product[]> {
|
|
190
|
-
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] })
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
const page = await browser.newPage()
|
|
194
|
-
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...')
|
|
195
|
-
|
|
196
|
-
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 })
|
|
197
|
-
await page.waitForSelector('[data-testid="product-card"]', { timeout: 10_000 })
|
|
198
|
-
|
|
199
|
-
const products = await page.evaluate(() =>
|
|
200
|
-
Array.from(document.querySelectorAll('[data-testid="product-card"]')).map((card) => ({
|
|
201
|
-
name: card.querySelector('h2')?.textContent?.trim() ?? null,
|
|
202
|
-
price: card.querySelector('[data-price]')?.getAttribute('data-price') ?? null,
|
|
203
|
-
url: (card.querySelector('a') as HTMLAnchorElement)?.href ?? null,
|
|
204
|
-
}))
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
const valid = products.filter((p) => p.name && p.price)
|
|
208
|
-
if (valid.length < products.length) {
|
|
209
|
-
this.logger.warn(`${products.length - valid.length} produtos com dados incompletos ignorados`)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return valid as Product[]
|
|
213
|
-
} finally {
|
|
214
|
-
await browser.close()
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
#### Interceptar chamadas de API interna
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
// ✅ Mais estável que scraping de HTML — dados estruturados direto da API interna
|
|
224
|
-
async function interceptApiData(url: string, apiPattern: RegExp): Promise<unknown[]> {
|
|
225
|
-
return withBrowser(async (browser) => {
|
|
226
|
-
const page = await browser.newPage()
|
|
227
|
-
const captured: unknown[] = []
|
|
228
|
-
|
|
229
|
-
await page.setRequestInterception(true)
|
|
230
|
-
|
|
231
|
-
page.on('request', (req) => req.continue())
|
|
232
|
-
|
|
233
|
-
page.on('response', async (response) => {
|
|
234
|
-
if (apiPattern.test(response.url()) && response.headers()['content-type']?.includes('json')) {
|
|
235
|
-
try {
|
|
236
|
-
captured.push(await response.json())
|
|
237
|
-
} catch {
|
|
238
|
-
// ignorar responses não-JSON
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 })
|
|
244
|
-
|
|
245
|
-
return captured
|
|
246
|
-
})
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Uso: interceptar chamadas de /api/products/*
|
|
250
|
-
const data = await interceptApiData('https://loja.com/produtos', /\/api\/products/)
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
#### Paginação automática
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
async function scrapeAllPages(baseUrl: string): Promise<unknown[]> {
|
|
257
|
-
return withBrowser(async (browser) => {
|
|
258
|
-
const page = await browser.newPage()
|
|
259
|
-
const allItems: unknown[] = []
|
|
260
|
-
let pageNum = 1
|
|
261
|
-
let hasNextPage = true
|
|
262
|
-
|
|
263
|
-
while (hasNextPage) {
|
|
264
|
-
await page.goto(`${baseUrl}?page=${pageNum}`, { waitUntil: 'networkidle2' })
|
|
265
|
-
|
|
266
|
-
const items = await page.evaluate(() =>
|
|
267
|
-
Array.from(document.querySelectorAll('.item')).map((el) => ({
|
|
268
|
-
title: el.querySelector('h3')?.textContent?.trim() ?? null,
|
|
269
|
-
}))
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
allItems.push(...items)
|
|
273
|
-
|
|
274
|
-
hasNextPage = (await page.$('[aria-label="Próxima página"]:not([disabled])')) !== null
|
|
275
|
-
pageNum++
|
|
276
|
-
|
|
277
|
-
await randomDelay(1000, 3000)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return allItems
|
|
281
|
-
})
|
|
282
|
-
}
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### Playwright — Alternativa (quando necessário)
|
|
286
|
-
|
|
287
|
-
Usar Playwright quando Puppeteer não resolver o caso de uso (ex: multi-browser testing, maior controle de contexto).
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
import { chromium } from 'playwright'
|
|
291
|
-
|
|
292
|
-
// Mesma lógica do Puppeteer — API similar com pequenas diferenças:
|
|
293
|
-
// page.goto(url, { waitUntil: 'networkidle' }) → sem o "2" no Playwright
|
|
294
|
-
// page.$() → page.locator() é o padrão moderno no Playwright
|
|
295
|
-
// page.evaluate() → idêntico
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### Cheerio — Scraping de HTML Estático
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
import * as cheerio from 'cheerio'
|
|
302
|
-
|
|
303
|
-
async function scrapeWithCheerio(url: string) {
|
|
304
|
-
const response = await fetch(url, {
|
|
305
|
-
headers: {
|
|
306
|
-
'User-Agent': 'Mozilla/5.0 (compatible; MyBot/1.0; +https://meusite.com/bot)',
|
|
307
|
-
'Accept-Language': 'pt-BR,pt;q=0.9',
|
|
308
|
-
},
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
if (!response.ok) {
|
|
312
|
-
throw new Error(`HTTP ${response.status}: ${url}`)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const html = await response.text()
|
|
316
|
-
const $ = cheerio.load(html)
|
|
317
|
-
|
|
318
|
-
// ✅ Extração com Cheerio
|
|
319
|
-
const articles = $('article.post').map((_, el) => ({
|
|
320
|
-
title: $(el).find('h2').text().trim() || null,
|
|
321
|
-
date: $(el).find('time').attr('datetime') || null,
|
|
322
|
-
url: $(el).find('a.read-more').attr('href') || null,
|
|
323
|
-
summary: $(el).find('.excerpt').text().trim() || null,
|
|
324
|
-
})).get()
|
|
325
|
-
|
|
326
|
-
return articles.filter((a) => a.title && a.url)
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
### Parsing de PDFs
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
import pdfParse from 'pdf-parse'
|
|
334
|
-
import { readFileSync } from 'fs'
|
|
335
|
-
|
|
336
|
-
async function extractTextFromPdf(filePath: string): Promise<string> {
|
|
337
|
-
const buffer = readFileSync(filePath)
|
|
338
|
-
const { text, numpages } = await pdfParse(buffer)
|
|
339
|
-
console.log(`PDF processado: ${numpages} páginas`)
|
|
340
|
-
return text
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// ✅ Extrair tabela de texto de PDF (regex + heurística)
|
|
344
|
-
function extractTableFromText(text: string, headers: string[]): Record<string, string>[] {
|
|
345
|
-
const lines = text.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
346
|
-
const results: Record<string, string>[] = []
|
|
347
|
-
|
|
348
|
-
for (const line of lines) {
|
|
349
|
-
// lógica específica para o formato do PDF alvo
|
|
350
|
-
const match = line.match(/(\w+)\s+([\d,.]+)\s+([\d,.]+)/)
|
|
351
|
-
if (match) {
|
|
352
|
-
results.push({
|
|
353
|
-
[headers[0]]: match[1],
|
|
354
|
-
[headers[1]]: match[2],
|
|
355
|
-
[headers[2]]: match[3],
|
|
356
|
-
})
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return results
|
|
361
|
-
}
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
### Técnicas Anti-Bot
|
|
365
|
-
|
|
366
|
-
#### Headers realistas
|
|
367
|
-
|
|
368
|
-
```typescript
|
|
369
|
-
// ✅ Headers que imitam browser real
|
|
370
|
-
const realisticHeaders = {
|
|
371
|
-
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
372
|
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
|
373
|
-
'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
374
|
-
'Accept-Encoding': 'gzip, deflate, br',
|
|
375
|
-
'Connection': 'keep-alive',
|
|
376
|
-
'Upgrade-Insecure-Requests': '1',
|
|
377
|
-
}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
#### Rate limiting e delays aleatórios
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
// ✅ Delay com jitter para comportamento orgânico
|
|
384
|
-
function randomDelay(minMs: number, maxMs: number): Promise<void> {
|
|
385
|
-
const ms = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs
|
|
386
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ✅ Controle de concorrência (evitar sobrecarga no alvo)
|
|
390
|
-
import PQueue from 'p-queue'
|
|
391
|
-
|
|
392
|
-
const queue = new PQueue({
|
|
393
|
-
concurrency: 2, // máximo 2 requests simultâneos
|
|
394
|
-
intervalCap: 5, // máximo 5 requests
|
|
395
|
-
interval: 10_000, // por 10 segundos
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
async function scrapeUrls(urls: string[]) {
|
|
399
|
-
return Promise.all(
|
|
400
|
-
urls.map((url) => queue.add(async () => {
|
|
401
|
-
await randomDelay(500, 1500)
|
|
402
|
-
return scrapeUrl(url)
|
|
403
|
-
}))
|
|
404
|
-
)
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
#### Puppeteer — reduzir fingerprint de automação
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
import puppeteer from 'puppeteer'
|
|
412
|
-
import { executablePath } from 'puppeteer'
|
|
413
|
-
|
|
414
|
-
// ✅ Ocultar sinais de automação via page.evaluateOnNewDocument
|
|
415
|
-
async function createStealthPage(browser: Browser): Promise<Page> {
|
|
416
|
-
const page = await browser.newPage()
|
|
417
|
-
|
|
418
|
-
await page.setUserAgent(
|
|
419
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
await page.setViewport({ width: 1366, height: 768 })
|
|
423
|
-
|
|
424
|
-
// Remover propriedades que identificam automação
|
|
425
|
-
await page.evaluateOnNewDocument(() => {
|
|
426
|
-
Object.defineProperty(navigator, 'webdriver', { get: () => undefined })
|
|
427
|
-
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3] })
|
|
428
|
-
Object.defineProperty(navigator, 'languages', { get: () => ['pt-BR', 'pt', 'en'] })
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
return page
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
#### Rotação de proxies
|
|
436
|
-
|
|
437
|
-
```typescript
|
|
438
|
-
// ✅ Pool de proxies com rotação
|
|
439
|
-
const proxies = [
|
|
440
|
-
'http://user:pass@proxy1:8080',
|
|
441
|
-
'http://user:pass@proxy2:8080',
|
|
442
|
-
'http://user:pass@proxy3:8080',
|
|
443
|
-
]
|
|
444
|
-
|
|
445
|
-
function getRandomProxy(): string {
|
|
446
|
-
return proxies[Math.floor(Math.random() * proxies.length)]
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const browser = await puppeteer.launch({
|
|
450
|
-
headless: true,
|
|
451
|
-
args: [`--proxy-server=${getRandomProxy()}`, '--no-sandbox'],
|
|
452
|
-
})
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
### Resiliência e Retry
|
|
456
|
-
|
|
457
|
-
```typescript
|
|
458
|
-
// ✅ Retry com backoff exponencial
|
|
459
|
-
async function withRetry<T>(
|
|
460
|
-
fn: () => Promise<T>,
|
|
461
|
-
options: { attempts?: number; baseDelay?: number; label?: string } = {}
|
|
462
|
-
): Promise<T> {
|
|
463
|
-
const { attempts = 3, baseDelay = 1000, label = 'operação' } = options
|
|
464
|
-
|
|
465
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
466
|
-
try {
|
|
467
|
-
return await fn()
|
|
468
|
-
} catch (error) {
|
|
469
|
-
if (attempt === attempts) throw error
|
|
470
|
-
|
|
471
|
-
const delay = baseDelay * Math.pow(2, attempt - 1)
|
|
472
|
-
console.warn(`${label} falhou (tentativa ${attempt}/${attempts}). Retry em ${delay}ms...`)
|
|
473
|
-
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
throw new Error('Unreachable')
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Uso
|
|
481
|
-
const data = await withRetry(
|
|
482
|
-
() => scrapePage(url),
|
|
483
|
-
{ attempts: 3, baseDelay: 2000, label: `scrape ${url}` }
|
|
484
|
-
)
|
|
485
|
-
```
|
|
148
|
+
### Referências (leia só o tema que a tarefa pedir)
|
|
486
149
|
|
|
487
|
-
|
|
150
|
+
Leia **só** o arquivo do tema que a tarefa pedir; o restante fica fora do contexto.
|
|
488
151
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
152
|
+
| Tema | Quando ler | Arquivo |
|
|
153
|
+
|------|-----------|---------|
|
|
154
|
+
| Puppeteer | A tarefa usar Puppeteer (ferramenta principal do projeto) para navegar e extrair dados | `references/1-puppeteer.md` |
|
|
155
|
+
| Playwright, Cheerio e parsing de PDFs | A tarefa exigir Playwright, scraping de HTML estático com Cheerio ou extração de PDFs | `references/2-playwright-cheerio-pdf.md` |
|
|
156
|
+
| Técnicas Anti-Bot | O site bloquear requests (403/429, captcha, fingerprint) | `references/3-anti-bot.md` |
|
|
157
|
+
| Resiliência, ETL leve e monitoramento de mudanças | A tarefa precisar de retry, pipeline ETL leve ou monitoramento de mudanças no site | `references/4-resiliencia-etl-monitoramento.md` |
|
|
493
158
|
|
|
494
|
-
|
|
495
|
-
requiredFields.some((field) => !item[field as keyof typeof item])
|
|
496
|
-
)
|
|
159
|
+
### Skills invocados durante a execução do skill
|
|
497
160
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
console.warn('Possível mudança de estrutura no site alvo. Verificar seletores.')
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return data.filter((item) => requiredFields.every((f) => item[f as keyof typeof item]))
|
|
505
|
-
}
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
### Pipelines ETL Leve
|
|
509
|
-
|
|
510
|
-
#### Estrutura básica de pipeline
|
|
511
|
-
|
|
512
|
-
```typescript
|
|
513
|
-
interface PipelineConfig {
|
|
514
|
-
sources: string[] // URLs a processar
|
|
515
|
-
transform: (raw: unknown) => unknown // transformação de dados
|
|
516
|
-
output: string // caminho do arquivo de saída
|
|
517
|
-
concurrency?: number // requests simultâneos
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
async function runPipeline(config: PipelineConfig) {
|
|
521
|
-
console.log(`Iniciando pipeline: ${config.sources.length} fontes`)
|
|
522
|
-
|
|
523
|
-
// Extract
|
|
524
|
-
const rawData = await scrapeUrls(config.sources)
|
|
525
|
-
|
|
526
|
-
// Transform
|
|
527
|
-
const transformed = rawData
|
|
528
|
-
.flat()
|
|
529
|
-
.map(config.transform)
|
|
530
|
-
.filter(Boolean)
|
|
531
|
-
|
|
532
|
-
// Deduplicação por URL ou ID
|
|
533
|
-
const deduped = [...new Map(transformed.map((item) => [item.id ?? item.url, item])).values()]
|
|
534
|
-
|
|
535
|
-
// Load
|
|
536
|
-
await saveToFile(config.output, deduped)
|
|
537
|
-
console.log(`Pipeline concluído: ${deduped.length} registros salvos em ${config.output}`)
|
|
538
|
-
|
|
539
|
-
return deduped
|
|
540
|
-
}
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
#### Formatos de saída
|
|
544
|
-
|
|
545
|
-
```typescript
|
|
546
|
-
import { writeFileSync } from 'fs'
|
|
547
|
-
import { stringify } from 'csv-stringify/sync'
|
|
548
|
-
|
|
549
|
-
function saveToFile(path: string, data: unknown[]) {
|
|
550
|
-
if (path.endsWith('.json')) {
|
|
551
|
-
writeFileSync(path, JSON.stringify(data, null, 2), 'utf-8')
|
|
552
|
-
} else if (path.endsWith('.csv')) {
|
|
553
|
-
const csv = stringify(data as object[], { header: true })
|
|
554
|
-
writeFileSync(path, csv, 'utf-8')
|
|
555
|
-
} else if (path.endsWith('.ndjson')) {
|
|
556
|
-
const ndjson = data.map((d) => JSON.stringify(d)).join('\n')
|
|
557
|
-
writeFileSync(path, ndjson, 'utf-8')
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
### Monitoramento de Mudanças
|
|
563
|
-
|
|
564
|
-
```typescript
|
|
565
|
-
// ✅ Detectar mudanças em páginas monitoradas
|
|
566
|
-
async function checkForChanges(url: string, storePath: string) {
|
|
567
|
-
const current = await scrapeWithCheerio(url)
|
|
568
|
-
const currentHash = crypto.createHash('md5').update(JSON.stringify(current)).digest('hex')
|
|
569
|
-
|
|
570
|
-
let previous: { hash: string; data: unknown; timestamp: string } | null = null
|
|
571
|
-
try {
|
|
572
|
-
previous = JSON.parse(readFileSync(storePath, 'utf-8'))
|
|
573
|
-
} catch {
|
|
574
|
-
// primeira execução
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (previous?.hash === currentHash) {
|
|
578
|
-
console.log('Sem mudanças detectadas.')
|
|
579
|
-
return { changed: false }
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Persistir estado atual
|
|
583
|
-
writeFileSync(storePath, JSON.stringify({ hash: currentHash, data: current, timestamp: new Date().toISOString() }))
|
|
584
|
-
|
|
585
|
-
return { changed: true, current, previous: previous?.data }
|
|
586
|
-
}
|
|
587
|
-
```
|
|
161
|
+
| Passo | Skill | Condição |
|
|
162
|
+
|-------|-------|----------|
|
|
163
|
+
| — | Nenhuma — skill autocontida | — |
|
|
588
164
|
|
|
589
165
|
---
|
|
590
166
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Puppeteer
|
|
2
|
+
|
|
3
|
+
> Parte do skill `eng-scraper`. Leia este arquivo quando a tarefa usar Puppeteer (ferramenta principal do projeto) para navegar e extrair dados.
|
|
4
|
+
|
|
5
|
+
### Puppeteer — Ferramenta Principal (padrão do projeto)
|
|
6
|
+
|
|
7
|
+
Puppeteer é a ferramenta padrão para scraping com browser headless. Todo código de scraping deve ser em TypeScript e, quando integrado ao sistema, implementado como `@Injectable()` NestJS.
|
|
8
|
+
|
|
9
|
+
#### Setup básico
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import puppeteer, { Browser, Page } from 'puppeteer'
|
|
13
|
+
|
|
14
|
+
async function createBrowser(): Promise<Browser> {
|
|
15
|
+
return puppeteer.launch({
|
|
16
|
+
headless: true,
|
|
17
|
+
args: [
|
|
18
|
+
'--no-sandbox',
|
|
19
|
+
'--disable-setuid-sandbox',
|
|
20
|
+
'--disable-blink-features=AutomationControlled',
|
|
21
|
+
],
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ✅ Sempre fechar browser após uso
|
|
26
|
+
async function withBrowser<T>(fn: (browser: Browser) => Promise<T>): Promise<T> {
|
|
27
|
+
const browser = await createBrowser()
|
|
28
|
+
try {
|
|
29
|
+
return await fn(browser)
|
|
30
|
+
} finally {
|
|
31
|
+
await browser.close()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
#### NestJS — ScraperService como @Injectable
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Injectable, Logger } from '@nestjs/common'
|
|
40
|
+
import puppeteer, { Browser, Page } from 'puppeteer'
|
|
41
|
+
|
|
42
|
+
@Injectable()
|
|
43
|
+
export class ScraperService {
|
|
44
|
+
private readonly logger = new Logger(ScraperService.name)
|
|
45
|
+
|
|
46
|
+
async scrapeProducts(url: string): Promise<Product[]> {
|
|
47
|
+
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] })
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const page = await browser.newPage()
|
|
51
|
+
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...')
|
|
52
|
+
|
|
53
|
+
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 })
|
|
54
|
+
await page.waitForSelector('[data-testid="product-card"]', { timeout: 10_000 })
|
|
55
|
+
|
|
56
|
+
const products = await page.evaluate(() =>
|
|
57
|
+
Array.from(document.querySelectorAll('[data-testid="product-card"]')).map((card) => ({
|
|
58
|
+
name: card.querySelector('h2')?.textContent?.trim() ?? null,
|
|
59
|
+
price: card.querySelector('[data-price]')?.getAttribute('data-price') ?? null,
|
|
60
|
+
url: (card.querySelector('a') as HTMLAnchorElement)?.href ?? null,
|
|
61
|
+
}))
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const valid = products.filter((p) => p.name && p.price)
|
|
65
|
+
if (valid.length < products.length) {
|
|
66
|
+
this.logger.warn(`${products.length - valid.length} produtos com dados incompletos ignorados`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return valid as Product[]
|
|
70
|
+
} finally {
|
|
71
|
+
await browser.close()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Interceptar chamadas de API interna
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// ✅ Mais estável que scraping de HTML — dados estruturados direto da API interna
|
|
81
|
+
async function interceptApiData(url: string, apiPattern: RegExp): Promise<unknown[]> {
|
|
82
|
+
return withBrowser(async (browser) => {
|
|
83
|
+
const page = await browser.newPage()
|
|
84
|
+
const captured: unknown[] = []
|
|
85
|
+
|
|
86
|
+
await page.setRequestInterception(true)
|
|
87
|
+
|
|
88
|
+
page.on('request', (req) => req.continue())
|
|
89
|
+
|
|
90
|
+
page.on('response', async (response) => {
|
|
91
|
+
if (apiPattern.test(response.url()) && response.headers()['content-type']?.includes('json')) {
|
|
92
|
+
try {
|
|
93
|
+
captured.push(await response.json())
|
|
94
|
+
} catch {
|
|
95
|
+
// ignorar responses não-JSON
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 })
|
|
101
|
+
|
|
102
|
+
return captured
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Uso: interceptar chamadas de /api/products/*
|
|
107
|
+
const data = await interceptApiData('https://loja.com/produtos', /\/api\/products/)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Paginação automática
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
async function scrapeAllPages(baseUrl: string): Promise<unknown[]> {
|
|
114
|
+
return withBrowser(async (browser) => {
|
|
115
|
+
const page = await browser.newPage()
|
|
116
|
+
const allItems: unknown[] = []
|
|
117
|
+
let pageNum = 1
|
|
118
|
+
let hasNextPage = true
|
|
119
|
+
|
|
120
|
+
while (hasNextPage) {
|
|
121
|
+
await page.goto(`${baseUrl}?page=${pageNum}`, { waitUntil: 'networkidle2' })
|
|
122
|
+
|
|
123
|
+
const items = await page.evaluate(() =>
|
|
124
|
+
Array.from(document.querySelectorAll('.item')).map((el) => ({
|
|
125
|
+
title: el.querySelector('h3')?.textContent?.trim() ?? null,
|
|
126
|
+
}))
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
allItems.push(...items)
|
|
130
|
+
|
|
131
|
+
hasNextPage = (await page.$('[aria-label="Próxima página"]:not([disabled])')) !== null
|
|
132
|
+
pageNum++
|
|
133
|
+
|
|
134
|
+
await randomDelay(1000, 3000)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return allItems
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Playwright, Cheerio e parsing de PDFs
|
|
2
|
+
|
|
3
|
+
> Parte do skill `eng-scraper`. Leia este arquivo quando a tarefa exigir Playwright, scraping de HTML estático com Cheerio ou extração de PDFs.
|
|
4
|
+
|
|
5
|
+
### Playwright — Alternativa (quando necessário)
|
|
6
|
+
|
|
7
|
+
Usar Playwright quando Puppeteer não resolver o caso de uso (ex: multi-browser testing, maior controle de contexto).
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { chromium } from 'playwright'
|
|
11
|
+
|
|
12
|
+
// Mesma lógica do Puppeteer — API similar com pequenas diferenças:
|
|
13
|
+
// page.goto(url, { waitUntil: 'networkidle' }) → sem o "2" no Playwright
|
|
14
|
+
// page.$() → page.locator() é o padrão moderno no Playwright
|
|
15
|
+
// page.evaluate() → idêntico
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Cheerio — Scraping de HTML Estático
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import * as cheerio from 'cheerio'
|
|
22
|
+
|
|
23
|
+
async function scrapeWithCheerio(url: string) {
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
headers: {
|
|
26
|
+
'User-Agent': 'Mozilla/5.0 (compatible; MyBot/1.0; +https://meusite.com/bot)',
|
|
27
|
+
'Accept-Language': 'pt-BR,pt;q=0.9',
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(`HTTP ${response.status}: ${url}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const html = await response.text()
|
|
36
|
+
const $ = cheerio.load(html)
|
|
37
|
+
|
|
38
|
+
// ✅ Extração com Cheerio
|
|
39
|
+
const articles = $('article.post').map((_, el) => ({
|
|
40
|
+
title: $(el).find('h2').text().trim() || null,
|
|
41
|
+
date: $(el).find('time').attr('datetime') || null,
|
|
42
|
+
url: $(el).find('a.read-more').attr('href') || null,
|
|
43
|
+
summary: $(el).find('.excerpt').text().trim() || null,
|
|
44
|
+
})).get()
|
|
45
|
+
|
|
46
|
+
return articles.filter((a) => a.title && a.url)
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Parsing de PDFs
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import pdfParse from 'pdf-parse'
|
|
54
|
+
import { readFileSync } from 'fs'
|
|
55
|
+
|
|
56
|
+
async function extractTextFromPdf(filePath: string): Promise<string> {
|
|
57
|
+
const buffer = readFileSync(filePath)
|
|
58
|
+
const { text, numpages } = await pdfParse(buffer)
|
|
59
|
+
console.log(`PDF processado: ${numpages} páginas`)
|
|
60
|
+
return text
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ✅ Extrair tabela de texto de PDF (regex + heurística)
|
|
64
|
+
function extractTableFromText(text: string, headers: string[]): Record<string, string>[] {
|
|
65
|
+
const lines = text.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
66
|
+
const results: Record<string, string>[] = []
|
|
67
|
+
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
// lógica específica para o formato do PDF alvo
|
|
70
|
+
const match = line.match(/(\w+)\s+([\d,.]+)\s+([\d,.]+)/)
|
|
71
|
+
if (match) {
|
|
72
|
+
results.push({
|
|
73
|
+
[headers[0]]: match[1],
|
|
74
|
+
[headers[1]]: match[2],
|
|
75
|
+
[headers[2]]: match[3],
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return results
|
|
81
|
+
}
|
|
82
|
+
```
|