dsh-plugin-rack 0.1.4 → 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.
- package/README.md +24 -1
- package/client.js +63 -8
- package/index.js +36 -5
- package/package.json +2 -2
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
2832
|
-
? t(
|
|
2833
|
-
|
|
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, {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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)",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
]
|
|
66
66
|
},
|
|
67
67
|
"engines": {
|
|
68
|
-
"dsh": ">=0.2.1-rc.2 <0.
|
|
68
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0-0"
|
|
69
69
|
},
|
|
70
70
|
"compatibility": {
|
|
71
71
|
"dshReleases": {
|