tt-help-cli-ycl 1.3.55 → 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/open.js +3 -4
- package/src/scraper/explore-core.js +2 -0
- package/src/watch/data-store.js +427 -101
- package/src/watch/public/index.html +110 -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-html-analysis.mjs +0 -128
- 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);
|
|
@@ -1385,19 +1434,20 @@
|
|
|
1385
1434
|
} catch (e) { }
|
|
1386
1435
|
}
|
|
1387
1436
|
|
|
1388
|
-
function formatStatNum(value) {
|
|
1437
|
+
function formatStatNum(value, { full = false } = {}) {
|
|
1389
1438
|
const num = Number(value) || 0;
|
|
1439
|
+
if (full) return num.toLocaleString('zh-CN');
|
|
1390
1440
|
if (Math.abs(num) < 1000) return String(num);
|
|
1391
1441
|
if (Math.abs(num) < 10000) return num.toLocaleString('zh-CN');
|
|
1392
1442
|
const wan = num / 10000;
|
|
1393
1443
|
return wan.toFixed(1).replace(/\.0+$/, '') + '万';
|
|
1394
1444
|
}
|
|
1395
1445
|
|
|
1396
|
-
function flashEl(id, value) {
|
|
1446
|
+
function flashEl(id, value, options) {
|
|
1397
1447
|
const el = document.getElementById(id);
|
|
1398
1448
|
if (!el) return;
|
|
1399
1449
|
const prev = prevStatValues[id];
|
|
1400
|
-
el.textContent = formatStatNum(value);
|
|
1450
|
+
el.textContent = formatStatNum(value, options);
|
|
1401
1451
|
if (prev !== undefined && prev !== value) {
|
|
1402
1452
|
el.classList.remove('flash-change');
|
|
1403
1453
|
void el.offsetWidth;
|
|
@@ -1415,8 +1465,8 @@
|
|
|
1415
1465
|
flashEl('statPending', d.pendingUsers);
|
|
1416
1466
|
flashEl('statError', d.errorUsers);
|
|
1417
1467
|
flashEl('statRestricted', d.restrictedUsers);
|
|
1418
|
-
flashEl('statTarget', d.targetUsers);
|
|
1419
|
-
flashEl('statUserUpdateTasks', d.userUpdateTasks || 0);
|
|
1468
|
+
flashEl('statTarget', d.targetUsers, { full: true });
|
|
1469
|
+
flashEl('statUserUpdateTasks', d.userUpdateTasks || 0, { full: true });
|
|
1420
1470
|
flashEl('statRawJobs', d.rawJobs || 0);
|
|
1421
1471
|
// 同步子页面 stats
|
|
1422
1472
|
const pendingTotal = document.getElementById('pendingStatTotal');
|
|
@@ -1424,7 +1474,7 @@
|
|
|
1424
1474
|
const pendingCount = document.getElementById('pendingStatPending');
|
|
1425
1475
|
if (pendingCount) pendingCount.textContent = formatStatNum(d.pendingUsers);
|
|
1426
1476
|
const pendingUserUpdate = document.getElementById('pendingStatUserUpdateTasks');
|
|
1427
|
-
if (pendingUserUpdate) pendingUserUpdate.textContent = formatStatNum(d.userUpdateTasks || 0);
|
|
1477
|
+
if (pendingUserUpdate) pendingUserUpdate.textContent = formatStatNum(d.userUpdateTasks || 0, { full: true });
|
|
1428
1478
|
const pendingRawJobs = document.getElementById('pendingStatRawJobs');
|
|
1429
1479
|
if (pendingRawJobs) pendingRawJobs.textContent = formatStatNum(d.rawJobs || 0);
|
|
1430
1480
|
const userUpdateTotal = document.getElementById('userUpdateStatTotal');
|
|
@@ -1432,7 +1482,7 @@
|
|
|
1432
1482
|
const userUpdatePending = document.getElementById('userUpdateStatPending');
|
|
1433
1483
|
if (userUpdatePending) userUpdatePending.textContent = formatStatNum(d.pendingUsers);
|
|
1434
1484
|
const userUpdateTasks = document.getElementById('userUpdateStatUserUpdateTasks');
|
|
1435
|
-
if (userUpdateTasks) userUpdateTasks.textContent = formatStatNum(d.userUpdateTasks || 0);
|
|
1485
|
+
if (userUpdateTasks) userUpdateTasks.textContent = formatStatNum(d.userUpdateTasks || 0, { full: true });
|
|
1436
1486
|
const userUpdateRawJobs = document.getElementById('userUpdateStatRawJobs');
|
|
1437
1487
|
if (userUpdateRawJobs) userUpdateRawJobs.textContent = formatStatNum(d.rawJobs || 0);
|
|
1438
1488
|
const rawPageRawJobs = document.getElementById('rawPageStatRawJobs');
|
|
@@ -1672,6 +1722,7 @@
|
|
|
1672
1722
|
const names = parseUsernames(raw);
|
|
1673
1723
|
if (names.length === 0) return;
|
|
1674
1724
|
|
|
1725
|
+
showLoading('正在添加用户...');
|
|
1675
1726
|
try {
|
|
1676
1727
|
const res = await fetch('/api/users', {
|
|
1677
1728
|
method: 'POST',
|
|
@@ -1686,6 +1737,8 @@
|
|
|
1686
1737
|
fetchUsers();
|
|
1687
1738
|
} catch (e) {
|
|
1688
1739
|
showToast('\u63d2\u5165\u5931\u8d25: ' + e.message, true);
|
|
1740
|
+
} finally {
|
|
1741
|
+
hideLoading();
|
|
1689
1742
|
}
|
|
1690
1743
|
}
|
|
1691
1744
|
|
|
@@ -1704,6 +1757,30 @@
|
|
|
1704
1757
|
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
|
1705
1758
|
}
|
|
1706
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
|
+
|
|
1707
1784
|
function escapeJsString(str) {
|
|
1708
1785
|
return String(str).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
1709
1786
|
}
|
|
@@ -1778,6 +1855,7 @@
|
|
|
1778
1855
|
});
|
|
1779
1856
|
|
|
1780
1857
|
async function togglePin(uniqueId) {
|
|
1858
|
+
showLoading('操作中...');
|
|
1781
1859
|
try {
|
|
1782
1860
|
const res = await fetch('/api/job/' + encodeURIComponent(uniqueId) + '/pin', {
|
|
1783
1861
|
method: 'POST',
|
|
@@ -1791,10 +1869,13 @@
|
|
|
1791
1869
|
}
|
|
1792
1870
|
} catch (e) {
|
|
1793
1871
|
showToast('操作失败: ' + e.message, true);
|
|
1872
|
+
} finally {
|
|
1873
|
+
hideLoading();
|
|
1794
1874
|
}
|
|
1795
1875
|
}
|
|
1796
1876
|
|
|
1797
1877
|
async function resetJob(uniqueId) {
|
|
1878
|
+
showLoading('重置任务...');
|
|
1798
1879
|
try {
|
|
1799
1880
|
const res = await fetch('/api/job/' + encodeURIComponent(uniqueId) + '/reset', {
|
|
1800
1881
|
method: 'POST',
|
|
@@ -1809,6 +1890,8 @@
|
|
|
1809
1890
|
}
|
|
1810
1891
|
} catch (e) {
|
|
1811
1892
|
showToast('\u91cd\u7f6e\u5931\u8d25: ' + e.message, true);
|
|
1893
|
+
} finally {
|
|
1894
|
+
hideLoading();
|
|
1812
1895
|
}
|
|
1813
1896
|
}
|
|
1814
1897
|
|
|
@@ -1820,11 +1903,7 @@
|
|
|
1820
1903
|
return;
|
|
1821
1904
|
}
|
|
1822
1905
|
const userIds = errorUsers.map(u => u.uniqueId);
|
|
1823
|
-
|
|
1824
|
-
btn.disabled = true;
|
|
1825
|
-
btn.style.opacity = '0.6';
|
|
1826
|
-
btn.style.cursor = 'not-allowed';
|
|
1827
|
-
btn.innerHTML = '↻ 处理中...';
|
|
1906
|
+
showLoading('正在批量重置...');
|
|
1828
1907
|
try {
|
|
1829
1908
|
const res = await fetch('/api/jobs/batch-reset', {
|
|
1830
1909
|
method: 'POST',
|
|
@@ -1842,14 +1921,12 @@
|
|
|
1842
1921
|
} catch (e) {
|
|
1843
1922
|
showToast('\u6279\u91cf\u91cd\u7f6e\u5931\u8d25: ' + e.message, true);
|
|
1844
1923
|
} finally {
|
|
1845
|
-
|
|
1846
|
-
btn.style.opacity = '1';
|
|
1847
|
-
btn.style.cursor = 'pointer';
|
|
1848
|
-
btn.innerHTML = origText;
|
|
1924
|
+
hideLoading();
|
|
1849
1925
|
}
|
|
1850
1926
|
}
|
|
1851
1927
|
|
|
1852
1928
|
document.getElementById('statTargetCard').addEventListener('click', async () => {
|
|
1929
|
+
showLoading('正在导出目标用户...');
|
|
1853
1930
|
try {
|
|
1854
1931
|
const res = await fetch('/api/target-users', {
|
|
1855
1932
|
headers: { 'Accept': 'text/csv' },
|
|
@@ -1877,6 +1954,8 @@
|
|
|
1877
1954
|
showToast('CSV 文件已开始下载');
|
|
1878
1955
|
} catch (e) {
|
|
1879
1956
|
showToast('获取失败: ' + e.message, true);
|
|
1957
|
+
} finally {
|
|
1958
|
+
hideLoading();
|
|
1880
1959
|
}
|
|
1881
1960
|
});
|
|
1882
1961
|
|
|
@@ -2090,6 +2169,7 @@
|
|
|
2090
2169
|
if (!window.confirm(`确认将 ${country} 下 attach 未成功的任务恢复为待补资料吗?${countText}`)) {
|
|
2091
2170
|
return;
|
|
2092
2171
|
}
|
|
2172
|
+
showLoading('正在恢复任务...');
|
|
2093
2173
|
try {
|
|
2094
2174
|
const res = await fetch('/api/attach-stuck/restore', {
|
|
2095
2175
|
method: 'POST',
|
|
@@ -2111,6 +2191,8 @@
|
|
|
2111
2191
|
}
|
|
2112
2192
|
} catch (e) {
|
|
2113
2193
|
showToast('恢复 attach 任务失败: ' + e.message, true);
|
|
2194
|
+
} finally {
|
|
2195
|
+
hideLoading();
|
|
2114
2196
|
}
|
|
2115
2197
|
}
|
|
2116
2198
|
|
|
@@ -2120,6 +2202,7 @@
|
|
|
2120
2202
|
if (!window.confirm(`确认将 ${country} 的${scopeLabel}移到毛料库吗?${countText} 这些任务会先暂存,不再进入当前处理队列。`)) {
|
|
2121
2203
|
return;
|
|
2122
2204
|
}
|
|
2205
|
+
showLoading('正在移到毛料库...');
|
|
2123
2206
|
try {
|
|
2124
2207
|
const res = await fetch('/api/jobs/move-to-raw', {
|
|
2125
2208
|
method: 'POST',
|
|
@@ -2143,6 +2226,8 @@
|
|
|
2143
2226
|
}
|
|
2144
2227
|
} catch (e) {
|
|
2145
2228
|
showToast('移到毛料库失败: ' + e.message, true);
|
|
2229
|
+
} finally {
|
|
2230
|
+
hideLoading();
|
|
2146
2231
|
}
|
|
2147
2232
|
}
|
|
2148
2233
|
|
|
@@ -2259,6 +2344,7 @@
|
|
|
2259
2344
|
if (!window.confirm(`确认将 @${uniqueId} 从毛料库恢复到 jobs 队列吗?`)) {
|
|
2260
2345
|
return;
|
|
2261
2346
|
}
|
|
2347
|
+
showLoading('正在恢复...');
|
|
2262
2348
|
try {
|
|
2263
2349
|
const res = await fetch('/api/raw-jobs/restore', {
|
|
2264
2350
|
method: 'POST',
|
|
@@ -2276,6 +2362,8 @@
|
|
|
2276
2362
|
await fetchRawJobs();
|
|
2277
2363
|
} catch (e) {
|
|
2278
2364
|
showToast('恢复失败: ' + e.message, true);
|
|
2365
|
+
} finally {
|
|
2366
|
+
hideLoading();
|
|
2279
2367
|
}
|
|
2280
2368
|
}
|
|
2281
2369
|
|
|
@@ -2289,6 +2377,7 @@
|
|
|
2289
2377
|
if (!window.confirm(`确认将毛料库中符合【${desc}】的任务恢复到 jobs 队列吗?`)) {
|
|
2290
2378
|
return;
|
|
2291
2379
|
}
|
|
2380
|
+
showLoading('正在恢复筛选任务...');
|
|
2292
2381
|
try {
|
|
2293
2382
|
const body = {};
|
|
2294
2383
|
if (search) body.search = search;
|
|
@@ -2309,6 +2398,8 @@
|
|
|
2309
2398
|
await fetchRawJobs();
|
|
2310
2399
|
} catch (e) {
|
|
2311
2400
|
showToast('恢复失败: ' + e.message, true);
|
|
2401
|
+
} finally {
|
|
2402
|
+
hideLoading();
|
|
2312
2403
|
}
|
|
2313
2404
|
}
|
|
2314
2405
|
|
|
@@ -2317,6 +2408,7 @@
|
|
|
2317
2408
|
if (!window.confirm(`确认将 ${country} 从毛料库恢复到 jobs 队列吗?${countText}`)) {
|
|
2318
2409
|
return;
|
|
2319
2410
|
}
|
|
2411
|
+
showLoading('正在恢复...');
|
|
2320
2412
|
try {
|
|
2321
2413
|
const res = await fetch('/api/raw-jobs/restore', {
|
|
2322
2414
|
method: 'POST',
|
|
@@ -2334,6 +2426,8 @@
|
|
|
2334
2426
|
await fetchRawJobs();
|
|
2335
2427
|
} catch (e) {
|
|
2336
2428
|
showToast('恢复失败: ' + e.message, true);
|
|
2429
|
+
} finally {
|
|
2430
|
+
hideLoading();
|
|
2337
2431
|
}
|
|
2338
2432
|
}
|
|
2339
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
|
-
});
|