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.
- package/README.md +17 -17
- package/cli.js +9 -9
- package/package.json +47 -47
- package/scripts/run-explore copy.bat +101 -68
- package/scripts/run-explore.bat +132 -97
- package/scripts/run-explore.ps1 +157 -107
- package/scripts/run-explore.sh +119 -98
- package/src/cli/attach.js +180 -180
- package/src/cli/auto.js +240 -240
- package/src/cli/config.js +152 -152
- package/src/cli/explore.js +488 -478
- package/src/cli/info.js +88 -88
- package/src/cli/open.js +111 -111
- package/src/cli/progress.js +111 -111
- package/src/cli/refresh.js +216 -216
- package/src/cli/scrape.js +47 -47
- package/src/cli/utils.js +18 -18
- package/src/cli/videos.js +41 -41
- package/src/cli/watch.js +31 -31
- package/src/lib/api-interceptor.js +44 -130
- package/src/lib/args.js +722 -718
- package/src/lib/browser/anti-detect.js +23 -23
- package/src/lib/browser/cdp.js +261 -261
- package/src/lib/browser/health-checker.js +114 -114
- package/src/lib/browser/launch.js +43 -43
- package/src/lib/browser/page.js +183 -183
- package/src/lib/constants.js +216 -213
- package/src/lib/delay.js +54 -54
- package/src/lib/explore-fetch.js +118 -118
- package/src/lib/fetcher.js +45 -45
- package/src/lib/filter.js +66 -66
- package/src/lib/io.js +54 -54
- package/src/lib/output.js +80 -80
- package/src/lib/page-error-detector.js +105 -105
- package/src/lib/parse-ssr.mjs +69 -69
- package/src/lib/parser.js +47 -47
- package/src/lib/retry.js +45 -45
- package/src/lib/scrape.js +89 -89
- package/src/lib/tiktok-scraper.mjs +194 -194
- package/src/lib/url.js +52 -52
- package/src/main.js +48 -48
- package/src/scraper/auto-core.js +203 -203
- package/src/scraper/core.js +211 -211
- package/src/scraper/explore-core.js +167 -167
- package/src/scraper/modules/captcha-handler.js +114 -114
- package/src/scraper/modules/follow-extractor.js +194 -118
- package/src/scraper/modules/guess-extractor.js +51 -51
- package/src/scraper/modules/page-helpers.js +48 -48
- package/src/scraper/refresh-core.js +179 -179
- package/src/videos/core.js +125 -125
- package/src/watch/data-store.js +1030 -855
- package/src/watch/public/index.html +753 -753
- package/src/watch/server.js +933 -734
- package/scripts/test-captcha-lib.mjs +0 -68
- package/scripts/test-captcha.mjs +0 -81
- package/scripts/test-incognito-lib.mjs +0 -36
- package/scripts/test-login-state.mjs +0 -128
- package/scripts/test-safe-click.mjs +0 -45
- 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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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/")
|
|
90
|
-
|
|
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(
|
|
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:
|
|
94
|
+
{ timeout: 30000 },
|
|
107
95
|
);
|
|
108
96
|
|
|
109
|
-
|
|
110
|
-
const elapsed = Date.now() - t0;
|
|
111
|
-
return { data, apiRequestUrl, elapsed };
|
|
97
|
+
data = await response.json();
|
|
112
98
|
} catch (e) {
|
|
113
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
124
|
+
if (pageError === "service_error") {
|
|
125
|
+
throw new Error(`被封: ${username}`);
|
|
126
|
+
}
|
|
210
127
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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 };
|