dsh-status-bar-plugin 0.2.6 → 0.2.7
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/lib/download.js +34 -9
- package/package.json +1 -1
package/lib/download.js
CHANGED
|
@@ -19,6 +19,11 @@ export function delay(ms, signal) {
|
|
|
19
19
|
const size = path => { try { return statSync(path).size; } catch { return 0; } };
|
|
20
20
|
const json = path => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return {}; } };
|
|
21
21
|
function reset(part) { for (const path of [part, `${part}.json`]) rmSync(path, { force: true }); }
|
|
22
|
+
const TLS_CODES = new Set([35, 51, 60]);
|
|
23
|
+
function tlsHint(code) {
|
|
24
|
+
if (!TLS_CODES.has(code)) return undefined;
|
|
25
|
+
return `与下载服务器建立安全连接失败(curl ${code}),通常是系统时间、代理或证书设置导致。`;
|
|
26
|
+
}
|
|
22
27
|
function validator(headers) {
|
|
23
28
|
const tag = headers.etag;
|
|
24
29
|
return typeof tag === 'string' && !tag.startsWith('W/') ? tag : headers['last-modified'];
|
|
@@ -35,17 +40,29 @@ function metadata(headers, source) {
|
|
|
35
40
|
const range = String(headers['content-range'] ?? '').match(/^bytes \d+-\d+\/(\d+)$/);
|
|
36
41
|
return { source, validator: validator(headers), total: range ? Number(range[1]) : Number(headers['content-length']) || 0 };
|
|
37
42
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
/** Schannel refuses the handshake when it cannot reach a revocation endpoint
|
|
44
|
+
* (`CRYPT_E_NO_REVOCATION_CHECK`, curl 35), which is the norm on locked-down
|
|
45
|
+
* networks. Best-effort keeps the check when it works and tolerates the failure.
|
|
46
|
+
* Curl older than 7.70 does not know the flag and exits 2; retry without it. */
|
|
47
|
+
export function curlArgs(url, part, headersFile, previous, options, quirks = {}) {
|
|
48
|
+
const schannel = quirks.schannel ?? process.platform === 'win32';
|
|
43
49
|
const args = ['--silent', '--show-error', '--location', '--fail', '--connect-timeout', '15', '--max-time', String(options.timeoutMs / 1000),
|
|
44
50
|
'--speed-limit', '1', '--speed-time', String(Math.max(1, Math.ceil(options.idleMs / 1000))), '--proto-redir', '=https,http',
|
|
45
51
|
'--dump-header', headersFile, '--output', part];
|
|
52
|
+
if (schannel && quirks.revoke !== false) args.push('--ssl-revoke-best-effort');
|
|
46
53
|
if (new URL(url).protocol === 'https:') args.push('--proto', '=https', '--proto-redir', '=https');
|
|
47
|
-
if (
|
|
54
|
+
if (previous.validator) args.push('--header', `If-Range: ${previous.validator}`);
|
|
55
|
+
const offset = size(part);
|
|
56
|
+
if (offset) args.push('--continue-at', String(offset));
|
|
48
57
|
args.push('--url', url);
|
|
58
|
+
return args;
|
|
59
|
+
}
|
|
60
|
+
function curlAttempt(url, part, previous, options) {
|
|
61
|
+
const headersFile = `${part}.headers`;
|
|
62
|
+
rmSync(headersFile, { force: true });
|
|
63
|
+
const offset = size(part);
|
|
64
|
+
const program = process.platform === 'win32' ? join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'curl.exe') : 'curl';
|
|
65
|
+
const args = curlArgs(url, part, headersFile, previous, options, { revoke: options.revoke });
|
|
49
66
|
return new Promise((resolve, reject) => {
|
|
50
67
|
let child;
|
|
51
68
|
const progress = () => {
|
|
@@ -73,7 +90,9 @@ function curlAttempt(url, part, previous, options) {
|
|
|
73
90
|
const completed = response.status === 416 && String(response.headers?.['content-range']).match(/^bytes \*\/(\d+)$/);
|
|
74
91
|
if (completed && Number(completed[1]) === offset && previous.total === offset) { resolve(); return; }
|
|
75
92
|
if (code === 33 || response.status === 416) reset(part);
|
|
76
|
-
|
|
93
|
+
const failure = new Error(response.status >= 400 ? `下载服务返回 HTTP ${response.status}。` : tlsHint(code) ?? `下载连接中断或超时(curl ${code ?? 'stopped'})。`);
|
|
94
|
+
if (typeof code === 'number') failure.curlCode = code;
|
|
95
|
+
reject(failure);
|
|
77
96
|
} catch (error) { reject(error); }
|
|
78
97
|
finally { rmSync(headersFile, { force: true }); }
|
|
79
98
|
});
|
|
@@ -123,7 +142,7 @@ export async function downloadArchive(url, part, { signal, onProgress = () => {}
|
|
|
123
142
|
const parsed = new URL(url);
|
|
124
143
|
if (parsed.username || parsed.password || !['https:', 'http:'].includes(parsed.protocol) || parsed.protocol === 'http:' && !['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname)) throw new Error('运行时地址必须使用 HTTPS。');
|
|
125
144
|
const source = createHash('sha256').update(url).digest('hex');
|
|
126
|
-
let useNode = backend === 'node'; let lastError;
|
|
145
|
+
let useNode = backend === 'node'; let revoke = true; let lastError;
|
|
127
146
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
128
147
|
checkAbort(signal);
|
|
129
148
|
const previous = json(`${part}.json`);
|
|
@@ -131,7 +150,7 @@ export async function downloadArchive(url, part, { signal, onProgress = () => {}
|
|
|
131
150
|
if (existsSync(part) && previous.source === source && previous.total > 0 && size(part) === previous.total) return;
|
|
132
151
|
onProgress({ phase: 'downloading', received: size(part), total: previous.total || 0, attempt, message: '正在下载桌面运行时,完成后自动打开悬浮窗。' });
|
|
133
152
|
let reported = 0;
|
|
134
|
-
const options = { source, signal, idleMs, timeoutMs, onProgress: progress => {
|
|
153
|
+
const options = { source, signal, idleMs, timeoutMs, revoke, onProgress: progress => {
|
|
135
154
|
const now = Date.now(); if (now - reported < 250 && progress.received !== progress.total) return;
|
|
136
155
|
reported = now; onProgress({ phase: 'downloading', attempt, ...progress });
|
|
137
156
|
} };
|
|
@@ -143,6 +162,12 @@ export async function downloadArchive(url, part, { signal, onProgress = () => {}
|
|
|
143
162
|
checkAbort(signal); return;
|
|
144
163
|
} catch (error) {
|
|
145
164
|
checkAbort(signal); lastError = error;
|
|
165
|
+
// An old curl without --ssl-revoke-best-effort exits 2; retry the same
|
|
166
|
+
// attempt without the flag instead of failing the whole download.
|
|
167
|
+
if (!useNode && revoke && error.curlCode === 2) { revoke = false; attempt--; continue; }
|
|
168
|
+
// A TLS failure that survives the flag is worth one Node attempt, since
|
|
169
|
+
// Node validates against its own trust store instead of Schannel's.
|
|
170
|
+
if (!useNode && TLS_CODES.has(error.curlCode)) { useNode = true; attempt--; continue; }
|
|
146
171
|
if (attempt < attempts) {
|
|
147
172
|
onProgress({ phase: 'retrying', message: `下载中断,${attempt} 秒后重试;可验证的已下载部分会保留。`, attempt });
|
|
148
173
|
await delay(attempt * 1000, signal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-status-bar-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Floating glass status bar for DeepSeek Harness: a frameless always-on-top window that shows session progress, pending approvals and questions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wfz2006",
|