tt-help-cli-ycl 1.4.95 → 1.4.97
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.mjs +306 -0
- package/src/watchdog/upgrader.js +9 -49
package/package.json
CHANGED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 补商品命令核心逻辑测试
|
|
3
|
+
*
|
|
4
|
+
* 验证「领取任务 → 补商品详情 → 回传服务端」全链路,自包含:
|
|
5
|
+
* - mock 服务端:/api/product-tasks(领任务)、/api/product-fill/:uniqueId(收回传)
|
|
6
|
+
* - mock webserver:/api/tiktok/product(商品详情)
|
|
7
|
+
*
|
|
8
|
+
* 运行: TMPDIR=$(mktemp -d) node scripts/test-product-fill.mjs
|
|
9
|
+
*/
|
|
10
|
+
import http from "node:http";
|
|
11
|
+
import assert from "node:assert";
|
|
12
|
+
|
|
13
|
+
// ────────────────────────────────────────────────
|
|
14
|
+
// mock webserver:/api/tiktok/product
|
|
15
|
+
// failUrls → 502(普通失败)
|
|
16
|
+
// captchaUrls → 503 retryable(验证码,跳过)
|
|
17
|
+
// 其余 → 200 {rating, reviewCount, soldCount, productName}
|
|
18
|
+
// ────────────────────────────────────────────────
|
|
19
|
+
async function startMockWebserver({
|
|
20
|
+
failUrls = new Set(),
|
|
21
|
+
captchaUrls = new Set(),
|
|
22
|
+
} = {}) {
|
|
23
|
+
const server = http.createServer(async (req, res) => {
|
|
24
|
+
if (req.method === "POST" && req.url.startsWith("/api/tiktok/product")) {
|
|
25
|
+
const body = await readBody(req);
|
|
26
|
+
const { url } = JSON.parse(body || "{}");
|
|
27
|
+
if (captchaUrls.has(url)) {
|
|
28
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
29
|
+
res.end(
|
|
30
|
+
JSON.stringify({
|
|
31
|
+
error: "Blocked by TikTok security check (captcha)",
|
|
32
|
+
retryable: true,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (failUrls.has(url)) {
|
|
38
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
39
|
+
res.end(JSON.stringify({ error: "Bad Gateway" }));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
43
|
+
res.end(
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
rating: 4.7,
|
|
46
|
+
reviewCount: 3303,
|
|
47
|
+
soldCount: 296,
|
|
48
|
+
productName: `商品-${url.slice(-8)}`,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
54
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
55
|
+
});
|
|
56
|
+
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
57
|
+
return {
|
|
58
|
+
url: `http://127.0.0.1:${server.address().port}`,
|
|
59
|
+
close: () => new Promise((r) => server.close(r)),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ────────────────────────────────────────────────
|
|
64
|
+
// mock 服务端:/api/product-tasks + /api/product-fill/:uniqueId
|
|
65
|
+
// tasksPool 可变:每次 GET 弹出 min(limit, 剩余) 个任务
|
|
66
|
+
// ────────────────────────────────────────────────
|
|
67
|
+
async function startMockServer(tasksPool) {
|
|
68
|
+
const received = []; // 回传记录
|
|
69
|
+
const server = http.createServer(async (req, res) => {
|
|
70
|
+
const url = new URL(req.url, "http://x");
|
|
71
|
+
if (req.method === "GET" && url.pathname === "/api/product-tasks") {
|
|
72
|
+
const limit = Number(url.searchParams.get("limit") || 1);
|
|
73
|
+
const tasks = tasksPool.splice(0, limit);
|
|
74
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
75
|
+
res.end(JSON.stringify({ total: tasks.length, tasks }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const m = url.pathname.match(/^\/api\/product-fill\/([^/]+)$/);
|
|
79
|
+
if (req.method === "POST" && m) {
|
|
80
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
81
|
+
received.push({ uniqueId: decodeURIComponent(m[1]), ...body });
|
|
82
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
83
|
+
res.end(JSON.stringify({ ok: true }));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
87
|
+
res.end(JSON.stringify({ error: "not found" }));
|
|
88
|
+
});
|
|
89
|
+
await new Promise((r) => server.listen(0, "127.0.0.1", r));
|
|
90
|
+
return {
|
|
91
|
+
url: `http://127.0.0.1:${server.address().port}`,
|
|
92
|
+
received,
|
|
93
|
+
close: () => new Promise((r) => server.close(r)),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readBody(req) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
let data = "";
|
|
100
|
+
req.on("data", (c) => (data += c));
|
|
101
|
+
req.on("end", () => resolve(data));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ────────────────────────────────────────────────
|
|
106
|
+
// 补商品客户端核心逻辑(未来命令的雏形)
|
|
107
|
+
// 返回 { batches, success, failed, skipped, received }
|
|
108
|
+
// ────────────────────────────────────────────────
|
|
109
|
+
async function fetchProductDetail(webserverUrl, seoUrl) {
|
|
110
|
+
const res = await fetch(`${webserverUrl}/api/tiktok/product`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({ url: seoUrl }),
|
|
114
|
+
signal: AbortSignal.timeout(10000),
|
|
115
|
+
});
|
|
116
|
+
if (res.status === 503) {
|
|
117
|
+
const err = new Error(
|
|
118
|
+
`captcha: ${(await res.json().catch(() => ({}))).error || "Blocked"}`,
|
|
119
|
+
);
|
|
120
|
+
err.retryable = true;
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const err = new Error(`HTTP ${res.status}`);
|
|
125
|
+
err.status = res.status;
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
return res.json();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function fillProducts({
|
|
132
|
+
serverUrl,
|
|
133
|
+
webserverUrl,
|
|
134
|
+
limit = 2,
|
|
135
|
+
intervalMs = 50,
|
|
136
|
+
maxBatches = 4,
|
|
137
|
+
}) {
|
|
138
|
+
let batches = 0,
|
|
139
|
+
success = 0,
|
|
140
|
+
failed = 0,
|
|
141
|
+
skipped = 0;
|
|
142
|
+
const received = [];
|
|
143
|
+
for (let b = 0; b < maxBatches; b++) {
|
|
144
|
+
batches++;
|
|
145
|
+
const res = await fetch(
|
|
146
|
+
`${serverUrl}/api/product-tasks?limit=${limit}&userId=test-user`,
|
|
147
|
+
);
|
|
148
|
+
const { tasks = [] } = await res.json();
|
|
149
|
+
if (tasks.length === 0) {
|
|
150
|
+
await new Promise((r) => setTimeout(r, intervalMs)); // 空闲等待
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
for (const task of tasks) {
|
|
154
|
+
try {
|
|
155
|
+
const detail = await fetchProductDetail(webserverUrl, task.seoUrl);
|
|
156
|
+
const post = await fetch(
|
|
157
|
+
`${serverUrl}/api/product-fill/${encodeURIComponent(task.uniqueId)}`,
|
|
158
|
+
{
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "Content-Type": "application/json" },
|
|
161
|
+
body: JSON.stringify({ productDetail: detail }),
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
if (post.ok) {
|
|
165
|
+
success++;
|
|
166
|
+
received.push({ uniqueId: task.uniqueId, productDetail: detail });
|
|
167
|
+
} else {
|
|
168
|
+
failed++;
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {
|
|
171
|
+
if (e.retryable) skipped++;
|
|
172
|
+
else failed++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { batches, success, failed, skipped, received };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ────────────────────────────────────────────────
|
|
180
|
+
// 测试用例
|
|
181
|
+
// ────────────────────────────────────────────────
|
|
182
|
+
let passed = 0;
|
|
183
|
+
function check(name, fn) {
|
|
184
|
+
try {
|
|
185
|
+
fn();
|
|
186
|
+
passed++;
|
|
187
|
+
console.log(` ✅ ${name}`);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error(` ❌ ${name}\n ${e.message}`);
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 用例 1:正常链路——领取 2 个缺商品任务 → 补商品成功 → 回传 payload 正确
|
|
195
|
+
async function testNormalFlow() {
|
|
196
|
+
console.log("\n[1] 正常链路:领取→补商品→回传");
|
|
197
|
+
const tasksPool = [
|
|
198
|
+
{ uniqueId: "userA", seoUrl: "https://www.tiktok.com/shop/pdp/a123" },
|
|
199
|
+
{ uniqueId: "userB", seoUrl: "https://www.tiktok.com/shop/pdp/b456" },
|
|
200
|
+
];
|
|
201
|
+
const ws = await startMockWebserver();
|
|
202
|
+
const srv = await startMockServer(tasksPool);
|
|
203
|
+
const r = await fillProducts({
|
|
204
|
+
serverUrl: srv.url,
|
|
205
|
+
webserverUrl: ws.url,
|
|
206
|
+
limit: 2,
|
|
207
|
+
maxBatches: 1,
|
|
208
|
+
});
|
|
209
|
+
check("成功数 = 2", () => assert.strictEqual(r.success, 2));
|
|
210
|
+
check("失败/跳过 = 0", () => assert.strictEqual(r.failed + r.skipped, 0));
|
|
211
|
+
check("回传数 = 2", () => assert.strictEqual(r.received.length, 2));
|
|
212
|
+
check("回传 payload 含 productDetail 且字段正确", () => {
|
|
213
|
+
assert.strictEqual(r.received[0].productDetail.rating, 4.7);
|
|
214
|
+
assert.strictEqual(r.received[0].productDetail.soldCount, 296);
|
|
215
|
+
assert.ok(r.received[0].productDetail.productName);
|
|
216
|
+
});
|
|
217
|
+
check("服务端收到 uniqueId 匹配", () =>
|
|
218
|
+
assert.strictEqual(srv.received[0].uniqueId, "userA"),
|
|
219
|
+
);
|
|
220
|
+
await ws.close();
|
|
221
|
+
await srv.close();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 用例 2:空任务——先无任务(空闲等待),后补任务仍能接上
|
|
225
|
+
async function testEmptyThenTask() {
|
|
226
|
+
console.log("\n[2] 空任务:空闲等待后仍能接到任务");
|
|
227
|
+
const tasksPool = [];
|
|
228
|
+
const ws = await startMockWebserver();
|
|
229
|
+
const srv = await startMockServer(tasksPool);
|
|
230
|
+
// 启动时任务池为空 → 第一轮空闲;随后注入任务
|
|
231
|
+
const runner = fillProducts({
|
|
232
|
+
serverUrl: srv.url,
|
|
233
|
+
webserverUrl: ws.url,
|
|
234
|
+
limit: 1,
|
|
235
|
+
intervalMs: 80,
|
|
236
|
+
maxBatches: 4,
|
|
237
|
+
});
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
tasksPool.push({
|
|
240
|
+
uniqueId: "lateUser",
|
|
241
|
+
seoUrl: "https://www.tiktok.com/shop/pdp/late789",
|
|
242
|
+
});
|
|
243
|
+
}, 150);
|
|
244
|
+
const r = await runner;
|
|
245
|
+
check("空闲等待后成功补到 1 个", () => assert.strictEqual(r.success, 1));
|
|
246
|
+
check("无失败", () => assert.strictEqual(r.failed, 0));
|
|
247
|
+
await ws.close();
|
|
248
|
+
await srv.close();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 用例 3:502 失败——该任务 failed,不回传成功
|
|
252
|
+
async function testHttpFail() {
|
|
253
|
+
console.log("\n[3] 补商品 502 失败:计入 failed 且不回传");
|
|
254
|
+
const failUrl = "https://www.tiktok.com/shop/pdp/fail502";
|
|
255
|
+
const tasksPool = [
|
|
256
|
+
{ uniqueId: "failUser", seoUrl: failUrl },
|
|
257
|
+
{ uniqueId: "okUser", seoUrl: "https://www.tiktok.com/shop/pdp/ok001" },
|
|
258
|
+
];
|
|
259
|
+
const ws = await startMockWebserver({ failUrls: new Set([failUrl]) });
|
|
260
|
+
const srv = await startMockServer(tasksPool);
|
|
261
|
+
const r = await fillProducts({
|
|
262
|
+
serverUrl: srv.url,
|
|
263
|
+
webserverUrl: ws.url,
|
|
264
|
+
limit: 2,
|
|
265
|
+
maxBatches: 1,
|
|
266
|
+
});
|
|
267
|
+
check("成功 1 个", () => assert.strictEqual(r.success, 1));
|
|
268
|
+
check("失败 1 个", () => assert.strictEqual(r.failed, 1));
|
|
269
|
+
check("只回传成功的(failUser 不在回传里)", () => {
|
|
270
|
+
assert.strictEqual(r.received.length, 1);
|
|
271
|
+
assert.strictEqual(r.received[0].uniqueId, "okUser");
|
|
272
|
+
});
|
|
273
|
+
await ws.close();
|
|
274
|
+
await srv.close();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 用例 4:captcha——503 retryable,计入 skipped
|
|
278
|
+
async function testCaptcha() {
|
|
279
|
+
console.log("\n[4] 商品详情验证码(503):计入 skipped");
|
|
280
|
+
const capUrl = "https://www.tiktok.com/shop/pdp/cap999";
|
|
281
|
+
const tasksPool = [{ uniqueId: "capUser", seoUrl: capUrl }];
|
|
282
|
+
const ws = await startMockWebserver({ captchaUrls: new Set([capUrl]) });
|
|
283
|
+
const srv = await startMockServer(tasksPool);
|
|
284
|
+
const r = await fillProducts({
|
|
285
|
+
serverUrl: srv.url,
|
|
286
|
+
webserverUrl: ws.url,
|
|
287
|
+
limit: 1,
|
|
288
|
+
maxBatches: 1,
|
|
289
|
+
});
|
|
290
|
+
check("skipped = 1", () => assert.strictEqual(r.skipped, 1));
|
|
291
|
+
check("失败/成功 = 0", () => assert.strictEqual(r.failed + r.success, 0));
|
|
292
|
+
check("未回传(验证码跳过,不提交)", () =>
|
|
293
|
+
assert.strictEqual(r.received.length, 0),
|
|
294
|
+
);
|
|
295
|
+
await ws.close();
|
|
296
|
+
await srv.close();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
console.log("===== 补商品命令核心逻辑测试 =====");
|
|
300
|
+
await testNormalFlow();
|
|
301
|
+
await testEmptyThenTask();
|
|
302
|
+
await testHttpFail();
|
|
303
|
+
await testCaptcha();
|
|
304
|
+
console.log(
|
|
305
|
+
`\n结果: ${passed} 通过${process.exitCode ? ", 有失败" : ", 0 失败"}`,
|
|
306
|
+
);
|
package/src/watchdog/upgrader.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* 自动升级模块 — 检测 tt-help-cli-ycl 新版本并执行升级
|
|
3
3
|
*
|
|
4
4
|
* 仅全局安装模式启用。每 10 分钟查询 npm registry 发现新版本,
|
|
5
|
-
* 由旧 watchdog spawn 独立升级进程 U:U 负责「下载 →
|
|
5
|
+
* 由旧 watchdog spawn 独立升级进程 U:U 负责「下载 → 验证版本 → 杀旧 → 启新」,
|
|
6
6
|
* 见 runUpgradeMode。旧 watchdog 收到 SIGTERM 后优雅退出,新版接管。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { exec, spawn } from "child_process";
|
|
10
10
|
import { readFileSync, openSync } from "fs";
|
|
11
|
-
import { fileURLToPath
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
12
|
import { dirname, join } from "path";
|
|
13
13
|
import { homedir, tmpdir } from "os";
|
|
14
14
|
|
|
@@ -115,8 +115,9 @@ export function performUpgrade() {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* 下载后验证:确认 npm install
|
|
119
|
-
*
|
|
118
|
+
* 下载后验证:确认 npm install 真正换了版本(npm 装 @latest,版本变了即已装上最新版)。
|
|
119
|
+
* 不做模块加载冒烟——主模块 import 链会引入可选依赖(如 better-sqlite3),未安装时
|
|
120
|
+
* 会误报验证失败、阻断升级(211 实测发现),因此仅以版本号为准。
|
|
120
121
|
*/
|
|
121
122
|
async function verifyUpgradedPackage(beforeVersion) {
|
|
122
123
|
const now = getVersion();
|
|
@@ -127,50 +128,9 @@ async function verifyUpgradedPackage(beforeVersion) {
|
|
|
127
128
|
ok: false,
|
|
128
129
|
reason: `版本未变化(仍 ${now}),疑似 npm install 未生效`,
|
|
129
130
|
};
|
|
130
|
-
const smoke = await smokeTestMainModule();
|
|
131
|
-
if (!smoke.ok) return { ok: false, reason: smoke.reason };
|
|
132
131
|
return { ok: true, version: now };
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
/** 冒烟:spawn 子进程加载新版主模块(仅加载不执行业务),能加载则代码/依赖基本可用 */
|
|
136
|
-
function smokeTestMainModule(timeoutMs = 15_000) {
|
|
137
|
-
return new Promise((resolve) => {
|
|
138
|
-
// 必须转成 file:// URL:Windows 上裸的 C:\... 路径会被 ESM loader 当作协议 c: 而报错
|
|
139
|
-
const mainModulePath = pathToFileURL(
|
|
140
|
-
join(__dirname, "..", "..", "src", "main.js"),
|
|
141
|
-
).href;
|
|
142
|
-
const code = `import(${JSON.stringify(
|
|
143
|
-
mainModulePath,
|
|
144
|
-
)}).then(()=>process.exit(0)).catch((e)=>{console.error(e&&e.message||e);process.exit(1)})`;
|
|
145
|
-
let child;
|
|
146
|
-
try {
|
|
147
|
-
child = spawn(process.execPath, ["--input-type=module", "-e", code], {
|
|
148
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
149
|
-
windowsHide: true,
|
|
150
|
-
});
|
|
151
|
-
} catch (e) {
|
|
152
|
-
resolve({ ok: false, reason: `冒烟启动失败: ${e.message}` });
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
let err = "";
|
|
156
|
-
child.stderr?.on("data", (d) => (err += d.toString()));
|
|
157
|
-
const timer = setTimeout(() => {
|
|
158
|
-
try {
|
|
159
|
-
child.kill("SIGKILL");
|
|
160
|
-
} catch {}
|
|
161
|
-
resolve({ ok: false, reason: `冒烟超时 ${timeoutMs}ms` });
|
|
162
|
-
}, timeoutMs);
|
|
163
|
-
child.on("exit", (code) => {
|
|
164
|
-
clearTimeout(timer);
|
|
165
|
-
resolve(
|
|
166
|
-
code === 0
|
|
167
|
-
? { ok: true }
|
|
168
|
-
: { ok: false, reason: `冒烟失败: ${err.slice(0, 200)}` },
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
134
|
/**
|
|
175
135
|
* 启动定时升级检测
|
|
176
136
|
* @param {{intervalMs?: number, onUpdateAvailable?: function}} opts
|
|
@@ -220,8 +180,8 @@ export function startUpgradeChecker({
|
|
|
220
180
|
// ══════════════════════════════════════════════
|
|
221
181
|
//
|
|
222
182
|
// 设计(定稿,无中间文件):旧 watchdog 检测到新版 → spawn 独立升级进程 U(detached)
|
|
223
|
-
// U:npm install 下载
|
|
224
|
-
// - 下载失败 /
|
|
183
|
+
// U:npm install 下载 →【验证版本】→【杀旧】→【启新】
|
|
184
|
+
// - 下载失败 / 验证未通过(版本未变化)→ 不杀旧,旧 watchdog 继续跑
|
|
225
185
|
// - 验证通过 → SIGTERM 旧 watchdog(优雅,5s 未退强杀兜底)→ 无并存窗口 → 启新版一定能起来
|
|
226
186
|
// 关键:U 的 stdout/stderr 独立落盘(tmpdir/tt-help-upgrader-*.log),旧 watchdog 退出后日志不断,可定位。
|
|
227
187
|
|
|
@@ -293,7 +253,7 @@ function buildWatchdogArgs({ serverUrl, clientId, intervalSec, noDaemon }) {
|
|
|
293
253
|
|
|
294
254
|
/**
|
|
295
255
|
* 独立升级器主流程(由旧 watchdog 以子进程方式启动)
|
|
296
|
-
* 顺序:下载 →
|
|
256
|
+
* 顺序:下载 → 验证版本 → 杀旧 → 启新。任一步失败都不杀旧,业务不断。
|
|
297
257
|
* @param {string[]} argv - process.argv.slice(2)
|
|
298
258
|
*/
|
|
299
259
|
export async function runUpgradeMode(argv) {
|
|
@@ -320,7 +280,7 @@ export async function runUpgradeMode(argv) {
|
|
|
320
280
|
process.exit(1);
|
|
321
281
|
}
|
|
322
282
|
|
|
323
|
-
// 2)
|
|
283
|
+
// 2) 下载后验证:版本确实更新(npm 装 @latest,版本变了即最新);通过才允许杀旧
|
|
324
284
|
const verify = await verifyUpgradedPackage(beforeVersion);
|
|
325
285
|
if (!verify.ok) {
|
|
326
286
|
console.error(
|