tt-help-cli-ycl 1.4.146 → 1.4.147
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/test-product-fill-cdp.mjs +124 -0
- package/src/cli/product-fill.js +148 -24
- package/src/lib/args.js +6 -0
- package/src/lib/constants.js +2 -1
package/package.json
CHANGED
|
@@ -138,6 +138,37 @@ try {
|
|
|
138
138
|
assert.strictEqual(parsed.subcommand, "product-fill"),
|
|
139
139
|
);
|
|
140
140
|
check("-l 3 生效", () => assert.strictEqual(parsed.productFillLimit, 3));
|
|
141
|
+
check("--task-delay 未传时默认 5", () =>
|
|
142
|
+
assert.strictEqual(parsed.productFillTaskDelay, 5),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
process.argv = [
|
|
146
|
+
"node",
|
|
147
|
+
"tt-help",
|
|
148
|
+
"product-fill",
|
|
149
|
+
"--profiles",
|
|
150
|
+
"US/9223",
|
|
151
|
+
"--task-delay",
|
|
152
|
+
"30",
|
|
153
|
+
];
|
|
154
|
+
const parsedDelay = parseArgs();
|
|
155
|
+
check("--task-delay 30 生效", () =>
|
|
156
|
+
assert.strictEqual(parsedDelay.productFillTaskDelay, 30),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
process.argv = [
|
|
160
|
+
"node",
|
|
161
|
+
"tt-help",
|
|
162
|
+
"product-fill",
|
|
163
|
+
"--profiles",
|
|
164
|
+
"US/9223",
|
|
165
|
+
"--task-delay",
|
|
166
|
+
"0",
|
|
167
|
+
];
|
|
168
|
+
const parsedDelayZero = parseArgs();
|
|
169
|
+
check("--task-delay 0 生效(关闭任务间延迟)", () =>
|
|
170
|
+
assert.strictEqual(parsedDelayZero.productFillTaskDelay, 0),
|
|
171
|
+
);
|
|
141
172
|
|
|
142
173
|
process.argv = [
|
|
143
174
|
"node",
|
|
@@ -163,6 +194,99 @@ try {
|
|
|
163
194
|
process.argv = origArgv;
|
|
164
195
|
}
|
|
165
196
|
|
|
197
|
+
console.log("\n[6] 浏览器断连判定(网络层错误不重启,防误杀 Edge)");
|
|
198
|
+
const { isBrowserDisconnected, isNetworkError, isCDPPortReady } =
|
|
199
|
+
await import("../src/cli/product-fill.js");
|
|
200
|
+
|
|
201
|
+
const onlineBrowser = { isConnected: () => true };
|
|
202
|
+
const offlineBrowser = { isConnected: () => false };
|
|
203
|
+
|
|
204
|
+
check("net::ERR_CONNECTION_CLOSED + 浏览器在线 → 不断连", () => {
|
|
205
|
+
assert.strictEqual(
|
|
206
|
+
isBrowserDisconnected(
|
|
207
|
+
onlineBrowser,
|
|
208
|
+
new Error("page.goto: net::ERR_CONNECTION_CLOSED at https://..."),
|
|
209
|
+
),
|
|
210
|
+
false,
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
check("net::ERR_CONNECTION_RESET + 浏览器在线 → 不断连", () => {
|
|
214
|
+
assert.strictEqual(
|
|
215
|
+
isBrowserDisconnected(
|
|
216
|
+
onlineBrowser,
|
|
217
|
+
new Error("net::ERR_CONNECTION_RESET"),
|
|
218
|
+
),
|
|
219
|
+
false,
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
check("net::ERR_PROXY_CONNECTION_FAILED + 浏览器在线 → 不断连", () => {
|
|
223
|
+
assert.strictEqual(
|
|
224
|
+
isBrowserDisconnected(
|
|
225
|
+
onlineBrowser,
|
|
226
|
+
new Error("net::ERR_PROXY_CONNECTION_FAILED"),
|
|
227
|
+
),
|
|
228
|
+
false,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
check("isConnected()=false → 断连", () => {
|
|
232
|
+
assert.strictEqual(
|
|
233
|
+
isBrowserDisconnected(offlineBrowser, new Error("any error")),
|
|
234
|
+
true,
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
check("Target closed + 在线 → 断连", () => {
|
|
238
|
+
assert.strictEqual(
|
|
239
|
+
isBrowserDisconnected(onlineBrowser, new Error("Target closed")),
|
|
240
|
+
true,
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
check("Browser has been closed + 在线 → 断连", () => {
|
|
244
|
+
assert.strictEqual(
|
|
245
|
+
isBrowserDisconnected(onlineBrowser, new Error("Browser has been closed")),
|
|
246
|
+
true,
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
check("Target page... closed + 在线 → 断连", () => {
|
|
250
|
+
assert.strictEqual(
|
|
251
|
+
isBrowserDisconnected(
|
|
252
|
+
onlineBrowser,
|
|
253
|
+
new Error("Target page, context or browser has been closed"),
|
|
254
|
+
),
|
|
255
|
+
true,
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
check("Protocol error + 在线 → 不断连(交给错误计数处理)", () => {
|
|
259
|
+
assert.strictEqual(
|
|
260
|
+
isBrowserDisconnected(onlineBrowser, new Error("Protocol error")),
|
|
261
|
+
false,
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
check("无错误参数 → 不断连", () => {
|
|
265
|
+
assert.strictEqual(isBrowserDisconnected(onlineBrowser, null), false);
|
|
266
|
+
});
|
|
267
|
+
check("isNetworkError(net::ERR_*) → true", () => {
|
|
268
|
+
assert.strictEqual(
|
|
269
|
+
isNetworkError(new Error("net::ERR_CONNECTION_CLOSED")),
|
|
270
|
+
true,
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
check("isNetworkError(普通错误) → false", () => {
|
|
274
|
+
assert.strictEqual(isNetworkError(new Error("Timeout")), false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
console.log("\n[7] CDP 端口就绪探测(isCDPPortReady)");
|
|
278
|
+
// 未监听端口应快速返回 false(本机无 Edge 监听该端口)
|
|
279
|
+
const probeResult = await isCDPPortReady(65531);
|
|
280
|
+
if (probeResult === false) {
|
|
281
|
+
passed++;
|
|
282
|
+
console.log(" ✅ 未监听端口 → false");
|
|
283
|
+
} else {
|
|
284
|
+
console.error(
|
|
285
|
+
" ❌ 未监听端口 → false\n 期望 false,实得 " + probeResult,
|
|
286
|
+
);
|
|
287
|
+
process.exitCode = 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
166
290
|
console.log(
|
|
167
291
|
`\n结果: ${passed} 通过${process.exitCode ? ", 有失败" : ", 0 失败"}`,
|
|
168
292
|
);
|
package/src/cli/product-fill.js
CHANGED
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
import { ProxyAgent, request } from "undici";
|
|
18
18
|
import { getMacOrUuid } from "../lib/mac-or-uuid.js";
|
|
19
19
|
import { createApiClient } from "../lib/api-client.js";
|
|
20
|
-
import { userId as configuredUserId, saveUserId } from "../lib/constants.js";
|
|
21
|
-
import { ensureBrowserReady } from "../lib/browser/cdp.js";
|
|
22
20
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from "../lib/
|
|
21
|
+
userId as configuredUserId,
|
|
22
|
+
saveUserId,
|
|
23
|
+
proxy as configuredProxy,
|
|
24
|
+
} from "../lib/constants.js";
|
|
25
|
+
import { ensureBrowserReady, killEdgeProcesses } from "../lib/browser/cdp.js";
|
|
26
|
+
import { getOrCreatePage, relaunchBrowser } from "../lib/browser/page.js";
|
|
27
27
|
import {
|
|
28
28
|
getProductInfoCDP,
|
|
29
29
|
isEmptyProductDetail,
|
|
@@ -37,7 +37,7 @@ const CAPTCHA_BACKOFF_MS = 30_000; // 验证码后冷却时长
|
|
|
37
37
|
const CDP_CAPTCHA_WAIT_MS = 180_000; // CDP 模式人工解验证码最长等待
|
|
38
38
|
const FAIL_MARK_LIMIT = 3; // 连续失败达到该次数后哨兵放弃
|
|
39
39
|
const SENTINEL_RATING = -1; // 哨兵:商品不可售/已下架(正常评分 0-5,-1 不会冲突)
|
|
40
|
-
const DEFAULT_PROXY = "http://127.0.0.1:7897"; //
|
|
40
|
+
const DEFAULT_PROXY = "http://127.0.0.1:7897"; // 未配置任何代理时,出口探测按本机 Clash 默认端口尝试
|
|
41
41
|
|
|
42
42
|
function formatNow() {
|
|
43
43
|
return new Date().toLocaleString("zh-CN", { hour12: false });
|
|
@@ -47,6 +47,62 @@ function log(message = "") {
|
|
|
47
47
|
console.error(`[${formatNow()}] ${message}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* 判断浏览器是否真的已断开(用户手动关窗口 / CDP 断连)。
|
|
52
|
+
* 网络层错误(net::ERR_CONNECTION_CLOSED/RESET/REFUSED...)是代理断连/节点切换,
|
|
53
|
+
* 浏览器进程还活着,不算断连 —— 只认两种确凿信号:
|
|
54
|
+
* 1. browser.isConnected() === false(CDP 连接已断)
|
|
55
|
+
* 2. 明确的关闭消息(Target closed / Browser has been closed)
|
|
56
|
+
* 参考 tag.js 的 isBrowserDisconnected;page.js 的 isBrowserClosedError
|
|
57
|
+
* 会把 net::ERR_CONNECTION_* 误判为浏览器关闭,进而误杀共用端口的 Edge。
|
|
58
|
+
*/
|
|
59
|
+
export function isBrowserDisconnected(browser, error) {
|
|
60
|
+
try {
|
|
61
|
+
if (
|
|
62
|
+
browser &&
|
|
63
|
+
typeof browser.isConnected === "function" &&
|
|
64
|
+
!browser.isConnected()
|
|
65
|
+
) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
} catch {}
|
|
69
|
+
if (!error) return false;
|
|
70
|
+
const msg = error.message || "";
|
|
71
|
+
if (msg.includes("net::ERR_")) return false; // 网络层错误不代表浏览器关闭
|
|
72
|
+
return (
|
|
73
|
+
msg.includes("Target closed") ||
|
|
74
|
+
msg.includes("Browser has been closed") ||
|
|
75
|
+
msg.includes("Target page, context or browser has been closed")
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 网络层错误(net::ERR_*):代理断连/节点切换,浏览器没死,不应触发重启 */
|
|
80
|
+
export function isNetworkError(error) {
|
|
81
|
+
if (!error) return false;
|
|
82
|
+
const msg = error.message || "";
|
|
83
|
+
return msg.includes("net::ERR_");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 探测 CDP 端口是否已有浏览器在监听(连接前调用,
|
|
88
|
+
* 用于区分"本进程启动的浏览器"和"复用的已存在实例",
|
|
89
|
+
* 退出时只关闭前者,不误杀用户自己开着的 Edge)。
|
|
90
|
+
*/
|
|
91
|
+
export async function isCDPPortReady(port) {
|
|
92
|
+
try {
|
|
93
|
+
const { statusCode, body } = await request(
|
|
94
|
+
`http://127.0.0.1:${port}/json`,
|
|
95
|
+
{ method: "GET", headersTimeout: 2000, bodyTimeout: 2000 },
|
|
96
|
+
);
|
|
97
|
+
try {
|
|
98
|
+
await body.dump();
|
|
99
|
+
} catch {}
|
|
100
|
+
return statusCode === 200;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
50
106
|
/**
|
|
51
107
|
* 通过代理探测出口 IP 国家码(ip-api.com 免费接口)。
|
|
52
108
|
* 探测失败(超时/代理未开/接口异常)返回 null。
|
|
@@ -77,10 +133,15 @@ export async function handleProductFill(options) {
|
|
|
77
133
|
productFillInterval,
|
|
78
134
|
serverUrl,
|
|
79
135
|
productFillProxy,
|
|
136
|
+
productFillTaskDelay,
|
|
80
137
|
productFillProfiles,
|
|
81
138
|
showHelp,
|
|
82
139
|
} = options;
|
|
83
140
|
|
|
141
|
+
// 与其他命令口径一致:--proxy 参数 > 配置文件 ~/.tt-help.json 的 proxy;
|
|
142
|
+
// 均未配置则不注入 proxyServer,浏览器走系统默认代理(explore/attach 同款)
|
|
143
|
+
const resolvedProxy = productFillProxy || configuredProxy || null;
|
|
144
|
+
|
|
84
145
|
if (showHelp) {
|
|
85
146
|
log(
|
|
86
147
|
"用法: tt-help product-fill --profiles <国家/端口>[,<国家/端口>...] [-s 服务端] [-i 间隔秒] [-l 每批上限] [--proxy URL]",
|
|
@@ -95,10 +156,13 @@ export async function handleProductFill(options) {
|
|
|
95
156
|
);
|
|
96
157
|
log(" -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)");
|
|
97
158
|
log(
|
|
98
|
-
" --proxy <URL>
|
|
159
|
+
" --proxy <URL> 浏览器启动代理及出口探测(未指定时用配置文件 ~/.tt-help.json 的 proxy;均未配置则浏览器走系统默认代理)",
|
|
99
160
|
);
|
|
100
161
|
log(" -i, --interval <N> 无任务时轮询间隔,单位秒(默认: 10)");
|
|
101
162
|
log(" -l, --limit <N> 每批领取任务数上限(默认: 5)");
|
|
163
|
+
log(
|
|
164
|
+
" --task-delay <N> 每个任务处理完后的随机延迟上限,秒(默认: 5,实际等待 N/2~N 秒随机)",
|
|
165
|
+
);
|
|
102
166
|
log("");
|
|
103
167
|
log("说明:");
|
|
104
168
|
log(" 循环向服务端 /api/product-tasks?country=XX 领取缺商品详情任务;");
|
|
@@ -171,6 +235,9 @@ export async function handleProductFill(options) {
|
|
|
171
235
|
let browser = null;
|
|
172
236
|
let page = null;
|
|
173
237
|
let currentPort = null;
|
|
238
|
+
// 本进程启动(或断连重启拉起)的浏览器端口:退出时按端口关闭;
|
|
239
|
+
// 连接前端口已有浏览器(用户自己开的)→ 不记录,退出时不杀
|
|
240
|
+
const launchedPorts = new Set();
|
|
174
241
|
const connectProfile = async (profile) => {
|
|
175
242
|
if (currentPort === profile.port && browser) return;
|
|
176
243
|
if (browser) {
|
|
@@ -179,18 +246,21 @@ export async function handleProductFill(options) {
|
|
|
179
246
|
page = null;
|
|
180
247
|
}
|
|
181
248
|
const cdpOptions = { port: profile.port };
|
|
182
|
-
if (
|
|
249
|
+
if (resolvedProxy) cdpOptions.proxyServer = resolvedProxy;
|
|
250
|
+
const wasReady = await isCDPPortReady(profile.port);
|
|
183
251
|
log(
|
|
184
|
-
`[ProductFill] 切换浏览器端口 ${currentPort ?? "-"} → ${profile.port}(country=${profile.country}${
|
|
252
|
+
`[ProductFill] 切换浏览器端口 ${currentPort ?? "-"} → ${profile.port}(country=${profile.country}${resolvedProxy ? `,代理 ${resolvedProxy}` : ""})`,
|
|
185
253
|
);
|
|
186
254
|
browser = await ensureBrowserReady(cdpOptions);
|
|
255
|
+
if (!wasReady) launchedPorts.add(profile.port);
|
|
187
256
|
page = await getOrCreatePage(browser);
|
|
188
257
|
currentPort = profile.port;
|
|
189
258
|
log(`[ProductFill] CDP 浏览器已就绪(当前页: ${page.url()})`);
|
|
190
259
|
};
|
|
191
260
|
|
|
192
261
|
// ── 代理出口国家探测 ──
|
|
193
|
-
|
|
262
|
+
// 与浏览器启动通道保持一致;未配置任何代理时按本机 Clash 默认端口探测
|
|
263
|
+
const effectiveProxyUrl = resolvedProxy || DEFAULT_PROXY;
|
|
194
264
|
let proxyCountry = null;
|
|
195
265
|
const refreshProxyCountry = async () => {
|
|
196
266
|
const prev = proxyCountry;
|
|
@@ -218,6 +288,18 @@ export async function handleProductFill(options) {
|
|
|
218
288
|
tick();
|
|
219
289
|
});
|
|
220
290
|
|
|
291
|
+
/**
|
|
292
|
+
* 任务间随机延迟(可被退出信号中断):--task-delay 为最大秒数 N,
|
|
293
|
+
* 实际等待 N/2 ~ N 秒随机(随机口径与 delay.js 的 delay(min, max) 一致)。
|
|
294
|
+
* N=0 时立即返回(关闭任务间延迟)。
|
|
295
|
+
*/
|
|
296
|
+
const taskRandomDelay = () => {
|
|
297
|
+
const maxMs = (productFillTaskDelay ?? 0) * 1000;
|
|
298
|
+
const minMs = Math.floor(maxMs / 2);
|
|
299
|
+
const ms = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;
|
|
300
|
+
return interruptibleSleep(ms);
|
|
301
|
+
};
|
|
302
|
+
|
|
221
303
|
let loopCount = 0;
|
|
222
304
|
let totalSuccess = 0;
|
|
223
305
|
let totalFailed = 0;
|
|
@@ -290,23 +372,43 @@ export async function handleProductFill(options) {
|
|
|
290
372
|
};
|
|
291
373
|
|
|
292
374
|
/** 抓取商品详情,带浏览器断连恢复与空数据防御 */
|
|
293
|
-
const getTaskDetail = async (seoUrl) => {
|
|
375
|
+
const getTaskDetail = async (seoUrl, country) => {
|
|
294
376
|
let detail;
|
|
295
377
|
try {
|
|
296
378
|
detail = await fetchViaCDP(seoUrl);
|
|
297
379
|
} catch (e) {
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
380
|
+
if (isBrowserDisconnected(browser, e)) {
|
|
381
|
+
// 确凿断连(CDP 断开 / 明确关闭消息)才重启浏览器
|
|
382
|
+
log(" ↻ 浏览器断连,重启浏览器后重试...");
|
|
383
|
+
try {
|
|
384
|
+
const cdpOptions = { port: currentPort };
|
|
385
|
+
if (resolvedProxy) cdpOptions.proxyServer = resolvedProxy;
|
|
386
|
+
browser = await relaunchBrowser(cdpOptions, currentPort);
|
|
387
|
+
page = await getOrCreatePage(browser);
|
|
388
|
+
launchedPorts.add(currentPort); // 重启拉起的实例归本进程管,退出时关闭
|
|
389
|
+
} catch (relaunchErr) {
|
|
390
|
+
relaunchErr.retryable = true;
|
|
391
|
+
throw relaunchErr;
|
|
392
|
+
}
|
|
393
|
+
// 重启后先确认代理出口恢复正常,避免刚拉起又撞上代理断连窗口
|
|
394
|
+
await refreshProxyCountry();
|
|
395
|
+
if (!proxyCountry || (country && proxyCountry !== country)) {
|
|
396
|
+
const err = new Error(
|
|
397
|
+
`浏览器重启后代理出口异常(${proxyCountry || "探测失败"}),本轮跳过`,
|
|
398
|
+
);
|
|
399
|
+
err.retryable = true;
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
detail = await fetchViaCDP(seoUrl);
|
|
403
|
+
} else if (isNetworkError(e)) {
|
|
404
|
+
// 网络层错误:代理断连/Clash 切节点,浏览器没死,不杀浏览器,
|
|
405
|
+
// 记为可重试错误跳过本轮,下轮代理探测自然跟上
|
|
406
|
+
const err = new Error(`网络层错误(浏览器未重启): ${e.message}`);
|
|
407
|
+
err.retryable = true;
|
|
408
|
+
throw err;
|
|
409
|
+
} else {
|
|
410
|
+
throw e;
|
|
308
411
|
}
|
|
309
|
-
detail = await fetchViaCDP(seoUrl);
|
|
310
412
|
}
|
|
311
413
|
// 空数据防御:回写字段全空,不回写(防止假成功死循环)
|
|
312
414
|
if (isEmptyProductDetail(detail)) {
|
|
@@ -390,7 +492,7 @@ export async function handleProductFill(options) {
|
|
|
390
492
|
}
|
|
391
493
|
log(` → 补商品 @${uniqueId} ...`);
|
|
392
494
|
try {
|
|
393
|
-
const detail = await getTaskDetail(seoUrl);
|
|
495
|
+
const detail = await getTaskDetail(seoUrl, profile.country);
|
|
394
496
|
const post = await apiPost(
|
|
395
497
|
`${serverUrl}/api/product-fill/${encodeURIComponent(uniqueId)}`,
|
|
396
498
|
{ productDetail: detail },
|
|
@@ -441,6 +543,8 @@ export async function handleProductFill(options) {
|
|
|
441
543
|
await noteFail(uniqueId);
|
|
442
544
|
}
|
|
443
545
|
}
|
|
546
|
+
// 每个任务处理完后随机延迟(防风控):N/2 ~ N 秒,Ctrl+C 可中断
|
|
547
|
+
if (!shuttingDown) await taskRandomDelay();
|
|
444
548
|
}
|
|
445
549
|
|
|
446
550
|
log(
|
|
@@ -453,6 +557,26 @@ export async function handleProductFill(options) {
|
|
|
453
557
|
throw err;
|
|
454
558
|
} finally {
|
|
455
559
|
if (forceExitTimer) clearTimeout(forceExitTimer);
|
|
560
|
+
// 退出清理:关闭本进程启动的浏览器实例。
|
|
561
|
+
// CDP 的 browser.close() 只断开连接不关浏览器进程,需按端口 kill;
|
|
562
|
+
// 只杀 launchedPorts 里的端口(本进程拉起的),用户自己开着的 Edge 不动
|
|
563
|
+
if (browser) {
|
|
564
|
+
await browser.close().catch(() => {});
|
|
565
|
+
}
|
|
566
|
+
if (launchedPorts.size > 0) {
|
|
567
|
+
log(
|
|
568
|
+
`[ProductFill] 正在关闭浏览器(端口 ${[...launchedPorts].join(", ")})...`,
|
|
569
|
+
);
|
|
570
|
+
await Promise.all(
|
|
571
|
+
[...launchedPorts].map((port) =>
|
|
572
|
+
Promise.race([
|
|
573
|
+
killEdgeProcesses(undefined, port),
|
|
574
|
+
new Promise((r) => setTimeout(r, 5000)),
|
|
575
|
+
]),
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
log(`[ProductFill] 浏览器已关闭`);
|
|
579
|
+
}
|
|
456
580
|
log(
|
|
457
581
|
`[ProductFill] 已退出。总计: 成功 ${totalSuccess} | 失败 ${totalFailed} | 跳过 ${totalSkipped} | 标记不可售 ${totalMarked}`,
|
|
458
582
|
);
|
package/src/lib/args.js
CHANGED
|
@@ -895,6 +895,7 @@ function parseProductFillArgs(args) {
|
|
|
895
895
|
let interval = 10;
|
|
896
896
|
let serverUrl = defaultServer;
|
|
897
897
|
let proxyUrl = null;
|
|
898
|
+
let taskDelay = 5; // --task-delay:每个任务处理完后的随机延迟上限,秒
|
|
898
899
|
/** 端口/国家配置:--profiles US/9223,JP/1222 → [{country:"US",port:9223},...] */
|
|
899
900
|
let profiles = [];
|
|
900
901
|
|
|
@@ -919,6 +920,10 @@ function parseProductFillArgs(args) {
|
|
|
919
920
|
interval = parseInt(args[++i], 10) || 10;
|
|
920
921
|
} else if (arg === "-l" || arg === "--limit") {
|
|
921
922
|
limit = parseInt(args[++i], 10) || 5;
|
|
923
|
+
} else if (arg === "--task-delay") {
|
|
924
|
+
// 允许 0(关闭任务间延迟),非法值回退默认 5
|
|
925
|
+
const v = parseInt(args[++i], 10);
|
|
926
|
+
taskDelay = Number.isFinite(v) && v >= 0 ? v : 5;
|
|
922
927
|
}
|
|
923
928
|
}
|
|
924
929
|
|
|
@@ -928,6 +933,7 @@ function parseProductFillArgs(args) {
|
|
|
928
933
|
productFillInterval: interval,
|
|
929
934
|
serverUrl,
|
|
930
935
|
productFillProxy: proxyUrl,
|
|
936
|
+
productFillTaskDelay: taskDelay,
|
|
931
937
|
productFillProfiles: profiles,
|
|
932
938
|
urls: [],
|
|
933
939
|
outputFormat: "json",
|
package/src/lib/constants.js
CHANGED
|
@@ -227,9 +227,10 @@ const HELP_TEXT = [
|
|
|
227
227
|
" --profiles 国家/端口对绑定(端口 profile 区域需与商品国家一致,如 US/9223,JP/1222);",
|
|
228
228
|
" 每轮探测代理出口,在配置中匹配端口领取对应国家任务,出口不在配置范围则跳过",
|
|
229
229
|
" -s, --server <URL> 服务端地址(默认: http://127.0.0.1:3001)",
|
|
230
|
-
" --proxy <URL>
|
|
230
|
+
" --proxy <URL> 浏览器启动代理及出口探测(未指定时用配置文件 proxy;均未配置则浏览器走系统默认代理)",
|
|
231
231
|
" -i, --interval <N> 无任务时轮询间隔,单位秒(默认: 10)",
|
|
232
232
|
" -l, --limit <N> 每批领取任务数上限(默认: 5)",
|
|
233
|
+
" --task-delay <N> 每个任务处理完后的随机延迟上限,秒(默认: 5,实际 N/2~N 秒随机)",
|
|
233
234
|
" 下架壳页/连续失败3次/无效URL → 回写 rating=-1 哨兵永久移出候选",
|
|
234
235
|
" 示例: tt-help product-fill --profiles US/9223",
|
|
235
236
|
" tt-help product-fill --profiles US/9223,JP/1222 -s http://117.71.53.99:17301 -i 5",
|