@skrillex1224/playwright-toolkit 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # Visitor Tools - Tools
2
+
3
+ > **类型**: Tools (通用工具库)
4
+ > **用途**: 面向 Apify/Crawlee Actor 开发者的实用工具库。
5
+
6
+
7
+ 一个面向 Apify/Crawlee Actor 开发者的实用工具库,提供实时截图展示(Live View)、健壮的步骤执行封装、以及常用的 Playwright 优化工具。
8
+
9
+ ## 📦 安装
10
+
11
+ ```bash
12
+ npm install @skrillex1224/visitor-tools
13
+ ```
14
+
15
+ ## ✨ 功能特性
16
+
17
+ ### 1. Live View (实时截图展示)
18
+
19
+ 启动一个本地 Express 服务器,在 Apify 平台的 "Live View" 选项卡中实时查看浏览器当前状态。
20
+
21
+ ```javascript
22
+ import { useLiveView } from '@skrillex1224/visitor-tools';
23
+
24
+ const liveView = useLiveView();
25
+
26
+ // 启动 Live View 服务器
27
+ await liveView.startLiveViewServer();
28
+
29
+ // 在循环或步骤中捕获截图
30
+ await liveView.takeLiveScreenshot(page, "正在处理步骤 1...");
31
+ ```
32
+
33
+ **工作原理**:
34
+ - 截图保存到 Apify Key-Value Store
35
+ - Express 服务器从 Store 读取最新截图
36
+ - 页面每 1 秒自动刷新,展示实时状态
37
+
38
+ ### 2. 健壮的步骤执行 (`runStep`)
39
+
40
+ 将你的逻辑包装在 `runStep` 中,自动处理日志记录和失败截图。如果步骤失败,会自动捕获全页截图并将错误详情推送到 Dataset。
41
+
42
+ ```javascript
43
+ import { Utils } from '@skrillex1224/visitor-tools';
44
+
45
+ await Utils.runStep('登录步骤', page, async () => {
46
+ await page.click('#login');
47
+ await page.fill('#username', 'user');
48
+ });
49
+ ```
50
+
51
+ **失败输出示例**:
52
+ ```json
53
+ {
54
+ "status": "FAILED",
55
+ "failedStep": "登录步骤",
56
+ "errorMessage": "Timeout 30000ms exceeded",
57
+ "errorStack": "...",
58
+ "screenshotBase64": "data:image/jpeg;base64,...",
59
+ "timestamp": "2025-12-15T03:00:00.000Z"
60
+ }
61
+ ```
62
+
63
+ ### 3. Playwright 优化工具
64
+
65
+ #### 资源拦截 - 加速页面加载
66
+
67
+ 通过屏蔽字体、图片、媒体等资源来加快爬取速度:
68
+
69
+ ```javascript
70
+ // 默认屏蔽 font, image, media
71
+ await Utils.setupBlockingResources(page);
72
+
73
+ // 自定义屏蔽类型
74
+ await Utils.setupBlockingResources(page, ['stylesheet', 'font', 'image']);
75
+ ```
76
+
77
+ #### 视口设置
78
+
79
+ ```javascript
80
+ await Utils.setupViewport(page, 1920, 1080);
81
+ ```
82
+
83
+ ### 4. SSE 解析工具
84
+
85
+ 解析 Server-Sent Events (SSE) 流式数据的辅助函数:
86
+
87
+ ```javascript
88
+ const events = Utils.parseSseStream(responseText);
89
+ // 返回一个数组,包含所有解析后的 JSON 对象
90
+ ```
91
+
92
+ ### 5. 失败键包装 (Failed Key Wrapping)
93
+
94
+ 为步骤名称添加自定义的失败标识符,方便在失败时进行分类和追踪:
95
+
96
+ ```javascript
97
+ import { wrapStepNameWithFailedKey, unwrapStepName, ErrorKeygen } from '@skrillex1224/visitor-tools';
98
+
99
+ // 包装步骤名称
100
+ const wrappedName = wrapStepNameWithFailedKey(ErrorKeygen.NotLogin, '等待登录');
101
+
102
+ // 解包
103
+ const [failedKey, stepName] = unwrapStepName(wrappedName);
104
+ // failedKey: 30000001
105
+ // stepName: '等待登录'
106
+ ```
107
+
108
+ ## 📚 完整示例
109
+
110
+ ```javascript
111
+ import { Actor } from 'apify';
112
+ import { PlaywrightCrawler } from 'crawlee';
113
+ import { useLiveView, Utils } from '@skrillex1224/visitor-tools';
114
+
115
+ await Actor.init();
116
+
117
+ const { startLiveViewServer, takeLiveScreenshot } = useLiveView();
118
+
119
+ const crawler = new PlaywrightCrawler({
120
+ preNavigationHooks: [
121
+ async ({ page }) => {
122
+ await Utils.setupViewport(page);
123
+ await Utils.setupBlockingResources(page);
124
+ },
125
+ ],
126
+ requestHandler: async ({ page }) => {
127
+ await takeLiveScreenshot(page, '页面加载完成');
128
+
129
+ await Utils.runStep('点击登录按钮', page, async () => {
130
+ await page.click('#login-btn');
131
+ });
132
+
133
+ await takeLiveScreenshot(page, '登录成功');
134
+ },
135
+ });
136
+
137
+ await startLiveViewServer();
138
+ await crawler.run(['https://example.com']);
139
+ await Actor.exit();
140
+ ```
141
+
142
+ ## 🛠️ API 文档
143
+
144
+ ### `useLiveView(liveViewKey?)`
145
+
146
+ 创建 Live View 实例。
147
+
148
+ - **参数**:
149
+ - `liveViewKey` (可选): Key-Value Store 中的键名,默认为 `'LIVE_VIEW_SCREENSHOT'`
150
+
151
+ - **返回对象**:
152
+ - `startLiveViewServer()`: 启动 Express 服务器
153
+ - `takeLiveScreenshot(page, logMessage?)`: 捕获截图并保存
154
+
155
+ ### `Utils.runStep(stepName, page, actionFn)`
156
+
157
+ 执行一个步骤并自动处理失败。## 模块文档
158
+
159
+ 详细文档请参阅 `docs/` 目录:
160
+
161
+ - [ApifyKit](./docs/apify-kit.md): Apify Actor 流程控制与数据全
162
+ - [Stealth](./docs/stealth.md): 反爬虫与指纹隐身
163
+ - [Humanize](./docs/humanize.md): 拟人化操作模拟
164
+ - [LiveView](./docs/live-view.md): 实时屏幕截图预览
165
+ - [Launch](./docs/launch.md): 浏览器启动配置
166
+ - [Utils](./docs/utils.md): 通用工具函数
167
+ - [Constants](./docs/constants.md): 常量定义
168
+
169
+ - **参数**:
170
+ - `stepName`: 步骤名称 (支持使用 `wrapStepNameWithFailedKey` 包装)
171
+ - `page`: Playwright Page 对象
172
+ - `actionFn`: 要执行的异步函数
173
+
174
+ ### `Utils.setupBlockingResources(page, resourceTypes?)`
175
+
176
+ 设置资源拦截器。
177
+
178
+ - **参数**:
179
+ - `page`: Playwright Page 对象
180
+ - `resourceTypes` (可选): 要屏蔽的资源类型数组,默认为 `['font', 'image', 'media']`
181
+
182
+ ### `Utils.setupViewport(page, width?, height?)`
183
+
184
+ 设置浏览器视口大小。
185
+
186
+ - **参数**:
187
+ - `page`: Playwright Page 对象
188
+ - `width` (可选): 宽度,默认 1920
189
+ - `height` (可选): 高度,默认 1080
190
+
191
+ ### `Utils.parseSseStream(sseStreamText)`
192
+
193
+ 解析 SSE 流文本为 JSON 对象数组。
194
+
195
+ - **参数**:
196
+ - `sseStreamText`: SSE 格式的文本
197
+
198
+ - **返回**: JSON 对象数组
199
+
200
+ ## 📝 注意事项
201
+
202
+ - Live View 仅在 Apify 平台运行时可见(通过 Live View 选项卡)
203
+ - `runStep` 捕获的截图是全页截图(JPEG 格式,质量 60),用于减少数据量
204
+ - 资源拦截会显著提升加载速度,但可能影响需要图片/样式的页面
205
+
206
+ ## 📄 License
207
+
208
+ ISC
@@ -0,0 +1,59 @@
1
+ # ApifyKit
2
+
3
+ `ApifyKit` 提供了一组用于简化 Apify Actor 开发的实用函数,核心是 `runStep` 机制和数据推送。
4
+
5
+ ## 引入
6
+
7
+ ```javascript
8
+ import { usePlaywrightToolKit } from '@skrillex1224/playwright-toolkit';
9
+ const { ApifyKit } = usePlaywrightToolKit();
10
+ ```
11
+
12
+ ## 方法
13
+
14
+ ### `runStep(stepName, page, stepFunction)`
15
+
16
+ 执行一个封装的步骤。会自动记录开始日志、执行函数,并捕获错误。
17
+ 如果步骤失败,会自动:
18
+ 1. 打印带颜色的错误日志。
19
+ 2. 拍摄错误截图。
20
+ 3. 将错误信息和截图保存到 Default Dataset 中。
21
+
22
+ **参数:**
23
+ - `stepName` (string): 步骤名称,用于日志和错误报告。
24
+ - `page` (Page): Playwright Page 对象,用于截图。
25
+ - `stepFunction` (function): 包含实际逻辑的异步函数。
26
+
27
+ **示例:**
28
+ ```javascript
29
+ await ApifyKit.runStep('填写登录表单', page, async () => {
30
+ await page.fill('#username', 'user');
31
+ await page.fill('#password', 'pass');
32
+ await page.click('#login');
33
+ });
34
+ ```
35
+
36
+ ### `pushSuccess(data)`
37
+
38
+ 将成功的数据推送到 Default Dataset。会自动添加 `status: 'SUCCESS'` 和时间戳。
39
+
40
+ **参数:**
41
+ - `data` (object): 要推送的数据对象。
42
+
43
+ **示例:**
44
+ ```javascript
45
+ await ApifyKit.pushSuccess({
46
+ title: 'Product A',
47
+ price: 99.9
48
+ });
49
+ ```
50
+
51
+ ### `pushFailed(stepName, errorMessage, screenshotBase64, kvStoreKey)`
52
+
53
+ (通常由 `runStep` 内部调用) 将失败信息推送到 Default Dataset。
54
+
55
+ ### `wrapStepNameWithFailedKey(failedKey, stepName)`
56
+ 将错误 Key (例如错误码) 绑定到步骤名称上,用于在失败时提取该 Key。
57
+
58
+ ### `unwrapStepName(stepName)`
59
+ 解包步骤名称,获取绑定的 Key (如果有)。
@@ -0,0 +1,25 @@
1
+ # Constants
2
+
3
+ `Constants` 模块定义了项目中共用的常量、枚举和键名映射。
4
+
5
+ ## 引入
6
+
7
+ ```javascript
8
+ import { usePlaywrightToolKit } from '@skrillex1224/playwright-toolkit';
9
+ const { Constants } = usePlaywrightToolKit();
10
+ ```
11
+
12
+ ## 导出
13
+
14
+ ### `ErrorKeygen`
15
+
16
+ 常用的错误代码 Key 枚举,用于 standardized error handling。
17
+
18
+ - `NotLogin`: 'not_login'
19
+ - `CaptchaDetected`: 'captcha_detected'
20
+ - `RegionRestricted`: 'region_restricted'
21
+ - `RateLimited`: 'rate_limited'
22
+
23
+ ### `FAILED_KEY_SEPARATOR`
24
+
25
+ 用于 `ApifyKit.wrapStepNameWithFailedKey` 的分隔符。
@@ -0,0 +1,40 @@
1
+ # Humanize
2
+
3
+ `Humanize` 模块用于模拟人类操作行为,如随机延迟和鼠标移动。
4
+
5
+ ## 引入
6
+
7
+ ```javascript
8
+ import { usePlaywrightToolKit } from '@skrillex1224/playwright-toolkit';
9
+ const { Humanize } = usePlaywrightToolKit();
10
+ ```
11
+
12
+ ## 方法
13
+
14
+ ### `randomSleep(min, max)`
15
+
16
+ 随机等待一段毫秒数。基于 `delay` 库。
17
+
18
+ **参数:**
19
+ - `min` (number): 最小延迟 (ms)。
20
+ - `max` (number): (可选) 最大延迟 (ms)。如果未提供,则等待固定的 `min` 时间。
21
+
22
+ **示例:**
23
+ ```javascript
24
+ await Humanize.randomSleep(1000, 3000); // 等待 1-3 秒
25
+ ```
26
+
27
+ ### `simulateGaze(cursor, durationMs)`
28
+
29
+ 模拟人类“注视”或“阅读”行为:控制鼠标在页面上进行随机的小幅度移动。需要配合 `ghost-cursor` 使用。
30
+
31
+ **参数:**
32
+ - `cursor` (GhostCursor): `ghost-cursor` 对象。
33
+ - `durationMs` (number): 持续时间 (ms),默认为 2000。
34
+
35
+ **示例:**
36
+ ```javascript
37
+ import { createCursor } from 'ghost-cursor-playwright';
38
+ const cursor = await createCursor(page);
39
+ await Humanize.simulateGaze(cursor, 5000);
40
+ ```
package/docs/launch.md ADDED
@@ -0,0 +1,32 @@
1
+ # Launch
2
+
3
+ `Launch` 模块提供与浏览器启动和指纹生成相关的辅助配置。
4
+
5
+ ## 引入
6
+
7
+ ```javascript
8
+ import { usePlaywrightToolKit } from '@skrillex1224/playwright-toolkit';
9
+ const { Launch } = usePlaywrightToolKit();
10
+ ```
11
+
12
+ ## 方法
13
+
14
+ ### `getFingerprintGeneratorOptions(options)`
15
+
16
+ 返回配置好的能够通过检测的指纹生成器选项。
17
+
18
+ **参数:**
19
+ - `options` (object): (可选) 覆盖默认配置。
20
+
21
+ **默认配置:**
22
+ - `devices`: ['desktop']
23
+ - `operatingSystems`: ['windows', 'macos']
24
+ - `browsers`: ['chrome', 'edge']
25
+ - `locales`: ['zh-CN', 'en-US']
26
+
27
+ ### `getLaunchOptions(extraArgs)`
28
+
29
+ 返回合并了 Stealth 参数的 Playwright 启动选项。
30
+
31
+ **参数:**
32
+ - `extraArgs` (string[]): (可选) 额外的启动参数。
@@ -0,0 +1,38 @@
1
+ # LiveView
2
+
3
+ `LiveView` 模块使得在 Apify 平台上运行 Playwright 爬虫(尤其是有头模式)时,能够实时查看浏览器的屏幕截图。
4
+
5
+ ## 引入
6
+
7
+ ```javascript
8
+ import { usePlaywrightToolKit } from '@skrillex1224/playwright-toolkit';
9
+ const { LiveView } = usePlaywrightToolKit();
10
+ const { startLiveViewServer, takeLiveScreenshot } = LiveView.useLiveView();
11
+ ```
12
+
13
+ ## 方法
14
+
15
+ ### `startLiveViewServer()`
16
+
17
+ 启动一个轻量级的 Express 服务器,用于展示最新的屏幕截图。
18
+ 通常在 Actor 启动时调用。
19
+
20
+ **注意:** 仅在 Apify 平台上且需要 Live View 功能时调用。
21
+
22
+ ### `takeLiveScreenshot(page, logMessage)`
23
+
24
+ 拍摄当前页面的截图,并将其保存到 Key-Value Store 中供 Live Server 展示。
25
+
26
+ **参数:**
27
+ - `page` (Page): Playwright Page 对象。
28
+ - `logMessage` (string): (可选) 日志消息。
29
+
30
+ **示例:**
31
+ ```javascript
32
+ // 定时截图
33
+ setInterval(() => takeLiveScreenshot(page), 5000);
34
+ ```
35
+
36
+ ## 配置
37
+
38
+ 默认使用的 Key 为 `LIVE_VIEW_SCREENSHOT`。可以通过 `useLiveView(customKey)` 自定义。
@@ -0,0 +1,55 @@
1
+ # Stealth
2
+
3
+ `Stealth` 模块提供了一组反爬虫和隐身技术,旨在提高爬虫的存活率和稳定性。
4
+
5
+ ## 引入
6
+
7
+ ```javascript
8
+ import { usePlaywrightToolKit } from '@skrillex1224/playwright-toolkit';
9
+ const { Stealth } = usePlaywrightToolKit();
10
+ ```
11
+
12
+ ## 方法
13
+
14
+ ### `syncViewportWithScreen(page)`
15
+
16
+ **关键功能**。将 Playwright 的 Page 视口大小调整为与浏览器指纹 (window.screen) 一致。
17
+ 这可以有效防止 "Viewport Mismatch" 类型的反爬检测(例如 Akamai, Datadome 等)。
18
+
19
+ **参数:**
20
+ - `page` (Page): Playwright Page 对象。
21
+
22
+ **示例:**
23
+ ```javascript
24
+ // 在 preNavigationHooks 中使用
25
+ preNavigationHooks: [
26
+ async ({ page }) => {
27
+ await Stealth.syncViewportWithScreen(page);
28
+ }
29
+ ]
30
+ ```
31
+
32
+ ### `hideWebdriver(page)`
33
+
34
+ 确保 `navigator.webdriver` 属性被隐藏或设置为 false。虽然 `puppeteer-extra-plugin-stealth` 已经做了这个,但这是一个双重保险。
35
+
36
+ ### `setupBlockingResources(page, resourceTypes)`
37
+
38
+ 拦截并屏蔽指定类型的资源请求,以加速页面加载并节省带宽。
39
+
40
+ **参数:**
41
+ - `page` (Page): Playwright Page 对象。
42
+ - `resourceTypes` (string[]): (可选) 默认为 `['font', 'image', 'media']`。
43
+
44
+ ### `getStealthLaunchArgs()`
45
+
46
+ 返回一组推荐的 Chrome 启动参数,用于隐藏自动化特征。
47
+
48
+ **示例:**
49
+ ```javascript
50
+ launchContext: {
51
+ launchOptions: {
52
+ args: Stealth.getStealthLaunchArgs()
53
+ }
54
+ }
55
+ ```
package/docs/utils.md ADDED
@@ -0,0 +1,28 @@
1
+ # Utils
2
+
3
+ `Utils` 模块包含通用的工具函数,不依赖于 Apify 或特定的爬虫逻辑。
4
+
5
+ ## 引入
6
+
7
+ ```javascript
8
+ import { usePlaywrightToolKit } from '@skrillex1224/playwright-toolkit';
9
+ const { Utils } = usePlaywrightToolKit();
10
+ ```
11
+
12
+ ## 方法
13
+
14
+ ### `parseSseStream(sseStreamText)`
15
+
16
+ 解析 Server-Sent Events (SSE) 格式的文本流。常用于处理 AI 模型的流式响应。
17
+
18
+ **参数:**
19
+ - `sseStreamText` (string): 完整的 SSE 文本。
20
+
21
+ **返回:**
22
+ - `Array<Object>`: 解析后的 JSON 对象数组。会自动过滤掉非 JSON 行和空行。
23
+
24
+ **示例:**
25
+ ```javascript
26
+ const events = Utils.parseSseStream(responseText);
27
+ console.log(events[0].data);
28
+ ```
package/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import { ApifyKit } from './src/apify-kit.js';
2
+ import { Utils } from './src/utils.js';
3
+ import { Stealth } from './src/stealth.js';
4
+ import { Humanize } from './src/humanize.js';
5
+ import { Launch } from './src/launch.js';
6
+ import { LiveView } from './src/live-view.js';
7
+ import * as Constants from './src/constants.js';
8
+
9
+ // Unified Entry Point
10
+ export const usePlaywrightToolKit = () => {
11
+ return {
12
+ ApifyKit,
13
+
14
+ Stealth,
15
+ Humanize,
16
+ Launch,
17
+ LiveView,
18
+ Constants,
19
+ Utils
20
+ };
21
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@skrillex1224/playwright-toolkit",
3
+ "version": "2.0.0",
4
+ "description": "一个在 Apify/Crawlee Actor 中启用实时截图视图的实用工具库。",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "keywords": [
11
+ "apify",
12
+ "crawlee",
13
+ "live-view",
14
+ "screenshot",
15
+ "playwright"
16
+ ],
17
+ "author": "FJH",
18
+ "license": "ISC",
19
+ "dependencies": {
20
+ "delay": "^7.0.0",
21
+ "express": "^4.18.2"
22
+ },
23
+ "peerDependencies": {
24
+ "apify": "*",
25
+ "crawlee": "*",
26
+ "playwright": "*"
27
+ }
28
+ }
@@ -0,0 +1,108 @@
1
+ import { log } from 'crawlee';
2
+ import { Actor } from 'apify';
3
+ import { Status, FAILED_KEY_SEPARATOR, StatusCode } from './constants.js';
4
+
5
+ export const ApifyKit = {
6
+
7
+ /**
8
+ * 包装 Step Name
9
+ */
10
+ wrapStepNameWithFailedKey(key, stepName) {
11
+ return `${key}${FAILED_KEY_SEPARATOR}${stepName}`;
12
+ },
13
+
14
+ /**
15
+ * 解包 Step Name
16
+ */
17
+ unwrapStepName(stepName) {
18
+ const splitIndex = stepName.indexOf(FAILED_KEY_SEPARATOR);
19
+ if (splitIndex === -1) {
20
+ return ['-', stepName];
21
+ }
22
+ const key = stepName.substring(0, splitIndex);
23
+ const value = stepName.substring(splitIndex + FAILED_KEY_SEPARATOR.length);
24
+ return [key, value];
25
+ },
26
+
27
+ /**
28
+ * 核心封装:执行步骤,带自动日志确认和失败截图处理
29
+ */
30
+ async runStep(pendingStepName, page, actionFn, options = {}) {
31
+ const { failActor = true } = options; // 默认调用 Actor.fail
32
+ const [failedKey, stepName] = this.unwrapStepName(pendingStepName);
33
+
34
+ log.info(`🔄 [正在执行] ${stepName}...`);
35
+
36
+ try {
37
+ const result = await actionFn();
38
+ log.info(`✅ [执行成功] ${stepName}`);
39
+ return result;
40
+ } catch (error) {
41
+ log.error(`❌ [执行失败] ${stepName}: ${error.message}`);
42
+
43
+ let screenshotBase64 = '截图失败';
44
+ try {
45
+ if (page) {
46
+ const buffer = await page.screenshot({ fullPage: true, type: 'jpeg', quality: 60 });
47
+ screenshotBase64 = `data:image/jpeg;base64,${buffer.toString('base64')}`;
48
+ }
49
+ } catch (snapErr) {
50
+ log.warning(`截图生成失败: ${snapErr.message}`);
51
+ }
52
+
53
+ // 使用 pushFailed 方法推送失败数据(私有使用)
54
+ await this.pushFailed(error, {
55
+ failedStep: stepName,
56
+ failedKey: failedKey,
57
+ errorMessage: error.message,
58
+ errorStack: error.stack,
59
+ screenshotBase64: screenshotBase64
60
+ });
61
+
62
+ // 根据 failActor 决定是否调用 Actor.fail
63
+ if (failActor) {
64
+ await Actor.fail(`Run Step ${stepName} 失败: ${error.message}`);
65
+ } else {
66
+ // 不调用 Actor.fail,直接抛出错误
67
+ throw error;
68
+ }
69
+ }
70
+ },
71
+
72
+ /**
73
+ * 宽松版runStep:失败时不调用Actor.fail,只抛出异常
74
+ */
75
+ async runStepLoose(stepName, page, fn) {
76
+ return await this.runStep(stepName, page, fn, { failActor: false });
77
+ },
78
+
79
+ /**
80
+ * 推送成功数据的通用方法
81
+ * @param {Object} data - 要推送的数据对象
82
+ */
83
+ async pushSuccess(data) {
84
+ await Actor.pushData({
85
+ code: StatusCode.Success,
86
+ status: Status.Success,
87
+ timestamp: new Date().toISOString(),
88
+ ...data
89
+ });
90
+ },
91
+
92
+ /**
93
+ * 推送失败数据的通用方法(私有方法,仅供runStep内部使用)
94
+ * @param {Error|Object} error - 错误对象(可包含其他的错误/或部分处理成功的额外信息)
95
+ * @param {Object} [meta] - 额外的数据(如failedStep, screenshotBase64等,仅runStep使用)
96
+ * @private
97
+ */
98
+ async pushFailed(error, meta = {}) {
99
+ await Actor.pushData({
100
+ code: StatusCode.Failed,
101
+ status: Status.Failed,
102
+ // 这里可能带其他错误信息
103
+ error,
104
+ timestamp: new Date().toISOString(),
105
+ ...meta
106
+ });
107
+ }
108
+ }
@@ -0,0 +1,18 @@
1
+ export const ErrorKeygen = {
2
+ NotLogin: 30000001,
3
+ Chaptcha: 30000002,
4
+ }
5
+
6
+ export const Status = {
7
+ Success: 'SUCCESS',
8
+ Failed: 'FAILED'
9
+ }
10
+
11
+ export const StatusCode = {
12
+ Success: 0,
13
+ Failed: -1
14
+ }
15
+
16
+ export const FAILED_KEY_SEPARATOR = '::<@>::';
17
+
18
+ export const PresetOfLiveViewKey = 'LIVE_VIEW_SCREENSHOT';
@@ -0,0 +1,37 @@
1
+ import delay from 'delay';
2
+ import { log } from 'crawlee';
3
+
4
+ export const Humanize = {
5
+ /**
6
+ * 随机延迟一段毫秒数 (API Wrapper for 'delay' package)
7
+ * @param {number} min - 最小毫秒
8
+ * @param {number} max - 最大毫秒
9
+ */
10
+ async randomSleep(min, max) {
11
+ const ms = typeof max === 'number'
12
+ ? delay.range(min, max)
13
+ : delay(min); // 如果只传一个参数,视为固定延迟或最小延迟
14
+
15
+ // log.debug(`[Humanize] Sleeping for ${await ms} ms...`); // delay return promise acts like number somewhat but best await it
16
+ // The delay package returns a promise that resolves after the delay.
17
+ // delay.range() returns a promise too.
18
+
19
+ await ms;
20
+ },
21
+
22
+ /**
23
+ * 模拟人类“注视”或“阅读”行为:鼠标在页面上随机微动。
24
+ * @param {import('ghost-cursor-playwright').GhostCursor} cursor
25
+ * @param {number} durationMs - 持续时间
26
+ */
27
+ async simulateGaze(cursor, durationMs = 2000) {
28
+ const startTime = Date.now();
29
+ while (Date.now() - startTime < durationMs) {
30
+ // 随机小幅度移动
31
+ const x = Math.random() * 800;
32
+ const y = Math.random() * 600;
33
+ await cursor.moveTo({ x, y });
34
+ await delay.range(200, 800);
35
+ }
36
+ }
37
+ }
package/src/launch.js ADDED
@@ -0,0 +1,26 @@
1
+ // 集中管理启动配置,暂时主要由 Stealth 模块提供 Args,这里作为扩展点
2
+ import { Stealth } from './stealth.js';
3
+
4
+ export const Launch = {
5
+ getLaunchOptions(customArgs = []) {
6
+ return {
7
+ args: [
8
+ ...Stealth.getStealthLaunchArgs(),
9
+ ...customArgs
10
+ ],
11
+ ignoreDefaultArgs: ['--enable-automation'],
12
+ };
13
+ },
14
+
15
+ /**
16
+ * 推荐的 Fingerprint Generator 选项
17
+ * 确保生成的是桌面端、较新的 Chrome,以匹配我们的脚本逻辑
18
+ */
19
+ getFingerprintGeneratorOptions() {
20
+ return {
21
+ browsers: [{ name: 'chrome', minVersion: 110 }],
22
+ devices: ['desktop'],
23
+ operatingSystems: ['windows', 'linux'], // 包含 Linux 兼容容器
24
+ };
25
+ }
26
+ }
@@ -0,0 +1,81 @@
1
+ import express from 'express';
2
+ import { log } from 'crawlee';
3
+ import { Actor } from 'apify';
4
+ import { PresetOfLiveViewKey } from './constants';
5
+
6
+ /**
7
+ * 启动一个 Web 服务器以在 Live View 选项卡中显示最新的屏幕截图。
8
+ */
9
+ async function startLiveViewServer(liveViewKey) {
10
+ const app = express();
11
+
12
+ app.get('/', async (req, res) => {
13
+ try {
14
+ // 从默认的 Key-Value Store 中读取最新的屏幕截图
15
+ const screenshotBuffer = await Actor.getValue(liveViewKey);
16
+
17
+ if (!screenshotBuffer) {
18
+ // 如果还没有截图,发送一个自动刷新的占位页面
19
+ res.send('<html><head><meta http-equiv="refresh" content="2"></head><body>等待第一个屏幕截图...</body></html>');
20
+ return;
21
+ }
22
+
23
+ // 将 Buffer 转换为 Base64 字符串
24
+ const screenshotBase64 = screenshotBuffer.toString('base64');
25
+
26
+ // 发送一个 HTML 页面,该页面每 1 秒自动刷新一次,并显示截图
27
+ res.send(`
28
+ <html>
29
+ <head>
30
+ <title>Live View (截图)</title>
31
+ <meta http-equiv="refresh" content="1">
32
+ </head>
33
+ <body style="margin:0; padding:0;">
34
+ <img src="data:image/png;base64,${screenshotBase64}"
35
+ alt="Live View Screenshot"
36
+ style="width: 100%; height: auto;" />
37
+ </body>
38
+ </html>
39
+ `);
40
+ } catch (error) {
41
+ log.error(`Live View 服务器错误: ${error.message}`);
42
+ res.status(500).send(`无法加载屏幕截图: ${error.message}`);
43
+ }
44
+ });
45
+
46
+ // 监听 Apify 容器端口
47
+ const port = process.env.APIFY_CONTAINER_PORT || 4321;
48
+ app.listen(port, () => { log.info(`Live View 服务器已启动,监听端口 ${port}。请打开 "Live View" 选项卡查看。`); });
49
+ }
50
+
51
+ /**
52
+ * 拍摄当前页面的屏幕截图并将其保存到 Key-Value Store。
53
+ * @param {import('playwright').Page} page
54
+ * @param {string} [logMessage] - 可选的日志消息。
55
+ */
56
+ async function takeLiveScreenshot(liveViewKey, page, logMessage) {
57
+ try {
58
+ const buffer = await page.screenshot({ type: 'png' });
59
+ await Actor.setValue(liveViewKey, buffer, { contentType: 'image/png' });
60
+ if (logMessage) {
61
+ log.info(`(截图): ${logMessage}`);
62
+ }
63
+ } catch (e) {
64
+ log.warning(`无法捕获 Live View 屏幕截图: ${e.message}`);
65
+ }
66
+ }
67
+
68
+ const useLiveView = (liveViewKey = PresetOfLiveViewKey) => {
69
+ return {
70
+ takeLiveScreenshot: async (page, logMessage) => {
71
+ return await takeLiveScreenshot(liveViewKey, page, logMessage)
72
+ },
73
+ startLiveViewServer: async () => {
74
+ return await startLiveViewServer(liveViewKey);
75
+ }
76
+ }
77
+ }
78
+
79
+ export const LiveView = {
80
+ useLiveView,
81
+ };
package/src/stealth.js ADDED
@@ -0,0 +1,75 @@
1
+ import { log } from 'crawlee';
2
+
3
+ export const Stealth = {
4
+ /**
5
+ * 关键修复:将 Page 视口调整为与浏览器指纹 (window.screen) 一致。
6
+ * 防止 "Viewport Mismatch" 类型的反爬检测。
7
+ * @param {import('playwright').Page} page
8
+ */
9
+ async syncViewportWithScreen(page) {
10
+ try {
11
+ // 获取指纹中的屏幕尺寸
12
+ const screen = await page.evaluate(() => ({
13
+ width: window.screen.width,
14
+ height: window.screen.height,
15
+ availWidth: window.screen.availWidth,
16
+ availHeight: window.screen.availHeight,
17
+ }));
18
+
19
+ // 调整视口
20
+ await page.setViewportSize({
21
+ width: screen.width,
22
+ height: screen.height
23
+ });
24
+
25
+ log.info(`[Stealth] Viewport synced to fingerprint: ${screen.width}x${screen.height}`);
26
+ } catch (e) {
27
+ log.warning(`[Stealth] Failed to sync viewport: ${e.message}. Fallback to 1920x1080.`);
28
+ await page.setViewportSize({ width: 1920, height: 1080 });
29
+ }
30
+ },
31
+
32
+ /**
33
+ * 确保 navigator.webdriver 隐藏 (通常 Playwright Stealth 插件已处理,但双重保险)
34
+ */
35
+ async hideWebdriver(page) {
36
+ await page.addInitScript(() => {
37
+ Object.defineProperty(navigator, 'webdriver', {
38
+ get: () => false,
39
+ });
40
+ });
41
+ },
42
+
43
+ /**
44
+ * 通用的 Playwright 资源拦截器,用于屏蔽不必要的资源以加速加载
45
+ * @param {import('playwright').Page} page
46
+ * @param {string[]} [resourceTypes] - 要屏蔽的资源类型,默认为 ['font', 'image', 'media']
47
+ */
48
+ async setupBlockingResources(page, resourceTypes = ['font', 'image', 'media']) {
49
+ await page.route('**/*', (route) => {
50
+ const request = route.request();
51
+ const type = request.resourceType();
52
+ if (resourceTypes.includes(type)) {
53
+ return route.abort();
54
+ }
55
+ return route.continue();
56
+ });
57
+ },
58
+
59
+ /**
60
+ * 获取推荐的 Stealth 启动参数
61
+ */
62
+ getStealthLaunchArgs() {
63
+ return [
64
+ '--disable-blink-features=AutomationControlled',
65
+ '--no-sandbox',
66
+ '--disable-setuid-sandbox',
67
+ '--disable-infobars',
68
+ '--window-position=0,0',
69
+ '--ignore-certificate-errors',
70
+ '--disable-web-security',
71
+ // 注意:不建议这里强制指定 window-size,让 syncViewportWithScreen 去动态调整
72
+ // '--window-size=1920,1080'
73
+ ];
74
+ }
75
+ }
package/src/utils.js ADDED
@@ -0,0 +1,22 @@
1
+ export const Utils = {
2
+ /**
3
+ * 解析 SSE 流文本
4
+ */
5
+ parseSseStream(sseStreamText) {
6
+ const events = [];
7
+ const lines = sseStreamText.split('\n');
8
+ for (const line of lines) {
9
+ if (line.startsWith('data: ')) {
10
+ try {
11
+ const jsonContent = line.substring(6).trim();
12
+ if (jsonContent && jsonContent !== '[DONE]') {
13
+ events.push(JSON.parse(jsonContent));
14
+ }
15
+ } catch (e) {
16
+ // Ignore lines that are not valid JSON
17
+ }
18
+ }
19
+ }
20
+ return events;
21
+ }
22
+ }