@1e0zj/dsh-plugin-mall 0.4.6 → 0.4.12

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
@@ -28,6 +28,8 @@ window.__ModuleLoader__.load({
28
28
  ".mkt_btnDanger:hover:not(:disabled){border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}",
29
29
  ".mkt_btnSm{padding:3px 10px;font-size:12px}",
30
30
  ".mkt_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}",
31
+ ".mkt_notice{border-left:3px solid var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-error-primary);padding:7px 10px;font-size:12px;line-height:18px;overflow-wrap:anywhere}",
32
+ ".mkt_noticeInfo{border-left-color:var(--dsw-alias-state-accent-primary);color:inherit}",
31
33
  ".mkt_list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;align-items:stretch;min-width:0}",
32
34
  "@media (max-width:820px){.mkt_list{grid-template-columns:minmax(0,1fr)}}",
33
35
  ".mkt_listHead{grid-column:1/-1;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
@@ -43,8 +45,9 @@ window.__ModuleLoader__.load({
43
45
  ".mkt_panelTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}",
44
46
  ".mkt_panelRow{display:flex;align-items:center;gap:8px}",
45
47
  ".mkt_panelRow .mkt_link{margin-left:auto}",
46
- ".mkt_pre{font-family:Consolas,Monaco,monospace;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-markdown-code-block);border-radius:6px;padding:8px;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-all}",
48
+ ".mkt_pre{box-sizing:border-box;width:100%;max-width:100%;min-width:0;font-family:Consolas,Monaco,monospace;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-markdown-code-block);border-radius:6px;padding:8px;max-height:280px;overflow:auto;white-space:pre;word-break:normal}",
47
49
  ".mkt_ok{color:var(--dsw-alias-state-success-primary)}",
50
+ ".mkt_warn{color:var(--dsw-alias-state-warn-label)}",
48
51
  ".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)}",
49
52
  ".mkt_badgeOk{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}",
50
53
  ".mkt_badgeWarn{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-label)}",
@@ -57,13 +60,31 @@ window.__ModuleLoader__.load({
57
60
  ".mkt_depList{display:flex;flex-direction:column;gap:6px;margin-top:8px}",
58
61
  ".mkt_depRow{display:flex;justify-content:space-between;align-items:center;gap:8px}",
59
62
  ".mkt_depActions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
60
- ".mkt_approve{display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;padding:10px 12px;margin:4px 0}",
61
- ".mkt_approveHead{font-size:13px;font-weight:600;color:var(--dsw-alias-state-error-primary)}",
63
+ ".mkt_decision{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-state-warn-secondary);border-radius:6px;overflow:hidden;margin-top:4px}",
64
+ ".mkt_decisionError{border-color:var(--dsw-alias-state-error-primary)}",
65
+ ".mkt_decisionHead{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;padding:8px 10px;background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}",
66
+ ".mkt_decisionError .mkt_decisionHead{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-error-primary)}",
67
+ ".mkt_decisionTitle{font-size:13px;font-weight:600}",
68
+ ".mkt_decisionBody{display:flex;flex-direction:column;gap:8px;min-width:0;padding:10px;max-height:300px;overflow:auto}",
69
+ ".mkt_decisionActions{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:8px 10px;border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3)}",
62
70
  ".mkt_approvePkg{display:flex;flex-direction:column;gap:4px}",
63
71
  ".mkt_approveName{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-right:8px;overflow-wrap:anywhere}",
64
72
  ".mkt_approveCmd{max-height:none;margin:0}",
65
73
  ".mkt_jobDone{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:2px}",
66
- ".mkt_logBlock{display:flex;flex-direction:column;gap:4px;align-items:flex-start}",
74
+ ".mkt_logBlock{display:flex;flex-direction:column;gap:4px;align-items:stretch;min-width:0;max-width:100%}",
75
+ ".mkt_linkButton{align-self:flex-start;border:0;background:none;padding:0;color:var(--dsw-alias-state-business-primary);font:inherit;font-size:12px;cursor:pointer}",
76
+ ".mkt_job{display:flex;flex-direction:column;gap:6px;min-width:0;max-width:100%;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}",
77
+ ".mkt_panelRow + .mkt_job{padding-top:0;border-top:0}",
78
+ ".mkt_jobHead{display:flex;align-items:center;gap:8px;min-width:0}",
79
+ ".mkt_jobIdentity{display:flex;align-items:center;gap:7px;min-width:0;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
80
+ ".mkt_jobHead .mkt_btn{margin-left:auto;flex:none}",
81
+ ".mkt_stateDot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-caption);flex:none}",
82
+ ".mkt_stateRunning{background:var(--dsw-alias-state-business-primary);animation:mktPulse 1.6s ease-in-out infinite}",
83
+ ".mkt_stateDone{background:var(--dsw-alias-state-success-primary)}",
84
+ ".mkt_stateWarn{background:var(--dsw-alias-state-warn-primary)}",
85
+ ".mkt_stateError{background:var(--dsw-alias-state-error-primary)}",
86
+ "@keyframes mktPulse{0%,100%{opacity:.45}50%{opacity:1}}",
87
+ "@media (prefers-reduced-motion:reduce){.mkt_stateRunning{animation:none}.mkt_switch,.mkt_switchKnob{transition:none}}",
67
88
  ".mkt_depOff{opacity:.5;text-decoration:line-through}",
68
89
  ".mkt_switch{position:relative;width:34px;height:18px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);cursor:pointer;padding:0;transition:background .15s,border-color .15s}",
69
90
  ".mkt_switch:disabled{opacity:.55;cursor:default}",
@@ -71,11 +92,16 @@ window.__ModuleLoader__.load({
71
92
  ".mkt_switchKnob{position:absolute;top:1px;left:1px;width:14px;height:14px;border-radius:50%;background:var(--dsw-alias-label-primary-foreground);transition:transform .15s;box-shadow:var(--dsw-shadow-lv1)}",
72
93
  ".mkt_switchOn .mkt_switchKnob{transform:translateX(16px)}",
73
94
  ".mkt_issueList{list-style:none;display:flex;flex-direction:column;gap:8px;margin:0;padding:0}",
74
- ".mkt_issue{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-3)}",
95
+ ".mkt_issue{border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:8px 10px;background:var(--dsw-alias-bg-layer-3)}",
75
96
  ".mkt_issueBlock{border-color:var(--dsw-alias-state-error-primary)}",
76
97
  ".mkt_issueWarn{border-color:var(--dsw-alias-state-warn-primary)}",
77
98
  ".mkt_issueTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}",
78
99
  ".mkt_issueDetail{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:4px;line-height:18px;overflow-wrap:anywhere}",
100
+ ".mkt_modalMask{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px;background:var(--dsw-alias-bg-mask-3)}",
101
+ ".mkt_modal{width:min(420px,100%);max-height:min(520px,calc(100vh - 40px));display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv3)}",
102
+ ".mkt_modalHead{padding:14px 16px 8px;font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}",
103
+ ".mkt_modalBody{padding:0 16px 14px;font-size:12.5px;line-height:19px;color:var(--dsw-alias-label-secondary);overflow:auto}",
104
+ ".mkt_modalActions{display:flex;justify-content:flex-end;gap:8px;padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2)}",
79
105
  ].join("\n");
80
106
  var tagId = "@1e0zj/dsh-plugin-mall/market-tab.css";
81
107
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
@@ -102,6 +128,10 @@ window.__ModuleLoader__.load({
102
128
  return String(e && e.message ? e.message : e);
103
129
  }
104
130
 
131
+ function isJobActive(status) {
132
+ return status === "running" || status === "stopping";
133
+ }
134
+
105
135
  function kindLabel(kind) {
106
136
  if (kind === "bundle") return "宿主插件层";
107
137
  if (kind === "client") return "浏览器UI插件";
@@ -159,7 +189,7 @@ window.__ModuleLoader__.load({
159
189
  var current = jobsRef.current;
160
190
  var running = Object.keys(current).filter(function (id) {
161
191
  var job = current[id];
162
- return job && job.status !== "completed" && job.status !== "failed" && job.status !== "killed";
192
+ return job && isJobActive(job.status);
163
193
  });
164
194
  if (running.length === 0) return;
165
195
  running.forEach(function (id) {
@@ -211,21 +241,23 @@ window.__ModuleLoader__.load({
211
241
  delete next[carryFromId];
212
242
  }
213
243
  for (var key in next) {
214
- if (key !== id && next[key] && next[key].spec === spec && next[key].status !== "running") {
244
+ if (key !== id && next[key] && next[key].spec === spec && !isJobActive(next[key].status)) {
215
245
  delete next[key];
216
246
  }
217
247
  }
218
248
  next[id] = { status: "running", spec: spec, output: carried };
219
249
  commit(next);
220
250
  }, [commit]);
221
- var clear = useCallback(function () {
222
- commit({});
223
- }, [commit]);
224
251
  var drop = useCallback(function (id) {
225
252
  var next = Object.assign({}, jobsRef.current);
226
253
  delete next[id];
227
254
  commit(next);
228
255
  }, [commit]);
256
+ var setStatus = useCallback(function (id, status) {
257
+ var current = jobsRef.current[id];
258
+ if (!current) return;
259
+ commit(Object.assign({}, jobsRef.current, { [id]: Object.assign({}, current, { status: status }) }));
260
+ }, [commit]);
229
261
  // 恢复后端任务记录(tracker.list 的形状):安装事务改写
230
262
  // cordis.patch.yml 会让 dsh 重放装配树、整个市场 UI 重挂载,React
231
263
  // state 全丢——任务面板、完成提醒、重启按钮一起消失(真实事故:
@@ -269,7 +301,7 @@ window.__ModuleLoader__.load({
269
301
  for (var key in next) {
270
302
  if (serverIds[key]) continue;
271
303
  var stale = next[key];
272
- if (stale.status === "running") {
304
+ if (isJobActive(stale.status)) {
273
305
  next[key] = Object.assign({}, stale, {
274
306
  status: "killed",
275
307
  detail: "宿主进程已重启,该任务的记录随之丢失",
@@ -282,7 +314,7 @@ window.__ModuleLoader__.load({
282
314
  }
283
315
  commit(next);
284
316
  }, [commit]);
285
- return { jobs: jobs, track: track, clear: clear, drop: drop, restore: restore };
317
+ return { jobs: jobs, track: track, drop: drop, setStatus: setStatus, restore: restore };
286
318
  }
287
319
 
288
320
  // ── plugin verification badge ───────────────────────────────────────────
@@ -319,7 +351,10 @@ window.__ModuleLoader__.load({
319
351
  var installLabel = props.alreadyInstalled === true ? "已装" : "安装";
320
352
  var installDisabled = installing || props.alreadyInstalled === true;
321
353
  if (!installing && job) {
322
- if (job.status === "running") { installLabel = "安装中…"; installDisabled = true; }
354
+ if (isJobActive(job.status)) {
355
+ installLabel = job.status === "stopping" ? "停止中…" : "安装中…";
356
+ installDisabled = true;
357
+ }
323
358
  else if (job.status === "completed") { installLabel = "已装 · 重启生效"; installDisabled = true; }
324
359
  else if (job.status === "failed") { installLabel = "安装失败 · 见任务日志"; }
325
360
  else { installLabel = "已取消 · 重试"; }
@@ -346,11 +381,69 @@ window.__ModuleLoader__.load({
346
381
  );
347
382
  }
348
383
 
384
+ function DecisionPanel(props) {
385
+ var tone = props.tone === "error" ? " mkt_decisionError" : "";
386
+ return h("div", { className: "mkt_decision" + tone },
387
+ h("div", { className: "mkt_decisionHead" },
388
+ h("span", { className: "mkt_decisionTitle" }, props.title),
389
+ props.meta ? h("span", { className: "mkt_meta" }, props.meta) : null),
390
+ h("div", { className: "mkt_decisionBody" }, props.children),
391
+ h("div", { className: "mkt_decisionActions" }, props.actions));
392
+ }
393
+
394
+ function ErrorNotice(props) {
395
+ if (!props.message) return null;
396
+ return h("div", { className: "mkt_notice", role: "alert" }, props.message);
397
+ }
398
+
399
+ // 状态性提示(区别于 ErrorNotice 的 role=alert):「重启已调度」这类
400
+ // 成功路径的进展信息,页面自动重连后随刷新自然消失。
401
+ function StatusNotice(props) {
402
+ if (!props.message) return null;
403
+ return h("div", { className: "mkt_notice mkt_noticeInfo", role: "status" }, props.message);
404
+ }
405
+
406
+ function RestartDialog(props) {
407
+ useEffect(function () {
408
+ function onKeyDown(event) {
409
+ if (event.key === "Escape" && props.busy !== true) props.onCancel();
410
+ }
411
+ document.addEventListener("keydown", onKeyDown);
412
+ return function () { document.removeEventListener("keydown", onKeyDown); };
413
+ }, [props.busy, props.onCancel]);
414
+ return h("div", {
415
+ className: "mkt_modalMask",
416
+ onMouseDown: function (event) {
417
+ if (event.target === event.currentTarget && props.busy !== true) props.onCancel();
418
+ },
419
+ }, h("div", { className: "mkt_modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "mkt-restart-title" },
420
+ h("div", { id: "mkt-restart-title", className: "mkt_modalHead" }, "重启 dsh"),
421
+ h("div", { className: "mkt_modalBody" }, props.activeCount > 0
422
+ ? "当前有 " + props.activeCount + " 个任务仍在进行。重启会中断这些任务,未完成的改动将由事务恢复逻辑处理。"
423
+ : "重启后,已经完成的插件安装、更新或卸载才会生效。"),
424
+ h("div", { className: "mkt_modalActions" },
425
+ h("button", { className: "mkt_btn", disabled: props.busy === true, autoFocus: true, onClick: props.onCancel }, "取消"),
426
+ h("button", { className: "mkt_btn mkt_btnPrimary", disabled: props.busy === true, onClick: props.onConfirm }, props.busy ? "重启中…" : "确认重启"))));
427
+ }
428
+
349
429
  // ── install-script approval ─────────────────────────────────────────────
350
430
  function ApprovalRequest(props) {
351
431
  var pkgs = props.needsApproval || [];
352
- return h("div", { className: "mkt_approve" },
353
- h("div", { className: "mkt_approveHead" }, "需要你确认:这次安装会执行安装期代码"),
432
+ var actions = [
433
+ h("button", { key: "cancel", className: "mkt_btn mkt_btnSm", disabled: props.busy === true, onClick: props.onDismiss }, "取消"),
434
+ h("button", {
435
+ key: "allow",
436
+ className: "mkt_btn mkt_btnPrimary mkt_btnSm",
437
+ disabled: props.busy === true,
438
+ onClick: function () { props.onApprove(pkgs.map(function (p) { return p.name; })); },
439
+ }, props.busy ? "继续中…" : "允许并继续"),
440
+ ];
441
+ return h(DecisionPanel, {
442
+ title: "安装期代码需要确认",
443
+ meta: clip(props.spec || "", 50),
444
+ actions: actions,
445
+ },
446
+ props.detail ? h("div", { className: "mkt_desc" }, props.detail) : null,
354
447
  pkgs.map(function (p, index) {
355
448
  var facts = [];
356
449
  if (typeof p.weeklyDownloads === "number") facts.push("周下载 " + p.weeklyDownloads.toLocaleString());
@@ -370,16 +463,7 @@ window.__ModuleLoader__.load({
370
463
  h("div", { className: "mkt_meta" }, facts.join(" · "))
371
464
  );
372
465
  }),
373
- h("div", { className: "mkt_meta" }, "这些命令会以你的权限在你的机器上运行,早于任何插件代码加载。"),
374
- h("div", { className: "mkt_row" },
375
- h("button", {
376
- className: "mkt_btn mkt_btnPrimary mkt_btnSm",
377
- disabled: props.busy === true,
378
- onClick: function () { props.onApprove(pkgs.map(function (p) { return p.name; })); },
379
- }, props.busy ? "继续中…" : "允许并继续"),
380
- h("button", { className: "mkt_btn mkt_btnSm", onClick: props.onDismiss }, "取消")
381
- )
382
- );
466
+ h("div", { className: "mkt_meta" }, "这些命令会以你的权限在你的机器上运行,早于任何插件代码加载。"));
383
467
  }
384
468
 
385
469
  // ── preflight result card ────────────────────────────────────────────────
@@ -391,11 +475,23 @@ window.__ModuleLoader__.load({
391
475
  var blocked = report.verdict === "blocked";
392
476
  var title = blocked ? "安装被阻止" : "安装存在风险";
393
477
  var issues = report.issues || [];
394
- return h("div", { className: "mkt_card" },
395
- h("div", { className: "mkt_panelRow" },
396
- h("p", { className: "mkt_panelTitle" + (blocked ? " mkt_error" : "") }, title),
397
- h("span", { className: "mkt_meta" }, clip(props.spec, 50))
398
- ),
478
+ var actions = blocked
479
+ ? h("button", { className: "mkt_btn mkt_btnSm", onClick: props.onClose }, "关闭")
480
+ : [
481
+ h("button", { key: "cancel", className: "mkt_btn mkt_btnSm", disabled: props.busy === true, onClick: props.onClose }, "取消"),
482
+ h("button", {
483
+ key: "confirm",
484
+ className: "mkt_btn mkt_btnPrimary mkt_btnSm",
485
+ disabled: props.busy === true,
486
+ onClick: props.onConfirm,
487
+ }, props.busy ? "安装中…" : "了解风险并继续"),
488
+ ];
489
+ return h(DecisionPanel, {
490
+ tone: blocked ? "error" : "warning",
491
+ title: title,
492
+ meta: clip(props.spec, 50),
493
+ actions: actions,
494
+ },
399
495
  report.summary ? h("div", { className: "mkt_desc" }, report.summary) : null,
400
496
  issues.length > 0 ? h("ul", { className: "mkt_issueList" },
401
497
  issues.map(function (issue) {
@@ -406,21 +502,7 @@ window.__ModuleLoader__.load({
406
502
  h("div", { className: "mkt_issueTitle" }, issue.title),
407
503
  issue.detail ? h("div", { className: "mkt_issueDetail" }, issue.detail) : null);
408
504
  })
409
- ) : null,
410
- h("div", { className: "mkt_row" },
411
- blocked
412
- ? h("button", { className: "mkt_btn mkt_btnSm", onClick: props.onClose }, "关闭")
413
- : [
414
- h("button", { key: "cancel", className: "mkt_btn mkt_btnSm", onClick: props.onClose }, "取消"),
415
- h("button", {
416
- key: "confirm",
417
- className: "mkt_btn mkt_btnPrimary mkt_btnSm",
418
- disabled: props.busy === true,
419
- onClick: props.onConfirm,
420
- }, props.busy ? "安装中…" : "我已了解风险,继续安装"),
421
- ]
422
- )
423
- );
505
+ ) : null);
424
506
  }
425
507
 
426
508
  var MARKET_PACKAGE = "@1e0zj/dsh-plugin-mall";
@@ -435,64 +517,99 @@ window.__ModuleLoader__.load({
435
517
  if (status === "completed") return "完成";
436
518
  if (status === "failed") return "失败";
437
519
  if (status === "killed") return "已取消";
520
+ if (status === "stopping") return "停止中";
438
521
  return "进行中";
439
522
  }
440
523
 
441
524
  function jobStatusClass(status) {
442
525
  if (status === "completed") return "mkt_ok";
443
- if (status === "failed" || status === "killed") return "mkt_error";
526
+ if (status === "failed") return "mkt_error";
527
+ if (status === "killed" || status === "stopping") return "mkt_warn";
444
528
  return "";
445
529
  }
446
530
 
531
+ function jobStateDotClass(status, awaitingDecision) {
532
+ if (awaitingDecision || status === "killed" || status === "stopping") return "mkt_stateWarn";
533
+ if (status === "completed") return "mkt_stateDone";
534
+ if (status === "failed") return "mkt_stateError";
535
+ return "mkt_stateRunning";
536
+ }
537
+
447
538
  // 任务日志:跑的时候只露尾部几行(够看进度,不霸屏),落定后默认折叠
448
539
  // ——成功的日志没人回头读,失败的才需要,点开即可。
449
- var LOG_TAIL_LINES = 8;
540
+ var LOG_TAIL_LINES = 12;
450
541
  function JobLog(props) {
451
542
  var _open = useState(false);
452
543
  var open = _open[0];
453
544
  var setOpen = _open[1];
454
545
  var text = String(props.output || "").trimEnd();
546
+ var failed = props.status === "failed";
547
+ useEffect(function () {
548
+ if (failed) setOpen(true);
549
+ }, [failed]);
455
550
  if (text.length === 0) return null;
456
551
  var lines = text.split("\n");
457
- var collapsed = props.done && !open;
458
- var shown = (props.done || lines.length <= LOG_TAIL_LINES) ? lines : lines.slice(-LOG_TAIL_LINES);
552
+ var settled = !isJobActive(props.status);
553
+ var collapsed = settled && !open;
554
+ var shown = settled || lines.length <= LOG_TAIL_LINES ? lines : lines.slice(-LOG_TAIL_LINES);
555
+ var rendered = shown.join("\n");
556
+ if (rendered.length > 64000) rendered = "…前部日志已截断…\n" + rendered.slice(-64000);
459
557
  // 展开/收起始终在日志块上方:放在下方的话,点开后按钮会被推到长长的
460
558
  // 日志末尾,想收起还得先滚回去。
461
559
  return h("div", { className: "mkt_logBlock" },
462
- props.done
463
- ? h("span", {
464
- className: "mkt_link",
560
+ settled
561
+ ? h("button", {
562
+ type: "button",
563
+ className: "mkt_linkButton",
564
+ "aria-expanded": open ? "true" : "false",
465
565
  onClick: function () { setOpen(!open); },
466
566
  }, open ? "收起日志" : "查看日志(" + lines.length + " 行)")
467
567
  : lines.length > LOG_TAIL_LINES
468
568
  ? h("div", { className: "mkt_meta" }, "只显示最后 " + LOG_TAIL_LINES + " 行")
469
569
  : null,
470
- collapsed ? null : h("pre", { className: "mkt_pre" }, shown.join("\n").slice(-4000))
570
+ collapsed ? null : h("pre", { className: "mkt_pre", "aria-label": "任务日志" }, rendered)
471
571
  );
472
572
  }
473
573
 
474
574
  // ── jobs panel ──────────────────────────────────────────────────────────
475
575
  function JobsPanel(props) {
476
576
  var ids = Object.keys(props.jobs);
477
- if (ids.length === 0) return null;
577
+ if (ids.length === 0 && !props.error) return null;
578
+ var hasSettled = ids.some(function (id) {
579
+ var job = props.jobs[id];
580
+ var hasDecision = (props.preflight && props.preflight.jobId === id)
581
+ || (job.needsApproval && job.needsApproval.length > 0);
582
+ return !isJobActive(job.status) && !hasDecision;
583
+ });
478
584
  return h("div", { className: "mkt_card" },
479
585
  h("div", { className: "mkt_panelRow" },
480
586
  h("p", { className: "mkt_panelTitle" }, "任务"),
481
- props.onClear ? h("span", { className: "mkt_link", onClick: props.onClear }, "清空") : null
587
+ props.onClear && hasSettled ? h("span", { className: "mkt_link", onClick: props.onClear }, "清空已结束") : null
482
588
  ),
589
+ h(ErrorNotice, { message: props.error }),
483
590
  ids.map(function (id) {
484
591
  var job = props.jobs[id];
485
592
  var done = job.status === "completed" || job.status === "failed" || job.status === "killed";
486
- return h("div", { key: id, style: { display: "flex", flexDirection: "column", gap: "4px" } },
593
+ var preflightDecision = props.preflight && props.preflight.jobId === id ? props.preflight : null;
594
+ var awaitingDecision = preflightDecision !== null || (job.needsApproval && job.needsApproval.length > 0);
595
+ return h("div", { key: id, className: "mkt_job" },
487
596
  // 不再显示 market-N 这种内部 id——对用户没有意义,反而让人以为
488
597
  // 自己点了两次。只说清「在做什么 · 到哪一步了」。
489
- // 状态词单独上色(完成绿、失败/取消红),前半段保持灰——整行
490
- // 都染色会喧宾夺主,只有结论需要一眼看见。
491
- h("div", { className: "mkt_meta" },
492
- jobKindLabel(job.kind) + clip(job.spec || "", 44) + " · ",
493
- h("span", { className: jobStatusClass(job.status) }, jobStatusLabel(job.status))),
598
+ // 状态词和状态点表达结论,任务类型与目标保持次要色。
599
+ h("div", { className: "mkt_jobHead" },
600
+ h("div", { className: "mkt_jobIdentity" },
601
+ h("span", { className: "mkt_stateDot " + jobStateDotClass(job.status, awaitingDecision), "aria-hidden": "true" }),
602
+ h("span", null, jobKindLabel(job.kind) + clip(job.spec || "", 44) + " · "),
603
+ h("span", { className: awaitingDecision ? "mkt_warn" : jobStatusClass(job.status) }, awaitingDecision ? "待确认" : jobStatusLabel(job.status))),
604
+ isJobActive(job.status) ? h("button", {
605
+ className: "mkt_btn mkt_btnSm",
606
+ disabled: job.status === "stopping",
607
+ onClick: function () { props.onCancel(id); },
608
+ }, job.status === "stopping" ? "停止中…" : "停止") : null),
494
609
  job.needsApproval && job.needsApproval.length > 0
495
610
  ? h(ApprovalRequest, {
611
+ spec: job.spec,
612
+ detail: job.detail,
496
613
  needsApproval: job.needsApproval,
497
614
  busy: props.approving === job.spec,
498
615
  onApprove: function (names) {
@@ -505,7 +622,15 @@ window.__ModuleLoader__.load({
505
622
  },
506
623
  onDismiss: function () { props.onDismiss(id); },
507
624
  })
508
- : job.detail ? h("div", { className: "mkt_desc" }, job.detail) : null,
625
+ : preflightDecision
626
+ ? h(PreflightCard, {
627
+ spec: preflightDecision.spec,
628
+ report: preflightDecision.report,
629
+ busy: props.preflightBusy === true,
630
+ onConfirm: props.onConfirmPreflight,
631
+ onClose: props.onClosePreflight,
632
+ })
633
+ : job.detail ? h("div", { className: job.status === "failed" ? "mkt_error" : "mkt_desc" }, job.detail) : null,
509
634
  // 预检完成不提示重启——它没有改动任何东西,接下来才是安装。
510
635
  // 也不再重复一个绿色「完成」:状态行已经写了「· 完成」。
511
636
  done && job.status === "completed" && job.kind !== "dsh-plugin-preflight"
@@ -520,10 +645,8 @@ window.__ModuleLoader__.load({
520
645
  disabled: props.restarting === true,
521
646
  onClick: props.onRestart,
522
647
  }, props.restarting ? "重启中…" : "重启 dsh 生效"))
523
- : job.status === "failed" && !(job.needsApproval && job.needsApproval.length > 0)
524
- ? h("div", { className: "mkt_error" }, "失败,见下方输出")
525
- : null,
526
- h(JobLog, { output: job.output, done: done })
648
+ : null,
649
+ h(JobLog, { output: job.output, status: job.status })
527
650
  );
528
651
  })
529
652
  );
@@ -548,6 +671,7 @@ window.__ModuleLoader__.load({
548
671
  h("span", { className: "mkt_badge" }, count + " 个"),
549
672
  h("span", { className: "mkt_meta mkt_installedToggle" }, open ? "收起" : "展开")
550
673
  ),
674
+ h(ErrorNotice, { message: props.error }),
551
675
  installed.error
552
676
  ? h("div", { className: "mkt_error" }, installed.error)
553
677
  : !open ? null
@@ -604,6 +728,9 @@ window.__ModuleLoader__.load({
604
728
  var _error = useState(null);
605
729
  var error = _error[0];
606
730
  var setError = _error[1];
731
+ var reportError = useCallback(function (scope, value) {
732
+ setError({ scope: scope, message: errorText(value) });
733
+ }, []);
607
734
  var _installed = useState(null);
608
735
  var installed = _installed[0];
609
736
  var setInstalled = _installed[1];
@@ -645,6 +772,12 @@ window.__ModuleLoader__.load({
645
772
  var _restarting = useState(false);
646
773
  var restarting = _restarting[0];
647
774
  var setRestarting = _restarting[1];
775
+ var _restartNote = useState(null);
776
+ var restartNote = _restartNote[0];
777
+ var setRestartNote = _restartNote[1];
778
+ var _restartConfirm = useState(false);
779
+ var restartConfirm = _restartConfirm[0];
780
+ var setRestartConfirm = _restartConfirm[1];
648
781
  // 本次宿主进程的启动时间(jobs 端点带回):完成时间早于它的任务,
649
782
  // 其「重启 dsh 生效」按钮已经兑现,改显示「重启已生效」。
650
783
  var _hostStartedAt = useState(0);
@@ -714,11 +847,11 @@ window.__ModuleLoader__.load({
714
847
  setCompat({});
715
848
  verifyPage(value.items);
716
849
  }).catch(function (e) {
717
- setError(errorText(e));
850
+ reportError("search", e);
718
851
  }).finally(function () {
719
852
  setLoading(false);
720
853
  });
721
- }, [call, query, sort, verifyPage]);
854
+ }, [call, query, reportError, sort, verifyPage]);
722
855
 
723
856
  var canLoadMore = results !== null && results.items.length < results.total && !reachedLimit;
724
857
  var loadMoreLock = useRef(false);
@@ -744,13 +877,13 @@ window.__ModuleLoader__.load({
744
877
  return { total: value.total, items: merged };
745
878
  });
746
879
  }).catch(function (e) {
747
- setError(errorText(e));
880
+ reportError("search", e);
748
881
  setRetryAt(Date.now() + 60000);
749
882
  }).finally(function () {
750
883
  loadMoreLock.current = false;
751
884
  setLoadingMore(false);
752
885
  });
753
- }, [call, canLoadMore, loading, loadingMore, page, query, sort, retryAt, verifyPage]);
886
+ }, [call, canLoadMore, loading, loadingMore, page, query, reportError, sort, retryAt, verifyPage]);
754
887
 
755
888
  useEffect(function () {
756
889
  var node = sentinelRef.current;
@@ -775,7 +908,7 @@ window.__ModuleLoader__.load({
775
908
  return prev && !prev.error ? Object.assign({}, prev, { entries: value.entries }) : prev;
776
909
  });
777
910
  }).catch(function (e) {
778
- setError(errorText(e));
911
+ reportError("installed", e);
779
912
  }).finally(function () {
780
913
  setToggling(function (prev) {
781
914
  var next = Object.assign({}, prev);
@@ -783,7 +916,7 @@ window.__ModuleLoader__.load({
783
916
  return next;
784
917
  });
785
918
  });
786
- }, [call]);
919
+ }, [call, reportError]);
787
920
 
788
921
  var refreshInstalled = useCallback(function () {
789
922
  call("installed", {}).then(function (value) {
@@ -821,8 +954,8 @@ window.__ModuleLoader__.load({
821
954
  var polling = useJobPolling(call, onJobSettled, onApprovalToken, onPreflightSettled);
822
955
  var track = polling.track;
823
956
  var jobs = polling.jobs;
824
- var clearJobs = polling.clear;
825
957
  var dropJob = polling.drop;
958
+ var setJobStatus = polling.setStatus;
826
959
 
827
960
  useEffect(function () {
828
961
  doSearch();
@@ -848,7 +981,7 @@ window.__ModuleLoader__.load({
848
981
  track(value.jobId, spec, carryFromId);
849
982
  }).catch(function (e) {
850
983
  delete approvalTokensRef.current[spec];
851
- setError(errorText(e));
984
+ reportError("tasks", e);
852
985
  }).finally(function () {
853
986
  setInstalling(function (prev) {
854
987
  var next = Object.assign({}, prev);
@@ -856,7 +989,7 @@ window.__ModuleLoader__.load({
856
989
  return next;
857
990
  });
858
991
  });
859
- }, [call, track]);
992
+ }, [call, reportError, track]);
860
993
 
861
994
  var doApprove = useCallback(function (spec, names, token, carryFromId) {
862
995
  var extra = { allowBuildScripts: names };
@@ -872,7 +1005,7 @@ window.__ModuleLoader__.load({
872
1005
  useEffect(function () {
873
1006
  preflightHandlerRef.current = function (spec, report, jobId) {
874
1007
  if (!report || !report.verdict) {
875
- setError("预检没有返回结论,请重试");
1008
+ reportError("tasks", "预检没有返回结论,请重试");
876
1009
  return;
877
1010
  }
878
1011
  if (report.verdict === "safe") {
@@ -882,7 +1015,7 @@ window.__ModuleLoader__.load({
882
1015
  setPreflight({ spec: spec, report: report, jobId: jobId });
883
1016
  }
884
1017
  };
885
- }, [doRawInstall]);
1018
+ }, [doRawInstall, reportError]);
886
1019
 
887
1020
  // 点安装 = 立刻起一个预检 job,面板马上有东西看、日志实时流。
888
1021
  // 结论由轮询经 onPreflightSettled 交回上面的 handler,这里不等待。
@@ -892,25 +1025,30 @@ window.__ModuleLoader__.load({
892
1025
  call("preflight", { spec: spec }).then(function (value) {
893
1026
  track(value.jobId, value.spec || spec);
894
1027
  }).catch(function (e) {
895
- setError(errorText(e));
1028
+ reportError("tasks", e);
896
1029
  });
897
- }, [call, track]);
1030
+ }, [call, reportError, track]);
898
1031
 
899
1032
  var doInstall = useCallback(function (repo) {
900
1033
  preflightAndInstall("github:" + repo);
901
1034
  }, [preflightAndInstall]);
902
1035
 
903
1036
  var doRestart = useCallback(function () {
904
- if (typeof window !== "undefined" && typeof window.confirm === "function") {
905
- if (window.confirm("重启 dsh?正在进行的任务会中断。") !== true) return;
906
- }
907
1037
  clearRestartPing();
908
1038
  setRestarting(true);
909
1039
  setError(null);
1040
+ setRestartNote(null);
910
1041
  var requestedAt = Date.now();
911
1042
  var previousHostStartedAt = Number(hostStartedAt);
912
- call("restart", {}).then(function () {
1043
+ var scheduledLogPath = null;
1044
+ call("restart", {}).then(function (value) {
913
1045
  if (restartControlRef.current.mounted !== true) return;
1046
+ // 响应语义是「已调度交接」而不是「重启成功」:新宿主何时接管
1047
+ // 由下面的轮询判断。这里只把输出会出现在哪里告诉用户。
1048
+ scheduledLogPath = value && value.logPath ? String(value.logPath) : null;
1049
+ setRestartNote(value && value.mode === "visible"
1050
+ ? "重启已调度——dsh 正在新的控制台窗口中启动,窗口里 Ctrl+C 可停止;输出同时写入 " + scheduledLogPath
1051
+ : "重启已调度,输出与日志:" + (scheduledLogPath || "<home>/guard/ 下"));
914
1052
  var tries = 0;
915
1053
  var ping = setInterval(function () {
916
1054
  if (restartControlRef.current.mounted !== true) {
@@ -921,7 +1059,8 @@ window.__ModuleLoader__.load({
921
1059
  if (tries > 40) {
922
1060
  clearRestartPing();
923
1061
  setRestarting(false);
924
- setError("2 分钟内未检测到 dsh 重启完成,请手动检查 dsh 状态后刷新页面。");
1062
+ reportError("restart", "2 分钟内未检测到 dsh 重启完成,请手动检查 dsh 状态后刷新页面。"
1063
+ + (scheduledLogPath ? "输出与日志:" + scheduledLogPath : ""));
925
1064
  return;
926
1065
  }
927
1066
  // Prefer identity change over wall-clock ordering: NTP may move the
@@ -944,9 +1083,10 @@ window.__ModuleLoader__.load({
944
1083
  if (restartControlRef.current.mounted !== true) return;
945
1084
  clearRestartPing();
946
1085
  setRestarting(false);
947
- setError(errorText(e));
1086
+ setRestartNote(null);
1087
+ reportError("restart", e);
948
1088
  });
949
- }, [call, clearRestartPing, hostStartedAt]);
1089
+ }, [call, clearRestartPing, hostStartedAt, reportError]);
950
1090
 
951
1091
  var doUninstall = useCallback(function (name) {
952
1092
  delete approvalTokensRef.current[name];
@@ -958,7 +1098,7 @@ window.__ModuleLoader__.load({
958
1098
  call("uninstall", { package: name }).then(function (value) {
959
1099
  track(value.jobId, name);
960
1100
  }).catch(function (e) {
961
- setError(errorText(e));
1101
+ reportError("installed", e);
962
1102
  }).finally(function () {
963
1103
  setRemoving(function (prev) {
964
1104
  var next = Object.assign({}, prev);
@@ -966,7 +1106,7 @@ window.__ModuleLoader__.load({
966
1106
  return next;
967
1107
  });
968
1108
  });
969
- }, [call, track]);
1109
+ }, [call, reportError, track]);
970
1110
 
971
1111
  useEffect(function () {
972
1112
  refreshInstalled();
@@ -978,6 +1118,9 @@ window.__ModuleLoader__.load({
978
1118
  return v !== undefined && (v.kind === "bundle" || v.kind === "client")
979
1119
  && !(v.hostDeps !== undefined && v.hostDeps.length > 0);
980
1120
  });
1121
+ var activeJobCount = Object.keys(jobs).filter(function (id) {
1122
+ return jobs[id] && isJobActive(jobs[id].status);
1123
+ }).length;
981
1124
  resultsRef.current = results;
982
1125
  var verifyPending = results !== null && results.items.some(function (it) { return verified[it.fullName] === undefined; });
983
1126
 
@@ -1000,12 +1143,13 @@ window.__ModuleLoader__.load({
1000
1143
  h("option", { value: "forks" }, "按 fork")
1001
1144
  ),
1002
1145
  h("button", { className: "mkt_btn mkt_btnPrimary", disabled: loading, onClick: doSearch }, loading ? "搜索中…" : "搜索"),
1003
- h("button", { className: "mkt_btn mkt_btnDanger", disabled: restarting, onClick: doRestart }, restarting ? "重启中…" : "重启 dsh"),
1146
+ h("button", { className: "mkt_btn mkt_btnDanger", disabled: restarting, onClick: function () { setRestartConfirm(true); } }, restarting ? "重启中…" : "重启 dsh"),
1004
1147
  h("label", { className: "mkt_check" },
1005
1148
  h("input", { type: "checkbox", checked: verifiedOnly, onChange: function (e) { setVerifiedOnly(e.target.checked); } }),
1006
1149
  "只看已验证插件")
1007
1150
  ),
1008
- error ? h("div", { className: "mkt_error" }, error) : null,
1151
+ h(ErrorNotice, { message: error && (error.scope === "search" || error.scope === "restart") ? error.message : null }),
1152
+ restarting ? h(StatusNotice, { message: restartNote }) : null,
1009
1153
  h(InstalledPanel, {
1010
1154
  installed: installed,
1011
1155
  removing: removing,
@@ -1015,19 +1159,50 @@ window.__ModuleLoader__.load({
1015
1159
  onToggle: doToggle,
1016
1160
  onUninstall: doUninstall,
1017
1161
  onInstallSpec: preflightAndInstall,
1162
+ error: error && error.scope === "installed" ? error.message : null,
1018
1163
  }),
1019
1164
  h(JobsPanel, {
1020
1165
  jobs: jobs,
1166
+ error: error && error.scope === "tasks" ? error.message : null,
1167
+ preflight: preflight,
1168
+ preflightBusy: preflight ? installing[preflight.spec] === true : false,
1021
1169
  onClear: function () {
1022
1170
  Object.keys(jobs).forEach(function (id) {
1023
1171
  var job = jobs[id];
1172
+ var hasDecision = job && ((preflight && preflight.jobId === id)
1173
+ || (job.needsApproval && job.needsApproval.length > 0));
1174
+ if (!job || isJobActive(job.status) || hasDecision) return;
1024
1175
  var token = job && job.approvalToken ? job.approvalToken : (job && job.spec ? approvalTokensRef.current[job.spec] : undefined);
1025
1176
  call("jobDismiss", { jobId: id, token: token }).catch(function () {});
1177
+ if (job.spec) delete approvalTokensRef.current[job.spec];
1178
+ dropJob(id);
1179
+ });
1180
+ },
1181
+ onCancel: function (id) {
1182
+ var job = jobs[id];
1183
+ if (!job || job.status !== "running") return;
1184
+ setJobStatus(id, "stopping");
1185
+ call("jobCancel", { jobId: id }).catch(function (e) {
1186
+ setJobStatus(id, "running");
1187
+ reportError("tasks", e);
1026
1188
  });
1027
- approvalTokensRef.current = {};
1028
- clearJobs();
1029
1189
  },
1030
1190
  onApprove: doApprove,
1191
+ onConfirmPreflight: function () {
1192
+ if (!preflight) return;
1193
+ var spec = preflight.spec;
1194
+ var carry = preflight.jobId;
1195
+ var report = preflight.report;
1196
+ setPreflight(null);
1197
+ // consentDigest 来自预检 job 的 extras,装的时候与当前报告比对:
1198
+ // 用户点「继续」到安装真正开跑之间候选包或 profile 变了,
1199
+ // 布尔同意不得沿用,要重新看新的警告。
1200
+ doRawInstall(spec, { acceptWarnings: true, acceptedReportDigest: report.consentDigest }, carry);
1201
+ },
1202
+ onClosePreflight: function () {
1203
+ if (preflight && preflight.spec) delete approvalTokensRef.current[preflight.spec];
1204
+ setPreflight(null);
1205
+ },
1031
1206
  onDismiss: function (id) {
1032
1207
  var job = jobs[id];
1033
1208
  var token = job && job.approvalToken ? job.approvalToken : (job && job.spec ? approvalTokensRef.current[job.spec] : undefined);
@@ -1036,29 +1211,11 @@ window.__ModuleLoader__.load({
1036
1211
  dropJob(id);
1037
1212
  },
1038
1213
  onDrop: dropJob,
1039
- onRestart: doRestart,
1214
+ onRestart: function () { setRestartConfirm(true); },
1040
1215
  restarting: restarting,
1041
1216
  hostStartedAt: hostStartedAt,
1042
1217
  approving: Object.keys(installing).filter(function (s) { return installing[s]; })[0],
1043
1218
  }),
1044
- preflight ? h(PreflightCard, {
1045
- spec: preflight.spec,
1046
- report: preflight.report,
1047
- busy: installing[preflight.spec] === true,
1048
- onConfirm: function () {
1049
- var spec = preflight.spec;
1050
- var carry = preflight.jobId;
1051
- setPreflight(null);
1052
- // consentDigest 来自预检 job 的 extras,装的时候与当前报告比对:
1053
- // 用户点「继续」到安装真正开跑之间候选包或 profile 变了,
1054
- // 布尔同意不得沿用,要重新看新的警告。
1055
- doRawInstall(spec, { acceptWarnings: true, acceptedReportDigest: preflight.report.consentDigest }, carry);
1056
- },
1057
- onClose: function () {
1058
- if (preflight && preflight.spec) delete approvalTokensRef.current[preflight.spec];
1059
- setPreflight(null);
1060
- },
1061
- }) : null,
1062
1219
  h("div", { className: "mkt_list" },
1063
1220
  results == null
1064
1221
  ? h("div", { className: "mkt_meta mkt_listHead" }, loading ? "正在加载最热插件…" : "—")
@@ -1095,7 +1252,16 @@ window.__ModuleLoader__.load({
1095
1252
  ? h("div", { className: "mkt_loadMore", ref: sentinelRef }, loadingMore ? "加载中…" : Date.now() < retryAt ? "GitHub 限流中,稍后再下滑加载" : "下滑加载更多")
1096
1253
  : h("div", { className: "mkt_loadMore" }, reachedLimit ? "已达 GitHub 搜索上限(前 1000 个结果)" : "已显示全部 " + results.items.length + " 个")
1097
1254
  )
1098
- )
1255
+ ),
1256
+ restartConfirm ? h(RestartDialog, {
1257
+ activeCount: activeJobCount,
1258
+ busy: restarting,
1259
+ onCancel: function () { setRestartConfirm(false); },
1260
+ onConfirm: function () {
1261
+ setRestartConfirm(false);
1262
+ doRestart();
1263
+ },
1264
+ }) : null
1099
1265
  );
1100
1266
  }
1101
1267