polite-crawler 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -121
- package/build/{browser-CKRNbIDT.mjs → browser-56dBBLec.mjs} +7 -3
- package/build/browser-56dBBLec.mjs.map +1 -0
- package/build/classify.d.mts +50 -2
- package/build/{cli-daemon--cTmq0Z5.mjs → cli-daemon-qS4PH2bM.mjs} +3 -3
- package/build/{cli-daemon--cTmq0Z5.mjs.map → cli-daemon-qS4PH2bM.mjs.map} +1 -1
- package/build/cli-recipes-qbLRVThF.mjs +59 -0
- package/build/cli-recipes-qbLRVThF.mjs.map +1 -0
- package/build/cli.mjs +431 -27
- package/build/cli.mjs.map +1 -1
- package/build/{daemon-fZTE24P5.mjs → daemon-BXBbUrni.mjs} +11 -7
- package/build/daemon-BXBbUrni.mjs.map +1 -0
- package/build/{diff-NQcQqKe9.mjs → diff-CpKN0Jdc.mjs} +7 -3
- package/build/{diff-NQcQqKe9.mjs.map → diff-CpKN0Jdc.mjs.map} +1 -1
- package/build/{extract-ByFfc8Zk.mjs → extract-DqeqiNHb.mjs} +12 -4
- package/build/extract-DqeqiNHb.mjs.map +1 -0
- package/build/{html-BACljlkf.d.mts → html-DMgIeQfJ.d.mts} +16 -2
- package/build/html.d.mts +1 -2
- package/build/index.d.mts +4227 -40
- package/build/index.mjs +17 -901
- package/build/{log-Dx-wXjJl.mjs → log-CmBWcyQm.mjs} +5 -1
- package/build/log-CmBWcyQm.mjs.map +1 -0
- package/build/mcp-package-Y0bREnOS.mjs +40 -0
- package/build/mcp-package-Y0bREnOS.mjs.map +1 -0
- package/build/{ollama-BJxkjny4.mjs → ollama-D1xRh3hY.mjs} +7 -3
- package/build/ollama-D1xRh3hY.mjs.map +1 -0
- package/build/{open-C4fhXJgD.mjs → open-CEo22Hpu.mjs} +84 -296
- package/build/open-CEo22Hpu.mjs.map +1 -0
- package/build/politeness.d.mts +366 -2
- package/build/recipe-B2SkepDZ.mjs +285 -0
- package/build/recipe-B2SkepDZ.mjs.map +1 -0
- package/build/recipe-product-CyZE3Ukd.mjs +914 -0
- package/build/recipe-product-CyZE3Ukd.mjs.map +1 -0
- package/build/{render-C1CUraWF.mjs → render-CKE2LILH.mjs} +10 -3
- package/build/render-CKE2LILH.mjs.map +1 -0
- package/build/robots.d.mts +143 -2
- package/build/robots.mjs +1 -1
- package/build/robots.mjs.map +1 -1
- package/build/{rolldown-runtime-D7D4PA-g.mjs → rolldown-runtime-8H4AJuhK.mjs} +1 -0
- package/build/{runtime-JToRO63o.mjs → runtime-C_2BQibH.mjs} +84 -18
- package/build/runtime-C_2BQibH.mjs.map +1 -0
- package/build/signals.d.mts +45 -2
- package/build/{transformers-BQMlaGj4.mjs → transformers-BNiuiLxT.mjs} +3 -3
- package/build/transformers-BNiuiLxT.mjs.map +1 -0
- package/build/{version-ntbY-6wh.mjs → version-Djva7JkN.mjs} +7 -2
- package/build/version-Djva7JkN.mjs.map +1 -0
- package/build/{wacz-BFNG42Kt.mjs → wacz-DKk2HBmx.mjs} +4 -4
- package/build/{wacz-BFNG42Kt.mjs.map → wacz-DKk2HBmx.mjs.map} +1 -1
- package/build/{warc-CVS0t1fW.mjs → warc-C_ew76bV.mjs} +4 -4
- package/build/{warc-CVS0t1fW.mjs.map → warc-C_ew76bV.mjs.map} +1 -1
- package/build/{webbotauth-CjJdTAfE.mjs → webbotauth-Dq_dA_u3.mjs} +2 -2
- package/build/webbotauth-Dq_dA_u3.mjs.map +1 -0
- package/package.json +5 -11
- package/build/browser-CKRNbIDT.mjs.map +0 -1
- package/build/classify-CrfyXcwo.d.mts +0 -51
- package/build/daemon-DP4EqjFL.d.mts +0 -3772
- package/build/daemon-fZTE24P5.mjs.map +0 -1
- package/build/extract-ByFfc8Zk.mjs.map +0 -1
- package/build/index.mjs.map +0 -1
- package/build/log-Dx-wXjJl.mjs.map +0 -1
- package/build/loopback-C_QYowSf.mjs +0 -22
- package/build/loopback-C_QYowSf.mjs.map +0 -1
- package/build/mcp.d.mts +0 -199
- package/build/mcp.mjs +0 -1555
- package/build/mcp.mjs.map +0 -1
- package/build/ollama-BJxkjny4.mjs.map +0 -1
- package/build/open-C4fhXJgD.mjs.map +0 -1
- package/build/politeness-yI3xBEx9.d.mts +0 -367
- package/build/render-C1CUraWF.mjs.map +0 -1
- package/build/robots-R-qaEBOb.d.mts +0 -144
- package/build/runtime-JToRO63o.mjs.map +0 -1
- package/build/signals-BHpy2xb2.d.mts +0 -46
- package/build/transformers-BQMlaGj4.mjs.map +0 -1
- package/build/urls-BTDSgDLC.d.mts +0 -16
- package/build/version-ntbY-6wh.mjs.map +0 -1
- package/build/webbotauth-CjJdTAfE.mjs.map +0 -1
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-
|
|
2
|
-
import { n as FileBlobStore, t as CaptureLog } from "./log-
|
|
3
|
-
import { r as playwrightRenderer } from "./render-
|
|
4
|
-
import { r as checkedRenderer } from "./browser-
|
|
5
|
-
import { i as durationMs, n as DEFAULT_TOKEN, r as VERSION, t as BOT_PAGE } from "./version-
|
|
6
|
-
import { A as
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
+
import { n as FileBlobStore, t as CaptureLog } from "./log-CmBWcyQm.mjs";
|
|
3
|
+
import { r as playwrightRenderer } from "./render-CKE2LILH.mjs";
|
|
4
|
+
import { r as checkedRenderer } from "./browser-56dBBLec.mjs";
|
|
5
|
+
import { i as durationMs, n as DEFAULT_TOKEN, r as VERSION, t as BOT_PAGE } from "./version-Djva7JkN.mjs";
|
|
6
|
+
import { A as DEADLINE_NOTE, C as fold, D as snippetOf, E as parseQuery, N as sha256$1, P as stableStringify, a as fetchPage, c as isHeldBack, f as reindex, i as fetchAndIndex, k as Crawler, o as findDocuments, p as verifyQuote, r as catchUpIndex, t as Extractors, u as readDocument, y as archivedOf } from "./runtime-C_2BQibH.mjs";
|
|
7
7
|
import { MemoryHostStateStore, nextValidatorMemo, realClock } from "./politeness.mjs";
|
|
8
8
|
import { matchesPattern } from "./robots.mjs";
|
|
9
9
|
import { n as canonicalUrl, r as scopeTest, t as cacheKeyOf } from "./urls-BHFeSyEK.mjs";
|
|
10
10
|
import { o as decodeBody, t as extractPage } from "./html-DkQUAE_d.mjs";
|
|
11
|
-
import { n as diffDocument, r as diffTexts } from "./diff-
|
|
12
|
-
import { r as extractDocument } from "./extract-
|
|
11
|
+
import { n as diffDocument, r as diffTexts } from "./diff-CpKN0Jdc.mjs";
|
|
12
|
+
import { r as extractDocument } from "./extract-DqeqiNHb.mjs";
|
|
13
|
+
import { o as registerRecipe } from "./recipe-B2SkepDZ.mjs";
|
|
13
14
|
import { createHash, randomUUID } from "node:crypto";
|
|
14
15
|
import { dirname, join } from "node:path";
|
|
15
16
|
import { existsSync, lstatSync, mkdirSync, realpathSync, statSync } from "node:fs";
|
|
@@ -17,7 +18,11 @@ import { createGunzip } from "node:zlib";
|
|
|
17
18
|
import { DatabaseSync } from "node:sqlite";
|
|
18
19
|
/** The search index reads this many characters of each text, at most. */
|
|
19
20
|
const MAX_INDEX_CHARS = 2e5;
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* In the mode `fast`, a part is common when more than this share of the documents hold it.
|
|
23
|
+
*
|
|
24
|
+
* @beta
|
|
25
|
+
*/
|
|
21
26
|
const FAST_RANK_SHARE = .2;
|
|
22
27
|
/** The text of the error of hybrid search with no embedder. */
|
|
23
28
|
const NO_EMBEDDER = "hybrid search needs an embedder: open the crawler with the option embedder (transformersEmbedder or ollamaEmbedder), or give --embedder transformers or ollama to the command line";
|
|
@@ -163,6 +168,7 @@ const toVersion = (row) => ({
|
|
|
163
168
|
cut: row.full_sha !== null
|
|
164
169
|
});
|
|
165
170
|
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
171
|
+
/** @beta */
|
|
166
172
|
var DocIndex = class {
|
|
167
173
|
path;
|
|
168
174
|
db;
|
|
@@ -813,7 +819,11 @@ var DocIndex = class {
|
|
|
813
819
|
};
|
|
814
820
|
//#endregion
|
|
815
821
|
//#region src/sitemaps.ts
|
|
816
|
-
/**
|
|
822
|
+
/**
|
|
823
|
+
* These are the default limits of the reader, and the maximum length of one value.
|
|
824
|
+
*
|
|
825
|
+
* @beta
|
|
826
|
+
*/
|
|
817
827
|
const SITEMAP_LIMITS = {
|
|
818
828
|
maxEntries: 5e4,
|
|
819
829
|
maxDepth: 32,
|
|
@@ -847,7 +857,7 @@ const XML_ENTITIES = {
|
|
|
847
857
|
};
|
|
848
858
|
/** A tag keeps this number of attributes at most. The reader reads the others and keeps nothing of them. */
|
|
849
859
|
const MAX_ATTRIBUTES = 64;
|
|
850
|
-
const NAME
|
|
860
|
+
const NAME = /^[A-Za-z_][\w.:-]*$/;
|
|
851
861
|
const W3C_DATE = /^\d{4}(?:-\d{2}(?:-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?)?)?$/;
|
|
852
862
|
const MAIL_DATE = /^(?:[A-Za-z]{3},?\s+)?\d{1,2}\s+[A-Za-z]{3,9}\s+\d{2,4}\s+\d{1,2}:\d{2}(?::\d{2})?(?:\s*(?:[A-Za-z]{1,5}|[+-]\d{4}))?$/;
|
|
853
863
|
const short = (name) => name === void 0 ? "nothing" : name.length > 40 ? `${name.slice(0, 40)}...` : name;
|
|
@@ -855,6 +865,8 @@ const short = (name) => name === void 0 ? "nothing" : name.length > 40 ? `${name
|
|
|
855
865
|
* This function changes a date of a sitemap or a feed into ISO 8601. It reads the W3C form of the
|
|
856
866
|
* sitemap protocol and Atom, and the mail form of RSS. A time with no zone is UTC. It gives null
|
|
857
867
|
* for a text that it cannot read.
|
|
868
|
+
*
|
|
869
|
+
* @beta
|
|
858
870
|
*/
|
|
859
871
|
const feedDate = (value) => {
|
|
860
872
|
const text = value.trim();
|
|
@@ -920,7 +932,7 @@ const tagAt = (text, from) => {
|
|
|
920
932
|
at += 1;
|
|
921
933
|
}
|
|
922
934
|
const name = text.slice(from, Math.min(at, from + 256));
|
|
923
|
-
if (at - from > 256 || !NAME
|
|
935
|
+
if (at - from > 256 || !NAME.test(name)) return void 0;
|
|
924
936
|
const attributes = [];
|
|
925
937
|
for (;;) {
|
|
926
938
|
while (at < n && isSpace(text.charCodeAt(at))) at += 1;
|
|
@@ -945,7 +957,7 @@ const tagAt = (text, from) => {
|
|
|
945
957
|
at += 1;
|
|
946
958
|
}
|
|
947
959
|
const key = text.slice(start, Math.min(at, start + 256));
|
|
948
|
-
if (at - start > 256 || !NAME
|
|
960
|
+
if (at - start > 256 || !NAME.test(key)) return void 0;
|
|
949
961
|
while (at < n && isSpace(text.charCodeAt(at))) at += 1;
|
|
950
962
|
if (text.charCodeAt(at) !== 61) return void 0;
|
|
951
963
|
at += 1;
|
|
@@ -1304,6 +1316,8 @@ const scanSitemap = (text, base, limits = {}, meter = { steps: 0 }) => {
|
|
|
1304
1316
|
* This function reads a sitemap, a sitemap index, an RSS or Atom feed, or a text sitemap. The
|
|
1305
1317
|
* root element of the file tells its kind. The entries of a feed can have URLs relative to `base`
|
|
1306
1318
|
* (the URL of the file). The entries of a sitemap must have absolute URLs.
|
|
1319
|
+
*
|
|
1320
|
+
* @beta
|
|
1307
1321
|
*/
|
|
1308
1322
|
const readSitemap = (text, base, limits) => scanSitemap(text, base, limits);
|
|
1309
1323
|
//#endregion
|
|
@@ -1322,7 +1336,11 @@ const readSitemap = (text, base, limits) => scanSitemap(text, base, limits);
|
|
|
1322
1336
|
* Discovery keeps a URL only when it is in scope (the same site by default) and robots.txt permits
|
|
1323
1337
|
* it. It stops at a maximum of files, a maximum of URLs and a maximum time.
|
|
1324
1338
|
*/
|
|
1325
|
-
/**
|
|
1339
|
+
/**
|
|
1340
|
+
* These are the default limits of `discover`.
|
|
1341
|
+
*
|
|
1342
|
+
* @beta
|
|
1343
|
+
*/
|
|
1326
1344
|
const DISCOVER_LIMITS = {
|
|
1327
1345
|
/** The most sitemap and feed files that one discovery reads. */
|
|
1328
1346
|
maxFiles: 20,
|
|
@@ -1339,7 +1357,7 @@ const DISCOVER_LIMITS = {
|
|
|
1339
1357
|
/** The most feeds that discovery reads from the links of the page. */
|
|
1340
1358
|
maxLinkedFeeds: 5
|
|
1341
1359
|
};
|
|
1342
|
-
const PAGE_ACCEPT
|
|
1360
|
+
const PAGE_ACCEPT = "text/html,application/xhtml+xml;q=0.9,*/*;q=0.5";
|
|
1343
1361
|
const SITEMAP_ACCEPT = "application/xml,text/xml;q=0.9,text/plain;q=0.8,*/*;q=0.5";
|
|
1344
1362
|
const FEED_ACCEPT = "application/rss+xml,application/atom+xml,application/xml;q=0.9,text/xml;q=0.8,*/*;q=0.5";
|
|
1345
1363
|
const FEED_KINDS = /* @__PURE__ */ new Set([
|
|
@@ -1412,6 +1430,8 @@ const pastDeadline = (note) => note?.startsWith(DEADLINE_NOTE) === true;
|
|
|
1412
1430
|
* This function finds the pages of a site from its sitemaps and its feeds. `PoliteCrawler.discover`
|
|
1413
1431
|
* uses it. Each file is a fetch of the crawler with the recipe `sitemap` or `feed`, so the
|
|
1414
1432
|
* capture log records it, and robots.txt applies to it.
|
|
1433
|
+
*
|
|
1434
|
+
* @beta
|
|
1415
1435
|
*/
|
|
1416
1436
|
const discoverSite = async (runtime, site, options = {}) => {
|
|
1417
1437
|
const start = parsedUrl(site);
|
|
@@ -1534,7 +1554,7 @@ const discoverSite = async (runtime, site, options = {}) => {
|
|
|
1534
1554
|
url: start.href,
|
|
1535
1555
|
mode: "page",
|
|
1536
1556
|
recipe: "page",
|
|
1537
|
-
accept: PAGE_ACCEPT
|
|
1557
|
+
accept: PAGE_ACCEPT,
|
|
1538
1558
|
maxAgeMs,
|
|
1539
1559
|
onBehalfOf: options.onBehalfOf,
|
|
1540
1560
|
signal,
|
|
@@ -1721,7 +1741,11 @@ const discoverSite = async (runtime, site, options = {}) => {
|
|
|
1721
1741
|
*
|
|
1722
1742
|
* The state of the frontier is in memory, for one call. A later version can make it resumable.
|
|
1723
1743
|
*/
|
|
1724
|
-
/**
|
|
1744
|
+
/**
|
|
1745
|
+
* These are the default limits of `crawl`, and the limits of the memory of the frontier.
|
|
1746
|
+
*
|
|
1747
|
+
* @beta
|
|
1748
|
+
*/
|
|
1725
1749
|
const CRAWL_LIMITS = {
|
|
1726
1750
|
/** The most pages that one crawl gives. */
|
|
1727
1751
|
maxPages: 100,
|
|
@@ -1819,6 +1843,8 @@ const REFUSALS$1 = /* @__PURE__ */ new Set([
|
|
|
1819
1843
|
* This function crawls a site from its seeds. `PoliteCrawler.crawl` uses it. It gives each page
|
|
1820
1844
|
* when its fetch ends, and a summary at the end. The seeds come first, then the pages of the
|
|
1821
1845
|
* sitemaps (the newest first), then the links, the lower depth first.
|
|
1846
|
+
*
|
|
1847
|
+
* @beta
|
|
1822
1848
|
*/
|
|
1823
1849
|
const crawlSite = (runtime, seeds, options = {}) => {
|
|
1824
1850
|
let resolveSummary = () => void 0;
|
|
@@ -2169,6 +2195,8 @@ const memoOf = (row) => ({
|
|
|
2169
2195
|
*
|
|
2170
2196
|
* The state is not a projection of the capture log. A deletion of the file loses only the pace,
|
|
2171
2197
|
* the counts, the stops and the memos, and the crawler starts them again.
|
|
2198
|
+
*
|
|
2199
|
+
* @beta
|
|
2172
2200
|
*/
|
|
2173
2201
|
var SqliteHostStateStore = class {
|
|
2174
2202
|
path;
|
|
@@ -2350,7 +2378,11 @@ var SqliteHostStateStore = class {
|
|
|
2350
2378
|
};
|
|
2351
2379
|
//#endregion
|
|
2352
2380
|
//#region src/embed.ts
|
|
2353
|
-
/**
|
|
2381
|
+
/**
|
|
2382
|
+
* These are the limits of the chunks of a text.
|
|
2383
|
+
*
|
|
2384
|
+
* @beta
|
|
2385
|
+
*/
|
|
2354
2386
|
const CHUNK_LIMITS = {
|
|
2355
2387
|
maxTokens: 512,
|
|
2356
2388
|
defaultTokens: 256,
|
|
@@ -2377,6 +2409,8 @@ const kindOf = (point) => {
|
|
|
2377
2409
|
* This function estimates the number of tokens of a part of a text. A word is one token for each
|
|
2378
2410
|
* six letters. A Chinese, Japanese or Thai character is one token. Each other character that is
|
|
2379
2411
|
* not a space is one token. The estimate is a little high for English text, so a chunk fits.
|
|
2412
|
+
*
|
|
2413
|
+
* @beta
|
|
2380
2414
|
*/
|
|
2381
2415
|
const estimateTokens = (text, start = 0, end = text.length) => {
|
|
2382
2416
|
let tokens = 0;
|
|
@@ -2503,6 +2537,8 @@ const positive = (name, value) => {
|
|
|
2503
2537
|
* sentence that is longer than the window is cut at its spaces. Two chunks next to each other share
|
|
2504
2538
|
* a few sentences (`overlapTokens`), so a passage at a cut is in one chunk. Each chunk keeps its
|
|
2505
2539
|
* position in the text, so a hit points at a passage.
|
|
2540
|
+
*
|
|
2541
|
+
* @beta
|
|
2506
2542
|
*/
|
|
2507
2543
|
const chunkText = (text, options) => {
|
|
2508
2544
|
const size = positive("maxTokens", options.maxTokens);
|
|
@@ -2552,13 +2588,19 @@ const chunkText = (text, options) => {
|
|
|
2552
2588
|
/**
|
|
2553
2589
|
* This function gives the number of tokens of a chunk for an embedder. It is the input limit with a
|
|
2554
2590
|
* margin for the estimate, and `CHUNK_LIMITS.maxTokens` at most.
|
|
2591
|
+
*
|
|
2592
|
+
* @beta
|
|
2555
2593
|
*/
|
|
2556
2594
|
const chunkTokensOf = (embedder) => {
|
|
2557
2595
|
const limit = embedder.maxInputTokens;
|
|
2558
2596
|
const tokens = limit === void 0 || !Number.isFinite(limit) || limit < 1 ? CHUNK_LIMITS.defaultTokens : Math.min(limit, CHUNK_LIMITS.maxTokens);
|
|
2559
2597
|
return Math.max(8, Math.floor(tokens * CHUNK_LIMITS.fill));
|
|
2560
2598
|
};
|
|
2561
|
-
/**
|
|
2599
|
+
/**
|
|
2600
|
+
* This function gives a copy of a vector with a length of 1. A vector of zeros stays zeros. A value that is not finite throws a `RangeError`.
|
|
2601
|
+
*
|
|
2602
|
+
* @beta
|
|
2603
|
+
*/
|
|
2562
2604
|
const unitVector = (vector) => {
|
|
2563
2605
|
const out = Float32Array.from(vector);
|
|
2564
2606
|
let sum = 0;
|
|
@@ -2582,6 +2624,8 @@ const HYBRID_DEFAULTS = {
|
|
|
2582
2624
|
* 1 / (k + rank) over the lists that hold it, with the rank from 1. The best item is first. A tie goes to
|
|
2583
2625
|
* the item with the best rank in a list, then to the earlier list, then to the smaller id. An item
|
|
2584
2626
|
* that is two times in one list counts at its first place.
|
|
2627
|
+
*
|
|
2628
|
+
* @beta
|
|
2585
2629
|
*/
|
|
2586
2630
|
const fuseRanks = (lists, k = HYBRID_DEFAULTS.fusionK) => {
|
|
2587
2631
|
if (!Number.isFinite(k) || k < 0) throw new RangeError(`the fusion constant k must be a number of 0 or more, not ${String(k)}`);
|
|
@@ -2658,7 +2702,11 @@ const round = (value, digits) => {
|
|
|
2658
2702
|
const scale = 10 ** digits;
|
|
2659
2703
|
return Math.round(value * scale) / scale;
|
|
2660
2704
|
};
|
|
2661
|
-
/**
|
|
2705
|
+
/**
|
|
2706
|
+
* This function makes a hybrid search over a runtime. The vectors go into the store, with the id of the embedder.
|
|
2707
|
+
*
|
|
2708
|
+
* @beta
|
|
2709
|
+
*/
|
|
2662
2710
|
const createHybridSearch = (runtime, options) => {
|
|
2663
2711
|
const { embedder, store } = options;
|
|
2664
2712
|
const tokens = chunkTokensOf(embedder);
|
|
@@ -3018,279 +3066,6 @@ async function* pagesOf(runtime) {
|
|
|
3018
3066
|
if (page.length > 0) yield page;
|
|
3019
3067
|
}
|
|
3020
3068
|
//#endregion
|
|
3021
|
-
//#region src/recipe.ts
|
|
3022
|
-
/**
|
|
3023
|
-
* This function reads a body as JSON, in the encoding that the response declares. A body that is
|
|
3024
|
-
* not JSON (a page about maintenance, or a login page) throws an error that names the URL and the
|
|
3025
|
-
* content type. In an extractor, the runtime then reports that error.
|
|
3026
|
-
*/
|
|
3027
|
-
const readJson = (body, capture) => {
|
|
3028
|
-
const text = decodeBody(body, capture.contentType);
|
|
3029
|
-
try {
|
|
3030
|
-
return JSON.parse(text);
|
|
3031
|
-
} catch {
|
|
3032
|
-
throw new Error(`${capture.url} answered ${capture.contentType ?? "a body of no stated type"}, not JSON`);
|
|
3033
|
-
}
|
|
3034
|
-
};
|
|
3035
|
-
/**
|
|
3036
|
-
* These recipe names belong to the crawler: the page reader, the archived fallback, robots.txt,
|
|
3037
|
-
* and the sitemaps and feeds of `discover`. A recipe cannot use them.
|
|
3038
|
-
*/
|
|
3039
|
-
const RESERVED_RECIPES = [
|
|
3040
|
-
"page",
|
|
3041
|
-
"archive-copy",
|
|
3042
|
-
"archive-index",
|
|
3043
|
-
"robots.txt",
|
|
3044
|
-
"sitemap",
|
|
3045
|
-
"feed"
|
|
3046
|
-
];
|
|
3047
|
-
/** These are the limits of a recipe. */
|
|
3048
|
-
const RECIPE_LIMITS = {
|
|
3049
|
-
nameChars: 64,
|
|
3050
|
-
docIdChars: 2048
|
|
3051
|
-
};
|
|
3052
|
-
const NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
3053
|
-
const PAGE_ACCEPT = "text/html,application/xhtml+xml;q=0.9,*/*;q=0.5";
|
|
3054
|
-
const API_ACCEPT = "application/json";
|
|
3055
|
-
const AI_INPUTS = [
|
|
3056
|
-
"yes",
|
|
3057
|
-
"no",
|
|
3058
|
-
"unset"
|
|
3059
|
-
];
|
|
3060
|
-
/** This function tells what is wrong with one document of an extractor, or gives undefined. */
|
|
3061
|
-
const faultOf = (value, seen) => {
|
|
3062
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return "a document must be an object";
|
|
3063
|
-
const doc = value;
|
|
3064
|
-
const id = doc["doc"];
|
|
3065
|
-
if (typeof id !== "string" || id.length === 0 || id.length > RECIPE_LIMITS.docIdChars) return `doc must be a text of 1 to ${String(RECIPE_LIMITS.docIdChars)} characters`;
|
|
3066
|
-
if (id.startsWith("archive:")) return `the id ${id} starts with archive:, which is for the archived copies`;
|
|
3067
|
-
if (seen.has(id)) return `two documents have the id ${id}`;
|
|
3068
|
-
seen.add(id);
|
|
3069
|
-
const kind = doc["kind"];
|
|
3070
|
-
if (typeof kind !== "string" || kind.length === 0) return "kind must be a text that is not empty";
|
|
3071
|
-
if (kind === "archived") return "the kind archived is for the archived copies";
|
|
3072
|
-
if (typeof doc["title"] !== "string") return "title must be a text";
|
|
3073
|
-
if (typeof doc["text"] !== "string") return "text must be a text";
|
|
3074
|
-
if (doc["url"] !== null && typeof doc["url"] !== "string") return "url must be a text or null";
|
|
3075
|
-
const meta = doc["meta"];
|
|
3076
|
-
if (meta === null || typeof meta !== "object" || Array.isArray(meta)) return "meta must be an object";
|
|
3077
|
-
try {
|
|
3078
|
-
JSON.stringify(meta);
|
|
3079
|
-
} catch {
|
|
3080
|
-
return "meta must be JSON: no cycle and no BigInt";
|
|
3081
|
-
}
|
|
3082
|
-
const merge = doc["merge"];
|
|
3083
|
-
if (merge !== void 0 && merge !== "latest" && merge !== "richest") return "merge must be \"latest\" or \"richest\"";
|
|
3084
|
-
if (doc["full_text"] !== void 0 && typeof doc["full_text"] !== "string") return "full_text must be a text";
|
|
3085
|
-
const aiInput = doc["ai_input"];
|
|
3086
|
-
if (aiInput !== void 0 && !AI_INPUTS.includes(aiInput)) return "ai_input must be \"yes\", \"no\" or \"unset\"";
|
|
3087
|
-
};
|
|
3088
|
-
/** This function gives the documents of an extractor, or throws an error that names the first fault. */
|
|
3089
|
-
const checkedDocs = (name, docs) => {
|
|
3090
|
-
if (!Array.isArray(docs)) throw new TypeError(`the recipe ${name} gave ${typeof docs}, not an array of documents`);
|
|
3091
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3092
|
-
for (const [index, doc] of docs.entries()) {
|
|
3093
|
-
const fault = faultOf(doc, seen);
|
|
3094
|
-
if (fault !== void 0) throw new TypeError(`the recipe ${name} gave a document that is not valid (${String(index)}): ${fault}`);
|
|
3095
|
-
}
|
|
3096
|
-
return docs;
|
|
3097
|
-
};
|
|
3098
|
-
/**
|
|
3099
|
-
* This function declares a recipe. It checks the definition at once: a name that is not valid, a
|
|
3100
|
-
* reserved name or a duration that it cannot read throws an error. The extractor of the recipe
|
|
3101
|
-
* checks each document of `extract`. A document that is not valid makes the extractor throw, and
|
|
3102
|
-
* the runtime reports it as an error of the extractor.
|
|
3103
|
-
*/
|
|
3104
|
-
const defineRecipe = (definition) => {
|
|
3105
|
-
const name = definition.name;
|
|
3106
|
-
if (typeof name !== "string" || name.length > RECIPE_LIMITS.nameChars || !NAME.test(name)) throw new RangeError(`a recipe name is 1 to ${String(RECIPE_LIMITS.nameChars)} letters, digits and . _ : -, and it starts with a letter or a digit, not ${JSON.stringify(name) ?? "undefined"}`);
|
|
3107
|
-
if (RESERVED_RECIPES.includes(name)) throw new RangeError(`the recipe name ${name} belongs to the crawler: give the recipe a name of its own`);
|
|
3108
|
-
if (typeof definition.extract !== "function") throw new TypeError(`the recipe ${name} needs extract, a function that reads a body into documents`);
|
|
3109
|
-
const mode = definition.mode ?? "page";
|
|
3110
|
-
if (mode !== "page" && mode !== "api") throw new RangeError(`the mode of the recipe ${name} must be "page" or "api", not ${String(mode)}`);
|
|
3111
|
-
const accept = definition.accept ?? (mode === "api" ? API_ACCEPT : PAGE_ACCEPT);
|
|
3112
|
-
const minIntervalMs = definition.minInterval === void 0 ? void 0 : durationMs(definition.minInterval);
|
|
3113
|
-
const maxAgeOf = (value) => value === "auto" ? "auto" : durationMs(value);
|
|
3114
|
-
const maxAge = maxAgeOf(definition.maxAge ?? "24h");
|
|
3115
|
-
const maxBytes = definition.maxBytes;
|
|
3116
|
-
if (maxBytes !== void 0 && (!Number.isSafeInteger(maxBytes) || maxBytes < 1)) throw new RangeError(`maxBytes of the recipe ${name} must be a whole number of 1 or more`);
|
|
3117
|
-
const extract = definition.extract;
|
|
3118
|
-
const extractor = async (body, capture) => checkedDocs(name, await extract(body, {
|
|
3119
|
-
url: capture.url,
|
|
3120
|
-
contentType: capture.contentType
|
|
3121
|
-
}));
|
|
3122
|
-
const fetch = definition.fetch;
|
|
3123
|
-
return {
|
|
3124
|
-
name,
|
|
3125
|
-
description: definition.description ?? null,
|
|
3126
|
-
mode,
|
|
3127
|
-
accept,
|
|
3128
|
-
extractor,
|
|
3129
|
-
request(input, options = {}) {
|
|
3130
|
-
const asked = fetch === void 0 ? input : fetch(input);
|
|
3131
|
-
const target = typeof asked === "string" ? { url: asked } : asked;
|
|
3132
|
-
const url = target?.url;
|
|
3133
|
-
let parsed;
|
|
3134
|
-
try {
|
|
3135
|
-
parsed = typeof url === "string" ? new URL(url) : void 0;
|
|
3136
|
-
} catch {
|
|
3137
|
-
parsed = void 0;
|
|
3138
|
-
}
|
|
3139
|
-
if (parsed === void 0 || parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new TypeError(`the recipe ${name} needs an http or https URL for its input, not ${JSON.stringify(url) ?? String(url)}`);
|
|
3140
|
-
return {
|
|
3141
|
-
url: parsed.href,
|
|
3142
|
-
mode,
|
|
3143
|
-
recipe: name,
|
|
3144
|
-
accept: target?.accept ?? accept,
|
|
3145
|
-
headers: target?.headers,
|
|
3146
|
-
secretParams: target?.secretParams,
|
|
3147
|
-
minIntervalMs,
|
|
3148
|
-
maxAgeMs: options.maxAge === void 0 ? maxAge : maxAgeOf(options.maxAge),
|
|
3149
|
-
maxBytes,
|
|
3150
|
-
onBehalfOf: options.onBehalfOf,
|
|
3151
|
-
job: options.job,
|
|
3152
|
-
signal: options.signal,
|
|
3153
|
-
scope: options.scope,
|
|
3154
|
-
labels: options.labels,
|
|
3155
|
-
deadline: options.deadline
|
|
3156
|
-
};
|
|
3157
|
-
}
|
|
3158
|
-
};
|
|
3159
|
-
};
|
|
3160
|
-
/**
|
|
3161
|
-
* This function gives the extractor of a recipe to the extractors of a runtime. A second
|
|
3162
|
-
* registration of the same recipe does nothing. A different extractor with the same name throws
|
|
3163
|
-
* an error, because `reindex` must read each capture with one extractor.
|
|
3164
|
-
*/
|
|
3165
|
-
const registerRecipe = (extractors, recipe) => {
|
|
3166
|
-
const known = extractors.get(recipe.name);
|
|
3167
|
-
if (known === recipe.extractor) return;
|
|
3168
|
-
if (known !== void 0) throw new Error(`the recipe ${recipe.name} has a different extractor in this runtime: give each recipe a name of its own`);
|
|
3169
|
-
extractors.register(recipe.name, recipe.extractor);
|
|
3170
|
-
};
|
|
3171
|
-
/** This function gives the runtime of a target. */
|
|
3172
|
-
const runtimeOf = (target) => "crawler" in target ? target : target.runtime;
|
|
3173
|
-
/** This function gives the deadline of a budget, on the clock of the runtime. */
|
|
3174
|
-
const deadlineOf = (runtime, budget) => budget === void 0 ? void 0 : Date.parse(runtime.now()) + durationMs(budget);
|
|
3175
|
-
/** This function gives one document of a run, with the rule for a held-back document. */
|
|
3176
|
-
const recipeDoc = async (runtime, doc, capture, userProvided, action) => {
|
|
3177
|
-
const current = runtime.index.get(doc.doc) ?? doc;
|
|
3178
|
-
const version = (capture === void 0 ? void 0 : runtime.index.versionText(doc.doc, capture)) ?? current;
|
|
3179
|
-
const aiInput = current.ai_input === "no" || version.ai_input === "no" ? "no" : version.ai_input;
|
|
3180
|
-
const held = isHeldBack({ ai_input: aiInput }, userProvided);
|
|
3181
|
-
if (aiInput === "no" && userProvided) await recordOverride(runtime, current, action);
|
|
3182
|
-
const archived = archivedOf(version.meta);
|
|
3183
|
-
return {
|
|
3184
|
-
doc: doc.doc,
|
|
3185
|
-
kind: version.kind,
|
|
3186
|
-
title: held ? "" : version.title,
|
|
3187
|
-
url: version.url,
|
|
3188
|
-
meta: held ? heldBackMeta(version.meta) : version.meta,
|
|
3189
|
-
chars: version.text.length,
|
|
3190
|
-
capture: version.capture,
|
|
3191
|
-
fetched_at: version.fetched_at,
|
|
3192
|
-
held_back: held,
|
|
3193
|
-
...archived === void 0 ? {} : { archived }
|
|
3194
|
-
};
|
|
3195
|
-
};
|
|
3196
|
-
const noteOf = (parts) => {
|
|
3197
|
-
const kept = parts.filter((part) => typeof part === "string" && part.length > 0);
|
|
3198
|
-
return kept.length > 0 ? kept.join("; ") : null;
|
|
3199
|
-
};
|
|
3200
|
-
/**
|
|
3201
|
-
* This function starts a recipe for one input. It fetches the request through the crawler, and it
|
|
3202
|
-
* reads the capture with the extractor of the recipe. It gives the documents of that capture.
|
|
3203
|
-
* These are its rules:
|
|
3204
|
-
*
|
|
3205
|
-
* - The runtime registers the recipe the first time that it gets it. Give the recipe to
|
|
3206
|
-
* `openCrawler` too, so that `reindex` reads its captures before the first fetch.
|
|
3207
|
-
* - A stored copy younger than `maxAge` answers with no request. When the extractor cannot read a
|
|
3208
|
-
* stored copy, the crawler asks the source one more time.
|
|
3209
|
-
* - An extractor that throws gives no document, and `error` tells why. The fetch still succeeds.
|
|
3210
|
-
* - A document that asked not to be AI input shows no title and only its structural metadata,
|
|
3211
|
-
* unless `userProvided`. The crawler logs each `userProvided` read of a held-back document.
|
|
3212
|
-
* - With `fallback: "archive"`, a page that the site refuses gets an archived copy, with
|
|
3213
|
-
* `archived`. The page reader reads the copy. The extractor of the recipe reads only live
|
|
3214
|
-
* captures. Thus its documents do not mix a live answer and an old copy of a third party.
|
|
3215
|
-
*/
|
|
3216
|
-
const runRecipe = async (target, recipe, input, options = {}) => {
|
|
3217
|
-
const runtime = runtimeOf(target);
|
|
3218
|
-
if (options.fallback === "archive" && recipe.mode !== "page") throw new RangeError(`the recipe ${recipe.name} reads an API: an archived copy is for a page, so give fallback only to a recipe of the mode page`);
|
|
3219
|
-
registerRecipe(runtime.extractors, recipe);
|
|
3220
|
-
const deadline = deadlineOf(runtime, options.budget);
|
|
3221
|
-
const request = recipe.request(input, {
|
|
3222
|
-
...options,
|
|
3223
|
-
deadline
|
|
3224
|
-
});
|
|
3225
|
-
let indexed = await fetchAndIndex(runtime, request);
|
|
3226
|
-
if (indexed.error !== void 0 && indexed.blocked === void 0 && indexed.fetched.fromCache) indexed = await fetchAndIndex(runtime, {
|
|
3227
|
-
...request,
|
|
3228
|
-
maxAgeMs: 0
|
|
3229
|
-
});
|
|
3230
|
-
const userProvided = options.userProvided ?? false;
|
|
3231
|
-
const action = options.job ?? `recipe ${recipe.name}`;
|
|
3232
|
-
const fetched = indexed.fetched;
|
|
3233
|
-
const notes = [fetched.note, indexed.error];
|
|
3234
|
-
if (indexed.docs.length === 0 && options.fallback === "archive") {
|
|
3235
|
-
const reason = fallbackReason(indexed);
|
|
3236
|
-
if (reason !== void 0) {
|
|
3237
|
-
const page = {
|
|
3238
|
-
render: "never",
|
|
3239
|
-
maxAgeMs: request.maxAgeMs,
|
|
3240
|
-
onBehalfOf: request.onBehalfOf,
|
|
3241
|
-
job: request.job,
|
|
3242
|
-
signal: request.signal,
|
|
3243
|
-
scope: request.scope,
|
|
3244
|
-
labels: request.labels,
|
|
3245
|
-
deadline,
|
|
3246
|
-
fallback: "archive",
|
|
3247
|
-
archiveDespiteRobots: options.archiveDespiteRobots
|
|
3248
|
-
};
|
|
3249
|
-
const answer = await archivedCopy(runtime, request.url, reason, page);
|
|
3250
|
-
if (answer.doc !== void 0 && answer.archived !== void 0) {
|
|
3251
|
-
const doc = await recipeDoc(runtime, answer.doc, answer.doc.capture, userProvided, action);
|
|
3252
|
-
return {
|
|
3253
|
-
recipe: recipe.name,
|
|
3254
|
-
url: fetched.url,
|
|
3255
|
-
outcome: fetched.outcome,
|
|
3256
|
-
status: fetched.status,
|
|
3257
|
-
docs: [doc],
|
|
3258
|
-
capture: doc.capture,
|
|
3259
|
-
fetched_at: doc.fetched_at,
|
|
3260
|
-
from_cache: answer.fromCache,
|
|
3261
|
-
held_back: doc.held_back,
|
|
3262
|
-
error: indexed.error ?? null,
|
|
3263
|
-
archived: answer.archived,
|
|
3264
|
-
note: noteOf([
|
|
3265
|
-
...notes,
|
|
3266
|
-
...answer.notes,
|
|
3267
|
-
"the page reader read the archived copy, not the extractor of the recipe",
|
|
3268
|
-
doc.held_back ? HELD_BACK : void 0
|
|
3269
|
-
])
|
|
3270
|
-
};
|
|
3271
|
-
}
|
|
3272
|
-
notes.push(...answer.notes);
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
const capture = fetched.capture;
|
|
3276
|
-
const docs = [];
|
|
3277
|
-
for (const doc of indexed.docs) docs.push(await recipeDoc(runtime, doc, capture, userProvided, action));
|
|
3278
|
-
const held = docs.some((doc) => doc.held_back);
|
|
3279
|
-
return {
|
|
3280
|
-
recipe: recipe.name,
|
|
3281
|
-
url: fetched.url,
|
|
3282
|
-
outcome: fetched.outcome,
|
|
3283
|
-
status: fetched.status,
|
|
3284
|
-
docs,
|
|
3285
|
-
capture: docs[0]?.capture ?? capture ?? null,
|
|
3286
|
-
fetched_at: docs[0]?.fetched_at ?? fetched.fetchedAt ?? null,
|
|
3287
|
-
from_cache: fetched.fromCache,
|
|
3288
|
-
held_back: held,
|
|
3289
|
-
error: indexed.error ?? null,
|
|
3290
|
-
note: noteOf([...notes, held ? HELD_BACK : void 0])
|
|
3291
|
-
};
|
|
3292
|
-
};
|
|
3293
|
-
//#endregion
|
|
3294
3069
|
//#region src/vectors.ts
|
|
3295
3070
|
/**
|
|
3296
3071
|
* The vector store keeps the vectors of hybrid search in `vectors.sqlite`, beside the index. It is
|
|
@@ -3312,7 +3087,11 @@ const runRecipe = async (target, recipe, input, options = {}) => {
|
|
|
3312
3087
|
* one transaction (`BEGIN IMMEDIATE`). The file has a schema version (`VECTORS_SCHEMA`). A file of
|
|
3313
3088
|
* a different version opens empty.
|
|
3314
3089
|
*/
|
|
3315
|
-
/**
|
|
3090
|
+
/**
|
|
3091
|
+
* This value is the version of the schema of the vector file.
|
|
3092
|
+
*
|
|
3093
|
+
* @beta
|
|
3094
|
+
*/
|
|
3316
3095
|
const VECTORS_SCHEMA = 1;
|
|
3317
3096
|
const DDL = `
|
|
3318
3097
|
CREATE TABLE IF NOT EXISTS vectors_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
@@ -3363,6 +3142,7 @@ const dot = (data, row, query, dims) => {
|
|
|
3363
3142
|
for (let at = 0; at < dims; at += 1) sum += data[base + at] * query[at];
|
|
3364
3143
|
return sum;
|
|
3365
3144
|
};
|
|
3145
|
+
/** @beta */
|
|
3366
3146
|
var VectorStore = class {
|
|
3367
3147
|
path;
|
|
3368
3148
|
db;
|
|
@@ -3768,6 +3548,8 @@ const targetOf = (url) => {
|
|
|
3768
3548
|
*
|
|
3769
3549
|
* The list is the decision of the operator, not a projection of the capture log. Thus it has its
|
|
3770
3550
|
* own file: a deletion of `hosts.sqlite` or of the index does not lose it.
|
|
3551
|
+
*
|
|
3552
|
+
* @beta
|
|
3771
3553
|
*/
|
|
3772
3554
|
var WatchStore = class {
|
|
3773
3555
|
path;
|
|
@@ -3967,6 +3749,8 @@ const sameState = (a, b) => a !== void 0 && a.state === b.state && (a.state ===
|
|
|
3967
3749
|
* it. Each check is a fetch of the page with `fetchPage`, with no stored copy (`maxAgeMs: 0`), so
|
|
3968
3750
|
* each polite rule applies. A check that the budget stops before its request stays due. The store
|
|
3969
3751
|
* can be a function that opens it: the run uses it before the first check.
|
|
3752
|
+
*
|
|
3753
|
+
* @beta
|
|
3970
3754
|
*/
|
|
3971
3755
|
const runWatch = (runtime, source, options = {}) => {
|
|
3972
3756
|
let resolveSummary = () => void 0;
|
|
@@ -4259,6 +4043,8 @@ const MAILBOX = /^(?:[^\s<>@:/]+@[^\s<>@/]+|[^<>]*<[^\s<>@:/]+@[^\s<>@/]+>)$/;
|
|
|
4259
4043
|
* This function tells if a contact is a mailbox. The `From` header holds only a mailbox (RFC 9110,
|
|
4260
4044
|
* section 10.1.2). Thus the crawler sends a mailbox in `From` and in the `User-Agent`, and a URL in
|
|
4261
4045
|
* the `User-Agent` only.
|
|
4046
|
+
*
|
|
4047
|
+
* @beta
|
|
4262
4048
|
*/
|
|
4263
4049
|
const isMailbox = (contact) => MAILBOX.test(contact.trim());
|
|
4264
4050
|
/** The parts of a store that can be a symbolic link to a different place only by mistake or by attack. */
|
|
@@ -4296,6 +4082,8 @@ const guardStore = (dir) => {
|
|
|
4296
4082
|
* This function makes a runtime. It is the one place that puts the log, the blob store, the index,
|
|
4297
4083
|
* the host state, the crawler and the extractors together. The function `openCrawler` uses it.
|
|
4298
4084
|
* An application that needs the parts uses it too. The runtime and the crawler share one clock.
|
|
4085
|
+
*
|
|
4086
|
+
* @beta
|
|
4299
4087
|
*/
|
|
4300
4088
|
const createRuntime = (options) => {
|
|
4301
4089
|
const dir = options.dir;
|
|
@@ -4539,6 +4327,6 @@ const openCrawler = (options) => {
|
|
|
4539
4327
|
};
|
|
4540
4328
|
};
|
|
4541
4329
|
//#endregion
|
|
4542
|
-
export {
|
|
4330
|
+
export { NO_EMBEDDER as A, DISCOVER_LIMITS as C, readSitemap as D, feedDate as E, DocIndex as O, crawlSite as S, SITEMAP_LIMITS as T, estimateTokens as _, open_exports as a, SqliteHostStateStore as b, WatchStore as c, VectorStore as d, createHybridSearch as f, chunkTokensOf as g, chunkText as h, openCrawler as i, FAST_RANK_SHARE as k, runWatch as l, HYBRID_DEFAULTS as m, defaultUserAgent as n, WATCH_LIMITS as o, CHUNK_LIMITS as p, isMailbox as r, WATCH_SCHEMA as s, createRuntime as t, VECTORS_SCHEMA as u, fuseRanks as v, discoverSite as w, CRAWL_LIMITS as x, unitVector as y };
|
|
4543
4331
|
|
|
4544
|
-
//# sourceMappingURL=open-
|
|
4332
|
+
//# sourceMappingURL=open-CEo22Hpu.mjs.map
|