dsh-tokenlens 1.5.1 → 1.5.2
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 +10 -0
- package/PROTOCOL.md +8 -0
- package/README.md +1 -1
- package/client.js +78 -11
- package/index.js +1 -1
- package/lib/providers.mjs +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
**版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
|
|
6
6
|
|
|
7
|
+
## 1.5.2 — 2026-11-02
|
|
8
|
+
|
|
9
|
+
(当日已发布版本:v1.3.5 / v1.3.6 / v1.3.7 / v1.3.8 / v1.3.9 / v1.4.0 / v1.5.0 / v1.5.1 / v1.5.2)
|
|
10
|
+
|
|
11
|
+
- **fix(open-design): 窗口重置后「已消耗」不归零**——Design Plan 的窗口是**锚定**的(从订阅时间起算,每 5 小时 / 7 天翻篇),但弹窗一直按「最近 5 小时 / 最近 7 天」从本机日志切片,两者根本不是同一区间。于是 5 小时窗口重置后仍把上一轮的量算进来,`已消耗` 居高不下、`预计可消耗` 被放大(实测 96.4% → 7.3% 时估算从 766M 暴涨到 10.1B)
|
|
12
|
+
- **fix(open-design): 7 天窗口存在同一问题**——已一并修复,不是只修 5 小时
|
|
13
|
+
- fix(wire): 窗口对象新增 `startedAt`(上游 `windowStart`),客户端据此按窗口真实 `[startedAt, resetsAt)` 区间累加本机每小时 Token,只统计完整落在区间内的小时;窗口边界落在整点内时不计该小时,宁可少算也不按比例猜
|
|
14
|
+
- fix(ui): 无法确定窗口区间时才回退到原来的滚动桶(旧 Host 没下发 `startedAt`),因此旧版本 Host 仍能工作,不会显示空白
|
|
15
|
+
- 测试:314 → 317(5h 重置后只统计新窗口的回归测试、7d 中途重置的锚点切片、Host 下发 `startedAt`)
|
|
16
|
+
|
|
7
17
|
## 1.5.1 — 2026-11-02
|
|
8
18
|
|
|
9
19
|
(当日已发布版本:v1.3.5 / v1.3.6 / v1.3.7 / v1.3.8 / v1.3.9 / v1.4.0 / v1.5.0 / v1.5.1)
|
package/PROTOCOL.md
CHANGED
|
@@ -78,6 +78,7 @@ from `dsh-app://app/` and forwards non-asset paths to the Host).
|
|
|
78
78
|
"windows": { // canonical ids, display order
|
|
79
79
|
"rolling": { "id": "rolling", "percent": 8, "status": "ok",
|
|
80
80
|
"resetsAt": "2026-10-25T11:31:25.000Z", "remainingMs": 123456,
|
|
81
|
+
"startedAt": "2026-10-25T06:31:25.000Z", // v1.5.2, when reported
|
|
81
82
|
"used": 0.5, "cap": 2.5, "unit": "USD" } | null,
|
|
82
83
|
"weekly": { … } | null,
|
|
83
84
|
"monthly": { … } | null
|
|
@@ -200,6 +201,13 @@ Rules:
|
|
|
200
201
|
- The model picker emits `deepseek-official` / `deepseek-account`, never a bare
|
|
201
202
|
`deepseek`: the Host aliases `deepseek-official` onto the `deepseek` adapter.
|
|
202
203
|
A provider without an adapter answers `unsupported-provider`.
|
|
204
|
+
- **`startedAt` (v1.5.2) is the window instance's own start**, not "now minus the
|
|
205
|
+
window length". Design Plan windows are anchored to the subscription time
|
|
206
|
+
(`resetMode: "anchored_recurring"`), so a window can begin at 09:53 and reset at
|
|
207
|
+
14:53 regardless of the current clock. A client that wants to describe a window
|
|
208
|
+
in local tokens MUST sum the interval `[startedAt, resetsAt)`; using a trailing
|
|
209
|
+
"last N hours" slice mixes in the previous window and keeps reporting consumed
|
|
210
|
+
tokens after a reset. Omitted when the upstream reports no `windowStart`.
|
|
203
211
|
- **Plan capacity vs local usage (v3 split):** the snapshot above is the
|
|
204
212
|
server-side **plan capacity** (provider-native units). `used` / `cap` / `unit`
|
|
205
213
|
appear only for providers that report their allowance in a native unit
|
package/README.md
CHANGED
|
@@ -439,7 +439,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
|
|
|
439
439
|
|
|
440
440
|
## 验证
|
|
441
441
|
|
|
442
|
-
- `node --test`:**
|
|
442
|
+
- `node --test`:**317 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本、窗口锚点切片、插件集成与浏览器端渲染 / 交互。
|
|
443
443
|
- 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
|
|
444
444
|
- 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
|
|
445
445
|
|
package/client.js
CHANGED
|
@@ -993,6 +993,10 @@ window.__ModuleLoader__.load({
|
|
|
993
993
|
status: typeof raw.status === 'string' && raw.status !== '' ? raw.status : 'unknown',
|
|
994
994
|
resetsAt: typeof raw.resetsAt === 'string' ? raw.resetsAt : null,
|
|
995
995
|
remainingMs: Number.isFinite(raw.remainingMs) ? raw.remainingMs : null,
|
|
996
|
+
// v1.5.2: the window instance's own start (an anchored Design Plan window
|
|
997
|
+
// does not begin "N hours ago"), so the local token slice is aligned with
|
|
998
|
+
// the allowance the percentage describes.
|
|
999
|
+
startedAt: typeof raw.startedAt === 'string' ? raw.startedAt : null,
|
|
996
1000
|
// Provider-native capacity (e.g. Command Code USD): shown only when all
|
|
997
1001
|
// three fields are usable, so an unsized window never renders "$undefined".
|
|
998
1002
|
used: numberOrNull(raw.used),
|
|
@@ -1680,7 +1684,72 @@ window.__ModuleLoader__.load({
|
|
|
1680
1684
|
}
|
|
1681
1685
|
|
|
1682
1686
|
/**
|
|
1683
|
-
*
|
|
1687
|
+
* Local tokens spent inside one allowance window (v1.5.2).
|
|
1688
|
+
*
|
|
1689
|
+
* A Design Plan window is ANCHORED: it starts at the subscription anchor and
|
|
1690
|
+
* resets every N hours, so it is not "the last N hours" (the old behaviour,
|
|
1691
|
+
* which kept showing pre-reset consumption after a reset and inflated the
|
|
1692
|
+
* estimated quota ~13x). The window's own `startedAt` / `resetsAt` bound the
|
|
1693
|
+
* slice, folded from the per-day, per-provider hour histograms.
|
|
1694
|
+
*
|
|
1695
|
+
* @returns the token total, or `null` when the range cannot be resolved (an
|
|
1696
|
+
* old Host without `startedAt`, or no histogram), in which case the caller
|
|
1697
|
+
* falls back to the rolling bucket.
|
|
1698
|
+
*/
|
|
1699
|
+
function localTokensInWindow(stats, statsRow, providerId, window) {
|
|
1700
|
+
if (stats === null || statsRow === null || window === null) return null
|
|
1701
|
+
const startMs = typeof window.startedAt === 'string' ? Date.parse(window.startedAt) : NaN
|
|
1702
|
+
const endMs = typeof window.resetsAt === 'string' ? Date.parse(window.resetsAt) : NaN
|
|
1703
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) return null
|
|
1704
|
+
const daily = stats.daily
|
|
1705
|
+
if (!Array.isArray(daily) || daily.length === 0) return null
|
|
1706
|
+
let total = null
|
|
1707
|
+
for (const entry of daily) {
|
|
1708
|
+
// A day bucket only contributes whole hours through `hours`; resolve the
|
|
1709
|
+
// day's local midnight, then walk its 24 slots.
|
|
1710
|
+
const dayStartMs = localDayStartMs(entry.day)
|
|
1711
|
+
if (!Number.isFinite(dayStartMs)) continue
|
|
1712
|
+
const providerHours = entry.providers?.[providerId]?.hours ?? null
|
|
1713
|
+
const hours = Array.isArray(providerHours) ? providerHours : null
|
|
1714
|
+
if (hours === null) continue
|
|
1715
|
+
for (let slot = 0; slot < hours.length && slot < 24; slot += 1) {
|
|
1716
|
+
const hourStart = dayStartMs + slot * 3600000
|
|
1717
|
+
const hourEnd = hourStart + 3600000
|
|
1718
|
+
// An hour counts only when the whole slot lies inside the window; a
|
|
1719
|
+
// window boundary mid-hour would otherwise need a proportional guess.
|
|
1720
|
+
if (hourStart < startMs || hourEnd > endMs) continue
|
|
1721
|
+
total = (total ?? 0) + (Number.isFinite(hours[slot]) ? hours[slot] : 0)
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return total
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/** Local midnight (epoch ms) of a `YYYY-MM-DD` day key, or NaN. */
|
|
1728
|
+
function localDayStartMs(day) {
|
|
1729
|
+
if (typeof day !== 'string') return Number.NaN
|
|
1730
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day)
|
|
1731
|
+
if (match === null) return Number.NaN
|
|
1732
|
+
const ms = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])).getTime()
|
|
1733
|
+
return Number.isFinite(ms) ? ms : Number.NaN
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
/**
|
|
1737
|
+
* The local tokens consumed inside one allowance window.
|
|
1738
|
+
*
|
|
1739
|
+
* Prefers the window-anchored slice; falls back to the rolling provider
|
|
1740
|
+
* bucket only when the window bounds are unavailable (an older Host).
|
|
1741
|
+
*/
|
|
1742
|
+
function windowLocalUsed(stats, statsRow, providerId, window) {
|
|
1743
|
+
const anchored = localTokensInWindow(stats, statsRow, providerId, window)
|
|
1744
|
+
if (anchored !== null) return anchored
|
|
1745
|
+
const id = window?.id ?? ''
|
|
1746
|
+
if (id === 'rolling') return statsRow?.fiveHours?.total ?? 0
|
|
1747
|
+
if (id === 'weekly') return statsRow?.last7Days?.total ?? 0
|
|
1748
|
+
if (id === 'monthly') return statsRow?.last30Days?.total ?? 0
|
|
1749
|
+
return 0
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
/** The `{short, full}` labels of a provider (v0.10.0).
|
|
1684
1753
|
*
|
|
1685
1754
|
* Wire labels cached from the providers payload win, then the local catalog,
|
|
1686
1755
|
* then the snapshot / raw id. Both fields are always usable strings, so the
|
|
@@ -3829,7 +3898,7 @@ window.__ModuleLoader__.load({
|
|
|
3829
3898
|
}
|
|
3830
3899
|
|
|
3831
3900
|
/** One allowance-window row: label + percent, bar, reset countdown, status. */
|
|
3832
|
-
function buildWindowRow(id, window, state, now, t, statsRow = null) {
|
|
3901
|
+
function buildWindowRow(id, window, state, now, t, statsRow = null, stats = null, snapshotProviderId = '') {
|
|
3833
3902
|
const label = rowLabel(id, t)
|
|
3834
3903
|
const level = window === null ? 'unknown' : levelOf(window.percent, window.status)
|
|
3835
3904
|
const percentText = formatPercent(window?.percent ?? null, t)
|
|
@@ -3838,14 +3907,10 @@ window.__ModuleLoader__.load({
|
|
|
3838
3907
|
// v0.30.13: local-token consumption + estimated consumable quota. The used
|
|
3839
3908
|
// number comes from the selected provider's local stats buckets; the quota
|
|
3840
3909
|
// is only an estimate derived from the provider-reported percentage.
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
? statsRow?.last7Days?.total ?? 0
|
|
3846
|
-
: id === 'monthly'
|
|
3847
|
-
? statsRow?.last30Days?.total ?? 0
|
|
3848
|
-
: 0
|
|
3910
|
+
// v1.5.2: slice from the window's OWN bounds, so a reset window starts from
|
|
3911
|
+
// zero instead of replaying the previous window's consumption.
|
|
3912
|
+
const providerId = statsRow?.providerId ?? snapshotProviderId ?? ''
|
|
3913
|
+
const localUsed = windowLocalUsed(stats, statsRow, providerId, window)
|
|
3849
3914
|
const percent = window !== null && Number.isFinite(window.percent) ? window.percent : null
|
|
3850
3915
|
const quota = percent !== null && percent > 0 && localUsed > 0 ? localUsed / (percent / 100) : null
|
|
3851
3916
|
// v0.30.13 follow-up: the estimate is INLINE on the existing reset line
|
|
@@ -5372,7 +5437,9 @@ window.__ModuleLoader__.load({
|
|
|
5372
5437
|
const balanceRow = buildCapacityBalanceRow(snapshot.balance, t)
|
|
5373
5438
|
const designPlanRow = buildDesignPlanRow(snapshot.designPlan, t)
|
|
5374
5439
|
if (snapshot.windows !== null) {
|
|
5375
|
-
const rows = WINDOW_ORDER.map((id) =>
|
|
5440
|
+
const rows = WINDOW_ORDER.map((id) =>
|
|
5441
|
+
buildWindowRow(id, snapshot.windows[id], state, now, t, statsRow, stats, snapshot.provider.id),
|
|
5442
|
+
)
|
|
5376
5443
|
if (balanceRow !== null) rows.push(balanceRow)
|
|
5377
5444
|
if (designPlanRow !== null) rows.push(designPlanRow)
|
|
5378
5445
|
section.push(h('div', { key: 'rows', className: 'dsh-tokenlens-rows' }, rows))
|
package/index.js
CHANGED
|
@@ -83,7 +83,7 @@ export const inject = []
|
|
|
83
83
|
export const PLUGIN_NAME = 'dsh-tokenlens'
|
|
84
84
|
|
|
85
85
|
/** Plugin version; mirrors package.json (kept here so diagnostics need no fs read). */
|
|
86
|
-
export const PLUGIN_VERSION = '1.5.
|
|
86
|
+
export const PLUGIN_VERSION = '1.5.2'
|
|
87
87
|
|
|
88
88
|
/** Documented defaults; every field of the raw row config is optional. */
|
|
89
89
|
const DEFAULTS = {
|
package/lib/providers.mjs
CHANGED
|
@@ -344,12 +344,17 @@ export function makeWindow(id, input, now) {
|
|
|
344
344
|
const used = toNumber(input?.used)
|
|
345
345
|
const cap = toNumber(input?.cap)
|
|
346
346
|
const unit = typeof input?.unit === 'string' && input.unit !== '' ? input.unit : undefined
|
|
347
|
+
// v1.5.2: the upstream window's own start instant, when the provider reports
|
|
348
|
+
// one. The client slices its local token counters from here so the numbers it
|
|
349
|
+
// shows describe THIS window rather than an arbitrary trailing period.
|
|
350
|
+
const startMs = parseTimestamp(input?.windowStart)
|
|
347
351
|
return {
|
|
348
352
|
id,
|
|
349
353
|
percent,
|
|
350
354
|
status: typeof input?.status === 'string' && input.status !== '' ? input.status : 'unknown',
|
|
351
355
|
resetsAt: resetsMs === null ? null : new Date(resetsMs).toISOString(),
|
|
352
356
|
remainingMs: resetsMs === null ? null : Math.max(0, resetsMs - now),
|
|
357
|
+
...(startMs === null ? {} : { startedAt: new Date(startMs).toISOString() }),
|
|
353
358
|
...(used === null ? {} : { used }),
|
|
354
359
|
...(cap === null ? {} : { cap }),
|
|
355
360
|
...(used === null || cap === null || unit === undefined ? {} : { unit }),
|
|
@@ -843,6 +848,10 @@ export function parseOpenDesignUsage(payload, workspaceId, now = Date.now()) {
|
|
|
843
848
|
status: 'ok',
|
|
844
849
|
resetsAt:
|
|
845
850
|
typeof row.resetsAt === 'string' || typeof row.resetsAt === 'number' ? row.resetsAt : undefined,
|
|
851
|
+
// v1.5.2: the ACTUAL start of this window instance. Design Plan windows are
|
|
852
|
+
// anchored to the subscription time and reset every N hours, so the local
|
|
853
|
+
// token counters must slice from here — not from a naive "last N hours".
|
|
854
|
+
windowStart: typeof row.windowStart === 'string' ? row.windowStart : undefined,
|
|
846
855
|
}
|
|
847
856
|
}
|
|
848
857
|
const windows = windowSet(inputs, now)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tokenlens",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "dsh-TokenLens (词元透镜) — provider-aware plan capacity plus local token statistics (today/7d/30d/all-time, by harness/provider/model/workspace) for DSH.",
|
|
6
6
|
"license": "MIT",
|