tt-help-cli-ycl 1.3.16 → 1.3.17
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/tiktok-scraper.mjs +2 -1
- package/src/watch/data-store.js +135 -13
- package/src/watch/server.js +20 -1
package/package.json
CHANGED
|
@@ -99,14 +99,15 @@ export class TikTokScraper {
|
|
|
99
99
|
async warmWaf() {
|
|
100
100
|
if (this.warmPromise) return this.warmPromise;
|
|
101
101
|
this.warmPromise = (async () => {
|
|
102
|
+
const page = await this.context.newPage();
|
|
102
103
|
try {
|
|
103
|
-
const page = this.slots[0].page;
|
|
104
104
|
await page.goto(this.warmUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
105
105
|
await delay(1500);
|
|
106
106
|
this.lastWarmTime = Date.now();
|
|
107
107
|
} catch (e) {
|
|
108
108
|
console.error(`[warmWaf] failed: ${e.message}`);
|
|
109
109
|
} finally {
|
|
110
|
+
await page.close();
|
|
110
111
|
this.warmPromise = null;
|
|
111
112
|
}
|
|
112
113
|
})();
|
package/src/watch/data-store.js
CHANGED
|
@@ -10,8 +10,53 @@ function inferStatus(u) {
|
|
|
10
10
|
|
|
11
11
|
export function createStore(filePath) {
|
|
12
12
|
let data = [];
|
|
13
|
+
// uniqueId → index 内存索引,O(1) 查找
|
|
14
|
+
let uidIndex = new Map();
|
|
13
15
|
let clientErrors = new Map();
|
|
14
16
|
|
|
17
|
+
// done 用户归档(独立文件,主文件只保留活跃用户)
|
|
18
|
+
let doneArchive = [];
|
|
19
|
+
let doneArchivePath = null;
|
|
20
|
+
let doneUidIndex = new Map();
|
|
21
|
+
if (filePath) {
|
|
22
|
+
doneArchivePath = path.resolve(filePath).replace(/\.json$/, '-done.json');
|
|
23
|
+
if (fs.existsSync(doneArchivePath)) {
|
|
24
|
+
try {
|
|
25
|
+
const content = fs.readFileSync(doneArchivePath, 'utf-8');
|
|
26
|
+
const parsed = JSON.parse(content);
|
|
27
|
+
if (Array.isArray(parsed)) doneArchive = parsed;
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error(`[data-store] 读取 done 归档失败: ${e.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// stats 缓存
|
|
35
|
+
let statsCache = null;
|
|
36
|
+
let statsDirty = true;
|
|
37
|
+
|
|
38
|
+
function markStatsDirty() {
|
|
39
|
+
statsDirty = true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function computeStatsInternal() {
|
|
43
|
+
const total = data.length;
|
|
44
|
+
const statusCounts = { pending: 0, processing: 0, done: 0, error: 0, restricted: 0 };
|
|
45
|
+
for (const u of data) {
|
|
46
|
+
statusCounts[u.status] = (statusCounts[u.status] || 0) + 1;
|
|
47
|
+
}
|
|
48
|
+
statsCache = { total, statusCounts };
|
|
49
|
+
statsDirty = false;
|
|
50
|
+
return statsCache;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getStats() {
|
|
54
|
+
if (statsDirty) {
|
|
55
|
+
return computeStatsInternal();
|
|
56
|
+
}
|
|
57
|
+
return statsCache;
|
|
58
|
+
}
|
|
59
|
+
|
|
15
60
|
// 视频存储(独立 JSON 文件)
|
|
16
61
|
let videos = [];
|
|
17
62
|
let videoFilePath = null;
|
|
@@ -74,15 +119,64 @@ export function createStore(filePath) {
|
|
|
74
119
|
backupTimer = setInterval(runBackup, 60 * 60 * 1000);
|
|
75
120
|
}
|
|
76
121
|
|
|
77
|
-
|
|
122
|
+
// 构建索引 + 推断 status
|
|
123
|
+
for (let i = 0; i < data.length; i++) {
|
|
124
|
+
const u = data[i];
|
|
78
125
|
if (!u.status) u.status = inferStatus(u);
|
|
126
|
+
uidIndex.set(u.uniqueId, i);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 构建 done 归档索引
|
|
130
|
+
for (let i = 0; i < doneArchive.length; i++) {
|
|
131
|
+
doneUidIndex.set(doneArchive[i].uniqueId, i);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function rebuildIndex() {
|
|
135
|
+
uidIndex = new Map();
|
|
136
|
+
for (let i = 0; i < data.length; i++) {
|
|
137
|
+
uidIndex.set(data[i].uniqueId, i);
|
|
138
|
+
}
|
|
79
139
|
}
|
|
80
140
|
|
|
81
141
|
function save() {
|
|
82
142
|
if (!filePath) return;
|
|
83
143
|
const resolved = path.resolve(filePath);
|
|
84
|
-
|
|
85
|
-
|
|
144
|
+
|
|
145
|
+
// 将 done 用户归档到独立文件
|
|
146
|
+
if (doneArchivePath && data.length > 1000) {
|
|
147
|
+
const active = [];
|
|
148
|
+
const toArchive = [];
|
|
149
|
+
for (const u of data) {
|
|
150
|
+
if (u.status === 'done') {
|
|
151
|
+
toArchive.push(u);
|
|
152
|
+
} else {
|
|
153
|
+
active.push(u);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (toArchive.length > 0) {
|
|
157
|
+
doneArchive = doneArchive.concat(toArchive);
|
|
158
|
+
data = active;
|
|
159
|
+
rebuildIndex();
|
|
160
|
+
// 重建 done 归档索引
|
|
161
|
+
doneUidIndex = new Map();
|
|
162
|
+
for (let i = 0; i < doneArchive.length; i++) {
|
|
163
|
+
doneUidIndex.set(doneArchive[i].uniqueId, i);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const json = JSON.stringify(data);
|
|
169
|
+
const tmpPath = resolved + '.tmp';
|
|
170
|
+
fs.writeFileSync(tmpPath, json, 'utf-8');
|
|
171
|
+
fs.renameSync(tmpPath, resolved);
|
|
172
|
+
|
|
173
|
+
// 写 done 归档
|
|
174
|
+
if (doneArchivePath && doneArchive.length > 0) {
|
|
175
|
+
const doneJson = JSON.stringify(doneArchive);
|
|
176
|
+
const doneTmp = doneArchivePath + '.tmp';
|
|
177
|
+
fs.writeFileSync(doneTmp, doneJson, 'utf-8');
|
|
178
|
+
fs.renameSync(doneTmp, doneArchivePath);
|
|
179
|
+
}
|
|
86
180
|
}
|
|
87
181
|
|
|
88
182
|
function saveVideos() {
|
|
@@ -99,15 +193,19 @@ export function createStore(filePath) {
|
|
|
99
193
|
}
|
|
100
194
|
|
|
101
195
|
function getUser(uid) {
|
|
102
|
-
|
|
196
|
+
const idx = uidIndex.get(uid);
|
|
197
|
+
if (idx !== undefined) return data[idx];
|
|
198
|
+
const doneIdx = doneUidIndex.get(uid);
|
|
199
|
+
if (doneIdx !== undefined) return doneArchive[doneIdx];
|
|
200
|
+
return undefined;
|
|
103
201
|
}
|
|
104
202
|
|
|
105
203
|
function hasUser(uid) {
|
|
106
|
-
return
|
|
204
|
+
return uidIndex.has(uid) || doneUidIndex.has(uid);
|
|
107
205
|
}
|
|
108
206
|
|
|
109
207
|
function userExists(uid) {
|
|
110
|
-
return
|
|
208
|
+
return uidIndex.has(uid) || doneUidIndex.has(uid);
|
|
111
209
|
}
|
|
112
210
|
|
|
113
211
|
function addUser(user, append) {
|
|
@@ -122,8 +220,15 @@ export function createStore(filePath) {
|
|
|
122
220
|
} else {
|
|
123
221
|
if (!user.status) user.status = inferStatus(user);
|
|
124
222
|
if (user.processed) user.processedAt = user.processedAt || Date.now();
|
|
125
|
-
if (append)
|
|
126
|
-
|
|
223
|
+
if (append) {
|
|
224
|
+
const idx = data.length;
|
|
225
|
+
data.push(user);
|
|
226
|
+
uidIndex.set(user.uniqueId, idx);
|
|
227
|
+
} else {
|
|
228
|
+
data.unshift(user);
|
|
229
|
+
uidIndex.set(user.uniqueId, 0);
|
|
230
|
+
}
|
|
231
|
+
markStatsDirty();
|
|
127
232
|
}
|
|
128
233
|
}
|
|
129
234
|
|
|
@@ -132,10 +237,13 @@ export function createStore(filePath) {
|
|
|
132
237
|
}
|
|
133
238
|
|
|
134
239
|
function getProcessedUsers() {
|
|
135
|
-
return data.filter(u => u.status === 'done');
|
|
240
|
+
return doneArchive.length > 0 ? data.filter(u => u.status === 'done').concat(doneArchive) : data.filter(u => u.status === 'done');
|
|
136
241
|
}
|
|
137
242
|
|
|
138
243
|
function getAllUsers() {
|
|
244
|
+
if (doneArchive.length > 0) {
|
|
245
|
+
return data.concat(doneArchive);
|
|
246
|
+
}
|
|
139
247
|
return data;
|
|
140
248
|
}
|
|
141
249
|
|
|
@@ -173,6 +281,7 @@ export function createStore(filePath) {
|
|
|
173
281
|
);
|
|
174
282
|
if (expired) {
|
|
175
283
|
expired.status = 'pending';
|
|
284
|
+
markStatsDirty();
|
|
176
285
|
delete expired.claimedAt;
|
|
177
286
|
next = expired;
|
|
178
287
|
}
|
|
@@ -204,6 +313,7 @@ export function createStore(filePath) {
|
|
|
204
313
|
|
|
205
314
|
if (next) {
|
|
206
315
|
next.status = 'processing';
|
|
316
|
+
markStatsDirty();
|
|
207
317
|
next.claimedAt = now;
|
|
208
318
|
next.claimedBy = userId;
|
|
209
319
|
return { uniqueId: next.uniqueId, nickname: next.nickname, claimedAt: next.claimedAt, claimedBy: userId };
|
|
@@ -241,10 +351,19 @@ export function createStore(filePath) {
|
|
|
241
351
|
}),
|
|
242
352
|
].filter(u => u.uniqueId);
|
|
243
353
|
|
|
244
|
-
|
|
354
|
+
// 先对 discovered 内部去重,再用 uidIndex 批量判断
|
|
355
|
+
const seen = new Set();
|
|
356
|
+
const unique = [];
|
|
245
357
|
for (const d of discovered) {
|
|
246
|
-
|
|
247
|
-
|
|
358
|
+
if (!seen.has(d.uniqueId)) {
|
|
359
|
+
seen.add(d.uniqueId);
|
|
360
|
+
unique.push(d);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const newUsers = [];
|
|
365
|
+
for (const d of unique) {
|
|
366
|
+
if (!hasUser(d.uniqueId)) {
|
|
248
367
|
addUser(d, true);
|
|
249
368
|
newUsers.push(d.uniqueId);
|
|
250
369
|
}
|
|
@@ -253,6 +372,7 @@ export function createStore(filePath) {
|
|
|
253
372
|
}
|
|
254
373
|
|
|
255
374
|
function updateUserFromResult(user, result) {
|
|
375
|
+
const oldStatus = user.status;
|
|
256
376
|
if (result.restricted) {
|
|
257
377
|
user.status = 'restricted';
|
|
258
378
|
if (result.userInfo) {
|
|
@@ -311,6 +431,7 @@ export function createStore(filePath) {
|
|
|
311
431
|
}
|
|
312
432
|
user.sources = [...new Set([...(user.sources || []), 'processed'])];
|
|
313
433
|
}
|
|
434
|
+
if (user.status !== oldStatus) markStatsDirty();
|
|
314
435
|
}
|
|
315
436
|
|
|
316
437
|
function commitJob(uniqueId, result) {
|
|
@@ -351,6 +472,7 @@ export function createStore(filePath) {
|
|
|
351
472
|
const user = getUser(uniqueId);
|
|
352
473
|
if (!user) return { saved: false, error: 'user not found' };
|
|
353
474
|
user.status = 'pending';
|
|
475
|
+
markStatsDirty();
|
|
354
476
|
delete user.claimedAt;
|
|
355
477
|
delete user.processedAt;
|
|
356
478
|
delete user.processed;
|
|
@@ -524,7 +646,7 @@ export function createStore(filePath) {
|
|
|
524
646
|
|
|
525
647
|
return {
|
|
526
648
|
save, getUser, hasUser, userExists, addUser,
|
|
527
|
-
getPendingUsers, getProcessedUsers, getAllUsers,
|
|
649
|
+
getPendingUsers, getProcessedUsers, getAllUsers, getStats,
|
|
528
650
|
claimNextJob, commitJob, commitNewExplore, resetJob, togglePin,
|
|
529
651
|
getNextRedoJob, commitRedoJob,
|
|
530
652
|
getPendingUserUpdateTasks, updateUserInfo,
|
package/src/watch/server.js
CHANGED
|
@@ -280,6 +280,8 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
280
280
|
|
|
281
281
|
if (req.method === 'GET' && routePath === '/api/stats') {
|
|
282
282
|
const stats = computeStats(store.getAllUsers());
|
|
283
|
+
const quick = store.getStats();
|
|
284
|
+
if (quick) stats.quickStats = quick;
|
|
283
285
|
sendJSON(res, 200, stats);
|
|
284
286
|
return;
|
|
285
287
|
}
|
|
@@ -467,7 +469,24 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
467
469
|
|
|
468
470
|
const limit = parseInt(params.limit) || 50;
|
|
469
471
|
const offset = parseInt(params.offset) || 0;
|
|
470
|
-
|
|
472
|
+
let paged = filtered.slice(offset, offset + limit);
|
|
473
|
+
|
|
474
|
+
// 字段裁剪:轻量视图只返回必要字段
|
|
475
|
+
if (params.view === 'light') {
|
|
476
|
+
paged = paged.map(u => ({
|
|
477
|
+
uniqueId: u.uniqueId,
|
|
478
|
+
nickname: u.nickname,
|
|
479
|
+
status: u.status,
|
|
480
|
+
sources: u.sources,
|
|
481
|
+
ttSeller: u.ttSeller,
|
|
482
|
+
verified: u.verified,
|
|
483
|
+
followerCount: u.followerCount,
|
|
484
|
+
locationCreated: u.locationCreated,
|
|
485
|
+
guessedLocation: u.guessedLocation,
|
|
486
|
+
pinned: u.pinned,
|
|
487
|
+
processedAt: u.processedAt,
|
|
488
|
+
}));
|
|
489
|
+
}
|
|
471
490
|
|
|
472
491
|
sendJSON(res, 200, { total: filtered.length, users: paged });
|
|
473
492
|
return;
|