tt-help-cli-ycl 1.4.43 → 1.4.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.43",
3
+ "version": "1.4.45",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -170,9 +170,6 @@ export async function handleExplore(options) {
170
170
 
171
171
  const page = await getOrCreatePage(browser);
172
172
 
173
- // 创建专用商品页(后台复用,避免重复打开/关闭闪屏)
174
- const shopPage = await browser.contexts()[0].newPage();
175
-
176
173
  // 先导航到 TikTok 页面,再检测登录状态
177
174
  await withRetry("启动页面导航", async () => {
178
175
  await page.goto(STARTUP_TIKTOK_URL, {
@@ -222,9 +219,6 @@ export async function handleExplore(options) {
222
219
  browser = newBrowser;
223
220
  const newPage = await setupNewPage(browser);
224
221
  Object.assign(page, newPage);
225
- // 重建商品页(旧 browser 已关闭)
226
- await shopPage.close().catch(() => {});
227
- Object.assign(shopPage, await newBrowser.contexts()[0].newPage());
228
222
  Object.assign(cdpOptions, {
229
223
  port: nextAccount.port,
230
224
  userDataDir: nextAccount.userDataDir,
@@ -394,14 +388,13 @@ export async function handleExplore(options) {
394
388
  {
395
389
  maxVideos: exploreMaxVideos,
396
390
  enableFollow: exploreEnableFollow,
391
+ enableVideos: false,
397
392
  loggedIn, // 传入登录状态,避免每次调用 isLoggedIn(page)
398
393
  maxFollowing: exploreMaxFollowing,
399
394
  maxFollowers: exploreMaxFollowers,
400
395
  location: exploreLocation,
401
396
  proxyServer: cdpOptions.proxyServer || null,
402
397
  webserverUrl: exploreWebserver || null,
403
- browser,
404
- shopPage,
405
398
  },
406
399
  console.error,
407
400
  );
@@ -415,9 +408,6 @@ export async function handleExplore(options) {
415
408
  browser = newBrowser;
416
409
  const newPage = await setupNewPage(browser);
417
410
  Object.assign(page, newPage);
418
- // 重建商品页
419
- await shopPage.close().catch(() => {});
420
- Object.assign(shopPage, await newBrowser.contexts()[0].newPage());
421
411
  // 重试当前用户
422
412
  result = await processExplore(
423
413
  page,
@@ -426,13 +416,13 @@ export async function handleExplore(options) {
426
416
  maxVideos: exploreMaxVideos,
427
417
  enableFollow: exploreEnableFollow,
428
418
  loggedIn, // 传入登录状态
419
+ enableVideos: false,
429
420
  maxFollowing: exploreMaxFollowing,
430
421
  maxFollowers: exploreMaxFollowers,
431
422
  location: exploreLocation,
432
423
  proxyServer: cdpOptions.proxyServer || null,
433
424
  webserverUrl: exploreWebserver || null,
434
425
  browser,
435
- shopPage,
436
426
  },
437
427
  console.error,
438
428
  );
@@ -475,13 +465,13 @@ export async function handleExplore(options) {
475
465
  maxVideos: exploreMaxVideos,
476
466
  enableFollow: exploreEnableFollow,
477
467
  loggedIn,
468
+ enableVideos: false,
478
469
  maxFollowing: exploreMaxFollowing,
479
470
  maxFollowers: exploreMaxFollowers,
480
471
  location: exploreLocation,
481
472
  proxyServer: cdpOptions.proxyServer || null,
482
473
  webserverUrl: exploreWebserver || null,
483
474
  browser,
484
- shopPage,
485
475
  },
486
476
  console.error,
487
477
  );
@@ -172,9 +172,6 @@ export async function handleRefresh(options) {
172
172
 
173
173
  const page = await getOrCreatePage(browser);
174
174
 
175
- // 创建专用商品页(后台复用,避免重复打开/关闭闪屏)
176
- const shopPage = await browser.contexts()[0].newPage();
177
-
178
175
  // 导航到 TikTok 页面
179
176
  await withRetry("启动页面导航", async () => {
180
177
  await page.goto(STARTUP_TIKTOK_URL, {
@@ -223,9 +220,6 @@ export async function handleRefresh(options) {
223
220
  browser = newBrowser;
224
221
  const newPage = await setupNewPage(browser);
225
222
  Object.assign(page, newPage);
226
- // 重建商品页(旧 browser 已关闭)
227
- await shopPage.close().catch(() => {});
228
- Object.assign(shopPage, await newBrowser.contexts()[0].newPage());
229
223
  Object.assign(cdpOptions, {
230
224
  port: nextAccount.port,
231
225
  userDataDir: nextAccount.userDataDir,
@@ -404,13 +398,13 @@ export async function handleRefresh(options) {
404
398
  {
405
399
  maxVideos: exploreMaxVideos || 16,
406
400
  enableFollow: exploreEnableFollow !== false,
401
+ enableVideos: false,
407
402
  loggedIn,
408
403
  maxFollowing: exploreMaxFollowing || 100,
409
404
  maxFollowers: exploreMaxFollowers || 100,
410
405
  location: exploreLocation,
411
406
  locationMode: "refresh",
412
407
  browser,
413
- shopPage,
414
408
  proxyServer: cdpOptions.proxyServer || null,
415
409
  webserverUrl: exploreWebserver || null,
416
410
  },
@@ -426,22 +420,19 @@ export async function handleRefresh(options) {
426
420
  browser = newBrowser;
427
421
  const newPage = await setupNewPage(browser);
428
422
  Object.assign(page, newPage);
429
- // 重建商品页
430
- await shopPage.close().catch(() => {});
431
- Object.assign(shopPage, await newBrowser.contexts()[0].newPage());
432
423
  result = await processExplore(
433
424
  page,
434
425
  username,
435
426
  {
436
427
  maxVideos: exploreMaxVideos || 16,
437
428
  enableFollow: exploreEnableFollow !== false,
429
+ enableVideos: false,
438
430
  loggedIn,
439
431
  maxFollowing: exploreMaxFollowing || 100,
440
432
  maxFollowers: exploreMaxFollowers || 100,
441
433
  location: exploreLocation,
442
434
  locationMode: "refresh",
443
435
  browser,
444
- shopPage,
445
436
  proxyServer: cdpOptions.proxyServer || null,
446
437
  webserverUrl: exploreWebserver || null,
447
438
  },
@@ -485,13 +476,13 @@ export async function handleRefresh(options) {
485
476
  {
486
477
  maxVideos: exploreMaxVideos || 16,
487
478
  enableFollow: exploreEnableFollow !== false,
479
+ enableVideos: false,
488
480
  loggedIn,
489
481
  maxFollowing: exploreMaxFollowing || 100,
490
482
  maxFollowers: exploreMaxFollowers || 100,
491
483
  location: exploreLocation,
492
484
  locationMode: "refresh",
493
485
  browser,
494
- shopPage,
495
486
  proxyServer: cdpOptions.proxyServer || null,
496
487
  webserverUrl: exploreWebserver || null,
497
488
  },
@@ -5,12 +5,14 @@ import {
5
5
  } from "../webserver/server.mjs";
6
6
 
7
7
  export async function handleWebserver(options) {
8
- const { webserverPort, webserverPoolSize, webserverProxy } = options;
8
+ const { webserverPort, webserverPoolSize, webserverProxy, webserverNotify } =
9
+ options;
9
10
 
10
11
  const { server, port } = await startWebserver(
11
12
  webserverPort,
12
13
  webserverPoolSize,
13
14
  webserverProxy,
15
+ webserverNotify,
14
16
  );
15
17
 
16
18
  let shuttingDown = false;
package/src/lib/args.js CHANGED
@@ -407,6 +407,7 @@ function parseWebserverArgs(args) {
407
407
  let webserverPort = 3000;
408
408
  let webserverPoolSize = 0;
409
409
  let webserverProxy = null;
410
+ let webserverNotify = null;
410
411
 
411
412
  for (let i = 0; i < args.length; i++) {
412
413
  const arg = args[i];
@@ -416,6 +417,8 @@ function parseWebserverArgs(args) {
416
417
  webserverPoolSize = parseInt(args[++i]) || 0;
417
418
  } else if (arg === "--proxy") {
418
419
  webserverProxy = args[++i] || null;
420
+ } else if (arg === "--notify") {
421
+ webserverNotify = args[++i] || null;
419
422
  }
420
423
  }
421
424
 
@@ -424,6 +427,7 @@ function parseWebserverArgs(args) {
424
427
  webserverPort,
425
428
  webserverPoolSize,
426
429
  webserverProxy,
430
+ webserverNotify,
427
431
  urls: [],
428
432
  outputFile: null,
429
433
  outputFormat: "json",
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { ProxyAgent, request } from "undici";
20
+ import { execSync } from "child_process";
20
21
  import { DEFAULT_PROXY } from "./constants.js";
21
22
 
22
23
  const PDP_HEADERS = {
@@ -26,6 +27,16 @@ const PDP_HEADERS = {
26
27
  "Accept-Language": "en-US,en;q=0.9",
27
28
  };
28
29
 
30
+ // 浏览器解验证码后缓存的 Cookie,给 undici 复用
31
+ let cachedCookies = null;
32
+ let cookiesExpireAt = 0;
33
+ const COOKIE_TTL_MS = 30 * 60 * 1000; // Cookie 有效期 30 分钟
34
+
35
+ function getCookieHeader() {
36
+ if (!cachedCookies || Date.now() > cookiesExpireAt) return null;
37
+ return cachedCookies.map((c) => `${c.name}=${c.value}`).join("; ");
38
+ }
39
+
29
40
  /**
30
41
  * Extract a JSON object by its key name, handling nested braces and strings.
31
42
  */
@@ -71,19 +82,77 @@ function extractJsonByKey(html, keyPattern) {
71
82
  }
72
83
 
73
84
  /**
74
- * Open a headed browser for the user to manually solve TikTok captcha.
75
- * Returns true once the user closes the browser (captcha assumed solved).
85
+ * Windows: 后台服务的 Playwright 浏览器在 Session 0(隔离桌面),
86
+ * RDP 看不到。改为用 start 命令推到交互桌面,然后轮询 undici 等 IP 恢复信用。
76
87
  */
88
+ async function resolveCaptchaWindows(url, proxyUrl) {
89
+ // 用 Windows start 命令在交互桌面打开 Edge
90
+ const edgeUrl = url.replace("shop.tiktok.com", "www.tiktok.com/shop/pdp");
91
+ try {
92
+ execSync(`start "" "microsoft-edge:${edgeUrl}"`, {
93
+ shell: "cmd.exe",
94
+ timeout: 5000,
95
+ });
96
+ console.error(
97
+ "[shop-scraper] Windows: 已用 start 命令在交互桌面打开 Edge,请在 RDP 中完成验证码",
98
+ );
99
+ } catch (e) {
100
+ console.error("[shop-scraper] Windows: start 命令失败:", e.message);
101
+ }
102
+
103
+ // 轮询 undici 等待 IP 信用恢复(用户解完验证码后代理 IP 就有信用了)
104
+ const proxy = proxyUrl || DEFAULT_PROXY;
105
+ const agent = proxy ? new ProxyAgent(proxy) : null;
106
+ const POLL_MS = 5000;
107
+ const MAX_WAIT_MS = 3600_000; // 最长等 1 小时
108
+ const start = Date.now();
109
+
110
+ while (Date.now() - start < MAX_WAIT_MS) {
111
+ await new Promise((r) => setTimeout(r, POLL_MS));
112
+ try {
113
+ const { body } = await request(url, {
114
+ headers: PDP_HEADERS,
115
+ ...(agent ? { dispatcher: agent } : {}),
116
+ });
117
+ const html = await body.text();
118
+ if (
119
+ html.includes('"product_model"') ||
120
+ html.includes('"review_ratings"')
121
+ ) {
122
+ console.error("[shop-scraper] Captcha solved, IP credit restored");
123
+ return true;
124
+ }
125
+ } catch {
126
+ // 继续轮询
127
+ }
128
+ // 每 60 秒打印一次状态
129
+ if (Math.round((Date.now() - start) / 60000) % 5 === 0) {
130
+ console.error(
131
+ `[shop-scraper] 等待验证码解决中... 已等待 ${Math.round((Date.now() - start) / 60000)} 分钟`,
132
+ );
133
+ }
134
+ }
135
+
136
+ console.error("[shop-scraper] Windows: 等待验证码超时");
137
+ return false;
138
+ }
139
+
77
140
  /**
78
- * Open a headed browser for manual captcha solving. Polls the page until
79
- * product content appears, then auto-closes the browser.
141
+ * Open a headed browser for manual captcha solving, extract cookies
142
+ * so undici can reuse them for a while.
80
143
  * @param {string} url - TikTok Shop PDP URL to open in browser
81
144
  * @param {string} [proxyUrl]
82
- * @returns {Promise<boolean>} true if captcha was solved, false if timed out
145
+ * @returns {Promise<boolean>} true if captcha was solved and cookies cached
83
146
  */
84
147
  export async function resolveCaptchaWithBrowser(url, proxyUrl) {
85
148
  const { chromium } = await import("playwright");
86
149
 
150
+ // Windows 后台服务启动的浏览器在 Session 0,RDP 看不到。
151
+ // 用 start 命令推到当前交互桌面。
152
+ if (process.platform === "win32") {
153
+ return resolveCaptchaWindows(url, proxyUrl);
154
+ }
155
+
87
156
  const args = ["--no-sandbox"];
88
157
  if (proxyUrl) {
89
158
  args.push(`--proxy-server=${proxyUrl.replace("http://", "http://")}`);
@@ -110,10 +179,10 @@ export async function resolveCaptchaWithBrowser(url, proxyUrl) {
110
179
  "[shop-scraper] Browser opened, waiting for captcha to be solved...",
111
180
  );
112
181
 
113
- // Poll until captcha is solved (page loads real content) or user closes browser
182
+ // Poll until captcha is solved (page loads real content) or browser closed
114
183
  let solved = false;
115
- const POLL_MS = 1000;
116
- const MAX_WAIT_MS = 120000;
184
+ const POLL_MS = 2000;
185
+ const MAX_WAIT_MS = 3600_000; // 最长等 1 小时
117
186
  const start = Date.now();
118
187
 
119
188
  while (!solved && Date.now() - start < MAX_WAIT_MS) {
@@ -126,19 +195,29 @@ export async function resolveCaptchaWithBrowser(url, proxyUrl) {
126
195
  solved = true;
127
196
  break;
128
197
  }
129
- // Check if browser was closed by user
130
198
  if (!browser.isConnected()) break;
131
199
  } catch (e) {
132
- // Page might be closed, check browser
133
200
  if (!browser.isConnected()) break;
134
201
  }
135
202
  await new Promise((r) => setTimeout(r, POLL_MS));
136
203
  }
137
204
 
138
205
  if (solved) {
139
- console.error("[shop-scraper] Captcha solved, closing browser...");
206
+ // 提取 Cookie 给 undici 复用
207
+ try {
208
+ const cookies = await page.context().cookies();
209
+ if (cookies.length > 0) {
210
+ cachedCookies = cookies;
211
+ cookiesExpireAt = Date.now() + COOKIE_TTL_MS;
212
+ console.error(
213
+ `[shop-scraper] Captcha solved, cached ${cookies.length} cookies for undici reuse`,
214
+ );
215
+ }
216
+ } catch (e) {
217
+ console.error("[shop-scraper] Failed to extract cookies:", e.message);
218
+ }
140
219
  } else {
141
- console.error("[shop-scraper] Timed out or browser closed, proceeding...");
220
+ console.error("[shop-scraper] Timed out or browser closed");
142
221
  }
143
222
 
144
223
  await browser.close().catch(() => {});
@@ -160,8 +239,14 @@ export async function getProductInfo(url, proxyUrl, options = {}) {
160
239
  let lastError;
161
240
  for (let attempt = 1; attempt <= 3; attempt++) {
162
241
  try {
242
+ const headers = { ...PDP_HEADERS };
243
+ const cookieStr = getCookieHeader();
244
+ if (cookieStr) {
245
+ headers["Cookie"] = cookieStr;
246
+ }
247
+
163
248
  const opts = {
164
- headers: PDP_HEADERS,
249
+ headers,
165
250
  ...(agent ? { dispatcher: agent } : {}),
166
251
  };
167
252
 
@@ -173,15 +258,18 @@ export async function getProductInfo(url, proxyUrl, options = {}) {
173
258
  } catch (err) {
174
259
  lastError = err;
175
260
 
176
- // Captcha: try browser fallback if enabled
261
+ // Captcha:清除过期 Cookie,弹浏览器重新获取
177
262
  if (
178
263
  err.message?.includes("Blocked by TikTok") &&
179
264
  options.captchaFallback
180
265
  ) {
181
- console.error("[shop-scraper] Captcha hit, opening browser...");
266
+ console.error("[shop-scraper] Captcha hit, clearing cached cookies...");
267
+ cachedCookies = null;
268
+ cookiesExpireAt = 0;
269
+ console.error("[shop-scraper] Opening browser for manual captcha...");
182
270
  await resolveCaptchaWithBrowser(url, proxy);
183
- // Retry HTTP after captcha solved
184
- attempt = 0; // reset counter for fresh retries
271
+ // 重试(带着新 Cookie)
272
+ attempt = 0;
185
273
  continue;
186
274
  }
187
275
 
@@ -60,7 +60,10 @@ async function processExplore(page, username, options, log) {
60
60
 
61
61
  try {
62
62
  log(` 访问 @${username} 主页...`);
63
- const videoList = await collectVideos(page, username, maxVideos, log);
63
+ const videoList =
64
+ options.enableVideos !== false
65
+ ? await collectVideos(page, username, maxVideos, log)
66
+ : null;
64
67
 
65
68
  log(" 获取用户信息...");
66
69
  const info = await getUserInfo(page);
@@ -130,19 +133,22 @@ async function processExplore(page, username, options, log) {
130
133
  seoUrl: topVideo.seoUrl || null,
131
134
  };
132
135
 
133
- // 如果有商品链接,抓取商品详情页(评分、销量等)
134
- if (topVideo.seoUrl && options.shopPage && !options.shopPage.isClosed()) {
136
+ // 如果有商品链接,通过 WebService API 抓取商品详情页(评分、销量等)
137
+ if (topVideo.seoUrl && options.webserverUrl) {
135
138
  try {
136
- const { getProductInfoCDP } = await import("../lib/shop-scraper.mjs");
137
- const shopUrl = topVideo.seoUrl.replace(
138
- "www.tiktok.com/shop/pdp",
139
- "shop.tiktok.com/us/pdp",
140
- );
141
-
142
- const productDetail = await getProductInfoCDP(
143
- options.shopPage,
144
- shopUrl,
139
+ const resp = await fetch(
140
+ `${options.webserverUrl}/api/tiktok/product`,
141
+ {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify({ url: topVideo.seoUrl }),
145
+ },
145
146
  );
147
+ if (!resp.ok) {
148
+ const err = await resp.json().catch(() => ({}));
149
+ throw new Error(err.error || `HTTP ${resp.status}`);
150
+ }
151
+ const productDetail = await resp.json();
146
152
  result.productDetail = productDetail;
147
153
  log(
148
154
  ` 商品详情: 评分${productDetail.rating ?? "-"} | 销量${productDetail.soldCount ?? "-"} | 评论${productDetail.reviewCount ?? "-"}`,
@@ -1,6 +1,7 @@
1
1
  import http from "http";
2
2
  import os from "os";
3
3
  import { TikTokScraper } from "../lib/tiktok-scraper.mjs";
4
+ import { getProductInfo } from "../lib/shop-scraper.mjs";
4
5
 
5
6
  // ---- 工具函数 ----
6
7
 
@@ -61,6 +62,7 @@ const SCRAPER_CONFIG = {
61
62
  wafTTL: 120000,
62
63
  maxRequestsPerPage: 50,
63
64
  proxyServer: null,
65
+ captchaNotifyUrl: null,
64
66
  };
65
67
 
66
68
  let scraperInstance = null;
@@ -144,6 +146,7 @@ const AVAILABLE_APIS = [
144
146
  "/api/tiktok/user",
145
147
  "/api/tiktok/video",
146
148
  "/api/tiktok/lookup",
149
+ "/api/tiktok/product",
147
150
  "/health",
148
151
  ];
149
152
 
@@ -192,6 +195,82 @@ async function handleTiktokLookup(req, res) {
192
195
  return sendJSON(res, 200, result);
193
196
  }
194
197
 
198
+ async function handleTiktokProduct(req, res) {
199
+ const body = await readBody(req);
200
+ if (!body.url) {
201
+ return sendJSON(res, 400, { error: "missing url" });
202
+ }
203
+ // URL 标准化:www.tiktok.com/shop/pdp → shop.tiktok.com/us/pdp
204
+ const normalizedUrl = body.url.replace(
205
+ "www.tiktok.com/shop/pdp",
206
+ "shop.tiktok.com/us/pdp",
207
+ );
208
+ try {
209
+ const result = await getProductInfo(
210
+ normalizedUrl,
211
+ SCRAPER_CONFIG.proxyServer,
212
+ {
213
+ captchaFallback: false,
214
+ },
215
+ );
216
+ return sendJSON(res, 200, result);
217
+ } catch (e) {
218
+ // 先返回错误,不阻塞请求
219
+ sendJSON(res, 502, { error: e.message });
220
+
221
+ // 如果是 captcha,异步弹浏览器(不阻塞其他请求)
222
+ if (e.message?.includes("Blocked by TikTok")) {
223
+ scheduleCaptchaRecovery(normalizedUrl, SCRAPER_CONFIG.proxyServer);
224
+ }
225
+ return;
226
+ }
227
+ }
228
+
229
+ // 防止重复弹浏览器
230
+ let captchaRecoveryRunning = false;
231
+
232
+ async function scheduleCaptchaRecovery(url, proxy) {
233
+ if (captchaRecoveryRunning) {
234
+ console.error("[webserver] Captcha recovery already in progress, skipping");
235
+ return;
236
+ }
237
+ captchaRecoveryRunning = true;
238
+
239
+ // 通知 Watch Server:这台机器出现验证码需要人工处理
240
+ if (SCRAPER_CONFIG.captchaNotifyUrl) {
241
+ try {
242
+ await fetch(`${SCRAPER_CONFIG.captchaNotifyUrl}/api/error-report`, {
243
+ method: "POST",
244
+ headers: { "Content-Type": "application/json" },
245
+ body: JSON.stringify({
246
+ userId: `webserver-${process.env.HOSTNAME || os.hostname() || "unknown"}`,
247
+ errorType: "captcha_blocked",
248
+ errorMessage:
249
+ "TikTok Shop captcha detected, browser opened - manual intervention required",
250
+ stage: "product_info",
251
+ errorStack: url,
252
+ }),
253
+ });
254
+ console.error("[webserver] Captcha alert sent to watch server");
255
+ } catch (e) {
256
+ console.error("[webserver] Failed to notify watch server:", e.message);
257
+ }
258
+ }
259
+
260
+ try {
261
+ const { getProductInfo } = await import("../lib/shop-scraper.mjs");
262
+ console.error(
263
+ "[webserver] Starting async captcha recovery, browser will open...",
264
+ );
265
+ await getProductInfo(url, proxy, { captchaFallback: true });
266
+ console.error("[webserver] Captcha recovery completed, cookies cached");
267
+ } catch (err) {
268
+ console.error("[webserver] Captcha recovery failed:", err.message);
269
+ } finally {
270
+ captchaRecoveryRunning = false;
271
+ }
272
+ }
273
+
195
274
  function handleHealth(req, res) {
196
275
  return sendJSON(res, 200, {
197
276
  status: "ok",
@@ -219,6 +298,7 @@ const ROUTES = [
219
298
  { method: "POST", path: "/api/tiktok/user", handler: handleTiktokUser },
220
299
  { method: "POST", path: "/api/tiktok/video", handler: handleTiktokVideo },
221
300
  { method: "POST", path: "/api/tiktok/lookup", handler: handleTiktokLookup },
301
+ { method: "POST", path: "/api/tiktok/product", handler: handleTiktokProduct },
222
302
  ];
223
303
 
224
304
  // ---- 服务器 ----
@@ -227,6 +307,7 @@ export async function startWebserver(
227
307
  port = 3000,
228
308
  poolSize = 0,
229
309
  proxyServer = null,
310
+ captchaNotifyUrl = null,
230
311
  ) {
231
312
  if (poolSize > 0) {
232
313
  SCRAPER_CONFIG.poolSize = poolSize;
@@ -234,6 +315,9 @@ export async function startWebserver(
234
315
  if (proxyServer) {
235
316
  SCRAPER_CONFIG.proxyServer = proxyServer;
236
317
  }
318
+ if (captchaNotifyUrl) {
319
+ SCRAPER_CONFIG.captchaNotifyUrl = captchaNotifyUrl;
320
+ }
237
321
  const server = http.createServer(async (req, res) => {
238
322
  const url = new URL(req.url, `http://127.0.0.1:${port}`);
239
323