dsh-plugin-rack 0.1.3 → 0.1.5

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 (4) hide show
  1. package/README.md +24 -1
  2. package/client.js +63 -8
  3. package/index.js +36 -5
  4. package/package.json +4 -4
package/README.md CHANGED
@@ -205,10 +205,33 @@ POST /api/dsh-plugin-rack/uninstall { name }
205
205
 
206
206
  写入 `cordis.patch.yml` 是原子操作(写临时文件 + rename),只删这个插件自己创建过的覆盖项,用户手写的内容原样保留。
207
207
 
208
+ ## 网络要求
209
+
210
+ 插件本身不带代理,也不改你的代理设置:它只是**按顺序试**「环境变量代理 → 系统的代理设置 → 直连」,把一个能通的路径交给自己的下载器;真正的安装动作交给宿主自己的 pnpm,所以走哪个 registry 完全听宿主的(`pluginManager.registries()` 解析出来的那个,没有就退回 `registry.npmjs.org`)。
211
+
212
+ | 你在做的事 | 需要访问 | 没有代理能行吗 |
213
+ | --- | --- | --- |
214
+ | 检查 npm 包的更新、安装/更新 npm 包 | 宿主的 registry(国内常见是 `registry.npmmirror.com`) | **行**,镜像本来就直连可用 |
215
+ | 检查 github 安装的插件有没有新提交 | `api.github.com` | 视网络而定;被墙时会报错,其它行不受影响 |
216
+ | 更新 github 安装的插件 | `github.com`、`codeload.github.com` | 视网络而定,同上 |
217
+
218
+ 几个具体结论:
219
+
220
+ - 系统代理**关掉但注册表里还留着旧的 `ProxyServer`** 不会再把下载带偏:现在会读 `ProxyEnable`,值为 `0` 时直接跳过(这就是「tried 127.0.0.1:7900 … direct: The operation was aborted」那个报错的成因)。
221
+ - 每一条回退路线各拿一份独立的超时预算,前一条失败不会把后一条的时间耗光。
222
+ - `GITHUB_TOKEN` 可以不设。不设时按匿名额度查 public 仓库(每小时 60 次,按 IP 算);包多或频繁检查时才需要它,面板的「接口」页会显示当前是否已设置。
223
+ - 不需要联网的操作:开/关插件、自动/手动清单、看日志、卸载、热替换。断网时面板照常可用,只有「检查更新」和一键更新会失败并给出原因。
224
+
208
225
  ## 兼容性
209
226
 
210
- - DSH `>=0.2.1-rc.2 <0.2.3-0`。已实测 `0.2.1-rc.2`、`0.2.1-rc.5`、`0.2.2-rc.1`、`0.2.2-rc.2`、`0.2.2-rc.3`;
227
+ - DSH `>=0.2.1-rc.2 <0.3.0-0`。已实测 `0.2.1-rc.2`、`0.2.1-rc.5`、`0.2.2-rc.1`、`0.2.2-rc.2`、`0.2.2-rc.3`;
211
228
  名单之外的版本会明说不确定,不会假装兼容。
229
+
230
+ 这个区间怎么理解:`engines.dsh` 是**宿主安装/启用插件时检查**的硬门槛,落在区间外会直接拒绝启用。
231
+ 所以上界特意放到 `0.3.0` 之前而不是紧贴当前版本——DSH `0.2.x` 的小版本和 rc 都不会把插件锁死,
232
+ 只有跨到 `0.3` 这种可能改表面的版本才需要重新确认一次。真到了那一天,插件本身也会先报错
233
+ (缺 `pluginManager`、缺 HMR、找不到 pnpm),不会静默半坏。
234
+
212
235
  - DSH 的 Web 界面(`dsh.client.platform = web`)。
213
236
  - 需要宿主提供 `pluginManager` 服务;没有它会明确报错,而不是假装成功。
214
237
  - 热替换依赖宿主的 HMR 服务;拿不到时退回提示「重启一次生效」,**不会骗你说已经生效了**。
