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.
Files changed (2) hide show
  1. package/lib/download.js +34 -9
  2. 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
- function curlAttempt(url, part, previous, options) {
39
- const headersFile = `${part}.headers`;
40
- rmSync(headersFile, { force: true });
41
- const offset = size(part);
42
- const program = process.platform === 'win32' ? join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'curl.exe') : 'curl';
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 (offset) args.push('--continue-at', String(offset), '--header', `If-Range: ${previous.validator}`);
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
- reject(new Error(response.status >= 400 ? `下载服务返回 HTTP ${response.status}。` : `下载连接中断或超时(curl ${code ?? 'stopped'})。`));
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.6",
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",