qmai-cli-public 0.1.3 → 0.1.4
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/npm/install.js +128 -32
- package/package.json +1 -1
package/npm/install.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("node:fs");
|
|
4
4
|
const fsp = require("node:fs/promises");
|
|
5
|
+
const http = require("node:http");
|
|
5
6
|
const https = require("node:https");
|
|
6
7
|
const path = require("node:path");
|
|
7
8
|
const { spawnSync } = require("node:child_process");
|
|
9
|
+
const { URL } = require("node:url");
|
|
8
10
|
|
|
9
11
|
const packageRoot = path.resolve(__dirname, "..");
|
|
10
12
|
const distDir = path.join(packageRoot, "npm", "dist");
|
|
@@ -21,6 +23,10 @@ const archMap = {
|
|
|
21
23
|
arm64: "arm64",
|
|
22
24
|
};
|
|
23
25
|
|
|
26
|
+
const DOWNLOAD_TIMEOUT_MS = parseInt(process.env.QMAI_CLI_DOWNLOAD_TIMEOUT_MS || "30000", 10);
|
|
27
|
+
const DOWNLOAD_RETRIES = parseInt(process.env.QMAI_CLI_DOWNLOAD_RETRIES || "3", 10);
|
|
28
|
+
const MAX_REDIRECTS = parseInt(process.env.QMAI_CLI_DOWNLOAD_MAX_REDIRECTS || "5", 10);
|
|
29
|
+
|
|
24
30
|
function fail(message) {
|
|
25
31
|
console.error(message);
|
|
26
32
|
process.exit(1);
|
|
@@ -59,22 +65,70 @@ function getAssetInfo() {
|
|
|
59
65
|
return { version, platform, arch, baseName, archiveName, binaryName };
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
function parseList(value) {
|
|
69
|
+
return (value || "")
|
|
70
|
+
.split(",")
|
|
71
|
+
.map((item) => item.trim())
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getDownloadCandidates(archiveName) {
|
|
76
|
+
const candidates = [];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
|
|
79
|
+
const add = (url) => {
|
|
80
|
+
if (!url || seen.has(url)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
seen.add(url);
|
|
84
|
+
candidates.push(url);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
add(process.env.QMAI_CLI_DOWNLOAD_URL);
|
|
88
|
+
parseList(process.env.QMAI_CLI_FALLBACK_DOWNLOAD_URLS).forEach(add);
|
|
89
|
+
|
|
90
|
+
const baseUrls = [
|
|
91
|
+
process.env.QMAI_CLI_RELEASE_BASE_URL || "https://github.com/feixiao629/qmai-cli-public/releases/download",
|
|
92
|
+
...parseList(process.env.QMAI_CLI_FALLBACK_RELEASE_BASE_URLS),
|
|
93
|
+
];
|
|
94
|
+
for (const baseUrl of baseUrls) {
|
|
95
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
96
|
+
add(`${normalized}/v${packageJson.version}/${archiveName}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return candidates;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getHttpClient(url) {
|
|
103
|
+
const protocol = new URL(url).protocol;
|
|
104
|
+
if (protocol === "https:") {
|
|
105
|
+
return https;
|
|
106
|
+
}
|
|
107
|
+
if (protocol === "http:") {
|
|
108
|
+
return http;
|
|
66
109
|
}
|
|
110
|
+
throw new Error(`unsupported protocol: ${protocol}`);
|
|
111
|
+
}
|
|
67
112
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"https://github.com/feixiao629/qmai-cli-public/releases/download";
|
|
71
|
-
return `${baseUrl}/v${packageJson.version}/${archiveName}`;
|
|
113
|
+
async function removeIfExists(filePath) {
|
|
114
|
+
await fsp.rm(filePath, { force: true });
|
|
72
115
|
}
|
|
73
116
|
|
|
74
|
-
async function
|
|
117
|
+
async function downloadFileOnce(url, outputPath, redirectCount = 0) {
|
|
118
|
+
if (redirectCount > MAX_REDIRECTS) {
|
|
119
|
+
throw new Error(`redirect limit exceeded (${MAX_REDIRECTS})`);
|
|
120
|
+
}
|
|
121
|
+
|
|
75
122
|
await new Promise((resolve, reject) => {
|
|
76
123
|
const file = fs.createWriteStream(outputPath);
|
|
77
|
-
const
|
|
124
|
+
const client = getHttpClient(url);
|
|
125
|
+
|
|
126
|
+
const cleanup = (callback) => {
|
|
127
|
+
file.destroy();
|
|
128
|
+
removeIfExists(outputPath).then(() => callback()).catch(callback);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const request = client.get(
|
|
78
132
|
url,
|
|
79
133
|
{
|
|
80
134
|
headers: {
|
|
@@ -83,42 +137,85 @@ async function downloadFile(url, outputPath) {
|
|
|
83
137
|
},
|
|
84
138
|
},
|
|
85
139
|
(response) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
response.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
140
|
+
const { statusCode = 0, statusMessage = "", headers } = response;
|
|
141
|
+
|
|
142
|
+
if (statusCode >= 300 && statusCode < 400 && headers.location) {
|
|
143
|
+
const nextUrl = new URL(headers.location, url).toString();
|
|
144
|
+
response.resume();
|
|
145
|
+
file.close(() => {
|
|
146
|
+
removeIfExists(outputPath)
|
|
147
|
+
.then(() => downloadFileOnce(nextUrl, outputPath, redirectCount + 1))
|
|
148
|
+
.then(resolve)
|
|
149
|
+
.catch(reject);
|
|
150
|
+
});
|
|
95
151
|
return;
|
|
96
152
|
}
|
|
97
153
|
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
new Error(`download failed: ${response.statusCode} ${response.statusMessage || ""}`.trim())
|
|
154
|
+
if (statusCode !== 200) {
|
|
155
|
+
response.resume();
|
|
156
|
+
cleanup(() =>
|
|
157
|
+
reject(new Error(`download failed: ${statusCode} ${statusMessage}`.trim()))
|
|
103
158
|
);
|
|
104
159
|
return;
|
|
105
160
|
}
|
|
106
161
|
|
|
107
162
|
response.pipe(file);
|
|
108
163
|
file.on("finish", () => file.close(resolve));
|
|
164
|
+
response.on("error", (error) => {
|
|
165
|
+
cleanup(() => reject(error));
|
|
166
|
+
});
|
|
109
167
|
}
|
|
110
168
|
);
|
|
111
169
|
|
|
170
|
+
request.setTimeout(DOWNLOAD_TIMEOUT_MS, () => {
|
|
171
|
+
request.destroy(new Error(`download timeout after ${DOWNLOAD_TIMEOUT_MS}ms`));
|
|
172
|
+
});
|
|
173
|
+
|
|
112
174
|
request.on("error", (error) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
175
|
+
cleanup(() => reject(error));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
file.on("error", (error) => {
|
|
179
|
+
request.destroy(error);
|
|
118
180
|
});
|
|
119
181
|
});
|
|
120
182
|
}
|
|
121
183
|
|
|
184
|
+
async function downloadFile(urls, outputPath) {
|
|
185
|
+
const attempts = [];
|
|
186
|
+
|
|
187
|
+
for (const url of urls) {
|
|
188
|
+
for (let attempt = 1; attempt <= DOWNLOAD_RETRIES; attempt += 1) {
|
|
189
|
+
try {
|
|
190
|
+
console.log(`下载 qmai 二进制(${attempt}/${DOWNLOAD_RETRIES}):${url}`);
|
|
191
|
+
await downloadFileOnce(url, outputPath);
|
|
192
|
+
return;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
attempts.push(`${url} [${attempt}/${DOWNLOAD_RETRIES}]: ${error.message}`);
|
|
195
|
+
console.warn(`下载失败:${error.message}`);
|
|
196
|
+
await removeIfExists(outputPath);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
throw new Error(
|
|
202
|
+
[
|
|
203
|
+
"无法下载 qmai 二进制。",
|
|
204
|
+
`已尝试 ${urls.length} 个下载源,每个最多重试 ${DOWNLOAD_RETRIES} 次。`,
|
|
205
|
+
`超时设置:${DOWNLOAD_TIMEOUT_MS}ms。`,
|
|
206
|
+
"可设置以下环境变量重试:",
|
|
207
|
+
"- QMAI_CLI_DOWNLOAD_TIMEOUT_MS=60000",
|
|
208
|
+
"- QMAI_CLI_DOWNLOAD_RETRIES=5",
|
|
209
|
+
"- QMAI_CLI_RELEASE_BASE_URL=https://<your-mirror>/releases/download",
|
|
210
|
+
"- QMAI_CLI_FALLBACK_RELEASE_BASE_URLS=https://mirror-a/releases/download,https://mirror-b/releases/download",
|
|
211
|
+
"- QMAI_CLI_DOWNLOAD_URL=https://.../qmai_<version>_<os>_<arch>.tar.gz",
|
|
212
|
+
"- QMAI_CLI_LOCAL_ASSET_DIR=/path/to/pre-downloaded-assets",
|
|
213
|
+
"失败明细:",
|
|
214
|
+
...attempts.map((item) => ` - ${item}`),
|
|
215
|
+
].join("\n")
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
122
219
|
async function extractArchive(archivePath, targetDir, platform) {
|
|
123
220
|
if (platform === "windows") {
|
|
124
221
|
runCommand("powershell.exe", [
|
|
@@ -153,9 +250,8 @@ async function main() {
|
|
|
153
250
|
if (localAssetDir) {
|
|
154
251
|
await copyLocalAsset(localAssetDir, archiveName, archivePath);
|
|
155
252
|
} else {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
await downloadFile(url, archivePath);
|
|
253
|
+
const urls = getDownloadCandidates(archiveName);
|
|
254
|
+
await downloadFile(urls, archivePath);
|
|
159
255
|
}
|
|
160
256
|
|
|
161
257
|
await extractArchive(archivePath, distDir, platform);
|