package/client.js CHANGED
@@ -144,6 +144,10 @@ window.__ModuleLoader__.load({
144
144
  updateNow: 'Update just this plugin',
145
145
  jobRunning: 'Host is updating in the background · {n} plugins',
146
146
  jobRollingBack: 'Rolling back — this cannot be cancelled',
147
+ jobPhaseWait: 'waiting for sessions to go idle',
148
+ jobPhaseDownload: 'downloading',
149
+ jobPhaseInstall: 'installing',
150
+ rowUpdating: 'updating',
147
151
  jobDone: 'Background update finished',
148
152
  jobFailed: 'Background update failed',
149
153
  jobInterrupted: 'The last update never finished (host restarted or interrupted)',
@@ -304,6 +308,10 @@ window.__ModuleLoader__.load({
304
308
  updateNow: '只更新这一个插件',
305
309
  jobRunning: '宿主正在后台更新 · {n} 个插件',
306
310
  jobRollingBack: '正在回滚,这一步不能取消',
311
+ jobPhaseWait: '等待会话空闲',
312
+ jobPhaseDownload: '正在下载',
313
+ jobPhaseInstall: '正在安装',
314
+ rowUpdating: '更新中',
307
315
  jobDone: '后台更新已完成',
308
316
  jobFailed: '后台更新失败',
309
317
  jobInterrupted: '上次更新没跑完(宿主重启或中断)',
@@ -690,6 +698,39 @@ function updatePageState(updates, checking) {
690
698
  return { kind: 'list', updatable: updatable, outOfRange: outOfRange };
691
699
  }
692
700
 
701
+ /**
702
+ * How many entries across both channels can be updated right now.
703
+ *
704
+ * The 更新 tab badge used to read the npm answer alone (`updates.updates`) while
705
+ * the 「全部更新」button counted the github / tarball rows too, so a panel with
706
+ * one github update showed "更新 7" next to "全部更新 (8)".
707
+ *
708
+ * @param {{rows?: Array<object>}|null} updates
709
+ * @param {{rows?: Array<object>}|null} extras
710
+ * @returns {number}
711
+ */
712
+ function updatableTotal(updates, extras) {
713
+ const countOf = (answer) => {
714
+ if (answer === null || typeof answer !== 'object' || !Array.isArray(answer.rows)) return 0;
715
+ return answer.rows.filter((row) => row !== null && typeof row === 'object' && row.hasUpdate === true).length;
716
+ };
717
+ return countOf(updates) + countOf(extras);
718
+ }
719
+
720
+ /**
721
+ * Which dictionary key describes the phase the host job is in.
722
+ *
723
+ * @param {string|null|undefined} phase
724
+ * @returns {string|null}
725
+ */
726
+ function jobPhaseKey(phase) {
727
+ if (phase === 'start') return 'jobPhaseWait';
728
+ if (phase === 'download') return 'jobPhaseDownload';
729
+ if (phase === 'install') return 'jobPhaseInstall';
730
+ if (phase === 'rollback') return 'jobRollingBack';
731
+ return null;
732
+ }
733
+
693
734
  const CSS = [
694
735
  '.pd-root{display:flex;flex-direction:column;height:100%;min-height:0;box-sizing:border-box;',
695
736
  'background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}',
@@ -924,6 +965,8 @@ function updatePageState(updates, checking) {
924
965
  '.pd-update-row{display:flex;align-items:center;gap:10px;padding:7px 10px;border-radius:9px;cursor:pointer}',
925
966
  // 已经是最新的行:压暗一档,让"需要动手的"和"不用管的"一眼分开。
926
967
  '.pd-update-row-done{opacity:.55;cursor:default}',
968
+ '.pd-update-row-busy{background:var(--dsw-alias-interactive-bg-hover);cursor:default}',
969
+ '.pd-update-busy{margin-left:auto;flex:none;border-color:var(--dsw-alias-brand-primary) transparent var(--dsw-alias-brand-primary) var(--dsw-alias-brand-primary)}',
927
970
  '.pd-update-row-done:hover{opacity:.8}',
928
971
  '.pd-update-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
929
972
  '.pd-update-name{flex:1;min-width:0;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
@@ -1821,7 +1864,7 @@ function updatePageState(updates, checking) {
1821
1864
  { key: 'plugins', title: t('tabPlugins') },
1822
1865
  { key: 'core', title: t('tabCore') },
1823
1866
  { key: 'failed', title: t('tabFailed') },
1824
- { key: 'updates', title: t('tabUpdates'), badge: updates === null ? null : updates.updates },
1867
+ { key: 'updates', title: t('tabUpdates'), badge: updates === null && extras === null ? null : updatableTotal(updates, extras) },
1825
1868
  ].map((page) => ({ ...page, rows: pageRows(page.key) }));
1826
1869
 
1827
1870
  const current = pages.find((page) => page.key === tab) || pages[0];
@@ -2473,15 +2516,23 @@ function updatePageState(updates, checking) {
2473
2516
  };
2474
2517
 
2475
2518
  const pickedNames = Object.keys(picked).filter((name) => picked[name] === true);
2476
- const updateRowNode = (row) =>
2477
- h(
2519
+ const updateRowNode = (row) => {
2520
+ // The host job runs pnpm once for the whole batch, so no row ever flips
2521
+ // its own phase to "running". Mark the names the job carries instead,
2522
+ // otherwise the banner spins while the list looks dead.
2523
+ const busyNow =
2524
+ job !== null &&
2525
+ job.running === true &&
2526
+ Array.isArray(job.names) &&
2527
+ job.names.indexOf(row.name) >= 0;
2528
+ return h(
2478
2529
  'label',
2479
- { key: row.name, className: 'pd-update-row' },
2530
+ { key: row.name, className: busyNow ? 'pd-update-row pd-update-row-busy' : 'pd-update-row' },
2480
2531
  h('input', {
2481
2532
  type: 'checkbox',
2482
2533
  className: 'pd-check',
2483
2534
  checked: picked[row.name] === true,
2484
- disabled: updatesBusy !== '',
2535
+ disabled: updatesBusy !== '' || busyNow,
2485
2536
  onChange: () =>
2486
2537
  setPicked((prev) => ({ ...prev, [row.name]: prev[row.name] !== true })),
2487
2538
  }),
@@ -2490,7 +2541,9 @@ function updatePageState(updates, checking) {
2490
2541
  typeof row.spec === 'string' && row.kind !== 'exact'
2491
2542
  ? h('code', { className: 'pd-update-spec', title: row.spec }, row.spec)
2492
2543
  : null,
2544
+ busyNow ? h('span', { className: 'pd-spin pd-update-busy', title: t('rowUpdating') }) : null,
2493
2545
  );
2546
+ };
2494
2547
 
2495
2548
  const installPanel = h(
2496
2549
  'div',
@@ -2828,9 +2881,11 @@ function updatePageState(updates, checking) {
2828
2881
  h(
2829
2882
  'span',
2830
2883
  null,
2831
- job.stage === 'rollback'
2832
- ? t('jobRollingBack')
2833
- : t('jobRunning', { n: String(Array.isArray(job.names) ? job.names.length : 0) }),
2884
+ t('jobRunning', { n: String(Array.isArray(job.names) ? job.names.length : 0) }) +
2885
+ (jobPhaseKey(job.phase) === null ? '' : ' · ' + t(jobPhaseKey(job.phase))) +
2886
+ (typeof job.startedAt === 'string' && Number.isFinite(Date.parse(job.startedAt))
2887
+ ? ' · ' + Math.max(0, Math.round((Date.now() - Date.parse(job.startedAt)) / 1000)) + 's'
2888
+ : ''),
2834
2889
  ),
2835
2890
  // 服务端判断这个任务此刻能不能取消。回滚阶段是唯一必须跑完的
2836
2891
  // 一段,client 不能靠 job.running 自己猜——那样回滚时按钮还在,
package/index.js CHANGED
@@ -385,7 +385,15 @@ function jobStatus() {
385
385
  const running = activeJob !== null && activeJob.id === job.id
386
386
  // 回滚期间不接受取消:那是唯一一段「后台必须跑完」的工作。
387
387
  const cancelable = running && job.stage !== 'rollback'
388
- return Object.assign({}, job, { running, cancelable, interrupted: !running && job.finishedAt === null })
388
+ return Object.assign({}, job, {
389
+ running,
390
+ cancelable,
391
+ // Which step the host is on right now. Without it the panel can only say
392
+ // "updating", which looks frozen during the (up to three minute) wait for
393
+ // every session to go idle.
394
+ phase: running ? activeJobStage : null,
395
+ interrupted: !running && job.finishedAt === null,
396
+ })
389
397
  }
390
398
 
391
399
  function emptyRoster() {
@@ -2625,17 +2633,36 @@ function noProxyMatch(host, raw) {
2625
2633
  return false
2626
2634
  }
2627
2635
 
2628
- /** The proxy Windows itself is configured with, when there is one. */
2636
+ /**
2637
+ * Whether Windows is actually routing through that proxy right now.
2638
+ *
2639
+ * The ProxyServer value stays in the registry after the user turns the proxy
2640
+ * off — only ProxyEnable flips. Reading ProxyServer alone therefore sends every
2641
+ * download into a dead port and burns the whole timeout budget before the direct
2642
+ * fallback even starts; that is the "tried 127.0.0.1:7900 … direct: The
2643
+ * operation was aborted" report. A missing ProxyEnable counts as enabled,
2644
+ * because older layouts only wrote ProxyServer.
2645
+ */
2646
+ function proxyEnableAllows(stdout) {
2647
+ if (typeof stdout !== 'string') return true
2648
+ const match = /ProxyEnable\s+REG_DWORD\s+0x([0-9a-fA-F]+)/.exec(stdout)
2649
+ if (match === null) return true
2650
+ return Number.parseInt(match[1], 16) !== 0
2651
+ }
2652
+
2653
+ /** The proxy Windows itself is configured with, when it is switched on. */
2629
2654
  function systemProxyRoute() {
2630
2655
  if (process.platform !== 'win32') return null
2631
2656
  try {
2632
2657
  const key = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'
2633
- const query = spawnSync('reg', ['query', key, '/v', 'ProxyServer'], {
2658
+ // One query for the whole key: ProxyEnable only exists as a sibling value.
2659
+ const query = spawnSync('reg', ['query', key], {
2634
2660
  encoding: 'utf8',
2635
2661
  timeout: 3000,
2636
2662
  windowsHide: true,
2637
2663
  })
2638
2664
  if (query.status !== 0 || typeof query.stdout !== 'string') return null
2665
+ if (!proxyEnableAllows(query.stdout)) return null
2639
2666
  const match = /ProxyServer\s+REG_SZ\s+(.+)/.exec(query.stdout)
2640
2667
  if (match === null) return null
2641
2668
  const parts = match[1]
@@ -3052,12 +3079,16 @@ export async function fetchAny(url, init) {
3052
3079
  return await fetch(url, init)
3053
3080
  } catch (error) {
3054
3081
  const target = new URL(url)
3055
- const signal = init !== undefined && init !== null ? init.signal : undefined
3056
3082
  const routes = downloadRoutes(target)
3057
3083
  const failures = []
3058
3084
  for (const route of routes) {
3059
3085
  try {
3060
- return await readResponseLike(target, route.route, signal)
3086
+ // A fresh budget per route: the caller's signal is usually a
3087
+ // AbortSignal.timeout(...) that the failed fetch has already spent, so
3088
+ // passing it on would abort every fallback instantly and report "The
3089
+ // operation was aborted" for a route that was never actually tried.
3090
+ const fallback = AbortSignal.timeout(REGISTRY_TIMEOUT)
3091
+ return await readResponseLike(target, route.route, fallback)
3061
3092
  } catch (next) {
3062
3093
  failures.push(route.label + ': ' + (next instanceof Error ? next.message : String(next)))
3063
3094
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-rack",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "DSH 插件管理:所有插件一屏看完,一个开关直接开关,更新完自动热替换不用重启。/ Plugin rack for DSH: every plugin in one list with a power switch, an auto/manual flag that scopes the one-click bulk actions, and hot-swapped updates that need no restart.",
6
6
  "author": "Jaside (LZM-P)",
@@ -47,8 +47,8 @@
47
47
  "test": "node scripts/build-client.mjs --check && node test/self-check.mjs && node test/behaviour.mjs && node test/spec.mjs && node test/client.mjs && node test/job-background.mjs && node test/download.mjs && node test/pnpm-command.mjs",
48
48
  "lint": "eslint .",
49
49
  "types": "tsc -p tsconfig.json",
50
- "publish": "node scripts/publish.mjs",
51
- "build": "node scripts/build-client.mjs"
50
+ "build": "node scripts/build-client.mjs",
51
+ "release": "node scripts/publish.mjs"
52
52
  },
53
53
  "dsh": {
54
54
  "manifestVersion": 1,
@@ -65,7 +65,7 @@
65
65
  ]
66
66
  },
67
67
  "engines": {
68
- "dsh": ">=0.2.1-rc.2 <0.2.3-0"
68
+ "dsh": ">=0.2.1-rc.2 <0.3.0-0"
69
69
  },
70
70
  "compatibility": {
71
71
  "dshReleases": {