@jayfong/x-utils 1.0.0-202410181400 → 3.3.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.
Files changed (112) hide show
  1. package/lib/case.d.mts +184 -0
  2. package/lib/case.mjs +120 -0
  3. package/lib/chinese.d.mts +39 -0
  4. package/lib/chinese.mjs +52 -0
  5. package/lib/cli.d.mts +64 -0
  6. package/lib/cli.mjs +59 -0
  7. package/lib/clipboard.d.mts +50 -0
  8. package/lib/clipboard.mjs +122 -0
  9. package/lib/cmd.d.mts +38 -0
  10. package/lib/cmd.mjs +52 -0
  11. package/lib/crawler.d.mts +330 -0
  12. package/lib/crawler.mjs +313 -0
  13. package/lib/csv.d.mts +67 -0
  14. package/lib/csv.mjs +76 -0
  15. package/lib/data_set.d.mts +76 -0
  16. package/lib/data_set.mjs +104 -0
  17. package/lib/fs.d.mts +49 -0
  18. package/lib/fs.mjs +65 -0
  19. package/lib/json.d.mts +50 -0
  20. package/lib/json.mjs +74 -0
  21. package/lib/puppeteer.d.mts +88 -0
  22. package/lib/puppeteer.mjs +219 -0
  23. package/lib/runner.d.mts +52 -0
  24. package/lib/runner.mjs +50 -0
  25. package/lib/task.d.mts +37 -0
  26. package/lib/task.mjs +43 -0
  27. package/lib/text.d.mts +84 -0
  28. package/lib/text.mjs +141 -0
  29. package/lib/yaml.d.mts +51 -0
  30. package/lib/yaml.mjs +64 -0
  31. package/package.json +50 -164
  32. package/_cjs/ai.js +0 -80
  33. package/_cjs/alipan.js +0 -173
  34. package/_cjs/cli.js +0 -51
  35. package/_cjs/clipboard.js +0 -65
  36. package/_cjs/cmd.js +0 -42
  37. package/_cjs/compressor.js +0 -47
  38. package/_cjs/crawler.js +0 -341
  39. package/_cjs/csv.js +0 -97
  40. package/_cjs/data_set.js +0 -115
  41. package/_cjs/env.js +0 -82
  42. package/_cjs/fs.js +0 -86
  43. package/_cjs/html.js +0 -62
  44. package/_cjs/http_server.js +0 -79
  45. package/_cjs/json.js +0 -49
  46. package/_cjs/ksb.js +0 -218
  47. package/_cjs/number.js +0 -39
  48. package/_cjs/ocr.js +0 -74
  49. package/_cjs/path.js +0 -35
  50. package/_cjs/pinyin.js +0 -37
  51. package/_cjs/puppeteer.js +0 -271
  52. package/_cjs/runner.js +0 -88
  53. package/_cjs/task.js +0 -41
  54. package/_cjs/text.js +0 -136
  55. package/_cjs/unicode/lib_unicode.js +0 -58643
  56. package/_cjs/unicode/unicode.js +0 -441
  57. package/_cjs/upload.js +0 -101
  58. package/_cjs/yaml.js +0 -52
  59. package/ai.d.ts +0 -26
  60. package/ai.js +0 -60
  61. package/alipan.d.ts +0 -209
  62. package/alipan.js +0 -153
  63. package/cli.d.ts +0 -14
  64. package/cli.js +0 -21
  65. package/clipboard.d.ts +0 -6
  66. package/clipboard.js +0 -35
  67. package/cmd.d.ts +0 -5
  68. package/cmd.js +0 -22
  69. package/compressor.d.ts +0 -5
  70. package/compressor.js +0 -17
  71. package/crawler.d.ts +0 -162
  72. package/crawler.js +0 -332
  73. package/csv.d.ts +0 -14
  74. package/csv.js +0 -67
  75. package/data_set.d.ts +0 -22
  76. package/data_set.js +0 -85
  77. package/env.d.ts +0 -6
  78. package/env.js +0 -52
  79. package/fs.d.ts +0 -23
  80. package/fs.js +0 -56
  81. package/html.d.ts +0 -7
  82. package/html.js +0 -45
  83. package/http_server.d.ts +0 -13
  84. package/http_server.js +0 -49
  85. package/json.d.ts +0 -6
  86. package/json.js +0 -19
  87. package/ksb.d.ts +0 -37
  88. package/ksb.js +0 -198
  89. package/number.d.ts +0 -3
  90. package/number.js +0 -9
  91. package/ocr.d.ts +0 -17
  92. package/ocr.js +0 -54
  93. package/path.d.ts +0 -2
  94. package/path.js +0 -5
  95. package/pinyin.d.ts +0 -8
  96. package/pinyin.js +0 -17
  97. package/puppeteer.d.ts +0 -38
  98. package/puppeteer.js +0 -241
  99. package/runner.d.ts +0 -13
  100. package/runner.js +0 -58
  101. package/task.d.ts +0 -6
  102. package/task.js +0 -21
  103. package/text.d.ts +0 -63
  104. package/text.js +0 -119
  105. package/unicode/lib_unicode.d.ts +0 -4
  106. package/unicode/lib_unicode.js +0 -58623
  107. package/unicode/unicode.d.ts +0 -347
  108. package/unicode/unicode.js +0 -426
  109. package/upload.d.ts +0 -34
  110. package/upload.js +0 -71
  111. package/yaml.d.ts +0 -6
  112. package/yaml.js +0 -22
