tt-help-cli-ycl 1.3.33 → 1.3.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +17 -17
  2. package/cli.js +9 -9
  3. package/package.json +47 -47
  4. package/scripts/run-explore copy.bat +101 -101
  5. package/scripts/run-explore.bat +132 -127
  6. package/scripts/run-explore.ps1 +157 -153
  7. package/scripts/run-explore.sh +119 -113
  8. package/src/cli/attach.js +180 -180
  9. package/src/cli/auto.js +240 -240
  10. package/src/cli/config.js +152 -152
  11. package/src/cli/explore.js +488 -481
  12. package/src/cli/info.js +88 -88
  13. package/src/cli/open.js +111 -111
  14. package/src/cli/progress.js +111 -111
  15. package/src/cli/refresh.js +216 -216
  16. package/src/cli/scrape.js +47 -47
  17. package/src/cli/utils.js +18 -18
  18. package/src/cli/videos.js +41 -41
  19. package/src/cli/watch.js +31 -31
  20. package/src/lib/api-interceptor.js +43 -202
  21. package/src/lib/args.js +722 -722
  22. package/src/lib/browser/anti-detect.js +23 -23
  23. package/src/lib/browser/cdp.js +261 -261
  24. package/src/lib/browser/health-checker.js +114 -114
  25. package/src/lib/browser/launch.js +43 -43
  26. package/src/lib/browser/page.js +183 -183
  27. package/src/lib/constants.js +216 -213
  28. package/src/lib/delay.js +54 -54
  29. package/src/lib/explore-fetch.js +118 -118
  30. package/src/lib/fetcher.js +45 -45
  31. package/src/lib/filter.js +66 -66
  32. package/src/lib/io.js +54 -54
  33. package/src/lib/output.js +80 -80
  34. package/src/lib/page-error-detector.js +105 -105
  35. package/src/lib/parse-ssr.mjs +69 -69
  36. package/src/lib/parser.js +47 -47
  37. package/src/lib/retry.js +45 -45
  38. package/src/lib/scrape.js +89 -89
  39. package/src/lib/tiktok-scraper.mjs +194 -194
  40. package/src/lib/url.js +52 -52
  41. package/src/main.js +48 -48
  42. package/src/scraper/auto-core.js +203 -203
  43. package/src/scraper/core.js +211 -211
  44. package/src/scraper/explore-core.js +167 -167
  45. package/src/scraper/modules/captcha-handler.js +114 -114
  46. package/src/scraper/modules/follow-extractor.js +194 -194
  47. package/src/scraper/modules/guess-extractor.js +51 -51
  48. package/src/scraper/modules/page-helpers.js +48 -48
  49. package/src/scraper/refresh-core.js +179 -179
  50. package/src/videos/core.js +125 -125
  51. package/src/watch/data-store.js +1030 -1030
  52. package/src/watch/public/index.html +753 -753
  53. package/src/watch/server.js +933 -933
  54. package/scripts/test-captcha-lib.mjs +0 -68
  55. package/scripts/test-captcha.mjs +0 -81
  56. package/scripts/test-incognito-lib.mjs +0 -36
  57. package/scripts/test-login-state.mjs +0 -128
  58. package/scripts/test-safe-click.mjs +0 -45
  59. package/src/results/user-videos-bar.lar.lar.moeta.json +0 -37
