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.
@@ -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
- const origText = btn.innerHTML;
1824
- btn.disabled = true;
1825
- btn.style.opacity = '0.6';
1826
- btn.style.cursor = 'not-allowed';
1827
- btn.innerHTML = '&#x21bb; 处理中...';
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
- btn.disabled = false;
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
 
@@ -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.split(",").map((c) => c.trim().toUpperCase()).filter(Boolean)
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(`[JOB ${ts}] USER-UPDATE-TASKS: ${tasks.length} tasks${countries.length ? ` (countries: ${countries.join(",")})` : ""}`);
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, { error: "missing filter: uniqueId, country, or search/location" });
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
- });
@@ -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
- });