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.
@@ -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
- const origText = btn.innerHTML;
1819
- btn.disabled = true;
1820
- btn.style.opacity = '0.6';
1821
- btn.style.cursor = 'not-allowed';
1822
- btn.innerHTML = '&#x21bb; 处理中...';
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
- btn.disabled = false;
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
 
@@ -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
- });
@@ -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
- });