tt-help-cli-ycl 1.3.59 → 1.3.60

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.3.59",
3
+ "version": "1.3.60",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,20 +67,50 @@ export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
67
67
  }
68
68
 
69
69
  /**
70
- * 稳定判断登录状态:先检查 sessionid Cookie,再用页面 DOM 做验真。
71
- * 仅有 sessionid 不足以说明会话仍然有效,因此需要二次确认。
70
+ * 判断登录状态:Cookie 为主,DOM 验真为辅。
71
+ * - sessionid Cookie → 未登录
72
+ * - sessionid Cookie 已过期 → 未登录
73
+ * - 有 Cookie + DOM 确认已登录 → 已登录
74
+ * - 有 Cookie + DOM 确认未登录(出现登录按钮)→ 未登录
75
+ * - 有 Cookie + DOM 无法判断(超时/元素未找到)→ 信任 Cookie,判定已登录
72
76
  */
73
77
  export async function isLoggedIn(page) {
74
78
  const cookies = await page.context().cookies("https://www.tiktok.com");
75
- const hasSessionId = cookies.some((c) => c.name === "sessionid");
76
- if (!hasSessionId) return false;
79
+ const sessionCookie = cookies.find((c) => c.name === "sessionid");
77
80
 
78
- return await isLoggedInByDom(page);
81
+ if (!sessionCookie) {
82
+ console.error("[登录检测] 无 sessionid Cookie,判定未登录");
83
+ return false;
84
+ }
85
+
86
+ // Cookie 过期检查:TikTok sessionid 可能是 session cookie(expires=-1)
87
+ // 只有当 expires > 0 且已过期时才判定无效
88
+ if (
89
+ sessionCookie.expires > 0 &&
90
+ sessionCookie.expires < Math.floor(Date.now() / 1000)
91
+ ) {
92
+ console.error("[登录检测] sessionid Cookie 已过期,判定未登录");
93
+ return false;
94
+ }
95
+
96
+ const domResult = await isLoggedInByDom(page);
97
+ // domResult: true=已登录, false=明确未登录, null=无法判断
98
+ if (domResult === true) {
99
+ console.error("[登录检测] DOM 验真确认已登录");
100
+ return true;
101
+ }
102
+ if (domResult === false) {
103
+ console.error("[登录检测] DOM 验真发现登录按钮,判定未登录(Cookie 可能已失效)");
104
+ return false;
105
+ }
106
+ // null: DOM 无法判断,信任 Cookie
107
+ console.error("[登录检测] DOM 验真超时,信任 Cookie 判定已登录");
108
+ return true;
79
109
  }
80
110
 
81
111
  /**
82
112
  * 通过 DOM 元素判断登录状态(验真方案)
83
- * 依赖页面渲染完成,因此不单独作为主判断,而是和 Cookie 组合使用。
113
+ * @returns {boolean|null} true=已登录, false=明确未登录, null=无法判断
84
114
  */
