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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.29",
3
+ "version": "1.4.31",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- session.page,
746
- { waitMs: 300 },
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("https://www.tiktok.com/passport/web/account/info/");
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,
@@ -316,19 +316,21 @@ const HELP_TEXT = [
316
316
  "",
317
317
  " search [选项]",
318
318
  " 基于 productive 标签搜索 TikTok 作者(score>70 的标签作为搜索词)",
319
- " 5 个搜索 Tab 并行采集(综合/视频/用户/直播/照片),去重后提交到 job_base",
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
- " - 并行搜索 5 个 tab,去重后提交到 job_base(来源=search)",
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
- await delay(4000);
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 existingPage = context.pages()[0];
171
+ const mainPage = context.pages()[0];
155
172
  const allIds = new Set();
156
173
 
157
- // 并行:每个 tab 独立页面同时跑
158
- const newPages = []; // 追踪所有新建页面,确保清理
159
- const tasks = tabs.map(async (tabName, i) => {
160
- let page = null;
161
- try {
162
- page = i === 0 ? existingPage : await context.newPage();
163
- if (i > 0) newPages.push(page);
164
- await processOneTab(page, tabName, keyword, allIds, excludeSet, log);
165
- } catch (e) {
166
- log(`[${tabName}] 异常: ${e.message}`);
167
- } finally {
168
- if (i > 0 && page) {
169
- try {
170
- await page.close();
171
- } catch {}
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();