ygc-dsh-plugin-autoupdate 0.2.1 → 0.5.0
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 +129 -1
- package/client.js +258 -0
- package/index.js +174 -18
- package/lib/api.js +59 -0
- package/lib/core.js +34 -0
- package/lib/machine.js +156 -0
- package/lib/share.js +1018 -0
- package/lib/updater.js +30 -7
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +13 -11
package/README.md
CHANGED
|
@@ -98,6 +98,129 @@
|
|
|
98
98
|
| POST | `/restore` | 用某份备份回滚清单 |
|
|
99
99
|
| GET | `/diagnose` | 看插件识别结果(排查「为什么某个包没被检查」) |
|
|
100
100
|
|
|
101
|
+
## 插件共享(特征文件夹)
|
|
102
|
+
|
|
103
|
+
把「我这台机器装了什么插件」沉淀成一份可以给别人看的东西,互相补全。
|
|
104
|
+
|
|
105
|
+
### 目录结构
|
|
106
|
+
|
|
107
|
+
`插件库` 是**共享文件夹**,一个人一格,自己的所有东西都锁在自己那一格里。
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
D:\Ai工作空间\插件库\ ← 共享文件夹,两台机器都能读写
|
|
111
|
+
├─ <本机标识>\ ← 我的格子,例 DSH-7e6b1bb7e052
|
|
112
|
+
│ ├─ PLUGINS.json 我的插件清单(含副本指纹表)
|
|
113
|
+
│ ├─ FEATURE.json 我的身份说明
|
|
114
|
+
│ ├─ OTHERS-PLUGINS.json 我的汇总(本机自用)
|
|
115
|
+
│ ├─ OTHERS-PLUGINS.md 同一份的人读版
|
|
116
|
+
│ └─ local\ 本地 / Git 来源插件的 .tgz 副本
|
|
117
|
+
└─ <别人的标识>\ ← 别人写,我只读
|
|
118
|
+
├─ PLUGINS.json
|
|
119
|
+
├─ FEATURE.json
|
|
120
|
+
└─ local\
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**没有公共分区。** 早先版本在插件库下分出「特征 / 汇集」两级子目录,那是错的:
|
|
124
|
+
插件库是多人共享的盘,公共目录等于公共广场,所有人的东西往一处堆,谁的产出分不清,
|
|
125
|
+
而且「汇集」会成为多方争写的热点。现在改成:
|
|
126
|
+
|
|
127
|
+
| 原位置 | 现位置 |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `插件库\特征\<标识>\` | `插件库\<标识>\` |
|
|
130
|
+
| `插件库\汇集\OTHERS-PLUGINS.*` | `插件库\<标识>\OTHERS-PLUGINS.*` |
|
|
131
|
+
|
|
132
|
+
**汇集只给本机自己用。** 每台机器各自扫一遍插件库下所有人的格子,在自己的格子里
|
|
133
|
+
生成自己的汇总——不做全局总表,因为那需要所有机器写同一个文件,必然是冲突源。
|
|
134
|
+
|
|
135
|
+
### 本机标识与会话同步插件共用
|
|
136
|
+
|
|
137
|
+
特征文件夹的目录名就是**本机标识**,格式 `<名称>-<特征值>`,例如 `DSH-7e6b1bb7e052`。
|
|
138
|
+
这套规则和会话同步插件(`ygc-dsh-sync-backup`)**逐字一致**,并且共用同一份身份:
|
|
139
|
+
|
|
140
|
+
| 项 | 规则 |
|
|
141
|
+
|---|---|
|
|
142
|
+
| 身份来源 | 优先读 `~/.dsh/storages/sync-backup.json` 的 `machineHash` |
|
|
143
|
+
| 特征值 | 6 字节随机数 → 12 位十六进制,**第一次确定后永久固定,永不重算** |
|
|
144
|
+
| 名称部分 | 面板可改;改名只换目录名,特征值不变,旧目录不删 |
|
|
145
|
+
| 落盘位置 | 固定进 `~/.dsh/storages/plugin-autoupdate.json` 的 `shareMachineHash` |
|
|
146
|
+
| 迁移 | 唯一来源是既有 state 里已有的哈希;没有旧值才新生成 |
|
|
147
|
+
|
|
148
|
+
**为什么必须共用**:两台机器之间的身份只能有一个。会话同步已经把这一格写成了
|
|
149
|
+
`machineId`,插件共享如果自己再算一份(比如主机名-用户名),同一台机器会在共享盘上
|
|
150
|
+
留下两个身份,别人汇总时看到两个「我」,本机自己也不确定该往哪格写。
|
|
151
|
+
|
|
152
|
+
名称里的非法字符按会话同步的 `joinId` 规则换成下划线;`特征/` 下只写自己那一格,
|
|
153
|
+
写别人的目录会被守卫拒绝(`WRITE_DENIED`)。想强行重算特征值,用
|
|
154
|
+
`POST /settings { shareResetHash: true }`——这是唯一的重算入口,其它情况一律沿用旧值。
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
### 清单里写什么
|
|
158
|
+
|
|
159
|
+
| 插件来源 | 清单里写什么 | 要不要打副本 |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `registry`(能联网更新) | 名称 + 版本 | **不打**,对方自己从 registry 拉 |
|
|
162
|
+
| `local`(`file:` 本地路径) | 名称 + 版本 + 来源说明 | **打**,`local/<name>-<version>.tgz` |
|
|
163
|
+
| `git`(GitHub 等) | 名称 + 版本 | **打**,同上 |
|
|
164
|
+
| `installation`(随 DSH 安装提供) | **不写** | 别人本来就有,也没版本号可装 |
|
|
165
|
+
|
|
166
|
+
副本是 npm 标准 tgz(`package/` 前缀),排除 `node_modules`、`.git`、`__pycache__`、`*.bak-*`。
|
|
167
|
+
单包上限 96 MB,单个特征的副本总量上限 512 MB,同名插件只留最近 2 个版本。
|
|
168
|
+
`link:` 装的包在 `node_modules` 下是符号链接,打包时按 `realpath` 追到真实目录。
|
|
169
|
+
|
|
170
|
+
### 什么时候跑
|
|
171
|
+
|
|
172
|
+
**每次巡检跑一次**——巡检 = 自动更新的那一轮定时检查(间隔见「检查频率」)。
|
|
173
|
+
顺序是先查更新、再同步共享;更新失败不影响共享同步。面板上也有「立即同步一次」。
|
|
174
|
+
|
|
175
|
+
每次同步都幂等:
|
|
176
|
+
|
|
177
|
+
- 清单内容(除 `generatedAt` / `lastSyncAt` 这类时间字段)没变 → 不重写文件
|
|
178
|
+
- 源目录指纹(文件数 + 最大 mtime + 总字节)没变 → 不重压 `.tgz`,直接复用
|
|
179
|
+
- 本机标识第一次同步时固定进 state,之后每轮原样传回,不做任何重算
|
|
180
|
+
|
|
181
|
+
所以几百 MB 的本地插件不会把每轮巡检拖慢。
|
|
182
|
+
|
|
183
|
+
### 拉别人的清单
|
|
184
|
+
|
|
185
|
+
把别人的 `<别人的标识>\` 整个目录直接放进 `插件库\`(同步盘自动同步也行),然后:
|
|
186
|
+
|
|
187
|
+
1. 面板点「重新读取」,或等下一次巡检;
|
|
188
|
+
2. 自己格子里的 `OTHERS-PLUGINS.json` 生成合并后的总清单;
|
|
189
|
+
3. 面板「他人插件(本机没有的)」里逐个点「安装」。
|
|
190
|
+
|
|
191
|
+
**不自动安装。** 别人的插件清单可能包含你不想要的东西,装机这个动作必须人工点。
|
|
192
|
+
|
|
193
|
+
合并去重规则:
|
|
194
|
+
|
|
195
|
+
- 去重键是包名;多本特征都有同一插件时,版本取**更高**的那份(按语义版本比,不看文件时间)
|
|
196
|
+
- 副本跟着最高版本走;`features` 里记下它出现在哪几本
|
|
197
|
+
- 「本机有没有」按 profile 的 `dsh.profile.bundles` 判断,不看 `node_modules`(那里塞满传递依赖)
|
|
198
|
+
- 没 `PLUGINS.json` 的目录只记一条提示,不报错
|
|
199
|
+
|
|
200
|
+
### 装的时候走哪条路
|
|
201
|
+
|
|
202
|
+
| 对方特征里有 | 用的 spec |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `.tgz` 副本 | `file:<对方副本的绝对路径>`(正斜杠) |
|
|
205
|
+
| 只有名称(registry 来源) | `<name>@<version>` |
|
|
206
|
+
| 既没副本又不是 registry | 拒绝,说明原因 |
|
|
207
|
+
|
|
208
|
+
装完自动把包名补进 profile 的 `dsh.profile.bundles`——不补的话插件装下来但不会加载。
|
|
209
|
+
装之前同样会快照 profile 三个清单文件,出问题用 `/restore` 回滚。**装完必须完全退出 DSH 再重开。**
|
|
210
|
+
|
|
211
|
+
### 共享相关接口
|
|
212
|
+
|
|
213
|
+
| 方法 | 路径 | 作用 |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| GET | `/share` | 我的特征现状 + 他人清单(含每项是否已装、有没有副本) |
|
|
216
|
+
| POST | `/share/sync` | 立即同步一次(我的清单 + 副本 + 他人汇集) |
|
|
217
|
+
| POST | `/share/install` | 安装他人清单里的一个插件(body: `{ name }`) |
|
|
218
|
+
|
|
219
|
+
设置项(走 `/settings`):`shareEnabled`、`shareLibraryRoot`、`shareFeatureName`、`shareResetHash`。
|
|
220
|
+
|
|
221
|
+
`shareFeatureName` 只改**名称部分**,特征值不动;`shareResetHash: true` 是唯一的重算入口,
|
|
222
|
+
会让下次同步重新解析特征值(正常情况别用,会让目录漂移、别人看到两个你)。
|
|
223
|
+
|
|
101
224
|
## 自检
|
|
102
225
|
|
|
103
226
|
```powershell
|
|
@@ -105,13 +228,15 @@ $node = "C:\DeepSeekHarness\resources\runtime\primary-runtime\dependencies\node\
|
|
|
105
228
|
& $node "C:\DSH-Work\plugin-autoupdate\pkg\test\all.mjs"
|
|
106
229
|
```
|
|
107
230
|
|
|
108
|
-
|
|
231
|
+
八个套件:
|
|
109
232
|
|
|
110
233
|
| 套件 | 覆盖 |
|
|
111
234
|
|---|---|
|
|
112
235
|
| `selftest.mjs` | 版本比较(含 prerelease)、状态规范化夹紧、延迟判定、registry packument 解析、本机 profile 识别 |
|
|
113
236
|
| `workspace-merge.test.mjs` | `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 合并(同名并成一行、不重复、CRLF 保持、段落连续) |
|
|
114
237
|
| `api-smoke.test.mjs` | 用假 ctx 真加载 Host 半,走完全部 API 路由;状态写在临时 HOME,不碰真实 profile |
|
|
238
|
+
| `machine.test.mjs` | 本机标识:继承会话同步的 machineId、固定后不漂移、改名不换特征值、卸载会话同步后旧值仍有效、写边界(别人的格 / `../` 逃逸 / 前缀相近) |
|
|
239
|
+
| `share.test.mjs` | 特征文件夹:清单分类(registry 只写名 / 本地打副本 / 宿主自带不写)、tgz 结构、同步幂等与副本复用、他人清单合并去重、安装路径选择、特征名防上跳 |
|
|
115
240
|
| `client-smoke.test.mjs` | 在 Node 里用假 `document` / React 求值 `client.js`,验证插槽注册、locale 注册、面板首渲染 |
|
|
116
241
|
|
|
117
242
|
## 边界
|
|
@@ -120,6 +245,9 @@ $node = "C:\DeepSeekHarness\resources\runtime\primary-runtime\dependencies\node\
|
|
|
120
245
|
- 只在 `registry` 来源上工作;`local` / `git` / `installation` 来源只显示、不处理
|
|
121
246
|
- 更新失败只记历史,不自动回滚(避免把用户自己手改的清单吞掉);要回滚用 `/restore`
|
|
122
247
|
- DSH 关闭期间不检查(进程内定时器的固有限制);重开后会立刻按上游发布时间重新判定
|
|
248
|
+
- 共享只**同步清单与副本**,不自动安装别人的插件;安装必须人工点选
|
|
249
|
+
- 只写 `插件库\<本机标识>\` 那一格,别人的格子只读(写入口有守卫)
|
|
250
|
+
- 不在插件库根下留任何公共文件或公共目录
|
|
123
251
|
|
|
124
252
|
## 踩过的坑
|
|
125
253
|
|
package/client.js
CHANGED
|
@@ -44,6 +44,13 @@ const CSS = `
|
|
|
44
44
|
.pau-switch{display:inline-flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;user-select:none}
|
|
45
45
|
.pau-switch input{width:16px;height:16px;cursor:pointer}
|
|
46
46
|
.pau-actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
|
|
47
|
+
.pau-share-path{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;word-break:break-all;color:var(--dsw-alias-label-secondary,#555)}
|
|
48
|
+
.pau-tag-mine{background:var(--dsw-alias-info-tint,#e0f2fe);color:var(--dsw-alias-info,#0369a1)}
|
|
49
|
+
.pau-grid{display:flex;flex-direction:column;gap:8px}
|
|
50
|
+
.pau-other{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--dsw-alias-border-l25,#f0f0f0);flex-wrap:wrap}
|
|
51
|
+
.pau-other:last-child{border-bottom:none}
|
|
52
|
+
.pau-other-main{flex:1;min-width:200px}
|
|
53
|
+
.pau-scroll{max-height:420px;overflow:auto}
|
|
47
54
|
`;
|
|
48
55
|
|
|
49
56
|
const API = '/api/local/plugin-autoupdate';
|
|
@@ -87,6 +94,7 @@ function makePanel(React) {
|
|
|
87
94
|
|
|
88
95
|
return function AutoUpdatePanel() {
|
|
89
96
|
const [snap, setSnap] = useState(null);
|
|
97
|
+
const [shareDetail, setShareDetail] = useState(null);
|
|
90
98
|
const [busy, setBusy] = useState('');
|
|
91
99
|
const [error, setError] = useState('');
|
|
92
100
|
const [note, setNote] = useState('');
|
|
@@ -95,6 +103,18 @@ function makePanel(React) {
|
|
|
95
103
|
const apply = useCallback((next) => {
|
|
96
104
|
if (!next) return;
|
|
97
105
|
setSnap(next);
|
|
106
|
+
// 共享面板的明细单独取一次:它是扫盘结果,比状态文件重,不跟 15 秒轮询一起刷
|
|
107
|
+
if (next.share && next.share.detail) setShareDetail(next.share.detail);
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const loadShareDetail = useCallback(async () => {
|
|
111
|
+
try {
|
|
112
|
+
const data = await call('/share');
|
|
113
|
+
if (data && data.share && data.share.detail) setShareDetail(data.share.detail);
|
|
114
|
+
if (data) setSnap(data);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
setError(String(err.message || err));
|
|
117
|
+
}
|
|
98
118
|
}, []);
|
|
99
119
|
|
|
100
120
|
const refresh = useCallback(async () => {
|
|
@@ -115,6 +135,11 @@ function makePanel(React) {
|
|
|
115
135
|
};
|
|
116
136
|
}, [refresh]);
|
|
117
137
|
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
// 共享明细只取一次,之后靠「立即同步」/ 安装后刷新
|
|
140
|
+
loadShareDetail();
|
|
141
|
+
}, [loadShareDetail]);
|
|
142
|
+
|
|
118
143
|
const run = async (name, fn) => {
|
|
119
144
|
setBusy(name);
|
|
120
145
|
setError('');
|
|
@@ -511,6 +536,238 @@ function makePanel(React) {
|
|
|
511
536
|
),
|
|
512
537
|
);
|
|
513
538
|
|
|
539
|
+
const share = snap.share || {};
|
|
540
|
+
const detail = shareDetail || null;
|
|
541
|
+
const myFeature = (detail && detail.myFeature) || share.effectiveFeature || '';
|
|
542
|
+
const libRoot = (detail && detail.libraryRoot) || share.effectiveRoot || '';
|
|
543
|
+
const others = (detail && Array.isArray(detail.others) ? detail.others : []).filter((p) => !p.installed);
|
|
544
|
+
const installedFromOthers = (detail && Array.isArray(detail.others) ? detail.others : []).filter(
|
|
545
|
+
(p) => p.installed,
|
|
546
|
+
);
|
|
547
|
+
const sources = (detail && Array.isArray(detail.sources) ? detail.sources : []) || [];
|
|
548
|
+
|
|
549
|
+
const shareCard = h(
|
|
550
|
+
'div',
|
|
551
|
+
{ className: 'pau-card' },
|
|
552
|
+
h(
|
|
553
|
+
'div',
|
|
554
|
+
{ className: 'pau-card-h' },
|
|
555
|
+
h('h3', { className: 'pau-card-t' }, '插件共享(特征文件夹)'),
|
|
556
|
+
h(
|
|
557
|
+
'span',
|
|
558
|
+
{ className: share.enabled ? 'pau-tag pau-tag-ok' : 'pau-tag' },
|
|
559
|
+
share.enabled ? '每次巡检自动同步' : '自动同步已关闭',
|
|
560
|
+
),
|
|
561
|
+
),
|
|
562
|
+
h(
|
|
563
|
+
'p',
|
|
564
|
+
{ className: 'pau-sub' },
|
|
565
|
+
'插件库是共享文件夹,一个人一格:我的清单、本地插件副本、我自己的汇总全都写在「插件库\\<我的特征>」这一格里面。能联网更新的插件只写名称,本地/Git 来源的打成 .tgz 副本一起放过去。别人的格子只读,汇总也是各机扫一遍自己算,不放在公共位置。',
|
|
566
|
+
),
|
|
567
|
+
h(
|
|
568
|
+
'label',
|
|
569
|
+
{ className: 'pau-switch' },
|
|
570
|
+
h('input', {
|
|
571
|
+
type: 'checkbox',
|
|
572
|
+
checked: !!share.enabled,
|
|
573
|
+
onChange: (e) => patchSettings({ shareEnabled: e.target.checked }),
|
|
574
|
+
}),
|
|
575
|
+
'每次巡检顺带同步一次特征文件夹',
|
|
576
|
+
),
|
|
577
|
+
h(
|
|
578
|
+
'div',
|
|
579
|
+
{ className: 'pau-field' },
|
|
580
|
+
h('span', { className: 'pau-field-l' }, '插件库根目录'),
|
|
581
|
+
h('input', {
|
|
582
|
+
className: 'pau-input',
|
|
583
|
+
style: { minWidth: 320, flex: 1 },
|
|
584
|
+
placeholder: '留空用默认 D:\\Ai工作空间\\插件库',
|
|
585
|
+
defaultValue: share.libraryRoot || '',
|
|
586
|
+
onBlur: (e) => patchSettings({ shareLibraryRoot: e.target.value.trim() }),
|
|
587
|
+
}),
|
|
588
|
+
),
|
|
589
|
+
h(
|
|
590
|
+
'div',
|
|
591
|
+
{ className: 'pau-field' },
|
|
592
|
+
h('span', { className: 'pau-field-l' }, '我的特征文件夹名'),
|
|
593
|
+
h('input', {
|
|
594
|
+
className: 'pau-input',
|
|
595
|
+
style: { minWidth: 220 },
|
|
596
|
+
placeholder: '名称部分,留空用电脑名',
|
|
597
|
+
defaultValue: share.featureName || '',
|
|
598
|
+
onBlur: (e) => patchSettings({ shareFeatureName: e.target.value.trim() }),
|
|
599
|
+
}),
|
|
600
|
+
h(
|
|
601
|
+
'span',
|
|
602
|
+
{ className: 'pau-row-d' },
|
|
603
|
+
share.machineHash ? `特征值 ${share.machineHash}(已固定,改名字不会变)` : '特征值首次同步时固定',
|
|
604
|
+
),
|
|
605
|
+
),
|
|
606
|
+
(detail && detail.identity && detail.identity.sharesWithSyncBackup) !== false
|
|
607
|
+
? h(
|
|
608
|
+
'div',
|
|
609
|
+
{ className: 'pau-row-d' },
|
|
610
|
+
'与会话同步插件共用同一个本机标识,两台机器之间只有「一个我」。',
|
|
611
|
+
)
|
|
612
|
+
: h(
|
|
613
|
+
'div',
|
|
614
|
+
{ className: 'pau-row-d' },
|
|
615
|
+
'注意:没读到会话同步插件的标识,本插件用了自己的特征值。',
|
|
616
|
+
),
|
|
617
|
+
h('div', { className: 'pau-row-d' }, `我的特征目录:`),
|
|
618
|
+
h('div', { className: 'pau-share-path' }, `${libRoot}${myFeature ? `\\${myFeature}` : ''}`),
|
|
619
|
+
h(
|
|
620
|
+
'div',
|
|
621
|
+
{ className: 'pau-row-d' },
|
|
622
|
+
`我的汇总(只给本机看,各机自己汇集):${myFeature ? `\\${myFeature}\\OTHERS-PLUGINS.json` : ''}`,
|
|
623
|
+
),
|
|
624
|
+
h(
|
|
625
|
+
'div',
|
|
626
|
+
{ className: 'pau-row-d', style: { marginTop: 6 } },
|
|
627
|
+
`上次同步:${fmtAgo(share.lastSyncAt)}${
|
|
628
|
+
share.lastResult
|
|
629
|
+
? ` · 本机 ${share.lastResult.total} 个插件(副本 ${share.lastResult.bundled},${share.lastResult.totalMb} MB) · 他人 ${share.lastResult.others} 个,可装 ${share.lastResult.installable}`
|
|
630
|
+
: ''
|
|
631
|
+
}`,
|
|
632
|
+
),
|
|
633
|
+
share.lastError
|
|
634
|
+
? h('div', { className: 'pau-banner pau-banner-warn', style: { marginTop: 8 } }, `上次同步失败:${share.lastError}`)
|
|
635
|
+
: null,
|
|
636
|
+
h(
|
|
637
|
+
'div',
|
|
638
|
+
{ className: 'pau-actions', style: { marginTop: 12 } },
|
|
639
|
+
h(
|
|
640
|
+
'button',
|
|
641
|
+
{
|
|
642
|
+
className: 'pau-btn pau-btn-primary',
|
|
643
|
+
disabled: !!busy,
|
|
644
|
+
onClick: () =>
|
|
645
|
+
run('share-sync', async () => {
|
|
646
|
+
const next = await call('/share/sync', {});
|
|
647
|
+
const result = next.result || {};
|
|
648
|
+
if (result && result.ok === false) {
|
|
649
|
+
setNote(`同步失败:${result.error || '未知错误'}`);
|
|
650
|
+
} else {
|
|
651
|
+
const counts = (result.mine && result.mine.counts) || {};
|
|
652
|
+
const others2 = (result.aggregate && result.aggregate.counts) || {};
|
|
653
|
+
setNote(
|
|
654
|
+
`同步完成:本机 ${counts.total || 0} 个插件(副本 ${counts.bundled || 0}),他人 ${others2.othersTotal || 0} 个,可装 ${others2.installable || 0} 个`,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (next.share && next.share.detail) setShareDetail(next.share.detail);
|
|
658
|
+
return next;
|
|
659
|
+
}),
|
|
660
|
+
},
|
|
661
|
+
busy === 'share-sync' ? '同步中…' : '立即同步一次',
|
|
662
|
+
),
|
|
663
|
+
h(
|
|
664
|
+
'button',
|
|
665
|
+
{
|
|
666
|
+
className: 'pau-btn',
|
|
667
|
+
disabled: !!busy,
|
|
668
|
+
onClick: () => run('share-reload', async () => {
|
|
669
|
+
await loadShareDetail();
|
|
670
|
+
setNote('已重新读取特征文件夹');
|
|
671
|
+
return null;
|
|
672
|
+
}),
|
|
673
|
+
},
|
|
674
|
+
'重新读取',
|
|
675
|
+
),
|
|
676
|
+
),
|
|
677
|
+
h(
|
|
678
|
+
'div',
|
|
679
|
+
{ className: 'pau-card-h', style: { marginTop: 18, marginBottom: 6 } },
|
|
680
|
+
h('h4', { className: 'pau-card-t' }, `他人插件(本机没有的 ${others.length} 个)`),
|
|
681
|
+
h(
|
|
682
|
+
'span',
|
|
683
|
+
{ className: 'pau-row-d' },
|
|
684
|
+
sources.length ? `${sources.filter((s) => !s.mine).length} 本外部特征` : '还没有拉到别人的特征',
|
|
685
|
+
),
|
|
686
|
+
),
|
|
687
|
+
others.length
|
|
688
|
+
? h(
|
|
689
|
+
'div',
|
|
690
|
+
{ className: 'pau-scroll' },
|
|
691
|
+
others.map((plugin) =>
|
|
692
|
+
h(
|
|
693
|
+
'div',
|
|
694
|
+
{ className: 'pau-other', key: plugin.name },
|
|
695
|
+
h(
|
|
696
|
+
'div',
|
|
697
|
+
{ className: 'pau-other-main' },
|
|
698
|
+
h(
|
|
699
|
+
'div',
|
|
700
|
+
{ className: 'pau-row-t' },
|
|
701
|
+
h('span', { className: 'pau-mono' }, plugin.name),
|
|
702
|
+
plugin.version ? h('span', { className: 'pau-tag' }, `v${plugin.version}`) : null,
|
|
703
|
+
h(
|
|
704
|
+
'span',
|
|
705
|
+
{
|
|
706
|
+
className:
|
|
707
|
+
plugin.bundle || plugin.kind === 'registry'
|
|
708
|
+
? 'pau-tag pau-tag-ok'
|
|
709
|
+
: 'pau-tag pau-tag-warn',
|
|
710
|
+
},
|
|
711
|
+
plugin.bundle
|
|
712
|
+
? '含副本,可离线装'
|
|
713
|
+
: plugin.kind === 'registry'
|
|
714
|
+
? '需联网拉取'
|
|
715
|
+
: '无副本,装不了',
|
|
716
|
+
),
|
|
717
|
+
h('span', { className: 'pau-tag pau-tag-mine' }, plugin.features.join(' / ')),
|
|
718
|
+
),
|
|
719
|
+
plugin.description
|
|
720
|
+
? h('div', { className: 'pau-row-d' }, plugin.description.slice(0, 140))
|
|
721
|
+
: null,
|
|
722
|
+
),
|
|
723
|
+
h(
|
|
724
|
+
'button',
|
|
725
|
+
{
|
|
726
|
+
className: 'pau-btn pau-btn-sm pau-btn-primary',
|
|
727
|
+
disabled: !!busy || (!plugin.bundle && plugin.kind !== 'registry'),
|
|
728
|
+
title: plugin.bundle ? `安装对方特征里的副本` : '从 registry 安装该版本',
|
|
729
|
+
onClick: () =>
|
|
730
|
+
run(`share-install:${plugin.name}`, async () => {
|
|
731
|
+
const next = await call('/share/install', { name: plugin.name });
|
|
732
|
+
setNote((next.result && next.result.message) || `${plugin.name} 已安装`);
|
|
733
|
+
if (next.share && next.share.detail) setShareDetail(next.share.detail);
|
|
734
|
+
return next;
|
|
735
|
+
}),
|
|
736
|
+
},
|
|
737
|
+
busy === `share-install:${plugin.name}` ? '安装中…' : '安装',
|
|
738
|
+
),
|
|
739
|
+
),
|
|
740
|
+
),
|
|
741
|
+
)
|
|
742
|
+
: h(
|
|
743
|
+
'div',
|
|
744
|
+
{ className: 'pau-empty' },
|
|
745
|
+
sources.length ? '他人清单里的插件本机都有了' : '把别人的特征文件夹直接放进 插件库\\ 后点「重新读取」',
|
|
746
|
+
),
|
|
747
|
+
installedFromOthers.length
|
|
748
|
+
? h(
|
|
749
|
+
'div',
|
|
750
|
+
{ className: 'pau-row-d', style: { marginTop: 10 } },
|
|
751
|
+
`已从共享装上 ${installedFromOthers.length} 个:${installedFromOthers
|
|
752
|
+
.map((p) => p.name)
|
|
753
|
+
.slice(0, 8)
|
|
754
|
+
.join('、')}${installedFromOthers.length > 8 ? ' 等' : ''}`,
|
|
755
|
+
)
|
|
756
|
+
: null,
|
|
757
|
+
(detail && detail.warnings && detail.warnings.length) || (detail && detail.myManifest && detail.myManifest.skipped && detail.myManifest.skipped.length)
|
|
758
|
+
? h(
|
|
759
|
+
'div',
|
|
760
|
+
{ className: 'pau-row-d', style: { marginTop: 10, color: 'var(--dsw-alias-warning,#a16207)' } },
|
|
761
|
+
[
|
|
762
|
+
...((detail && detail.warnings) || []),
|
|
763
|
+
...((detail && detail.myManifest && detail.myManifest.skipped) || []).map(
|
|
764
|
+
(item) => `${item.name}:${item.reason}`,
|
|
765
|
+
),
|
|
766
|
+
].slice(0, 5),
|
|
767
|
+
)
|
|
768
|
+
: null,
|
|
769
|
+
);
|
|
770
|
+
|
|
514
771
|
const historyCard = h(
|
|
515
772
|
'div',
|
|
516
773
|
{ className: 'pau-card' },
|
|
@@ -542,6 +799,7 @@ function makePanel(React) {
|
|
|
542
799
|
masterCard,
|
|
543
800
|
policyCard,
|
|
544
801
|
pluginsCard,
|
|
802
|
+
shareCard,
|
|
545
803
|
restartCard,
|
|
546
804
|
historyCard,
|
|
547
805
|
);
|