@1e0zj/dsh-plugin-mall 0.1.18 → 0.2.1
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 +111 -5
- package/package.json +6 -2
- package/src/cli.js +1638 -0
- package/src/client.js +306 -67
- package/src/github.js +71 -3
- package/src/guard.js +2413 -0
- package/src/index.js +1761 -88
- package/src/installer.js +1585 -77
package/src/client.js
CHANGED
|
@@ -49,6 +49,7 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
".mkt_ok{color:var(--dsw-alias-state-success-primary,#2f855a)}",
|
|
50
50
|
".mkt_badge{display:inline-block;border-radius:999px;padding:1px 8px;font-size:11px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}",
|
|
51
51
|
".mkt_badgeOk{border-color:var(--dsw-alias-state-success-primary,#2f855a);color:var(--dsw-alias-state-success-primary,#2f855a)}",
|
|
52
|
+
".mkt_badgeWarn{border-color:var(--dsw-alias-state-warning-primary,#b7791f);color:var(--dsw-alias-state-warning-primary,#b7791f)}",
|
|
52
53
|
".mkt_badgeBad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}",
|
|
53
54
|
".mkt_check{display:flex;align-items:center;gap:4px;font-size:12.5px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap}",
|
|
54
55
|
".mkt_link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;cursor:pointer}",
|
|
@@ -63,7 +64,14 @@ window.__ModuleLoader__.load({
|
|
|
63
64
|
".mkt_approvePkg{display:flex;flex-direction:column;gap:4px}",
|
|
64
65
|
".mkt_approveName{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-right:8px;overflow-wrap:anywhere}",
|
|
65
66
|
".mkt_approveCmd{max-height:none;margin:0}",
|
|
66
|
-
".mkt_jobDone{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
67
|
+
".mkt_jobDone{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:2px}",
|
|
68
|
+
".mkt_logBlock{display:flex;flex-direction:column;gap:4px;align-items:flex-start}",
|
|
69
|
+
".mkt_issueList{list-style:none;display:flex;flex-direction:column;gap:8px;margin:0;padding:0}",
|
|
70
|
+
".mkt_issue{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-secondary,#fff)}",
|
|
71
|
+
".mkt_issueBlock{border-color:var(--dsw-alias-state-error-primary)}",
|
|
72
|
+
".mkt_issueWarn{border-color:var(--dsw-alias-state-warning-primary,var(--dsw-alias-state-business-primary))}",
|
|
73
|
+
".mkt_issueTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
74
|
+
".mkt_issueDetail{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:4px;line-height:18px;overflow-wrap:anywhere}",
|
|
67
75
|
].join("\n");
|
|
68
76
|
var tagId = "@1e0zj/dsh-plugin-mall/market-tab.css";
|
|
69
77
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
@@ -97,8 +105,28 @@ window.__ModuleLoader__.load({
|
|
|
97
105
|
return "普通依赖";
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
function generateSessionNonce() {
|
|
109
|
+
if (typeof crypto !== "undefined") {
|
|
110
|
+
if (typeof crypto.randomUUID === "function") {
|
|
111
|
+
return "sess_" + crypto.randomUUID();
|
|
112
|
+
}
|
|
113
|
+
if (typeof crypto.getRandomValues === "function") {
|
|
114
|
+
var bytes = new Uint8Array(16);
|
|
115
|
+
crypto.getRandomValues(bytes);
|
|
116
|
+
var hex = "";
|
|
117
|
+
for (var i = 0; i < bytes.length; i++) {
|
|
118
|
+
hex += (bytes[i] < 16 ? "0" : "") + bytes[i].toString(16);
|
|
119
|
+
}
|
|
120
|
+
return "sess_" + hex;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
throw new Error("Secure browser randomness is unavailable; plugin marketplace actions are disabled");
|
|
124
|
+
}
|
|
125
|
+
|
|
100
126
|
// ── job polling ─────────────────────────────────────────────────────────
|
|
101
|
-
|
|
127
|
+
// onPreflightSettled:预检 job(kind=dsh-plugin-preflight)落定时回调,
|
|
128
|
+
// 携带 (spec, report)。safe 由调用方直接续装;有风险由调用方出内联卡片。
|
|
129
|
+
function useJobPolling(call, onSettled, onApprovalToken, onPreflightSettled) {
|
|
102
130
|
var jobsRef = useRef({});
|
|
103
131
|
var _jobs = useState({});
|
|
104
132
|
var jobs = _jobs[0];
|
|
@@ -117,14 +145,26 @@ window.__ModuleLoader__.load({
|
|
|
117
145
|
var old = jobsRef.current[id] || {};
|
|
118
146
|
var wasTerminal = old.status === "completed" || old.status === "failed" || old.status === "killed";
|
|
119
147
|
var nowTerminal = snapshot.status === "completed" || snapshot.status === "failed" || snapshot.status === "killed";
|
|
120
|
-
if (
|
|
121
|
-
try {
|
|
148
|
+
if (snapshot.needsApproval && snapshot.approvalToken && onApprovalToken) {
|
|
149
|
+
try { onApprovalToken(snapshot.spec, snapshot.approvalToken); } catch (e) { /* best effort */ }
|
|
150
|
+
}
|
|
151
|
+
if (!wasTerminal && nowTerminal) {
|
|
152
|
+
if (onSettled) {
|
|
153
|
+
try { onSettled(id, snapshot); } catch (e) { /* best effort */ }
|
|
154
|
+
}
|
|
155
|
+
// 预检 job 落定 → 把结论交给上层后继(只触发一次)
|
|
156
|
+
if (snapshot.kind === "dsh-plugin-preflight" && !old.preflightHandled && onPreflightSettled) {
|
|
157
|
+
jobsRef.current[id] = Object.assign({}, old, { preflightHandled: true });
|
|
158
|
+
try { onPreflightSettled(snapshot.spec, snapshot.extras, id); } catch (e) { /* best effort */ }
|
|
159
|
+
}
|
|
122
160
|
}
|
|
123
161
|
var output = (old.output || "") + (value.output || "");
|
|
124
162
|
jobsRef.current = Object.assign({}, jobsRef.current, { [id]: Object.assign({}, old, {
|
|
125
163
|
status: snapshot.status,
|
|
126
164
|
detail: snapshot.detail,
|
|
127
165
|
needsApproval: snapshot.needsApproval,
|
|
166
|
+
approvalToken: snapshot.approvalToken,
|
|
167
|
+
kind: snapshot.kind,
|
|
128
168
|
output: output,
|
|
129
169
|
}) });
|
|
130
170
|
setJobs(Object.assign({}, jobsRef.current));
|
|
@@ -132,10 +172,20 @@ window.__ModuleLoader__.load({
|
|
|
132
172
|
});
|
|
133
173
|
}, 1200);
|
|
134
174
|
return function () { clearInterval(timer); };
|
|
135
|
-
}, [call, onSettled]);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
175
|
+
}, [call, onSettled, onApprovalToken]);
|
|
176
|
+
// carryFromId:把上一阶段(预检)的日志接过来并撤掉它的条目。一次点击
|
|
177
|
+
// 只应该在面板里留下一个任务,日志连续——而不是 market-1 预检、
|
|
178
|
+
// market-2 安装两条并排,让人以为自己点了两次。
|
|
179
|
+
var track = useCallback(function (id, spec, carryFromId) {
|
|
180
|
+
var next = Object.assign({}, jobsRef.current);
|
|
181
|
+
var carried = "";
|
|
182
|
+
if (carryFromId && next[carryFromId]) {
|
|
183
|
+
carried = next[carryFromId].output || "";
|
|
184
|
+
delete next[carryFromId];
|
|
185
|
+
}
|
|
186
|
+
next[id] = { status: "running", spec: spec, output: carried };
|
|
187
|
+
jobsRef.current = next;
|
|
188
|
+
setJobs(Object.assign({}, next));
|
|
139
189
|
}, []);
|
|
140
190
|
var clear = useCallback(function () {
|
|
141
191
|
jobsRef.current = {};
|
|
@@ -151,8 +201,6 @@ window.__ModuleLoader__.load({
|
|
|
151
201
|
}
|
|
152
202
|
|
|
153
203
|
// ── plugin verification badge ───────────────────────────────────────────
|
|
154
|
-
// verified 来自 /market verify 端点(node 侧拉 raw package.json 判定
|
|
155
|
-
// dsh.bundle/dsh.client 声明,进程内缓存)。unknown 不显示徽章。
|
|
156
204
|
function verifyBadge(verified) {
|
|
157
205
|
if (verified === undefined || verified === null) return null;
|
|
158
206
|
if (verified.hostDeps !== undefined && verified.hostDeps.length > 0) {
|
|
@@ -165,6 +213,19 @@ window.__ModuleLoader__.load({
|
|
|
165
213
|
return null;
|
|
166
214
|
}
|
|
167
215
|
|
|
216
|
+
// ── browsing-time compat badge ──────────────────────────────────────────
|
|
217
|
+
// Static server-side scan of the repo manifest/patch against the profile.
|
|
218
|
+
// Advisory only: the install preflight remains the enforcing gate.
|
|
219
|
+
function compatBadge(compat) {
|
|
220
|
+
if (compat === undefined || compat === null) return null;
|
|
221
|
+
var titles = (compat.issues || []).map(function (item) { return (item.severity === "block" ? "[阻断] " : "[警告] ") + item.title; });
|
|
222
|
+
var tip = (compat.summary || "") + (titles.length > 0 ? "\n" + titles.join("\n") : "") + (compat.patchChecked === false ? "\n补丁未获取,加载冲突未检查" : "");
|
|
223
|
+
if (compat.state === "conflict") return h("span", { className: "mkt_badge mkt_badgeBad", title: tip }, "冲突");
|
|
224
|
+
if (compat.state === "warning") return h("span", { className: "mkt_badge mkt_badgeWarn", title: tip }, "有风险");
|
|
225
|
+
if (compat.state === "compatible") return h("span", { className: "mkt_badge mkt_badgeOk", title: tip }, "适配");
|
|
226
|
+
return h("span", { className: "mkt_badge", title: tip || "兼容性未知" }, "适配未知");
|
|
227
|
+
}
|
|
228
|
+
|
|
168
229
|
// ── repo card ───────────────────────────────────────────────────────────
|
|
169
230
|
function RepoCard(props) {
|
|
170
231
|
var item = props.item;
|
|
@@ -182,6 +243,7 @@ window.__ModuleLoader__.load({
|
|
|
182
243
|
h("div", { className: "mkt_cardHead" },
|
|
183
244
|
h("span", { className: "mkt_name" }, item.fullName),
|
|
184
245
|
verifyBadge(props.verified),
|
|
246
|
+
compatBadge(props.compat),
|
|
185
247
|
item.archived ? h("span", { className: "mkt_badge" }, "archived") : null
|
|
186
248
|
),
|
|
187
249
|
item.description ? h("div", { className: "mkt_desc" }, item.description) : null,
|
|
@@ -200,21 +262,17 @@ window.__ModuleLoader__.load({
|
|
|
200
262
|
}
|
|
201
263
|
|
|
202
264
|
// ── install-script approval ─────────────────────────────────────────────
|
|
203
|
-
// pnpm 默认拦掉依赖的安装期脚本。放行它们 = 让这些命令以用户的权限在用户
|
|
204
|
-
// 机器上运行,而且在任何插件代码加载之前。这个决定属于用户,所以这里如实
|
|
205
|
-
// 摆出「要批准的到底是什么」——包名、版本、确切的命令、以及它究竟是用户
|
|
206
|
-
// 选的那个插件,还是一个他从没听说过的传递依赖。
|
|
207
|
-
// 措辞刻意不写「安全检查」:批准这些脚本,对插件装好之后会做什么一无所证。
|
|
208
265
|
function ApprovalRequest(props) {
|
|
209
266
|
var pkgs = props.needsApproval || [];
|
|
210
267
|
return h("div", { className: "mkt_approve" },
|
|
211
268
|
h("div", { className: "mkt_approveHead" }, "需要你确认:这次安装会执行安装期代码"),
|
|
212
|
-
pkgs.map(function (p) {
|
|
269
|
+
pkgs.map(function (p, index) {
|
|
213
270
|
var facts = [];
|
|
214
271
|
if (typeof p.weeklyDownloads === "number") facts.push("周下载 " + p.weeklyDownloads.toLocaleString());
|
|
215
272
|
facts.push(p.provenance === true ? "有来源证明" : "无来源证明");
|
|
216
273
|
if (typeof p.unpackedSize === "number") facts.push((Math.round(p.unpackedSize / 104857.6) / 10) + " MB");
|
|
217
|
-
|
|
274
|
+
if (p.selector) facts.push("pnpm 标识 " + p.selector);
|
|
275
|
+
return h("div", { key: p.selector || (p.name + ":" + index), className: "mkt_approvePkg" },
|
|
218
276
|
h("div", null,
|
|
219
277
|
h("span", { className: "mkt_approveName" }, p.name + (p.version ? "@" + p.version : "")),
|
|
220
278
|
h("span", { className: "mkt_badge " + (p.direct ? "" : "mkt_badgeBad") },
|
|
@@ -223,6 +281,7 @@ window.__ModuleLoader__.load({
|
|
|
223
281
|
Object.keys(p.scripts || {}).map(function (k) {
|
|
224
282
|
return h("pre", { key: k, className: "mkt_pre mkt_approveCmd" }, k + ": " + p.scripts[k]);
|
|
225
283
|
}),
|
|
284
|
+
p.contentHash ? h("div", { className: "mkt_pre mkt_approveCmd" }, "制品 SHA-256: " + p.contentHash) : null,
|
|
226
285
|
h("div", { className: "mkt_meta" }, facts.join(" · "))
|
|
227
286
|
);
|
|
228
287
|
}),
|
|
@@ -238,6 +297,93 @@ window.__ModuleLoader__.load({
|
|
|
238
297
|
);
|
|
239
298
|
}
|
|
240
299
|
|
|
300
|
+
// ── preflight result card ────────────────────────────────────────────────
|
|
301
|
+
// 预检通过(safe)根本不渲染这张卡片——直接开始安装,不打扰。只有
|
|
302
|
+
// warning / blocked 才出现,且用任务面板同款的内联卡片语言,不弹模态框:
|
|
303
|
+
// 市场里一切任务都在面板里流动,弹窗会打断这个节奏。
|
|
304
|
+
function PreflightCard(props) {
|
|
305
|
+
var report = props.report || {};
|
|
306
|
+
var blocked = report.verdict === "blocked";
|
|
307
|
+
var title = blocked ? "安装被阻止" : "安装存在风险";
|
|
308
|
+
var issues = report.issues || [];
|
|
309
|
+
return h("div", { className: "mkt_card" },
|
|
310
|
+
h("div", { className: "mkt_panelRow" },
|
|
311
|
+
h("p", { className: "mkt_panelTitle" + (blocked ? " mkt_error" : "") }, title),
|
|
312
|
+
h("span", { className: "mkt_meta" }, clip(props.spec, 50))
|
|
313
|
+
),
|
|
314
|
+
report.summary ? h("div", { className: "mkt_desc" }, report.summary) : null,
|
|
315
|
+
issues.length > 0 ? h("ul", { className: "mkt_issueList" },
|
|
316
|
+
issues.map(function (issue) {
|
|
317
|
+
return h("li", {
|
|
318
|
+
key: (issue.code || "") + "-" + (issue.title || ""),
|
|
319
|
+
className: "mkt_issue " + (issue.severity === "block" ? "mkt_issueBlock" : "mkt_issueWarn"),
|
|
320
|
+
},
|
|
321
|
+
h("div", { className: "mkt_issueTitle" }, issue.title),
|
|
322
|
+
issue.detail ? h("div", { className: "mkt_issueDetail" }, issue.detail) : null);
|
|
323
|
+
})
|
|
324
|
+
) : null,
|
|
325
|
+
h("div", { className: "mkt_row" },
|
|
326
|
+
blocked
|
|
327
|
+
? h("button", { className: "mkt_btn mkt_btnSm", onClick: props.onClose }, "关闭")
|
|
328
|
+
: [
|
|
329
|
+
h("button", { key: "cancel", className: "mkt_btn mkt_btnSm", onClick: props.onClose }, "取消"),
|
|
330
|
+
h("button", {
|
|
331
|
+
key: "confirm",
|
|
332
|
+
className: "mkt_btn mkt_btnPrimary mkt_btnSm",
|
|
333
|
+
disabled: props.busy === true,
|
|
334
|
+
onClick: props.onConfirm,
|
|
335
|
+
}, props.busy ? "安装中…" : "我已了解风险,继续安装"),
|
|
336
|
+
]
|
|
337
|
+
)
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function jobKindLabel(kind) {
|
|
342
|
+
if (kind === "dsh-plugin-preflight") return "预检 ";
|
|
343
|
+
if (kind === "dsh-plugin-uninstall") return "卸载 ";
|
|
344
|
+
return "安装 ";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function jobStatusLabel(status) {
|
|
348
|
+
if (status === "completed") return "完成";
|
|
349
|
+
if (status === "failed") return "失败";
|
|
350
|
+
if (status === "killed") return "已取消";
|
|
351
|
+
return "进行中";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function jobStatusClass(status) {
|
|
355
|
+
if (status === "completed") return "mkt_ok";
|
|
356
|
+
if (status === "failed" || status === "killed") return "mkt_error";
|
|
357
|
+
return "";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 任务日志:跑的时候只露尾部几行(够看进度,不霸屏),落定后默认折叠
|
|
361
|
+
// ——成功的日志没人回头读,失败的才需要,点开即可。
|
|
362
|
+
var LOG_TAIL_LINES = 8;
|
|
363
|
+
function JobLog(props) {
|
|
364
|
+
var _open = useState(false);
|
|
365
|
+
var open = _open[0];
|
|
366
|
+
var setOpen = _open[1];
|
|
367
|
+
var text = String(props.output || "").trimEnd();
|
|
368
|
+
if (text.length === 0) return null;
|
|
369
|
+
var lines = text.split("\n");
|
|
370
|
+
var collapsed = props.done && !open;
|
|
371
|
+
var shown = (props.done || lines.length <= LOG_TAIL_LINES) ? lines : lines.slice(-LOG_TAIL_LINES);
|
|
372
|
+
// 展开/收起始终在日志块上方:放在下方的话,点开后按钮会被推到长长的
|
|
373
|
+
// 日志末尾,想收起还得先滚回去。
|
|
374
|
+
return h("div", { className: "mkt_logBlock" },
|
|
375
|
+
props.done
|
|
376
|
+
? h("span", {
|
|
377
|
+
className: "mkt_link",
|
|
378
|
+
onClick: function () { setOpen(!open); },
|
|
379
|
+
}, open ? "收起日志" : "查看日志(" + lines.length + " 行)")
|
|
380
|
+
: lines.length > LOG_TAIL_LINES
|
|
381
|
+
? h("div", { className: "mkt_meta" }, "只显示最后 " + LOG_TAIL_LINES + " 行")
|
|
382
|
+
: null,
|
|
383
|
+
collapsed ? null : h("pre", { className: "mkt_pre" }, shown.join("\n").slice(-4000))
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
241
387
|
// ── jobs panel ──────────────────────────────────────────────────────────
|
|
242
388
|
function JobsPanel(props) {
|
|
243
389
|
var ids = Object.keys(props.jobs);
|
|
@@ -251,21 +397,30 @@ window.__ModuleLoader__.load({
|
|
|
251
397
|
var job = props.jobs[id];
|
|
252
398
|
var done = job.status === "completed" || job.status === "failed" || job.status === "killed";
|
|
253
399
|
return h("div", { key: id, style: { display: "flex", flexDirection: "column", gap: "4px" } },
|
|
400
|
+
// 不再显示 market-N 这种内部 id——对用户没有意义,反而让人以为
|
|
401
|
+
// 自己点了两次。只说清「在做什么 · 到哪一步了」。
|
|
402
|
+
// 状态词单独上色(完成绿、失败/取消红),前半段保持灰——整行
|
|
403
|
+
// 都染色会喧宾夺主,只有结论需要一眼看见。
|
|
254
404
|
h("div", { className: "mkt_meta" },
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// 失败文案,直接给出「要批准什么」和两个按钮。
|
|
405
|
+
jobKindLabel(job.kind) + clip(job.spec || "", 44) + " · ",
|
|
406
|
+
h("span", { className: jobStatusClass(job.status) }, jobStatusLabel(job.status))),
|
|
258
407
|
job.needsApproval && job.needsApproval.length > 0
|
|
259
408
|
? h(ApprovalRequest, {
|
|
260
409
|
needsApproval: job.needsApproval,
|
|
261
410
|
busy: props.approving === job.spec,
|
|
262
|
-
onApprove: function (names) {
|
|
411
|
+
onApprove: function (names) {
|
|
412
|
+
if (typeof props.onDrop === "function") {
|
|
413
|
+
props.onDrop(id);
|
|
414
|
+
}
|
|
415
|
+
props.onApprove(job.spec, names, job.approvalToken);
|
|
416
|
+
},
|
|
263
417
|
onDismiss: function () { props.onDismiss(id); },
|
|
264
418
|
})
|
|
265
419
|
: job.detail ? h("div", { className: "mkt_desc" }, job.detail) : null,
|
|
266
|
-
|
|
420
|
+
// 预检完成不提示重启——它没有改动任何东西,接下来才是安装。
|
|
421
|
+
// 也不再重复一个绿色「完成」:状态行已经写了「· 完成」。
|
|
422
|
+
done && job.status === "completed" && job.kind !== "dsh-plugin-preflight"
|
|
267
423
|
? h("div", { className: "mkt_jobDone" },
|
|
268
|
-
h("span", { className: "mkt_ok" }, "完成"),
|
|
269
424
|
h("button", {
|
|
270
425
|
className: "mkt_btn mkt_btnPrimary mkt_btnSm",
|
|
271
426
|
disabled: props.restarting === true,
|
|
@@ -274,7 +429,7 @@ window.__ModuleLoader__.load({
|
|
|
274
429
|
: job.status === "failed" && !(job.needsApproval && job.needsApproval.length > 0)
|
|
275
430
|
? h("div", { className: "mkt_error" }, "失败,见下方输出")
|
|
276
431
|
: null,
|
|
277
|
-
|
|
432
|
+
h(JobLog, { output: job.output, done: done })
|
|
278
433
|
);
|
|
279
434
|
})
|
|
280
435
|
);
|
|
@@ -311,11 +466,6 @@ window.__ModuleLoader__.load({
|
|
|
311
466
|
h("span", { className: "mkt_desc" }, dep.name + "@" + dep.version),
|
|
312
467
|
h("span", { className: "mkt_depActions" },
|
|
313
468
|
h("span", { className: "mkt_badge" }, kindLabel(dep.kind)),
|
|
314
|
-
// 必须带上版本号。传裸包名等于让 pnpm 自己挑"最新可安装
|
|
315
|
-
// 版本",而 pnpm 11 的 minimumReleaseAge 会拒绝发布不足
|
|
316
|
-
// 24 小时的版本并回落到旧版 —— 按钮写着"更新至 0.12.3",
|
|
317
|
-
// pnpm 却报 "Already up to date",点多少次都不动。
|
|
318
|
-
// 带上版本是明确指定,pnpm 照装并自行记一条豁免。
|
|
319
469
|
upd && upd.hasUpdate ? h("button", {
|
|
320
470
|
className: "mkt_btn mkt_btnSm",
|
|
321
471
|
onClick: function () { props.onInstallSpec(dep.name + "@" + upd.latest); },
|
|
@@ -363,9 +513,13 @@ window.__ModuleLoader__.load({
|
|
|
363
513
|
var loadingMore = _loadingMore[0];
|
|
364
514
|
var setLoadingMore = _loadingMore[1];
|
|
365
515
|
var sentinelRef = useRef(null);
|
|
516
|
+
var resultsRef = useRef(null);
|
|
366
517
|
var _verified = useState({});
|
|
367
518
|
var verified = _verified[0];
|
|
368
519
|
var setVerified = _verified[1];
|
|
520
|
+
var _compat = useState({});
|
|
521
|
+
var compat = _compat[0];
|
|
522
|
+
var setCompat = _compat[1];
|
|
369
523
|
var _verifiedOnly = useState(true);
|
|
370
524
|
var verifiedOnly = _verifiedOnly[0];
|
|
371
525
|
var setVerifiedOnly = _verifiedOnly[1];
|
|
@@ -381,9 +535,19 @@ window.__ModuleLoader__.load({
|
|
|
381
535
|
var _restarting = useState(false);
|
|
382
536
|
var restarting = _restarting[0];
|
|
383
537
|
var setRestarting = _restarting[1];
|
|
538
|
+
var _preflight = useState(null);
|
|
539
|
+
var preflight = _preflight[0];
|
|
540
|
+
var setPreflight = _preflight[1];
|
|
541
|
+
|
|
542
|
+
// Opaque one-shot approval tokens issued from needsApproval outcomes (spec -> token).
|
|
543
|
+
// Cleared on success, unrelated failure, cancel, modal close, or spec change.
|
|
544
|
+
// No boolean warning-consent map is kept.
|
|
545
|
+
var sessionNonce = useRef(generateSessionNonce()).current;
|
|
546
|
+
var approvalTokensRef = useRef({});
|
|
384
547
|
|
|
385
548
|
var call = useCallback(function (endpoint, payload) {
|
|
386
|
-
|
|
549
|
+
var body = Object.assign({ session: sessionNonce }, payload || {});
|
|
550
|
+
return rpc.call("/market", endpoint, body).then(function (res) {
|
|
387
551
|
if (!res || res.ok !== true) {
|
|
388
552
|
var err = res && res.error;
|
|
389
553
|
var msg = err && typeof err === "object" ? (err.message || JSON.stringify(err)) : (err || "market request failed");
|
|
@@ -391,16 +555,17 @@ window.__ModuleLoader__.load({
|
|
|
391
555
|
}
|
|
392
556
|
return res.value;
|
|
393
557
|
});
|
|
394
|
-
}, [rpc]);
|
|
558
|
+
}, [rpc, sessionNonce]);
|
|
395
559
|
|
|
396
|
-
// 每页搜索结果到货后批量验证(node 侧 raw CDN,带缓存),失败静默——
|
|
397
|
-
// 徽章缺失可接受,不该打断浏览。
|
|
398
560
|
var verifyPage = useCallback(function (items) {
|
|
399
561
|
var repos = (items || []).map(function (it) { return it.fullName; });
|
|
400
562
|
if (repos.length === 0) return;
|
|
401
563
|
call("verify", { repos: repos }).then(function (value) {
|
|
402
564
|
setVerified(function (prev) { return Object.assign({}, prev, value.results); });
|
|
403
|
-
}).catch(function () { /*
|
|
565
|
+
}).catch(function () { /* optional */ });
|
|
566
|
+
call("compat", { repos: repos }).then(function (value) {
|
|
567
|
+
setCompat(function (prev) { return Object.assign({}, prev, value.results); });
|
|
568
|
+
}).catch(function () { /* optional */ });
|
|
404
569
|
}, [call]);
|
|
405
570
|
|
|
406
571
|
var doSearch = useCallback(function () {
|
|
@@ -412,6 +577,7 @@ window.__ModuleLoader__.load({
|
|
|
412
577
|
call("search", { query: query, sort: sort, perPage: 20 }).then(function (value) {
|
|
413
578
|
setResults(value);
|
|
414
579
|
setVerified({});
|
|
580
|
+
setCompat({});
|
|
415
581
|
verifyPage(value.items);
|
|
416
582
|
}).catch(function (e) {
|
|
417
583
|
setError(errorText(e));
|
|
@@ -420,17 +586,11 @@ window.__ModuleLoader__.load({
|
|
|
420
586
|
});
|
|
421
587
|
}, [call, query, sort, verifyPage]);
|
|
422
588
|
|
|
423
|
-
// 无限滚动:哨兵进入视口时拉下一页并追加。GitHub topic 的翻页间数据
|
|
424
|
-
// 可能移动造成重复,按 fullName 去重;已显示数追上 total 即到底。
|
|
425
589
|
var canLoadMore = results !== null && results.items.length < results.total && !reachedLimit;
|
|
426
590
|
var loadMoreLock = useRef(false);
|
|
427
591
|
var loadMore = useCallback(function () {
|
|
428
592
|
if (!canLoadMore || loading || loadingMore) return;
|
|
429
|
-
// 同一 tick 内 observer 可能连续触发两次,state 更新不阻塞闭包读值,
|
|
430
|
-
// 用 ref 做重入闸。
|
|
431
593
|
if (loadMoreLock.current) return;
|
|
432
|
-
// 限流熔断:失败后 60s 内不再自动请求(哨兵重渲染会反复触发
|
|
433
|
-
// IntersectionObserver,不熔断会连环 500 直到限流窗口过去)。
|
|
434
594
|
if (Date.now() < retryAt) return;
|
|
435
595
|
loadMoreLock.current = true;
|
|
436
596
|
setLoadingMore(true);
|
|
@@ -456,7 +616,7 @@ window.__ModuleLoader__.load({
|
|
|
456
616
|
loadMoreLock.current = false;
|
|
457
617
|
setLoadingMore(false);
|
|
458
618
|
});
|
|
459
|
-
}, [call, canLoadMore, loading, loadingMore, page, query, sort, retryAt]);
|
|
619
|
+
}, [call, canLoadMore, loading, loadingMore, page, query, sort, retryAt, verifyPage]);
|
|
460
620
|
|
|
461
621
|
useEffect(function () {
|
|
462
622
|
var node = sentinelRef.current;
|
|
@@ -480,8 +640,31 @@ window.__ModuleLoader__.load({
|
|
|
480
640
|
});
|
|
481
641
|
}, [call]);
|
|
482
642
|
|
|
483
|
-
|
|
484
|
-
|
|
643
|
+
var onJobSettled = useCallback(function (id, snapshot) {
|
|
644
|
+
refreshInstalled();
|
|
645
|
+
// 装/卸改变了 profile,旧适配徽标全部失效,按当前列表重扫。
|
|
646
|
+
setCompat({});
|
|
647
|
+
var items = resultsRef.current ? resultsRef.current.items : [];
|
|
648
|
+
if (items.length > 0) verifyPage(items);
|
|
649
|
+
if (snapshot && (snapshot.status === "completed" || (snapshot.status === "failed" && !snapshot.needsApproval) || snapshot.status === "killed")) {
|
|
650
|
+
if (snapshot.spec) delete approvalTokensRef.current[snapshot.spec];
|
|
651
|
+
}
|
|
652
|
+
}, [refreshInstalled, verifyPage]);
|
|
653
|
+
|
|
654
|
+
var onApprovalToken = useCallback(function (spec, token) {
|
|
655
|
+
if (spec && token) {
|
|
656
|
+
approvalTokensRef.current[spec] = token;
|
|
657
|
+
}
|
|
658
|
+
}, []);
|
|
659
|
+
|
|
660
|
+
// 预检落定的后继动作要用到下面才定义的 doRawInstall,用 ref 转接,
|
|
661
|
+
// 免得为了闭包顺序把整块逻辑往上搬。
|
|
662
|
+
var preflightHandlerRef = useRef(null);
|
|
663
|
+
var onPreflightSettled = useCallback(function (spec, report, jobId) {
|
|
664
|
+
if (preflightHandlerRef.current) preflightHandlerRef.current(spec, report, jobId);
|
|
665
|
+
}, []);
|
|
666
|
+
|
|
667
|
+
var polling = useJobPolling(call, onJobSettled, onApprovalToken, onPreflightSettled);
|
|
485
668
|
var track = polling.track;
|
|
486
669
|
var jobs = polling.jobs;
|
|
487
670
|
var clearJobs = polling.clear;
|
|
@@ -492,20 +675,17 @@ window.__ModuleLoader__.load({
|
|
|
492
675
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
493
676
|
}, []);
|
|
494
677
|
|
|
495
|
-
|
|
496
|
-
// (已装面板的更新按钮)。后端会把同源发布的 github spec 改写为 npm
|
|
497
|
-
// tarball 安装(更快),job 里记录的是最终 spec。
|
|
498
|
-
var doInstallSpec = useCallback(function (spec, allowBuildScripts) {
|
|
678
|
+
var doRawInstall = useCallback(function (spec, extra, carryFromId) {
|
|
499
679
|
setInstalling(function (prev) {
|
|
500
680
|
var next = Object.assign({}, prev, { [spec]: true });
|
|
501
681
|
return next;
|
|
502
682
|
});
|
|
503
683
|
setError(null);
|
|
504
|
-
var payload = { spec: spec };
|
|
505
|
-
if (allowBuildScripts) payload.allowBuildScripts = allowBuildScripts;
|
|
684
|
+
var payload = Object.assign({ spec: spec }, extra || {});
|
|
506
685
|
call("install", payload).then(function (value) {
|
|
507
|
-
track(value.jobId, spec);
|
|
686
|
+
track(value.jobId, spec, carryFromId);
|
|
508
687
|
}).catch(function (e) {
|
|
688
|
+
delete approvalTokensRef.current[spec];
|
|
509
689
|
setError(errorText(e));
|
|
510
690
|
}).finally(function () {
|
|
511
691
|
setInstalling(function (prev) {
|
|
@@ -516,18 +696,48 @@ window.__ModuleLoader__.load({
|
|
|
516
696
|
});
|
|
517
697
|
}, [call, track]);
|
|
518
698
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
699
|
+
var doApprove = useCallback(function (spec, names, token) {
|
|
700
|
+
var extra = { allowBuildScripts: names };
|
|
701
|
+
var apprToken = token || approvalTokensRef.current[spec];
|
|
702
|
+
if (apprToken) {
|
|
703
|
+
extra.approvalToken = apprToken;
|
|
704
|
+
delete approvalTokensRef.current[spec];
|
|
705
|
+
}
|
|
706
|
+
doRawInstall(spec, extra);
|
|
707
|
+
}, [doRawInstall]);
|
|
708
|
+
|
|
709
|
+
// 预检落定后的去向:safe 直接续装,其余出内联风险卡片。
|
|
710
|
+
useEffect(function () {
|
|
711
|
+
preflightHandlerRef.current = function (spec, report, jobId) {
|
|
712
|
+
if (!report || !report.verdict) {
|
|
713
|
+
setError("预检没有返回结论,请重试");
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (report.verdict === "safe") {
|
|
717
|
+
// 安全:接过预检的日志、撤掉预检条目,面板上只剩这一个任务。
|
|
718
|
+
doRawInstall(spec, {}, jobId);
|
|
719
|
+
} else {
|
|
720
|
+
setPreflight({ spec: spec, report: report, jobId: jobId });
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
}, [doRawInstall]);
|
|
724
|
+
|
|
725
|
+
// 点安装 = 立刻起一个预检 job,面板马上有东西看、日志实时流。
|
|
726
|
+
// 结论由轮询经 onPreflightSettled 交回上面的 handler,这里不等待。
|
|
727
|
+
var preflightAndInstall = useCallback(function (spec) {
|
|
728
|
+
delete approvalTokensRef.current[spec];
|
|
729
|
+
setError(null);
|
|
730
|
+
call("preflight", { spec: spec }).then(function (value) {
|
|
731
|
+
track(value.jobId, value.spec || spec);
|
|
732
|
+
}).catch(function (e) {
|
|
733
|
+
setError(errorText(e));
|
|
734
|
+
});
|
|
735
|
+
}, [call, track]);
|
|
524
736
|
|
|
525
737
|
var doInstall = useCallback(function (repo) {
|
|
526
|
-
|
|
527
|
-
}, [
|
|
738
|
+
preflightAndInstall("github:" + repo);
|
|
739
|
+
}, [preflightAndInstall]);
|
|
528
740
|
|
|
529
|
-
// 一键重启:loopback-only 的 /market restart 端点会 detached 拉起新的
|
|
530
|
-
// dsh 进程后退出当前进程;这里轮询 host 恢复后自动刷新页面。
|
|
531
741
|
var doRestart = useCallback(function () {
|
|
532
742
|
if (typeof window !== "undefined" && typeof window.confirm === "function") {
|
|
533
743
|
if (window.confirm("重启 dsh?正在进行的任务会中断。") !== true) return;
|
|
@@ -547,7 +757,7 @@ window.__ModuleLoader__.load({
|
|
|
547
757
|
rpc.call("/market", "installed", {}).then(function () {
|
|
548
758
|
clearInterval(ping);
|
|
549
759
|
window.location.reload();
|
|
550
|
-
}).catch(function () { /* host
|
|
760
|
+
}).catch(function () { /* host restarting */ });
|
|
551
761
|
}, 3000);
|
|
552
762
|
}).catch(function (e) {
|
|
553
763
|
setRestarting(false);
|
|
@@ -556,6 +766,7 @@ window.__ModuleLoader__.load({
|
|
|
556
766
|
}, [call, rpc]);
|
|
557
767
|
|
|
558
768
|
var doUninstall = useCallback(function (name) {
|
|
769
|
+
delete approvalTokensRef.current[name];
|
|
559
770
|
setRemoving(function (prev) {
|
|
560
771
|
var next = Object.assign({}, prev, { [name]: true });
|
|
561
772
|
return next;
|
|
@@ -578,16 +789,13 @@ window.__ModuleLoader__.load({
|
|
|
578
789
|
refreshInstalled();
|
|
579
790
|
}, [refreshInstalled]);
|
|
580
791
|
|
|
581
|
-
// 任务面板位置固定(已装面板之后):不随任务状态在顶部/底部之间
|
|
582
|
-
// 跳——npm 安装几秒即完成,"完成即落底"曾让结果凭空消失。
|
|
583
|
-
// "只看已验证"开关下的可见项:verify 判定 bundle/client 的才算插件。
|
|
584
|
-
// verifyPending:当前加载的仓库里还有未验证完的(verified map 尚未覆盖)。
|
|
585
792
|
var visibleItems = results === null ? [] : results.items.filter(function (it) {
|
|
586
793
|
if (verifiedOnly !== true) return true;
|
|
587
794
|
var v = verified[it.fullName];
|
|
588
795
|
return v !== undefined && (v.kind === "bundle" || v.kind === "client")
|
|
589
796
|
&& !(v.hostDeps !== undefined && v.hostDeps.length > 0);
|
|
590
797
|
});
|
|
798
|
+
resultsRef.current = results;
|
|
591
799
|
var verifyPending = results !== null && results.items.some(function (it) { return verified[it.fullName] === undefined; });
|
|
592
800
|
|
|
593
801
|
return h("div", { className: "mkt_root" },
|
|
@@ -620,16 +828,46 @@ window.__ModuleLoader__.load({
|
|
|
620
828
|
"只看已验证插件")
|
|
621
829
|
),
|
|
622
830
|
error ? h("div", { className: "mkt_error" }, error) : null,
|
|
623
|
-
h(InstalledPanel, { installed: installed, removing: removing, updates: updates, onUninstall: doUninstall, onInstallSpec:
|
|
831
|
+
h(InstalledPanel, { installed: installed, removing: removing, updates: updates, onUninstall: doUninstall, onInstallSpec: preflightAndInstall }),
|
|
624
832
|
h(JobsPanel, {
|
|
625
833
|
jobs: jobs,
|
|
626
|
-
onClear:
|
|
834
|
+
onClear: function () {
|
|
835
|
+
Object.keys(jobs).forEach(function (id) {
|
|
836
|
+
var job = jobs[id];
|
|
837
|
+
var token = job && job.approvalToken ? job.approvalToken : (job && job.spec ? approvalTokensRef.current[job.spec] : undefined);
|
|
838
|
+
call("jobDismiss", { jobId: id, token: token }).catch(function () {});
|
|
839
|
+
});
|
|
840
|
+
approvalTokensRef.current = {};
|
|
841
|
+
clearJobs();
|
|
842
|
+
},
|
|
627
843
|
onApprove: doApprove,
|
|
628
|
-
onDismiss:
|
|
844
|
+
onDismiss: function (id) {
|
|
845
|
+
var job = jobs[id];
|
|
846
|
+
var token = job && job.approvalToken ? job.approvalToken : (job && job.spec ? approvalTokensRef.current[job.spec] : undefined);
|
|
847
|
+
if (job && job.spec) delete approvalTokensRef.current[job.spec];
|
|
848
|
+
call("jobDismiss", { jobId: id, token: token }).catch(function () {});
|
|
849
|
+
dropJob(id);
|
|
850
|
+
},
|
|
851
|
+
onDrop: dropJob,
|
|
629
852
|
onRestart: doRestart,
|
|
630
853
|
restarting: restarting,
|
|
631
854
|
approving: Object.keys(installing).filter(function (s) { return installing[s]; })[0],
|
|
632
855
|
}),
|
|
856
|
+
preflight ? h(PreflightCard, {
|
|
857
|
+
spec: preflight.spec,
|
|
858
|
+
report: preflight.report,
|
|
859
|
+
busy: installing[preflight.spec] === true,
|
|
860
|
+
onConfirm: function () {
|
|
861
|
+
var spec = preflight.spec;
|
|
862
|
+
var carry = preflight.jobId;
|
|
863
|
+
setPreflight(null);
|
|
864
|
+
doRawInstall(spec, { acceptWarnings: true }, carry);
|
|
865
|
+
},
|
|
866
|
+
onClose: function () {
|
|
867
|
+
if (preflight && preflight.spec) delete approvalTokensRef.current[preflight.spec];
|
|
868
|
+
setPreflight(null);
|
|
869
|
+
},
|
|
870
|
+
}) : null,
|
|
633
871
|
h("div", { className: "mkt_list" },
|
|
634
872
|
results == null
|
|
635
873
|
? h("div", { className: "mkt_meta mkt_listHead" }, loading ? "正在加载最热插件…" : "—")
|
|
@@ -657,6 +895,7 @@ window.__ModuleLoader__.load({
|
|
|
657
895
|
installing: installing[item.fullName] === true || installing["github:" + item.fullName] === true,
|
|
658
896
|
installJob: installJob,
|
|
659
897
|
verified: vInfo,
|
|
898
|
+
compat: compat[item.fullName],
|
|
660
899
|
alreadyInstalled: alreadyInstalled,
|
|
661
900
|
onInstall: doInstall,
|
|
662
901
|
});
|