tt-help-cli-ycl 1.4.29 → 1.4.31
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 +1 -1
- package/src/cli/comments.js +4 -6
- package/src/cli/search.js +7 -1
- package/src/lib/args.js +4 -1
- package/src/lib/constants.js +4 -2
- package/src/lib/search-collector.js +43 -29
package/package.json
CHANGED
package/src/cli/comments.js
CHANGED
|
@@ -736,15 +736,13 @@ async function runAutoMode(options) {
|
|
|
736
736
|
try {
|
|
737
737
|
const modals = await detectModals(session.page);
|
|
738
738
|
const loginModal = modals.find(
|
|
739
|
-
(m) =>
|
|
740
|
-
m.type === "login-modal" || m.type === "modal-container",
|
|
739
|
+
(m) => m.type === "login-modal" || m.type === "modal-container",
|
|
741
740
|
);
|
|
742
741
|
if (loginModal) {
|
|
743
742
|
console.error(` 🔔 检测到登录弹窗,正在关闭...`);
|
|
744
|
-
const { closed, failed } = await closeAllModals(
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
);
|
|
743
|
+
const { closed, failed } = await closeAllModals(session.page, {
|
|
744
|
+
waitMs: 300,
|
|
745
|
+
});
|
|
748
746
|
console.error(
|
|
749
747
|
` 🔔 弹窗关闭: closed=${JSON.stringify(closed)} failed=${JSON.stringify(failed)}`,
|
|
750
748
|
);
|
package/src/cli/search.js
CHANGED
|
@@ -71,6 +71,7 @@ export async function handleSearch(parsed) {
|
|
|
71
71
|
serverUrl,
|
|
72
72
|
basePort = 3333,
|
|
73
73
|
portCount = 10,
|
|
74
|
+
parallel = 1,
|
|
74
75
|
proxy: cliProxy,
|
|
75
76
|
} = searchOpts || {};
|
|
76
77
|
|
|
@@ -196,7 +197,9 @@ export async function handleSearch(parsed) {
|
|
|
196
197
|
try {
|
|
197
198
|
return await p.evaluate(async () => {
|
|
198
199
|
try {
|
|
199
|
-
const res = await fetch(
|
|
200
|
+
const res = await fetch(
|
|
201
|
+
"https://www.tiktok.com/passport/web/account/info/",
|
|
202
|
+
);
|
|
200
203
|
const data = await res.json();
|
|
201
204
|
return data?.data?.unique_id || null;
|
|
202
205
|
} catch {}
|
|
@@ -410,6 +413,7 @@ export async function handleSearch(parsed) {
|
|
|
410
413
|
let searchError = null;
|
|
411
414
|
try {
|
|
412
415
|
ids = await collectSearchAuthors(browser, tag, {
|
|
416
|
+
parallel,
|
|
413
417
|
log,
|
|
414
418
|
exclude: myUsername ? [myUsername] : [],
|
|
415
419
|
});
|
|
@@ -432,6 +436,7 @@ export async function handleSearch(parsed) {
|
|
|
432
436
|
// 重试搜索
|
|
433
437
|
try {
|
|
434
438
|
ids = await collectSearchAuthors(browser, tag, {
|
|
439
|
+
parallel,
|
|
435
440
|
log,
|
|
436
441
|
exclude: myUsername ? [myUsername] : [],
|
|
437
442
|
});
|
|
@@ -457,6 +462,7 @@ export async function handleSearch(parsed) {
|
|
|
457
462
|
await new Promise((r) => setTimeout(r, 5000));
|
|
458
463
|
try {
|
|
459
464
|
ids = await collectSearchAuthors(browser, tag, {
|
|
465
|
+
parallel,
|
|
460
466
|
log,
|
|
461
467
|
exclude: myUsername ? [myUsername] : [],
|
|
462
468
|
});
|
package/src/lib/args.js
CHANGED
|
@@ -560,6 +560,7 @@ function parseSearchArgs(args) {
|
|
|
560
560
|
let serverUrl = null;
|
|
561
561
|
let basePort = 3333;
|
|
562
562
|
let portCount = 10;
|
|
563
|
+
let parallel = 1;
|
|
563
564
|
let proxy = null;
|
|
564
565
|
|
|
565
566
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -570,6 +571,8 @@ function parseSearchArgs(args) {
|
|
|
570
571
|
basePort = parseInt(args[++i]) || 3333;
|
|
571
572
|
} else if (arg === "--port-count") {
|
|
572
573
|
portCount = parseInt(args[++i]) || 10;
|
|
574
|
+
} else if (arg === "-p" || arg === "--parallel") {
|
|
575
|
+
parallel = parseInt(args[++i]) || 1;
|
|
573
576
|
} else if (arg === "--proxy") {
|
|
574
577
|
proxy = args[++i];
|
|
575
578
|
}
|
|
@@ -577,7 +580,7 @@ function parseSearchArgs(args) {
|
|
|
577
580
|
|
|
578
581
|
return {
|
|
579
582
|
subcommand: "search",
|
|
580
|
-
search: { serverUrl, basePort, portCount, proxy },
|
|
583
|
+
search: { serverUrl, basePort, portCount, parallel, proxy },
|
|
581
584
|
urls: [],
|
|
582
585
|
outputFormat: "json",
|
|
583
586
|
exploreCount: 0,
|
package/src/lib/constants.js
CHANGED
|
@@ -316,19 +316,21 @@ const HELP_TEXT = [
|
|
|
316
316
|
"",
|
|
317
317
|
" search [选项]",
|
|
318
318
|
" 基于 productive 标签搜索 TikTok 作者(score>70 的标签作为搜索词)",
|
|
319
|
-
"
|
|
319
|
+
" 4 个搜索 Tab 采集(综合/视频/用户/照片),去重后提交到 job_base",
|
|
320
320
|
" 选项:",
|
|
321
321
|
" -s, --server <URL> 服务端地址(默认 http://127.0.0.1:3000)",
|
|
322
322
|
" --base-port <端口号> 起始 CDP 端口,默认 3333",
|
|
323
323
|
" --port-count <数量> 端口数量(账户数),默认 10",
|
|
324
|
+
" -p, --parallel <N> 并行搜索数,默认 1(串行)",
|
|
324
325
|
" --proxy <地址> 浏览器代理",
|
|
325
326
|
" 行为:",
|
|
326
327
|
" - 从 tags 表取 score>70 的 productive 标签",
|
|
327
328
|
" - 去掉 # 号后作为搜索关键词",
|
|
328
|
-
" -
|
|
329
|
+
" - 搜索 4 个 tab,去重后提交到 job_base(来源=search)",
|
|
329
330
|
" - 猜测国家取标签 countries 数组的第一个",
|
|
330
331
|
" 示例: tt-help search",
|
|
331
332
|
" tt-help search -s http://127.0.0.1:3001",
|
|
333
|
+
" tt-help search -p 2 -s http://127.0.0.1:3001",
|
|
332
334
|
" tt-help search --base-port 3333 -s http://127.0.0.1:3001",
|
|
333
335
|
" tt-help search --proxy http://127.0.0.1:7890",
|
|
334
336
|
"",
|
|
@@ -14,7 +14,7 @@ const TAB_URLS = {
|
|
|
14
14
|
照片: (q) => `https://www.tiktok.com/search/photo?q=${encodeURIComponent(q)}`,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
const DEFAULT_TABS = ["综合", "视频", "用户", "
|
|
17
|
+
const DEFAULT_TABS = ["综合", "视频", "用户", "照片"];
|
|
18
18
|
|
|
19
19
|
const SCROLL = {
|
|
20
20
|
maxRounds: 60,
|
|
@@ -58,6 +58,17 @@ async function scrollPage(page) {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
async function waitForSearchResults(page, timeoutMs = 20000) {
|
|
62
|
+
try {
|
|
63
|
+
await page.waitForSelector('[class*="DivItemContainer"]', {
|
|
64
|
+
timeout: timeoutMs,
|
|
65
|
+
});
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
async function processOneTab(page, tabName, keyword, allIds, excludeSet, log) {
|
|
62
73
|
const urlFn = TAB_URLS[tabName];
|
|
63
74
|
if (!urlFn) {
|
|
@@ -68,7 +79,7 @@ async function processOneTab(page, tabName, keyword, allIds, excludeSet, log) {
|
|
|
68
79
|
const tag = `[${tabName}]`;
|
|
69
80
|
const url = urlFn(keyword);
|
|
70
81
|
|
|
71
|
-
//
|
|
82
|
+
// 首屏加载
|
|
72
83
|
log(`${tag} 加载...`);
|
|
73
84
|
try {
|
|
74
85
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
@@ -76,7 +87,12 @@ async function processOneTab(page, tabName, keyword, allIds, excludeSet, log) {
|
|
|
76
87
|
log(`${tag} 导航失败: ${e.message}`);
|
|
77
88
|
return;
|
|
78
89
|
}
|
|
79
|
-
|
|
90
|
+
|
|
91
|
+
// 等待搜索结果实际加载(最长 20 秒)
|
|
92
|
+
const ready = await waitForSearchResults(page, 20000);
|
|
93
|
+
if (!ready) {
|
|
94
|
+
log(`${tag} 等待超时,继续采集`);
|
|
95
|
+
}
|
|
80
96
|
|
|
81
97
|
// 首屏渲染完成后屏蔽图片/字体/媒体,加速后续滚动
|
|
82
98
|
await page.route("**/*", (route) => {
|
|
@@ -143,43 +159,41 @@ async function processOneTab(page, tabName, keyword, allIds, excludeSet, log) {
|
|
|
143
159
|
*/
|
|
144
160
|
export async function collectSearchAuthors(browser, keyword, opts = {}) {
|
|
145
161
|
const tabs = opts.tabs || DEFAULT_TABS;
|
|
162
|
+
const parallel = Math.max(1, opts.parallel || 1);
|
|
146
163
|
const log = opts.log || ((...args) => console.error(...args));
|
|
147
164
|
const excludeSet = new Set(opts.exclude || []);
|
|
148
165
|
|
|
149
|
-
log(`[collect] "${keyword}" Tab: [${tabs.join(", ")}]`);
|
|
166
|
+
log(`[collect] "${keyword}" Tab: [${tabs.join(", ")}] 并行: ${parallel}`);
|
|
150
167
|
if (excludeSet.size > 0)
|
|
151
168
|
log(`[collect] 排除: [${[...excludeSet].join(", ")}]`);
|
|
152
169
|
|
|
153
170
|
const context = browser.contexts()[0];
|
|
154
|
-
const
|
|
171
|
+
const mainPage = context.pages()[0];
|
|
155
172
|
const allIds = new Set();
|
|
156
173
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
174
|
+
// 分批并行:每批 parallel 个 tab,批内并行、批间串行
|
|
175
|
+
for (let i = 0; i < tabs.length; i += parallel) {
|
|
176
|
+
const batch = tabs.slice(i, i + parallel);
|
|
177
|
+
const batchPages = [];
|
|
178
|
+
|
|
179
|
+
const tasks = batch.map(async (tabName, j) => {
|
|
180
|
+
let page = null;
|
|
181
|
+
try {
|
|
182
|
+
page = j === 0 ? mainPage : await context.newPage();
|
|
183
|
+
if (j > 0) batchPages.push(page);
|
|
184
|
+
await processOneTab(page, tabName, keyword, allIds, excludeSet, log);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
log(`[${tabName}] 异常: ${e.message}`);
|
|
187
|
+
} finally {
|
|
188
|
+
if (j > 0 && page) {
|
|
189
|
+
try {
|
|
190
|
+
await page.close();
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
172
193
|
}
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
await Promise.all(tasks);
|
|
194
|
+
});
|
|
177
195
|
|
|
178
|
-
|
|
179
|
-
for (const p of newPages) {
|
|
180
|
-
try {
|
|
181
|
-
if (!p.isClosed()) await p.close();
|
|
182
|
-
} catch {}
|
|
196
|
+
await Promise.all(tasks);
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
return [...allIds].sort();
|