@wenathlan/saddle 1.8.4 → 1.8.6
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 +10 -10
- package/browser/playwright.js +22 -0
- package/docs/.gitkeep +0 -0
- package/docs/actionsincident.md +29 -0
- package/docs/branchaudit.md +21 -0
- package/docs/ecosystemplan.md +3 -3
- package/docs/featureaudit.md +1 -1
- package/docs/gapmatrix.md +8 -7
- 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 +6 -2
- package/docs/registryresearch.md +2 -0
- package/docs/release.md +4 -4
- package/docs/release184notes.md +2 -2
- package/docs/release185notes.md +7 -0
- package/docs/releaseassets.md +16 -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 +2 -2
- package/extension/build.js +1 -1
- package/extension/content.js +46 -4
- package/extension/manifest.json +1 -1
- package/extension/pagebridge.js +41 -0
- package/extension/protocol.js +21 -2
- package/extension/serviceworker.js +12 -5
- package/package.json +165 -10
- package/release/assets.js +70 -0
- 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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* release assets create deterministic checksums, SBOM data and provenance statements.
|
|
3
|
+
* The adapter reads caller-selected artifacts and never publishes or handles credentials.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const rootpath = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
|
|
13
|
+
/** Builds deterministic release metadata files for caller-selected artifacts. */
|
|
14
|
+
export async function createassets(options = {}) {
|
|
15
|
+
const packagefile = resolve(options.packagefile ?? join(rootpath, "package.json"));
|
|
16
|
+
const lockfile = resolve(options.lockfile ?? join(rootpath, "package-lock.json"));
|
|
17
|
+
const output = resolve(options.output ?? join(rootpath, "build", "release"));
|
|
18
|
+
const artifactroot = resolve(options.artifactroot ?? rootpath);
|
|
19
|
+
const packagejson = JSON.parse(await readFile(packagefile, "utf8"));
|
|
20
|
+
const lockjson = JSON.parse(await readFile(lockfile, "utf8"));
|
|
21
|
+
const artifacts = [...new Set((options.artifacts ?? []).map((artifact) => resolve(String(artifact))))].sort();
|
|
22
|
+
const subjects = await Promise.all(artifacts.map(async (artifact) => ({ name: relative(artifactroot, artifact).replaceAll("\\", "/"), digest: await sha256(artifact) })));
|
|
23
|
+
await mkdir(output, { recursive: true });
|
|
24
|
+
const checksums = `${subjects.map((subject) => `${subject.digest} ${subject.name}`).join("\n")}${subjects.length ? "\n" : ""}`;
|
|
25
|
+
const sbom = createsbom(packagejson, lockjson);
|
|
26
|
+
const provenance = createprovenance(packagejson, subjects, options);
|
|
27
|
+
const files = { checksums: join(output, "SHA256SUMS"), sbom: join(output, "sbom.cdx.json"), provenance: join(output, "provenance.intoto.jsonl") };
|
|
28
|
+
await writeFile(files.checksums, checksums);
|
|
29
|
+
await writeFile(files.sbom, `${JSON.stringify(sbom, null, 2)}\n`);
|
|
30
|
+
await writeFile(files.provenance, `${JSON.stringify(provenance)}\n`);
|
|
31
|
+
return { output, files, subjects, sbom, provenance };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Creates a compact CycloneDX component list from the root lockfile dependencies. */
|
|
35
|
+
export function createsbom(packagejson, lockjson = {}) {
|
|
36
|
+
const root = lockjson.packages?.[""] ?? {};
|
|
37
|
+
const dependencies = { ...(root.dependencies ?? {}), ...(root.devDependencies ?? {}) };
|
|
38
|
+
const components = Object.keys(dependencies).map((name) => {
|
|
39
|
+
const entry = lockjson.packages?.[`node_modules/${name}`] ?? {};
|
|
40
|
+
const version = String(entry.version ?? dependencies[name]).replace(/^[^0-9]*/, "");
|
|
41
|
+
return { "bom-ref": `pkg:npm/${name}@${version}`, name, version, purl: `pkg:npm/${name}@${version}`, scope: root.devDependencies?.[name] ? "optional" : "required", type: "library" };
|
|
42
|
+
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
43
|
+
return { bomFormat: "CycloneDX", specVersion: "1.5", serialNumber: `urn:uuid:${stableuuid(`${packagejson.name}@${packagejson.version}`)}`, version: 1, metadata: { component: { type: "application", name: String(packagejson.name), version: String(packagejson.version) } }, components };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Creates an in-toto statement whose subjects are the caller-selected release artifacts. */
|
|
47
|
+
export function createprovenance(packagejson, subjects = [], options = {}) {
|
|
48
|
+
const normalized = subjects.map((subject) => ({ name: String(subject.name), digest: { sha256: String(subject.digest) } })).sort((left, right) => left.name.localeCompare(right.name));
|
|
49
|
+
return { _type: "https://in-toto.io/Statement/v1", subject: normalized, predicateType: "https://slsa.dev/provenance/v1", predicate: { buildDefinition: { buildType: String(options.buildtype ?? "caller-defined"), externalParameters: { package: String(packagejson.name), version: String(options.version ?? packagejson.version) }, internalParameters: {} }, runDetails: { builder: { id: String(options.builder ?? "caller-defined") }, metadata: { invocationId: stableuuid(`${packagejson.name}@${options.version ?? packagejson.version}:${normalized.map((subject) => subject.name).join(",")}`) } } } };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function sha256(path) { const hash = createHash("sha256"); hash.update(await readFile(path)); return hash.digest("hex"); }
|
|
53
|
+
|
|
54
|
+
function stableuuid(value) { const hex = createHash("sha256").update(String(value)).digest("hex").slice(0, 32); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${(8 + (Number.parseInt(hex.slice(16, 17), 16) % 4).toString(16))}${hex.slice(17, 20)}-${hex.slice(20)}`; }
|
|
55
|
+
|
|
56
|
+
function parsearguments(argumentslist) {
|
|
57
|
+
const options = { artifacts: [] };
|
|
58
|
+
for (let index = 0; index < argumentslist.length; index += 1) {
|
|
59
|
+
const argument = argumentslist[index];
|
|
60
|
+
if (argument === "--output") options.output = argumentslist[++index];
|
|
61
|
+
else if (argument === "--version") options.version = argumentslist[++index];
|
|
62
|
+
else if (argument === "--artifact") options.artifacts.push(argumentslist[++index]);
|
|
63
|
+
else if (argument === "--build-type") options.buildtype = argumentslist[++index];
|
|
64
|
+
else if (argument === "--builder") options.builder = argumentslist[++index];
|
|
65
|
+
else throw new TypeError(`unsupported release asset argument: ${argument}`);
|
|
66
|
+
}
|
|
67
|
+
return options;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) createassets(parsearguments(process.argv.slice(2))).then(({ output }) => { console.log(`release assets: ${output}`); }).catch((error) => { console.error(error.message); process.exitCode = 1; });
|
package/scrape/agent.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ScrapeResult, AgentOutput } from './types.js';
|
|
2
|
+
import { estimateTokens } from './tokens.js';
|
|
3
|
+
import { chunkMarkdown } from './chunking.js';
|
|
4
|
+
|
|
5
|
+
export interface AgentFormatOptions {
|
|
6
|
+
includeSummary?: boolean;
|
|
7
|
+
includeKeyPoints?: boolean;
|
|
8
|
+
includeUrls?: boolean;
|
|
9
|
+
includeRaw?: boolean;
|
|
10
|
+
maxTokens?: number;
|
|
11
|
+
chunkSize?: number;
|
|
12
|
+
model?: 'gpt-4o' | 'gpt-4' | 'gpt-3.5-turbo' | 'claude' | 'gemini' | 'default';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_OPTIONS: Required<AgentFormatOptions> = {
|
|
16
|
+
includeSummary: true,
|
|
17
|
+
includeKeyPoints: true,
|
|
18
|
+
includeUrls: true,
|
|
19
|
+
includeRaw: true,
|
|
20
|
+
maxTokens: 8000,
|
|
21
|
+
chunkSize: 512,
|
|
22
|
+
model: 'default',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function generateSummary(result: ScrapeResult): string {
|
|
26
|
+
const wordCount = result.text.split(/\s+/).length;
|
|
27
|
+
return `This page titled "${result.title}" contains approximately ${wordCount} words with ${result.links.length} links and ${result.images.length} images.`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function generateKeyPoints(result: ScrapeResult): string[] {
|
|
31
|
+
const points: string[] = [];
|
|
32
|
+
const text = result.text.slice(0, 5000);
|
|
33
|
+
|
|
34
|
+
if (result.metadata.author) points.push(`Author: ${result.metadata.author}`);
|
|
35
|
+
if (result.metadata.publishedDate) points.push(`Published: ${result.metadata.publishedDate}`);
|
|
36
|
+
if (result.metadata.description) points.push(`Description: ${result.metadata.description.slice(0, 200)}`);
|
|
37
|
+
|
|
38
|
+
const sentences = text.match(/[^.!?]+[.!?]+/g) || [];
|
|
39
|
+
for (const s of sentences.slice(0, 5)) {
|
|
40
|
+
const trimmed = s.trim();
|
|
41
|
+
if (trimmed.length > 30) points.push(trimmed);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return points.slice(0, 8);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildSystemPrompt(result: ScrapeResult): string {
|
|
48
|
+
let prompt = `# Web Page Content\n\n`;
|
|
49
|
+
prompt += `## Title\n${result.title}\n\n`;
|
|
50
|
+
prompt += `## URL\n${result.url}\n\n`;
|
|
51
|
+
|
|
52
|
+
if (result.metadata.description) {
|
|
53
|
+
prompt += `## Description\n${result.metadata.description}\n\n`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (result.tables.length > 0) {
|
|
57
|
+
prompt += `## Tables (${result.tables.length} found)\n`;
|
|
58
|
+
for (const t of result.tables.slice(0, 3)) {
|
|
59
|
+
prompt += `- ${t.caption || 'Unnamed table'}: ${t.headers.join(', ')}\n`;
|
|
60
|
+
}
|
|
61
|
+
prompt += '\n';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
prompt += `## Content\n${result.content}\n\n`;
|
|
65
|
+
return prompt;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function formatForAgent(result: ScrapeResult, options?: AgentFormatOptions): AgentOutput {
|
|
69
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
70
|
+
let content = '';
|
|
71
|
+
|
|
72
|
+
if (opts.includeSummary) {
|
|
73
|
+
content += `## Summary\n${generateSummary(result)}\n\n`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (opts.includeKeyPoints) {
|
|
77
|
+
const points = generateKeyPoints(result);
|
|
78
|
+
content += `## Key Points\n${points.map(p => `- ${p}`).join('\n')}\n\n`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (opts.includeUrls) {
|
|
82
|
+
const externalUrls = result.links.filter(l => l.isExternal).slice(0, 15);
|
|
83
|
+
if (externalUrls.length) {
|
|
84
|
+
content += `## External Links\n${externalUrls.map(l => `- [${l.text || l.href}](${l.href})`).join('\n')}\n\n`;
|
|
85
|
+
}
|
|
86
|
+
const internalUrls = result.links.filter(l => l.isInternal).slice(0, 10);
|
|
87
|
+
if (internalUrls.length) {
|
|
88
|
+
content += `## Internal Links\n${internalUrls.map(l => `- [${l.text || l.href}](${l.href})`).join('\n')}\n\n`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (opts.includeRaw) {
|
|
93
|
+
content += `## Full Content\n${result.content}\n\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const totalTokens = estimateTokens(content, opts.model);
|
|
97
|
+
|
|
98
|
+
if (totalTokens > opts.maxTokens) {
|
|
99
|
+
const charLimit = Math.floor(opts.maxTokens * 4);
|
|
100
|
+
content = content.slice(0, charLimit) + '\n\n... (truncated)';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const chunks = chunkMarkdown(content, { maxTokens: opts.chunkSize });
|
|
104
|
+
|
|
105
|
+
const output: AgentOutput = {
|
|
106
|
+
summary: generateSummary(result),
|
|
107
|
+
content,
|
|
108
|
+
keyPoints: generateKeyPoints(result),
|
|
109
|
+
relevantUrls: result.links.filter(l => l.isExternal).map(l => l.href).slice(0, 10),
|
|
110
|
+
tokens: estimateTokens(content, opts.model),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (chunks.length > 1) {
|
|
114
|
+
(output as unknown as Record<string, unknown>).chunks = chunks.map(c => c.content);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return output;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildContext(result: ScrapeResult): string {
|
|
121
|
+
return buildSystemPrompt(result);
|
|
122
|
+
}
|
package/scrape/batch.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import pLimit from 'p-limit';
|
|
2
|
+
import type { ScrapeOptions, ScrapeResult, BatchOptions } from './types.js';
|
|
3
|
+
import { scrapeUrl } from './scrape.js';
|
|
4
|
+
|
|
5
|
+
export interface BatchResult {
|
|
6
|
+
results: ScrapeResult[];
|
|
7
|
+
errors: { url: string; error: Error }[];
|
|
8
|
+
successful: number;
|
|
9
|
+
failed: number;
|
|
10
|
+
duration: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function batchScrape(options: BatchOptions, scrapeOpts: ScrapeOptions = {}): Promise<BatchResult> {
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
const limit = pLimit(options.concurrency || 5);
|
|
16
|
+
const results: ScrapeResult[] = [];
|
|
17
|
+
const errors: { url: string; error: Error }[] = [];
|
|
18
|
+
|
|
19
|
+
const tasks = options.urls.map((url) =>
|
|
20
|
+
limit(async () => {
|
|
21
|
+
try {
|
|
22
|
+
const result = await scrapeUrl(url, { ...scrapeOpts, mode: scrapeOpts.mode || 'auto' });
|
|
23
|
+
results.push(result);
|
|
24
|
+
options.onProgress?.(results.length + errors.length, options.urls.length, url);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
errors.push({ url, error: error as Error });
|
|
27
|
+
options.onError?.(url, error as Error);
|
|
28
|
+
options.onProgress?.(results.length + errors.length, options.urls.length, url);
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
await Promise.allSettled(tasks);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
results,
|
|
37
|
+
errors,
|
|
38
|
+
successful: results.length,
|
|
39
|
+
failed: errors.length,
|
|
40
|
+
duration: Date.now() - startTime,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function batchScrapeSequential(
|
|
45
|
+
urls: string[],
|
|
46
|
+
options: ScrapeOptions = {},
|
|
47
|
+
callbacks?: {
|
|
48
|
+
onResult?: (result: ScrapeResult, index: number) => void;
|
|
49
|
+
onError?: (url: string, error: Error, index: number) => void;
|
|
50
|
+
delayMs?: number;
|
|
51
|
+
}
|
|
52
|
+
): Promise<BatchResult> {
|
|
53
|
+
const startTime = Date.now();
|
|
54
|
+
const results: ScrapeResult[] = [];
|
|
55
|
+
const errors: { url: string; error: Error }[] = [];
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < urls.length; i++) {
|
|
58
|
+
try {
|
|
59
|
+
const result = await scrapeUrl(urls[i], options);
|
|
60
|
+
results.push(result);
|
|
61
|
+
callbacks?.onResult?.(result, i);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
errors.push({ url: urls[i], error: error as Error });
|
|
64
|
+
callbacks?.onError?.(urls[i], error as Error, i);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (callbacks?.delayMs && i < urls.length - 1) {
|
|
68
|
+
await new Promise(r => setTimeout(r, callbacks.delayMs));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
results,
|
|
74
|
+
errors,
|
|
75
|
+
successful: results.length,
|
|
76
|
+
failed: errors.length,
|
|
77
|
+
duration: Date.now() - startTime,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
|
|
3
|
+
"formatter": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"indentStyle": "tab",
|
|
6
|
+
"indentWidth": 2,
|
|
7
|
+
"lineWidth": 100,
|
|
8
|
+
"lineEnding": "lf",
|
|
9
|
+
"formatWithErrors": false
|
|
10
|
+
},
|
|
11
|
+
"linter": {
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"rules": {
|
|
14
|
+
"recommended": true,
|
|
15
|
+
"a11y": {
|
|
16
|
+
"noStaticElementInteractions": "off",
|
|
17
|
+
"useKeyWithClickEvents": "off",
|
|
18
|
+
"useSemanticElements": "off"
|
|
19
|
+
},
|
|
20
|
+
"correctness": {
|
|
21
|
+
"useExhaustiveDependencies": "off"
|
|
22
|
+
},
|
|
23
|
+
"style": {
|
|
24
|
+
"noNonNullAssertion": "off",
|
|
25
|
+
"useNodejsImportProtocol": "off",
|
|
26
|
+
"useConst": "error"
|
|
27
|
+
},
|
|
28
|
+
"suspicious": {
|
|
29
|
+
"noExplicitAny": "off",
|
|
30
|
+
"noEmptyBlock": "warn"
|
|
31
|
+
},
|
|
32
|
+
"complexity": {
|
|
33
|
+
"noUselessFragments": "warn",
|
|
34
|
+
"noForEach": "off"
|
|
35
|
+
},
|
|
36
|
+
"performance": {
|
|
37
|
+
"noDelete": "off"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"assist": {
|
|
42
|
+
"enabled": true,
|
|
43
|
+
"actions": {
|
|
44
|
+
"source": {
|
|
45
|
+
"organizeImports": "on"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"javascript": {
|
|
50
|
+
"formatter": {
|
|
51
|
+
"quoteStyle": "double",
|
|
52
|
+
"semicolons": "always",
|
|
53
|
+
"trailingCommas": "es5",
|
|
54
|
+
"arrowParentheses": "always"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"css": {
|
|
58
|
+
"parser": {
|
|
59
|
+
"cssModules": true,
|
|
60
|
+
"allowWrongLineComments": true
|
|
61
|
+
},
|
|
62
|
+
"formatter": {
|
|
63
|
+
"enabled": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"files": {
|
|
67
|
+
"includes": ["*.ts", "web/**", "tests/**"],
|
|
68
|
+
"ignore": ["node_modules", "dist", "*.min.*"],
|
|
69
|
+
"ignoreUnknown": true
|
|
70
|
+
},
|
|
71
|
+
"vcs": {
|
|
72
|
+
"enabled": true,
|
|
73
|
+
"clientKind": "git",
|
|
74
|
+
"useIgnoreFile": true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -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
|
+
}
|