@wwkit/opm 1.0.14 → 1.0.16
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/README.md +33 -8
- package/docs/managers/apt.md +0 -1
- package/docs/managers/brew.md +0 -4
- package/docs/managers/bun.md +0 -2
- package/docs/managers/composer.md +0 -4
- package/docs/managers/dnf.md +0 -1
- package/docs/managers/npm.md +0 -4
- package/docs/managers/pip.md +0 -4
- package/docs/opm.html +8 -21
- package/docs/test/linux-environment-test-report.md +0 -1
- package/package.json +13 -4
- package/src/binaries/base.js +1 -1
- package/src/cli/groups/env.js +451 -0
- package/src/cli/groups/package.js +30 -10
- package/src/cli/groups/ping.js +60 -20
- package/src/cli/index.js +3 -1
- package/src/index.js +2 -1
- package/src/managers/apt.js +0 -17
- package/src/managers/base.js +0 -15
- package/src/managers/brew.js +1 -32
- package/src/managers/bun.js +7 -58
- package/src/managers/composer.js +1 -16
- package/src/managers/dnf.js +0 -17
- package/src/managers/npm.js +0 -20
- package/src/managers/pip.js +1 -22
- package/src/managers/proc.js +7 -1
- package/src/managers/runtime.js +0 -4
- package/src/managers/winget.js +0 -14
- package/src/managers/http.js +0 -432
package/src/managers/http.js
DELETED
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 共享 HTTP 拉取工具(支持代理隧道)
|
|
3
|
-
*
|
|
4
|
-
* 提取自 pip 管理器的 _fetchText 实现,供各包管理器复用:
|
|
5
|
-
* 无代理时使用 native fetch;有代理时根据代理协议建立隧道:
|
|
6
|
-
* - http(s):// HTTP 代理:HTTPS 目标走 CONNECT 隧道,HTTP 目标直接以绝对 URI 转发
|
|
7
|
-
* - socks5(h):// Socks5 代理:握手后隧道转发(支持无认证 / username-password 认证)
|
|
8
|
-
* 支持自动跟随重定向(最多 5 次)。
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from 'node:fs'
|
|
12
|
-
import http from 'node:http'
|
|
13
|
-
import net from 'node:net'
|
|
14
|
-
import https from 'node:https'
|
|
15
|
-
import tls from 'node:tls'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 建立裸 TCP 连接
|
|
19
|
-
* @param {string} host
|
|
20
|
-
* @param {number} port
|
|
21
|
-
* @param {number} timeout
|
|
22
|
-
* @returns {Promise<import('node:net').Socket>}
|
|
23
|
-
* @private
|
|
24
|
-
*/
|
|
25
|
-
function openTcp(host, port, timeout) {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
const socket = net.connect({ host, port })
|
|
28
|
-
socket.setTimeout(timeout)
|
|
29
|
-
socket.setNoDelay(true)
|
|
30
|
-
socket.once('connect', () => {
|
|
31
|
-
socket.setTimeout(0)
|
|
32
|
-
resolve(socket)
|
|
33
|
-
})
|
|
34
|
-
socket.once('error', reject)
|
|
35
|
-
socket.once('timeout', () => {
|
|
36
|
-
socket.destroy()
|
|
37
|
-
reject(new Error('tcp connect timeout'))
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 对已连接的 socket 执行 TLS 握手
|
|
44
|
-
* @param {import('node:net').Socket} socket
|
|
45
|
-
* @param {string} host - SNI
|
|
46
|
-
* @param {number} timeout
|
|
47
|
-
* @returns {Promise<import('node:tls').TLSSocket>}
|
|
48
|
-
* @private
|
|
49
|
-
*/
|
|
50
|
-
function wrapTls(socket, host, timeout) {
|
|
51
|
-
return new Promise((resolve, reject) => {
|
|
52
|
-
const tlsSocket = tls.connect({ socket, servername: host, timeout }, () => {
|
|
53
|
-
tlsSocket.setTimeout(0)
|
|
54
|
-
resolve(tlsSocket)
|
|
55
|
-
})
|
|
56
|
-
tlsSocket.once('error', (err) => {
|
|
57
|
-
socket.destroy()
|
|
58
|
-
reject(err)
|
|
59
|
-
})
|
|
60
|
-
tlsSocket.once('timeout', () => {
|
|
61
|
-
tlsSocket.destroy()
|
|
62
|
-
reject(new Error('tls handshake timeout'))
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 通过 HTTP 代理建立 CONNECT 隧道
|
|
69
|
-
* @param {URL} proxyUrl
|
|
70
|
-
* @param {string} host
|
|
71
|
-
* @param {number} port
|
|
72
|
-
* @param {number} timeout
|
|
73
|
-
* @returns {Promise<import('node:net').Socket>}
|
|
74
|
-
* @private
|
|
75
|
-
*/
|
|
76
|
-
function httpConnect(proxyUrl, host, port, timeout) {
|
|
77
|
-
return new Promise((resolve, reject) => {
|
|
78
|
-
const req = http.request({
|
|
79
|
-
host: proxyUrl.hostname,
|
|
80
|
-
port: proxyUrl.port || 80,
|
|
81
|
-
method: 'CONNECT',
|
|
82
|
-
path: `${host}:${port}`,
|
|
83
|
-
timeout
|
|
84
|
-
})
|
|
85
|
-
req.once('connect', (res, socket) => {
|
|
86
|
-
if (res.statusCode !== 200) {
|
|
87
|
-
socket.destroy()
|
|
88
|
-
reject(new Error(`CONNECT failed: ${res.statusCode}`))
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
socket.setTimeout(0)
|
|
92
|
-
resolve(socket)
|
|
93
|
-
})
|
|
94
|
-
req.once('error', reject)
|
|
95
|
-
req.once('timeout', () => {
|
|
96
|
-
req.destroy()
|
|
97
|
-
reject(new Error('CONNECT timeout'))
|
|
98
|
-
})
|
|
99
|
-
req.end()
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 通过 Socks5 代理建立隧道(RFC 1928)
|
|
105
|
-
*
|
|
106
|
-
* 支持三种地址类型(DOMAIN/IPv4/IPv6)与两种认证方式:
|
|
107
|
-
* 无认证(0x00)、username/password(0x02,RFC 1929)。
|
|
108
|
-
* 代理 URL 带用户名密码时自动使用 0x02 认证。
|
|
109
|
-
*
|
|
110
|
-
* @param {URL} proxyUrl - socks5://host:port 或 socks5://user:pass@host:port
|
|
111
|
-
* @param {string} host - 目标主机名
|
|
112
|
-
* @param {number} port - 目标端口
|
|
113
|
-
* @param {number} timeout
|
|
114
|
-
* @returns {Promise<import('node:net').Socket>}
|
|
115
|
-
* @private
|
|
116
|
-
*/
|
|
117
|
-
function socks5Connect(proxyUrl, host, port, timeout) {
|
|
118
|
-
const auth = proxyUrl.username
|
|
119
|
-
? {
|
|
120
|
-
user: decodeURIComponent(proxyUrl.username),
|
|
121
|
-
pass: decodeURIComponent(proxyUrl.password || '')
|
|
122
|
-
}
|
|
123
|
-
: null
|
|
124
|
-
|
|
125
|
-
return openTcp(proxyUrl.hostname, proxyUrl.port || 1080, timeout).then(
|
|
126
|
-
(socket) =>
|
|
127
|
-
new Promise((resolve, reject) => {
|
|
128
|
-
const fail = (err) => {
|
|
129
|
-
socket.destroy()
|
|
130
|
-
reject(err instanceof Error ? err : new Error(String(err)))
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const methods = auth ? [0x00, 0x02] : [0x00]
|
|
134
|
-
socket.write(Buffer.from([0x05, methods.length, ...methods]))
|
|
135
|
-
|
|
136
|
-
let stage = 'greeting'
|
|
137
|
-
let buf = Buffer.alloc(0)
|
|
138
|
-
|
|
139
|
-
const sendConnect = () => {
|
|
140
|
-
stage = 'connect'
|
|
141
|
-
const hostBuf = Buffer.from(host, 'utf8')
|
|
142
|
-
const req = Buffer.alloc(7 + hostBuf.length)
|
|
143
|
-
req[0] = 0x05
|
|
144
|
-
req[1] = 0x01
|
|
145
|
-
req[2] = 0x00
|
|
146
|
-
req[3] = 0x03
|
|
147
|
-
req[4] = hostBuf.length
|
|
148
|
-
hostBuf.copy(req, 5)
|
|
149
|
-
req.writeUInt16BE(port, 5 + hostBuf.length)
|
|
150
|
-
socket.write(req)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const onData = (chunk) => {
|
|
154
|
-
buf = Buffer.concat([buf, chunk])
|
|
155
|
-
try {
|
|
156
|
-
if (stage === 'greeting') {
|
|
157
|
-
if (buf.length < 2) return
|
|
158
|
-
const method = buf.readUInt8(1)
|
|
159
|
-
buf = buf.subarray(2)
|
|
160
|
-
if (method === 0xff) {
|
|
161
|
-
throw new Error('socks5: no acceptable auth method')
|
|
162
|
-
}
|
|
163
|
-
if (method === 0x02) {
|
|
164
|
-
if (!auth) {
|
|
165
|
-
throw new Error('socks5: server requires auth, but none provided')
|
|
166
|
-
}
|
|
167
|
-
const userBuf = Buffer.from(auth.user, 'utf8')
|
|
168
|
-
const passBuf = Buffer.from(auth.pass, 'utf8')
|
|
169
|
-
const req = Buffer.alloc(3 + userBuf.length + passBuf.length)
|
|
170
|
-
req[0] = 0x01
|
|
171
|
-
req[1] = userBuf.length
|
|
172
|
-
userBuf.copy(req, 2)
|
|
173
|
-
req[2 + userBuf.length] = passBuf.length
|
|
174
|
-
passBuf.copy(req, 3 + userBuf.length)
|
|
175
|
-
socket.write(req)
|
|
176
|
-
stage = 'auth'
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
sendConnect()
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
if (stage === 'auth') {
|
|
183
|
-
if (buf.length < 2) return
|
|
184
|
-
if (buf.readUInt8(1) !== 0x00) {
|
|
185
|
-
throw new Error('socks5: username/password auth failed')
|
|
186
|
-
}
|
|
187
|
-
buf = buf.subarray(2)
|
|
188
|
-
sendConnect()
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
if (stage === 'connect') {
|
|
192
|
-
if (buf.length < 4) return
|
|
193
|
-
if (buf.readUInt8(0) !== 0x05) {
|
|
194
|
-
throw new Error('socks5: invalid reply version')
|
|
195
|
-
}
|
|
196
|
-
const rep = buf.readUInt8(1)
|
|
197
|
-
if (rep !== 0x00) {
|
|
198
|
-
throw new Error(`socks5: connect failed with code ${rep}`)
|
|
199
|
-
}
|
|
200
|
-
const atyp = buf.readUInt8(3)
|
|
201
|
-
let addrLen
|
|
202
|
-
if (atyp === 0x01) addrLen = 4
|
|
203
|
-
else if (atyp === 0x03) addrLen = buf.readUInt8(4) + 1
|
|
204
|
-
else if (atyp === 0x04) addrLen = 16
|
|
205
|
-
else {
|
|
206
|
-
throw new Error('socks5: invalid address type')
|
|
207
|
-
}
|
|
208
|
-
const total = 4 + addrLen + 2
|
|
209
|
-
if (buf.length < total) return
|
|
210
|
-
cleanup()
|
|
211
|
-
socket.setTimeout(0)
|
|
212
|
-
resolve(socket)
|
|
213
|
-
}
|
|
214
|
-
} catch (err) {
|
|
215
|
-
cleanup()
|
|
216
|
-
fail(err)
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const cleanup = () => {
|
|
221
|
-
socket.removeListener('data', onData)
|
|
222
|
-
socket.removeListener('error', onError)
|
|
223
|
-
socket.removeListener('timeout', onTimeout)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const onError = (err) => {
|
|
227
|
-
cleanup()
|
|
228
|
-
fail(err)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const onTimeout = () => {
|
|
232
|
-
cleanup()
|
|
233
|
-
fail(new Error('socks5: timeout'))
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
socket.setTimeout(timeout)
|
|
237
|
-
socket.on('data', onData)
|
|
238
|
-
socket.on('error', onError)
|
|
239
|
-
socket.on('timeout', onTimeout)
|
|
240
|
-
})
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* 建立到目标主机的隧道 socket(HTTPS 目标会完成 TLS 握手)
|
|
246
|
-
*
|
|
247
|
-
* @param {URL} proxyUrl - 代理 URL
|
|
248
|
-
* @param {string} scheme - 代理协议(http/https/socks5/socks5h)
|
|
249
|
-
* @param {string} host - 目标主机名
|
|
250
|
-
* @param {number} port - 目标端口
|
|
251
|
-
* @param {boolean} isHttps - 目标是否为 HTTPS
|
|
252
|
-
* @param {number} timeout - 超时(ms)
|
|
253
|
-
* @returns {Promise<import('node:net').Socket>}
|
|
254
|
-
* @private
|
|
255
|
-
*/
|
|
256
|
-
function tunnelConnect(proxyUrl, scheme, host, port, isHttps, timeout) {
|
|
257
|
-
if (scheme === 'socks5' || scheme === 'socks5h') {
|
|
258
|
-
return socks5Connect(proxyUrl, host, port, timeout).then((socket) =>
|
|
259
|
-
isHttps ? wrapTls(socket, host, timeout) : socket
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
if (scheme === 'http' || scheme === 'https') {
|
|
263
|
-
if (isHttps) {
|
|
264
|
-
return httpConnect(proxyUrl, host, port, timeout).then((socket) =>
|
|
265
|
-
wrapTls(socket, host, timeout)
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
return openTcp(proxyUrl.hostname, proxyUrl.port || 80, timeout)
|
|
269
|
-
}
|
|
270
|
-
return Promise.reject(new Error(`Unsupported proxy scheme: ${scheme}`))
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* 通过代理或直连把 URL 内容流式下载到文件
|
|
275
|
-
*
|
|
276
|
-
* @param {string} url - 请求 URL
|
|
277
|
-
* @param {string} destPath - 目标文件路径
|
|
278
|
-
* @param {{ proxy?: string, timeout?: number, onProgress?: (pct: number) => void }} [opts]
|
|
279
|
-
* @returns {Promise<void>}
|
|
280
|
-
*/
|
|
281
|
-
export async function downloadToFile(url, destPath, { proxy, timeout = 600000, onProgress } = {}) {
|
|
282
|
-
const target = new URL(url)
|
|
283
|
-
const isHttps = target.protocol === 'https:'
|
|
284
|
-
const targetPort = target.port || (isHttps ? 443 : 80)
|
|
285
|
-
|
|
286
|
-
if (!proxy) {
|
|
287
|
-
await new Promise((resolve, reject) => {
|
|
288
|
-
const proto = isHttps ? https : http
|
|
289
|
-
const req = proto.get(url, { timeout }, (res) => {
|
|
290
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
291
|
-
res.resume()
|
|
292
|
-
const next = new URL(res.headers.location, url).href
|
|
293
|
-
downloadToFile(next, destPath, { proxy, timeout, onProgress }).then(resolve, reject)
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
if (res.statusCode !== 200) {
|
|
297
|
-
reject(new Error(`HTTP ${res.statusCode}: ${url}`))
|
|
298
|
-
return
|
|
299
|
-
}
|
|
300
|
-
pipeToFile(res, destPath, url, onProgress).then(resolve, reject)
|
|
301
|
-
})
|
|
302
|
-
req.on('error', reject)
|
|
303
|
-
req.on('timeout', () => { req.destroy(); reject(new Error(`下载超时: ${url}`)) })
|
|
304
|
-
})
|
|
305
|
-
return
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const proxyUrl = new URL(proxy)
|
|
309
|
-
const scheme = proxyUrl.protocol.replace(/:$/, '')
|
|
310
|
-
|
|
311
|
-
await new Promise((resolve, reject) => {
|
|
312
|
-
tunnelConnect(proxyUrl, scheme, target.hostname, targetPort, isHttps, timeout)
|
|
313
|
-
.then((socket) => {
|
|
314
|
-
const isProxiedPlainHttp = !isHttps && (scheme === 'http' || scheme === 'https')
|
|
315
|
-
const req = http.request({
|
|
316
|
-
createConnection: () => socket,
|
|
317
|
-
method: 'GET',
|
|
318
|
-
path: isProxiedPlainHttp ? url : target.pathname + target.search,
|
|
319
|
-
headers: { Host: target.host },
|
|
320
|
-
timeout,
|
|
321
|
-
}, (res) => {
|
|
322
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
323
|
-
res.resume()
|
|
324
|
-
const next = new URL(res.headers.location, url).href
|
|
325
|
-
downloadToFile(next, destPath, { proxy, timeout, onProgress }).then(resolve, reject)
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
if (res.statusCode !== 200) {
|
|
329
|
-
reject(new Error(`HTTP ${res.statusCode}: ${url}`))
|
|
330
|
-
return
|
|
331
|
-
}
|
|
332
|
-
pipeToFile(res, destPath, url, onProgress).then(resolve, reject)
|
|
333
|
-
})
|
|
334
|
-
req.on('error', reject)
|
|
335
|
-
req.on('timeout', () => { req.destroy(); reject(new Error(`下载超时: ${url}`)) })
|
|
336
|
-
req.end()
|
|
337
|
-
})
|
|
338
|
-
.catch(reject)
|
|
339
|
-
})
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* 把响应流写入文件(附进度回调)
|
|
344
|
-
* @private
|
|
345
|
-
*/
|
|
346
|
-
function pipeToFile(res, destPath, url, onProgress) {
|
|
347
|
-
return new Promise((resolve, reject) => {
|
|
348
|
-
const file = fs.createWriteStream(destPath)
|
|
349
|
-
const total = parseInt(res.headers['content-length'] || '0', 10)
|
|
350
|
-
let written = 0
|
|
351
|
-
let lastPct = 0
|
|
352
|
-
res.on('data', (chunk) => {
|
|
353
|
-
written += chunk.length
|
|
354
|
-
if (total > 0 && onProgress) {
|
|
355
|
-
const pct = Math.floor(written * 100 / total)
|
|
356
|
-
if (pct >= lastPct + 10 || written === total) {
|
|
357
|
-
lastPct = pct
|
|
358
|
-
onProgress(pct)
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
})
|
|
362
|
-
res.pipe(file)
|
|
363
|
-
file.on('finish', () => { file.close(); resolve() })
|
|
364
|
-
file.on('error', reject)
|
|
365
|
-
res.on('error', reject)
|
|
366
|
-
})
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* 通过代理或直连获取 URL 文本内容
|
|
371
|
-
*
|
|
372
|
-
* @param {string} url - 请求 URL
|
|
373
|
-
* @param {{ proxy?: string, timeout?: number, maxRedirects?: number }} [opts] - 选项
|
|
374
|
-
* @returns {Promise<string|null>}
|
|
375
|
-
*/
|
|
376
|
-
export async function fetchText(url, { proxy, timeout = 15000, maxRedirects = 5 } = {}) {
|
|
377
|
-
if (!proxy) {
|
|
378
|
-
try {
|
|
379
|
-
const resp = await fetch(url, { signal: AbortSignal.timeout(timeout) })
|
|
380
|
-
if (!resp.ok) return null
|
|
381
|
-
return await resp.text()
|
|
382
|
-
} catch {
|
|
383
|
-
return null
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const target = new URL(url)
|
|
388
|
-
const proxyUrl = new URL(proxy)
|
|
389
|
-
const isHttps = target.protocol === 'https:'
|
|
390
|
-
const targetPort = target.port || (isHttps ? 443 : 80)
|
|
391
|
-
const scheme = proxyUrl.protocol.replace(/:$/, '')
|
|
392
|
-
|
|
393
|
-
return new Promise((resolve) => {
|
|
394
|
-
if (maxRedirects <= 0) {
|
|
395
|
-
resolve(null)
|
|
396
|
-
return
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
tunnelConnect(proxyUrl, scheme, target.hostname, targetPort, isHttps, timeout)
|
|
400
|
-
.then((socket) => {
|
|
401
|
-
// HTTP 目标走 HTTP 代理时,需要发送绝对 URI(RFC 7230 5.3.2)
|
|
402
|
-
const isProxiedPlainHttp = !isHttps && (scheme === 'http' || scheme === 'https')
|
|
403
|
-
|
|
404
|
-
const req = http.request({
|
|
405
|
-
createConnection: () => socket,
|
|
406
|
-
method: 'GET',
|
|
407
|
-
path: isProxiedPlainHttp ? url : target.pathname + target.search,
|
|
408
|
-
headers: { Host: target.host },
|
|
409
|
-
timeout
|
|
410
|
-
}, (response) => {
|
|
411
|
-
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
412
|
-
const redirectUrl = new URL(response.headers.location, url).href
|
|
413
|
-
fetchText(redirectUrl, { proxy, timeout, maxRedirects: maxRedirects - 1 }).then(resolve)
|
|
414
|
-
return
|
|
415
|
-
}
|
|
416
|
-
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
417
|
-
resolve(null)
|
|
418
|
-
return
|
|
419
|
-
}
|
|
420
|
-
let data = ''
|
|
421
|
-
response.setEncoding('utf8')
|
|
422
|
-
response.on('data', (chunk) => { data += chunk })
|
|
423
|
-
response.on('end', () => resolve(data))
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
req.on('error', () => resolve(null))
|
|
427
|
-
req.on('timeout', () => { req.destroy(); resolve(null) })
|
|
428
|
-
req.end()
|
|
429
|
-
})
|
|
430
|
-
.catch(() => resolve(null))
|
|
431
|
-
})
|
|
432
|
-
}
|