tt-help-cli-ycl 1.3.13 → 1.3.14
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 +3 -1
- package/src/cli/attach.js +160 -0
- package/src/cli/config.js +39 -3
- package/src/cli/explore.js +17 -10
- package/src/cli/info.js +88 -0
- package/src/lib/args.js +76 -15
- package/src/lib/constants.js +100 -20
- package/src/lib/parse-ssr.mjs +69 -0
- package/src/lib/scrape.js +69 -20
- package/src/lib/tiktok-scraper.mjs +176 -0
- package/src/lib/url.js +2 -2
- package/src/main.js +12 -18
- package/src/scraper/explore-core.js +56 -74
- package/src/watch/data-store.js +112 -7
- package/src/watch/server.js +44 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
delay,
|
|
3
3
|
ensureBrowserReady,
|
|
4
|
-
setDelayConfig,
|
|
5
|
-
closeCommentPanel,
|
|
6
4
|
retryWithBackoff,
|
|
7
5
|
detectPageError,
|
|
8
6
|
isLoggedIn,
|
|
@@ -10,19 +8,14 @@ import {
|
|
|
10
8
|
} from './modules/page-helpers.js';
|
|
11
9
|
import { detectCaptcha } from './modules/captcha-handler.js';
|
|
12
10
|
export { ensureBrowserReady };
|
|
13
|
-
import {
|
|
14
|
-
getUserInfo,
|
|
15
|
-
collectVideos,
|
|
16
|
-
} from '../videos/core.js';
|
|
17
|
-
import { scrapeSingleVideo } from './core.js';
|
|
11
|
+
import { getUserInfo, collectVideos } from '../videos/core.js';
|
|
18
12
|
import { extractFollowAndFollowers } from './modules/follow-extractor.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
13
|
+
import { extractVideoLocation } from '../lib/scrape.js';
|
|
14
|
+
import { maxFollowing as globalMaxFollowing, maxFollowers as globalMaxFollowers, maxVideos as globalMaxVideos } from '../lib/constants.js';
|
|
21
15
|
|
|
22
16
|
async function processExplore(page, username, options, log) {
|
|
23
17
|
const {
|
|
24
|
-
|
|
25
|
-
maxGuess = 0,
|
|
18
|
+
maxVideos = 1,
|
|
26
19
|
enableFollow = true,
|
|
27
20
|
maxFollowing = 5,
|
|
28
21
|
maxFollowers = 5,
|
|
@@ -70,7 +63,10 @@ async function processExplore(page, username, options, log) {
|
|
|
70
63
|
result.captchaMessage = result.captchaMessage || '视频页出现验证码';
|
|
71
64
|
}
|
|
72
65
|
|
|
73
|
-
const
|
|
66
|
+
const isSeller = result.userInfo?.ttSeller === true;
|
|
67
|
+
const effectiveMaxVideos = isSeller ? globalMaxVideos : maxVideos;
|
|
68
|
+
if (isSeller) log(` 商家用户,视频采集数: ${effectiveMaxVideos}`);
|
|
69
|
+
const videoList = await collectVideos(page, username, effectiveMaxVideos, log);
|
|
74
70
|
const videoArray = videoList ? [...videoList.values()] : [];
|
|
75
71
|
result.collectedVideos = videoArray.length;
|
|
76
72
|
|
|
@@ -87,84 +83,70 @@ async function processExplore(page, username, options, log) {
|
|
|
87
83
|
return result;
|
|
88
84
|
}
|
|
89
85
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
);
|
|
103
|
-
result.discoveredFollowing = following || [];
|
|
104
|
-
result.discoveredFollowers = followers || [];
|
|
105
|
-
result.hasFollowData = true;
|
|
106
|
-
log(` 关注: ${result.discoveredFollowing.length}, 粉丝: ${result.discoveredFollowers.length}`);
|
|
107
|
-
} catch (e) {
|
|
108
|
-
log(` 关注/粉丝提取失败: ${e.message}`);
|
|
109
|
-
result.hasFollowData = false;
|
|
110
|
-
result.discoveredFollowing = [];
|
|
111
|
-
result.discoveredFollowers = [];
|
|
112
|
-
}
|
|
86
|
+
// 从第一个视频获取 locationCreated
|
|
87
|
+
let locationCreated = null;
|
|
88
|
+
if (videoArray.length > 0) {
|
|
89
|
+
const firstVideo = videoArray[0];
|
|
90
|
+
const firstVideoUrl = firstVideo.href.startsWith('http')
|
|
91
|
+
? firstVideo.href
|
|
92
|
+
: `https://www.tiktok.com${firstVideo.href}`;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
locationCreated = await extractVideoLocation(firstVideoUrl);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
log(` 获取视频国家失败: ${e.message}`);
|
|
113
98
|
}
|
|
114
99
|
}
|
|
115
100
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
? firstVideo.href
|
|
119
|
-
: `https://www.tiktok.com${firstVideo.href}`;
|
|
120
|
-
|
|
121
|
-
log(` 进入第一个视频: ${videoUrl}`);
|
|
122
|
-
await retryWithBackoff(() => page.goto(videoUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }), { log });
|
|
123
|
-
assertPageUrl(page, videoUrl.split('/video/')[0]);
|
|
124
|
-
await delay(1500, 2500);
|
|
125
|
-
|
|
126
|
-
const videoData = await scrapeSingleVideo(page, 0, 0, log, 'NEVER_MATCH');
|
|
127
|
-
result.locationCreated = videoData.locationCreated || null;
|
|
128
|
-
log(` 视频作者: ${videoData.videoAuthor} | 国家: ${result.locationCreated || '未知'}`);
|
|
101
|
+
result.locationCreated = locationCreated || null;
|
|
102
|
+
log(` 国家: ${result.locationCreated || '未知'}`);
|
|
129
103
|
|
|
104
|
+
// 国家筛选
|
|
130
105
|
const locationList = (location || 'ES').split(',').map(s => s.trim().toUpperCase());
|
|
131
106
|
const isTargetLocation = locationList.includes(result.locationCreated?.toUpperCase?.() || result.locationCreated);
|
|
132
107
|
|
|
133
108
|
if (isTargetLocation) {
|
|
134
109
|
result.keepFollow = true;
|
|
135
|
-
log(`
|
|
136
|
-
|
|
137
|
-
if (maxComments > 0) {
|
|
138
|
-
const commentResult = await extractCommentAuthors(page, maxComments);
|
|
139
|
-
result.discoveredCommentAuthors = commentResult.authors || [];
|
|
140
|
-
if (commentResult.captchaDetected) {
|
|
141
|
-
result.captchaDetected = true;
|
|
142
|
-
result.captchaStage = 'comment';
|
|
143
|
-
result.captchaMessage = '评论阶段出现验证码';
|
|
144
|
-
}
|
|
145
|
-
await closeCommentPanel(page);
|
|
146
|
-
await delay(500, 1000);
|
|
147
|
-
log(` 评论用户: ${result.discoveredCommentAuthors.length}`);
|
|
148
|
-
}
|
|
110
|
+
log(` 国家匹配,获取关注/粉丝...`);
|
|
149
111
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
112
|
+
// 提取关注/粉丝
|
|
113
|
+
if (enableFollow) {
|
|
114
|
+
const loggedIn = await isLoggedIn(page);
|
|
115
|
+
if (!loggedIn) {
|
|
116
|
+
log(' [跳过] 获取关注/粉丝:未登录,请先登录 TikTok');
|
|
117
|
+
result.hasFollowData = false;
|
|
118
|
+
result.discoveredFollowing = [];
|
|
119
|
+
result.discoveredFollowers = [];
|
|
120
|
+
} else {
|
|
121
|
+
try {
|
|
122
|
+
const effectiveMaxFollowing = isSeller ? globalMaxFollowing : maxFollowing;
|
|
123
|
+
const effectiveMaxFollowers = isSeller ? globalMaxFollowers : maxFollowers;
|
|
124
|
+
if (isSeller) log(` 商家用户,关注采集: ${effectiveMaxFollowing}, 粉丝采集: ${effectiveMaxFollowers}`);
|
|
125
|
+
const { following, followers } = await extractFollowAndFollowers(
|
|
126
|
+
page, { maxFollowing: effectiveMaxFollowing, maxFollowers: effectiveMaxFollowers, log }
|
|
127
|
+
);
|
|
128
|
+
result.discoveredFollowing = following || [];
|
|
129
|
+
result.discoveredFollowers = followers || [];
|
|
130
|
+
result.hasFollowData = true;
|
|
131
|
+
log(` 关注: ${result.discoveredFollowing.length}, 粉丝: ${result.discoveredFollowers.length}`);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
log(` 关注/粉丝提取失败: ${e.message}`);
|
|
134
|
+
result.hasFollowData = false;
|
|
135
|
+
result.discoveredFollowing = [];
|
|
136
|
+
result.discoveredFollowers = [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
156
139
|
}
|
|
157
140
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
nickname: videoData.nickname,
|
|
161
|
-
locationCreated: videoData.locationCreated,
|
|
162
|
-
}];
|
|
141
|
+
// 携带视频列表供登记
|
|
142
|
+
result.videoList = videoArray;
|
|
163
143
|
} else {
|
|
144
|
+
// 国家不匹配
|
|
164
145
|
result.keepFollow = false;
|
|
165
|
-
log(` 国家不匹配 (${result.locationCreated} not in [${location}]),跳过评论/猜你喜欢,丢弃关注/粉丝`);
|
|
166
146
|
result.discoveredFollowing = [];
|
|
167
147
|
result.discoveredFollowers = [];
|
|
148
|
+
result.hasFollowData = false;
|
|
149
|
+
log(` 国家不匹配,跳过`);
|
|
168
150
|
}
|
|
169
151
|
|
|
170
152
|
result.processed = true;
|
package/src/watch/data-store.js
CHANGED
|
@@ -12,6 +12,25 @@ export function createStore(filePath) {
|
|
|
12
12
|
let data = [];
|
|
13
13
|
let clientErrors = new Map();
|
|
14
14
|
|
|
15
|
+
// 视频存储(独立 JSON 文件)
|
|
16
|
+
let videos = [];
|
|
17
|
+
let videoFilePath = null;
|
|
18
|
+
if (filePath) {
|
|
19
|
+
const resolved = path.resolve(filePath);
|
|
20
|
+
videoFilePath = resolved.replace(/\.json$/, '-videos.json');
|
|
21
|
+
if (fs.existsSync(videoFilePath)) {
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(videoFilePath, 'utf-8');
|
|
24
|
+
const parsed = JSON.parse(content);
|
|
25
|
+
if (Array.isArray(parsed)) {
|
|
26
|
+
videos = parsed;
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error(`[data-store] 读取视频文件失败: ${e.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
let backupTimer = null;
|
|
16
35
|
|
|
17
36
|
if (filePath) {
|
|
@@ -66,6 +85,12 @@ export function createStore(filePath) {
|
|
|
66
85
|
fs.writeFileSync(resolved, json, 'utf-8');
|
|
67
86
|
}
|
|
68
87
|
|
|
88
|
+
function saveVideos() {
|
|
89
|
+
if (!videoFilePath) return;
|
|
90
|
+
const json = JSON.stringify(videos, null, 2);
|
|
91
|
+
fs.writeFileSync(videoFilePath, json, 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
|
|
69
94
|
function stopBackup() {
|
|
70
95
|
if (backupTimer) {
|
|
71
96
|
clearInterval(backupTimer);
|
|
@@ -159,6 +184,12 @@ export function createStore(filePath) {
|
|
|
159
184
|
next = pickFirst(seed);
|
|
160
185
|
}
|
|
161
186
|
|
|
187
|
+
if (!next) {
|
|
188
|
+
const ttSeller = data.filter(u => u.status === 'pending' && u.ttSeller === true && u.verified === false);
|
|
189
|
+
ttSeller.sort((a, b) => locationTier(a) - locationTier(b));
|
|
190
|
+
next = pickFirst(ttSeller);
|
|
191
|
+
}
|
|
192
|
+
|
|
162
193
|
if (!next) {
|
|
163
194
|
const follow = data.filter(u => u.status === 'pending' && u.sources && (u.sources.includes('following') || u.sources.includes('follower')));
|
|
164
195
|
follow.sort((a, b) => locationTier(a) - locationTier(b));
|
|
@@ -188,25 +219,25 @@ export function createStore(filePath) {
|
|
|
188
219
|
nickname: typeof v === 'string' ? null : v.nickname || null,
|
|
189
220
|
locationCreated: typeof v === 'string' ? null : v.locationCreated || null,
|
|
190
221
|
guessedLocation: typeof v === 'string' ? guessedLocation : (v.guessedLocation || guessedLocation),
|
|
191
|
-
|
|
222
|
+
sources: ['video'],
|
|
192
223
|
})),
|
|
193
224
|
...(result.discoveredCommentAuthors || []).map(c => {
|
|
194
|
-
if (typeof c === 'string') return { uniqueId: c.replace(/^@/, ''),
|
|
195
|
-
return { uniqueId: (c.author || c.uniqueId || '').replace(/^@/, ''), nickname: c.nickname || null,
|
|
225
|
+
if (typeof c === 'string') return { uniqueId: c.replace(/^@/, ''), sources: ['comment'], guessedLocation };
|
|
226
|
+
return { uniqueId: (c.author || c.uniqueId || '').replace(/^@/, ''), nickname: c.nickname || null, sources: ['comment'], guessedLocation: c.guessedLocation || guessedLocation };
|
|
196
227
|
}),
|
|
197
228
|
...(result.discoveredGuessAuthors || []).map(g => {
|
|
198
|
-
if (typeof g === 'string') return { uniqueId: g.replace(/^@/, ''),
|
|
199
|
-
return { uniqueId: (g.author || g.uniqueId || '').replace(/^@/, ''), nickname: g.nickname || null,
|
|
229
|
+
if (typeof g === 'string') return { uniqueId: g.replace(/^@/, ''), sources: ['guess'], guessedLocation };
|
|
230
|
+
return { uniqueId: (g.author || g.uniqueId || '').replace(/^@/, ''), nickname: g.nickname || null, sources: ['guess'], guessedLocation: g.guessedLocation || guessedLocation };
|
|
200
231
|
}),
|
|
201
232
|
...(result.discoveredFollowing || []).map(f => {
|
|
202
233
|
const handle = Array.isArray(f) ? f[0] : (f.handle || '');
|
|
203
234
|
const name = Array.isArray(f) ? f[1] : (f.displayName || null);
|
|
204
|
-
return { uniqueId: handle.replace(/^@/, ''), nickname: name,
|
|
235
|
+
return { uniqueId: handle.replace(/^@/, ''), nickname: name, sources: ['following'], guessedLocation: (typeof f === 'object' && f.guessedLocation) || guessedLocation };
|
|
205
236
|
}),
|
|
206
237
|
...(result.discoveredFollowers || []).map(f => {
|
|
207
238
|
const handle = Array.isArray(f) ? f[0] : (f.handle || '');
|
|
208
239
|
const name = Array.isArray(f) ? f[1] : (f.displayName || null);
|
|
209
|
-
return { uniqueId: handle.replace(/^@/, ''), nickname: name,
|
|
240
|
+
return { uniqueId: handle.replace(/^@/, ''), nickname: name, sources: ['follower'], guessedLocation: (typeof f === 'object' && f.guessedLocation) || guessedLocation };
|
|
210
241
|
}),
|
|
211
242
|
].filter(u => u.uniqueId);
|
|
212
243
|
|
|
@@ -278,6 +309,7 @@ export function createStore(filePath) {
|
|
|
278
309
|
user[key] = result[key];
|
|
279
310
|
}
|
|
280
311
|
}
|
|
312
|
+
user.sources = [...new Set([...(user.sources || []), 'processed'])];
|
|
281
313
|
}
|
|
282
314
|
}
|
|
283
315
|
|
|
@@ -419,12 +451,85 @@ export function createStore(filePath) {
|
|
|
419
451
|
return Array.from(clientErrors.values());
|
|
420
452
|
}
|
|
421
453
|
|
|
454
|
+
function getPendingUserUpdateTasks(limit) {
|
|
455
|
+
const l = Math.max(1, parseInt(limit) || 5);
|
|
456
|
+
const pending = data.filter(u => {
|
|
457
|
+
const updateCount = u.userUpdateCount;
|
|
458
|
+
const ttSellerEmpty = u.ttSeller === null || u.ttSeller === undefined || u.ttSeller === '';
|
|
459
|
+
if (!ttSellerEmpty) return false;
|
|
460
|
+
return updateCount === null || updateCount === undefined || updateCount <= 0;
|
|
461
|
+
}).slice(0, l);
|
|
462
|
+
// 接受任务时 userUpdateCount + 1
|
|
463
|
+
pending.forEach(u => {
|
|
464
|
+
u.userUpdateCount = (u.userUpdateCount || 0) + 1;
|
|
465
|
+
u.updatedAt = Date.now();
|
|
466
|
+
});
|
|
467
|
+
save();
|
|
468
|
+
return pending;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function updateUserInfo(uniqueId, info) {
|
|
472
|
+
const user = getUser(uniqueId);
|
|
473
|
+
if (!user) return { error: 'user not found' };
|
|
474
|
+
for (const key of Object.keys(info)) {
|
|
475
|
+
if (key === 'uniqueId' || key === 'sources') continue;
|
|
476
|
+
if (info[key] !== undefined && info[key] !== null && info[key] !== '') {
|
|
477
|
+
user[key] = info[key];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
user.userUpdateCount = (user.userUpdateCount || 0) + 1;
|
|
481
|
+
user.updatedAt = Date.now();
|
|
482
|
+
save();
|
|
483
|
+
return { ok: true, userUpdateCount: user.userUpdateCount };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 视频登记
|
|
487
|
+
function registerVideos(sourceUser, videoList, locationCreated, ttSeller) {
|
|
488
|
+
if (!videoList || !Array.isArray(videoList) || videoList.length === 0) {
|
|
489
|
+
return { registered: 0, skipped: 0 };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const existingIds = new Set(videos.map(v => v.id));
|
|
493
|
+
let registered = 0;
|
|
494
|
+
let skipped = 0;
|
|
495
|
+
|
|
496
|
+
for (const item of videoList) {
|
|
497
|
+
if (existingIds.has(item.id)) {
|
|
498
|
+
skipped++;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
videos.push({
|
|
502
|
+
id: item.id,
|
|
503
|
+
href: item.href,
|
|
504
|
+
authorUniqueId: sourceUser,
|
|
505
|
+
locationCreated: locationCreated || null,
|
|
506
|
+
ttSeller: ttSeller || false,
|
|
507
|
+
registeredAt: Date.now(),
|
|
508
|
+
});
|
|
509
|
+
existingIds.add(item.id);
|
|
510
|
+
registered++;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
saveVideos();
|
|
514
|
+
return { registered, skipped };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function getVideos() {
|
|
518
|
+
return videos;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function getVideoCount() {
|
|
522
|
+
return videos.length;
|
|
523
|
+
}
|
|
524
|
+
|
|
422
525
|
return {
|
|
423
526
|
save, getUser, hasUser, userExists, addUser,
|
|
424
527
|
getPendingUsers, getProcessedUsers, getAllUsers,
|
|
425
528
|
claimNextJob, commitJob, commitNewExplore, resetJob, togglePin,
|
|
426
529
|
getNextRedoJob, commitRedoJob,
|
|
530
|
+
getPendingUserUpdateTasks, updateUserInfo,
|
|
427
531
|
reportClientError, deleteClientError, getClientErrors,
|
|
532
|
+
registerVideos, getVideos, getVideoCount,
|
|
428
533
|
stopBackup,
|
|
429
534
|
data,
|
|
430
535
|
};
|
package/src/watch/server.js
CHANGED
|
@@ -257,6 +257,19 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
257
257
|
return;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
// 视频登记
|
|
261
|
+
if (req.method === 'POST' && routePath === '/api/videos') {
|
|
262
|
+
const body = await readBody(req);
|
|
263
|
+
const { sourceUser, videoList, locationCreated, ttSeller } = body;
|
|
264
|
+
if (!sourceUser) {
|
|
265
|
+
sendJSON(res, 400, { error: 'sourceUser 不能为空' });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const ret = store.registerVideos(sourceUser, videoList || [], locationCreated, ttSeller);
|
|
269
|
+
sendJSON(res, 200, ret);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
260
273
|
const jobPinMatch = routePath.match(/^\/api\/job\/([^/]+)\/pin$/);
|
|
261
274
|
if (req.method === 'POST' && jobPinMatch) {
|
|
262
275
|
const uniqueId = jobPinMatch[1];
|
|
@@ -285,6 +298,34 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
285
298
|
return;
|
|
286
299
|
}
|
|
287
300
|
|
|
301
|
+
if (req.method === 'GET' && routePath === '/api/user-update-tasks') {
|
|
302
|
+
const limit = params.limit;
|
|
303
|
+
const tasks = store.getPendingUserUpdateTasks(limit);
|
|
304
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
305
|
+
console.error(`[JOB ${ts}] USER-UPDATE-TASKS: ${tasks.length} tasks`);
|
|
306
|
+
sendJSON(res, 200, { total: tasks.length, tasks });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const userInfoCommitMatch = routePath.match(/^\/api\/user-info\/([^/]+)$/);
|
|
311
|
+
if (req.method === 'PUT' && userInfoCommitMatch) {
|
|
312
|
+
const uniqueId = userInfoCommitMatch[1];
|
|
313
|
+
try {
|
|
314
|
+
const body = await readBody(req);
|
|
315
|
+
const ret = store.updateUserInfo(uniqueId, body);
|
|
316
|
+
if (ret.error) {
|
|
317
|
+
sendJSON(res, 404, { error: ret.error });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
321
|
+
console.error(`[JOB ${ts}] USER-INFO-UPDATE: ${uniqueId} (userUpdateCount=${ret.userUpdateCount})`);
|
|
322
|
+
sendJSON(res, 200, ret);
|
|
323
|
+
} catch (e) {
|
|
324
|
+
sendJSON(res, 400, { error: e.message });
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
288
329
|
const userExistsMatch = routePath.match(/^\/api\/user-exists\/([^/]+)$/);
|
|
289
330
|
if (req.method === 'GET' && userExistsMatch) {
|
|
290
331
|
const uniqueId = userExistsMatch[1];
|
|
@@ -415,6 +456,9 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
|
|
|
415
456
|
if (sa !== sb) return sa - sb;
|
|
416
457
|
if (a.status === 'done' && b.status === 'done') return (b.processedAt || 0) - (a.processedAt || 0);
|
|
417
458
|
if (a.status === 'pending' && b.status === 'pending') {
|
|
459
|
+
const aSeller = a.ttSeller === true && a.verified === false ? 0 : 1;
|
|
460
|
+
const bSeller = b.ttSeller === true && b.verified === false ? 0 : 1;
|
|
461
|
+
if (aSeller !== bSeller) return aSeller - bSeller;
|
|
418
462
|
const la = locationTier(a), lb = locationTier(b);
|
|
419
463
|
if (la !== lb) return la - lb;
|
|
420
464
|
}
|