package/lib/fs.mjs ADDED
@@ -0,0 +1,65 @@
1
+ import fs from "fs-extra";
2
+ import mime from "mime";
3
+ // src/fs.ts
4
+ /**
5
+ * 额外的文件类型辅助方法:
6
+ * - `getMimeType`:将扩展名 / 文件名 / MIME 字符串统一转换为 MIME 类型;
7
+ * - `getExtension`:将 MIME 字符串 / 文件名统一转换为扩展名。
8
+ *
9
+ * 这些方法会被合并到 `XFs` 中,与 `fs-extra` 的 API 一起导出。
10
+ */
11
+ const XFsExtra = {
12
+ /**
13
+ * 获取 MIME 类型。
14
+ *
15
+ * 入参支持以下形式:
16
+ * - 扩展名:`json`、`png`
17
+ * - 文件名:`file.txt`、`archive.tar.gz`
18
+ * - MIME 字符串:`application/json`
19
+ *
20
+ * 处理规则:
21
+ * 1. 去除首尾空白后,如果为空,返回通用兜底值 `application/octet-stream`;
22
+ * 2. 如果已是 MIME 形式(包含 `/`),直接返回原值;
23
+ * 3. 否则交给 `mime.getType` 推断;
24
+ * 4. 推断失败时,返回 `application/octet-stream`。
25
+ */
26
+ getMimeType(extOrName) {
27
+ const value = extOrName.trim();
28
+ if (!value) return "application/octet-stream";
29
+ if (value.includes("/")) return value;
30
+ return mime.getType(value) || "application/octet-stream";
31
+ },
32
+ /**
33
+ * 获取扩展名(不带 `.`)。
34
+ *
35
+ * 入参支持以下形式:
36
+ * - MIME 字符串:`image/png`、`text/plain`
37
+ * - 文件名:`file.txt`、`archive.tar.gz`
38
+ * - 扩展名:`json`
39
+ *
40
+ * 处理规则:
41
+ * 1. 去除首尾空白后,如果为空,返回空字符串;
42
+ * 2. 如果是 MIME 形式(包含 `/`),优先通过 `mime.getExtension` 推断,
43
+ * 若推断失败则退化为取 `/` 右侧片段;
44
+ * 3. 如果是文件名(包含 `.`),返回最后一段后缀;
45
+ * 4. 否则按扩展名/MIME 简写尝试 `mime.getExtension`,失败返回空字符串。
46
+ */
47
+ getExtension(typeOrName) {
48
+ const value = typeOrName.trim();
49
+ if (!value) return "";
50
+ if (value.includes("/")) return mime.getExtension(value) || value.split("/").pop() || "";
51
+ if (value.includes(".")) return value.split(".").pop();
52
+ return mime.getExtension(value) || "";
53
+ }
54
+ };
55
+ /**
56
+ * `XFs` = `fs-extra` + 轻量文件类型工具。
57
+ *
58
+ * 这样调用方既可以直接使用完整的文件系统 API(如 `readFile`、`writeFile`),
59
+ * 也可以在同一对象上使用 `getMimeType` 和 `getExtension`。
60
+ */
61
+ const XFs = {
62
+ ...fs,
63
+ ...XFsExtra
64
+ };
65
+ export { XFs };
package/lib/json.d.mts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * JSON 文本与文件读写工具。
3
+ *
4
+ * 提供两类能力:
5
+ * - 严格方法:`parse` / `read`,遇到非法 JSON 或文件异常时抛错;
6
+ * - 安全方法:`safeParse` / `safeRead`,遇到异常时返回 `undefined`,适合可选配置读取场景。
7
+ */
8
+ export declare class XJson {
9
+ /**
10
+ * 严格解析 JSON 文本。
11
+ *
12
+ * - 解析成功:返回目标类型值;
13
+ * - 解析失败:抛出 `SyntaxError`。
14
+ */
15
+ static parse<T>(value: string): T;
16
+ /**
17
+ * 安全解析 JSON 文本。
18
+ *
19
+ * - 解析成功:返回目标类型值;
20
+ * - 解析失败:返回 `undefined`。
21
+ *
22
+ * 适用于“配置可缺省、失败可兜底”的场景,例如从环境变量读取可选 JSON。
23
+ */
24
+ static safeParse<T>(value: string): T | undefined;
25
+ /**
26
+ * 序列化为 JSON 字符串。
27
+ *
28
+ * @param compact `true` 时输出紧凑格式(无缩进);`false`(默认)时输出 2 空格缩进,便于阅读与 git diff。
29
+ */
30
+ static stringify(value: unknown, compact?: boolean): string;
31
+ /**
32
+ * 严格读取并解析 JSON 文件(UTF-8)。
33
+ *
34
+ * 读取失败(如文件不存在)或解析失败时会抛错。
35
+ */
36
+ static read<T>(file: string): Promise<T>;
37
+ /**
38
+ * 安全读取并解析 JSON 文件(UTF-8)。
39
+ *
40
+ * - 文件不存在、权限问题、JSON 非法等任意异常:返回 `undefined`;
41
+ * - 读取和解析都成功:返回目标类型值。
42
+ *
43
+ * 适合读取“可选存在”的本地缓存、临时状态文件。
44
+ */
45
+ static safeRead<T>(file: string): Promise<T | undefined>;
46
+ /**
47
+ * 将值写入 JSON 文件(UTF-8)。
48
+ */
49
+ static write(file: string, value: unknown, compact?: boolean): Promise<void>;
50
+ }
package/lib/json.mjs ADDED
@@ -0,0 +1,74 @@
1
+ import * as fs from "node:fs";
2
+ // src/json.ts
3
+ /**
4
+ * JSON 文本与文件读写工具。
5
+ *
6
+ * 提供两类能力:
7
+ * - 严格方法:`parse` / `read`,遇到非法 JSON 或文件异常时抛错;
8
+ * - 安全方法:`safeParse` / `safeRead`,遇到异常时返回 `undefined`,适合可选配置读取场景。
9
+ */
10
+ var XJson = class XJson {
11
+ /**
12
+ * 严格解析 JSON 文本。
13
+ *
14
+ * - 解析成功:返回目标类型值;
15
+ * - 解析失败:抛出 `SyntaxError`。
16
+ */
17
+ static parse(value) {
18
+ return JSON.parse(value);
19
+ }
20
+ /**
21
+ * 安全解析 JSON 文本。
22
+ *
23
+ * - 解析成功:返回目标类型值;
24
+ * - 解析失败:返回 `undefined`。
25
+ *
26
+ * 适用于“配置可缺省、失败可兜底”的场景,例如从环境变量读取可选 JSON。
27
+ */
28
+ static safeParse(value) {
29
+ try {
30
+ return XJson.parse(value);
31
+ } catch {
32
+ return;
33
+ }
34
+ }
35
+ /**
36
+ * 序列化为 JSON 字符串。
37
+ *
38
+ * @param compact `true` 时输出紧凑格式(无缩进);`false`(默认)时输出 2 空格缩进,便于阅读与 git diff。
39
+ */
40
+ static stringify(value, compact = false) {
41
+ return JSON.stringify(value, null, compact ? void 0 : 2);
42
+ }
43
+ /**
44
+ * 严格读取并解析 JSON 文件(UTF-8)。
45
+ *
46
+ * 读取失败(如文件不存在)或解析失败时会抛错。
47
+ */
48
+ static async read(file) {
49
+ const value = await fs.promises.readFile(file, "utf8");
50
+ return XJson.parse(value);
51
+ }
52
+ /**
53
+ * 安全读取并解析 JSON 文件(UTF-8)。
54
+ *
55
+ * - 文件不存在、权限问题、JSON 非法等任意异常:返回 `undefined`;
56
+ * - 读取和解析都成功:返回目标类型值。
57
+ *
58
+ * 适合读取“可选存在”的本地缓存、临时状态文件。
59
+ */
60
+ static async safeRead(file) {
61
+ try {
62
+ return await XJson.read(file);
63
+ } catch {
64
+ return;
65
+ }
66
+ }
67
+ /**
68
+ * 将值写入 JSON 文件(UTF-8)。
69
+ */
70
+ static async write(file, value, compact = false) {
71
+ await fs.promises.writeFile(file, XJson.stringify(value, compact));
72
+ }
73
+ };
74
+ export { XJson };
@@ -0,0 +1,88 @@
1
+ import { Browser, ElementHandle, Frame, Page } from "puppeteer";
2
+ export type XPuppeteerBrowser = Browser;
3
+ export type XPuppeteerPage = Page;
4
+ export interface XPuppeteerOpenBrowserOptions {
5
+ /** 是否以有头模式启动浏览器;默认 `false`(无头)。 */
6
+ headful?: boolean;
7
+ /** 代理地址,例如 `http://127.0.0.1:7890`。 */
8
+ proxy?: string;
9
+ /** 浏览器用户数据目录。 */
10
+ dataDir?: string;
11
+ }
12
+ export interface XPuppeteerOpenPageOptions {
13
+ /**
14
+ * 当目标 URL 已在现有页面中打开时的匹配规则。
15
+ *
16
+ * - `string`:匹配 `pathname + search` 是否包含该片段;
17
+ * - `RegExp`:匹配 `pathname + search`。
18
+ */
19
+ match?: string | RegExp;
20
+ /** 是否拦截图片/字体/样式资源,以提升打开速度。 */
21
+ blockAssets?: boolean;
22
+ /** `goto` 时使用的 referer;未传则使用目标 URL。 */
23
+ referer?: string;
24
+ }
25
+ export interface XPuppeteerGetPageHtmlOptions extends XPuppeteerOpenBrowserOptions, XPuppeteerOpenPageOptions {
26
+ /** 复用浏览器实例的标识;传入后会命中 memoized 实例。 */
27
+ instanceId?: string | number;
28
+ /** 是否连接系统已启动的调试浏览器(默认 `false`)。 */
29
+ useSystemBrowser?: boolean;
30
+ /**
31
+ * 页面打开后的扩展逻辑钩子,例如等待某个 selector 出现再取 HTML。
32
+ */
33
+ afterOpenPage?: (payload: {
34
+ /** 当前 `XPuppeteer` 实例。 */
35
+ browser: XPuppeteer;
36
+ /** 已打开的页面实例。 */
37
+ page: XPuppeteerPage;
38
+ }) => any;
39
+ }
40
+ export declare class XPuppeteer {
41
+ browser: Browser;
42
+ /**
43
+ * 连接系统中已开启远程调试端口(9222)的浏览器。
44
+ */
45
+ connectSystemBrowser(): Promise<XPuppeteerBrowser>;
46
+ /**
47
+ * 启动新的浏览器实例并绑定退出钩子。
48
+ */
49
+ openBrowser(options?: XPuppeteerOpenBrowserOptions): Promise<XPuppeteerBrowser>;
50
+ /**
51
+ * 打开 URL 或直接渲染 HTML。
52
+ *
53
+ * 当入参为 URL 时,会优先尝试复用已打开且匹配的页面。
54
+ */
55
+ openPage(urlOrHtml: string, options?: XPuppeteerOpenPageOptions): Promise<XPuppeteerPage>;
56
+ input(page: Page, selector: string, textOrHtml: string, frameIndex?: number): Promise<void>;
57
+ /**
58
+ * 在当前激活元素上执行“全选”。
59
+ */
60
+ selectAll(page: Page | Frame): Promise<void>;
61
+ /**
62
+ * 通过键盘事件执行粘贴(Shift + Insert)。
63
+ */
64
+ paste(page: Page): Promise<void>;
65
+ queryByText<T extends Node = HTMLDivElement>(page: Page, text: string | RegExp | ((el: T) => boolean)): Promise<ElementHandle<T>>;
66
+ /**
67
+ * 断开与浏览器的连接(不关闭浏览器进程)。
68
+ */
69
+ disconnect(): Promise<void>;
70
+ /**
71
+ * 关闭当前浏览器实例。
72
+ */
73
+ close(): Promise<void>;
74
+ /**
75
+ * 创建并打开浏览器,返回 `XPuppeteer` 实例。
76
+ */
77
+ static openBrowser(options?: XPuppeteerOpenBrowserOptions): Promise<XPuppeteer>;
78
+ /**
79
+ * 便捷方法:打开页面并返回 `Page`。
80
+ *
81
+ * 返回的 `page.close()` 会被替换为关闭整个浏览器实例。
82
+ */
83
+ static openPage(urlOrHtml: string, options?: XPuppeteerGetPageHtmlOptions): Promise<XPuppeteerPage>;
84
+ static getPageHtmlXp: ((options: XPuppeteerGetPageHtmlOptions) => Promise<XPuppeteer>) & {
85
+ cache: import("vtils").AsyncMemoizeCacheMap;
86
+ };
87
+ static getPageHtml(url: string, options?: XPuppeteerGetPageHtmlOptions): Promise<string>;
88
+ }
@@ -0,0 +1,219 @@
1
+ import { XClipboard } from "./clipboard.mjs";
2
+ import { xr } from "@jayfong/x-request";
3
+ import { asyncMemoize, isFunction, isRegExp } from "vtils";
4
+ import { asyncExitHook } from "exit-hook";
5
+ import puppeteer from "puppeteer";
6
+ // src/puppeteer.ts
7
+ var XPuppeteer = class XPuppeteer {
8
+ browser;
9
+ /**
10
+ * 连接系统中已开启远程调试端口(9222)的浏览器。
11
+ */
12
+ async connectSystemBrowser() {
13
+ const { data: { webSocketDebuggerUrl } } = await xr.getJson({ url: "http://localhost:9222/json/version" });
14
+ const browser = await puppeteer.connect({
15
+ browserWSEndpoint: webSocketDebuggerUrl,
16
+ defaultViewport: {
17
+ width: 0,
18
+ height: 0
19
+ }
20
+ });
21
+ this.browser = browser;
22
+ asyncExitHook(() => this.disconnect(), { wait: 1e3 });
23
+ return browser;
24
+ }
25
+ /**
26
+ * 启动新的浏览器实例并绑定退出钩子。
27
+ */
28
+ async openBrowser(options = {}) {
29
+ const browser = await puppeteer.launch({
30
+ headless: !options.headful,
31
+ defaultViewport: {
32
+ width: 1440,
33
+ height: 900
34
+ },
35
+ userDataDir: options.dataDir,
36
+ args: options.proxy ? [`--proxy-server=${options.proxy}`] : void 0
37
+ });
38
+ this.browser = browser;
39
+ asyncExitHook(() => this.close(), { wait: 1e3 });
40
+ return browser;
41
+ }
42
+ /**
43
+ * 打开 URL 或直接渲染 HTML。
44
+ *
45
+ * 当入参为 URL 时,会优先尝试复用已打开且匹配的页面。
46
+ */
47
+ async openPage(urlOrHtml, options = {}) {
48
+ const isUrl = /^https?:\/\//i.test(urlOrHtml);
49
+ const pages = await this.browser.pages();
50
+ if (isUrl) {
51
+ const inputUrl = new URL(urlOrHtml);
52
+ const foundPage = pages.find((item) => {
53
+ const pageUrl = item.url();
54
+ if (!/^https?:\/\//i.test(pageUrl)) return false;
55
+ const url = new URL(pageUrl);
56
+ return inputUrl.origin === url.origin && (pageUrl === urlOrHtml || !!options.match && (typeof options.match === "string" ? `${url.pathname}${url.search}`.includes(options.match) : options.match.test(`${url.pathname}${url.search}`)));
57
+ });
58
+ if (foundPage) return foundPage;
59
+ }
60
+ const page = await this.browser.newPage();
61
+ await page.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36");
62
+ if (options.blockAssets) {
63
+ await page.setRequestInterception(true);
64
+ page.on("request", (req) => {
65
+ const resourceType = req.resourceType();
66
+ if (resourceType === "stylesheet" || resourceType === "font" || resourceType === "image") req.abort();
67
+ else req.continue();
68
+ });
69
+ }
70
+ if (isUrl) await page.goto(urlOrHtml, { referer: options.referer || urlOrHtml });
71
+ else await page.setContent(urlOrHtml);
72
+ return page;
73
+ }
74
+ async input(page, selector, textOrHtml, frameIndex) {
75
+ const mainPage = page;
76
+ const keyboard = page.keyboard;
77
+ let queryRoot = page;
78
+ if (frameIndex != null) {
79
+ const frame = page.frames()[frameIndex];
80
+ if (!frame) throw new Error(`Frame 不存在: ${frameIndex}`);
81
+ queryRoot = frame;
82
+ }
83
+ const el = await queryRoot.$(selector);
84
+ if (!el) throw new Error(`未找到输入元素: ${selector}`);
85
+ if (await el.evaluate((el) => {
86
+ return el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SPAN";
87
+ })) await XClipboard.writeText(textOrHtml);
88
+ else await XClipboard.writeHtml(textOrHtml);
89
+ await el.click();
90
+ await this.selectAll(queryRoot);
91
+ await keyboard.press("Backspace");
92
+ await this.paste(mainPage);
93
+ }
94
+ /**
95
+ * 在当前激活元素上执行“全选”。
96
+ */
97
+ async selectAll(page) {
98
+ await page.evaluate(() => {
99
+ const isMac = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
100
+ document.activeElement.dispatchEvent(new KeyboardEvent("keydown", {
101
+ bubbles: true,
102
+ cancelable: true,
103
+ key: isMac ? "Meta" : "Control",
104
+ code: isMac ? "MetaLeft" : "ControlLeft",
105
+ location: window.KeyboardEvent.DOM_KEY_LOCATION_LEFT,
106
+ getModifierState: (keyArg) => keyArg === (isMac ? "Meta" : "Control"),
107
+ ctrlKey: !isMac,
108
+ metaKey: isMac,
109
+ charCode: 0,
110
+ keyCode: isMac ? 93 : 17,
111
+ which: isMac ? 93 : 17
112
+ }));
113
+ const preventableEvent = new KeyboardEvent("keydown", {
114
+ bubbles: true,
115
+ cancelable: true,
116
+ key: "a",
117
+ code: "KeyA",
118
+ location: window.KeyboardEvent.DOM_KEY_LOCATION_STANDARD,
119
+ getModifierState: (keyArg) => keyArg === (isMac ? "Meta" : "Control"),
120
+ ctrlKey: !isMac,
121
+ metaKey: isMac,
122
+ charCode: 0,
123
+ keyCode: 65,
124
+ which: 65
125
+ });
126
+ if (!(!document.activeElement.dispatchEvent(preventableEvent) || preventableEvent.defaultPrevented)) document.execCommand("selectall", false, null);
127
+ document.activeElement.dispatchEvent(new KeyboardEvent("keyup", {
128
+ bubbles: true,
129
+ cancelable: true,
130
+ key: isMac ? "Meta" : "Control",
131
+ code: isMac ? "MetaLeft" : "ControlLeft",
132
+ location: window.KeyboardEvent.DOM_KEY_LOCATION_LEFT,
133
+ getModifierState: () => false,
134
+ charCode: 0,
135
+ keyCode: isMac ? 93 : 17,
136
+ which: isMac ? 93 : 17
137
+ }));
138
+ });
139
+ }
140
+ /**
141
+ * 通过键盘事件执行粘贴(Shift + Insert)。
142
+ */
143
+ async paste(page) {
144
+ await page.keyboard.down("Shift");
145
+ await page.keyboard.press("Insert");
146
+ await page.keyboard.up("Shift");
147
+ }
148
+ async queryByText(page, text) {
149
+ const el = await page.evaluateHandle((text, isRegExp, isFunction) => {
150
+ const textRe = isRegExp ? (() => {
151
+ const m = text.match(/^\/(.+)\/([^/]*)$/);
152
+ return new RegExp(m[1], m[2] || void 0);
153
+ })() : void 0;
154
+ const textFn = isFunction ? eval(`(() => {return ${text}})()`) : void 0;
155
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => {
156
+ return (isRegExp ? textRe.test(node.textContent) : isFunction ? textFn(node) : node.textContent.includes(text)) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
157
+ } });
158
+ walker.nextNode();
159
+ return walker.currentNode.parentElement;
160
+ }, String(text), isRegExp(text), isFunction(text));
161
+ return el;
162
+ }
163
+ /**
164
+ * 断开与浏览器的连接(不关闭浏览器进程)。
165
+ */
166
+ async disconnect() {
167
+ await this.browser.disconnect();
168
+ }
169
+ /**
170
+ * 关闭当前浏览器实例。
171
+ */
172
+ async close() {
173
+ await this.browser.close();
174
+ }
175
+ /**
176
+ * 创建并打开浏览器,返回 `XPuppeteer` 实例。
177
+ */
178
+ static async openBrowser(options = {}) {
179
+ const xp = new XPuppeteer();
180
+ await xp.openBrowser(options);
181
+ return xp;
182
+ }
183
+ /**
184
+ * 便捷方法:打开页面并返回 `Page`。
185
+ *
186
+ * 返回的 `page.close()` 会被替换为关闭整个浏览器实例。
187
+ */
188
+ static async openPage(urlOrHtml, options = {}) {
189
+ const xp = new XPuppeteer();
190
+ await xp.openBrowser(options);
191
+ const page = await xp.openPage(urlOrHtml, options);
192
+ page.close = () => xp.close();
193
+ return page;
194
+ }
195
+ static getPageHtmlXp = asyncMemoize(async (options) => {
196
+ const xp = new XPuppeteer();
197
+ await (options.useSystemBrowser ? xp.connectSystemBrowser() : xp.openBrowser(options));
198
+ return xp;
199
+ }, { cacheKey: (options) => options.instanceId });
200
+ static async getPageHtml(url, options = {}) {
201
+ const hasInstanceId = !!options.instanceId;
202
+ options.instanceId ||= Date.now();
203
+ const xp = await XPuppeteer.getPageHtmlXp(options);
204
+ try {
205
+ const page = await xp.openPage(url, options);
206
+ if (options.afterOpenPage) await options.afterOpenPage({
207
+ browser: xp,
208
+ page
209
+ });
210
+ return await page.content();
211
+ } finally {
212
+ if (!hasInstanceId) {
213
+ if (options.useSystemBrowser) await xp.disconnect();
214
+ else await xp.close();
215
+ }
216
+ }
217
+ }
218
+ };
219
+ export { XPuppeteer };
@@ -0,0 +1,52 @@
1
+ import { Answers, DistinctQuestion } from "inquirer";
2
+ import { MsValue } from "vtils";
3
+ /**
4
+ * `XRunner` 交互问题定义。
5
+ *
6
+ * 为了让配置回填逻辑可安全工作,要求问题必须包含 `name` 与 `message`。
7
+ */
8
+ export interface XRunnerQuestion<TConfig extends Answers> extends Omit<DistinctQuestion<TConfig>, 'name' | 'message'> {
9
+ /** 问题对应的配置字段名。 */
10
+ name: Extract<keyof TConfig, string>;
11
+ /** 展示给用户的问题文案。 */
12
+ message: string;
13
+ }
14
+ /**
15
+ * `XRunner` 的运行配置。
16
+ */
17
+ export interface XRunnerOptions<TConfig extends Answers> {
18
+ /** 交互配置持久化文件路径(JSON)。 */
19
+ configFile: string;
20
+ /**
21
+ * 失败重试间隔。
22
+ *
23
+ * - 固定值:每次失败都等待同一个时长;
24
+ * - 函数:可按重试次数动态计算,`retryCount` 从 `1` 开始计数。
25
+ */
26
+ delay: MsValue | ((retryCount: number) => MsValue);
27
+ /** inquirer 交互问题列表。 */
28
+ questions: Array<XRunnerQuestion<TConfig>>;
29
+ /**
30
+ * 核心执行函数。
31
+ *
32
+ * 返回成功(resolve)时结束运行;抛错(reject)时进入重试分支。
33
+ */
34
+ run: (config: TConfig) => Promise<void> | void;
35
+ }
36
+ /**
37
+ * 带配置持久化与失败重试能力的交互式运行器。
38
+ *
39
+ * 行为概要:
40
+ * 1. 启动时读取历史配置,作为本次提问默认值;
41
+ * 2. 用户确认后回写配置文件,保证下次可复用;
42
+ * 3. 执行业务函数,成功即退出;
43
+ * 4. 失败时按 `delay` 等待后重试,直到成功或出现不可恢复错误。
44
+ */
45
+ export declare class XRunner<TConfig extends Answers> {
46
+ private options;
47
+ constructor(options: XRunnerOptions<TConfig>);
48
+ /**
49
+ * 启动运行器。
50
+ */
51
+ run(): Promise<void>;
52
+ }
package/lib/runner.mjs ADDED
@@ -0,0 +1,50 @@
1
+ import { XFs } from "./fs.mjs";
2
+ import inquirer from "inquirer";
3
+ import { ms, wait } from "vtils";
4
+ // src/runner.ts
5
+ /**
6
+ * 带配置持久化与失败重试能力的交互式运行器。
7
+ *
8
+ * 行为概要:
9
+ * 1. 启动时读取历史配置,作为本次提问默认值;
10
+ * 2. 用户确认后回写配置文件,保证下次可复用;
11
+ * 3. 执行业务函数,成功即退出;
12
+ * 4. 失败时按 `delay` 等待后重试,直到成功或出现不可恢复错误。
13
+ */
14
+ var XRunner = class {
15
+ options;
16
+ constructor(options) {
17
+ this.options = options;
18
+ }
19
+ /**
20
+ * 启动运行器。
21
+ */
22
+ async run() {
23
+ await XFs.ensureFile(this.options.configFile);
24
+ const savedConfig = await XFs.readJson(this.options.configFile, { throws: false }) || {};
25
+ const questions = this.options.questions.map((item) => {
26
+ const defaultValue = savedConfig[item.name];
27
+ if (defaultValue === void 0) return item;
28
+ return {
29
+ ...item,
30
+ default: defaultValue
31
+ };
32
+ });
33
+ const config = await inquirer.prompt(questions);
34
+ await XFs.writeJson(this.options.configFile, config, { spaces: 2 });
35
+ let retryCount = 0;
36
+ while (true) try {
37
+ await this.options.run(config);
38
+ return;
39
+ } catch (err) {
40
+ retryCount++;
41
+ const delay = typeof this.options.delay === "function" ? this.options.delay(retryCount) : this.options.delay;
42
+ const delayMs = ms(delay);
43
+ if (delayMs == null) throw new Error(`无效的重试等待时长: ${String(delay)}`);
44
+ console.error(err);
45
+ console.log(`执行报错, 第${retryCount}次重试前等待${delay}`);
46
+ await wait(delayMs);
47
+ }
48
+ }
49
+ };
50
+ export { XRunner };
package/lib/task.d.mts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 可复用任务基类。
3
+ *
4
+ * 封装了进程退出时的资源清理与退出码处理:
5
+ * 1. `run()` 启动前注册退出钩子,进程退出时自动调用 `dispose()` 释放资源;
6
+ * 2. `start()` 成功后以退出码 `0` 优雅退出;
7
+ * 3. `start()` 失败时记录错误日志,并以退出码 `1` 优雅退出。
8
+ *
9
+ * 使用方式:
10
+ * - 继承并实现 `start()`,将任务主逻辑放入其中;
11
+ * - 可选地覆写 `dispose()` 以释放连接、文件句柄等资源。
12
+ */
13
+ export declare abstract class XTask {
14
+ /** 日志实例,子类可直接通过 `this.log` 输出日志。 */
15
+ log: import("consola").ConsolaInstance;
16
+ /**
17
+ * 任务主入口,子类必须实现。
18
+ *
19
+ * 成功 resolve 视为任务成功;抛错(reject)则触发错误日志与失败退出码。
20
+ */
21
+ abstract start(): Promise<void>;
22
+ /**
23
+ * 进程退出前的资源清理钩子,默认无操作。
24
+ *
25
+ * 子类可覆写以释放数据库连接、临时文件等资源;退出钩子最多等待 3000ms。
26
+ */
27
+ dispose(): Promise<void>;
28
+ /**
29
+ * 运行任务。
30
+ *
31
+ * 行为说明:
32
+ * 1. 注册退出钩子:进程退出时调用 `dispose()`,等待时长 3000ms;
33
+ * 2. 执行 `start()`,成功后以退出码 `0` 优雅退出;
34
+ * 3. 执行失败时记录错误并以退出码 `1` 优雅退出。
35
+ */
36
+ run(): Promise<void>;
37
+ }
package/lib/task.mjs ADDED
@@ -0,0 +1,43 @@
1
+ import { asyncExitHook, gracefulExit } from "exit-hook";
2
+ import { consola } from "consola";
3
+ // src/task.ts
4
+ /**
5
+ * 可复用任务基类。
6
+ *
7
+ * 封装了进程退出时的资源清理与退出码处理:
8
+ * 1. `run()` 启动前注册退出钩子,进程退出时自动调用 `dispose()` 释放资源;
9
+ * 2. `start()` 成功后以退出码 `0` 优雅退出;
10
+ * 3. `start()` 失败时记录错误日志,并以退出码 `1` 优雅退出。
11
+ *
12
+ * 使用方式:
13
+ * - 继承并实现 `start()`,将任务主逻辑放入其中;
14
+ * - 可选地覆写 `dispose()` 以释放连接、文件句柄等资源。
15
+ */
16
+ var XTask = class {
17
+ /** 日志实例,子类可直接通过 `this.log` 输出日志。 */
18
+ log = consola;
19
+ /**
20
+ * 进程退出前的资源清理钩子,默认无操作。
21
+ *
22
+ * 子类可覆写以释放数据库连接、临时文件等资源;退出钩子最多等待 3000ms。
23
+ */
24
+ async dispose() {}
25
+ /**
26
+ * 运行任务。
27
+ *
28
+ * 行为说明:
29
+ * 1. 注册退出钩子:进程退出时调用 `dispose()`,等待时长 3000ms;
30
+ * 2. 执行 `start()`,成功后以退出码 `0` 优雅退出;
31
+ * 3. 执行失败时记录错误并以退出码 `1` 优雅退出。
32
+ */
33
+ run() {
34
+ asyncExitHook(() => this.dispose(), { wait: 3e3 });
35
+ return this.start().then(() => {
36
+ gracefulExit(0);
37
+ }).catch((err) => {
38
+ consola.error(err);
39
+ gracefulExit(1);
40
+ });
41
+ }
42
+ };
43
+ export { XTask };