tt-help-cli-ycl 1.3.32 → 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 -68
  5. package/scripts/run-explore.bat +132 -97
  6. package/scripts/run-explore.ps1 +157 -107
  7. package/scripts/run-explore.sh +119 -98
  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 -478
  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 +44 -130
  21. package/src/lib/args.js +722 -718
  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 -118
  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 -855
  52. package/src/watch/public/index.html +753 -753
  53. package/src/watch/server.js +933 -734
  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,159 +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}`;
80
- let apiRequestUrl = null;
66
+ async function fetchUserVideosAPI(page, username, maxVideos, log) {
67
+ log(` [API拦截] 获取 @${username} 视频 ...`);
81
68
 
82
- const responseHandler = async (response) => {
83
- if (response.url().includes("/api/post/item_list/")) {
84
- // 只记录 URL,数据通过 waitForResponse 获取
85
- }
86
- };
69
+ let apiRequestUrl = null;
70
+ let sawApiRequest = false;
87
71
 
88
72
  const requestHandler = (request) => {
89
- if (request.url().includes("/api/post/item_list/") && !apiRequestUrl) {
90
- apiRequestUrl = request.url();
73
+ if (request.url().includes("/api/post/item_list/")) {
74
+ sawApiRequest = true;
75
+ if (!apiRequestUrl) apiRequestUrl = request.url();
91
76
  }
92
77
  };
93
78
 
94
- page.on("response", responseHandler);
95
79
  page.on("request", requestHandler);
96
80
 
97
81
  const t0 = Date.now();
82
+ let data = null;
83
+ let interceptionError = null;
98
84
 
99
85
  try {
100
- await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
86
+ await page.goto(
87
+ `https://www.tiktok.com/@${username}`,
88
+ { waitUntil: "domcontentloaded", timeout: 30000 },
89
+ );
101
90
  await assertPageUrl(page, `@${username}`);
102
91
 
103
- // 用 waitForResponse 精确等待 API 响应(超时时间可配置)
104
92
  const response = await page.waitForResponse(
105
93
  (res) => res.url().includes("/api/post/item_list/"),
106
- { timeout: apiTimeout },
94
+ { timeout: 30000 },
107
95
  );
108
96
 
109
- const data = await response.json();
110
- const elapsed = Date.now() - t0;
111
- return { data, apiRequestUrl, elapsed };
97
+ data = await response.json();
112
98
  } catch (e) {
113
- const elapsed = Date.now() - t0;
114
- return { data: null, apiRequestUrl, elapsed, error: e.message };
99
+ interceptionError = e.message;
115
100
  } finally {
116
- page.off("response", responseHandler);
117
101
  page.off("request", requestHandler);
118
102
  }
119
- }
120
103
 
