tt-help-cli-ycl 1.3.59 → 1.3.61
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
package/src/lib/browser/page.js
CHANGED
|
@@ -66,21 +66,45 @@ export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
const DOM_CHECK_TIMEOUT = 20000; // 单次 DOM 检测超时 20 秒
|
|
70
|
+
const DOM_CHECK_RETRIES = 3; // DOM 检测最大重试次数
|
|
71
|
+
|
|
69
72
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
73
|
+
* 判断登录状态:Cookie 为主,DOM 验真为辅。
|
|
74
|
+
* - 无 sessionid Cookie → 未登录
|
|
75
|
+
* - 有 Cookie + DOM 确认已登录 → 已登录
|
|
76
|
+
* - 有 Cookie + DOM 确认未登录(出现登录按钮)→ 未登录
|
|
77
|
+
* - 有 Cookie + DOM 无法判断(超时/元素未找到),重试后仍无法判断 → 信任 Cookie,判定已登录
|
|
72
78
|
*/
|
|
73
79
|
export async function isLoggedIn(page) {
|
|
74
80
|
const cookies = await page.context().cookies("https://www.tiktok.com");
|
|
75
81
|
const hasSessionId = cookies.some((c) => c.name === "sessionid");
|
|
76
82
|
if (!hasSessionId) return false;
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
// 重试 DOM 检测,直到得到明确结果或耗尽重试次数
|
|
85
|
+
for (let attempt = 1; attempt <= DOM_CHECK_RETRIES; attempt++) {
|
|
86
|
+
const domResult = await isLoggedInByDom(page);
|
|
87
|
+
// domResult: true=已登录, false=明确未登录, null=无法判断
|
|
88
|
+
if (domResult === true) return true;
|
|
89
|
+
if (domResult === false) return false;
|
|
90
|
+
// null: DOM 无法判断,刷新页面后重试
|
|
91
|
+
if (attempt < DOM_CHECK_RETRIES) {
|
|
92
|
+
console.error(
|
|
93
|
+
` [登录检测] DOM 无法判断,刷新页面后重试 (${attempt}/${DOM_CHECK_RETRIES})...`,
|
|
94
|
+
);
|
|
95
|
+
await page.reload({ waitUntil: "domcontentloaded" });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 重试后仍无法判断,信任 Cookie
|
|
99
|
+
console.error(
|
|
100
|
+
` [登录检测] DOM 检测 ${DOM_CHECK_RETRIES} 次均未明确结果,信任 Cookie 判定为已登录`,
|
|
101
|
+
);
|
|
102
|
+
return true;
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
/**
|
|
82
106
|
* 通过 DOM 元素判断登录状态(验真方案)
|
|
83
|
-
*
|
|
107
|
+
* @returns {boolean|null} true=已登录, false=明确未登录, null=无法判断
|
|
84
108
|
*/
|
|
85
109
|
export async function isLoggedInByDom(page) {
|
|
86
110
|
// 先等客户端渲染完成:登录态元素或登录按钮,哪个先出现就停止等待
|
|
@@ -94,9 +118,15 @@ export async function isLoggedInByDom(page) {
|
|
|
94
118
|
'button:has-text("Sign in")',
|
|
95
119
|
].join(", ");
|
|
96
120
|
|
|
97
|
-
await page
|
|
98
|
-
.waitForSelector(loginOrLoggedInSelector, { timeout:
|
|
99
|
-
.
|
|
121
|
+
const selectorFound = await page
|
|
122
|
+
.waitForSelector(loginOrLoggedInSelector, { timeout: DOM_CHECK_TIMEOUT })
|
|
123
|
+
.then(() => true)
|
|
124
|
+
.catch(() => false);
|
|
125
|
+
|
|
126
|
+
if (!selectorFound) {
|
|
127
|
+
// 等待超时,DOM 无法判断
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
100
130
|
|
|
101
131
|
return page.evaluate(() => {
|
|
102
132
|
const hasProfileContainer = !!document.querySelector(
|
|
@@ -109,7 +139,12 @@ export async function isLoggedInByDom(page) {
|
|
|
109
139
|
document.querySelectorAll('button, [role="button"]'),
|
|
110
140
|
).some((el) => /^(登录|Log in|Sign in)$/i.test(el.textContent.trim()));
|
|
111
141
|
|
|
112
|
-
|
|
142
|
+
// 明确看到登录按钮 → 未登录
|
|
143
|
+
if (hasLoginButton) return false;
|
|
144
|
+
// 看到已登录元素 → 已登录
|
|
145
|
+
if (hasProfileContainer || hasUserMenu) return true;
|
|
146
|
+
// 元素已出现但都不是登录/未登录标志 → 无法判断
|
|
147
|
+
return null;
|
|
113
148
|
});
|
|
114
149
|
}
|
|
115
150
|
|
package/src/watch/data-store.js
CHANGED
|
@@ -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,
|
package/src/watch/public/app.js
CHANGED
|
@@ -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">${
|
|
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);
|
|
@@ -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;
|
package/src/watch/server.js
CHANGED
|
@@ -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
|
}
|