dsh-tokenlens 1.5.1 → 1.5.3
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 +18 -0
- package/PROTOCOL.md +29 -11
- package/README.md +2 -2
- package/client.js +85 -17
- package/index.js +6 -9
- package/lib/providers.mjs +104 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
**版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
|
|
6
6
|
|
|
7
|
+
## 1.5.3 — 2026-11-03
|
|
8
|
+
|
|
9
|
+
- **feat(open-design): 初次配置只需一个 Control Key**——工作区 ID 改为**可选**。Control Key 本身就能确定它属于哪个工作区,插件改用 `GET /api/v1/workspaces/current` 自动识别(回退 `GET /api/v1/workspaces`,仅当可见工作区恰好一个时采纳)
|
|
10
|
+
- feat(open-design): **多工作区不猜**——`/workspaces/current` 没有 active 且有多个可见工作区时返回 `missing-key` 并提示手动填写「工作区 ID」,因为猜错会把别的团队的用量当成自己的
|
|
11
|
+
- feat(open-design): 显式配置的工作区 ID 优先于自动识别(管理页 / `providers.open-design.workspaceId` / `OPENDESIGN_WORKSPACE_ID`),多工作区账号据此指定目标
|
|
12
|
+
- feat(ui): 管理页「工作区 ID」文案改为可选,说明留空即自动识别、仅在多工作区时需要填
|
|
13
|
+
- 测试:317 → 318(仅凭 Key 自动识别工作区并用于后续所有计费读取、多工作区拒绝猜测)
|
|
14
|
+
|
|
15
|
+
## 1.5.2 — 2026-11-02
|
|
16
|
+
|
|
17
|
+
(当日已发布版本: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)
|
|
18
|
+
|
|
19
|
+
- **fix(open-design): 窗口重置后「已消耗」不归零**——Design Plan 的窗口是**锚定**的(从订阅时间起算,每 5 小时 / 7 天翻篇),但弹窗一直按「最近 5 小时 / 最近 7 天」从本机日志切片,两者根本不是同一区间。于是 5 小时窗口重置后仍把上一轮的量算进来,`已消耗` 居高不下、`预计可消耗` 被放大(实测 96.4% → 7.3% 时估算从 766M 暴涨到 10.1B)
|
|
20
|
+
- **fix(open-design): 7 天窗口存在同一问题**——已一并修复,不是只修 5 小时
|
|
21
|
+
- fix(wire): 窗口对象新增 `startedAt`(上游 `windowStart`),客户端据此按窗口真实 `[startedAt, resetsAt)` 区间累加本机每小时 Token,只统计完整落在区间内的小时;窗口边界落在整点内时不计该小时,宁可少算也不按比例猜
|
|
22
|
+
- fix(ui): 无法确定窗口区间时才回退到原来的滚动桶(旧 Host 没下发 `startedAt`),因此旧版本 Host 仍能工作,不会显示空白
|
|
23
|
+
- 测试:314 → 317(5h 重置后只统计新窗口的回归测试、7d 中途重置的锚点切片、Host 下发 `startedAt`)
|
|
24
|
+
|
|
7
25
|
## 1.5.1 — 2026-11-02
|
|
8
26
|
|
|
9
27
|
(当日已发布版本: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
|
|
@@ -112,17 +113,27 @@ A `windows` snapshot may also carry a non-null `balance` (Open Design reports
|
|
|
112
113
|
allowance windows and a wallet independently); the Client renders it as one extra
|
|
113
114
|
row at the end of the capacity card and appends the amount to the chip tooltip.
|
|
114
115
|
|
|
115
|
-
`open-design` needs
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
(`
|
|
125
|
-
|
|
116
|
+
`open-design` needs a **Control Key** (the `profiles.prod.controlKey` the desktop
|
|
117
|
+
app / `vela login` writes to `~/.amr/config.json`). The inference key from the
|
|
118
|
+
Cloud API Keys page answers HTTP 401 on the billing endpoints, so it cannot be
|
|
119
|
+
used here. A missing Control Key is a `missing-key` error.
|
|
120
|
+
|
|
121
|
+
**The workspace id is optional (v1.5.3).** A Control Key identifies the workspace
|
|
122
|
+
it belongs to, so the plugin discovers it:
|
|
123
|
+
|
|
124
|
+
1. `GET /api/v1/workspaces/current` — the key's active workspace
|
|
125
|
+
(`workspaceId`, else `activeWorkspaceId`); this is the normal path.
|
|
126
|
+
2. `GET /api/v1/workspaces` — fallback: accepted only when exactly one workspace
|
|
127
|
+
is visible. Several visible workspaces with none marked active is a
|
|
128
|
+
`missing-key` error rather than a guess, because picking the wrong one would
|
|
129
|
+
report another team's usage as this account's.
|
|
130
|
+
|
|
131
|
+
A configured id always wins over discovery (management page → 工作区 ID, persisted
|
|
132
|
+
in `providers.json`; or `OPENDESIGN_WORKSPACE_ID`), which is how a multi-workspace
|
|
133
|
+
account selects one. The Control Key is written through the same `apiKey` override
|
|
134
|
+
field the store already supported (write-only — the Host never returns it, only
|
|
135
|
+
`hasKey`/`keySource`). `OPENDESIGN_CONTROL_KEY` / `VELA_CONTROL_KEY` are still
|
|
136
|
+
honoured and the page override wins.
|
|
126
137
|
|
|
127
138
|
Reads, in independent layers (v1.4.0) — each may fail without voiding the others:
|
|
128
139
|
|
|
@@ -200,6 +211,13 @@ Rules:
|
|
|
200
211
|
- The model picker emits `deepseek-official` / `deepseek-account`, never a bare
|
|
201
212
|
`deepseek`: the Host aliases `deepseek-official` onto the `deepseek` adapter.
|
|
202
213
|
A provider without an adapter answers `unsupported-provider`.
|
|
214
|
+
- **`startedAt` (v1.5.2) is the window instance's own start**, not "now minus the
|
|
215
|
+
window length". Design Plan windows are anchored to the subscription time
|
|
216
|
+
(`resetMode: "anchored_recurring"`), so a window can begin at 09:53 and reset at
|
|
217
|
+
14:53 regardless of the current clock. A client that wants to describe a window
|
|
218
|
+
in local tokens MUST sum the interval `[startedAt, resetsAt)`; using a trailing
|
|
219
|
+
"last N hours" slice mixes in the previous window and keeps reporting consumed
|
|
220
|
+
tokens after a reset. Omitted when the upstream reports no `windowStart`.
|
|
203
221
|
- **Plan capacity vs local usage (v3 split):** the snapshot above is the
|
|
204
222
|
server-side **plan capacity** (provider-native units). `used` / `cap` / `unit`
|
|
205
223
|
appear only for providers that report their allowance in a native unit
|
package/README.md
CHANGED
|
@@ -326,7 +326,7 @@ Control Key 是 **Open Design 桌面端/CLI 登录时写入本机的会话凭据
|
|
|
326
326
|
|
|
327
327
|
- OpenCode 的额度定义见[官方文档](https://opencode.ai/docs/go/#usage-limits)(5h = 月度额度的 20%、周 = 50%、月 = 100%);接口只返回百分比,插件也只画百分比。
|
|
328
328
|
- Command Code 的 `/alpha/*` 是官方 CLI 自用的私有计费路由;免费 / 无方案账户返回空窗口(显示 `—`),插件不会用方案目录猜额度。
|
|
329
|
-
- Open Design 的窗口按账户实际返回的时长渲染:Go 同时有 5 小时和 7 天窗口,Plus / Pro / Max 只有 7 天窗口;钱包余额是独立于窗口的 USD 额度,用尽后继续扣钱包。查询要求工作区权限,鉴权失败会明确显示错误而不是 0
|
|
329
|
+
- Open Design 的窗口按账户实际返回的时长渲染:Go 同时有 5 小时和 7 天窗口,Plus / Pro / Max 只有 7 天窗口;钱包余额是独立于窗口的 USD 额度,用尽后继续扣钱包。查询要求工作区权限,鉴权失败会明确显示错误而不是 0。**只需填一个 Control Key**:工作区 ID 是可选项(v1.5.3),插件会用 Key 调 `/api/v1/workspaces/current` 自动识别你的工作区;只有账号有多个工作区、需要指定其中一个时才在管理页填「工作区 ID」。真正缺 Key 时弹窗会指出并给出「去配置」入口。窗口、余额、套餐档位是**三层独立读取**(v1.4.0):任一接口不可用时只少那一块数据,不会整套报错。
|
|
330
330
|
- **chip 的「月 N%」与「本期 Design Plan 累计」是插件本机算的估算值**(v1.5.0 / v1.5.1)。Open Design 有月度额度(Go 为每月 $120 = 4 × 每周 $30),但**没有「本账期已用」接口**——控制台的 `/billing/usage`、`/billing/coding-plan-usage` 都靠浏览器 Cookie 鉴权,Control Key 一律 401。于是插件累加它观测到的 **7 天窗口**(只记 7 天,因为 5 小时窗口测量的是同一笔消耗,相加会重复计数),再按官方「月度 = 4 × 周额度」的比例折算成百分比填进 chip 的「月」槽位,并按 `creditsPerUsd` 折算美元。弹窗会标注覆盖情况(「已覆盖本账期全部窗口」或「仅统计已观测的 N 个窗口」);覆盖不完整时百分比仍显示但**不会触发告警配色**,插件没运行期间错过的窗口无法计入。
|
|
331
331
|
- 未配置凭据的供应商 chip 静默隐藏;`zai`、`minimax`、`deepinfra` 属 best-effort 适配(未公开接口,失败时按可重试错误优雅降级)。
|
|
332
332
|
- 各供应商的元数据(`kind` / `label` / `shortLabel` / `baseURL` / `apiKeyRef` / `consoleUrl`)集中在 `lib/providers.mjs` 的 `PROVIDER_DEFAULTS` 与 `ADAPTERS` 中,新增适配器只需在该文件登记并补测试。
|
|
@@ -439,7 +439,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
|
|
|
439
439
|
|
|
440
440
|
## 验证
|
|
441
441
|
|
|
442
|
-
- `node --test`:**
|
|
442
|
+
- `node --test`:**318 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、Design Plan 周期账本、窗口锚点切片、工作区自动识别、插件集成与浏览器端渲染 / 交互。
|
|
443
443
|
- 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
|
|
444
444
|
- 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
|
|
445
445
|
|
package/client.js
CHANGED
|
@@ -329,9 +329,9 @@ window.__ModuleLoader__.load({
|
|
|
329
329
|
presetCollapse: '收起',
|
|
330
330
|
providerConsoleURL: '控制台地址(Console URL)',
|
|
331
331
|
providerConsoleURLPlaceholder: '留空则使用内置默认地址',
|
|
332
|
-
providerWorkspaceId: '工作区 ID(Workspace ID
|
|
333
|
-
providerWorkspaceIdHint: '
|
|
334
|
-
providerWorkspaceIdPlaceholder: '
|
|
332
|
+
providerWorkspaceId: '工作区 ID(Workspace ID,可选)',
|
|
333
|
+
providerWorkspaceIdHint: '可留空——插件会用 Control Key 自动识别你的工作区;仅在账号有多个工作区、需要指定其中一个时才填',
|
|
334
|
+
providerWorkspaceIdPlaceholder: '留空自动识别;或粘贴工作区 ID / 控制台地址',
|
|
335
335
|
providerWorkspaceIdTooLong: '工作区 ID 最多 120 个字符',
|
|
336
336
|
providerControlKey: 'Control Key',
|
|
337
337
|
providerControlKeyHint: '本机 Open Design 登录后写在 ~/.amr/config.json 的 profiles.prod.controlKey;不是 Cloud API Keys 页的推理 Key',
|
|
@@ -590,9 +590,10 @@ window.__ModuleLoader__.load({
|
|
|
590
590
|
presetExpand: 'Expand',
|
|
591
591
|
presetCollapse: 'Collapse',
|
|
592
592
|
providerConsoleURL: 'Console URL',
|
|
593
|
-
providerWorkspaceId: 'Workspace ID',
|
|
594
|
-
providerWorkspaceIdHint:
|
|
595
|
-
|
|
593
|
+
providerWorkspaceId: 'Workspace ID (optional)',
|
|
594
|
+
providerWorkspaceIdHint:
|
|
595
|
+
'Leave empty — the plugin finds your workspace from the Control Key. Fill it only when the account has several workspaces and you need a specific one.',
|
|
596
|
+
providerWorkspaceIdPlaceholder: 'Auto-detected; or paste a workspace ID / console URL',
|
|
596
597
|
providerWorkspaceIdTooLong: 'The workspace ID may be at most 120 characters',
|
|
597
598
|
providerControlKey: 'Control Key',
|
|
598
599
|
providerControlKeyHint: 'Written to ~/.amr/config.json as profiles.prod.controlKey after signing in to Open Design; not the inference key from the Cloud API Keys page',
|
|
@@ -993,6 +994,10 @@ window.__ModuleLoader__.load({
|
|
|
993
994
|
status: typeof raw.status === 'string' && raw.status !== '' ? raw.status : 'unknown',
|
|
994
995
|
resetsAt: typeof raw.resetsAt === 'string' ? raw.resetsAt : null,
|
|
995
996
|
remainingMs: Number.isFinite(raw.remainingMs) ? raw.remainingMs : null,
|
|
997
|
+
// v1.5.2: the window instance's own start (an anchored Design Plan window
|
|
998
|
+
// does not begin "N hours ago"), so the local token slice is aligned with
|
|
999
|
+
// the allowance the percentage describes.
|
|
1000
|
+
startedAt: typeof raw.startedAt === 'string' ? raw.startedAt : null,
|
|
996
1001
|
// Provider-native capacity (e.g. Command Code USD): shown only when all
|
|
997
1002
|
// three fields are usable, so an unsized window never renders "$undefined".
|
|
998
1003
|
used: numberOrNull(raw.used),
|
|
@@ -1680,7 +1685,72 @@ window.__ModuleLoader__.load({
|
|
|
1680
1685
|
}
|
|
1681
1686
|
|
|
1682
1687
|
/**
|
|
1683
|
-
*
|
|
1688
|
+
* Local tokens spent inside one allowance window (v1.5.2).
|
|
1689
|
+
*
|
|
1690
|
+
* A Design Plan window is ANCHORED: it starts at the subscription anchor and
|
|
1691
|
+
* resets every N hours, so it is not "the last N hours" (the old behaviour,
|
|
1692
|
+
* which kept showing pre-reset consumption after a reset and inflated the
|
|
1693
|
+
* estimated quota ~13x). The window's own `startedAt` / `resetsAt` bound the
|
|
1694
|
+
* slice, folded from the per-day, per-provider hour histograms.
|
|
1695
|
+
*
|
|
1696
|
+
* @returns the token total, or `null` when the range cannot be resolved (an
|
|
1697
|
+
* old Host without `startedAt`, or no histogram), in which case the caller
|
|
1698
|
+
* falls back to the rolling bucket.
|
|
1699
|
+
*/
|
|
1700
|
+
function localTokensInWindow(stats, statsRow, providerId, window) {
|
|
1701
|
+
if (stats === null || statsRow === null || window === null) return null
|
|
1702
|
+
const startMs = typeof window.startedAt === 'string' ? Date.parse(window.startedAt) : NaN
|
|
1703
|
+
const endMs = typeof window.resetsAt === 'string' ? Date.parse(window.resetsAt) : NaN
|
|
1704
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) return null
|
|
1705
|
+
const daily = stats.daily
|
|
1706
|
+
if (!Array.isArray(daily) || daily.length === 0) return null
|
|
1707
|
+
let total = null
|
|
1708
|
+
for (const entry of daily) {
|
|
1709
|
+
// A day bucket only contributes whole hours through `hours`; resolve the
|
|
1710
|
+
// day's local midnight, then walk its 24 slots.
|
|
1711
|
+
const dayStartMs = localDayStartMs(entry.day)
|
|
1712
|
+
if (!Number.isFinite(dayStartMs)) continue
|
|
1713
|
+
const providerHours = entry.providers?.[providerId]?.hours ?? null
|
|
1714
|
+
const hours = Array.isArray(providerHours) ? providerHours : null
|
|
1715
|
+
if (hours === null) continue
|
|
1716
|
+
for (let slot = 0; slot < hours.length && slot < 24; slot += 1) {
|
|
1717
|
+
const hourStart = dayStartMs + slot * 3600000
|
|
1718
|
+
const hourEnd = hourStart + 3600000
|
|
1719
|
+
// An hour counts only when the whole slot lies inside the window; a
|
|
1720
|
+
// window boundary mid-hour would otherwise need a proportional guess.
|
|
1721
|
+
if (hourStart < startMs || hourEnd > endMs) continue
|
|
1722
|
+
total = (total ?? 0) + (Number.isFinite(hours[slot]) ? hours[slot] : 0)
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
return total
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/** Local midnight (epoch ms) of a `YYYY-MM-DD` day key, or NaN. */
|
|
1729
|
+
function localDayStartMs(day) {
|
|
1730
|
+
if (typeof day !== 'string') return Number.NaN
|
|
1731
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day)
|
|
1732
|
+
if (match === null) return Number.NaN
|
|
1733
|
+
const ms = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])).getTime()
|
|
1734
|
+
return Number.isFinite(ms) ? ms : Number.NaN
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* The local tokens consumed inside one allowance window.
|
|
1739
|
+
*
|
|
1740
|
+
* Prefers the window-anchored slice; falls back to the rolling provider
|
|
1741
|
+
* bucket only when the window bounds are unavailable (an older Host).
|
|
1742
|
+
*/
|
|
1743
|
+
function windowLocalUsed(stats, statsRow, providerId, window) {
|
|
1744
|
+
const anchored = localTokensInWindow(stats, statsRow, providerId, window)
|
|
1745
|
+
if (anchored !== null) return anchored
|
|
1746
|
+
const id = window?.id ?? ''
|
|
1747
|
+
if (id === 'rolling') return statsRow?.fiveHours?.total ?? 0
|
|
1748
|
+
if (id === 'weekly') return statsRow?.last7Days?.total ?? 0
|
|
1749
|
+
if (id === 'monthly') return statsRow?.last30Days?.total ?? 0
|
|
1750
|
+
return 0
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/** The `{short, full}` labels of a provider (v0.10.0).
|
|
1684
1754
|
*
|
|
1685
1755
|
* Wire labels cached from the providers payload win, then the local catalog,
|
|
1686
1756
|
* then the snapshot / raw id. Both fields are always usable strings, so the
|
|
@@ -3829,7 +3899,7 @@ window.__ModuleLoader__.load({
|
|
|
3829
3899
|
}
|
|
3830
3900
|
|
|
3831
3901
|
/** One allowance-window row: label + percent, bar, reset countdown, status. */
|
|
3832
|
-
function buildWindowRow(id, window, state, now, t, statsRow = null) {
|
|
3902
|
+
function buildWindowRow(id, window, state, now, t, statsRow = null, stats = null, snapshotProviderId = '') {
|
|
3833
3903
|
const label = rowLabel(id, t)
|
|
3834
3904
|
const level = window === null ? 'unknown' : levelOf(window.percent, window.status)
|
|
3835
3905
|
const percentText = formatPercent(window?.percent ?? null, t)
|
|
@@ -3838,14 +3908,10 @@ window.__ModuleLoader__.load({
|
|
|
3838
3908
|
// v0.30.13: local-token consumption + estimated consumable quota. The used
|
|
3839
3909
|
// number comes from the selected provider's local stats buckets; the quota
|
|
3840
3910
|
// 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
|
|
3911
|
+
// v1.5.2: slice from the window's OWN bounds, so a reset window starts from
|
|
3912
|
+
// zero instead of replaying the previous window's consumption.
|
|
3913
|
+
const providerId = statsRow?.providerId ?? snapshotProviderId ?? ''
|
|
3914
|
+
const localUsed = windowLocalUsed(stats, statsRow, providerId, window)
|
|
3849
3915
|
const percent = window !== null && Number.isFinite(window.percent) ? window.percent : null
|
|
3850
3916
|
const quota = percent !== null && percent > 0 && localUsed > 0 ? localUsed / (percent / 100) : null
|
|
3851
3917
|
// v0.30.13 follow-up: the estimate is INLINE on the existing reset line
|
|
@@ -5372,7 +5438,9 @@ window.__ModuleLoader__.load({
|
|
|
5372
5438
|
const balanceRow = buildCapacityBalanceRow(snapshot.balance, t)
|
|
5373
5439
|
const designPlanRow = buildDesignPlanRow(snapshot.designPlan, t)
|
|
5374
5440
|
if (snapshot.windows !== null) {
|
|
5375
|
-
const rows = WINDOW_ORDER.map((id) =>
|
|
5441
|
+
const rows = WINDOW_ORDER.map((id) =>
|
|
5442
|
+
buildWindowRow(id, snapshot.windows[id], state, now, t, statsRow, stats, snapshot.provider.id),
|
|
5443
|
+
)
|
|
5376
5444
|
if (balanceRow !== null) rows.push(balanceRow)
|
|
5377
5445
|
if (designPlanRow !== null) rows.push(designPlanRow)
|
|
5378
5446
|
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.3'
|
|
87
87
|
|
|
88
88
|
/** Documented defaults; every field of the raw row config is optional. */
|
|
89
89
|
const DEFAULTS = {
|
|
@@ -1097,6 +1097,10 @@ export function apply(ctx, rawConfig) {
|
|
|
1097
1097
|
provider = { ...provider, accessKeyId: accessValue, secretAccessKey: secretValue }
|
|
1098
1098
|
keyInfo = { ref: accessRef, source: access.source, detail: access.detail }
|
|
1099
1099
|
} else if (adapterId === 'open-design') {
|
|
1100
|
+
// v1.5.3: the workspace id is OPTIONAL. A Control Key identifies its own
|
|
1101
|
+
// active workspace, so the adapter discovers it when none is configured;
|
|
1102
|
+
// an explicit value (management page / config / env) always wins for
|
|
1103
|
+
// accounts with several workspaces.
|
|
1100
1104
|
const workspaceId =
|
|
1101
1105
|
String(provider.workspaceId ?? '').trim() || String(process.env.OPENDESIGN_WORKSPACE_ID ?? '').trim()
|
|
1102
1106
|
const refs = []
|
|
@@ -1125,14 +1129,6 @@ export function apply(ctx, rawConfig) {
|
|
|
1125
1129
|
break
|
|
1126
1130
|
}
|
|
1127
1131
|
}
|
|
1128
|
-
if (workspaceId === '') {
|
|
1129
|
-
throw new UsageError(
|
|
1130
|
-
'missing-key',
|
|
1131
|
-
'OpenDesign quota needs a workspace id: set it on the plugin management page (open-design → 工作区 ID), ' +
|
|
1132
|
-
'in providers.open-design.workspaceId, or in OPENDESIGN_WORKSPACE_ID',
|
|
1133
|
-
{ retryable: false },
|
|
1134
|
-
)
|
|
1135
|
-
}
|
|
1136
1132
|
if (controlKey === '') {
|
|
1137
1133
|
throw new UsageError(
|
|
1138
1134
|
'missing-key',
|
|
@@ -1140,6 +1136,7 @@ export function apply(ctx, rawConfig) {
|
|
|
1140
1136
|
{ retryable: false },
|
|
1141
1137
|
)
|
|
1142
1138
|
}
|
|
1139
|
+
// An empty workspaceId is fine: the adapter resolves it from the key.
|
|
1143
1140
|
provider = { ...provider, workspaceId, controlKey }
|
|
1144
1141
|
keyInfo = { ref: controlRef, source: controlSource, detail: controlDetail }
|
|
1145
1142
|
} else if (typeof provider.apiKeyRef === 'string' && provider.apiKeyRef.trim() !== '') {
|
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 }),
|
|
@@ -778,6 +783,82 @@ const OPEN_DESIGN_WINDOW_SECONDS = {
|
|
|
778
783
|
2592000: 'monthly',
|
|
779
784
|
}
|
|
780
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Find the workspace a Control Key belongs to.
|
|
788
|
+
*
|
|
789
|
+
* `GET /api/v1/workspaces/current` answers with the key's active workspace
|
|
790
|
+
* (`workspaceId` / `activeWorkspaceId`), and `GET /api/v1/workspaces` lists every
|
|
791
|
+
* workspace the key can see. A Control Key is workspace-scoped in practice (this
|
|
792
|
+
* plugin talks to one billing account at a time), so the active workspace is the
|
|
793
|
+
* right default; the list is only a fallback for an account without an "active"
|
|
794
|
+
* one, and a single-entry list is unambiguous.
|
|
795
|
+
*
|
|
796
|
+
* @returns `{ workspaceId, name, source, error, retryable }`; `workspaceId` is ''
|
|
797
|
+
* when the workspace could not be determined (the caller turns that into a
|
|
798
|
+
* `missing-key` error carrying `error`).
|
|
799
|
+
*/
|
|
800
|
+
export async function discoverOpenDesignWorkspace(baseURL, controlKey, httpJson) {
|
|
801
|
+
const headers = { accept: 'application/json', authorization: `Bearer ${controlKey}` }
|
|
802
|
+
const failed = (error, retryable = false) => ({ workspaceId: '', name: '', source: '', error, retryable })
|
|
803
|
+
try {
|
|
804
|
+
const current = await httpJson(joinUrl(baseURL, '/api/v1/workspaces/current'), { method: 'GET', headers })
|
|
805
|
+
const parsed = parseOpenDesignWorkspace(current)
|
|
806
|
+
if (parsed.workspaceId !== '') {
|
|
807
|
+
return { workspaceId: parsed.workspaceId, name: parsed.name, source: 'current', error: '', retryable: false }
|
|
808
|
+
}
|
|
809
|
+
} catch (error) {
|
|
810
|
+
// A 404/401 here is not fatal: the list endpoint may still answer.
|
|
811
|
+
if (error instanceof UsageError && error.code === 'network') {
|
|
812
|
+
return failed(String(error.message ?? 'network error'), true)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const list = await httpJson(joinUrl(baseURL, '/api/v1/workspaces'), { method: 'GET', headers })
|
|
817
|
+
const items = Array.isArray(list?.items) ? list.items : []
|
|
818
|
+
const usable = items
|
|
819
|
+
.filter((item) => item !== null && typeof item === 'object' && typeof item.workspaceId === 'string')
|
|
820
|
+
.filter((item) => item.workspaceId.trim() !== '')
|
|
821
|
+
if (usable.length === 1) {
|
|
822
|
+
const only = usable[0]
|
|
823
|
+
return {
|
|
824
|
+
workspaceId: only.workspaceId.trim(),
|
|
825
|
+
name: typeof only.workspaceName === 'string' ? only.workspaceName : '',
|
|
826
|
+
source: 'list',
|
|
827
|
+
error: '',
|
|
828
|
+
retryable: false,
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (usable.length > 1) {
|
|
832
|
+
// Never guess between several workspaces: picking the wrong one would
|
|
833
|
+
// report another team's usage as this account's.
|
|
834
|
+
return failed(`${usable.length} workspaces are visible and none is marked active`, false)
|
|
835
|
+
}
|
|
836
|
+
} catch (error) {
|
|
837
|
+
return failed(String(error?.message ?? 'the workspace list request failed'), true)
|
|
838
|
+
}
|
|
839
|
+
return failed('no workspace is available to this Control Key', false)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Read a workspace id from a `/workspaces/current` or `/workspaces` document.
|
|
844
|
+
*
|
|
845
|
+
* @returns `{ workspaceId, name }` where `workspaceId` is '' when absent.
|
|
846
|
+
*/
|
|
847
|
+
export function parseOpenDesignWorkspace(payload) {
|
|
848
|
+
const document = payload !== null && typeof payload === 'object' && !Array.isArray(payload) ? payload : null
|
|
849
|
+
if (document === null) return { workspaceId: '', name: '' }
|
|
850
|
+
for (const key of ['workspaceId', 'activeWorkspaceId']) {
|
|
851
|
+
const value = document[key]
|
|
852
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
853
|
+
return {
|
|
854
|
+
workspaceId: value.trim(),
|
|
855
|
+
name: typeof document.workspaceName === 'string' ? document.workspaceName.trim() : '',
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return { workspaceId: '', name: '' }
|
|
860
|
+
}
|
|
861
|
+
|
|
781
862
|
/** A non-negative integer credit amount serialized as a decimal string. */
|
|
782
863
|
function openDesignCredits(value) {
|
|
783
864
|
return typeof value === 'string' && /^\d{1,40}$/.test(value) ? BigInt(value) : null
|
|
@@ -843,6 +924,10 @@ export function parseOpenDesignUsage(payload, workspaceId, now = Date.now()) {
|
|
|
843
924
|
status: 'ok',
|
|
844
925
|
resetsAt:
|
|
845
926
|
typeof row.resetsAt === 'string' || typeof row.resetsAt === 'number' ? row.resetsAt : undefined,
|
|
927
|
+
// v1.5.2: the ACTUAL start of this window instance. Design Plan windows are
|
|
928
|
+
// anchored to the subscription time and reset every N hours, so the local
|
|
929
|
+
// token counters must slice from here — not from a naive "last N hours".
|
|
930
|
+
windowStart: typeof row.windowStart === 'string' ? row.windowStart : undefined,
|
|
846
931
|
}
|
|
847
932
|
}
|
|
848
933
|
const windows = windowSet(inputs, now)
|
|
@@ -957,20 +1042,31 @@ export const ADAPTERS = {
|
|
|
957
1042
|
kind: 'windows',
|
|
958
1043
|
async read({ providerId = 'open-design', provider, httpJson, now }) {
|
|
959
1044
|
const config = providerConfig(providerId, provider)
|
|
960
|
-
const workspaceId = String(config.workspaceId ?? '').trim()
|
|
961
|
-
if (workspaceId === '') {
|
|
962
|
-
throw new UsageError(
|
|
963
|
-
'missing-key',
|
|
964
|
-
'OpenDesign quota needs a workspace id (providers.open-design.workspaceId or OPENDESIGN_WORKSPACE_ID)',
|
|
965
|
-
{ retryable: false },
|
|
966
|
-
)
|
|
967
|
-
}
|
|
968
1045
|
const controlKey = typeof provider.controlKey === 'string' ? provider.controlKey : ''
|
|
969
1046
|
if (controlKey === '') {
|
|
970
1047
|
throw new UsageError('missing-key', 'OpenDesign quota needs a Control Key (OPENDESIGN_CONTROL_KEY or VELA_CONTROL_KEY)', {
|
|
971
1048
|
retryable: false,
|
|
972
1049
|
})
|
|
973
1050
|
}
|
|
1051
|
+
// v1.5.3: the workspace id is DERIVABLE from a Control Key, so a configured
|
|
1052
|
+
// id is only a preference. `GET /api/v1/workspaces/current` answers with the
|
|
1053
|
+
// key's active workspace, which removes the second setup field entirely.
|
|
1054
|
+
let workspaceId = String(config.workspaceId ?? '').trim()
|
|
1055
|
+
if (workspaceId === '') {
|
|
1056
|
+
const adopted = await discoverOpenDesignWorkspace(config.baseURL, controlKey, httpJson)
|
|
1057
|
+
workspaceId = adopted.workspaceId
|
|
1058
|
+
if (workspaceId === '') {
|
|
1059
|
+
throw new UsageError(
|
|
1060
|
+
'missing-key',
|
|
1061
|
+
'OpenDesign could not determine the workspace for this Control Key: ' +
|
|
1062
|
+
(adopted.error === ''
|
|
1063
|
+
? 'the key is not valid for any workspace'
|
|
1064
|
+
: `the discovery request failed (${adopted.error})`) +
|
|
1065
|
+
'. Set 工作区 ID on the plugin management page only if your account has several workspaces.',
|
|
1066
|
+
{ retryable: adopted.retryable },
|
|
1067
|
+
)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
974
1070
|
const workspaceBase = joinUrl(config.baseURL, `/api/v1/workspaces/${encodeURIComponent(workspaceId)}`)
|
|
975
1071
|
const headers = {
|
|
976
1072
|
accept: 'application/json',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tokenlens",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
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",
|