121
- /**
122
- * 通过拦截 TikTok 内部 API 获取用户视频列表
123
- * 比 DOM 滚动解析快 5-10 倍
124
- *
125
- * 弱网优化:
126
- * - 使用 waitForResponse 精确等待 API 响应(而非固定超时)
127
- * - 最多重试 3 次,递增等待 + 递增超时:
128
- * - 第 1 次:等待 0s, 超时 15s → 正常尝试
129
- * - 第 2 次:等待 3s, 超时 20s → 短暂波动恢复
130
- * - 第 3 次:等待 10s, 超时 30s → 网络缓慢给更多时间
131
- * - 每次重试前清理拦截器
132
- *
133
- * @param {import('playwright').Page} page - Playwright page (CDP 连接)
134
- * @param {string} username - TikTok 用户名
135
- * @param {number} maxVideos - 最大视频数
136
- * @param {Function} log - 日志函数
137
- * @returns {Map<string, {id: string, href: string}>} 与 collectVideos 返回格式一致
138
- * @throws {Error} API 超时或网络异常时抛出
139
- */
140
- async function fetchUserVideosAPI(page, username, maxVideos, log) {
141
- const items = [];
142
- const maxRetries = 3;
143
- // 重试策略:[等待时间(ms), API超时(ms)]
144
- const retryStrategy = [
145
- [0, 15000], // 第 1 次:不等待,15s 超时
146
- [3000, 20000], // 第 2 次:等 3s,20s 超时(短暂波动)
147
- [10000, 30000], // 第 3 次:等 10s,30s 超时(网络缓慢)
148
- ];
149
- const noRetryErrors = new Set([
150
- "service_error",
151
- "not_found",
152
- "region_blocked",
153
- "login_required",
154
- ]);
155
-
156
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
157
- const [, apiTimeout] = retryStrategy[attempt - 1];
158
- if (attempt > 1) {
159
- log(
160
- ` [API拦截] 重试 ${attempt}/${maxRetries} (超时 ${apiTimeout / 1000}s)`,
161
- );
162
- } else {
163
- log(` [API拦截] 获取 @${username} 视频 ...`);
164
- }
104
+ const elapsed = Date.now() - t0;
165
105
 
166
- const { data, apiRequestUrl, elapsed, error } = await tryInterceptAPI(
167
- page,
106
+ if (data && Array.isArray(data.itemList) && data.itemList.length > 0) {
107
+ log(` [API拦截] 成功,耗时 ${elapsed}ms`);
108
+ return await processAPIResponse(
109
+ data,
168
110
  username,
111
+ maxVideos,
112
+ [],
113
+ page,
114
+ apiRequestUrl,
169
115
  log,
170
- apiTimeout,
171
116
  );
117
+ }
172
118
 
173
- if (data && Array.isArray(data.itemList) && data.itemList.length > 0) {
174
- // 成功拦截且拿到了视频,处理数据
175
- return await processAPIResponse(
176
- data,
177
- username,
178
- maxVideos,
179
- items,
180
- page,
181
- apiRequestUrl,
182
- log,
183
- );
184
- }
185
-
186
- // API 已返回但没有视频时,优先判定是否是确定性的页面错误。
187
- let pageError = await detectPageError(page);
188
-
189
- // service_error 经常比 API 判定晚一点渲染,补一个短等待避免漏判“被封”。
190
- if (!pageError) {
191
- const delayedPageError = await detectPageErrorWithWait(page, 3000);
192
- if (delayedPageError) pageError = delayedPageError;
193
- }
194
-
195
- if (pageError === "service_error") {
196
- throw new Error(`被封: ${username}`);
197
- }
198
-
199
- if (data && Array.isArray(data.itemList) && data.itemList.length === 0) {
200
- if (noRetryErrors.has(pageError)) {
201
- return new Map();
202
- }
203
-
204
- return new Map();
205
- }
119
+ let pageError = await detectPageError(page);
120
+ if (!pageError) {
121
+ pageError = await detectPageErrorWithWait(page, 5000);
122
+ }
206
123
 
207
- if (noRetryErrors.has(pageError)) {
208
- return new Map();
209
- }
124
+ if (pageError === "service_error") {
125
+ throw new Error(`被封: ${username}`);
126
+ }
210
127
 
211
- // 最后一次重试也失败了
212
- if (attempt === maxRetries) {
213
- if (!pageError) {
214
- throw new Error(
215
- `@${username} 页面异常(${maxRetries} 次重试均未拦截到 API)`,
216
- );
217
- }
218
- return new Map();
219
- }
128
+ if (pageError) {
129
+ log(` [API拦截] 数据为空,识别到页面错误: ${pageError}`);
130
+ return new Map();
131
+ }
220
132
 
221
- // 还有重试次数,等待后重试
222
- const [waitTime] = retryStrategy[attempt - 1];
223
- log(` [API拦截] 失败,${waitTime / 1000}s 后重试...`);
224
- await new Promise((r) => setTimeout(r, waitTime));
133
+ if (interceptionError) {
134
+ log(` [API拦截] 超时/拦截失败: ${interceptionError},耗时 ${elapsed}ms`);
135
+ throw new Error(`@${username} 页面异常(API拦截超时,未识别到错误类型)`);
225
136
  }
137
+
138
+ log(` [API拦截] 数据为空,未识别到错误类型,当作无视频处理`);
139
+ return new Map();
226
140
  }
227
141
 
228
- export { fetchUserVideosAPI };
142
+ export { fetchUserVideosAPI };