dsh-status-bar-plugin 0.2.6 → 0.2.8

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 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/lib/runtime.js CHANGED
@@ -25,6 +25,15 @@ export function runtimeUrl(config, root) {
25
25
  const url = explicit ?? advertised ?? DEFAULT_RUNTIME_URL;
26
26
  return !explicit && url === DEFAULT_RUNTIME_URL ? `https://github.com/wfz2006/dsh-status-bar/releases/download/v${packageVersion(root)}/dsh-status-bar.tgz` : url;
27
27
  }
28
+ /** The npm-hosted runtime is the primary source; the release asset stays as a
29
+ * fallback for networks where one of the two is unreachable. An explicitly
30
+ * configured address is used alone, so operators keep full control. */
31
+ export function runtimeUrls(config, root) {
32
+ const primary = runtimeUrl(config, root);
33
+ if (config.downloadUrl ?? process.env.DSH_STATUS_BAR_RUNTIME_URL ?? process.env.DSH_GLASS_RUNTIME_URL) return [primary];
34
+ const fallback = `https://github.com/wfz2006/dsh-status-bar/releases/download/v${packageVersion(root)}/dsh-status-bar.tgz`;
35
+ return primary === fallback ? [primary] : [primary, fallback];
36
+ }
28
37
  function binary(dist) {
29
38
  const name = process.platform === 'win32' ? 'electron.exe' : process.platform === 'darwin' ? join('Electron.app', 'Contents', 'MacOS', 'Electron') : 'electron';
30
39
  return firstFile([join(dist, name)]);
@@ -90,6 +99,22 @@ function removeOwned(path, root) {
90
99
  if (dirname(path) !== dirname(root) || !basename(path).startsWith(`${basename(root)}.`)) throw new Error('缓存临时路径无效。');
91
100
  rmSync(path, { recursive: true, force: true });
92
101
  }
102
+ async function downloadWithFallback(config, pluginRoot, part, { signal, onProgress }) {
103
+ const urls = runtimeUrls(config, pluginRoot);
104
+ let lastError;
105
+ for (const url of urls) {
106
+ try {
107
+ await downloadArchive(url, part, { signal, onProgress });
108
+ return;
109
+ } catch (error) {
110
+ if (error?.name === 'AbortError') throw error;
111
+ checkAbort(signal);
112
+ lastError = error;
113
+ if (url !== urls.at(-1)) onProgress({ phase: 'downloading', message: '这个下载源不可用,正在改用备用源。' });
114
+ }
115
+ }
116
+ throw lastError ?? new Error('运行时下载失败。');
117
+ }
93
118
  export async function ensureRuntime(config, pluginRoot, { signal, onProgress = () => {} } = {}) {
94
119
  const version = packageVersion(pluginRoot); const base = cacheBase(config); const root = join(base, `runtime-${version}`);
95
120
  const cached = cachedRuntime(root, version); if (cached) return cached;
@@ -101,7 +126,7 @@ export async function ensureRuntime(config, pluginRoot, { signal, onProgress = (
101
126
  // The exclusive lock proves that no other preparer owns these generated
102
127
  // staging folders. An abrupt Host exit must not leave one unpack per retry.
103
128
  for (const name of readdirSync(base)) if (name.startsWith(`${basename(root)}.staging-`) && /^[0-9a-f-]{36}$/.test(name.slice(`${basename(root)}.staging-`.length))) removeOwned(join(base, name), root);
104
- await downloadArchive(runtimeUrl(config, pluginRoot), part, { signal, onProgress }); checkAbort(signal);
129
+ await downloadWithFallback(config, pluginRoot, part, { signal, onProgress }); checkAbort(signal);
105
130
  onProgress({ phase: 'unpacking', message: '下载完成,正在校验并解压运行时。' });
106
131
  let entries;
107
132
  try { entries = await run('tar', ['-tvzf', part], signal, true); }
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.8",
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",
@@ -44,7 +44,7 @@
44
44
  "patch": "./cordis.patch.yml"
45
45
  },
46
46
  "statusBar": {
47
- "runtime": "https://github.com/wfz2006/dsh-status-bar/releases/latest/download/dsh-status-bar.tgz"
47
+ "runtime": "https://registry.npmjs.org/dsh-status-bar-runtime/-/dsh-status-bar-runtime-0.2.8.tgz"
48
48
  }
49
49
  }
50
50
  }