@wenathlan/saddle 1.8.2 → 1.8.5
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/README.md +9 -9
- package/browser/playwright.js +22 -0
- package/docs/.gitkeep +0 -0
- package/docs/gapmatrix.md +3 -3
- package/docs/libraryapi.md +1 -0
- package/docs/logs/.gitkeep +0 -0
- package/docs/packageaudit185.md +23 -0
- package/docs/plans/00.index.md +50 -0
- package/docs/plans/01.architecture.md +86 -0
- package/docs/plans/02.research.computer.use.md +58 -0
- package/docs/plans/03.research.captcha.bypass.md +68 -0
- package/docs/plans/04.research.sandbox.ai.md +52 -0
- package/docs/plans/05.capture.platform.md +57 -0
- package/docs/plans/06.dependencies.md +97 -0
- package/docs/plans/07.captcha.test.page.md +41 -0
- package/docs/plans/08.production.infra.md +70 -0
- package/docs/plans/09.database.schema.md +121 -0
- package/docs/plans/10.cloudinary.storage.md +57 -0
- package/docs/plans/11.movement.logs.json.md +72 -0
- package/docs/plans/12.research.atlas.agent.browser.md +79 -0
- package/docs/plans/13.research.anti.detection.md +898 -0
- package/docs/plans/14.research.proxy.md +1495 -0
- package/docs/plans/15.research.retry.rate.limit.md +1958 -0
- package/docs/plans/16.research.crawling.md +1417 -0
- package/docs/plans/17.research.caching.md +1610 -0
- package/docs/plans/18.research.content.extraction.md +1952 -0
- package/docs/plans/19.research.errors.events.md +1523 -0
- package/docs/plans/20.research.zod.validation.md +1350 -0
- package/docs/plans/21.research.batch.concurrency.md +1888 -0
- package/docs/plans/22.research.universal.runtime.md +944 -0
- package/docs/plans/23.research.ai.integration.md +1465 -0
- package/docs/plans/24.research.memory.persistence.md +1979 -0
- package/docs/plans/25.research.server.api.md +342 -0
- package/docs/plans/26.research.compilation.md +249 -0
- package/docs/plans/27.research.html.parsing.md +251 -0
- package/docs/plans/28.action.plan.md +50 -0
- package/docs/plans/29.api.reference.md +174 -0
- package/docs/plans/30.architecture.plan.md +94 -0
- package/docs/plans/31.auditoria.dados.md +163 -0
- package/docs/plans/32.bots.automacao.computacional.md +214 -0
- package/docs/plans/33.bots.codigo.revisao.md +220 -0
- package/docs/plans/34.bots.seguranca.cicd.md +366 -0
- package/docs/plans/35.comparativo.concorrencia.md +464 -0
- package/docs/plans/36.computational.memory.md +340 -0
- package/docs/plans/37.deploystrategy.md +394 -0
- package/docs/plans/38.flow.md +155 -0
- package/docs/plans/39.multi.platform.bot.md +252 -0
- package/docs/plans/40.npm.publish.md +250 -0
- package/docs/plans/41.o.que.falta.md +407 -0
- package/docs/plans/42.pesquisa.concorrencia.md +721 -0
- package/docs/plans/43.plan.universal.architecture.md +496 -0
- package/docs/plans/44.reference.md +100 -0
- package/docs/plans/45.robotarchitecture.md +237 -0
- package/docs/plans/46.scdnintegration.md +284 -0
- package/docs/plans/47.multiforge.readme.md +129 -0
- package/docs/plans/48.theory.v4.repo.os.md +152 -0
- package/docs/plans/49.third.party.infra.md +12 -0
- package/docs/plans/50.file.as.compute.md +39 -0
- package/docs/plans/51.architecture.virtual.processor.md +80 -0
- package/docs/plans/52.manifesto.v8.md +11 -0
- package/docs/plans/58.cdn.list.md +23 -0
- package/docs/plans/59.sql.frameworks.md +33 -0
- package/docs/plans/60.sql.thirdparty.md +26 -0
- package/docs/plans/61.objective.multiforge.md +63 -0
- package/docs/plans/62.huggingface.upload.md +26 -0
- package/docs/plans/63.kaggle.upload.md +24 -0
- package/docs/plans/64.npm.storage.md +30 -0
- package/docs/plans/65.rclone.terabox.md +32 -0
- package/docs/plans/66.buckets.and.models.todo.md +14 -0
- package/docs/plans/67.database.todo.md +13 -0
- package/docs/plans/68.deploy.packages.todo.md +12 -0
- package/docs/plans/69.report.human.operator.md +133 -0
- package/docs/plans/70.report.brain2qwerty.ems.md +135 -0
- package/docs/plans/71.report.hd.infinito.vram.md +155 -0
- package/docs/plans/72.plan.hd.infinito.node.md +146 -0
- package/docs/plans/73.plan.scifi.repos.md +125 -0
- package/docs/plans/74.000.manifesto.v8.flat.2..md +11 -0
- package/docs/plans/README.md +489 -0
- package/docs/plans/aggregate_platforms.mjs +146 -0
- package/docs/plans/examplesession.json +36 -0
- package/docs/plans/missing-facts.md +192 -0
- package/docs/plans/models.md +64 -0
- package/docs/plans/organize.cjs +270 -0
- package/docs/plans/platforms.md +2887 -0
- package/docs/plans/sites.md +31322 -0
- package/docs/platformpipelineaudit.md +18 -0
- package/docs/platformpipelines.md +13 -0
- package/docs/registryresearch.md +4 -0
- package/docs/release.md +4 -4
- package/docs/release182notes.md +2 -0
- package/docs/release184notes.md +7 -0
- package/docs/release185notes.md +7 -0
- package/docs/sources/farm.py +117 -0
- package/docs/sources/html/saddle1.html +132 -0
- package/docs/sources/html/saddle2.html +157 -0
- package/docs/sources/html/saddle3.html +119 -0
- package/docs/sources/html/saddle4.html +144 -0
- package/docs/sources/html/saddle5.html +72 -0
- package/docs/sources/html/saddle6.html +171 -0
- package/docs/sources/html/saddle7.html +236 -0
- package/docs/sources/saddle.ts +74 -0
- package/docs/sources/schema.prisma +88 -0
- package/docs/sources/script.sh +64 -0
- package/docs/sources/workflows.yml +458 -0
- package/docs/talks1/_body.txt +14 -0
- package/docs/talks1/_index.md +15 -0
- package/docs/talks1/_screenshot.png +0 -0
- package/docs/talks1/assistant-01.md +5 -0
- package/docs/talks1/assistant-02.md +5 -0
- package/docs/talks1/assistant-03.md +531 -0
- package/docs/talks1/assistant-04.md +26 -0
- package/docs/talks1/assistant-05.md +774 -0
- package/docs/talks1/assistant-06.md +1718 -0
- package/docs/talks1/scrape-share.cjs +185 -0
- package/docs/talks1/scrape-share.ts +183 -0
- package/docs/talks1/user-01.md +3 -0
- package/docs/talks1/user-02.md +3 -0
- package/docs/talks1/user-03.md +88 -0
- package/docs/talks1/user-04.md +3 -0
- package/docs/talks1/user-05.md +3 -0
- package/docs/talks1/user-06.md +88 -0
- package/docs/talks1/user-07.md +88 -0
- package/docs/talks2/_body.txt +14 -0
- package/docs/talks2/_index.md +16 -0
- package/docs/talks2/_screenshot.png +0 -0
- package/docs/talks2/assistant-01.md +5 -0
- package/docs/talks2/assistant-02.md +5 -0
- package/docs/talks2/assistant-03.md +424 -0
- package/docs/talks2/assistant-04.md +598 -0
- package/docs/talks2/assistant-05.md +1280 -0
- package/docs/talks2/assistant-06.md +1227 -0
- package/docs/talks2/assistant-07.md +1252 -0
- package/docs/talks2/user-01.md +3 -0
- package/docs/talks2/user-02.md +3 -0
- package/docs/talks2/user-03.md +88 -0
- package/docs/talks2/user-04.md +88 -0
- package/docs/talks2/user-05.md +88 -0
- package/docs/talks2/user-06.md +88 -0
- package/docs/talks2/user-07.md +3 -0
- package/docs/talks3/_body.txt +467 -0
- package/docs/talks3/_index.md +10 -0
- package/docs/talks3/_screenshot.png +0 -0
- package/docs/talks3/assistant-01.md +417 -0
- package/docs/talks3/assistant-02.md +417 -0
- package/docs/talks3/assistant-03.md +29 -0
- package/docs/talks3/assistant-04.md +727 -0
- package/docs/talks3/user-01.md +88 -0
- package/docs/talks3/user-02.md +88 -0
- package/docs/talks3/user-03.md +3 -0
- package/docs/talks3/user-04.md +3 -0
- package/docs/talks4/_body.txt +14 -0
- package/docs/talks4/_index.md +12 -0
- package/docs/talks4/_screenshot.png +0 -0
- package/docs/talks4/assistant-01.md +5 -0
- package/docs/talks4/assistant-02.md +5 -0
- package/docs/talks4/assistant-03.md +35 -0
- package/docs/talks4/assistant-04.md +512 -0
- package/docs/talks4/assistant-05.md +599 -0
- package/docs/talks4/user-01.md +3 -0
- package/docs/talks4/user-02.md +3 -0
- package/docs/talks4/user-03.md +88 -0
- package/docs/talks4/user-04.md +88 -0
- package/docs/talks4/user-05.md +7 -0
- package/docs/talks5/_body.txt +14 -0
- package/docs/talks5/_index.md +13 -0
- package/docs/talks5/_screenshot.png +0 -0
- package/docs/talks5/assistant-01.md +5 -0
- package/docs/talks5/assistant-02.md +5 -0
- package/docs/talks5/assistant-03.md +690 -0
- package/docs/talks5/assistant-04.md +758 -0
- package/docs/talks5/assistant-05.md +974 -0
- package/docs/talks5/user-01.md +3 -0
- package/docs/talks5/user-02.md +3 -0
- package/docs/talks5/user-03.md +105 -0
- package/docs/talks5/user-04.md +105 -0
- package/docs/talks5/user-05.md +63 -0
- package/docs/talks5/user-06.md +105 -0
- package/docs/talks6/_body.txt +14 -0
- package/docs/talks6/_index.md +9 -0
- package/docs/talks6/_screenshot.png +0 -0
- package/docs/talks6/assistant-01.md +5 -0
- package/docs/talks6/assistant-02.md +5 -0
- package/docs/talks6/assistant-03.md +1499 -0
- package/docs/talks6/user-01.md +3 -0
- package/docs/talks6/user-02.md +3 -0
- package/docs/talks6/user-03.md +88 -0
- package/docs/talks6/user-04.md +88 -0
- package/docs/talks7/_body.txt +14 -0
- package/docs/talks7/_index.md +10 -0
- package/docs/talks7/_screenshot.png +0 -0
- package/docs/talks7/assistant-01.md +5 -0
- package/docs/talks7/assistant-02.md +5 -0
- package/docs/talks7/assistant-03.md +523 -0
- package/docs/talks7/assistant-04.md +617 -0
- package/docs/talks7/user-01.md +3 -0
- package/docs/talks7/user-02.md +3 -0
- package/docs/talks7/user-03.md +105 -0
- package/docs/talks7/user-04.md +67 -0
- package/docs/talks8/conversa1.txt +1322 -0
- package/docs/talks8/conversa2.txt +237 -0
- package/docs/talks9/Beyond the Obvious_ 50 Plataformas Auto-Hospedadas de Forja de C/303/263digo para Al/303/251m de Gitea e GitLab.md" +174 -0
- package/docs/talks9/De NPM a Multi-Linguagem_ Uma Arquitetura T/303/251cnica para a Execu/303/247/303/243o Integrada de C/303/263digo no Ecossistema Node.js.md" +59 -0
- package/docs/talks9/De NPM a VMs Virtuais_ Uma An/303/241lise Arquitet/303/264nica para a Realiza/303/247/303/243o do Ciclo de Vida do Projeto SADDLE.md" +91 -0
- package/docs/talks9/Mapeamento da Engrenagem Computacional_ Uma Arquitetura para Execu/303/247/303/243o Isolada e Persist/303/252ncia em Ambientes Distribu/303/255dos.md" +116 -0
- package/docs/talks9/O Cen/303/241rio Pr/303/241tico do SADDLE_ Uma An/303/241lise de Viabilidade e Modelo de Ciclo de Vida Integrado.md" +128 -0
- package/docs/talks9/README (2).md +489 -0
- package/docs/talks9/README.md +198 -0
- package/docs/talks9/Viabilidade do Saddle_ Uma An/303/241lise T/303/251cnica da Transforma/303/247/303/243o de Armazenamento Remoto em Mem/303/263ria Computacional.md" +80 -0
- package/docs/talks9/conversa.txt +544 -0
- package/docs/talks9/other (2).md +39 -0
- package/docs/talks9/other.md +57 -0
- package/docs/talks9/outro.txt +24 -0
- package/extension/README.md +3 -3
- package/extension/serviceworker.js +63 -4
- package/extension/worker.js +3 -1
- package/package.json +12 -2
- package/packager/manifest.js +1 -1
- package/scrape/agent.ts +122 -0
- package/scrape/batch.ts +79 -0
- package/scrape/biome.json +76 -0
- package/scrape/browser.ts +222 -0
- package/scrape/cache.ts +84 -0
- package/scrape/chunking.ts +193 -0
- package/scrape/cli.ts +105 -0
- package/scrape/crawler.ts +115 -0
- package/scrape/dev-server.ts +94 -0
- package/scrape/errors.ts +132 -0
- package/scrape/events.ts +26 -0
- package/scrape/extract.ts +165 -0
- package/scrape/fetch.ts +105 -0
- package/scrape/formats.ts +85 -0
- package/scrape/headers.ts +71 -0
- package/scrape/index.ts +92 -0
- package/scrape/jsdom.d.ts +6 -0
- package/scrape/llms-txt.ts +84 -0
- package/scrape/middleware.ts +90 -0
- package/scrape/package-lock.json +9397 -0
- package/scrape/package.json +1420 -0
- package/scrape/pool.ts +95 -0
- package/scrape/port.ts +18 -0
- package/scrape/proxy.ts +103 -0
- package/scrape/rate-limiter.ts +95 -0
- package/scrape/renderer.ts +194 -0
- package/scrape/retry.ts +64 -0
- package/scrape/robots.ts +137 -0
- package/scrape/scrape.ts +123 -0
- package/scrape/serialize.ts +310 -0
- package/scrape/server.ts +137 -0
- package/scrape/session.ts +109 -0
- package/scrape/sitemap.ts +131 -0
- package/scrape/tokens.ts +45 -0
- package/scrape/tsconfig.json +28 -0
- package/scrape/types.ts +214 -0
- package/scrape/utils.ts +77 -0
- package/scrape/vite.config.ts +55 -0
- package/scrape/vitest.config.ts +17 -0
- package/workflow/templates.js +4 -4
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { chromium, firefox, webkit, type Browser, type BrowserContext, type Page } from 'playwright';
|
|
2
|
+
import type { BrowserAgentConfig, ChromeCommand, RenderOptions } from './types.js';
|
|
3
|
+
import { WebScrapeError, ErrorCode, BrowserNotAvailableError } from './errors.js';
|
|
4
|
+
|
|
5
|
+
export class AgentBrowser {
|
|
6
|
+
private browser: Browser | null = null;
|
|
7
|
+
private context: BrowserContext | null = null;
|
|
8
|
+
private page: Page | null = null;
|
|
9
|
+
private config: Required<Omit<BrowserAgentConfig, 'userAgent' | 'geolocation'>> & Pick<BrowserAgentConfig, 'userAgent' | 'geolocation'>;
|
|
10
|
+
|
|
11
|
+
private static DEFAULTS = {
|
|
12
|
+
headless: true,
|
|
13
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
14
|
+
viewport: { width: 1280, height: 720 },
|
|
15
|
+
locale: 'en-US',
|
|
16
|
+
timeout: 30000,
|
|
17
|
+
recordVideo: false,
|
|
18
|
+
proxy: '',
|
|
19
|
+
storageState: '',
|
|
20
|
+
blockAds: true,
|
|
21
|
+
stealth: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
constructor(config: Partial<BrowserAgentConfig> = {}) {
|
|
25
|
+
this.config = { ...AgentBrowser.DEFAULTS, ...config };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async launch(browserType: 'chromium' | 'firefox' | 'webkit' = 'chromium'): Promise<void> {
|
|
29
|
+
try {
|
|
30
|
+
const launcher = { chromium, firefox, webkit }[browserType];
|
|
31
|
+
const launchOpts: Parameters<typeof chromium.launch>[0] = {
|
|
32
|
+
headless: this.config.headless,
|
|
33
|
+
args: [
|
|
34
|
+
'--no-sandbox',
|
|
35
|
+
'--disable-setuid-sandbox',
|
|
36
|
+
'--disable-dev-shm-usage',
|
|
37
|
+
'--disable-web-security',
|
|
38
|
+
'--disable-features=IsolateOrigins,site-per-process',
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
if (this.config.proxy) {
|
|
42
|
+
launchOpts.proxy = { server: this.config.proxy };
|
|
43
|
+
}
|
|
44
|
+
this.browser = await launcher.launch(launchOpts);
|
|
45
|
+
|
|
46
|
+
const ctxOpts: Parameters<Browser['newContext']>[0] = {
|
|
47
|
+
viewport: this.config.viewport,
|
|
48
|
+
locale: this.config.locale,
|
|
49
|
+
userAgent: this.config.userAgent,
|
|
50
|
+
};
|
|
51
|
+
if (this.config.recordVideo) {
|
|
52
|
+
ctxOpts.recordVideo = { dir: './videos' };
|
|
53
|
+
}
|
|
54
|
+
if (this.config.geolocation) {
|
|
55
|
+
ctxOpts.geolocation = this.config.geolocation;
|
|
56
|
+
ctxOpts.permissions = ['geolocation'];
|
|
57
|
+
}
|
|
58
|
+
if (this.config.storageState) {
|
|
59
|
+
ctxOpts.storageState = this.config.storageState;
|
|
60
|
+
}
|
|
61
|
+
this.context = await this.browser.newContext(ctxOpts);
|
|
62
|
+
|
|
63
|
+
if (this.config.blockAds) {
|
|
64
|
+
await this.context.route('**/*.{png,jpg,jpeg,gif,svg,ico,css,woff,woff2,mp4,webm}', r => r.abort());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.page = await this.context.newPage();
|
|
68
|
+
this.page.setDefaultTimeout(this.config.timeout);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if ((error as Error).message?.includes('playwright') || (error as Error).message?.includes('chromium')) {
|
|
71
|
+
throw new BrowserNotAvailableError();
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async navigate(url: string, waitUntil: 'load' | 'domcontentloaded' | 'networkidle' = 'networkidle'): Promise<void> {
|
|
78
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
79
|
+
await this.page.goto(url, { waitUntil, timeout: this.config.timeout });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async screenshot(opts: RenderOptions = {}): Promise<Buffer> {
|
|
83
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
84
|
+
return this.page.screenshot({
|
|
85
|
+
fullPage: opts.fullPage ?? false,
|
|
86
|
+
type: opts.type ?? 'png',
|
|
87
|
+
quality: opts.quality ?? 80,
|
|
88
|
+
}) as Promise<Buffer>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async evaluate<T = unknown>(fn: string | (() => T), args?: unknown[]): Promise<T> {
|
|
92
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
93
|
+
if (typeof fn === 'string') {
|
|
94
|
+
return this.page.evaluate(new Function(fn) as () => T);
|
|
95
|
+
}
|
|
96
|
+
return this.page.evaluate(fn, args);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async click(selector: string): Promise<void> {
|
|
100
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
101
|
+
await this.page.click(selector);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async type(selector: string, text: string, delayMs = 0): Promise<void> {
|
|
105
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
106
|
+
await this.page.fill(selector, '');
|
|
107
|
+
await this.page.type(selector, text, { delay: delayMs });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async scrollToBottom(step = 800, delayMs = 300, maxScrolls = 50): Promise<number> {
|
|
111
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
112
|
+
let scrolls = 0;
|
|
113
|
+
let prevHeight = 0;
|
|
114
|
+
while (scrolls < maxScrolls) {
|
|
115
|
+
const height = await this.page.evaluate(() => document.body.scrollHeight);
|
|
116
|
+
if (height === prevHeight && scrolls > 2) break;
|
|
117
|
+
prevHeight = height;
|
|
118
|
+
await this.page.evaluate(y => window.scrollBy(0, y), step);
|
|
119
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
120
|
+
scrolls++;
|
|
121
|
+
}
|
|
122
|
+
return scrolls;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async scrollTo(x: number, y: number): Promise<void> {
|
|
126
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
127
|
+
await this.page.evaluate(([px, py]) => window.scrollTo(px, py), [x, y]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async waitFor(selector: string, timeout?: number): Promise<void> {
|
|
131
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
132
|
+
await this.page.waitForSelector(selector, { timeout: timeout ?? this.config.timeout });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async waitForLoad(): Promise<void> {
|
|
136
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
137
|
+
await this.page.waitForLoadState('networkidle');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async html(): Promise<string> {
|
|
141
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
142
|
+
return this.page.content();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async text(): Promise<string> {
|
|
146
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
147
|
+
return this.page.evaluate(() => document.body.innerText);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async title(): Promise<string> {
|
|
151
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
152
|
+
return this.page.title();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async url(): Promise<string> {
|
|
156
|
+
if (!this.page) throw new WebScrapeError('Browser not launched', ErrorCode.BROWSER_NOT_AVAILABLE, 500, false);
|
|
157
|
+
return this.page.url();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async executeCommands(commands: ChromeCommand[]): Promise<unknown[]> {
|
|
161
|
+
const results: unknown[] = [];
|
|
162
|
+
for (const cmd of commands) {
|
|
163
|
+
switch (cmd.action) {
|
|
164
|
+
case 'goto':
|
|
165
|
+
await this.navigate(cmd.args?.url as string);
|
|
166
|
+
results.push({ ok: true });
|
|
167
|
+
break;
|
|
168
|
+
case 'click':
|
|
169
|
+
await this.click(cmd.args?.selector as string);
|
|
170
|
+
results.push({ ok: true });
|
|
171
|
+
break;
|
|
172
|
+
case 'type':
|
|
173
|
+
await this.type(cmd.args?.selector as string, cmd.args?.text as string);
|
|
174
|
+
results.push({ ok: true });
|
|
175
|
+
break;
|
|
176
|
+
case 'screenshot':
|
|
177
|
+
results.push(await this.screenshot(cmd.args as RenderOptions));
|
|
178
|
+
break;
|
|
179
|
+
case 'evaluate':
|
|
180
|
+
results.push(await this.evaluate(cmd.args?.fn as string));
|
|
181
|
+
break;
|
|
182
|
+
case 'wait':
|
|
183
|
+
await this.waitFor(cmd.args?.selector as string);
|
|
184
|
+
results.push({ ok: true });
|
|
185
|
+
break;
|
|
186
|
+
case 'scroll':
|
|
187
|
+
if (cmd.args?.to) {
|
|
188
|
+
await this.scrollTo(0, cmd.args.to as number);
|
|
189
|
+
} else {
|
|
190
|
+
await this.scrollToBottom();
|
|
191
|
+
}
|
|
192
|
+
results.push({ ok: true });
|
|
193
|
+
break;
|
|
194
|
+
case 'extract':
|
|
195
|
+
results.push({
|
|
196
|
+
html: await this.html(),
|
|
197
|
+
text: await this.text(),
|
|
198
|
+
title: await this.title(),
|
|
199
|
+
});
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async close(): Promise<void> {
|
|
207
|
+
if (this.page) await this.page.close().catch(() => {});
|
|
208
|
+
if (this.context) await this.context.close().catch(() => {});
|
|
209
|
+
if (this.browser) await this.browser.close().catch(() => {});
|
|
210
|
+
this.page = null;
|
|
211
|
+
this.context = null;
|
|
212
|
+
this.browser = null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
isConnected(): boolean {
|
|
216
|
+
return this.browser?.isConnected() ?? false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function createBrowser(config?: BrowserAgentConfig): AgentBrowser {
|
|
221
|
+
return new AgentBrowser(config);
|
|
222
|
+
}
|
package/scrape/cache.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
|
|
3
|
+
export interface CacheEntry<T = unknown> {
|
|
4
|
+
value: T;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
expires?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CacheConfig {
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
defaultTtlMs?: number;
|
|
12
|
+
checkPeriodMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_CONFIG: Required<CacheConfig> = {
|
|
16
|
+
maxSize: 1000,
|
|
17
|
+
defaultTtlMs: 5 * 60 * 1000,
|
|
18
|
+
checkPeriodMs: 60 * 1000,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class WebScrapeCache {
|
|
22
|
+
private store: LRUCache<string, CacheEntry>;
|
|
23
|
+
|
|
24
|
+
constructor(config: Partial<CacheConfig> = {}) {
|
|
25
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
26
|
+
this.store = new LRUCache<string, CacheEntry>({
|
|
27
|
+
max: cfg.maxSize,
|
|
28
|
+
ttl: cfg.defaultTtlMs,
|
|
29
|
+
updateAgeOnGet: true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get<T = unknown>(key: string): T | undefined {
|
|
34
|
+
const entry = this.store.get(key);
|
|
35
|
+
if (!entry) return undefined;
|
|
36
|
+
if (entry.expires && Date.now() > entry.expires) {
|
|
37
|
+
this.store.delete(key);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return entry.value as T;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
set<T = unknown>(key: string, value: T, ttlMs?: number): void {
|
|
44
|
+
const entry: CacheEntry<T> = {
|
|
45
|
+
value,
|
|
46
|
+
timestamp: Date.now(),
|
|
47
|
+
expires: ttlMs ? Date.now() + ttlMs : undefined,
|
|
48
|
+
};
|
|
49
|
+
this.store.set(key, entry, { ttl: ttlMs });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
has(key: string): boolean {
|
|
53
|
+
return this.get(key) !== undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
delete(key: string): boolean {
|
|
57
|
+
return this.store.delete(key);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clear(): void {
|
|
61
|
+
this.store.clear();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getOrSet<T = unknown>(key: string, factory: () => Promise<T>, ttlMs?: number): Promise<T> {
|
|
65
|
+
const cached = this.get<T>(key);
|
|
66
|
+
if (cached !== undefined) return Promise.resolve(cached);
|
|
67
|
+
return factory().then(value => {
|
|
68
|
+
this.set(key, value, ttlMs);
|
|
69
|
+
return value;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
size(): number {
|
|
74
|
+
return this.store.size;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
keys(): string[] {
|
|
78
|
+
return [...this.store.keys()];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createCache(config?: Partial<CacheConfig>): WebScrapeCache {
|
|
83
|
+
return new WebScrapeCache(config);
|
|
84
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { estimateTokens } from './tokens.js';
|
|
2
|
+
|
|
3
|
+
export interface ChunkOptions {
|
|
4
|
+
maxTokens?: number;
|
|
5
|
+
overlapTokens?: number;
|
|
6
|
+
preserveCodeBlocks?: boolean;
|
|
7
|
+
includeHeadingPath?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Chunk {
|
|
11
|
+
content: string;
|
|
12
|
+
headingPath: string[];
|
|
13
|
+
tokenCount: number;
|
|
14
|
+
index: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_CHUNK_OPTIONS: Required<ChunkOptions> = {
|
|
18
|
+
maxTokens: 512,
|
|
19
|
+
overlapTokens: 50,
|
|
20
|
+
preserveCodeBlocks: true,
|
|
21
|
+
includeHeadingPath: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function splitByHeaders(markdown: string): { heading: string; level: number; content: string }[] {
|
|
25
|
+
const sections: { heading: string; level: number; content: string }[] = [];
|
|
26
|
+
const lines = markdown.split('\n');
|
|
27
|
+
let currentHeading = '';
|
|
28
|
+
let currentLevel = 0;
|
|
29
|
+
let currentContent: string[] = [];
|
|
30
|
+
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
const headerMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
33
|
+
if (headerMatch) {
|
|
34
|
+
if (currentContent.length > 0 || currentHeading) {
|
|
35
|
+
sections.push({
|
|
36
|
+
heading: currentHeading,
|
|
37
|
+
level: currentLevel,
|
|
38
|
+
content: currentContent.join('\n').trim(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
currentLevel = headerMatch[1].length;
|
|
42
|
+
currentHeading = headerMatch[2];
|
|
43
|
+
currentContent = [];
|
|
44
|
+
} else {
|
|
45
|
+
currentContent.push(line);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (currentContent.length > 0 || currentHeading) {
|
|
50
|
+
sections.push({
|
|
51
|
+
heading: currentHeading,
|
|
52
|
+
level: currentLevel,
|
|
53
|
+
content: currentContent.join('\n').trim(),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return sections;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function splitByParagraphs(text: string): string[] {
|
|
61
|
+
return text.split(/\n\n+/).filter(p => p.trim().length > 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function splitBySentences(text: string): string[] {
|
|
65
|
+
return text.match(/[^.!?]+[.!?]+/g) || [text];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function chunkMarkdown(markdown: string, options: ChunkOptions = {}): Chunk[] {
|
|
69
|
+
const opts = { ...DEFAULT_CHUNK_OPTIONS, ...options };
|
|
70
|
+
const chunks: Chunk[] = [];
|
|
71
|
+
const sections = splitByHeaders(markdown);
|
|
72
|
+
const headingPath: string[] = [];
|
|
73
|
+
let headingDepth = 0;
|
|
74
|
+
|
|
75
|
+
for (const section of sections) {
|
|
76
|
+
while (headingDepth > 0 && headingDepth >= section.level) {
|
|
77
|
+
headingDepth--;
|
|
78
|
+
headingPath.pop();
|
|
79
|
+
}
|
|
80
|
+
if (section.heading) {
|
|
81
|
+
headingDepth = section.level;
|
|
82
|
+
headingPath.push(section.heading);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const sectionText = section.heading ? `#${'#'.repeat(section.level - 1)} ${section.heading}\n\n${section.content}` : section.content;
|
|
86
|
+
|
|
87
|
+
if (estimateTokens(sectionText) <= opts.maxTokens) {
|
|
88
|
+
chunks.push({
|
|
89
|
+
content: sectionText,
|
|
90
|
+
headingPath: opts.includeHeadingPath ? [...headingPath] : [],
|
|
91
|
+
tokenCount: estimateTokens(sectionText),
|
|
92
|
+
index: chunks.length,
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
const paragraphs = splitByParagraphs(sectionText);
|
|
96
|
+
let currentChunk = '';
|
|
97
|
+
|
|
98
|
+
for (const paragraph of paragraphs) {
|
|
99
|
+
const testChunk = currentChunk ? `${currentChunk}\n\n${paragraph}` : paragraph;
|
|
100
|
+
|
|
101
|
+
if (estimateTokens(testChunk) <= opts.maxTokens) {
|
|
102
|
+
currentChunk = testChunk;
|
|
103
|
+
} else {
|
|
104
|
+
if (currentChunk) {
|
|
105
|
+
chunks.push({
|
|
106
|
+
content: currentChunk,
|
|
107
|
+
headingPath: opts.includeHeadingPath ? [...headingPath] : [],
|
|
108
|
+
tokenCount: estimateTokens(currentChunk),
|
|
109
|
+
index: chunks.length,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (estimateTokens(paragraph) > opts.maxTokens) {
|
|
114
|
+
const sentences = splitBySentences(paragraph);
|
|
115
|
+
let sentenceChunk = '';
|
|
116
|
+
for (const sentence of sentences) {
|
|
117
|
+
const testSentence = sentenceChunk ? `${sentenceChunk} ${sentence}` : sentence;
|
|
118
|
+
if (estimateTokens(testSentence) <= opts.maxTokens) {
|
|
119
|
+
sentenceChunk = testSentence;
|
|
120
|
+
} else {
|
|
121
|
+
if (sentenceChunk) {
|
|
122
|
+
chunks.push({
|
|
123
|
+
content: sentenceChunk,
|
|
124
|
+
headingPath: opts.includeHeadingPath ? [...headingPath] : [],
|
|
125
|
+
tokenCount: estimateTokens(sentenceChunk),
|
|
126
|
+
index: chunks.length,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
sentenceChunk = sentence;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
currentChunk = sentenceChunk;
|
|
133
|
+
} else {
|
|
134
|
+
currentChunk = paragraph;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (currentChunk) {
|
|
140
|
+
chunks.push({
|
|
141
|
+
content: currentChunk,
|
|
142
|
+
headingPath: opts.includeHeadingPath ? [...headingPath] : [],
|
|
143
|
+
tokenCount: estimateTokens(currentChunk),
|
|
144
|
+
index: chunks.length,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return chunks;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function chunkText(text: string, options: ChunkOptions = {}): Chunk[] {
|
|
154
|
+
const opts = { ...DEFAULT_CHUNK_OPTIONS, ...options };
|
|
155
|
+
const chunks: Chunk[] = [];
|
|
156
|
+
const paragraphs = splitByParagraphs(text);
|
|
157
|
+
let currentChunk = '';
|
|
158
|
+
|
|
159
|
+
for (const paragraph of paragraphs) {
|
|
160
|
+
const testChunk = currentChunk ? `${currentChunk}\n\n${paragraph}` : paragraph;
|
|
161
|
+
if (estimateTokens(testChunk) <= opts.maxTokens) {
|
|
162
|
+
currentChunk = testChunk;
|
|
163
|
+
} else {
|
|
164
|
+
if (currentChunk) {
|
|
165
|
+
chunks.push({
|
|
166
|
+
content: currentChunk,
|
|
167
|
+
headingPath: [],
|
|
168
|
+
tokenCount: estimateTokens(currentChunk),
|
|
169
|
+
index: chunks.length,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
currentChunk = paragraph;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (currentChunk) {
|
|
177
|
+
chunks.push({
|
|
178
|
+
content: currentChunk,
|
|
179
|
+
headingPath: [],
|
|
180
|
+
tokenCount: estimateTokens(currentChunk),
|
|
181
|
+
index: chunks.length,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return chunks;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function formatChunksForRAG(chunks: Chunk[]): string {
|
|
189
|
+
return chunks.map(chunk => {
|
|
190
|
+
const heading = chunk.headingPath.length > 0 ? `## ${chunk.headingPath.join(' > ')}\n\n` : '';
|
|
191
|
+
return `${heading}${chunk.content}`;
|
|
192
|
+
}).join('\n\n---\n\n');
|
|
193
|
+
}
|
package/scrape/cli.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { scrapeUrl, scrapeHtml } from './scrape.js';
|
|
6
|
+
import { serializeResult } from './serialize.js';
|
|
7
|
+
import { resolveFormat, extensionForFormat, buildSerializeOptions } from './formats.js';
|
|
8
|
+
import { formatForAgent } from './agent.js';
|
|
9
|
+
import { writeOutput } from './utils.js';
|
|
10
|
+
import type { ScrapeOptions, ScrapeResult } from './types.js';
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('webscrape')
|
|
16
|
+
.description('DevThink WebScrape — Universal AI-powered web scraping toolkit')
|
|
17
|
+
.version('2.0.0');
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.argument('[url]', 'URL to scrape')
|
|
21
|
+
.option('-f, --format <type>', 'Output format: markdown, html, text, json, xml, redis', 'markdown')
|
|
22
|
+
.option('-o, --output <file>', 'Write output to file')
|
|
23
|
+
.option('--file <path>', 'Scrape HTML from local file')
|
|
24
|
+
.option('--mode <mode>', 'Scraping mode: auto, fetch, browser', 'auto')
|
|
25
|
+
.option('--no-headless', 'Show browser window')
|
|
26
|
+
.option('--timeout <ms>', 'Navigation timeout', '30000')
|
|
27
|
+
.option('--scroll', 'Scroll page to load dynamic content')
|
|
28
|
+
.option('--readable', 'Extract readable content only')
|
|
29
|
+
.option('--agent', 'Format output for AI agent consumption')
|
|
30
|
+
.option('--pretty', 'Pretty-print JSON output')
|
|
31
|
+
.option('--proxy <url>', 'Proxy server URL')
|
|
32
|
+
.option('--retries <n>', 'Number of retries', '3')
|
|
33
|
+
.option('--user-agent <ua>', 'Custom User-Agent string')
|
|
34
|
+
.action(async (url, options) => {
|
|
35
|
+
if (!url && !options.file) {
|
|
36
|
+
program.help();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const format = resolveFormat(options.format);
|
|
40
|
+
const scrapeOpts: ScrapeOptions = {
|
|
41
|
+
format,
|
|
42
|
+
timeout: parseInt(options.timeout),
|
|
43
|
+
scroll: options.scroll,
|
|
44
|
+
readable: options.readable,
|
|
45
|
+
mode: options.mode,
|
|
46
|
+
proxy: options.proxy,
|
|
47
|
+
retries: parseInt(options.retries),
|
|
48
|
+
userAgent: options.userAgent,
|
|
49
|
+
extractLinks: true,
|
|
50
|
+
extractImages: true,
|
|
51
|
+
extractTables: true,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (options.readable) {
|
|
55
|
+
scrapeOpts.removeSelectors = [
|
|
56
|
+
'script', 'style', 'nav', 'footer', 'header',
|
|
57
|
+
'.sidebar', '.advertisement', '.ads', '.menu',
|
|
58
|
+
'.comments', '.comment', '#comments',
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.error(`Scraping: ${url || options.file}`);
|
|
63
|
+
|
|
64
|
+
let result: ScrapeResult;
|
|
65
|
+
|
|
66
|
+
if (options.file) {
|
|
67
|
+
const filePath = resolve(options.file);
|
|
68
|
+
if (!existsSync(filePath)) {
|
|
69
|
+
console.error(`File not found: ${filePath}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const html = readFileSync(filePath, 'utf-8');
|
|
73
|
+
result = await scrapeHtml(html, scrapeOpts);
|
|
74
|
+
} else {
|
|
75
|
+
result = await scrapeUrl(url, scrapeOpts);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.error(`Done in ${result.duration}ms (${(result.size / 1024).toFixed(1)} KB)`);
|
|
79
|
+
|
|
80
|
+
if (options.agent) {
|
|
81
|
+
const agentOutput = formatForAgent(result);
|
|
82
|
+
const output = JSON.stringify(agentOutput, null, 2);
|
|
83
|
+
|
|
84
|
+
if (options.output) {
|
|
85
|
+
writeOutput(options.output, output);
|
|
86
|
+
console.error(`Saved to ${options.output}`);
|
|
87
|
+
} else {
|
|
88
|
+
console.log(output);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
const serializeOpts = buildSerializeOptions(format, options.pretty);
|
|
92
|
+
const serialized = serializeResult(result, serializeOpts);
|
|
93
|
+
|
|
94
|
+
if (options.output) {
|
|
95
|
+
const ext = extensionForFormat(format);
|
|
96
|
+
const finalPath = options.output.endsWith(ext) ? options.output : options.output + ext;
|
|
97
|
+
writeOutput(finalPath, serialized.content);
|
|
98
|
+
console.error(`Saved to ${finalPath}`);
|
|
99
|
+
} else {
|
|
100
|
+
console.log(serialized.content);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
program.parse();
|