@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/lib/index.mjs
CHANGED
|
@@ -65,6 +65,18 @@ function readBody(req) {
|
|
|
65
65
|
* forwarder answers 405 rather than forwarding a GET to a POST-only upstream
|
|
66
66
|
* and surfacing hq-edge's 404 as a plugin bug.
|
|
67
67
|
*/
|
|
68
|
+
/**
|
|
69
|
+
* Match a dynamic `/status/<jobId>` sub-path. Returns the job id for a single
|
|
70
|
+
* non-empty segment (no nested slashes), else null. The id is opaque — this
|
|
71
|
+
* route forwards it verbatim; hq-edge owns validation and the 404 for unknown
|
|
72
|
+
* ids.
|
|
73
|
+
*/
|
|
74
|
+
function matchJobStatusPath(sub) {
|
|
75
|
+
if (!sub.startsWith("/status/")) return null;
|
|
76
|
+
const id = sub.slice(8);
|
|
77
|
+
if (id.length === 0 || id.includes("/") || id.includes("?")) return null;
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
68
80
|
function routeFor(sub, method) {
|
|
69
81
|
if (sub === "/runtime-source") {
|
|
70
82
|
if (method === "GET") return {
|
|
@@ -92,6 +104,15 @@ function routeFor(sub, method) {
|
|
|
92
104
|
method: "GET",
|
|
93
105
|
path: "/api/v1/plugin-updates"
|
|
94
106
|
} : null;
|
|
107
|
+
if (sub === "/status") return method === "GET" ? {
|
|
108
|
+
method: "GET",
|
|
109
|
+
path: "/api/v1/plugin-updates/status"
|
|
110
|
+
} : null;
|
|
111
|
+
const jobId = matchJobStatusPath(sub);
|
|
112
|
+
if (jobId !== null) return method === "GET" ? {
|
|
113
|
+
method: "GET",
|
|
114
|
+
path: `/api/v1/plugin-updates/status/${encodeURIComponent(jobId)}`
|
|
115
|
+
} : null;
|
|
95
116
|
if (sub === "/runtime") return method === "GET" ? {
|
|
96
117
|
method: "GET",
|
|
97
118
|
path: "/api/v1/runtime-update"
|
|
@@ -119,8 +140,9 @@ function routeFor(sub, method) {
|
|
|
119
140
|
} : null;
|
|
120
141
|
return null;
|
|
121
142
|
}
|
|
122
|
-
/** Every sub-path this forwarder serves — used to tell a 404 from a
|
|
123
|
-
|
|
143
|
+
/** Every static sub-path this forwarder serves — used to tell a 404 from a
|
|
144
|
+
* 405. The dynamic `/status/<id>` path is matched separately. */
|
|
145
|
+
const STATIC_SUB_PATHS = /* @__PURE__ */ new Set([
|
|
124
146
|
"",
|
|
125
147
|
"/report",
|
|
126
148
|
"/check",
|
|
@@ -130,8 +152,17 @@ const ROUTE_SUB_PATHS = /* @__PURE__ */ new Set([
|
|
|
130
152
|
"/runtime",
|
|
131
153
|
"/runtime-request",
|
|
132
154
|
"/runtime-source",
|
|
133
|
-
"/plugin-source"
|
|
155
|
+
"/plugin-source",
|
|
156
|
+
"/status"
|
|
134
157
|
]);
|
|
158
|
+
/**
|
|
159
|
+
* Whether a sub-path names a route this forwarder recognizes at all. A
|
|
160
|
+
* recognized sub-path hit with an unsupported method is a 405; an unrecognized
|
|
161
|
+
* one is a 404. `/status/<id>` is recognized for any single-segment id.
|
|
162
|
+
*/
|
|
163
|
+
function isKnownSubPath(sub) {
|
|
164
|
+
return STATIC_SUB_PATHS.has(sub) || matchJobStatusPath(sub) !== null;
|
|
165
|
+
}
|
|
135
166
|
/** Create the prefix-route handler forwarding to hq-edge. */
|
|
136
167
|
function createUpdateForwarder(baseUrlResolver, timeoutMs = DEFAULT_FORWARD_TIMEOUT_MS) {
|
|
137
168
|
return async (req, res) => {
|
|
@@ -142,7 +173,8 @@ function createUpdateForwarder(baseUrlResolver, timeoutMs = DEFAULT_FORWARD_TIME
|
|
|
142
173
|
}
|
|
143
174
|
const target = routeFor(sub, req.method === "HEAD" ? "GET" : req.method ?? "");
|
|
144
175
|
if (target === null) {
|
|
145
|
-
|
|
176
|
+
const known = isKnownSubPath(sub);
|
|
177
|
+
sendJson(res, known ? 405 : 404, { error: known ? `method ${req.method} not allowed on ${sub || "/"}` : "not found" });
|
|
146
178
|
return;
|
|
147
179
|
}
|
|
148
180
|
if (req.method !== "GET" && req.method !== "POST" && req.method !== "HEAD") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaqiu/dsh-tool-plugin-update",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.mjs",
|
|
6
6
|
"types": "./lib/index.d.mts",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.2"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@huaqiu/dsh-
|
|
35
|
-
"@huaqiu/dsh-
|
|
34
|
+
"@huaqiu/dsh-sidebar-entry": "0.6.7",
|
|
35
|
+
"@huaqiu/dsh-plugin-log": "0.6.7"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"lib",
|
package/src/client/api.ts
CHANGED
|
@@ -29,6 +29,15 @@ export interface PluginUpdateItem {
|
|
|
29
29
|
ignored?: boolean
|
|
30
30
|
reasons?: string[]
|
|
31
31
|
notes?: string[]
|
|
32
|
+
/**
|
|
33
|
+
* User-facing release metadata (schemaVersion-2 manifests). ALL optional and
|
|
34
|
+
* absent for v1 manifests — the view falls back to the package short name
|
|
35
|
+
* and version line when they are missing.
|
|
36
|
+
*/
|
|
37
|
+
displayName?: string
|
|
38
|
+
description?: string
|
|
39
|
+
sizeBytes?: number
|
|
40
|
+
changes?: string[]
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
/**
|
|
@@ -49,6 +58,33 @@ export interface RuntimeUpdateEntry {
|
|
|
49
58
|
arch?: string
|
|
50
59
|
/** Exact absolute HTTPS URL of the release archive. */
|
|
51
60
|
url: string
|
|
61
|
+
/**
|
|
62
|
+
* Optional user-facing metadata (schemaVersion-2 runtime manifest). All
|
|
63
|
+
* absent for v1 manifests; the panel degrades gracefully without them.
|
|
64
|
+
*/
|
|
65
|
+
publishedAt?: string
|
|
66
|
+
sizeBytes?: number
|
|
67
|
+
summary?: string
|
|
68
|
+
notesUrl?: string
|
|
69
|
+
notes?: string[]
|
|
70
|
+
breaking?: string[]
|
|
71
|
+
channel?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The EDA host's own report of the last runtime replacement, read from
|
|
76
|
+
* `runtime-update-result.json` by hq-edge. This is the authoritative outcome
|
|
77
|
+
* after HQ Edge exits and the host swaps the runtime — it must be rendered,
|
|
78
|
+
* not discarded. `state` is `succeeded`, `rolled_back` (swap failed but the
|
|
79
|
+
* previous runtime was restored) or `failed` (no confirmed rollback).
|
|
80
|
+
*/
|
|
81
|
+
export interface RuntimeLastResult {
|
|
82
|
+
state: 'succeeded' | 'rolled_back' | 'failed'
|
|
83
|
+
targetVersion?: string | null
|
|
84
|
+
installedVersion?: string | null
|
|
85
|
+
at?: string
|
|
86
|
+
errorCode?: string
|
|
87
|
+
message?: string
|
|
52
88
|
}
|
|
53
89
|
|
|
54
90
|
export interface RuntimeUpdateInfo {
|
|
@@ -59,6 +95,8 @@ export interface RuntimeUpdateInfo {
|
|
|
59
95
|
updateAvailable: boolean
|
|
60
96
|
checkedAt?: string | null
|
|
61
97
|
lastError?: string | null
|
|
98
|
+
/** The host-reported outcome of the last runtime swap; null when none. */
|
|
99
|
+
lastResult?: RuntimeLastResult | null
|
|
62
100
|
/**
|
|
63
101
|
* Where the runtime-update manifest is read from. Optional: an older hq-edge
|
|
64
102
|
* does not send it, and the panel must degrade to a read-only source row.
|
|
@@ -110,6 +148,21 @@ export function runtimeLatestVersion(
|
|
|
110
148
|
return null
|
|
111
149
|
}
|
|
112
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Set-level release metadata from a schemaVersion-2 plugin manifest. ALL
|
|
153
|
+
* fields optional: v1 manifests send none, and the panel must fall back to
|
|
154
|
+
* showing just the version then.
|
|
155
|
+
*/
|
|
156
|
+
export interface ReleaseInfo {
|
|
157
|
+
publishedAt?: string
|
|
158
|
+
channel?: string
|
|
159
|
+
requiresRestart?: boolean
|
|
160
|
+
summary?: string
|
|
161
|
+
notesUrl?: string
|
|
162
|
+
notes?: string[]
|
|
163
|
+
breaking?: string[]
|
|
164
|
+
}
|
|
165
|
+
|
|
113
166
|
export interface PluginUpdateReport {
|
|
114
167
|
/** The evaluated release set; null while no manifest has been read. */
|
|
115
168
|
manifestVersion: string | null
|
|
@@ -118,7 +171,13 @@ export interface PluginUpdateReport {
|
|
|
118
171
|
actionableCount: number
|
|
119
172
|
ignoredSet: boolean
|
|
120
173
|
fetchedAt?: string
|
|
174
|
+
/** When the last plugin discovery ran (ISO); drives the "上次检查" row. */
|
|
175
|
+
checkedAt?: string | null
|
|
176
|
+
/** Non-empty when the last plugin discovery FAILED — the panel must show
|
|
177
|
+
* an error row with a Retry, never the silent "no updates" empty state. */
|
|
121
178
|
lastError?: string | null
|
|
179
|
+
/** Set-level release metadata; absent for v1 manifests. */
|
|
180
|
+
release?: ReleaseInfo | null
|
|
122
181
|
runtime: RuntimeUpdateInfo
|
|
123
182
|
/**
|
|
124
183
|
* Where the plugin release manifest is read from. Optional: an older
|
|
@@ -127,6 +186,198 @@ export interface PluginUpdateReport {
|
|
|
127
186
|
pluginSource?: PluginUpdateSource
|
|
128
187
|
}
|
|
129
188
|
|
|
189
|
+
// ── Durable update-job lifecycle (GET /status, GET /status/:id) ────────────
|
|
190
|
+
|
|
191
|
+
/** Lifecycle phases of one durable update job. Terminal phases end the poll. */
|
|
192
|
+
export type UpdatePhase =
|
|
193
|
+
| 'queued'
|
|
194
|
+
| 'resolving'
|
|
195
|
+
| 'downloading'
|
|
196
|
+
| 'verifying'
|
|
197
|
+
| 'staging'
|
|
198
|
+
| 'activating'
|
|
199
|
+
| 'awaiting-host'
|
|
200
|
+
| 'restarting'
|
|
201
|
+
| 'verifying-after-restart'
|
|
202
|
+
| 'done'
|
|
203
|
+
| 'failed'
|
|
204
|
+
| 'rolled-back'
|
|
205
|
+
| 'abandoned'
|
|
206
|
+
|
|
207
|
+
/** Phases after which a job never runs again — polling stops and a result view
|
|
208
|
+
* replaces the active-job view. */
|
|
209
|
+
export const TERMINAL_PHASES: ReadonlySet<UpdatePhase> = new Set<UpdatePhase>([
|
|
210
|
+
'done',
|
|
211
|
+
'failed',
|
|
212
|
+
'rolled-back',
|
|
213
|
+
'abandoned',
|
|
214
|
+
])
|
|
215
|
+
|
|
216
|
+
export type UpdateKind = 'plugin-set' | 'runtime'
|
|
217
|
+
|
|
218
|
+
export interface UpdateJobProgress {
|
|
219
|
+
/** 0..100 when the phase has a meaningful percentage; null otherwise. */
|
|
220
|
+
percent: number | null
|
|
221
|
+
bytesDone?: number
|
|
222
|
+
bytesTotal?: number
|
|
223
|
+
bytesPerSecond?: number
|
|
224
|
+
etaSeconds?: number
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface UpdateStep {
|
|
228
|
+
name: string
|
|
229
|
+
state: 'pending' | 'running' | 'installed' | 'skipped' | 'failed'
|
|
230
|
+
percent?: number
|
|
231
|
+
message?: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface UpdateJobResult {
|
|
235
|
+
ok: boolean
|
|
236
|
+
installedVersion: string | null
|
|
237
|
+
errorCode?: string
|
|
238
|
+
message?: string
|
|
239
|
+
rolledBack: boolean
|
|
240
|
+
finishedAt: string
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* One durable update job, as hq-edge reports it (`GET /status` and
|
|
245
|
+
* `/status/:id`). The UI is a projection of this job: opening the panel
|
|
246
|
+
* resumes whatever the server says is active, rather than owning the
|
|
247
|
+
* lifecycle in a DOM closure.
|
|
248
|
+
*/
|
|
249
|
+
export interface UpdateJob {
|
|
250
|
+
id: string
|
|
251
|
+
kind: UpdateKind
|
|
252
|
+
targetVersion: string
|
|
253
|
+
previousVersion: string | null
|
|
254
|
+
startedAt: string
|
|
255
|
+
updatedAt: string
|
|
256
|
+
phase: UpdatePhase
|
|
257
|
+
message?: string
|
|
258
|
+
progress: UpdateJobProgress
|
|
259
|
+
steps?: UpdateStep[]
|
|
260
|
+
result?: UpdateJobResult
|
|
261
|
+
logHint?: string
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** `GET /status`: the current active job plus a bounded list of recent ones. */
|
|
265
|
+
export interface UpdateJobStatus {
|
|
266
|
+
activeJob: UpdateJob | null
|
|
267
|
+
recentJobs: UpdateJob[]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const PHASE_VALUES: ReadonlySet<string> = new Set<string>([
|
|
271
|
+
'queued',
|
|
272
|
+
'resolving',
|
|
273
|
+
'downloading',
|
|
274
|
+
'verifying',
|
|
275
|
+
'staging',
|
|
276
|
+
'activating',
|
|
277
|
+
'awaiting-host',
|
|
278
|
+
'restarting',
|
|
279
|
+
'verifying-after-restart',
|
|
280
|
+
'done',
|
|
281
|
+
'failed',
|
|
282
|
+
'rolled-back',
|
|
283
|
+
'abandoned',
|
|
284
|
+
])
|
|
285
|
+
|
|
286
|
+
function asStringArray(value: unknown): string[] | undefined {
|
|
287
|
+
if (!Array.isArray(value)) return undefined
|
|
288
|
+
const out = value.filter((entry): entry is string => typeof entry === 'string')
|
|
289
|
+
return out.length > 0 ? out : undefined
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function asFiniteNumber(value: unknown): number | undefined {
|
|
293
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Defensively normalize one job payload from the wire into {@link UpdateJob}.
|
|
297
|
+
* Unknown/older servers may omit fields; a malformed `phase` falls back to
|
|
298
|
+
* `queued` rather than crashing the view. */
|
|
299
|
+
export function normalizeJob(raw: unknown): UpdateJob | null {
|
|
300
|
+
if (raw === null || typeof raw !== 'object') return null
|
|
301
|
+
const r = raw as Record<string, unknown>
|
|
302
|
+
if (typeof r.id !== 'string' || r.id.length === 0) return null
|
|
303
|
+
const phase = typeof r.phase === 'string' && PHASE_VALUES.has(r.phase)
|
|
304
|
+
? (r.phase as UpdatePhase)
|
|
305
|
+
: 'queued'
|
|
306
|
+
const progressRaw = (r.progress ?? {}) as Record<string, unknown>
|
|
307
|
+
const percentRaw = asFiniteNumber(progressRaw.percent)
|
|
308
|
+
const progress: UpdateJobProgress = {
|
|
309
|
+
percent: percentRaw === undefined ? null : Math.min(100, Math.max(0, percentRaw)),
|
|
310
|
+
}
|
|
311
|
+
const bytesDone = asFiniteNumber(progressRaw.bytesDone)
|
|
312
|
+
const bytesTotal = asFiniteNumber(progressRaw.bytesTotal)
|
|
313
|
+
const bytesPerSecond = asFiniteNumber(progressRaw.bytesPerSecond)
|
|
314
|
+
const etaSeconds = asFiniteNumber(progressRaw.etaSeconds)
|
|
315
|
+
if (bytesDone !== undefined) progress.bytesDone = bytesDone
|
|
316
|
+
if (bytesTotal !== undefined) progress.bytesTotal = bytesTotal
|
|
317
|
+
if (bytesPerSecond !== undefined) progress.bytesPerSecond = bytesPerSecond
|
|
318
|
+
if (etaSeconds !== undefined) progress.etaSeconds = etaSeconds
|
|
319
|
+
|
|
320
|
+
const job: UpdateJob = {
|
|
321
|
+
id: r.id,
|
|
322
|
+
kind: r.kind === 'runtime' ? 'runtime' : 'plugin-set',
|
|
323
|
+
targetVersion: typeof r.targetVersion === 'string' ? r.targetVersion : '',
|
|
324
|
+
previousVersion:
|
|
325
|
+
typeof r.previousVersion === 'string' ? r.previousVersion : null,
|
|
326
|
+
startedAt: typeof r.startedAt === 'string' ? r.startedAt : '',
|
|
327
|
+
updatedAt: typeof r.updatedAt === 'string' ? r.updatedAt : '',
|
|
328
|
+
phase,
|
|
329
|
+
progress,
|
|
330
|
+
}
|
|
331
|
+
if (typeof r.message === 'string' && r.message.length > 0) job.message = r.message
|
|
332
|
+
if (typeof r.logHint === 'string' && r.logHint.length > 0) job.logHint = r.logHint
|
|
333
|
+
|
|
334
|
+
if (Array.isArray(r.steps)) {
|
|
335
|
+
const steps: UpdateStep[] = []
|
|
336
|
+
for (const entry of r.steps) {
|
|
337
|
+
if (entry === null || typeof entry !== 'object') continue
|
|
338
|
+
const s = entry as Record<string, unknown>
|
|
339
|
+
if (typeof s.name !== 'string') continue
|
|
340
|
+
const state =
|
|
341
|
+
s.state === 'running' || s.state === 'installed' || s.state === 'skipped' || s.state === 'failed'
|
|
342
|
+
? s.state
|
|
343
|
+
: 'pending'
|
|
344
|
+
const step: UpdateStep = { name: s.name, state }
|
|
345
|
+
const pct = asFiniteNumber(s.percent)
|
|
346
|
+
if (pct !== undefined) step.percent = pct
|
|
347
|
+
if (typeof s.message === 'string' && s.message.length > 0) step.message = s.message
|
|
348
|
+
steps.push(step)
|
|
349
|
+
}
|
|
350
|
+
if (steps.length > 0) job.steps = steps
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (r.result !== null && typeof r.result === 'object') {
|
|
354
|
+
const res = r.result as Record<string, unknown>
|
|
355
|
+
const result: UpdateJobResult = {
|
|
356
|
+
ok: res.ok === true,
|
|
357
|
+
installedVersion:
|
|
358
|
+
typeof res.installedVersion === 'string' ? res.installedVersion : null,
|
|
359
|
+
rolledBack: res.rolledBack === true,
|
|
360
|
+
finishedAt: typeof res.finishedAt === 'string' ? res.finishedAt : '',
|
|
361
|
+
}
|
|
362
|
+
if (typeof res.errorCode === 'string' && res.errorCode.length > 0) result.errorCode = res.errorCode
|
|
363
|
+
if (typeof res.message === 'string' && res.message.length > 0) result.message = res.message
|
|
364
|
+
job.result = result
|
|
365
|
+
}
|
|
366
|
+
return job
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Normalize a `GET /status` payload; an empty/absent body degrades to "no
|
|
370
|
+
* active job" rather than throwing. */
|
|
371
|
+
export function normalizeJobStatus(raw: unknown): UpdateJobStatus {
|
|
372
|
+
if (raw === null || typeof raw !== 'object') return { activeJob: null, recentJobs: [] }
|
|
373
|
+
const r = raw as Record<string, unknown>
|
|
374
|
+
const activeJob = normalizeJob(r.activeJob)
|
|
375
|
+
const recentJobs = Array.isArray(r.recentJobs)
|
|
376
|
+
? r.recentJobs.map(normalizeJob).filter((j): j is UpdateJob => j !== null)
|
|
377
|
+
: []
|
|
378
|
+
return { activeJob, recentJobs }
|
|
379
|
+
}
|
|
380
|
+
|
|
130
381
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
131
382
|
const res = await fetch(`${UPDATE_BASE}${path}`, {
|
|
132
383
|
cache: 'no-store',
|
|
@@ -153,7 +404,14 @@ export function checkUpdates(): Promise<PluginUpdateReport> {
|
|
|
153
404
|
}
|
|
154
405
|
|
|
155
406
|
export interface InstallResult {
|
|
156
|
-
|
|
407
|
+
/**
|
|
408
|
+
* New durable contract: `202 { jobId, state: 'accepted' }`. The install is
|
|
409
|
+
* now an async job — poll `/status/:jobId` for progress, don't read success
|
|
410
|
+
* off this response. Legacy boolean fields are kept for older hq-edge.
|
|
411
|
+
*/
|
|
412
|
+
jobId?: string
|
|
413
|
+
state?: string
|
|
414
|
+
ok?: boolean
|
|
157
415
|
installedSetVersion?: string
|
|
158
416
|
installed?: number
|
|
159
417
|
skipped?: number
|
|
@@ -183,6 +441,8 @@ export function ignoreOffer(setVersion: string): Promise<unknown> {
|
|
|
183
441
|
* updating → `busy`), 422 (preflight/manifest/download refused) or 503.
|
|
184
442
|
*/
|
|
185
443
|
export interface RuntimeUpdateRequestResult {
|
|
444
|
+
/** Durable job id — poll `/status/:jobId` for the accepted job's progress. */
|
|
445
|
+
jobId?: string
|
|
186
446
|
requestId?: string
|
|
187
447
|
targetVersion?: string
|
|
188
448
|
state?: string
|
|
@@ -258,3 +518,23 @@ export function setPluginUpdateSource(url: string): Promise<PluginUpdateSource>
|
|
|
258
518
|
body: JSON.stringify({ url }),
|
|
259
519
|
})
|
|
260
520
|
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* GET the durable job status: the currently active job (if any) plus a bounded
|
|
524
|
+
* list of recent jobs. This is what the panel reads BEFORE running discovery
|
|
525
|
+
* on open, so an in-flight install/runtime update is resumed instead of
|
|
526
|
+
* forgotten. An older hq-edge without the status route 404s — callers catch
|
|
527
|
+
* that and degrade to "no active job".
|
|
528
|
+
*/
|
|
529
|
+
export function fetchJobStatus(): Promise<UpdateJobStatus> {
|
|
530
|
+
return request<unknown>('/status').then(normalizeJobStatus)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** GET one job by id; rejects (upstream 404 `unknown job`) for an unknown id. */
|
|
534
|
+
export function fetchJob(id: string): Promise<UpdateJob> {
|
|
535
|
+
return request<unknown>(`/status/${encodeURIComponent(id)}`).then((raw) => {
|
|
536
|
+
const job = normalizeJob(raw)
|
|
537
|
+
if (job === null) throw new Error('malformed job payload')
|
|
538
|
+
return job
|
|
539
|
+
})
|
|
540
|
+
}
|
package/src/client/copy.ts
CHANGED
|
@@ -20,9 +20,16 @@ const zh = {
|
|
|
20
20
|
loading: '正在读取插件更新…',
|
|
21
21
|
empty: '没有可用的插件更新',
|
|
22
22
|
checkFailed: '检查更新失败,请重试',
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// The button installs the ENTIRE offered set at once (not one card) — label
|
|
24
|
+
// it honestly rather than per-plugin.
|
|
25
|
+
installAll: (count: number) => `安装整套更新(${count} 项)`,
|
|
26
|
+
installOne: '安装整套',
|
|
27
|
+
installOneTip: '将安装整套 N 项更新并自动重启 DSH,而不仅是此插件',
|
|
25
28
|
installing: '正在安装…(DSH 将自动重启)',
|
|
29
|
+
retry: '重试',
|
|
30
|
+
checkedAt: (when: string) => `上次检查:${when}`,
|
|
31
|
+
checkFailedBanner: (message: string) => `检查更新失败:${message}`,
|
|
32
|
+
staleNote: '以下为上次成功检查的结果,可能已过期',
|
|
26
33
|
nothingToInstall: '暂无可安装的更新',
|
|
27
34
|
ignore: '忽略此版本',
|
|
28
35
|
unignore: '恢复提醒',
|
|
@@ -56,11 +63,23 @@ const zh = {
|
|
|
56
63
|
unknown: '版本未知',
|
|
57
64
|
notConfigured: '未配置更新源',
|
|
58
65
|
note: '运行时更新由 EDA 安装器处理',
|
|
66
|
+
sizeLabel: '包大小',
|
|
59
67
|
update: '安装运行时更新',
|
|
60
68
|
updateConfirm: (version: string) =>
|
|
61
69
|
`安装 HQ Edge 运行时 ${version}?HQ Edge 会先关闭并完成替换,然后重新启动。`,
|
|
62
|
-
|
|
70
|
+
// No fixed "takes one minute" claim: duration depends on download speed
|
|
71
|
+
// and package size (the bundle is hundreds of MB).
|
|
72
|
+
updating: '正在下载并校验运行时包…',
|
|
73
|
+
durationNote: '耗时取决于下载速度与包体大小;运行时包较大,替换后 HQ Edge 会自动重启。',
|
|
63
74
|
updateAccepted: '运行时更新已受理,HQ Edge 正在重启…',
|
|
75
|
+
size: (bytes: number) => `约 ${formatMb(bytes)}`,
|
|
76
|
+
lastResult: {
|
|
77
|
+
title: '上次运行时更新结果',
|
|
78
|
+
succeeded: '运行时更新成功',
|
|
79
|
+
failed: '运行时更新失败',
|
|
80
|
+
rolledBack: '运行时更新已回滚(已恢复旧版本)',
|
|
81
|
+
elevation: '系统级安装可能弹出 UAC 提权窗口;拒绝提权会导致替换失败并自动回滚到旧版本。',
|
|
82
|
+
},
|
|
64
83
|
// ── Update-source editor ──
|
|
65
84
|
sourceHint: '运行时更新清单的下载地址,保存后立即生效',
|
|
66
85
|
sourceEdit: '修改',
|
|
@@ -92,6 +111,59 @@ const zh = {
|
|
|
92
111
|
installSuccess: '更新已安装,正在重启 DSH…',
|
|
93
112
|
confirmInstall: '确认安装',
|
|
94
113
|
cancel: '取消',
|
|
114
|
+
// ── Durable update job (active / result / recent) ──
|
|
115
|
+
job: {
|
|
116
|
+
kindPluginSet: '插件集更新',
|
|
117
|
+
kindRuntime: 'HQ Edge 运行时更新',
|
|
118
|
+
phase: {
|
|
119
|
+
queued: '排队中',
|
|
120
|
+
resolving: '解析版本',
|
|
121
|
+
downloading: '下载中',
|
|
122
|
+
verifying: '校验完整性',
|
|
123
|
+
staging: '暂存文件',
|
|
124
|
+
activating: '启用插件集',
|
|
125
|
+
'awaiting-host': '等待主机替换运行时',
|
|
126
|
+
restarting: '正在重启',
|
|
127
|
+
'verifying-after-restart': '重启后校验',
|
|
128
|
+
done: '已完成',
|
|
129
|
+
failed: '失败',
|
|
130
|
+
'rolled-back': '已回滚',
|
|
131
|
+
abandoned: '已中断',
|
|
132
|
+
} as Record<string, string>,
|
|
133
|
+
willRestartDsh: '完成后 DSH 将自动重启,更新本身会在后台继续。',
|
|
134
|
+
willSwapRuntime: 'HQ Edge 会下载完整运行时包并退出,由 EDA 主机替换后重启;系统级安装可能弹出 UAC 提权窗口。',
|
|
135
|
+
closeNote: '关闭此面板不会取消更新。',
|
|
136
|
+
bytes: (done: string, total: string) => `${done} / ${total}`,
|
|
137
|
+
speed: (v: string) => `${v}/s`,
|
|
138
|
+
eta: (s: string) => `剩余约 ${s}`,
|
|
139
|
+
nextDone: '正在完成,请稍候…',
|
|
140
|
+
// Terminal result view
|
|
141
|
+
resultDone: '更新已完成',
|
|
142
|
+
resultFailed: '更新失败',
|
|
143
|
+
resultRolledBack: '更新已回滚',
|
|
144
|
+
resultAbandoned: '更新未完成',
|
|
145
|
+
retry: '重新尝试(发起新的更新)',
|
|
146
|
+
recentTitle: '最近更新',
|
|
147
|
+
stepsTitle: '各组件进度',
|
|
148
|
+
logHint: (hint: string) => `日志标识:${hint}`,
|
|
149
|
+
},
|
|
150
|
+
// ── Release metadata ──
|
|
151
|
+
release: {
|
|
152
|
+
summaryTitle: '版本说明',
|
|
153
|
+
notesLink: '查看发布说明',
|
|
154
|
+
breakingTitle: '破坏性变更',
|
|
155
|
+
changesTitle: '更新内容',
|
|
156
|
+
advanced: '高级:更新源',
|
|
157
|
+
publishedAt: (when: string) => `发布于 ${when}`,
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Compact a byte count for display: "约 12.3 MB" / "约 480 MB". */
|
|
162
|
+
export function formatMb(bytes: number): string {
|
|
163
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return ''
|
|
164
|
+
const mb = bytes / (1024 * 1024)
|
|
165
|
+
if (mb >= 100) return `${Math.round(mb)} MB`
|
|
166
|
+
return `${mb.toFixed(1)} MB`
|
|
95
167
|
}
|
|
96
168
|
|
|
97
169
|
export type UpdateCopy = typeof zh
|
|
@@ -107,9 +179,14 @@ const en: UpdateCopy = {
|
|
|
107
179
|
loading: 'Reading plugin updates…',
|
|
108
180
|
empty: 'No plugin updates available',
|
|
109
181
|
checkFailed: 'Failed to check for updates — please retry',
|
|
110
|
-
installAll: (count: number) => `Install
|
|
111
|
-
installOne: 'Install',
|
|
182
|
+
installAll: (count: number) => `Install whole set (${count})`,
|
|
183
|
+
installOne: 'Install set',
|
|
184
|
+
installOneTip: 'Installs the whole set of N updates and restarts DSH — not just this plugin',
|
|
112
185
|
installing: 'Installing… (DSH will restart)',
|
|
186
|
+
retry: 'Retry',
|
|
187
|
+
checkedAt: (when: string) => `Last checked: ${when}`,
|
|
188
|
+
checkFailedBanner: (message: string) => `Update check failed: ${message}`,
|
|
189
|
+
staleNote: 'Showing the last successful check — it may be stale',
|
|
113
190
|
nothingToInstall: 'Nothing to install right now',
|
|
114
191
|
ignore: 'Ignore this release',
|
|
115
192
|
unignore: 'Show again',
|
|
@@ -143,11 +220,21 @@ const en: UpdateCopy = {
|
|
|
143
220
|
unknown: 'Version unknown',
|
|
144
221
|
notConfigured: 'No update source configured',
|
|
145
222
|
note: 'Runtime updates are handled by the EDA installer',
|
|
223
|
+
sizeLabel: 'Package size',
|
|
146
224
|
update: 'Install runtime update',
|
|
147
225
|
updateConfirm: (version: string) =>
|
|
148
226
|
`Install HQ Edge runtime ${version}? HQ Edge shuts down to swap the runtime, then restarts.`,
|
|
149
|
-
updating: 'Downloading and verifying the runtime bundle
|
|
227
|
+
updating: 'Downloading and verifying the runtime bundle…',
|
|
228
|
+
durationNote: 'Duration depends on download speed and package size; the runtime bundle is large, and HQ Edge restarts after the swap.',
|
|
150
229
|
updateAccepted: 'Runtime update accepted — HQ Edge is restarting…',
|
|
230
|
+
size: (bytes: number) => `~ ${formatMb(bytes)}`,
|
|
231
|
+
lastResult: {
|
|
232
|
+
title: 'Last runtime update result',
|
|
233
|
+
succeeded: 'Runtime update succeeded',
|
|
234
|
+
failed: 'Runtime update failed',
|
|
235
|
+
rolledBack: 'Runtime update rolled back (previous version restored)',
|
|
236
|
+
elevation: 'A system-wide install may show a UAC elevation prompt; declining it fails the swap and rolls back to the previous version.',
|
|
237
|
+
},
|
|
151
238
|
// ── Update-source editor ──
|
|
152
239
|
sourceHint: 'Where the runtime update manifest is fetched from; saved changes take effect immediately',
|
|
153
240
|
sourceEdit: 'Edit',
|
|
@@ -180,6 +267,50 @@ const en: UpdateCopy = {
|
|
|
180
267
|
installSuccess: 'Update installed — restarting DSH…',
|
|
181
268
|
confirmInstall: 'Confirm install',
|
|
182
269
|
cancel: 'Cancel',
|
|
270
|
+
// ── Durable update job (active / result / recent) ──
|
|
271
|
+
job: {
|
|
272
|
+
kindPluginSet: 'Plugin-set update',
|
|
273
|
+
kindRuntime: 'HQ Edge runtime update',
|
|
274
|
+
phase: {
|
|
275
|
+
queued: 'Queued',
|
|
276
|
+
resolving: 'Resolving version',
|
|
277
|
+
downloading: 'Downloading',
|
|
278
|
+
verifying: 'Verifying integrity',
|
|
279
|
+
staging: 'Staging files',
|
|
280
|
+
activating: 'Activating plugin set',
|
|
281
|
+
'awaiting-host': 'Waiting for host to swap runtime',
|
|
282
|
+
restarting: 'Restarting',
|
|
283
|
+
'verifying-after-restart': 'Verifying after restart',
|
|
284
|
+
done: 'Done',
|
|
285
|
+
failed: 'Failed',
|
|
286
|
+
'rolled-back': 'Rolled back',
|
|
287
|
+
abandoned: 'Abandoned',
|
|
288
|
+
} as Record<string, string>,
|
|
289
|
+
willRestartDsh: 'DSH restarts automatically once this finishes; the update continues in the background.',
|
|
290
|
+
willSwapRuntime: 'HQ Edge downloads the full runtime bundle and exits; the EDA host swaps and relaunches it. A system-wide install may show a UAC prompt.',
|
|
291
|
+
closeNote: 'Closing this panel does not cancel the update.',
|
|
292
|
+
bytes: (done: string, total: string) => `${done} / ${total}`,
|
|
293
|
+
speed: (v: string) => `${v}/s`,
|
|
294
|
+
eta: (s: string) => `~${s} left`,
|
|
295
|
+
nextDone: 'Finishing — please wait…',
|
|
296
|
+
resultDone: 'Update completed',
|
|
297
|
+
resultFailed: 'Update failed',
|
|
298
|
+
resultRolledBack: 'Update rolled back',
|
|
299
|
+
resultAbandoned: 'Update did not complete',
|
|
300
|
+
retry: 'Retry (start a new update)',
|
|
301
|
+
recentTitle: 'Recent updates',
|
|
302
|
+
stepsTitle: 'Per-component progress',
|
|
303
|
+
logHint: (hint: string) => `Log id: ${hint}`,
|
|
304
|
+
},
|
|
305
|
+
// ── Release metadata ──
|
|
306
|
+
release: {
|
|
307
|
+
summaryTitle: 'Release notes',
|
|
308
|
+
notesLink: 'View release notes',
|
|
309
|
+
breakingTitle: 'Breaking changes',
|
|
310
|
+
changesTitle: 'What changed',
|
|
311
|
+
advanced: 'Advanced: update sources',
|
|
312
|
+
publishedAt: (when: string) => `Published ${when}`,
|
|
313
|
+
},
|
|
183
314
|
}
|
|
184
315
|
|
|
185
316
|
export const COPY = { zh, en } as const
|