tt-help-cli-ycl 1.3.52 → 1.3.57
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/attach.js +25 -13
- package/src/cli/info.js +50 -28
- package/src/cli/open.js +3 -2
- package/src/lib/parse-ssr.mjs +70 -15
- package/src/lib/scrape.js +2 -3
- package/src/lib/tiktok-scraper.mjs +118 -217
- package/src/scraper/explore-core.js +2 -0
- package/src/watch/data-store.js +488 -112
- package/src/watch/public/index.html +115 -16
- package/src/watch/server.js +45 -3
- package/scripts/run-explore copy.bat +0 -101
- package/scripts/test-captcha-lib.mjs +0 -68
- package/scripts/test-captcha.mjs +0 -81
- package/scripts/test-incognito-lib.mjs +0 -36
- package/scripts/test-login-state.mjs +0 -128
- package/scripts/test-safe-click.mjs +0 -45
- package/scripts/test-watch-db-smoke.mjs +0 -246
|
@@ -421,6 +421,55 @@
|
|
|
421
421
|
color: #fff;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
.loading-overlay {
|
|
425
|
+
position: fixed;
|
|
426
|
+
inset: 0;
|
|
427
|
+
background: rgba(0, 0, 0, 0.5);
|
|
428
|
+
display: flex;
|
|
429
|
+
align-items: center;
|
|
430
|
+
justify-content: center;
|
|
431
|
+
z-index: 9999;
|
|
432
|
+
opacity: 0;
|
|
433
|
+
transition: opacity 0.2s;
|
|
434
|
+
pointer-events: none;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.loading-overlay.visible {
|
|
438
|
+
opacity: 1;
|
|
439
|
+
pointer-events: auto;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.loading-spinner {
|
|
443
|
+
display: flex;
|
|
444
|
+
flex-direction: column;
|
|
445
|
+
align-items: center;
|
|
446
|
+
gap: 12px;
|
|
447
|
+
background: #1a1a24;
|
|
448
|
+
padding: 24px 36px;
|
|
449
|
+
border-radius: 12px;
|
|
450
|
+
border: 1px solid #2a2a3a;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.loading-spinner .spinner {
|
|
454
|
+
width: 36px;
|
|
455
|
+
height: 36px;
|
|
456
|
+
border: 3px solid #2a2a3a;
|
|
457
|
+
border-top-color: #fe2c55;
|
|
458
|
+
border-radius: 50%;
|
|
459
|
+
animation: spin 0.8s linear infinite;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.loading-spinner .loading-text {
|
|
463
|
+
color: #ccc;
|
|
464
|
+
font-size: 14px;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@keyframes spin {
|
|
468
|
+
to {
|
|
469
|
+
transform: rotate(360deg);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
424
473
|
@keyframes flashChange {
|
|
425
474
|
0% {
|
|
426
475
|
filter: brightness(1.8);
|
|
@@ -1150,6 +1199,7 @@
|
|
|
1150
1199
|
<th>猜测国家</th>
|
|
1151
1200
|
<th>来源</th>
|
|
1152
1201
|
<th>状态</th>
|
|
1202
|
+
<th>StatusCode</th>
|
|
1153
1203
|
<th>处理端</th>
|
|
1154
1204
|
<th>领取时间</th>
|
|
1155
1205
|
<th>完成时间</th>
|
|
@@ -1384,19 +1434,20 @@
|
|
|
1384
1434
|
} catch (e) { }
|
|
1385
1435
|
}
|
|
1386
1436
|
|
|
1387
|
-
function formatStatNum(value) {
|
|
1437
|
+
function formatStatNum(value, { full = false } = {}) {
|
|
1388
1438
|
const num = Number(value) || 0;
|
|
1439
|
+
if (full) return num.toLocaleString('zh-CN');
|
|
1389
1440
|
if (Math.abs(num) < 1000) return String(num);
|
|
1390
1441
|
if (Math.abs(num) < 10000) return num.toLocaleString('zh-CN');
|
|
1391
1442
|
const wan = num / 10000;
|
|
1392
1443
|
return wan.toFixed(1).replace(/\.0+$/, '') + '万';
|
|
1393
1444
|
}
|
|
1394
1445
|
|
|
1395
|
-
function flashEl(id, value) {
|
|
1446
|
+
function flashEl(id, value, options) {
|
|
1396
1447
|
const el = document.getElementById(id);
|
|
1397
1448
|
if (!el) return;
|
|
1398
1449
|
const prev = prevStatValues[id];
|
|
1399
|
-
el.textContent = formatStatNum(value);
|
|
1450
|
+
el.textContent = formatStatNum(value, options);
|
|
1400
1451
|
if (prev !== undefined && prev !== value) {
|
|
1401
1452
|
el.classList.remove('flash-change');
|
|
1402
1453
|
void el.offsetWidth;
|
|
@@ -1414,8 +1465,8 @@
|
|
|
1414
1465
|
flashEl('statPending', d.pendingUsers);
|
|
1415
1466
|
flashEl('statError', d.errorUsers);
|
|
1416
1467
|
flashEl('statRestricted', d.restrictedUsers);
|
|
1417
|
-
flashEl('statTarget', d.targetUsers);
|
|
1418
|
-
flashEl('statUserUpdateTasks', d.userUpdateTasks || 0);
|
|
1468
|
+
flashEl('statTarget', d.targetUsers, { full: true });
|
|
1469
|
+
flashEl('statUserUpdateTasks', d.userUpdateTasks || 0, { full: true });
|
|
1419
1470
|
flashEl('statRawJobs', d.rawJobs || 0);
|
|
1420
1471
|
// 同步子页面 stats
|
|
1421
1472
|
const pendingTotal = document.getElementById('pendingStatTotal');
|
|
@@ -1423,7 +1474,7 @@
|
|
|
1423
1474
|
const pendingCount = document.getElementById('pendingStatPending');
|
|
1424
1475
|
if (pendingCount) pendingCount.textContent = formatStatNum(d.pendingUsers);
|
|
1425
1476
|
const pendingUserUpdate = document.getElementById('pendingStatUserUpdateTasks');
|
|
1426
|
-
if (pendingUserUpdate) pendingUserUpdate.textContent = formatStatNum(d.userUpdateTasks || 0);
|
|
1477
|
+
if (pendingUserUpdate) pendingUserUpdate.textContent = formatStatNum(d.userUpdateTasks || 0, { full: true });
|
|
1427
1478
|
const pendingRawJobs = document.getElementById('pendingStatRawJobs');
|
|
1428
1479
|
if (pendingRawJobs) pendingRawJobs.textContent = formatStatNum(d.rawJobs || 0);
|
|
1429
1480
|
const userUpdateTotal = document.getElementById('userUpdateStatTotal');
|
|
@@ -1431,7 +1482,7 @@
|
|
|
1431
1482
|
const userUpdatePending = document.getElementById('userUpdateStatPending');
|
|
1432
1483
|
if (userUpdatePending) userUpdatePending.textContent = formatStatNum(d.pendingUsers);
|
|
1433
1484
|
const userUpdateTasks = document.getElementById('userUpdateStatUserUpdateTasks');
|
|
1434
|
-
if (userUpdateTasks) userUpdateTasks.textContent = formatStatNum(d.userUpdateTasks || 0);
|
|
1485
|
+
if (userUpdateTasks) userUpdateTasks.textContent = formatStatNum(d.userUpdateTasks || 0, { full: true });
|
|
1435
1486
|
const userUpdateRawJobs = document.getElementById('userUpdateStatRawJobs');
|
|
1436
1487
|
if (userUpdateRawJobs) userUpdateRawJobs.textContent = formatStatNum(d.rawJobs || 0);
|
|
1437
1488
|
const rawPageRawJobs = document.getElementById('rawPageStatRawJobs');
|
|
@@ -1530,6 +1581,9 @@
|
|
|
1530
1581
|
const claimer = u.claimedBy || '-';
|
|
1531
1582
|
const claimTime = u.claimedAt ? formatTime(u.claimedAt) : '-';
|
|
1532
1583
|
const procTime = u.processedAt ? formatTime(u.processedAt) : '-';
|
|
1584
|
+
const statusCodeDisplay = u.statusCode != null && u.statusCode !== 0
|
|
1585
|
+
? `<span class="tag error" style="font-size:10px">${u.statusCode}</span>`
|
|
1586
|
+
: '';
|
|
1533
1587
|
return `<tr${rowClass}>
|
|
1534
1588
|
<td class="user-id" data-label="用户名">@${u.uniqueId}</td>
|
|
1535
1589
|
<td data-label="昵称">${nick}</td>
|
|
@@ -1539,6 +1593,7 @@
|
|
|
1539
1593
|
<td data-label="猜测国家">${guessedLoc}</td>
|
|
1540
1594
|
<td data-label="来源">${sources || '-'}</td>
|
|
1541
1595
|
<td data-label="状态">${statusTag} ${extraTags.join(' ')}</td>
|
|
1596
|
+
<td data-label="StatusCode">${statusCodeDisplay}</td>
|
|
1542
1597
|
<td data-label="处理端" style="font-size:11px;color:#888">${claimer}</td>
|
|
1543
1598
|
<td data-label="领取时间" style="font-size:11px;color:#888">${claimTime}</td>
|
|
1544
1599
|
<td data-label="完成时间" style="font-size:11px;color:#888">${procTime}</td>
|
|
@@ -1667,6 +1722,7 @@
|
|
|
1667
1722
|
const names = parseUsernames(raw);
|
|
1668
1723
|
if (names.length === 0) return;
|
|
1669
1724
|
|
|
1725
|
+
showLoading('正在添加用户...');
|
|
1670
1726
|
try {
|
|
1671
1727
|
const res = await fetch('/api/users', {
|
|
1672
1728
|
method: 'POST',
|
|
@@ -1681,6 +1737,8 @@
|
|
|
1681
1737
|
fetchUsers();
|
|
1682
1738
|
} catch (e) {
|
|
1683
1739
|
showToast('\u63d2\u5165\u5931\u8d25: ' + e.message, true);
|
|
1740
|
+
} finally {
|
|
1741
|
+
hideLoading();
|
|
1684
1742
|
}
|
|
1685
1743
|
}
|
|
1686
1744
|
|
|
@@ -1699,6 +1757,30 @@
|
|
|
1699
1757
|
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
|
1700
1758
|
}
|
|
1701
1759
|
|
|
1760
|
+
function showLoading(text) {
|
|
1761
|
+
let overlay = document.getElementById('loadingOverlay');
|
|
1762
|
+
if (!overlay) {
|
|
1763
|
+
overlay = document.createElement('div');
|
|
1764
|
+
overlay.id = 'loadingOverlay';
|
|
1765
|
+
overlay.className = 'loading-overlay';
|
|
1766
|
+
overlay.innerHTML = `
|
|
1767
|
+
<div class="loading-spinner">
|
|
1768
|
+
<div class="spinner"></div>
|
|
1769
|
+
<div class="loading-text" id="loadingText">处理中...</div>
|
|
1770
|
+
</div>
|
|
1771
|
+
`;
|
|
1772
|
+
document.body.appendChild(overlay);
|
|
1773
|
+
}
|
|
1774
|
+
const textEl = document.getElementById('loadingText');
|
|
1775
|
+
if (textEl) textEl.textContent = text || '处理中...';
|
|
1776
|
+
overlay.classList.add('visible');
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function hideLoading() {
|
|
1780
|
+
const overlay = document.getElementById('loadingOverlay');
|
|
1781
|
+
if (overlay) overlay.classList.remove('visible');
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1702
1784
|
function escapeJsString(str) {
|
|
1703
1785
|
return String(str).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
1704
1786
|
}
|
|
@@ -1773,6 +1855,7 @@
|
|
|
1773
1855
|
});
|
|
1774
1856
|
|
|
1775
1857
|
async function togglePin(uniqueId) {
|
|
1858
|
+
showLoading('操作中...');
|
|
1776
1859
|
try {
|
|
1777
1860
|
const res = await fetch('/api/job/' + encodeURIComponent(uniqueId) + '/pin', {
|
|
1778
1861
|
method: 'POST',
|
|
@@ -1786,10 +1869,13 @@
|
|
|
1786
1869
|
}
|
|
1787
1870
|
} catch (e) {
|
|
1788
1871
|
showToast('操作失败: ' + e.message, true);
|
|
1872
|
+
} finally {
|
|
1873
|
+
hideLoading();
|
|
1789
1874
|
}
|
|
1790
1875
|
}
|
|
1791
1876
|
|
|
1792
1877
|
async function resetJob(uniqueId) {
|
|
1878
|
+
showLoading('重置任务...');
|
|
1793
1879
|
try {
|
|
1794
1880
|
const res = await fetch('/api/job/' + encodeURIComponent(uniqueId) + '/reset', {
|
|
1795
1881
|
method: 'POST',
|
|
@@ -1804,6 +1890,8 @@
|
|
|
1804
1890
|
}
|
|
1805
1891
|
} catch (e) {
|
|
1806
1892
|
showToast('\u91cd\u7f6e\u5931\u8d25: ' + e.message, true);
|
|
1893
|
+
} finally {
|
|
1894
|
+
hideLoading();
|
|
1807
1895
|
}
|
|
1808
1896
|
}
|
|
1809
1897
|
|
|
@@ -1815,11 +1903,7 @@
|
|
|
1815
1903
|
return;
|
|
1816
1904
|
}
|
|
1817
1905
|
const userIds = errorUsers.map(u => u.uniqueId);
|
|
1818
|
-
|
|
1819
|
-
btn.disabled = true;
|
|
1820
|
-
btn.style.opacity = '0.6';
|
|
1821
|
-
btn.style.cursor = 'not-allowed';
|
|
1822
|
-
btn.innerHTML = '↻ 处理中...';
|
|
1906
|
+
showLoading('正在批量重置...');
|
|
1823
1907
|
try {
|
|
1824
1908
|
const res = await fetch('/api/jobs/batch-reset', {
|
|
1825
1909
|
method: 'POST',
|
|
@@ -1837,14 +1921,12 @@
|
|
|
1837
1921
|
} catch (e) {
|
|
1838
1922
|
showToast('\u6279\u91cf\u91cd\u7f6e\u5931\u8d25: ' + e.message, true);
|
|
1839
1923
|
} finally {
|
|
1840
|
-
|
|
1841
|
-
btn.style.opacity = '1';
|
|
1842
|
-
btn.style.cursor = 'pointer';
|
|
1843
|
-
btn.innerHTML = origText;
|
|
1924
|
+
hideLoading();
|
|
1844
1925
|
}
|
|
1845
1926
|
}
|
|
1846
1927
|
|
|
1847
1928
|
document.getElementById('statTargetCard').addEventListener('click', async () => {
|
|
1929
|
+
showLoading('正在导出目标用户...');
|
|
1848
1930
|
try {
|
|
1849
1931
|
const res = await fetch('/api/target-users', {
|
|
1850
1932
|
headers: { 'Accept': 'text/csv' },
|
|
@@ -1872,6 +1954,8 @@
|
|
|
1872
1954
|
showToast('CSV 文件已开始下载');
|
|
1873
1955
|
} catch (e) {
|
|
1874
1956
|
showToast('获取失败: ' + e.message, true);
|
|
1957
|
+
} finally {
|
|
1958
|
+
hideLoading();
|
|
1875
1959
|
}
|
|
1876
1960
|
});
|
|
1877
1961
|
|
|
@@ -2085,6 +2169,7 @@
|
|
|
2085
2169
|
if (!window.confirm(`确认将 ${country} 下 attach 未成功的任务恢复为待补资料吗?${countText}`)) {
|
|
2086
2170
|
return;
|
|
2087
2171
|
}
|
|
2172
|
+
showLoading('正在恢复任务...');
|
|
2088
2173
|
try {
|
|
2089
2174
|
const res = await fetch('/api/attach-stuck/restore', {
|
|
2090
2175
|
method: 'POST',
|
|
@@ -2106,6 +2191,8 @@
|
|
|
2106
2191
|
}
|
|
2107
2192
|
} catch (e) {
|
|
2108
2193
|
showToast('恢复 attach 任务失败: ' + e.message, true);
|
|
2194
|
+
} finally {
|
|
2195
|
+
hideLoading();
|
|
2109
2196
|
}
|
|
2110
2197
|
}
|
|
2111
2198
|
|
|
@@ -2115,6 +2202,7 @@
|
|
|
2115
2202
|
if (!window.confirm(`确认将 ${country} 的${scopeLabel}移到毛料库吗?${countText} 这些任务会先暂存,不再进入当前处理队列。`)) {
|
|
2116
2203
|
return;
|
|
2117
2204
|
}
|
|
2205
|
+
showLoading('正在移到毛料库...');
|
|
2118
2206
|
try {
|
|
2119
2207
|
const res = await fetch('/api/jobs/move-to-raw', {
|
|
2120
2208
|
method: 'POST',
|
|
@@ -2138,6 +2226,8 @@
|
|
|
2138
2226
|
}
|
|
2139
2227
|
} catch (e) {
|
|
2140
2228
|
showToast('移到毛料库失败: ' + e.message, true);
|
|
2229
|
+
} finally {
|
|
2230
|
+
hideLoading();
|
|
2141
2231
|
}
|
|
2142
2232
|
}
|
|
2143
2233
|
|
|
@@ -2254,6 +2344,7 @@
|
|
|
2254
2344
|
if (!window.confirm(`确认将 @${uniqueId} 从毛料库恢复到 jobs 队列吗?`)) {
|
|
2255
2345
|
return;
|
|
2256
2346
|
}
|
|
2347
|
+
showLoading('正在恢复...');
|
|
2257
2348
|
try {
|
|
2258
2349
|
const res = await fetch('/api/raw-jobs/restore', {
|
|
2259
2350
|
method: 'POST',
|
|
@@ -2271,6 +2362,8 @@
|
|
|
2271
2362
|
await fetchRawJobs();
|
|
2272
2363
|
} catch (e) {
|
|
2273
2364
|
showToast('恢复失败: ' + e.message, true);
|
|
2365
|
+
} finally {
|
|
2366
|
+
hideLoading();
|
|
2274
2367
|
}
|
|
2275
2368
|
}
|
|
2276
2369
|
|
|
@@ -2284,6 +2377,7 @@
|
|
|
2284
2377
|
if (!window.confirm(`确认将毛料库中符合【${desc}】的任务恢复到 jobs 队列吗?`)) {
|
|
2285
2378
|
return;
|
|
2286
2379
|
}
|
|
2380
|
+
showLoading('正在恢复筛选任务...');
|
|
2287
2381
|
try {
|
|
2288
2382
|
const body = {};
|
|
2289
2383
|
if (search) body.search = search;
|
|
@@ -2304,6 +2398,8 @@
|
|
|
2304
2398
|
await fetchRawJobs();
|
|
2305
2399
|
} catch (e) {
|
|
2306
2400
|
showToast('恢复失败: ' + e.message, true);
|
|
2401
|
+
} finally {
|
|
2402
|
+
hideLoading();
|
|
2307
2403
|
}
|
|
2308
2404
|
}
|
|
2309
2405
|
|
|
@@ -2312,6 +2408,7 @@
|
|
|
2312
2408
|
if (!window.confirm(`确认将 ${country} 从毛料库恢复到 jobs 队列吗?${countText}`)) {
|
|
2313
2409
|
return;
|
|
2314
2410
|
}
|
|
2411
|
+
showLoading('正在恢复...');
|
|
2315
2412
|
try {
|
|
2316
2413
|
const res = await fetch('/api/raw-jobs/restore', {
|
|
2317
2414
|
method: 'POST',
|
|
@@ -2329,6 +2426,8 @@
|
|
|
2329
2426
|
await fetchRawJobs();
|
|
2330
2427
|
} catch (e) {
|
|
2331
2428
|
showToast('恢复失败: ' + e.message, true);
|
|
2429
|
+
} finally {
|
|
2430
|
+
hideLoading();
|
|
2332
2431
|
}
|
|
2333
2432
|
}
|
|
2334
2433
|
|
package/src/watch/server.js
CHANGED
|
@@ -180,6 +180,41 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
if (req.method === "GET" && routePath === "/api/job-debug") {
|
|
184
|
+
const userId = params.userId || "";
|
|
185
|
+
const locationsParam = params.locations || "";
|
|
186
|
+
const locations = locationsParam
|
|
187
|
+
? locationsParam
|
|
188
|
+
.split(",")
|
|
189
|
+
.map((s) => s.trim().toUpperCase())
|
|
190
|
+
.filter(Boolean)
|
|
191
|
+
: null;
|
|
192
|
+
const loggedIn = params.loggedIn === "true";
|
|
193
|
+
const debug = store.debugClaimNextJob(
|
|
194
|
+
userId,
|
|
195
|
+
5 * 60 * 1000,
|
|
196
|
+
locations,
|
|
197
|
+
loggedIn,
|
|
198
|
+
);
|
|
199
|
+
sendJSON(res, 200, debug);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 调试接口:直接查询数据库原始数据
|
|
204
|
+
if (req.method === "GET" && routePath === "/api/db-query") {
|
|
205
|
+
const sql = params.sql || "SELECT * FROM jobs LIMIT 10";
|
|
206
|
+
const limit = Math.min(parseInt(params.limit) || 100, 1000);
|
|
207
|
+
// 安全限制:自动加 LIMIT
|
|
208
|
+
const safeSql = sql.replace(/LIMIT\s+\d+/gi, "") + ` LIMIT ${limit}`;
|
|
209
|
+
try {
|
|
210
|
+
const result = store.rawQuery(safeSql);
|
|
211
|
+
sendJSON(res, 200, result);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
sendJSON(res, 400, { error: e.message });
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
183
218
|
const jobCommitMatch = routePath.match(/^\/api\/job\/([^/]+)$/);
|
|
184
219
|
if (req.method === "POST" && jobCommitMatch) {
|
|
185
220
|
const uniqueId = jobCommitMatch[1];
|
|
@@ -334,11 +369,16 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
334
369
|
if (req.method === "GET" && routePath === "/api/user-update-tasks") {
|
|
335
370
|
const limit = params.limit;
|
|
336
371
|
const countries = params.countries
|
|
337
|
-
? params.countries
|
|
372
|
+
? params.countries
|
|
373
|
+
.split(",")
|
|
374
|
+
.map((c) => c.trim().toUpperCase())
|
|
375
|
+
.filter(Boolean)
|
|
338
376
|
: [];
|
|
339
377
|
const tasks = store.getPendingUserUpdateTasks(limit, countries);
|
|
340
378
|
const ts = new Date().toISOString().slice(11, 19);
|
|
341
|
-
console.error(
|
|
379
|
+
console.error(
|
|
380
|
+
`[JOB ${ts}] USER-UPDATE-TASKS: ${tasks.length} tasks${countries.length ? ` (countries: ${countries.join(",")})` : ""}`,
|
|
381
|
+
);
|
|
342
382
|
sendJSON(res, 200, { total: tasks.length, tasks });
|
|
343
383
|
return;
|
|
344
384
|
}
|
|
@@ -554,7 +594,9 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
|
|
|
554
594
|
} else if (body.country) {
|
|
555
595
|
result = store.restoreRawJobsByCountry(body.country);
|
|
556
596
|
} else {
|
|
557
|
-
sendJSON(res, 400, {
|
|
597
|
+
sendJSON(res, 400, {
|
|
598
|
+
error: "missing filter: uniqueId, country, or search/location",
|
|
599
|
+
});
|
|
558
600
|
return;
|
|
559
601
|
}
|
|
560
602
|
if (result.error) {
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
@ECHO OFF
|
|
2
|
-
SETLOCAL EnableDelayedExpansion
|
|
3
|
-
|
|
4
|
-
SET "PACKAGENAME=tt-help-cli-ycl"
|
|
5
|
-
SET "TARGET_SERVER=http://117.71.53.99:17301"
|
|
6
|
-
SET "LOCAL_IP="
|
|
7
|
-
SET "GET_IP_PS1=%TEMP%\tt_get_local_ip.ps1"
|
|
8
|
-
>"%GET_IP_PS1%" ECHO $ip = $null
|
|
9
|
-
>>"%GET_IP_PS1%" ECHO try {
|
|
10
|
-
>>"%GET_IP_PS1%" ECHO $ip = Get-WmiObject Win32_NetworkAdapterConfiguration ^| Where-Object { $_.IPEnabled -eq $true } ^| ForEach-Object { $_.IPAddress } ^| Where-Object { $_ -match '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' -and $_ -ne '127.0.0.1' -and $_ -notlike '169.254.*' } ^| Select-Object -First 1
|
|
11
|
-
>>"%GET_IP_PS1%" ECHO } catch {}
|
|
12
|
-
>>"%GET_IP_PS1%" ECHO if (-not $ip) {
|
|
13
|
-
>>"%GET_IP_PS1%" ECHO try {
|
|
14
|
-
>>"%GET_IP_PS1%" ECHO $ip = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop ^| Where-Object { $_.IPAddress -ne '127.0.0.1' -and $_.IPAddress -notlike '169.254.*' } ^| Select-Object -First 1 -ExpandProperty IPAddress
|
|
15
|
-
>>"%GET_IP_PS1%" ECHO } catch {}
|
|
16
|
-
>>"%GET_IP_PS1%" ECHO }
|
|
17
|
-
>>"%GET_IP_PS1%" ECHO if (-not $ip) {
|
|
18
|
-
>>"%GET_IP_PS1%" ECHO try {
|
|
19
|
-
>>"%GET_IP_PS1%" ECHO $ip = [System.Net.Dns]::GetHostAddresses([System.Net.Dns]::GetHostName()) ^| Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork -and $_.IPAddressToString -ne '127.0.0.1' -and $_.IPAddressToString -notlike '169.254.*' } ^| Select-Object -First 1 -ExpandProperty IPAddressToString
|
|
20
|
-
>>"%GET_IP_PS1%" ECHO } catch {}
|
|
21
|
-
>>"%GET_IP_PS1%" ECHO }
|
|
22
|
-
>>"%GET_IP_PS1%" ECHO if ($ip) { [Console]::Write($ip) }
|
|
23
|
-
FOR /F "usebackq delims=" %%I IN (`powershell -NoProfile -ExecutionPolicy Bypass -File "%GET_IP_PS1%"`) DO (
|
|
24
|
-
SET "LOCAL_IP=%%I"
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
ECHO [INFO] Local IP: %LOCAL_IP%
|
|
28
|
-
|
|
29
|
-
IF DEFINED LOCAL_IP IF "%LOCAL_IP:~0,11%"=="172.18.154." SET "TARGET_SERVER=http://172.18.154.201:3001"
|
|
30
|
-
|
|
31
|
-
IF NOT DEFINED LOCAL_IP (
|
|
32
|
-
ECHO [INFO] No local IPv4 detected, using public server
|
|
33
|
-
) ELSE IF "%LOCAL_IP:~0,11%"=="172.18.154." (
|
|
34
|
-
ECHO [INFO] Intranet IP detected, using intranet server
|
|
35
|
-
) ELSE (
|
|
36
|
-
ECHO [INFO] No intranet IP detected, using public server
|
|
37
|
-
)
|
|
38
|
-
SET "CONFIG_PATH=%USERPROFILE%\.tt-help.json"
|
|
39
|
-
|
|
40
|
-
ECHO ========================================
|
|
41
|
-
ECHO tt-help-cli-ycl one-click launcher (Windows CMD)
|
|
42
|
-
ECHO ========================================
|
|
43
|
-
|
|
44
|
-
REM ---------- 1. Check/install latest version ----------
|
|
45
|
-
FOR /F "delims=" %%V IN ('npm view %PACKAGENAME% version 2^>nul') DO SET "LATEST_VERSION=%%V"
|
|
46
|
-
|
|
47
|
-
IF NOT DEFINED LATEST_VERSION (
|
|
48
|
-
ECHO [ERROR] Cannot get latest version from npm
|
|
49
|
-
EXIT /B 1
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
FOR /F "tokens=2 delims=@" %%V IN ('npm list -g %PACKAGENAME% --depth=0 2^>nul ^| findstr /i "%PACKAGENAME%"') DO SET "INSTALLED_VERSION=%%V"
|
|
53
|
-
|
|
54
|
-
IF NOT DEFINED INSTALLED_VERSION (
|
|
55
|
-
ECHO [INFO] %PACKAGENAME% not installed, installing latest...
|
|
56
|
-
CALL npm install -g %PACKAGENAME%
|
|
57
|
-
IF %ERRORLEVEL% EQU 0 (
|
|
58
|
-
ECHO [OK] Installed: %LATEST_VERSION%
|
|
59
|
-
) ELSE (
|
|
60
|
-
ECHO [ERROR] Install failed, run manually: npm install -g %PACKAGENAME%
|
|
61
|
-
EXIT /B 1
|
|
62
|
-
)
|
|
63
|
-
) ELSE IF "%INSTALLED_VERSION%"=="%LATEST_VERSION%" (
|
|
64
|
-
ECHO [OK] %PACKAGENAME% is up to date: %LATEST_VERSION%
|
|
65
|
-
) ELSE (
|
|
66
|
-
ECHO [INFO] Current: %INSTALLED_VERSION%, Latest: %LATEST_VERSION%
|
|
67
|
-
ECHO [INFO] Upgrading to latest...
|
|
68
|
-
CALL npm install -g %PACKAGENAME%
|
|
69
|
-
IF %ERRORLEVEL% EQU 0 (
|
|
70
|
-
ECHO [OK] Upgraded: %LATEST_VERSION%
|
|
71
|
-
) ELSE (
|
|
72
|
-
ECHO [WARN] Upgrade failed, run manually: npm install -g %PACKAGENAME%
|
|
73
|
-
)
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
REM ---------- 2. Check/set server config ----------
|
|
77
|
-
SET "CURRENT_SERVER="
|
|
78
|
-
IF EXIST "%CONFIG_PATH%" (
|
|
79
|
-
FOR /F "usebackq delims=" %%S IN (`powershell -NoProfile -Command "$p=$env:CONFIG_PATH; if (Test-Path $p) { try { $cfg = Get-Content $p -Raw | ConvertFrom-Json; if ($null -ne $cfg.server) { [Console]::Write($cfg.server) } } catch {} }"`) DO SET "CURRENT_SERVER=%%S"
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
IF "%CURRENT_SERVER%"=="%TARGET_SERVER%" (
|
|
83
|
-
ECHO [OK] Server config is correct: %TARGET_SERVER%
|
|
84
|
-
) ELSE (
|
|
85
|
-
IF "%CURRENT_SERVER%"=="" (
|
|
86
|
-
ECHO [INFO] Current server: not set, target: %TARGET_SERVER%
|
|
87
|
-
) ELSE (
|
|
88
|
-
ECHO [INFO] Current server: %CURRENT_SERVER%, target: %TARGET_SERVER%
|
|
89
|
-
)
|
|
90
|
-
ECHO [INFO] Setting server config...
|
|
91
|
-
node -e "const fs=require('fs'),path=require('path');const p=path.join(require('os').homedir(),'.tt-help.json');let c={};try{c=JSON.parse(fs.readFileSync(p,'utf-8'))}catch(e){}c.server='%TARGET_SERVER%';fs.writeFileSync(p,JSON.stringify(c,null,2),'utf-8');console.log(' Written to: '+p);"
|
|
92
|
-
ECHO [OK] Server config set
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
REM ---------- 3. Start tt-help explore ----------
|
|
96
|
-
ECHO.
|
|
97
|
-
ECHO ========================================
|
|
98
|
-
ECHO Starting tt-help explore
|
|
99
|
-
ECHO ========================================
|
|
100
|
-
tt-help explore --port 9223 --profile p9223
|
|
101
|
-
DEL "%GET_IP_PS1%" 2>NUL
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { chromium } from 'playwright';
|
|
2
|
-
|
|
3
|
-
const URL = 'https://www.tiktok.com/@mariaelenasanchez607/video/7630110959650000150';
|
|
4
|
-
|
|
5
|
-
async function main() {
|
|
6
|
-
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
|
|
7
|
-
const page = browser.contexts()[0].pages()[0];
|
|
8
|
-
|
|
9
|
-
// 测试 detectCaptcha
|
|
10
|
-
console.error('=== 测试 detectCaptcha (无验证码) ===');
|
|
11
|
-
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
12
|
-
await page.waitForTimeout(3000);
|
|
13
|
-
|
|
14
|
-
const { detectCaptcha, closeCaptcha, handleCaptcha } = await import('../src/scraper/modules/captcha-handler.mjs');
|
|
15
|
-
|
|
16
|
-
const r1 = await detectCaptcha(page);
|
|
17
|
-
console.error('未点击评论:', JSON.stringify(r1));
|
|
18
|
-
|
|
19
|
-
// 点击评论触发验证码
|
|
20
|
-
await page.evaluate(() => {
|
|
21
|
-
const all = document.querySelectorAll('button');
|
|
22
|
-
for (const el of all) {
|
|
23
|
-
if (/^评论$/.test(el.textContent?.trim()) && el.offsetParent !== null && el.getBoundingClientRect().width > 0) {
|
|
24
|
-
el.click();
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
await page.waitForTimeout(3000);
|
|
30
|
-
|
|
31
|
-
console.error('\n=== 测试 detectCaptcha (有验证码) ===');
|
|
32
|
-
const r2 = await detectCaptcha(page);
|
|
33
|
-
console.error('点击评论后:', JSON.stringify(r2));
|
|
34
|
-
|
|
35
|
-
console.error('\n=== 测试 closeCaptcha ===');
|
|
36
|
-
const r3 = await closeCaptcha(page);
|
|
37
|
-
await page.waitForTimeout(1000);
|
|
38
|
-
console.error('关闭结果:', JSON.stringify(r3));
|
|
39
|
-
|
|
40
|
-
const r4 = await detectCaptcha(page);
|
|
41
|
-
console.error('关闭后检测:', JSON.stringify(r4));
|
|
42
|
-
|
|
43
|
-
console.error('\n=== 测试 handleCaptcha (完整流程) ===');
|
|
44
|
-
// 重新触发
|
|
45
|
-
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
46
|
-
await page.waitForTimeout(3000);
|
|
47
|
-
await page.evaluate(() => {
|
|
48
|
-
const all = document.querySelectorAll('button');
|
|
49
|
-
for (const el of all) {
|
|
50
|
-
if (/^评论$/.test(el.textContent?.trim()) && el.offsetParent !== null && el.getBoundingClientRect().width > 0) {
|
|
51
|
-
el.click();
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
await page.waitForTimeout(3000);
|
|
57
|
-
|
|
58
|
-
const r5 = await handleCaptcha(page);
|
|
59
|
-
console.error('handleCaptcha 结果:', JSON.stringify(r5));
|
|
60
|
-
|
|
61
|
-
await page.screenshot({ path: '/tmp/lib-test-final.png' });
|
|
62
|
-
console.error('\n最终截图: /tmp/lib-test-final.png');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
main().catch(err => {
|
|
66
|
-
console.error('错误:', err);
|
|
67
|
-
process.exit(1);
|
|
68
|
-
});
|
package/scripts/test-captcha.mjs
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { ensureBrowserReady } from '../src/lib/browser/cdp.js';
|
|
2
|
-
|
|
3
|
-
const url = 'https://www.tiktok.com/@mariaelenasanchez607/video/7630110959650000150';
|
|
4
|
-
|
|
5
|
-
async function main() {
|
|
6
|
-
const browser = await ensureBrowserReady();
|
|
7
|
-
const defaultContext = browser.contexts()[0];
|
|
8
|
-
const pages = defaultContext.pages();
|
|
9
|
-
const page = pages[0] || await defaultContext.newPage();
|
|
10
|
-
|
|
11
|
-
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
12
|
-
await page.waitForTimeout(5000);
|
|
13
|
-
|
|
14
|
-
// 用 force: true 点击评论按钮
|
|
15
|
-
const clicked = await page.evaluate(() => {
|
|
16
|
-
const btn = document.querySelector('[data-e2e="comments"]');
|
|
17
|
-
if (btn && btn.getBoundingClientRect().width > 0) {
|
|
18
|
-
btn.click();
|
|
19
|
-
return { success: true, rect: btn.getBoundingClientRect() };
|
|
20
|
-
}
|
|
21
|
-
return { success: false };
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
console.error('点击结果:', JSON.stringify(clicked));
|
|
25
|
-
|
|
26
|
-
// 等待可能的验证码
|
|
27
|
-
await page.waitForTimeout(5000);
|
|
28
|
-
|
|
29
|
-
// 截图
|
|
30
|
-
await page.screenshot({ path: '/tmp/tiktok-comment-clicked.png' });
|
|
31
|
-
console.error('截图: /tmp/tiktok-comment-clicked.png');
|
|
32
|
-
|
|
33
|
-
// 全面检测验证码
|
|
34
|
-
const captcha = await page.evaluate(() => {
|
|
35
|
-
const result = {};
|
|
36
|
-
|
|
37
|
-
// 大尺寸 Verify 元素
|
|
38
|
-
const verifyEls = Array.from(document.querySelectorAll('[class*="Verify"], [class*="verify"]'));
|
|
39
|
-
result.verifyElements = verifyEls.filter(el => {
|
|
40
|
-
const r = el.getBoundingClientRect();
|
|
41
|
-
return r.width > 100 && r.height > 100 && el.offsetParent !== null;
|
|
42
|
-
}).map(el => ({
|
|
43
|
-
class: el.className.substring(0, 200),
|
|
44
|
-
text: el.textContent?.substring(0, 300),
|
|
45
|
-
rect: { w: Math.round(el.getBoundingClientRect().width), h: Math.round(el.getBoundingClientRect().height), x: Math.round(el.getBoundingClientRect().x), y: Math.round(el.getBoundingClientRect().y) }
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
|
-
// 全屏遮罩
|
|
49
|
-
result.fullScreenOverlays = Array.from(document.querySelectorAll('div')).filter(d => {
|
|
50
|
-
const r = d.getBoundingClientRect();
|
|
51
|
-
const style = window.getComputedStyle(d);
|
|
52
|
-
return r.width > 500 && r.height > 500 && parseInt(style.zIndex) > 900 && d.offsetParent !== null;
|
|
53
|
-
}).map(d => ({
|
|
54
|
-
class: d.className.substring(0, 100),
|
|
55
|
-
zIndex: window.getComputedStyle(d).zIndex,
|
|
56
|
-
rect: { w: Math.round(d.getBoundingClientRect().width), h: Math.round(d.getBoundingClientRect().height) }
|
|
57
|
-
}));
|
|
58
|
-
|
|
59
|
-
result.iframes = Array.from(document.querySelectorAll('iframe')).map(f => ({
|
|
60
|
-
src: (f.src || f.getAttribute('src') || '').substring(0, 300)
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
return result;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
console.error('\n=== 验证码检测 ===');
|
|
67
|
-
console.error(JSON.stringify(captcha, null, 2));
|
|
68
|
-
|
|
69
|
-
if (captcha.verifyElements.length > 0 || captcha.fullScreenOverlays.length > 0 || captcha.iframes.length > 0) {
|
|
70
|
-
console.error('\n⚠️ 检测到验证码或遮罩层!');
|
|
71
|
-
} else {
|
|
72
|
-
console.error('\n✅ 未检测到验证码');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
await browser.close();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
main().catch(err => {
|
|
79
|
-
console.error('错误:', err);
|
|
80
|
-
process.exit(1);
|
|
81
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { chromium } from 'playwright';
|
|
2
|
-
import { detectCaptcha, closeCaptcha, handleCaptcha, getIncognitoPage } from '../src/scraper/modules/captcha-handler.mjs';
|
|
3
|
-
|
|
4
|
-
async function main() {
|
|
5
|
-
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
|
|
6
|
-
const url = 'https://www.tiktok.com/@mariaelenasanchez607/video/7630110959650000150';
|
|
7
|
-
|
|
8
|
-
// 测试1: 无痕模式打开 + 点击评论
|
|
9
|
-
console.error('=== 测试: 无痕模式 ===');
|
|
10
|
-
const { page, context } = await getIncognitoPage(browser, url);
|
|
11
|
-
console.error('URL:', page.url());
|
|
12
|
-
|
|
13
|
-
await page.evaluate(() => {
|
|
14
|
-
const all = document.querySelectorAll('button');
|
|
15
|
-
for (const el of all) {
|
|
16
|
-
if (/^评论$/.test(el.textContent?.trim()) && el.offsetParent !== null && el.getBoundingClientRect().width > 0) {
|
|
17
|
-
el.click();
|
|
18
|
-
break;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
23
|
-
|
|
24
|
-
const captcha = await detectCaptcha(page);
|
|
25
|
-
console.error('验证码:', captcha);
|
|
26
|
-
|
|
27
|
-
await page.screenshot({ path: '/tmp/incognito-lib-test.png' });
|
|
28
|
-
console.error('截图: /tmp/incognito-lib-test.png');
|
|
29
|
-
|
|
30
|
-
await context.close();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
main().catch(err => {
|
|
34
|
-
console.error('错误:', err);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
});
|