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

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/client.js +255 -111
  3. package/src/index.js +42 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1e0zj/dsh-plugin-mall",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "dsh 插件市场:搜索 GitHub dsh-plugin 话题下的插件仓库,一键安装到本地 dsh profile(agent 工具 + 设置页插件市场 tab)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/client.js CHANGED
@@ -28,6 +28,7 @@ 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}",
31
32
  ".mkt_list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;align-items:stretch;min-width:0}",
32
33
  "@media (max-width:820px){.mkt_list{grid-template-columns:minmax(0,1fr)}}",
33
34
  ".mkt_listHead{grid-column:1/-1;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
@@ -43,8 +44,9 @@ window.__ModuleLoader__.load({
43
44
  ".mkt_panelTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}",
44
45
  ".mkt_panelRow{display:flex;align-items:center;gap:8px}",
45
46
  ".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}",
47
+ ".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
48
  ".mkt_ok{color:var(--dsw-alias-state-success-primary)}",
49
+ ".mkt_warn{color:var(--dsw-alias-state-warn-label)}",
48
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)}",
49
51
  ".mkt_badgeOk{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}",
50
52
  ".mkt_badgeWarn{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-label)}",
@@ -57,13 +59,31 @@ window.__ModuleLoader__.load({
57
59
  ".mkt_depList{display:flex;flex-direction:column;gap:6px;margin-top:8px}",
58
60
  ".mkt_depRow{display:flex;justify-content:space-between;align-items:center;gap:8px}",
59
61
  ".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)}",
62
+ ".mkt_decision{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-state-warn-secondary);border-radius:6px;overflow:hidden;margin-top:4px}",
63
+ ".mkt_decisionError{border-color:var(--dsw-alias-state-error-primary)}",
64
+ ".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)}",
65
+ ".mkt_decisionError .mkt_decisionHead{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-state-error-primary)}",
66
+ ".mkt_decisionTitle{font-size:13px;font-weight:600}",
67
+ ".mkt_decisionBody{display:flex;flex-direction:column;gap:8px;min-width:0;padding:10px;max-height:300px;overflow:auto}",
68
+ ".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
69
  ".mkt_approvePkg{display:flex;flex-direction:column;gap:4px}",
63
70
  ".mkt_approveName{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-right:8px;overflow-wrap:anywhere}",
64
71
  ".mkt_approveCmd{max-height:none;margin:0}",
65
72
  ".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}",
73
+ ".mkt_logBlock{display:flex;flex-direction:column;gap:4px;align-items:stretch;min-width:0;max-width:100%}",
74
+ ".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}",
75
+ ".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)}",
76
+ ".mkt_panelRow + .mkt_job{padding-top:0;border-top:0}",
77
+ ".mkt_jobHead{display:flex;align-items:center;gap:8px;min-width:0}",
78
+ ".mkt_jobIdentity{display:flex;align-items:center;gap:7px;min-width:0;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
79
+ ".mkt_jobHead .mkt_btn{margin-left:auto;flex:none}",
80
+ ".mkt_stateDot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-caption);flex:none}",
81
+ ".mkt_stateRunning{background:var(--dsw-alias-state-business-primary);animation:mktPulse 1.6s ease-in-out infinite}",
82
+ ".mkt_stateDone{background:var(--dsw-alias-state-success-primary)}",
83
+ ".mkt_stateWarn{background:var(--dsw-alias-state-warn-primary)}",
84
+ ".mkt_stateError{background:var(--dsw-alias-state-error-primary)}",
85
+ "@keyframes mktPulse{0%,100%{opacity:.45}50%{opacity:1}}",
86
+ "@media (prefers-reduced-motion:reduce){.mkt_stateRunning{animation:none}.mkt_switch,.mkt_switchKnob{transition:none}}",
67
87
  ".mkt_depOff{opacity:.5;text-decoration:line-through}",
68
88
  ".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
89
  ".mkt_switch:disabled{opacity:.55;cursor:default}",
@@ -71,11 +91,16 @@ window.__ModuleLoader__.load({
71
91
  ".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
92
  ".mkt_switchOn .mkt_switchKnob{transform:translateX(16px)}",
73
93
  ".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)}",
94
+ ".mkt_issue{border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:8px 10px;background:var(--dsw-alias-bg-layer-3)}",
75
95
  ".mkt_issueBlock{border-color:var(--dsw-alias-state-error-primary)}",
76
96
  ".mkt_issueWarn{border-color:var(--dsw-alias-state-warn-primary)}",
77
97
  ".mkt_issueTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}",
78
98
  ".mkt_issueDetail{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:4px;line-height:18px;overflow-wrap:anywhere}",
99
+ ".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)}",
100
+ ".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)}",
101
+ ".mkt_modalHead{padding:14px 16px 8px;font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}",
102
+ ".mkt_modalBody{padding:0 16px 14px;font-size:12.5px;line-height:19px;color:var(--dsw-alias-label-secondary);overflow:auto}",
103
+ ".mkt_modalActions{display:flex;justify-content:flex-end;gap:8px;padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2)}",
79
104
  ].join("\n");
80
105
  var tagId = "@1e0zj/dsh-plugin-mall/market-tab.css";
81
106
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
@@ -102,6 +127,10 @@ window.__ModuleLoader__.load({
102
127
  return String(e && e.message ? e.message : e);
103
128
  }
104
129
 
130
+ function isJobActive(status) {
131
+ return status === "running" || status === "stopping";
132
+ }
133
+
105
134
  function kindLabel(kind) {
106
135
  if (kind === "bundle") return "宿主插件层";
107
136
  if (kind === "client") return "浏览器UI插件";
@@ -159,7 +188,7 @@ window.__ModuleLoader__.load({
159
188
  var current = jobsRef.current;
160
189
  var running = Object.keys(current).filter(function (id) {
161
190
  var job = current[id];
162
- return job && job.status !== "completed" && job.status !== "failed" && job.status !== "killed";
191
+ return job && isJobActive(job.status);
163
192
  });
164
193
  if (running.length === 0) return;
165
194
  running.forEach(function (id) {
@@ -211,21 +240,23 @@ window.__ModuleLoader__.load({
211
240
  delete next[carryFromId];
212
241
  }
213
242
  for (var key in next) {
214
- if (key !== id && next[key] && next[key].spec === spec && next[key].status !== "running") {
243
+ if (key !== id && next[key] && next[key].spec === spec && !isJobActive(next[key].status)) {
215
244
  delete next[key];
216
245
  }
217
246
  }
218
247
  next[id] = { status: "running", spec: spec, output: carried };
219
248
  commit(next);
220
249
  }, [commit]);
221
- var clear = useCallback(function () {
222
- commit({});
223
- }, [commit]);
224
250
  var drop = useCallback(function (id) {
225
251
  var next = Object.assign({}, jobsRef.current);
226
252
  delete next[id];
227
253
  commit(next);
228
254
  }, [commit]);
255
+ var setStatus = useCallback(function (id, status) {
256
+ var current = jobsRef.current[id];
257
+ if (!current) return;
258
+ commit(Object.assign({}, jobsRef.current, { [id]: Object.assign({}, current, { status: status }) }));
259
+ }, [commit]);
229
260
  // 恢复后端任务记录(tracker.list 的形状):安装事务改写
230
261
  // cordis.patch.yml 会让 dsh 重放装配树、整个市场 UI 重挂载,React
231
262
  // state 全丢——任务面板、完成提醒、重启按钮一起消失(真实事故:
@@ -269,7 +300,7 @@ window.__ModuleLoader__.load({
269
300
  for (var key in next) {
270
301
  if (serverIds[key]) continue;
271
302
  var stale = next[key];
272
- if (stale.status === "running") {
303
+ if (isJobActive(stale.status)) {
273
304
  next[key] = Object.assign({}, stale, {
274
305
  status: "killed",
275
306
  detail: "宿主进程已重启,该任务的记录随之丢失",
@@ -282,7 +313,7 @@ window.__ModuleLoader__.load({
282
313
  }
283
314
  commit(next);
284
315
  }, [commit]);
285
- return { jobs: jobs, track: track, clear: clear, drop: drop, restore: restore };
316
+ return { jobs: jobs, track: track, drop: drop, setStatus: setStatus, restore: restore };
286
317
  }
287
318
 
288
319
  // ── plugin verification badge ───────────────────────────────────────────
@@ -319,7 +350,10 @@ window.__ModuleLoader__.load({
319
350
  var installLabel = props.alreadyInstalled === true ? "已装" : "安装";
320
351
  var installDisabled = installing || props.alreadyInstalled === true;
321
352
  if (!installing && job) {
322
- if (job.status === "running") { installLabel = "安装中…"; installDisabled = true; }
353
+ if (isJobActive(job.status)) {
354
+ installLabel = job.status === "stopping" ? "停止中…" : "安装中…";
355
+ installDisabled = true;
356
+ }
323
357
  else if (job.status === "completed") { installLabel = "已装 · 重启生效"; installDisabled = true; }
324
358
  else if (job.status === "failed") { installLabel = "安装失败 · 见任务日志"; }
325
359
  else { installLabel = "已取消 · 重试"; }
@@ -346,11 +380,62 @@ window.__ModuleLoader__.load({
346
380
  );
347
381
  }
348
382
 
383
+ function DecisionPanel(props) {
384
+ var tone = props.tone === "error" ? " mkt_decisionError" : "";
385
+ return h("div", { className: "mkt_decision" + tone },
386
+ h("div", { className: "mkt_decisionHead" },
387
+ h("span", { className: "mkt_decisionTitle" }, props.title),
388
+ props.meta ? h("span", { className: "mkt_meta" }, props.meta) : null),
389
+ h("div", { className: "mkt_decisionBody" }, props.children),
390
+ h("div", { className: "mkt_decisionActions" }, props.actions));
391
+ }
392
+
393
+ function ErrorNotice(props) {
394
+ if (!props.message) return null;
395
+ return h("div", { className: "mkt_notice", role: "alert" }, props.message);
396
+ }
397
+
398
+ function RestartDialog(props) {
399
+ useEffect(function () {
400
+ function onKeyDown(event) {
401
+ if (event.key === "Escape" && props.busy !== true) props.onCancel();
402
+ }
403
+ document.addEventListener("keydown", onKeyDown);
404
+ return function () { document.removeEventListener("keydown", onKeyDown); };
405
+ }, [props.busy, props.onCancel]);
406
+ return h("div", {
407
+ className: "mkt_modalMask",
408
+ onMouseDown: function (event) {
409
+ if (event.target === event.currentTarget && props.busy !== true) props.onCancel();
410
+ },
411
+ }, h("div", { className: "mkt_modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "mkt-restart-title" },
412
+ h("div", { id: "mkt-restart-title", className: "mkt_modalHead" }, "重启 dsh"),
413
+ h("div", { className: "mkt_modalBody" }, props.activeCount > 0
414
+ ? "当前有 " + props.activeCount + " 个任务仍在进行。重启会中断这些任务,未完成的改动将由事务恢复逻辑处理。"
415
+ : "重启后,已经完成的插件安装、更新或卸载才会生效。"),
416
+ h("div", { className: "mkt_modalActions" },
417
+ h("button", { className: "mkt_btn", disabled: props.busy === true, autoFocus: true, onClick: props.onCancel }, "取消"),
418
+ h("button", { className: "mkt_btn mkt_btnPrimary", disabled: props.busy === true, onClick: props.onConfirm }, props.busy ? "重启中…" : "确认重启"))));
419
+ }
420
+
349
421
  // ── install-script approval ─────────────────────────────────────────────
350
422
  function ApprovalRequest(props) {
351
423
  var pkgs = props.needsApproval || [];
352
- return h("div", { className: "mkt_approve" },
353
- h("div", { className: "mkt_approveHead" }, "需要你确认:这次安装会执行安装期代码"),
424
+ var actions = [
425
+ h("button", { key: "cancel", className: "mkt_btn mkt_btnSm", disabled: props.busy === true, onClick: props.onDismiss }, "取消"),
426
+ h("button", {
427
+ key: "allow",
428
+ className: "mkt_btn mkt_btnPrimary mkt_btnSm",
429
+ disabled: props.busy === true,
430
+ onClick: function () { props.onApprove(pkgs.map(function (p) { return p.name; })); },
431
+ }, props.busy ? "继续中…" : "允许并继续"),
432
+ ];
433
+ return h(DecisionPanel, {
434
+ title: "安装期代码需要确认",
435
+ meta: clip(props.spec || "", 50),
436
+ actions: actions,
437
+ },
438
+ props.detail ? h("div", { className: "mkt_desc" }, props.detail) : null,
354
439
  pkgs.map(function (p, index) {
355
440
  var facts = [];
356
441
  if (typeof p.weeklyDownloads === "number") facts.push("周下载 " + p.weeklyDownloads.toLocaleString());
@@ -370,16 +455,7 @@ window.__ModuleLoader__.load({
370
455
  h("div", { className: "mkt_meta" }, facts.join(" · "))
371
456
  );
372
457
  }),
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
- );
458
+ h("div", { className: "mkt_meta" }, "这些命令会以你的权限在你的机器上运行,早于任何插件代码加载。"));
383
459
  }
384
460
 
385
461
  // ── preflight result card ────────────────────────────────────────────────
@@ -391,11 +467,23 @@ window.__ModuleLoader__.load({
391
467
  var blocked = report.verdict === "blocked";
392
468
  var title = blocked ? "安装被阻止" : "安装存在风险";
393
469
  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
- ),
470
+ var actions = blocked
471
+ ? h("button", { className: "mkt_btn mkt_btnSm", onClick: props.onClose }, "关闭")
472
+ : [
473
+ h("button", { key: "cancel", className: "mkt_btn mkt_btnSm", disabled: props.busy === true, onClick: props.onClose }, "取消"),
474
+ h("button", {
475
+ key: "confirm",
476
+ className: "mkt_btn mkt_btnPrimary mkt_btnSm",
477
+ disabled: props.busy === true,
478
+ onClick: props.onConfirm,
479
+ }, props.busy ? "安装中…" : "了解风险并继续"),
480
+ ];
481
+ return h(DecisionPanel, {
482
+ tone: blocked ? "error" : "warning",
483
+ title: title,
484
+ meta: clip(props.spec, 50),
485
+ actions: actions,
486
+ },
399
487
  report.summary ? h("div", { className: "mkt_desc" }, report.summary) : null,
400
488
  issues.length > 0 ? h("ul", { className: "mkt_issueList" },
401
489
  issues.map(function (issue) {
@@ -406,21 +494,7 @@ window.__ModuleLoader__.load({
406
494
  h("div", { className: "mkt_issueTitle" }, issue.title),
407
495
  issue.detail ? h("div", { className: "mkt_issueDetail" }, issue.detail) : null);
408
496
  })
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
- );
497
+ ) : null);
424
498
  }
425
499
 
426
500
  var MARKET_PACKAGE = "@1e0zj/dsh-plugin-mall";
@@ -435,64 +509,99 @@ window.__ModuleLoader__.load({
435
509
  if (status === "completed") return "完成";
436
510
  if (status === "failed") return "失败";
437
511
  if (status === "killed") return "已取消";
512
+ if (status === "stopping") return "停止中";
438
513
  return "进行中";
439
514
  }
440
515
 
441
516
  function jobStatusClass(status) {
442
517
  if (status === "completed") return "mkt_ok";
443
- if (status === "failed" || status === "killed") return "mkt_error";
518
+ if (status === "failed") return "mkt_error";
519
+ if (status === "killed" || status === "stopping") return "mkt_warn";
444
520
  return "";
445
521
  }
446
522
 
523
+ function jobStateDotClass(status, awaitingDecision) {
524
+ if (awaitingDecision || status === "killed" || status === "stopping") return "mkt_stateWarn";
525
+ if (status === "completed") return "mkt_stateDone";
526
+ if (status === "failed") return "mkt_stateError";
527
+ return "mkt_stateRunning";
528
+ }
529
+
447
530
  // 任务日志:跑的时候只露尾部几行(够看进度,不霸屏),落定后默认折叠
448
531
  // ——成功的日志没人回头读,失败的才需要,点开即可。
449
- var LOG_TAIL_LINES = 8;
532
+ var LOG_TAIL_LINES = 12;
450
533
  function JobLog(props) {
451
534
  var _open = useState(false);
452
535
  var open = _open[0];
453
536
  var setOpen = _open[1];
454
537
  var text = String(props.output || "").trimEnd();
538
+ var failed = props.status === "failed";
539
+ useEffect(function () {
540
+ if (failed) setOpen(true);
541
+ }, [failed]);
455
542
  if (text.length === 0) return null;
456
543
  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);
544
+ var settled = !isJobActive(props.status);
545
+ var collapsed = settled && !open;
546
+ var shown = settled || lines.length <= LOG_TAIL_LINES ? lines : lines.slice(-LOG_TAIL_LINES);
547
+ var rendered = shown.join("\n");
548
+ if (rendered.length > 64000) rendered = "…前部日志已截断…\n" + rendered.slice(-64000);
459
549
  // 展开/收起始终在日志块上方:放在下方的话,点开后按钮会被推到长长的
460
550
  // 日志末尾,想收起还得先滚回去。
461
551
  return h("div", { className: "mkt_logBlock" },
462
- props.done
463
- ? h("span", {
464
- className: "mkt_link",
552
+ settled
553
+ ? h("button", {
554
+ type: "button",
555
+ className: "mkt_linkButton",
556
+ "aria-expanded": open ? "true" : "false",
465
557
  onClick: function () { setOpen(!open); },
466
558
  }, open ? "收起日志" : "查看日志(" + lines.length + " 行)")
467
559
  : lines.length > LOG_TAIL_LINES
468
560
  ? h("div", { className: "mkt_meta" }, "只显示最后 " + LOG_TAIL_LINES + " 行")
469
561
  : null,
470
- collapsed ? null : h("pre", { className: "mkt_pre" }, shown.join("\n").slice(-4000))
562
+ collapsed ? null : h("pre", { className: "mkt_pre", "aria-label": "任务日志" }, rendered)
471
563
  );
472
564
  }
473
565
 
474
566
  // ── jobs panel ──────────────────────────────────────────────────────────
475
567
  function JobsPanel(props) {
476
568
  var ids = Object.keys(props.jobs);
477
- if (ids.length === 0) return null;
569
+ if (ids.length === 0 && !props.error) return null;
570
+ var hasSettled = ids.some(function (id) {
571
+ var job = props.jobs[id];
572
+ var hasDecision = (props.preflight && props.preflight.jobId === id)
573
+ || (job.needsApproval && job.needsApproval.length > 0);
574
+ return !isJobActive(job.status) && !hasDecision;
575
+ });
478
576
  return h("div", { className: "mkt_card" },
479
577
  h("div", { className: "mkt_panelRow" },
480
578
  h("p", { className: "mkt_panelTitle" }, "任务"),
481
- props.onClear ? h("span", { className: "mkt_link", onClick: props.onClear }, "清空") : null
579
+ props.onClear && hasSettled ? h("span", { className: "mkt_link", onClick: props.onClear }, "清空已结束") : null
482
580
  ),
581
+ h(ErrorNotice, { message: props.error }),
483
582
  ids.map(function (id) {
484
583
  var job = props.jobs[id];
485
584
  var done = job.status === "completed" || job.status === "failed" || job.status === "killed";
486
- return h("div", { key: id, style: { display: "flex", flexDirection: "column", gap: "4px" } },
585
+ var preflightDecision = props.preflight && props.preflight.jobId === id ? props.preflight : null;
586
+ var awaitingDecision = preflightDecision !== null || (job.needsApproval && job.needsApproval.length > 0);
587
+ return h("div", { key: id, className: "mkt_job" },
487
588
  // 不再显示 market-N 这种内部 id——对用户没有意义,反而让人以为
488
589
  // 自己点了两次。只说清「在做什么 · 到哪一步了」。
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))),
590
+ // 状态词和状态点表达结论,任务类型与目标保持次要色。
591
+ h("div", { className: "mkt_jobHead" },
592
+ h("div", { className: "mkt_jobIdentity" },
593
+ h("span", { className: "mkt_stateDot " + jobStateDotClass(job.status, awaitingDecision), "aria-hidden": "true" }),
594
+ h("span", null, jobKindLabel(job.kind) + clip(job.spec || "", 44) + " · "),
595
+ h("span", { className: awaitingDecision ? "mkt_warn" : jobStatusClass(job.status) }, awaitingDecision ? "待确认" : jobStatusLabel(job.status))),
596
+ isJobActive(job.status) ? h("button", {
597
+ className: "mkt_btn mkt_btnSm",
598
+ disabled: job.status === "stopping",
599
+ onClick: function () { props.onCancel(id); },
600
+ }, job.status === "stopping" ? "停止中…" : "停止") : null),
494
601
  job.needsApproval && job.needsApproval.length > 0
495
602
  ? h(ApprovalRequest, {
603
+ spec: job.spec,
604
+ detail: job.detail,
496
605
  needsApproval: job.needsApproval,
497
606
  busy: props.approving === job.spec,
498
607
  onApprove: function (names) {
@@ -505,7 +614,15 @@ window.__ModuleLoader__.load({
505
614
  },
506
615
  onDismiss: function () { props.onDismiss(id); },
507
616
  })
508
- : job.detail ? h("div", { className: "mkt_desc" }, job.detail) : null,
617
+ : preflightDecision
618
+ ? h(PreflightCard, {
619
+ spec: preflightDecision.spec,
620
+ report: preflightDecision.report,
621
+ busy: props.preflightBusy === true,
622
+ onConfirm: props.onConfirmPreflight,
623
+ onClose: props.onClosePreflight,
624
+ })
625
+ : job.detail ? h("div", { className: job.status === "failed" ? "mkt_error" : "mkt_desc" }, job.detail) : null,
509
626
  // 预检完成不提示重启——它没有改动任何东西,接下来才是安装。
510
627
  // 也不再重复一个绿色「完成」:状态行已经写了「· 完成」。
511
628
  done && job.status === "completed" && job.kind !== "dsh-plugin-preflight"
@@ -520,10 +637,8 @@ window.__ModuleLoader__.load({
520
637
  disabled: props.restarting === true,
521
638
  onClick: props.onRestart,
522
639
  }, 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 })
640
+ : null,
641
+ h(JobLog, { output: job.output, status: job.status })
527
642
  );
528
643
  })
529
644
  );
@@ -548,6 +663,7 @@ window.__ModuleLoader__.load({
548
663
  h("span", { className: "mkt_badge" }, count + " 个"),
549
664
  h("span", { className: "mkt_meta mkt_installedToggle" }, open ? "收起" : "展开")
550
665
  ),
666
+ h(ErrorNotice, { message: props.error }),
551
667
  installed.error
552
668
  ? h("div", { className: "mkt_error" }, installed.error)
553
669
  : !open ? null
@@ -604,6 +720,9 @@ window.__ModuleLoader__.load({
604
720
  var _error = useState(null);
605
721
  var error = _error[0];
606
722
  var setError = _error[1];
723
+ var reportError = useCallback(function (scope, value) {
724
+ setError({ scope: scope, message: errorText(value) });
725
+ }, []);
607
726
  var _installed = useState(null);
608
727
  var installed = _installed[0];
609
728
  var setInstalled = _installed[1];
@@ -645,6 +764,9 @@ window.__ModuleLoader__.load({
645
764
  var _restarting = useState(false);
646
765
  var restarting = _restarting[0];
647
766
  var setRestarting = _restarting[1];
767
+ var _restartConfirm = useState(false);
768
+ var restartConfirm = _restartConfirm[0];
769
+ var setRestartConfirm = _restartConfirm[1];
648
770
  // 本次宿主进程的启动时间(jobs 端点带回):完成时间早于它的任务,
649
771
  // 其「重启 dsh 生效」按钮已经兑现,改显示「重启已生效」。
650
772
  var _hostStartedAt = useState(0);
@@ -714,11 +836,11 @@ window.__ModuleLoader__.load({
714
836
  setCompat({});
715
837
  verifyPage(value.items);
716
838
  }).catch(function (e) {
717
- setError(errorText(e));
839
+ reportError("search", e);
718
840
  }).finally(function () {
719
841
  setLoading(false);
720
842
  });
721
- }, [call, query, sort, verifyPage]);
843
+ }, [call, query, reportError, sort, verifyPage]);
722
844
 
723
845
  var canLoadMore = results !== null && results.items.length < results.total && !reachedLimit;
724
846
  var loadMoreLock = useRef(false);
@@ -744,13 +866,13 @@ window.__ModuleLoader__.load({
744
866
  return { total: value.total, items: merged };
745
867
  });
746
868
  }).catch(function (e) {
747
- setError(errorText(e));
869
+ reportError("search", e);
748
870
  setRetryAt(Date.now() + 60000);
749
871
  }).finally(function () {
750
872
  loadMoreLock.current = false;
751
873
  setLoadingMore(false);
752
874
  });
753
- }, [call, canLoadMore, loading, loadingMore, page, query, sort, retryAt, verifyPage]);
875
+ }, [call, canLoadMore, loading, loadingMore, page, query, reportError, sort, retryAt, verifyPage]);
754
876
 
755
877
  useEffect(function () {
756
878
  var node = sentinelRef.current;
@@ -775,7 +897,7 @@ window.__ModuleLoader__.load({
775
897
  return prev && !prev.error ? Object.assign({}, prev, { entries: value.entries }) : prev;
776
898
  });
777
899
  }).catch(function (e) {
778
- setError(errorText(e));
900
+ reportError("installed", e);
779
901
  }).finally(function () {
780
902
  setToggling(function (prev) {
781
903
  var next = Object.assign({}, prev);
@@ -783,7 +905,7 @@ window.__ModuleLoader__.load({
783
905
  return next;
784
906
  });
785
907
  });
786
- }, [call]);
908
+ }, [call, reportError]);
787
909
 
788
910
  var refreshInstalled = useCallback(function () {
789
911
  call("installed", {}).then(function (value) {
@@ -821,8 +943,8 @@ window.__ModuleLoader__.load({
821
943
  var polling = useJobPolling(call, onJobSettled, onApprovalToken, onPreflightSettled);
822
944
  var track = polling.track;
823
945
  var jobs = polling.jobs;
824
- var clearJobs = polling.clear;
825
946
  var dropJob = polling.drop;
947
+ var setJobStatus = polling.setStatus;
826
948
 
827
949
  useEffect(function () {
828
950
  doSearch();
@@ -848,7 +970,7 @@ window.__ModuleLoader__.load({
848
970
  track(value.jobId, spec, carryFromId);
849
971
  }).catch(function (e) {
850
972
  delete approvalTokensRef.current[spec];
851
- setError(errorText(e));
973
+ reportError("tasks", e);
852
974
  }).finally(function () {
853
975
  setInstalling(function (prev) {
854
976
  var next = Object.assign({}, prev);
@@ -856,7 +978,7 @@ window.__ModuleLoader__.load({
856
978
  return next;
857
979
  });
858
980
  });
859
- }, [call, track]);
981
+ }, [call, reportError, track]);
860
982
 
861
983
  var doApprove = useCallback(function (spec, names, token, carryFromId) {
862
984
  var extra = { allowBuildScripts: names };
@@ -872,7 +994,7 @@ window.__ModuleLoader__.load({
872
994
  useEffect(function () {
873
995
  preflightHandlerRef.current = function (spec, report, jobId) {
874
996
  if (!report || !report.verdict) {
875
- setError("预检没有返回结论,请重试");
997
+ reportError("tasks", "预检没有返回结论,请重试");
876
998
  return;
877
999
  }
878
1000
  if (report.verdict === "safe") {
@@ -882,7 +1004,7 @@ window.__ModuleLoader__.load({
882
1004
  setPreflight({ spec: spec, report: report, jobId: jobId });
883
1005
  }
884
1006
  };
885
- }, [doRawInstall]);
1007
+ }, [doRawInstall, reportError]);
886
1008
 
887
1009
  // 点安装 = 立刻起一个预检 job,面板马上有东西看、日志实时流。
888
1010
  // 结论由轮询经 onPreflightSettled 交回上面的 handler,这里不等待。
@@ -892,18 +1014,15 @@ window.__ModuleLoader__.load({
892
1014
  call("preflight", { spec: spec }).then(function (value) {
893
1015
  track(value.jobId, value.spec || spec);
894
1016
  }).catch(function (e) {
895
- setError(errorText(e));
1017
+ reportError("tasks", e);
896
1018
  });
897
- }, [call, track]);
1019
+ }, [call, reportError, track]);
898
1020
 
899
1021
  var doInstall = useCallback(function (repo) {
900
1022
  preflightAndInstall("github:" + repo);
901
1023
  }, [preflightAndInstall]);
902
1024
 
903
1025
  var doRestart = useCallback(function () {
904
- if (typeof window !== "undefined" && typeof window.confirm === "function") {
905
- if (window.confirm("重启 dsh?正在进行的任务会中断。") !== true) return;
906
- }
907
1026
  clearRestartPing();
908
1027
  setRestarting(true);
909
1028
  setError(null);
@@ -921,7 +1040,7 @@ window.__ModuleLoader__.load({
921
1040
  if (tries > 40) {
922
1041
  clearRestartPing();
923
1042
  setRestarting(false);
924
- setError("2 分钟内未检测到 dsh 重启完成,请手动检查 dsh 状态后刷新页面。");
1043
+ reportError("restart", "2 分钟内未检测到 dsh 重启完成,请手动检查 dsh 状态后刷新页面。");
925
1044
  return;
926
1045
  }
927
1046
  // Prefer identity change over wall-clock ordering: NTP may move the
@@ -944,9 +1063,9 @@ window.__ModuleLoader__.load({
944
1063
  if (restartControlRef.current.mounted !== true) return;
945
1064
  clearRestartPing();
946
1065
  setRestarting(false);
947
- setError(errorText(e));
1066
+ reportError("restart", e);
948
1067
  });
949
- }, [call, clearRestartPing, hostStartedAt]);
1068
+ }, [call, clearRestartPing, hostStartedAt, reportError]);
950
1069
 
951
1070
  var doUninstall = useCallback(function (name) {
952
1071
  delete approvalTokensRef.current[name];
@@ -958,7 +1077,7 @@ window.__ModuleLoader__.load({
958
1077
  call("uninstall", { package: name }).then(function (value) {
959
1078
  track(value.jobId, name);
960
1079
  }).catch(function (e) {
961
- setError(errorText(e));
1080
+ reportError("installed", e);
962
1081
  }).finally(function () {
963
1082
  setRemoving(function (prev) {
964
1083
  var next = Object.assign({}, prev);
@@ -966,7 +1085,7 @@ window.__ModuleLoader__.load({
966
1085
  return next;
967
1086
  });
968
1087
  });
969
- }, [call, track]);
1088
+ }, [call, reportError, track]);
970
1089
 
971
1090
  useEffect(function () {
972
1091
  refreshInstalled();
@@ -978,6 +1097,9 @@ window.__ModuleLoader__.load({
978
1097
  return v !== undefined && (v.kind === "bundle" || v.kind === "client")
979
1098
  && !(v.hostDeps !== undefined && v.hostDeps.length > 0);
980
1099
  });
1100
+ var activeJobCount = Object.keys(jobs).filter(function (id) {
1101
+ return jobs[id] && isJobActive(jobs[id].status);
1102
+ }).length;
981
1103
  resultsRef.current = results;
982
1104
  var verifyPending = results !== null && results.items.some(function (it) { return verified[it.fullName] === undefined; });
983
1105
 
@@ -1000,12 +1122,12 @@ window.__ModuleLoader__.load({
1000
1122
  h("option", { value: "forks" }, "按 fork")
1001
1123
  ),
1002
1124
  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"),
1125
+ h("button", { className: "mkt_btn mkt_btnDanger", disabled: restarting, onClick: function () { setRestartConfirm(true); } }, restarting ? "重启中…" : "重启 dsh"),
1004
1126
  h("label", { className: "mkt_check" },
1005
1127
  h("input", { type: "checkbox", checked: verifiedOnly, onChange: function (e) { setVerifiedOnly(e.target.checked); } }),
1006
1128
  "只看已验证插件")
1007
1129
  ),
1008
- error ? h("div", { className: "mkt_error" }, error) : null,
1130
+ h(ErrorNotice, { message: error && (error.scope === "search" || error.scope === "restart") ? error.message : null }),
1009
1131
  h(InstalledPanel, {
1010
1132
  installed: installed,
1011
1133
  removing: removing,
@@ -1015,19 +1137,50 @@ window.__ModuleLoader__.load({
1015
1137
  onToggle: doToggle,
1016
1138
  onUninstall: doUninstall,
1017
1139
  onInstallSpec: preflightAndInstall,
1140
+ error: error && error.scope === "installed" ? error.message : null,
1018
1141
  }),
1019
1142
  h(JobsPanel, {
1020
1143
  jobs: jobs,
1144
+ error: error && error.scope === "tasks" ? error.message : null,
1145
+ preflight: preflight,
1146
+ preflightBusy: preflight ? installing[preflight.spec] === true : false,
1021
1147
  onClear: function () {
1022
1148
  Object.keys(jobs).forEach(function (id) {
1023
1149
  var job = jobs[id];
1150
+ var hasDecision = job && ((preflight && preflight.jobId === id)
1151
+ || (job.needsApproval && job.needsApproval.length > 0));
1152
+ if (!job || isJobActive(job.status) || hasDecision) return;
1024
1153
  var token = job && job.approvalToken ? job.approvalToken : (job && job.spec ? approvalTokensRef.current[job.spec] : undefined);
1025
1154
  call("jobDismiss", { jobId: id, token: token }).catch(function () {});
1155
+ if (job.spec) delete approvalTokensRef.current[job.spec];
1156
+ dropJob(id);
1157
+ });
1158
+ },
1159
+ onCancel: function (id) {
1160
+ var job = jobs[id];
1161
+ if (!job || job.status !== "running") return;
1162
+ setJobStatus(id, "stopping");
1163
+ call("jobCancel", { jobId: id }).catch(function (e) {
1164
+ setJobStatus(id, "running");
1165
+ reportError("tasks", e);
1026
1166
  });
1027
- approvalTokensRef.current = {};
1028
- clearJobs();
1029
1167
  },
1030
1168
  onApprove: doApprove,
1169
+ onConfirmPreflight: function () {
1170
+ if (!preflight) return;
1171
+ var spec = preflight.spec;
1172
+ var carry = preflight.jobId;
1173
+ var report = preflight.report;
1174
+ setPreflight(null);
1175
+ // consentDigest 来自预检 job 的 extras,装的时候与当前报告比对:
1176
+ // 用户点「继续」到安装真正开跑之间候选包或 profile 变了,
1177
+ // 布尔同意不得沿用,要重新看新的警告。
1178
+ doRawInstall(spec, { acceptWarnings: true, acceptedReportDigest: report.consentDigest }, carry);
1179
+ },
1180
+ onClosePreflight: function () {
1181
+ if (preflight && preflight.spec) delete approvalTokensRef.current[preflight.spec];
1182
+ setPreflight(null);
1183
+ },
1031
1184
  onDismiss: function (id) {
1032
1185
  var job = jobs[id];
1033
1186
  var token = job && job.approvalToken ? job.approvalToken : (job && job.spec ? approvalTokensRef.current[job.spec] : undefined);
@@ -1036,29 +1189,11 @@ window.__ModuleLoader__.load({
1036
1189
  dropJob(id);
1037
1190
  },
1038
1191
  onDrop: dropJob,
1039
- onRestart: doRestart,
1192
+ onRestart: function () { setRestartConfirm(true); },
1040
1193
  restarting: restarting,
1041
1194
  hostStartedAt: hostStartedAt,
1042
1195
  approving: Object.keys(installing).filter(function (s) { return installing[s]; })[0],
1043
1196
  }),
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
1197
  h("div", { className: "mkt_list" },
1063
1198
  results == null
1064
1199
  ? h("div", { className: "mkt_meta mkt_listHead" }, loading ? "正在加载最热插件…" : "—")
@@ -1095,7 +1230,16 @@ window.__ModuleLoader__.load({
1095
1230
  ? h("div", { className: "mkt_loadMore", ref: sentinelRef }, loadingMore ? "加载中…" : Date.now() < retryAt ? "GitHub 限流中,稍后再下滑加载" : "下滑加载更多")
1096
1231
  : h("div", { className: "mkt_loadMore" }, reachedLimit ? "已达 GitHub 搜索上限(前 1000 个结果)" : "已显示全部 " + results.items.length + " 个")
1097
1232
  )
1098
- )
1233
+ ),
1234
+ restartConfirm ? h(RestartDialog, {
1235
+ activeCount: activeJobCount,
1236
+ busy: restarting,
1237
+ onCancel: function () { setRestartConfirm(false); },
1238
+ onConfirm: function () {
1239
+ setRestartConfirm(false);
1240
+ doRestart();
1241
+ },
1242
+ }) : null
1099
1243
  );
1100
1244
  }
1101
1245
 
package/src/index.js CHANGED
@@ -1084,14 +1084,13 @@ export function createJobTracker({ producerFactory } = {}) {
1084
1084
  const prune = () => {
1085
1085
  const now = Date.now();
1086
1086
  for (const [id, record] of records) {
1087
- const terminal = record.status !== "running";
1088
- if (terminal && record.finishedAt !== undefined && now - record.finishedAt > 3600000) records.delete(id);
1087
+ if (record.finishedAt !== undefined && now - record.finishedAt > 3600000) records.delete(id);
1089
1088
  }
1090
1089
  if (records.size > 20) {
1091
1090
  const ordered = [...records.entries()].sort((a, b) => a[1].startedAt - b[1].startedAt);
1092
1091
  for (const [id, record] of ordered) {
1093
1092
  if (records.size <= 20) break;
1094
- if (record.status !== "running") records.delete(id);
1093
+ if (record.finishedAt !== undefined) records.delete(id);
1095
1094
  }
1096
1095
  }
1097
1096
  };
@@ -1314,6 +1313,9 @@ export function createJobTracker({ producerFactory } = {}) {
1314
1313
  }
1315
1314
  }
1316
1315
  if (typeof record.producer?.cancel === "function") {
1316
+ if (record.finishedAt === undefined && record.status === "running") {
1317
+ record.status = "stopping";
1318
+ }
1317
1319
  record.producer.cancel();
1318
1320
  }
1319
1321
  if (record.approvalToken) {
@@ -1330,6 +1332,9 @@ export function createJobTracker({ producerFactory } = {}) {
1330
1332
  if (record.surface === "browser" && (record.session === "" || record.session !== session)) {
1331
1333
  return false;
1332
1334
  }
1335
+ // Dismiss hides settled history. Live work must go through cancel and
1336
+ // remain observable until its producer reports a terminal outcome.
1337
+ if (record.finishedAt === undefined) return false;
1333
1338
  // Marked, not deleted: `list` (panel restore after a remount) skips these,
1334
1339
  // so a cleared panel stays cleared across remounts.
1335
1340
  record.dismissed = true;
@@ -3012,6 +3017,40 @@ export async function runSelfTests() {
3012
3017
  const snapAfterDismiss = sessionTracker.get(sessionJobId, "session-alpha").snapshot;
3013
3018
  check("dismiss 后 job snapshot 中 approvalToken 为 undefined", snapAfterDismiss.approvalToken === undefined);
3014
3019
 
3020
+ // 运行中任务不能靠 dismiss 从观察面消失;cancel 先进入 stopping,等
3021
+ // producer 真正结算后才成为可清理的历史。
3022
+ {
3023
+ let settleLive;
3024
+ let cancelCalls = 0;
3025
+ const liveProducer = {
3026
+ cancel: () => { cancelCalls++; },
3027
+ done: new Promise((resolvePromise) => { settleLive = resolvePromise; }),
3028
+ readOutput: () => "",
3029
+ };
3030
+ const liveTracker = createJobTracker({ producerFactory: () => liveProducer });
3031
+ const liveJobId = liveTracker.start({
3032
+ profile: "web",
3033
+ spec: "live-pkg",
3034
+ surface: "browser",
3035
+ session: "session-alpha",
3036
+ });
3037
+ check("运行中 job 拒绝 dismiss", liveTracker.dismiss(liveJobId, "session-alpha") === false);
3038
+ check("dismiss 被拒后运行中 job 仍可恢复",
3039
+ liveTracker.list("session-alpha").some((entry) => entry.id === liveJobId));
3040
+ check("cancel 请求把可杀 job 标为 stopping",
3041
+ liveTracker.cancel(liveJobId, "session-alpha") === "requested"
3042
+ && liveTracker.get(liveJobId, "session-alpha").snapshot.status === "stopping"
3043
+ && cancelCalls === 1);
3044
+ check("stopping job 仍拒绝 dismiss", liveTracker.dismiss(liveJobId, "session-alpha") === false);
3045
+ settleLive({ status: "killed", detail: "cancelled" });
3046
+ await new Promise((resolvePromise) => setImmediate(resolvePromise));
3047
+ check("producer 结算后 job 进入 killed",
3048
+ liveTracker.get(liveJobId, "session-alpha").snapshot.status === "killed");
3049
+ check("终态 job 可 dismiss 并不再恢复",
3050
+ liveTracker.dismiss(liveJobId, "session-alpha") === true
3051
+ && liveTracker.list("session-alpha").every((entry) => entry.id !== liveJobId));
3052
+ }
3053
+
3015
3054
  // ── 5b. list(重挂载恢复):日志累积、dismissed 过滤、session 隔离 ────────
3016
3055
  // 安装事务改写 cordis.patch.yml 会让 dsh 重放装配树、市场 UI 整体重挂载,
3017
3056
  // 前端 state 全丢。任务记录在后端活着——list 就是恢复通道:drain 过的