@1e0zj/dsh-plugin-mall 0.1.17 → 0.2.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/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}",
@@ -64,6 +65,12 @@ window.__ModuleLoader__.load({
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
67
  ".mkt_jobDone{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
68
+ ".mkt_issueList{list-style:none;display:flex;flex-direction:column;gap:8px;margin:0;padding:0}",
69
+ ".mkt_issue{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-secondary,#fff)}",
70
+ ".mkt_issueBlock{border-color:var(--dsw-alias-state-error-primary)}",
71
+ ".mkt_issueWarn{border-color:var(--dsw-alias-state-warning-primary,var(--dsw-alias-state-business-primary))}",
72
+ ".mkt_issueTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}",
73
+ ".mkt_issueDetail{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:4px;line-height:18px;overflow-wrap:anywhere}",
67
74
  ].join("\n");
68
75
  var tagId = "@1e0zj/dsh-plugin-mall/market-tab.css";
69
76
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
@@ -97,8 +104,26 @@ window.__ModuleLoader__.load({
97
104
  return "普通依赖";
98
105
  }
99
106
 
107
+ function generateSessionNonce() {
108
+ if (typeof crypto !== "undefined") {
109
+ if (typeof crypto.randomUUID === "function") {
110
+ return "sess_" + crypto.randomUUID();
111
+ }
112
+ if (typeof crypto.getRandomValues === "function") {
113
+ var bytes = new Uint8Array(16);
114
+ crypto.getRandomValues(bytes);
115
+ var hex = "";
116
+ for (var i = 0; i < bytes.length; i++) {
117
+ hex += (bytes[i] < 16 ? "0" : "") + bytes[i].toString(16);
118
+ }
119
+ return "sess_" + hex;
120
+ }
121
+ }
122
+ throw new Error("Secure browser randomness is unavailable; plugin marketplace actions are disabled");
123
+ }
124
+
100
125
  // ── job polling ─────────────────────────────────────────────────────────
101
- function useJobPolling(call, onSettled) {
126
+ function useJobPolling(call, onSettled, onApprovalToken) {
102
127
  var jobsRef = useRef({});
103
128
  var _jobs = useState({});
104
129
  var jobs = _jobs[0];
@@ -117,14 +142,18 @@ window.__ModuleLoader__.load({
117
142
  var old = jobsRef.current[id] || {};
118
143
  var wasTerminal = old.status === "completed" || old.status === "failed" || old.status === "killed";
119
144
  var nowTerminal = snapshot.status === "completed" || snapshot.status === "failed" || snapshot.status === "killed";
145
+ if (snapshot.needsApproval && snapshot.approvalToken && onApprovalToken) {
146
+ try { onApprovalToken(snapshot.spec, snapshot.approvalToken); } catch (e) { /* best effort */ }
147
+ }
120
148
  if (!wasTerminal && nowTerminal && onSettled) {
121
- try { onSettled(id); } catch (e) { /* best effort */ }
149
+ try { onSettled(id, snapshot); } catch (e) { /* best effort */ }
122
150
  }
123
151
  var output = (old.output || "") + (value.output || "");
124
152
  jobsRef.current = Object.assign({}, jobsRef.current, { [id]: Object.assign({}, old, {
125
153
  status: snapshot.status,
126
154
  detail: snapshot.detail,
127
155
  needsApproval: snapshot.needsApproval,
156
+ approvalToken: snapshot.approvalToken,
128
157
  output: output,
129
158
  }) });
130
159
  setJobs(Object.assign({}, jobsRef.current));
@@ -132,7 +161,7 @@ window.__ModuleLoader__.load({
132
161
  });
133
162
  }, 1200);
134
163
  return function () { clearInterval(timer); };
135
- }, [call, onSettled]);
164
+ }, [call, onSettled, onApprovalToken]);
136
165
  var track = useCallback(function (id, spec) {
137
166
  jobsRef.current = Object.assign({}, jobsRef.current, { [id]: { status: "running", spec: spec, output: "" } });
138
167
  setJobs(Object.assign({}, jobsRef.current));
@@ -151,8 +180,6 @@ window.__ModuleLoader__.load({
151
180
  }
152
181
 
153
182
  // ── plugin verification badge ───────────────────────────────────────────
154
- // verified 来自 /market verify 端点(node 侧拉 raw package.json 判定
155
- // dsh.bundle/dsh.client 声明,进程内缓存)。unknown 不显示徽章。
156
183
  function verifyBadge(verified) {
157
184
  if (verified === undefined || verified === null) return null;
158
185
  if (verified.hostDeps !== undefined && verified.hostDeps.length > 0) {
@@ -165,6 +192,19 @@ window.__ModuleLoader__.load({
165
192
  return null;
166
193
  }
167
194
 
195
+ // ── browsing-time compat badge ──────────────────────────────────────────
196
+ // Static server-side scan of the repo manifest/patch against the profile.
197
+ // Advisory only: the install preflight remains the enforcing gate.
198
+ function compatBadge(compat) {
199
+ if (compat === undefined || compat === null) return null;
200
+ var titles = (compat.issues || []).map(function (item) { return (item.severity === "block" ? "[阻断] " : "[警告] ") + item.title; });
201
+ var tip = (compat.summary || "") + (titles.length > 0 ? "\n" + titles.join("\n") : "") + (compat.patchChecked === false ? "\n补丁未获取,加载冲突未检查" : "");
202
+ if (compat.state === "conflict") return h("span", { className: "mkt_badge mkt_badgeBad", title: tip }, "冲突");
203
+ if (compat.state === "warning") return h("span", { className: "mkt_badge mkt_badgeWarn", title: tip }, "有风险");
204
+ if (compat.state === "compatible") return h("span", { className: "mkt_badge mkt_badgeOk", title: tip }, "适配");
205
+ return h("span", { className: "mkt_badge", title: tip || "兼容性未知" }, "适配未知");
206
+ }
207
+
168
208
  // ── repo card ───────────────────────────────────────────────────────────
169
209
  function RepoCard(props) {
170
210
  var item = props.item;
@@ -182,6 +222,7 @@ window.__ModuleLoader__.load({
182
222
  h("div", { className: "mkt_cardHead" },
183
223
  h("span", { className: "mkt_name" }, item.fullName),
184
224
  verifyBadge(props.verified),
225
+ compatBadge(props.compat),
185
226
  item.archived ? h("span", { className: "mkt_badge" }, "archived") : null
186
227
  ),
187
228
  item.description ? h("div", { className: "mkt_desc" }, item.description) : null,
@@ -200,21 +241,17 @@ window.__ModuleLoader__.load({
200
241
  }
201
242
 
202
243
  // ── install-script approval ─────────────────────────────────────────────
203
- // pnpm 默认拦掉依赖的安装期脚本。放行它们 = 让这些命令以用户的权限在用户
204
- // 机器上运行,而且在任何插件代码加载之前。这个决定属于用户,所以这里如实
205
- // 摆出「要批准的到底是什么」——包名、版本、确切的命令、以及它究竟是用户
206
- // 选的那个插件,还是一个他从没听说过的传递依赖。
207
- // 措辞刻意不写「安全检查」:批准这些脚本,对插件装好之后会做什么一无所证。
208
244
  function ApprovalRequest(props) {
209
245
  var pkgs = props.needsApproval || [];
210
246
  return h("div", { className: "mkt_approve" },
211
247
  h("div", { className: "mkt_approveHead" }, "需要你确认:这次安装会执行安装期代码"),
212
- pkgs.map(function (p) {
248
+ pkgs.map(function (p, index) {
213
249
  var facts = [];
214
250
  if (typeof p.weeklyDownloads === "number") facts.push("周下载 " + p.weeklyDownloads.toLocaleString());
215
251
  facts.push(p.provenance === true ? "有来源证明" : "无来源证明");
216
252
  if (typeof p.unpackedSize === "number") facts.push((Math.round(p.unpackedSize / 104857.6) / 10) + " MB");
217
- return h("div", { key: p.name, className: "mkt_approvePkg" },
253
+ if (p.selector) facts.push("pnpm 标识 " + p.selector);
254
+ return h("div", { key: p.selector || (p.name + ":" + index), className: "mkt_approvePkg" },
218
255
  h("div", null,
219
256
  h("span", { className: "mkt_approveName" }, p.name + (p.version ? "@" + p.version : "")),
220
257
  h("span", { className: "mkt_badge " + (p.direct ? "" : "mkt_badgeBad") },
@@ -223,6 +260,7 @@ window.__ModuleLoader__.load({
223
260
  Object.keys(p.scripts || {}).map(function (k) {
224
261
  return h("pre", { key: k, className: "mkt_pre mkt_approveCmd" }, k + ": " + p.scripts[k]);
225
262
  }),
263
+ p.contentHash ? h("div", { className: "mkt_pre mkt_approveCmd" }, "制品 SHA-256: " + p.contentHash) : null,
226
264
  h("div", { className: "mkt_meta" }, facts.join(" · "))
227
265
  );
228
266
  }),
@@ -238,6 +276,47 @@ window.__ModuleLoader__.load({
238
276
  );
239
277
  }
240
278
 
279
+ // ── preflight result card ────────────────────────────────────────────────
280
+ // 预检通过(safe)根本不渲染这张卡片——直接开始安装,不打扰。只有
281
+ // warning / blocked 才出现,且用任务面板同款的内联卡片语言,不弹模态框:
282
+ // 市场里一切任务都在面板里流动,弹窗会打断这个节奏。
283
+ function PreflightCard(props) {
284
+ var report = props.report || {};
285
+ var blocked = report.verdict === "blocked";
286
+ var title = blocked ? "安装被阻止" : "安装存在风险";
287
+ var issues = report.issues || [];
288
+ return h("div", { className: "mkt_card" },
289
+ h("div", { className: "mkt_panelRow" },
290
+ h("p", { className: "mkt_panelTitle" + (blocked ? " mkt_error" : "") }, title),
291
+ h("span", { className: "mkt_meta" }, clip(props.spec, 50))
292
+ ),
293
+ report.summary ? h("div", { className: "mkt_desc" }, report.summary) : null,
294
+ issues.length > 0 ? h("ul", { className: "mkt_issueList" },
295
+ issues.map(function (issue) {
296
+ return h("li", {
297
+ key: (issue.code || "") + "-" + (issue.title || ""),
298
+ className: "mkt_issue " + (issue.severity === "block" ? "mkt_issueBlock" : "mkt_issueWarn"),
299
+ },
300
+ h("div", { className: "mkt_issueTitle" }, issue.title),
301
+ issue.detail ? h("div", { className: "mkt_issueDetail" }, issue.detail) : null);
302
+ })
303
+ ) : null,
304
+ h("div", { className: "mkt_row" },
305
+ blocked
306
+ ? h("button", { className: "mkt_btn mkt_btnSm", onClick: props.onClose }, "关闭")
307
+ : [
308
+ h("button", { key: "cancel", className: "mkt_btn mkt_btnSm", onClick: props.onClose }, "取消"),
309
+ h("button", {
310
+ key: "confirm",
311
+ className: "mkt_btn mkt_btnPrimary mkt_btnSm",
312
+ disabled: props.busy === true,
313
+ onClick: props.onConfirm,
314
+ }, props.busy ? "安装中…" : "我已了解风险,继续安装"),
315
+ ]
316
+ )
317
+ );
318
+ }
319
+
241
320
  // ── jobs panel ──────────────────────────────────────────────────────────
242
321
  function JobsPanel(props) {
243
322
  var ids = Object.keys(props.jobs);
@@ -253,13 +332,16 @@ window.__ModuleLoader__.load({
253
332
  return h("div", { key: id, style: { display: "flex", flexDirection: "column", gap: "4px" } },
254
333
  h("div", { className: "mkt_meta" },
255
334
  id + " · " + clip(job.spec || "", 40) + " · " + (job.status || "running")),
256
- // 待批准不是失败:什么都没执行,profile 也没动过。所以不显示
257
- // 失败文案,直接给出「要批准什么」和两个按钮。
258
335
  job.needsApproval && job.needsApproval.length > 0
259
336
  ? h(ApprovalRequest, {
260
337
  needsApproval: job.needsApproval,
261
338
  busy: props.approving === job.spec,
262
- onApprove: function (names) { props.onDismiss(id); props.onApprove(job.spec, names); },
339
+ onApprove: function (names) {
340
+ if (typeof props.onDrop === "function") {
341
+ props.onDrop(id);
342
+ }
343
+ props.onApprove(job.spec, names, job.approvalToken);
344
+ },
263
345
  onDismiss: function () { props.onDismiss(id); },
264
346
  })
265
347
  : job.detail ? h("div", { className: "mkt_desc" }, job.detail) : null,
@@ -311,11 +393,6 @@ window.__ModuleLoader__.load({
311
393
  h("span", { className: "mkt_desc" }, dep.name + "@" + dep.version),
312
394
  h("span", { className: "mkt_depActions" },
313
395
  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
396
  upd && upd.hasUpdate ? h("button", {
320
397
  className: "mkt_btn mkt_btnSm",
321
398
  onClick: function () { props.onInstallSpec(dep.name + "@" + upd.latest); },
@@ -363,9 +440,13 @@ window.__ModuleLoader__.load({
363
440
  var loadingMore = _loadingMore[0];
364
441
  var setLoadingMore = _loadingMore[1];
365
442
  var sentinelRef = useRef(null);
443
+ var resultsRef = useRef(null);
366
444
  var _verified = useState({});
367
445
  var verified = _verified[0];
368
446
  var setVerified = _verified[1];
447
+ var _compat = useState({});
448
+ var compat = _compat[0];
449
+ var setCompat = _compat[1];
369
450
  var _verifiedOnly = useState(true);
370
451
  var verifiedOnly = _verifiedOnly[0];
371
452
  var setVerifiedOnly = _verifiedOnly[1];
@@ -381,9 +462,19 @@ window.__ModuleLoader__.load({
381
462
  var _restarting = useState(false);
382
463
  var restarting = _restarting[0];
383
464
  var setRestarting = _restarting[1];
465
+ var _preflight = useState(null);
466
+ var preflight = _preflight[0];
467
+ var setPreflight = _preflight[1];
468
+
469
+ // Opaque one-shot approval tokens issued from needsApproval outcomes (spec -> token).
470
+ // Cleared on success, unrelated failure, cancel, modal close, or spec change.
471
+ // No boolean warning-consent map is kept.
472
+ var sessionNonce = useRef(generateSessionNonce()).current;
473
+ var approvalTokensRef = useRef({});
384
474
 
385
475
  var call = useCallback(function (endpoint, payload) {
386
- return rpc.call("/market", endpoint, payload || {}).then(function (res) {
476
+ var body = Object.assign({ session: sessionNonce }, payload || {});
477
+ return rpc.call("/market", endpoint, body).then(function (res) {
387
478
  if (!res || res.ok !== true) {
388
479
  var err = res && res.error;
389
480
  var msg = err && typeof err === "object" ? (err.message || JSON.stringify(err)) : (err || "market request failed");
@@ -391,16 +482,17 @@ window.__ModuleLoader__.load({
391
482
  }
392
483
  return res.value;
393
484
  });
394
- }, [rpc]);
485
+ }, [rpc, sessionNonce]);
395
486
 
396
- // 每页搜索结果到货后批量验证(node 侧 raw CDN,带缓存),失败静默——
397
- // 徽章缺失可接受,不该打断浏览。
398
487
  var verifyPage = useCallback(function (items) {
399
488
  var repos = (items || []).map(function (it) { return it.fullName; });
400
489
  if (repos.length === 0) return;
401
490
  call("verify", { repos: repos }).then(function (value) {
402
491
  setVerified(function (prev) { return Object.assign({}, prev, value.results); });
403
- }).catch(function () { /* 徽章缺失即可 */ });
492
+ }).catch(function () { /* optional */ });
493
+ call("compat", { repos: repos }).then(function (value) {
494
+ setCompat(function (prev) { return Object.assign({}, prev, value.results); });
495
+ }).catch(function () { /* optional */ });
404
496
  }, [call]);
405
497
 
406
498
  var doSearch = useCallback(function () {
@@ -412,6 +504,7 @@ window.__ModuleLoader__.load({
412
504
  call("search", { query: query, sort: sort, perPage: 20 }).then(function (value) {
413
505
  setResults(value);
414
506
  setVerified({});
507
+ setCompat({});
415
508
  verifyPage(value.items);
416
509
  }).catch(function (e) {
417
510
  setError(errorText(e));
@@ -420,17 +513,11 @@ window.__ModuleLoader__.load({
420
513
  });
421
514
  }, [call, query, sort, verifyPage]);
422
515
 
423
- // 无限滚动:哨兵进入视口时拉下一页并追加。GitHub topic 的翻页间数据
424
- // 可能移动造成重复,按 fullName 去重;已显示数追上 total 即到底。
425
516
  var canLoadMore = results !== null && results.items.length < results.total && !reachedLimit;
426
517
  var loadMoreLock = useRef(false);
427
518
  var loadMore = useCallback(function () {
428
519
  if (!canLoadMore || loading || loadingMore) return;
429
- // 同一 tick 内 observer 可能连续触发两次,state 更新不阻塞闭包读值,
430
- // 用 ref 做重入闸。
431
520
  if (loadMoreLock.current) return;
432
- // 限流熔断:失败后 60s 内不再自动请求(哨兵重渲染会反复触发
433
- // IntersectionObserver,不熔断会连环 500 直到限流窗口过去)。
434
521
  if (Date.now() < retryAt) return;
435
522
  loadMoreLock.current = true;
436
523
  setLoadingMore(true);
@@ -456,7 +543,7 @@ window.__ModuleLoader__.load({
456
543
  loadMoreLock.current = false;
457
544
  setLoadingMore(false);
458
545
  });
459
- }, [call, canLoadMore, loading, loadingMore, page, query, sort, retryAt]);
546
+ }, [call, canLoadMore, loading, loadingMore, page, query, sort, retryAt, verifyPage]);
460
547
 
461
548
  useEffect(function () {
462
549
  var node = sentinelRef.current;
@@ -480,8 +567,24 @@ window.__ModuleLoader__.load({
480
567
  });
481
568
  }, [call]);
482
569
 
483
- // 点开即自动搜索(空关键词 = 最热插件),安装任务结束自动刷新已装列表。
484
- var polling = useJobPolling(call, refreshInstalled);
570
+ var onJobSettled = useCallback(function (id, snapshot) {
571
+ refreshInstalled();
572
+ // 装/卸改变了 profile,旧适配徽标全部失效,按当前列表重扫。
573
+ setCompat({});
574
+ var items = resultsRef.current ? resultsRef.current.items : [];
575
+ if (items.length > 0) verifyPage(items);
576
+ if (snapshot && (snapshot.status === "completed" || (snapshot.status === "failed" && !snapshot.needsApproval) || snapshot.status === "killed")) {
577
+ if (snapshot.spec) delete approvalTokensRef.current[snapshot.spec];
578
+ }
579
+ }, [refreshInstalled, verifyPage]);
580
+
581
+ var onApprovalToken = useCallback(function (spec, token) {
582
+ if (spec && token) {
583
+ approvalTokensRef.current[spec] = token;
584
+ }
585
+ }, []);
586
+
587
+ var polling = useJobPolling(call, onJobSettled, onApprovalToken);
485
588
  var track = polling.track;
486
589
  var jobs = polling.jobs;
487
590
  var clearJobs = polling.clear;
@@ -492,20 +595,17 @@ window.__ModuleLoader__.load({
492
595
  // eslint-disable-next-line react-hooks/exhaustive-deps
493
596
  }, []);
494
597
 
495
- // 通用安装入口:spec 可以是 github:owner/repo(卡片按钮)或 npm 包名
496
- // (已装面板的更新按钮)。后端会把同源发布的 github spec 改写为 npm
497
- // tarball 安装(更快),job 里记录的是最终 spec。
498
- var doInstallSpec = useCallback(function (spec, allowBuildScripts) {
598
+ var doRawInstall = useCallback(function (spec, extra) {
499
599
  setInstalling(function (prev) {
500
600
  var next = Object.assign({}, prev, { [spec]: true });
501
601
  return next;
502
602
  });
503
603
  setError(null);
504
- var payload = { spec: spec };
505
- if (allowBuildScripts) payload.allowBuildScripts = allowBuildScripts;
604
+ var payload = Object.assign({ spec: spec }, extra || {});
506
605
  call("install", payload).then(function (value) {
507
606
  track(value.jobId, spec);
508
607
  }).catch(function (e) {
608
+ delete approvalTokensRef.current[spec];
509
609
  setError(errorText(e));
510
610
  }).finally(function () {
511
611
  setInstalling(function (prev) {
@@ -516,18 +616,46 @@ window.__ModuleLoader__.load({
516
616
  });
517
617
  }, [call, track]);
518
618
 
519
- // 批准 = 用同一个 spec 带着点名的同意重新发起一次安装。不需要可恢复的
520
- // 任务状态机:pnpm store 已经缓存了刚下载的东西,第二次几乎不花时间。
521
- var doApprove = useCallback(function (spec, names) {
522
- doInstallSpec(spec, names);
523
- }, [doInstallSpec]);
619
+ var doApprove = useCallback(function (spec, names, token) {
620
+ var extra = { allowBuildScripts: names };
621
+ var apprToken = token || approvalTokensRef.current[spec];
622
+ if (apprToken) {
623
+ extra.approvalToken = apprToken;
624
+ delete approvalTokensRef.current[spec];
625
+ }
626
+ doRawInstall(spec, extra);
627
+ }, [doRawInstall]);
628
+
629
+ var preflightAndInstall = useCallback(function (spec) {
630
+ delete approvalTokensRef.current[spec];
631
+ setInstalling(function (prev) {
632
+ var next = Object.assign({}, prev, { [spec]: true });
633
+ return next;
634
+ });
635
+ setError(null);
636
+ call("preflight", { spec: spec }).then(function (report) {
637
+ // 检查通过直接装,不打扰;有风险才出内联卡片等人表态;被阻止
638
+ // 只告知原因。任何路径都不再弹模态框。
639
+ if (report.verdict === "safe") {
640
+ doRawInstall(spec, {});
641
+ } else {
642
+ setPreflight({ spec: spec, report: report });
643
+ }
644
+ }).catch(function (e) {
645
+ setError(errorText(e));
646
+ }).finally(function () {
647
+ setInstalling(function (prev) {
648
+ var next = Object.assign({}, prev);
649
+ delete next[spec];
650
+ return next;
651
+ });
652
+ });
653
+ }, [call, doRawInstall]);
524
654
 
525
655
  var doInstall = useCallback(function (repo) {
526
- doInstallSpec("github:" + repo);
527
- }, [doInstallSpec]);
656
+ preflightAndInstall("github:" + repo);
657
+ }, [preflightAndInstall]);
528
658
 
529
- // 一键重启:loopback-only 的 /market restart 端点会 detached 拉起新的
530
- // dsh 进程后退出当前进程;这里轮询 host 恢复后自动刷新页面。
531
659
  var doRestart = useCallback(function () {
532
660
  if (typeof window !== "undefined" && typeof window.confirm === "function") {
533
661
  if (window.confirm("重启 dsh?正在进行的任务会中断。") !== true) return;
@@ -547,7 +675,7 @@ window.__ModuleLoader__.load({
547
675
  rpc.call("/market", "installed", {}).then(function () {
548
676
  clearInterval(ping);
549
677
  window.location.reload();
550
- }).catch(function () { /* host 还在重启,继续等 */ });
678
+ }).catch(function () { /* host restarting */ });
551
679
  }, 3000);
552
680
  }).catch(function (e) {
553
681
  setRestarting(false);
@@ -556,6 +684,7 @@ window.__ModuleLoader__.load({
556
684
  }, [call, rpc]);
557
685
 
558
686
  var doUninstall = useCallback(function (name) {
687
+ delete approvalTokensRef.current[name];
559
688
  setRemoving(function (prev) {
560
689
  var next = Object.assign({}, prev, { [name]: true });
561
690
  return next;
@@ -578,16 +707,13 @@ window.__ModuleLoader__.load({
578
707
  refreshInstalled();
579
708
  }, [refreshInstalled]);
580
709
 
581
- // 任务面板位置固定(已装面板之后):不随任务状态在顶部/底部之间
582
- // 跳——npm 安装几秒即完成,"完成即落底"曾让结果凭空消失。
583
- // "只看已验证"开关下的可见项:verify 判定 bundle/client 的才算插件。
584
- // verifyPending:当前加载的仓库里还有未验证完的(verified map 尚未覆盖)。
585
710
  var visibleItems = results === null ? [] : results.items.filter(function (it) {
586
711
  if (verifiedOnly !== true) return true;
587
712
  var v = verified[it.fullName];
588
713
  return v !== undefined && (v.kind === "bundle" || v.kind === "client")
589
714
  && !(v.hostDeps !== undefined && v.hostDeps.length > 0);
590
715
  });
716
+ resultsRef.current = results;
591
717
  var verifyPending = results !== null && results.items.some(function (it) { return verified[it.fullName] === undefined; });
592
718
 
593
719
  return h("div", { className: "mkt_root" },
@@ -620,16 +746,45 @@ window.__ModuleLoader__.load({
620
746
  "只看已验证插件")
621
747
  ),
622
748
  error ? h("div", { className: "mkt_error" }, error) : null,
623
- h(InstalledPanel, { installed: installed, removing: removing, updates: updates, onUninstall: doUninstall, onInstallSpec: doInstallSpec }),
749
+ h(InstalledPanel, { installed: installed, removing: removing, updates: updates, onUninstall: doUninstall, onInstallSpec: preflightAndInstall }),
624
750
  h(JobsPanel, {
625
751
  jobs: jobs,
626
- onClear: clearJobs,
752
+ onClear: function () {
753
+ Object.keys(jobs).forEach(function (id) {
754
+ var job = jobs[id];
755
+ var token = job && job.approvalToken ? job.approvalToken : (job && job.spec ? approvalTokensRef.current[job.spec] : undefined);
756
+ call("jobDismiss", { jobId: id, token: token }).catch(function () {});
757
+ });
758
+ approvalTokensRef.current = {};
759
+ clearJobs();
760
+ },
627
761
  onApprove: doApprove,
628
- onDismiss: dropJob,
762
+ onDismiss: function (id) {
763
+ var job = jobs[id];
764
+ var token = job && job.approvalToken ? job.approvalToken : (job && job.spec ? approvalTokensRef.current[job.spec] : undefined);
765
+ if (job && job.spec) delete approvalTokensRef.current[job.spec];
766
+ call("jobDismiss", { jobId: id, token: token }).catch(function () {});
767
+ dropJob(id);
768
+ },
769
+ onDrop: dropJob,
629
770
  onRestart: doRestart,
630
771
  restarting: restarting,
631
772
  approving: Object.keys(installing).filter(function (s) { return installing[s]; })[0],
632
773
  }),
774
+ preflight ? h(PreflightCard, {
775
+ spec: preflight.spec,
776
+ report: preflight.report,
777
+ busy: installing[preflight.spec] === true,
778
+ onConfirm: function () {
779
+ var spec = preflight.spec;
780
+ setPreflight(null);
781
+ doRawInstall(spec, { acceptWarnings: true });
782
+ },
783
+ onClose: function () {
784
+ if (preflight && preflight.spec) delete approvalTokensRef.current[preflight.spec];
785
+ setPreflight(null);
786
+ },
787
+ }) : null,
633
788
  h("div", { className: "mkt_list" },
634
789
  results == null
635
790
  ? h("div", { className: "mkt_meta mkt_listHead" }, loading ? "正在加载最热插件…" : "—")
@@ -657,6 +812,7 @@ window.__ModuleLoader__.load({
657
812
  installing: installing[item.fullName] === true || installing["github:" + item.fullName] === true,
658
813
  installJob: installJob,
659
814
  verified: vInfo,
815
+ compat: compat[item.fullName],
660
816
  alreadyInstalled: alreadyInstalled,
661
817
  onInstall: doInstall,
662
818
  });