tt-help-cli-ycl 1.4.40 → 1.4.41
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/tag.js +210 -74
- package/src/lib/args.js +6 -0
package/package.json
CHANGED
package/src/cli/tag.js
CHANGED
|
@@ -297,11 +297,64 @@ function applyFilterAndScore(videos, targetCountries, result) {
|
|
|
297
297
|
return { filtered, matchedAuthorSet };
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* 通过 webserver API 获取视频的国家信息(不走本地 HTTP view-source)
|
|
302
|
+
*/
|
|
303
|
+
async function enrichViaWebserver(videos, webserverUrl, onProgress) {
|
|
304
|
+
const total = videos.length;
|
|
305
|
+
let done = 0;
|
|
306
|
+
const CONCURRENCY = 16;
|
|
307
|
+
|
|
308
|
+
async function worker(queue) {
|
|
309
|
+
while (queue.length > 0) {
|
|
310
|
+
const v = queue.shift();
|
|
311
|
+
if (!v) break;
|
|
312
|
+
const videoUrl = `https://www.tiktok.com/@${v.authorUniqueId}/video/${v.id}`;
|
|
313
|
+
try {
|
|
314
|
+
const res = await fetch(`${webserverUrl}/api/tiktok/video`, {
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: { "Content-Type": "application/json" },
|
|
317
|
+
body: JSON.stringify({ videoUrl }),
|
|
318
|
+
signal: AbortSignal.timeout(30000),
|
|
319
|
+
});
|
|
320
|
+
const data = await res.json();
|
|
321
|
+
v.locationCreated = data?.locationCreated || null;
|
|
322
|
+
} catch {
|
|
323
|
+
v.locationCreated = null;
|
|
324
|
+
}
|
|
325
|
+
done++;
|
|
326
|
+
if (onProgress) {
|
|
327
|
+
onProgress({
|
|
328
|
+
done,
|
|
329
|
+
total,
|
|
330
|
+
current: videoUrl,
|
|
331
|
+
locationCreated: v.locationCreated,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const queue = [...videos];
|
|
338
|
+
const workers = Array.from(
|
|
339
|
+
{ length: Math.min(CONCURRENCY, queue.length) },
|
|
340
|
+
() => worker(queue),
|
|
341
|
+
);
|
|
342
|
+
await Promise.all(workers);
|
|
343
|
+
}
|
|
344
|
+
|
|
300
345
|
async function processTag(
|
|
301
346
|
tag,
|
|
302
347
|
index,
|
|
303
348
|
total,
|
|
304
|
-
{
|
|
349
|
+
{
|
|
350
|
+
enrich,
|
|
351
|
+
targetLocations,
|
|
352
|
+
noFilter,
|
|
353
|
+
serverUrl,
|
|
354
|
+
recordTags,
|
|
355
|
+
port,
|
|
356
|
+
webserverUrl,
|
|
357
|
+
},
|
|
305
358
|
) {
|
|
306
359
|
const prefix = total > 1 ? `[${index + 1}/${total}]` : "";
|
|
307
360
|
process.stderr.write(`${prefix} 正在获取 #${tag} ... `);
|
|
@@ -323,33 +376,53 @@ async function processTag(
|
|
|
323
376
|
let videos = result.videos;
|
|
324
377
|
let filteredAuthors = result.uniqueAuthors;
|
|
325
378
|
|
|
326
|
-
if (enrich) {
|
|
327
|
-
|
|
328
|
-
if (noFilter) {
|
|
379
|
+
if (webserverUrl || enrich) {
|
|
380
|
+
if (webserverUrl) {
|
|
329
381
|
process.stderr.write(
|
|
330
|
-
`\n 正在补充国家信息 (${
|
|
382
|
+
`\n 正在补充国家信息 (webserver API,目标: ${targetLocations.join(",")})...\n`,
|
|
331
383
|
);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
384
|
+
await enrichViaWebserver(
|
|
385
|
+
videos,
|
|
386
|
+
webserverUrl,
|
|
387
|
+
({ done, total, current, locationCreated }) => {
|
|
388
|
+
const loc = locationCreated || "-";
|
|
389
|
+
const hit =
|
|
390
|
+
locationCreated &&
|
|
391
|
+
isLocationInList(locationCreated, targetLocations);
|
|
392
|
+
process.stderr.write(
|
|
393
|
+
`\r [${done}/${total}] ${current.split("/").pop().slice(0, 20)} → ${loc}${hit ? " ✓" : ""}\x1b[K`,
|
|
394
|
+
);
|
|
395
|
+
},
|
|
335
396
|
);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
onProgress: ({ done, total, current, locationCreated }) => {
|
|
341
|
-
const label = enrichMode === "users" ? `@${current}` : current;
|
|
342
|
-
const loc = locationCreated || "-";
|
|
343
|
-
const hit =
|
|
344
|
-
locationCreated &&
|
|
345
|
-
isLocationInList(locationCreated, targetLocations);
|
|
397
|
+
process.stderr.write("\n");
|
|
398
|
+
} else {
|
|
399
|
+
const enrichMode = enrich === true ? "videos" : enrich;
|
|
400
|
+
if (noFilter) {
|
|
346
401
|
process.stderr.write(
|
|
347
|
-
`\
|
|
402
|
+
`\n 正在补充国家信息 (${enrichMode} 模式,不过滤)...\n`,
|
|
348
403
|
);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
404
|
+
} else {
|
|
405
|
+
process.stderr.write(
|
|
406
|
+
`\n 正在补充国家信息 (${enrichMode} 模式,目标: ${targetLocations.join(",")})...\n`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const enriched = await enrichVideosWithLocation(videos, {
|
|
411
|
+
mode: enrichMode,
|
|
412
|
+
onProgress: ({ done, total, current, locationCreated }) => {
|
|
413
|
+
const label = enrichMode === "users" ? `@${current}` : current;
|
|
414
|
+
const loc = locationCreated || "-";
|
|
415
|
+
const hit =
|
|
416
|
+
locationCreated &&
|
|
417
|
+
isLocationInList(locationCreated, targetLocations);
|
|
418
|
+
process.stderr.write(
|
|
419
|
+
`\r [${done}/${total}] ${label} → ${loc}${hit ? " ✓" : ""}\x1b[K`,
|
|
420
|
+
);
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
videos = enriched.videos;
|
|
424
|
+
process.stderr.write("\n");
|
|
425
|
+
}
|
|
353
426
|
|
|
354
427
|
if (!noFilter) {
|
|
355
428
|
const before = videos.length;
|
|
@@ -482,7 +555,7 @@ function askTagQuestion(question) {
|
|
|
482
555
|
|
|
483
556
|
async function scoreSingleTag(
|
|
484
557
|
tag,
|
|
485
|
-
{ baseUrl, cdpPort, targetCountries, effectiveProxy },
|
|
558
|
+
{ baseUrl, cdpPort, targetCountries, effectiveProxy, webserverUrl },
|
|
486
559
|
) {
|
|
487
560
|
const log = (...args) => process.stderr.write(args.join(" ") + "\n");
|
|
488
561
|
const progress = (msg) => process.stderr.write(`\r ${msg}\x1b[K`);
|
|
@@ -538,22 +611,41 @@ async function scoreSingleTag(
|
|
|
538
611
|
}
|
|
539
612
|
|
|
540
613
|
// Step 2: 补充国家信息
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
614
|
+
if (webserverUrl) {
|
|
615
|
+
log(
|
|
616
|
+
` ⏳ 补充国家信息 (${videos.length} 个视频, via webserver ${webserverUrl})...`,
|
|
617
|
+
);
|
|
618
|
+
await enrichViaWebserver(
|
|
619
|
+
videos,
|
|
620
|
+
webserverUrl,
|
|
621
|
+
({ done, total, current, locationCreated }) => {
|
|
622
|
+
if (done % 10 === 0 || done === total) {
|
|
623
|
+
const name = current.split("/").pop().slice(0, 20);
|
|
624
|
+
progress(`[${done}/${total}] ${name} → ${locationCreated || "-"}`);
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
);
|
|
628
|
+
clearLine();
|
|
629
|
+
const withLoc = videos.filter((v) => v.locationCreated).length;
|
|
630
|
+
log(` ✓ 国家信息: ${withLoc}/${videos.length} 个视频有国家`);
|
|
631
|
+
} else {
|
|
632
|
+
log(` ⏳ 补充国家信息 (${videos.length} 个视频)...`);
|
|
633
|
+
const enrichOpts = {
|
|
634
|
+
mode: "videos",
|
|
635
|
+
onProgress: ({ done, total, current, locationCreated }) => {
|
|
636
|
+
if (done % 10 === 0 || done === total) {
|
|
637
|
+
const name = current.split("/").pop().slice(0, 20);
|
|
638
|
+
progress(`[${done}/${total}] ${name} → ${locationCreated || "-"}`);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
if (effectiveProxy) enrichOpts.proxyServer = effectiveProxy;
|
|
643
|
+
const enriched = await enrichVideosWithLocation(videos, enrichOpts);
|
|
644
|
+
clearLine();
|
|
645
|
+
videos = enriched.videos;
|
|
646
|
+
const withLoc = videos.filter((v) => v.locationCreated).length;
|
|
647
|
+
log(` ✓ 国家信息: ${withLoc}/${videos.length} 个视频有国家`);
|
|
648
|
+
}
|
|
557
649
|
|
|
558
650
|
// Step 3: 过滤 + 算分
|
|
559
651
|
const { matchedAuthorSet } = applyFilterAndScore(
|
|
@@ -610,7 +702,14 @@ async function scoreSingleTag(
|
|
|
610
702
|
|
|
611
703
|
export async function handleScore(parsed) {
|
|
612
704
|
const { tagScore } = parsed;
|
|
613
|
-
const {
|
|
705
|
+
const {
|
|
706
|
+
tag,
|
|
707
|
+
countries,
|
|
708
|
+
serverUrl,
|
|
709
|
+
port,
|
|
710
|
+
proxy: cliProxy,
|
|
711
|
+
webserver,
|
|
712
|
+
} = tagScore || {};
|
|
614
713
|
|
|
615
714
|
const baseUrl = serverUrl || DEFAULT_SERVER;
|
|
616
715
|
const cdpPort = port || 9222;
|
|
@@ -680,6 +779,7 @@ export async function handleScore(parsed) {
|
|
|
680
779
|
cdpPort,
|
|
681
780
|
targetCountries,
|
|
682
781
|
effectiveProxy,
|
|
782
|
+
webserverUrl: webserver,
|
|
683
783
|
});
|
|
684
784
|
|
|
685
785
|
// 如果队列还有 tag,自动继续;否则询问
|
|
@@ -743,7 +843,13 @@ async function reportToServer(baseUrl, result, clientId, meta, userId = "") {
|
|
|
743
843
|
|
|
744
844
|
export async function handleScoreAll(parsed) {
|
|
745
845
|
const { tagScoreAll } = parsed;
|
|
746
|
-
let {
|
|
846
|
+
let {
|
|
847
|
+
countries,
|
|
848
|
+
serverUrl,
|
|
849
|
+
port,
|
|
850
|
+
proxy: cliProxy,
|
|
851
|
+
webserver,
|
|
852
|
+
} = tagScoreAll || {};
|
|
747
853
|
|
|
748
854
|
const baseUrl = serverUrl || DEFAULT_SERVER;
|
|
749
855
|
const defaultScoreAllPort =
|
|
@@ -951,19 +1057,24 @@ export async function handleScoreAll(parsed) {
|
|
|
951
1057
|
const clientMeta = { type: "scoring", port: cdpPort };
|
|
952
1058
|
|
|
953
1059
|
// 复用 TikTokScraper 实例,避免每次 enrich 都启动/关闭 headless 浏览器
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1060
|
+
// webserver 模式下不需要本地 scraper
|
|
1061
|
+
const enrichScraper = webserver
|
|
1062
|
+
? null
|
|
1063
|
+
: new TikTokScraper({
|
|
1064
|
+
poolSize: 3,
|
|
1065
|
+
proxyServer: effectiveProxy || undefined,
|
|
1066
|
+
});
|
|
1067
|
+
if (enrichScraper) {
|
|
1068
|
+
await enrichScraper.init();
|
|
1069
|
+
log("✅ TikTokScraper 已就绪 (enrich 复用)");
|
|
1070
|
+
}
|
|
960
1071
|
log(` 客户端 ID: ${clientId.substring(0, 8)}...`);
|
|
961
1072
|
log("");
|
|
962
1073
|
|
|
963
1074
|
// Ctrl+C 时关闭浏览器和 scraper
|
|
964
1075
|
const cleanup = () => {
|
|
965
1076
|
log("\n正在清理资源...");
|
|
966
|
-
enrichScraper.close().catch(() => {});
|
|
1077
|
+
if (enrichScraper) enrichScraper.close().catch(() => {});
|
|
967
1078
|
killEdgeProcesses(null, cdpPort);
|
|
968
1079
|
process.exit(0);
|
|
969
1080
|
};
|
|
@@ -1087,32 +1198,55 @@ export async function handleScoreAll(parsed) {
|
|
|
1087
1198
|
throw new Error("no videos returned (network/captcha/slow-load)");
|
|
1088
1199
|
}
|
|
1089
1200
|
|
|
1090
|
-
// enrich:
|
|
1091
|
-
|
|
1092
|
-
videos,
|
|
1093
|
-
baseUrl,
|
|
1094
|
-
enrichScraper,
|
|
1095
|
-
{
|
|
1096
|
-
clientId,
|
|
1097
|
-
clientMeta,
|
|
1098
|
-
effectiveProxy,
|
|
1099
|
-
log,
|
|
1100
|
-
},
|
|
1101
|
-
);
|
|
1102
|
-
videos = enriched.videos;
|
|
1103
|
-
|
|
1104
|
-
// CDN 限流检测:有拦截则冷却 + 重启 scraper
|
|
1105
|
-
const cdnBlocked = enriched.cdnBlockedCount || 0;
|
|
1106
|
-
if (cdnBlocked > 0) {
|
|
1107
|
-
const cdnRatio = cdnBlocked / (videos.length || 1);
|
|
1108
|
-
const coolSec = cdnRatio > 0.3 ? 120 : 60;
|
|
1201
|
+
// enrich: webserver API 或本地 HTTP view-source
|
|
1202
|
+
if (webserver) {
|
|
1109
1203
|
log(
|
|
1110
|
-
`
|
|
1204
|
+
` ⏳ 补充国家信息 (${videos.length} 个视频, via webserver ${webserver})...`,
|
|
1111
1205
|
);
|
|
1112
|
-
await
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1206
|
+
await enrichViaWebserver(
|
|
1207
|
+
videos,
|
|
1208
|
+
webserver,
|
|
1209
|
+
({ done, total, current, locationCreated }) => {
|
|
1210
|
+
if (done % 10 === 0 || done === total) {
|
|
1211
|
+
const name = current.split("/").pop().slice(0, 20);
|
|
1212
|
+
process.stderr.write(
|
|
1213
|
+
`\r [${done}/${total}] ${name} → ${locationCreated || "-"}\x1b[K`,
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
},
|
|
1217
|
+
);
|
|
1218
|
+
process.stderr.write("\n");
|
|
1219
|
+
log(
|
|
1220
|
+
` ✓ 国家信息: ${videos.filter((v) => v.locationCreated).length}/${videos.length} 个视频有国家`,
|
|
1221
|
+
);
|
|
1222
|
+
} else {
|
|
1223
|
+
// enrich: 先查服务端缓存,只对未知视频发 HTTP 请求
|
|
1224
|
+
const enriched = await enrichWithServerCache(
|
|
1225
|
+
videos,
|
|
1226
|
+
baseUrl,
|
|
1227
|
+
enrichScraper,
|
|
1228
|
+
{
|
|
1229
|
+
clientId,
|
|
1230
|
+
clientMeta,
|
|
1231
|
+
effectiveProxy,
|
|
1232
|
+
log,
|
|
1233
|
+
},
|
|
1234
|
+
);
|
|
1235
|
+
videos = enriched.videos;
|
|
1236
|
+
|
|
1237
|
+
// CDN 限流检测:有拦截则冷却 + 重启 scraper
|
|
1238
|
+
const cdnBlocked = enriched.cdnBlockedCount || 0;
|
|
1239
|
+
if (cdnBlocked > 0) {
|
|
1240
|
+
const cdnRatio = cdnBlocked / (videos.length || 1);
|
|
1241
|
+
const coolSec = cdnRatio > 0.3 ? 120 : 60;
|
|
1242
|
+
log(
|
|
1243
|
+
` ⚠️ CDN 限流: ${cdnBlocked}/${videos.length} (${(cdnRatio * 100).toFixed(0)}%),冷却 ${coolSec} 秒后重启 scraper`,
|
|
1244
|
+
);
|
|
1245
|
+
await new Promise((r) => setTimeout(r, coolSec * 1000));
|
|
1246
|
+
log(` 正在重启 TikTokScraper...`);
|
|
1247
|
+
await enrichScraper.restart();
|
|
1248
|
+
log(` ✅ TikTokScraper 已重启`);
|
|
1249
|
+
}
|
|
1116
1250
|
}
|
|
1117
1251
|
|
|
1118
1252
|
// 过滤 + 算分 (共用函数)
|
|
@@ -1281,7 +1415,7 @@ export async function handleScoreAll(parsed) {
|
|
|
1281
1415
|
}
|
|
1282
1416
|
}
|
|
1283
1417
|
} finally {
|
|
1284
|
-
await enrichScraper.close();
|
|
1418
|
+
if (enrichScraper) await enrichScraper.close();
|
|
1285
1419
|
log("✅ TikTokScraper 已关闭");
|
|
1286
1420
|
log("正在关闭 CDP 浏览器...");
|
|
1287
1421
|
try {
|
|
@@ -1336,6 +1470,7 @@ export async function handleTag(parsed) {
|
|
|
1336
1470
|
noFilter,
|
|
1337
1471
|
serverUrl,
|
|
1338
1472
|
discover,
|
|
1473
|
+
webserver,
|
|
1339
1474
|
} = tagTags;
|
|
1340
1475
|
|
|
1341
1476
|
const targetLocations = locations
|
|
@@ -1373,6 +1508,7 @@ export async function handleTag(parsed) {
|
|
|
1373
1508
|
noFilter,
|
|
1374
1509
|
serverUrl,
|
|
1375
1510
|
recordTags: !!discover,
|
|
1511
|
+
webserverUrl: webserver,
|
|
1376
1512
|
});
|
|
1377
1513
|
|
|
1378
1514
|
const output = { tag: result.tag };
|
package/src/lib/args.js
CHANGED
|
@@ -983,6 +983,7 @@ function parseTagArgs(args) {
|
|
|
983
983
|
let scoreTag = null;
|
|
984
984
|
let scoreCountries = null;
|
|
985
985
|
let scorePort = 9222;
|
|
986
|
+
let webserverUrl = null;
|
|
986
987
|
// 延迟路由:--port 的值先暂存,等循环结束后根据已确定的 subcommand 分配
|
|
987
988
|
let rawTagPort = null;
|
|
988
989
|
|
|
@@ -1033,6 +1034,8 @@ function parseTagArgs(args) {
|
|
|
1033
1034
|
scoreProxy = args[++i];
|
|
1034
1035
|
} else if (arg === "-p" || arg === "--prompt") {
|
|
1035
1036
|
discoverPrompt = args[++i];
|
|
1037
|
+
} else if (arg === "--webserver") {
|
|
1038
|
+
webserverUrl = args[++i];
|
|
1036
1039
|
} else if (!arg.startsWith("-")) {
|
|
1037
1040
|
const cleaned = arg.replace("#", "").trim();
|
|
1038
1041
|
if (cleaned.toLowerCase() === "discover") {
|
|
@@ -1107,6 +1110,7 @@ function parseTagArgs(args) {
|
|
|
1107
1110
|
serverUrl,
|
|
1108
1111
|
port: scorePort,
|
|
1109
1112
|
proxy: scoreProxy,
|
|
1113
|
+
webserver: webserverUrl,
|
|
1110
1114
|
},
|
|
1111
1115
|
urls: [],
|
|
1112
1116
|
outputFormat: "json",
|
|
@@ -1129,6 +1133,7 @@ function parseTagArgs(args) {
|
|
|
1129
1133
|
serverUrl,
|
|
1130
1134
|
port: scoreAllPort,
|
|
1131
1135
|
proxy: scoreProxy,
|
|
1136
|
+
webserver: webserverUrl,
|
|
1132
1137
|
},
|
|
1133
1138
|
urls: [],
|
|
1134
1139
|
outputFormat: "json",
|
|
@@ -1198,6 +1203,7 @@ function parseTagArgs(args) {
|
|
|
1198
1203
|
noFilter,
|
|
1199
1204
|
serverUrl,
|
|
1200
1205
|
discover,
|
|
1206
|
+
webserver: webserverUrl,
|
|
1201
1207
|
},
|
|
1202
1208
|
urls: [],
|
|
1203
1209
|
outputFormat: "json",
|