tt-help-cli-ycl 1.4.18 → 1.4.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/recheck.js +381 -0
- package/src/cli/tag.js +1 -1
- package/src/lib/args.js +64 -7
- package/src/lib/constants.js +11 -0
- package/src/main.js +3 -0
- package/src/watch/data-store.js +270 -9
- package/src/watch/db-tags.js +6 -6
- package/src/watch/public/app.js +247 -1
- package/src/watch/public/index.html +2 -0
- package/src/watch/public/style.css +11 -0
- package/src/watch/server.js +95 -0
package/package.json
CHANGED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recheck — 重检 jobs 表中已完成的非商家用户
|
|
3
|
+
*
|
|
4
|
+
* 基于 attach.js 的核心循环,但任务源和提交逻辑不同:
|
|
5
|
+
* - 拉任务:GET /api/recheck-jobs(只取 status='done' 且 tt_seller!=1 且 7 天未更新的用户)
|
|
6
|
+
* - 提交结果:POST /api/recheck-jobs-batch(直接 UPDATE jobs 表,不做表迁移)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { TikTokScraper } from "../lib/tiktok-scraper.mjs";
|
|
10
|
+
import { CDNBlockedError } from "../lib/parse-ssr.mjs";
|
|
11
|
+
import { proxy as configuredProxy } from "../lib/constants.js";
|
|
12
|
+
import { createApiClient } from "../lib/api-client.js";
|
|
13
|
+
import v8 from "node:v8";
|
|
14
|
+
|
|
15
|
+
const HEAP_RESTART_RATIO = 0.72;
|
|
16
|
+
const MAX_TASK_BATCHES_BEFORE_RESTART = 200;
|
|
17
|
+
|
|
18
|
+
function formatNow() {
|
|
19
|
+
return new Date().toLocaleString("zh-CN", {
|
|
20
|
+
hour12: false,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function recheckLog(message = "") {
|
|
25
|
+
console.error(`[${formatNow()}] ${message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isBrowserClosedError(err) {
|
|
29
|
+
if (!err) return false;
|
|
30
|
+
const msg = err.message || err.toString() || "";
|
|
31
|
+
return (
|
|
32
|
+
msg.includes("Target page, context or browser has been closed") ||
|
|
33
|
+
msg.includes("browser has been closed") ||
|
|
34
|
+
msg.includes("browserContext.newPage") ||
|
|
35
|
+
msg.includes("Protocol error")
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getHeapUsage() {
|
|
40
|
+
const heapUsed = process.memoryUsage().heapUsed;
|
|
41
|
+
const heapLimit = v8.getHeapStatistics().heap_size_limit;
|
|
42
|
+
return {
|
|
43
|
+
heapUsed,
|
|
44
|
+
heapLimit,
|
|
45
|
+
ratio: heapLimit > 0 ? heapUsed / heapLimit : 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function recycleScraper(scraper, reason) {
|
|
50
|
+
recheckLog(`[Recheck] ${reason},正在重启浏览器实例...`);
|
|
51
|
+
await scraper.restart();
|
|
52
|
+
recheckLog("[Recheck] 浏览器实例重启完成");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function handleRecheck(options) {
|
|
56
|
+
const {
|
|
57
|
+
attachParallel,
|
|
58
|
+
attachInterval,
|
|
59
|
+
serverUrl,
|
|
60
|
+
attachCountries,
|
|
61
|
+
customProxy,
|
|
62
|
+
attachPoolSize,
|
|
63
|
+
showHelp,
|
|
64
|
+
} = options;
|
|
65
|
+
const effectiveProxy = customProxy || configuredProxy;
|
|
66
|
+
|
|
67
|
+
if (effectiveProxy) {
|
|
68
|
+
recheckLog(`[Recheck] 使用代理: ${effectiveProxy}`);
|
|
69
|
+
}
|
|
70
|
+
let shuttingDown = false;
|
|
71
|
+
let forceExitTimer = null;
|
|
72
|
+
|
|
73
|
+
if (showHelp) {
|
|
74
|
+
recheckLog(
|
|
75
|
+
"用法: tt-help recheck [-p 并行数] [-i 间隔秒数] [-s 服务端地址] [-c 国家列表]",
|
|
76
|
+
);
|
|
77
|
+
recheckLog("");
|
|
78
|
+
recheckLog("参数:");
|
|
79
|
+
recheckLog(" -p, --parallel <N> 并行抓取数(默认: 1)");
|
|
80
|
+
recheckLog(" -i, --interval <N> 无任务时轮询间隔,单位秒(默认: 10)");
|
|
81
|
+
recheckLog(
|
|
82
|
+
" -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)",
|
|
83
|
+
);
|
|
84
|
+
recheckLog(
|
|
85
|
+
" -c, --countries <A,B,C> 目标国家列表(逗号分隔,如 PL,DE,FR),默认全部目标国家",
|
|
86
|
+
);
|
|
87
|
+
recheckLog(
|
|
88
|
+
" --proxy <代理地址> HTTP 代理地址(如 http://127.0.0.1:7890),不配置则从 ~/.tt-help.json 读取",
|
|
89
|
+
);
|
|
90
|
+
recheckLog(
|
|
91
|
+
" --size <N> 浏览器页面池大小(默认: 3,不建议超过 3)",
|
|
92
|
+
);
|
|
93
|
+
recheckLog("");
|
|
94
|
+
recheckLog("说明:");
|
|
95
|
+
recheckLog(
|
|
96
|
+
" 从 jobs 表中拉取 status='done' 且非商家(tt_seller!=1)且 7 天未更新的用户,",
|
|
97
|
+
);
|
|
98
|
+
recheckLog(
|
|
99
|
+
" 重新抓取 TikTok 用户基本信息(重点检查 tt_seller),更新回 jobs 表。",
|
|
100
|
+
);
|
|
101
|
+
recheckLog(
|
|
102
|
+
" 每 10 轮打印汇总:已跑轮数、发现 ttSeller 数量、累计检测数。",
|
|
103
|
+
);
|
|
104
|
+
recheckLog(" 一轮跑完后自动空转,7 天后可重新运行。");
|
|
105
|
+
recheckLog(" 支持多客户端并行,服务端自动分配不同用户。");
|
|
106
|
+
recheckLog("");
|
|
107
|
+
recheckLog("示例:");
|
|
108
|
+
recheckLog(" tt-help recheck");
|
|
109
|
+
recheckLog(" tt-help recheck -p 5 -i 10");
|
|
110
|
+
recheckLog(" tt-help recheck -c PL,DE,FR -p 5");
|
|
111
|
+
recheckLog(" tt-help recheck -p 5 --size 5");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const countryStr =
|
|
116
|
+
attachCountries && attachCountries.length > 0
|
|
117
|
+
? `, 目标国家: ${attachCountries.join(", ")}`
|
|
118
|
+
: ", 目标国家: 全部";
|
|
119
|
+
recheckLog(
|
|
120
|
+
`[Recheck] 并行数: ${attachParallel}, 空闲间隔: ${attachInterval}秒, 服务端: ${serverUrl}${countryStr}`,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const { apiGet, apiPost } = createApiClient({ log: false });
|
|
124
|
+
|
|
125
|
+
const scraper = new TikTokScraper({
|
|
126
|
+
poolSize: attachPoolSize || 3,
|
|
127
|
+
proxyServer: effectiveProxy || null,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const shutdown = async (signal) => {
|
|
131
|
+
if (shuttingDown) return;
|
|
132
|
+
shuttingDown = true;
|
|
133
|
+
forceExitTimer = setTimeout(() => {
|
|
134
|
+
recheckLog("[Recheck] 关闭超时,强制退出");
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}, 30000);
|
|
137
|
+
forceExitTimer.unref?.();
|
|
138
|
+
recheckLog(`\n[Recheck] 收到 ${signal},等待当前任务完成后退出...`);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const onSigint = () => {
|
|
142
|
+
void shutdown("SIGINT");
|
|
143
|
+
};
|
|
144
|
+
const onSigterm = () => {
|
|
145
|
+
void shutdown("SIGTERM");
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
process.on("SIGINT", onSigint);
|
|
149
|
+
process.on("SIGTERM", onSigterm);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await scraper.init();
|
|
153
|
+
recheckLog("[Recheck] 浏览器初始化完成,开始循环接收任务...");
|
|
154
|
+
|
|
155
|
+
let loopCount = 0;
|
|
156
|
+
let browserRestartCount = 0;
|
|
157
|
+
let taskBatchCount = 0;
|
|
158
|
+
let totalChecked = 0;
|
|
159
|
+
let totalSellerFound = 0;
|
|
160
|
+
|
|
161
|
+
while (!shuttingDown) {
|
|
162
|
+
loopCount++;
|
|
163
|
+
|
|
164
|
+
// 检查浏览器是否存活
|
|
165
|
+
if (!scraper.isAlive()) {
|
|
166
|
+
recheckLog(
|
|
167
|
+
`[Recheck] 浏览器已关闭,正在重启 (${++browserRestartCount})...`,
|
|
168
|
+
);
|
|
169
|
+
await scraper.restart();
|
|
170
|
+
recheckLog("[Recheck] 浏览器重启完成");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 收到退出信号且当前无任务正在处理,则退出
|
|
174
|
+
if (shuttingDown) {
|
|
175
|
+
recheckLog("[Recheck] 当前任务已完成,正在关闭浏览器...");
|
|
176
|
+
await scraper.close().catch(() => {});
|
|
177
|
+
if (forceExitTimer) {
|
|
178
|
+
clearTimeout(forceExitTimer);
|
|
179
|
+
forceExitTimer = null;
|
|
180
|
+
}
|
|
181
|
+
recheckLog(
|
|
182
|
+
`[Recheck] 已退出,共检测 ${totalChecked} 个用户,发现 ttSeller: ${totalSellerFound}`,
|
|
183
|
+
);
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const countryParam =
|
|
188
|
+
attachCountries && attachCountries.length > 0
|
|
189
|
+
? `&countries=${encodeURIComponent(attachCountries.join(","))}`
|
|
190
|
+
: "";
|
|
191
|
+
const { total, tasks } = await apiGet(
|
|
192
|
+
`${serverUrl}/api/recheck-jobs?limit=${attachParallel}${countryParam}`,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (!tasks || tasks.length === 0) {
|
|
196
|
+
const heap = getHeapUsage();
|
|
197
|
+
if (heap.ratio >= HEAP_RESTART_RATIO) {
|
|
198
|
+
await recycleScraper(
|
|
199
|
+
scraper,
|
|
200
|
+
`空闲时堆占用 ${(heap.ratio * 100).toFixed(1)}%`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (loopCount === 1) {
|
|
204
|
+
recheckLog(
|
|
205
|
+
`[Recheck] 当前无待重检任务(7 天内已全部检测过或无非商家用户),${attachInterval}秒后重试...`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
await new Promise((r) => setTimeout(r, attachInterval * 1000));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
recheckLog(`\n[Recheck] 获取到 ${tasks.length} 个待重检任务...`);
|
|
213
|
+
taskBatchCount++;
|
|
214
|
+
|
|
215
|
+
if (shuttingDown) {
|
|
216
|
+
recheckLog("[Recheck] 正在处理当前批次任务,完成后退出...");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const results = await Promise.allSettled(
|
|
220
|
+
tasks.map(async (task) => {
|
|
221
|
+
const uniqueId = task.uniqueId;
|
|
222
|
+
const countryTag = task.guessedLocation
|
|
223
|
+
? ` [国家: ${task.guessedLocation}]`
|
|
224
|
+
: "";
|
|
225
|
+
recheckLog(` → 重检 @${uniqueId}...${countryTag}`);
|
|
226
|
+
try {
|
|
227
|
+
const info = await scraper.getUserInfo(uniqueId);
|
|
228
|
+
return { uniqueId, info, error: null };
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return { uniqueId, info: null, error: err };
|
|
231
|
+
}
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
let successCount = 0;
|
|
236
|
+
let failCount = 0;
|
|
237
|
+
let cdnBlockedCount = 0;
|
|
238
|
+
let needRestart = false;
|
|
239
|
+
|
|
240
|
+
const successTasks = [];
|
|
241
|
+
for (const result of results) {
|
|
242
|
+
if (result.status === "fulfilled") {
|
|
243
|
+
const { uniqueId, info, error } = result.value;
|
|
244
|
+
if (error) {
|
|
245
|
+
if (error instanceof CDNBlockedError) {
|
|
246
|
+
recheckLog(` ⚠ @${uniqueId} CDN限流 (Access Denied)`);
|
|
247
|
+
cdnBlockedCount++;
|
|
248
|
+
failCount++;
|
|
249
|
+
} else if (isBrowserClosedError(error)) {
|
|
250
|
+
needRestart = true;
|
|
251
|
+
recheckLog(` ✗ @${uniqueId} 浏览器断开: ${error.message}`);
|
|
252
|
+
failCount++;
|
|
253
|
+
} else {
|
|
254
|
+
recheckLog(
|
|
255
|
+
` ✗ @${uniqueId} 获取失败: ${error.message || "未知错误"}`,
|
|
256
|
+
);
|
|
257
|
+
failCount++;
|
|
258
|
+
}
|
|
259
|
+
} else if (info) {
|
|
260
|
+
if (info.error) {
|
|
261
|
+
// TikTok 返回了明确状态码(如 404),也提交记录
|
|
262
|
+
successTasks.push({ uniqueId, info });
|
|
263
|
+
} else {
|
|
264
|
+
successTasks.push({ uniqueId, info });
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
recheckLog(` ✗ @${uniqueId} 未获取到用户信息`);
|
|
268
|
+
failCount++;
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
recheckLog(
|
|
272
|
+
` ✗ 任务执行异常: ${result.reason?.message || "未知错误"}`,
|
|
273
|
+
);
|
|
274
|
+
failCount++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 批量提交结果
|
|
279
|
+
if (successTasks.length > 0) {
|
|
280
|
+
try {
|
|
281
|
+
const batchRet = await apiPost(
|
|
282
|
+
`${serverUrl}/api/recheck-jobs-batch`,
|
|
283
|
+
{ updates: successTasks },
|
|
284
|
+
);
|
|
285
|
+
if (batchRet && batchRet.results && Array.isArray(batchRet.results)) {
|
|
286
|
+
for (const r of batchRet.results) {
|
|
287
|
+
if (r.ok) {
|
|
288
|
+
successCount++;
|
|
289
|
+
totalChecked++;
|
|
290
|
+
const task = successTasks.find(
|
|
291
|
+
(t) => t.uniqueId === r.uniqueId,
|
|
292
|
+
);
|
|
293
|
+
const info = task?.info;
|
|
294
|
+
const isSeller = info?.ttSeller;
|
|
295
|
+
const sellerTag = isSeller ? " [ttSeller=true ✓]" : "";
|
|
296
|
+
if (isSeller) totalSellerFound++;
|
|
297
|
+
if (info && info.error) {
|
|
298
|
+
recheckLog(
|
|
299
|
+
` ⚠ @${r.uniqueId} 已跳过 (statusCode=${info.statusCode})`,
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
recheckLog(` ✓${sellerTag} @${r.uniqueId} 已更新`);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
failCount++;
|
|
306
|
+
recheckLog(` ✗ @${r.uniqueId} 更新失败: ${r.error}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
recheckLog(` ⚠ 后端响应格式异常,降级为批量成功`);
|
|
311
|
+
successCount = successTasks.length;
|
|
312
|
+
totalChecked += successTasks.length;
|
|
313
|
+
}
|
|
314
|
+
} catch (err) {
|
|
315
|
+
failCount += successTasks.length;
|
|
316
|
+
recheckLog(` ✗ 批量提交失败: ${err.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
recheckLog(
|
|
321
|
+
` 本批结果: ${successCount} 成功, ${failCount} 失败 (累计: ${totalChecked})`,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// 每 10 轮打印汇总
|
|
325
|
+
if (taskBatchCount % 10 === 0) {
|
|
326
|
+
recheckLog(
|
|
327
|
+
` ── 已跑 ${taskBatchCount} 轮,发现 ttSeller: ${totalSellerFound},累计检测: ${totalChecked}`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// CDN 限流比例超过 30% 时,冷却 + 重启浏览器
|
|
332
|
+
const cdnRatio = tasks.length > 0 ? cdnBlockedCount / tasks.length : 0;
|
|
333
|
+
if (cdnRatio > 0.3) {
|
|
334
|
+
const coolDownSeconds = cdnRatio > 0.8 ? 120 : 60;
|
|
335
|
+
recheckLog(
|
|
336
|
+
` [Recheck] CDN限流比例 ${(cdnRatio * 100).toFixed(0)}% (${cdnBlockedCount}/${tasks.length}),冷却 ${coolDownSeconds} 秒后重启浏览器...`,
|
|
337
|
+
);
|
|
338
|
+
await new Promise((r) => setTimeout(r, coolDownSeconds * 1000));
|
|
339
|
+
await recycleScraper(
|
|
340
|
+
scraper,
|
|
341
|
+
`CDN限流比例过高 (${cdnBlockedCount}/${tasks.length})`,
|
|
342
|
+
);
|
|
343
|
+
browserRestartCount++;
|
|
344
|
+
taskBatchCount = 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const heap = getHeapUsage();
|
|
348
|
+
if (heap.ratio >= HEAP_RESTART_RATIO) {
|
|
349
|
+
await recycleScraper(
|
|
350
|
+
scraper,
|
|
351
|
+
`本批后堆占用 ${(heap.ratio * 100).toFixed(1)}%`,
|
|
352
|
+
);
|
|
353
|
+
browserRestartCount++;
|
|
354
|
+
taskBatchCount = 0;
|
|
355
|
+
} else if (taskBatchCount >= MAX_TASK_BATCHES_BEFORE_RESTART) {
|
|
356
|
+
await recycleScraper(scraper, `已连续处理 ${taskBatchCount} 批任务`);
|
|
357
|
+
browserRestartCount++;
|
|
358
|
+
taskBatchCount = 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (needRestart) {
|
|
362
|
+
recheckLog("[Recheck] 检测到浏览器异常,将在下一轮重启...");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
366
|
+
}
|
|
367
|
+
} catch (err) {
|
|
368
|
+
recheckLog(`[Recheck] 运行异常: ${err.message}`);
|
|
369
|
+
throw err;
|
|
370
|
+
} finally {
|
|
371
|
+
if (forceExitTimer) {
|
|
372
|
+
clearTimeout(forceExitTimer);
|
|
373
|
+
forceExitTimer = null;
|
|
374
|
+
}
|
|
375
|
+
process.removeListener("SIGINT", onSigint);
|
|
376
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
377
|
+
if (!shuttingDown) {
|
|
378
|
+
await scraper.close().catch(() => {});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
package/src/cli/tag.js
CHANGED
|
@@ -1424,7 +1424,7 @@ export async function handleTagRefresh(parsed) {
|
|
|
1424
1424
|
cdpPort +
|
|
1425
1425
|
(effectiveProxy ? " 代理: " + effectiveProxy : ""),
|
|
1426
1426
|
);
|
|
1427
|
-
log("║ 只抓取不重新打分,score>
|
|
1427
|
+
log("║ 只抓取不重新打分,score>60 的标签");
|
|
1428
1428
|
log("╚══════════════════════════════════════════╝");
|
|
1429
1429
|
log("");
|
|
1430
1430
|
|
package/src/lib/args.js
CHANGED
|
@@ -604,6 +604,54 @@ function parseAttachArgs(args) {
|
|
|
604
604
|
};
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
+
function parseRecheckArgs(args) {
|
|
608
|
+
let parallel = 1;
|
|
609
|
+
let interval = 10;
|
|
610
|
+
let serverUrl = defaultServer;
|
|
611
|
+
let countries = [];
|
|
612
|
+
let customProxy = null;
|
|
613
|
+
let poolSize = 3;
|
|
614
|
+
|
|
615
|
+
for (let i = 0; i < args.length; i++) {
|
|
616
|
+
const arg = args[i];
|
|
617
|
+
if (arg === "-p" || arg === "--parallel") {
|
|
618
|
+
parallel = parseInt(args[++i], 10) || 1;
|
|
619
|
+
} else if (arg === "-i" || arg === "--interval") {
|
|
620
|
+
interval = parseInt(args[++i], 10) || 10;
|
|
621
|
+
} else if (arg === "-s" || arg === "--server") {
|
|
622
|
+
serverUrl = args[++i];
|
|
623
|
+
} else if (arg === "-c" || arg === "--countries") {
|
|
624
|
+
countries = args[++i]
|
|
625
|
+
.split(",")
|
|
626
|
+
.map((c) => c.trim().toUpperCase())
|
|
627
|
+
.filter(Boolean);
|
|
628
|
+
} else if (arg === "--proxy") {
|
|
629
|
+
customProxy = args[++i];
|
|
630
|
+
} else if (arg === "--size") {
|
|
631
|
+
poolSize = parseInt(args[++i], 10) || 3;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
subcommand: "recheck",
|
|
637
|
+
attachParallel: parallel,
|
|
638
|
+
attachInterval: interval,
|
|
639
|
+
serverUrl,
|
|
640
|
+
attachCountries: countries,
|
|
641
|
+
customProxy,
|
|
642
|
+
attachPoolSize: poolSize,
|
|
643
|
+
urls: [],
|
|
644
|
+
outputFormat: "json",
|
|
645
|
+
exploreCount: 0,
|
|
646
|
+
showConfig: false,
|
|
647
|
+
showHelp: false,
|
|
648
|
+
configAction: null,
|
|
649
|
+
configValue: null,
|
|
650
|
+
pipeMode: false,
|
|
651
|
+
filterStr: null,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
607
655
|
function parseOpenArgs(args) {
|
|
608
656
|
let openPort = null;
|
|
609
657
|
let openList = false;
|
|
@@ -767,6 +815,8 @@ function parseTagArgs(args) {
|
|
|
767
815
|
let scoreTag = null;
|
|
768
816
|
let scoreCountries = null;
|
|
769
817
|
let scorePort = 9222;
|
|
818
|
+
// 延迟路由:--port 的值先暂存,等循环结束后根据已确定的 subcommand 分配
|
|
819
|
+
let rawTagPort = null;
|
|
770
820
|
|
|
771
821
|
for (let i = 0; i < args.length; i++) {
|
|
772
822
|
const arg = args[i];
|
|
@@ -808,13 +858,9 @@ function parseTagArgs(args) {
|
|
|
808
858
|
.map((s) => s.trim().toUpperCase())
|
|
809
859
|
.filter(Boolean);
|
|
810
860
|
} else if (arg === "--port") {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
refreshPort = parseInt(args[++i]) || 8222;
|
|
815
|
-
} else {
|
|
816
|
-
scorePort = parseInt(args[++i]) || 9222;
|
|
817
|
-
}
|
|
861
|
+
// 延迟路由:暂存端口值,等循环结束后根据 subcommand 分配
|
|
862
|
+
// 避免 --port 出现在 refresh/score-all 之前时被错误分配到其他变量
|
|
863
|
+
rawTagPort = parseInt(args[++i]);
|
|
818
864
|
} else if (arg === "--proxy") {
|
|
819
865
|
scoreProxy = args[++i];
|
|
820
866
|
} else if (arg === "-p" || arg === "--prompt") {
|
|
@@ -852,6 +898,13 @@ function parseTagArgs(args) {
|
|
|
852
898
|
}
|
|
853
899
|
}
|
|
854
900
|
|
|
901
|
+
// ── 延迟路由:将 --port 值分配到正确的 subcommand 变量 ──
|
|
902
|
+
if (rawTagPort !== null && !isNaN(rawTagPort)) {
|
|
903
|
+
if (isScoreAll) scoreAllPort = rawTagPort;
|
|
904
|
+
else if (isRefresh) refreshPort = rawTagPort;
|
|
905
|
+
else scorePort = rawTagPort;
|
|
906
|
+
}
|
|
907
|
+
|
|
855
908
|
if (isDiscover) {
|
|
856
909
|
return {
|
|
857
910
|
subcommand: "tag-discover",
|
|
@@ -1024,6 +1077,10 @@ export function parseArgs() {
|
|
|
1024
1077
|
return parseAttachArgs(args.slice(1));
|
|
1025
1078
|
}
|
|
1026
1079
|
|
|
1080
|
+
if (args.length > 0 && args[0] === "recheck") {
|
|
1081
|
+
return parseRecheckArgs(args.slice(1));
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1027
1084
|
if (args.length > 0 && args[0] === "open") {
|
|
1028
1085
|
return parseOpenArgs(args.slice(1));
|
|
1029
1086
|
}
|
package/src/lib/constants.js
CHANGED
|
@@ -177,6 +177,17 @@ const HELP_TEXT = [
|
|
|
177
177
|
" 示例: tt-help attach -p 5 -i 10",
|
|
178
178
|
" tt-help attach -c PL,DE,FR -p 5",
|
|
179
179
|
"",
|
|
180
|
+
" tt-help recheck [-p 并行数] [-i 间隔秒数] [-s 服务端地址] [-c 国家列表]",
|
|
181
|
+
" 重检 jobs 表中 done 非商家用户(tt_seller!=1),7 天未更新才检测",
|
|
182
|
+
" 重新抓取用户基本信息(重点检查 tt_seller),更新回 jobs 表",
|
|
183
|
+
" -p, --parallel <N> 并行抓取数(默认: 1)",
|
|
184
|
+
" -i, --interval <N> 无任务时轮询间隔,单位秒(默认: 10)",
|
|
185
|
+
" -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)",
|
|
186
|
+
" -c, --countries <A,B> 目标国家列表(逗号分隔,如 PL,DE,FR),默认全部",
|
|
187
|
+
" 示例: tt-help recheck -p 5 -i 10",
|
|
188
|
+
" tt-help recheck -c PL,DE,FR -p 5",
|
|
189
|
+
" tt-help recheck -p 5 --size 5",
|
|
190
|
+
"",
|
|
180
191
|
" tt-help watch -o <db路径> [-p 端口] [--no-llm-refill] [--no-llm-discover]",
|
|
181
192
|
" 启动监控服务;运行期主数据仅来自 SQLite",
|
|
182
193
|
" 选项:",
|
package/src/main.js
CHANGED
|
@@ -3,6 +3,7 @@ import { proxy, HELP_TEXT, getConfigText } from "./lib/constants.js";
|
|
|
3
3
|
import { handleInfo } from "./cli/info.js";
|
|
4
4
|
import { handleExplore } from "./cli/explore.js";
|
|
5
5
|
import { handleAttach } from "./cli/attach.js";
|
|
6
|
+
import { handleRecheck } from "./cli/recheck.js";
|
|
6
7
|
import { handleWatch } from "./cli/watch.js";
|
|
7
8
|
import { handleConfig, showConfig, showUsage, version } from "./cli/config.js";
|
|
8
9
|
import { handleOpen } from "./cli/open.js";
|
|
@@ -29,6 +30,8 @@ async function main() {
|
|
|
29
30
|
return handleInfo(parsed);
|
|
30
31
|
case "attach":
|
|
31
32
|
return handleAttach(parsed);
|
|
33
|
+
case "recheck":
|
|
34
|
+
return handleRecheck(parsed);
|
|
32
35
|
case "watch":
|
|
33
36
|
return handleWatch(parsed);
|
|
34
37
|
case "webserver":
|