dsh-plugin-rack 0.1.1 → 0.1.3
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 +35 -9
- package/client.js +191 -84
- package/index.js +61 -51
- package/lib/spec.js +285 -0
- package/package.json +17 -8
- package/test/behaviour.mjs +0 -549
- package/test/download.mjs +0 -221
- package/test/job-background.mjs +0 -205
- package/test/pnpm-command.mjs +0 -49
- package/test/self-check.mjs +0 -124
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-plugin-
|
|
1
|
+
# dsh-plugin-rack · 插件管理台
|
|
2
2
|
|
|
3
3
|
DSH(DeepSeek Harness)的插件面板:**所有插件一屏看完,一个开关直接开关,更新完不用重启。**
|
|
4
4
|
|
|
@@ -37,6 +37,7 @@ DSH(DeepSeek Harness)的插件面板:**所有插件一屏看完,一个
|
|
|
37
37
|
### 更新
|
|
38
38
|
|
|
39
39
|
- 「检查更新」把 profile 里按精确版本锁定的依赖逐个查 registry 最新版,有新的直接在列表行里标出来:`⬆ v0.1.29 ➡ v0.1.30 npm`,**点一下只更新那一个插件**。
|
|
40
|
+
- **尊重你写的版本范围**:`package.json` 里锁死精确版本(`1.2.3`)时照旧只看那一个版本;写成 `^1.2.3` / `~1.2.3` / `1.2` / `1.x` / `>=1.2.3` / `>1.2.3` / `<2.0.0` / `1.2.3 - 2.0.0` / `^1 || ^2` 时,只有落在范围内的新版才算「可更新」。超出范围的新版会单独列在下方,标「超出声明范围」并附上你写的那段 spec —— 不会被批量更新悄悄顶到下一个大版本。`link:` / `file:` / `github:` / tarball 这些不可比的范围仍走「其他安装方式」,不参与版本更新。
|
|
40
41
|
- 更新由宿主执行:**点完就可以关掉面板**,pnpm 照样跑完。重新打开面板时,顶部会显示「后台更新中 · N 个插件」,跑完了补一条结果提示。不用守着这个界面,也不用去翻日志。卡住了可以点横幅右边的「取消」。
|
|
41
42
|
- **会话正忙时也不白点**:需要下载的更新会先把附件取下来、核对 sha256,然后排队;宿主每 10 秒看一眼,等所有会话都空了就自己装上(下载过的东西不会重下)。排队期间横幅显示「已排队…」。
|
|
42
43
|
- **网络不用你配**:下载按这个顺序自动选路,`config.json` 里明确指定的代理 → 环境变量(`HTTPS_PROXY` / `ALL_PROXY`,`NO_PROXY` 会照规矩跳过)→ 系统代理(Windows 的 Internet 设置)→ 直连;一条走不通就换下一条,不会因为代理没开就整个更新失败。HTTP 代理和 SOCKS5 都支持,带用户名密码也行,查 GitHub 接口的部分同样走这些代理。
|
|
@@ -48,7 +49,7 @@ DSH(DeepSeek Harness)的插件面板:**所有插件一屏看完,一个
|
|
|
48
49
|
- 能识别 **GitHub 直装 / npm 镜像 tar 包 / 本地文件包 / 本地软链** 这四种装法,并在同一个列表里标出各自来源;本地软链没有「升级」这回事,不会被勾选。
|
|
49
50
|
- registry 取 profile 自己的配置(`pluginManager.registries()`),不强行走 npmjs;装了 `GITHUB_TOKEN` 的话查 GitHub 版本会用它提额。
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
## 安装与卸载
|
|
52
53
|
|
|
53
54
|
- 输入包名先「检查」:**只做预检,什么都不动**。它会告诉你这是不是个 DSH 插件包、版本多少、干什么用的。像 `is-odd` 这种普通 npm 包会被拦下来,因为它没声明 `dsh.bundle`,装了也不会被加载。
|
|
54
55
|
- 确认了再「安装」,走宿主官方的插件管理接口。
|
|
@@ -82,13 +83,13 @@ node test/self-check.mjs
|
|
|
82
83
|
**① 从 GitHub 装(推荐,不依赖 npm)**
|
|
83
84
|
|
|
84
85
|
```bash
|
|
85
|
-
dsh plugin add github:LZM-P/dsh-plugin-
|
|
86
|
+
dsh plugin add github:LZM-P/dsh-plugin-rack
|
|
86
87
|
```
|
|
87
88
|
|
|
88
89
|
**② 从 npm 装(发布后可用)**
|
|
89
90
|
|
|
90
91
|
```bash
|
|
91
|
-
dsh plugin add dsh-plugin-
|
|
92
|
+
dsh plugin add dsh-plugin-rack
|
|
92
93
|
```
|
|
93
94
|
|
|
94
95
|
**③ 在插件管理界面里点「安装」**,把上面任意一条的 spec 填进去。
|
|
@@ -96,7 +97,7 @@ dsh plugin add dsh-plugin-dashboard
|
|
|
96
97
|
装好后侧边栏会出现「插件管理」。
|
|
97
98
|
|
|
98
99
|
> 如果宿主版本不认 `github:` 前缀,改用 tarball 地址:
|
|
99
|
-
> `dsh plugin add https://github.com/LZM-P/dsh-plugin-
|
|
100
|
+
> `dsh plugin add https://github.com/LZM-P/dsh-plugin-rack/archive/refs/heads/main.tar.gz`
|
|
100
101
|
|
|
101
102
|
## 数据放在哪
|
|
102
103
|
|
|
@@ -124,11 +125,21 @@ dsh plugin add dsh-plugin-dashboard
|
|
|
124
125
|
| 文件 | 作用 |
|
|
125
126
|
| --- | --- |
|
|
126
127
|
| `index.js` | 宿主端:注册 `/api/dsh-plugin-rack` 前缀路由,负责读取插件状态、批量操作、更新与安装卸载 |
|
|
127
|
-
| `client.js` | 客户端:侧边栏图标 + `main`
|
|
128
|
-
| `
|
|
128
|
+
| `client.js` | 客户端:侧边栏图标 + `main` 面板的界面。**生成文件**,由 `src/client/*.js` 拼装(`npm run build`) |
|
|
129
|
+
| `src/client/i18n.js` | 中英词条表与插值(纯函数,可单独测) |
|
|
130
|
+
| `src/client/view.js` | 面板的判定逻辑:失败归类、勾选分流、报告合并、行尾徽章、更新页状态 |
|
|
131
|
+
| `src/client/style.js` | 面板样式表 |
|
|
132
|
+
| `src/client/panel.js` | 面板组件本身(React `h`,无构建期依赖) |
|
|
133
|
+
| `scripts/build-client.mjs` | 把上面几块拼成 `client.js`;`--check` 供 CI 判断是否过期 |
|
|
134
|
+
| `lib/spec.js` | 版本范围算术(精确 / `^` / `~` / `>=` / `<` / 通配 / 连字符 / `||` 并集),纯函数,`index.js` 与测试共用同一份 |
|
|
135
|
+
| `cordis.patch.yml` | bundle 补丁,插入 `plugin-rack` 这一行 |
|
|
129
136
|
| `locale/{en,zh}.json` | 插件名与描述(插件管理页展示用) |
|
|
130
137
|
| `icon.svg` | 插件图标 |
|
|
131
138
|
| `test/self-check.mjs` | 自检脚本:词条对照、路由对照、hook 顺序、调试残留 |
|
|
139
|
+
| `test/spec.mjs` | 范围算术单测:`^0.2.3` 不含 `0.3.0`、`~1.2.3` 不含 `1.3.0`、预发布版不穿越等边界 |
|
|
140
|
+
| `test/*.mjs` | 行为 / 后台作业 / 下载 / pnpm 启动四套测试 |
|
|
141
|
+
| `scripts/publish.mjs` | 发布脚本:token 只从 `NPM_TOKEN` 读,版本重复直接拒绝 |
|
|
142
|
+
| `eslint.config.js` / `tsconfig.json` | lint 与 `tsc --checkJs`(类型只覆盖 `lib/`) |
|
|
132
143
|
|
|
133
144
|
## 接口
|
|
134
145
|
|
|
@@ -178,7 +189,7 @@ POST /api/dsh-plugin-rack/uninstall { name }
|
|
|
178
189
|
|
|
179
190
|
1. **网络暴露面**:API 只在 `127.0.0.1` / `localhost` / `::1` 上可达。宿主的 web server 本来就只绑 loopback,所以外网碰不到这个端口。
|
|
180
191
|
2. **Host / Origin 校验**:每个请求都先过两道闸:Host 头必须指向 loopback,Origin 头(如果存在)也必须解析到 loopback。这挡的是同机浏览器里的恶意网页:它无法用 `<img>` 或 `fetch` 跨站调用你的 API(DNS rebinding / CSRF 都被这两道闸拦下)。
|
|
181
|
-
2b. **写操作还要求"你确实是从面板来的"**:`POST` 除了要 `application/json`,还必须带 `Origin`、或 `Sec-Fetch-Site: same-origin`、或显式加上 `x-dsh-plugin-
|
|
192
|
+
2b. **写操作还要求"你确实是从面板来的"**:`POST` 除了要 `application/json`,还必须带 `Origin`、或 `Sec-Fetch-Site: same-origin`、或显式加上 `x-dsh-plugin-rack: 1` 请求头。
|
|
182
193
|
|
|
183
194
|
这道门挡的是**无意的跨站 POST**(比如别处一个表单顺手打过来、或者某个页面被 XSS 后盲发请求),它对「完全不带 Origin」的请求本来是不设防的,而 `curl -X POST` 正好属于这一类。
|
|
184
195
|
|
|
@@ -186,7 +197,7 @@ POST /api/dsh-plugin-rack/uninstall { name }
|
|
|
186
197
|
```bash
|
|
187
198
|
curl -X POST http://127.0.0.1:9546/api/dsh-plugin-rack/bulk \
|
|
188
199
|
-H 'content-type: application/json' \
|
|
189
|
-
-H 'x-dsh-plugin-
|
|
200
|
+
-H 'x-dsh-plugin-rack: 1' \
|
|
190
201
|
-d '{"enabled":false,"scope":"installed"}'
|
|
191
202
|
```
|
|
192
203
|
3. **卸载白名单**:界面上的「卸载」按钮不是对所有行都显示。只有宿主 `pluginManager` 服务明确标记为 `removable` 的包才会出现按钮。核心组件、面板自己、harness 命脉都**永远不会**出现在可卸载列表里,所以误点卸载也不会把系统拆坏。
|
|
@@ -196,10 +207,25 @@ POST /api/dsh-plugin-rack/uninstall { name }
|
|
|
196
207
|
|
|
197
208
|
## 兼容性
|
|
198
209
|
|
|
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`;
|
|
211
|
+
名单之外的版本会明说不确定,不会假装兼容。
|
|
199
212
|
- DSH 的 Web 界面(`dsh.client.platform = web`)。
|
|
200
213
|
- 需要宿主提供 `pluginManager` 服务;没有它会明确报错,而不是假装成功。
|
|
201
214
|
- 热替换依赖宿主的 HMR 服务;拿不到时退回提示「重启一次生效」,**不会骗你说已经生效了**。
|
|
202
215
|
|
|
216
|
+
## 开发
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
npm ci # 只装 eslint + typescript(开发依赖,发布包不含 node_modules)
|
|
220
|
+
npm test # 六套:自检 / 行为 / 范围算术 / 后台作业 / 下载 / pnpm 启动
|
|
221
|
+
npm run lint # eslint(no-dupe-keys、no-undef 在这套配置下抓出过两处真 bug)
|
|
222
|
+
npm run types # tsc --checkJs,只覆盖 lib/(JSDoc 当类型,不引入构建步骤)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- 范围算术只有一份实现:`lib/spec.js`。`index.js` import 它,`test/spec.mjs` 直接跑它,所以「测试通过的是一套逻辑、线上跑的是另一套」不会发生。
|
|
226
|
+
- CI 在 Windows 上跑 Node 20 与 22;另有一格把 `npm pack` 出来的 tarball 解压再跑一遍测试——`files` 里漏了哪个文件,本地是发现不了的。
|
|
227
|
+
- 发布:`NPM_TOKEN=… npm run publish`。token 只从环境变量读,版本已存在会直接拒绝,不会落进任何文件。
|
|
228
|
+
|
|
203
229
|
## English
|
|
204
230
|
|
|
205
231
|
A plugin dashboard panel for DSH (DeepSeek Harness): every plugin in one list, a power switch per plugin,
|
package/client.js
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client half of the plugin
|
|
2
|
+
* Client half of the plugin rack.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* GENERATED FILE — edit src/client/*.js and run `node scripts/build-client.mjs`.
|
|
5
|
+
* The module loader takes one plain script, so the parts are concatenated here
|
|
6
|
+
* instead of being imported at runtime.
|
|
7
7
|
*/
|
|
8
8
|
window.__ModuleLoader__.load({
|
|
9
9
|
id: 'dsh-plugin-rack',
|
|
10
10
|
factory(require) {
|
|
11
|
-
const React = require('react');
|
|
12
|
-
const h = React.createElement;
|
|
13
|
-
|
|
14
|
-
const PANEL_ID = 'plugin-rack';
|
|
15
|
-
const API = '/api/dsh-plugin-rack';
|
|
16
|
-
const NS = 'plugin-rack';
|
|
17
|
-
|
|
18
11
|
const DICT = {
|
|
19
12
|
en: {
|
|
20
13
|
title: 'Plugin rack',
|
|
@@ -145,6 +138,8 @@ window.__ModuleLoader__.load({
|
|
|
145
138
|
updateRestart: 'Restart DSH once to load the new versions',
|
|
146
139
|
updatePartialSwap: 'Some packages are live, some need a restart',
|
|
147
140
|
updateSwapped: 'New versions are live; refresh the page for client-side plugins',
|
|
141
|
+
updateOutOfRange: 'outside the declared range',
|
|
142
|
+
updateOutOfRangeHint: 'Newest {v} does not satisfy "{spec}" — not updated by default',
|
|
148
143
|
hasNewVersion: 'Update available',
|
|
149
144
|
updateNow: 'Update just this plugin',
|
|
150
145
|
jobRunning: 'Host is updating in the background · {n} plugins',
|
|
@@ -199,8 +194,6 @@ window.__ModuleLoader__.load({
|
|
|
199
194
|
groupAllAuto: '整组已设为自动',
|
|
200
195
|
groupAllManual: '整组已设为手动',
|
|
201
196
|
colName: '插件名称',
|
|
202
|
-
colPower: '开启 / 关闭',
|
|
203
|
-
colMode: '自动 / 手动',
|
|
204
197
|
modeAuto: '自动',
|
|
205
198
|
modeManual: '手动',
|
|
206
199
|
filterAll: '全部',
|
|
@@ -305,6 +298,8 @@ window.__ModuleLoader__.load({
|
|
|
305
298
|
updateRestart: '重启一次 DSH 让新版本生效',
|
|
306
299
|
updatePartialSwap: '部分包已生效,部分需要重启',
|
|
307
300
|
updateSwapped: '新版本已即时生效;界面类插件刷新页面即可',
|
|
301
|
+
updateOutOfRange: '超出声明范围',
|
|
302
|
+
updateOutOfRangeHint: '最新 {v} 不满足「{spec}」,默认不更新',
|
|
308
303
|
hasNewVersion: '有新版',
|
|
309
304
|
updateNow: '只更新这一个插件',
|
|
310
305
|
jobRunning: '宿主正在后台更新 · {n} 个插件',
|
|
@@ -416,57 +411,6 @@ window.__ModuleLoader__.load({
|
|
|
416
411
|
// working — otherwise the UI reports a failure for a run that succeeds.
|
|
417
412
|
const WRITE_TIMEOUT_MS = 60 * 60 * 1000;
|
|
418
413
|
|
|
419
|
-
async function request(path, body, options) {
|
|
420
|
-
const timeoutMs =
|
|
421
|
-
options && typeof options.timeoutMs === 'number' ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
422
|
-
const init =
|
|
423
|
-
body === undefined
|
|
424
|
-
? { method: 'GET', headers: { accept: 'application/json' } }
|
|
425
|
-
: {
|
|
426
|
-
method: 'POST',
|
|
427
|
-
headers: {
|
|
428
|
-
'content-type': 'application/json',
|
|
429
|
-
accept: 'application/json',
|
|
430
|
-
// 和宿主端的写操作闸门配对:面板自己永远带这个头,所以即使
|
|
431
|
-
// 某个环境下浏览器没发 Origin / Sec-Fetch-Site 也照样放行。
|
|
432
|
-
'x-dsh-plugin-rack': '1',
|
|
433
|
-
},
|
|
434
|
-
body: JSON.stringify(body),
|
|
435
|
-
};
|
|
436
|
-
// Without this a stuck Host call pins busy/pending forever and the only
|
|
437
|
-
// way out is reloading the page.
|
|
438
|
-
const controller = new AbortController();
|
|
439
|
-
init.signal = controller.signal;
|
|
440
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
441
|
-
let response;
|
|
442
|
-
try {
|
|
443
|
-
response = await fetch(API + path, init);
|
|
444
|
-
} catch (error) {
|
|
445
|
-
void error;
|
|
446
|
-
throw new Error(controller.signal.aborted ? 'timeout' : 'unreachable');
|
|
447
|
-
} finally {
|
|
448
|
-
clearTimeout(timer);
|
|
449
|
-
}
|
|
450
|
-
let data = null;
|
|
451
|
-
try {
|
|
452
|
-
data = await response.json();
|
|
453
|
-
} catch (error) {
|
|
454
|
-
void error;
|
|
455
|
-
data = null;
|
|
456
|
-
}
|
|
457
|
-
// 服务端失败时常常带着细节(回滚那段 pnpm 输出在 data.failed.output 里),
|
|
458
|
-
// 之前只取 message 就抛,于是「为什么失败」的信息到不了界面。把整个 data
|
|
459
|
-
// 挂到 error 上,调用方要用就有。
|
|
460
|
-
if (!response.ok || data === null || data.ok !== true) {
|
|
461
|
-
const message =
|
|
462
|
-
(data && data.message) || (response.ok ? 'malformed response' : response.status + ' ' + response.statusText);
|
|
463
|
-
const error = new Error(message);
|
|
464
|
-
if (data !== null && typeof data === 'object') error.data = data;
|
|
465
|
-
throw error;
|
|
466
|
-
}
|
|
467
|
-
return data;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
414
|
/**
|
|
471
415
|
* 把勾选的包名按更新通道分组。
|
|
472
416
|
*
|
|
@@ -699,6 +643,53 @@ window.__ModuleLoader__.load({
|
|
|
699
643
|
});
|
|
700
644
|
}
|
|
701
645
|
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* What to show at the right end of an installed plugin row.
|
|
649
|
+
*
|
|
650
|
+
* The cases are deliberately distinct: a plain npm install gets no label at all
|
|
651
|
+
* (it is the default — a label there is pure noise), a row installed some other
|
|
652
|
+
* way gets a small tag naming the source, and a row with an update gets a button
|
|
653
|
+
* — unless the release sits outside the version range the user wrote, in which
|
|
654
|
+
* case a button would be a lie, so it becomes a warning tag instead.
|
|
655
|
+
*
|
|
656
|
+
* @param {object|undefined} info the check result for this row, if any
|
|
657
|
+
* @param {string|undefined} src how the row itself was installed
|
|
658
|
+
* @returns {{kind: 'none'}|{kind: 'src', label: string}|{kind: 'outOfRange', label: string, spec: string}|{kind: 'update', label: string, source: string}}
|
|
659
|
+
*/
|
|
660
|
+
function badgeOf(info, src) {
|
|
661
|
+
if (info === undefined || info === null) {
|
|
662
|
+
return src === undefined || src === 'npm' ? { kind: 'none' } : { kind: 'src', label: src };
|
|
663
|
+
}
|
|
664
|
+
if (info.outOfRange === true) {
|
|
665
|
+
return { kind: 'outOfRange', label: info.label ?? '', spec: info.spec ?? '' };
|
|
666
|
+
}
|
|
667
|
+
return { kind: 'update', label: info.label ?? '', source: info.source ?? 'npm' };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Which of the update page's states to render.
|
|
672
|
+
*
|
|
673
|
+
* 'none' used to be decided on hasUpdate alone, which made the page claim there
|
|
674
|
+
* was nothing to do while listing out-of-range releases underneath. Both counts
|
|
675
|
+
* decide it now.
|
|
676
|
+
*
|
|
677
|
+
* @param {{ok?: boolean, message?: string, rows?: Array<{hasUpdate?: boolean, outOfRange?: boolean}>}|null} updates
|
|
678
|
+
* @param {boolean} checking
|
|
679
|
+
* @returns {{kind: 'idle'|'checking'|'error'|'none'|'list', message?: string, updatable?: Array<object>, outOfRange?: Array<object>}}
|
|
680
|
+
*/
|
|
681
|
+
function updatePageState(updates, checking) {
|
|
682
|
+
if (updates === null || updates === undefined) {
|
|
683
|
+
return { kind: checking === true ? 'checking' : 'idle' };
|
|
684
|
+
}
|
|
685
|
+
if (updates.ok !== true) return { kind: 'error', message: String(updates.message ?? 'error') };
|
|
686
|
+
const rows = Array.isArray(updates.rows) ? updates.rows : [];
|
|
687
|
+
const updatable = rows.filter((row) => row.hasUpdate === true);
|
|
688
|
+
const outOfRange = rows.filter((row) => row.outOfRange === true);
|
|
689
|
+
if (updatable.length === 0 && outOfRange.length === 0) return { kind: 'none' };
|
|
690
|
+
return { kind: 'list', updatable: updatable, outOfRange: outOfRange };
|
|
691
|
+
}
|
|
692
|
+
|
|
702
693
|
const CSS = [
|
|
703
694
|
'.pd-root{display:flex;flex-direction:column;height:100%;min-height:0;box-sizing:border-box;',
|
|
704
695
|
'background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}',
|
|
@@ -832,6 +823,14 @@ window.__ModuleLoader__.load({
|
|
|
832
823
|
'.pd-upbtn-open{text-decoration:none}',
|
|
833
824
|
'.pd-src{flex:none;font-size:10px;line-height:16px;padding:0 6px;border-radius:6px;white-space:nowrap;',
|
|
834
825
|
'border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-tertiary)}',
|
|
826
|
+
'.pd-src-warn{border-color:var(--dsw-alias-state-warn-primary);',
|
|
827
|
+
'color:var(--dsw-alias-state-warn-label);background:var(--dsw-alias-state-warn-tertiary)}',
|
|
828
|
+
'.pd-up-section{margin-top:10px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}',
|
|
829
|
+
'.pd-up-oob{display:flex;flex-direction:column;gap:2px;margin-top:6px}',
|
|
830
|
+
'.pd-up-oob-row{display:flex;gap:8px;align-items:baseline;font-size:11px;line-height:18px;',
|
|
831
|
+
'color:var(--dsw-alias-label-tertiary)}',
|
|
832
|
+
'.pd-up-oob-row b{font-weight:600;color:var(--dsw-alias-label-secondary)}',
|
|
833
|
+
'.pd-up-oob-row code{font-size:10px;opacity:.85}',
|
|
835
834
|
'.pd-upbtn{display:inline-flex;align-items:center;gap:5px;flex:none;font-family:inherit;',
|
|
836
835
|
'font-size:11px;line-height:16px;padding:1px 7px;border-radius:8px;white-space:nowrap;',
|
|
837
836
|
'font-variant-numeric:tabular-nums;cursor:pointer;',
|
|
@@ -929,6 +928,8 @@ window.__ModuleLoader__.load({
|
|
|
929
928
|
'.pd-update-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
930
929
|
'.pd-update-name{flex:1;min-width:0;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
931
930
|
'.pd-update-ver{flex:none;font-size:12px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}',
|
|
931
|
+
'.pd-update-spec{flex:none;font-family:inherit;font-size:10px;line-height:16px;padding:0 5px;border-radius:5px;',
|
|
932
|
+
'border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-tertiary)}',
|
|
932
933
|
'.pd-check{flex:none;width:15px;height:15px;margin:0;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer}',
|
|
933
934
|
'.pd-update-log{padding:10px 12px;border-radius:9px;font-size:12px;line-height:18px;background:var(--dsw-alias-bg-layer-2)}',
|
|
934
935
|
'.pd-update-log-ok{border:1px solid var(--dsw-alias-state-success-primary)}',
|
|
@@ -938,6 +939,63 @@ window.__ModuleLoader__.load({
|
|
|
938
939
|
'.pd-update-log-out{margin-top:6px;padding-top:6px;border-top:1px solid var(--dsw-alias-border-l3);font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-all;max-height:140px;overflow:auto}',
|
|
939
940
|
].join('');
|
|
940
941
|
|
|
942
|
+
const React = require('react');
|
|
943
|
+
const h = React.createElement;
|
|
944
|
+
|
|
945
|
+
const PANEL_ID = 'plugin-rack';
|
|
946
|
+
const API = '/api/dsh-plugin-rack';
|
|
947
|
+
const NS = 'plugin-rack';
|
|
948
|
+
async function request(path, body, options) {
|
|
949
|
+
const timeoutMs =
|
|
950
|
+
options && typeof options.timeoutMs === 'number' ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
951
|
+
const init =
|
|
952
|
+
body === undefined
|
|
953
|
+
? { method: 'GET', headers: { accept: 'application/json' } }
|
|
954
|
+
: {
|
|
955
|
+
method: 'POST',
|
|
956
|
+
headers: {
|
|
957
|
+
'content-type': 'application/json',
|
|
958
|
+
accept: 'application/json',
|
|
959
|
+
// 和宿主端的写操作闸门配对:面板自己永远带这个头,所以即使
|
|
960
|
+
// 某个环境下浏览器没发 Origin / Sec-Fetch-Site 也照样放行。
|
|
961
|
+
'x-dsh-plugin-rack': '1',
|
|
962
|
+
},
|
|
963
|
+
body: JSON.stringify(body),
|
|
964
|
+
};
|
|
965
|
+
// Without this a stuck Host call pins busy/pending forever and the only
|
|
966
|
+
// way out is reloading the page.
|
|
967
|
+
const controller = new AbortController();
|
|
968
|
+
init.signal = controller.signal;
|
|
969
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
970
|
+
let response;
|
|
971
|
+
try {
|
|
972
|
+
response = await fetch(API + path, init);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
void error;
|
|
975
|
+
throw new Error(controller.signal.aborted ? 'timeout' : 'unreachable');
|
|
976
|
+
} finally {
|
|
977
|
+
clearTimeout(timer);
|
|
978
|
+
}
|
|
979
|
+
let data = null;
|
|
980
|
+
try {
|
|
981
|
+
data = await response.json();
|
|
982
|
+
} catch (error) {
|
|
983
|
+
void error;
|
|
984
|
+
data = null;
|
|
985
|
+
}
|
|
986
|
+
// 服务端失败时常常带着细节(回滚那段 pnpm 输出在 data.failed.output 里),
|
|
987
|
+
// 之前只取 message 就抛,于是「为什么失败」的信息到不了界面。把整个 data
|
|
988
|
+
// 挂到 error 上,调用方要用就有。
|
|
989
|
+
if (!response.ok || data === null || data.ok !== true) {
|
|
990
|
+
const message =
|
|
991
|
+
(data && data.message) || (response.ok ? 'malformed response' : response.status + ' ' + response.statusText);
|
|
992
|
+
const error = new Error(message);
|
|
993
|
+
if (data !== null && typeof data === 'object') error.data = data;
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
return data;
|
|
997
|
+
}
|
|
998
|
+
|
|
941
999
|
function SidebarIcon(props) {
|
|
942
1000
|
const size = props && typeof props.size === 'number' ? props.size : 18;
|
|
943
1001
|
return h(
|
|
@@ -1333,7 +1391,6 @@ window.__ModuleLoader__.load({
|
|
|
1333
1391
|
setExtrasBusy('check');
|
|
1334
1392
|
try {
|
|
1335
1393
|
setExtras(await request('/extras'));
|
|
1336
|
-
setExtraLog(null);
|
|
1337
1394
|
} catch (error) {
|
|
1338
1395
|
// 进面板那一次自动检查失败时静默,别一打开就弹错。
|
|
1339
1396
|
if (quiet !== true) setExtras({ ok: false, message: String(error && error.message) });
|
|
@@ -1553,7 +1610,7 @@ window.__ModuleLoader__.load({
|
|
|
1553
1610
|
const doBulk = async (ask) => {
|
|
1554
1611
|
if (ask === null) return;
|
|
1555
1612
|
setBulkAsk(null);
|
|
1556
|
-
const { enabled, scope,
|
|
1613
|
+
const { enabled, scope, save } = ask;
|
|
1557
1614
|
let accepted;
|
|
1558
1615
|
try {
|
|
1559
1616
|
accepted = await request('/bulk', {
|
|
@@ -1856,7 +1913,24 @@ window.__ModuleLoader__.load({
|
|
|
1856
1913
|
if (typeof item.name !== 'string') continue;
|
|
1857
1914
|
srcOf.set(item.name, 'npm');
|
|
1858
1915
|
if (item.hasUpdate === true && typeof item.latest === 'string') {
|
|
1859
|
-
updateOf.set(item.name, {
|
|
1916
|
+
updateOf.set(item.name, {
|
|
1917
|
+
label: 'v' + item.latest,
|
|
1918
|
+
source: 'npm',
|
|
1919
|
+
action: 'npm',
|
|
1920
|
+
spec: typeof item.spec === 'string' ? item.spec : '',
|
|
1921
|
+
ranged: item.kind !== 'exact',
|
|
1922
|
+
});
|
|
1923
|
+
} else if (item.outOfRange === true && typeof item.latest === 'string') {
|
|
1924
|
+
// 服务端已经判定这个新版本不满足 package.json 里写的范围,
|
|
1925
|
+
// 批量更新也不会动它。主列表仍然要让人看见「有新版但不升」,
|
|
1926
|
+
// 否则用户只会觉得「检查更新坏了」。
|
|
1927
|
+
updateOf.set(item.name, {
|
|
1928
|
+
label: 'v' + item.latest,
|
|
1929
|
+
source: 'npm',
|
|
1930
|
+
action: 'npm',
|
|
1931
|
+
outOfRange: true,
|
|
1932
|
+
spec: typeof item.spec === 'string' ? item.spec : '',
|
|
1933
|
+
});
|
|
1860
1934
|
}
|
|
1861
1935
|
}
|
|
1862
1936
|
}
|
|
@@ -2027,13 +2101,22 @@ window.__ModuleLoader__.load({
|
|
|
2027
2101
|
{ className: 'pd-name-act', onClick: (event) => event.stopPropagation() },
|
|
2028
2102
|
(() => {
|
|
2029
2103
|
const info = updateOf.get(row.name);
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
// 装的」才值得在主列表里露一个标签。
|
|
2034
|
-
return src === undefined || src === 'npm' ? null : h('span', { className: 'pd-src' }, src);
|
|
2035
|
-
}
|
|
2104
|
+
const badge = badgeOf(info, srcOf.get(row.name));
|
|
2105
|
+
if (badge.kind === 'none') return null;
|
|
2106
|
+
if (badge.kind === 'src') return h('span', { className: 'pd-src' }, badge.label);
|
|
2036
2107
|
const updating = pending['update:' + row.entryId] === true;
|
|
2108
|
+
if (badge.kind === 'outOfRange') {
|
|
2109
|
+
// 有新版、但不在 package.json 声明的范围内:这里不能给按钮,
|
|
2110
|
+
// 点了也不会升,只会让人以为按钮失灵。
|
|
2111
|
+
return h(
|
|
2112
|
+
'span',
|
|
2113
|
+
{
|
|
2114
|
+
className: 'pd-src pd-src-warn',
|
|
2115
|
+
title: t('updateOutOfRangeHint', { v: badge.label, spec: badge.spec }),
|
|
2116
|
+
},
|
|
2117
|
+
t('updateOutOfRange'),
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2037
2120
|
const parts = [
|
|
2038
2121
|
updating ? h('span', { key: 'spin', className: 'pd-spin pd-upbtn-spin' }) : null,
|
|
2039
2122
|
'v' + (row.version ?? '?'),
|
|
@@ -2404,6 +2487,9 @@ window.__ModuleLoader__.load({
|
|
|
2404
2487
|
}),
|
|
2405
2488
|
h('span', { className: 'pd-update-name' }, row.name),
|
|
2406
2489
|
h('span', { className: 'pd-update-ver' }, row.current + ' → ' + row.latest),
|
|
2490
|
+
typeof row.spec === 'string' && row.kind !== 'exact'
|
|
2491
|
+
? h('code', { className: 'pd-update-spec', title: row.spec }, row.spec)
|
|
2492
|
+
: null,
|
|
2407
2493
|
);
|
|
2408
2494
|
|
|
2409
2495
|
const installPanel = h(
|
|
@@ -2529,17 +2615,39 @@ window.__ModuleLoader__.load({
|
|
|
2529
2615
|
: String(logText.text).slice(-4000),
|
|
2530
2616
|
),
|
|
2531
2617
|
),
|
|
2532
|
-
|
|
2533
|
-
|
|
2618
|
+
(() => {
|
|
2619
|
+
const state = updatePageState(updates, updatesBusy === 'check');
|
|
2620
|
+
if (state.kind === 'list') return state.updatable.map(updateRowNode);
|
|
2621
|
+
if (state.kind === 'error') return h('div', { className: 'pd-empty' }, state.message);
|
|
2622
|
+
if (state.kind === 'none') return h('div', { className: 'pd-empty' }, t('updateNone'));
|
|
2623
|
+
const key = state.kind === 'checking' ? 'updateChecking' : 'updateIdle';
|
|
2624
|
+
return h('div', { className: 'pd-empty' }, t(key));
|
|
2625
|
+
})(),
|
|
2626
|
+
(() => {
|
|
2627
|
+
// 「有新版但超出范围」单独列一段:它不是错误,也不该被批量更新
|
|
2628
|
+
// 静默吞掉,放在主列表下面说明原因最省解释成本。
|
|
2629
|
+
const state = updatePageState(updates, false);
|
|
2630
|
+
const outOfRange = state.kind === 'list' ? state.outOfRange : [];
|
|
2631
|
+
if (outOfRange.length === 0) return null;
|
|
2632
|
+
return h(
|
|
2633
|
+
'div',
|
|
2634
|
+
{ className: 'pd-up-section' },
|
|
2635
|
+
h('div', null, t('updateOutOfRange') + ' (' + outOfRange.length + ')'),
|
|
2636
|
+
h(
|
|
2534
2637
|
'div',
|
|
2535
|
-
{ className: 'pd-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2638
|
+
{ className: 'pd-up-oob' },
|
|
2639
|
+
outOfRange.map((row) =>
|
|
2640
|
+
h(
|
|
2641
|
+
'div',
|
|
2642
|
+
{ key: row.name, className: 'pd-up-oob-row' },
|
|
2643
|
+
h('b', null, row.name),
|
|
2644
|
+
h('span', null, row.current + ' → ' + row.latest),
|
|
2645
|
+
typeof row.spec === 'string' ? h('code', null, row.spec) : null,
|
|
2646
|
+
),
|
|
2647
|
+
),
|
|
2648
|
+
),
|
|
2649
|
+
);
|
|
2650
|
+
})(),
|
|
2543
2651
|
updateLog === null
|
|
2544
2652
|
? null
|
|
2545
2653
|
: h(
|
|
@@ -2837,7 +2945,6 @@ window.__ModuleLoader__.load({
|
|
|
2837
2945
|
|
|
2838
2946
|
return Dashboard;
|
|
2839
2947
|
}
|
|
2840
|
-
|
|
2841
2948
|
return {
|
|
2842
2949
|
inject: ['slots', 'locale', 'layout'],
|
|
2843
2950
|
apply(ctx) {
|