@huaqiu/dsh-tool-plugin-update 0.6.6 → 0.6.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.
- package/lib/client.js +839 -120
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +36 -4
- package/package.json +3 -3
- package/src/client/api.ts +281 -1
- package/src/client/copy.ts +137 -6
- package/src/client/index.ts +241 -11
- package/src/client/update-view.ts +474 -41
- package/src/routes.ts +49 -7
package/src/client/index.ts
CHANGED
|
@@ -21,7 +21,15 @@ import {
|
|
|
21
21
|
observeHostLocale,
|
|
22
22
|
type HostLocale,
|
|
23
23
|
} from '@huaqiu/dsh-sidebar-entry'
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
checkUpdates,
|
|
26
|
+
fetchJobStatus,
|
|
27
|
+
fetchReport,
|
|
28
|
+
TERMINAL_PHASES,
|
|
29
|
+
type PluginUpdateReport,
|
|
30
|
+
type UpdateJob,
|
|
31
|
+
type UpdateJobStatus,
|
|
32
|
+
} from './api.js'
|
|
25
33
|
import { COPY } from './copy.js'
|
|
26
34
|
import { PLUGIN_ICON } from './icons.js'
|
|
27
35
|
import { openUpdateView, type UpdateStoreLike } from './update-view.js'
|
|
@@ -767,6 +775,97 @@ const VIEW_CSS_TEXT = `
|
|
|
767
775
|
padding: 0 24px 20px;
|
|
768
776
|
}
|
|
769
777
|
|
|
778
|
+
/* ── Durable job (active / result) ────────────────────────────────────── */
|
|
779
|
+
.hqpu-job {
|
|
780
|
+
display: flex;
|
|
781
|
+
flex-direction: column;
|
|
782
|
+
gap: 6px;
|
|
783
|
+
padding: 12px;
|
|
784
|
+
border: 0.5px solid var(--dsw-alias-border-l2);
|
|
785
|
+
border-radius: var(--dsw-radius-lg);
|
|
786
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
787
|
+
}
|
|
788
|
+
.hqpu-job-meta {
|
|
789
|
+
font-variant-numeric: tabular-nums;
|
|
790
|
+
}
|
|
791
|
+
.hqpu-progress {
|
|
792
|
+
height: 6px;
|
|
793
|
+
border-radius: 999px;
|
|
794
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
795
|
+
overflow: hidden;
|
|
796
|
+
}
|
|
797
|
+
.hqpu-progress-fill {
|
|
798
|
+
height: 100%;
|
|
799
|
+
border-radius: 999px;
|
|
800
|
+
background: var(--dsw-alias-state-business-primary);
|
|
801
|
+
transition: width var(--ds-transition-duration, 0.2s) ease;
|
|
802
|
+
}
|
|
803
|
+
.hqpu-job-steps {
|
|
804
|
+
display: flex;
|
|
805
|
+
flex-direction: column;
|
|
806
|
+
gap: 4px;
|
|
807
|
+
}
|
|
808
|
+
.hqpu-steps {
|
|
809
|
+
margin: 0;
|
|
810
|
+
padding: 0;
|
|
811
|
+
list-style: none;
|
|
812
|
+
display: flex;
|
|
813
|
+
flex-direction: column;
|
|
814
|
+
gap: 4px;
|
|
815
|
+
}
|
|
816
|
+
.hqpu-step {
|
|
817
|
+
display: flex;
|
|
818
|
+
align-items: center;
|
|
819
|
+
gap: 6px;
|
|
820
|
+
font-size: 12.5px;
|
|
821
|
+
line-height: 18px;
|
|
822
|
+
color: var(--dsw-alias-label-secondary);
|
|
823
|
+
}
|
|
824
|
+
.hqpu-step--skipped,
|
|
825
|
+
.hqpu-step--pending { opacity: 0.6; }
|
|
826
|
+
|
|
827
|
+
/* ── Per-plugin change list + release notes ────────────────────────────── */
|
|
828
|
+
.hqpu-changes {
|
|
829
|
+
padding: 0 8px 8px 70px;
|
|
830
|
+
}
|
|
831
|
+
.hqpu-changes-list,
|
|
832
|
+
.hqpu-release-notes {
|
|
833
|
+
margin: 4px 0 0;
|
|
834
|
+
padding-left: 18px;
|
|
835
|
+
font-size: 12.5px;
|
|
836
|
+
line-height: 19px;
|
|
837
|
+
color: var(--dsw-alias-label-secondary);
|
|
838
|
+
}
|
|
839
|
+
.hqpu-release-link {
|
|
840
|
+
align-self: flex-start;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/* ── Advanced disclosure (source editors) ───────────────────────────────── */
|
|
844
|
+
.hqpu-advanced > summary {
|
|
845
|
+
cursor: pointer;
|
|
846
|
+
font-size: 12.5px;
|
|
847
|
+
line-height: 20px;
|
|
848
|
+
color: var(--dsw-alias-label-tertiary);
|
|
849
|
+
list-style: none;
|
|
850
|
+
padding: 2px 0;
|
|
851
|
+
}
|
|
852
|
+
.hqpu-advanced > summary::-webkit-details-marker { display: none; }
|
|
853
|
+
.hqpu-advanced > summary::before {
|
|
854
|
+
content: '▸ ';
|
|
855
|
+
}
|
|
856
|
+
.hqpu-advanced[open] > summary::before {
|
|
857
|
+
content: '▾ ';
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/* Danger-toned banner (check failure, breaking changes) */
|
|
861
|
+
.hqpu-banner[data-tone='danger'] {
|
|
862
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent);
|
|
863
|
+
color: var(--dsw-alias-label-secondary);
|
|
864
|
+
}
|
|
865
|
+
.hqpu-banner[data-tone='danger'] > svg {
|
|
866
|
+
color: var(--dsw-alias-state-error-primary);
|
|
867
|
+
}
|
|
868
|
+
|
|
770
869
|
.hqpu-visually-hidden {
|
|
771
870
|
position: absolute;
|
|
772
871
|
width: 1px;
|
|
@@ -929,8 +1028,15 @@ export interface ClientContext {
|
|
|
929
1028
|
class UpdateStore implements UpdateStoreLike {
|
|
930
1029
|
private current: PluginUpdateReport | null = null
|
|
931
1030
|
private installingFlag = false
|
|
1031
|
+
/** The durable job the server currently reports as active (resumed across
|
|
1032
|
+
* panel close and DSH restart); null when idle. */
|
|
1033
|
+
private active: UpdateJob | null = null
|
|
1034
|
+
private recent: UpdateJob[] = []
|
|
932
1035
|
private readonly listeners = new Set<() => void>()
|
|
933
1036
|
private refreshTimer: number | null = null
|
|
1037
|
+
private pollTimer: number | null = null
|
|
1038
|
+
private pollBackoffMs = 1_000
|
|
1039
|
+
private pollInFlight = false
|
|
934
1040
|
|
|
935
1041
|
report(): PluginUpdateReport | null {
|
|
936
1042
|
return this.current
|
|
@@ -940,6 +1046,14 @@ class UpdateStore implements UpdateStoreLike {
|
|
|
940
1046
|
return this.installingFlag
|
|
941
1047
|
}
|
|
942
1048
|
|
|
1049
|
+
activeJob(): UpdateJob | null {
|
|
1050
|
+
return this.active
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
recentJobs(): UpdateJob[] {
|
|
1054
|
+
return [...this.recent]
|
|
1055
|
+
}
|
|
1056
|
+
|
|
943
1057
|
subscribe(listener: () => void): () => void {
|
|
944
1058
|
this.listeners.add(listener)
|
|
945
1059
|
return () => {
|
|
@@ -947,15 +1061,28 @@ class UpdateStore implements UpdateStoreLike {
|
|
|
947
1061
|
}
|
|
948
1062
|
}
|
|
949
1063
|
|
|
950
|
-
/**
|
|
951
|
-
*
|
|
952
|
-
*
|
|
1064
|
+
/**
|
|
1065
|
+
* Badge: counts actionable plugin updates AND an available runtime update AND
|
|
1066
|
+
* (while a job runs) an attention pill. A recent failed/rolled-back job with
|
|
1067
|
+
* nothing actionable left also keeps a dot, so the outcome is never silently
|
|
1068
|
+
* forgotten after the panel closes.
|
|
1069
|
+
*/
|
|
953
1070
|
badgeCount(): number {
|
|
1071
|
+
const job = this.active
|
|
1072
|
+
if (job !== null && !TERMINAL_PHASES.has(job.phase)) return 1
|
|
954
1073
|
const report = this.current
|
|
955
1074
|
if (report === null) return 0
|
|
956
|
-
|
|
1075
|
+
let count = report.items.filter(
|
|
957
1076
|
(item) => item.state === 'update' || item.state === 'install',
|
|
958
1077
|
).length
|
|
1078
|
+
if (report.runtime?.updateAvailable) count += 1
|
|
1079
|
+
if (count === 0) {
|
|
1080
|
+
const latest = this.recent[0]
|
|
1081
|
+
if (latest !== undefined && (latest.phase === 'failed' || latest.phase === 'rolled-back')) {
|
|
1082
|
+
count = 1
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return count
|
|
959
1086
|
}
|
|
960
1087
|
|
|
961
1088
|
private emit(): void {
|
|
@@ -968,6 +1095,71 @@ class UpdateStore implements UpdateStoreLike {
|
|
|
968
1095
|
}
|
|
969
1096
|
}
|
|
970
1097
|
|
|
1098
|
+
/** Adopt a fresh `/status` snapshot and (re)arm the poll loop. */
|
|
1099
|
+
private applyStatus(status: UpdateJobStatus): void {
|
|
1100
|
+
const wasActive = this.active !== null
|
|
1101
|
+
this.active = status.activeJob
|
|
1102
|
+
this.recent = status.recentJobs
|
|
1103
|
+
if (wasActive && this.active === null) {
|
|
1104
|
+
// The active job settled (moved to recent/terminal) — the installing
|
|
1105
|
+
// footer state must clear on its own, without the panel's closure.
|
|
1106
|
+
this.installingFlag = false
|
|
1107
|
+
}
|
|
1108
|
+
this.emit()
|
|
1109
|
+
this.schedulePoll()
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Fetch the durable job status. Called on mount, on panel open (BEFORE
|
|
1114
|
+
* discovery) and by the poll loop. A failure (older hq-edge with no
|
|
1115
|
+
* `/status`, or a transient HTTP error) is swallowed: the last known job is
|
|
1116
|
+
* kept and the poller backs off and retries.
|
|
1117
|
+
*/
|
|
1118
|
+
async fetchStatus(): Promise<void> {
|
|
1119
|
+
try {
|
|
1120
|
+
const status = await fetchJobStatus()
|
|
1121
|
+
this.pollBackoffMs = 1_000
|
|
1122
|
+
this.applyStatus(status)
|
|
1123
|
+
} catch {
|
|
1124
|
+
this.schedulePoll()
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/** ~1 s poll while a job is active; stop on terminal; back off on error
|
|
1129
|
+
* without discarding the last known job; never overlap requests. */
|
|
1130
|
+
private schedulePoll(): void {
|
|
1131
|
+
if (this.pollTimer !== null) {
|
|
1132
|
+
window.clearTimeout(this.pollTimer)
|
|
1133
|
+
this.pollTimer = null
|
|
1134
|
+
}
|
|
1135
|
+
const job = this.active
|
|
1136
|
+
if (job === null || TERMINAL_PHASES.has(job.phase)) {
|
|
1137
|
+
this.pollBackoffMs = 1_000
|
|
1138
|
+
return
|
|
1139
|
+
}
|
|
1140
|
+
this.pollTimer = window.setTimeout(() => {
|
|
1141
|
+
void this.pollTick()
|
|
1142
|
+
}, this.pollBackoffMs)
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
private async pollTick(): Promise<void> {
|
|
1146
|
+
if (this.pollInFlight) {
|
|
1147
|
+
this.schedulePoll()
|
|
1148
|
+
return
|
|
1149
|
+
}
|
|
1150
|
+
this.pollInFlight = true
|
|
1151
|
+
try {
|
|
1152
|
+
const status = await fetchJobStatus()
|
|
1153
|
+
this.pollBackoffMs = 1_000
|
|
1154
|
+
this.applyStatus(status)
|
|
1155
|
+
} catch {
|
|
1156
|
+
this.pollBackoffMs = Math.min(5_000, Math.max(1_000, this.pollBackoffMs * 2))
|
|
1157
|
+
this.schedulePoll()
|
|
1158
|
+
} finally {
|
|
1159
|
+
this.pollInFlight = false
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
971
1163
|
async refresh(): Promise<PluginUpdateReport | null> {
|
|
972
1164
|
try {
|
|
973
1165
|
const report = await fetchReport()
|
|
@@ -1009,6 +1201,10 @@ class UpdateStore implements UpdateStoreLike {
|
|
|
1009
1201
|
window.clearInterval(this.refreshTimer)
|
|
1010
1202
|
this.refreshTimer = null
|
|
1011
1203
|
}
|
|
1204
|
+
if (this.pollTimer !== null) {
|
|
1205
|
+
window.clearTimeout(this.pollTimer)
|
|
1206
|
+
this.pollTimer = null
|
|
1207
|
+
}
|
|
1012
1208
|
this.listeners.clear()
|
|
1013
1209
|
}
|
|
1014
1210
|
}
|
|
@@ -1060,10 +1256,31 @@ export function apply(ctx: ClientContext): () => void {
|
|
|
1060
1256
|
// publishes them after this plugin mounted must win, and one that never
|
|
1061
1257
|
// does must keep the canonical fallbacks.
|
|
1062
1258
|
ensureDesignTokens()
|
|
1063
|
-
//
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1259
|
+
// Resume-before-discovery: read the durable job status FIRST so an in-flight
|
|
1260
|
+
// install/runtime update is resumed (and its view rendered) even while the
|
|
1261
|
+
// update check round trip is still running. The panel itself opens from
|
|
1262
|
+
// whatever the store already holds; check() refreshes the list behind it.
|
|
1263
|
+
void store.fetchStatus()
|
|
1264
|
+
openUpdateView(store, copyFor())
|
|
1265
|
+
void store.check()
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// While a job runs the row label shows its progress ("更新中 42%"), and the
|
|
1269
|
+
// tooltip names the phase. Store changes re-apply the label (the badge
|
|
1270
|
+
// subscription already re-renders the count pill).
|
|
1271
|
+
const statusLabel = (): { label: string; tooltip: string } => {
|
|
1272
|
+
const base = entryCopy(locale)
|
|
1273
|
+
const job = store.activeJob()
|
|
1274
|
+
if (job !== null) {
|
|
1275
|
+
const cc = copyFor()
|
|
1276
|
+
const pct = job.progress.percent
|
|
1277
|
+
const phase = cc.job.phase[job.phase] ?? job.phase
|
|
1278
|
+
return {
|
|
1279
|
+
label: pct !== null ? `${base.label} · ${Math.round(pct)}%` : `${base.label} · ${phase}`,
|
|
1280
|
+
tooltip: `${base.tooltip} · ${phase}`,
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return base
|
|
1067
1284
|
}
|
|
1068
1285
|
|
|
1069
1286
|
const disposeEntry = mountSidebarEntry({
|
|
@@ -1076,8 +1293,8 @@ export function apply(ctx: ClientContext): () => void {
|
|
|
1076
1293
|
// `refresh.subscribe`'s listener, which re-applies these) so the row
|
|
1077
1294
|
// re-localizes when the host UI language changes — matching the sibling
|
|
1078
1295
|
// family rows, which read a live `currentLang` variable the same way.
|
|
1079
|
-
label: () =>
|
|
1080
|
-
tooltip: () =>
|
|
1296
|
+
label: () => statusLabel().label,
|
|
1297
|
+
tooltip: () => statusLabel().tooltip,
|
|
1081
1298
|
refresh: { subscribe: refreshSubscribe },
|
|
1082
1299
|
badge: {
|
|
1083
1300
|
subscribe: (listener) => store.subscribe(listener),
|
|
@@ -1088,8 +1305,21 @@ export function apply(ctx: ClientContext): () => void {
|
|
|
1088
1305
|
familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY,
|
|
1089
1306
|
})
|
|
1090
1307
|
|
|
1308
|
+
// Store changes (active-job percentage, result dot) also re-apply the row
|
|
1309
|
+
// label, not just the badge count.
|
|
1310
|
+
store.subscribe(() => {
|
|
1311
|
+
for (const fn of [...localeSubscribers]) {
|
|
1312
|
+
try {
|
|
1313
|
+
fn()
|
|
1314
|
+
} catch {
|
|
1315
|
+
/* one bad subscriber must not strand the rest */
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
})
|
|
1319
|
+
|
|
1091
1320
|
store.startIdleRefresh()
|
|
1092
1321
|
void store.refresh()
|
|
1322
|
+
void store.fetchStatus()
|
|
1093
1323
|
|
|
1094
1324
|
const dispose = (): void => {
|
|
1095
1325
|
try {
|