dsh-ab-wechat-scrape 0.4.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 +330 -0
- package/cordis.patch.yml +8 -0
- package/lib/clean.d.ts +32 -0
- package/lib/clean.d.ts.map +1 -0
- package/lib/clean.js +193 -0
- package/lib/clean.js.map +1 -0
- package/lib/document.d.ts +27 -0
- package/lib/document.d.ts.map +1 -0
- package/lib/document.js +79 -0
- package/lib/document.js.map +1 -0
- package/lib/fetch.d.ts +84 -0
- package/lib/fetch.d.ts.map +1 -0
- package/lib/fetch.js +238 -0
- package/lib/fetch.js.map +1 -0
- package/lib/filename.d.ts +36 -0
- package/lib/filename.d.ts.map +1 -0
- package/lib/filename.js +92 -0
- package/lib/filename.js.map +1 -0
- package/lib/html.d.ts +114 -0
- package/lib/html.d.ts.map +1 -0
- package/lib/html.js +402 -0
- package/lib/html.js.map +1 -0
- package/lib/index.d.ts +219 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +2523 -0
- package/lib/index.js.map +1 -0
- package/lib/list.d.ts +55 -0
- package/lib/list.d.ts.map +1 -0
- package/lib/list.js +191 -0
- package/lib/list.js.map +1 -0
- package/lib/markdown.d.ts +20 -0
- package/lib/markdown.d.ts.map +1 -0
- package/lib/markdown.js +250 -0
- package/lib/markdown.js.map +1 -0
- package/lib/render.d.ts +103 -0
- package/lib/render.d.ts.map +1 -0
- package/lib/render.js +209 -0
- package/lib/render.js.map +1 -0
- package/lib/store.d.ts +50 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +173 -0
- package/lib/store.js.map +1 -0
- package/lib/types.d.ts +133 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +8 -0
- package/lib/types.js.map +1 -0
- package/package.json +91 -0
- package/tsconfig.json +30 -0
- package/tsdown.config.ts +18 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Markdown record one article becomes: the file that is written and the text
|
|
3
|
+
* the model reads.
|
|
4
|
+
*
|
|
5
|
+
* They are **not** the same text. The file holds the whole body; the model reads a
|
|
6
|
+
* bounded head of it plus the path, because a call that carried whole articles
|
|
7
|
+
* would pay their full length in context and the session log would carry it too.
|
|
8
|
+
* Both render the same facts, so a saved article and the call that produced it
|
|
9
|
+
* never disagree about what was retrieved.
|
|
10
|
+
* @module @deepseek-ai/dsh-ab-wechat-scrape/document
|
|
11
|
+
*/
|
|
12
|
+
import type { DraftedArticle, WechatArticle } from './types.ts';
|
|
13
|
+
/**
|
|
14
|
+
* Render the Markdown file one article is saved as. This is the complete record:
|
|
15
|
+
* the whole body, never a window of it.
|
|
16
|
+
* @param article - the article as it was built, body included.
|
|
17
|
+
* @returns YAML front matter, the full body, and the image list.
|
|
18
|
+
*/
|
|
19
|
+
export declare function articleDocument(article: DraftedArticle): string;
|
|
20
|
+
/**
|
|
21
|
+
* Render the model-facing text of one saved article: the facts, a head of the
|
|
22
|
+
* body, how long the body is, and where the whole of it lives.
|
|
23
|
+
* @param article - the article's canonical value, saved path included.
|
|
24
|
+
* @returns the article facts, the excerpt, the body's length, and the saved path.
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderArticle(article: WechatArticle): string;
|
|
27
|
+
//# sourceMappingURL=document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAoB,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAgCjF;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAc/D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAU5D"}
|
package/lib/document.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Markdown record one article becomes: the file that is written and the text
|
|
3
|
+
* the model reads.
|
|
4
|
+
*
|
|
5
|
+
* They are **not** the same text. The file holds the whole body; the model reads a
|
|
6
|
+
* bounded head of it plus the path, because a call that carried whole articles
|
|
7
|
+
* would pay their full length in context and the session log would carry it too.
|
|
8
|
+
* Both render the same facts, so a saved article and the call that produced it
|
|
9
|
+
* never disagree about what was retrieved.
|
|
10
|
+
* @module @deepseek-ai/dsh-ab-wechat-scrape/document
|
|
11
|
+
*/
|
|
12
|
+
/** Quote one value as a YAML double-quoted scalar, which is a JSON string. */
|
|
13
|
+
function yamlString(value) {
|
|
14
|
+
return JSON.stringify(value);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The facts shared by the saved file and the model-facing text.
|
|
18
|
+
* @param article - the article's canonical value.
|
|
19
|
+
* @param body - the text to place in the body position.
|
|
20
|
+
* @returns the heading, the metadata line, and the body.
|
|
21
|
+
*/
|
|
22
|
+
function articleLines(article, body) {
|
|
23
|
+
const meta = [];
|
|
24
|
+
if (article.account !== '')
|
|
25
|
+
meta.push('公众号:' + article.account);
|
|
26
|
+
if (article.author !== '')
|
|
27
|
+
meta.push('作者:' + article.author);
|
|
28
|
+
if (article.publishTime !== '')
|
|
29
|
+
meta.push('发布:' + article.publishTime);
|
|
30
|
+
meta.push('链接:' + article.resolvedUrl);
|
|
31
|
+
const lines = [
|
|
32
|
+
'# ' + (article.title === '' ? '(无标题)' : article.title),
|
|
33
|
+
'',
|
|
34
|
+
meta.join(' | '),
|
|
35
|
+
'',
|
|
36
|
+
body,
|
|
37
|
+
];
|
|
38
|
+
if (article.images.length > 0) {
|
|
39
|
+
lines.push('', '## 图片', ...article.images.map((src, index) => String(index + 1) + '. ' + src));
|
|
40
|
+
}
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Render the Markdown file one article is saved as. This is the complete record:
|
|
45
|
+
* the whole body, never a window of it.
|
|
46
|
+
* @param article - the article as it was built, body included.
|
|
47
|
+
* @returns YAML front matter, the full body, and the image list.
|
|
48
|
+
*/
|
|
49
|
+
export function articleDocument(article) {
|
|
50
|
+
return [
|
|
51
|
+
'---',
|
|
52
|
+
'title: ' + yamlString(article.title),
|
|
53
|
+
'account: ' + yamlString(article.account),
|
|
54
|
+
'author: ' + yamlString(article.author),
|
|
55
|
+
'publishTime: ' + yamlString(article.publishTime),
|
|
56
|
+
'source: ' + yamlString(article.resolvedUrl),
|
|
57
|
+
'cover: ' + yamlString(article.cover),
|
|
58
|
+
'chars: ' + String(article.chars),
|
|
59
|
+
'rendered: ' + String(article.rendered),
|
|
60
|
+
'---',
|
|
61
|
+
...articleLines(article, article.body),
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Render the model-facing text of one saved article: the facts, a head of the
|
|
66
|
+
* body, how long the body is, and where the whole of it lives.
|
|
67
|
+
* @param article - the article's canonical value, saved path included.
|
|
68
|
+
* @returns the article facts, the excerpt, the body's length, and the saved path.
|
|
69
|
+
*/
|
|
70
|
+
export function renderArticle(article) {
|
|
71
|
+
const lines = articleLines(article, article.excerpt);
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push(article.excerptClipped
|
|
74
|
+
? `(以上为节选,正文共 ${String(article.chars)} 字;完整内容见文件)`
|
|
75
|
+
: `(正文共 ${String(article.chars)} 字)`);
|
|
76
|
+
lines.push('文件:' + article.file);
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.js","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,8EAA8E;AAC9E,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,OAAyB,EAAE,IAAY;IAC3D,MAAM,IAAI,GAAa,EAAE,CAAA;IACzB,IAAI,OAAO,CAAC,OAAO,KAAK,EAAE;QAAE,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/D,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE;QAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC5D,IAAI,OAAO,CAAC,WAAW,KAAK,EAAE;QAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IACtE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IACtC,MAAM,KAAK,GAAG;QACZ,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;QACvD,EAAE;QACF,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAChB,EAAE;QACF,IAAI;KACL,CAAA;IACD,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,GAAG,GAAG,CAAC,CAAC,CAAA;IAChG,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,OAAuB;IACrD,OAAO;QACL,KAAK;QACL,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC;QACrC,WAAW,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC;QACzC,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;QACvC,eAAe,GAAG,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC;QACjD,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC;QAC5C,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC;QACrC,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;QACjC,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;QACvC,KAAK;QACL,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC;KACvC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,OAAsB;IAClD,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;IACpD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACd,KAAK,CAAC,IAAI,CACR,OAAO,CAAC,cAAc;QACpB,CAAC,CAAC,cAAc,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa;QAClD,CAAC,CAAC,QAAQ,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CACvC,CAAA;IACD,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC"}
|
package/lib/fetch.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin's HTTP layer: browser-like request headers, a session cookie jar,
|
|
3
|
+
* a minimum interval between requests, a cool-down a refused request imposes on
|
|
4
|
+
* the rest of the instance, bounded retries that honour the server's own retry
|
|
5
|
+
* hint, and the deployment's timeout. Kept separate from page parsing, and
|
|
6
|
+
* driven through injected transport and clock ports so tests never sleep or
|
|
7
|
+
* reach the network.
|
|
8
|
+
* @module @deepseek-ai/dsh-ab-wechat-scrape/fetch
|
|
9
|
+
*/
|
|
10
|
+
/** Deployment values the request layer reads. */
|
|
11
|
+
export interface FetchPolicy {
|
|
12
|
+
/** User agent every request sends. */
|
|
13
|
+
userAgent: string;
|
|
14
|
+
/** Referer every request sends; omitted when empty. */
|
|
15
|
+
referer: string;
|
|
16
|
+
/** Cookie header every request sends; omitted when empty. */
|
|
17
|
+
cookie: string;
|
|
18
|
+
/** Whether cookies a response sets are replayed for the rest of the instance. */
|
|
19
|
+
cookieJar: boolean;
|
|
20
|
+
/** Minimum milliseconds between two requests from this plugin instance. */
|
|
21
|
+
minIntervalMs: number;
|
|
22
|
+
/** Random milliseconds added to each wait, so the cadence is not a fixed period. */
|
|
23
|
+
minIntervalJitterMs: number;
|
|
24
|
+
/** Per-request timeout in milliseconds. */
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
/** Retries after the first attempt. */
|
|
27
|
+
maxRetries: number;
|
|
28
|
+
/** Base delay of the exponential backoff, in milliseconds. */
|
|
29
|
+
backoffBaseMs: number;
|
|
30
|
+
/** Longest wait a server-supplied retry hint may impose, in milliseconds. */
|
|
31
|
+
maxRetryAfterMs: number;
|
|
32
|
+
}
|
|
33
|
+
/** One HTML response the caller parses. */
|
|
34
|
+
export interface HtmlResponse {
|
|
35
|
+
/** Final URL after redirects. */
|
|
36
|
+
url: string;
|
|
37
|
+
/** HTTP status of the final response. */
|
|
38
|
+
status: number;
|
|
39
|
+
/** Response body text. */
|
|
40
|
+
body: string;
|
|
41
|
+
/** `Set-Cookie` header values the response carried, in the order it sent them. */
|
|
42
|
+
setCookie: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
/** Injected transport, clock, and jitter source. */
|
|
45
|
+
export interface FetchDeps {
|
|
46
|
+
/** Transport used for one attempt. */
|
|
47
|
+
fetchImpl: typeof fetch;
|
|
48
|
+
/** Sleep used between attempts. */
|
|
49
|
+
sleep: (ms: number) => Promise<void>;
|
|
50
|
+
/** Clock in milliseconds. */
|
|
51
|
+
now: () => number;
|
|
52
|
+
/** Random source in [0, 1) used to jitter the backoff. */
|
|
53
|
+
random: () => number;
|
|
54
|
+
}
|
|
55
|
+
/** The request entry point one plugin instance owns. */
|
|
56
|
+
export interface Fetcher {
|
|
57
|
+
/**
|
|
58
|
+
* Retrieve one page, pacing and retrying per the deployment policy.
|
|
59
|
+
* @param url - the page to retrieve.
|
|
60
|
+
* @param signal - caller cancellation.
|
|
61
|
+
* @returns the final response, including non-2xx statuses the caller inspects.
|
|
62
|
+
*/
|
|
63
|
+
get(url: string, signal?: AbortSignal): Promise<HtmlResponse>;
|
|
64
|
+
/**
|
|
65
|
+
* Wait until the deployment's minimum interval has elapsed.
|
|
66
|
+
* @param signal - caller cancellation.
|
|
67
|
+
* @returns a promise that settles once a request may start.
|
|
68
|
+
*/
|
|
69
|
+
pace(signal?: AbortSignal): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Hold every later request back by one cool-down, because a page WeChat
|
|
72
|
+
* refused says the instance is already asking too fast.
|
|
73
|
+
* @param ms - milliseconds the instance stands down for.
|
|
74
|
+
*/
|
|
75
|
+
coolDown(ms: number): void;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build the plugin instance's fetcher.
|
|
79
|
+
* @param policy - the deployment's header, pacing, retry, and timeout values.
|
|
80
|
+
* @param overrides - test ports replacing the default transport, clock, or jitter.
|
|
81
|
+
* @returns the fetcher whose pace() the browser renderer shares.
|
|
82
|
+
*/
|
|
83
|
+
export declare function createFetcher(policy: FetchPolicy, overrides?: Partial<FetchDeps>): Fetcher;
|
|
84
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC1B,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAA;IACf,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAA;IACd,iFAAiF;IACjF,SAAS,EAAE,OAAO,CAAA;IAClB,2EAA2E;IAC3E,aAAa,EAAE,MAAM,CAAA;IACrB,oFAAoF;IACpF,mBAAmB,EAAE,MAAM,CAAA;IAC3B,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAA;IACd,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAED,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,sCAAsC;IACtC,SAAS,EAAE,OAAO,KAAK,CAAA;IACvB,mCAAmC;IACnC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpC,6BAA6B;IAC7B,GAAG,EAAE,MAAM,MAAM,CAAA;IACjB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,MAAM,CAAA;CACrB;AAED,wDAAwD;AACxD,MAAM,WAAW,OAAO;IACtB;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC7D;;;;OAIG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AA2JD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,GAAE,OAAO,CAAC,SAAS,CAAM,GAAG,OAAO,CA4E9F"}
|
package/lib/fetch.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin's HTTP layer: browser-like request headers, a session cookie jar,
|
|
3
|
+
* a minimum interval between requests, a cool-down a refused request imposes on
|
|
4
|
+
* the rest of the instance, bounded retries that honour the server's own retry
|
|
5
|
+
* hint, and the deployment's timeout. Kept separate from page parsing, and
|
|
6
|
+
* driven through injected transport and clock ports so tests never sleep or
|
|
7
|
+
* reach the network.
|
|
8
|
+
* @module @deepseek-ai/dsh-ab-wechat-scrape/fetch
|
|
9
|
+
*/
|
|
10
|
+
/** Statuses worth another attempt: rate limits and server-side failures. */
|
|
11
|
+
const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
12
|
+
/**
|
|
13
|
+
* Headers a browser sends when navigating to a page. The client hints are
|
|
14
|
+
* deliberately absent: they must agree with the configured user agent, and a
|
|
15
|
+
* mismatch is a stronger signal than their absence. `sec-fetch-site` is absent
|
|
16
|
+
* too, because it describes the navigation the request belongs to and is set
|
|
17
|
+
* per request.
|
|
18
|
+
*/
|
|
19
|
+
const NAVIGATION_HEADERS = {
|
|
20
|
+
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
21
|
+
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
22
|
+
'cache-control': 'max-age=0',
|
|
23
|
+
'sec-fetch-dest': 'document',
|
|
24
|
+
'sec-fetch-mode': 'navigate',
|
|
25
|
+
'sec-fetch-user': '?1',
|
|
26
|
+
'upgrade-insecure-requests': '1',
|
|
27
|
+
};
|
|
28
|
+
/** `sec-fetch-site` for the instance's first navigation, which no page referred. */
|
|
29
|
+
const FIRST_NAVIGATION_SITE = 'none';
|
|
30
|
+
/** `sec-fetch-site` for every navigation after the first, which a fetched page referred. */
|
|
31
|
+
const REFERRED_NAVIGATION_SITE = 'same-origin';
|
|
32
|
+
const DEFAULT_DEPS = {
|
|
33
|
+
fetchImpl: fetch,
|
|
34
|
+
sleep: (ms) => new Promise(resolve => { setTimeout(resolve, ms); }),
|
|
35
|
+
now: () => Date.now(),
|
|
36
|
+
random: () => Math.random(),
|
|
37
|
+
};
|
|
38
|
+
/** Whether the caller already cancelled; a function call keeps TS from narrowing the flag. */
|
|
39
|
+
function cancelled(signal) {
|
|
40
|
+
return signal !== undefined && signal.aborted;
|
|
41
|
+
}
|
|
42
|
+
/** A cancellation error the caller can distinguish from a failed request. */
|
|
43
|
+
function abortError() {
|
|
44
|
+
const error = new Error('wechat: the call was cancelled');
|
|
45
|
+
error.name = 'AbortError';
|
|
46
|
+
return error;
|
|
47
|
+
}
|
|
48
|
+
/** Milliseconds to wait before one retry, jittered so parallel callers do not sync. */
|
|
49
|
+
function backoffDelay(base, attempt, random) {
|
|
50
|
+
return Math.round(base * 2 ** attempt * (0.5 + random()));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Milliseconds to wait before the next attempt. A usable server hint wins,
|
|
54
|
+
* capped by the deployment, because it is the rate the server is willing to
|
|
55
|
+
* serve; otherwise the jittered backoff applies.
|
|
56
|
+
*/
|
|
57
|
+
function retryDelay(policy, attempt, retryAfterMs, random) {
|
|
58
|
+
if (retryAfterMs > 0)
|
|
59
|
+
return Math.min(retryAfterMs, policy.maxRetryAfterMs);
|
|
60
|
+
return backoffDelay(policy.backoffBaseMs, attempt, random);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read a Retry-After header, in either of its two legal forms.
|
|
64
|
+
* @param value - the header value, or null when the response carried none.
|
|
65
|
+
* @param now - current time in milliseconds, for the HTTP-date form.
|
|
66
|
+
* @returns milliseconds to wait, or 0 when the header is absent or unusable.
|
|
67
|
+
*/
|
|
68
|
+
function parseRetryAfter(value, now) {
|
|
69
|
+
if (value === null || value.trim() === '')
|
|
70
|
+
return 0;
|
|
71
|
+
const seconds = Number(value);
|
|
72
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
73
|
+
return Math.round(seconds * 1000);
|
|
74
|
+
const date = Date.parse(value);
|
|
75
|
+
return Number.isNaN(date) ? 0 : Math.max(0, date - now);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Every `Set-Cookie` value one response carried. `Headers.getSetCookie` is the
|
|
79
|
+
* only accessor that returns more than one; a runtime without it yields the
|
|
80
|
+
* single folded header, which is still better than dropping the cookies.
|
|
81
|
+
* @param headers - the response headers.
|
|
82
|
+
* @returns the header values, in the order the server sent them.
|
|
83
|
+
*/
|
|
84
|
+
function setCookieValues(headers) {
|
|
85
|
+
const multi = headers.getSetCookie;
|
|
86
|
+
if (typeof multi === 'function')
|
|
87
|
+
return multi.call(headers);
|
|
88
|
+
const single = headers.get('set-cookie');
|
|
89
|
+
return single === null ? [] : [single];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read the name and value out of one `Set-Cookie` value, ignoring its
|
|
93
|
+
* attributes. A deletion (`Max-Age=0`) is read like any other; the server's own
|
|
94
|
+
* later value replaces it, so a stale entry cannot outlive the response that
|
|
95
|
+
* removed it on any page the walk reaches afterwards.
|
|
96
|
+
* @param value - one `Set-Cookie` header value.
|
|
97
|
+
* @returns the cookie pair, or undefined when the value carries no `name=value`.
|
|
98
|
+
*/
|
|
99
|
+
function cookiePair(value) {
|
|
100
|
+
const pair = value.split(';', 1)[0]?.trim() ?? '';
|
|
101
|
+
const equals = pair.indexOf('=');
|
|
102
|
+
if (equals <= 0)
|
|
103
|
+
return undefined;
|
|
104
|
+
return [pair.slice(0, equals).trim(), pair.slice(equals + 1).trim()];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The cookie header one request sends: the deployment's own cookies first, in
|
|
108
|
+
* the order it wrote them, then every cookie this instance has been given that
|
|
109
|
+
* the deployment's header does not already name.
|
|
110
|
+
* @param configured - the cookies parsed from the deployment's header.
|
|
111
|
+
* @param jar - cookies harvested from earlier responses, keyed by name.
|
|
112
|
+
* @returns the header value, or an empty string when no cookie is held.
|
|
113
|
+
*/
|
|
114
|
+
function cookieHeader(configured, jar) {
|
|
115
|
+
const named = new Set(configured.map(pair => pair[0]));
|
|
116
|
+
const parts = configured.map(pair => pair[0] + '=' + pair[1]);
|
|
117
|
+
for (const [name, value] of jar) {
|
|
118
|
+
if (!named.has(name))
|
|
119
|
+
parts.push(name + '=' + value);
|
|
120
|
+
}
|
|
121
|
+
return parts.join('; ');
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parse a Cookie header into ordered pairs.
|
|
125
|
+
* @param header - the deployment's Cookie header.
|
|
126
|
+
* @returns one pair per non-empty entry it carries.
|
|
127
|
+
*/
|
|
128
|
+
function configuredCookies(header) {
|
|
129
|
+
if (header.trim() === '')
|
|
130
|
+
return [];
|
|
131
|
+
const pairs = [];
|
|
132
|
+
for (const part of header.split(';')) {
|
|
133
|
+
const pair = cookiePair(part);
|
|
134
|
+
if (pair !== undefined)
|
|
135
|
+
pairs.push(pair);
|
|
136
|
+
}
|
|
137
|
+
return pairs;
|
|
138
|
+
}
|
|
139
|
+
/** Sleep that settles early when the caller cancels. */
|
|
140
|
+
function sleepWithSignal(ms, sleep, signal) {
|
|
141
|
+
if (signal === undefined)
|
|
142
|
+
return sleep(ms);
|
|
143
|
+
if (signal.aborted)
|
|
144
|
+
return Promise.reject(abortError());
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const onAbort = () => { reject(abortError()); };
|
|
147
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
148
|
+
sleep(ms).then(() => { signal.removeEventListener('abort', onAbort); resolve(); }, (error) => { signal.removeEventListener('abort', onAbort); reject(error); });
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Build the plugin instance's fetcher.
|
|
153
|
+
* @param policy - the deployment's header, pacing, retry, and timeout values.
|
|
154
|
+
* @param overrides - test ports replacing the default transport, clock, or jitter.
|
|
155
|
+
* @returns the fetcher whose pace() the browser renderer shares.
|
|
156
|
+
*/
|
|
157
|
+
export function createFetcher(policy, overrides = {}) {
|
|
158
|
+
const deps = { ...DEFAULT_DEPS, ...overrides };
|
|
159
|
+
const configured = configuredCookies(policy.cookie);
|
|
160
|
+
const jar = new Map();
|
|
161
|
+
let lastRequestAt = Number.NEGATIVE_INFINITY;
|
|
162
|
+
let resumeAt = Number.NEGATIVE_INFINITY;
|
|
163
|
+
let navigations = 0;
|
|
164
|
+
const pace = async (signal) => {
|
|
165
|
+
// The jitter is re-drawn for every wait, so a batch is not a fixed-period
|
|
166
|
+
// request train the way a constant interval would be.
|
|
167
|
+
const jitter = policy.minIntervalJitterMs > 0 ? Math.round(deps.random() * policy.minIntervalJitterMs) : 0;
|
|
168
|
+
const earliest = Math.max(lastRequestAt + policy.minIntervalMs + jitter, resumeAt);
|
|
169
|
+
const wait = earliest - deps.now();
|
|
170
|
+
if (wait > 0)
|
|
171
|
+
await sleepWithSignal(wait, deps.sleep, signal);
|
|
172
|
+
lastRequestAt = deps.now();
|
|
173
|
+
};
|
|
174
|
+
const coolDown = (ms) => {
|
|
175
|
+
if (ms > 0)
|
|
176
|
+
resumeAt = Math.max(resumeAt, deps.now() + ms);
|
|
177
|
+
};
|
|
178
|
+
const once = async (url, signal) => {
|
|
179
|
+
const timeout = AbortSignal.timeout(policy.timeoutMs);
|
|
180
|
+
const combined = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
181
|
+
const headers = {
|
|
182
|
+
...NAVIGATION_HEADERS,
|
|
183
|
+
'user-agent': policy.userAgent,
|
|
184
|
+
// A browser sends the first navigation of a tab unreferred and every
|
|
185
|
+
// navigation after it as coming from the page it was already on.
|
|
186
|
+
'sec-fetch-site': navigations === 0 ? FIRST_NAVIGATION_SITE : REFERRED_NAVIGATION_SITE,
|
|
187
|
+
};
|
|
188
|
+
navigations += 1;
|
|
189
|
+
if (policy.referer !== '')
|
|
190
|
+
headers.referer = policy.referer;
|
|
191
|
+
const cookie = policy.cookieJar ? cookieHeader(configured, jar) : policy.cookie;
|
|
192
|
+
if (cookie !== '')
|
|
193
|
+
headers.cookie = cookie;
|
|
194
|
+
const response = await deps.fetchImpl(url, { headers, redirect: 'follow', signal: combined });
|
|
195
|
+
const setCookie = setCookieValues(response.headers);
|
|
196
|
+
if (policy.cookieJar) {
|
|
197
|
+
for (const value of setCookie) {
|
|
198
|
+
const pair = cookiePair(value);
|
|
199
|
+
if (pair !== undefined)
|
|
200
|
+
jar.set(pair[0], pair[1]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const body = await response.text();
|
|
204
|
+
return {
|
|
205
|
+
response: { url: response.url === '' ? url : response.url, status: response.status, body, setCookie },
|
|
206
|
+
retryAfterMs: parseRetryAfter(response.headers.get('retry-after'), deps.now()),
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
const get = async (url, signal) => {
|
|
210
|
+
if (cancelled(signal))
|
|
211
|
+
throw abortError();
|
|
212
|
+
let lastError;
|
|
213
|
+
let delayMs = 0;
|
|
214
|
+
for (let attempt = 0; attempt <= policy.maxRetries; attempt += 1) {
|
|
215
|
+
if (attempt > 0)
|
|
216
|
+
await sleepWithSignal(delayMs, deps.sleep, signal);
|
|
217
|
+
await pace(signal);
|
|
218
|
+
try {
|
|
219
|
+
const { response, retryAfterMs } = await once(url, signal);
|
|
220
|
+
if (RETRYABLE_STATUS.has(response.status) && attempt < policy.maxRetries) {
|
|
221
|
+
lastError = new Error('wechat: HTTP ' + response.status + ' from ' + url);
|
|
222
|
+
delayMs = retryDelay(policy, attempt, retryAfterMs, deps.random);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
return response;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (cancelled(signal))
|
|
229
|
+
throw error;
|
|
230
|
+
lastError = error;
|
|
231
|
+
delayMs = backoffDelay(policy.backoffBaseMs, attempt, deps.random);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
throw lastError instanceof Error ? lastError : new Error('wechat: the request to ' + url + ' failed');
|
|
235
|
+
};
|
|
236
|
+
return { get, pace, coolDown };
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=fetch.js.map
|
package/lib/fetch.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAyEH,4EAA4E;AAC5E,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;AAErE;;;;;;GAMG;AACH,MAAM,kBAAkB,GAAqC;IAC3D,MAAM,EAAE,iEAAiE;IACzE,iBAAiB,EAAE,yBAAyB;IAC5C,eAAe,EAAE,WAAW;IAC5B,gBAAgB,EAAE,UAAU;IAC5B,gBAAgB,EAAE,UAAU;IAC5B,gBAAgB,EAAE,IAAI;IACtB,2BAA2B,EAAE,GAAG;CACjC,CAAA;AAED,oFAAoF;AACpF,MAAM,qBAAqB,GAAG,MAAM,CAAA;AAEpC,4FAA4F;AAC5F,MAAM,wBAAwB,GAAG,aAAa,CAAA;AAE9C,MAAM,YAAY,GAAc;IAC9B,SAAS,EAAE,KAAK;IAChB,KAAK,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA,CAAC,CAAC,CAAC;IAChF,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;IACrB,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;CAC5B,CAAA;AAED,8FAA8F;AAC9F,SAAS,SAAS,CAAC,MAA+B;IAChD,OAAO,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,OAAO,CAAA;AAC/C,CAAC;AAED,6EAA6E;AAC7E,SAAS,UAAU;IACjB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAA;IACzD,KAAK,CAAC,IAAI,GAAG,YAAY,CAAA;IACzB,OAAO,KAAK,CAAA;AACd,CAAC;AAED,uFAAuF;AACvF,SAAS,YAAY,CAAC,IAAY,EAAE,OAAe,EAAE,MAAoB;IACvE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,GAAG,GAAG,MAAM,EAAE,CAAC,CAAC,CAAA;AAC3D,CAAC;AAED;;;;GAIG;AACH,SAAS,UAAU,CAAC,MAAmB,EAAE,OAAe,EAAE,YAAoB,EAAE,MAAoB;IAClG,IAAI,YAAY,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,eAAe,CAAC,CAAA;IAC3E,OAAO,YAAY,CAAC,MAAM,CAAC,aAAa,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;GAKG;AACH,SAAS,eAAe,CAAC,KAAoB,EAAE,GAAW;IACxD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,CAAC,CAAA;IACnD,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC7B,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;IAC/E,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC9B,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,GAAG,CAAC,CAAA;AACzD,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,OAAgB;IACvC,MAAM,KAAK,GAAI,OAAuD,CAAC,YAAY,CAAA;IACnF,IAAI,OAAO,KAAK,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC3D,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IACxC,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AACxC,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,KAAa;IAC/B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IACjD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IACjC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;AACtE,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,YAAY,CAAC,UAAkD,EAAE,GAAgC;IACxG,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACtD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7D,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC;QAChC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,GAAG,GAAG,KAAK,CAAC,CAAA;IACtD,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,MAAc;IACvC,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,CAAA;IACnC,MAAM,KAAK,GAAuB,EAAE,CAAA;IACpC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;QAC7B,IAAI,IAAI,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,wDAAwD;AACxD,SAAS,eAAe,CACtB,EAAU,EACV,KAAoC,EACpC,MAAoB;IAEpB,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,EAAE,CAAC,CAAA;IAC1C,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAA;IACvD,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,OAAO,GAAG,GAAS,EAAE,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,CAAA,CAAC,CAAC,CAAA;QACpD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QACzD,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CACZ,GAAG,EAAE,GAAG,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,CAAA,CAAC,CAAC,EACjE,CAAC,KAAc,EAAE,EAAE,GAAG,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC,CACpF,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,MAAmB,EAAE,YAAgC,EAAE;IACnF,MAAM,IAAI,GAAc,EAAE,GAAG,YAAY,EAAE,GAAG,SAAS,EAAE,CAAA;IACzD,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACnD,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAA;IACrC,IAAI,aAAa,GAAG,MAAM,CAAC,iBAAiB,CAAA;IAC5C,IAAI,QAAQ,GAAG,MAAM,CAAC,iBAAiB,CAAA;IACvC,IAAI,WAAW,GAAG,CAAC,CAAA;IAEnB,MAAM,IAAI,GAAG,KAAK,EAAE,MAAoB,EAAiB,EAAE;QACzD,0EAA0E;QAC1E,sDAAsD;QACtD,MAAM,MAAM,GAAG,MAAM,CAAC,mBAAmB,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAC1G,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,GAAG,MAAM,CAAC,aAAa,GAAG,MAAM,EAAE,QAAQ,CAAC,CAAA;QAClF,MAAM,IAAI,GAAG,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QAClC,IAAI,IAAI,GAAG,CAAC;YAAE,MAAM,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QAC7D,aAAa,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IAC5B,CAAC,CAAA;IAED,MAAM,QAAQ,GAAG,CAAC,EAAU,EAAQ,EAAE;QACpC,IAAI,EAAE,GAAG,CAAC;YAAE,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAA;IAC5D,CAAC,CAAA;IAED,MAAM,IAAI,GAAG,KAAK,EAAE,GAAW,EAAE,MAAoB,EAA6D,EAAE;QAClH,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QACrD,MAAM,QAAQ,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;QACpF,MAAM,OAAO,GAA2B;YACtC,GAAG,kBAAkB;YACrB,YAAY,EAAE,MAAM,CAAC,SAAS;YAC9B,qEAAqE;YACrE,iEAAiE;YACjE,gBAAgB,EAAE,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,wBAAwB;SACvF,CAAA;QACD,WAAW,IAAI,CAAC,CAAA;QAChB,IAAI,MAAM,CAAC,OAAO,KAAK,EAAE;YAAE,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;QAC3D,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAA;QAC/E,IAAI,MAAM,KAAK,EAAE;YAAE,OAAO,CAAC,MAAM,GAAG,MAAM,CAAA;QAC1C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC7F,MAAM,SAAS,GAAG,eAAe,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;QACnD,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACrB,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;gBAC9B,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;gBAC9B,IAAI,IAAI,KAAK,SAAS;oBAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YACnD,CAAC;QACH,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QAClC,OAAO;YACL,QAAQ,EAAE,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE;YACrG,YAAY,EAAE,eAAe,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;SAC/E,CAAA;IACH,CAAC,CAAA;IAED,MAAM,GAAG,GAAG,KAAK,EAAE,GAAW,EAAE,MAAoB,EAAyB,EAAE;QAC7E,IAAI,SAAS,CAAC,MAAM,CAAC;YAAE,MAAM,UAAU,EAAE,CAAA;QACzC,IAAI,SAAkB,CAAA;QACtB,IAAI,OAAO,GAAG,CAAC,CAAA;QACf,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,UAAU,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;YACjE,IAAI,OAAO,GAAG,CAAC;gBAAE,MAAM,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;YACnE,MAAM,IAAI,CAAC,MAAM,CAAC,CAAA;YAClB,IAAI,CAAC;gBACH,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;gBAC1D,IAAI,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,OAAO,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;oBACzE,SAAS,GAAG,IAAI,KAAK,CAAC,eAAe,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,GAAG,GAAG,CAAC,CAAA;oBACzE,OAAO,GAAG,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;oBAChE,SAAQ;gBACV,CAAC;gBACD,OAAO,QAAQ,CAAA;YACjB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,SAAS,CAAC,MAAM,CAAC;oBAAE,MAAM,KAAK,CAAA;gBAClC,SAAS,GAAG,KAAK,CAAA;gBACjB,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;YACpE,CAAC;QACH,CAAC;QACD,MAAM,SAAS,YAAY,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,yBAAyB,GAAG,GAAG,GAAG,SAAS,CAAC,CAAA;IACvG,CAAC,CAAA;IAED,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;AAChC,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Article-title to file-name normalization. A title arrives with path
|
|
3
|
+
* separators, characters Windows rejects, C0/C1 controls, invisible formatting
|
|
4
|
+
* characters, device names, and unbounded length, so every article passes
|
|
5
|
+
* through this module before its file is created.
|
|
6
|
+
* @module @deepseek-ai/dsh-ab-wechat-scrape/filename
|
|
7
|
+
*/
|
|
8
|
+
/** Extension every saved article carries. */
|
|
9
|
+
export declare const FILE_EXTENSION = ".md";
|
|
10
|
+
/** Base name used when neither the title nor the link yields anything usable. */
|
|
11
|
+
export declare const UNTITLED = "untitled";
|
|
12
|
+
/**
|
|
13
|
+
* Fold one article title into a file-name base: Unicode-normalized, with the
|
|
14
|
+
* characters no file system accepts replaced, and whitespace, separators, and
|
|
15
|
+
* edge noise collapsed.
|
|
16
|
+
* @param title - the article title as the page carried it.
|
|
17
|
+
* @returns the normalized base, or an empty string when nothing survives.
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizeTitle(title: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* A stable name for an article whose title yielded none, taken from its own
|
|
22
|
+
* link: the message identity is what makes two untitled articles different, and
|
|
23
|
+
* a name derived from it separates them without a collision suffix.
|
|
24
|
+
* @param source - the article's resolved link.
|
|
25
|
+
* @returns the normalized stem, or an empty string when the link names no identity.
|
|
26
|
+
*/
|
|
27
|
+
export declare function linkStem(source: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Build the file name one article is saved under.
|
|
30
|
+
* @param title - the article title as the page carried it.
|
|
31
|
+
* @param maxChars - longest base name, in code points, the deployment allows.
|
|
32
|
+
* @param source - the article's resolved link, used when the title yields no name.
|
|
33
|
+
* @returns a base name carrying {@link FILE_EXTENSION}, creatable on Windows and POSIX.
|
|
34
|
+
*/
|
|
35
|
+
export declare function articleFileName(title: string, maxChars: number, source?: string): string;
|
|
36
|
+
//# sourceMappingURL=filename.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,6CAA6C;AAC7C,eAAO,MAAM,cAAc,QAAQ,CAAA;AAEnC,iFAAiF;AACjF,eAAO,MAAM,QAAQ,aAAa,CAAA;AAsBlC;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQpD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAa/C;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,MAAM,CAKpF"}
|
package/lib/filename.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Article-title to file-name normalization. A title arrives with path
|
|
3
|
+
* separators, characters Windows rejects, C0/C1 controls, invisible formatting
|
|
4
|
+
* characters, device names, and unbounded length, so every article passes
|
|
5
|
+
* through this module before its file is created.
|
|
6
|
+
* @module @deepseek-ai/dsh-ab-wechat-scrape/filename
|
|
7
|
+
*/
|
|
8
|
+
/** Extension every saved article carries. */
|
|
9
|
+
export const FILE_EXTENSION = '.md';
|
|
10
|
+
/** Base name used when neither the title nor the link yields anything usable. */
|
|
11
|
+
export const UNTITLED = 'untitled';
|
|
12
|
+
/** Characters no file name may carry: Windows' reserved set, the POSIX separator, and controls. */
|
|
13
|
+
const ILLEGAL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f<>:"/\\|?*]/g;
|
|
14
|
+
/**
|
|
15
|
+
* Characters a file name may carry but a reader cannot see: zero-width spaces,
|
|
16
|
+
* bidirectional overrides, word joiners, and the byte-order mark. WeChat titles
|
|
17
|
+
* are full of them, and a name made only of them looks empty in a directory
|
|
18
|
+
* listing while still being a distinct file.
|
|
19
|
+
*/
|
|
20
|
+
const INVISIBLE_CHARACTERS = /[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u206a-\u206f\ufeff]/g;
|
|
21
|
+
/** Device names Windows reserves in every directory, with or without an extension. */
|
|
22
|
+
const RESERVED_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
23
|
+
/** Leading and trailing characters a file name may not carry on Windows. */
|
|
24
|
+
const EDGE_NOISE = /^[\s.\-]+|[\s.\-]+$/g;
|
|
25
|
+
/** Message identity a short link carries in its path, e.g. `/s/AbCdEf123`. */
|
|
26
|
+
const SHORT_LINK_PATH = /^\/s\/([^/?#]+)$/;
|
|
27
|
+
/**
|
|
28
|
+
* Fold one article title into a file-name base: Unicode-normalized, with the
|
|
29
|
+
* characters no file system accepts replaced, and whitespace, separators, and
|
|
30
|
+
* edge noise collapsed.
|
|
31
|
+
* @param title - the article title as the page carried it.
|
|
32
|
+
* @returns the normalized base, or an empty string when nothing survives.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeTitle(title) {
|
|
35
|
+
return title
|
|
36
|
+
.normalize('NFC')
|
|
37
|
+
.replace(INVISIBLE_CHARACTERS, '')
|
|
38
|
+
.replace(ILLEGAL_CHARACTERS, '-')
|
|
39
|
+
.replace(/\s+/g, ' ')
|
|
40
|
+
.replace(/-{2,}/g, '-')
|
|
41
|
+
.replace(EDGE_NOISE, '');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A stable name for an article whose title yielded none, taken from its own
|
|
45
|
+
* link: the message identity is what makes two untitled articles different, and
|
|
46
|
+
* a name derived from it separates them without a collision suffix.
|
|
47
|
+
* @param source - the article's resolved link.
|
|
48
|
+
* @returns the normalized stem, or an empty string when the link names no identity.
|
|
49
|
+
*/
|
|
50
|
+
export function linkStem(source) {
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = new URL(source);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// A link that cannot be parsed names no identity to derive a stem from.
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
const short = SHORT_LINK_PATH.exec(parsed.pathname);
|
|
60
|
+
const identity = short?.[1]
|
|
61
|
+
?? parsed.searchParams.get('sn')
|
|
62
|
+
?? [parsed.searchParams.get('mid'), parsed.searchParams.get('idx')].filter(part => part !== null).join('-');
|
|
63
|
+
return identity === undefined || identity === '' ? '' : normalizeTitle(identity);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the file name one article is saved under.
|
|
67
|
+
* @param title - the article title as the page carried it.
|
|
68
|
+
* @param maxChars - longest base name, in code points, the deployment allows.
|
|
69
|
+
* @param source - the article's resolved link, used when the title yields no name.
|
|
70
|
+
* @returns a base name carrying {@link FILE_EXTENSION}, creatable on Windows and POSIX.
|
|
71
|
+
*/
|
|
72
|
+
export function articleFileName(title, maxChars, source = '') {
|
|
73
|
+
const normalized = normalizeTitle(title);
|
|
74
|
+
if (normalized !== '')
|
|
75
|
+
return finish(normalized, maxChars);
|
|
76
|
+
const stem = linkStem(source);
|
|
77
|
+
return finish(stem === '' ? UNTITLED : stem, maxChars);
|
|
78
|
+
}
|
|
79
|
+
/** Clip one base name and escape the device names Windows reserves. */
|
|
80
|
+
function finish(base, maxChars) {
|
|
81
|
+
const clipped = clip(base, maxChars);
|
|
82
|
+
return (RESERVED_DEVICE_NAME.test(clipped) ? '_' + clipped : clipped) + FILE_EXTENSION;
|
|
83
|
+
}
|
|
84
|
+
/** Cut a base name to the ceiling without leaving edge noise or an empty name behind. */
|
|
85
|
+
function clip(base, maxChars) {
|
|
86
|
+
const characters = [...base];
|
|
87
|
+
if (characters.length <= maxChars)
|
|
88
|
+
return base;
|
|
89
|
+
const cut = characters.slice(0, maxChars).join('').replace(EDGE_NOISE, '');
|
|
90
|
+
return cut === '' ? UNTITLED : cut;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=filename.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filename.js","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"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"}
|