amenbo 0.1.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/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/adapters/index.d.ts +22 -0
- package/dist/adapters/index.js +51 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +21 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cache.d.ts +90 -0
- package/dist/cache.js +286 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.js +327 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.d.ts +78 -0
- package/dist/core.js +649 -0
- package/dist/core.js.map +1 -0
- package/dist/diff.d.ts +20 -0
- package/dist/diff.js +44 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +97 -0
- package/dist/errors.js +209 -0
- package/dist/errors.js.map +1 -0
- package/dist/extract/dataSources.d.ts +10 -0
- package/dist/extract/dataSources.js +115 -0
- package/dist/extract/dataSources.js.map +1 -0
- package/dist/extract/geometry.d.ts +63 -0
- package/dist/extract/geometry.js +77 -0
- package/dist/extract/geometry.js.map +1 -0
- package/dist/extract/markdown.d.ts +24 -0
- package/dist/extract/markdown.js +221 -0
- package/dist/extract/markdown.js.map +1 -0
- package/dist/extract/outline.d.ts +33 -0
- package/dist/extract/outline.js +128 -0
- package/dist/extract/outline.js.map +1 -0
- package/dist/extract/pdf.d.ts +22 -0
- package/dist/extract/pdf.js +95 -0
- package/dist/extract/pdf.js.map +1 -0
- package/dist/extract/preview.d.ts +19 -0
- package/dist/extract/preview.js +94 -0
- package/dist/extract/preview.js.map +1 -0
- package/dist/extract/pruning.d.ts +61 -0
- package/dist/extract/pruning.js +101 -0
- package/dist/extract/pruning.js.map +1 -0
- package/dist/extract/qualityScore.d.ts +67 -0
- package/dist/extract/qualityScore.js +98 -0
- package/dist/extract/qualityScore.js.map +1 -0
- package/dist/fetcher/browser.d.ts +85 -0
- package/dist/fetcher/browser.js +343 -0
- package/dist/fetcher/browser.js.map +1 -0
- package/dist/fetcher/http.d.ts +105 -0
- package/dist/fetcher/http.js +538 -0
- package/dist/fetcher/http.js.map +1 -0
- package/dist/fetcher/index.d.ts +41 -0
- package/dist/fetcher/index.js +92 -0
- package/dist/fetcher/index.js.map +1 -0
- package/dist/jp/consentBanner.d.ts +27 -0
- package/dist/jp/consentBanner.js +48 -0
- package/dist/jp/consentBanner.js.map +1 -0
- package/dist/jp/normalize.d.ts +30 -0
- package/dist/jp/normalize.js +65 -0
- package/dist/jp/normalize.js.map +1 -0
- package/dist/links.d.ts +20 -0
- package/dist/links.js +223 -0
- package/dist/links.js.map +1 -0
- package/dist/politeness.d.ts +66 -0
- package/dist/politeness.js +170 -0
- package/dist/politeness.js.map +1 -0
- package/dist/screenshot.d.ts +38 -0
- package/dist/screenshot.js +90 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +111 -0
- package/dist/server.js.map +1 -0
- package/dist/templateLearning.d.ts +11 -0
- package/dist/templateLearning.js +42 -0
- package/dist/templateLearning.js.map +1 -0
- package/dist/tokens.d.ts +47 -0
- package/dist/tokens.js +141 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fetcher/index.ts — 二段フェッチ。
|
|
3
|
+
*
|
|
4
|
+
* まず素のHTTP GET(fetcher/http.ts)を行い、SPA判定(ヒューリスティック)に該当した
|
|
5
|
+
* 場合のみheadless Chromium(fetcher/browser.ts)へ昇格する。
|
|
6
|
+
* 大半の静的/SSRページはHTTP GETのみで完結し、先方負荷・自機資源を最小化する。
|
|
7
|
+
*/
|
|
8
|
+
import { parseHTML } from "linkedom";
|
|
9
|
+
import { UnsupportedContentError } from "../errors.js";
|
|
10
|
+
import { fetchWithBrowser } from "./browser.js";
|
|
11
|
+
import { httpGetRouted } from "./http.js";
|
|
12
|
+
// ---- SPA判定ヒューリスティック ----
|
|
13
|
+
const SPA_ROOT_IDS = ["root", "app", "__next", "__nuxt"];
|
|
14
|
+
const MIN_HTML_LENGTH_FOR_RATIO_CHECK = 2000;
|
|
15
|
+
const TEXT_TO_HTML_RATIO_THRESHOLD = 0.02;
|
|
16
|
+
const MIN_VISIBLE_TEXT_LENGTH = 200;
|
|
17
|
+
function detectSpaSignals(html) {
|
|
18
|
+
const { document } = parseHTML(html);
|
|
19
|
+
for (const tag of Array.from(document.querySelectorAll("script, style"))) {
|
|
20
|
+
tag.remove();
|
|
21
|
+
}
|
|
22
|
+
const visibleText = (document.body?.textContent ?? "").replace(/\s+/g, "");
|
|
23
|
+
for (const id of SPA_ROOT_IDS) {
|
|
24
|
+
const el = document.getElementById(id);
|
|
25
|
+
if (el && el.textContent.replace(/\s+/g, "").length < 10) {
|
|
26
|
+
return { escalate: true, reason: `SPAルートコンテナ(#${id})が空です` };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const noscriptWarning = Array.from(document.querySelectorAll("noscript")).some((el) => /javascript|有効に|enable/i.test(el.textContent ?? ""));
|
|
30
|
+
if (noscriptWarning) {
|
|
31
|
+
return { escalate: true, reason: "noscriptにJavaScript要求の警告があります" };
|
|
32
|
+
}
|
|
33
|
+
if (html.length > MIN_HTML_LENGTH_FOR_RATIO_CHECK && visibleText.length / html.length < TEXT_TO_HTML_RATIO_THRESHOLD) {
|
|
34
|
+
return { escalate: true, reason: "抽出テキスト量/HTMLサイズ比が低いです" };
|
|
35
|
+
}
|
|
36
|
+
if (html.length > MIN_HTML_LENGTH_FOR_RATIO_CHECK && visibleText.length < MIN_VISIBLE_TEXT_LENGTH) {
|
|
37
|
+
return { escalate: true, reason: "可視テキスト量が閾値未満です" };
|
|
38
|
+
}
|
|
39
|
+
return { escalate: false, reason: null };
|
|
40
|
+
}
|
|
41
|
+
/** 二段フェッチ本体。条件付きGETでstatus 304が返った場合はNotModifiedResultを返す。 */
|
|
42
|
+
export async function fetchPage(url, options = {}) {
|
|
43
|
+
const routed = await httpGetRouted(url, options);
|
|
44
|
+
if (routed.kind === "notModified") {
|
|
45
|
+
return { notModified: true, finalUrl: routed.finalUrl };
|
|
46
|
+
}
|
|
47
|
+
if (routed.kind === "handoff") {
|
|
48
|
+
// PDFの既存処理フロー(server.tsのURL拡張子判定 → handlePdfFetch)は変更しない。
|
|
49
|
+
// URL拡張子で検出できなかったPDF(content-typeのみで判明するケース)は、
|
|
50
|
+
// 機能B以前と同じくUnsupportedContentErrorのままにする(バイナリをテキストとして
|
|
51
|
+
// プレビューしてしまう回帰を避けるため)。
|
|
52
|
+
if (routed.contentType && /application\/pdf/i.test(routed.contentType)) {
|
|
53
|
+
throw new UnsupportedContentError(routed.finalUrl, routed.contentType);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
handoff: true,
|
|
57
|
+
finalUrl: routed.finalUrl,
|
|
58
|
+
status: routed.status,
|
|
59
|
+
contentType: routed.contentType,
|
|
60
|
+
bytes: routed.bytes,
|
|
61
|
+
declaredSize: routed.declaredSize,
|
|
62
|
+
truncated: routed.truncated,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const spaSignals = options.forceBrowser ? { escalate: true, reason: "forceBrowser指定" } : detectSpaSignals(routed.html);
|
|
66
|
+
if (!spaSignals.escalate) {
|
|
67
|
+
return {
|
|
68
|
+
finalUrl: routed.finalUrl,
|
|
69
|
+
html: routed.html,
|
|
70
|
+
tier: "http",
|
|
71
|
+
status: routed.status,
|
|
72
|
+
encoding: routed.encoding,
|
|
73
|
+
etag: routed.headers.get("etag"),
|
|
74
|
+
lastModified: routed.headers.get("last-modified"),
|
|
75
|
+
escalationReason: null,
|
|
76
|
+
geometry: null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const browserResult = await fetchWithBrowser(routed.finalUrl, options.timeoutMs);
|
|
80
|
+
return {
|
|
81
|
+
finalUrl: browserResult.finalUrl,
|
|
82
|
+
html: browserResult.html,
|
|
83
|
+
tier: "browser",
|
|
84
|
+
status: browserResult.status,
|
|
85
|
+
encoding: "UTF-8",
|
|
86
|
+
etag: null,
|
|
87
|
+
lastModified: null,
|
|
88
|
+
escalationReason: spaSignals.reason,
|
|
89
|
+
geometry: browserResult.geometry,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/fetcher/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jp/consentBanner.ts — J8: 国内同意バナー・アプリ誘導インタースティシャル除去。
|
|
3
|
+
*
|
|
4
|
+
* Cookie同意バナー・アプリ誘導オーバーレイ等、本文を覆う/汚染する要素を除去する。
|
|
5
|
+
* Crawl4AIの40+CMP対応の国内版に相当するが、Phase 3では個別CMPのSDK名を列挙するのではなく、
|
|
6
|
+
* 「よくある実装パターン」の汎用ヒューリスティックでカバーする:
|
|
7
|
+
* - id/class名にcookie/consent/cmp/gdpr/app-banner/interstitial等が含まれる
|
|
8
|
+
* - 「同意して閉じる」「アプリで開く」等の日本語頻出文言を含む
|
|
9
|
+
* - (fixed overlay判定はDOM抽出時は不可。ブラウザレンダリング時はfetcher/browser.tsの
|
|
10
|
+
* hideConsentBanners が実際のcomputed styleで判定する)
|
|
11
|
+
* のうち複数シグナルが揃った場合のみ除去することで、誤除去(本文の一部を消してしまう)を防ぐ。
|
|
12
|
+
*/
|
|
13
|
+
interface RemovableElement {
|
|
14
|
+
id: string;
|
|
15
|
+
className: string;
|
|
16
|
+
textContent: string;
|
|
17
|
+
remove(): void;
|
|
18
|
+
}
|
|
19
|
+
export interface ConsentBannerHostDocument {
|
|
20
|
+
querySelectorAll(selector: string): ArrayLike<RemovableElement>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* DOM上のCookie同意バナー/アプリ誘導インタースティシャルを除去する。
|
|
24
|
+
* 除去したブロック数を返す。
|
|
25
|
+
*/
|
|
26
|
+
export declare function removeConsentBanners(document: ConsentBannerHostDocument): number;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jp/consentBanner.ts — J8: 国内同意バナー・アプリ誘導インタースティシャル除去。
|
|
3
|
+
*
|
|
4
|
+
* Cookie同意バナー・アプリ誘導オーバーレイ等、本文を覆う/汚染する要素を除去する。
|
|
5
|
+
* Crawl4AIの40+CMP対応の国内版に相当するが、Phase 3では個別CMPのSDK名を列挙するのではなく、
|
|
6
|
+
* 「よくある実装パターン」の汎用ヒューリスティックでカバーする:
|
|
7
|
+
* - id/class名にcookie/consent/cmp/gdpr/app-banner/interstitial等が含まれる
|
|
8
|
+
* - 「同意して閉じる」「アプリで開く」等の日本語頻出文言を含む
|
|
9
|
+
* - (fixed overlay判定はDOM抽出時は不可。ブラウザレンダリング時はfetcher/browser.tsの
|
|
10
|
+
* hideConsentBanners が実際のcomputed styleで判定する)
|
|
11
|
+
* のうち複数シグナルが揃った場合のみ除去することで、誤除去(本文の一部を消してしまう)を防ぐ。
|
|
12
|
+
*/
|
|
13
|
+
const BANNER_TEXT_PATTERNS = [
|
|
14
|
+
/同意して閉じる/,
|
|
15
|
+
/同意する/,
|
|
16
|
+
/Cookie.{0,10}(の使用に|に)?同意/i,
|
|
17
|
+
/このサイトはCookieを使用/,
|
|
18
|
+
/アプリで(開く|見る|読む)/,
|
|
19
|
+
/アプリをダウンロード/,
|
|
20
|
+
/アプリ内で開く/,
|
|
21
|
+
/ストアで見る/,
|
|
22
|
+
/App Store|Google Play.{0,10}(で|から)開く/i,
|
|
23
|
+
];
|
|
24
|
+
const BANNER_ID_CLASS_PATTERN = /cookie|consent|cmp[-_]|gdpr|app[-_]?banner|interstitial|smart-?banner/i;
|
|
25
|
+
/** 本文ブロックを誤って除去しないよう、この文字数を超えるブロックは対象外にする(バナーは短文が通例)。 */
|
|
26
|
+
const MAX_BANNER_TEXT_LENGTH = 400;
|
|
27
|
+
/**
|
|
28
|
+
* DOM上のCookie同意バナー/アプリ誘導インタースティシャルを除去する。
|
|
29
|
+
* 除去したブロック数を返す。
|
|
30
|
+
*/
|
|
31
|
+
export function removeConsentBanners(document) {
|
|
32
|
+
let removed = 0;
|
|
33
|
+
const candidates = Array.from(document.querySelectorAll("div, section, aside, dialog"));
|
|
34
|
+
for (const el of candidates) {
|
|
35
|
+
const text = el.textContent ?? "";
|
|
36
|
+
if (text.length === 0 || text.length > MAX_BANNER_TEXT_LENGTH)
|
|
37
|
+
continue;
|
|
38
|
+
const idClassMatch = BANNER_ID_CLASS_PATTERN.test(`${el.id ?? ""} ${el.className ?? ""}`);
|
|
39
|
+
const textMatch = BANNER_TEXT_PATTERNS.some((pattern) => pattern.test(text));
|
|
40
|
+
// id/class名パターンと文言パターンの両方が揃った場合のみ除去する(誤除去防止)
|
|
41
|
+
if (idClassMatch && textMatch) {
|
|
42
|
+
el.remove();
|
|
43
|
+
removed++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return removed;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=consentBanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consentBanner.js","sourceRoot":"","sources":["../../src/jp/consentBanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,oBAAoB,GAAG;IAC3B,SAAS;IACT,MAAM;IACN,2BAA2B;IAC3B,iBAAiB;IACjB,gBAAgB;IAChB,YAAY;IACZ,SAAS;IACT,QAAQ;IACR,uCAAuC;CACxC,CAAC;AAEF,MAAM,uBAAuB,GAAG,wEAAwE,CAAC;AAEzG,yDAAyD;AACzD,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAanC;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAmC;IACtE,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,6BAA6B,CAAC,CAAC,CAAC;IAExF,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,EAAE,CAAC,WAAW,IAAI,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,sBAAsB;YAAE,SAAS;QAExE,MAAM,YAAY,GAAG,uBAAuB,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC,CAAC;QAC1F,MAAM,SAAS,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAE7E,6CAA6C;QAC7C,IAAI,YAAY,IAAI,SAAS,EAAE,CAAC;YAC9B,EAAE,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* J1-J3: 日本語Webページ向けの正規化処理。
|
|
3
|
+
*
|
|
4
|
+
* - stripRubyAnnotations: J2 ruby正規化(DOM段階で <rt>/<rp> を除去し、読み仮名の本文二重化を防ぐ)
|
|
5
|
+
* - normalizeCjkText: J3 CJKテキスト正規化(NFKC・全角英数→半角・Turndownが挿入する
|
|
6
|
+
* CJK文字間の不要な空白/改行の除去)。Markdown化後の文字列に対して適用し、
|
|
7
|
+
* フェンスコードブロック(```)とインラインコード(`...`)の中身は変更しない。
|
|
8
|
+
*/
|
|
9
|
+
/** DOM要素の最小インターフェース(linkedom/ブラウザDOM双方と構造的に互換)。 */
|
|
10
|
+
interface RemovableElement {
|
|
11
|
+
remove(): void;
|
|
12
|
+
}
|
|
13
|
+
/** ruby要素の除去対象を検索できる最小限のDocument互換インターフェース。 */
|
|
14
|
+
export interface RubyHostDocument {
|
|
15
|
+
querySelectorAll(selectors: string): ArrayLike<RemovableElement> | Iterable<RemovableElement>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* J2: DOM上の <rt>(読み仮名)・<rp>(括弧等の補助表示)要素を除去する。
|
|
19
|
+
* Markdown変換前のDOM段階で行うことで、「漢字かんじ」のような読み仮名の
|
|
20
|
+
* 本文への混入(トークン浪費・検索性劣化)を防ぐ。
|
|
21
|
+
*/
|
|
22
|
+
export declare function stripRubyAnnotations(document: RubyHostDocument): void;
|
|
23
|
+
/**
|
|
24
|
+
* J3: CJKテキスト正規化。
|
|
25
|
+
* pre/code由来のフェンスコード・インラインコードの中身は変更せず、
|
|
26
|
+
* それ以外の地の文にのみ NFKC正規化(全角英数→半角を含む)と
|
|
27
|
+
* CJK文字間の不要な空白・改行の除去を適用する。
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeCjkText(markdown: string): string;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* J1-J3: 日本語Webページ向けの正規化処理。
|
|
3
|
+
*
|
|
4
|
+
* - stripRubyAnnotations: J2 ruby正規化(DOM段階で <rt>/<rp> を除去し、読み仮名の本文二重化を防ぐ)
|
|
5
|
+
* - normalizeCjkText: J3 CJKテキスト正規化(NFKC・全角英数→半角・Turndownが挿入する
|
|
6
|
+
* CJK文字間の不要な空白/改行の除去)。Markdown化後の文字列に対して適用し、
|
|
7
|
+
* フェンスコードブロック(```)とインラインコード(`...`)の中身は変更しない。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* J2: DOM上の <rt>(読み仮名)・<rp>(括弧等の補助表示)要素を除去する。
|
|
11
|
+
* Markdown変換前のDOM段階で行うことで、「漢字かんじ」のような読み仮名の
|
|
12
|
+
* 本文への混入(トークン浪費・検索性劣化)を防ぐ。
|
|
13
|
+
*/
|
|
14
|
+
export function stripRubyAnnotations(document) {
|
|
15
|
+
const nodes = Array.from(document.querySelectorAll("rt, rp"));
|
|
16
|
+
for (const node of nodes) {
|
|
17
|
+
node.remove();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// CJK文字クラス(tokens.ts の isCjkCodePoint と同じ範囲。全角/CJK記号のスペース(U+3000/U+FF00)は除く)
|
|
21
|
+
const CJK_CLASS = "\\u3040-\\u309f\\u30a0-\\u30ff\\u3400-\\u4dbf\\u4e00-\\u9fff\\uf900-\\ufaff\\u3001-\\u303f\\uff01-\\uffef";
|
|
22
|
+
// CJK文字同士の間にある空白・単一改行(段落区切りの空行は除く)を除去する
|
|
23
|
+
const CJK_ADJACENT_WHITESPACE = new RegExp(`(?<=[${CJK_CLASS}])(?:[ \\t]|\\r?\\n(?!\\r?\\n))+(?=[${CJK_CLASS}])`, "gu");
|
|
24
|
+
// Turndownは<strong>/<em>等をCJKに隣接して変換する際、Markdownの曖昧性回避のため
|
|
25
|
+
// "とても **重要** です" のように装飾記号の前後へ空白を挿入することがある。
|
|
26
|
+
// 日本語の地の文では強調記号の前後に空白は不要なため、装飾記号(*_~)越しにCJKが
|
|
27
|
+
// 隣接している場合もその空白を除去する(Phase 2で残課題としていたJ3の拡張)。
|
|
28
|
+
const CJK_SPACE_BEFORE_MARKER = new RegExp(`(?<=[${CJK_CLASS}])[ \\t]+(?=[*_~]{1,3}[${CJK_CLASS}])`, "gu");
|
|
29
|
+
const CJK_SPACE_AFTER_MARKER = new RegExp(`(?<=[${CJK_CLASS}][*_~]{1,3})[ \\t]+(?=[${CJK_CLASS}])`, "gu");
|
|
30
|
+
// Markdownのフェンスコードブロック / インラインコードを保護するためのパターン
|
|
31
|
+
const CODE_PATTERN = /```[\s\S]*?```|`[^`\n]*`/g;
|
|
32
|
+
/** Markdown文字列をコード区間(保護対象)とそれ以外(正規化対象)に分割する。 */
|
|
33
|
+
function splitProtectingCode(markdown) {
|
|
34
|
+
const segments = [];
|
|
35
|
+
let lastIndex = 0;
|
|
36
|
+
for (const match of markdown.matchAll(CODE_PATTERN)) {
|
|
37
|
+
const index = match.index ?? 0;
|
|
38
|
+
if (index > lastIndex) {
|
|
39
|
+
segments.push({ isCode: false, text: markdown.slice(lastIndex, index) });
|
|
40
|
+
}
|
|
41
|
+
segments.push({ isCode: true, text: match[0] });
|
|
42
|
+
lastIndex = index + match[0].length;
|
|
43
|
+
}
|
|
44
|
+
if (lastIndex < markdown.length) {
|
|
45
|
+
segments.push({ isCode: false, text: markdown.slice(lastIndex) });
|
|
46
|
+
}
|
|
47
|
+
return segments;
|
|
48
|
+
}
|
|
49
|
+
/** コード以外の地の文に対して NFKC正規化 + CJK隣接空白除去を適用する。 */
|
|
50
|
+
function normalizeProse(text) {
|
|
51
|
+
const nfkc = text.normalize("NFKC");
|
|
52
|
+
return nfkc.replace(CJK_ADJACENT_WHITESPACE, "").replace(CJK_SPACE_BEFORE_MARKER, "").replace(CJK_SPACE_AFTER_MARKER, "");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* J3: CJKテキスト正規化。
|
|
56
|
+
* pre/code由来のフェンスコード・インラインコードの中身は変更せず、
|
|
57
|
+
* それ以外の地の文にのみ NFKC正規化(全角英数→半角を含む)と
|
|
58
|
+
* CJK文字間の不要な空白・改行の除去を適用する。
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeCjkText(markdown) {
|
|
61
|
+
return splitProtectingCode(markdown)
|
|
62
|
+
.map((segment) => (segment.isCode ? segment.text : normalizeProse(segment.text)))
|
|
63
|
+
.join("");
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=normalize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../src/jp/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAYH;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAA0B;IAC7D,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAA+B,CAAC,CAAC;IAC5F,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,MAAM,SAAS,GAAG,2GAA2G,CAAC;AAE9H,wCAAwC;AACxC,MAAM,uBAAuB,GAAG,IAAI,MAAM,CACxC,QAAQ,SAAS,uCAAuC,SAAS,IAAI,EACrE,IAAI,CACL,CAAC;AAEF,0DAA0D;AAC1D,4CAA4C;AAC5C,6CAA6C;AAC7C,6CAA6C;AAC7C,MAAM,uBAAuB,GAAG,IAAI,MAAM,CAAC,QAAQ,SAAS,0BAA0B,SAAS,IAAI,EAAE,IAAI,CAAC,CAAC;AAC3G,MAAM,sBAAsB,GAAG,IAAI,MAAM,CAAC,QAAQ,SAAS,0BAA0B,SAAS,IAAI,EAAE,IAAI,CAAC,CAAC;AAE1G,8CAA8C;AAC9C,MAAM,YAAY,GAAG,2BAA2B,CAAC;AAOjD,gDAAgD;AAChD,SAAS,mBAAmB,CAAC,QAAgB;IAC3C,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QACpD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;QAC/B,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QAC3E,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAChD,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACtC,CAAC;IACD,IAAI,SAAS,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;QAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,8CAA8C;AAC9C,SAAS,cAAc,CAAC,IAAY;IAClC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACpC,OAAO,IAAI,CAAC,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC;AAC5H,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,OAAO,mBAAmB,CAAC,QAAQ,CAAC;SACjC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;SAChF,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC"}
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PolitenessManager } from "./politeness.js";
|
|
2
|
+
export type LinkSource = "sitemap" | "rss" | "page";
|
|
3
|
+
export interface LinkEntry {
|
|
4
|
+
url: string;
|
|
5
|
+
title: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface LinksResult {
|
|
8
|
+
source: LinkSource;
|
|
9
|
+
links: LinkEntry[];
|
|
10
|
+
/** MAX_LINKSを超えて切り捨てた場合true。 */
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** ページ内の<a href>リンクを抽出する(機能C: extract/dataSources.tsからも再利用される)。 */
|
|
14
|
+
export declare function extractPageLinks(html: string, baseUrl: string): LinkEntry[];
|
|
15
|
+
export interface DiscoverLinksOptions {
|
|
16
|
+
filter?: string;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/** URLからリンク一覧を発見する。sitemap → RSS/Atom → ページ内リンクの優先順で試す。 */
|
|
20
|
+
export declare function discoverLinks(url: string, politeness: PolitenessManager, options?: DiscoverLinksOptions): Promise<LinksResult>;
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* links.ts — linksツール。sitemap.xml / RSS・Atomフィード優先のリンク列挙。
|
|
3
|
+
*
|
|
4
|
+
* ページを舐めて全リンクを辿るのではなく、まずsitemap.xml/RSS・Atomフィードという
|
|
5
|
+
* 「サイト側が既に用意した索引」を優先利用することで、収集先への負荷を最小化する
|
|
6
|
+
* (plan.md §4「sitemap/RSSが在ればリンク探索はそちらを優先」)。
|
|
7
|
+
*
|
|
8
|
+
* 優先順位: robots.txt宣言のsitemap(→慣例パス`/sitemap.xml`) → ページの<link rel=alternate>
|
|
9
|
+
* が指すRSS/Atomフィード → 最終手段としてページ内の<a>リンク抽出。
|
|
10
|
+
*/
|
|
11
|
+
import { parseHTML } from "linkedom";
|
|
12
|
+
import { HttpStatusError, PrivateAddressError, RobotsDeniedError, UnsupportedContentError } from "./errors.js";
|
|
13
|
+
import { fetchPage } from "./fetcher/index.js";
|
|
14
|
+
import { assertHttpScheme, httpGet } from "./fetcher/http.js";
|
|
15
|
+
/**
|
|
16
|
+
* M5: robots拒否(RobotsDeniedError)・SSRF拒否(PrivateAddressError)は「sitemap/feedが
|
|
17
|
+
* 無いだけ」として握りつぶさず再送出すべき明示的な拒否シグナルである。
|
|
18
|
+
*
|
|
19
|
+
* 公開品質バグ修正: 一方でHttpStatusError(404/403/5xx等)は、sitemap.xml/feedが
|
|
20
|
+
* 単に存在しないという極めて一般的なケースで発生する(実際、これがsitemap→RSS→
|
|
21
|
+
* ページ内リンクへのフォールバックの主な発生源になっている)。これをAmenboError全般として
|
|
22
|
+
* 再送出してしまうと、sitemap.xmlの無い(=大半の)サイトでlinksツールが丸ごと失敗する
|
|
23
|
+
* 深刻な回帰になるため、再送出するのは拒否シグナルのみに絞る。
|
|
24
|
+
*/
|
|
25
|
+
function isFallbackBlockingError(error) {
|
|
26
|
+
return error instanceof PrivateAddressError || error instanceof RobotsDeniedError;
|
|
27
|
+
}
|
|
28
|
+
const MAX_LINKS = 200;
|
|
29
|
+
/** sitemapindexの場合、負荷を抑えるため先頭N件の子sitemapのみ辿る。 */
|
|
30
|
+
const MAX_SITEMAP_CHILDREN = 3;
|
|
31
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
32
|
+
function escapeRegExp(value) {
|
|
33
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34
|
+
}
|
|
35
|
+
/** filter未指定なら常に真。*を含む場合はglob、それ以外は部分一致。 */
|
|
36
|
+
function matchesFilter(value, filter) {
|
|
37
|
+
if (!filter)
|
|
38
|
+
return true;
|
|
39
|
+
if (filter.includes("*")) {
|
|
40
|
+
const pattern = new RegExp(`^${filter.split("*").map(escapeRegExp).join(".*")}$`, "i");
|
|
41
|
+
return pattern.test(value);
|
|
42
|
+
}
|
|
43
|
+
return value.toLowerCase().includes(filter.toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
async function fetchXml(url, timeoutMs, rewrite) {
|
|
46
|
+
const result = await httpGet(url, { timeoutMs });
|
|
47
|
+
const { document } = parseHTML(rewrite ? rewrite(result.html) : result.html);
|
|
48
|
+
return document;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* RSS 2.0の `<link>URL</link>` は、linkedomがHTMLパーサとして`<link>`をvoid要素
|
|
52
|
+
* (`<link>`/`<br>`/`<img>`等、閉じタグ不要かつ子を持てない要素)として扱ってしまうため、
|
|
53
|
+
* テキスト内容(URL)が失われる(Atomの`<link href="...">`は属性ベースなので影響を受けない)。
|
|
54
|
+
* パース前に `<link>` を衝突しない別タグ名へ機械的にリネームして回避する。
|
|
55
|
+
*/
|
|
56
|
+
function escapeRssLinkTag(xml) {
|
|
57
|
+
return xml.replace(/<link>([\s\S]*?)<\/link>/gi, "<rss-link>$1</rss-link>");
|
|
58
|
+
}
|
|
59
|
+
async function tryParseSitemap(sitemapUrl, politeness, timeoutMs, depth = 0) {
|
|
60
|
+
let document;
|
|
61
|
+
try {
|
|
62
|
+
await politeness.guard(sitemapUrl);
|
|
63
|
+
document = await fetchXml(sitemapUrl, timeoutMs);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (isFallbackBlockingError(error))
|
|
67
|
+
throw error;
|
|
68
|
+
// sitemap.xmlが存在しない(404等)のは大半のサイトで起きる通常のフォールバック経路なので
|
|
69
|
+
// ログを出さない。それ以外(タイムアウト等の予期しない失敗)のみ診断用にログする。
|
|
70
|
+
if (!(error instanceof HttpStatusError)) {
|
|
71
|
+
console.error(`sitemapの取得に失敗しました: ${sitemapUrl}`, error);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
// sitemapindex(子sitemapへの参照一覧)の場合、先頭数件のみ辿って合算する
|
|
76
|
+
const childLocs = Array.from(document.querySelectorAll("sitemapindex > sitemap > loc"));
|
|
77
|
+
if (childLocs.length > 0 && depth === 0) {
|
|
78
|
+
const results = [];
|
|
79
|
+
for (const loc of childLocs.slice(0, MAX_SITEMAP_CHILDREN)) {
|
|
80
|
+
const childUrl = loc.textContent.trim();
|
|
81
|
+
if (!childUrl)
|
|
82
|
+
continue;
|
|
83
|
+
const childLinks = await tryParseSitemap(childUrl, politeness, timeoutMs, depth + 1);
|
|
84
|
+
if (childLinks)
|
|
85
|
+
results.push(...childLinks);
|
|
86
|
+
if (results.length >= MAX_LINKS)
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
return results.length > 0 ? results : null;
|
|
90
|
+
}
|
|
91
|
+
const urlLocs = Array.from(document.querySelectorAll("urlset > url > loc"));
|
|
92
|
+
if (urlLocs.length === 0)
|
|
93
|
+
return null;
|
|
94
|
+
return urlLocs.map((loc) => ({ url: loc.textContent.trim(), title: null })).filter((entry) => entry.url.length > 0);
|
|
95
|
+
}
|
|
96
|
+
async function tryParseFeed(feedUrl, politeness, timeoutMs) {
|
|
97
|
+
let document;
|
|
98
|
+
try {
|
|
99
|
+
await politeness.guard(feedUrl);
|
|
100
|
+
// RSS 2.0の<link>はHTMLパーサにvoid要素として扱われるためリネームして回避する(下記コメント参照)
|
|
101
|
+
document = await fetchXml(feedUrl, timeoutMs, escapeRssLinkTag);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (isFallbackBlockingError(error))
|
|
105
|
+
throw error;
|
|
106
|
+
if (!(error instanceof HttpStatusError)) {
|
|
107
|
+
console.error(`フィードの取得に失敗しました: ${feedUrl}`, error);
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// RSS 2.0
|
|
112
|
+
const items = Array.from(document.querySelectorAll("item"));
|
|
113
|
+
if (items.length > 0) {
|
|
114
|
+
const entries = items
|
|
115
|
+
.map((item) => {
|
|
116
|
+
const link = item.querySelector?.("rss-link")?.textContent?.trim();
|
|
117
|
+
const title = item.querySelector?.("title")?.textContent?.trim() ?? null;
|
|
118
|
+
return link ? { url: link, title } : null;
|
|
119
|
+
})
|
|
120
|
+
.filter((entry) => entry !== null);
|
|
121
|
+
if (entries.length > 0)
|
|
122
|
+
return entries;
|
|
123
|
+
}
|
|
124
|
+
// Atom
|
|
125
|
+
const atomEntries = Array.from(document.querySelectorAll("entry"));
|
|
126
|
+
if (atomEntries.length > 0) {
|
|
127
|
+
const entries = atomEntries
|
|
128
|
+
.map((entry) => {
|
|
129
|
+
const link = entry.querySelector?.("link")?.getAttribute?.("href")?.trim();
|
|
130
|
+
const title = entry.querySelector?.("title")?.textContent?.trim() ?? null;
|
|
131
|
+
return link ? { url: link, title } : null;
|
|
132
|
+
})
|
|
133
|
+
.filter((entry) => entry !== null);
|
|
134
|
+
if (entries.length > 0)
|
|
135
|
+
return entries;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/** ページ内の<a href>リンクを抽出する(機能C: extract/dataSources.tsからも再利用される)。 */
|
|
140
|
+
export function extractPageLinks(html, baseUrl) {
|
|
141
|
+
const { document } = parseHTML(html);
|
|
142
|
+
const anchors = Array.from(document.querySelectorAll("a[href]"));
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
const links = [];
|
|
145
|
+
for (const a of anchors) {
|
|
146
|
+
const href = a.getAttribute("href");
|
|
147
|
+
if (!href || href.startsWith("#") || /^javascript:/i.test(href))
|
|
148
|
+
continue;
|
|
149
|
+
let resolved;
|
|
150
|
+
try {
|
|
151
|
+
resolved = new URL(href, baseUrl).toString();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!/^https?:/i.test(resolved))
|
|
157
|
+
continue;
|
|
158
|
+
if (seen.has(resolved))
|
|
159
|
+
continue;
|
|
160
|
+
seen.add(resolved);
|
|
161
|
+
const title = (a.textContent ?? "").replace(/\s+/g, " ").trim() || null;
|
|
162
|
+
links.push({ url: resolved, title });
|
|
163
|
+
}
|
|
164
|
+
return links;
|
|
165
|
+
}
|
|
166
|
+
function dedupeByUrl(links) {
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
const result = [];
|
|
169
|
+
for (const link of links) {
|
|
170
|
+
if (seen.has(link.url))
|
|
171
|
+
continue;
|
|
172
|
+
seen.add(link.url);
|
|
173
|
+
result.push(link);
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
function finalize(source, links, filter) {
|
|
178
|
+
const filtered = dedupeByUrl(links).filter((link) => matchesFilter(link.url, filter) || matchesFilter(link.title ?? "", filter));
|
|
179
|
+
return { source, links: filtered.slice(0, MAX_LINKS), truncated: filtered.length > MAX_LINKS };
|
|
180
|
+
}
|
|
181
|
+
/** URLからリンク一覧を発見する。sitemap → RSS/Atom → ページ内リンクの優先順で試す。 */
|
|
182
|
+
export async function discoverLinks(url, politeness, options = {}) {
|
|
183
|
+
// 公開品質バグ修正: robots.txt/sitemap取得より前にスキームを検証する。file:等を先に弾かないと
|
|
184
|
+
// origin("null"等)から `null/sitemap.xml` のような壊れたURLを組み立ててしまい、
|
|
185
|
+
// new URL()の生のTypeErrorがstderrに漏れてしまう。
|
|
186
|
+
assertHttpScheme(url);
|
|
187
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
188
|
+
const origin = new URL(url).origin;
|
|
189
|
+
// 1. sitemap優先(robots.txtのSitemap宣言 → 見つからなければ慣例パス)
|
|
190
|
+
const declaredSitemaps = await politeness.getSitemaps(url);
|
|
191
|
+
const candidateSitemaps = declaredSitemaps.length > 0 ? declaredSitemaps : [`${origin}/sitemap.xml`];
|
|
192
|
+
for (const sitemapUrl of candidateSitemaps) {
|
|
193
|
+
const links = await tryParseSitemap(sitemapUrl, politeness, timeoutMs);
|
|
194
|
+
if (links && links.length > 0) {
|
|
195
|
+
return finalize("sitemap", links, options.filter);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// 2. ページを取得し、<link rel=alternate>が指すRSS/Atomフィードを試す
|
|
199
|
+
await politeness.guard(url);
|
|
200
|
+
const pageResult = await fetchPage(url, { timeoutMs });
|
|
201
|
+
if ("notModified" in pageResult) {
|
|
202
|
+
return finalize("page", [], options.filter);
|
|
203
|
+
}
|
|
204
|
+
// 機能B: 非HTMLコンテンツはハンドオフ応答(fetchツール向け)の対象であり、linksツールでは
|
|
205
|
+
// DOMが無くリンク抽出できない。以前のfetchPage(HTML以外はUnsupportedContentError)と
|
|
206
|
+
// 同じ挙動を維持する(linksツールの応答形式は変えない)。
|
|
207
|
+
if ("handoff" in pageResult) {
|
|
208
|
+
throw new UnsupportedContentError(pageResult.finalUrl, pageResult.contentType ?? "(不明)");
|
|
209
|
+
}
|
|
210
|
+
const { document } = parseHTML(pageResult.html);
|
|
211
|
+
const feedLinkEl = document.querySelector('link[rel="alternate"][type="application/rss+xml"], link[rel="alternate"][type="application/atom+xml"]');
|
|
212
|
+
const feedHref = feedLinkEl?.getAttribute("href");
|
|
213
|
+
if (feedHref) {
|
|
214
|
+
const feedUrl = new URL(feedHref, pageResult.finalUrl).toString();
|
|
215
|
+
const feedLinks = await tryParseFeed(feedUrl, politeness, timeoutMs);
|
|
216
|
+
if (feedLinks && feedLinks.length > 0) {
|
|
217
|
+
return finalize("rss", feedLinks, options.filter);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// 3. 最終手段: ページ内リンク抽出
|
|
221
|
+
return finalize("page", extractPageLinks(pageResult.html, pageResult.finalUrl), options.filter);
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI併設対応: ドメイン毎の最終リクエスト時刻をプロセス間で共有するための永続化ストア。
|
|
3
|
+
* CLIは1コマンド=1プロセスのためインメモリのlastRequestAtだけでは直列/最小間隔制御が
|
|
4
|
+
* プロセスを跨いで機能しない。cache.ts(host_requestsテーブル)をこの形で注入する想定。
|
|
5
|
+
*
|
|
6
|
+
* code-reviewer指摘(重要な制約): waitTurn内の「store読み取り→待機時間の算出→sleep→
|
|
7
|
+
* store書き込み」は、同一プロセス内ではlocks(直列キュー)で完全に直列化されるが、
|
|
8
|
+
* 複数プロセス(MCPサーバー+複数のCLI実行等)が同一ホストへほぼ同時にこの一連の処理へ
|
|
9
|
+
* 入った場合、プロセスを跨いだ排他制御は無い(read-modify-writeがアトミックではない)ため
|
|
10
|
+
* 最小間隔がすり抜けうる。低負荷"優先"のベストエフォートな安全網であり、分散ロックのような
|
|
11
|
+
* 複雑な再試行機構は追加しない(単一プロセス内の直列化という強い保証・低実装コストという
|
|
12
|
+
* コンセプトを崩さないため)。cache.ts側のsetHostLastRequestAtはSQLのMAXで時刻の巻き戻りだけは
|
|
13
|
+
* 防いでいるが、read-modify-writeの競合そのものは解消しない。
|
|
14
|
+
*/
|
|
15
|
+
export interface PolitenessStore {
|
|
16
|
+
getLastRequestAt(host: string): number | null;
|
|
17
|
+
setLastRequestAt(host: string, at: number): void;
|
|
18
|
+
}
|
|
19
|
+
export interface PolitenessOptions {
|
|
20
|
+
/** ドメイン毎の最小リクエスト間隔(ミリ秒、既定1000ms)。robots.txtのCrawl-Delayがこれより長ければそちらを優先。 */
|
|
21
|
+
minIntervalMs?: number;
|
|
22
|
+
/** robots.txtキャッシュのTTL(ミリ秒、既定1時間)。 */
|
|
23
|
+
robotsTtlMs?: number;
|
|
24
|
+
/** テスト用の時刻取得関数差し替え。 */
|
|
25
|
+
now?: () => number;
|
|
26
|
+
/** テスト用のsleep関数差し替え。 */
|
|
27
|
+
sleep?: (ms: number) => Promise<void>;
|
|
28
|
+
/** 未指定時は従来通りインメモリのみでレート制御する(完全後方互換)。 */
|
|
29
|
+
store?: PolitenessStore;
|
|
30
|
+
}
|
|
31
|
+
export declare class PolitenessManager {
|
|
32
|
+
private readonly minIntervalMs;
|
|
33
|
+
private readonly robotsTtlMs;
|
|
34
|
+
private readonly now;
|
|
35
|
+
private readonly sleep;
|
|
36
|
+
private readonly store;
|
|
37
|
+
private readonly robotsCache;
|
|
38
|
+
private readonly locks;
|
|
39
|
+
private readonly lastRequestAt;
|
|
40
|
+
constructor(options?: PolitenessOptions);
|
|
41
|
+
/** robots.txtとレート制御の両方を確認し、アクセス可能になるまで待機する。拒否時はRobotsDeniedErrorを投げる。 */
|
|
42
|
+
guard(url: string): Promise<void>;
|
|
43
|
+
/** robots.txtのみ確認する(waitTurnは行わない)。 */
|
|
44
|
+
checkRobotsAllowed(url: string): Promise<void>;
|
|
45
|
+
/** robots.txtが宣言するSitemap URLの一覧を返す(linksツールのsitemap優先探索用)。 */
|
|
46
|
+
getSitemaps(url: string): Promise<string[]>;
|
|
47
|
+
/**
|
|
48
|
+
* ドメイン毎の直列キュー+最小間隔(robots.txtのCrawl-Delay考慮)を適用して順番を待つ。
|
|
49
|
+
* 直列キュー(locks)による直列化はプロセス内のみ有効。store(PolitenessStore)経由の
|
|
50
|
+
* プロセス間共有はread-modify-writeがアトミックではないベストエフォート(PolitenessStoreの
|
|
51
|
+
* JSDoc参照)。
|
|
52
|
+
*/
|
|
53
|
+
waitTurn(url: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* N8: robotsCache/locks/lastRequestAtは訪問したホスト毎に1エントリずつ増え、長時間稼働する
|
|
56
|
+
* MCPサーバープロセスでは無制限に増加しうる。robotsTtlMsを「エントリの生存期間」の目安として
|
|
57
|
+
* 転用し、しばらく使われていないホストの状態を掃除する。
|
|
58
|
+
*
|
|
59
|
+
* locksは、まだ完了していない(settled=false)呼び出しが並んでいる可能性があるホストを
|
|
60
|
+
* 誤って削除すると直列化が壊れる(新しい呼び出しが空のロックを掴んで先に進んでしまう)ため、
|
|
61
|
+
* settled=trueのエントリのみ削除する。
|
|
62
|
+
*/
|
|
63
|
+
private pruneStaleHostState;
|
|
64
|
+
private getCrawlDelayMs;
|
|
65
|
+
private getRobots;
|
|
66
|
+
}
|