85
115
  export async function isLoggedInByDom(page) {
86
116
  // 先等客户端渲染完成:登录态元素或登录按钮,哪个先出现就停止等待
@@ -94,9 +124,33 @@ export async function isLoggedInByDom(page) {
94
124
  'button:has-text("Sign in")',
95
125
  ].join(", ");
96
126
 
97
- await page
98
- .waitForSelector(loginOrLoggedInSelector, { timeout: 10000 })
99
- .catch(() => {});
127
+ // 分阶段等待:先 5 秒,超时再重试 10 秒
128
+ const timeouts = [5000, 10000];
129
+ let selectorFound = false;
130
+
131
+ for (let i = 0; i < timeouts.length; i++) {
132
+ const found = await page
133
+ .waitForSelector(loginOrLoggedInSelector, { timeout: timeouts[i] })
134
+ .then(() => true)
135
+ .catch(() => false);
136
+
137
+ if (found) {
138
+ selectorFound = true;
139
+ break;
140
+ }
141
+ // 非最后一次,输出重试日志
142
+ if (i < timeouts.length - 1) {
143
+ console.error(
144
+ `[登录检测] DOM 元素等待超时 (${timeouts[i]}ms),重试中...`,
145
+ );
146
+ }
147
+ }
148
+
149
+ if (!selectorFound) {
150
+ // 所有阶段都超时,DOM 无法判断
151
+ console.error(`[登录检测] DOM 验真超时 (${timeouts[timeouts.length - 1]}ms),无法判断`);
152
+ return null;
153
+ }
100
154
 
101
155
  return page.evaluate(() => {
102
156
  const hasProfileContainer = !!document.querySelector(
@@ -109,7 +163,12 @@ export async function isLoggedInByDom(page) {
109
163
  document.querySelectorAll('button, [role="button"]'),
110
164
  ).some((el) => /^(登录|Log in|Sign in)$/i.test(el.textContent.trim()));
111
165
 
112
- return (hasProfileContainer || hasUserMenu) && !hasLoginButton;
166
+ // 明确看到登录按钮 未登录
167
+ if (hasLoginButton) return false;
168
+ // 看到已登录元素 → 已登录
169
+ if (hasProfileContainer || hasUserMenu) return true;
170
+ // 元素已出现但都不是登录/未登录标志 → 无法判断
171
+ return null;
113
172
  });
114
173
  }
115
174
 
@@ -1519,6 +1519,8 @@ export function createStore(filePath) {
1519
1519
  // uniqueId → index 内存索引,O(1) 查找
1520
1520
  let uidIndex = new Map();
1521
1521
  let clientErrors = new Map();
1522
+ // 客户端登录状态:userId → boolean
1523
+ let clientLoginStatus = new Map();
1522
1524
  if (filePath) {
1523
1525
  // 初始化 SQLite 用户表(用于判重)
1524
1526
  initUserDb(filePath);
@@ -1782,6 +1784,8 @@ export function createStore(filePath) {
1782
1784
  locations = null,
1783
1785
  loggedIn = true,
1784
1786
  ) {
1787
+ // 记录客户端登录状态
1788
+ clientLoginStatus.set(userId, !!loggedIn);
1785
1789
  if (db) {
1786
1790
  const now = Date.now();
1787
1791
  const ongoingRow = db
@@ -2959,6 +2963,10 @@ export function createStore(filePath) {
2959
2963
  return Array.from(clientErrors.values());
2960
2964
  }
2961
2965
 
2966
+ function getClientLoginStatus() {
2967
+ return Object.fromEntries(clientLoginStatus);
2968
+ }
2969
+
2962
2970
  function getPendingUserUpdateTasks(limit, countries) {
2963
2971
  const targetCountries = countries
2964
2972
  ? countries.map((c) => String(c).trim().toUpperCase())
@@ -3363,6 +3371,7 @@ export function createStore(filePath) {
3363
3371
  reportClientError,
3364
3372
  deleteClientError,
3365
3373
  getClientErrors,
3374
+ getClientLoginStatus,
3366
3375
  registerVideos,
3367
3376
  getVideo,
3368
3377
  getVideos,
@@ -18,6 +18,7 @@ let currentUsers = [];
18
18
  let currentLocation = "";
19
19
  let currentTargetLocation = "";
20
20
  let currentRawLocation = "";
21
+ let currentSort = { key: null, asc: true };
21
22
  let prevStatValues = {};
22
23
  let prevUserMap = {};
23
24
 
@@ -285,6 +286,23 @@ function renderSourceChart(sources) {
285
286
  }
286
287
 
287
288
  function renderTable(users) {
289
+ // 应用本地排序
290
+ if (currentSort.key) {
291
+ users = [...users].sort((a, b) => {
292
+ let va = a[currentSort.key];
293
+ let vb = b[currentSort.key];
294
+ if (va == null && vb == null) return 0;
295
+ if (va == null) return 1;
296
+ if (vb == null) return -1;
297
+ if (typeof va === "number" && typeof vb === "number") {
298
+ return currentSort.asc ? va - vb : vb - va;
299
+ }
300
+ va = String(va).toLowerCase();
301
+ vb = String(vb).toLowerCase();
302
+ return currentSort.asc ? va.localeCompare(vb) : vb.localeCompare(va);
303
+ });
304
+ }
305
+
288
306
  const el = document.getElementById("userTable");
289
307
 
290
308
  const newUserMap = {};
@@ -332,6 +350,22 @@ function renderTable(users) {
332
350
  : "-";
333
351
  const guessedLoc = u.guessedLocation || "-";
334
352
  const claimer = u.claimedBy || "-";
353
+ // 只有处理中的用户才显示登录状态
354
+ const loginStatus =
355
+ u.status === "processing"
356
+ ? currentStats?.clientLoginStatus?.[claimer]
357
+ : undefined;
358
+ let claimerDisplay = claimer;
359
+ let loginBadge = "";
360
+ if (claimer && claimer !== "-" && u.status === "processing") {
361
+ if (loginStatus === true) {
362
+ loginBadge =
363
+ ' <span class="tag keep-follow" style="font-size:9px">已登录</span>';
364
+ } else if (loginStatus === false) {
365
+ loginBadge =
366
+ ' <span class="tag no-follow" style="font-size:9px">未登录</span>';
367
+ }
368
+ }
335
369
  const claimTime = u.claimedAt ? formatTime(u.claimedAt) : "-";
336
370
  const procTime = u.processedAt ? formatTime(u.processedAt) : "-";
337
371
  const statusCodeDisplay =
@@ -349,7 +383,7 @@ function renderTable(users) {
349
383
  <td data-label="来源">${sources || "-"}</td>
350
384
  <td data-label="状态">${statusTag} ${extraTags.join(" ")}</td>
351
385
  <td data-label="StatusCode">${statusCodeDisplay}</td>
352
- <td data-label="处理端" style="font-size:11px;color:#888">${claimer}</td>
386
+ <td data-label="处理端" style="font-size:11px;color:#888">${claimerDisplay}${loginBadge}</td>
353
387
  <td data-label="领取时间" style="font-size:11px;color:#888">${claimTime}</td>
354
388
  <td data-label="完成时间" style="font-size:11px;color:#888">${procTime}</td>
355
389
  </tr>`;
@@ -1256,10 +1290,36 @@ function filterByUserUpdateCountry(country) {
1256
1290
  }, 100);
1257
1291
  }
1258
1292
 
1293
+ function initTableSorting() {
1294
+ document.querySelectorAll("th.sortable").forEach((th) => {
1295
+ th.addEventListener("click", () => {
1296
+ const key = th.dataset.sort;
1297
+ if (!key) return;
1298
+ if (currentSort.key === key) {
1299
+ currentSort.asc = !currentSort.asc;
1300
+ } else {
1301
+ currentSort.key = key;
1302
+ currentSort.asc = true;
1303
+ }
1304
+ // 更新排序指示器
1305
+ document.querySelectorAll("th.sortable").forEach((h) => {
1306
+ h.classList.remove("sort-asc", "sort-desc");
1307
+ const icon = h.querySelector(".sort-icon");
1308
+ if (icon) icon.textContent = "↕";
1309
+ });
1310
+ th.classList.add(currentSort.asc ? "sort-asc" : "sort-desc");
1311
+ const icon = th.querySelector(".sort-icon");
1312
+ if (icon) icon.textContent = currentSort.asc ? "↑" : "↓";
1313
+ renderTable(currentUsers);
1314
+ });
1315
+ });
1316
+ }
1317
+
1259
1318
  // 初始化
1260
1319
  fetchStats();
1261
1320
  fetchUsers();
1262
1321
  fetchClientErrors();
1322
+ initTableSorting();
1263
1323
  setInterval(fetchStats, 10000);
1264
1324
  setInterval(fetchUsers, 10000);
1265
1325
  setInterval(fetchClientErrors, 10000);
@@ -125,7 +125,7 @@
125
125
  <th>粉丝</th>
126
126
  <th>视频</th>
127
127
  <th>国家</th>
128
- <th>最近发布</th>
128
+ <th class="sortable" data-sort="latestVideoTime">最近发布 <span class="sort-icon">↕</span></th>
129
129
  <th>猜测国家</th>
130
130
  <th>来源</th>
131
131
  <th>状态</th>
@@ -529,6 +529,31 @@ th {
529
529
  white-space: nowrap;
530
530
  }
531
531
 
532
+ th.sortable {
533
+ cursor: pointer;
534
+ user-select: none;
535
+ }
536
+
537
+ th.sortable:hover {
538
+ color: #fe2c55;
539
+ }
540
+
541
+ th.sortable .sort-icon {
542
+ font-size: 10px;
543
+ opacity: 0.4;
544
+ margin-left: 2px;
545
+ }
546
+
547
+ th.sortable.sort-asc .sort-icon {
548
+ opacity: 1;
549
+ color: #fe2c55;
550
+ }
551
+
552
+ th.sortable.sort-desc .sort-icon {
553
+ opacity: 1;
554
+ color: #fe2c55;
555
+ }
556
+
532
557
  td {
533
558
  padding: 6px 10px;
534
559
  border-bottom: 1px solid #1f1f2a;
@@ -349,6 +349,7 @@ export function startWatchServer(dataAnchor, port = 3000, existingStore) {
349
349
  if (req.method === "GET" && routePath === "/api/stats") {
350
350
  const stats = computeStatsIncremental(store);
351
351
  stats.targetLocations = DEFAULT_TARGET_LOCATIONS;
352
+ stats.clientLoginStatus = store.getClientLoginStatus();
352
353
  sendJSON(res, 200, stats);
353
354
  return;
354
355
  }