tt-help-cli-ycl 1.3.32 → 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.
- package/package.json +1 -1
- package/scripts/run-explore copy.bat +35 -2
- package/scripts/run-explore.bat +40 -10
- package/scripts/run-explore.ps1 +51 -5
- package/scripts/run-explore.sh +20 -5
- package/src/cli/explore.js +6 -3
- package/src/lib/api-interceptor.js +110 -37
- package/src/lib/args.js +4 -0
- package/src/scraper/modules/follow-extractor.js +82 -6
- package/src/watch/data-store.js +340 -165
- package/src/watch/server.js +400 -201
package/src/watch/server.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import http from
|
|
2
|
-
import os from
|
|
1
|
+
import http from "http";
|
|
2
|
+
import os from "os";
|
|
3
3
|
|
|
4
|
-
import { readFileSync, existsSync } from
|
|
5
|
-
import { join, dirname } from
|
|
6
|
-
import { fileURLToPath } from
|
|
7
|
-
import { spawn } from
|
|
8
|
-
import { createStore } from
|
|
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 = [
|
|
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 ===
|
|
18
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
19
19
|
return iface.address;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
return
|
|
23
|
+
return "0.0.0.0";
|
|
24
24
|
}
|
|
25
25
|
const __dirname = dirname(__filename);
|
|
26
|
-
const publicDir = join(__dirname,
|
|
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 = {
|
|
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(
|
|
47
|
-
|
|
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 !==
|
|
53
|
-
const loc = u.locationCreated ||
|
|
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 = {
|
|
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 ===
|
|
63
|
-
|
|
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 ===
|
|
67
|
-
if (sources.includes(
|
|
68
|
-
if (sources.includes(
|
|
69
|
-
|
|
70
|
-
if (sources.includes(
|
|
71
|
-
if (sources.includes(
|
|
72
|
-
|
|
73
|
-
|
|
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 = {
|
|
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 ===
|
|
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 =
|
|
109
|
-
|
|
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 (
|
|
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 ===
|
|
121
|
-
|
|
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 ===
|
|
125
|
-
if (sources.includes(
|
|
126
|
-
if (sources.includes(
|
|
127
|
-
|
|
128
|
-
if (sources.includes(
|
|
129
|
-
if (sources.includes(
|
|
130
|
-
|
|
131
|
-
|
|
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(
|
|
159
|
-
req.on(
|
|
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(
|
|
237
|
+
req.on("error", reject);
|
|
167
238
|
});
|
|
168
239
|
}
|
|
169
240
|
|
|
170
241
|
function sendJSON(res, code, data) {
|
|
171
|
-
res.writeHead(code, {
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
187
|
-
|
|
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(
|
|
198
|
-
const d = detail
|
|
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 ===
|
|
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, {
|
|
285
|
+
sendJSON(res, 400, {
|
|
286
|
+
error: "usernames \u6570\u7ec4\u4e0d\u80fd\u4e3a\u7a7a",
|
|
287
|
+
});
|
|
210
288
|
return;
|
|
211
289
|
}
|
|
212
|
-
const userSources = sources || [
|
|
213
|
-
const existingIds = new Set(
|
|
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(/^@/,
|
|
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({
|
|
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 ===
|
|
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:
|
|
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, {
|
|
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 ===
|
|
254
|
-
const userId = params.userId ||
|
|
255
|
-
const
|
|
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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
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:
|
|
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 ===
|
|
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:
|
|
444
|
+
sendJSON(res, 400, { error: "sourceUser 不能为空" });
|
|
333
445
|
return;
|
|
334
446
|
}
|
|
335
|
-
const ret = store.registerVideos(
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
476
|
+
logJob("REDO-CLAIM", { user: job.uniqueId, clientId: userId });
|
|
360
477
|
sendJSON(res, 200, { hasJob: true, user: job });
|
|
361
478
|
} else {
|
|
362
|
-
logJob(
|
|
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 ===
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
394
|
-
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
430
|
-
|
|
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(
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
597
|
+
if (req.method === "GET" && routePath === "/api/target-users") {
|
|
466
598
|
const all = store.getAllUsers();
|
|
467
|
-
const targets = all.filter(
|
|
468
|
-
|
|
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[
|
|
471
|
-
const columns = [
|
|
472
|
-
|
|
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 ===
|
|
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 (
|
|
500
|
-
|
|
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:
|
|
652
|
+
sendJSON(res, 400, { error: "missing userId" });
|
|
506
653
|
}
|
|
507
654
|
return;
|
|
508
655
|
}
|
|
509
656
|
|
|
510
|
-
if (req.method ===
|
|
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 ||
|
|
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:
|
|
670
|
+
sendJSON(res, 400, { error: "missing userId" });
|
|
524
671
|
}
|
|
525
672
|
return;
|
|
526
673
|
}
|
|
527
674
|
|
|
528
|
-
if (req.method ===
|
|
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 (
|
|
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 !==
|
|
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 = {
|
|
545
|
-
|
|
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 !==
|
|
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 ===
|
|
565
|
-
filtered = filtered.filter(
|
|
566
|
-
|
|
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(
|
|
572
|
-
u
|
|
573
|
-
|
|
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(
|
|
741
|
+
filtered = filtered.filter(
|
|
742
|
+
(u) => u.locationCreated === params.location,
|
|
743
|
+
);
|
|
578
744
|
}
|
|
579
745
|
if (params.targetLocation) {
|
|
580
|
-
filtered = filtered.filter(
|
|
581
|
-
u
|
|
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 = {
|
|
587
|
-
|
|
588
|
-
|
|
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 ||
|
|
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 ||
|
|
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(
|
|
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 ===
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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 ===
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const
|
|
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),
|
|
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 ===
|
|
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 (
|
|
670
|
-
|
|
671
|
-
|
|
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 ===
|
|
678
|
-
const scriptsDir = join(__dirname,
|
|
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
|
-
|
|
684
|
-
|
|
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
|
-
|
|
687
|
-
|
|
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(
|
|
892
|
+
res.end("Not Found");
|
|
696
893
|
});
|
|
697
894
|
|
|
698
|
-
server.on(
|
|
699
|
-
if (err.code ===
|
|
700
|
-
console.error(
|
|
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,
|
|
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(
|
|
917
|
+
console.error("[server] HTTP 服务已关闭");
|
|
719
918
|
});
|
|
720
919
|
await store.flushSave();
|
|
721
|
-
console.error(
|
|
920
|
+
console.error("[server] 数据已保存,退出");
|
|
722
921
|
process.exit(0);
|
|
723
922
|
}
|
|
724
923
|
|
|
725
|
-
process.on(
|
|
726
|
-
process.on(
|
|
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(
|
|
930
|
+
spawn("open", [`http://127.0.0.1:${port}`]).on("error", () => {});
|
|
732
931
|
}
|
|
733
932
|
|
|
734
933
|
export { getLocalIP };
|