package/src/cli/watch.js CHANGED
@@ -1,31 +1,31 @@
1
- import { existsSync } from 'fs';
2
- import { createStore } from '../watch/data-store.js';
3
- import { startWatchServer, openBrowser } from '../watch/server.js';
4
-
5
- export async function handleWatch(options) {
6
- const { outputFile, watchPort } = options;
7
-
8
- if (!outputFile) {
9
- console.error('用法: tt-help watch -o <数据文件> [-p 端口]');
10
- console.error('示例: tt-help watch -o data.json');
11
- console.error(' tt-help watch -o data.json -p 8080');
12
- process.exit(1);
13
- }
14
-
15
- if (!existsSync(outputFile)) {
16
- console.error(`文件不存�? ${outputFile}`);
17
- process.exit(1);
18
- }
19
-
20
- const store = createStore(outputFile);
21
- const { server, port } = await startWatchServer(outputFile, watchPort, store);
22
- openBrowser(port);
23
-
24
- process.once('SIGINT', () => {
25
- store.stopBackup();
26
- server.close();
27
- process.exit(0);
28
- });
29
-
30
- console.error('�?Ctrl+C 停止监控服务');
31
- }
1
+ import { existsSync } from 'fs';
2
+ import { createStore } from '../watch/data-store.js';
3
+ import { startWatchServer, openBrowser } from '../watch/server.js';
4
+
5
+ export async function handleWatch(options) {
6
+ const { outputFile, watchPort } = options;
7
+
8
+ if (!outputFile) {
9
+ console.error('用法: tt-help watch -o <数据文件> [-p 端口]');
10
+ console.error('示例: tt-help watch -o data.json');
11
+ console.error(' tt-help watch -o data.json -p 8080');
12
+ process.exit(1);
13
+ }
14
+
15
+ if (!existsSync(outputFile)) {
16
+ console.error(`文件不存�? ${outputFile}`);
17
+ process.exit(1);
18
+ }
19
+
20
+ const store = createStore(outputFile);
21
+ const { server, port } = await startWatchServer(outputFile, watchPort, store);
22
+ openBrowser(port);
23
+
24
+ process.once('SIGINT', () => {
25
+ store.stopBackup();
26
+ server.close();
27
+ process.exit(0);
28
+ });
29
+
30
+ console.error('�?Ctrl+C 停止监控服务');
31
+ }
@@ -1,14 +1,10 @@
1
1
  import { delay } from "./delay.js";
2
- import { retryWithBackoff } from "./retry.js";
3
2
  import { assertPageUrl } from "./browser/page.js";
4
3
  import {
5
4
  detectPageError,
6
5
  detectPageErrorWithWait,
7
6
  } from "./page-error-detector.js";
8
7
 
