tt-help-cli-ycl 1.3.63 → 1.3.65
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/scripts/run-explore.bat +1 -1
- package/src/cli/attach.js +11 -1
- package/src/cli/config.js +46 -3
- package/src/cli/explore.js +25 -8
- package/src/cli/refresh.js +30 -9
- package/src/lib/args.js +11 -2
- package/src/lib/browser/cdp.js +12 -4
- package/src/lib/browser/page.js +41 -0
- package/src/lib/constants.js +37 -36
- package/src/lib/scrape.js +20 -4
- package/src/lib/tiktok-scraper.mjs +11 -5
- package/src/scraper/explore-core.js +7 -1
- package/src/watch/data-store.js +128 -2
- package/src/watch/public/app.js +293 -42
- package/src/watch/public/index.html +63 -0
- package/src/watch/public/style.css +80 -3
- package/src/watch/server.js +88 -1
|
@@ -245,6 +245,13 @@
|
|
|
245
245
|
style="padding:6px 10px;border:1px solid #333;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
|
|
246
246
|
<option value="">全部国家</option>
|
|
247
247
|
</select>
|
|
248
|
+
<label style="display:inline-flex;align-items:center;gap:3px;font-size:12px;color:#999;cursor:pointer;">
|
|
249
|
+
<input type="checkbox" id="rawVideoFilter" onchange="onRawFilterChange()" style="accent-color:#22c55e;"> 有视频
|
|
250
|
+
</label>
|
|
251
|
+
<label style="display:inline-flex;align-items:center;gap:3px;font-size:12px;color:#999;cursor:pointer;">
|
|
252
|
+
<input type="checkbox" id="rawFollowerFilter" onchange="onRawFilterChange()" style="accent-color:#22c55e;">
|
|
253
|
+
有粉丝
|
|
254
|
+
</label>
|
|
248
255
|
<button onclick="clearRawFilters()">清空筛选</button>
|
|
249
256
|
<button onclick="restoreFilteredRawJobs()"
|
|
250
257
|
style="background:#22c55e;color:#fff;border:none;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;">恢复当前筛选到
|
|
@@ -272,6 +279,62 @@
|
|
|
272
279
|
</div>
|
|
273
280
|
</div>
|
|
274
281
|
</div>
|
|
282
|
+
<div id="targetPage">
|
|
283
|
+
<div class="stats" style="margin-bottom:16px">
|
|
284
|
+
<div class="stat-card">
|
|
285
|
+
<div class="label">目标商家</div>
|
|
286
|
+
<div class="value target" id="targetPageStatTotal">0</div>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="stat-card clickable pending-card" onclick="navigateToMain()" style="background:rgba(167,139,250,0.1)">
|
|
289
|
+
<div class="label">← 返回主页面</div>
|
|
290
|
+
</div>
|
|
291
|
+
<div class="stat-card">
|
|
292
|
+
<div class="label">目标国家</div>
|
|
293
|
+
<div class="value" id="targetPageStatCountries" style="font-size:17px;color:#a78bfa">0</div>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="stat-card clickable" id="exportTargetCsvBtn" style="background:rgba(34,197,94,0.12);cursor:pointer">
|
|
296
|
+
<div class="label">导出 CSV</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="target-page-layout">
|
|
300
|
+
<div class="target-side-card">
|
|
301
|
+
<h3>🎯 目标商家国家分布</h3>
|
|
302
|
+
<div class="pending-country-grid" id="targetCountryGrid">
|
|
303
|
+
<span style="color:#666;font-size:12px">加载中...</span>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="muted-tip">点击国家卡片可筛选列表。</div>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="target-table-card">
|
|
308
|
+
<h3>目标商家列表</h3>
|
|
309
|
+
<div class="controls">
|
|
310
|
+
<input type="text" id="targetSearchInput" placeholder="搜索目标商家用户名 / 昵称...">
|
|
311
|
+
<select id="targetLocationFilter" onchange="onTargetLocationChange()"
|
|
312
|
+
style="padding:6px 10px;border:1px solid #7c3aed;border-radius:6px;background:#2a2a3a;color:#ccc;font-size:12px;cursor:pointer;outline:none;">
|
|
313
|
+
<option value="">全部国家</option>
|
|
314
|
+
</select>
|
|
315
|
+
<button onclick="clearTargetFilters()">清空筛选</button>
|
|
316
|
+
</div>
|
|
317
|
+
<div class="table-scroll">
|
|
318
|
+
<table>
|
|
319
|
+
<thead>
|
|
320
|
+
<tr>
|
|
321
|
+
<th>用户名</th>
|
|
322
|
+
<th>昵称</th>
|
|
323
|
+
<th>粉丝</th>
|
|
324
|
+
<th>视频</th>
|
|
325
|
+
<th>国家</th>
|
|
326
|
+
<th>最近发布</th>
|
|
327
|
+
<th>最近刷新</th>
|
|
328
|
+
<th>来源</th>
|
|
329
|
+
<th>状态</th>
|
|
330
|
+
</tr>
|
|
331
|
+
</thead>
|
|
332
|
+
<tbody id="targetTable"></tbody>
|
|
333
|
+
</table>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
275
338
|
<script src="app.js"></script>
|
|
276
339
|
</body>
|
|
277
340
|
|
|
@@ -634,6 +634,14 @@ td.user-id:hover {
|
|
|
634
634
|
display: block;
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
+
#targetPage {
|
|
638
|
+
display: none;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
#targetPage.active {
|
|
642
|
+
display: block;
|
|
643
|
+
}
|
|
644
|
+
|
|
637
645
|
#mainPage.hidden {
|
|
638
646
|
display: none;
|
|
639
647
|
}
|
|
@@ -670,10 +678,15 @@ td.user-id:hover {
|
|
|
670
678
|
position: relative;
|
|
671
679
|
}
|
|
672
680
|
|
|
673
|
-
.country-action-
|
|
681
|
+
.country-action-btns {
|
|
674
682
|
position: absolute;
|
|
675
|
-
top:
|
|
676
|
-
right:
|
|
683
|
+
top: 8px;
|
|
684
|
+
right: 8px;
|
|
685
|
+
display: flex;
|
|
686
|
+
gap: 4px;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.country-action-btn {
|
|
677
690
|
width: 28px;
|
|
678
691
|
height: 28px;
|
|
679
692
|
border: 1px solid rgba(248, 113, 113, 0.35);
|
|
@@ -937,6 +950,66 @@ td.user-id:hover {
|
|
|
937
950
|
color: #a78bfa;
|
|
938
951
|
}
|
|
939
952
|
|
|
953
|
+
.target-country-group {
|
|
954
|
+
background: #1a1a24;
|
|
955
|
+
border-radius: 8px;
|
|
956
|
+
padding: 20px;
|
|
957
|
+
margin-bottom: 20px;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.target-country-header {
|
|
961
|
+
display: flex;
|
|
962
|
+
align-items: center;
|
|
963
|
+
gap: 12px;
|
|
964
|
+
margin-bottom: 16px;
|
|
965
|
+
padding-bottom: 12px;
|
|
966
|
+
border-bottom: 1px solid #2a2a3a;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.target-country-header .country-flag {
|
|
970
|
+
font-size: 20px;
|
|
971
|
+
font-weight: 700;
|
|
972
|
+
color: #a78bfa;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.target-country-header .country-user-count {
|
|
976
|
+
font-size: 13px;
|
|
977
|
+
color: #888;
|
|
978
|
+
background: #2a2a3a;
|
|
979
|
+
padding: 2px 10px;
|
|
980
|
+
border-radius: 10px;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
.target-page-layout {
|
|
984
|
+
display: grid;
|
|
985
|
+
grid-template-columns: 320px 1fr;
|
|
986
|
+
gap: 16px;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.target-side-card {
|
|
990
|
+
background: #1a1a24;
|
|
991
|
+
border-radius: 8px;
|
|
992
|
+
padding: 16px;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
.target-side-card h3 {
|
|
996
|
+
font-size: 14px;
|
|
997
|
+
color: #a78bfa;
|
|
998
|
+
margin-bottom: 12px;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.target-table-card {
|
|
1002
|
+
background: #1a1a24;
|
|
1003
|
+
border-radius: 8px;
|
|
1004
|
+
padding: 16px;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.target-table-card h3 {
|
|
1008
|
+
font-size: 14px;
|
|
1009
|
+
color: #888;
|
|
1010
|
+
margin-bottom: 12px;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
940
1013
|
@media (max-width: 768px) {
|
|
941
1014
|
body {
|
|
942
1015
|
padding: 8px;
|
|
@@ -977,6 +1050,10 @@ td.user-id:hover {
|
|
|
977
1050
|
grid-template-columns: 1fr;
|
|
978
1051
|
}
|
|
979
1052
|
|
|
1053
|
+
.target-page-layout {
|
|
1054
|
+
grid-template-columns: 1fr;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
980
1057
|
.table-wrap {
|
|
981
1058
|
padding: 10px;
|
|
982
1059
|
}
|
package/src/watch/server.js
CHANGED
|
@@ -526,6 +526,66 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
526
526
|
return;
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
if (
|
|
530
|
+
req.method === "GET" &&
|
|
531
|
+
routePath === "/api/target-users-by-country"
|
|
532
|
+
) {
|
|
533
|
+
const result = store.getTargetUsersByCountry(DEFAULT_TARGET_LOCATIONS);
|
|
534
|
+
if (req.headers["accept"]?.includes("text/csv")) {
|
|
535
|
+
const columns = [
|
|
536
|
+
"uniqueId",
|
|
537
|
+
"nickname",
|
|
538
|
+
"followerCount",
|
|
539
|
+
"videoCount",
|
|
540
|
+
"locationCreated",
|
|
541
|
+
"latestVideoTime",
|
|
542
|
+
"refreshTime",
|
|
543
|
+
"status",
|
|
544
|
+
"sources",
|
|
545
|
+
];
|
|
546
|
+
const allUsers = [];
|
|
547
|
+
for (const country of result.countries) {
|
|
548
|
+
for (const u of country.users) {
|
|
549
|
+
allUsers.push({
|
|
550
|
+
uniqueId: u.uniqueId,
|
|
551
|
+
nickname: u.nickname || "",
|
|
552
|
+
followerCount: u.followerCount ?? 0,
|
|
553
|
+
videoCount: u.videoCount ?? 0,
|
|
554
|
+
locationCreated: u.locationCreated || "",
|
|
555
|
+
latestVideoTime: u.latestVideoTime
|
|
556
|
+
? new Date(u.latestVideoTime * 1000)
|
|
557
|
+
.toISOString()
|
|
558
|
+
.slice(0, 19)
|
|
559
|
+
.replace("T", " ")
|
|
560
|
+
: "",
|
|
561
|
+
refreshTime: u.refreshTime
|
|
562
|
+
? new Date(u.refreshTime)
|
|
563
|
+
.toISOString()
|
|
564
|
+
.slice(0, 19)
|
|
565
|
+
.replace("T", " ")
|
|
566
|
+
: "",
|
|
567
|
+
status: u.status || "",
|
|
568
|
+
sources: (u.sources || []).join(";"),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
res.writeHead(200, {
|
|
573
|
+
"Content-Type": "text/csv; charset=utf-8",
|
|
574
|
+
"Content-Disposition":
|
|
575
|
+
'attachment; filename="target-users-by-country.csv"',
|
|
576
|
+
});
|
|
577
|
+
const BOM = "\uFEFF";
|
|
578
|
+
const header = columns.join(",");
|
|
579
|
+
const lines = allUsers.map((r) =>
|
|
580
|
+
columns.map((c) => csvEscape(r[c])).join(","),
|
|
581
|
+
);
|
|
582
|
+
res.end(BOM + [header, ...lines].join("\r\n"));
|
|
583
|
+
} else {
|
|
584
|
+
sendJSON(res, 200, result);
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
529
589
|
if (req.method === "GET" && routePath === "/api/client-errors") {
|
|
530
590
|
sendJSON(res, 200, { clients: store.getClientErrors() });
|
|
531
591
|
return;
|
|
@@ -564,6 +624,8 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
564
624
|
location: params.location,
|
|
565
625
|
limit: params.limit,
|
|
566
626
|
offset: params.offset,
|
|
627
|
+
hasVideo: params.hasVideo,
|
|
628
|
+
hasFollower: params.hasFollower,
|
|
567
629
|
});
|
|
568
630
|
sendJSON(res, 200, result);
|
|
569
631
|
return;
|
|
@@ -590,10 +652,17 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
590
652
|
let result;
|
|
591
653
|
if (body.uniqueId) {
|
|
592
654
|
result = store.restoreRawJobById(body.uniqueId);
|
|
593
|
-
} else if (
|
|
655
|
+
} else if (
|
|
656
|
+
body.search ||
|
|
657
|
+
body.location ||
|
|
658
|
+
body.hasVideo ||
|
|
659
|
+
body.hasFollower
|
|
660
|
+
) {
|
|
594
661
|
result = store.restoreRawJobsByFilter({
|
|
595
662
|
search: body.search || "",
|
|
596
663
|
location: body.location || "",
|
|
664
|
+
hasVideo: body.hasVideo,
|
|
665
|
+
hasFollower: body.hasFollower,
|
|
597
666
|
});
|
|
598
667
|
} else if (body.country) {
|
|
599
668
|
result = store.restoreRawJobsByCountry(body.country);
|
|
@@ -629,6 +698,24 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
629
698
|
return;
|
|
630
699
|
}
|
|
631
700
|
|
|
701
|
+
if (
|
|
702
|
+
req.method === "POST" &&
|
|
703
|
+
routePath === "/api/pending-by-country/reset"
|
|
704
|
+
) {
|
|
705
|
+
try {
|
|
706
|
+
const body = await readBody(req);
|
|
707
|
+
const result = store.resetPendingByCountry(body.country);
|
|
708
|
+
if (result.error) {
|
|
709
|
+
sendJSON(res, 400, result);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
sendJSON(res, 200, result);
|
|
713
|
+
} catch (e) {
|
|
714
|
+
sendJSON(res, 400, { error: e.message });
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
632
719
|
if (
|
|
633
720
|
req.method === "DELETE" &&
|
|
634
721
|
routePath.startsWith("/api/client-error/")
|