@huaqiu/dsh-tool-plugin-update 0.6.17 → 0.6.18
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/lib/client.js +197 -10
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +15 -2
- package/package.json +3 -3
- package/src/client/api.ts +56 -0
- package/src/client/copy.ts +6 -0
- package/src/client/index.ts +149 -15
- package/src/client/update-view.ts +53 -5
- package/src/routes.ts +32 -2
package/lib/client.js
CHANGED
|
@@ -639,6 +639,37 @@ window.__ModuleLoader__.load({
|
|
|
639
639
|
function fetchJobStatus() {
|
|
640
640
|
return request("/status").then(normalizeJobStatus);
|
|
641
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Normalize `GET /activity`. Unknown/missing fields degrade to a usable
|
|
644
|
+
* snapshot: a `bootId` empty string simply never triggers a re-attach, and an
|
|
645
|
+
* unparseable age reads as 0 rather than "stalled".
|
|
646
|
+
*/
|
|
647
|
+
function normalizeActivity(raw) {
|
|
648
|
+
const r = raw !== null && typeof raw === "object" ? raw : {};
|
|
649
|
+
const str = (v) => typeof v === "string" ? v : "";
|
|
650
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
651
|
+
return {
|
|
652
|
+
bootId: str(r.bootId),
|
|
653
|
+
startedAt: str(r.startedAt),
|
|
654
|
+
now: str(r.now),
|
|
655
|
+
activeJob: normalizeJob(r.activeJob),
|
|
656
|
+
job: normalizeJob(r.job),
|
|
657
|
+
phaseAgeMs: Math.max(0, num(r.phaseAgeMs)),
|
|
658
|
+
stalled: r.stalled === true,
|
|
659
|
+
setVersion: typeof r.setVersion === "string" && r.setVersion.length > 0 ? r.setVersion : null,
|
|
660
|
+
actionableCount: num(r.actionableCount)
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* GET the re-attach snapshot, optionally naming the job being watched.
|
|
665
|
+
*
|
|
666
|
+
* Used instead of `/status` whenever the panel is following a job it started:
|
|
667
|
+
* it answers "what happened to MY job" across a DSH restart, and each read lets
|
|
668
|
+
* hq-edge settle a job that its own process could not finish.
|
|
669
|
+
*/
|
|
670
|
+
function fetchActivity(jobId) {
|
|
671
|
+
return request(`/activity${jobId ? `?job=${encodeURIComponent(jobId)}` : ""}`).then(normalizeActivity);
|
|
672
|
+
}
|
|
642
673
|
//#endregion
|
|
643
674
|
//#region src/client/copy.ts
|
|
644
675
|
/**
|
|
@@ -812,6 +843,7 @@ window.__ModuleLoader__.load({
|
|
|
812
843
|
ctaNoSet: "暂无可安装的更新",
|
|
813
844
|
footerCount: (n) => `准备安装 ${n} 项兼容插件`,
|
|
814
845
|
footerBusy: "后台处理中,请勿关闭 DSH",
|
|
846
|
+
footerConfirming: "正在确认安装结果…",
|
|
815
847
|
footerFailed: "无法确认插件版本兼容性",
|
|
816
848
|
footerAllGood: "所有插件与运行时均正常",
|
|
817
849
|
footerRuntimeOnly: "运行时更新需在上方单独确认",
|
|
@@ -847,6 +879,8 @@ window.__ModuleLoader__.load({
|
|
|
847
879
|
"重启"
|
|
848
880
|
],
|
|
849
881
|
activeCloseNote: "关闭此面板不会取消更新,完成后将自动重启。",
|
|
882
|
+
stalledNote: "进度已停止回执,正在向 HQ Edge 确认安装结果…",
|
|
883
|
+
stalledAction: "立即确认",
|
|
850
884
|
rollbackTitle: "上次 HQ Edge 运行时更新已自动回滚",
|
|
851
885
|
failedTitle: "上次 HQ Edge 运行时更新失败",
|
|
852
886
|
rollbackBody: (oldVer, code) => `系统已安全恢复至旧版本 (v${oldVer})。代码: ${code}`,
|
|
@@ -1023,6 +1057,7 @@ window.__ModuleLoader__.load({
|
|
|
1023
1057
|
ctaNoSet: "Nothing to install yet",
|
|
1024
1058
|
footerCount: (n) => `Ready to install ${n} compatible plugin${n === 1 ? "" : "s"}`,
|
|
1025
1059
|
footerBusy: "Running in the background — keep DSH open",
|
|
1060
|
+
footerConfirming: "Confirming the install result…",
|
|
1026
1061
|
footerFailed: "Cannot confirm plugin compatibility",
|
|
1027
1062
|
footerAllGood: "All plugins and the runtime are current",
|
|
1028
1063
|
footerRuntimeOnly: "Confirm the runtime upgrade on its card",
|
|
@@ -1058,6 +1093,8 @@ window.__ModuleLoader__.load({
|
|
|
1058
1093
|
"Restart"
|
|
1059
1094
|
],
|
|
1060
1095
|
activeCloseNote: "Closing this panel does not cancel the update; DSH restarts when it finishes.",
|
|
1096
|
+
stalledNote: "Progress has stopped reporting — confirming the result with HQ Edge…",
|
|
1097
|
+
stalledAction: "Check now",
|
|
1061
1098
|
rollbackTitle: "Last HQ Edge runtime update rolled back automatically",
|
|
1062
1099
|
failedTitle: "Last HQ Edge runtime update failed",
|
|
1063
1100
|
rollbackBody: (oldVer, code) => `Safely restored to the previous version (v${oldVer}). Code: ${code}`,
|
|
@@ -1450,11 +1487,19 @@ window.__ModuleLoader__.load({
|
|
|
1450
1487
|
* The active job card: title (with the target version) + percent, a
|
|
1451
1488
|
* determinate bar when the server publishes one, phase + telemetry, the honest
|
|
1452
1489
|
* "closing does not cancel" note, and the 4-step pipeline.
|
|
1490
|
+
*
|
|
1491
|
+
* `stalled` marks a job the server has stopped advancing. That is the normal
|
|
1492
|
+
* shape of "the set being installed contains this very plugin": activating it
|
|
1493
|
+
* restarts DSH, so the process that owned the job is gone and the last thing
|
|
1494
|
+
* the page saw was 100 % on the final step. Instead of animating a bar that
|
|
1495
|
+
* will never move, the card says it is confirming the outcome and offers a
|
|
1496
|
+
* retry — and the next `/activity` read settles it from the installed set.
|
|
1453
1497
|
*/
|
|
1454
|
-
function activeJobView(job, copy) {
|
|
1498
|
+
function activeJobView(job, copy, opts = {}) {
|
|
1455
1499
|
const section = el("section", "hqpu-group");
|
|
1456
1500
|
section.dataset.group = "active-job";
|
|
1457
1501
|
const card = el("div", "hqpu-active");
|
|
1502
|
+
if (opts.stalled === true) card.dataset.stalled = "1";
|
|
1458
1503
|
const top = el("div", "hqpu-active-top");
|
|
1459
1504
|
const titleWrap = el("div", "hqpu-active-title");
|
|
1460
1505
|
const pulse = el("span", "hqpu-pulse-dot");
|
|
@@ -1487,6 +1532,14 @@ window.__ModuleLoader__.load({
|
|
|
1487
1532
|
card.append(meta);
|
|
1488
1533
|
card.append(el("div", "hqpu-active-versions", job.previousVersion !== null && job.previousVersion.length > 0 ? `${job.previousVersion} → ${job.targetVersion}` : job.targetVersion));
|
|
1489
1534
|
card.append(el("p", "hqpu-active-note", job.kind === "runtime" ? copy.job.willSwapRuntime : copy.ui.activeCloseNote));
|
|
1535
|
+
if (opts.stalled === true) {
|
|
1536
|
+
const stalled = el("div", "hqpu-active-stalled");
|
|
1537
|
+
stalled.append(el("span", "", copy.ui.stalledNote));
|
|
1538
|
+
const recheck = linkButton("hqpu-link", copy.ui.stalledAction, () => opts.onRecheck?.());
|
|
1539
|
+
recheck.setAttribute("data-stalled-recheck", "");
|
|
1540
|
+
stalled.append(recheck);
|
|
1541
|
+
card.append(stalled);
|
|
1542
|
+
}
|
|
1490
1543
|
const current = stepIndex(job.phase);
|
|
1491
1544
|
const stepsRow = el("div", "hqpu-job-steps4");
|
|
1492
1545
|
copy.ui.jobSteps.forEach((label, index) => {
|
|
@@ -2024,6 +2077,7 @@ window.__ModuleLoader__.load({
|
|
|
2024
2077
|
return;
|
|
2025
2078
|
}
|
|
2026
2079
|
runtimeAccepted = true;
|
|
2080
|
+
store.watchJob?.(result.jobId ?? null);
|
|
2027
2081
|
showToast(copy.runtime.updateAccepted, "success");
|
|
2028
2082
|
store.fetchStatus();
|
|
2029
2083
|
} catch (err) {
|
|
@@ -2152,7 +2206,12 @@ window.__ModuleLoader__.load({
|
|
|
2152
2206
|
function renderJobSection() {
|
|
2153
2207
|
jobSection.textContent = "";
|
|
2154
2208
|
const active = store.activeJob();
|
|
2155
|
-
if (active !== null && !TERMINAL_PHASES.has(active.phase)) jobSection.append(activeJobView(active, copy
|
|
2209
|
+
if (active !== null && !TERMINAL_PHASES.has(active.phase)) jobSection.append(activeJobView(active, copy, {
|
|
2210
|
+
stalled: store.stalled?.() === true,
|
|
2211
|
+
onRecheck: () => {
|
|
2212
|
+
store.fetchStatus();
|
|
2213
|
+
}
|
|
2214
|
+
}));
|
|
2156
2215
|
}
|
|
2157
2216
|
function renderBanners(report, jobActive) {
|
|
2158
2217
|
bannerSection.textContent = "";
|
|
@@ -2285,7 +2344,7 @@ window.__ModuleLoader__.load({
|
|
|
2285
2344
|
if (busy) {
|
|
2286
2345
|
ctaLabel.textContent = copy.ui.ctaInstalling;
|
|
2287
2346
|
cta.disabled = true;
|
|
2288
|
-
footCount.textContent = copy.ui.footerBusy;
|
|
2347
|
+
footCount.textContent = store.stalled?.() === true ? copy.ui.footerConfirming : copy.ui.footerBusy;
|
|
2289
2348
|
} else if (report === null) {
|
|
2290
2349
|
ctaLabel.textContent = copy.ui.ctaNoSet;
|
|
2291
2350
|
cta.disabled = true;
|
|
@@ -2453,6 +2512,7 @@ window.__ModuleLoader__.load({
|
|
|
2453
2512
|
showToast(`${copy.installError}: ${result.error}`, "error");
|
|
2454
2513
|
return;
|
|
2455
2514
|
}
|
|
2515
|
+
store.watchJob?.(result.jobId ?? null);
|
|
2456
2516
|
showToast(copy.installSuccess, "success");
|
|
2457
2517
|
store.fetchStatus();
|
|
2458
2518
|
} catch (err) {
|
|
@@ -3739,6 +3799,45 @@ window.__ModuleLoader__.load({
|
|
|
3739
3799
|
line-height: 14px;
|
|
3740
3800
|
color: var(--hq-faint);
|
|
3741
3801
|
}
|
|
3802
|
+
/* A stalled job: the card keeps its shape but stops claiming progress. The
|
|
3803
|
+
* pulse stops, the bar dims, and the note line carries the retry. */
|
|
3804
|
+
.hqpu-active[data-stalled='1'] .hqpu-pulse-ring {
|
|
3805
|
+
animation: none;
|
|
3806
|
+
opacity: 0;
|
|
3807
|
+
}
|
|
3808
|
+
.hqpu-active[data-stalled='1'] .hqpu-pulse-dot::before {
|
|
3809
|
+
background: var(--hq-amber);
|
|
3810
|
+
}
|
|
3811
|
+
.hqpu-active[data-stalled='1'] .hqpu-active-percent {
|
|
3812
|
+
color: var(--hq-amber-ink);
|
|
3813
|
+
}
|
|
3814
|
+
.hqpu-active[data-stalled='1'] .hqpu-progress-fill {
|
|
3815
|
+
background: color-mix(in srgb, var(--hq-amber) 55%, var(--hq-border));
|
|
3816
|
+
transition: none;
|
|
3817
|
+
}
|
|
3818
|
+
/* The active step stops blinking too: nothing is advancing. */
|
|
3819
|
+
.hqpu-active[data-stalled='1'] .hqpu-job-step--active {
|
|
3820
|
+
color: var(--hq-amber-ink);
|
|
3821
|
+
animation: none;
|
|
3822
|
+
}
|
|
3823
|
+
.hqpu-active-stalled {
|
|
3824
|
+
display: flex;
|
|
3825
|
+
align-items: baseline;
|
|
3826
|
+
justify-content: space-between;
|
|
3827
|
+
gap: 8px;
|
|
3828
|
+
padding: 6px 8px;
|
|
3829
|
+
border-radius: 6px;
|
|
3830
|
+
background: color-mix(in srgb, var(--hq-amber) 10%, transparent);
|
|
3831
|
+
font-size: var(--hq-fs-micro);
|
|
3832
|
+
line-height: 14px;
|
|
3833
|
+
color: var(--hq-amber-ink);
|
|
3834
|
+
}
|
|
3835
|
+
.hqpu-active-stalled .hqpu-link {
|
|
3836
|
+
flex: none;
|
|
3837
|
+
font-size: var(--hq-fs-micro);
|
|
3838
|
+
color: var(--hq-amber-ink);
|
|
3839
|
+
text-decoration: underline;
|
|
3840
|
+
}
|
|
3742
3841
|
.hqpu-job-steps4 {
|
|
3743
3842
|
display: grid;
|
|
3744
3843
|
grid-template-columns: repeat(4, 1fr);
|
|
@@ -4343,7 +4442,13 @@ window.__ModuleLoader__.load({
|
|
|
4343
4442
|
tag.textContent = `${ENTRY_CSS_TEXT}\n${VIEW_CSS_TEXT}`;
|
|
4344
4443
|
document.head.appendChild(tag);
|
|
4345
4444
|
}
|
|
4346
|
-
/**
|
|
4445
|
+
/**
|
|
4446
|
+
* In-memory update-report store shared by the badge and the modal.
|
|
4447
|
+
*
|
|
4448
|
+
* Exported for tests: the re-attach logic (watch a job by id, notice that the
|
|
4449
|
+
* server was replaced, stop claiming progress) is the part that cannot be
|
|
4450
|
+
* exercised through the view alone.
|
|
4451
|
+
*/
|
|
4347
4452
|
var UpdateStore = class {
|
|
4348
4453
|
current = null;
|
|
4349
4454
|
installingFlag = false;
|
|
@@ -4356,6 +4461,12 @@ window.__ModuleLoader__.load({
|
|
|
4356
4461
|
pollTimer = null;
|
|
4357
4462
|
pollBackoffMs = 1e3;
|
|
4358
4463
|
pollInFlight = false;
|
|
4464
|
+
/** The job this panel started (or adopted) and is following by id. */
|
|
4465
|
+
watchedJobId = null;
|
|
4466
|
+
/** hq-edge process identity from the last `/activity` read. */
|
|
4467
|
+
activityBootId = null;
|
|
4468
|
+
stalledFlag = false;
|
|
4469
|
+
phaseAgeMsValue = 0;
|
|
4359
4470
|
report() {
|
|
4360
4471
|
return this.current;
|
|
4361
4472
|
}
|
|
@@ -4368,6 +4479,24 @@ window.__ModuleLoader__.load({
|
|
|
4368
4479
|
recentJobs() {
|
|
4369
4480
|
return [...this.recent];
|
|
4370
4481
|
}
|
|
4482
|
+
/** True when the server says the active job stopped advancing. */
|
|
4483
|
+
stalled() {
|
|
4484
|
+
return this.stalledFlag;
|
|
4485
|
+
}
|
|
4486
|
+
/** Milliseconds since the active job last advanced (server clock). */
|
|
4487
|
+
phaseAgeMs() {
|
|
4488
|
+
return this.phaseAgeMsValue;
|
|
4489
|
+
}
|
|
4490
|
+
/**
|
|
4491
|
+
* Follow a job by id. The panel calls this with the `jobId` an accept
|
|
4492
|
+
* response handed back, so every later read asks about THAT job — the only
|
|
4493
|
+
* way to learn its outcome when the install replaced this very plugin (the
|
|
4494
|
+
* page comes back to a server that has no "active job" left to report).
|
|
4495
|
+
*/
|
|
4496
|
+
watchJob(id) {
|
|
4497
|
+
this.watchedJobId = id;
|
|
4498
|
+
if (id !== null) this.schedulePoll();
|
|
4499
|
+
}
|
|
4371
4500
|
subscribe(listener) {
|
|
4372
4501
|
this.listeners.add(listener);
|
|
4373
4502
|
return () => {
|
|
@@ -4394,28 +4523,86 @@ window.__ModuleLoader__.load({
|
|
|
4394
4523
|
const wasActive = this.active !== null;
|
|
4395
4524
|
this.active = status.activeJob;
|
|
4396
4525
|
this.recent = status.recentJobs;
|
|
4397
|
-
if (
|
|
4526
|
+
if (this.active !== null) this.watchedJobId = this.active.id;
|
|
4527
|
+
if (wasActive && this.active === null) this.observeSettled();
|
|
4528
|
+
this.emit();
|
|
4529
|
+
this.schedulePoll();
|
|
4530
|
+
}
|
|
4531
|
+
/**
|
|
4532
|
+
* Adopt a `/activity` snapshot.
|
|
4533
|
+
*
|
|
4534
|
+
* Two things only this read can tell us:
|
|
4535
|
+
*
|
|
4536
|
+
* - `bootId` changed → hq-edge was recreated underneath the page. That is
|
|
4537
|
+
* what an install of this plugin's own package looks like from here: the
|
|
4538
|
+
* DSH process serving this route was replaced. Nothing in memory can be
|
|
4539
|
+
* trusted, so the installing flag is cleared and the report re-read.
|
|
4540
|
+
* - `stalled` → the active job has stopped advancing. The progress bar must
|
|
4541
|
+
* stop pretending; the panel shows a "confirming" note with a retry.
|
|
4542
|
+
*/
|
|
4543
|
+
applyActivity(activity) {
|
|
4544
|
+
const rebooted = this.activityBootId !== null && this.activityBootId !== activity.bootId;
|
|
4545
|
+
this.activityBootId = activity.bootId;
|
|
4546
|
+
this.stalledFlag = activity.stalled;
|
|
4547
|
+
this.phaseAgeMsValue = activity.phaseAgeMs;
|
|
4548
|
+
const wasActive = this.active !== null;
|
|
4549
|
+
this.active = activity.activeJob;
|
|
4550
|
+
if (activity.job !== null) this.upsertRecent(activity.job);
|
|
4551
|
+
if (rebooted) {
|
|
4398
4552
|
this.installingFlag = false;
|
|
4553
|
+
this.stalledFlag = false;
|
|
4554
|
+
this.phaseAgeMsValue = 0;
|
|
4399
4555
|
this.refresh();
|
|
4400
4556
|
}
|
|
4557
|
+
if (wasActive && this.active === null) this.observeSettled();
|
|
4401
4558
|
this.emit();
|
|
4402
4559
|
this.schedulePoll();
|
|
4403
4560
|
}
|
|
4404
4561
|
/**
|
|
4405
|
-
*
|
|
4562
|
+
* The job we were following reached a terminal phase (or vanished from the
|
|
4563
|
+
* active slot). Clear the transient flags and re-read the report — it still
|
|
4564
|
+
* names the versions that job installed.
|
|
4565
|
+
*/
|
|
4566
|
+
observeSettled() {
|
|
4567
|
+
this.installingFlag = false;
|
|
4568
|
+
this.stalledFlag = false;
|
|
4569
|
+
this.phaseAgeMsValue = 0;
|
|
4570
|
+
this.watchedJobId = null;
|
|
4571
|
+
this.refresh();
|
|
4572
|
+
}
|
|
4573
|
+
/** Keep one job fresh in the local history without duplicating it. */
|
|
4574
|
+
upsertRecent(job) {
|
|
4575
|
+
const rest = this.recent.filter((entry) => entry.id !== job.id);
|
|
4576
|
+
this.recent = [job, ...rest].slice(0, 20);
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* Fetch the durable job state. Called on mount, on panel open (BEFORE
|
|
4406
4580
|
* discovery) and by the poll loop. A failure (older hq-edge with no
|
|
4407
4581
|
* `/status`, or a transient HTTP error) is swallowed: the last known job is
|
|
4408
4582
|
* kept and the poller backs off and retries.
|
|
4409
4583
|
*/
|
|
4410
4584
|
async fetchStatus() {
|
|
4411
4585
|
try {
|
|
4412
|
-
|
|
4586
|
+
await this.pollOnce();
|
|
4413
4587
|
this.pollBackoffMs = 1e3;
|
|
4414
|
-
this.applyStatus(status);
|
|
4415
4588
|
} catch {
|
|
4416
4589
|
this.schedulePoll();
|
|
4417
4590
|
}
|
|
4418
4591
|
}
|
|
4592
|
+
/**
|
|
4593
|
+
* One read. While a job is being followed, ask `/activity` about THAT job
|
|
4594
|
+
* (survives the restart an update of this plugin causes); otherwise fall back
|
|
4595
|
+
* to the plain `/status` list. An older hq-edge without `/activity` 404s and
|
|
4596
|
+
* the status read still answers.
|
|
4597
|
+
*/
|
|
4598
|
+
async pollOnce() {
|
|
4599
|
+
const watched = this.watchedJobId ?? this.active?.id ?? null;
|
|
4600
|
+
if (watched !== null) try {
|
|
4601
|
+
this.applyActivity(await fetchActivity(watched));
|
|
4602
|
+
return;
|
|
4603
|
+
} catch {}
|
|
4604
|
+
this.applyStatus(await fetchJobStatus());
|
|
4605
|
+
}
|
|
4419
4606
|
/** ~1 s poll while a job is active; stop on terminal; back off on error
|
|
4420
4607
|
* without discarding the last known job; never overlap requests. */
|
|
4421
4608
|
schedulePoll() {
|
|
@@ -4439,9 +4626,8 @@ window.__ModuleLoader__.load({
|
|
|
4439
4626
|
}
|
|
4440
4627
|
this.pollInFlight = true;
|
|
4441
4628
|
try {
|
|
4442
|
-
|
|
4629
|
+
await this.pollOnce();
|
|
4443
4630
|
this.pollBackoffMs = 1e3;
|
|
4444
|
-
this.applyStatus(status);
|
|
4445
4631
|
} catch {
|
|
4446
4632
|
this.pollBackoffMs = Math.min(5e3, Math.max(1e3, this.pollBackoffMs * 2));
|
|
4447
4633
|
this.schedulePoll();
|
|
@@ -4587,6 +4773,7 @@ window.__ModuleLoader__.load({
|
|
|
4587
4773
|
exports.UPDATE_ENTRY_ATTRIBUTE = UPDATE_ENTRY_ATTRIBUTE;
|
|
4588
4774
|
exports.UPDATE_ENTRY_SELECTOR = UPDATE_ENTRY_SELECTOR;
|
|
4589
4775
|
exports.UPDATE_ICON = UPDATE_ICON;
|
|
4776
|
+
exports.UpdateStore = UpdateStore;
|
|
4590
4777
|
exports.apply = apply;
|
|
4591
4778
|
exports.ensureDesignTokens = ensureDesignTokens;
|
|
4592
4779
|
exports.inject = inject;
|