tt-help-cli-ycl 1.3.58 → 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 +1 -1
- package/src/lib/browser/page.js +69 -10
- package/src/scraper/explore-core.js +1 -1
- package/src/watch/data-store.js +9 -0
- package/src/watch/public/app.js +61 -1
- package/src/watch/public/index.html +1 -1
- package/src/watch/public/style.css +25 -0
- package/src/watch/server.js +1 -0
package/package.json
CHANGED
package/src/lib/browser/page.js
CHANGED
|
@@ -67,20 +67,50 @@ export async function withBrowserRecovery(fn, browser, page, cdpOptions, port) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
76
|
-
if (!hasSessionId) return false;
|
|
79
|
+
const sessionCookie = cookies.find((c) => c.name === "sessionid");
|
|
77
80
|
|
|
78
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
166
|
+
// 明确看到登录按钮 → 未登录
|
|
167
|
+
if (hasLoginButton) return false;
|
|
168
|
+
// 看到已登录元素 → 已登录
|
|
169
|
+
if (hasProfileContainer || hasUserMenu) return true;
|
|
170
|
+
// 元素已出现但都不是登录/未登录标志 → 无法判断
|
|
171
|
+
return null;
|
|
113
172
|
});
|
|
114
173
|
}
|
|
115
174
|
|
|
@@ -183,7 +183,7 @@ async function processExplore(page, username, options, log) {
|
|
|
183
183
|
result.videoList = videoArray;
|
|
184
184
|
} else {
|
|
185
185
|
// 国家不匹配
|
|
186
|
-
result.restricted = true
|
|
186
|
+
// result.restricted = true;//不应该属于受限
|
|
187
187
|
result.keepFollow = false;
|
|
188
188
|
result.discoveredFollowing = [];
|
|
189
189
|
result.discoveredFollowers = [];
|
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
|
}
|