9
- /**
10
- * 处理 API 响应数据:提取首页视频 + 翻页
11
- */
12
8
  async function processAPIResponse(
13
9
  data,
14
10
  username,
@@ -18,7 +14,6 @@ async function processAPIResponse(
18
14
  apiRequestUrl,
19
15
  log,
20
16
  ) {
21
- // 提取首页视频
22
17
  const firstPageItems = data.itemList || [];
23
18
  for (const item of firstPageItems) {
24
19
  if (items.length >= maxVideos) break;
@@ -26,7 +21,6 @@ async function processAPIResponse(
26
21
  items.push({ id: item.id, href });
27
22
  }
28
23
 
29
- // 翻页获取后续视频
30
24
  let cursor = data.cursor;
31
25
  let hasMore = data.hasMore;
32
26
 
@@ -61,7 +55,6 @@ async function processAPIResponse(
61
55
 
62
56
  log(` [API拦截] 共 ${items.length} 条视频`);
63
57
 
64
- // 转成 Map 返回,与 collectVideos 一致
65
58
  const videoMap = new Map();
66
59
  for (const v of items) {
67
60
  if (!videoMap.has(v.id)) videoMap.set(v.id, v);
@@ -70,232 +63,80 @@ async function processAPIResponse(
70
63
  return videoMap;
71
64
  }
72
65
 
73
- /**
74
- * 单次 API 拦截尝试:注册拦截器 导航 → 用 waitForResponse 精确等待 API
75
- * @param {number} apiTimeout - API 响应超时时间(ms),默认 15000
76
- * @returns {{data: object|null, apiRequestUrl: string|null, elapsed: number}}
77
- */
78
- async function tryInterceptAPI(page, username, log, apiTimeout = 15000) {
79
- const url = `https://www.tiktok.com/@${username}`;
66
+ async function fetchUserVideosAPI(page, username, maxVideos, log) {
67
+ log(` [API拦截] 获取 @${username} 视频 ...`);
68
+
80
69
  let apiRequestUrl = null;
81
70
  let sawApiRequest = false;
82
- let sawApiResponse = false;
83
-
84
- const responseHandler = async (response) => {
85
- if (response.url().includes("/api/post/item_list/")) {
86
- sawApiResponse = true;
87
- }
88
- };
89
71
 
90
72
  const requestHandler = (request) => {
91
- if (request.url().includes("/api/post/item_list/") && !apiRequestUrl) {
92
- sawApiRequest = true;
93
- apiRequestUrl = request.url();
94
- } else if (request.url().includes("/api/post/item_list/")) {
73
+ if (request.url().includes("/api/post/item_list/")) {
95
74
  sawApiRequest = true;
75
+ if (!apiRequestUrl) apiRequestUrl = request.url();
96
76
  }
97
77
  };
98
78
 
99
- page.on("response", responseHandler);
100
79
  page.on("request", requestHandler);
101
80
 
102
81
  const t0 = Date.now();
82
+ let data = null;
83
+ let interceptionError = null;
103
84
 
104
85
  try {
105
- await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
106
- await assertPageUrl(page, `@${username}`);
107
-
108
- // 用 waitForResponse 精确等待 API 响应(超时时间可配置)
109
- const response = await page.waitForResponse(
110
- (res) => res.url().includes("/api/post/item_list/"),
111
- { timeout: apiTimeout },
86
+ await page.goto(
87
+ `https://www.tiktok.com/@${username}`,
88
+ { waitUntil: "domcontentloaded", timeout: 30000 },
112
89
  );
90
+ await assertPageUrl(page, `@${username}`);
113
91
 
114
- sawApiResponse = true;
115
- const data = await response.json();
116
- const elapsed = Date.now() - t0;
117
- return { data, apiRequestUrl, elapsed, sawApiRequest, sawApiResponse };
118
- } catch (e) {
119
- const elapsed = Date.now() - t0;
120
- return {
121
- data: null,
122
- apiRequestUrl,
123
- elapsed,
124
- error: e.message,
125
- sawApiRequest,
126
- sawApiResponse,
127
- };
128
- } finally {
129
- page.off("response", responseHandler);
130
- page.off("request", requestHandler);
131
- }
132
- }
133
-
134
- /**
135
- * 软重试:不重新 goto,直接在当前页继续等待 item_list API。
136
- */
137
- async function tryWaitCurrentPageAPI(page, apiTimeout = 12000) {
138
- let apiRequestUrl = null;
139
- let sawApiRequest = false;
140
- let sawApiResponse = false;
141
-
142
- const requestHandler = (request) => {
143
- if (request.url().includes("/api/post/item_list/") && !apiRequestUrl) {
144
- sawApiRequest = true;
145
- apiRequestUrl = request.url();
146
- } else if (request.url().includes("/api/post/item_list/")) {
147
- sawApiRequest = true;
148
- }
149
- };
150
-
151
- const responseHandler = (response) => {
152
- if (response.url().includes("/api/post/item_list/")) {
153
- sawApiResponse = true;
154
- }
155
- };
156
-
157
- const t0 = Date.now();
158
- page.on("request", requestHandler);
159
- page.on("response", responseHandler);
160
-
161
- try {
162
92
  const response = await page.waitForResponse(
163
93
  (res) => res.url().includes("/api/post/item_list/"),
164
- { timeout: apiTimeout },
94
+ { timeout: 30000 },
165
95
  );
166
96
 
167
- sawApiResponse = true;
168
- const data = await response.json();
169
- const elapsed = Date.now() - t0;
170
- return { data, apiRequestUrl, elapsed, sawApiRequest, sawApiResponse };
97
+ data = await response.json();
171
98
  } catch (e) {
172
- const elapsed = Date.now() - t0;
173
- return {
174
- data: null,
175
- apiRequestUrl,
176
- elapsed,
177
- error: e.message,
178
- sawApiRequest,
179
- sawApiResponse,
180
- };
99
+ interceptionError = e.message;
181
100
  } finally {
182
101
  page.off("request", requestHandler);
183
- page.off("response", responseHandler);
184
102
  }
185
- }
186
-
187
- /**
188
- * 通过拦截 TikTok 内部 API 获取用户视频列表
189
- * 比 DOM 滚动解析快 5-10 倍
190
- *
191
- * 弱网优化:
192
- * - 第 1 轮:正常 goto,等待 15s
193
- * - 第 1.5 轮:不重新 goto,留在当前页继续等待 12s
194
- * - 第 2 轮:重新 goto,等待 30s
195
- * - 使用 waitForResponse 精确等待 API 响应(而非固定超时)
196
- *
197
- * @param {import('playwright').Page} page - Playwright page (CDP 连接)
198
- * @param {string} username - TikTok 用户名
199
- * @param {number} maxVideos - 最大视频数
200
- * @param {Function} log - 日志函数
201
- * @returns {Map<string, {id: string, href: string}>} 与 collectVideos 返回格式一致
202
- * @throws {Error} API 超时或网络异常时抛出
203
- */
204
- async function fetchUserVideosAPI(page, username, maxVideos, log) {
205
- const items = [];
206
- const noRetryErrors = new Set([
207
- "service_error",
208
- "not_found",
209
- "region_blocked",
210
- "login_required",
211
- ]);
212
103
 
213
- const attempts = [
214
- {
215
- label: ` [API拦截] 获取 @${username} 视频 ...`,
216
- mode: "goto",
217
- apiTimeout: 15000,
218
- },
219
- {
220
- label: " [API拦截] 首轮未命中,当前页继续补等 12s ...",
221
- mode: "wait",
222
- apiTimeout: 12000,
223
- },
224
- {
225
- label: " [API拦截] 重新加载主页后重试 (超时 30s)",
226
- mode: "goto",
227
- apiTimeout: 30000,
228
- },
229
- ];
104
+ const elapsed = Date.now() - t0;
230
105
 
231
- for (let attempt = 0; attempt < attempts.length; attempt++) {
232
- const currentAttempt = attempts[attempt];
233
- log(currentAttempt.label);
234
-
235
- const {
106
+ if (data && Array.isArray(data.itemList) && data.itemList.length > 0) {
107
+ log(` [API拦截] 成功,耗时 ${elapsed}ms`);
108
+ return await processAPIResponse(
236
109
  data,
110
+ username,
111
+ maxVideos,
112
+ [],
113
+ page,
237
114
  apiRequestUrl,
238
- elapsed,
239
- error,
240
- sawApiRequest,
241
- sawApiResponse,
242
- } =
243
- currentAttempt.mode === "goto"
244
- ? await tryInterceptAPI(page, username, log, currentAttempt.apiTimeout)
245
- : await tryWaitCurrentPageAPI(page, currentAttempt.apiTimeout);
246
-
247
- if (data && Array.isArray(data.itemList) && data.itemList.length > 0) {
248
- // 成功拦截且拿到了视频,处理数据
249
- return await processAPIResponse(
250
- data,
251
- username,
252
- maxVideos,
253
- items,
254
- page,
255
- apiRequestUrl,
256
- log,
257
- );
258
- }
259
-
260
- // API 已返回但没有视频时,优先判定是否是确定性的页面错误。
261
- let pageError = await detectPageError(page);
262
-
263
- // service_error 经常比 API 判定晚一点渲染,补一个短等待避免漏判“被封”。
264
- if (!pageError) {
265
- const delayedPageError = await detectPageErrorWithWait(page, 3000);
266
- if (delayedPageError) pageError = delayedPageError;
267
- }
268
-
269
- if (pageError === "service_error") {
270
- throw new Error(`被封: ${username}`);
271
- }
272
-
273
- if (data && Array.isArray(data.itemList) && data.itemList.length === 0) {
274
- if (noRetryErrors.has(pageError)) {
275
- return new Map();
276
- }
115
+ log,
116
+ );
117
+ }
277
118
 
278
- return new Map();
279
- }
119
+ let pageError = await detectPageError(page);
120
+ if (!pageError) {
121
+ pageError = await detectPageErrorWithWait(page, 5000);
122
+ }
280
123
 
281
- if (noRetryErrors.has(pageError)) {
282
- return new Map();
283
- }
124
+ if (pageError === "service_error") {
125
+ throw new Error(`被封: ${username}`);
126
+ }
284
127
 
285
- log(
286
- ` [API拦截] 未命中: request=${sawApiRequest ? "Y" : "N"}, response=${sawApiResponse ? "Y" : "N"}, elapsed=${elapsed}ms${error ? `, error=${error}` : ""}${apiRequestUrl ? ", apiUrl=Y" : ", apiUrl=N"}`,
287
- );
128
+ if (pageError) {
129
+ log(` [API拦截] 数据为空,识别到页面错误: ${pageError}`);
130
+ return new Map();
131
+ }
288
132
 
289
- // 最后一次重试也失败了
290
- if (attempt === attempts.length - 1) {
291
- if (!pageError) {
292
- throw new Error(
293
- `@${username} 页面异常(首轮 + 补等 + 重载后仍未拦截到 API)`,
294
- );
295
- }
296
- return new Map();
297
- }
133
+ if (interceptionError) {
134
+ log(` [API拦截] 超时/拦截失败: ${interceptionError},耗时 ${elapsed}ms`);
135
+ throw new Error(`@${username} 页面异常(API拦截超时,未识别到错误类型)`);
298
136
  }
137
+
138
+ log(` [API拦截] 数据为空,未识别到错误类型,当作无视频处理`);
139
+ return new Map();
299
140
  }
300
141
 
301
- export { fetchUserVideosAPI };
142
+ export { fetchUserVideosAPI };