tt-help-cli-ycl 1.4.144 → 1.4.146
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/test-daemon-failover.mjs +61 -4
- package/scripts/test-product-fill-cdp.mjs +168 -0
- package/src/clash/checker.js +41 -0
- package/src/clash/daemon.js +26 -0
- package/src/cli/comments.js +265 -388
- package/src/cli/product-fill.js +255 -128
- package/src/cli/tag.js +143 -334
- package/src/lib/args.js +15 -5
- package/src/lib/constants.js +14 -10
- package/src/lib/shop-scraper.mjs +69 -6
- package/src/watch/data-store.js +129 -6
- package/src/watch/public/app.js +290 -0
- package/src/watch/public/index.html +99 -1
- package/src/watch/public/style.css +202 -0
- package/src/watch/server.js +64 -1
- package/src/watchdog/process-manager.js +11 -6
- package/scripts/test-product-fill.mjs +0 -306
package/src/watch/public/app.js
CHANGED
|
@@ -185,6 +185,7 @@ function renderStats() {
|
|
|
185
185
|
flashEl("statPending", d.pendingUsers);
|
|
186
186
|
flashEl("statError", d.errorUsers);
|
|
187
187
|
flashEl("statTarget", d.targetUsers, { full: true });
|
|
188
|
+
flashEl("statPrecise", d.preciseMerchants, { full: true });
|
|
188
189
|
// 检测目标商家新增,放烟花(仅首页,且开关开启时)
|
|
189
190
|
const hash = window.location.hash.replace(/^#/, "");
|
|
190
191
|
if (
|
|
@@ -1137,6 +1138,10 @@ document.getElementById("statTargetCard").addEventListener("click", () => {
|
|
|
1137
1138
|
navigateToTarget();
|
|
1138
1139
|
});
|
|
1139
1140
|
|
|
1141
|
+
document.getElementById("statPreciseCard").addEventListener("click", () => {
|
|
1142
|
+
navigateToPrecise();
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1140
1145
|
// Hash 路由
|
|
1141
1146
|
window.addEventListener("hashchange", handleRoute);
|
|
1142
1147
|
window.addEventListener("DOMContentLoaded", handleRoute);
|
|
@@ -1290,6 +1295,8 @@ function handleRoute() {
|
|
|
1290
1295
|
showRawPage();
|
|
1291
1296
|
} else if (hash === "#target") {
|
|
1292
1297
|
showTargetPage();
|
|
1298
|
+
} else if (hash === "#precise") {
|
|
1299
|
+
showPrecisePage();
|
|
1293
1300
|
} else if (hash === "#tags") {
|
|
1294
1301
|
showTagsPage();
|
|
1295
1302
|
} else if (hash === "#settings") {
|
|
@@ -1323,6 +1330,7 @@ function showPendingPage() {
|
|
|
1323
1330
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1324
1331
|
document.getElementById("rawPage").classList.remove("active");
|
|
1325
1332
|
document.getElementById("targetPage").classList.remove("active");
|
|
1333
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1326
1334
|
document.getElementById("tagsPage").classList.remove("active");
|
|
1327
1335
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1328
1336
|
fetchPendingByCountry();
|
|
@@ -1335,6 +1343,7 @@ function showUserUpdatePage() {
|
|
|
1335
1343
|
document.getElementById("userUpdatePage").classList.add("active");
|
|
1336
1344
|
document.getElementById("rawPage").classList.remove("active");
|
|
1337
1345
|
document.getElementById("targetPage").classList.remove("active");
|
|
1346
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1338
1347
|
document.getElementById("tagsPage").classList.remove("active");
|
|
1339
1348
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1340
1349
|
fetchUserUpdateByCountry();
|
|
@@ -1354,6 +1363,7 @@ function showRawPage() {
|
|
|
1354
1363
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1355
1364
|
document.getElementById("rawPage").classList.add("active");
|
|
1356
1365
|
document.getElementById("targetPage").classList.remove("active");
|
|
1366
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1357
1367
|
document.getElementById("tagsPage").classList.remove("active");
|
|
1358
1368
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1359
1369
|
fetchRawByCountry();
|
|
@@ -1366,6 +1376,7 @@ function showMainPage() {
|
|
|
1366
1376
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1367
1377
|
document.getElementById("rawPage").classList.remove("active");
|
|
1368
1378
|
document.getElementById("targetPage").classList.remove("active");
|
|
1379
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1369
1380
|
document.getElementById("tagsPage").classList.remove("active");
|
|
1370
1381
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1371
1382
|
document.getElementById("watchdogPage").classList.remove("active");
|
|
@@ -1379,6 +1390,20 @@ function navigateToTarget() {
|
|
|
1379
1390
|
window.location.hash = "#target";
|
|
1380
1391
|
}
|
|
1381
1392
|
|
|
1393
|
+
function navigateToPrecise() {
|
|
1394
|
+
window.location.hash = "#precise";
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// product-fill 任务查询条件表单:折叠 / 展开
|
|
1398
|
+
function togglePreciseCriteria() {
|
|
1399
|
+
const body = document.getElementById("preciseCriteriaBody");
|
|
1400
|
+
const arrow = document.getElementById("preciseCriteriaArrow");
|
|
1401
|
+
if (!body) return;
|
|
1402
|
+
const collapsed = body.style.display === "none";
|
|
1403
|
+
body.style.display = collapsed ? "" : "none";
|
|
1404
|
+
if (arrow) arrow.textContent = collapsed ? "▼" : "▶";
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1382
1407
|
function navigateToTags() {
|
|
1383
1408
|
window.location.hash = "#tags";
|
|
1384
1409
|
}
|
|
@@ -1399,6 +1424,7 @@ function hideAllPages() {
|
|
|
1399
1424
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1400
1425
|
document.getElementById("rawPage").classList.remove("active");
|
|
1401
1426
|
document.getElementById("targetPage").classList.remove("active");
|
|
1427
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1402
1428
|
document.getElementById("tagsPage").classList.remove("active");
|
|
1403
1429
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1404
1430
|
document.getElementById("watchdogPage").classList.remove("active");
|
|
@@ -1858,6 +1884,220 @@ async function submitAddTags() {
|
|
|
1858
1884
|
}
|
|
1859
1885
|
}
|
|
1860
1886
|
|
|
1887
|
+
// ========== 精准商家页面 ==========
|
|
1888
|
+
|
|
1889
|
+
async function showPrecisePage() {
|
|
1890
|
+
hideAllPages();
|
|
1891
|
+
document.getElementById("precisePage").classList.add("active");
|
|
1892
|
+
// 每次进入都默认视频模式
|
|
1893
|
+
setPreciseView("video");
|
|
1894
|
+
showLoading("正在加载精准商家数据...");
|
|
1895
|
+
await loadPreciseUsersPage(false, true);
|
|
1896
|
+
hideLoading();
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
let currentPreciseUsers = [];
|
|
1900
|
+
let currentPreciseTotal = 0;
|
|
1901
|
+
let currentPreciseLoading = false;
|
|
1902
|
+
let currentPreciseAllLoaded = false;
|
|
1903
|
+
let currentPreciseSeq = 0;
|
|
1904
|
+
let currentPreciseView = "video";
|
|
1905
|
+
const PRECISE_PAGE_SIZE = 200;
|
|
1906
|
+
|
|
1907
|
+
async function loadPreciseUsersPage(append = false, skipLoading = false) {
|
|
1908
|
+
if (currentPreciseLoading) return;
|
|
1909
|
+
currentPreciseLoading = true;
|
|
1910
|
+
const seq = ++currentPreciseSeq;
|
|
1911
|
+
const search = document.getElementById("preciseSearchInput")
|
|
1912
|
+
? document.getElementById("preciseSearchInput").value.trim()
|
|
1913
|
+
: "";
|
|
1914
|
+
const offset = append ? currentPreciseUsers.length : 0;
|
|
1915
|
+
if (!append && !skipLoading) showLoading("正在加载数据...");
|
|
1916
|
+
|
|
1917
|
+
let data = null;
|
|
1918
|
+
let error = null;
|
|
1919
|
+
try {
|
|
1920
|
+
let url = `/api/precise-merchants?limit=${PRECISE_PAGE_SIZE}&offset=${offset}`;
|
|
1921
|
+
if (search) url += `&search=${encodeURIComponent(search)}`;
|
|
1922
|
+
const res = await fetch(url);
|
|
1923
|
+
data = await res.json();
|
|
1924
|
+
} catch (e) {
|
|
1925
|
+
error = e;
|
|
1926
|
+
console.error("加载精准商家失败:", e);
|
|
1927
|
+
} finally {
|
|
1928
|
+
if (!append && !skipLoading) hideLoading();
|
|
1929
|
+
currentPreciseLoading = false;
|
|
1930
|
+
if (error || !data) return;
|
|
1931
|
+
if (seq !== currentPreciseSeq && !append) return;
|
|
1932
|
+
if (!append) {
|
|
1933
|
+
currentPreciseUsers = data.users || [];
|
|
1934
|
+
currentPreciseTotal = data.total || 0;
|
|
1935
|
+
} else {
|
|
1936
|
+
currentPreciseUsers = currentPreciseUsers.concat(data.users || []);
|
|
1937
|
+
currentPreciseTotal = data.total || 0;
|
|
1938
|
+
}
|
|
1939
|
+
currentPreciseAllLoaded = currentPreciseUsers.length >= currentPreciseTotal;
|
|
1940
|
+
renderPreciseView();
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function setPreciseView(view) {
|
|
1945
|
+
currentPreciseView = view === "table" ? "table" : "video";
|
|
1946
|
+
const tableBtn = document.getElementById("preciseViewTableBtn");
|
|
1947
|
+
const videoBtn = document.getElementById("preciseViewVideoBtn");
|
|
1948
|
+
if (tableBtn)
|
|
1949
|
+
tableBtn.classList.toggle("active", currentPreciseView === "table");
|
|
1950
|
+
if (videoBtn)
|
|
1951
|
+
videoBtn.classList.toggle("active", currentPreciseView === "video");
|
|
1952
|
+
const videoWrap = document.getElementById("preciseVideoWrap");
|
|
1953
|
+
const tableWrap = document.getElementById("preciseTableWrap");
|
|
1954
|
+
if (videoWrap)
|
|
1955
|
+
videoWrap.style.display = currentPreciseView === "video" ? "" : "none";
|
|
1956
|
+
if (tableWrap)
|
|
1957
|
+
tableWrap.style.display = currentPreciseView === "table" ? "" : "none";
|
|
1958
|
+
renderPreciseView();
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function renderPreciseView() {
|
|
1962
|
+
if (currentPreciseView === "table") {
|
|
1963
|
+
renderPreciseTable();
|
|
1964
|
+
} else {
|
|
1965
|
+
renderPreciseVideos();
|
|
1966
|
+
}
|
|
1967
|
+
updatePreciseMoreHint();
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function updatePreciseMoreHint() {
|
|
1971
|
+
const moreHint = document.getElementById("preciseMoreHint");
|
|
1972
|
+
const totalEl = document.getElementById("precisePageStatTotal");
|
|
1973
|
+
if (totalEl)
|
|
1974
|
+
totalEl.textContent = formatStatNum(currentPreciseTotal, { full: true });
|
|
1975
|
+
if (!moreHint) return;
|
|
1976
|
+
if (currentPreciseTotal === 0) {
|
|
1977
|
+
moreHint.textContent = "";
|
|
1978
|
+
moreHint.style.display = "none";
|
|
1979
|
+
} else if (currentPreciseAllLoaded) {
|
|
1980
|
+
moreHint.textContent = `共 ${currentPreciseTotal} 个,已全部加载`;
|
|
1981
|
+
moreHint.onclick = null;
|
|
1982
|
+
moreHint.style.cursor = "default";
|
|
1983
|
+
moreHint.style.color = "#6b7280";
|
|
1984
|
+
} else {
|
|
1985
|
+
moreHint.textContent = "点击加载更多";
|
|
1986
|
+
moreHint.onclick = () => loadPreciseUsersPage(true);
|
|
1987
|
+
moreHint.style.cursor = "pointer";
|
|
1988
|
+
moreHint.style.color = "#60a5fa";
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function renderPreciseTable() {
|
|
1993
|
+
const el = document.getElementById("preciseTable");
|
|
1994
|
+
if (!el) return;
|
|
1995
|
+
if (!currentPreciseUsers.length) {
|
|
1996
|
+
el.innerHTML =
|
|
1997
|
+
'<tr><td colspan="12" style="text-align:center;color:#666;padding:20px">暂无精准商家</td></tr>';
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
el.innerHTML = currentPreciseUsers
|
|
2001
|
+
.map((u, i) => {
|
|
2002
|
+
const sold = u.productSoldCount ?? 0;
|
|
2003
|
+
const ec = u.ecOver1k ?? 0;
|
|
2004
|
+
const maxEc = u.maxEcPlay ?? 0;
|
|
2005
|
+
const title = (u.productName || "").replace(/</g, "<");
|
|
2006
|
+
return `<tr>
|
|
2007
|
+
<td style="text-align:right;color:#666">${i + 1}</td>
|
|
2008
|
+
<td><a href="https://www.tiktok.com/@${encodeURIComponent(u.uniqueId)}" target="_blank">@${escapeHtml(u.uniqueId)}</a></td>
|
|
2009
|
+
<td>${escapeHtml(u.nickname || "-")}</td>
|
|
2010
|
+
<td>${formatNum(u.followerCount || 0)}</td>
|
|
2011
|
+
<td>${u.videoCount || 0}</td>
|
|
2012
|
+
<td>${u.locationCreated || "-"}</td>
|
|
2013
|
+
<td style="color:#22c55e">${formatNum(sold)}</td>
|
|
2014
|
+
<td style="color:#f87171">${u.productReviewCount ?? 0}</td>
|
|
2015
|
+
<td>${u.productRating != null ? u.productRating : "-"}</td>
|
|
2016
|
+
<td>${ec}</td>
|
|
2017
|
+
<td>${formatNum(maxEc)}</td>
|
|
2018
|
+
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${title}">${title || "-"}</td>
|
|
2019
|
+
</tr>`;
|
|
2020
|
+
})
|
|
2021
|
+
.join("");
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
function renderPreciseVideos() {
|
|
2025
|
+
const wrap = document.getElementById("preciseVideoWrap");
|
|
2026
|
+
if (!wrap) return;
|
|
2027
|
+
if (!currentPreciseUsers.length) {
|
|
2028
|
+
wrap.innerHTML =
|
|
2029
|
+
'<div style="color:#666;text-align:center;padding:24px">暂无精准商家</div>';
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
wrap.innerHTML = currentPreciseUsers
|
|
2033
|
+
.map((u) => {
|
|
2034
|
+
const vids = Array.isArray(u.recentEcVideos) ? u.recentEcVideos : [];
|
|
2035
|
+
const maxPlay = vids.reduce((m, v) => Math.max(m, v.playCount || 0), 0);
|
|
2036
|
+
const sold = u.productSoldCount ?? 0;
|
|
2037
|
+
const review = u.productReviewCount ?? 0;
|
|
2038
|
+
const rating = u.productRating != null ? u.productRating : "-";
|
|
2039
|
+
const userLink = `https://www.tiktok.com/@${encodeURIComponent(u.uniqueId)}`;
|
|
2040
|
+
const tiles = vids
|
|
2041
|
+
.reduce((acc, v) => {
|
|
2042
|
+
const play = v.playCount || 0;
|
|
2043
|
+
const time = v.createTime ? formatShortDate(v.createTime) : "";
|
|
2044
|
+
const isAd = v.isAd ? '<span class="pv-ad">广告</span>' : "";
|
|
2045
|
+
const prod = v.seoUrl
|
|
2046
|
+
? `<a class="pv-product" href="${escapeAttr(v.seoUrl)}" target="_blank" title="${escapeHtml(v.productTitle || "查看商品")}"><span class="pv-product-icon">🛒</span>${escapeHtml((v.productTitle || "查看商品").slice(0, 80))}</a>`
|
|
2047
|
+
: "";
|
|
2048
|
+
acc.push(`<div class="precise-video-item">
|
|
2049
|
+
<a class="precise-video-tile" href="${escapeAttr(v.href || "#")}" target="_blank" title="播放视频">
|
|
2050
|
+
<span class="pv-play">▶</span>
|
|
2051
|
+
<span class="pv-count${maxPlay > 0 && play === maxPlay ? " pv-count-max" : ""}">${formatNum(play)}</span>
|
|
2052
|
+
<span class="pv-time">${time}</span>
|
|
2053
|
+
${isAd}
|
|
2054
|
+
</a>
|
|
2055
|
+
${prod}
|
|
2056
|
+
</div>`);
|
|
2057
|
+
return acc;
|
|
2058
|
+
}, [])
|
|
2059
|
+
.join("");
|
|
2060
|
+
return `<div class="precise-merchant-card">
|
|
2061
|
+
<div class="precise-merchant-head">
|
|
2062
|
+
<a class="pm-user" href="${userLink}" target="_blank">@${escapeHtml(u.uniqueId)}</a>
|
|
2063
|
+
<span class="pm-nick">${escapeHtml(u.nickname || "-")}</span>
|
|
2064
|
+
<span class="pm-country">${u.locationCreated || "-"}</span>
|
|
2065
|
+
<span class="pm-stats">粉丝 ${formatNum(u.followerCount || 0)} · 视频 ${u.videoCount || 0} · 销量 ${formatNum(sold)} · 评论 ${review} · 评分 ${rating} · EC视频 ${vids.length}</span>
|
|
2066
|
+
</div>
|
|
2067
|
+
${tiles ? `<div class="precise-video-grid">${tiles}</div>` : '<div style="color:#666;font-size:12px;padding:6px 0">该商家暂无 EC 视频数据</div>'}
|
|
2068
|
+
</div>`;
|
|
2069
|
+
})
|
|
2070
|
+
.join("");
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function formatShortDate(sec) {
|
|
2074
|
+
if (!sec) return "";
|
|
2075
|
+
const d = new Date(sec * 1000);
|
|
2076
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2077
|
+
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function escapeAttr(str) {
|
|
2081
|
+
return String(str)
|
|
2082
|
+
.replace(/&/g, "&")
|
|
2083
|
+
.replace(/"/g, """)
|
|
2084
|
+
.replace(/</g, "<");
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
function clearPreciseFilters() {
|
|
2088
|
+
const input = document.getElementById("preciseSearchInput");
|
|
2089
|
+
if (input) input.value = "";
|
|
2090
|
+
loadPreciseUsersPage();
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
let preciseSearchTimer = null;
|
|
2094
|
+
document.getElementById("preciseSearchInput").addEventListener("input", () => {
|
|
2095
|
+
if (preciseSearchTimer) clearTimeout(preciseSearchTimer);
|
|
2096
|
+
preciseSearchTimer = setTimeout(() => {
|
|
2097
|
+
loadPreciseUsersPage();
|
|
2098
|
+
}, 300);
|
|
2099
|
+
});
|
|
2100
|
+
|
|
1861
2101
|
// ========== 关键词页面 ==========
|
|
1862
2102
|
|
|
1863
2103
|
let currentTags = [];
|
|
@@ -1879,6 +2119,7 @@ function showTagsPage() {
|
|
|
1879
2119
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
1880
2120
|
document.getElementById("rawPage").classList.remove("active");
|
|
1881
2121
|
document.getElementById("targetPage").classList.remove("active");
|
|
2122
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
1882
2123
|
document.getElementById("tagsPage").classList.add("active");
|
|
1883
2124
|
document.getElementById("settingsPage").classList.remove("active");
|
|
1884
2125
|
showLoading("正在加载关键词...");
|
|
@@ -2353,6 +2594,7 @@ function showTargetPage() {
|
|
|
2353
2594
|
document.getElementById("userUpdatePage").classList.remove("active");
|
|
2354
2595
|
document.getElementById("rawPage").classList.remove("active");
|
|
2355
2596
|
document.getElementById("targetPage").classList.add("active");
|
|
2597
|
+
document.getElementById("precisePage").classList.remove("active");
|
|
2356
2598
|
document.getElementById("tagsPage").classList.remove("active");
|
|
2357
2599
|
document.getElementById("settingsPage").classList.remove("active");
|
|
2358
2600
|
showLoading("正在加载目标商家数据...");
|
|
@@ -3569,12 +3811,14 @@ fetchUsers();
|
|
|
3569
3811
|
fetchClientErrors();
|
|
3570
3812
|
initTableSorting();
|
|
3571
3813
|
initTargetTableSorting();
|
|
3814
|
+
fetchWatchdogBadge();
|
|
3572
3815
|
|
|
3573
3816
|
// 错峰轮询(服务端已缓存,瞬时响应无压力)
|
|
3574
3817
|
setInterval(fetchStats, 10000);
|
|
3575
3818
|
setTimeout(() => setInterval(fetchUsers, 10000), 2500);
|
|
3576
3819
|
setTimeout(() => setInterval(fetchClientErrors, 10000), 5000);
|
|
3577
3820
|
setTimeout(() => setInterval(refreshUserUpdateIfActive, 10000), 7500);
|
|
3821
|
+
setTimeout(() => setInterval(fetchWatchdogBadge, 30000), 9000);
|
|
3578
3822
|
|
|
3579
3823
|
// ========== 看门狗页面 ==========
|
|
3580
3824
|
|
|
@@ -3598,6 +3842,7 @@ async function fetchWatchdogData() {
|
|
|
3598
3842
|
try {
|
|
3599
3843
|
const res = await fetch("/api/watchdog/clients");
|
|
3600
3844
|
_wdData = await res.json();
|
|
3845
|
+
updateWatchdogBadge(_wdData.clients || [], _wdData.clientCommands || {});
|
|
3601
3846
|
renderWatchdogDashboard(
|
|
3602
3847
|
_wdData.clients || [],
|
|
3603
3848
|
_wdData.clientCommands || {},
|
|
@@ -3613,6 +3858,51 @@ async function fetchWatchdogData() {
|
|
|
3613
3858
|
}
|
|
3614
3859
|
}
|
|
3615
3860
|
|
|
3861
|
+
// ── 导航栏"看门狗"离线 badge ──
|
|
3862
|
+
// 离线数口径与看门狗页面卡片一致:
|
|
3863
|
+
// = 已注册但离线的客户端数 + 配置了命令但从未注册(renderOfflineCard)的 userId 数
|
|
3864
|
+
function updateWatchdogBadge(clients, clientCommands) {
|
|
3865
|
+
const badge = document.getElementById("wdOfflineBadge");
|
|
3866
|
+
if (!badge) return;
|
|
3867
|
+
const offlineClients = clients.filter(function (c) {
|
|
3868
|
+
return !c.online;
|
|
3869
|
+
}).length;
|
|
3870
|
+
const registeredUids = {};
|
|
3871
|
+
clients.forEach(function (c) {
|
|
3872
|
+
if (c.config && c.config.userId) {
|
|
3873
|
+
registeredUids[c.config.userId] = true;
|
|
3874
|
+
}
|
|
3875
|
+
});
|
|
3876
|
+
let neverRegistered = 0;
|
|
3877
|
+
for (var uid in clientCommands) {
|
|
3878
|
+
if (
|
|
3879
|
+
clientCommands[uid] &&
|
|
3880
|
+
clientCommands[uid].length > 0 &&
|
|
3881
|
+
!registeredUids[uid]
|
|
3882
|
+
) {
|
|
3883
|
+
neverRegistered++;
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
const total = offlineClients + neverRegistered;
|
|
3887
|
+
if (total > 0) {
|
|
3888
|
+
badge.textContent = total;
|
|
3889
|
+
badge.style.display = "";
|
|
3890
|
+
} else {
|
|
3891
|
+
badge.style.display = "none";
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
// 独立轻量轮询:导航栏 badge 在任何页面都实时显示离线数
|
|
3896
|
+
async function fetchWatchdogBadge() {
|
|
3897
|
+
try {
|
|
3898
|
+
const res = await fetch("/api/watchdog/clients");
|
|
3899
|
+
const data = await res.json();
|
|
3900
|
+
updateWatchdogBadge(data.clients || [], data.clientCommands || {});
|
|
3901
|
+
} catch (e) {
|
|
3902
|
+
// 静默失败,保留旧值
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3616
3906
|
// ── 渲染辅助 ──
|
|
3617
3907
|
|
|
3618
3908
|
function tag(tagName, attrs, content) {
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
<span>🎆</span>
|
|
22
22
|
</label>
|
|
23
23
|
<a class="script-link" onclick="navigateToSettings()" style="cursor:pointer">⚙️ 配置</a>
|
|
24
|
-
<a class="script-link" onclick="navigateToWatchdog()" style="cursor:pointer"
|
|
24
|
+
<a class="script-link" onclick="navigateToWatchdog()" style="cursor:pointer;position:relative"
|
|
25
|
+
title="看门狗离线客户端数">🐕 看门狗<span id="wdOfflineBadge"
|
|
26
|
+
style="display:none;position:absolute;top:-9px;right:-8px;background:#ef4444;color:#fff;border-radius:10px;font-size:10px;font-weight:700;line-height:1;padding:3px 6px;box-shadow:0 0 6px rgba(239,68,68,0.6);white-space:nowrap">0</span></a>
|
|
25
27
|
<span class="status" id="lastUpdate">--</span>
|
|
26
28
|
</div>
|
|
27
29
|
</div>
|
|
@@ -59,6 +61,10 @@
|
|
|
59
61
|
<div class="label">目标商家</div>
|
|
60
62
|
<div class="value target" id="statTarget">0</div>
|
|
61
63
|
</div>
|
|
64
|
+
<div class="stat-card clickable" id="statPreciseCard" onclick="navigateToPrecise()">
|
|
65
|
+
<div class="label">精准商家</div>
|
|
66
|
+
<div class="value target" id="statPrecise">0</div>
|
|
67
|
+
</div>
|
|
62
68
|
<div class="stat-card clickable pending-card" id="statCommentTaskCard">
|
|
63
69
|
<div class="label">待评论任务</div>
|
|
64
70
|
<div class="value rose" id="statCommentTask">0</div>
|
|
@@ -379,6 +385,98 @@
|
|
|
379
385
|
</div>
|
|
380
386
|
<div id="backToTop" title="回到顶部(可拖动)">↑</div>
|
|
381
387
|
</div>
|
|
388
|
+
<div id="precisePage">
|
|
389
|
+
<div class="stats" style="margin-bottom:16px">
|
|
390
|
+
<div class="stat-card">
|
|
391
|
+
<div class="label">精准商家</div>
|
|
392
|
+
<div class="value target" id="precisePageStatTotal">0</div>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="stat-card clickable pending-card" onclick="navigateToMain()" style="background:rgba(167,139,250,0.1)">
|
|
395
|
+
<div class="label">← 返回主页面</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
<!-- 精准商家检索条件(静态表单展示,与 src/watch/data-store.js getPreciseMerchantsFromDb 保持一致) -->
|
|
399
|
+
<div style="margin-bottom:16px;background:#1a1a24;border:1px solid #333;border-radius:8px;overflow:hidden">
|
|
400
|
+
<div
|
|
401
|
+
style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;cursor:pointer;user-select:none"
|
|
402
|
+
onclick="togglePreciseCriteria()" title="点击展开/收起">
|
|
403
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
404
|
+
<span style="color:#f59e0b;font-size:14px">🎯</span>
|
|
405
|
+
<span style="font-size:13px;color:#e0e0e0;font-weight:600">精准商家检索条件</span>
|
|
406
|
+
<span style="font-size:11px;color:#666">(阈值参数 · jobs 表)</span>
|
|
407
|
+
</div>
|
|
408
|
+
<span id="preciseCriteriaArrow" style="color:#888;font-size:12px">▼</span>
|
|
409
|
+
</div>
|
|
410
|
+
<div id="preciseCriteriaBody" style="padding:2px 14px 14px">
|
|
411
|
+
<div style="font-size:11px;color:#888;margin-bottom:10px">以下阈值条件全部满足(AND),其余条件为固有规则:</div>
|
|
412
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px">
|
|
413
|
+
<div style="background:#14141d;border:1px solid #2a2a3a;border-radius:8px;padding:10px 12px">
|
|
414
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
|
415
|
+
<span style="font-size:12px;color:#60a5fa;font-weight:600">商品评论数</span>
|
|
416
|
+
<span style="font-size:10px;color:#666;font-family:monospace">product_review_count</span>
|
|
417
|
+
</div>
|
|
418
|
+
<div style="margin-top:6px;font-size:16px;color:#4ade80;font-weight:700">= 0</div>
|
|
419
|
+
<div style="margin-top:4px;font-size:11px;color:#888;line-height:1.5">商品评论数为 0</div>
|
|
420
|
+
</div>
|
|
421
|
+
<div style="background:#14141d;border:1px solid #2a2a3a;border-radius:8px;padding:10px 12px">
|
|
422
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
|
423
|
+
<span style="font-size:12px;color:#60a5fa;font-weight:600">商品销量</span>
|
|
424
|
+
<span style="font-size:10px;color:#666;font-family:monospace">product_sold_count</span>
|
|
425
|
+
</div>
|
|
426
|
+
<div style="margin-top:6px;font-size:16px;color:#4ade80;font-weight:700">> 10</div>
|
|
427
|
+
<div style="margin-top:4px;font-size:11px;color:#888;line-height:1.5">商品销量大于 10</div>
|
|
428
|
+
</div>
|
|
429
|
+
<div style="background:#14141d;border:1px solid #2a2a3a;border-radius:8px;padding:10px 12px">
|
|
430
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
|
431
|
+
<span style="font-size:12px;color:#60a5fa;font-weight:600">EC 视频播放量</span>
|
|
432
|
+
<span style="font-size:10px;color:#666;font-family:monospace">playCount</span>
|
|
433
|
+
</div>
|
|
434
|
+
<div style="margin-top:6px;font-size:16px;color:#4ade80;font-weight:700">> 1000</div>
|
|
435
|
+
<div style="margin-top:4px;font-size:11px;color:#888;line-height:1.5">最近 7 天发布的 EC 视频(isECVideo)中,存在播放量超过
|
|
436
|
+
1000 的视频</div>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
<div class="table-wrap">
|
|
442
|
+
<div class="precise-list-header">
|
|
443
|
+
<h3>精准商家列表</h3>
|
|
444
|
+
<div class="view-toggle">
|
|
445
|
+
<button id="preciseViewTableBtn" onclick="setPreciseView('table')">表格</button>
|
|
446
|
+
<button id="preciseViewVideoBtn" onclick="setPreciseView('video')" class="active">视频</button>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
<div class="controls">
|
|
450
|
+
<input type="text" id="preciseSearchInput" placeholder="搜索精准商家用户名 / 昵称...">
|
|
451
|
+
<button onclick="clearPreciseFilters()">清空筛选</button>
|
|
452
|
+
</div>
|
|
453
|
+
<div id="preciseVideoWrap"></div>
|
|
454
|
+
<div class="table-scroll" id="preciseTableWrap" style="display:none">
|
|
455
|
+
<table>
|
|
456
|
+
<thead>
|
|
457
|
+
<tr>
|
|
458
|
+
<th style="width:48px">#</th>
|
|
459
|
+
<th>用户名</th>
|
|
460
|
+
<th>昵称</th>
|
|
461
|
+
<th>粉丝</th>
|
|
462
|
+
<th>视频</th>
|
|
463
|
+
<th>国家</th>
|
|
464
|
+
<th>销量</th>
|
|
465
|
+
<th>评论</th>
|
|
466
|
+
<th>评分</th>
|
|
467
|
+
<th>EC超1k视频数</th>
|
|
468
|
+
<th>最高EC播放</th>
|
|
469
|
+
<th>商品</th>
|
|
470
|
+
</tr>
|
|
471
|
+
</thead>
|
|
472
|
+
<tbody id="preciseTable"></tbody>
|
|
473
|
+
</table>
|
|
474
|
+
</div>
|
|
475
|
+
<div id="preciseMoreHint" onclick="loadPreciseUsersPage(true)"
|
|
476
|
+
style="text-align:center;padding:10px 8px;font-size:13px;color:#6b7280;cursor:default;user-select:none;transition:color 0.2s">
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
382
480
|
<div id="tagsPage">
|
|
383
481
|
<div class="stats" style="margin-bottom:16px">
|
|
384
482
|
<div class="stat-card">
|