dsh-tokenlens 1.2.2 → 1.3.1
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/CHANGELOG.md +35 -73
- package/PROTOCOL.md +45 -7
- package/README.en.md +6 -5
- package/README.md +7 -6
- package/client.js +496 -10
- package/index.js +90 -3
- package/lib/provider-store.mjs +5 -0
- package/lib/update-check.mjs +87 -0
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -68,6 +68,14 @@ window.__ModuleLoader__.load({
|
|
|
68
68
|
const MANAGEMENT_SYNC_MS = 15000
|
|
69
69
|
/** Retry delay of a 明细-window save blocked by an in-flight settings request (v1.2.1). */
|
|
70
70
|
const DETAILS_WINDOW_SAVE_RETRY_MS = 200
|
|
71
|
+
/**
|
|
72
|
+
* Delays of the network-free providers re-reads while 「启动时检查更新」 is on
|
|
73
|
+
* and no result exists yet: the Host's own startup / toggle check runs in
|
|
74
|
+
* the background, so hello or a settings answer can win the race. Two
|
|
75
|
+
* attempts cover a normal registry round-trip without ever asking the
|
|
76
|
+
* registry twice; the management page's silent check covers the rest.
|
|
77
|
+
*/
|
|
78
|
+
const UPDATE_RESULT_REFRESH_MS = [1200, 4000]
|
|
71
79
|
/** How many per-provider / per-model stats rows the dialog lists. */
|
|
72
80
|
const MAX_PROVIDER_ROWS = 5
|
|
73
81
|
const MAX_MODEL_ROWS = 3
|
|
@@ -137,7 +145,13 @@ window.__ModuleLoader__.load({
|
|
|
137
145
|
defaultScope: 'provider',
|
|
138
146
|
detailsExpanded: 'auto',
|
|
139
147
|
detailsWindow: 'total',
|
|
148
|
+
// 「启动时检查更新」 is persisted too. ON by default (v1.3.1) so users learn
|
|
149
|
+
// about new releases; switching it off keeps the plugin fully local-only.
|
|
150
|
+
// Every other value (an old Host omitting the field included) means ON.
|
|
151
|
+
updateCheck: true,
|
|
140
152
|
}
|
|
153
|
+
/** The npm package page the 「打开 npm」 update links point at. */
|
|
154
|
+
const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/dsh-tokenlens'
|
|
141
155
|
|
|
142
156
|
const zh = {
|
|
143
157
|
title: '词元透镜(dsh-TokenLens)',
|
|
@@ -340,6 +354,23 @@ window.__ModuleLoader__.load({
|
|
|
340
354
|
settingsDetailsCollapsed: '收起',
|
|
341
355
|
settingsResetDefaults: '恢复默认设置',
|
|
342
356
|
settingsResetDefaultsHint: '恢复为插件默认的显示设置',
|
|
357
|
+
updateCheckLabel: '启动时检查更新',
|
|
358
|
+
updateCheckHint: '默认开启:开启时访问 npm registry 检查新版本;关闭后插件完全本地运行',
|
|
359
|
+
updateCheckOff: '关闭',
|
|
360
|
+
updateCheckOn: '开启',
|
|
361
|
+
updateChecking: '正在检查…',
|
|
362
|
+
updateLatest: '已是最新版本 v{version}',
|
|
363
|
+
updateAvailable: '发现新版本 v{latest}(当前 v{current})',
|
|
364
|
+
updateCheckFailed: '检查失败',
|
|
365
|
+
updateCheckNow: '立即检查',
|
|
366
|
+
updateCheckNever: '尚未检查',
|
|
367
|
+
updateOpenNpm: '打开 npm',
|
|
368
|
+
// The update status now lives on the plugin-version line of the dialog
|
|
369
|
+
// body (footer line removed); these two key the version-line suffix.
|
|
370
|
+
updateVersionNew: '有新版本 v{latest}',
|
|
371
|
+
updateVersionLatest: '已是最新版本',
|
|
372
|
+
// Kept for compatibility (the footer line is no longer rendered).
|
|
373
|
+
updateDialogLine: '发现新版本 v{latest} · 打开 npm',
|
|
343
374
|
providerBothLabels: '短标签:{short} · 全名:{full}',
|
|
344
375
|
providerShortLabel: '短标签',
|
|
345
376
|
providerShortLabelHint: '1–12 字符',
|
|
@@ -568,6 +599,23 @@ window.__ModuleLoader__.load({
|
|
|
568
599
|
settingsDetailsCollapsed: 'Collapsed',
|
|
569
600
|
settingsResetDefaults: 'Restore defaults',
|
|
570
601
|
settingsResetDefaultsHint: 'Restore the plugin default display settings',
|
|
602
|
+
updateCheckLabel: 'Check for updates at startup',
|
|
603
|
+
updateCheckHint: 'On by default: contacts the npm registry to check for a new version; switch it off to keep the plugin fully local',
|
|
604
|
+
updateCheckOff: 'Off',
|
|
605
|
+
updateCheckOn: 'On',
|
|
606
|
+
updateChecking: 'Checking…',
|
|
607
|
+
updateLatest: 'Up to date: v{version}',
|
|
608
|
+
updateAvailable: 'New version v{latest} available (current v{current})',
|
|
609
|
+
updateCheckFailed: 'Update check failed',
|
|
610
|
+
updateCheckNow: 'Check now',
|
|
611
|
+
updateCheckNever: 'Not checked yet',
|
|
612
|
+
updateOpenNpm: 'Open on npm',
|
|
613
|
+
// The update status now lives on the plugin-version line of the dialog
|
|
614
|
+
// body (footer line removed); these two key the version-line suffix.
|
|
615
|
+
updateVersionNew: 'New version v{latest}',
|
|
616
|
+
updateVersionLatest: 'Up to date',
|
|
617
|
+
// Kept for compatibility (the footer line is no longer rendered).
|
|
618
|
+
updateDialogLine: 'New version v{latest} · Open on npm',
|
|
571
619
|
providerBothLabels: 'Short label: {short} · Full name: {full}',
|
|
572
620
|
providerShortLabel: 'Short label',
|
|
573
621
|
providerShortLabelHint: '1–12 characters',
|
|
@@ -1111,9 +1159,9 @@ window.__ModuleLoader__.load({
|
|
|
1111
1159
|
}
|
|
1112
1160
|
|
|
1113
1161
|
/**
|
|
1114
|
-
* The five display settings of the `providers`
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1162
|
+
* The five display settings plus 「启动时检查更新」 of the `providers`
|
|
1163
|
+
* payload, every field normalized to a usable value so old Hosts keep the
|
|
1164
|
+
* previous defaults.
|
|
1117
1165
|
*/
|
|
1118
1166
|
function normalizePluginSettings(raw) {
|
|
1119
1167
|
const source = raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
|
|
@@ -1123,7 +1171,49 @@ window.__ModuleLoader__.load({
|
|
|
1123
1171
|
defaultScope: normalizeDisplayScope(source.defaultScope),
|
|
1124
1172
|
detailsExpanded: normalizeDetailsExpanded(source.detailsExpanded),
|
|
1125
1173
|
detailsWindow: normalizeDetailsWindow(source.detailsWindow),
|
|
1174
|
+
// The startup update check is ON by default (v1.3.1): only an explicit
|
|
1175
|
+
// boolean is honored, so an old Host that omits the field stays on.
|
|
1176
|
+
updateCheck:
|
|
1177
|
+
typeof source.updateCheck === 'boolean' ? source.updateCheck : DEFAULT_PLUGIN_SETTINGS.updateCheck,
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* The optional `update` object of the hello / providers envelope: the last
|
|
1183
|
+
* known update check result, or null when the Host never checked.
|
|
1184
|
+
*
|
|
1185
|
+
* Defensive by design — the update UI must never throw on hostile wire
|
|
1186
|
+
* data. String fields collapse to `''`/null, `available` is only claimed
|
|
1187
|
+
* when it is literally `true` *and* names a version, and a wire error keeps
|
|
1188
|
+
* the shared normalized error shape.
|
|
1189
|
+
*/
|
|
1190
|
+
function normalizeUpdate(raw) {
|
|
1191
|
+
if (raw === null || raw === undefined || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
1192
|
+
const current = stringOrEmpty(raw.current).trim()
|
|
1193
|
+
const latestText = stringOrEmpty(raw.latest).trim()
|
|
1194
|
+
const latest = latestText === '' ? null : latestText
|
|
1195
|
+
return {
|
|
1196
|
+
current,
|
|
1197
|
+
latest,
|
|
1198
|
+
available: raw.available === true && latest !== null,
|
|
1199
|
+
checkedAt: stringOrEmpty(raw.checkedAt).trim(),
|
|
1200
|
+
error: raw.error === null || raw.error === undefined ? null : normalizeError(raw.error),
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/** Whether two normalized update infos are render-identical (avoids store churn). */
|
|
1205
|
+
function sameUpdate(a, b) {
|
|
1206
|
+
if (a === b) return true
|
|
1207
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false
|
|
1208
|
+
if (a.current !== b.current || a.latest !== b.latest || a.available !== b.available || a.checkedAt !== b.checkedAt) {
|
|
1209
|
+
return false
|
|
1126
1210
|
}
|
|
1211
|
+
if (a.error === null || b.error === null) return a.error === b.error
|
|
1212
|
+
return (
|
|
1213
|
+
a.error.code === b.error.code &&
|
|
1214
|
+
a.error.message === b.error.message &&
|
|
1215
|
+
a.error.retryable === b.error.retryable
|
|
1216
|
+
)
|
|
1127
1217
|
}
|
|
1128
1218
|
|
|
1129
1219
|
/**
|
|
@@ -1184,6 +1274,10 @@ window.__ModuleLoader__.load({
|
|
|
1184
1274
|
? raw.dshProviders.map((entry) => normalizeDshProviderEntry(entry)).filter((entry) => entry !== null)
|
|
1185
1275
|
: []
|
|
1186
1276
|
const settings = raw.settings !== null && typeof raw.settings === 'object' ? raw.settings : {}
|
|
1277
|
+
// The update check result travels with every providers payload; `hasUpdate`
|
|
1278
|
+
// distinguishes "the Host said null" from "an old Host omitted the field",
|
|
1279
|
+
// so a hello-provided result is never wiped by a payload without it.
|
|
1280
|
+
const hasUpdate = Object.prototype.hasOwnProperty.call(raw, 'update')
|
|
1187
1281
|
return {
|
|
1188
1282
|
// v0.17.0 identity: dedupe by providerId, last-wins, so the filter tabs
|
|
1189
1283
|
// never list the same provider twice even if the Host sends a duplicate.
|
|
@@ -1196,6 +1290,10 @@ window.__ModuleLoader__.load({
|
|
|
1196
1290
|
storeFile: stringOrEmpty(raw.storeFile).trim(),
|
|
1197
1291
|
storeFileDisplay: stringOrEmpty(raw.storeFileDisplay).trim(),
|
|
1198
1292
|
settings: normalizePluginSettings(settings),
|
|
1293
|
+
update: hasUpdate ? normalizeUpdate(raw.update) : null,
|
|
1294
|
+
hasUpdate,
|
|
1295
|
+
// Optional: the Host may advertise the npm page its links should open.
|
|
1296
|
+
updatePackageUrl: stringOrEmpty(raw.updatePackageUrl).trim(),
|
|
1199
1297
|
labelCatalog,
|
|
1200
1298
|
}
|
|
1201
1299
|
}
|
|
@@ -1773,6 +1871,16 @@ window.__ModuleLoader__.load({
|
|
|
1773
1871
|
labelCatalog: {},
|
|
1774
1872
|
// Plugin version from the hello envelope (dialog footnote).
|
|
1775
1873
|
pluginVersion: '',
|
|
1874
|
+
// 「启动时检查更新」 (persisted setting) and the last check result the
|
|
1875
|
+
// hello / providers envelope reported, plus the in-flight flag of an
|
|
1876
|
+
// explicit 「立即检查」. The flag is deliberately independent from
|
|
1877
|
+
// `management.settingsBusy`: a check never blocks a settings save.
|
|
1878
|
+
updateCheck: DEFAULT_PLUGIN_SETTINGS.updateCheck,
|
|
1879
|
+
update: null,
|
|
1880
|
+
updateChecking: false,
|
|
1881
|
+
// The canonical npm page the update links open; the Host may advertise
|
|
1882
|
+
// its own (newer bundles may move the package), this is the fallback.
|
|
1883
|
+
updatePackageUrl: NPM_PACKAGE_URL,
|
|
1776
1884
|
// Extra DSH-only detail blocks (provider split / top models). Collapsed by
|
|
1777
1885
|
// default on short screens so the dialog fits without a scrollbar.
|
|
1778
1886
|
showDetails:
|
|
@@ -1808,8 +1916,8 @@ window.__ModuleLoader__.load({
|
|
|
1808
1916
|
let usageEpoch = 0
|
|
1809
1917
|
|
|
1810
1918
|
/**
|
|
1811
|
-
* The
|
|
1812
|
-
*
|
|
1919
|
+
* The display settings currently in effect: the five v0.29.0 defaults (the
|
|
1920
|
+
* v1.2.1 明细 donut window included) plus 「启动时检查更新」.
|
|
1813
1921
|
*/
|
|
1814
1922
|
function currentPluginSettings() {
|
|
1815
1923
|
const state = store.get()
|
|
@@ -1819,6 +1927,7 @@ window.__ModuleLoader__.load({
|
|
|
1819
1927
|
defaultScope: normalizeDisplayScope(state.defaultScope),
|
|
1820
1928
|
detailsExpanded: normalizeDetailsExpanded(state.detailsExpanded),
|
|
1821
1929
|
detailsWindow: normalizeDetailsWindow(state.detailsWindow),
|
|
1930
|
+
updateCheck: state.updateCheck === true,
|
|
1822
1931
|
}
|
|
1823
1932
|
}
|
|
1824
1933
|
|
|
@@ -2065,6 +2174,18 @@ window.__ModuleLoader__.load({
|
|
|
2065
2174
|
if (options.adoptDetailsWindow === true && current.detailsWindow !== settings.detailsWindow) {
|
|
2066
2175
|
patch.detailsWindow = settings.detailsWindow
|
|
2067
2176
|
}
|
|
2177
|
+
// 「启动时检查更新」 is adopted on every answer (it is the persisted value
|
|
2178
|
+
// echoing back); the answered checkbox state must win after a toggle.
|
|
2179
|
+
if (current.updateCheck !== settings.updateCheck) patch.updateCheck = settings.updateCheck
|
|
2180
|
+
// A payload that carries `update` (even as null) is authoritative for the
|
|
2181
|
+
// last check result; one that omits it (old Host) keeps what hello said.
|
|
2182
|
+
if (parsed.hasUpdate === true && !sameUpdate(current.update, parsed.update)) {
|
|
2183
|
+
patch.update = parsed.update
|
|
2184
|
+
}
|
|
2185
|
+
// The Host-advertised npm page travels with the same payload.
|
|
2186
|
+
if (parsed.updatePackageUrl !== '' && current.updatePackageUrl !== parsed.updatePackageUrl) {
|
|
2187
|
+
patch.updatePackageUrl = parsed.updatePackageUrl
|
|
2188
|
+
}
|
|
2068
2189
|
if (parsed.labelCatalog !== null && typeof parsed.labelCatalog === 'object') {
|
|
2069
2190
|
const catalog = { ...current.labelCatalog, ...parsed.labelCatalog }
|
|
2070
2191
|
if (Object.keys(catalog).length > 0) patch.labelCatalog = catalog
|
|
@@ -2123,6 +2244,9 @@ window.__ModuleLoader__.load({
|
|
|
2123
2244
|
storeFileDisplay: parsed !== null ? parsed.storeFileDisplay : '',
|
|
2124
2245
|
loadedAt: Date.now(),
|
|
2125
2246
|
})
|
|
2247
|
+
// 「启动时检查更新」: the first management-page load runs one silent
|
|
2248
|
+
// check when the setting is on and no result is known yet.
|
|
2249
|
+
maybeAutoUpdateCheck()
|
|
2126
2250
|
return { kind: 'ok' }
|
|
2127
2251
|
}
|
|
2128
2252
|
patchManagement({ inFlight: false, phase: 'error', error: normalizeError(data.error) })
|
|
@@ -2273,6 +2397,10 @@ window.__ModuleLoader__.load({
|
|
|
2273
2397
|
}
|
|
2274
2398
|
: {}),
|
|
2275
2399
|
})
|
|
2400
|
+
// Turning 「启动时检查更新」 on makes the Host run one check right away;
|
|
2401
|
+
// its answer may still be in flight, so read the local result shortly
|
|
2402
|
+
// after (no extra registry request).
|
|
2403
|
+
if (store.get().updateCheck === true && store.get().update === null) scheduleUpdateResultRefresh()
|
|
2276
2404
|
return { kind: 'ok' }
|
|
2277
2405
|
}
|
|
2278
2406
|
patchManagement({ settingsBusy: false, settingsError: normalizeError(data.error) })
|
|
@@ -2331,6 +2459,128 @@ window.__ModuleLoader__.load({
|
|
|
2331
2459
|
void requestSettingsUpdate({ ...DEFAULT_PLUGIN_SETTINGS })
|
|
2332
2460
|
}
|
|
2333
2461
|
|
|
2462
|
+
/** Guards the one silent check the management page may start after opening. */
|
|
2463
|
+
let autoUpdateCheckStarted = false
|
|
2464
|
+
/** Whether the settings error banner currently shows an update-check failure. */
|
|
2465
|
+
let updateCheckErrorShown = false
|
|
2466
|
+
/** One delayed retry when the Host says "a check is already running". */
|
|
2467
|
+
const UPDATE_CHECK_RETRY_MS = 1200
|
|
2468
|
+
let updateCheckRetryTimer = null
|
|
2469
|
+
|
|
2470
|
+
/**
|
|
2471
|
+
* Run one update check (「立即检查」, and the management page's one silent
|
|
2472
|
+
* startup check when 「启动时检查更新」 is on).
|
|
2473
|
+
*
|
|
2474
|
+
* `updateChecking` is an independent flag, never `settingsBusy`: a check may
|
|
2475
|
+
* not block a settings save (and vice versa). A Host-level failure travels
|
|
2476
|
+
* inside `update.error` (the status row renders 检查失败); a transport or
|
|
2477
|
+
* envelope failure uses the management page's existing error banner. Never
|
|
2478
|
+
* throws.
|
|
2479
|
+
*/
|
|
2480
|
+
async function requestUpdateCheck(options = {}) {
|
|
2481
|
+
const retried = options.retried === true
|
|
2482
|
+
if (store.get().updateChecking === true) return { kind: 'busy' }
|
|
2483
|
+
store.set({ updateChecking: true })
|
|
2484
|
+
let data
|
|
2485
|
+
try {
|
|
2486
|
+
data = await call({ action: 'updateCheck' })
|
|
2487
|
+
} catch (error) {
|
|
2488
|
+
store.set({ updateChecking: false })
|
|
2489
|
+
updateCheckErrorShown = true
|
|
2490
|
+
patchManagement({ settingsError: managementErrorOf(error) })
|
|
2491
|
+
return { kind: 'transport' }
|
|
2492
|
+
}
|
|
2493
|
+
if (data !== null && typeof data === 'object' && typeof data.ok === 'boolean') {
|
|
2494
|
+
if (data.ok === true) {
|
|
2495
|
+
const update = normalizeUpdate(data.update)
|
|
2496
|
+
if (update !== null) {
|
|
2497
|
+
store.set({ updateChecking: false, update })
|
|
2498
|
+
// A successful retry retires the banner of the failed one instead of
|
|
2499
|
+
// leaving a stale check failure above a fresh result.
|
|
2500
|
+
if (updateCheckErrorShown === true) {
|
|
2501
|
+
updateCheckErrorShown = false
|
|
2502
|
+
patchManagement({ settingsError: null })
|
|
2503
|
+
}
|
|
2504
|
+
return { kind: 'ok' }
|
|
2505
|
+
}
|
|
2506
|
+
// A first-ever check that is already in flight comes back as
|
|
2507
|
+
// `{ok:true, update:null}` (the Host's single-flight returns the last
|
|
2508
|
+
// result). Retry once after a short delay instead of reporting a bad
|
|
2509
|
+
// response; a result that arrived meanwhile (management sync) retires
|
|
2510
|
+
// the retry.
|
|
2511
|
+
if (!retried) {
|
|
2512
|
+
if (updateCheckRetryTimer !== null) window.clearTimeout(updateCheckRetryTimer)
|
|
2513
|
+
updateCheckRetryTimer = window.setTimeout(() => {
|
|
2514
|
+
updateCheckRetryTimer = null
|
|
2515
|
+
store.set({ updateChecking: false })
|
|
2516
|
+
if (store.get().update !== null) return
|
|
2517
|
+
void requestUpdateCheck({ retried: true })
|
|
2518
|
+
}, UPDATE_CHECK_RETRY_MS)
|
|
2519
|
+
return { kind: 'pending' }
|
|
2520
|
+
}
|
|
2521
|
+
store.set({ updateChecking: false })
|
|
2522
|
+
return { kind: 'pending' }
|
|
2523
|
+
}
|
|
2524
|
+
store.set({ updateChecking: false })
|
|
2525
|
+
updateCheckErrorShown = true
|
|
2526
|
+
patchManagement({ settingsError: normalizeError(data.error) })
|
|
2527
|
+
return { kind: 'envelope' }
|
|
2528
|
+
}
|
|
2529
|
+
store.set({ updateChecking: false })
|
|
2530
|
+
updateCheckErrorShown = true
|
|
2531
|
+
patchManagement({
|
|
2532
|
+
settingsError: { code: 'internal', message: 'unexpected updateCheck response', retryable: true },
|
|
2533
|
+
})
|
|
2534
|
+
return { kind: 'transport' }
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
/**
|
|
2538
|
+
* The management page's ONE silent check: only while the setting is on and
|
|
2539
|
+
* no result is known yet. The module-level flag makes a re-open, a tab
|
|
2540
|
+
* switch or a provider-list refresh impossible to turn into a second check.
|
|
2541
|
+
*/
|
|
2542
|
+
function maybeAutoUpdateCheck() {
|
|
2543
|
+
if (autoUpdateCheckStarted === true) return
|
|
2544
|
+
const state = store.get()
|
|
2545
|
+
if (state.updateCheck !== true || state.update !== null) return
|
|
2546
|
+
autoUpdateCheckStarted = true
|
|
2547
|
+
void requestUpdateCheck()
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
/** The pending network-free providers re-read (see UPDATE_RESULT_REFRESH_MS). */
|
|
2551
|
+
let updateResultRefreshTimer = null
|
|
2552
|
+
let updateResultRefreshAttempt = 0
|
|
2553
|
+
|
|
2554
|
+
/**
|
|
2555
|
+
* Read the local providers payload once more after the Host's background
|
|
2556
|
+
* update check had time to finish (`updateCheck` on, no result known yet).
|
|
2557
|
+
*
|
|
2558
|
+
* This never asks the npm registry itself — it only re-reads the Host state
|
|
2559
|
+
* that the Host's own startup / toggle check fills in. No-op once a result
|
|
2560
|
+
* exists or the setting is off, so it can be armed from several places.
|
|
2561
|
+
*/
|
|
2562
|
+
function scheduleUpdateResultRefresh() {
|
|
2563
|
+
if (updateResultRefreshTimer !== null) return
|
|
2564
|
+
const delay = UPDATE_RESULT_REFRESH_MS[updateResultRefreshAttempt]
|
|
2565
|
+
if (delay === undefined) return
|
|
2566
|
+
updateResultRefreshAttempt += 1
|
|
2567
|
+
updateResultRefreshTimer = window.setTimeout(() => {
|
|
2568
|
+
updateResultRefreshTimer = null
|
|
2569
|
+
const state = store.get()
|
|
2570
|
+
if (state.updateCheck !== true || state.update !== null) {
|
|
2571
|
+
updateResultRefreshAttempt = 0
|
|
2572
|
+
return
|
|
2573
|
+
}
|
|
2574
|
+
void bootstrapProviders().then(() => {
|
|
2575
|
+
if (store.get().update !== null) {
|
|
2576
|
+
updateResultRefreshAttempt = 0
|
|
2577
|
+
return
|
|
2578
|
+
}
|
|
2579
|
+
scheduleUpdateResultRefresh()
|
|
2580
|
+
})
|
|
2581
|
+
}, delay)
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2334
2584
|
/** Force one upstream read for a provider and keep the row outcome. Never throws. */
|
|
2335
2585
|
async function requestProviderTest(providerId) {
|
|
2336
2586
|
const before = store.get().management
|
|
@@ -2519,6 +2769,9 @@ window.__ModuleLoader__.load({
|
|
|
2519
2769
|
const current = store.get().management
|
|
2520
2770
|
const stale = current.loadedAt === 0 || Date.now() - current.loadedAt >= PROVIDERS_TTL_MS
|
|
2521
2771
|
if (current.inFlight !== true && stale) void requestProviders()
|
|
2772
|
+
// A fresh check when the setting is already known and no result exists
|
|
2773
|
+
// (the requestProviders answer triggers the same one-shot path otherwise).
|
|
2774
|
+
maybeAutoUpdateCheck()
|
|
2522
2775
|
}
|
|
2523
2776
|
|
|
2524
2777
|
/** Switch back to the usage page (the modal chrome stays). */
|
|
@@ -2633,12 +2886,20 @@ window.__ModuleLoader__.load({
|
|
|
2633
2886
|
void requestProviderUpdate(provider.providerId, { shortLabel: '', label: '' })
|
|
2634
2887
|
}
|
|
2635
2888
|
|
|
2636
|
-
/** Store the plugin version
|
|
2889
|
+
/** Store the plugin version and the last update check result of the hello envelope. */
|
|
2637
2890
|
function applyHelloInfo(data) {
|
|
2638
2891
|
if (data === null || typeof data !== 'object') return
|
|
2892
|
+
const patch = {}
|
|
2639
2893
|
if (typeof data.version === 'string' && data.version !== '') {
|
|
2640
|
-
if (store.get().pluginVersion !== data.version)
|
|
2894
|
+
if (store.get().pluginVersion !== data.version) patch.pluginVersion = data.version
|
|
2895
|
+
}
|
|
2896
|
+
// `update` is optional on the wire: only an explicit field (null included)
|
|
2897
|
+
// replaces what the store already knows.
|
|
2898
|
+
if (Object.prototype.hasOwnProperty.call(data, 'update')) {
|
|
2899
|
+
const update = normalizeUpdate(data.update)
|
|
2900
|
+
if (!sameUpdate(store.get().update, update)) patch.update = update
|
|
2641
2901
|
}
|
|
2902
|
+
if (Object.keys(patch).length > 0) store.set(patch)
|
|
2642
2903
|
}
|
|
2643
2904
|
|
|
2644
2905
|
/** Announce this browser half to the Host (also records the current provider). */
|
|
@@ -2772,6 +3033,11 @@ window.__ModuleLoader__.load({
|
|
|
2772
3033
|
// v0.10.0: learn the persisted chip label style + wire labels once, without
|
|
2773
3034
|
// touching the management page state (silent when an old Host answers nothing).
|
|
2774
3035
|
void bootstrapProviders()
|
|
3036
|
+
// The Host runs its optional startup update check in the background; the
|
|
3037
|
+
// hello / bootstrap answers above can win the race and carry no result yet.
|
|
3038
|
+
// Re-read the (local, no-network) providers payload shortly after so the
|
|
3039
|
+
// dialog footer can show the result without opening the management page.
|
|
3040
|
+
scheduleUpdateResultRefresh()
|
|
2775
3041
|
pollTimer = window.setInterval(() => {
|
|
2776
3042
|
void poll()
|
|
2777
3043
|
}, POLL_INTERVAL_MS)
|
|
@@ -2783,6 +3049,16 @@ window.__ModuleLoader__.load({
|
|
|
2783
3049
|
disposed = true
|
|
2784
3050
|
if (pollTimer !== null) window.clearInterval(pollTimer)
|
|
2785
3051
|
if (retryTimer !== null) window.clearTimeout(retryTimer)
|
|
3052
|
+
// Pending update-check timers must not fire into a disposed controller.
|
|
3053
|
+
updateResultRefreshAttempt = 0
|
|
3054
|
+
if (updateResultRefreshTimer !== null) {
|
|
3055
|
+
window.clearTimeout(updateResultRefreshTimer)
|
|
3056
|
+
updateResultRefreshTimer = null
|
|
3057
|
+
}
|
|
3058
|
+
if (updateCheckRetryTimer !== null) {
|
|
3059
|
+
window.clearTimeout(updateCheckRetryTimer)
|
|
3060
|
+
updateCheckRetryTimer = null
|
|
3061
|
+
}
|
|
2786
3062
|
if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
|
|
2787
3063
|
document.removeEventListener('visibilitychange', onVisibilityChange)
|
|
2788
3064
|
}
|
|
@@ -2974,7 +3250,11 @@ window.__ModuleLoader__.load({
|
|
|
2974
3250
|
.dsh-tokenlens-chart-drill-title{color:var(--dsw-alias-label-secondary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.4;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
2975
3251
|
/* 明细 donut window switch (v1.2.0): one pill row under the details title. */
|
|
2976
3252
|
.dsh-tokenlens-details-window{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
|
|
2977
|
-
.dsh-tokenlens-version{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;text-align:right;opacity:.75}
|
|
3253
|
+
.dsh-tokenlens-version{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;text-align:right;opacity:.75;overflow-wrap:anywhere}
|
|
3254
|
+
/* Update status on the plugin-version line (dialog body): wraps on narrow screens. */
|
|
3255
|
+
.dsh-tokenlens-version-update{white-space:normal;overflow-wrap:anywhere}
|
|
3256
|
+
.dsh-tokenlens-version .dsh-tokenlens-update-link{color:inherit;font-size:inherit;vertical-align:baseline}
|
|
3257
|
+
.dsh-tokenlens-version .dsh-tokenlens-update-link .dsh-tokenlens-icon{margin-left:2px;vertical-align:-1px}
|
|
2978
3258
|
.dsh-tokenlens-cards{gap:6px;flex-wrap:wrap;display:flex}
|
|
2979
3259
|
.dsh-tokenlens-cards-caption{color:var(--dsw-alias-label-tertiary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.4;margin-bottom:-3px}
|
|
2980
3260
|
.dsh-tokenlens-harness-breakdown{display:flex;flex-direction:column;gap:4px}
|
|
@@ -3118,6 +3398,20 @@ window.__ModuleLoader__.load({
|
|
|
3118
3398
|
.dsh-tokenlens-settings-options .dsh-tokenlens-settings-option{display:inline-flex;align-items:center;gap:4px}
|
|
3119
3399
|
.dsh-tokenlens-settings-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:8px;min-width:0}
|
|
3120
3400
|
@media (max-width:560px){.dsh-tokenlens-settings-row{grid-template-columns:minmax(0,1fr)}}
|
|
3401
|
+
/* Update check block (update UI): toggle row, privacy hint, status line + 立即检查. */
|
|
3402
|
+
.dsh-tokenlens-update-section{display:flex;flex-direction:column;gap:6px;min-width:0;margin-top:2px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-subtle,rgba(127,127,127,.18))}
|
|
3403
|
+
.dsh-tokenlens-update-hint{opacity:.8;font-size:11px}
|
|
3404
|
+
.dsh-tokenlens-update-status{align-items:center;flex-wrap:wrap;display:flex;gap:5px;min-width:0;color:var(--dsw-alias-label-secondary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.4;overflow-wrap:anywhere}
|
|
3405
|
+
.dsh-tokenlens-update-status[data-kind=error]{color:var(--dsw-alias-label-error,#d9534f)}
|
|
3406
|
+
.dsh-tokenlens-update-status[data-kind=available]{color:var(--dsw-alias-label-primary)}
|
|
3407
|
+
.dsh-tokenlens-update-error-detail{opacity:.85;min-width:0;overflow-wrap:anywhere}
|
|
3408
|
+
.dsh-tokenlens-update-retry{font:inherit;font-size:11px;line-height:inherit;padding:0;border:0;background:0 0;color:inherit;cursor:pointer;text-decoration:underline;text-underline-offset:2px}
|
|
3409
|
+
.dsh-tokenlens-update-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;min-width:0}
|
|
3410
|
+
.dsh-tokenlens-update-link{display:inline-flex;align-items:center;gap:4px;color:var(--dsw-alias-label-secondary);font-size:inherit;text-decoration:underline;text-underline-offset:2px;min-width:0}
|
|
3411
|
+
.dsh-tokenlens-update-link:hover,.dsh-tokenlens-update-retry:hover{color:var(--dsw-alias-label-primary)}
|
|
3412
|
+
.dsh-tokenlens-update-link:focus-visible,.dsh-tokenlens-update-retry:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-strong,rgba(127,127,127,.5));border-radius:2px}
|
|
3413
|
+
.dsh-tokenlens-update-line{display:inline-flex;flex-wrap:wrap;align-items:center;gap:4px 6px;min-width:0;margin-right:auto;color:var(--dsw-alias-label-tertiary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.4}
|
|
3414
|
+
.dsh-tokenlens-update-line .dsh-tokenlens-update-link{color:inherit}
|
|
3121
3415
|
.dsh-tokenlens-provider-labels{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.4;overflow-wrap:anywhere;display:flex;flex-wrap:wrap;align-items:flex-start;gap:4px}
|
|
3122
3416
|
.dsh-tokenlens-provider-labels .dsh-tokenlens-icon{margin-top:1px}
|
|
3123
3417
|
/* Inline icons: currentColor stroke, never a text node (keeps textOf stable). */
|
|
@@ -5891,11 +6185,13 @@ window.__ModuleLoader__.load({
|
|
|
5891
6185
|
h(
|
|
5892
6186
|
'button',
|
|
5893
6187
|
{
|
|
5894
|
-
key: option.value,
|
|
6188
|
+
key: String(option.value),
|
|
5895
6189
|
type: 'button',
|
|
5896
6190
|
className: `dsh-tokenlens-scope-option dsh-tokenlens-settings-option${field === 'labelStyle' ? ' dsh-tokenlens-label-style-option' : ''}`,
|
|
5897
6191
|
'data-setting': field,
|
|
5898
|
-
|
|
6192
|
+
// A boolean row (updateCheck) is stringified so `data-*` matches
|
|
6193
|
+
// the real DOM; the string rows keep their wire value verbatim.
|
|
6194
|
+
'data-value': typeof option.value === 'string' ? option.value : String(option.value),
|
|
5899
6195
|
'data-label-style': field === 'labelStyle' ? option.value : undefined,
|
|
5900
6196
|
'data-active': active === option.value ? 'true' : 'false',
|
|
5901
6197
|
'aria-pressed': active === option.value ? 'true' : 'false',
|
|
@@ -5913,6 +6209,127 @@ window.__ModuleLoader__.load({
|
|
|
5913
6209
|
)
|
|
5914
6210
|
}
|
|
5915
6211
|
|
|
6212
|
+
/** The npm page the update links open (the Host-advertised one when present). */
|
|
6213
|
+
function updatePackageUrlOf(state) {
|
|
6214
|
+
return typeof state?.updatePackageUrl === 'string' && state.updatePackageUrl !== ''
|
|
6215
|
+
? state.updatePackageUrl
|
|
6216
|
+
: NPM_PACKAGE_URL
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6219
|
+
/**
|
|
6220
|
+
* The 「更新检查」 status line: checking → failed → update available → up to
|
|
6221
|
+
* date → never checked. `data-update-status` names the branch for tests and
|
|
6222
|
+
* styling; the error branch keeps a 重试 action only when retryable.
|
|
6223
|
+
*/
|
|
6224
|
+
function buildUpdateStatusLine(state, t) {
|
|
6225
|
+
const update = state.update
|
|
6226
|
+
const packageUrl = updatePackageUrlOf(state)
|
|
6227
|
+
const line = (kind, children) =>
|
|
6228
|
+
h(
|
|
6229
|
+
'div',
|
|
6230
|
+
{
|
|
6231
|
+
key: 'update-status',
|
|
6232
|
+
className: 'dsh-tokenlens-update-status',
|
|
6233
|
+
'data-update-status': kind,
|
|
6234
|
+
'data-kind': kind,
|
|
6235
|
+
role: 'status',
|
|
6236
|
+
},
|
|
6237
|
+
Icon(kind === 'error' ? 'alert' : kind === 'available' ? 'bolt' : kind === 'latest' ? 'check' : 'info', 12),
|
|
6238
|
+
children,
|
|
6239
|
+
)
|
|
6240
|
+
if (state.updateChecking === true) {
|
|
6241
|
+
return line('checking', h('span', { key: 'text' }, t('updateChecking')))
|
|
6242
|
+
}
|
|
6243
|
+
if (update !== null && update.error !== null) {
|
|
6244
|
+
return line('error', [
|
|
6245
|
+
h('span', { key: 'text' }, t('updateCheckFailed')),
|
|
6246
|
+
h('span', { key: 'detail', className: 'dsh-tokenlens-update-error-detail', title: update.error.message }, managementErrorText(update.error, t)),
|
|
6247
|
+
update.error.retryable === true
|
|
6248
|
+
? h(
|
|
6249
|
+
'button',
|
|
6250
|
+
{
|
|
6251
|
+
key: 'retry',
|
|
6252
|
+
type: 'button',
|
|
6253
|
+
className: 'dsh-tokenlens-update-retry',
|
|
6254
|
+
'data-update-retry': 'true',
|
|
6255
|
+
onClick: () => {
|
|
6256
|
+
void requestUpdateCheck()
|
|
6257
|
+
},
|
|
6258
|
+
},
|
|
6259
|
+
t('retry'),
|
|
6260
|
+
)
|
|
6261
|
+
: null,
|
|
6262
|
+
])
|
|
6263
|
+
}
|
|
6264
|
+
if (update !== null && update.available === true) {
|
|
6265
|
+
return line('available', [
|
|
6266
|
+
h('span', { key: 'text' }, t('updateAvailable', { latest: update.latest, current: update.current })),
|
|
6267
|
+
h(
|
|
6268
|
+
'a',
|
|
6269
|
+
{
|
|
6270
|
+
key: 'link',
|
|
6271
|
+
className: 'dsh-tokenlens-update-link',
|
|
6272
|
+
'data-update-link': 'true',
|
|
6273
|
+
href: packageUrl,
|
|
6274
|
+
target: '_blank',
|
|
6275
|
+
rel: 'noreferrer noopener',
|
|
6276
|
+
title: packageUrl,
|
|
6277
|
+
onClick: (event) => openConsole(packageUrl, event),
|
|
6278
|
+
},
|
|
6279
|
+
Icon('external', 12),
|
|
6280
|
+
t('updateOpenNpm'),
|
|
6281
|
+
),
|
|
6282
|
+
])
|
|
6283
|
+
}
|
|
6284
|
+
if (update !== null && update.latest !== null && update.available === false) {
|
|
6285
|
+
return line('latest', h('span', { key: 'text' }, t('updateLatest', { version: update.latest })))
|
|
6286
|
+
}
|
|
6287
|
+
return line('never', h('span', { key: 'text' }, t('updateCheckNever')))
|
|
6288
|
+
}
|
|
6289
|
+
|
|
6290
|
+
/**
|
|
6291
|
+
* The 「更新检查」 block of 插件设置: the startup toggle (the shared settings
|
|
6292
|
+
* row), the registry-privacy hint, the last check result and 「立即检查」.
|
|
6293
|
+
* The check button works whether the startup toggle is on or off, and its
|
|
6294
|
+
* `updateChecking` flag is independent from `settingsBusy`.
|
|
6295
|
+
*/
|
|
6296
|
+
function buildUpdateSection(state, t) {
|
|
6297
|
+
const checking = state.updateChecking === true
|
|
6298
|
+
return h(
|
|
6299
|
+
'div',
|
|
6300
|
+
{ key: 'update-section', className: 'dsh-tokenlens-update-section', 'data-update-section': 'true' },
|
|
6301
|
+
buildSettingsRow(state, t, 'updateCheck', t('updateCheckLabel'), [
|
|
6302
|
+
{ value: false, label: t('updateCheckOff'), icon: 'ban' },
|
|
6303
|
+
{ value: true, label: t('updateCheckOn'), icon: 'check' },
|
|
6304
|
+
]),
|
|
6305
|
+
h(
|
|
6306
|
+
'div',
|
|
6307
|
+
{ key: 'hint', className: 'dsh-tokenlens-subtitle dsh-tokenlens-update-hint', 'data-update-hint': 'true' },
|
|
6308
|
+
t('updateCheckHint'),
|
|
6309
|
+
),
|
|
6310
|
+
buildUpdateStatusLine(state, t),
|
|
6311
|
+
h(
|
|
6312
|
+
'div',
|
|
6313
|
+
{ key: 'actions', className: 'dsh-tokenlens-update-actions' },
|
|
6314
|
+
h(
|
|
6315
|
+
Button,
|
|
6316
|
+
{
|
|
6317
|
+
variant: 'outline',
|
|
6318
|
+
className: 'dsh-tokenlens-update-check',
|
|
6319
|
+
'data-update-check-now': 'true',
|
|
6320
|
+
title: t('updateCheckNow'),
|
|
6321
|
+
disabled: checking,
|
|
6322
|
+
onClick: () => {
|
|
6323
|
+
void requestUpdateCheck()
|
|
6324
|
+
},
|
|
6325
|
+
},
|
|
6326
|
+
Icon('spinner', 12),
|
|
6327
|
+
checking ? t('updateChecking') : t('updateCheckNow'),
|
|
6328
|
+
),
|
|
6329
|
+
),
|
|
6330
|
+
)
|
|
6331
|
+
}
|
|
6332
|
+
|
|
5916
6333
|
/** The 插件设置 tab (v0.29.0): display defaults, reset button and store path. */
|
|
5917
6334
|
function buildSettingsPanel(state, t) {
|
|
5918
6335
|
const management = state.management
|
|
@@ -5937,6 +6354,9 @@ window.__ModuleLoader__.load({
|
|
|
5937
6354
|
{ value: 'collapsed', label: t('settingsDetailsCollapsed'), icon: 'list' },
|
|
5938
6355
|
]),
|
|
5939
6356
|
]
|
|
6357
|
+
// 「更新检查」 is its own block (own status + own in-flight flag), kept
|
|
6358
|
+
// apart from the display-defaults rows and their reset button.
|
|
6359
|
+
children.push(buildUpdateSection(state, t))
|
|
5940
6360
|
const actions = h(
|
|
5941
6361
|
'div',
|
|
5942
6362
|
{ key: 'actions', className: 'dsh-tokenlens-settings-actions' },
|
|
@@ -6381,6 +6801,69 @@ window.__ModuleLoader__.load({
|
|
|
6381
6801
|
})
|
|
6382
6802
|
}
|
|
6383
6803
|
|
|
6804
|
+
/**
|
|
6805
|
+
* The update status appended to the plugin-version line of the usage dialog
|
|
6806
|
+
* body, or null when it must stay silent.
|
|
6807
|
+
*
|
|
6808
|
+
* Rules (a check result only): an available release renders as
|
|
6809
|
+
* 「 · 有新版本 vX」 with the version phrase linking to the npm page; a
|
|
6810
|
+
* successful check without a newer release renders 「 · 已是最新版本」.
|
|
6811
|
+
* No result, a failed check or an explicitly disabled setting adds nothing
|
|
6812
|
+
* (never show a misleading status).
|
|
6813
|
+
*/
|
|
6814
|
+
function buildVersionUpdateStatus(state, t) {
|
|
6815
|
+
if (state.updateCheck !== true) return null
|
|
6816
|
+
const update = state.update
|
|
6817
|
+
if (update === null || update.error !== null) return null
|
|
6818
|
+
if (update.available === true && update.latest !== null) {
|
|
6819
|
+
const packageUrl = updatePackageUrlOf(state)
|
|
6820
|
+
// The version NUMBER inside the sentence is the real npm link; the i18n
|
|
6821
|
+
// sentence itself stays a single string (`updateVersionNew`).
|
|
6822
|
+
const sentence = t('updateVersionNew', { latest: update.latest })
|
|
6823
|
+
const versionText = `v${update.latest}`
|
|
6824
|
+
const index = sentence.indexOf(versionText)
|
|
6825
|
+
const link = h(
|
|
6826
|
+
'a',
|
|
6827
|
+
{
|
|
6828
|
+
className: 'dsh-tokenlens-update-link dsh-tokenlens-version-link',
|
|
6829
|
+
'data-update-link': 'true',
|
|
6830
|
+
'data-update-version-link': 'true',
|
|
6831
|
+
href: packageUrl,
|
|
6832
|
+
target: '_blank',
|
|
6833
|
+
rel: 'noreferrer noopener',
|
|
6834
|
+
title: packageUrl,
|
|
6835
|
+
onClick: (event) => openConsole(packageUrl, event),
|
|
6836
|
+
},
|
|
6837
|
+
index >= 0 ? versionText : `v${update.latest}`,
|
|
6838
|
+
Icon('external', 12),
|
|
6839
|
+
)
|
|
6840
|
+
return h(
|
|
6841
|
+
'span',
|
|
6842
|
+
{
|
|
6843
|
+
key: 'update-status',
|
|
6844
|
+
className: 'dsh-tokenlens-version-update',
|
|
6845
|
+
'data-update-version-status': 'available',
|
|
6846
|
+
},
|
|
6847
|
+
' · ',
|
|
6848
|
+
index >= 0 ? sentence.slice(0, index) : `${sentence} `,
|
|
6849
|
+
link,
|
|
6850
|
+
)
|
|
6851
|
+
}
|
|
6852
|
+
if (update.latest !== null) {
|
|
6853
|
+
return h(
|
|
6854
|
+
'span',
|
|
6855
|
+
{
|
|
6856
|
+
key: 'update-status',
|
|
6857
|
+
className: 'dsh-tokenlens-version-update',
|
|
6858
|
+
'data-update-version-status': 'latest',
|
|
6859
|
+
},
|
|
6860
|
+
' · ',
|
|
6861
|
+
t('updateVersionLatest'),
|
|
6862
|
+
)
|
|
6863
|
+
}
|
|
6864
|
+
return null
|
|
6865
|
+
}
|
|
6866
|
+
|
|
6384
6867
|
/** The dialog contents (body + footer); may throw, the caller guards it. */
|
|
6385
6868
|
function buildDialog(state, now, t) {
|
|
6386
6869
|
if (state.view === 'management') return buildManagementDialog(state, t)
|
|
@@ -6412,6 +6895,9 @@ window.__ModuleLoader__.load({
|
|
|
6412
6895
|
'div',
|
|
6413
6896
|
{ key: 'version', className: 'dsh-tokenlens-version' },
|
|
6414
6897
|
t('pluginVersion', { version: state.pluginVersion }),
|
|
6898
|
+
// Update status sits right next to the plugin version: 有新版本 vX
|
|
6899
|
+
// (link to the npm page) or 已是最新版本, only for a successful check.
|
|
6900
|
+
buildVersionUpdateStatus(state, t),
|
|
6415
6901
|
),
|
|
6416
6902
|
)
|
|
6417
6903
|
}
|