@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/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
- function useJobPolling(call, onSettled) {
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 (!wasTerminal && nowTerminal && onSettled) {
121
- try { onSettled(id); } catch (e) { /* best effort */ }
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
- var track = useCallback(function (id, spec) {
137
- jobsRef.current = Object.assign({}, jobsRef.current, { [id]: { status: "running", spec: spec, output: "" } });
138
- setJobs(Object.assign({}, jobsRef.current));
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
- return h("div", { key: p.name, className: "mkt_approvePkg" },
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
- id + " · " + clip(job.spec || "", 40) + " · " + (job.status || "running")),
256
- // 待批准不是失败:什么都没执行,profile 也没动过。所以不显示
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) { props.onDismiss(id); props.onApprove(job.spec, 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
- done && job.status === "completed"
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
- job.output ? h("pre", { className: "mkt_pre" }, job.output.slice(-4000)) : null
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
- return rpc.call("/market", endpoint, payload || {}).then(function (res) {
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
- var polling = useJobPolling(call, refreshInstalled);
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
- // 通用安装入口:spec 可以是 github:owner/repo(卡片按钮)或 npm 包名
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
- // 批准 = 用同一个 spec 带着点名的同意重新发起一次安装。不需要可恢复的
520
- // 任务状态机:pnpm store 已经缓存了刚下载的东西,第二次几乎不花时间。
521
- var doApprove = useCallback(function (spec, names) {
522
- doInstallSpec(spec, names);
523
- }, [doInstallSpec]);
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
- doInstallSpec("github:" + repo);
527
- }, [doInstallSpec]);
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: doInstallSpec }),
831
+ h(InstalledPanel, { installed: installed, removing: removing, updates: updates, onUninstall: doUninstall, onInstallSpec: preflightAndInstall }),
624
832
  h(JobsPanel, {
625
833
  jobs: jobs,
626
- onClear: clearJobs,
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: dropJob,
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
  });