tt-help-cli-ycl 1.4.39 → 1.4.40

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.39",
3
+ "version": "1.4.40",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -66,6 +66,10 @@ const SCRAPER_CONFIG = {
66
66
  let scraperInstance = null;
67
67
  let scraperClosing = false;
68
68
  let scraperInitPromise = null;
69
+ let consecutiveErrors = 0;
70
+ const MAX_CONSECUTIVE_ERRORS = 10;
71
+ let warmTimer = null;
72
+ let lastWarmTime = 0;
69
73
 
70
74
  async function getScraper() {
71
75
  if (scraperInstance && !scraperClosing) {
@@ -82,6 +86,8 @@ async function getScraper() {
82
86
  const scraper = new TikTokScraper(SCRAPER_CONFIG);
83
87
  await scraper.init();
84
88
  scraperInstance = scraper;
89
+ consecutiveErrors = 0;
90
+ lastWarmTime = Date.now();
85
91
  return scraper;
86
92
  })();
87
93
  try {
@@ -91,6 +97,47 @@ async function getScraper() {
91
97
  }
92
98
  }
93
99
 
100
+ async function restartScraper(reason) {
101
+ console.error(`[webserver] 重启 scraper: ${reason}`);
102
+ if (scraperInstance) {
103
+ try {
104
+ await scraperInstance.close();
105
+ } catch {}
106
+ scraperInstance = null;
107
+ }
108
+ scraperInitPromise = null;
109
+ consecutiveErrors = 0;
110
+ // 下次请求时懒初始化
111
+ }
112
+
113
+ async function warmScraper() {
114
+ if (scraperClosing || !scraperInstance) return;
115
+ const now = Date.now();
116
+ if (now - lastWarmTime < SCRAPER_CONFIG.wafTTL) return;
117
+ try {
118
+ await scraperInstance.warmWaf();
119
+ lastWarmTime = Date.now();
120
+ } catch (e) {
121
+ console.error(`[webserver] warmWaf 失败: ${e.message}`);
122
+ }
123
+ }
124
+
125
+ function startWarmTimer() {
126
+ if (warmTimer) return;
127
+ // 每 wafTTL/2 检查一次是否需要预热
128
+ warmTimer = setInterval(() => {
129
+ warmScraper().catch(() => {});
130
+ }, SCRAPER_CONFIG.wafTTL / 2);
131
+ warmTimer.unref?.();
132
+ }
133
+
134
+ function stopWarmTimer() {
135
+ if (warmTimer) {
136
+ clearInterval(warmTimer);
137
+ warmTimer = null;
138
+ }
139
+ }
140
+
94
141
  // ---- API 处理器 ----
95
142
 
96
143
  const AVAILABLE_APIS = [
@@ -149,6 +196,10 @@ function handleHealth(req, res) {
149
196
  return sendJSON(res, 200, {
150
197
  status: "ok",
151
198
  scraperReady: scraperInstance !== null && !scraperClosing,
199
+ consecutiveErrors,
200
+ lastWarmSec: lastWarmTime
201
+ ? Math.round((Date.now() - lastWarmTime) / 1000)
202
+ : null,
152
203
  uptime: process.uptime(),
153
204
  });
154
205
  }
@@ -208,6 +259,11 @@ export async function startWebserver(
208
259
  try {
209
260
  return await route.handler(req, res);
210
261
  } catch (e) {
262
+ // 自动恢复:连续错误超过阈值时重启 scraper
263
+ consecutiveErrors++;
264
+ if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
265
+ await restartScraper(`连续 ${consecutiveErrors} 次错误`);
266
+ }
211
267
  if (!res.headersSent) {
212
268
  return sendJSON(res, 500, { error: e.message });
213
269
  }
@@ -242,6 +298,9 @@ export async function startWebserver(
242
298
  });
243
299
  server.once("error", reject);
244
300
  });
301
+
302
+ // 启动定期 WAF 预热
303
+ startWarmTimer();
245
304
  }
246
305
 
247
306
  export async function closeServer(server) {
@@ -255,6 +314,7 @@ export async function closeServer(server) {
255
314
  }
256
315
 
257
316
  export async function closeScraper() {
317
+ stopWarmTimer();
258
318
  if (scraperInstance && !scraperClosing) {
259
319
  scraperClosing = true;
260
320
  await scraperInstance.close();