tt-help-cli-ycl 1.3.81 → 1.3.83
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/explore.js +27 -4
- package/src/cli/refresh.js +1 -0
- package/src/cli/watch.js +25 -4
- package/src/lib/api-interceptor-comment.js +56 -14
- package/src/lib/api-interceptor.js +18 -2
- package/src/lib/args.js +14 -0
- package/src/scraper/explore-core.js +27 -1
- package/src/watch/data-store.js +586 -68
- package/src/watch/public/app.js +60 -5
- package/src/watch/public/index.html +2 -1
- package/src/watch/public/style.css +25 -0
- package/src/watch/server.js +66 -3
package/src/watch/public/app.js
CHANGED
|
@@ -961,6 +961,7 @@ let currentTargetTotal = 0;
|
|
|
961
961
|
let currentTargetLoading = false;
|
|
962
962
|
let currentTargetAllLoaded = false;
|
|
963
963
|
let currentTargetSeq = 0;
|
|
964
|
+
let currentTargetSort = { key: null, asc: true };
|
|
964
965
|
const TARGET_PAGE_SIZE = 200;
|
|
965
966
|
|
|
966
967
|
async function fetchTargetByCountry() {
|
|
@@ -1087,16 +1088,36 @@ function renderTargetTable() {
|
|
|
1087
1088
|
const el = document.getElementById("targetTable");
|
|
1088
1089
|
const moreHint = document.getElementById("targetMoreHint");
|
|
1089
1090
|
|
|
1090
|
-
|
|
1091
|
+
// 应用本地排序
|
|
1092
|
+
let displayUsers = currentTargetUsers;
|
|
1093
|
+
if (currentTargetSort.key) {
|
|
1094
|
+
displayUsers = [...currentTargetUsers].sort((a, b) => {
|
|
1095
|
+
let va = a[currentTargetSort.key];
|
|
1096
|
+
let vb = b[currentTargetSort.key];
|
|
1097
|
+
if (va == null && vb == null) return 0;
|
|
1098
|
+
if (va == null) return 1;
|
|
1099
|
+
if (vb == null) return -1;
|
|
1100
|
+
if (typeof va === "number" && typeof vb === "number") {
|
|
1101
|
+
return currentTargetSort.asc ? va - vb : vb - va;
|
|
1102
|
+
}
|
|
1103
|
+
va = String(va).toLowerCase();
|
|
1104
|
+
vb = String(vb).toLowerCase();
|
|
1105
|
+
return currentTargetSort.asc
|
|
1106
|
+
? va.localeCompare(vb)
|
|
1107
|
+
: vb.localeCompare(va);
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (displayUsers.length === 0) {
|
|
1091
1112
|
el.innerHTML =
|
|
1092
|
-
'<tr><td colspan="
|
|
1113
|
+
'<tr><td colspan="10" style="text-align:center;color:#888;padding:24px">暂无数据</td></tr>';
|
|
1093
1114
|
if (moreHint) {
|
|
1094
1115
|
moreHint.style.display = "none";
|
|
1095
1116
|
}
|
|
1096
1117
|
return;
|
|
1097
1118
|
}
|
|
1098
1119
|
|
|
1099
|
-
el.innerHTML =
|
|
1120
|
+
el.innerHTML = displayUsers
|
|
1100
1121
|
.map((u, i) => {
|
|
1101
1122
|
const nick = (u.nickname || "")
|
|
1102
1123
|
.replace(/</g, "<")
|
|
@@ -1113,6 +1134,13 @@ function renderTargetTable() {
|
|
|
1113
1134
|
? formatTime(u.latestVideoTime * 1000)
|
|
1114
1135
|
: "-";
|
|
1115
1136
|
const refreshTime = u.refreshTime ? formatTime(u.refreshTime) : "-";
|
|
1137
|
+
const topPlayCount =
|
|
1138
|
+
u.topVideoPlayCount != null && u.topVideoPlayCount > 0
|
|
1139
|
+
? formatNum(u.topVideoPlayCount)
|
|
1140
|
+
: "-";
|
|
1141
|
+
const topPlayCountCell = u.topVideoHref
|
|
1142
|
+
? `<td data-label="最大播放量" style="font-size:11px;color:#888"><a href="${u.topVideoHref}" target="_blank" style="color:#3b82f6;text-decoration:none" title="点击查看视频">${topPlayCount}</a></td>`
|
|
1143
|
+
: `<td data-label="最大播放量" style="font-size:11px;color:#888">${topPlayCount}</td>`;
|
|
1116
1144
|
|
|
1117
1145
|
const editIcon = ' <span style="font-size:10px;opacity:0.5">✏️</span>';
|
|
1118
1146
|
const locationCell = u.modifiedAt
|
|
@@ -1134,12 +1162,13 @@ function renderTargetTable() {
|
|
|
1134
1162
|
|
|
1135
1163
|
return `<tr data-user="${u.uniqueId}">
|
|
1136
1164
|
<td style="color:#9ca3af;font-size:12px;text-align:center" data-label="#">${i + 1}</td>
|
|
1137
|
-
<td class="user-id" data-label="用户名">@${u.uniqueId}</td>
|
|
1165
|
+
<td class="user-id" data-label="用户名"><a href="https://www.tiktok.com/@${u.uniqueId}" target="_blank" style="color:#3b82f6;text-decoration:none">@${u.uniqueId}</a></td>
|
|
1138
1166
|
<td data-label="昵称">${nick}</td>
|
|
1139
1167
|
<td data-label="粉丝">${fans}</td>
|
|
1140
1168
|
<td data-label="视频">${videos}</td>
|
|
1141
1169
|
${locationCell}
|
|
1142
1170
|
<td data-label="确认国家" style="font-size:11px">${confirmedLocation}</td>
|
|
1171
|
+
${topPlayCountCell}
|
|
1143
1172
|
<td data-label="最近发布" style="font-size:11px;color:#888">${latestVideo}</td>
|
|
1144
1173
|
<td data-label="最近刷新" style="font-size:11px;color:#888">${refreshTime}</td>
|
|
1145
1174
|
</tr>`;
|
|
@@ -1325,7 +1354,7 @@ function renderPendingCountryGrid(countries) {
|
|
|
1325
1354
|
<div class="pending-country-item${isUnknown ? "" : " has-target"}"
|
|
1326
1355
|
onclick="filterByPendingCountry('${safeCountry}')">
|
|
1327
1356
|
<div class="country-action-btns">
|
|
1328
|
-
<button class="country-action-btn restore" title="重置为需要预处理" onclick="event.stopPropagation(); resetPendingByCountry('${safeCountry}', ${c.count})">↺</button>
|
|
1357
|
+
<!-- <button class="country-action-btn restore" title="重置为需要预处理" onclick="event.stopPropagation(); resetPendingByCountry('${safeCountry}', ${c.count})">↺</button> -->
|
|
1329
1358
|
<button class="country-action-btn" title="移到毛料库,暂不处理" onclick="event.stopPropagation(); moveCountryJobsToRaw('pending', '${safeCountry}', ${c.count})">✕</button>
|
|
1330
1359
|
</div>
|
|
1331
1360
|
<div class="country-name">${isUnknown ? "🌍 " : ""}${c.country}</div>
|
|
@@ -1836,11 +1865,37 @@ function initTableSorting() {
|
|
|
1836
1865
|
});
|
|
1837
1866
|
}
|
|
1838
1867
|
|
|
1868
|
+
function initTargetTableSorting() {
|
|
1869
|
+
document.querySelectorAll("th.sortable-target").forEach((th) => {
|
|
1870
|
+
th.addEventListener("click", () => {
|
|
1871
|
+
const key = th.dataset.sort;
|
|
1872
|
+
if (!key) return;
|
|
1873
|
+
if (currentTargetSort.key === key) {
|
|
1874
|
+
currentTargetSort.asc = !currentTargetSort.asc;
|
|
1875
|
+
} else {
|
|
1876
|
+
currentTargetSort.key = key;
|
|
1877
|
+
currentTargetSort.asc = true;
|
|
1878
|
+
}
|
|
1879
|
+
// 更新排序指示器
|
|
1880
|
+
document.querySelectorAll("th.sortable-target").forEach((h) => {
|
|
1881
|
+
h.classList.remove("sort-asc", "sort-desc");
|
|
1882
|
+
const icon = h.querySelector(".sort-icon");
|
|
1883
|
+
if (icon) icon.textContent = "↕";
|
|
1884
|
+
});
|
|
1885
|
+
th.classList.add(currentTargetSort.asc ? "sort-asc" : "sort-desc");
|
|
1886
|
+
const icon = th.querySelector(".sort-icon");
|
|
1887
|
+
if (icon) icon.textContent = currentTargetSort.asc ? "↑" : "↓";
|
|
1888
|
+
renderTargetTable();
|
|
1889
|
+
});
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1839
1893
|
// 初始化
|
|
1840
1894
|
fetchStats();
|
|
1841
1895
|
fetchUsers();
|
|
1842
1896
|
fetchClientErrors();
|
|
1843
1897
|
initTableSorting();
|
|
1898
|
+
initTargetTableSorting();
|
|
1844
1899
|
|
|
1845
1900
|
setInterval(fetchStats, 10000);
|
|
1846
1901
|
setInterval(fetchUsers, 10000);
|
|
@@ -327,7 +327,8 @@
|
|
|
327
327
|
<th>视频</th>
|
|
328
328
|
<th>国家</th>
|
|
329
329
|
<th>确认国家</th>
|
|
330
|
-
<th
|
|
330
|
+
<th class="sortable-target" data-sort="topVideoPlayCount">最大播放量 <span class="sort-icon">↕</span></th>
|
|
331
|
+
<th class="sortable-target" data-sort="latestVideoTime">最近发布 <span class="sort-icon">↕</span></th>
|
|
331
332
|
<th>最近刷新</th>
|
|
332
333
|
</tr>
|
|
333
334
|
</thead>
|
|
@@ -619,6 +619,31 @@ th.sortable.sort-desc .sort-icon {
|
|
|
619
619
|
color: #fe2c55;
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
th.sortable-target {
|
|
623
|
+
cursor: pointer;
|
|
624
|
+
user-select: none;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
th.sortable-target:hover {
|
|
628
|
+
color: #fe2c55;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
th.sortable-target .sort-icon {
|
|
632
|
+
font-size: 10px;
|
|
633
|
+
opacity: 0.4;
|
|
634
|
+
margin-left: 2px;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
th.sortable-target.sort-asc .sort-icon {
|
|
638
|
+
opacity: 1;
|
|
639
|
+
color: #fe2c55;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
th.sortable-target.sort-desc .sort-icon {
|
|
643
|
+
opacity: 1;
|
|
644
|
+
color: #fe2c55;
|
|
645
|
+
}
|
|
646
|
+
|
|
622
647
|
td {
|
|
623
648
|
padding: 6px 10px;
|
|
624
649
|
border-bottom: 1px solid #1f1f2a;
|
package/src/watch/server.js
CHANGED
|
@@ -89,9 +89,14 @@ function sendCSV(res, columns, rows) {
|
|
|
89
89
|
res.end(body);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export function startWatchServer(
|
|
92
|
+
export function startWatchServer(
|
|
93
|
+
dataAnchor,
|
|
94
|
+
port = 3000,
|
|
95
|
+
existingStore,
|
|
96
|
+
options = {},
|
|
97
|
+
) {
|
|
93
98
|
return new Promise((_resolve, reject) => {
|
|
94
|
-
const store = existingStore || createStore(dataAnchor);
|
|
99
|
+
const store = existingStore || createStore(dataAnchor, options);
|
|
95
100
|
|
|
96
101
|
function logJob(action, detail) {
|
|
97
102
|
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
@@ -172,7 +177,7 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
172
177
|
.filter(Boolean)
|
|
173
178
|
: null;
|
|
174
179
|
const loggedIn = params.loggedIn === "true";
|
|
175
|
-
const job = store.claimNextJob(
|
|
180
|
+
const job = await store.claimNextJob(
|
|
176
181
|
userId,
|
|
177
182
|
5 * 60 * 1000,
|
|
178
183
|
locations,
|
|
@@ -377,6 +382,7 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
377
382
|
const stats = computeStatsIncremental(store);
|
|
378
383
|
stats.targetLocations = DEFAULT_TARGET_LOCATIONS;
|
|
379
384
|
stats.clientLoginStatus = store.getClientLoginStatus();
|
|
385
|
+
stats.llmSampleOffsets = store.getLlmSampleOffsets(); // 添加偏移量状态
|
|
380
386
|
sendJSON(res, 200, stats);
|
|
381
387
|
return;
|
|
382
388
|
}
|
|
@@ -570,6 +576,8 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
570
576
|
uniqueId: u.uniqueId,
|
|
571
577
|
nickname: u.nickname || "",
|
|
572
578
|
followerCount: u.followerCount || 0,
|
|
579
|
+
topVideoPlayCount: u.topVideoPlayCount || null,
|
|
580
|
+
topVideoHref: u.topVideoHref || null,
|
|
573
581
|
}));
|
|
574
582
|
sendJSON(res, 200, { total: targets.length, users });
|
|
575
583
|
}
|
|
@@ -657,12 +665,32 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
657
665
|
offset: parseInt(params.offset || "0", 10),
|
|
658
666
|
},
|
|
659
667
|
);
|
|
668
|
+
// 确保每个用户对象包含 topVideoPlayCount 和 topVideoHref
|
|
669
|
+
if (result.users && Array.isArray(result.users)) {
|
|
670
|
+
result.users = result.users.map((u) => ({
|
|
671
|
+
...u,
|
|
672
|
+
topVideoPlayCount: u.topVideoPlayCount || null,
|
|
673
|
+
topVideoHref: u.topVideoHref || null,
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
660
676
|
sendJSON(res, 200, result);
|
|
661
677
|
return;
|
|
662
678
|
}
|
|
663
679
|
|
|
664
680
|
// 默认:全量(兼容旧调用)
|
|
665
681
|
const result = store.getTargetUsersByCountry(DEFAULT_TARGET_LOCATIONS);
|
|
682
|
+
// 确保每个用户对象包含 topVideoPlayCount 和 topVideoHref
|
|
683
|
+
if (result.countries && Array.isArray(result.countries)) {
|
|
684
|
+
for (const country of result.countries) {
|
|
685
|
+
if (country.users && Array.isArray(country.users)) {
|
|
686
|
+
country.users = country.users.map((u) => ({
|
|
687
|
+
...u,
|
|
688
|
+
topVideoPlayCount: u.topVideoPlayCount || null,
|
|
689
|
+
topVideoHref: u.topVideoHref || null,
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
666
694
|
sendJSON(res, 200, result);
|
|
667
695
|
return;
|
|
668
696
|
}
|
|
@@ -764,6 +792,41 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
764
792
|
return;
|
|
765
793
|
}
|
|
766
794
|
|
|
795
|
+
// 手动触发从 raw_jobs 补充任务到 jobs
|
|
796
|
+
if (req.method === "POST" && routePath === "/api/raw-jobs/refill") {
|
|
797
|
+
try {
|
|
798
|
+
const body = await readBody(req);
|
|
799
|
+
const locationsParam = body.locations || "";
|
|
800
|
+
const locations = locationsParam
|
|
801
|
+
? locationsParam
|
|
802
|
+
.split(",")
|
|
803
|
+
.map((s) => s.trim().toUpperCase())
|
|
804
|
+
.filter(Boolean)
|
|
805
|
+
: null;
|
|
806
|
+
const limit = body.limit || 500;
|
|
807
|
+
const options = {
|
|
808
|
+
llmScore: !!body.llmScore,
|
|
809
|
+
llmMinScore: body.llmMinScore ?? 60,
|
|
810
|
+
llmSampleSize: body.llmSampleSize ?? 100,
|
|
811
|
+
llmMinReturn: body.llmMinReturn ?? 60,
|
|
812
|
+
llmMaxBatches: body.llmMaxBatches ?? 10,
|
|
813
|
+
};
|
|
814
|
+
const result = await store.refillJobsFromRaw(
|
|
815
|
+
locations,
|
|
816
|
+
limit,
|
|
817
|
+
options,
|
|
818
|
+
);
|
|
819
|
+
if (result.error) {
|
|
820
|
+
sendJSON(res, 400, result);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
sendJSON(res, 200, result);
|
|
824
|
+
} catch (e) {
|
|
825
|
+
sendJSON(res, 400, { error: e.message });
|
|
826
|
+
}
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
767
830
|
if (req.method === "POST" && routePath === "/api/attach-stuck/restore") {
|
|
768
831
|
try {
|
|
769
832
|
const body = await readBody(req);
|