tt-help-cli-ycl 1.3.31 → 1.3.33

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.
@@ -1,13 +1,13 @@
1
- import http from 'http';
2
- import os from 'os';
1
+ import http from "http";
2
+ import os from "os";
3
3
 
4
- import { readFileSync, existsSync } from 'fs';
5
- import { join, dirname } from 'path';
6
- import { fileURLToPath } from 'url';
7
- import { spawn } from 'child_process';
8
- import { createStore } from './data-store.js';
4
+ import { readFileSync, existsSync } from "fs";
5
+ import { join, dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { spawn } from "child_process";
8
+ import { createStore } from "./data-store.js";
9
9
 
10
- const TARGET_LOCATIONS = ['ES', 'PL', 'NL', 'BE', 'DE', 'FR', 'IT', 'IE'];
10
+ const TARGET_LOCATIONS = ["ES", "PL", "NL", "BE", "DE", "FR", "IT", "IE"];
11
11
 
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
 
@@ -15,62 +15,98 @@ function getLocalIP() {
15
15
  const ifaces = os.networkInterfaces();
16
16
  for (const name of Object.keys(ifaces)) {
17
17
  for (const iface of ifaces[name]) {
18
- if (iface.family === 'IPv4' && !iface.internal) {
18
+ if (iface.family === "IPv4" && !iface.internal) {
19
19
  return iface.address;
20
20
  }
21
21
  }
22
22
  }
23
- return '0.0.0.0';
23
+ return "0.0.0.0";
24
24
  }
25
25
  const __dirname = dirname(__filename);
26
- const publicDir = join(__dirname, 'public');
26
+ const publicDir = join(__dirname, "public");
27
27
 
28
28
  function parseQuery(url) {
29
- const idx = url.indexOf('?');
29
+ const idx = url.indexOf("?");
30
30
  if (idx === -1) return { path: url, params: {} };
31
31
  const params = {};
32
- for (const kv of url.slice(idx + 1).split('&')) {
33
- const [k, v] = kv.split('=');
34
- params[decodeURIComponent(k)] = decodeURIComponent(v || '');
32
+ for (const kv of url.slice(idx + 1).split("&")) {
33
+ const [k, v] = kv.split("=");
34
+ params[decodeURIComponent(k)] = decodeURIComponent(v || "");
35
35
  }
36
36
  return { path: url.slice(0, idx), params };
37
37
  }
38
38
 
39
39
  function computeStats(users) {
40
40
  const total = users.length;
41
- const statusCounts = { pending: 0, processing: 0, done: 0, error: 0, restricted: 0 };
41
+ const statusCounts = {
42
+ pending: 0,
43
+ processing: 0,
44
+ done: 0,
45
+ error: 0,
46
+ restricted: 0,
47
+ };
42
48
  for (const u of users) {
43
49
  statusCounts[u.status] = (statusCounts[u.status] || 0) + 1;
44
50
  }
45
51
 
46
- const targetUsers = users.filter(u =>
47
- u.ttSeller && u.verified === false && TARGET_LOCATIONS.includes(u.locationCreated)
52
+ const targetUsers = users.filter(
53
+ (u) =>
54
+ u.ttSeller &&
55
+ u.verified === false &&
56
+ TARGET_LOCATIONS.includes(u.locationCreated),
48
57
  ).length;
49
58
 
50
59
  const countryMap = {};
51
60
  for (const u of users) {
52
- if (u.status !== 'done') continue;
53
- const loc = u.locationCreated || '\u672a\u77e5';
61
+ if (u.status !== "done") continue;
62
+ const loc = u.locationCreated || "\u672a\u77e5";
54
63
  countryMap[loc] = (countryMap[loc] || 0) + 1;
55
64
  }
56
65
  const countryStats = Object.entries(countryMap)
57
66
  .map(([country, count]) => ({ country, count }))
58
67
  .sort((a, b) => b.count - a.count);
59
68
 
60
- const sourceCounts = { seed: 0, video: 0, comment: 0, guess: 0, following: 0, follower: 0, processed: 0, restricted: 0, error: 0, noVideo: 0 };
69
+ const sourceCounts = {
70
+ seed: 0,
71
+ video: 0,
72
+ comment: 0,
73
+ guess: 0,
74
+ following: 0,
75
+ follower: 0,
76
+ processed: 0,
77
+ restricted: 0,
78
+ error: 0,
79
+ noVideo: 0,
80
+ };
61
81
  for (const u of users) {
62
- if (u.status === 'restricted') { sourceCounts.restricted++; continue; }
63
- if (u.status === 'error') { sourceCounts.error++; continue; }
82
+ if (u.status === "restricted") {
83
+ sourceCounts.restricted++;
84
+ continue;
85
+ }
86
+ if (u.status === "error") {
87
+ sourceCounts.error++;
88
+ continue;
89
+ }
64
90
  if (u.noVideo) sourceCounts.noVideo++;
65
91
  const sources = u.sources || [];
66
- if (u.status === 'done') sourceCounts.processed++;
67
- if (sources.includes('video') && u.status !== 'done') sourceCounts.video++;
68
- if (sources.includes('comment') && u.status !== 'done') sourceCounts.comment++;
69
- if (sources.includes('guess') && u.status !== 'done') sourceCounts.guess++;
70
- if (sources.includes('following') && u.status !== 'done') sourceCounts.following++;
71
- if (sources.includes('follower') && u.status !== 'done') sourceCounts.follower++;
72
- if (!sources.includes('video') && !sources.includes('comment') && !sources.includes('guess') &&
73
- !sources.includes('following') && !sources.includes('follower') && u.status !== 'done') sourceCounts.seed++;
92
+ if (u.status === "done") sourceCounts.processed++;
93
+ if (sources.includes("video") && u.status !== "done") sourceCounts.video++;
94
+ if (sources.includes("comment") && u.status !== "done")
95
+ sourceCounts.comment++;
96
+ if (sources.includes("guess") && u.status !== "done") sourceCounts.guess++;
97
+ if (sources.includes("following") && u.status !== "done")
98
+ sourceCounts.following++;
99
+ if (sources.includes("follower") && u.status !== "done")
100
+ sourceCounts.follower++;
101
+ if (
102
+ !sources.includes("video") &&
103
+ !sources.includes("comment") &&
104
+ !sources.includes("guess") &&
105
+ !sources.includes("following") &&
106
+ !sources.includes("follower") &&
107
+ u.status !== "done"
108
+ )
109
+ sourceCounts.seed++;
74
110
  }
75
111
 
76
112
  return {
@@ -93,42 +129,77 @@ function computeStatsIncremental(st) {
93
129
  const statusCounts = quick.statusCounts;
94
130
 
95
131
  const countryMap = {};
96
- const sourceCounts = { seed: 0, video: 0, comment: 0, guess: 0, following: 0, follower: 0, processed: 0, restricted: 0, error: 0, noVideo: 0 };
132
+ const sourceCounts = {
133
+ seed: 0,
134
+ video: 0,
135
+ comment: 0,
136
+ guess: 0,
137
+ following: 0,
138
+ follower: 0,
139
+ processed: 0,
140
+ restricted: 0,
141
+ error: 0,
142
+ noVideo: 0,
143
+ };
97
144
  let targetUsers = 0;
98
145
  let userUpdateTasks = 0;
99
146
  const targetCountryMap = {};
100
147
 
101
148
  for (const u of all) {
102
149
  // 国家统计
103
- if (u.status === 'done') {
104
- const loc = u.locationCreated || '未知';
150
+ if (u.status === "done") {
151
+ const loc = u.locationCreated || "未知";
105
152
  countryMap[loc] = (countryMap[loc] || 0) + 1;
106
153
  }
107
154
  // 预处理任务统计(与 /api/user-update-tasks 条件一致,不做 continue 跳过)
108
- const ttSellerEmpty = u.ttSeller === null || u.ttSeller === undefined || u.ttSeller === '';
109
- const updateCountNotSet = u.userUpdateCount === null || u.userUpdateCount === undefined || u.userUpdateCount <= 0;
155
+ const ttSellerEmpty =
156
+ u.ttSeller === null || u.ttSeller === undefined || u.ttSeller === "";
157
+ const updateCountNotSet =
158
+ u.userUpdateCount === null ||
159
+ u.userUpdateCount === undefined ||
160
+ u.userUpdateCount <= 0;
110
161
  if (ttSellerEmpty && updateCountNotSet) userUpdateTasks++;
111
162
 
112
163
  // 目标用户统计(按国家分组)
113
- if (u.ttSeller && u.verified === false && TARGET_LOCATIONS.includes(u.locationCreated)) {
164
+ if (
165
+ u.ttSeller &&
166
+ u.verified === false &&
167
+ TARGET_LOCATIONS.includes(u.locationCreated)
168
+ ) {
114
169
  targetUsers++;
115
170
  const loc = u.locationCreated;
116
171
  targetCountryMap[loc] = (targetCountryMap[loc] || 0) + 1;
117
172
  }
118
173
 
119
174
  // 来源统计(restricted/error 跳过后续统计)
120
- if (u.status === 'restricted') { sourceCounts.restricted++; continue; }
121
- if (u.status === 'error') { sourceCounts.error++; continue; }
175
+ if (u.status === "restricted") {
176
+ sourceCounts.restricted++;
177
+ continue;
178
+ }
179
+ if (u.status === "error") {
180
+ sourceCounts.error++;
181
+ continue;
182
+ }
122
183
  if (u.noVideo) sourceCounts.noVideo++;
123
184
  const sources = u.sources || [];
124
- if (u.status === 'done') sourceCounts.processed++;
125
- if (sources.includes('video') && u.status !== 'done') sourceCounts.video++;
126
- if (sources.includes('comment') && u.status !== 'done') sourceCounts.comment++;
127
- if (sources.includes('guess') && u.status !== 'done') sourceCounts.guess++;
128
- if (sources.includes('following') && u.status !== 'done') sourceCounts.following++;
129
- if (sources.includes('follower') && u.status !== 'done') sourceCounts.follower++;
130
- if (!sources.includes('video') && !sources.includes('comment') && !sources.includes('guess') &&
131
- !sources.includes('following') && !sources.includes('follower') && u.status !== 'done') sourceCounts.seed++;
185
+ if (u.status === "done") sourceCounts.processed++;
186
+ if (sources.includes("video") && u.status !== "done") sourceCounts.video++;
187
+ if (sources.includes("comment") && u.status !== "done")
188
+ sourceCounts.comment++;
189
+ if (sources.includes("guess") && u.status !== "done") sourceCounts.guess++;
190
+ if (sources.includes("following") && u.status !== "done")
191
+ sourceCounts.following++;
192
+ if (sources.includes("follower") && u.status !== "done")
193
+ sourceCounts.follower++;
194
+ if (
195
+ !sources.includes("video") &&
196
+ !sources.includes("comment") &&
197
+ !sources.includes("guess") &&
198
+ !sources.includes("following") &&
199
+ !sources.includes("follower") &&
200
+ u.status !== "done"
201
+ )
202
+ sourceCounts.seed++;
132
203
  }
133
204
  const countryStats = Object.entries(countryMap)
134
205
  .map(([country, count]) => ({ country, count }))
@@ -154,37 +225,37 @@ function computeStatsIncremental(st) {
154
225
 
155
226
  function readBody(req) {
156
227
  return new Promise((resolve, reject) => {
157
- let body = '';
158
- req.on('data', chunk => body += chunk);
159
- req.on('end', () => {
228
+ let body = "";
229
+ req.on("data", (chunk) => (body += chunk));
230
+ req.on("end", () => {
160
231
  try {
161
232
  resolve(body ? JSON.parse(body) : {});
162
233
  } catch (e) {
163
234
  reject(e);
164
235
  }
165
236
  });
166
- req.on('error', reject);
237
+ req.on("error", reject);
167
238
  });
168
239
  }
169
240
 
170
241
  function sendJSON(res, code, data) {
171
- res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
242
+ res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
172
243
  res.end(JSON.stringify(data));
173
244
  }
174
245
 
175
246
  function csvEscape(val) {
176
- const s = String(val ?? '');
247
+ const s = String(val ?? "");
177
248
  return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
178
249
  }
179
250
 
180
251
  function sendCSV(res, columns, rows) {
181
- const BOM = '\uFEFF';
182
- const header = columns.join(',');
183
- const lines = rows.map(r => columns.map(c => csvEscape(r[c])).join(','));
184
- const body = BOM + [header, ...lines].join('\r\n');
252
+ const BOM = "\uFEFF";
253
+ const header = columns.join(",");
254
+ const lines = rows.map((r) => columns.map((c) => csvEscape(r[c])).join(","));
255
+ const body = BOM + [header, ...lines].join("\r\n");
185
256
  res.writeHead(200, {
186
- 'Content-Type': 'text/csv; charset=utf-8',
187
- 'Content-Disposition': 'attachment; filename="target-users.csv"',
257
+ "Content-Type": "text/csv; charset=utf-8",
258
+ "Content-Disposition": 'attachment; filename="target-users.csv"',
188
259
  });
189
260
  res.end(body);
190
261
  }
@@ -194,34 +265,48 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
194
265
  const store = existingStore || createStore(outputFile);
195
266
 
196
267
  function logJob(action, detail) {
197
- const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false });
198
- const d = detail ? ' ' + Object.entries(detail).map(([k, v]) => `${k}=${v}`).join(' ') : '';
268
+ const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
269
+ const d = detail
270
+ ? " " +
271
+ Object.entries(detail)
272
+ .map(([k, v]) => `${k}=${v}`)
273
+ .join(" ")
274
+ : "";
199
275
  console.error(`[JOB ${ts}] ${action}${d}`);
200
276
  }
201
277
 
202
278
  const server = http.createServer(async (req, res) => {
203
279
  const { path: routePath, params } = parseQuery(req.url);
204
280
 
205
- if (req.method === 'POST' && routePath === '/api/users') {
281
+ if (req.method === "POST" && routePath === "/api/users") {
206
282
  try {
207
283
  const { usernames, sources, guessedLocation } = await readBody(req);
208
284
  if (!Array.isArray(usernames) || usernames.length === 0) {
209
- sendJSON(res, 400, { error: 'usernames \u6570\u7ec4\u4e0d\u80fd\u4e3a\u7a7a' });
285
+ sendJSON(res, 400, {
286
+ error: "usernames \u6570\u7ec4\u4e0d\u80fd\u4e3a\u7a7a",
287
+ });
210
288
  return;
211
289
  }
212
- const userSources = sources || ['seed'];
213
- const existingIds = new Set(store.getAllUsers().map(u => u.uniqueId));
290
+ const userSources = sources || ["seed"];
291
+ const existingIds = new Set(
292
+ store.getAllUsers().map((u) => u.uniqueId),
293
+ );
214
294
  const newUsers = usernames
215
- .map(u => u.replace(/^@/, '').trim())
216
- .filter(u => u && !existingIds.has(u));
295
+ .map((u) => u.replace(/^@/, "").trim())
296
+ .filter((u) => u && !existingIds.has(u));
217
297
  for (const nu of newUsers) {
218
- store.addUser({ uniqueId: nu, sources: userSources, status: 'pending', guessedLocation: guessedLocation || null });
298
+ store.addUser({
299
+ uniqueId: nu,
300
+ sources: userSources,
301
+ status: "pending",
302
+ guessedLocation: guessedLocation || null,
303
+ });
219
304
  }
220
305
  store.save();
221
306
  sendJSON(res, 200, {
222
307
  added: newUsers.length,
223
308
  skipped: usernames.length - newUsers.length,
224
- message: `\u5df2\u63d2\u5165 ${newUsers.length} \u4e2a\u7528\u6237`
309
+ message: `\u5df2\u63d2\u5165 ${newUsers.length} \u4e2a\u7528\u6237`,
225
310
  });
226
311
  } catch (e) {
227
312
  sendJSON(res, 400, { error: e.message });
@@ -229,16 +314,19 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
229
314
  return;
230
315
  }
231
316
 
232
- if (req.method === 'POST' && routePath === '/api/user') {
317
+ if (req.method === "POST" && routePath === "/api/user") {
233
318
  try {
234
319
  const userData = await readBody(req);
235
320
  if (!userData || !userData.uniqueId) {
236
- sendJSON(res, 400, { error: 'missing uniqueId' });
321
+ sendJSON(res, 400, { error: "missing uniqueId" });
237
322
  return;
238
323
  }
239
324
  const existing = store.getUser(userData.uniqueId);
240
325
  if (existing) {
241
- sendJSON(res, 200, { added: false, message: 'user already exists' });
326
+ sendJSON(res, 200, {
327
+ added: false,
328
+ message: "user already exists",
329
+ });
242
330
  return;
243
331
  }
244
332
  store.addUser(userData);
@@ -250,27 +338,46 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
250
338
  return;
251
339
  }
252
340
 
253
- if (req.method === 'GET' && routePath === '/api/job') {
254
- const userId = params.userId || '';
255
- const job = store.claimNextJob(userId);
341
+ if (req.method === "GET" && routePath === "/api/job") {
342
+ const userId = params.userId || "";
343
+ const locationsParam = params.locations || "";
344
+ const locations = locationsParam
345
+ ? locationsParam
346
+ .split(",")
347
+ .map((s) => s.trim().toUpperCase())
348
+ .filter(Boolean)
349
+ : null;
350
+ const job = store.claimNextJob(userId, 5 * 60 * 1000, locations);
256
351
  if (job) {
257
352
  store.save();
258
- logJob('CLAIM', { user: job.uniqueId, clientId: userId });
353
+ logJob("CLAIM", {
354
+ user: job.uniqueId,
355
+ clientId: userId,
356
+ locations: locations,
357
+ });
259
358
  sendJSON(res, 200, { hasJob: true, user: job });
260
359
  } else {
261
- logJob('CLAIM', { result: 'no-job', clientId: userId });
360
+ logJob("CLAIM", {
361
+ result: "no-job",
362
+ clientId: userId,
363
+ locations: locations,
364
+ });
262
365
  sendJSON(res, 200, { hasJob: false });
263
366
  }
264
367
  return;
265
368
  }
266
369
 
267
370
  const jobCommitMatch = routePath.match(/^\/api\/job\/([^/]+)$/);
268
- if (req.method === 'POST' && jobCommitMatch) {
371
+ if (req.method === "POST" && jobCommitMatch) {
269
372
  const uniqueId = jobCommitMatch[1];
270
373
  try {
271
374
  const result = await readBody(req);
272
375
  const ret = store.commitJob(uniqueId, result);
273
- logJob('COMMIT', { user: uniqueId, status: ret.status, newUsers: ret.newUsers?.length || 0 });
376
+ logJob("COMMIT", {
377
+ user: uniqueId,
378
+ status: ret.status,
379
+ newUsers: ret.newUsers?.length || 0,
380
+ });
274
381
  if (ret.saved) {
275
382
  sendJSON(res, 200, ret);
276
383
  } else {
@@ -283,12 +390,17 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
283
390
  }
284
391
 
285
392
  const exploreNewMatch = routePath.match(/^\/api\/explore-new\/([^/]+)$/);
286
- if (req.method === 'POST' && exploreNewMatch) {
393
+ if (req.method === "POST" && exploreNewMatch) {
287
394
  const uniqueId = exploreNewMatch[1];
288
395
  try {
289
396
  const result = await readBody(req);
290
397
  const ret = store.commitNewExplore(uniqueId, result);
291
- logJob('COMMIT_NEW', { user: uniqueId, created: ret.created, status: ret.status, newUsers: ret.newUsers?.length || 0 });
398
+ logJob("COMMIT_NEW", {
399
+ user: uniqueId,
400
+ created: ret.created,
401
+ status: ret.status,
402
+ newUsers: ret.newUsers?.length || 0,
403
+ });
292
404
  sendJSON(res, 200, ret);
293
405
  } catch (e) {
294
406
  sendJSON(res, 400, { error: e.message });
@@ -297,7 +409,7 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
297
409
  }
298
410
 
299
411
  const jobResetMatch = routePath.match(/^\/api\/job\/([^/]+)\/reset$/);
300
- if (req.method === 'POST' && jobResetMatch) {
412
+ if (req.method === "POST" && jobResetMatch) {
301
413
  const uniqueId = jobResetMatch[1];
302
414
  const ret = store.resetJob(uniqueId);
303
415
  if (ret.saved) {
@@ -308,11 +420,11 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
308
420
  return;
309
421
  }
310
422
 
311
- if (req.method === 'POST' && routePath === '/api/jobs/batch-reset') {
423
+ if (req.method === "POST" && routePath === "/api/jobs/batch-reset") {
312
424
  const body = await readBody(req);
313
425
  const ids = Array.isArray(body.userIds) ? body.userIds : [];
314
426
  if (ids.length === 0) {
315
- sendJSON(res, 400, { error: 'userIds 不能为空' });
427
+ sendJSON(res, 400, { error: "userIds 不能为空" });
316
428
  return;
317
429
  }
318
430
  let count = 0;
@@ -325,47 +437,52 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
325
437
  }
326
438
 
327
439
  // 视频登记
328
- if (req.method === 'POST' && routePath === '/api/videos') {
440
+ if (req.method === "POST" && routePath === "/api/videos") {
329
441
  const body = await readBody(req);
330
442
  const { sourceUser, videoList, locationCreated, ttSeller } = body;
331
443
  if (!sourceUser) {
332
- sendJSON(res, 400, { error: 'sourceUser 不能为空' });
444
+ sendJSON(res, 400, { error: "sourceUser 不能为空" });
333
445
  return;
334
446
  }
335
- const ret = store.registerVideos(sourceUser, videoList || [], locationCreated, ttSeller);
447
+ const ret = store.registerVideos(
448
+ sourceUser,
449
+ videoList || [],
450
+ locationCreated,
451
+ ttSeller,
452
+ );
336
453
  sendJSON(res, 200, ret);
337
454
  return;
338
455
  }
339
456
 
340
457
  const jobPinMatch = routePath.match(/^\/api\/job\/([^/]+)\/pin$/);
341
- if (req.method === 'POST' && jobPinMatch) {
458
+ if (req.method === "POST" && jobPinMatch) {
342
459
  const uniqueId = jobPinMatch[1];
343
460
  const ret = store.togglePin(uniqueId);
344
461
  sendJSON(res, ret.saved ? 200 : 404, ret);
345
462
  return;
346
463
  }
347
464
 
348
- if (req.method === 'GET' && routePath === '/api/stats') {
465
+ if (req.method === "GET" && routePath === "/api/stats") {
349
466
  const stats = computeStatsIncremental(store);
350
467
  sendJSON(res, 200, stats);
351
468
  return;
352
469
  }
353
470
 
354
- if (req.method === 'GET' && routePath === '/api/redo-job') {
355
- const userId = params.userId || '';
471
+ if (req.method === "GET" && routePath === "/api/redo-job") {
472
+ const userId = params.userId || "";
356
473
  const job = store.getNextRedoJob(userId);
357
474
  if (job) {
358
475
  store.save();
359
- logJob('REDO-CLAIM', { user: job.uniqueId, clientId: userId });
476
+ logJob("REDO-CLAIM", { user: job.uniqueId, clientId: userId });
360
477
  sendJSON(res, 200, { hasJob: true, user: job });
361
478
  } else {
362
- logJob('REDO-CLAIM', { result: 'no-job', clientId: userId });
479
+ logJob("REDO-CLAIM", { result: "no-job", clientId: userId });
363
480
  sendJSON(res, 200, { hasJob: false });
364
481
  }
365
482
  return;
366
483
  }
367
484
 
368
- if (req.method === 'GET' && routePath === '/api/user-update-tasks') {
485
+ if (req.method === "GET" && routePath === "/api/user-update-tasks") {
369
486
  const limit = params.limit;
370
487
  const tasks = store.getPendingUserUpdateTasks(limit);
371
488
  const ts = new Date().toISOString().slice(11, 19);
@@ -374,24 +491,33 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
374
491
  return;
375
492
  }
376
493
 
377
- if (req.method === 'POST' && routePath === '/api/user-info-batch') {
378
- try {
379
- const body = await readBody(req);
380
- const updates = body.updates || [];
381
- const results = store.batchUpdateUserInfo(updates);
382
- const okCount = results.filter(r => r.ok).length;
383
- const errCount = results.filter(r => r.error).length;
384
- const ts = new Date().toISOString().slice(11, 19);
385
- console.error(`[JOB ${ts}] USER-INFO-BATCH: ${okCount} ok, ${errCount} error (total=${updates.length})`);
386
- sendJSON(res, 200, { results, total: updates.length, ok: okCount, error: errCount });
387
- } catch (e) {
388
- sendJSON(res, 400, { error: e.message });
389
- }
390
- return;
494
+ if (req.method === "POST" && routePath === "/api/user-info-batch") {
495
+ try {
496
+ const body = await readBody(req);
497
+ const updates = body.updates || [];
498
+ const results = store.batchUpdateUserInfo(updates);
499
+ const okCount = results.filter((r) => r.ok).length;
500
+ const errCount = results.filter((r) => r.error).length;
501
+ const ts = new Date().toISOString().slice(11, 19);
502
+ console.error(
503
+ `[JOB ${ts}] USER-INFO-BATCH: ${okCount} ok, ${errCount} error (total=${updates.length})`,
504
+ );
505
+ sendJSON(res, 200, {
506
+ results,
507
+ total: updates.length,
508
+ ok: okCount,
509
+ error: errCount,
510
+ });
511
+ } catch (e) {
512
+ sendJSON(res, 400, { error: e.message });
391
513
  }
514
+ return;
515
+ }
392
516
 
393
- const userInfoCommitMatch = routePath.match(/^\/api\/user-info\/([^/]+)$/);
394
- if (req.method === 'PUT' && userInfoCommitMatch) {
517
+ const userInfoCommitMatch = routePath.match(
518
+ /^\/api\/user-info\/([^/]+)$/,
519
+ );
520
+ if (req.method === "PUT" && userInfoCommitMatch) {
395
521
  const uniqueId = userInfoCommitMatch[1];
396
522
  try {
397
523
  const body = await readBody(req);
@@ -401,7 +527,9 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
401
527
  return;
402
528
  }
403
529
  const ts = new Date().toISOString().slice(11, 19);
404
- console.error(`[JOB ${ts}] USER-INFO-UPDATE: ${uniqueId} (userUpdateCount=${ret.userUpdateCount})`);
530
+ console.error(
531
+ `[JOB ${ts}] USER-INFO-UPDATE: ${uniqueId} (userUpdateCount=${ret.userUpdateCount})`,
532
+ );
405
533
  sendJSON(res, 200, ret);
406
534
  } catch (e) {
407
535
  sendJSON(res, 400, { error: e.message });
@@ -410,14 +538,14 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
410
538
  }
411
539
 
412
540
  const userExistsMatch = routePath.match(/^\/api\/user-exists\/([^/]+)$/);
413
- if (req.method === 'GET' && userExistsMatch) {
541
+ if (req.method === "GET" && userExistsMatch) {
414
542
  const uniqueId = userExistsMatch[1];
415
543
  const exists = store.userExists(uniqueId);
416
544
  sendJSON(res, 200, { exists });
417
545
  return;
418
546
  }
419
547
 
420
- if (req.method === 'GET' && routePath === '/api/comment-tasks') {
548
+ if (req.method === "GET" && routePath === "/api/comment-tasks") {
421
549
  const limit = parseInt(params.limit) || 1;
422
550
  const tasks = store.getPendingCommentTasks(limit);
423
551
  const ts = new Date().toISOString().slice(11, 19);
@@ -426,8 +554,10 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
426
554
  return;
427
555
  }
428
556
 
429
- const commentTaskMatch = routePath.match(/^\/api\/comment-task\/([^/]+)$/);
430
- if (req.method === 'PUT' && commentTaskMatch) {
557
+ const commentTaskMatch = routePath.match(
558
+ /^\/api\/comment-task\/([^/]+)$/,
559
+ );
560
+ if (req.method === "PUT" && commentTaskMatch) {
431
561
  const videoId = commentTaskMatch[1];
432
562
  try {
433
563
  const ret = store.commitCommentTask(videoId);
@@ -436,7 +566,9 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
436
566
  return;
437
567
  }
438
568
  const ts = new Date().toISOString().slice(11, 19);
439
- console.error(`[JOB ${ts}] COMMENT-TASK-COMMIT: ${videoId} (userUpdateCount=${ret.userUpdateCount})`);
569
+ console.error(
570
+ `[JOB ${ts}] COMMENT-TASK-COMMIT: ${videoId} (userUpdateCount=${ret.userUpdateCount})`,
571
+ );
440
572
  sendJSON(res, 200, ret);
441
573
  } catch (e) {
442
574
  sendJSON(res, 400, { error: e.message });
@@ -445,12 +577,12 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
445
577
  }
446
578
 
447
579
  const redoCommitMatch = routePath.match(/^\/api\/redo-job\/([^/]+)$/);
448
- if (req.method === 'POST' && redoCommitMatch) {
580
+ if (req.method === "POST" && redoCommitMatch) {
449
581
  const uniqueId = redoCommitMatch[1];
450
582
  try {
451
583
  const result = await readBody(req);
452
584
  const ret = store.commitRedoJob(uniqueId, result);
453
- logJob('REDO-COMMIT', { user: uniqueId, status: ret.status });
585
+ logJob("REDO-COMMIT", { user: uniqueId, status: ret.status });
454
586
  if (ret.saved) {
455
587
  sendJSON(res, 200, ret);
456
588
  } else {
@@ -462,28 +594,40 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
462
594
  return;
463
595
  }
464
596
 
465
- if (req.method === 'GET' && routePath === '/api/target-users') {
597
+ if (req.method === "GET" && routePath === "/api/target-users") {
466
598
  const all = store.getAllUsers();
467
- const targets = all.filter(u =>
468
- u.ttSeller && u.verified === false && TARGET_LOCATIONS.includes(u.locationCreated)
599
+ const targets = all.filter(
600
+ (u) =>
601
+ u.ttSeller &&
602
+ u.verified === false &&
603
+ TARGET_LOCATIONS.includes(u.locationCreated),
469
604
  );
470
- if (req.headers['accept']?.includes('text/csv')) {
471
- const columns = ['uniqueId', 'nickname', 'followerCount', 'ttSeller', 'verified', 'locationCreated', 'status', 'sources'];
472
- const rows = targets.map(u => ({
605
+ if (req.headers["accept"]?.includes("text/csv")) {
606
+ const columns = [
607
+ "uniqueId",
608
+ "nickname",
609
+ "followerCount",
610
+ "ttSeller",
611
+ "verified",
612
+ "locationCreated",
613
+ "status",
614
+ "sources",
615
+ ];
616
+ const rows = targets.map((u) => ({
473
617
  uniqueId: u.uniqueId,
474
- nickname: u.nickname || '',
618
+ nickname: u.nickname || "",
475
619
  followerCount: u.followerCount ?? 0,
476
620
  ttSeller: u.ttSeller,
477
621
  verified: u.verified,
478
- locationCreated: u.locationCreated || '',
479
- status: u.status || '',
480
- sources: (u.sources || []).join(';'),
622
+ locationCreated: u.locationCreated || "",
623
+ status: u.status || "",
624
+ sources: (u.sources || []).join(";"),
481
625
  }));
482
626
  sendCSV(res, columns, rows);
483
627
  } else {
484
- const users = targets.map(u => ({
628
+ const users = targets.map((u) => ({
485
629
  uniqueId: u.uniqueId,
486
- nickname: u.nickname || '',
630
+ nickname: u.nickname || "",
487
631
  followerCount: u.followerCount || 0,
488
632
  }));
489
633
  sendJSON(res, 200, { total: targets.length, users });
@@ -491,49 +635,57 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
491
635
  return;
492
636
  }
493
637
 
494
- if (req.method === 'GET' && routePath === '/api/client-errors') {
638
+ if (req.method === "GET" && routePath === "/api/client-errors") {
495
639
  sendJSON(res, 200, { clients: store.getClientErrors() });
496
640
  return;
497
641
  }
498
642
 
499
- if (req.method === 'DELETE' && routePath.startsWith('/api/client-error/')) {
500
- const userId = routePath.replace('/api/client-error/', '');
643
+ if (
644
+ req.method === "DELETE" &&
645
+ routePath.startsWith("/api/client-error/")
646
+ ) {
647
+ const userId = routePath.replace("/api/client-error/", "");
501
648
  if (userId) {
502
649
  store.deleteClientError(userId);
503
650
  sendJSON(res, 200, { ok: true });
504
651
  } else {
505
- sendJSON(res, 400, { error: 'missing userId' });
652
+ sendJSON(res, 400, { error: "missing userId" });
506
653
  }
507
654
  return;
508
655
  }
509
656
 
510
- if (req.method === 'POST' && routePath === '/api/error-report') {
657
+ if (req.method === "POST" && routePath === "/api/error-report") {
511
658
  const body = await readBody(req);
512
659
  if (body && body.userId) {
513
660
  store.reportClientError(
514
661
  body.userId,
515
- body.errorType || 'other',
516
- body.errorMessage || '',
517
- body.username || '',
518
- body.stage || '',
519
- body.errorStack || ''
662
+ body.errorType || "other",
663
+ body.errorMessage || "",
664
+ body.username || "",
665
+ body.stage || "",
666
+ body.errorStack || "",
520
667
  );
521
668
  sendJSON(res, 200, { ok: true });
522
669
  } else {
523
- sendJSON(res, 400, { error: 'missing userId' });
670
+ sendJSON(res, 400, { error: "missing userId" });
524
671
  }
525
672
  return;
526
673
  }
527
674
 
528
- if (req.method === 'GET' && routePath === '/api/users') {
675
+ if (req.method === "GET" && routePath === "/api/users") {
529
676
  const all = store.getAllUsers();
530
677
  const limit = parseInt(params.limit) || 50;
531
678
  const offset = parseInt(params.offset) || 0;
532
679
 
533
680
  // 简单筛选:直接用预分组索引(已排序,免全量遍历)
534
- if (!params.search && !params.target && !params.location && !params.targetLocation) {
681
+ if (
682
+ !params.search &&
683
+ !params.target &&
684
+ !params.location &&
685
+ !params.targetLocation
686
+ ) {
535
687
  const groups = store.getStatusGroups();
536
- if (params.status && params.status !== 'all') {
688
+ if (params.status && params.status !== "all") {
537
689
  // 单状态快路径:直接取已排序的组
538
690
  const group = groups[params.status] || [];
539
691
  const paged = group.slice(offset, offset + limit);
@@ -541,8 +693,16 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
541
693
  return;
542
694
  }
543
695
  // status=all 快路径:按分组顺序 early-exit(各组已排序)
544
- const sOrder = { processing: 0, pending: 1, done: 2, error: 3, restricted: 4 };
545
- const sortedKeys = Object.keys(groups).sort((a, b) => (sOrder[a] ?? 9) - (sOrder[b] ?? 9));
696
+ const sOrder = {
697
+ processing: 0,
698
+ pending: 1,
699
+ done: 2,
700
+ error: 3,
701
+ restricted: 4,
702
+ };
703
+ const sortedKeys = Object.keys(groups).sort(
704
+ (a, b) => (sOrder[a] ?? 9) - (sOrder[b] ?? 9),
705
+ );
546
706
  let totalCount = 0;
547
707
  for (const key of sortedKeys) totalCount += groups[key].length;
548
708
  const result = [];
@@ -558,36 +718,51 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
558
718
  }
559
719
 
560
720
  let filtered = all;
561
- if (params.status && params.status !== 'all') {
562
- filtered = filtered.filter(u => u.status === params.status);
721
+ if (params.status && params.status !== "all") {
722
+ filtered = filtered.filter((u) => u.status === params.status);
563
723
  }
564
- if (params.target === '1') {
565
- filtered = filtered.filter(u =>
566
- u.ttSeller && u.verified === false && TARGET_LOCATIONS.includes(u.locationCreated)
724
+ if (params.target === "1") {
725
+ filtered = filtered.filter(
726
+ (u) =>
727
+ u.ttSeller &&
728
+ u.verified === false &&
729
+ TARGET_LOCATIONS.includes(u.locationCreated),
567
730
  );
568
731
  }
569
732
  if (params.search) {
570
733
  const s = params.search.toLowerCase();
571
- filtered = filtered.filter(u =>
572
- u.uniqueId.toLowerCase().includes(s) ||
573
- (u.nickname || '').toLowerCase().includes(s)
734
+ filtered = filtered.filter(
735
+ (u) =>
736
+ u.uniqueId.toLowerCase().includes(s) ||
737
+ (u.nickname || "").toLowerCase().includes(s),
574
738
  );
575
739
  }
576
740
  if (params.location) {
577
- filtered = filtered.filter(u => u.locationCreated === params.location);
741
+ filtered = filtered.filter(
742
+ (u) => u.locationCreated === params.location,
743
+ );
578
744
  }
579
745
  if (params.targetLocation) {
580
- filtered = filtered.filter(u =>
581
- u.ttSeller && u.verified === false && u.locationCreated === params.targetLocation
746
+ filtered = filtered.filter(
747
+ (u) =>
748
+ u.ttSeller &&
749
+ u.verified === false &&
750
+ u.locationCreated === params.targetLocation,
582
751
  );
583
752
  }
584
753
 
585
754
  const needCount = offset + limit;
586
- const statusOrder = { processing: 0, pending: 1, done: 2, error: 3, restricted: 4 };
587
- const tier1Loc = new Set(['PL', 'NL', 'BE']);
588
- const tier2Loc = new Set(['DE', 'FR', 'IT', 'IE', 'ES']);
755
+ const statusOrder = {
756
+ processing: 0,
757
+ pending: 1,
758
+ done: 2,
759
+ error: 3,
760
+ restricted: 4,
761
+ };
762
+ const tier1Loc = new Set(["PL", "NL", "BE"]);
763
+ const tier2Loc = new Set(["DE", "FR", "IT", "IE", "ES"]);
589
764
  function locationTier(u) {
590
- const loc = (u.guessedLocation || '').toUpperCase();
765
+ const loc = (u.guessedLocation || "").toUpperCase();
591
766
  if (tier1Loc.has(loc)) return 0;
592
767
  if (tier2Loc.has(loc)) return 1;
593
768
  return 2;
@@ -597,25 +772,33 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
597
772
  if (filtered.length > needCount * 3) {
598
773
  const groups = {};
599
774
  for (const u of filtered) {
600
- const key = u.status || 'pending';
775
+ const key = u.status || "pending";
601
776
  if (!groups[key]) groups[key] = [];
602
777
  groups[key].push(u);
603
778
  }
604
- const sortedKeys = Object.keys(groups).sort((a, b) => (statusOrder[a] ?? 9) - (statusOrder[b] ?? 9));
779
+ const sortedKeys = Object.keys(groups).sort(
780
+ (a, b) => (statusOrder[a] ?? 9) - (statusOrder[b] ?? 9),
781
+ );
605
782
  for (const key of sortedKeys) {
606
783
  const g = groups[key];
607
- if (key === 'done') g.sort((a, b) => (b.processedAt || 0) - (a.processedAt || 0));
608
- else if (key === 'pending') g.sort((a, b) => {
609
- const aSeller = a.ttSeller === true && a.verified === false ? 0 : 1;
610
- const bSeller = b.ttSeller === true && b.verified === false ? 0 : 1;
611
- if (aSeller !== bSeller) return aSeller - bSeller;
612
- const la = locationTier(a), lb = locationTier(b);
613
- if (la !== lb) return la - lb;
614
- return (b.followerCount || 0) - (a.followerCount || 0);
615
- });
616
- else g.sort((a, b) => (b.followerCount || 0) - (a.followerCount || 0));
617
- const pinned = g.filter(u => u.pinned);
618
- const unpinned = g.filter(u => !u.pinned);
784
+ if (key === "done")
785
+ g.sort((a, b) => (b.processedAt || 0) - (a.processedAt || 0));
786
+ else if (key === "pending")
787
+ g.sort((a, b) => {
788
+ const aSeller =
789
+ a.ttSeller === true && a.verified === false ? 0 : 1;
790
+ const bSeller =
791
+ b.ttSeller === true && b.verified === false ? 0 : 1;
792
+ if (aSeller !== bSeller) return aSeller - bSeller;
793
+ const la = locationTier(a),
794
+ lb = locationTier(b);
795
+ if (la !== lb) return la - lb;
796
+ return (b.followerCount || 0) - (a.followerCount || 0);
797
+ });
798
+ else
799
+ g.sort((a, b) => (b.followerCount || 0) - (a.followerCount || 0));
800
+ const pinned = g.filter((u) => u.pinned);
801
+ const unpinned = g.filter((u) => !u.pinned);
619
802
  groups[key] = pinned.concat(unpinned);
620
803
  }
621
804
  sorted = [];
@@ -632,12 +815,16 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
632
815
  const sa = statusOrder[a.status] ?? 9;
633
816
  const sb = statusOrder[b.status] ?? 9;
634
817
  if (sa !== sb) return sa - sb;
635
- if (a.status === 'done' && b.status === 'done') return (b.processedAt || 0) - (a.processedAt || 0);
636
- if (a.status === 'pending' && b.status === 'pending') {
637
- const aSeller = a.ttSeller === true && a.verified === false ? 0 : 1;
638
- const bSeller = b.ttSeller === true && b.verified === false ? 0 : 1;
818
+ if (a.status === "done" && b.status === "done")
819
+ return (b.processedAt || 0) - (a.processedAt || 0);
820
+ if (a.status === "pending" && b.status === "pending") {
821
+ const aSeller =
822
+ a.ttSeller === true && a.verified === false ? 0 : 1;
823
+ const bSeller =
824
+ b.ttSeller === true && b.verified === false ? 0 : 1;
639
825
  if (aSeller !== bSeller) return aSeller - bSeller;
640
- const la = locationTier(a), lb = locationTier(b);
826
+ const la = locationTier(a),
827
+ lb = locationTier(b);
641
828
  if (la !== lb) return la - lb;
642
829
  }
643
830
  return (b.followerCount || 0) - (a.followerCount || 0);
@@ -646,8 +833,8 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
646
833
 
647
834
  let paged = sorted.slice(offset, offset + limit);
648
835
 
649
- if (params.view === 'light') {
650
- paged = paged.map(u => ({
836
+ if (params.view === "light") {
837
+ paged = paged.map((u) => ({
651
838
  uniqueId: u.uniqueId,
652
839
  nickname: u.nickname,
653
840
  status: u.status,
@@ -666,25 +853,35 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
666
853
  return;
667
854
  }
668
855
 
669
- if ((req.method === 'GET' && (routePath === '/' || routePath === '/index.html'))) {
670
- const html = readFileSync(join(publicDir, 'index.html'), 'utf-8');
671
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
856
+ if (
857
+ req.method === "GET" &&
858
+ (routePath === "/" || routePath === "/index.html")
859
+ ) {
860
+ const html = readFileSync(join(publicDir, "index.html"), "utf-8");
861
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
672
862
  res.end(html);
673
863
  return;
674
864
  }
675
865
 
676
866
  const scriptMatch = routePath.match(/^\/scripts\/(.+)$/);
677
- if (req.method === 'GET' && scriptMatch) {
678
- const scriptsDir = join(__dirname, '../../scripts');
867
+ if (req.method === "GET" && scriptMatch) {
868
+ const scriptsDir = join(__dirname, "../../scripts");
679
869
  const scriptFile = join(scriptsDir, scriptMatch[1]);
680
870
  if (existsSync(scriptFile)) {
681
871
  const content = readFileSync(scriptFile);
682
872
  const fileName = scriptMatch[1];
683
- const ext = fileName.split('.').pop();
684
- const mime = ext === 'sh' ? 'text/x-shellscript' : ext === 'bat' ? 'text/x-msdos-batch' : ext === 'ps1' ? 'text/x-powershell' : 'text/plain';
873
+ const ext = fileName.split(".").pop();
874
+ const mime =
875
+ ext === "sh"
876
+ ? "text/x-shellscript"
877
+ : ext === "bat"
878
+ ? "text/x-msdos-batch"
879
+ : ext === "ps1"
880
+ ? "text/x-powershell"
881
+ : "text/plain";
685
882
  res.writeHead(200, {
686
- 'Content-Type': `${mime}; charset=utf-8`,
687
- 'Content-Disposition': `attachment; filename="${fileName}"`,
883
+ "Content-Type": `${mime}; charset=utf-8`,
884
+ "Content-Disposition": `attachment; filename="${fileName}"`,
688
885
  });
689
886
  res.end(content);
690
887
  return;
@@ -692,12 +889,14 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
692
889
  }
693
890
 
694
891
  res.writeHead(404);
695
- res.end('Not Found');
892
+ res.end("Not Found");
696
893
  });
697
894
 
698
- server.on('error', (err) => {
699
- if (err.code === 'EADDRINUSE') {
700
- console.error(`\u7aef\u53e3 ${port} \u5df2\u88ab\u5360\u7528\uff0c\u8bf7\u66f4\u6362\u7aef\u53e3\u540e\u91cd\u8bd5`);
895
+ server.on("error", (err) => {
896
+ if (err.code === "EADDRINUSE") {
897
+ console.error(
898
+ `\u7aef\u53e3 ${port} \u5df2\u88ab\u5360\u7528\uff0c\u8bf7\u66f4\u6362\u7aef\u53e3\u540e\u91cd\u8bd5`,
899
+ );
701
900
  reject(err);
702
901
  } else {
703
902
  reject(err);
@@ -705,7 +904,7 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
705
904
  });
706
905
 
707
906
  const localIP = getLocalIP();
708
- server.listen(port, '0.0.0.0', () => {
907
+ server.listen(port, "0.0.0.0", () => {
709
908
  console.error(`Watch 监控服务已启动:`);
710
909
  console.error(` 本地访问: http://127.0.0.1:${port}`);
711
910
  console.error(` 局域网访问: http://${localIP}:${port}`);
@@ -715,20 +914,20 @@ export function startWatchServer(outputFile, port = 3000, existingStore) {
715
914
  async function gracefulShutdown(signal) {
716
915
  console.error(`\n[server] 收到 ${signal},正在保存数据...`);
717
916
  server.close(() => {
718
- console.error('[server] HTTP 服务已关闭');
917
+ console.error("[server] HTTP 服务已关闭");
719
918
  });
720
919
  await store.flushSave();
721
- console.error('[server] 数据已保存,退出');
920
+ console.error("[server] 数据已保存,退出");
722
921
  process.exit(0);
723
922
  }
724
923
 
725
- process.on('SIGINT', () => gracefulShutdown('SIGINT'));
726
- process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
924
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
925
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
727
926
  });
728
927
  }
729
928
 
730
929
  export function openBrowser(port) {
731
- spawn('open', [`http://127.0.0.1:${port}`]).on('error', () => {});
930
+ spawn("open", [`http://127.0.0.1:${port}`]).on("error", () => {});
732
931
  }
733
932
 
734
933
  export { getLocalIP };