tt-help-cli-ycl 1.4.27 → 1.4.29

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/src/lib/args.js CHANGED
@@ -556,6 +556,41 @@ function parseRefreshArgs(args) {
556
556
  };
557
557
  }
558
558
 
559
+ function parseSearchArgs(args) {
560
+ let serverUrl = null;
561
+ let basePort = 3333;
562
+ let portCount = 10;
563
+ let proxy = null;
564
+
565
+ for (let i = 0; i < args.length; i++) {
566
+ const arg = args[i];
567
+ if (arg === "-s" || arg === "--server") {
568
+ serverUrl = args[++i];
569
+ } else if (arg === "--base-port") {
570
+ basePort = parseInt(args[++i]) || 3333;
571
+ } else if (arg === "--port-count") {
572
+ portCount = parseInt(args[++i]) || 10;
573
+ } else if (arg === "--proxy") {
574
+ proxy = args[++i];
575
+ }
576
+ }
577
+
578
+ return {
579
+ subcommand: "search",
580
+ search: { serverUrl, basePort, portCount, proxy },
581
+ urls: [],
582
+ outputFormat: "json",
583
+ exploreCount: 0,
584
+ showConfig: false,
585
+ showHelp: false,
586
+ customProxy: null,
587
+ configAction: null,
588
+ configValue: null,
589
+ pipeMode: false,
590
+ filterStr: null,
591
+ };
592
+ }
593
+
559
594
  function parseAttachArgs(args) {
560
595
  let parallel = 1;
561
596
  let interval = 10;
@@ -690,8 +725,10 @@ function parseRawSyncArgs(args) {
690
725
  let rawSyncDb = null;
691
726
  let rawSyncArchive = null;
692
727
  let rawSyncInterval = null;
693
- let rawSyncMax = null;
694
- let rawSyncBatch = null;
728
+ let rawSyncThreshold = null;
729
+ let rawSyncLlm = false;
730
+ let rawSyncLlmMin = null;
731
+ let rawSyncLlmSample = null;
695
732
  let rawSyncOnce = false;
696
733
 
697
734
  for (let i = 0; i < args.length; i++) {
@@ -702,10 +739,17 @@ function parseRawSyncArgs(args) {
702
739
  rawSyncArchive = args[++i];
703
740
  } else if (arg === "--interval" && i + 1 < args.length) {
704
741
  rawSyncInterval = parseInt(args[++i]) || null;
705
- } else if (arg === "--max" && i + 1 < args.length) {
706
- rawSyncMax = parseInt(args[++i]) || null;
707
- } else if (arg === "--batch" && i + 1 < args.length) {
708
- rawSyncBatch = parseInt(args[++i]) || null;
742
+ } else if (
743
+ (arg === "--threshold" || arg === "--max") &&
744
+ i + 1 < args.length
745
+ ) {
746
+ rawSyncThreshold = parseInt(args[++i]) || null;
747
+ } else if (arg === "--llm") {
748
+ rawSyncLlm = true;
749
+ } else if (arg === "--llm-min" && i + 1 < args.length) {
750
+ rawSyncLlmMin = parseInt(args[++i]) || null;
751
+ } else if (arg === "--llm-sample" && i + 1 < args.length) {
752
+ rawSyncLlmSample = parseInt(args[++i]) || null;
709
753
  } else if (arg === "--once") {
710
754
  rawSyncOnce = true;
711
755
  }
@@ -716,8 +760,10 @@ function parseRawSyncArgs(args) {
716
760
  rawSyncDb,
717
761
  rawSyncArchive,
718
762
  rawSyncInterval,
719
- rawSyncMax,
720
- rawSyncBatch,
763
+ rawSyncThreshold,
764
+ rawSyncLlm,
765
+ rawSyncLlmMin,
766
+ rawSyncLlmSample,
721
767
  rawSyncOnce,
722
768
  urls: [],
723
769
  outputFormat: "json",
@@ -855,8 +901,10 @@ function parseTagArgs(args) {
855
901
  let isScore = false;
856
902
  let isScoreAll = false;
857
903
  let isRefresh = false;
904
+ let isSearch = false;
858
905
  let scoreAllPort = 7222;
859
906
  let refreshPort = 4222;
907
+ let searchPort = 3333;
860
908
  let scoreProxy = null;
861
909
  let scoreTag = null;
862
910
  let scoreCountries = null;
@@ -938,6 +986,8 @@ function parseTagArgs(args) {
938
986
  isScoreAll = true;
939
987
  } else if (cleaned.toLowerCase() === "refresh") {
940
988
  isRefresh = true;
989
+ } else if (cleaned.toLowerCase() === "search") {
990
+ isSearch = true;
941
991
  } else {
942
992
  tags.push(cleaned.toLowerCase());
943
993
  }
@@ -948,6 +998,7 @@ function parseTagArgs(args) {
948
998
  if (rawTagPort !== null && !isNaN(rawTagPort)) {
949
999
  if (isScoreAll) scoreAllPort = rawTagPort;
950
1000
  else if (isRefresh) refreshPort = rawTagPort;
1001
+ else if (isSearch) searchPort = rawTagPort;
951
1002
  else scorePort = rawTagPort;
952
1003
  }
953
1004
 
@@ -1039,6 +1090,28 @@ function parseTagArgs(args) {
1039
1090
  };
1040
1091
  }
1041
1092
 
1093
+ if (isSearch) {
1094
+ return {
1095
+ subcommand: "search",
1096
+ search: {
1097
+ serverUrl,
1098
+ basePort: searchPort,
1099
+ portCount: 10,
1100
+ proxy: scoreProxy,
1101
+ },
1102
+ urls: [],
1103
+ outputFormat: "json",
1104
+ exploreCount: 0,
1105
+ showConfig: false,
1106
+ showHelp: false,
1107
+ customProxy: null,
1108
+ configAction: null,
1109
+ configValue: null,
1110
+ pipeMode: false,
1111
+ filterStr: null,
1112
+ };
1113
+ }
1114
+
1042
1115
  return {
1043
1116
  subcommand: "tag",
1044
1117
  tagTags: {
@@ -1143,6 +1216,10 @@ export function parseArgs() {
1143
1216
  return parseRefreshArgs(args.slice(1));
1144
1217
  }
1145
1218
 
1219
+ if (args.length > 0 && args[0] === "search") {
1220
+ return parseSearchArgs(args.slice(1));
1221
+ }
1222
+
1146
1223
  if (args.length > 0 && args[0] === "raw-sync") {
1147
1224
  return parseRawSyncArgs(args.slice(1));
1148
1225
  }
@@ -314,18 +314,38 @@ const HELP_TEXT = [
314
314
  " tt-help tag refresh -s http://127.0.0.1:3001",
315
315
  " tt-help tag refresh --port 8222 -s http://127.0.0.1:3001",
316
316
  "",
317
+ " search [选项]",
318
+ " 基于 productive 标签搜索 TikTok 作者(score>70 的标签作为搜索词)",
319
+ " 5 个搜索 Tab 并行采集(综合/视频/用户/直播/照片),去重后提交到 job_base",
320
+ " 选项:",
321
+ " -s, --server <URL> 服务端地址(默认 http://127.0.0.1:3000)",
322
+ " --base-port <端口号> 起始 CDP 端口,默认 3333",
323
+ " --port-count <数量> 端口数量(账户数),默认 10",
324
+ " --proxy <地址> 浏览器代理",
325
+ " 行为:",
326
+ " - 从 tags 表取 score>70 的 productive 标签",
327
+ " - 去掉 # 号后作为搜索关键词",
328
+ " - 并行搜索 5 个 tab,去重后提交到 job_base(来源=search)",
329
+ " - 猜测国家取标签 countries 数组的第一个",
330
+ " 示例: tt-help search",
331
+ " tt-help search -s http://127.0.0.1:3001",
332
+ " tt-help search --base-port 3333 -s http://127.0.0.1:3001",
333
+ " tt-help search --proxy http://127.0.0.1:7890",
334
+ "",
317
335
  " raw-sync --db <主库路径> --archive <归档库路径> [选项]",
318
- " 从归档库向主库 raw_jobs 同步数据,保持主库总量不超上限",
336
+ " 归档新用户 + 按需补充任务(从归档库向主库 jobs 移入)",
319
337
  " 选项:",
320
338
  " --db <路径> 主库路径(必填)",
321
339
  " --archive <路径> 归档库路径(必填)",
322
- " --interval <秒> 检测间隔,默认 30",
323
- " --max <条> 主库 raw_jobs 保有量上限,默认 10000",
324
- " --batch <条> 每次搬移条数,默认 500",
340
+ " --interval <秒> 检测间隔,默认 120(2分钟)",
341
+ " --threshold <条> pending 低于此值时触发补任务,默认 50",
342
+ " --llm 启用 LLM 打分模式",
343
+ " --llm-min <分> LLM 最低分阈值,默认 60",
344
+ " --llm-sample <条> LLM 每批采样数,默认 100",
325
345
  " --once 只执行一次后退出",
326
346
  " 示例: tt-help raw-sync --db data/result.db --archive data/result-archive.db",
327
347
  " tt-help raw-sync --db data/result.db --archive data/result-archive.db --once",
328
- " tt-help raw-sync --db data/result.db --archive data/result-archive.db --interval 60 --max 20000",
348
+ " tt-help raw-sync --db data/result.db --archive data/result-archive.db --llm --threshold 300",
329
349
  "",
330
350
  " config [show|set|unset|reset]",
331
351
  " config 查看当前配置",
@@ -0,0 +1,203 @@
1
+ /**
2
+ * TikTok 搜索页作者采集 — DOM 滚动版
3
+ *
4
+ * 用法:
5
+ * import { collectSearchAuthors } from "../lib/search-collector.js";
6
+ * const ids = await collectSearchAuthors(browser, "tienda");
7
+ */
8
+
9
+ const TAB_URLS = {
10
+ 综合: (q) => `https://www.tiktok.com/search?q=${encodeURIComponent(q)}`,
11
+ 视频: (q) => `https://www.tiktok.com/search/video?q=${encodeURIComponent(q)}`,
12
+ 用户: (q) => `https://www.tiktok.com/search/user?q=${encodeURIComponent(q)}`,
13
+ 直播: (q) => `https://www.tiktok.com/search/live?q=${encodeURIComponent(q)}`,
14
+ 照片: (q) => `https://www.tiktok.com/search/photo?q=${encodeURIComponent(q)}`,
15
+ };
16
+
17
+ const DEFAULT_TABS = ["综合", "视频", "用户", "直播", "照片"];
18
+
19
+ const SCROLL = {
20
+ maxRounds: 60,
21
+ staleThreshold: 3,
22
+ delayMin: 800,
23
+ delayMax: 1500,
24
+ waitAfterScroll: 4000,
25
+ };
26
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
27
+ const randDelay = () =>
28
+ delay(
29
+ Math.floor(Math.random() * (SCROLL.delayMax - SCROLL.delayMin + 1)) +
30
+ SCROLL.delayMin,
31
+ );
32
+
33
+ async function extractAuthors(page, excludeSet) {
34
+ return page.evaluate(
35
+ (exclude) => {
36
+ const ids = new Set();
37
+ const skip = new Set(exclude || []);
38
+ for (const a of document.querySelectorAll('a[href*="/@"]')) {
39
+ const m = (a.getAttribute("href") || "").match(/\/@([^/?]+)/);
40
+ if (m && m[1] !== "undefined" && m[1].length <= 30 && !skip.has(m[1])) {
41
+ ids.add(m[1]);
42
+ }
43
+ }
44
+ return [...ids];
45
+ },
46
+ [...excludeSet],
47
+ );
48
+ }
49
+
50
+ async function scrollPage(page) {
51
+ await page.evaluate(() => {
52
+ const main = document.querySelector("main");
53
+ if (main && main.scrollHeight > main.clientHeight + 10) {
54
+ main.scrollBy(0, main.clientHeight * 0.8);
55
+ } else {
56
+ window.scrollBy(0, window.innerHeight * 0.8);
57
+ }
58
+ });
59
+ }
60
+
61
+ async function processOneTab(page, tabName, keyword, allIds, excludeSet, log) {
62
+ const urlFn = TAB_URLS[tabName];
63
+ if (!urlFn) {
64
+ log(`[${tabName}] 不支持`);
65
+ return;
66
+ }
67
+
68
+ const tag = `[${tabName}]`;
69
+ const url = urlFn(keyword);
70
+
71
+ // 首屏正常加载
72
+ log(`${tag} 加载...`);
73
+ try {
74
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
75
+ } catch (e) {
76
+ log(`${tag} 导航失败: ${e.message}`);
77
+ return;
78
+ }
79
+ await delay(4000);
80
+
81
+ // 首屏渲染完成后屏蔽图片/字体/媒体,加速后续滚动
82
+ await page.route("**/*", (route) => {
83
+ const t = route.request().resourceType();
84
+ if (t === "image" || t === "font" || t === "media") route.abort();
85
+ else route.continue();
86
+ });
87
+
88
+ const tabSet = new Set();
89
+ let stale = 0;
90
+
91
+ for (let round = 0; round <= SCROLL.maxRounds; round++) {
92
+ if (round > 0) {
93
+ await scrollPage(page);
94
+ await randDelay();
95
+ await delay(SCROLL.waitAfterScroll);
96
+ }
97
+
98
+ const ids = await extractAuthors(page, excludeSet);
99
+ let added = 0;
100
+ for (const id of ids) {
101
+ if (!tabSet.has(id)) {
102
+ tabSet.add(id);
103
+ added++;
104
+ }
105
+ }
106
+
107
+ if (round === 0) {
108
+ log(`${tag} 初始: ${tabSet.size}`);
109
+ continue;
110
+ }
111
+
112
+ if (added === 0) {
113
+ stale++;
114
+ if (stale >= SCROLL.staleThreshold) {
115
+ log(`${tag} ${SCROLL.staleThreshold}轮无新`);
116
+ break;
117
+ }
118
+ } else {
119
+ stale = 0;
120
+ if (round % 5 === 0 || added >= 5)
121
+ log(`${tag} R${round}: +${added}, 计${tabSet.size}`);
122
+ }
123
+ if (tabSet.size >= 500) break;
124
+ }
125
+
126
+ let added = 0;
127
+ for (const id of tabSet) {
128
+ if (!allIds.has(id)) {
129
+ allIds.add(id);
130
+ added++;
131
+ }
132
+ }
133
+ log(`${tag} 完成: ${tabSet.size}个, 新增${added} (累计${allIds.size})`);
134
+ }
135
+
136
+ /**
137
+ * @param {import("playwright").Browser} browser
138
+ * @param {string} keyword
139
+ * @param {object} [opts]
140
+ * @param {string[]} [opts.tabs]
141
+ * @param {function} [opts.log] - 日志函数,默认 console.error
142
+ * @returns {Promise<string[]>}
143
+ */
144
+ export async function collectSearchAuthors(browser, keyword, opts = {}) {
145
+ const tabs = opts.tabs || DEFAULT_TABS;
146
+ const log = opts.log || ((...args) => console.error(...args));
147
+ const excludeSet = new Set(opts.exclude || []);
148
+
149
+ log(`[collect] "${keyword}" Tab: [${tabs.join(", ")}]`);
150
+ if (excludeSet.size > 0)
151
+ log(`[collect] 排除: [${[...excludeSet].join(", ")}]`);
152
+
153
+ const context = browser.contexts()[0];
154
+ const existingPage = context.pages()[0];
155
+ const allIds = new Set();
156
+
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 {}
172
+ }
173
+ }
174
+ });
175
+
176
+ await Promise.all(tasks);
177
+
178
+ // 兜底:确保所有新建页面都已关闭
179
+ for (const p of newPages) {
180
+ try {
181
+ if (!p.isClosed()) await p.close();
182
+ } catch {}
183
+ }
184
+
185
+ return [...allIds].sort();
186
+ }
187
+
188
+ /**
189
+ * 采集并保存到文件(便捷函数)
190
+ * @param {import("playwright").Browser} browser
191
+ * @param {string} keyword
192
+ * @param {string} outputFile - 输出文件路径
193
+ * @param {object} [opts] - 同 collectSearchAuthors
194
+ * @returns {Promise<string[]>}
195
+ */
196
+ export async function searchAndSave(browser, keyword, outputFile, opts = {}) {
197
+ const fs = await import("fs");
198
+ const path = await import("path");
199
+ const ids = await collectSearchAuthors(browser, keyword, opts);
200
+ fs.mkdirSync(path.dirname(outputFile), { recursive: true });
201
+ fs.writeFileSync(outputFile, ids.join("\n") + "\n", "utf-8");
202
+ return ids;
203
+ }
@@ -12,6 +12,7 @@ const DEFAULT_TARGET_LOCATIONS = [
12
12
  "IT",
13
13
  "IE",
14
14
  "AT",
15
+ "US",
15
16
  ];
16
17
 
17
18
  const DEFAULT_TARGET_LOCATIONS_CSV = DEFAULT_TARGET_LOCATIONS.join(",");
package/src/main.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  handleTagRefresh,
21
21
  } from "./cli/tag.js";
22
22
  import { handleRawSync } from "./cli/raw-sync.js";
23
+ import { handleSearch } from "./cli/search.js";
23
24
 
24
25
  async function main() {
25
26
  const parsed = parseArgs();
@@ -59,6 +60,8 @@ async function main() {
59
60
  return handleScoreAll(parsed);
60
61
  case "tag-refresh":
61
62
  return handleTagRefresh(parsed);
63
+ case "search":
64
+ return handleSearch(parsed);
62
65
  }
63
66
 
64
67
  const {
package/src/npm-main.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  handleTagRefresh,
17
17
  } from "./cli/tag.js";
18
18
  import { handleRawSync } from "./cli/raw-sync.js";
19
+ import { handleSearch } from "./cli/search.js";
19
20
 
20
21
  function exitUnsupportedCommand(command) {
21
22
  console.error(
@@ -59,6 +60,8 @@ async function main() {
59
60
  return handleTagRefresh(parsed);
60
61
  case "raw-sync":
61
62
  return handleRawSync(parsed);
63
+ case "search":
64
+ return handleSearch(parsed);
62
65
  }
63
66
 
64
67
  const {