dsh-all-usage 1.1.18 → 1.2.0
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 +453 -394
- package/LICENSE +21 -21
- package/README.en.md +270 -0
- package/README.md +142 -279
- package/assets/model-icons/ai2-color.svg +1 -0
- package/assets/model-icons/ai21-brand-color.svg +1 -0
- package/assets/model-icons/aionlabs.svg +1 -0
- package/assets/model-icons/alephalpha.svg +1 -0
- package/assets/model-icons/baai.svg +1 -0
- package/assets/model-icons/baichuan-color.svg +1 -0
- package/assets/model-icons/baidu-color.svg +1 -0
- package/assets/model-icons/bytedance-color.svg +1 -0
- package/assets/model-icons/cohere-color.svg +1 -0
- package/assets/model-icons/hunyuan-color.svg +1 -0
- package/assets/model-icons/ibm.svg +1 -0
- package/assets/model-icons/iflytekcloud-color.svg +1 -0
- package/assets/model-icons/internlm-color.svg +1 -0
- package/assets/model-icons/kling-color.svg +1 -0
- package/assets/model-icons/liquid.svg +1 -0
- package/assets/model-icons/longcat-color.svg +1 -0
- package/assets/model-icons/manifest.json +739 -213
- package/assets/model-icons/microsoft-color.svg +1 -0
- package/assets/model-icons/mistral-color.svg +1 -0
- package/assets/model-icons/nousresearch.svg +1 -0
- package/assets/model-icons/nova-color.svg +1 -0
- package/assets/model-icons/nvidia-color.svg +1 -0
- package/assets/model-icons/perplexity-color.svg +1 -0
- package/assets/model-icons/sensenova-color.svg +1 -0
- package/assets/model-icons/skywork-color.svg +1 -0
- package/assets/model-icons/snowflake-color.svg +1 -0
- package/assets/model-icons/stepfun-color.svg +1 -0
- package/assets/model-icons/tencent-color.svg +1 -0
- package/assets/model-icons/tii-color.svg +1 -0
- package/assets/model-icons/upstage-color.svg +1 -0
- package/assets/model-icons/voyage-color.svg +1 -0
- package/assets/model-icons/xiaomimimo.svg +1 -0
- package/assets/model-icons/yandex.svg +1 -0
- package/assets/readme-architecture-en.svg +95 -0
- package/assets/readme-architecture-zh.svg +95 -0
- package/assets/readme-sync-en.svg +81 -0
- package/assets/readme-sync-zh.svg +81 -0
- package/assets/screenshots/en/01-overview.png +0 -0
- package/assets/screenshots/en/02-heatmap.png +0 -0
- package/assets/screenshots/en/03-model-usage.png +0 -0
- package/assets/screenshots/en/04-request-logs.png +0 -0
- package/assets/screenshots/en/05-cost-settings.png +0 -0
- package/assets/screenshots/zh/01-overview.png +0 -0
- package/assets/screenshots/zh/02-heatmap.png +0 -0
- package/assets/screenshots/zh/03-model-usage.png +0 -0
- package/assets/screenshots/zh/04-request-logs.png +0 -0
- package/assets/screenshots/zh/05-cost-settings.png +0 -0
- package/cordis.patch.yml +5 -5
- package/lib/aggregation.js +1842 -1112
- package/lib/client.js +1 -1
- package/lib/http.js +417 -385
- package/lib/icons.js +40 -0
- package/lib/ledger.js +1000 -531
- package/lib/plugin.js +347 -327
- package/lib/pricing-runtime.js +615 -551
- package/lib/pricing.js +1463 -1412
- package/lib/records-cache.js +113 -0
- package/lib/session-sync.js +976 -841
- package/lib/usage-core.js +33 -10
- package/lib/version-check.js +133 -133
- package/package.json +7 -3
- package/screenshots.json +10 -3
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
package/lib/usage-core.js
CHANGED
|
@@ -37,7 +37,7 @@ export function normalizeUsageValues(usage) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
function hasUsageValues(values) {
|
|
41
41
|
return values !== null && values !== undefined && (values.input > 0 || values.output > 0 || values.cacheRead > 0 || values.cacheWrite > 0 || values.reasoning > 0)
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -114,17 +114,39 @@ export function usageStepKey(sid, data, seq, fallback) {
|
|
|
114
114
|
return safeSid + ':event:' + (serialized === undefined ? String(fallback === undefined ? '' : fallback) : serialized)
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
117
|
+
/**
|
|
118
|
+
* A context stamp describes one request, and DSH can emit `request/context` once
|
|
119
|
+
* per session rather than once per call: reading such a stamp back prices an
|
|
120
|
+
* evening request inside the morning peak window. Anything older than this is not
|
|
121
|
+
* the request being billed, so the usage event's own time is used instead.
|
|
122
|
+
*/
|
|
123
|
+
export const MAX_CONTEXT_AGE_MS = 30 * 60 * 1000
|
|
124
|
+
|
|
125
|
+
/** True when a context stamp is close enough to the event to belong to it. */
|
|
126
|
+
export function contextTimeIsFresh(candidate, eventTime) {
|
|
127
|
+
return Number.isFinite(candidate) && validEventTime(eventTime) && candidate <= eventTime && eventTime - candidate <= MAX_CONTEXT_AGE_MS
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Recalculate the billing instant for a re-priced sample, preferring the recorded
|
|
132
|
+
* request time. `trusted` is false when the caller proved the recorded stamp was
|
|
133
|
+
* a stale session-level context rather than this request's own.
|
|
134
|
+
*/
|
|
135
|
+
export function billingInstantOf(item, oldCost, trusted = true) {
|
|
119
136
|
const oldAt = oldCost != null && Number.isFinite(oldCost.pricingAt) ? oldCost.pricingAt : null
|
|
120
|
-
const
|
|
137
|
+
const itemTime = item !== null && typeof item === 'object' ? item.time : undefined
|
|
138
|
+
const itemAt = item !== null && typeof item === 'object' && Number.isFinite(item.pricingAt) ? item.pricingAt : null
|
|
139
|
+
const at = trusted
|
|
140
|
+
? (itemAt !== null ? itemAt : oldAt !== null ? oldAt : validEventTime(itemTime) ? itemTime : null)
|
|
141
|
+
: (validEventTime(itemTime) ? itemTime : oldAt)
|
|
121
142
|
const oldSource = oldCost != null && (oldCost.pricingTimeSource === 'request-context' || oldCost.pricingTimeSource === 'usage-event') ? oldCost.pricingTimeSource : null
|
|
122
|
-
const
|
|
143
|
+
const recordedSource = typeof item.pricingTimeSource === 'string' && (item.pricingTimeSource === 'request-context' || item.pricingTimeSource === 'usage-event') ? item.pricingTimeSource : (oldSource || 'usage-event')
|
|
144
|
+
const source = trusted ? recordedSource : 'usage-event'
|
|
123
145
|
return { at, source }
|
|
124
146
|
}
|
|
125
147
|
|
|
126
148
|
/** Bounded context archive shared by the live fold and the persisted ledger. */
|
|
127
|
-
|
|
149
|
+
const MAX_CONTEXT_TIMES = 512
|
|
128
150
|
|
|
129
151
|
/** Insert-or-refresh a context entry with real LRU eviction (delete + set). */
|
|
130
152
|
export function touchContextTimes(times, key, time) {
|
|
@@ -150,10 +172,11 @@ export function contextTimeKey(turn, step) {
|
|
|
150
172
|
export function pickPricingTime(contextTimes, eventTime, turn, step) {
|
|
151
173
|
if (contextTimes instanceof Map && validEventTime(eventTime)) {
|
|
152
174
|
const exact = contextTimes.get(contextTimeKey(turn, step))
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
if (Number.isFinite(exact) && exact <= eventTime) return { time: exact, source: 'request-context' }
|
|
176
|
+
// The session-level fallback is only this request's context while it is recent;
|
|
177
|
+
// an old one would price the call in a window it never ran in.
|
|
178
|
+
const latest = contextTimes.get(contextTimeKey(null, null))
|
|
179
|
+
if (contextTimeIsFresh(latest, eventTime)) return { time: latest, source: 'request-context' }
|
|
157
180
|
}
|
|
158
181
|
return { time: Number.isFinite(eventTime) && eventTime >= 0 ? eventTime : null, source: 'usage-event' }
|
|
159
182
|
}
|
package/lib/version-check.js
CHANGED
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
// Version check for the dashboard header. The running version is known locally;
|
|
2
|
-
// this only asks the npm registry for the newest published one, at most once per
|
|
3
|
-
// TTL and only while a dashboard is open (the page asks for it, the host never
|
|
4
|
-
// polls on its own). A plain GET of a small JSON document, no body, no cookies.
|
|
5
|
-
const VERSION_CHECK_URL = 'https://registry.npmjs.org/dsh-all-usage/latest'
|
|
6
|
-
const VERSION_CHECK_TTL_MS = 6 * 60 * 60 * 1000
|
|
7
|
-
const VERSION_CHECK_TIMEOUT_MS = 8000
|
|
8
|
-
// The route is a plain local read (like /api/all-usage/status it needs no
|
|
9
|
-
// capability), so this floor is what keeps one page - or a hostile local page -
|
|
10
|
-
// from turning a burst of requests into a burst of registry requests.
|
|
11
|
-
const VERSION_CHECK_MIN_INTERVAL_MS = 30 * 1000
|
|
12
|
-
const MAX_VERSION_BYTES = 64 * 1024
|
|
13
|
-
|
|
14
|
-
function parseVersion(text) {
|
|
15
|
-
const value = String(text === undefined || text === null ? '' : text).trim().replace(/^v/, '')
|
|
16
|
-
const match = value.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/)
|
|
17
|
-
if (match === null) return null
|
|
18
|
-
return {
|
|
19
|
-
major: Number(match[1]),
|
|
20
|
-
minor: Number(match[2]),
|
|
21
|
-
patch: Number(match[3]),
|
|
22
|
-
prerelease: match[4] === undefined ? null : match[4].split('.'),
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Compare two dotted versions the way semver orders them: numeric parts first,
|
|
28
|
-
* then a prerelease ranks below its release and numeric identifiers rank below
|
|
29
|
-
* alphanumeric ones ('1.1.18-rc.1' < '1.1.18' < '1.1.19'). Returns -1, 0 or 1,
|
|
30
|
-
* and 0 for anything unparseable so a malformed tag never claims an update.
|
|
31
|
-
*/
|
|
32
|
-
export function compareVersions(left, right) {
|
|
33
|
-
const a = parseVersion(left)
|
|
34
|
-
const b = parseVersion(right)
|
|
35
|
-
if (a === null || b === null) return 0
|
|
36
|
-
for (const key of ['major', 'minor', 'patch']) {
|
|
37
|
-
if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1
|
|
38
|
-
}
|
|
39
|
-
if (a.prerelease === null && b.prerelease === null) return 0
|
|
40
|
-
if (a.prerelease === null) return 1
|
|
41
|
-
if (b.prerelease === null) return -1
|
|
42
|
-
const length = Math.max(a.prerelease.length, b.prerelease.length)
|
|
43
|
-
for (let index = 0; index < length; index += 1) {
|
|
44
|
-
const leftPart = a.prerelease[index]
|
|
45
|
-
const rightPart = b.prerelease[index]
|
|
46
|
-
if (leftPart === undefined) return -1
|
|
47
|
-
if (rightPart === undefined) return 1
|
|
48
|
-
if (leftPart === rightPart) continue
|
|
49
|
-
const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null
|
|
50
|
-
const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null
|
|
51
|
-
if (leftNumber !== null && rightNumber !== null) return leftNumber < rightNumber ? -1 : 1
|
|
52
|
-
if (leftNumber !== null) return -1
|
|
53
|
-
if (rightNumber !== null) return 1
|
|
54
|
-
return leftPart < rightPart ? -1 : 1
|
|
55
|
-
}
|
|
56
|
-
return 0
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 'latest' when the running version is at least the published one, else 'outdated'. */
|
|
60
|
-
export function versionStatus(current, latest) {
|
|
61
|
-
if (typeof current !== 'string' || current === '') return 'unknown'
|
|
62
|
-
if (typeof latest !== 'string' || latest === '') return 'unknown'
|
|
63
|
-
if (parseVersion(current) === null || parseVersion(latest) === null) return 'unknown'
|
|
64
|
-
return compareVersions(current, latest) < 0 ? 'outdated' : 'latest'
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function fetchLatestVersion(fetchImpl = globalThis.fetch, options = {}) {
|
|
68
|
-
if (typeof fetchImpl !== 'function') return { ok: false, error: 'fetch-unavailable' }
|
|
69
|
-
const url = typeof options.url === 'string' && options.url !== '' ? options.url : VERSION_CHECK_URL
|
|
70
|
-
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : VERSION_CHECK_TIMEOUT_MS
|
|
71
|
-
let controller = null
|
|
72
|
-
let timer = null
|
|
73
|
-
try {
|
|
74
|
-
if (typeof AbortController === 'function') {
|
|
75
|
-
controller = new AbortController()
|
|
76
|
-
timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
77
|
-
}
|
|
78
|
-
const response = await fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller ? controller.signal : undefined })
|
|
79
|
-
if (!response || response.ok !== true) return { ok: false, error: 'http-' + String((response && response.status) || 0) }
|
|
80
|
-
const text = await response.text()
|
|
81
|
-
if (new TextEncoder().encode(text).length > MAX_VERSION_BYTES) return { ok: false, error: 'version-too-large' }
|
|
82
|
-
let parsed = null
|
|
83
|
-
try { parsed = JSON.parse(text.replace(/^\uFEFF/, '')) } catch (err) { return { ok: false, error: 'version-invalid-json' } }
|
|
84
|
-
const version = parsed !== null && typeof parsed === 'object' && typeof parsed.version === 'string' ? parsed.version.trim() : ''
|
|
85
|
-
if (parseVersion(version) === null) return { ok: false, error: 'version-invalid' }
|
|
86
|
-
return { ok: true, version, sourceUrl: url, fetchedAt: Date.now() }
|
|
87
|
-
} catch (err) {
|
|
88
|
-
return { ok: false, error: 'version-fetch-failed' }
|
|
89
|
-
} finally {
|
|
90
|
-
if (timer !== null) clearTimeout(timer)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function createVersionCheck(options = {}) {
|
|
95
|
-
const fetchImpl = options.fetchImpl === undefined ? globalThis.fetch : options.fetchImpl
|
|
96
|
-
const currentVersion = typeof options.currentVersion === 'string' ? options.currentVersion : ''
|
|
97
|
-
const ttlMs = Number.isFinite(options.ttlMs) && options.ttlMs > 0 ? options.ttlMs : VERSION_CHECK_TTL_MS
|
|
98
|
-
const minIntervalMs = Number.isFinite(options.minIntervalMs) && options.minIntervalMs >= 0 ? options.minIntervalMs : VERSION_CHECK_MIN_INTERVAL_MS
|
|
99
|
-
let entry = null
|
|
100
|
-
let failure = null
|
|
101
|
-
let lastAttemptAt = 0
|
|
102
|
-
function snapshot(cached) {
|
|
103
|
-
return {
|
|
104
|
-
current: currentVersion,
|
|
105
|
-
latest: entry === null ? null : entry.version,
|
|
106
|
-
status: entry === null ? 'unknown' : versionStatus(currentVersion, entry.version),
|
|
107
|
-
checkedAt: entry === null ? null : entry.fetchedAt,
|
|
108
|
-
sourceUrl: entry === null ? VERSION_CHECK_URL : entry.sourceUrl,
|
|
109
|
-
error: failure === null ? null : failure.error,
|
|
110
|
-
cached: cached === true,
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/** Read the cached verdict, or ask the registry again when it is stale or forced. */
|
|
114
|
-
async function read(force) {
|
|
115
|
-
const now = Date.now()
|
|
116
|
-
if (!force && entry !== null && now - entry.fetchedAt < ttlMs) return snapshot(true)
|
|
117
|
-
// A forced read still waits out the floor: force comes from the refresh button,
|
|
118
|
-
// and no human needs a second registry round trip inside the same half minute.
|
|
119
|
-
if (lastAttemptAt > 0 && now - lastAttemptAt < minIntervalMs) return snapshot(false)
|
|
120
|
-
lastAttemptAt = now
|
|
121
|
-
const result = await fetchLatestVersion(fetchImpl, options)
|
|
122
|
-
if (result.ok) {
|
|
123
|
-
entry = { version: result.version, fetchedAt: result.fetchedAt, sourceUrl: result.sourceUrl }
|
|
124
|
-
failure = null
|
|
125
|
-
} else {
|
|
126
|
-
failure = { error: result.error, failedAt: Date.now() }
|
|
127
|
-
}
|
|
128
|
-
return snapshot(false)
|
|
129
|
-
}
|
|
130
|
-
return { read, snapshot: () => snapshot(true), currentVersion }
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export { VERSION_CHECK_MIN_INTERVAL_MS, VERSION_CHECK_TTL_MS, VERSION_CHECK_URL }
|
|
1
|
+
// Version check for the dashboard header. The running version is known locally;
|
|
2
|
+
// this only asks the npm registry for the newest published one, at most once per
|
|
3
|
+
// TTL and only while a dashboard is open (the page asks for it, the host never
|
|
4
|
+
// polls on its own). A plain GET of a small JSON document, no body, no cookies.
|
|
5
|
+
const VERSION_CHECK_URL = 'https://registry.npmjs.org/dsh-all-usage/latest'
|
|
6
|
+
const VERSION_CHECK_TTL_MS = 6 * 60 * 60 * 1000
|
|
7
|
+
const VERSION_CHECK_TIMEOUT_MS = 8000
|
|
8
|
+
// The route is a plain local read (like /api/all-usage/status it needs no
|
|
9
|
+
// capability), so this floor is what keeps one page - or a hostile local page -
|
|
10
|
+
// from turning a burst of requests into a burst of registry requests.
|
|
11
|
+
const VERSION_CHECK_MIN_INTERVAL_MS = 30 * 1000
|
|
12
|
+
const MAX_VERSION_BYTES = 64 * 1024
|
|
13
|
+
|
|
14
|
+
function parseVersion(text) {
|
|
15
|
+
const value = String(text === undefined || text === null ? '' : text).trim().replace(/^v/, '')
|
|
16
|
+
const match = value.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/)
|
|
17
|
+
if (match === null) return null
|
|
18
|
+
return {
|
|
19
|
+
major: Number(match[1]),
|
|
20
|
+
minor: Number(match[2]),
|
|
21
|
+
patch: Number(match[3]),
|
|
22
|
+
prerelease: match[4] === undefined ? null : match[4].split('.'),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compare two dotted versions the way semver orders them: numeric parts first,
|
|
28
|
+
* then a prerelease ranks below its release and numeric identifiers rank below
|
|
29
|
+
* alphanumeric ones ('1.1.18-rc.1' < '1.1.18' < '1.1.19'). Returns -1, 0 or 1,
|
|
30
|
+
* and 0 for anything unparseable so a malformed tag never claims an update.
|
|
31
|
+
*/
|
|
32
|
+
export function compareVersions(left, right) {
|
|
33
|
+
const a = parseVersion(left)
|
|
34
|
+
const b = parseVersion(right)
|
|
35
|
+
if (a === null || b === null) return 0
|
|
36
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
37
|
+
if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1
|
|
38
|
+
}
|
|
39
|
+
if (a.prerelease === null && b.prerelease === null) return 0
|
|
40
|
+
if (a.prerelease === null) return 1
|
|
41
|
+
if (b.prerelease === null) return -1
|
|
42
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length)
|
|
43
|
+
for (let index = 0; index < length; index += 1) {
|
|
44
|
+
const leftPart = a.prerelease[index]
|
|
45
|
+
const rightPart = b.prerelease[index]
|
|
46
|
+
if (leftPart === undefined) return -1
|
|
47
|
+
if (rightPart === undefined) return 1
|
|
48
|
+
if (leftPart === rightPart) continue
|
|
49
|
+
const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null
|
|
50
|
+
const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null
|
|
51
|
+
if (leftNumber !== null && rightNumber !== null) return leftNumber < rightNumber ? -1 : 1
|
|
52
|
+
if (leftNumber !== null) return -1
|
|
53
|
+
if (rightNumber !== null) return 1
|
|
54
|
+
return leftPart < rightPart ? -1 : 1
|
|
55
|
+
}
|
|
56
|
+
return 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 'latest' when the running version is at least the published one, else 'outdated'. */
|
|
60
|
+
export function versionStatus(current, latest) {
|
|
61
|
+
if (typeof current !== 'string' || current === '') return 'unknown'
|
|
62
|
+
if (typeof latest !== 'string' || latest === '') return 'unknown'
|
|
63
|
+
if (parseVersion(current) === null || parseVersion(latest) === null) return 'unknown'
|
|
64
|
+
return compareVersions(current, latest) < 0 ? 'outdated' : 'latest'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fetchLatestVersion(fetchImpl = globalThis.fetch, options = {}) {
|
|
68
|
+
if (typeof fetchImpl !== 'function') return { ok: false, error: 'fetch-unavailable' }
|
|
69
|
+
const url = typeof options.url === 'string' && options.url !== '' ? options.url : VERSION_CHECK_URL
|
|
70
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : VERSION_CHECK_TIMEOUT_MS
|
|
71
|
+
let controller = null
|
|
72
|
+
let timer = null
|
|
73
|
+
try {
|
|
74
|
+
if (typeof AbortController === 'function') {
|
|
75
|
+
controller = new AbortController()
|
|
76
|
+
timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
77
|
+
}
|
|
78
|
+
const response = await fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller ? controller.signal : undefined })
|
|
79
|
+
if (!response || response.ok !== true) return { ok: false, error: 'http-' + String((response && response.status) || 0) }
|
|
80
|
+
const text = await response.text()
|
|
81
|
+
if (new TextEncoder().encode(text).length > MAX_VERSION_BYTES) return { ok: false, error: 'version-too-large' }
|
|
82
|
+
let parsed = null
|
|
83
|
+
try { parsed = JSON.parse(text.replace(/^\uFEFF/, '')) } catch (err) { return { ok: false, error: 'version-invalid-json' } }
|
|
84
|
+
const version = parsed !== null && typeof parsed === 'object' && typeof parsed.version === 'string' ? parsed.version.trim() : ''
|
|
85
|
+
if (parseVersion(version) === null) return { ok: false, error: 'version-invalid' }
|
|
86
|
+
return { ok: true, version, sourceUrl: url, fetchedAt: Date.now() }
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return { ok: false, error: 'version-fetch-failed' }
|
|
89
|
+
} finally {
|
|
90
|
+
if (timer !== null) clearTimeout(timer)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createVersionCheck(options = {}) {
|
|
95
|
+
const fetchImpl = options.fetchImpl === undefined ? globalThis.fetch : options.fetchImpl
|
|
96
|
+
const currentVersion = typeof options.currentVersion === 'string' ? options.currentVersion : ''
|
|
97
|
+
const ttlMs = Number.isFinite(options.ttlMs) && options.ttlMs > 0 ? options.ttlMs : VERSION_CHECK_TTL_MS
|
|
98
|
+
const minIntervalMs = Number.isFinite(options.minIntervalMs) && options.minIntervalMs >= 0 ? options.minIntervalMs : VERSION_CHECK_MIN_INTERVAL_MS
|
|
99
|
+
let entry = null
|
|
100
|
+
let failure = null
|
|
101
|
+
let lastAttemptAt = 0
|
|
102
|
+
function snapshot(cached) {
|
|
103
|
+
return {
|
|
104
|
+
current: currentVersion,
|
|
105
|
+
latest: entry === null ? null : entry.version,
|
|
106
|
+
status: entry === null ? 'unknown' : versionStatus(currentVersion, entry.version),
|
|
107
|
+
checkedAt: entry === null ? null : entry.fetchedAt,
|
|
108
|
+
sourceUrl: entry === null ? VERSION_CHECK_URL : entry.sourceUrl,
|
|
109
|
+
error: failure === null ? null : failure.error,
|
|
110
|
+
cached: cached === true,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Read the cached verdict, or ask the registry again when it is stale or forced. */
|
|
114
|
+
async function read(force) {
|
|
115
|
+
const now = Date.now()
|
|
116
|
+
if (!force && entry !== null && now - entry.fetchedAt < ttlMs) return snapshot(true)
|
|
117
|
+
// A forced read still waits out the floor: force comes from the refresh button,
|
|
118
|
+
// and no human needs a second registry round trip inside the same half minute.
|
|
119
|
+
if (lastAttemptAt > 0 && now - lastAttemptAt < minIntervalMs) return snapshot(false)
|
|
120
|
+
lastAttemptAt = now
|
|
121
|
+
const result = await fetchLatestVersion(fetchImpl, options)
|
|
122
|
+
if (result.ok) {
|
|
123
|
+
entry = { version: result.version, fetchedAt: result.fetchedAt, sourceUrl: result.sourceUrl }
|
|
124
|
+
failure = null
|
|
125
|
+
} else {
|
|
126
|
+
failure = { error: result.error, failedAt: Date.now() }
|
|
127
|
+
}
|
|
128
|
+
return snapshot(false)
|
|
129
|
+
}
|
|
130
|
+
return { read, snapshot: () => snapshot(true), currentVersion }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { VERSION_CHECK_MIN_INTERVAL_MS, VERSION_CHECK_TTL_MS, VERSION_CHECK_URL }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-all-usage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "DeepSeek Harness usage dashboard with model, provider, workspace, cache, balance, and CSV insights",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dsh": {
|
|
33
33
|
"compatibility": {
|
|
34
|
-
"runtime": ">=0.1.1-rc.1 <0.1.5-0 || >=0.1.5-rc.1 <0.1.6-0 || >=0.1.7-rc.2 <0.1.8-0 || >=0.2.0-rc.2 <0.2.1-0",
|
|
35
|
-
"verified": ["0.2.0-rc.2", "0.1.7-rc.2", "0.1.5-rc.2", "0.1.5-rc.1", "0.1.1-rc.2", "0.1.1-rc.1"]
|
|
34
|
+
"runtime": ">=0.1.1-rc.1 <0.1.5-0 || >=0.1.5-rc.1 <0.1.6-0 || >=0.1.7-rc.2 <0.1.8-0 || >=0.2.0-rc.2 <0.2.1-0 || 0.2.1-alpha.2",
|
|
35
|
+
"verified": ["0.2.1-alpha.2", "0.2.0-rc.2", "0.1.7-rc.2", "0.1.5-rc.2", "0.1.5-rc.1", "0.1.1-rc.2", "0.1.1-rc.1"]
|
|
36
36
|
},
|
|
37
37
|
"bundle": {
|
|
38
38
|
"patch": "./cordis.patch.yml"
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"lib/index.js",
|
|
51
51
|
"lib/plugin.js",
|
|
52
52
|
"lib/aggregation.js",
|
|
53
|
+
"lib/records-cache.js",
|
|
53
54
|
"lib/ledger.js",
|
|
54
55
|
"lib/session-sync.js",
|
|
55
56
|
"lib/pricing-runtime.js",
|
|
@@ -59,7 +60,10 @@
|
|
|
59
60
|
"lib/pricing.js",
|
|
60
61
|
"lib/usage-core.js",
|
|
61
62
|
"lib/version-check.js",
|
|
63
|
+
"lib/icons.js",
|
|
62
64
|
"CHANGELOG.md",
|
|
65
|
+
"README.md",
|
|
66
|
+
"README.en.md",
|
|
63
67
|
"cordis.patch.yml",
|
|
64
68
|
"screenshots.json",
|
|
65
69
|
"fixtures/usage-events.json",
|
package/screenshots.json
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
[
|
|
2
|
-
"assets/
|
|
3
|
-
"assets/
|
|
4
|
-
"assets/
|
|
2
|
+
"assets/screenshots/zh/01-overview.png",
|
|
3
|
+
"assets/screenshots/zh/02-heatmap.png",
|
|
4
|
+
"assets/screenshots/zh/03-model-usage.png",
|
|
5
|
+
"assets/screenshots/zh/04-request-logs.png",
|
|
6
|
+
"assets/screenshots/zh/05-cost-settings.png",
|
|
7
|
+
"assets/screenshots/en/01-overview.png",
|
|
8
|
+
"assets/screenshots/en/02-heatmap.png",
|
|
9
|
+
"assets/screenshots/en/03-model-usage.png",
|
|
10
|
+
"assets/screenshots/en/04-request-logs.png",
|
|
11
|
+
"assets/screenshots/en/05-cost-settings.png"
|
|
5
12
|
]
|
package/assets/screenshot-1.png
DELETED
|
Binary file
|
package/assets/screenshot-2.png
DELETED
|
Binary file
|
package/assets/screenshot-3.png
DELETED
|
Binary file
|