tt-help-cli-ycl 1.4.150 → 1.4.152
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-api-interceptor-captcha.mjs +134 -0
- package/scripts/test-photo-post.mjs +156 -0
- package/scripts/test-precheck-perf.mjs +270 -0
- package/scripts/test-product-fill-cdp.mjs +44 -0
- package/scripts/test-product-fill-db.mjs +43 -0
- package/scripts/test-sample-all-fail.mjs +142 -0
- package/scripts/test-seller-any-country.mjs +167 -0
- package/src/cli/explore.js +22 -2
- package/src/cli/product-fill.js +13 -1
- package/src/cli/refresh-mini.js +11 -0
- package/src/cli/refresh.js +11 -0
- package/src/lib/api-interceptor.js +25 -1
- package/src/lib/args.js +26 -0
- package/src/lib/constants.js +13 -2
- package/src/lib/shop-scraper.mjs +79 -0
- package/src/scraper/auto-core.js +61 -37
- package/src/scraper/explore-core.js +45 -4
- package/src/videos/core.js +5 -0
- package/src/watch/data-store.js +67 -20
- package/src/watch/db-columns.js +1 -0
- package/src/watch/db-schema.js +3 -0
- package/src/watch/db-tags.js +15 -5
- package/src/watch/public/app.js +89 -17
- package/src/watch/public/index.html +60 -55
- package/src/watch/public/style.css +80 -4
- package/src/watch/server.js +14 -0
package/src/watch/public/app.js
CHANGED
|
@@ -996,7 +996,10 @@ function escapeJsString(str) {
|
|
|
996
996
|
}
|
|
997
997
|
|
|
998
998
|
document.addEventListener("keydown", (e) => {
|
|
999
|
-
if (e.key === "Escape")
|
|
999
|
+
if (e.key === "Escape") {
|
|
1000
|
+
closeAddModal();
|
|
1001
|
+
closeSkuModal();
|
|
1002
|
+
}
|
|
1000
1003
|
});
|
|
1001
1004
|
|
|
1002
1005
|
document.getElementById("userTable").addEventListener("click", (e) => {
|
|
@@ -1527,16 +1530,12 @@ function normalizeCountryInput(str) {
|
|
|
1527
1530
|
function countryFlag(code) {
|
|
1528
1531
|
if (!/^[A-Za-z]{2}$/.test(code)) return "🏳️";
|
|
1529
1532
|
return String.fromCodePoint(
|
|
1530
|
-
...[...code.toUpperCase()].map(
|
|
1531
|
-
(c) => 0x1f1e6 + c.charCodeAt(0) - 65,
|
|
1532
|
-
),
|
|
1533
|
+
...[...code.toUpperCase()].map((c) => 0x1f1e6 + c.charCodeAt(0) - 65),
|
|
1533
1534
|
);
|
|
1534
1535
|
}
|
|
1535
1536
|
|
|
1536
1537
|
function targetLocationsDirty() {
|
|
1537
|
-
return (
|
|
1538
|
-
_targetLocations.join(",") !== _targetLocationsSaved.join(",")
|
|
1539
|
-
);
|
|
1538
|
+
return _targetLocations.join(",") !== _targetLocationsSaved.join(",");
|
|
1540
1539
|
}
|
|
1541
1540
|
|
|
1542
1541
|
function updateTargetLocationsStatus() {
|
|
@@ -1625,10 +1624,7 @@ function bindLocChipDrag(grid) {
|
|
|
1625
1624
|
if (_dragLoc == null || chip.dataset.loc === _dragLoc) return;
|
|
1626
1625
|
const rect = chip.getBoundingClientRect();
|
|
1627
1626
|
const before = e.clientX < rect.left + rect.width / 2;
|
|
1628
|
-
moveTargetLocation(
|
|
1629
|
-
_dragLoc,
|
|
1630
|
-
Number(chip.dataset.idx) + (before ? 0 : 1),
|
|
1631
|
-
);
|
|
1627
|
+
moveTargetLocation(_dragLoc, Number(chip.dataset.idx) + (before ? 0 : 1));
|
|
1632
1628
|
});
|
|
1633
1629
|
});
|
|
1634
1630
|
}
|
|
@@ -1652,7 +1648,9 @@ function removeTargetLocation(loc) {
|
|
|
1652
1648
|
}
|
|
1653
1649
|
|
|
1654
1650
|
function addTargetLocation(locRaw) {
|
|
1655
|
-
const loc = String(locRaw || "")
|
|
1651
|
+
const loc = String(locRaw || "")
|
|
1652
|
+
.trim()
|
|
1653
|
+
.toUpperCase();
|
|
1656
1654
|
if (!/^[A-Z]{2}$/.test(loc)) return false;
|
|
1657
1655
|
if (_targetLocations.includes(loc)) return false;
|
|
1658
1656
|
_targetLocations.push(loc); // 新增排最后(优先级最低),可拖动调整
|
|
@@ -1675,9 +1673,7 @@ function renderCountryPicker() {
|
|
|
1675
1673
|
const selected = new Set(_targetLocations);
|
|
1676
1674
|
const grid = document.getElementById("countryPickerGrid");
|
|
1677
1675
|
if (grid) {
|
|
1678
|
-
grid.innerHTML = AVAILABLE_COUNTRIES.filter(
|
|
1679
|
-
(loc) => !selected.has(loc),
|
|
1680
|
-
)
|
|
1676
|
+
grid.innerHTML = AVAILABLE_COUNTRIES.filter((loc) => !selected.has(loc))
|
|
1681
1677
|
.map(
|
|
1682
1678
|
(loc) =>
|
|
1683
1679
|
`<button type="button" class="loc-option" onclick="addTargetLocation('${loc}')">${countryFlag(loc)} ${loc}</button>`,
|
|
@@ -2157,7 +2153,7 @@ function renderPreciseTable() {
|
|
|
2157
2153
|
if (!el) return;
|
|
2158
2154
|
if (!currentPreciseUsers.length) {
|
|
2159
2155
|
el.innerHTML =
|
|
2160
|
-
'<tr><td colspan="
|
|
2156
|
+
'<tr><td colspan="13" style="text-align:center;color:#666;padding:20px">暂无精准商家</td></tr>';
|
|
2161
2157
|
return;
|
|
2162
2158
|
}
|
|
2163
2159
|
el.innerHTML = currentPreciseUsers
|
|
@@ -2166,6 +2162,10 @@ function renderPreciseTable() {
|
|
|
2166
2162
|
const ec = u.ecOver1k ?? 0;
|
|
2167
2163
|
const maxEc = u.maxEcPlay ?? 0;
|
|
2168
2164
|
const title = (u.productName || "").replace(/</g, "<");
|
|
2165
|
+
const skus = Array.isArray(u.productSkus) ? u.productSkus : [];
|
|
2166
|
+
const skuCell = skus.length
|
|
2167
|
+
? `<span class="sku-cell" onclick="openSkuModal(${i})" title="点击查看 ${skus.length} 个 SKU 明细">📦 ${skus.length} · 库${formatNum(u.skuTotalStock || 0)}</span>`
|
|
2168
|
+
: '<span style="color:#555">-</span>';
|
|
2169
2169
|
return `<tr>
|
|
2170
2170
|
<td style="text-align:right;color:#666">${i + 1}</td>
|
|
2171
2171
|
<td><a href="https://www.tiktok.com/@${encodeURIComponent(u.uniqueId)}" target="_blank">@${escapeHtml(u.uniqueId)}</a></td>
|
|
@@ -2176,6 +2176,7 @@ function renderPreciseTable() {
|
|
|
2176
2176
|
<td style="color:#22c55e">${formatNum(sold)}</td>
|
|
2177
2177
|
<td style="color:#f87171">${u.productReviewCount ?? 0}</td>
|
|
2178
2178
|
<td>${u.productRating != null ? u.productRating : "-"}</td>
|
|
2179
|
+
<td>${skuCell}</td>
|
|
2179
2180
|
<td>${ec}</td>
|
|
2180
2181
|
<td>${formatNum(maxEc)}</td>
|
|
2181
2182
|
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${title}">${title || "-"}</td>
|
|
@@ -2184,6 +2185,72 @@ function renderPreciseTable() {
|
|
|
2184
2185
|
.join("");
|
|
2185
2186
|
}
|
|
2186
2187
|
|
|
2188
|
+
// ── SKU 明细弹窗(精准商家页)──
|
|
2189
|
+
function openSkuModal(idx) {
|
|
2190
|
+
const u = currentPreciseUsers[idx];
|
|
2191
|
+
if (!u) return;
|
|
2192
|
+
const skus = Array.isArray(u.productSkus) ? u.productSkus : [];
|
|
2193
|
+
if (!skus.length) return;
|
|
2194
|
+
let overlay = document.getElementById("skuModalOverlay");
|
|
2195
|
+
if (overlay) overlay.remove();
|
|
2196
|
+
overlay = document.createElement("div");
|
|
2197
|
+
overlay.id = "skuModalOverlay";
|
|
2198
|
+
overlay.className = "modal-overlay";
|
|
2199
|
+
const totalStock = skus.reduce((s, x) => s + (x.stock || 0), 0);
|
|
2200
|
+
const rows = skus
|
|
2201
|
+
.map((s) => {
|
|
2202
|
+
const stock = s.stock ?? null;
|
|
2203
|
+
const stockCls =
|
|
2204
|
+
stock === null
|
|
2205
|
+
? ""
|
|
2206
|
+
: stock === 0 || s.stockStatus === 2
|
|
2207
|
+
? "sku-oos"
|
|
2208
|
+
: stock <= 20
|
|
2209
|
+
? "sku-low"
|
|
2210
|
+
: "sku-ok";
|
|
2211
|
+
const stockText =
|
|
2212
|
+
stock === null
|
|
2213
|
+
? "-"
|
|
2214
|
+
: stock === 0 || s.stockStatus === 2
|
|
2215
|
+
? "售罄"
|
|
2216
|
+
: formatNum(stock);
|
|
2217
|
+
return `<tr>
|
|
2218
|
+
<td class="sku-spec" title="${escapeAttr(s.specs || "")}">${escapeHtml(s.specs || "-")}</td>
|
|
2219
|
+
<td class="sku-id" title="${escapeAttr(s.skuId || "")}">${escapeHtml(String(s.skuId || "-").slice(-6))}</td>
|
|
2220
|
+
<td class="${stockCls}">${stockText}</td>
|
|
2221
|
+
<td>${s.stockStatus ?? "-"}</td>
|
|
2222
|
+
</tr>`;
|
|
2223
|
+
})
|
|
2224
|
+
.join("");
|
|
2225
|
+
overlay.innerHTML = `
|
|
2226
|
+
<div class="modal sku-modal">
|
|
2227
|
+
<h3>📦 @${escapeHtml(u.uniqueId)} 的 SKU(${skus.length} 个)</h3>
|
|
2228
|
+
<div class="sku-modal-meta">
|
|
2229
|
+
${escapeHtml(u.productName || "") ? `<div class="sku-modal-name">${escapeHtml(u.productName)}</div>` : ""}
|
|
2230
|
+
<div>总库存 <b>${formatNum(totalStock)}</b> · 销量 <b style="color:#22c55e">${formatNum(u.productSoldCount ?? 0)}</b> · 评分 <b>${u.productRating ?? "-"}</b></div>
|
|
2231
|
+
${u.topVideoSeoUrl ? `<a href="${escapeAttr(u.topVideoSeoUrl)}" target="_blank" class="sku-modal-link">🛒 打开商品页</a>` : ""}
|
|
2232
|
+
</div>
|
|
2233
|
+
<div class="sku-table-wrap">
|
|
2234
|
+
<table class="sku-table">
|
|
2235
|
+
<thead><tr><th>规格</th><th>SKU尾号</th><th>库存</th><th>状态</th></tr></thead>
|
|
2236
|
+
<tbody>${rows}</tbody>
|
|
2237
|
+
</table>
|
|
2238
|
+
</div>
|
|
2239
|
+
<div class="btn-row" style="margin-top:14px">
|
|
2240
|
+
<button class="btn-cancel" onclick="closeSkuModal()">关闭</button>
|
|
2241
|
+
</div>
|
|
2242
|
+
</div>`;
|
|
2243
|
+
document.body.appendChild(overlay);
|
|
2244
|
+
overlay.addEventListener("click", (e) => {
|
|
2245
|
+
if (e.target === overlay) closeSkuModal();
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function closeSkuModal() {
|
|
2250
|
+
const overlay = document.getElementById("skuModalOverlay");
|
|
2251
|
+
if (overlay) overlay.remove();
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2187
2254
|
function renderPreciseVideos() {
|
|
2188
2255
|
const wrap = document.getElementById("preciseVideoWrap");
|
|
2189
2256
|
if (!wrap) return;
|
|
@@ -2200,6 +2267,11 @@ function renderPreciseVideos() {
|
|
|
2200
2267
|
const review = u.productReviewCount ?? 0;
|
|
2201
2268
|
const rating = u.productRating != null ? u.productRating : "-";
|
|
2202
2269
|
const userLink = `https://www.tiktok.com/@${encodeURIComponent(u.uniqueId)}`;
|
|
2270
|
+
const skus = Array.isArray(u.productSkus) ? u.productSkus : [];
|
|
2271
|
+
const idx = currentPreciseUsers.indexOf(u);
|
|
2272
|
+
const skuBadge = skus.length
|
|
2273
|
+
? ` · <span class="sku-cell" onclick="event.stopPropagation();openSkuModal(${idx})" title="点击查看 ${skus.length} 个 SKU 明细">📦 SKU ${skus.length} · 库${formatNum(u.skuTotalStock || skus.reduce((s, x) => s + (x.stock || 0), 0))}</span>`
|
|
2274
|
+
: "";
|
|
2203
2275
|
const tiles = vids
|
|
2204
2276
|
.reduce((acc, v) => {
|
|
2205
2277
|
const play = v.playCount || 0;
|
|
@@ -2225,7 +2297,7 @@ function renderPreciseVideos() {
|
|
|
2225
2297
|
<a class="pm-user" href="${userLink}" target="_blank">@${escapeHtml(u.uniqueId)}</a>
|
|
2226
2298
|
<span class="pm-nick">${escapeHtml(u.nickname || "-")}</span>
|
|
2227
2299
|
<span class="pm-country">${u.locationCreated || "-"}</span>
|
|
2228
|
-
<span class="pm-stats">粉丝 ${formatNum(u.followerCount || 0)} · 视频 ${u.videoCount || 0} · 销量 ${formatNum(sold)} · 评论 ${review} · 评分 ${rating} · EC视频 ${vids.length}</span>
|
|
2300
|
+
<span class="pm-stats">粉丝 ${formatNum(u.followerCount || 0)} · 视频 ${u.videoCount || 0} · 销量 ${formatNum(sold)} · 评论 ${review} · 评分 ${rating} · EC视频 ${vids.length}${skuBadge}</span>
|
|
2229
2301
|
</div>
|
|
2230
2302
|
${tiles ? `<div class="precise-video-grid">${tiles}</div>` : '<div style="color:#666;font-size:12px;padding:6px 0">该商家暂无 EC 视频数据</div>'}
|
|
2231
2303
|
</div>`;
|
|
@@ -464,6 +464,7 @@
|
|
|
464
464
|
<th>销量</th>
|
|
465
465
|
<th>评论</th>
|
|
466
466
|
<th>评分</th>
|
|
467
|
+
<th>SKU</th>
|
|
467
468
|
<th>EC超1k视频数</th>
|
|
468
469
|
<th>最高EC播放</th>
|
|
469
470
|
<th>商品</th>
|
|
@@ -557,70 +558,74 @@
|
|
|
557
558
|
</div>
|
|
558
559
|
<div class="settings-layout">
|
|
559
560
|
<div class="settings-nav" id="settingsNav">
|
|
560
|
-
<button type="button" class="settings-nav-item active" data-panel="llm" onclick="switchSettingsPanel('llm')">⚙️
|
|
561
|
-
|
|
561
|
+
<button type="button" class="settings-nav-item active" data-panel="llm" onclick="switchSettingsPanel('llm')">⚙️
|
|
562
|
+
LLM 模型</button>
|
|
563
|
+
<button type="button" class="settings-nav-item" data-panel="countries"
|
|
564
|
+
onclick="switchSettingsPanel('countries')">🎯 目标国家</button>
|
|
562
565
|
</div>
|
|
563
566
|
<div class="settings-panels">
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
567
|
+
<div class="settings-panel active" id="settingsPanel-llm">
|
|
568
|
+
<div class="settings-card">
|
|
569
|
+
<h3>⚙️ LLM 模型配置</h3>
|
|
570
|
+
<p class="settings-hint">切换后立即生效,无需重启服务。配置保存在数据库中。</p>
|
|
571
|
+
<div class="settings-form">
|
|
572
|
+
<div class="settings-field">
|
|
573
|
+
<label>服务商</label>
|
|
574
|
+
<select id="settingsProvider" onchange="onProviderChange()"></select>
|
|
575
|
+
</div>
|
|
576
|
+
<div class="settings-field">
|
|
577
|
+
<label>模型</label>
|
|
578
|
+
<select id="settingsModel"></select>
|
|
579
|
+
</div>
|
|
580
|
+
<div class="settings-field">
|
|
581
|
+
<label>API 地址</label>
|
|
582
|
+
<input type="text" id="settingsUrl" placeholder="https://..." readonly>
|
|
583
|
+
<span class="settings-note">选服务商后自动填入,也可手动修改</span>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="settings-field">
|
|
586
|
+
<label>API Key</label>
|
|
587
|
+
<div class="settings-key-row">
|
|
588
|
+
<input type="password" id="settingsApiKey" placeholder="sk-...">
|
|
589
|
+
<button id="settingsToggleKey" onclick="toggleApiKeyVisibility()" title="显示/隐藏">👁</button>
|
|
590
|
+
</div>
|
|
591
|
+
<span class="settings-note" id="settingsKeyHint"></span>
|
|
592
|
+
</div>
|
|
593
|
+
<div class="settings-status" id="settingsStatus"></div>
|
|
594
|
+
<div class="btn-row">
|
|
595
|
+
<button class="btn-submit" onclick="saveLlmConfig()">保存配置</button>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
587
598
|
</div>
|
|
588
|
-
<span class="settings-note" id="settingsKeyHint"></span>
|
|
589
|
-
</div>
|
|
590
|
-
<div class="settings-status" id="settingsStatus"></div>
|
|
591
|
-
<div class="btn-row">
|
|
592
|
-
<button class="btn-submit" onclick="saveLlmConfig()">保存配置</button>
|
|
593
599
|
</div>
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
600
|
+
<div class="settings-panel" id="settingsPanel-countries">
|
|
601
|
+
<div class="settings-card">
|
|
602
|
+
<h3>🎯 目标国家</h3>
|
|
603
|
+
<p class="settings-hint">设置采集的目标国家列表及领取优先级,保存后写入通用配置文件(~/.tt-help-config.json),立即生效。列表顺序即任务领取优先级:越靠前越先领取。
|
|
604
|
+
</p>
|
|
605
|
+
<div class="settings-form">
|
|
606
|
+
<div class="settings-field">
|
|
607
|
+
<label>目标国家(拖动卡片排序,位置即领取优先级:越靠前越先领取)</label>
|
|
608
|
+
<div class="loc-chips" id="targetLocationsGrid"></div>
|
|
609
|
+
<div class="country-picker" id="countryPicker" style="display:none">
|
|
610
|
+
<div class="country-picker-title">选择要添加的国家(新加的排最后,可拖动调整优先级)</div>
|
|
611
|
+
<div class="loc-grid" id="countryPickerGrid"></div>
|
|
612
|
+
<div class="custom-loc-row">
|
|
613
|
+
<input type="text" id="customCountryInput" class="custom-loc-input" placeholder="其他代码,如 GB"
|
|
614
|
+
maxlength="2" onkeydown="if(event.key==='Enter')addCustomCountry()">
|
|
615
|
+
<button type="button" class="btn-submit" onclick="addCustomCountry()">添加</button>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
<div class="settings-status" id="targetLocationsStatus"></div>
|
|
620
|
+
<div class="btn-row">
|
|
621
|
+
<button class="btn-submit" onclick="saveTargetLocations()">保存目标国家</button>
|
|
622
|
+
<button class="btn-cancel" onclick="resetTargetLocations()">恢复默认</button>
|
|
623
|
+
</div>
|
|
611
624
|
</div>
|
|
612
625
|
</div>
|
|
613
626
|
</div>
|
|
614
|
-
<div class="settings-status" id="targetLocationsStatus"></div>
|
|
615
|
-
<div class="btn-row">
|
|
616
|
-
<button class="btn-submit" onclick="saveTargetLocations()">保存目标国家</button>
|
|
617
|
-
<button class="btn-cancel" onclick="resetTargetLocations()">恢复默认</button>
|
|
618
|
-
</div>
|
|
619
627
|
</div>
|
|
620
628
|
</div>
|
|
621
|
-
</div>
|
|
622
|
-
</div>
|
|
623
|
-
</div>
|
|
624
629
|
</div>
|
|
625
630
|
<div id="watchdogPage">
|
|
626
631
|
<div class="stats" style="margin-bottom:16px">
|
|
@@ -1730,10 +1730,7 @@ td.user-id:hover {
|
|
|
1730
1730
|
color: #999;
|
|
1731
1731
|
cursor: pointer;
|
|
1732
1732
|
font-size: 13px;
|
|
1733
|
-
transition:
|
|
1734
|
-
color 0.15s,
|
|
1735
|
-
background 0.15s,
|
|
1736
|
-
border-color 0.15s;
|
|
1733
|
+
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
1737
1734
|
}
|
|
1738
1735
|
|
|
1739
1736
|
.settings-nav-item:hover {
|
|
@@ -2099,6 +2096,85 @@ td.user-id:hover {
|
|
|
2099
2096
|
}
|
|
2100
2097
|
|
|
2101
2098
|
/* ===== 精准商家:视频卡片视图 ===== */
|
|
2099
|
+
/* ── 精准商家页 SKU 列与弹窗 ── */
|
|
2100
|
+
.sku-cell {
|
|
2101
|
+
cursor: pointer;
|
|
2102
|
+
color: #60a5fa;
|
|
2103
|
+
white-space: nowrap;
|
|
2104
|
+
}
|
|
2105
|
+
.sku-cell:hover {
|
|
2106
|
+
text-decoration: underline;
|
|
2107
|
+
}
|
|
2108
|
+
.sku-modal {
|
|
2109
|
+
width: 640px;
|
|
2110
|
+
display: flex;
|
|
2111
|
+
flex-direction: column;
|
|
2112
|
+
max-height: 80vh;
|
|
2113
|
+
}
|
|
2114
|
+
.sku-modal-meta {
|
|
2115
|
+
font-size: 12px;
|
|
2116
|
+
color: #9ca3af;
|
|
2117
|
+
margin: 6px 0 12px;
|
|
2118
|
+
display: flex;
|
|
2119
|
+
flex-direction: column;
|
|
2120
|
+
gap: 4px;
|
|
2121
|
+
}
|
|
2122
|
+
.sku-modal-name {
|
|
2123
|
+
color: #e0e0e0;
|
|
2124
|
+
font-size: 13px;
|
|
2125
|
+
}
|
|
2126
|
+
.sku-modal-link {
|
|
2127
|
+
color: #60a5fa;
|
|
2128
|
+
text-decoration: none;
|
|
2129
|
+
font-size: 12px;
|
|
2130
|
+
}
|
|
2131
|
+
.sku-modal-link:hover {
|
|
2132
|
+
text-decoration: underline;
|
|
2133
|
+
}
|
|
2134
|
+
.sku-table-wrap {
|
|
2135
|
+
overflow: auto;
|
|
2136
|
+
border: 1px solid #2a2a3a;
|
|
2137
|
+
border-radius: 8px;
|
|
2138
|
+
}
|
|
2139
|
+
.sku-table {
|
|
2140
|
+
width: 100%;
|
|
2141
|
+
border-collapse: collapse;
|
|
2142
|
+
font-size: 12px;
|
|
2143
|
+
}
|
|
2144
|
+
.sku-table th {
|
|
2145
|
+
position: sticky;
|
|
2146
|
+
top: 0;
|
|
2147
|
+
background: #14141d;
|
|
2148
|
+
color: #9ca3af;
|
|
2149
|
+
text-align: left;
|
|
2150
|
+
padding: 8px 10px;
|
|
2151
|
+
border-bottom: 1px solid #2a2a3a;
|
|
2152
|
+
}
|
|
2153
|
+
.sku-table td {
|
|
2154
|
+
padding: 6px 10px;
|
|
2155
|
+
border-bottom: 1px solid #22222e;
|
|
2156
|
+
color: #d1d5db;
|
|
2157
|
+
}
|
|
2158
|
+
.sku-spec {
|
|
2159
|
+
max-width: 320px;
|
|
2160
|
+
overflow: hidden;
|
|
2161
|
+
text-overflow: ellipsis;
|
|
2162
|
+
white-space: nowrap;
|
|
2163
|
+
}
|
|
2164
|
+
.sku-id {
|
|
2165
|
+
color: #6b7280;
|
|
2166
|
+
font-family: monospace;
|
|
2167
|
+
}
|
|
2168
|
+
.sku-ok {
|
|
2169
|
+
color: #4ade80;
|
|
2170
|
+
}
|
|
2171
|
+
.sku-low {
|
|
2172
|
+
color: #fbbf24;
|
|
2173
|
+
}
|
|
2174
|
+
.sku-oos {
|
|
2175
|
+
color: #f87171;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2102
2178
|
.precise-list-header {
|
|
2103
2179
|
display: flex;
|
|
2104
2180
|
align-items: center;
|
package/src/watch/server.js
CHANGED
|
@@ -592,6 +592,9 @@ export function startWatchServer(
|
|
|
592
592
|
"CREATE INDEX IF NOT EXISTS idx_videos_user_update ON videos(user_update_count)",
|
|
593
593
|
// product-fill 领任务查询的部分索引:只索引候选行,避免全表扫描 164 万行
|
|
594
594
|
"CREATE INDEX IF NOT EXISTS idx_jobs_product_fill ON jobs(claimed_at, updated_at) WHERE top_video_seo_url IS NOT NULL AND top_video_seo_url != '' AND (product_name IS NULL OR product_name = '') AND product_rating IS NULL AND product_sold_count IS NULL",
|
|
595
|
+
// precheck 候选查询部分索引:预缩候选集且天然按 refresh_time 有序,
|
|
596
|
+
// 消除 TEMP B-TREE 排序(76 万行实测 ~1s → 毫秒级)
|
|
597
|
+
"CREATE INDEX IF NOT EXISTS idx_jobs_precheck ON jobs(refresh_time) WHERE tt_seller = 1 AND verified = 0 AND (pending_refresh IS NULL OR pending_refresh = 0)",
|
|
595
598
|
];
|
|
596
599
|
for (const sql of ensureIdx) {
|
|
597
600
|
try {
|
|
@@ -1607,6 +1610,17 @@ export function startWatchServer(
|
|
|
1607
1610
|
productReviewCount: u.productReviewCount ?? null,
|
|
1608
1611
|
productSoldCount: u.productSoldCount ?? null,
|
|
1609
1612
|
productName: u.productName || null,
|
|
1613
|
+
skuCount: u.skuCount || 0,
|
|
1614
|
+
skuTotalStock: u.skuTotalStock || 0,
|
|
1615
|
+
productSkus: (() => {
|
|
1616
|
+
try {
|
|
1617
|
+
return Array.isArray(u.productSkus)
|
|
1618
|
+
? u.productSkus
|
|
1619
|
+
: JSON.parse(u.productSkus || "[]");
|
|
1620
|
+
} catch {
|
|
1621
|
+
return [];
|
|
1622
|
+
}
|
|
1623
|
+
})(),
|
|
1610
1624
|
ecOver1k: u.ecOver1k || 0,
|
|
1611
1625
|
maxEcPlay: u.maxEcPlay || null,
|
|
1612
1626
|
recentEcVideos: (() => {
|