@skrillex1224/playwright-toolkit 2.1.50 → 2.1.52

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/dist/index.js CHANGED
@@ -5,7 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // entrys/node.js
8
- import { log as crawleeLog2 } from "crawlee";
8
+ import { log as crawleeLog } from "crawlee";
9
9
 
10
10
  // src/constants.js
11
11
  var constants_exports = {};
@@ -31,7 +31,7 @@ var Status = {
31
31
  var FAILED_KEY_SEPARATOR = "::<@>::";
32
32
  var PresetOfLiveViewKey = "LIVE_VIEW_SCREENSHOT";
33
33
 
34
- // src/logger.js
34
+ // src/internals/logger.js
35
35
  var formatLine = (prefix, icon, message) => {
36
36
  const parts = [];
37
37
  if (prefix) parts.push(`[${prefix}]`);
@@ -58,9 +58,9 @@ var defaultLogger = null;
58
58
  var setDefaultLogger = (logger10) => {
59
59
  defaultLogger = logger10;
60
60
  };
61
- var resolveLogger = (logger10) => {
62
- if (logger10 && typeof logger10.info === "function") {
63
- return logger10;
61
+ var resolveLogger = (explicitLogger) => {
62
+ if (explicitLogger && typeof explicitLogger.info === "function") {
63
+ return explicitLogger;
64
64
  }
65
65
  if (defaultLogger && typeof defaultLogger.info === "function") {
66
66
  return defaultLogger;
@@ -76,2226 +76,2203 @@ var ANSI = {
76
76
  blue: "\x1B[34m",
77
77
  cyan: "\x1B[36m"
78
78
  };
79
- var stripAnsi = (input) => {
80
- if (!input) return "";
81
- return String(input).replace(/\x1b\[[0-9;]*m/g, "");
82
- };
83
79
  var colorize = (text, color) => {
84
80
  if (!text || !color) return text;
85
81
  return `${color}${text}${ANSI.reset}`;
86
82
  };
87
- var createBaseLogger = (prefix = "", logger10) => {
83
+ var createBaseLogger = (prefix = "", explicitLogger) => {
88
84
  const name = prefix ? String(prefix) : "";
89
- const targetLogger = resolveLogger(logger10);
90
- const info = resolveLogMethod(targetLogger, "info");
91
- const warning = resolveLogMethod(targetLogger, "warning");
92
- const error = resolveLogMethod(targetLogger, "error");
93
- const debug = resolveLogMethod(targetLogger, "debug");
85
+ const dispatch = (methodName, icon, message, color) => {
86
+ const logger10 = resolveLogger(explicitLogger);
87
+ const logFn = resolveLogMethod(logger10, methodName);
88
+ logFn(colorize(formatLine(name, icon, message), color));
89
+ };
94
90
  return {
95
- info: (message) => info(colorize(formatLine(name, "\u{1F4D6}", message), ANSI.cyan)),
96
- success: (message) => info(colorize(formatLine(name, "\u2705", message), ANSI.green)),
97
- warning: (message) => warning(colorize(formatLine(name, "\u26A0\uFE0F", message), ANSI.yellow)),
98
- warn: (message) => warning(colorize(formatLine(name, "\u26A0\uFE0F", message), ANSI.yellow)),
99
- error: (message) => error(colorize(formatLine(name, "\u274C", message), ANSI.red)),
100
- debug: (message) => debug(colorize(formatLine(name, "\u{1F539}", message), ANSI.gray)),
101
- start: (message) => info(colorize(formatLine(name, "\u{1F537}", message), ANSI.blue))
91
+ info: (message) => dispatch("info", "\u{1F4D6}", message, ANSI.cyan),
92
+ success: (message) => dispatch("info", "\u2705", message, ANSI.green),
93
+ warning: (message) => dispatch("warning", "\u26A0\uFE0F", message, ANSI.yellow),
94
+ warn: (message) => dispatch("warning", "\u26A0\uFE0F", message, ANSI.yellow),
95
+ error: (message) => dispatch("error", "\u274C", message, ANSI.red),
96
+ debug: (message) => dispatch("debug", "\u{1F539}", message, ANSI.gray),
97
+ start: (message) => dispatch("info", "\u{1F537}", message, ANSI.blue)
102
98
  };
103
99
  };
104
- var STEP_PREFIX = "\u6B65\u9AA4:";
105
- var STEP_SEPARATOR = " | ";
106
- var STEP_EMOJIS = [
107
- { match: "\u4EFB\u52A1", emoji: "\u{1F9ED}" },
108
- { match: "\u8FD0\u884C\u6A21\u5F0F", emoji: "\u{1F9E9}" },
109
- { match: "\u767B\u5F55", emoji: "\u{1F510}" },
110
- { match: "\u73AF\u5883", emoji: "\u{1F9EA}" },
111
- { match: "\u8F93\u5165", emoji: "\u2328\uFE0F" },
112
- { match: "\u53D1\u9001", emoji: "\u{1F4E4}" },
113
- { match: "\u54CD\u5E94\u76D1\u542C", emoji: "\u{1F4E1}" },
114
- { match: "\u7B49\u5F85\u54CD\u5E94", emoji: "\u23F3" },
115
- { match: "\u6D41\u5F0F", emoji: "\u{1F9F5}" },
116
- { match: "\u5F15\u7528", emoji: "\u{1F4CE}" },
117
- { match: "\u622A\u56FE", emoji: "\u{1F5BC}\uFE0F" },
118
- { match: "\u5206\u4EAB\u94FE\u63A5", emoji: "\u{1F517}" },
119
- { match: "\u6570\u636E\u63A8\u9001", emoji: "\u{1F4E6}" },
120
- { match: "\u5F39\u7A97", emoji: "\u{1FA9F}" }
121
- ];
122
- var STATUS_EMOJIS = [
123
- { match: "\u5F00\u59CB", emoji: "\u{1F680}" },
124
- { match: "\u5B8C\u6210", emoji: "\u2705" },
125
- { match: "\u6210\u529F", emoji: "\u2705" },
126
- { match: "\u5931\u8D25", emoji: "\u274C" },
127
- { match: "\u8DF3\u8FC7", emoji: "\u23ED\uFE0F" },
128
- { match: "\u8D85\u65F6", emoji: "\u23F1\uFE0F" },
129
- { match: "\u91CD\u8BD5", emoji: "\u{1F501}" },
130
- { match: "\u7ED3\u675F", emoji: "\u{1F3C1}" },
131
- { match: "\u5DF2\u914D\u7F6E", emoji: "\u{1F9F7}" },
132
- { match: "\u5DF2\u68C0\u6D4B", emoji: "\u{1F50E}" }
133
- ];
134
- var toErrorMessage = (error) => {
135
- if (!error) return "";
136
- if (error instanceof Error) return error.message;
137
- if (typeof error === "string") return error;
138
- try {
139
- return JSON.stringify(error);
140
- } catch {
141
- return String(error);
100
+ function createInternalLogger(moduleName, explicitLogger) {
101
+ const baseLogger = createBaseLogger(moduleName, explicitLogger);
102
+ return {
103
+ start(methodName, params = "") {
104
+ const paramStr = params ? ` (${params})` : "";
105
+ baseLogger.start(`${methodName} \u5F00\u59CB${paramStr}`);
106
+ },
107
+ success(methodName, result = "") {
108
+ const resultStr = result ? ` (${result})` : "";
109
+ baseLogger.success(`${methodName} \u5B8C\u6210${resultStr}`);
110
+ },
111
+ fail(methodName, error) {
112
+ const message = error instanceof Error ? error.message : error;
113
+ baseLogger.error(`${methodName} \u5931\u8D25: ${message}`);
114
+ },
115
+ debug(message) {
116
+ baseLogger.debug(message);
117
+ },
118
+ warn(message) {
119
+ baseLogger.warning(message);
120
+ },
121
+ warning(message) {
122
+ baseLogger.warning(message);
123
+ },
124
+ info(message) {
125
+ baseLogger.info(message);
126
+ }
127
+ };
128
+ }
129
+
130
+ // src/errors.js
131
+ var errors_exports = {};
132
+ __export(errors_exports, {
133
+ CrawlerError: () => CrawlerError
134
+ });
135
+ import { serializeError } from "serialize-error";
136
+ var CrawlerError = class _CrawlerError extends Error {
137
+ /**
138
+ * @param {string|Object} info - 错误信息字符串或配置对象
139
+ * @param {string} info.message - 错误信息
140
+ * @param {number} [info.code] - ErrorKeygen 枚举值(用于错误分类)
141
+ * @param {Object} [info.context] - 上下文信息对象
142
+ */
143
+ constructor(info) {
144
+ if (typeof info === "string") {
145
+ info = { message: info };
146
+ }
147
+ super(info.message);
148
+ this.name = "CrawlerError";
149
+ this.code = info.code ?? Code.UnknownError;
150
+ this.context = info.context ?? {};
151
+ this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
152
+ if (Error.captureStackTrace) {
153
+ Error.captureStackTrace(this, _CrawlerError);
154
+ }
142
155
  }
143
- };
144
- var decorateLabel = (label, mappings) => {
145
- if (!label) return "";
146
- const mapping = mappings.find((item) => label.includes(item.match));
147
- if (!mapping) return label;
148
- return `${mapping.emoji} ${label}`;
149
- };
150
- var normalizeSnippet = (snippet, maxLen = 120) => {
151
- if (!snippet) return "";
152
- const text = String(snippet).replace(/\s+/g, " ").trim();
153
- if (!text) return "";
154
- const cleaned = text.replace(/"/g, "'");
155
- if (cleaned.length <= maxLen) return cleaned;
156
- return `${cleaned.slice(0, maxLen)}...`;
157
- };
158
- var LOG_TAG_PREFIX = "[#log:";
159
- var LOG_TAG_SUFFIX = "]";
160
- var buildLogTag = (key) => `${LOG_TAG_PREFIX}${key}${LOG_TAG_SUFFIX}`;
161
- var escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
162
- var buildStepPattern = (step, status) => {
163
- if (!step) return null;
164
- let pattern = `\u6B65\u9AA4: .*${escapeRegExp(step)}`;
165
- if (status) {
166
- pattern += `.*${escapeRegExp(status)}`;
156
+ /**
157
+ * 转换为可推送的数据对象
158
+ * @returns {Object}
159
+ */
160
+ toJSON() {
161
+ return serializeError(this);
167
162
  }
168
- return new RegExp(pattern);
169
- };
170
- var buildDefinitionPatterns = (definition) => {
171
- const patterns = [new RegExp(`\\[#log:${escapeRegExp(definition.key)}\\]`)];
172
- const fallback = buildStepPattern(definition.step, definition.status);
173
- if (fallback) patterns.push(fallback);
174
- if (Array.isArray(definition.extraPatterns)) {
175
- patterns.push(...definition.extraPatterns);
163
+ /**
164
+ * 检查一个 error 是否是 CrawlerError
165
+ * @param {Error} error
166
+ * @returns {boolean}
167
+ */
168
+ static isCrawlerError(error) {
169
+ return error instanceof _CrawlerError || error?.name === "CrawlerError";
170
+ }
171
+ /**
172
+ * 从普通 Error 创建 CrawlerError
173
+ * @param {Error} error - 原始错误
174
+ * @param {Object} [options={}] - 选项对象 (包含 code 和 context)
175
+ * @returns {CrawlerError}
176
+ */
177
+ static from(error, options = {}) {
178
+ const crawlerError = new _CrawlerError({
179
+ message: error.message,
180
+ ...options
181
+ });
182
+ crawlerError.stack = error.stack;
183
+ return crawlerError;
176
184
  }
177
- return patterns;
178
- };
179
- var ATTENTION_RANK = {
180
- low: 1,
181
- medium: 2,
182
- high: 3,
183
- critical: 4
184
185
  };
185
- var DEFAULT_ATTENTION_RANK = ATTENTION_RANK.high;
186
- var LOG_DEFINITIONS = [
187
- {
188
- key: "task_start",
189
- method: "taskStart",
190
- label: "\u4EFB\u52A1\u5F00\u59CB",
191
- group: "\u4EFB\u52A1",
192
- step: "\u4EFB\u52A1",
193
- status: "\u5F00\u59CB",
194
- level: "start",
195
- attention: "low",
196
- buildDetails: (url) => [url ? `url=${url}` : ""]
197
- },
198
- {
199
- key: "task_success",
200
- method: "taskSuccess",
201
- label: "\u4EFB\u52A1\u5B8C\u6210",
202
- group: "\u4EFB\u52A1",
203
- step: "\u4EFB\u52A1",
204
- status: "\u5B8C\u6210",
205
- level: "success",
206
- attention: "high"
207
- },
208
- {
209
- key: "task_fail",
210
- method: "taskFail",
211
- label: "\u4EFB\u52A1\u5931\u8D25",
212
- group: "\u4EFB\u52A1",
213
- step: "\u4EFB\u52A1",
214
- status: "\u5931\u8D25",
215
- level: "error",
216
- attention: "critical",
217
- buildDetails: (url, err) => [
218
- url ? `url=${url}` : "",
219
- err ? `err=${toErrorMessage(err)}` : ""
220
- ]
221
- },
222
- {
223
- key: "runtime_headless",
224
- method: "runtimeHeadless",
225
- label: "\u8FD0\u884C\u6A21\u5F0F\u5F3A\u5236\u65E0\u5934",
226
- group: "\u8FD0\u884C\u6A21\u5F0F",
227
- step: "\u8FD0\u884C\u6A21\u5F0F",
228
- status: "Apify \u73AF\u5883\u5F3A\u5236\u65E0\u5934",
229
- level: "warning",
230
- attention: "medium"
231
- },
232
- {
233
- key: "login_inject_success",
234
- method: "loginInjectSuccess",
235
- label: "\u767B\u5F55\u6001\u6CE8\u5165\u6210\u529F",
236
- group: "\u767B\u5F55",
237
- step: "\u767B\u5F55\u6001\u6CE8\u5165",
238
- status: "\u6210\u529F",
239
- level: "success",
240
- attention: "medium",
241
- buildDetails: (detail) => [detail ? `detail=${detail}` : ""]
242
- },
243
- {
244
- key: "login_inject_skip",
245
- method: "loginInjectSkip",
246
- label: "\u767B\u5F55\u6001\u6CE8\u5165\u8DF3\u8FC7",
247
- group: "\u767B\u5F55",
248
- step: "\u767B\u5F55\u6001\u6CE8\u5165",
249
- status: "\u8DF3\u8FC7",
250
- level: "warning",
251
- attention: "medium",
252
- buildDetails: (reason) => [reason ? `\u539F\u56E0=${reason}` : ""]
253
- },
254
- {
255
- key: "login_inject_fail",
256
- method: "loginInjectFail",
257
- label: "\u767B\u5F55\u6001\u6CE8\u5165\u5931\u8D25",
258
- group: "\u767B\u5F55",
259
- step: "\u767B\u5F55\u6001\u6CE8\u5165",
260
- status: "\u5931\u8D25",
261
- level: "error",
262
- attention: "high",
263
- buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
264
- },
265
- {
266
- key: "login_verify_success",
267
- method: "loginVerifySuccess",
268
- label: "\u767B\u5F55\u9A8C\u8BC1\u6210\u529F",
269
- group: "\u767B\u5F55",
270
- step: "\u767B\u5F55\u9A8C\u8BC1",
271
- status: "\u6210\u529F",
272
- level: "success",
273
- attention: "high",
274
- buildDetails: (detail) => [detail ? `detail=${detail}` : ""]
275
- },
276
- {
277
- key: "login_verify_skip",
278
- method: "loginVerifySkip",
279
- label: "\u767B\u5F55\u9A8C\u8BC1\u8DF3\u8FC7",
280
- group: "\u767B\u5F55",
281
- step: "\u767B\u5F55\u9A8C\u8BC1",
282
- status: "\u8DF3\u8FC7",
283
- level: "warning",
284
- attention: "medium",
285
- buildDetails: (reason) => [reason ? `\u539F\u56E0=${reason}` : ""]
286
- },
287
- {
288
- key: "login_verify_fail",
289
- method: "loginVerifyFail",
290
- label: "\u767B\u5F55\u9A8C\u8BC1\u5931\u8D25",
291
- group: "\u767B\u5F55",
292
- step: "\u767B\u5F55\u9A8C\u8BC1",
293
- status: "\u5931\u8D25",
294
- level: "error",
295
- attention: "critical",
296
- buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
297
- },
298
- {
299
- key: "env_check_success",
300
- method: "envCheckSuccess",
301
- label: "\u73AF\u5883\u68C0\u67E5\u6210\u529F",
302
- group: "\u73AF\u5883",
303
- step: "\u73AF\u5883\u68C0\u67E5",
304
- status: "\u6210\u529F",
305
- level: "success",
306
- attention: "medium",
307
- buildDetails: (detail) => [detail ? `detail=${detail}` : ""]
308
- },
309
- {
310
- key: "env_check_fail",
311
- method: "envCheckFail",
312
- label: "\u73AF\u5883\u68C0\u67E5\u5931\u8D25",
313
- group: "\u73AF\u5883",
314
- step: "\u73AF\u5883\u68C0\u67E5",
315
- status: "\u5931\u8D25",
316
- level: "error",
317
- attention: "high",
318
- buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
319
- },
320
- {
321
- key: "input_query_start",
322
- method: "inputQuery",
323
- label: "\u8F93\u5165\u67E5\u8BE2\u5F00\u59CB",
324
- group: "\u8F93\u5165",
325
- step: "\u8F93\u5165\u67E5\u8BE2",
326
- status: "\u5F00\u59CB",
327
- level: "start",
328
- attention: "low",
329
- buildDetails: (query) => [query ? `query=${query}` : ""]
330
- },
331
- {
332
- key: "send_action",
333
- method: "sendAction",
334
- label: "\u53D1\u9001\u8BF7\u6C42",
335
- group: "\u53D1\u9001",
336
- step: "\u53D1\u9001\u8BF7\u6C42",
337
- status: "\u70B9\u51FB\u53D1\u9001",
338
- level: "info",
339
- attention: "low"
340
- },
341
- {
342
- key: "response_listen_start",
343
- method: "responseListenStart",
344
- label: "\u54CD\u5E94\u76D1\u542C\u5F00\u59CB",
345
- group: "\u54CD\u5E94\u76D1\u542C",
346
- step: "\u54CD\u5E94\u76D1\u542C",
347
- status: "\u5F00\u59CB",
348
- level: "start",
349
- attention: "low",
350
- buildDetails: (label, timeoutSec) => [
351
- label ? `\u76EE\u6807=${label}` : "",
352
- timeoutSec ? `timeout=${timeoutSec}s` : ""
353
- ]
354
- },
355
- {
356
- key: "response_listen_ready",
357
- method: "responseListenReady",
358
- label: "\u54CD\u5E94\u76D1\u542C\u5DF2\u914D\u7F6E",
359
- group: "\u54CD\u5E94\u76D1\u542C",
360
- step: "\u54CD\u5E94\u76D1\u542C",
361
- status: "\u5DF2\u914D\u7F6E",
362
- level: "info",
363
- attention: "medium",
364
- buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
365
- },
366
- {
367
- key: "response_listen_detected",
368
- method: "responseListenDetected",
369
- label: "\u54CD\u5E94\u76D1\u542C\u5DF2\u68C0\u6D4B",
370
- group: "\u54CD\u5E94\u76D1\u542C",
371
- step: "\u54CD\u5E94\u76D1\u542C",
372
- status: "\u5DF2\u68C0\u6D4B",
373
- level: "success",
374
- attention: "medium",
375
- buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
376
- },
377
- {
378
- key: "response_listen_timeout",
379
- method: "responseListenTimeout",
380
- label: "\u54CD\u5E94\u76D1\u542C\u8D85\u65F6",
381
- group: "\u54CD\u5E94\u76D1\u542C",
382
- step: "\u54CD\u5E94\u76D1\u542C",
383
- status: "\u8D85\u65F6",
384
- level: "error",
385
- attention: "high",
386
- buildDetails: (label, err) => [
387
- label ? `\u76EE\u6807=${label}` : "",
388
- err ? `err=${toErrorMessage(err)}` : ""
389
- ]
390
- },
391
- {
392
- key: "response_listen_end",
393
- method: "responseListenEnd",
394
- label: "\u54CD\u5E94\u76D1\u542C\u7ED3\u675F",
395
- group: "\u54CD\u5E94\u76D1\u542C",
396
- step: "\u54CD\u5E94\u76D1\u542C",
397
- status: "\u7ED3\u675F",
398
- level: "info",
399
- attention: "low",
400
- buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
401
- },
402
- {
403
- key: "response_wait_start",
404
- method: "responseWaitStart",
405
- label: "\u7B49\u5F85\u54CD\u5E94\u5F00\u59CB",
406
- group: "\u7B49\u5F85\u54CD\u5E94",
407
- step: "\u7B49\u5F85\u54CD\u5E94",
408
- status: "\u5F00\u59CB",
409
- level: "start",
410
- attention: "low",
411
- buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
412
- },
413
- {
414
- key: "response_wait_success",
415
- method: "responseWaitSuccess",
416
- label: "\u7B49\u5F85\u54CD\u5E94\u5B8C\u6210",
417
- group: "\u7B49\u5F85\u54CD\u5E94",
418
- step: "\u7B49\u5F85\u54CD\u5E94",
419
- status: "\u5B8C\u6210",
420
- level: "success",
421
- attention: "medium",
422
- buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
423
- },
424
- {
425
- key: "response_wait_fail",
426
- method: "responseWaitFail",
427
- label: "\u7B49\u5F85\u54CD\u5E94\u5931\u8D25",
428
- group: "\u7B49\u5F85\u54CD\u5E94",
429
- step: "\u7B49\u5F85\u54CD\u5E94",
430
- status: "\u5931\u8D25",
431
- level: "warning",
432
- attention: "high",
433
- buildDetails: (label, err) => [
434
- label ? `\u76EE\u6807=${label}` : "",
435
- err ? `err=${toErrorMessage(err)}` : ""
436
- ]
437
- },
438
- {
439
- key: "response_wait_retry",
440
- method: "responseWaitRetry",
441
- label: "\u7B49\u5F85\u54CD\u5E94\u91CD\u8BD5",
442
- group: "\u7B49\u5F85\u54CD\u5E94",
443
- step: "\u7B49\u5F85\u54CD\u5E94",
444
- status: "\u91CD\u8BD5",
445
- level: "warning",
446
- attention: "medium",
447
- buildDetails: (label, attempt) => [
448
- label ? `\u76EE\u6807=${label}` : "",
449
- attempt !== void 0 ? `\u5C1D\u8BD5=${attempt}` : ""
450
- ]
451
- },
452
- {
453
- key: "dom_chunk",
454
- method: "domChunk",
455
- label: "DOM\u7247\u6BB5",
456
- group: "DOM",
457
- step: "DOM\u7247\u6BB5",
458
- status: "",
459
- level: "info",
460
- attention: "low",
461
- throttleKey: "dom-chunk",
462
- throttleMs: 2e3,
463
- buildDetails: (length, snippet, paused) => [
464
- length !== void 0 ? `len=${length}` : "",
465
- snippet ? `preview="${normalizeSnippet(snippet)}"` : "",
466
- paused ? "paused=1" : ""
467
- ]
468
- },
469
- {
470
- key: "dom_complete",
471
- method: "domComplete",
472
- label: "DOM\u7A33\u5B9A\u5B8C\u6210",
473
- group: "DOM",
474
- step: "DOM\u7A33\u5B9A",
475
- status: "\u5B8C\u6210",
476
- level: "success",
477
- attention: "medium",
478
- buildDetails: (mutationCount, stableTime, wasPaused) => [
479
- mutationCount !== void 0 ? `mutations=${mutationCount}` : "",
480
- stableTime !== void 0 ? `stableTime=${stableTime}ms` : "",
481
- wasPaused ? "paused=1" : ""
482
- ]
483
- },
484
- {
485
- key: "stream_chunk",
486
- method: "streamChunk",
487
- label: "\u6D41\u5F0F\u7247\u6BB5",
488
- group: "\u6D41\u5F0F",
489
- step: "\u6D41\u5F0F\u7247\u6BB5",
490
- status: "",
491
- level: "info",
492
- attention: "low",
493
- throttleKey: "stream-chunk",
494
- throttleMs: 2e3,
495
- buildDetails: (length, snippet) => [
496
- length !== void 0 ? `len=${length}` : "",
497
- snippet ? `preview="${normalizeSnippet(snippet)}"` : ""
498
- ]
499
- },
500
- {
501
- key: "stream_events_parsed",
502
- method: "streamEventsParsed",
503
- label: "\u6D41\u5F0F\u4E8B\u4EF6\u89E3\u6790\u5B8C\u6210",
504
- group: "\u6D41\u5F0F",
505
- step: "\u6D41\u5F0F\u4E8B\u4EF6\u89E3\u6790",
506
- status: "\u5B8C\u6210",
507
- level: "info",
508
- attention: "low",
509
- throttleKey: "stream-events",
510
- throttleMs: 4e3,
511
- buildDetails: (count) => [count !== void 0 ? `count=${count}` : ""]
512
- },
513
- {
514
- key: "stream_complete_event",
515
- method: "streamCompleteEvent",
516
- label: "\u6D41\u5F0F\u5B8C\u6210\u4E8B\u4EF6\u6355\u83B7",
517
- group: "\u6D41\u5F0F",
518
- step: "\u6D41\u5F0F\u4E8B\u4EF6",
519
- status: "\u5B8C\u6210\u4E8B\u4EF6\u5DF2\u6355\u83B7",
520
- level: "success",
521
- attention: "medium"
522
- },
523
- {
524
- key: "stream_end",
525
- method: "streamEnd",
526
- label: "\u6D41\u5F0F\u54CD\u5E94\u7ED3\u675F",
527
- group: "\u6D41\u5F0F",
528
- step: "\u6D41\u5F0F\u54CD\u5E94",
529
- status: "\u7ED3\u675F",
530
- level: "info",
531
- attention: "low"
532
- },
533
- {
534
- key: "reference_expand_start",
535
- method: "referenceExpandStart",
536
- label: "\u5F15\u7528\u5C55\u5F00\u5F00\u59CB",
537
- group: "\u5F15\u7528",
538
- step: "\u5F15\u7528\u5C55\u5F00",
539
- status: "\u5F00\u59CB",
540
- level: "start",
541
- attention: "low",
542
- buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
543
- },
544
- {
545
- key: "reference_expand_fail",
546
- method: "referenceExpandFail",
547
- label: "\u5F15\u7528\u5C55\u5F00\u5931\u8D25",
548
- group: "\u5F15\u7528",
549
- step: "\u5F15\u7528\u5C55\u5F00",
550
- status: "\u5931\u8D25",
551
- level: "warning",
552
- attention: "medium",
553
- buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
554
- },
555
- {
556
- key: "screenshot_start",
557
- method: "screenshotStart",
558
- label: "\u622A\u56FE\u5F00\u59CB",
559
- group: "\u622A\u56FE",
560
- step: "\u622A\u56FE",
561
- status: "\u5F00\u59CB",
562
- level: "start",
563
- attention: "low"
186
+
187
+ // src/apify-kit.js
188
+ import { serializeError as serializeError2 } from "serialize-error";
189
+ var logger = createInternalLogger("ApifyKit");
190
+ async function createApifyKit() {
191
+ let apify = null;
192
+ try {
193
+ apify = await import("apify");
194
+ } catch (error) {
195
+ throw new Error("\u26A0\uFE0F apify \u5E93\u672A\u5B89\u88C5\uFF0CApifyKit \u7684 Actor \u76F8\u5173\u529F\u80FD\u4E0D\u53EF\u7528");
196
+ }
197
+ const { Actor: Actor2 } = apify;
198
+ return {
199
+ /**
200
+ * 核心封装:执行步骤,带自动日志确认、失败截图处理和重试机制
201
+ *
202
+ * @param {string} step - 步骤名称
203
+ * @param {import('playwright').Page} page - Playwright page 对象
204
+ * @param {Function} actionFn - 执行的异步操作
205
+ * @param {Object} [options] - 配置选项
206
+ * @param {boolean} [options.failActor=true] - 失败时是否调用 Actor.fail
207
+ * @param {Object} [options.retry] - 重试配置
208
+ * @param {number} [options.retry.times=0] - 重试次数
209
+ * @param {'direct'|'refresh'} [options.retry.mode='direct'] - 重试模式
210
+ * @param {Function} [options.retry.before] - 重试前钩子,可覆盖默认等待行为
211
+ */
212
+ async runStep(step, page, actionFn, options = {}) {
213
+ const { failActor = true, retry = {} } = options;
214
+ const { times: retryTimes = 0, mode: retryMode = "direct", before: beforeRetry } = retry;
215
+ const executeAction = async (attemptNumber) => {
216
+ const attemptLabel = attemptNumber > 0 ? ` (\u91CD\u8BD5 #${attemptNumber})` : "";
217
+ logger.start(`[Step] ${step}${attemptLabel}`);
218
+ try {
219
+ const result = await actionFn();
220
+ logger.success(`[Step] ${step}${attemptLabel}`);
221
+ return { success: true, result };
222
+ } catch (error) {
223
+ logger.fail(`[Step] ${step}${attemptLabel}`, error);
224
+ return { success: false, error };
225
+ }
226
+ };
227
+ const prepareForRetry = async (attemptNumber) => {
228
+ if (typeof beforeRetry === "function") {
229
+ logger.start(`[RetryStep] \u6267\u884C\u81EA\u5B9A\u4E49 before \u94A9\u5B50 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
230
+ await beforeRetry(page, attemptNumber);
231
+ logger.success(`[RetryStep] before \u94A9\u5B50\u5B8C\u6210`);
232
+ } else if (retryMode === "refresh") {
233
+ logger.start(`[RetryStep] \u5237\u65B0\u9875\u9762 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
234
+ await page.reload({ waitUntil: "domcontentloaded" });
235
+ logger.success(`[RetryStep] \u9875\u9762\u5237\u65B0\u5B8C\u6210`);
236
+ } else {
237
+ logger.start(`[RetryStep] \u7B49\u5F85 3 \u79D2 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
238
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
239
+ logger.success(`[RetryStep] \u7B49\u5F85\u5B8C\u6210`);
240
+ }
241
+ };
242
+ let lastResult = await executeAction(0);
243
+ if (lastResult.success) {
244
+ return lastResult.result;
245
+ }
246
+ for (let attempt = 1; attempt <= retryTimes; attempt++) {
247
+ logger.start(`[RetryStep] \u51C6\u5907\u7B2C ${attempt}/${retryTimes} \u6B21\u91CD\u8BD5: ${step}`);
248
+ try {
249
+ await prepareForRetry(attempt);
250
+ } catch (prepareError) {
251
+ logger.warn(`[RetryStep] \u91CD\u8BD5\u51C6\u5907\u5931\u8D25: ${prepareError.message}`);
252
+ continue;
253
+ }
254
+ lastResult = await executeAction(attempt);
255
+ if (lastResult.success) {
256
+ return lastResult.result;
257
+ }
258
+ }
259
+ const finalError = lastResult.error;
260
+ if (failActor) {
261
+ let base64 = "\u622A\u56FE\u5931\u8D25";
262
+ try {
263
+ if (page) {
264
+ const buffer = await page.screenshot({ fullPage: true, type: "jpeg", quality: 60 });
265
+ base64 = `data:image/jpeg;base64,${buffer.toString("base64")}`;
266
+ }
267
+ } catch (snapErr) {
268
+ logger.warn(`\u622A\u56FE\u751F\u6210\u5931\u8D25: ${snapErr.message}`);
269
+ }
270
+ await this.pushFailed(finalError, {
271
+ step,
272
+ page,
273
+ options,
274
+ base64,
275
+ retryAttempts: retryTimes
276
+ });
277
+ await Actor2.fail(`Run Step ${step} \u5931\u8D25 (\u5DF2\u91CD\u8BD5 ${retryTimes} \u6B21): ${finalError.message}`);
278
+ } else {
279
+ throw finalError;
280
+ }
281
+ },
282
+ /**
283
+ * 宽松版runStep:失败时不调用Actor.fail,只抛出异常
284
+ */
285
+ async runStepLoose(step, page, fn) {
286
+ return await this.runStep(step, page, fn, { failActor: false });
287
+ },
288
+ /**
289
+ * 推送成功数据的通用方法
290
+ * @param {Object} data - 要推送的数据对象
291
+ */
292
+ async pushSuccess(data) {
293
+ await Actor2.pushData({
294
+ // 固定为0
295
+ code: Code.Success,
296
+ status: Status.Success,
297
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
298
+ data
299
+ });
300
+ logger.success("pushSuccess", "Data pushed");
301
+ },
302
+ /**
303
+ * 推送失败数据的通用方法(私有方法,仅供runStep内部使用)
304
+ * 自动解析 CrawlerError 的 code 和 context
305
+ * @param {Error|CrawlerError} error - 错误对象
306
+ * @param {Object} [meta] - 额外的数据(如failedStep, screenshotBase64等)
307
+ * @private
308
+ */
309
+ async pushFailed(error, meta = {}) {
310
+ const isCrawlerError = CrawlerError.isCrawlerError(error);
311
+ const code = isCrawlerError ? error.code : Code.UnknownError;
312
+ const context = isCrawlerError ? error.context : {};
313
+ await Actor2.pushData({
314
+ // 如果是 CrawlerError,使用其 code,否则使用默认 Failed code
315
+ code,
316
+ status: Status.Failed,
317
+ error: serializeError2(error),
318
+ meta,
319
+ context,
320
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
321
+ });
322
+ logger.success("pushFailed", "Error data pushed");
323
+ }
324
+ };
325
+ }
326
+ var instance = null;
327
+ async function useApifyKit() {
328
+ if (!instance) {
329
+ instance = await createApifyKit();
330
+ }
331
+ return instance;
332
+ }
333
+ var ApifyKit = {
334
+ useApifyKit
335
+ };
336
+
337
+ // src/utils.js
338
+ import delay from "delay";
339
+ var logger2 = createInternalLogger("Utils");
340
+ var Utils = {
341
+ /**
342
+ * 解析 Cookie 字符串为 Playwright 格式的 Cookie 数组
343
+ * @param {string} cookieString - Cookie 字符串
344
+ * @param {string} [domain] - Cookie 域名 (可选)
345
+ * @returns {Array} Cookie 数组
346
+ */
347
+ parseCookies(cookieString, domain) {
348
+ const cookies = [];
349
+ const pairs = cookieString.split(";").map((c) => c.trim());
350
+ for (const pair of pairs) {
351
+ const [name, ...valueParts] = pair.split("=");
352
+ if (name && valueParts.length > 0) {
353
+ const cookie = {
354
+ name: name.trim(),
355
+ value: valueParts.join("=").trim(),
356
+ path: "/"
357
+ };
358
+ if (domain) {
359
+ cookie.domain = domain;
360
+ }
361
+ cookies.push(cookie);
362
+ }
363
+ }
364
+ logger2.success("parseCookies", `parsed ${cookies.length} cookies`);
365
+ return cookies;
564
366
  },
565
- {
566
- key: "screenshot_success",
567
- method: "screenshotSuccess",
568
- label: "\u622A\u56FE\u6210\u529F",
569
- group: "\u622A\u56FE",
570
- step: "\u622A\u56FE",
571
- status: "\u6210\u529F",
572
- level: "success",
573
- attention: "high"
367
+ /**
368
+ * 全页面滚动截图
369
+ * 自动检测页面所有可滚动元素,取最大高度,强制展开后截图
370
+ *
371
+ * @param {import('playwright').Page} page - Playwright page 对象
372
+ * @param {Object} [options] - 配置选项
373
+ * @param {number} [options.buffer] - 额外缓冲高度 (默认: 视口高度的一半)
374
+ * @param {boolean} [options.restore] - 截图后是否恢复页面高度和样式 (默认: false)
375
+ * @param {number} [options.maxHeight] - 最大截图高度 (默认: 8000px)
376
+ * @returns {Promise<string>} - base64 编码的 PNG 图片
377
+ */
378
+ async fullPageScreenshot(page, options = {}) {
379
+ logger2.start("fullPageScreenshot", "detecting scrollable elements");
380
+ const originalViewport = page.viewportSize();
381
+ const defaultBuffer = Math.round((originalViewport?.height || 1080) / 2);
382
+ const buffer = options.buffer ?? defaultBuffer;
383
+ const restore = options.restore ?? false;
384
+ const maxHeight = options.maxHeight ?? 8e3;
385
+ try {
386
+ const maxScrollHeight = await page.evaluate(() => {
387
+ let maxHeight2 = document.body.scrollHeight;
388
+ document.querySelectorAll("*").forEach((el) => {
389
+ const style = window.getComputedStyle(el);
390
+ const overflowY = style.overflowY;
391
+ if ((overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight) {
392
+ if (el.scrollHeight > maxHeight2) {
393
+ maxHeight2 = el.scrollHeight;
394
+ }
395
+ el.dataset.pkOrigOverflow = el.style.overflow;
396
+ el.dataset.pkOrigHeight = el.style.height;
397
+ el.dataset.pkOrigMaxHeight = el.style.maxHeight;
398
+ el.classList.add("__pk_expanded__");
399
+ el.style.overflow = "visible";
400
+ el.style.height = "auto";
401
+ el.style.maxHeight = "none";
402
+ }
403
+ });
404
+ return maxHeight2;
405
+ });
406
+ const targetHeight = Math.min(maxScrollHeight + buffer, maxHeight);
407
+ await page.setViewportSize({
408
+ width: originalViewport?.width || 1280,
409
+ height: targetHeight
410
+ });
411
+ await delay(1e3);
412
+ const buffer_ = await page.screenshot({
413
+ fullPage: true,
414
+ type: "png"
415
+ });
416
+ logger2.success("fullPageScreenshot", `captured ${Math.round(buffer_.length / 1024)} KB`);
417
+ return buffer_.toString("base64");
418
+ } finally {
419
+ if (restore) {
420
+ await page.evaluate(() => {
421
+ document.querySelectorAll(".__pk_expanded__").forEach((el) => {
422
+ el.style.overflow = el.dataset.pkOrigOverflow || "";
423
+ el.style.height = el.dataset.pkOrigHeight || "";
424
+ el.style.maxHeight = el.dataset.pkOrigMaxHeight || "";
425
+ delete el.dataset.pkOrigOverflow;
426
+ delete el.dataset.pkOrigHeight;
427
+ delete el.dataset.pkOrigMaxHeight;
428
+ el.classList.remove("__pk_expanded__");
429
+ });
430
+ });
431
+ if (originalViewport) {
432
+ await page.setViewportSize(originalViewport);
433
+ }
434
+ }
435
+ }
436
+ }
437
+ };
438
+
439
+ // src/anti-cheat.js
440
+ var logger3 = createInternalLogger("AntiCheat");
441
+ var BASE_CONFIG = Object.freeze({
442
+ locale: "zh-CN",
443
+ acceptLanguage: "zh-CN,zh;q=0.9",
444
+ timezoneId: "Asia/Shanghai",
445
+ timezoneOffset: -480,
446
+ geolocation: null
447
+ });
448
+ var DEFAULT_LAUNCH_ARGS = [
449
+ // '--disable-blink-features=AutomationControlled', // Crawlee 可能会自动处理,过多干预反而会被识别
450
+ "--no-sandbox",
451
+ "--disable-setuid-sandbox",
452
+ "--window-position=0,0",
453
+ `--lang=${BASE_CONFIG.locale}`
454
+ ];
455
+ function buildFingerprintOptions(locale) {
456
+ return {
457
+ browsers: [{ name: "chrome", minVersion: 110 }],
458
+ devices: ["desktop"],
459
+ operatingSystems: ["windows", "macos", "linux"],
460
+ locales: [locale]
461
+ };
462
+ }
463
+ var AntiCheat = {
464
+ /**
465
+ * 获取统一的基础配置
466
+ */
467
+ getBaseConfig() {
468
+ return { ...BASE_CONFIG };
574
469
  },
575
- {
576
- key: "screenshot_fail",
577
- method: "screenshotFail",
578
- label: "\u622A\u56FE\u5931\u8D25",
579
- group: "\u622A\u56FE",
580
- step: "\u622A\u56FE",
581
- status: "\u5931\u8D25",
582
- level: "warning",
583
- attention: "high",
584
- buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
470
+ /**
471
+ * 用于 Crawlee fingerprint generator 的统一配置(桌面端)。
472
+ */
473
+ getFingerprintGeneratorOptions() {
474
+ return buildFingerprintOptions(BASE_CONFIG.locale);
585
475
  },
586
- {
587
- key: "share_start",
588
- method: "shareStart",
589
- label: "\u5206\u4EAB\u94FE\u63A5\u5F00\u59CB",
590
- group: "\u5206\u4EAB",
591
- step: "\u5206\u4EAB\u94FE\u63A5",
592
- status: "\u5F00\u59CB",
593
- level: "start",
594
- attention: "low"
476
+ /**
477
+ * 获取基础启动参数。
478
+ */
479
+ getLaunchArgs() {
480
+ return [...DEFAULT_LAUNCH_ARGS];
595
481
  },
596
- {
597
- key: "share_progress",
598
- method: "shareProgress",
599
- label: "\u5206\u4EAB\u94FE\u63A5\u6B65\u9AA4",
600
- group: "\u5206\u4EAB",
601
- step: "\u5206\u4EAB\u94FE\u63A5",
602
- status: "\u6B65\u9AA4",
603
- level: "info",
604
- attention: "low",
605
- buildDetails: (label) => [label ? `\u6B65\u9AA4=${label}` : ""]
482
+ /**
483
+ * 为 got-scraping 生成与浏览器一致的 TLS 指纹配置(桌面端)。
484
+ */
485
+ getTlsFingerprintOptions(userAgent = "", acceptLanguage = "") {
486
+ return buildFingerprintOptions(BASE_CONFIG.locale);
606
487
  },
607
- {
608
- key: "share_success",
609
- method: "shareSuccess",
610
- label: "\u5206\u4EAB\u94FE\u63A5\u6210\u529F",
611
- group: "\u5206\u4EAB",
612
- step: "\u5206\u4EAB\u94FE\u63A5",
613
- status: "\u6210\u529F",
614
- level: "success",
615
- attention: "high",
616
- buildDetails: (link) => [link ? `\u94FE\u63A5=${link}` : ""]
488
+ /**
489
+ * 规范化请求头
490
+ */
491
+ applyLocaleHeaders(headers, acceptLanguage = "") {
492
+ if (!headers["accept-language"]) {
493
+ headers["accept-language"] = acceptLanguage || BASE_CONFIG.acceptLanguage;
494
+ }
495
+ return headers;
496
+ }
497
+ };
498
+
499
+ // src/humanize.js
500
+ import delay2 from "delay";
501
+ import { createCursor } from "ghost-cursor-playwright";
502
+ var logger4 = createInternalLogger("Humanize");
503
+ var $CursorWeakMap = /* @__PURE__ */ new WeakMap();
504
+ function $GetCursor(page) {
505
+ const cursor = $CursorWeakMap.get(page);
506
+ if (!cursor) {
507
+ throw new Error("Cursor \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 Humanize.initializeCursor(page)");
508
+ }
509
+ return cursor;
510
+ }
511
+ var Humanize = {
512
+ /**
513
+ * 生成带抖动的毫秒数 - 基于基础值添加随机浮动 (±30% 默认)
514
+ * @param {number} base - 基础延迟 (ms)
515
+ * @param {number} [jitterPercent=0.3] - 抖动百分比 (0.3 = ±30%)
516
+ * @returns {number} 抖动后的毫秒数
517
+ */
518
+ jitterMs(base, jitterPercent = 0.3) {
519
+ const jitter = base * jitterPercent * (Math.random() * 2 - 1);
520
+ return Math.max(10, Math.round(base + jitter));
617
521
  },
618
- {
619
- key: "share_fail",
620
- method: "shareFail",
621
- label: "\u5206\u4EAB\u94FE\u63A5\u5931\u8D25",
622
- group: "\u5206\u4EAB",
623
- step: "\u5206\u4EAB\u94FE\u63A5",
624
- status: "\u5931\u8D25",
625
- level: "warning",
626
- attention: "high",
627
- buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
522
+ /**
523
+ * 初始化页面的 Ghost Cursor(必须在使用其他 cursor 相关方法前调用)
524
+ *
525
+ * @param {import('playwright').Page} page
526
+ * @returns {Promise<void>}
527
+ */
528
+ async initializeCursor(page) {
529
+ if ($CursorWeakMap.has(page)) {
530
+ logger4.debug("initializeCursor: cursor already exists, skipping");
531
+ return;
532
+ }
533
+ logger4.start("initializeCursor", "creating cursor");
534
+ const cursor = await createCursor(page);
535
+ $CursorWeakMap.set(page, cursor);
536
+ logger4.success("initializeCursor", "cursor initialized");
628
537
  },
629
- {
630
- key: "share_skip",
631
- method: "shareSkip",
632
- label: "\u5206\u4EAB\u94FE\u63A5\u8DF3\u8FC7",
633
- group: "\u5206\u4EAB",
634
- step: "\u5206\u4EAB\u94FE\u63A5",
635
- status: "\u8DF3\u8FC7",
636
- level: "warning",
637
- attention: "medium",
638
- buildDetails: (reason) => [reason ? `\u539F\u56E0=${reason}` : ""]
538
+ /**
539
+ * 人类化鼠标移动 - 使用 ghost-cursor 移动到指定位置或元素
540
+ *
541
+ * @param {import('playwright').Page} page
542
+ * @param {string|{x: number, y: number}|import('playwright').ElementHandle} target - CSS选择器、坐标对象或元素句柄
543
+ */
544
+ async humanMove(page, target) {
545
+ const cursor = $GetCursor(page);
546
+ logger4.start("humanMove", `target=${typeof target === "string" ? target : "element/coords"}`);
547
+ try {
548
+ if (typeof target === "string") {
549
+ const element = await page.$(target);
550
+ if (!element) {
551
+ logger4.warn(`humanMove: \u5143\u7D20\u4E0D\u5B58\u5728 ${target}`);
552
+ return false;
553
+ }
554
+ const box = await element.boundingBox();
555
+ if (!box) {
556
+ logger4.warn(`humanMove: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E ${target}`);
557
+ return false;
558
+ }
559
+ const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
560
+ const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
561
+ await cursor.actions.move({ x, y });
562
+ } else if (target && typeof target.x === "number" && typeof target.y === "number") {
563
+ await cursor.actions.move(target);
564
+ } else if (target && typeof target.boundingBox === "function") {
565
+ const box = await target.boundingBox();
566
+ if (box) {
567
+ const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
568
+ const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
569
+ await cursor.actions.move({ x, y });
570
+ }
571
+ }
572
+ logger4.success("humanMove");
573
+ return true;
574
+ } catch (error) {
575
+ logger4.fail("humanMove", error);
576
+ throw error;
577
+ }
639
578
  },
640
- {
641
- key: "data_push_success",
642
- method: "dataPushSuccess",
643
- label: "\u6570\u636E\u63A8\u9001\u6210\u529F",
644
- group: "\u6570\u636E\u63A8\u9001",
645
- step: "\u6570\u636E\u63A8\u9001",
646
- status: "\u6210\u529F",
647
- level: "success",
648
- attention: "medium",
649
- buildDetails: (label) => [label ? `\u8BF4\u660E=${label}` : ""]
579
+ /**
580
+ * 渐进式滚动到元素可见(仅处理 Y 轴滚动)
581
+ * 返回 restore 方法,用于将滚动容器恢复到原位置
582
+ *
583
+ * @param {import('playwright').Page} page
584
+ * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
585
+ * @param {Object} [options]
586
+ * @param {number} [options.maxSteps=25] - 最大滚动步数
587
+ * @param {number} [options.minStep=80] - 单次滚动最小步长
588
+ * @param {number} [options.maxStep=220] - 单次滚动最大步长
589
+ */
590
+ async humanScroll(page, target, options = {}) {
591
+ const { maxSteps = 30, minStep = 150, maxStep = 400 } = options;
592
+ const targetDesc = typeof target === "string" ? target : "ElementHandle";
593
+ logger4.debug(`humanScroll | \u76EE\u6807=${targetDesc}`);
594
+ let element;
595
+ if (typeof target === "string") {
596
+ element = await page.$(target);
597
+ if (!element) {
598
+ logger4.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
599
+ return { element: null, didScroll: false };
600
+ }
601
+ } else {
602
+ element = target;
603
+ }
604
+ const cursor = $GetCursor(page);
605
+ let didScroll = false;
606
+ const checkVisibility = async () => {
607
+ return await element.evaluate((el) => {
608
+ const rect = el.getBoundingClientRect();
609
+ if (!rect || rect.width === 0 || rect.height === 0) {
610
+ return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
611
+ }
612
+ const cx = rect.left + rect.width / 2;
613
+ const cy = rect.top + rect.height / 2;
614
+ const viewH = window.innerHeight;
615
+ const viewW = window.innerWidth;
616
+ if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
617
+ const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
618
+ return { code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy, viewH };
619
+ }
620
+ const pointElement = document.elementFromPoint(cx, cy);
621
+ if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
622
+ return {
623
+ code: "OBSTRUCTED",
624
+ reason: "\u88AB\u906E\u6321",
625
+ obstruction: {
626
+ tag: pointElement.tagName,
627
+ id: pointElement.id,
628
+ className: pointElement.className
629
+ },
630
+ cy,
631
+ // Return Center Y for smart direction calculation
632
+ viewH
633
+ };
634
+ }
635
+ return { code: "VISIBLE" };
636
+ });
637
+ };
638
+ try {
639
+ for (let i = 0; i < maxSteps; i++) {
640
+ const status = await checkVisibility();
641
+ if (status.code === "VISIBLE") {
642
+ logger4.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
643
+ return { element, didScroll };
644
+ }
645
+ logger4.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${status.direction ? `(${status.direction})` : ""}`);
646
+ if (status.code === "OBSTRUCTED" && status.obstruction) {
647
+ logger4.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
648
+ }
649
+ let deltaY = 0;
650
+ if (status.code === "OUT_OF_VIEWPORT") {
651
+ if (status.direction === "down") {
652
+ deltaY = minStep + Math.random() * (maxStep - minStep);
653
+ } else if (status.direction === "up") {
654
+ deltaY = -(minStep + Math.random() * (maxStep - minStep));
655
+ } else {
656
+ deltaY = 100;
657
+ }
658
+ } else if (status.code === "OBSTRUCTED") {
659
+ const isBottomHalf = status.cy > status.viewH / 2;
660
+ const direction = isBottomHalf ? 1 : -1;
661
+ deltaY = direction * (minStep + Math.random() * 50);
662
+ }
663
+ if (i === 0 || Math.random() < 0.2) {
664
+ const viewSize = page.viewportSize();
665
+ if (viewSize) {
666
+ const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 100;
667
+ const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 100;
668
+ await cursor.actions.move({ x: safeX, y: safeY });
669
+ }
670
+ }
671
+ await page.mouse.wheel(0, deltaY);
672
+ didScroll = true;
673
+ await delay2(this.jitterMs(100 + Math.random() * 150, 0.2));
674
+ }
675
+ logger4.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
676
+ return { element, didScroll };
677
+ } catch (error) {
678
+ logger4.fail("humanScroll", error);
679
+ throw error;
680
+ }
650
681
  },
651
- {
652
- key: "data_push_fail",
653
- method: "dataPushFail",
654
- label: "\u6570\u636E\u63A8\u9001\u5931\u8D25",
655
- group: "\u6570\u636E\u63A8\u9001",
656
- step: "\u6570\u636E\u63A8\u9001",
657
- status: "\u5931\u8D25",
658
- level: "error",
659
- attention: "high",
660
- buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
682
+ /**
683
+ * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
684
+ *
685
+ * @param {import('playwright').Page} page
686
+ * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
687
+ * @param {Object} [options]
688
+ * @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
689
+ * @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
690
+ * @param {boolean} [options.scrollIfNeeded=true] - 元素不在视口时是否自动滚动
691
+ */
692
+ async humanClick(page, target, options = {}) {
693
+ const cursor = $GetCursor(page);
694
+ const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
695
+ const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
696
+ logger4.start("humanClick", `target=${targetDesc}`);
697
+ const restoreOnce = async () => {
698
+ if (restoreOnce.restored) return;
699
+ restoreOnce.restored = true;
700
+ if (typeof restoreOnce.do !== "function") return;
701
+ try {
702
+ await delay2(this.jitterMs(1e3));
703
+ await restoreOnce.do();
704
+ } catch (restoreError) {
705
+ logger4.warn(`humanClick: \u6062\u590D\u6EDA\u52A8\u4F4D\u7F6E\u5931\u8D25: ${restoreError.message}`);
706
+ }
707
+ };
708
+ try {
709
+ if (target == null) {
710
+ await delay2(this.jitterMs(reactionDelay, 0.4));
711
+ await cursor.actions.click();
712
+ logger4.success("humanClick", "Clicked current position");
713
+ return true;
714
+ }
715
+ let element;
716
+ if (typeof target === "string") {
717
+ element = await page.$(target);
718
+ if (!element) {
719
+ if (throwOnMissing) {
720
+ throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
721
+ }
722
+ logger4.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
723
+ return false;
724
+ }
725
+ } else {
726
+ element = target;
727
+ }
728
+ if (scrollIfNeeded) {
729
+ const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
730
+ restoreOnce.do = didScroll && restore ? restoreFn : null;
731
+ }
732
+ const box = await element.boundingBox();
733
+ if (!box) {
734
+ await restoreOnce();
735
+ if (throwOnMissing) {
736
+ throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
737
+ }
738
+ logger4.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
739
+ return false;
740
+ }
741
+ const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
742
+ const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
743
+ await cursor.actions.move({ x, y });
744
+ await delay2(this.jitterMs(reactionDelay, 0.4));
745
+ await cursor.actions.click();
746
+ await restoreOnce();
747
+ logger4.success("humanClick");
748
+ return true;
749
+ } catch (error) {
750
+ await restoreOnce();
751
+ logger4.fail("humanClick", error);
752
+ throw error;
753
+ }
661
754
  },
662
- {
663
- key: "popup_detected",
664
- method: "popupDetected",
665
- label: "\u5F39\u7A97\u68C0\u6D4B",
666
- group: "\u5F39\u7A97",
667
- step: "\u5F39\u7A97\u5904\u7406",
668
- status: "\u68C0\u6D4B\u5230\u906E\u7F69",
669
- level: "warning",
670
- attention: "medium",
671
- buildDetails: (detail) => [detail ? `detail=${detail}` : ""]
755
+ /**
756
+ * 随机延迟一段毫秒数(带 ±30% 抖动)
757
+ * @param {number} baseMs - 基础延迟毫秒数
758
+ * @param {number} [jitterPercent=0.3] - 抖动百分比
759
+ */
760
+ async randomSleep(baseMs, jitterPercent = 0.3) {
761
+ const ms = this.jitterMs(baseMs, jitterPercent);
762
+ logger4.start("randomSleep", `base=${baseMs}, actual=${ms}ms`);
763
+ await delay2(ms);
764
+ logger4.success("randomSleep");
672
765
  },
673
- {
674
- key: "popup_close_attempt",
675
- method: "popupCloseAttempt",
676
- label: "\u5F39\u7A97\u5173\u95ED\u5C1D\u8BD5",
677
- group: "\u5F39\u7A97",
678
- step: "\u5F39\u7A97\u5904\u7406",
679
- status: "\u5C1D\u8BD5\u5173\u95ED",
680
- level: "info",
681
- attention: "low",
682
- buildDetails: (detail) => [detail ? `detail=${detail}` : ""]
766
+ /**
767
+ * 模拟人类"注视"或"阅读"行为:鼠标在页面上随机微动
768
+ * @param {import('playwright').Page} page
769
+ * @param {number} [baseDurationMs=2500] - 基础持续时间 (±40% 抖动)
770
+ */
771
+ async simulateGaze(page, baseDurationMs = 2500) {
772
+ const cursor = $GetCursor(page);
773
+ const durationMs = this.jitterMs(baseDurationMs, 0.4);
774
+ logger4.start("simulateGaze", `duration=${durationMs}ms`);
775
+ const startTime = Date.now();
776
+ const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
777
+ while (Date.now() - startTime < durationMs) {
778
+ const x = 100 + Math.random() * (viewportSize.width - 200);
779
+ const y = 100 + Math.random() * (viewportSize.height - 200);
780
+ await cursor.actions.move({ x, y });
781
+ await delay2(this.jitterMs(600, 0.5));
782
+ }
783
+ logger4.success("simulateGaze");
683
784
  },
684
- {
685
- key: "popup_close_success",
686
- method: "popupCloseSuccess",
687
- label: "\u5F39\u7A97\u5173\u95ED\u5B8C\u6210",
688
- group: "\u5F39\u7A97",
689
- step: "\u5F39\u7A97\u5904\u7406",
690
- status: "\u5173\u95ED\u5B8C\u6210",
691
- level: "success",
692
- attention: "medium"
785
+ /**
786
+ * 人类化输入 - 带节奏变化(快-慢-停顿-偶尔加速)
787
+ * @param {import('playwright').Page} page
788
+ * @param {string} selector - 输入框选择器
789
+ * @param {string} text - 要输入的文本
790
+ * @param {Object} [options]
791
+ * @param {number} [options.baseDelay=180] - 基础按键延迟 (ms),实际 ±40% 抖动
792
+ * @param {number} [options.pauseProbability=0.08] - 停顿概率 (0-1)
793
+ * @param {number} [options.pauseBase=800] - 停顿时长基础值 (ms),实际 ±50% 抖动
794
+ */
795
+ async humanType(page, selector, text, options = {}) {
796
+ logger4.start("humanType", `selector=${selector}, textLen=${text.length}`);
797
+ const {
798
+ baseDelay = 180,
799
+ pauseProbability = 0.08,
800
+ pauseBase = 800
801
+ } = options;
802
+ try {
803
+ const locator = page.locator(selector);
804
+ await Humanize.humanClick(page, locator);
805
+ await delay2(this.jitterMs(200, 0.4));
806
+ for (let i = 0; i < text.length; i++) {
807
+ const char = text[i];
808
+ let charDelay;
809
+ if (char === " ") {
810
+ charDelay = this.jitterMs(baseDelay * 0.6, 0.3);
811
+ } else if (/[,.!?;:,。!?;:]/.test(char)) {
812
+ charDelay = this.jitterMs(baseDelay * 1.5, 0.4);
813
+ } else {
814
+ charDelay = this.jitterMs(baseDelay, 0.4);
815
+ }
816
+ await page.keyboard.type(char);
817
+ await delay2(charDelay);
818
+ if (Math.random() < pauseProbability && i < text.length - 1) {
819
+ const pauseTime = this.jitterMs(pauseBase, 0.5);
820
+ logger4.debug(`\u505C\u987F ${pauseTime}ms...`);
821
+ await delay2(pauseTime);
822
+ }
823
+ }
824
+ logger4.success("humanType");
825
+ } catch (error) {
826
+ logger4.fail("humanType", error);
827
+ throw error;
828
+ }
693
829
  },
694
- {
695
- key: "popup_close_fail",
696
- method: "popupCloseFail",
697
- label: "\u5F39\u7A97\u5173\u95ED\u5931\u8D25",
698
- group: "\u5F39\u7A97",
699
- step: "\u5F39\u7A97\u5904\u7406",
700
- status: "\u5173\u95ED\u5931\u8D25",
701
- level: "warning",
702
- attention: "medium",
703
- buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
704
- }
705
- ];
706
- var LOG_TEMPLATES = LOG_DEFINITIONS.map((definition) => {
707
- const attention = definition.attention || "medium";
708
- const attentionRank = ATTENTION_RANK[attention] || ATTENTION_RANK.medium;
709
- const defaultSelected = attentionRank >= DEFAULT_ATTENTION_RANK;
710
- return {
711
- key: definition.key,
712
- label: definition.label,
713
- group: definition.group,
714
- attention,
715
- patterns: buildDefinitionPatterns(definition),
716
- defaultSelected
717
- };
718
- });
719
- var buildStepLine = (step, status, details = []) => {
720
- const parts = [];
721
- const decoratedStep = step ? decorateLabel(step, STEP_EMOJIS) : "";
722
- const base = decoratedStep ? `${STEP_PREFIX} ${decoratedStep}` : STEP_PREFIX;
723
- parts.push(base.trim());
724
- if (status) parts.push(decorateLabel(status, STATUS_EMOJIS));
725
- const detailParts = details.filter(Boolean);
726
- if (detailParts.length > 0) {
727
- parts.push(...detailParts);
728
- }
729
- return parts.join(STEP_SEPARATOR);
730
- };
731
- var createThrottle = () => {
732
- const lastMap = /* @__PURE__ */ new Map();
733
- return (key, intervalMs, fn) => {
734
- const now = Date.now();
735
- const last = lastMap.get(key) || 0;
736
- if (now - last >= intervalMs) {
737
- lastMap.set(key, now);
738
- fn();
830
+ /**
831
+ * 人类化清空输入框 - 模拟人类删除文本的行为
832
+ * @param {import('playwright').Page} page
833
+ * @param {string} selector - 输入框选择器
834
+ */
835
+ async humanClear(page, selector) {
836
+ logger4.start("humanClear", `selector=${selector}`);
837
+ try {
838
+ const locator = page.locator(selector);
839
+ await locator.click();
840
+ await delay2(this.jitterMs(200, 0.4));
841
+ const currentValue = await locator.inputValue();
842
+ if (!currentValue || currentValue.length === 0) {
843
+ logger4.success("humanClear", "already empty");
844
+ return;
845
+ }
846
+ await page.keyboard.press("Meta+A");
847
+ await delay2(this.jitterMs(100, 0.4));
848
+ await page.keyboard.press("Backspace");
849
+ logger4.success("humanClear");
850
+ } catch (error) {
851
+ logger4.fail("humanClear", error);
852
+ throw error;
739
853
  }
740
- };
741
- };
742
- var createTemplateLogger = (baseLogger = createBaseLogger()) => {
743
- const throttle = createThrottle();
744
- const info = (line) => baseLogger.info(line);
745
- const success = (line) => baseLogger.success(line);
746
- const warning = (line) => baseLogger.warning(line);
747
- const error = (line) => baseLogger.error(line);
748
- const debug = (line) => baseLogger.debug(line);
749
- const start = (line) => baseLogger.start(line);
750
- const stepInfo = (step, status, details = []) => info(buildStepLine(step, status, details));
751
- const stepSuccess = (step, status, details = []) => success(buildStepLine(step, status, details));
752
- const stepWarn = (step, status, details = []) => warning(buildStepLine(step, status, details));
753
- const stepError = (step, status, details = []) => error(buildStepLine(step, status, details));
754
- const stepStart = (step, status, details = []) => start(buildStepLine(step, status, details));
755
- const stepHandlers = {
756
- info: stepInfo,
757
- success: stepSuccess,
758
- warning: stepWarn,
759
- error: stepError,
760
- start: stepStart
761
- };
762
- const logFromDefinition = (definition, details = []) => {
763
- const handler = stepHandlers[definition.level] || stepInfo;
764
- const payload = [...details, buildLogTag(definition.key)];
765
- const emit = () => handler(definition.step, definition.status, payload);
766
- if (definition.throttleMs) {
767
- throttle(definition.throttleKey || definition.key, definition.throttleMs, emit);
768
- return;
854
+ },
855
+ /**
856
+ * 页面预热浏览 - 模拟人类进入页面后的探索行为
857
+ * @param {import('playwright').Page} page
858
+ * @param {number} [baseDuration=3500] - 预热时长基础值 (±40% 抖动)
859
+ */
860
+ async warmUpBrowsing(page, baseDuration = 3500) {
861
+ const cursor = $GetCursor(page);
862
+ const durationMs = this.jitterMs(baseDuration, 0.4);
863
+ logger4.start("warmUpBrowsing", `duration=${durationMs}ms`);
864
+ const startTime = Date.now();
865
+ const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
866
+ try {
867
+ while (Date.now() - startTime < durationMs) {
868
+ const action = Math.random();
869
+ if (action < 0.4) {
870
+ const x = 100 + Math.random() * (viewportSize.width - 200);
871
+ const y = 100 + Math.random() * (viewportSize.height - 200);
872
+ await cursor.actions.move({ x, y });
873
+ await delay2(this.jitterMs(350, 0.4));
874
+ } else if (action < 0.7) {
875
+ const scrollY = (Math.random() - 0.5) * 200;
876
+ await page.mouse.wheel(0, scrollY);
877
+ await delay2(this.jitterMs(500, 0.4));
878
+ } else {
879
+ await delay2(this.jitterMs(800, 0.5));
880
+ }
881
+ }
882
+ logger4.success("warmUpBrowsing");
883
+ } catch (error) {
884
+ logger4.fail("warmUpBrowsing", error);
885
+ throw error;
769
886
  }
770
- emit();
771
- };
772
- const definitionMethods = {};
773
- LOG_DEFINITIONS.forEach((definition) => {
774
- if (!definition.method) return;
775
- definitionMethods[definition.method] = (...args) => {
776
- const details = definition.buildDetails ? definition.buildDetails(...args) : [];
777
- logFromDefinition(definition, details);
778
- };
779
- });
780
- return {
781
- step: (step, status, details, level = "info") => {
782
- if (level === "error") return stepError(step, status, details);
783
- if (level === "warn" || level === "warning") return stepWarn(step, status, details);
784
- if (level === "start") return stepStart(step, status, details);
785
- if (level === "success") return stepSuccess(step, status, details);
786
- return stepInfo(step, status, details);
787
- },
788
- info: (message) => info(message),
789
- success: (message) => success(message),
790
- warning: (message) => warning(message),
791
- warn: (message) => warning(message),
792
- error: (message) => error(message),
793
- debug: (message) => debug(message),
794
- start: (message) => start(message),
795
- ...definitionMethods
796
- };
797
- };
798
- var getDefaultBaseLogger = () => createBaseLogger("", defaultLogger);
799
- var Logger = {
800
- setLogger: (logger10) => setDefaultLogger(logger10),
801
- info: (message) => getDefaultBaseLogger().info(message),
802
- success: (message) => getDefaultBaseLogger().success(message),
803
- warning: (message) => getDefaultBaseLogger().warning(message),
804
- warn: (message) => getDefaultBaseLogger().warning(message),
805
- error: (message) => getDefaultBaseLogger().error(message),
806
- debug: (message) => getDefaultBaseLogger().debug(message),
807
- start: (message) => getDefaultBaseLogger().start(message),
808
- useTemplate: (logger10) => createTemplateLogger(createBaseLogger("", logger10 || defaultLogger))
809
- };
810
-
811
- // src/internal/logger.js
812
- import { log as crawleeLog } from "crawlee";
813
- function createLogger(moduleName) {
814
- const baseLogger = createBaseLogger(moduleName, crawleeLog);
815
- return {
816
- /**
817
- * 方法开始日志
818
- * @param {string} methodName - 方法名称
819
- * @param {string} [params] - 参数摘要 (可选)
820
- */
821
- start(methodName, params = "") {
822
- const paramStr = params ? ` (${params})` : "";
823
- baseLogger.start(`${methodName} \u5F00\u59CB${paramStr}`);
824
- },
825
- /**
826
- * 方法成功日志
827
- * @param {string} methodName - 方法名称
828
- * @param {string} [result] - 结果摘要 (可选)
829
- */
830
- success(methodName, result = "") {
831
- const resultStr = result ? ` (${result})` : "";
832
- baseLogger.success(`${methodName} \u5B8C\u6210${resultStr}`);
833
- },
834
- /**
835
- * 方法失败日志
836
- * @param {string} methodName - 方法名称
837
- * @param {Error|string} error - 错误对象或信息
838
- */
839
- fail(methodName, error) {
840
- const message = error instanceof Error ? error.message : error;
841
- baseLogger.error(`${methodName} \u5931\u8D25: ${message}`);
842
- },
843
- /**
844
- * 调试日志
845
- * @param {string} message - 详情
846
- */
847
- debug(message) {
848
- baseLogger.debug(message);
849
- },
850
- /**
851
- * 警告日志
852
- * @param {string} message - 警告信息
853
- */
854
- warn(message) {
855
- baseLogger.warning(message);
856
- },
857
- warning(message) {
858
- baseLogger.warning(message);
859
- },
860
- /**
861
- * 普通信息日志
862
- * @param {string} message - 信息
863
- */
864
- info(message) {
865
- baseLogger.info(message);
887
+ },
888
+ /**
889
+ * 自然滚动 - 带惯性、减速效果和随机抖动
890
+ * @param {import('playwright').Page} page
891
+ * @param {'up' | 'down'} [direction='down'] - 滚动方向
892
+ * @param {number} [distance=300] - 总滚动距离基础值 (px),±15% 抖动
893
+ * @param {number} [baseSteps=5] - 分几步完成基础值,±1 随机
894
+ */
895
+ async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
896
+ const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
897
+ const actualDistance = this.jitterMs(distance, 0.15);
898
+ logger4.start("naturalScroll", `dir=${direction}, dist=${actualDistance}, steps=${steps}`);
899
+ const sign = direction === "down" ? 1 : -1;
900
+ const stepDistance = actualDistance / steps;
901
+ try {
902
+ for (let i = 0; i < steps; i++) {
903
+ const factor = 1 - i / steps * 0.5;
904
+ const jitter = 0.9 + Math.random() * 0.2;
905
+ const scrollAmount = stepDistance * factor * sign * jitter;
906
+ await page.mouse.wheel(0, scrollAmount);
907
+ const baseDelay = 60 + i * 25;
908
+ await delay2(this.jitterMs(baseDelay, 0.3));
909
+ }
910
+ logger4.success("naturalScroll");
911
+ } catch (error) {
912
+ logger4.fail("naturalScroll", error);
913
+ throw error;
866
914
  }
867
- };
868
- }
915
+ }
916
+ };
869
917
 
870
- // src/errors.js
871
- var errors_exports = {};
872
- __export(errors_exports, {
873
- CrawlerError: () => CrawlerError
874
- });
875
- import { serializeError } from "serialize-error";
876
- var CrawlerError = class _CrawlerError extends Error {
918
+ // src/launch.js
919
+ var Launch = {
920
+ getLaunchOptions(customArgs = []) {
921
+ return {
922
+ args: [
923
+ ...AntiCheat.getLaunchArgs(),
924
+ ...customArgs
925
+ ],
926
+ ignoreDefaultArgs: ["--enable-automation"]
927
+ };
928
+ },
877
929
  /**
878
- * @param {string|Object} info - 错误信息字符串或配置对象
879
- * @param {string} info.message - 错误信息
880
- * @param {number} [info.code] - ErrorKeygen 枚举值(用于错误分类)
881
- * @param {Object} [info.context] - 上下文信息对象
930
+ * 推荐的 Fingerprint Generator 选项
931
+ * 确保生成的是桌面端、较新的 Chrome,以匹配我们的脚本逻辑
882
932
  */
883
- constructor(info) {
884
- if (typeof info === "string") {
885
- info = { message: info };
933
+ getFingerprintGeneratorOptions() {
934
+ return AntiCheat.getFingerprintGeneratorOptions();
935
+ }
936
+ };
937
+
938
+ // src/live-view.js
939
+ import express from "express";
940
+ import { Actor } from "apify";
941
+ var logger5 = createInternalLogger("LiveView");
942
+ async function startLiveViewServer(liveViewKey) {
943
+ const app = express();
944
+ app.get("/", async (req, res) => {
945
+ try {
946
+ const screenshotBuffer = await Actor.getValue(liveViewKey);
947
+ if (!screenshotBuffer) {
948
+ res.send('<html><head><meta http-equiv="refresh" content="2"></head><body>\u7B49\u5F85\u7B2C\u4E00\u4E2A\u5C4F\u5E55\u622A\u56FE...</body></html>');
949
+ return;
950
+ }
951
+ const screenshotBase64 = screenshotBuffer.toString("base64");
952
+ res.send(`
953
+ <html>
954
+ <head>
955
+ <title>Live View (\u622A\u56FE)</title>
956
+ <meta http-equiv="refresh" content="1">
957
+ </head>
958
+ <body style="margin:0; padding:0;">
959
+ <img src="data:image/png;base64,${screenshotBase64}"
960
+ alt="Live View Screenshot"
961
+ style="width: 100%; height: auto;" />
962
+ </body>
963
+ </html>
964
+ `);
965
+ } catch (error) {
966
+ logger5.fail("Live View Server", error);
967
+ res.status(500).send(`\u65E0\u6CD5\u52A0\u8F7D\u5C4F\u5E55\u622A\u56FE: ${error.message}`);
886
968
  }
887
- super(info.message);
888
- this.name = "CrawlerError";
889
- this.code = info.code ?? Code.UnknownError;
890
- this.context = info.context ?? {};
891
- this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
892
- if (Error.captureStackTrace) {
893
- Error.captureStackTrace(this, _CrawlerError);
969
+ });
970
+ const port = process.env.APIFY_CONTAINER_PORT || 4321;
971
+ app.listen(port, () => {
972
+ logger5.success("startLiveViewServer", `\u76D1\u542C\u7AEF\u53E3 ${port}`);
973
+ });
974
+ }
975
+ async function takeLiveScreenshot(liveViewKey, page, logMessage) {
976
+ try {
977
+ const buffer = await page.screenshot({ type: "png" });
978
+ await Actor.setValue(liveViewKey, buffer, { contentType: "image/png" });
979
+ if (logMessage) {
980
+ logger5.info(`(\u622A\u56FE): ${logMessage}`);
894
981
  }
982
+ } catch (e) {
983
+ logger5.warn(`\u65E0\u6CD5\u6355\u83B7 Live View \u5C4F\u5E55\u622A\u56FE: ${e.message}`);
895
984
  }
896
- /**
897
- * 转换为可推送的数据对象
898
- * @returns {Object}
899
- */
900
- toJSON() {
901
- return serializeError(this);
902
- }
903
- /**
904
- * 检查一个 error 是否是 CrawlerError
905
- * @param {Error} error
906
- * @returns {boolean}
907
- */
908
- static isCrawlerError(error) {
909
- return error instanceof _CrawlerError || error?.name === "CrawlerError";
910
- }
911
- /**
912
- * 从普通 Error 创建 CrawlerError
913
- * @param {Error} error - 原始错误
914
- * @param {Object} [options={}] - 选项对象 (包含 code 和 context)
915
- * @returns {CrawlerError}
916
- */
917
- static from(error, options = {}) {
918
- const crawlerError = new _CrawlerError({
919
- message: error.message,
920
- ...options
921
- });
922
- crawlerError.stack = error.stack;
923
- return crawlerError;
924
- }
985
+ }
986
+ var useLiveView = (liveViewKey = PresetOfLiveViewKey) => {
987
+ return {
988
+ takeLiveScreenshot: async (page, logMessage) => {
989
+ return await takeLiveScreenshot(liveViewKey, page, logMessage);
990
+ },
991
+ startLiveViewServer: async () => {
992
+ return await startLiveViewServer(liveViewKey);
993
+ }
994
+ };
995
+ };
996
+ var LiveView = {
997
+ useLiveView
925
998
  };
926
999
 
927
- // src/apify-kit.js
928
- import { serializeError as serializeError2 } from "serialize-error";
929
- var logger = createLogger("ApifyKit");
930
- async function createApifyKit() {
931
- let apify = null;
932
- try {
933
- apify = await import("apify");
934
- } catch (error) {
935
- throw new Error("\u26A0\uFE0F apify \u5E93\u672A\u5B89\u88C5\uFF0CApifyKit \u7684 Actor \u76F8\u5173\u529F\u80FD\u4E0D\u53EF\u7528");
1000
+ // src/captcha-monitor.js
1001
+ import { v4 as uuidv4 } from "uuid";
1002
+ var logger6 = createInternalLogger("Captcha");
1003
+ function useCaptchaMonitor(page, options) {
1004
+ const { domSelector, urlPattern, onDetected } = options;
1005
+ if (!domSelector && !urlPattern) {
1006
+ throw new Error("[CaptchaMonitor] \u5FC5\u987B\u63D0\u4F9B domSelector \u6216 urlPattern \u81F3\u5C11\u4E00\u4E2A");
936
1007
  }
937
- const { Actor: Actor2 } = apify;
938
- return {
939
- /**
940
- * 核心封装:执行步骤,带自动日志确认、失败截图处理和重试机制
941
- *
942
- * @param {string} step - 步骤名称
943
- * @param {import('playwright').Page} page - Playwright page 对象
944
- * @param {Function} actionFn - 执行的异步操作
945
- * @param {Object} [options] - 配置选项
946
- * @param {boolean} [options.failActor=true] - 失败时是否调用 Actor.fail
947
- * @param {Object} [options.retry] - 重试配置
948
- * @param {number} [options.retry.times=0] - 重试次数
949
- * @param {'direct'|'refresh'} [options.retry.mode='direct'] - 重试模式
950
- * @param {Function} [options.retry.before] - 重试前钩子,可覆盖默认等待行为
951
- */
952
- async runStep(step, page, actionFn, options = {}) {
953
- const { failActor = true, retry = {} } = options;
954
- const { times: retryTimes = 0, mode: retryMode = "direct", before: beforeRetry } = retry;
955
- const executeAction = async (attemptNumber) => {
956
- const attemptLabel = attemptNumber > 0 ? ` (\u91CD\u8BD5 #${attemptNumber})` : "";
957
- logger.start(`[Step] ${step}${attemptLabel}`);
958
- try {
959
- const result = await actionFn();
960
- logger.success(`[Step] ${step}${attemptLabel}`);
961
- return { success: true, result };
962
- } catch (error) {
963
- logger.fail(`[Step] ${step}${attemptLabel}`, error);
964
- return { success: false, error };
965
- }
966
- };
967
- const prepareForRetry = async (attemptNumber) => {
968
- if (typeof beforeRetry === "function") {
969
- logger.start(`[RetryStep] \u6267\u884C\u81EA\u5B9A\u4E49 before \u94A9\u5B50 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
970
- await beforeRetry(page, attemptNumber);
971
- logger.success(`[RetryStep] before \u94A9\u5B50\u5B8C\u6210`);
972
- } else if (retryMode === "refresh") {
973
- logger.start(`[RetryStep] \u5237\u65B0\u9875\u9762 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
974
- await page.reload({ waitUntil: "domcontentloaded" });
975
- logger.success(`[RetryStep] \u9875\u9762\u5237\u65B0\u5B8C\u6210`);
1008
+ if (!onDetected || typeof onDetected !== "function") {
1009
+ throw new Error("[CaptchaMonitor] onDetected \u5FC5\u987B\u662F\u4E00\u4E2A\u51FD\u6570");
1010
+ }
1011
+ let isHandled = false;
1012
+ let frameHandler = null;
1013
+ let exposedFunctionName = null;
1014
+ const triggerDetected = async () => {
1015
+ if (isHandled) return;
1016
+ isHandled = true;
1017
+ await onDetected();
1018
+ };
1019
+ const cleanupFns = [];
1020
+ if (domSelector) {
1021
+ exposedFunctionName = `__c_d_${uuidv4().replace(/-/g, "_")}`;
1022
+ const cleanerName = `__c_cleaner_${uuidv4().replace(/-/g, "_")}`;
1023
+ page.exposeFunction(exposedFunctionName, triggerDetected).catch(() => {
1024
+ });
1025
+ page.addInitScript(({ selector, callbackName, cleanerName: cleanerName2 }) => {
1026
+ (() => {
1027
+ let observer = null;
1028
+ const checkAndReport = () => {
1029
+ const element = document.querySelector(selector);
1030
+ if (element) {
1031
+ if (observer) {
1032
+ observer.disconnect();
1033
+ observer = null;
1034
+ }
1035
+ if (window[callbackName]) {
1036
+ window[callbackName]();
1037
+ }
1038
+ return true;
1039
+ }
1040
+ return false;
1041
+ };
1042
+ if (checkAndReport()) return;
1043
+ observer = new MutationObserver((mutations) => {
1044
+ let shouldCheck = false;
1045
+ for (const mutation of mutations) {
1046
+ if (mutation.addedNodes.length > 0) {
1047
+ shouldCheck = true;
1048
+ break;
1049
+ }
1050
+ }
1051
+ if (shouldCheck && observer) {
1052
+ checkAndReport();
1053
+ }
1054
+ });
1055
+ const mountObserver = () => {
1056
+ const target = document.documentElement;
1057
+ if (target && observer) {
1058
+ observer.observe(target, { childList: true, subtree: true });
1059
+ }
1060
+ };
1061
+ if (document.readyState === "loading") {
1062
+ window.addEventListener("DOMContentLoaded", mountObserver);
976
1063
  } else {
977
- logger.start(`[RetryStep] \u7B49\u5F85 3 \u79D2 (\u7B2C ${attemptNumber} \u6B21\u91CD\u8BD5)`);
978
- await new Promise((resolve) => setTimeout(resolve, 3e3));
979
- logger.success(`[RetryStep] \u7B49\u5F85\u5B8C\u6210`);
980
- }
981
- };
982
- let lastResult = await executeAction(0);
983
- if (lastResult.success) {
984
- return lastResult.result;
985
- }
986
- for (let attempt = 1; attempt <= retryTimes; attempt++) {
987
- logger.start(`[RetryStep] \u51C6\u5907\u7B2C ${attempt}/${retryTimes} \u6B21\u91CD\u8BD5: ${step}`);
988
- try {
989
- await prepareForRetry(attempt);
990
- } catch (prepareError) {
991
- logger.warn(`[RetryStep] \u91CD\u8BD5\u51C6\u5907\u5931\u8D25: ${prepareError.message}`);
992
- continue;
993
- }
994
- lastResult = await executeAction(attempt);
995
- if (lastResult.success) {
996
- return lastResult.result;
1064
+ mountObserver();
997
1065
  }
998
- }
999
- const finalError = lastResult.error;
1000
- if (failActor) {
1001
- let base64 = "\u622A\u56FE\u5931\u8D25";
1002
- try {
1003
- if (page) {
1004
- const buffer = await page.screenshot({ fullPage: true, type: "jpeg", quality: 60 });
1005
- base64 = `data:image/jpeg;base64,${buffer.toString("base64")}`;
1066
+ window[cleanerName2] = () => {
1067
+ if (observer) {
1068
+ observer.disconnect();
1069
+ observer = null;
1006
1070
  }
1007
- } catch (snapErr) {
1008
- logger.warn(`\u622A\u56FE\u751F\u6210\u5931\u8D25: ${snapErr.message}`);
1009
- }
1010
- await this.pushFailed(finalError, {
1011
- step,
1012
- page,
1013
- options,
1014
- base64,
1015
- retryAttempts: retryTimes
1016
- });
1017
- await Actor2.fail(`Run Step ${step} \u5931\u8D25 (\u5DF2\u91CD\u8BD5 ${retryTimes} \u6B21): ${finalError.message}`);
1018
- } else {
1019
- throw finalError;
1020
- }
1021
- },
1022
- /**
1023
- * 宽松版runStep:失败时不调用Actor.fail,只抛出异常
1024
- */
1025
- async runStepLoose(step, page, fn) {
1026
- return await this.runStep(step, page, fn, { failActor: false });
1027
- },
1028
- /**
1029
- * 推送成功数据的通用方法
1030
- * @param {Object} data - 要推送的数据对象
1031
- */
1032
- async pushSuccess(data) {
1033
- await Actor2.pushData({
1034
- // 固定为0
1035
- code: Code.Success,
1036
- status: Status.Success,
1037
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1038
- data
1039
- });
1040
- logger.success("pushSuccess", "Data pushed");
1041
- },
1042
- /**
1043
- * 推送失败数据的通用方法(私有方法,仅供runStep内部使用)
1044
- * 自动解析 CrawlerError 的 code 和 context
1045
- * @param {Error|CrawlerError} error - 错误对象
1046
- * @param {Object} [meta] - 额外的数据(如failedStep, screenshotBase64等)
1047
- * @private
1048
- */
1049
- async pushFailed(error, meta = {}) {
1050
- const isCrawlerError = CrawlerError.isCrawlerError(error);
1051
- const code = isCrawlerError ? error.code : Code.UnknownError;
1052
- const context = isCrawlerError ? error.context : {};
1053
- await Actor2.pushData({
1054
- // 如果是 CrawlerError,使用其 code,否则使用默认 Failed code
1055
- code,
1056
- status: Status.Failed,
1057
- error: serializeError2(error),
1058
- meta,
1059
- context,
1060
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1061
- });
1062
- logger.success("pushFailed", "Error data pushed");
1071
+ };
1072
+ })();
1073
+ }, { selector: domSelector, callbackName: exposedFunctionName, cleanerName });
1074
+ logger6.success("useCaptchaMonitor", `DOM \u76D1\u63A7\u5DF2\u542F\u7528: ${domSelector}`);
1075
+ cleanupFns.push(async () => {
1076
+ try {
1077
+ await page.evaluate((name) => {
1078
+ if (window[name]) {
1079
+ window[name]();
1080
+ delete window[name];
1081
+ }
1082
+ }, cleanerName);
1083
+ } catch (e) {
1084
+ }
1085
+ });
1086
+ }
1087
+ if (urlPattern) {
1088
+ frameHandler = async (frame) => {
1089
+ if (frame === page.mainFrame()) {
1090
+ const currentUrl = page.url();
1091
+ if (currentUrl.includes(urlPattern)) {
1092
+ await triggerDetected();
1093
+ }
1094
+ }
1095
+ };
1096
+ page.on("framenavigated", frameHandler);
1097
+ logger6.success("useCaptchaMonitor", `URL \u76D1\u63A7\u5DF2\u542F\u7528: ${urlPattern}`);
1098
+ cleanupFns.push(async () => {
1099
+ page.off("framenavigated", frameHandler);
1100
+ });
1101
+ }
1102
+ return {
1103
+ stop: async () => {
1104
+ logger6.info("useCaptchaMonitor", "\u6B63\u5728\u505C\u6B62\u76D1\u63A7...");
1105
+ for (const fn of cleanupFns) {
1106
+ await fn();
1107
+ }
1108
+ isHandled = true;
1063
1109
  }
1064
1110
  };
1065
1111
  }
1066
- var instance = null;
1067
- async function useApifyKit() {
1068
- if (!instance) {
1069
- instance = await createApifyKit();
1070
- }
1071
- return instance;
1072
- }
1073
- var ApifyKit = {
1074
- useApifyKit
1112
+ var Captcha = {
1113
+ useCaptchaMonitor
1075
1114
  };
1076
1115
 
1077
- // src/utils.js
1078
- import delay from "delay";
1079
- var logger2 = createLogger("Utils");
1080
- var Utils = {
1116
+ // src/sse.js
1117
+ import https from "https";
1118
+ import { URL as URL2 } from "url";
1119
+ var logger7 = createInternalLogger("Sse");
1120
+ var Sse = {
1081
1121
  /**
1082
- * 解析 Cookie 字符串为 Playwright 格式的 Cookie 数组
1083
- * @param {string} cookieString - Cookie 字符串
1084
- * @param {string} [domain] - Cookie 域名 (可选)
1085
- * @returns {Array} Cookie 数组
1122
+ * 解析 SSE 流文本
1123
+ * 支持 `data: {...}` `data:{...}` 两种格式
1124
+ * @param {string} sseStreamText
1125
+ * @returns {Array<Object>} events
1086
1126
  */
1087
- parseCookies(cookieString, domain) {
1088
- const cookies = [];
1089
- const pairs = cookieString.split(";").map((c) => c.trim());
1090
- for (const pair of pairs) {
1091
- const [name, ...valueParts] = pair.split("=");
1092
- if (name && valueParts.length > 0) {
1093
- const cookie = {
1094
- name: name.trim(),
1095
- value: valueParts.join("=").trim(),
1096
- path: "/"
1097
- };
1098
- if (domain) {
1099
- cookie.domain = domain;
1127
+ parseSseStream(sseStreamText) {
1128
+ const events = [];
1129
+ const lines = sseStreamText.split("\n");
1130
+ for (const line of lines) {
1131
+ if (line.startsWith("data:")) {
1132
+ try {
1133
+ const jsonContent = line.substring(5).trim();
1134
+ if (jsonContent) {
1135
+ events.push(JSON.parse(jsonContent));
1136
+ }
1137
+ } catch (e) {
1138
+ logger7.debug("parseSseStream", `JSON \u89E3\u6790\u5931\u8D25: ${e.message}, line: ${line.substring(0, 100)}...`);
1100
1139
  }
1101
- cookies.push(cookie);
1102
1140
  }
1103
1141
  }
1104
- logger2.success("parseCookies", `parsed ${cookies.length} cookies`);
1105
- return cookies;
1142
+ logger7.success("parseSseStream", `\u89E3\u6790\u5B8C\u6210, events \u6570\u91CF: ${events.length}`);
1143
+ return events;
1106
1144
  },
1107
1145
  /**
1108
- * 全页面滚动截图
1109
- * 自动检测页面所有可滚动元素,取最大高度,强制展开后截图
1110
- *
1111
- * @param {import('playwright').Page} page - Playwright page 对象
1112
- * @param {Object} [options] - 配置选项
1113
- * @param {number} [options.buffer] - 额外缓冲高度 (默认: 视口高度的一半)
1114
- * @param {boolean} [options.restore] - 截图后是否恢复页面高度和样式 (默认: false)
1115
- * @param {number} [options.maxHeight] - 最大截图高度 (默认: 8000px)
1116
- * @returns {Promise<string>} - base64 编码的 PNG 图片
1146
+ * 拦截网络请求并使用 Node.js 原生 https 模块转发,以解决流式数据捕获问题。
1147
+ * @param {import('playwright').Page} page
1148
+ * @param {string|RegExp} urlPattern - 拦截的 URL 模式
1149
+ * @param {object} options
1150
+ * @param {function(string, function, string): void} [options.onData] - (textChunk, resolve, accumulatedText) => void
1151
+ * @param {function(string, function): void} [options.onEnd] - (fullText, resolve) => void
1152
+ * @param {function(Error, function): void} [options.onTimeout] - (error, reject) => void
1153
+ * @param {number} [options.initialTimeout=90000] - 初始数据接收超时 (ms),默认 90s
1154
+ * @param {number} [options.timeout=180000] - 整体请求超时时间 (ms),默认 180s
1155
+ * @returns {Promise<any>} - 返回 Promise,当流满足条件时 resolve
1117
1156
  */
1118
- async fullPageScreenshot(page, options = {}) {
1119
- logger2.start("fullPageScreenshot", "detecting scrollable elements");
1120
- const originalViewport = page.viewportSize();
1121
- const defaultBuffer = Math.round((originalViewport?.height || 1080) / 2);
1122
- const buffer = options.buffer ?? defaultBuffer;
1123
- const restore = options.restore ?? false;
1124
- const maxHeight = options.maxHeight ?? 8e3;
1125
- try {
1126
- const maxScrollHeight = await page.evaluate(() => {
1127
- let maxHeight2 = document.body.scrollHeight;
1128
- document.querySelectorAll("*").forEach((el) => {
1129
- const style = window.getComputedStyle(el);
1130
- const overflowY = style.overflowY;
1131
- if ((overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight) {
1132
- if (el.scrollHeight > maxHeight2) {
1133
- maxHeight2 = el.scrollHeight;
1134
- }
1135
- el.dataset.pkOrigOverflow = el.style.overflow;
1136
- el.dataset.pkOrigHeight = el.style.height;
1137
- el.dataset.pkOrigMaxHeight = el.style.maxHeight;
1138
- el.classList.add("__pk_expanded__");
1139
- el.style.overflow = "visible";
1140
- el.style.height = "auto";
1141
- el.style.maxHeight = "none";
1142
- }
1143
- });
1144
- return maxHeight2;
1145
- });
1146
- const targetHeight = Math.min(maxScrollHeight + buffer, maxHeight);
1147
- await page.setViewportSize({
1148
- width: originalViewport?.width || 1280,
1149
- height: targetHeight
1150
- });
1151
- await delay(1e3);
1152
- const buffer_ = await page.screenshot({
1153
- fullPage: true,
1154
- type: "png"
1155
- });
1156
- logger2.success("fullPageScreenshot", `captured ${Math.round(buffer_.length / 1024)} KB`);
1157
- return buffer_.toString("base64");
1158
- } finally {
1159
- if (restore) {
1160
- await page.evaluate(() => {
1161
- document.querySelectorAll(".__pk_expanded__").forEach((el) => {
1162
- el.style.overflow = el.dataset.pkOrigOverflow || "";
1163
- el.style.height = el.dataset.pkOrigHeight || "";
1164
- el.style.maxHeight = el.dataset.pkOrigMaxHeight || "";
1165
- delete el.dataset.pkOrigOverflow;
1166
- delete el.dataset.pkOrigHeight;
1167
- delete el.dataset.pkOrigMaxHeight;
1168
- el.classList.remove("__pk_expanded__");
1157
+ async intercept(page, urlPattern, options = {}) {
1158
+ const {
1159
+ onData,
1160
+ onEnd,
1161
+ onTimeout,
1162
+ initialTimeout = 9e4,
1163
+ overallTimeout = 18e4
1164
+ } = options;
1165
+ let initialTimer = null;
1166
+ let overallTimer = null;
1167
+ let hasReceivedInitialData = false;
1168
+ const clearAllTimers = () => {
1169
+ if (initialTimer) clearTimeout(initialTimer);
1170
+ if (overallTimer) clearTimeout(overallTimer);
1171
+ initialTimer = null;
1172
+ overallTimer = null;
1173
+ };
1174
+ const workPromise = new Promise((resolve, reject) => {
1175
+ page.route(urlPattern, async (route) => {
1176
+ const request = route.request();
1177
+ logger7.info(`[MITM] \u5DF2\u62E6\u622A\u8BF7\u6C42: ${request.url()}`);
1178
+ try {
1179
+ const headers = await request.allHeaders();
1180
+ const postData = request.postData();
1181
+ const urlObj = new URL2(request.url());
1182
+ delete headers["accept-encoding"];
1183
+ delete headers["content-length"];
1184
+ const reqOptions = {
1185
+ hostname: urlObj.hostname,
1186
+ port: 443,
1187
+ path: urlObj.pathname + urlObj.search,
1188
+ method: request.method(),
1189
+ headers,
1190
+ timeout: overallTimeout
1191
+ };
1192
+ const req = https.request(reqOptions, (res) => {
1193
+ const chunks = [];
1194
+ let accumulatedText = "";
1195
+ res.on("data", (chunk) => {
1196
+ if (!hasReceivedInitialData) {
1197
+ hasReceivedInitialData = true;
1198
+ if (initialTimer) {
1199
+ clearTimeout(initialTimer);
1200
+ initialTimer = null;
1201
+ }
1202
+ logger7.debug("[Intercept] \u5DF2\u63A5\u6536\u521D\u59CB\u6570\u636E");
1203
+ }
1204
+ chunks.push(chunk);
1205
+ const textChunk = chunk.toString("utf-8");
1206
+ accumulatedText += textChunk;
1207
+ if (onData) {
1208
+ try {
1209
+ onData(textChunk, resolve, accumulatedText);
1210
+ } catch (e) {
1211
+ logger7.fail(`onData \u9519\u8BEF`, e);
1212
+ }
1213
+ }
1214
+ });
1215
+ res.on("end", () => {
1216
+ logger7.info("[MITM] \u4E0A\u6E38\u54CD\u5E94\u7ED3\u675F");
1217
+ clearAllTimers();
1218
+ if (onEnd) {
1219
+ try {
1220
+ onEnd(accumulatedText, resolve);
1221
+ } catch (e) {
1222
+ logger7.fail(`onEnd \u9519\u8BEF`, e);
1223
+ }
1224
+ } else if (!onData) {
1225
+ resolve(accumulatedText);
1226
+ }
1227
+ route.fulfill({
1228
+ status: res.statusCode,
1229
+ headers: res.headers,
1230
+ body: Buffer.concat(chunks)
1231
+ }).catch(() => {
1232
+ });
1233
+ });
1234
+ });
1235
+ req.on("error", (e) => {
1236
+ clearAllTimers();
1237
+ route.abort().catch(() => {
1238
+ });
1239
+ reject(e);
1240
+ });
1241
+ if (postData) req.write(postData);
1242
+ req.end();
1243
+ } catch (e) {
1244
+ clearAllTimers();
1245
+ route.continue().catch(() => {
1246
+ });
1247
+ reject(e);
1248
+ }
1249
+ }).catch(reject);
1250
+ });
1251
+ const timeoutPromise = new Promise((_, reject) => {
1252
+ initialTimer = setTimeout(() => {
1253
+ if (!hasReceivedInitialData) {
1254
+ const error = new CrawlerError({
1255
+ message: `\u521D\u59CB\u6570\u636E\u63A5\u6536\u8D85\u65F6 (${initialTimeout}ms)`,
1256
+ code: Code.InitialTimeout,
1257
+ context: { timeout: initialTimeout }
1169
1258
  });
1259
+ clearAllTimers();
1260
+ if (onTimeout) {
1261
+ try {
1262
+ onTimeout(error, reject);
1263
+ } catch (e) {
1264
+ reject(e);
1265
+ }
1266
+ } else {
1267
+ reject(error);
1268
+ }
1269
+ }
1270
+ }, initialTimeout);
1271
+ overallTimer = setTimeout(() => {
1272
+ const error = new CrawlerError({
1273
+ message: `\u6574\u4F53\u8BF7\u6C42\u8D85\u65F6 (${overallTimeout}ms)`,
1274
+ code: Code.OverallTimeout,
1275
+ context: { timeout: overallTimeout }
1170
1276
  });
1171
- if (originalViewport) {
1172
- await page.setViewportSize(originalViewport);
1277
+ clearAllTimers();
1278
+ if (onTimeout) {
1279
+ try {
1280
+ onTimeout(error, reject);
1281
+ } catch (e) {
1282
+ reject(e);
1283
+ }
1284
+ } else {
1285
+ reject(error);
1173
1286
  }
1174
- }
1175
- }
1287
+ }, overallTimeout);
1288
+ });
1289
+ workPromise.catch(() => {
1290
+ });
1291
+ timeoutPromise.catch(() => {
1292
+ });
1293
+ const racePromise = Promise.race([workPromise, timeoutPromise]);
1294
+ racePromise.catch(() => {
1295
+ });
1296
+ return racePromise;
1176
1297
  }
1177
1298
  };
1178
1299
 
1179
- // src/anti-cheat.js
1180
- var logger3 = createLogger("AntiCheat");
1181
- var BASE_CONFIG = Object.freeze({
1182
- locale: "zh-CN",
1183
- acceptLanguage: "zh-CN,zh;q=0.9",
1184
- timezoneId: "Asia/Shanghai",
1185
- timezoneOffset: -480,
1186
- geolocation: null
1187
- });
1188
- var DEFAULT_LAUNCH_ARGS = [
1189
- // '--disable-blink-features=AutomationControlled', // Crawlee 可能会自动处理,过多干预反而会被识别
1190
- "--no-sandbox",
1191
- "--disable-setuid-sandbox",
1192
- "--window-position=0,0",
1193
- `--lang=${BASE_CONFIG.locale}`
1300
+ // src/interception.js
1301
+ import { gotScraping } from "got-scraping";
1302
+ import { Agent as HttpAgent } from "http";
1303
+ import { Agent as HttpsAgent } from "https";
1304
+ var logger8 = createInternalLogger("Interception");
1305
+ var SHARED_HTTP_AGENT = new HttpAgent({ keepAlive: false });
1306
+ var SHARED_HTTPS_AGENT = new HttpsAgent({ keepAlive: false, rejectUnauthorized: false });
1307
+ var DirectConfig = {
1308
+ /** 直连请求超时时间(秒) */
1309
+ directTimeout: 12,
1310
+ /** 静默扩展名:这些扩展名的直连成功日志用 debug 级别 */
1311
+ silentExtensions: [".js"]
1312
+ };
1313
+ var ARCHIVE_EXTENSIONS = [".7z", ".zip", ".rar", ".gz", ".bz2", ".tar", ".zst"];
1314
+ var EXECUTABLE_EXTENSIONS = [".exe", ".apk", ".bin", ".dmg", ".jar", ".class"];
1315
+ var DOCUMENT_EXTENSIONS = [".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".csv"];
1316
+ var IMAGE_EXTENSIONS = [
1317
+ ".jpg",
1318
+ ".jpeg",
1319
+ ".png",
1320
+ ".gif",
1321
+ ".bmp",
1322
+ ".ico",
1323
+ ".svg",
1324
+ ".svgz",
1325
+ ".webp",
1326
+ ".avif",
1327
+ ".pis",
1328
+ ".pict",
1329
+ ".tif",
1330
+ ".tiff",
1331
+ ".eps",
1332
+ ".ejs",
1333
+ ".eot"
1194
1334
  ];
1195
- function buildFingerprintOptions(locale) {
1196
- return {
1197
- browsers: [{ name: "chrome", minVersion: 110 }],
1198
- devices: ["desktop"],
1199
- operatingSystems: ["windows", "macos", "linux"],
1200
- locales: [locale]
1201
- };
1202
- }
1203
- var AntiCheat = {
1204
- /**
1205
- * 获取统一的基础配置
1206
- */
1207
- getBaseConfig() {
1208
- return { ...BASE_CONFIG };
1209
- },
1210
- /**
1211
- * 用于 Crawlee fingerprint generator 的统一配置(桌面端)。
1212
- */
1213
- getFingerprintGeneratorOptions() {
1214
- return buildFingerprintOptions(BASE_CONFIG.locale);
1215
- },
1216
- /**
1217
- * 获取基础启动参数。
1218
- */
1219
- getLaunchArgs() {
1220
- return [...DEFAULT_LAUNCH_ARGS];
1221
- },
1222
- /**
1223
- * 为 got-scraping 生成与浏览器一致的 TLS 指纹配置(桌面端)。
1224
- */
1225
- getTlsFingerprintOptions(userAgent = "", acceptLanguage = "") {
1226
- return buildFingerprintOptions(BASE_CONFIG.locale);
1227
- },
1228
- /**
1229
- * 规范化请求头
1230
- */
1231
- applyLocaleHeaders(headers, acceptLanguage = "") {
1232
- if (!headers["accept-language"]) {
1233
- headers["accept-language"] = acceptLanguage || BASE_CONFIG.acceptLanguage;
1234
- }
1235
- return headers;
1236
- }
1335
+ var MEDIA_EXTENSIONS = [".mp3", ".mp4", ".avi", ".mkv", ".webm", ".midi", ".mid", ".ogg", ".flac", ".swf"];
1336
+ var FONT_EXTENSIONS = [".woff", ".woff2", ".ttf", ".otf"];
1337
+ var CSS_EXTENSIONS = [".css"];
1338
+ var OTHER_EXTENSIONS = [".ps", ".iso"];
1339
+ var DEFAULT_BLOCKING_CONFIG = {
1340
+ /** 屏蔽压缩包 */
1341
+ blockArchive: true,
1342
+ /** 屏蔽可执行文件 */
1343
+ blockExecutable: true,
1344
+ /** 屏蔽办公文档 */
1345
+ blockDocument: true,
1346
+ /** 屏蔽图片 */
1347
+ blockImage: true,
1348
+ /** 屏蔽音视频 */
1349
+ blockMedia: true,
1350
+ /** 屏蔽字体 */
1351
+ blockFont: true,
1352
+ /** 屏蔽 CSS (注意:可能影响页面视觉效果) */
1353
+ blockCss: false,
1354
+ /** 屏蔽其他资源 */
1355
+ blockOther: true,
1356
+ /** 额外自定义扩展名列表 */
1357
+ customExtensions: []
1237
1358
  };
1238
-
1239
- // src/humanize.js
1240
- import delay2 from "delay";
1241
- import { createCursor } from "ghost-cursor-playwright";
1242
- var logger4 = createLogger("Humanize");
1243
- var $CursorWeakMap = /* @__PURE__ */ new WeakMap();
1244
- function $GetCursor(page) {
1245
- const cursor = $CursorWeakMap.get(page);
1246
- if (!cursor) {
1247
- throw new Error("Cursor \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 Humanize.initializeCursor(page)");
1248
- }
1249
- return cursor;
1250
- }
1251
- var Humanize = {
1252
- /**
1253
- * 生成带抖动的毫秒数 - 基于基础值添加随机浮动 (±30% 默认)
1254
- * @param {number} base - 基础延迟 (ms)
1255
- * @param {number} [jitterPercent=0.3] - 抖动百分比 (0.3 = ±30%)
1256
- * @returns {number} 抖动后的毫秒数
1257
- */
1258
- jitterMs(base, jitterPercent = 0.3) {
1259
- const jitter = base * jitterPercent * (Math.random() * 2 - 1);
1260
- return Math.max(10, Math.round(base + jitter));
1261
- },
1359
+ var SHARED_GOT_OPTIONS = {
1360
+ http2: false,
1361
+ // 禁用 HTTP2 避免在拦截场景下的握手兼容性问题
1362
+ retry: { limit: 0 },
1363
+ // Playwright 或外层逻辑处理重试
1364
+ throwHttpErrors: false
1365
+ // 404/500 等错误不抛出异常,直接透传给浏览器
1366
+ };
1367
+ var Interception = {
1262
1368
  /**
1263
- * 初始化页面的 Ghost Cursor(必须在使用其他 cursor 相关方法前调用)
1369
+ * 根据配置生成需要屏蔽的扩展名列表
1264
1370
  *
1265
- * @param {import('playwright').Page} page
1266
- * @returns {Promise<void>}
1371
+ * @param {Object} [config] - 屏蔽配置
1372
+ * @returns {string[]} 需要屏蔽的扩展名列表
1267
1373
  */
1268
- async initializeCursor(page) {
1269
- if ($CursorWeakMap.has(page)) {
1270
- logger4.debug("initializeCursor: cursor already exists, skipping");
1271
- return;
1374
+ getBlockedExtensions(config = {}) {
1375
+ const mergedConfig = { ...DEFAULT_BLOCKING_CONFIG, ...config };
1376
+ const extensions = [];
1377
+ if (mergedConfig.blockArchive) extensions.push(...ARCHIVE_EXTENSIONS);
1378
+ if (mergedConfig.blockExecutable) extensions.push(...EXECUTABLE_EXTENSIONS);
1379
+ if (mergedConfig.blockDocument) extensions.push(...DOCUMENT_EXTENSIONS);
1380
+ if (mergedConfig.blockImage) extensions.push(...IMAGE_EXTENSIONS);
1381
+ if (mergedConfig.blockMedia) extensions.push(...MEDIA_EXTENSIONS);
1382
+ if (mergedConfig.blockFont) extensions.push(...FONT_EXTENSIONS);
1383
+ if (mergedConfig.blockCss) extensions.push(...CSS_EXTENSIONS);
1384
+ if (mergedConfig.blockOther) extensions.push(...OTHER_EXTENSIONS);
1385
+ if (mergedConfig.customExtensions?.length > 0) {
1386
+ extensions.push(...mergedConfig.customExtensions);
1272
1387
  }
1273
- logger4.start("initializeCursor", "creating cursor");
1274
- const cursor = await createCursor(page);
1275
- $CursorWeakMap.set(page, cursor);
1276
- logger4.success("initializeCursor", "cursor initialized");
1388
+ return [...new Set(extensions)];
1277
1389
  },
1278
1390
  /**
1279
- * 人类化鼠标移动 - 使用 ghost-cursor 移动到指定位置或元素
1391
+ * 获取所有可用的扩展名分类信息
1280
1392
  *
1281
- * @param {import('playwright').Page} page
1282
- * @param {string|{x: number, y: number}|import('playwright').ElementHandle} target - CSS选择器、坐标对象或元素句柄
1393
+ * @returns {Object} 分类信息
1283
1394
  */
1284
- async humanMove(page, target) {
1285
- const cursor = $GetCursor(page);
1286
- logger4.start("humanMove", `target=${typeof target === "string" ? target : "element/coords"}`);
1287
- try {
1288
- if (typeof target === "string") {
1289
- const element = await page.$(target);
1290
- if (!element) {
1291
- logger4.warn(`humanMove: \u5143\u7D20\u4E0D\u5B58\u5728 ${target}`);
1292
- return false;
1293
- }
1294
- const box = await element.boundingBox();
1295
- if (!box) {
1296
- logger4.warn(`humanMove: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E ${target}`);
1297
- return false;
1298
- }
1299
- const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
1300
- const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
1301
- await cursor.actions.move({ x, y });
1302
- } else if (target && typeof target.x === "number" && typeof target.y === "number") {
1303
- await cursor.actions.move(target);
1304
- } else if (target && typeof target.boundingBox === "function") {
1305
- const box = await target.boundingBox();
1306
- if (box) {
1307
- const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.2;
1308
- const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.2;
1309
- await cursor.actions.move({ x, y });
1310
- }
1311
- }
1312
- logger4.success("humanMove");
1313
- return true;
1314
- } catch (error) {
1315
- logger4.fail("humanMove", error);
1316
- throw error;
1317
- }
1395
+ getExtensionCategories() {
1396
+ return {
1397
+ archive: { name: "\u538B\u7F29\u5305", extensions: ARCHIVE_EXTENSIONS },
1398
+ executable: { name: "\u53EF\u6267\u884C\u6587\u4EF6", extensions: EXECUTABLE_EXTENSIONS },
1399
+ document: { name: "\u529E\u516C\u6587\u6863", extensions: DOCUMENT_EXTENSIONS },
1400
+ image: { name: "\u56FE\u7247", extensions: IMAGE_EXTENSIONS },
1401
+ media: { name: "\u97F3\u89C6\u9891", extensions: MEDIA_EXTENSIONS },
1402
+ font: { name: "\u5B57\u4F53", extensions: FONT_EXTENSIONS },
1403
+ css: { name: "CSS \u6837\u5F0F", extensions: CSS_EXTENSIONS },
1404
+ other: { name: "\u5176\u4ED6\u8D44\u6E90", extensions: OTHER_EXTENSIONS }
1405
+ };
1318
1406
  },
1319
1407
  /**
1320
- * 渐进式滚动到元素可见(仅处理 Y 轴滚动)
1321
- * 返回 restore 方法,用于将滚动容器恢复到原位置
1408
+ * 设置网络拦截规则(资源屏蔽 + CDN 直连)
1322
1409
  *
1323
- * @param {import('playwright').Page} page
1324
- * @param {string|import('playwright').ElementHandle} target - CSS 选择器或元素句柄
1325
- * @param {Object} [options]
1326
- * @param {number} [options.maxSteps=25] - 最大滚动步数
1327
- * @param {number} [options.minStep=80] - 单次滚动最小步长
1328
- * @param {number} [options.maxStep=220] - 单次滚动最大步长
1410
+ * @param {import('playwright').Page} page - Playwright Page 对象
1411
+ * @param {Object} [options] - 配置选项
1412
+ * @param {string[]} [options.directDomains] - 需要直连的域名列表
1413
+ * @param {Object} [options.blockingConfig] - 资源屏蔽配置
1414
+ * @param {boolean} [options.fallbackToProxy] - 直连失败时是否回退到代理(默认 true)
1415
+ * @returns {Promise<void>}
1329
1416
  */
1330
- async humanScroll(page, target, options = {}) {
1331
- const { maxSteps = 30, minStep = 150, maxStep = 400 } = options;
1332
- const targetDesc = typeof target === "string" ? target : "ElementHandle";
1333
- logger4.debug(`humanScroll | \u76EE\u6807=${targetDesc}`);
1334
- let element;
1335
- if (typeof target === "string") {
1336
- element = await page.$(target);
1337
- if (!element) {
1338
- logger4.warn(`humanScroll | \u5143\u7D20\u672A\u627E\u5230: ${target}`);
1339
- return { element: null, didScroll: false };
1340
- }
1341
- } else {
1342
- element = target;
1343
- }
1344
- const cursor = $GetCursor(page);
1345
- let didScroll = false;
1346
- const checkVisibility = async () => {
1347
- return await element.evaluate((el) => {
1348
- const rect = el.getBoundingClientRect();
1349
- if (!rect || rect.width === 0 || rect.height === 0) {
1350
- return { code: "ZERO_DIMENSIONS", reason: "\u5C3A\u5BF8\u4E3A\u96F6" };
1351
- }
1352
- const cx = rect.left + rect.width / 2;
1353
- const cy = rect.top + rect.height / 2;
1354
- const viewH = window.innerHeight;
1355
- const viewW = window.innerWidth;
1356
- if (cy < 0 || cy > viewH || cx < 0 || cx > viewW) {
1357
- const direction = cy < 0 ? "up" : cy > viewH ? "down" : "unknown";
1358
- return { code: "OUT_OF_VIEWPORT", reason: "\u4E0D\u5728\u89C6\u53E3\u5185", direction, cy, viewH };
1359
- }
1360
- const pointElement = document.elementFromPoint(cx, cy);
1361
- if (pointElement && !el.contains(pointElement) && !pointElement.contains(el)) {
1362
- return {
1363
- code: "OBSTRUCTED",
1364
- reason: "\u88AB\u906E\u6321",
1365
- obstruction: {
1366
- tag: pointElement.tagName,
1367
- id: pointElement.id,
1368
- className: pointElement.className
1369
- },
1370
- cy,
1371
- // Return Center Y for smart direction calculation
1372
- viewH
1373
- };
1374
- }
1375
- return { code: "VISIBLE" };
1376
- });
1377
- };
1378
- try {
1379
- for (let i = 0; i < maxSteps; i++) {
1380
- const status = await checkVisibility();
1381
- if (status.code === "VISIBLE") {
1382
- logger4.debug("humanScroll | \u5143\u7D20\u53EF\u89C1\u4E14\u65E0\u906E\u6321");
1383
- return { element, didScroll };
1384
- }
1385
- logger4.debug(`humanScroll | \u6B65\u9AA4 ${i + 1}/${maxSteps}: ${status.reason} ${status.direction ? `(${status.direction})` : ""}`);
1386
- if (status.code === "OBSTRUCTED" && status.obstruction) {
1387
- logger4.debug(`humanScroll | \u88AB\u4EE5\u4E0B\u5143\u7D20\u906E\u6321 <${status.obstruction.tag} id="${status.obstruction.id}">`);
1417
+ async setup(page, options = {}) {
1418
+ const {
1419
+ directDomains = [],
1420
+ blockingConfig = {},
1421
+ fallbackToProxy = true
1422
+ } = options;
1423
+ const mergedBlockingConfig = { ...DEFAULT_BLOCKING_CONFIG, ...blockingConfig };
1424
+ const blockedExtensions = this.getBlockedExtensions(mergedBlockingConfig);
1425
+ const hasDirectDomains = directDomains.length > 0;
1426
+ const enabledCategories = [];
1427
+ if (mergedBlockingConfig.blockArchive) enabledCategories.push("\u538B\u7F29\u5305");
1428
+ if (mergedBlockingConfig.blockExecutable) enabledCategories.push("\u53EF\u6267\u884C\u6587\u4EF6");
1429
+ if (mergedBlockingConfig.blockDocument) enabledCategories.push("\u529E\u516C\u6587\u6863");
1430
+ if (mergedBlockingConfig.blockImage) enabledCategories.push("\u56FE\u7247");
1431
+ if (mergedBlockingConfig.blockMedia) enabledCategories.push("\u97F3\u89C6\u9891");
1432
+ if (mergedBlockingConfig.blockFont) enabledCategories.push("\u5B57\u4F53");
1433
+ if (mergedBlockingConfig.blockCss) enabledCategories.push("CSS");
1434
+ if (mergedBlockingConfig.blockOther) enabledCategories.push("\u5176\u4ED6");
1435
+ logger8.start("setup", hasDirectDomains ? `\u76F4\u8FDE\u57DF\u540D: [${directDomains.length} \u4E2A] | \u5C4F\u853D: [${enabledCategories.join(", ")}]` : `\u4EC5\u8D44\u6E90\u5C4F\u853D\u6A21\u5F0F | \u5C4F\u853D: [${enabledCategories.join(", ")}]`);
1436
+ await page.route("**/*", async (route) => {
1437
+ let handled = false;
1438
+ try {
1439
+ const request = route.request();
1440
+ const url = request.url();
1441
+ const urlLower = url.toLowerCase();
1442
+ const urlPath = urlLower.split("?")[0];
1443
+ const isSilent = DirectConfig.silentExtensions.some((ext) => urlPath.endsWith(ext));
1444
+ const shouldBlock = blockedExtensions.some((ext) => urlPath.endsWith(ext));
1445
+ if (shouldBlock) {
1446
+ await route.abort();
1447
+ handled = true;
1448
+ return;
1388
1449
  }
1389
- let deltaY = 0;
1390
- if (status.code === "OUT_OF_VIEWPORT") {
1391
- if (status.direction === "down") {
1392
- deltaY = minStep + Math.random() * (maxStep - minStep);
1393
- } else if (status.direction === "up") {
1394
- deltaY = -(minStep + Math.random() * (maxStep - minStep));
1395
- } else {
1396
- deltaY = 100;
1450
+ let isDirect = false;
1451
+ if (hasDirectDomains) {
1452
+ try {
1453
+ const hostname = new URL(url).hostname;
1454
+ isDirect = directDomains.some((domain) => hostname.startsWith(domain));
1455
+ } catch (e) {
1397
1456
  }
1398
- } else if (status.code === "OBSTRUCTED") {
1399
- const isBottomHalf = status.cy > status.viewH / 2;
1400
- const direction = isBottomHalf ? 1 : -1;
1401
- deltaY = direction * (minStep + Math.random() * 50);
1402
1457
  }
1403
- if (i === 0 || Math.random() < 0.2) {
1404
- const viewSize = page.viewportSize();
1405
- if (viewSize) {
1406
- const safeX = viewSize.width * 0.5 + (Math.random() - 0.5) * 100;
1407
- const safeY = viewSize.height * 0.5 + (Math.random() - 0.5) * 100;
1408
- await cursor.actions.move({ x: safeX, y: safeY });
1458
+ if (isDirect) {
1459
+ try {
1460
+ const reqHeaders = await request.allHeaders();
1461
+ delete reqHeaders["host"];
1462
+ const resolvedAcceptLanguage = reqHeaders["accept-language"] || "";
1463
+ const userAgent = reqHeaders["user-agent"] || "";
1464
+ const method = request.method();
1465
+ const postData = method !== "GET" && method !== "HEAD" ? request.postDataBuffer() : void 0;
1466
+ const response = await gotScraping({
1467
+ ...SHARED_GOT_OPTIONS,
1468
+ // 应用通用配置
1469
+ url,
1470
+ method,
1471
+ headers: reqHeaders,
1472
+ body: postData,
1473
+ responseType: "buffer",
1474
+ // 强制获取 Buffer
1475
+ // 移除手动 TLS 指纹配置,使用 got-scraping 默认的高质量指纹
1476
+ // headerGeneratorOptions: ...
1477
+ // 使用共享的 Agent 单例(keepAlive: false,不会池化连接)
1478
+ agent: {
1479
+ http: SHARED_HTTP_AGENT,
1480
+ https: SHARED_HTTPS_AGENT
1481
+ },
1482
+ // 超时时间
1483
+ timeout: { request: DirectConfig.directTimeout * 1e3 }
1484
+ });
1485
+ const resHeaders = {};
1486
+ for (const [key, value] of Object.entries(response.headers)) {
1487
+ if (Array.isArray(value)) {
1488
+ resHeaders[key] = value.join(", ");
1489
+ } else if (value) {
1490
+ resHeaders[key] = String(value);
1491
+ }
1492
+ }
1493
+ delete resHeaders["content-encoding"];
1494
+ delete resHeaders["content-length"];
1495
+ delete resHeaders["transfer-encoding"];
1496
+ delete resHeaders["connection"];
1497
+ delete resHeaders["keep-alive"];
1498
+ isSilent ? logger8.debug(`\u76F4\u8FDE\u6210\u529F: ${urlPath}`) : logger8.info(`\u76F4\u8FDE\u6210\u529F: ${urlPath}`);
1499
+ await safeFulfill(route, {
1500
+ status: response.statusCode,
1501
+ headers: resHeaders,
1502
+ body: response.body
1503
+ });
1504
+ handled = true;
1505
+ return;
1506
+ } catch (e) {
1507
+ const isTimeout = e.code === "ETIMEDOUT" || e.message.toLowerCase().includes("timeout");
1508
+ const action = fallbackToProxy ? "\u56DE\u9000\u4EE3\u7406" : "\u5DF2\u653E\u5F03";
1509
+ const reason = isTimeout ? `\u8D85\u65F6(${DirectConfig.directTimeout}s)` : `\u5F02\u5E38: ${e.message}`;
1510
+ logger8.warn(`\u76F4\u8FDE${reason}\uFF0C${action}: ${urlPath}`);
1511
+ if (fallbackToProxy) {
1512
+ await safeContinue(route);
1513
+ } else {
1514
+ await route.abort();
1515
+ }
1516
+ handled = true;
1517
+ return;
1409
1518
  }
1410
1519
  }
1411
- await page.mouse.wheel(0, deltaY);
1412
- didScroll = true;
1413
- await delay2(this.jitterMs(100 + Math.random() * 150, 0.2));
1414
- }
1415
- logger4.warn(`humanScroll | \u5728 ${maxSteps} \u6B65\u540E\u65E0\u6CD5\u786E\u4FDD\u53EF\u89C1\u6027`);
1416
- return { element, didScroll };
1417
- } catch (error) {
1418
- logger4.fail("humanScroll", error);
1419
- throw error;
1420
- }
1421
- },
1422
- /**
1423
- * 人类化点击 - 使用 ghost-cursor 模拟人类鼠标移动轨迹并点击
1424
- *
1425
- * @param {import('playwright').Page} page
1426
- * @param {string|import('playwright').ElementHandle} [target] - CSS 选择器或元素句柄。如果为空,则点击当前鼠标位置
1427
- * @param {Object} [options]
1428
- * @param {number} [options.reactionDelay=250] - 反应延迟基础值 (ms),实际 ±30% 抖动
1429
- * @param {boolean} [options.throwOnMissing=true] - 元素不存在时是否抛出错误
1430
- * @param {boolean} [options.scrollIfNeeded=true] - 元素不在视口时是否自动滚动
1431
- */
1432
- async humanClick(page, target, options = {}) {
1433
- const cursor = $GetCursor(page);
1434
- const { reactionDelay = 250, throwOnMissing = true, scrollIfNeeded = true, restore = false } = options;
1435
- const targetDesc = target == null ? "Current Position" : typeof target === "string" ? target : "ElementHandle";
1436
- logger4.start("humanClick", `target=${targetDesc}`);
1437
- const restoreOnce = async () => {
1438
- if (restoreOnce.restored) return;
1439
- restoreOnce.restored = true;
1440
- if (typeof restoreOnce.do !== "function") return;
1441
- try {
1442
- await delay2(this.jitterMs(1e3));
1443
- await restoreOnce.do();
1444
- } catch (restoreError) {
1445
- logger4.warn(`humanClick: \u6062\u590D\u6EDA\u52A8\u4F4D\u7F6E\u5931\u8D25: ${restoreError.message}`);
1446
- }
1447
- };
1448
- try {
1449
- if (target == null) {
1450
- await delay2(this.jitterMs(reactionDelay, 0.4));
1451
- await cursor.actions.click();
1452
- logger4.success("humanClick", "Clicked current position");
1453
- return true;
1454
- }
1455
- let element;
1456
- if (typeof target === "string") {
1457
- element = await page.$(target);
1458
- if (!element) {
1459
- if (throwOnMissing) {
1460
- throw new Error(`\u627E\u4E0D\u5230\u5143\u7D20 ${target}`);
1520
+ await safeContinue(route);
1521
+ handled = true;
1522
+ } catch (err) {
1523
+ logger8.warn(`\u8DEF\u7531\u5904\u7406\u5F02\u5E38: ${err.message}`);
1524
+ if (!handled) {
1525
+ try {
1526
+ await route.continue();
1527
+ } catch (_) {
1461
1528
  }
1462
- logger4.warn(`humanClick: \u5143\u7D20\u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u70B9\u51FB ${target}`);
1463
- return false;
1464
- }
1465
- } else {
1466
- element = target;
1467
- }
1468
- if (scrollIfNeeded) {
1469
- const { restore: restoreFn, didScroll } = await this.humanScroll(page, element);
1470
- restoreOnce.do = didScroll && restore ? restoreFn : null;
1471
- }
1472
- const box = await element.boundingBox();
1473
- if (!box) {
1474
- await restoreOnce();
1475
- if (throwOnMissing) {
1476
- throw new Error("\u65E0\u6CD5\u83B7\u53D6\u5143\u7D20\u4F4D\u7F6E");
1477
1529
  }
1478
- logger4.warn("humanClick: \u65E0\u6CD5\u83B7\u53D6\u4F4D\u7F6E\uFF0C\u8DF3\u8FC7\u70B9\u51FB");
1479
- return false;
1480
1530
  }
1481
- const x = box.x + box.width / 2 + (Math.random() - 0.5) * box.width * 0.3;
1482
- const y = box.y + box.height / 2 + (Math.random() - 0.5) * box.height * 0.3;
1483
- await cursor.actions.move({ x, y });
1484
- await delay2(this.jitterMs(reactionDelay, 0.4));
1485
- await cursor.actions.click();
1486
- await restoreOnce();
1487
- logger4.success("humanClick");
1488
- return true;
1489
- } catch (error) {
1490
- await restoreOnce();
1491
- logger4.fail("humanClick", error);
1492
- throw error;
1531
+ });
1532
+ }
1533
+ };
1534
+ async function safeFulfill(route, options) {
1535
+ try {
1536
+ await route.fulfill(options);
1537
+ } catch (error) {
1538
+ if (!isIgnorableError(error)) {
1539
+ console.error(`[Interception] Fulfill Error: ${error.message}`);
1540
+ }
1541
+ }
1542
+ }
1543
+ async function safeContinue(route) {
1544
+ try {
1545
+ await route.continue();
1546
+ } catch (error) {
1547
+ if (!isIgnorableError(error)) {
1493
1548
  }
1549
+ }
1550
+ }
1551
+ function isIgnorableError(error) {
1552
+ const msg = error.message;
1553
+ return msg.includes("already handled") || msg.includes("Target closed") || msg.includes("closed");
1554
+ }
1555
+
1556
+ // src/mutation.js
1557
+ import { v4 as uuidv42 } from "uuid";
1558
+
1559
+ // src/logger.js
1560
+ var stripAnsi = (input) => {
1561
+ if (!input) return "";
1562
+ return String(input).replace(/\x1b\[[0-9;]*m/g, "");
1563
+ };
1564
+ var STEP_PREFIX = "\u6B65\u9AA4:";
1565
+ var STEP_SEPARATOR = " | ";
1566
+ var STEP_EMOJIS = [
1567
+ { match: "\u4EFB\u52A1", emoji: "\u{1F9ED}" },
1568
+ { match: "\u8FD0\u884C\u6A21\u5F0F", emoji: "\u{1F9E9}" },
1569
+ { match: "\u767B\u5F55", emoji: "\u{1F510}" },
1570
+ { match: "\u73AF\u5883", emoji: "\u{1F9EA}" },
1571
+ { match: "\u8F93\u5165", emoji: "\u2328\uFE0F" },
1572
+ { match: "\u53D1\u9001", emoji: "\u{1F4E4}" },
1573
+ { match: "\u54CD\u5E94\u76D1\u542C", emoji: "\u{1F4E1}" },
1574
+ { match: "\u7B49\u5F85\u54CD\u5E94", emoji: "\u23F3" },
1575
+ { match: "\u6D41\u5F0F", emoji: "\u{1F9F5}" },
1576
+ { match: "\u5F15\u7528", emoji: "\u{1F4CE}" },
1577
+ { match: "\u622A\u56FE", emoji: "\u{1F5BC}\uFE0F" },
1578
+ { match: "\u5206\u4EAB\u94FE\u63A5", emoji: "\u{1F517}" },
1579
+ { match: "\u6570\u636E\u63A8\u9001", emoji: "\u{1F4E6}" },
1580
+ { match: "\u5F39\u7A97", emoji: "\u{1FA9F}" }
1581
+ ];
1582
+ var STATUS_EMOJIS = [
1583
+ { match: "\u5F00\u59CB", emoji: "\u{1F680}" },
1584
+ { match: "\u5B8C\u6210", emoji: "\u2705" },
1585
+ { match: "\u6210\u529F", emoji: "\u2705" },
1586
+ { match: "\u5931\u8D25", emoji: "\u274C" },
1587
+ { match: "\u8DF3\u8FC7", emoji: "\u23ED\uFE0F" },
1588
+ { match: "\u8D85\u65F6", emoji: "\u23F1\uFE0F" },
1589
+ { match: "\u91CD\u8BD5", emoji: "\u{1F501}" },
1590
+ { match: "\u7ED3\u675F", emoji: "\u{1F3C1}" },
1591
+ { match: "\u5DF2\u914D\u7F6E", emoji: "\u{1F9F7}" },
1592
+ { match: "\u5DF2\u68C0\u6D4B", emoji: "\u{1F50E}" }
1593
+ ];
1594
+ var toErrorMessage = (error) => {
1595
+ if (!error) return "";
1596
+ if (error instanceof Error) return error.message;
1597
+ if (typeof error === "string") return error;
1598
+ try {
1599
+ return JSON.stringify(error);
1600
+ } catch {
1601
+ return String(error);
1602
+ }
1603
+ };
1604
+ var decorateLabel = (label, mappings) => {
1605
+ if (!label) return "";
1606
+ const mapping = mappings.find((item) => label.includes(item.match));
1607
+ if (!mapping) return label;
1608
+ return `${mapping.emoji} ${label}`;
1609
+ };
1610
+ var normalizeSnippet = (snippet, maxLen = 120) => {
1611
+ if (!snippet) return "";
1612
+ const text = String(snippet).replace(/\s+/g, " ").trim();
1613
+ if (!text) return "";
1614
+ const cleaned = text.replace(/"/g, "'");
1615
+ if (cleaned.length <= maxLen) return cleaned;
1616
+ return `${cleaned.slice(0, maxLen)}...`;
1617
+ };
1618
+ var LOG_TAG_PREFIX = "[#log:";
1619
+ var LOG_TAG_SUFFIX = "]";
1620
+ var buildLogTag = (key) => `${LOG_TAG_PREFIX}${key}${LOG_TAG_SUFFIX}`;
1621
+ var escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1622
+ var buildStepPattern = (step, status) => {
1623
+ if (!step) return null;
1624
+ let pattern = `\u6B65\u9AA4: .*${escapeRegExp(step)}`;
1625
+ if (status) {
1626
+ pattern += `.*${escapeRegExp(status)}`;
1627
+ }
1628
+ return new RegExp(pattern);
1629
+ };
1630
+ var buildDefinitionPatterns = (definition) => {
1631
+ const patterns = [new RegExp(`\\[#log:${escapeRegExp(definition.key)}\\]`)];
1632
+ const fallback = buildStepPattern(definition.step, definition.status);
1633
+ if (fallback) patterns.push(fallback);
1634
+ if (Array.isArray(definition.extraPatterns)) {
1635
+ patterns.push(...definition.extraPatterns);
1636
+ }
1637
+ return patterns;
1638
+ };
1639
+ var ATTENTION_RANK = {
1640
+ low: 1,
1641
+ medium: 2,
1642
+ high: 3,
1643
+ critical: 4
1644
+ };
1645
+ var DEFAULT_ATTENTION_RANK = ATTENTION_RANK.high;
1646
+ var LOG_DEFINITIONS = [
1647
+ {
1648
+ key: "task_start",
1649
+ method: "taskStart",
1650
+ label: "\u4EFB\u52A1\u5F00\u59CB",
1651
+ group: "\u4EFB\u52A1",
1652
+ step: "\u4EFB\u52A1",
1653
+ status: "\u5F00\u59CB",
1654
+ level: "start",
1655
+ attention: "low",
1656
+ buildDetails: (url) => [url ? `url=${url}` : ""]
1657
+ },
1658
+ {
1659
+ key: "task_success",
1660
+ method: "taskSuccess",
1661
+ label: "\u4EFB\u52A1\u5B8C\u6210",
1662
+ group: "\u4EFB\u52A1",
1663
+ step: "\u4EFB\u52A1",
1664
+ status: "\u5B8C\u6210",
1665
+ level: "success",
1666
+ attention: "high"
1667
+ },
1668
+ {
1669
+ key: "task_fail",
1670
+ method: "taskFail",
1671
+ label: "\u4EFB\u52A1\u5931\u8D25",
1672
+ group: "\u4EFB\u52A1",
1673
+ step: "\u4EFB\u52A1",
1674
+ status: "\u5931\u8D25",
1675
+ level: "error",
1676
+ attention: "critical",
1677
+ buildDetails: (url, err) => [
1678
+ url ? `url=${url}` : "",
1679
+ err ? `err=${toErrorMessage(err)}` : ""
1680
+ ]
1681
+ },
1682
+ {
1683
+ key: "runtime_headless",
1684
+ method: "runtimeHeadless",
1685
+ label: "\u8FD0\u884C\u6A21\u5F0F\u5F3A\u5236\u65E0\u5934",
1686
+ group: "\u8FD0\u884C\u6A21\u5F0F",
1687
+ step: "\u8FD0\u884C\u6A21\u5F0F",
1688
+ status: "Apify \u73AF\u5883\u5F3A\u5236\u65E0\u5934",
1689
+ level: "warning",
1690
+ attention: "medium"
1691
+ },
1692
+ {
1693
+ key: "login_inject_success",
1694
+ method: "loginInjectSuccess",
1695
+ label: "\u767B\u5F55\u6001\u6CE8\u5165\u6210\u529F",
1696
+ group: "\u767B\u5F55",
1697
+ step: "\u767B\u5F55\u6001\u6CE8\u5165",
1698
+ status: "\u6210\u529F",
1699
+ level: "success",
1700
+ attention: "medium",
1701
+ buildDetails: (detail) => [detail ? `detail=${detail}` : ""]
1702
+ },
1703
+ {
1704
+ key: "login_inject_skip",
1705
+ method: "loginInjectSkip",
1706
+ label: "\u767B\u5F55\u6001\u6CE8\u5165\u8DF3\u8FC7",
1707
+ group: "\u767B\u5F55",
1708
+ step: "\u767B\u5F55\u6001\u6CE8\u5165",
1709
+ status: "\u8DF3\u8FC7",
1710
+ level: "warning",
1711
+ attention: "medium",
1712
+ buildDetails: (reason) => [reason ? `\u539F\u56E0=${reason}` : ""]
1713
+ },
1714
+ {
1715
+ key: "login_inject_fail",
1716
+ method: "loginInjectFail",
1717
+ label: "\u767B\u5F55\u6001\u6CE8\u5165\u5931\u8D25",
1718
+ group: "\u767B\u5F55",
1719
+ step: "\u767B\u5F55\u6001\u6CE8\u5165",
1720
+ status: "\u5931\u8D25",
1721
+ level: "error",
1722
+ attention: "high",
1723
+ buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
1724
+ },
1725
+ {
1726
+ key: "login_verify_success",
1727
+ method: "loginVerifySuccess",
1728
+ label: "\u767B\u5F55\u9A8C\u8BC1\u6210\u529F",
1729
+ group: "\u767B\u5F55",
1730
+ step: "\u767B\u5F55\u9A8C\u8BC1",
1731
+ status: "\u6210\u529F",
1732
+ level: "success",
1733
+ attention: "high",
1734
+ buildDetails: (detail) => [detail ? `detail=${detail}` : ""]
1735
+ },
1736
+ {
1737
+ key: "login_verify_skip",
1738
+ method: "loginVerifySkip",
1739
+ label: "\u767B\u5F55\u9A8C\u8BC1\u8DF3\u8FC7",
1740
+ group: "\u767B\u5F55",
1741
+ step: "\u767B\u5F55\u9A8C\u8BC1",
1742
+ status: "\u8DF3\u8FC7",
1743
+ level: "warning",
1744
+ attention: "medium",
1745
+ buildDetails: (reason) => [reason ? `\u539F\u56E0=${reason}` : ""]
1746
+ },
1747
+ {
1748
+ key: "login_verify_fail",
1749
+ method: "loginVerifyFail",
1750
+ label: "\u767B\u5F55\u9A8C\u8BC1\u5931\u8D25",
1751
+ group: "\u767B\u5F55",
1752
+ step: "\u767B\u5F55\u9A8C\u8BC1",
1753
+ status: "\u5931\u8D25",
1754
+ level: "error",
1755
+ attention: "critical",
1756
+ buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
1494
1757
  },
1495
- /**
1496
- * 随机延迟一段毫秒数(带 ±30% 抖动)
1497
- * @param {number} baseMs - 基础延迟毫秒数
1498
- * @param {number} [jitterPercent=0.3] - 抖动百分比
1499
- */
1500
- async randomSleep(baseMs, jitterPercent = 0.3) {
1501
- const ms = this.jitterMs(baseMs, jitterPercent);
1502
- logger4.start("randomSleep", `base=${baseMs}, actual=${ms}ms`);
1503
- await delay2(ms);
1504
- logger4.success("randomSleep");
1758
+ {
1759
+ key: "env_check_success",
1760
+ method: "envCheckSuccess",
1761
+ label: "\u73AF\u5883\u68C0\u67E5\u6210\u529F",
1762
+ group: "\u73AF\u5883",
1763
+ step: "\u73AF\u5883\u68C0\u67E5",
1764
+ status: "\u6210\u529F",
1765
+ level: "success",
1766
+ attention: "medium",
1767
+ buildDetails: (detail) => [detail ? `detail=${detail}` : ""]
1505
1768
  },
1506
- /**
1507
- * 模拟人类"注视"或"阅读"行为:鼠标在页面上随机微动
1508
- * @param {import('playwright').Page} page
1509
- * @param {number} [baseDurationMs=2500] - 基础持续时间 (±40% 抖动)
1510
- */
1511
- async simulateGaze(page, baseDurationMs = 2500) {
1512
- const cursor = $GetCursor(page);
1513
- const durationMs = this.jitterMs(baseDurationMs, 0.4);
1514
- logger4.start("simulateGaze", `duration=${durationMs}ms`);
1515
- const startTime = Date.now();
1516
- const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
1517
- while (Date.now() - startTime < durationMs) {
1518
- const x = 100 + Math.random() * (viewportSize.width - 200);
1519
- const y = 100 + Math.random() * (viewportSize.height - 200);
1520
- await cursor.actions.move({ x, y });
1521
- await delay2(this.jitterMs(600, 0.5));
1522
- }
1523
- logger4.success("simulateGaze");
1769
+ {
1770
+ key: "env_check_fail",
1771
+ method: "envCheckFail",
1772
+ label: "\u73AF\u5883\u68C0\u67E5\u5931\u8D25",
1773
+ group: "\u73AF\u5883",
1774
+ step: "\u73AF\u5883\u68C0\u67E5",
1775
+ status: "\u5931\u8D25",
1776
+ level: "error",
1777
+ attention: "high",
1778
+ buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
1524
1779
  },
1525
- /**
1526
- * 人类化输入 - 带节奏变化(快-慢-停顿-偶尔加速)
1527
- * @param {import('playwright').Page} page
1528
- * @param {string} selector - 输入框选择器
1529
- * @param {string} text - 要输入的文本
1530
- * @param {Object} [options]
1531
- * @param {number} [options.baseDelay=180] - 基础按键延迟 (ms),实际 ±40% 抖动
1532
- * @param {number} [options.pauseProbability=0.08] - 停顿概率 (0-1)
1533
- * @param {number} [options.pauseBase=800] - 停顿时长基础值 (ms),实际 ±50% 抖动
1534
- */
1535
- async humanType(page, selector, text, options = {}) {
1536
- logger4.start("humanType", `selector=${selector}, textLen=${text.length}`);
1537
- const {
1538
- baseDelay = 180,
1539
- pauseProbability = 0.08,
1540
- pauseBase = 800
1541
- } = options;
1542
- try {
1543
- const locator = page.locator(selector);
1544
- await Humanize.humanClick(page, locator);
1545
- await delay2(this.jitterMs(200, 0.4));
1546
- for (let i = 0; i < text.length; i++) {
1547
- const char = text[i];
1548
- let charDelay;
1549
- if (char === " ") {
1550
- charDelay = this.jitterMs(baseDelay * 0.6, 0.3);
1551
- } else if (/[,.!?;:,。!?;:]/.test(char)) {
1552
- charDelay = this.jitterMs(baseDelay * 1.5, 0.4);
1553
- } else {
1554
- charDelay = this.jitterMs(baseDelay, 0.4);
1555
- }
1556
- await page.keyboard.type(char);
1557
- await delay2(charDelay);
1558
- if (Math.random() < pauseProbability && i < text.length - 1) {
1559
- const pauseTime = this.jitterMs(pauseBase, 0.5);
1560
- logger4.debug(`\u505C\u987F ${pauseTime}ms...`);
1561
- await delay2(pauseTime);
1562
- }
1563
- }
1564
- logger4.success("humanType");
1565
- } catch (error) {
1566
- logger4.fail("humanType", error);
1567
- throw error;
1568
- }
1780
+ {
1781
+ key: "input_query_start",
1782
+ method: "inputQuery",
1783
+ label: "\u8F93\u5165\u67E5\u8BE2\u5F00\u59CB",
1784
+ group: "\u8F93\u5165",
1785
+ step: "\u8F93\u5165\u67E5\u8BE2",
1786
+ status: "\u5F00\u59CB",
1787
+ level: "start",
1788
+ attention: "low",
1789
+ buildDetails: (query) => [query ? `query=${query}` : ""]
1569
1790
  },
1570
- /**
1571
- * 人类化清空输入框 - 模拟人类删除文本的行为
1572
- * @param {import('playwright').Page} page
1573
- * @param {string} selector - 输入框选择器
1574
- */
1575
- async humanClear(page, selector) {
1576
- logger4.start("humanClear", `selector=${selector}`);
1577
- try {
1578
- const locator = page.locator(selector);
1579
- await locator.click();
1580
- await delay2(this.jitterMs(200, 0.4));
1581
- const currentValue = await locator.inputValue();
1582
- if (!currentValue || currentValue.length === 0) {
1583
- logger4.success("humanClear", "already empty");
1584
- return;
1585
- }
1586
- await page.keyboard.press("Meta+A");
1587
- await delay2(this.jitterMs(100, 0.4));
1588
- await page.keyboard.press("Backspace");
1589
- logger4.success("humanClear");
1590
- } catch (error) {
1591
- logger4.fail("humanClear", error);
1592
- throw error;
1593
- }
1791
+ {
1792
+ key: "send_action",
1793
+ method: "sendAction",
1794
+ label: "\u53D1\u9001\u8BF7\u6C42",
1795
+ group: "\u53D1\u9001",
1796
+ step: "\u53D1\u9001\u8BF7\u6C42",
1797
+ status: "\u70B9\u51FB\u53D1\u9001",
1798
+ level: "info",
1799
+ attention: "low"
1594
1800
  },
1595
- /**
1596
- * 页面预热浏览 - 模拟人类进入页面后的探索行为
1597
- * @param {import('playwright').Page} page
1598
- * @param {number} [baseDuration=3500] - 预热时长基础值 (±40% 抖动)
1599
- */
1600
- async warmUpBrowsing(page, baseDuration = 3500) {
1601
- const cursor = $GetCursor(page);
1602
- const durationMs = this.jitterMs(baseDuration, 0.4);
1603
- logger4.start("warmUpBrowsing", `duration=${durationMs}ms`);
1604
- const startTime = Date.now();
1605
- const viewportSize = page.viewportSize() || { width: 1920, height: 1080 };
1606
- try {
1607
- while (Date.now() - startTime < durationMs) {
1608
- const action = Math.random();
1609
- if (action < 0.4) {
1610
- const x = 100 + Math.random() * (viewportSize.width - 200);
1611
- const y = 100 + Math.random() * (viewportSize.height - 200);
1612
- await cursor.actions.move({ x, y });
1613
- await delay2(this.jitterMs(350, 0.4));
1614
- } else if (action < 0.7) {
1615
- const scrollY = (Math.random() - 0.5) * 200;
1616
- await page.mouse.wheel(0, scrollY);
1617
- await delay2(this.jitterMs(500, 0.4));
1618
- } else {
1619
- await delay2(this.jitterMs(800, 0.5));
1620
- }
1621
- }
1622
- logger4.success("warmUpBrowsing");
1623
- } catch (error) {
1624
- logger4.fail("warmUpBrowsing", error);
1625
- throw error;
1626
- }
1801
+ {
1802
+ key: "response_listen_start",
1803
+ method: "responseListenStart",
1804
+ label: "\u54CD\u5E94\u76D1\u542C\u5F00\u59CB",
1805
+ group: "\u54CD\u5E94\u76D1\u542C",
1806
+ step: "\u54CD\u5E94\u76D1\u542C",
1807
+ status: "\u5F00\u59CB",
1808
+ level: "start",
1809
+ attention: "low",
1810
+ buildDetails: (label, timeoutSec) => [
1811
+ label ? `\u76EE\u6807=${label}` : "",
1812
+ timeoutSec ? `timeout=${timeoutSec}s` : ""
1813
+ ]
1627
1814
  },
1628
- /**
1629
- * 自然滚动 - 带惯性、减速效果和随机抖动
1630
- * @param {import('playwright').Page} page
1631
- * @param {'up' | 'down'} [direction='down'] - 滚动方向
1632
- * @param {number} [distance=300] - 总滚动距离基础值 (px),±15% 抖动
1633
- * @param {number} [baseSteps=5] - 分几步完成基础值,±1 随机
1634
- */
1635
- async naturalScroll(page, direction = "down", distance = 300, baseSteps = 5) {
1636
- const steps = Math.max(3, baseSteps + Math.floor(Math.random() * 3) - 1);
1637
- const actualDistance = this.jitterMs(distance, 0.15);
1638
- logger4.start("naturalScroll", `dir=${direction}, dist=${actualDistance}, steps=${steps}`);
1639
- const sign = direction === "down" ? 1 : -1;
1640
- const stepDistance = actualDistance / steps;
1641
- try {
1642
- for (let i = 0; i < steps; i++) {
1643
- const factor = 1 - i / steps * 0.5;
1644
- const jitter = 0.9 + Math.random() * 0.2;
1645
- const scrollAmount = stepDistance * factor * sign * jitter;
1646
- await page.mouse.wheel(0, scrollAmount);
1647
- const baseDelay = 60 + i * 25;
1648
- await delay2(this.jitterMs(baseDelay, 0.3));
1649
- }
1650
- logger4.success("naturalScroll");
1651
- } catch (error) {
1652
- logger4.fail("naturalScroll", error);
1653
- throw error;
1654
- }
1655
- }
1656
- };
1657
-
1658
- // src/launch.js
1659
- var Launch = {
1660
- getLaunchOptions(customArgs = []) {
1661
- return {
1662
- args: [
1663
- ...AntiCheat.getLaunchArgs(),
1664
- ...customArgs
1665
- ],
1666
- ignoreDefaultArgs: ["--enable-automation"]
1667
- };
1815
+ {
1816
+ key: "response_listen_ready",
1817
+ method: "responseListenReady",
1818
+ label: "\u54CD\u5E94\u76D1\u542C\u5DF2\u914D\u7F6E",
1819
+ group: "\u54CD\u5E94\u76D1\u542C",
1820
+ step: "\u54CD\u5E94\u76D1\u542C",
1821
+ status: "\u5DF2\u914D\u7F6E",
1822
+ level: "info",
1823
+ attention: "medium",
1824
+ buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
1825
+ },
1826
+ {
1827
+ key: "response_listen_detected",
1828
+ method: "responseListenDetected",
1829
+ label: "\u54CD\u5E94\u76D1\u542C\u5DF2\u68C0\u6D4B",
1830
+ group: "\u54CD\u5E94\u76D1\u542C",
1831
+ step: "\u54CD\u5E94\u76D1\u542C",
1832
+ status: "\u5DF2\u68C0\u6D4B",
1833
+ level: "success",
1834
+ attention: "medium",
1835
+ buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
1836
+ },
1837
+ {
1838
+ key: "response_listen_timeout",
1839
+ method: "responseListenTimeout",
1840
+ label: "\u54CD\u5E94\u76D1\u542C\u8D85\u65F6",
1841
+ group: "\u54CD\u5E94\u76D1\u542C",
1842
+ step: "\u54CD\u5E94\u76D1\u542C",
1843
+ status: "\u8D85\u65F6",
1844
+ level: "error",
1845
+ attention: "high",
1846
+ buildDetails: (label, err) => [
1847
+ label ? `\u76EE\u6807=${label}` : "",
1848
+ err ? `err=${toErrorMessage(err)}` : ""
1849
+ ]
1850
+ },
1851
+ {
1852
+ key: "response_listen_end",
1853
+ method: "responseListenEnd",
1854
+ label: "\u54CD\u5E94\u76D1\u542C\u7ED3\u675F",
1855
+ group: "\u54CD\u5E94\u76D1\u542C",
1856
+ step: "\u54CD\u5E94\u76D1\u542C",
1857
+ status: "\u7ED3\u675F",
1858
+ level: "info",
1859
+ attention: "low",
1860
+ buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
1668
1861
  },
1669
- /**
1670
- * 推荐的 Fingerprint Generator 选项
1671
- * 确保生成的是桌面端、较新的 Chrome,以匹配我们的脚本逻辑
1672
- */
1673
- getFingerprintGeneratorOptions() {
1674
- return AntiCheat.getFingerprintGeneratorOptions();
1675
- }
1676
- };
1677
-
1678
- // src/live-view.js
1679
- import express from "express";
1680
- import { Actor } from "apify";
1681
- var logger5 = createLogger("LiveView");
1682
- async function startLiveViewServer(liveViewKey) {
1683
- const app = express();
1684
- app.get("/", async (req, res) => {
1685
- try {
1686
- const screenshotBuffer = await Actor.getValue(liveViewKey);
1687
- if (!screenshotBuffer) {
1688
- res.send('<html><head><meta http-equiv="refresh" content="2"></head><body>\u7B49\u5F85\u7B2C\u4E00\u4E2A\u5C4F\u5E55\u622A\u56FE...</body></html>');
1689
- return;
1690
- }
1691
- const screenshotBase64 = screenshotBuffer.toString("base64");
1692
- res.send(`
1693
- <html>
1694
- <head>
1695
- <title>Live View (\u622A\u56FE)</title>
1696
- <meta http-equiv="refresh" content="1">
1697
- </head>
1698
- <body style="margin:0; padding:0;">
1699
- <img src="data:image/png;base64,${screenshotBase64}"
1700
- alt="Live View Screenshot"
1701
- style="width: 100%; height: auto;" />
1702
- </body>
1703
- </html>
1704
- `);
1705
- } catch (error) {
1706
- logger5.fail("Live View Server", error);
1707
- res.status(500).send(`\u65E0\u6CD5\u52A0\u8F7D\u5C4F\u5E55\u622A\u56FE: ${error.message}`);
1708
- }
1709
- });
1710
- const port = process.env.APIFY_CONTAINER_PORT || 4321;
1711
- app.listen(port, () => {
1712
- logger5.success("startLiveViewServer", `\u76D1\u542C\u7AEF\u53E3 ${port}`);
1713
- });
1714
- }
1715
- async function takeLiveScreenshot(liveViewKey, page, logMessage) {
1716
- try {
1717
- const buffer = await page.screenshot({ type: "png" });
1718
- await Actor.setValue(liveViewKey, buffer, { contentType: "image/png" });
1719
- if (logMessage) {
1720
- logger5.info(`(\u622A\u56FE): ${logMessage}`);
1721
- }
1722
- } catch (e) {
1723
- logger5.warn(`\u65E0\u6CD5\u6355\u83B7 Live View \u5C4F\u5E55\u622A\u56FE: ${e.message}`);
1724
- }
1725
- }
1726
- var useLiveView = (liveViewKey = PresetOfLiveViewKey) => {
1727
- return {
1728
- takeLiveScreenshot: async (page, logMessage) => {
1729
- return await takeLiveScreenshot(liveViewKey, page, logMessage);
1730
- },
1731
- startLiveViewServer: async () => {
1732
- return await startLiveViewServer(liveViewKey);
1733
- }
1734
- };
1735
- };
1736
- var LiveView = {
1737
- useLiveView
1738
- };
1739
-
1740
- // src/captcha-monitor.js
1741
- import { v4 as uuidv4 } from "uuid";
1742
- var logger6 = createLogger("Captcha");
1743
- function useCaptchaMonitor(page, options) {
1744
- const { domSelector, urlPattern, onDetected } = options;
1745
- if (!domSelector && !urlPattern) {
1746
- throw new Error("[CaptchaMonitor] \u5FC5\u987B\u63D0\u4F9B domSelector \u6216 urlPattern \u81F3\u5C11\u4E00\u4E2A");
1747
- }
1748
- if (!onDetected || typeof onDetected !== "function") {
1749
- throw new Error("[CaptchaMonitor] onDetected \u5FC5\u987B\u662F\u4E00\u4E2A\u51FD\u6570");
1750
- }
1751
- let isHandled = false;
1752
- let frameHandler = null;
1753
- let exposedFunctionName = null;
1754
- const triggerDetected = async () => {
1755
- if (isHandled) return;
1756
- isHandled = true;
1757
- await onDetected();
1758
- };
1759
- const cleanupFns = [];
1760
- if (domSelector) {
1761
- exposedFunctionName = `__c_d_${uuidv4().replace(/-/g, "_")}`;
1762
- const cleanerName = `__c_cleaner_${uuidv4().replace(/-/g, "_")}`;
1763
- page.exposeFunction(exposedFunctionName, triggerDetected).catch(() => {
1764
- });
1765
- page.addInitScript(({ selector, callbackName, cleanerName: cleanerName2 }) => {
1766
- (() => {
1767
- let observer = null;
1768
- const checkAndReport = () => {
1769
- const element = document.querySelector(selector);
1770
- if (element) {
1771
- if (observer) {
1772
- observer.disconnect();
1773
- observer = null;
1774
- }
1775
- if (window[callbackName]) {
1776
- window[callbackName]();
1777
- }
1778
- return true;
1779
- }
1780
- return false;
1781
- };
1782
- if (checkAndReport()) return;
1783
- observer = new MutationObserver((mutations) => {
1784
- let shouldCheck = false;
1785
- for (const mutation of mutations) {
1786
- if (mutation.addedNodes.length > 0) {
1787
- shouldCheck = true;
1788
- break;
1789
- }
1790
- }
1791
- if (shouldCheck && observer) {
1792
- checkAndReport();
1793
- }
1794
- });
1795
- const mountObserver = () => {
1796
- const target = document.documentElement;
1797
- if (target && observer) {
1798
- observer.observe(target, { childList: true, subtree: true });
1799
- }
1800
- };
1801
- if (document.readyState === "loading") {
1802
- window.addEventListener("DOMContentLoaded", mountObserver);
1803
- } else {
1804
- mountObserver();
1805
- }
1806
- window[cleanerName2] = () => {
1807
- if (observer) {
1808
- observer.disconnect();
1809
- observer = null;
1810
- }
1811
- };
1812
- })();
1813
- }, { selector: domSelector, callbackName: exposedFunctionName, cleanerName });
1814
- logger6.success("useCaptchaMonitor", `DOM \u76D1\u63A7\u5DF2\u542F\u7528: ${domSelector}`);
1815
- cleanupFns.push(async () => {
1816
- try {
1817
- await page.evaluate((name) => {
1818
- if (window[name]) {
1819
- window[name]();
1820
- delete window[name];
1821
- }
1822
- }, cleanerName);
1823
- } catch (e) {
1824
- }
1825
- });
1826
- }
1827
- if (urlPattern) {
1828
- frameHandler = async (frame) => {
1829
- if (frame === page.mainFrame()) {
1830
- const currentUrl = page.url();
1831
- if (currentUrl.includes(urlPattern)) {
1832
- await triggerDetected();
1833
- }
1834
- }
1835
- };
1836
- page.on("framenavigated", frameHandler);
1837
- logger6.success("useCaptchaMonitor", `URL \u76D1\u63A7\u5DF2\u542F\u7528: ${urlPattern}`);
1838
- cleanupFns.push(async () => {
1839
- page.off("framenavigated", frameHandler);
1840
- });
1841
- }
1842
- return {
1843
- stop: async () => {
1844
- logger6.info("useCaptchaMonitor", "\u6B63\u5728\u505C\u6B62\u76D1\u63A7...");
1845
- for (const fn of cleanupFns) {
1846
- await fn();
1847
- }
1848
- isHandled = true;
1849
- }
1850
- };
1851
- }
1852
- var Captcha = {
1853
- useCaptchaMonitor
1854
- };
1855
-
1856
- // src/sse.js
1857
- import https from "https";
1858
- import { URL as URL2 } from "url";
1859
- var logger7 = createLogger("Sse");
1860
- var Sse = {
1861
- /**
1862
- * 解析 SSE 流文本
1863
- * 支持 `data: {...}` 和 `data:{...}` 两种格式
1864
- * @param {string} sseStreamText
1865
- * @returns {Array<Object>} events
1866
- */
1867
- parseSseStream(sseStreamText) {
1868
- const events = [];
1869
- const lines = sseStreamText.split("\n");
1870
- for (const line of lines) {
1871
- if (line.startsWith("data:")) {
1872
- try {
1873
- const jsonContent = line.substring(5).trim();
1874
- if (jsonContent) {
1875
- events.push(JSON.parse(jsonContent));
1876
- }
1877
- } catch (e) {
1878
- logger7.debug("parseSseStream", `JSON \u89E3\u6790\u5931\u8D25: ${e.message}, line: ${line.substring(0, 100)}...`);
1879
- }
1880
- }
1881
- }
1882
- logger7.success("parseSseStream", `\u89E3\u6790\u5B8C\u6210, events \u6570\u91CF: ${events.length}`);
1883
- return events;
1862
+ {
1863
+ key: "response_wait_start",
1864
+ method: "responseWaitStart",
1865
+ label: "\u7B49\u5F85\u54CD\u5E94\u5F00\u59CB",
1866
+ group: "\u7B49\u5F85\u54CD\u5E94",
1867
+ step: "\u7B49\u5F85\u54CD\u5E94",
1868
+ status: "\u5F00\u59CB",
1869
+ level: "start",
1870
+ attention: "low",
1871
+ buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
1872
+ },
1873
+ {
1874
+ key: "response_wait_success",
1875
+ method: "responseWaitSuccess",
1876
+ label: "\u7B49\u5F85\u54CD\u5E94\u5B8C\u6210",
1877
+ group: "\u7B49\u5F85\u54CD\u5E94",
1878
+ step: "\u7B49\u5F85\u54CD\u5E94",
1879
+ status: "\u5B8C\u6210",
1880
+ level: "success",
1881
+ attention: "medium",
1882
+ buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
1883
+ },
1884
+ {
1885
+ key: "response_wait_fail",
1886
+ method: "responseWaitFail",
1887
+ label: "\u7B49\u5F85\u54CD\u5E94\u5931\u8D25",
1888
+ group: "\u7B49\u5F85\u54CD\u5E94",
1889
+ step: "\u7B49\u5F85\u54CD\u5E94",
1890
+ status: "\u5931\u8D25",
1891
+ level: "warning",
1892
+ attention: "high",
1893
+ buildDetails: (label, err) => [
1894
+ label ? `\u76EE\u6807=${label}` : "",
1895
+ err ? `err=${toErrorMessage(err)}` : ""
1896
+ ]
1884
1897
  },
1885
- /**
1886
- * 拦截网络请求并使用 Node.js 原生 https 模块转发,以解决流式数据捕获问题。
1887
- * @param {import('playwright').Page} page
1888
- * @param {string|RegExp} urlPattern - 拦截的 URL 模式
1889
- * @param {object} options
1890
- * @param {function(string, function, string): void} [options.onData] - (textChunk, resolve, accumulatedText) => void
1891
- * @param {function(string, function): void} [options.onEnd] - (fullText, resolve) => void
1892
- * @param {function(Error, function): void} [options.onTimeout] - (error, reject) => void
1893
- * @param {number} [options.initialTimeout=90000] - 初始数据接收超时 (ms),默认 90s
1894
- * @param {number} [options.timeout=180000] - 整体请求超时时间 (ms),默认 180s
1895
- * @returns {Promise<any>} - 返回 Promise,当流满足条件时 resolve
1896
- */
1897
- async intercept(page, urlPattern, options = {}) {
1898
- const {
1899
- onData,
1900
- onEnd,
1901
- onTimeout,
1902
- initialTimeout = 9e4,
1903
- overallTimeout = 18e4
1904
- } = options;
1905
- let initialTimer = null;
1906
- let overallTimer = null;
1907
- let hasReceivedInitialData = false;
1908
- const clearAllTimers = () => {
1909
- if (initialTimer) clearTimeout(initialTimer);
1910
- if (overallTimer) clearTimeout(overallTimer);
1911
- initialTimer = null;
1912
- overallTimer = null;
1913
- };
1914
- const workPromise = new Promise((resolve, reject) => {
1915
- page.route(urlPattern, async (route) => {
1916
- const request = route.request();
1917
- logger7.info(`[MITM] \u5DF2\u62E6\u622A\u8BF7\u6C42: ${request.url()}`);
1918
- try {
1919
- const headers = await request.allHeaders();
1920
- const postData = request.postData();
1921
- const urlObj = new URL2(request.url());
1922
- delete headers["accept-encoding"];
1923
- delete headers["content-length"];
1924
- const reqOptions = {
1925
- hostname: urlObj.hostname,
1926
- port: 443,
1927
- path: urlObj.pathname + urlObj.search,
1928
- method: request.method(),
1929
- headers,
1930
- timeout: overallTimeout
1931
- };
1932
- const req = https.request(reqOptions, (res) => {
1933
- const chunks = [];
1934
- let accumulatedText = "";
1935
- res.on("data", (chunk) => {
1936
- if (!hasReceivedInitialData) {
1937
- hasReceivedInitialData = true;
1938
- if (initialTimer) {
1939
- clearTimeout(initialTimer);
1940
- initialTimer = null;
1941
- }
1942
- logger7.debug("[Intercept] \u5DF2\u63A5\u6536\u521D\u59CB\u6570\u636E");
1943
- }
1944
- chunks.push(chunk);
1945
- const textChunk = chunk.toString("utf-8");
1946
- accumulatedText += textChunk;
1947
- if (onData) {
1948
- try {
1949
- onData(textChunk, resolve, accumulatedText);
1950
- } catch (e) {
1951
- logger7.fail(`onData \u9519\u8BEF`, e);
1952
- }
1953
- }
1954
- });
1955
- res.on("end", () => {
1956
- logger7.info("[MITM] \u4E0A\u6E38\u54CD\u5E94\u7ED3\u675F");
1957
- clearAllTimers();
1958
- if (onEnd) {
1959
- try {
1960
- onEnd(accumulatedText, resolve);
1961
- } catch (e) {
1962
- logger7.fail(`onEnd \u9519\u8BEF`, e);
1963
- }
1964
- } else if (!onData) {
1965
- resolve(accumulatedText);
1966
- }
1967
- route.fulfill({
1968
- status: res.statusCode,
1969
- headers: res.headers,
1970
- body: Buffer.concat(chunks)
1971
- }).catch(() => {
1972
- });
1973
- });
1974
- });
1975
- req.on("error", (e) => {
1976
- clearAllTimers();
1977
- route.abort().catch(() => {
1978
- });
1979
- reject(e);
1980
- });
1981
- if (postData) req.write(postData);
1982
- req.end();
1983
- } catch (e) {
1984
- clearAllTimers();
1985
- route.continue().catch(() => {
1986
- });
1987
- reject(e);
1988
- }
1989
- }).catch(reject);
1990
- });
1991
- const timeoutPromise = new Promise((_, reject) => {
1992
- initialTimer = setTimeout(() => {
1993
- if (!hasReceivedInitialData) {
1994
- const error = new CrawlerError({
1995
- message: `\u521D\u59CB\u6570\u636E\u63A5\u6536\u8D85\u65F6 (${initialTimeout}ms)`,
1996
- code: Code.InitialTimeout,
1997
- context: { timeout: initialTimeout }
1998
- });
1999
- clearAllTimers();
2000
- if (onTimeout) {
2001
- try {
2002
- onTimeout(error, reject);
2003
- } catch (e) {
2004
- reject(e);
2005
- }
2006
- } else {
2007
- reject(error);
2008
- }
2009
- }
2010
- }, initialTimeout);
2011
- overallTimer = setTimeout(() => {
2012
- const error = new CrawlerError({
2013
- message: `\u6574\u4F53\u8BF7\u6C42\u8D85\u65F6 (${overallTimeout}ms)`,
2014
- code: Code.OverallTimeout,
2015
- context: { timeout: overallTimeout }
2016
- });
2017
- clearAllTimers();
2018
- if (onTimeout) {
2019
- try {
2020
- onTimeout(error, reject);
2021
- } catch (e) {
2022
- reject(e);
2023
- }
2024
- } else {
2025
- reject(error);
2026
- }
2027
- }, overallTimeout);
2028
- });
2029
- workPromise.catch(() => {
2030
- });
2031
- timeoutPromise.catch(() => {
2032
- });
2033
- const racePromise = Promise.race([workPromise, timeoutPromise]);
2034
- racePromise.catch(() => {
2035
- });
2036
- return racePromise;
2037
- }
2038
- };
2039
-
2040
- // src/interception.js
2041
- import { gotScraping } from "got-scraping";
2042
- import { Agent as HttpAgent } from "http";
2043
- import { Agent as HttpsAgent } from "https";
2044
- var logger8 = createLogger("Interception");
2045
- var SHARED_HTTP_AGENT = new HttpAgent({ keepAlive: false });
2046
- var SHARED_HTTPS_AGENT = new HttpsAgent({ keepAlive: false, rejectUnauthorized: false });
2047
- var DirectConfig = {
2048
- /** 直连请求超时时间(秒) */
2049
- directTimeout: 12,
2050
- /** 静默扩展名:这些扩展名的直连成功日志用 debug 级别 */
2051
- silentExtensions: [".js"]
2052
- };
2053
- var ARCHIVE_EXTENSIONS = [".7z", ".zip", ".rar", ".gz", ".bz2", ".tar", ".zst"];
2054
- var EXECUTABLE_EXTENSIONS = [".exe", ".apk", ".bin", ".dmg", ".jar", ".class"];
2055
- var DOCUMENT_EXTENSIONS = [".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".csv"];
2056
- var IMAGE_EXTENSIONS = [
2057
- ".jpg",
2058
- ".jpeg",
2059
- ".png",
2060
- ".gif",
2061
- ".bmp",
2062
- ".ico",
2063
- ".svg",
2064
- ".svgz",
2065
- ".webp",
2066
- ".avif",
2067
- ".pis",
2068
- ".pict",
2069
- ".tif",
2070
- ".tiff",
2071
- ".eps",
2072
- ".ejs",
2073
- ".eot"
2074
- ];
2075
- var MEDIA_EXTENSIONS = [".mp3", ".mp4", ".avi", ".mkv", ".webm", ".midi", ".mid", ".ogg", ".flac", ".swf"];
2076
- var FONT_EXTENSIONS = [".woff", ".woff2", ".ttf", ".otf"];
2077
- var CSS_EXTENSIONS = [".css"];
2078
- var OTHER_EXTENSIONS = [".ps", ".iso"];
2079
- var DEFAULT_BLOCKING_CONFIG = {
2080
- /** 屏蔽压缩包 */
2081
- blockArchive: true,
2082
- /** 屏蔽可执行文件 */
2083
- blockExecutable: true,
2084
- /** 屏蔽办公文档 */
2085
- blockDocument: true,
2086
- /** 屏蔽图片 */
2087
- blockImage: true,
2088
- /** 屏蔽音视频 */
2089
- blockMedia: true,
2090
- /** 屏蔽字体 */
2091
- blockFont: true,
2092
- /** 屏蔽 CSS (注意:可能影响页面视觉效果) */
2093
- blockCss: false,
2094
- /** 屏蔽其他资源 */
2095
- blockOther: true,
2096
- /** 额外自定义扩展名列表 */
2097
- customExtensions: []
2098
- };
2099
- var SHARED_GOT_OPTIONS = {
2100
- http2: false,
2101
- // 禁用 HTTP2 避免在拦截场景下的握手兼容性问题
2102
- retry: { limit: 0 },
2103
- // 让 Playwright 或外层逻辑处理重试
2104
- throwHttpErrors: false
2105
- // 404/500 等错误不抛出异常,直接透传给浏览器
2106
- };
2107
- var Interception = {
2108
- /**
2109
- * 根据配置生成需要屏蔽的扩展名列表
2110
- *
2111
- * @param {Object} [config] - 屏蔽配置
2112
- * @returns {string[]} 需要屏蔽的扩展名列表
2113
- */
2114
- getBlockedExtensions(config = {}) {
2115
- const mergedConfig = { ...DEFAULT_BLOCKING_CONFIG, ...config };
2116
- const extensions = [];
2117
- if (mergedConfig.blockArchive) extensions.push(...ARCHIVE_EXTENSIONS);
2118
- if (mergedConfig.blockExecutable) extensions.push(...EXECUTABLE_EXTENSIONS);
2119
- if (mergedConfig.blockDocument) extensions.push(...DOCUMENT_EXTENSIONS);
2120
- if (mergedConfig.blockImage) extensions.push(...IMAGE_EXTENSIONS);
2121
- if (mergedConfig.blockMedia) extensions.push(...MEDIA_EXTENSIONS);
2122
- if (mergedConfig.blockFont) extensions.push(...FONT_EXTENSIONS);
2123
- if (mergedConfig.blockCss) extensions.push(...CSS_EXTENSIONS);
2124
- if (mergedConfig.blockOther) extensions.push(...OTHER_EXTENSIONS);
2125
- if (mergedConfig.customExtensions?.length > 0) {
2126
- extensions.push(...mergedConfig.customExtensions);
2127
- }
2128
- return [...new Set(extensions)];
1898
+ {
1899
+ key: "response_wait_retry",
1900
+ method: "responseWaitRetry",
1901
+ label: "\u7B49\u5F85\u54CD\u5E94\u91CD\u8BD5",
1902
+ group: "\u7B49\u5F85\u54CD\u5E94",
1903
+ step: "\u7B49\u5F85\u54CD\u5E94",
1904
+ status: "\u91CD\u8BD5",
1905
+ level: "warning",
1906
+ attention: "medium",
1907
+ buildDetails: (label, attempt) => [
1908
+ label ? `\u76EE\u6807=${label}` : "",
1909
+ attempt !== void 0 ? `\u5C1D\u8BD5=${attempt}` : ""
1910
+ ]
2129
1911
  },
2130
- /**
2131
- * 获取所有可用的扩展名分类信息
2132
- *
2133
- * @returns {Object} 分类信息
2134
- */
2135
- getExtensionCategories() {
2136
- return {
2137
- archive: { name: "\u538B\u7F29\u5305", extensions: ARCHIVE_EXTENSIONS },
2138
- executable: { name: "\u53EF\u6267\u884C\u6587\u4EF6", extensions: EXECUTABLE_EXTENSIONS },
2139
- document: { name: "\u529E\u516C\u6587\u6863", extensions: DOCUMENT_EXTENSIONS },
2140
- image: { name: "\u56FE\u7247", extensions: IMAGE_EXTENSIONS },
2141
- media: { name: "\u97F3\u89C6\u9891", extensions: MEDIA_EXTENSIONS },
2142
- font: { name: "\u5B57\u4F53", extensions: FONT_EXTENSIONS },
2143
- css: { name: "CSS \u6837\u5F0F", extensions: CSS_EXTENSIONS },
2144
- other: { name: "\u5176\u4ED6\u8D44\u6E90", extensions: OTHER_EXTENSIONS }
2145
- };
1912
+ {
1913
+ key: "dom_chunk",
1914
+ method: "domChunk",
1915
+ label: "DOM\u7247\u6BB5",
1916
+ group: "DOM",
1917
+ step: "DOM\u7247\u6BB5",
1918
+ status: "",
1919
+ level: "info",
1920
+ attention: "low",
1921
+ throttleKey: "dom-chunk",
1922
+ throttleMs: 2e3,
1923
+ buildDetails: (length, snippet, paused) => [
1924
+ length !== void 0 ? `len=${length}` : "",
1925
+ snippet ? `preview="${normalizeSnippet(snippet)}"` : "",
1926
+ paused ? "paused=1" : ""
1927
+ ]
1928
+ },
1929
+ {
1930
+ key: "dom_complete",
1931
+ method: "domComplete",
1932
+ label: "DOM\u7A33\u5B9A\u5B8C\u6210",
1933
+ group: "DOM",
1934
+ step: "DOM\u7A33\u5B9A",
1935
+ status: "\u5B8C\u6210",
1936
+ level: "success",
1937
+ attention: "medium",
1938
+ buildDetails: (mutationCount, stableTime, wasPaused) => [
1939
+ mutationCount !== void 0 ? `mutations=${mutationCount}` : "",
1940
+ stableTime !== void 0 ? `stableTime=${stableTime}ms` : "",
1941
+ wasPaused ? "paused=1" : ""
1942
+ ]
1943
+ },
1944
+ {
1945
+ key: "stream_chunk",
1946
+ method: "streamChunk",
1947
+ label: "\u6D41\u5F0F\u7247\u6BB5",
1948
+ group: "\u6D41\u5F0F",
1949
+ step: "\u6D41\u5F0F\u7247\u6BB5",
1950
+ status: "",
1951
+ level: "info",
1952
+ attention: "low",
1953
+ throttleKey: "stream-chunk",
1954
+ throttleMs: 2e3,
1955
+ buildDetails: (length, snippet) => [
1956
+ length !== void 0 ? `len=${length}` : "",
1957
+ snippet ? `preview="${normalizeSnippet(snippet)}"` : ""
1958
+ ]
1959
+ },
1960
+ {
1961
+ key: "stream_events_parsed",
1962
+ method: "streamEventsParsed",
1963
+ label: "\u6D41\u5F0F\u4E8B\u4EF6\u89E3\u6790\u5B8C\u6210",
1964
+ group: "\u6D41\u5F0F",
1965
+ step: "\u6D41\u5F0F\u4E8B\u4EF6\u89E3\u6790",
1966
+ status: "\u5B8C\u6210",
1967
+ level: "info",
1968
+ attention: "low",
1969
+ throttleKey: "stream-events",
1970
+ throttleMs: 4e3,
1971
+ buildDetails: (count) => [count !== void 0 ? `count=${count}` : ""]
1972
+ },
1973
+ {
1974
+ key: "stream_complete_event",
1975
+ method: "streamCompleteEvent",
1976
+ label: "\u6D41\u5F0F\u5B8C\u6210\u4E8B\u4EF6\u6355\u83B7",
1977
+ group: "\u6D41\u5F0F",
1978
+ step: "\u6D41\u5F0F\u4E8B\u4EF6",
1979
+ status: "\u5B8C\u6210\u4E8B\u4EF6\u5DF2\u6355\u83B7",
1980
+ level: "success",
1981
+ attention: "medium"
1982
+ },
1983
+ {
1984
+ key: "stream_end",
1985
+ method: "streamEnd",
1986
+ label: "\u6D41\u5F0F\u54CD\u5E94\u7ED3\u675F",
1987
+ group: "\u6D41\u5F0F",
1988
+ step: "\u6D41\u5F0F\u54CD\u5E94",
1989
+ status: "\u7ED3\u675F",
1990
+ level: "info",
1991
+ attention: "low"
1992
+ },
1993
+ {
1994
+ key: "reference_expand_start",
1995
+ method: "referenceExpandStart",
1996
+ label: "\u5F15\u7528\u5C55\u5F00\u5F00\u59CB",
1997
+ group: "\u5F15\u7528",
1998
+ step: "\u5F15\u7528\u5C55\u5F00",
1999
+ status: "\u5F00\u59CB",
2000
+ level: "start",
2001
+ attention: "low",
2002
+ buildDetails: (label) => [label ? `\u76EE\u6807=${label}` : ""]
2003
+ },
2004
+ {
2005
+ key: "reference_expand_fail",
2006
+ method: "referenceExpandFail",
2007
+ label: "\u5F15\u7528\u5C55\u5F00\u5931\u8D25",
2008
+ group: "\u5F15\u7528",
2009
+ step: "\u5F15\u7528\u5C55\u5F00",
2010
+ status: "\u5931\u8D25",
2011
+ level: "warning",
2012
+ attention: "medium",
2013
+ buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
2014
+ },
2015
+ {
2016
+ key: "screenshot_start",
2017
+ method: "screenshotStart",
2018
+ label: "\u622A\u56FE\u5F00\u59CB",
2019
+ group: "\u622A\u56FE",
2020
+ step: "\u622A\u56FE",
2021
+ status: "\u5F00\u59CB",
2022
+ level: "start",
2023
+ attention: "low"
2024
+ },
2025
+ {
2026
+ key: "screenshot_success",
2027
+ method: "screenshotSuccess",
2028
+ label: "\u622A\u56FE\u6210\u529F",
2029
+ group: "\u622A\u56FE",
2030
+ step: "\u622A\u56FE",
2031
+ status: "\u6210\u529F",
2032
+ level: "success",
2033
+ attention: "high"
2034
+ },
2035
+ {
2036
+ key: "screenshot_fail",
2037
+ method: "screenshotFail",
2038
+ label: "\u622A\u56FE\u5931\u8D25",
2039
+ group: "\u622A\u56FE",
2040
+ step: "\u622A\u56FE",
2041
+ status: "\u5931\u8D25",
2042
+ level: "warning",
2043
+ attention: "high",
2044
+ buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
2045
+ },
2046
+ {
2047
+ key: "share_start",
2048
+ method: "shareStart",
2049
+ label: "\u5206\u4EAB\u94FE\u63A5\u5F00\u59CB",
2050
+ group: "\u5206\u4EAB",
2051
+ step: "\u5206\u4EAB\u94FE\u63A5",
2052
+ status: "\u5F00\u59CB",
2053
+ level: "start",
2054
+ attention: "low"
2055
+ },
2056
+ {
2057
+ key: "share_progress",
2058
+ method: "shareProgress",
2059
+ label: "\u5206\u4EAB\u94FE\u63A5\u6B65\u9AA4",
2060
+ group: "\u5206\u4EAB",
2061
+ step: "\u5206\u4EAB\u94FE\u63A5",
2062
+ status: "\u6B65\u9AA4",
2063
+ level: "info",
2064
+ attention: "low",
2065
+ buildDetails: (label) => [label ? `\u6B65\u9AA4=${label}` : ""]
2066
+ },
2067
+ {
2068
+ key: "share_success",
2069
+ method: "shareSuccess",
2070
+ label: "\u5206\u4EAB\u94FE\u63A5\u6210\u529F",
2071
+ group: "\u5206\u4EAB",
2072
+ step: "\u5206\u4EAB\u94FE\u63A5",
2073
+ status: "\u6210\u529F",
2074
+ level: "success",
2075
+ attention: "high",
2076
+ buildDetails: (link) => [link ? `\u94FE\u63A5=${link}` : ""]
2077
+ },
2078
+ {
2079
+ key: "share_fail",
2080
+ method: "shareFail",
2081
+ label: "\u5206\u4EAB\u94FE\u63A5\u5931\u8D25",
2082
+ group: "\u5206\u4EAB",
2083
+ step: "\u5206\u4EAB\u94FE\u63A5",
2084
+ status: "\u5931\u8D25",
2085
+ level: "warning",
2086
+ attention: "high",
2087
+ buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
2088
+ },
2089
+ {
2090
+ key: "share_skip",
2091
+ method: "shareSkip",
2092
+ label: "\u5206\u4EAB\u94FE\u63A5\u8DF3\u8FC7",
2093
+ group: "\u5206\u4EAB",
2094
+ step: "\u5206\u4EAB\u94FE\u63A5",
2095
+ status: "\u8DF3\u8FC7",
2096
+ level: "warning",
2097
+ attention: "medium",
2098
+ buildDetails: (reason) => [reason ? `\u539F\u56E0=${reason}` : ""]
2099
+ },
2100
+ {
2101
+ key: "data_push_success",
2102
+ method: "dataPushSuccess",
2103
+ label: "\u6570\u636E\u63A8\u9001\u6210\u529F",
2104
+ group: "\u6570\u636E\u63A8\u9001",
2105
+ step: "\u6570\u636E\u63A8\u9001",
2106
+ status: "\u6210\u529F",
2107
+ level: "success",
2108
+ attention: "medium",
2109
+ buildDetails: (label) => [label ? `\u8BF4\u660E=${label}` : ""]
2110
+ },
2111
+ {
2112
+ key: "data_push_fail",
2113
+ method: "dataPushFail",
2114
+ label: "\u6570\u636E\u63A8\u9001\u5931\u8D25",
2115
+ group: "\u6570\u636E\u63A8\u9001",
2116
+ step: "\u6570\u636E\u63A8\u9001",
2117
+ status: "\u5931\u8D25",
2118
+ level: "error",
2119
+ attention: "high",
2120
+ buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
2121
+ },
2122
+ {
2123
+ key: "popup_detected",
2124
+ method: "popupDetected",
2125
+ label: "\u5F39\u7A97\u68C0\u6D4B",
2126
+ group: "\u5F39\u7A97",
2127
+ step: "\u5F39\u7A97\u5904\u7406",
2128
+ status: "\u68C0\u6D4B\u5230\u906E\u7F69",
2129
+ level: "warning",
2130
+ attention: "medium",
2131
+ buildDetails: (detail) => [detail ? `detail=${detail}` : ""]
2132
+ },
2133
+ {
2134
+ key: "popup_close_attempt",
2135
+ method: "popupCloseAttempt",
2136
+ label: "\u5F39\u7A97\u5173\u95ED\u5C1D\u8BD5",
2137
+ group: "\u5F39\u7A97",
2138
+ step: "\u5F39\u7A97\u5904\u7406",
2139
+ status: "\u5C1D\u8BD5\u5173\u95ED",
2140
+ level: "info",
2141
+ attention: "low",
2142
+ buildDetails: (detail) => [detail ? `detail=${detail}` : ""]
2143
+ },
2144
+ {
2145
+ key: "popup_close_success",
2146
+ method: "popupCloseSuccess",
2147
+ label: "\u5F39\u7A97\u5173\u95ED\u5B8C\u6210",
2148
+ group: "\u5F39\u7A97",
2149
+ step: "\u5F39\u7A97\u5904\u7406",
2150
+ status: "\u5173\u95ED\u5B8C\u6210",
2151
+ level: "success",
2152
+ attention: "medium"
2146
2153
  },
2147
- /**
2148
- * 设置网络拦截规则(资源屏蔽 + CDN 直连)
2149
- *
2150
- * @param {import('playwright').Page} page - Playwright Page 对象
2151
- * @param {Object} [options] - 配置选项
2152
- * @param {string[]} [options.directDomains] - 需要直连的域名列表
2153
- * @param {Object} [options.blockingConfig] - 资源屏蔽配置
2154
- * @param {boolean} [options.fallbackToProxy] - 直连失败时是否回退到代理(默认 true)
2155
- * @returns {Promise<void>}
2156
- */
2157
- async setup(page, options = {}) {
2158
- const {
2159
- directDomains = [],
2160
- blockingConfig = {},
2161
- fallbackToProxy = true
2162
- } = options;
2163
- const mergedBlockingConfig = { ...DEFAULT_BLOCKING_CONFIG, ...blockingConfig };
2164
- const blockedExtensions = this.getBlockedExtensions(mergedBlockingConfig);
2165
- const hasDirectDomains = directDomains.length > 0;
2166
- const enabledCategories = [];
2167
- if (mergedBlockingConfig.blockArchive) enabledCategories.push("\u538B\u7F29\u5305");
2168
- if (mergedBlockingConfig.blockExecutable) enabledCategories.push("\u53EF\u6267\u884C\u6587\u4EF6");
2169
- if (mergedBlockingConfig.blockDocument) enabledCategories.push("\u529E\u516C\u6587\u6863");
2170
- if (mergedBlockingConfig.blockImage) enabledCategories.push("\u56FE\u7247");
2171
- if (mergedBlockingConfig.blockMedia) enabledCategories.push("\u97F3\u89C6\u9891");
2172
- if (mergedBlockingConfig.blockFont) enabledCategories.push("\u5B57\u4F53");
2173
- if (mergedBlockingConfig.blockCss) enabledCategories.push("CSS");
2174
- if (mergedBlockingConfig.blockOther) enabledCategories.push("\u5176\u4ED6");
2175
- logger8.start("setup", hasDirectDomains ? `\u76F4\u8FDE\u57DF\u540D: [${directDomains.length} \u4E2A] | \u5C4F\u853D: [${enabledCategories.join(", ")}]` : `\u4EC5\u8D44\u6E90\u5C4F\u853D\u6A21\u5F0F | \u5C4F\u853D: [${enabledCategories.join(", ")}]`);
2176
- await page.route("**/*", async (route) => {
2177
- let handled = false;
2178
- try {
2179
- const request = route.request();
2180
- const url = request.url();
2181
- const urlLower = url.toLowerCase();
2182
- const urlPath = urlLower.split("?")[0];
2183
- const isSilent = DirectConfig.silentExtensions.some((ext) => urlPath.endsWith(ext));
2184
- const shouldBlock = blockedExtensions.some((ext) => urlPath.endsWith(ext));
2185
- if (shouldBlock) {
2186
- await route.abort();
2187
- handled = true;
2188
- return;
2189
- }
2190
- let isDirect = false;
2191
- if (hasDirectDomains) {
2192
- try {
2193
- const hostname = new URL(url).hostname;
2194
- isDirect = directDomains.some((domain) => hostname.startsWith(domain));
2195
- } catch (e) {
2196
- }
2197
- }
2198
- if (isDirect) {
2199
- try {
2200
- const reqHeaders = await request.allHeaders();
2201
- delete reqHeaders["host"];
2202
- const resolvedAcceptLanguage = reqHeaders["accept-language"] || "";
2203
- const userAgent = reqHeaders["user-agent"] || "";
2204
- const method = request.method();
2205
- const postData = method !== "GET" && method !== "HEAD" ? request.postDataBuffer() : void 0;
2206
- const response = await gotScraping({
2207
- ...SHARED_GOT_OPTIONS,
2208
- // 应用通用配置
2209
- url,
2210
- method,
2211
- headers: reqHeaders,
2212
- body: postData,
2213
- responseType: "buffer",
2214
- // 强制获取 Buffer
2215
- // 移除手动 TLS 指纹配置,使用 got-scraping 默认的高质量指纹
2216
- // headerGeneratorOptions: ...
2217
- // 使用共享的 Agent 单例(keepAlive: false,不会池化连接)
2218
- agent: {
2219
- http: SHARED_HTTP_AGENT,
2220
- https: SHARED_HTTPS_AGENT
2221
- },
2222
- // 超时时间
2223
- timeout: { request: DirectConfig.directTimeout * 1e3 }
2224
- });
2225
- const resHeaders = {};
2226
- for (const [key, value] of Object.entries(response.headers)) {
2227
- if (Array.isArray(value)) {
2228
- resHeaders[key] = value.join(", ");
2229
- } else if (value) {
2230
- resHeaders[key] = String(value);
2231
- }
2232
- }
2233
- delete resHeaders["content-encoding"];
2234
- delete resHeaders["content-length"];
2235
- delete resHeaders["transfer-encoding"];
2236
- delete resHeaders["connection"];
2237
- delete resHeaders["keep-alive"];
2238
- isSilent ? logger8.debug(`\u76F4\u8FDE\u6210\u529F: ${urlPath}`) : logger8.info(`\u76F4\u8FDE\u6210\u529F: ${urlPath}`);
2239
- await safeFulfill(route, {
2240
- status: response.statusCode,
2241
- headers: resHeaders,
2242
- body: response.body
2243
- });
2244
- handled = true;
2245
- return;
2246
- } catch (e) {
2247
- const isTimeout = e.code === "ETIMEDOUT" || e.message.toLowerCase().includes("timeout");
2248
- const action = fallbackToProxy ? "\u56DE\u9000\u4EE3\u7406" : "\u5DF2\u653E\u5F03";
2249
- const reason = isTimeout ? `\u8D85\u65F6(${DirectConfig.directTimeout}s)` : `\u5F02\u5E38: ${e.message}`;
2250
- logger8.warn(`\u76F4\u8FDE${reason}\uFF0C${action}: ${urlPath}`);
2251
- if (fallbackToProxy) {
2252
- await safeContinue(route);
2253
- } else {
2254
- await route.abort();
2255
- }
2256
- handled = true;
2257
- return;
2258
- }
2259
- }
2260
- await safeContinue(route);
2261
- handled = true;
2262
- } catch (err) {
2263
- logger8.warn(`\u8DEF\u7531\u5904\u7406\u5F02\u5E38: ${err.message}`);
2264
- if (!handled) {
2265
- try {
2266
- await route.continue();
2267
- } catch (_) {
2268
- }
2269
- }
2270
- }
2271
- });
2154
+ {
2155
+ key: "popup_close_fail",
2156
+ method: "popupCloseFail",
2157
+ label: "\u5F39\u7A97\u5173\u95ED\u5931\u8D25",
2158
+ group: "\u5F39\u7A97",
2159
+ step: "\u5F39\u7A97\u5904\u7406",
2160
+ status: "\u5173\u95ED\u5931\u8D25",
2161
+ level: "warning",
2162
+ attention: "medium",
2163
+ buildDetails: (err) => [err ? `err=${toErrorMessage(err)}` : ""]
2164
+ }
2165
+ ];
2166
+ var LOG_TEMPLATES = LOG_DEFINITIONS.map((definition) => {
2167
+ const attention = definition.attention || "medium";
2168
+ const attentionRank = ATTENTION_RANK[attention] || ATTENTION_RANK.medium;
2169
+ const defaultSelected = attentionRank >= DEFAULT_ATTENTION_RANK;
2170
+ return {
2171
+ key: definition.key,
2172
+ label: definition.label,
2173
+ group: definition.group,
2174
+ attention,
2175
+ patterns: buildDefinitionPatterns(definition),
2176
+ defaultSelected
2177
+ };
2178
+ });
2179
+ var buildStepLine = (step, status, details = []) => {
2180
+ const parts = [];
2181
+ const decoratedStep = step ? decorateLabel(step, STEP_EMOJIS) : "";
2182
+ const base = decoratedStep ? `${STEP_PREFIX} ${decoratedStep}` : STEP_PREFIX;
2183
+ parts.push(base.trim());
2184
+ if (status) parts.push(decorateLabel(status, STATUS_EMOJIS));
2185
+ const detailParts = details.filter(Boolean);
2186
+ if (detailParts.length > 0) {
2187
+ parts.push(...detailParts);
2272
2188
  }
2189
+ return parts.join(STEP_SEPARATOR);
2273
2190
  };
2274
- async function safeFulfill(route, options) {
2275
- try {
2276
- await route.fulfill(options);
2277
- } catch (error) {
2278
- if (!isIgnorableError(error)) {
2279
- console.error(`[Interception] Fulfill Error: ${error.message}`);
2191
+ var createThrottle = () => {
2192
+ const lastMap = /* @__PURE__ */ new Map();
2193
+ return (key, intervalMs, fn) => {
2194
+ const now = Date.now();
2195
+ const last = lastMap.get(key) || 0;
2196
+ if (now - last >= intervalMs) {
2197
+ lastMap.set(key, now);
2198
+ fn();
2280
2199
  }
2281
- }
2282
- }
2283
- async function safeContinue(route) {
2284
- try {
2285
- await route.continue();
2286
- } catch (error) {
2287
- if (!isIgnorableError(error)) {
2200
+ };
2201
+ };
2202
+ var createTemplateLogger = (baseLogger = createBaseLogger()) => {
2203
+ const throttle = createThrottle();
2204
+ const info = (line) => baseLogger.info(line);
2205
+ const success = (line) => baseLogger.success(line);
2206
+ const warning = (line) => baseLogger.warning(line);
2207
+ const error = (line) => baseLogger.error(line);
2208
+ const debug = (line) => baseLogger.debug(line);
2209
+ const start = (line) => baseLogger.start(line);
2210
+ const stepInfo = (step, status, details = []) => info(buildStepLine(step, status, details));
2211
+ const stepSuccess = (step, status, details = []) => success(buildStepLine(step, status, details));
2212
+ const stepWarn = (step, status, details = []) => warning(buildStepLine(step, status, details));
2213
+ const stepError = (step, status, details = []) => error(buildStepLine(step, status, details));
2214
+ const stepStart = (step, status, details = []) => start(buildStepLine(step, status, details));
2215
+ const stepHandlers = {
2216
+ info: stepInfo,
2217
+ success: stepSuccess,
2218
+ warning: stepWarn,
2219
+ error: stepError,
2220
+ start: stepStart
2221
+ };
2222
+ const logFromDefinition = (definition, details = []) => {
2223
+ const handler = stepHandlers[definition.level] || stepInfo;
2224
+ const payload = [...details, buildLogTag(definition.key)];
2225
+ const emit = () => handler(definition.step, definition.status, payload);
2226
+ if (definition.throttleMs) {
2227
+ throttle(definition.throttleKey || definition.key, definition.throttleMs, emit);
2228
+ return;
2288
2229
  }
2230
+ emit();
2231
+ };
2232
+ const definitionMethods = {};
2233
+ LOG_DEFINITIONS.forEach((definition) => {
2234
+ if (!definition.method) return;
2235
+ definitionMethods[definition.method] = (...args) => {
2236
+ const details = definition.buildDetails ? definition.buildDetails(...args) : [];
2237
+ logFromDefinition(definition, details);
2238
+ };
2239
+ });
2240
+ return {
2241
+ step: (step, status, details, level = "info") => {
2242
+ if (level === "error") return stepError(step, status, details);
2243
+ if (level === "warn" || level === "warning") return stepWarn(step, status, details);
2244
+ if (level === "start") return stepStart(step, status, details);
2245
+ if (level === "success") return stepSuccess(step, status, details);
2246
+ return stepInfo(step, status, details);
2247
+ },
2248
+ info: (message) => info(message),
2249
+ success: (message) => success(message),
2250
+ warning: (message) => warning(message),
2251
+ warn: (message) => warning(message),
2252
+ error: (message) => error(message),
2253
+ debug: (message) => debug(message),
2254
+ start: (message) => start(message),
2255
+ ...definitionMethods
2256
+ };
2257
+ };
2258
+ var getDefaultBaseLogger = () => createBaseLogger("");
2259
+ var Logger = {
2260
+ setLogger: (logger10) => setDefaultLogger(logger10),
2261
+ info: (message) => getDefaultBaseLogger().info(message),
2262
+ success: (message) => getDefaultBaseLogger().success(message),
2263
+ warning: (message) => getDefaultBaseLogger().warning(message),
2264
+ warn: (message) => getDefaultBaseLogger().warning(message),
2265
+ error: (message) => getDefaultBaseLogger().error(message),
2266
+ debug: (message) => getDefaultBaseLogger().debug(message),
2267
+ start: (message) => getDefaultBaseLogger().start(message),
2268
+ useTemplate: (logger10) => {
2269
+ if (logger10) return createTemplateLogger(createBaseLogger("", logger10));
2270
+ return createTemplateLogger();
2289
2271
  }
2290
- }
2291
- function isIgnorableError(error) {
2292
- const msg = error.message;
2293
- return msg.includes("already handled") || msg.includes("Target closed") || msg.includes("closed");
2294
- }
2272
+ };
2295
2273
 
2296
2274
  // src/mutation.js
2297
- import { v4 as uuidv42 } from "uuid";
2298
- var logger9 = createLogger("Mutation");
2275
+ var logger9 = createInternalLogger("Mutation");
2299
2276
  function generateKey(prefix) {
2300
2277
  return `__${prefix}_${uuidv42().replace(/-/g, "_")}`;
2301
2278
  }
@@ -2527,7 +2504,7 @@ var Mutation = {
2527
2504
  };
2528
2505
 
2529
2506
  // entrys/node.js
2530
- Logger.setLogger(crawleeLog2);
2507
+ Logger.setLogger(crawleeLog);
2531
2508
  var usePlaywrightToolKit = () => {
2532
2509
  return {
2533
2510
  ApifyKit,
@@ -2542,15 +2519,11 @@ var usePlaywrightToolKit = () => {
2542
2519
  Errors: errors_exports,
2543
2520
  Interception,
2544
2521
  Mutation,
2545
- Logger
2522
+ Logger,
2523
+ $Internals: { LOG_TEMPLATES, stripAnsi }
2546
2524
  };
2547
2525
  };
2548
- var browser = { Logger, LOG_TEMPLATES, stripAnsi };
2549
2526
  export {
2550
- LOG_TEMPLATES,
2551
- Logger,
2552
- browser,
2553
- stripAnsi,
2554
2527
  usePlaywrightToolKit
2555
2528
  };
2556
2529
  //# sourceMappingURL=index.js.map