dsh-tokenlens 1.2.0 → 1.2.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 +14 -0
- package/PROTOCOL.md +5 -6
- package/README.en.md +6 -4
- package/README.md +8 -6
- package/client.js +110 -37
- package/index.js +19 -6
- package/lib/provider-store.mjs +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Semantic Versioning](https://semver.org/):修复 = PATCH,新功能 = MINOR,不兼容变更 = MAJOR。发布时同步更新 `package.json` 的 `version`、`index.js` 的 `PLUGIN_VERSION` 与本文件(`node scripts/bump-version.mjs <version> "<note>"`);已发布的条目保持原样。
|
|
4
4
|
|
|
5
|
+
## 1.2.2 — 2026-11-01
|
|
6
|
+
|
|
7
|
+
- fix(workspaces): 修复「工作区悬浮卡片不显示 Token 明细」——Host 在裁剪本地路径时把 `path` 字段误删成空串,客户端只剩标题精确匹配;默认工作区等「界面标题 ≠ 统计标签」的场景因此匹配不到统计(影响 v1.2.1 及更早版本)
|
|
8
|
+
- fix(workspaces): 悬浮卡片匹配增强——标题匹配改为大小写不敏感;Host 只下发文件夹名 basename(不含完整路径);旧 Host 未下发 `path` 时用文件夹名与标签兜底
|
|
9
|
+
- docs: README 增加「用 `link:` 安装的插件不要从插件市场重复安装/升级」的排错说明(Windows 下 pnpm 会遇到 EPERM)
|
|
10
|
+
- 测试:256 个自动化用例(新增工作区 wire basename 回归;客户端匹配逻辑断言)
|
|
11
|
+
|
|
12
|
+
## 1.2.1 — 2026-10-31
|
|
13
|
+
|
|
14
|
+
- feat(settings): 图表模式「明细」环形图的窗口选择(今日 / 最近7天 / 最近X天(或最近30天)/ 总计)持久化到插件设置:点击 pill 立即保存,重开弹窗、切换供应商或重启 DSH 后仍保留上次选择;当前供应商没有该窗口时自动回落到总计
|
|
15
|
+
- feat(wire): `providers` 载荷与 `settingsUpdate` 增加 `settings.detailsWindow`(`today|last7Days|second|total`),旧 Host 会忽略未知字段、旧 client 读取时回落 `total`,双向兼容
|
|
16
|
+
- 修复:请求进行中连续点击窗口时只保留最后一个选择(单定时器 200ms 重试);「恢复默认设置」同时把窗口重置为总计
|
|
17
|
+
- 测试:255 个自动化用例(新增 5 个持久化回归,更新 8 个既有设置/窗口用例)
|
|
18
|
+
|
|
5
19
|
## 1.2.0 — 2026-10-31
|
|
6
20
|
|
|
7
21
|
- feat(chart): 图表模式点击某一天的柱子可下钻查看**当天 24 小时**的 Token 消耗棒状图(横轴 00:00–23:00,悬浮显示小时与占比,回车/空格同样可操作,返回或再次点击回到日趋势)
|
package/PROTOCOL.md
CHANGED
|
@@ -135,8 +135,9 @@ counts toward the `opencode-go` provider selected in DSH.
|
|
|
135
135
|
v3 additionally carries the rolling **最近 7 天** window (`last7Days`), the
|
|
136
136
|
provider effective-date fallback (`effectiveDay`, `effectiveDays`,
|
|
137
137
|
`sinceEffective`), per-workspace totals (`workspaces[]`: `workspaceId`, `label`,
|
|
138
|
-
`total`, `messages`, `today`/`last7Days`/`last30Days
|
|
139
|
-
|
|
138
|
+
`total`, `messages`, `today`/`last7Days`/`last30Days`, and `path` — only the
|
|
139
|
+
folder basename, used by the client to match a sidebar hover card; the full
|
|
140
|
+
`cwd` is Host-only and never sent on the wire), the rolling 5-hour local window
|
|
140
141
|
(`fiveHours`, DSH-only because external harnesses are day-granular), and the
|
|
141
142
|
bounded `daily[]` series used by the chart view. Absolute local paths are
|
|
142
143
|
stripped from the wire; free-form error messages have path substrings replaced
|
|
@@ -318,7 +319,7 @@ needs no provider catalog of its own.
|
|
|
318
319
|
|
|
319
320
|
The dialog 「供应商管理」 page talks to three additional actions:
|
|
320
321
|
|
|
321
|
-
- `{action:"providers"}` → `{ok:true, providers:[{providerId,label,shortLabel,kind,enabled,hasKey,keySource,apiKeyRef,baseURL,consoleUrl,builtin,dshManaged,dshProviderId}], dshProviders:[{providerId,label,supported,usageProviderId}], settings:{labelStyle
|
|
322
|
+
- `{action:"providers"}` → `{ok:true, providers:[{providerId,label,shortLabel,kind,enabled,hasKey,keySource,apiKeyRef,baseURL,consoleUrl,builtin,dshManaged,dshProviderId}], dshProviders:[{providerId,label,supported,usageProviderId}], settings:{labelStyle,defaultView,defaultScope,detailsExpanded,detailsWindow}, storeFile, storeFileDisplay}`. `storeFile` is the real path (local tooling); `storeFileDisplay` replaces the user's home directory with `~` and is what the management page renders.
|
|
322
323
|
- `{action:"providerUpdate", providerId, update:{enabled?,apiKey?,apiKeyRef?,baseURL?,consoleUrl?,label?,shortLabel?}}` → same list payload;
|
|
323
324
|
empty strings clear a stored value (falling back to the profile/default). `label` (全名, ≤40 chars) and
|
|
324
325
|
`shortLabel` (短标签, ≤12 chars) let the management page rename a provider. The shipped management page no
|
|
@@ -326,9 +327,7 @@ The dialog 「供应商管理」 page talks to three additional actions:
|
|
|
326
327
|
saves enabled/consoleUrl/label/shortLabel. Overrides are persisted atomically to
|
|
327
328
|
`$DSH_HOME/dsh-tokenlens/providers.json` (plugin-owned, not the profile patch); the same file also
|
|
328
329
|
- `{action:"providerTest", providerId}` → the normal snapshot envelope (`{ok,snapshot}` or `{ok:false,error}`), forcing one upstream read.
|
|
329
|
-
- `{action:"settingsUpdate", settings:{labelStyle
|
|
330
|
-
persisted in the store (`settings.labelStyle`) and controls whether the composer chip shows each
|
|
331
|
-
provider's `shortLabel` or full `label`. Invalid values answer `unsupported-provider`.
|
|
330
|
+
- `{action:"settingsUpdate", settings:{labelStyle?:"short"|"full", defaultView?:"chart"|"data", defaultScope?:"provider"|"all", detailsExpanded?:"auto"|"expanded"|"collapsed", detailsWindow?:"today"|"last7Days"|"second"|"total"}}` → the same providers payload; every field is optional and persisted in the store (`settings.*`). `labelStyle` controls whether the composer chip shows each provider's `shortLabel` or full `label`; `defaultView` / `defaultScope` / `detailsExpanded` are the usage-dialog defaults; `detailsWindow` (v1.2.1) is the remembered 明细 donut window (falls back visually to `total` when that window is unavailable on the current provider/host). Invalid values answer `unsupported-provider`.
|
|
332
331
|
|
|
333
332
|
`dshProviders` lists every route registered in DSH model settings (in order) with a `supported` flag (whether a usage adapter exists); the shipped management page renders those routes as the primary list and keeps unconfigured preset adapters in a collapsed secondary section. `dshManaged`/`dshProviderId` tell the management page that the route already exists in DSH model settings
|
|
334
333
|
(opencode-go / commandcode / deepseek), so the page can inherit the key and base URL instead of asking for them
|
package/README.en.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
> One glance at your usage: plan capacity for the selected provider plus local token statistics across harnesses, right below the DSH composer.
|
|
11
11
|
|
|
12
|
-
**dsh-TokenLens** is a usage plugin for DSH (DeepSeek Harness), version v1.2.
|
|
12
|
+
**dsh-TokenLens** is a usage plugin for DSH (DeepSeek Harness), version v1.2.2. It keeps two kinds of numbers separate:
|
|
13
13
|
|
|
14
14
|
- **Plan capacity** — the provider's authoritative server-side values (percentage windows, used / cap, balance or remaining credit);
|
|
15
15
|
- **Local token stats** — tokens consumed on this machine across every connected harness, for the selected provider or for all providers combined.
|
|
@@ -21,7 +21,7 @@ Everything is computed locally: zero runtime dependencies, no external database,
|
|
|
21
21
|
- **Usage chip below the composer** — follows the selected model provider and shows its plan capacity; providers without an adapter or credentials stay hidden.
|
|
22
22
|
- **Details dialog** — plan capacity windows with reset times (plus an optional "consumed · estimated consumable" hint) and local token stats for today / last 7 days / last 30 days / all-time, scoped to the current provider or all harnesses.
|
|
23
23
|
- **Data and chart modes** — tables, a daily trend chart and ring charts with breakdowns by harness, provider, top model and unmapped providers. In chart mode, clicking a day's bar drills into that day's 24 hourly bars; the 明细 rings switch between today / last 7 days / last X days (or last 30 days) / all time.
|
|
24
|
-
- **Provider management page** — enabled / built-in / all filters, enable switches, credential status, console links, label editing, connection tests, subscription badges and plugin settings.
|
|
24
|
+
- **Provider management page** — enabled / built-in / all filters, enable switches, credential status, console links, label editing, connection tests, subscription badges and plugin settings (label style, default view/scope, details expansion, default 明细 donut window).
|
|
25
25
|
- **Auto-discovery and agent tool** — providers configured in DSH Settings → Model Services are detected by route id or base-URL host and reuse their DSH credentials; `tokenlens_usage` lets the model query snapshots, local stats and diagnostics.
|
|
26
26
|
- **Local-only** — 5 harnesses (DSH, ZCode, OpenCode CLI, WorkBuddy / CodeBuddy CLI, Qoder CN CLI), 25 built-in providers (16 with plan / balance adapters), read-only local data, no telemetry.
|
|
27
27
|
|
|
@@ -66,7 +66,7 @@ dsh plugin --profile <profile> add dsh-tokenlens
|
|
|
66
66
|
Then make sure `dsh.profile.bundles` in `$DSH_HOME/profiles/<profile>/package.json` contains `"dsh-tokenlens"`:
|
|
67
67
|
|
|
68
68
|
```jsonc
|
|
69
|
-
{ "dependencies": { "dsh-tokenlens": "^1.2.
|
|
69
|
+
{ "dependencies": { "dsh-tokenlens": "^1.2.2" },
|
|
70
70
|
"dsh": { "profile": { "bundles": ["…", "dsh-tokenlens"] } } }
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -82,6 +82,8 @@ GitHub installs track the latest commit on the default branch (not a tag); updat
|
|
|
82
82
|
|
|
83
83
|
**Option C — from source.** `dsh plugin --profile <profile> add "link:<path-to-dsh-tokenlens>"`.
|
|
84
84
|
|
|
85
|
+
> ⚠️ **Do not mix a link install with a market install**: clicking install/upgrade from the market on a `link:` profile fails on Windows (pnpm cannot rename a package over a directory junction — `EPERM ... rename`). To switch, uninstall the link first (quit DSH, remove `profiles/<profile>/node_modules/dsh-tokenlens` with `cmd /c rmdir` — never `Remove-Item -Recurse` — and delete the `"dsh-tokenlens": "link:..."` dependency line), then install from the market.
|
|
86
|
+
|
|
85
87
|
**Option D — from the DSH plugin market.** This repository maintains a DSH plugin market index ([`market/index.json`](market/index.json)); add it once and the plugin shows up in the **Discover** tab with a one-click install:
|
|
86
88
|
|
|
87
89
|
1. DSH → **Plugins** → **More** next to *Experimental plugins* → enable **Custom plugin market**;
|
|
@@ -134,7 +136,7 @@ Host bridge route: `/api/tokenlens`; agent tool: `tokenlens_usage`.
|
|
|
134
136
|
|
|
135
137
|
## Development
|
|
136
138
|
|
|
137
|
-
Node.js ≥ 24, zero runtime dependencies: `node --test` runs the
|
|
139
|
+
Node.js ≥ 24, zero runtime dependencies: `node --test` runs the 256 tests.
|
|
138
140
|
|
|
139
141
|
Release: `node scripts/bump-version.mjs <version> "<note>"` updates `package.json`, `PLUGIN_VERSION` and `CHANGELOG.md`; see [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).
|
|
140
142
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
> **一眼看清你的用量**:在 DSH 输入框下方显示当前供应商的套餐容量,并汇总本机跨 Harness 的 Token 消耗。
|
|
11
11
|
|
|
12
|
-
**dsh-TokenLens(词元透镜)** 是 DSH(DeepSeek Harness)的用量插件(v1.2.
|
|
12
|
+
**dsh-TokenLens(词元透镜)** 是 DSH(DeepSeek Harness)的用量插件(v1.2.2,公开版本)。它把两类数字**按不同口径**分开呈现:
|
|
13
13
|
|
|
14
14
|
- **套餐容量**(供应商服务端权威值):百分比窗口、`已用 / 上限`、账户余额或剩余额度;
|
|
15
15
|
- **本机 Token 统计**(本机记录):当前供应商在本机所有已接入 Harness 上的合计用量,也可切到「全部 Harness」看总账。
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
- **本机 Token 统计**:今日 / 最近 7 天 / 最近 30 天 / 总计(订阅类供应商显示「最近 X 天」生效窗口),可按「当前供应商 · 全部 Harness」与「全部 Harness」两个视角查看。
|
|
42
42
|
- **明细与图表**:按 Harness、按供应商、Top 模型、未映射供应商分组;支持「数据」(表格)与「图表」两种模式切换。图表模式里点击某一天的柱子可下钻查看**当天 24 小时**的 Token 消耗,底部「明细」环形图支持 **今日 / 最近 7 天 / 最近 X 天(或最近 30 天)/ 总计** 窗口切换。
|
|
43
43
|
- **工作区徽标**(实验性):左侧工作区列表的名称旁显示该工作区的本机 Token 合计,悬浮卡片显示今日 / 7 天 / 30 天 / 总计;纯展示,找不到对应 UI 时自动降级。
|
|
44
|
-
- **供应商管理页**:「已配置 / 内置 /
|
|
44
|
+
- **供应商管理页**:「已配置 / 内置 / 全部」筛选、启用开关、凭据状态、控制台链接、短标签与全名编辑、测试连接、已订阅徽章;另有「插件设置」可配置标签样式、默认展示模式、默认统计范围、明细展开状态与图表「明细」环形图的默认窗口(今日 / 最近 7 天 / 最近 X 天 / 总计,记住上次选择)。
|
|
45
45
|
- **自动识别**:在 DSH「设置 → 模型服务」中配置过的供应商会自动出现在管理页,并按路由 id 或 baseURL 主机匹配内置用量适配器,自动继承 DSH 中的凭据,无需重复填写。
|
|
46
46
|
- **Agent 工具**:注册 `tokenlens_usage`,模型可以自行查询套餐容量与本机用量。
|
|
47
47
|
|
|
@@ -106,7 +106,7 @@ dsh plugin --profile <profile> add dsh-tokenlens
|
|
|
106
106
|
```jsonc
|
|
107
107
|
{
|
|
108
108
|
"dependencies": {
|
|
109
|
-
"dsh-tokenlens": "^1.2.
|
|
109
|
+
"dsh-tokenlens": "^1.2.2"
|
|
110
110
|
},
|
|
111
111
|
"dsh": {
|
|
112
112
|
"profile": {
|
|
@@ -156,6 +156,8 @@ dsh plugin --profile <profile> add "link:<path-to-dsh-tokenlens>"
|
|
|
156
156
|
|
|
157
157
|
本地开发时,Host 端(`index.js` / `lib/`)改动需要重启或重新加载 profile 才能生效;浏览器端 `client.js` 的改动可能被客户端热替换。
|
|
158
158
|
|
|
159
|
+
> ⚠️ **不要混用 link 安装与市场安装**:用 `link:` 装好的 profile 里再从市场点「安装/升级」会失败(Windows 下 pnpm 无法用 npm 包覆盖目录链接,报 `EPERM ... rename`)。需要切换时,先卸载 link(退出 DSH 后删除 `profiles/<profile>/node_modules/dsh-tokenlens` 这个链接——junction 请用 `cmd /c rmdir`,不要用 `Remove-Item -Recurse` 以免误删源码——并从 profile 的 `package.json` 删掉 `"dsh-tokenlens": "link:..."` 这一行),再从市场安装。
|
|
160
|
+
|
|
159
161
|
### 方式 D:从 DSH 插件市场安装
|
|
160
162
|
|
|
161
163
|
本仓库维护了一份 DSH 插件市场索引([`market/index.json`](market/index.json)),在 DSH 里添加一次索引源,就能在「发现」页搜索、查看并一键安装:
|
|
@@ -382,14 +384,14 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
|
|
|
382
384
|
- Harness 卸载后其数据目录消失即判定为未安装,默认从弹窗隐藏(可用 `stats.showUninstalled: true` 显示「未安装」行);数据目录仍在磁盘时按历史记录继续计入。
|
|
383
385
|
- ZCode 数据库通常很大且持续写入:插件只做聚合查询并缓存结果,数据库被锁时会短暂显示不可用。
|
|
384
386
|
- 供应商映射是启发式的(alias → 同 id → vendor-namespaced 模型唯一匹配);语义模糊的 provider 会留在未映射列表,可用 `providerAliases` 指定。
|
|
385
|
-
- 工作区 Token
|
|
387
|
+
- 工作区 Token 徽标/悬浮卡片为实验特性,依赖 DSH 侧栏的 DOM 结构;悬浮卡片按工作区标题(大小写不敏感)或文件夹名匹配,DSH 界面变化时可能自动降级为不显示(v1.2.2 修复了 Host 未下发文件夹名、导致标题与统计标签不一致时悬浮卡片不显示 Token 明细的问题)。
|
|
386
388
|
- 多个 composer 同时挂载时,详情对话框只归属发起点击的会话。
|
|
387
389
|
|
|
388
390
|
**商标声明**:OpenCode、Command Code、DeepSeek、ZCode、WorkBuddy、CodeBuddy、Qoder 等名称与商标归各自权利人所有。本插件为独立第三方项目,与上述公司无关联,也未获得其背书。
|
|
389
391
|
|
|
390
392
|
## 验证
|
|
391
393
|
|
|
392
|
-
- `node --test`:**
|
|
394
|
+
- `node --test`:**256 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、插件集成与浏览器端渲染 / 交互。
|
|
393
395
|
- 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
|
|
394
396
|
- 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
|
|
395
397
|
|
|
@@ -400,7 +402,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
|
|
|
400
402
|
```powershell
|
|
401
403
|
git clone https://github.com/daigahou/dsh-tokenlens.git
|
|
402
404
|
cd dsh-tokenlens
|
|
403
|
-
node --test #
|
|
405
|
+
node --test # 256 个用例
|
|
404
406
|
```
|
|
405
407
|
|
|
406
408
|
本地联调安装方式见[方式 C](#方式-c本地开发从源码-link)。
|
package/client.js
CHANGED
|
@@ -66,6 +66,8 @@ window.__ModuleLoader__.load({
|
|
|
66
66
|
const PROVIDERS_TTL_MS = 30000
|
|
67
67
|
/** Auto-sync cadence of the provider list while the management page is open (v0.17.0). */
|
|
68
68
|
const MANAGEMENT_SYNC_MS = 15000
|
|
69
|
+
/** Retry delay of a 明细-window save blocked by an in-flight settings request (v1.2.1). */
|
|
70
|
+
const DETAILS_WINDOW_SAVE_RETRY_MS = 200
|
|
69
71
|
/** How many per-provider / per-model stats rows the dialog lists. */
|
|
70
72
|
const MAX_PROVIDER_ROWS = 5
|
|
71
73
|
const MAX_MODEL_ROWS = 3
|
|
@@ -125,14 +127,16 @@ window.__ModuleLoader__.load({
|
|
|
125
127
|
/** Fixed segment palette of the 明细 donut charts (v0.27.0, color-blind friendly order). */
|
|
126
128
|
const DONUT_PALETTE = ['#22c55e', '#3b82f6', '#f59e0b', '#a855f7', '#06b6d4', '#f97316']
|
|
127
129
|
/**
|
|
128
|
-
* Host-side display defaults (v0.29.0
|
|
129
|
-
*
|
|
130
|
+
* Host-side display defaults (v0.29.0; the five fields include the v1.2.1
|
|
131
|
+
* 明细 donut window). 「恢复默认设置」 sends exactly these; must stay in sync
|
|
132
|
+
* with DEFAULT_SETTINGS in lib/provider-store.mjs.
|
|
130
133
|
*/
|
|
131
134
|
const DEFAULT_PLUGIN_SETTINGS = {
|
|
132
135
|
labelStyle: 'full',
|
|
133
136
|
defaultView: 'chart',
|
|
134
137
|
defaultScope: 'provider',
|
|
135
138
|
detailsExpanded: 'auto',
|
|
139
|
+
detailsWindow: 'total',
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
const zh = {
|
|
@@ -1101,9 +1105,15 @@ window.__ModuleLoader__.load({
|
|
|
1101
1105
|
return raw === 'expanded' || raw === 'collapsed' ? raw : 'auto'
|
|
1102
1106
|
}
|
|
1103
1107
|
|
|
1108
|
+
/** One `detailsWindow` setting: 'today' | 'last7Days' | 'second' | 'total' (junk means 总计). */
|
|
1109
|
+
function normalizeDetailsWindow(raw) {
|
|
1110
|
+
return raw === 'today' || raw === 'last7Days' || raw === 'second' ? raw : 'total'
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1104
1113
|
/**
|
|
1105
|
-
* The
|
|
1106
|
-
*
|
|
1114
|
+
* The five display settings of the `providers` payload (v0.29.0; the 明细
|
|
1115
|
+
* donut window joined them in v1.2.1), every field normalized to a usable
|
|
1116
|
+
* value so old Hosts keep the previous defaults.
|
|
1107
1117
|
*/
|
|
1108
1118
|
function normalizePluginSettings(raw) {
|
|
1109
1119
|
const source = raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
|
|
@@ -1112,6 +1122,7 @@ window.__ModuleLoader__.load({
|
|
|
1112
1122
|
defaultView: normalizeDisplayView(source.defaultView),
|
|
1113
1123
|
defaultScope: normalizeDisplayScope(source.defaultScope),
|
|
1114
1124
|
detailsExpanded: normalizeDetailsExpanded(source.detailsExpanded),
|
|
1125
|
+
detailsWindow: normalizeDetailsWindow(source.detailsWindow),
|
|
1115
1126
|
}
|
|
1116
1127
|
}
|
|
1117
1128
|
|
|
@@ -1743,8 +1754,9 @@ window.__ModuleLoader__.load({
|
|
|
1743
1754
|
// is charted instead of the daily trend, or null (daily trend).
|
|
1744
1755
|
chartDay: null,
|
|
1745
1756
|
// v1.2.0 明细 donut window: 'today' | 'last7Days' | 'second' | 'total'.
|
|
1746
|
-
//
|
|
1747
|
-
|
|
1757
|
+
// v1.2.1: persisted setting (the last choice survives reopening the dialog
|
|
1758
|
+
// and restarting DSH; the startup bootstrap loads it back).
|
|
1759
|
+
detailsWindow: DEFAULT_PLUGIN_SETTINGS.detailsWindow,
|
|
1748
1760
|
// Provider management page (v0.9.0).
|
|
1749
1761
|
view: 'usage', // 'usage' | 'management'
|
|
1750
1762
|
management: defaultManagementState(),
|
|
@@ -1795,7 +1807,10 @@ window.__ModuleLoader__.load({
|
|
|
1795
1807
|
/** Bumped whenever the selected provider changes; stale responses are dropped. */
|
|
1796
1808
|
let usageEpoch = 0
|
|
1797
1809
|
|
|
1798
|
-
/**
|
|
1810
|
+
/**
|
|
1811
|
+
* The five display settings currently in effect (v0.29.0; the 明细 donut
|
|
1812
|
+
* window joined them in v1.2.1).
|
|
1813
|
+
*/
|
|
1799
1814
|
function currentPluginSettings() {
|
|
1800
1815
|
const state = store.get()
|
|
1801
1816
|
return {
|
|
@@ -1803,6 +1818,7 @@ window.__ModuleLoader__.load({
|
|
|
1803
1818
|
defaultView: normalizeDisplayView(state.defaultView),
|
|
1804
1819
|
defaultScope: normalizeDisplayScope(state.defaultScope),
|
|
1805
1820
|
detailsExpanded: normalizeDetailsExpanded(state.detailsExpanded),
|
|
1821
|
+
detailsWindow: normalizeDetailsWindow(state.detailsWindow),
|
|
1806
1822
|
}
|
|
1807
1823
|
}
|
|
1808
1824
|
|
|
@@ -1825,9 +1841,13 @@ window.__ModuleLoader__.load({
|
|
|
1825
1841
|
statsView: settings.defaultView,
|
|
1826
1842
|
statsScope: settings.defaultScope,
|
|
1827
1843
|
showDetails: showDetailsFor(settings.detailsExpanded),
|
|
1844
|
+
// v1.2.1: the 明细 donut window is a persisted setting, so reopening the
|
|
1845
|
+
// dialog or switching provider keeps the last saved choice instead of
|
|
1846
|
+
// always snapping back to 总计 (detailsWindowOf still falls back when the
|
|
1847
|
+
// chosen window is absent from the stats payload).
|
|
1848
|
+
detailsWindow: settings.detailsWindow,
|
|
1828
1849
|
chartHover: null,
|
|
1829
1850
|
chartDay: null,
|
|
1830
|
-
detailsWindow: 'total',
|
|
1831
1851
|
}
|
|
1832
1852
|
}
|
|
1833
1853
|
|
|
@@ -2024,11 +2044,16 @@ window.__ModuleLoader__.load({
|
|
|
2024
2044
|
|
|
2025
2045
|
/**
|
|
2026
2046
|
* Adopt the display settings and wire label cache of a providers payload
|
|
2027
|
-
* (v0.10.0, extended v0.29.0 with the plugin display defaults).
|
|
2028
|
-
*
|
|
2029
|
-
*
|
|
2047
|
+
* (v0.10.0, extended v0.29.0 with the plugin display defaults).
|
|
2048
|
+
*
|
|
2049
|
+
* `options.adoptDetailsWindow` is only set by the startup bootstrap: the
|
|
2050
|
+
* persisted 明细 window must be loaded once, but a plain settingsUpdate
|
|
2051
|
+
* answer must never repaint a just-clicked pill (the answer of an earlier
|
|
2052
|
+
* save would otherwise overwrite the last of a fast click series, v1.2.1).
|
|
2053
|
+
* The management list itself is applied by the caller, so a read-only
|
|
2054
|
+
* startup load can reuse this without touching the page state.
|
|
2030
2055
|
*/
|
|
2031
|
-
function adoptProvidersMeta(parsed) {
|
|
2056
|
+
function adoptProvidersMeta(parsed, options = {}) {
|
|
2032
2057
|
if (parsed === null || typeof parsed !== 'object') return
|
|
2033
2058
|
const current = store.get()
|
|
2034
2059
|
const patch = {}
|
|
@@ -2037,6 +2062,9 @@ window.__ModuleLoader__.load({
|
|
|
2037
2062
|
if (current.defaultView !== settings.defaultView) patch.defaultView = settings.defaultView
|
|
2038
2063
|
if (current.defaultScope !== settings.defaultScope) patch.defaultScope = settings.defaultScope
|
|
2039
2064
|
if (current.detailsExpanded !== settings.detailsExpanded) patch.detailsExpanded = settings.detailsExpanded
|
|
2065
|
+
if (options.adoptDetailsWindow === true && current.detailsWindow !== settings.detailsWindow) {
|
|
2066
|
+
patch.detailsWindow = settings.detailsWindow
|
|
2067
|
+
}
|
|
2040
2068
|
if (parsed.labelCatalog !== null && typeof parsed.labelCatalog === 'object') {
|
|
2041
2069
|
const catalog = { ...current.labelCatalog, ...parsed.labelCatalog }
|
|
2042
2070
|
if (Object.keys(catalog).length > 0) patch.labelCatalog = catalog
|
|
@@ -2047,10 +2075,11 @@ window.__ModuleLoader__.load({
|
|
|
2047
2075
|
/**
|
|
2048
2076
|
* Read the providers payload once at controller startup (v0.10.0).
|
|
2049
2077
|
*
|
|
2050
|
-
* Only the display style
|
|
2051
|
-
* honor a persisted 'full' style
|
|
2052
|
-
*
|
|
2053
|
-
* the
|
|
2078
|
+
* Only the display style, the persisted 明细 window and the wire label
|
|
2079
|
+
* cache are adopted: the chip must honor a persisted 'full' style and the
|
|
2080
|
+
* dialog must restore the saved donut window before the 管理 page is ever
|
|
2081
|
+
* opened, while the page keeps its own lazy load. Silent on failure — an
|
|
2082
|
+
* old Host without the action (or an unreachable bridge) keeps the defaults.
|
|
2054
2083
|
*/
|
|
2055
2084
|
async function bootstrapProviders() {
|
|
2056
2085
|
let data
|
|
@@ -2062,7 +2091,7 @@ window.__ModuleLoader__.load({
|
|
|
2062
2091
|
if (data === null || typeof data !== 'object' || data.ok !== true) return { kind: 'envelope' }
|
|
2063
2092
|
const parsed = normalizeProvidersPayload(data)
|
|
2064
2093
|
if (parsed === null) return { kind: 'transport' }
|
|
2065
|
-
adoptProvidersMeta(parsed)
|
|
2094
|
+
adoptProvidersMeta(parsed, { adoptDetailsWindow: true })
|
|
2066
2095
|
return { kind: 'ok' }
|
|
2067
2096
|
}
|
|
2068
2097
|
|
|
@@ -2203,10 +2232,11 @@ window.__ModuleLoader__.load({
|
|
|
2203
2232
|
}
|
|
2204
2233
|
|
|
2205
2234
|
/**
|
|
2206
|
-
* Persist one display-settings change (v0.29.0): the FULL
|
|
2207
|
-
* object is always sent
|
|
2208
|
-
* keeps a single source of truth. The
|
|
2209
|
-
* labels and provider list are adopted.
|
|
2235
|
+
* Persist one display-settings change (v0.29.0): the FULL five-field settings
|
|
2236
|
+
* object is always sent (the v1.2.1 明细 window included), because the Host
|
|
2237
|
+
* accepts any subset but the page keeps a single source of truth. The
|
|
2238
|
+
* answered payload's settings, wire labels and provider list are adopted.
|
|
2239
|
+
* Never throws.
|
|
2210
2240
|
*/
|
|
2211
2241
|
async function requestSettingsUpdate(settings) {
|
|
2212
2242
|
const next = normalizePluginSettings(settings)
|
|
@@ -2255,7 +2285,37 @@ window.__ModuleLoader__.load({
|
|
|
2255
2285
|
return { kind: 'transport' }
|
|
2256
2286
|
}
|
|
2257
2287
|
|
|
2258
|
-
/**
|
|
2288
|
+
/** Pending retry timer of the 明细-window save (a single timer, v1.2.1). */
|
|
2289
|
+
let detailsWindowSaveTimer = null
|
|
2290
|
+
|
|
2291
|
+
/**
|
|
2292
|
+
* Persist the 明细 donut window choice (v1.2.1).
|
|
2293
|
+
*
|
|
2294
|
+
* The store field is also the persisted setting value, so a pill click saves
|
|
2295
|
+
* the FULL current settings through `settingsUpdate`. When another settings
|
|
2296
|
+
* request is still in flight, one 200ms retry timer keeps the LAST click of a
|
|
2297
|
+
* fast series (the earlier answer must not overwrite it: adoptProvidersMeta
|
|
2298
|
+
* ignores detailsWindow for non-bootstrap answers). The bridge answers
|
|
2299
|
+
* locally within a few milliseconds, so a single retry is enough. The Host's
|
|
2300
|
+
* answer then refreshes the provider list as usual.
|
|
2301
|
+
*/
|
|
2302
|
+
function persistDetailsWindow() {
|
|
2303
|
+
if (detailsWindowSaveTimer !== null) {
|
|
2304
|
+
window.clearTimeout(detailsWindowSaveTimer)
|
|
2305
|
+
detailsWindowSaveTimer = null
|
|
2306
|
+
}
|
|
2307
|
+
if (store.get().management.settingsBusy === true) {
|
|
2308
|
+
detailsWindowSaveTimer = window.setTimeout(() => {
|
|
2309
|
+
detailsWindowSaveTimer = null
|
|
2310
|
+
if (store.get().management.settingsBusy === true) return
|
|
2311
|
+
void requestSettingsUpdate(currentPluginSettings())
|
|
2312
|
+
}, DETAILS_WINDOW_SAVE_RETRY_MS)
|
|
2313
|
+
return
|
|
2314
|
+
}
|
|
2315
|
+
void requestSettingsUpdate(currentPluginSettings())
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
/** Change one display setting; the other four keep their current values. */
|
|
2259
2319
|
function savePluginSetting(field, value) {
|
|
2260
2320
|
if (store.get().management.settingsBusy === true) return
|
|
2261
2321
|
const next = { ...currentPluginSettings(), [field]: value }
|
|
@@ -2265,6 +2325,9 @@ window.__ModuleLoader__.load({
|
|
|
2265
2325
|
/** 「恢复默认设置」: send the Host-side display defaults as one full payload. */
|
|
2266
2326
|
function resetPluginSettings() {
|
|
2267
2327
|
if (store.get().management.settingsBusy === true) return
|
|
2328
|
+
// v1.2.1: reset the persisted 明细 window locally too — a plain
|
|
2329
|
+
// settingsUpdate answer intentionally does not adopt that field.
|
|
2330
|
+
store.set({ detailsWindow: DEFAULT_PLUGIN_SETTINGS.detailsWindow })
|
|
2268
2331
|
void requestSettingsUpdate({ ...DEFAULT_PLUGIN_SETTINGS })
|
|
2269
2332
|
}
|
|
2270
2333
|
|
|
@@ -4382,9 +4445,10 @@ window.__ModuleLoader__.load({
|
|
|
4382
4445
|
}
|
|
4383
4446
|
|
|
4384
4447
|
/**
|
|
4385
|
-
* The active 明细 donut window (v1.2.
|
|
4386
|
-
* still exists, else 总计.
|
|
4387
|
-
*
|
|
4448
|
+
* The active 明细 donut window (v1.2.1): the stored choice while that window
|
|
4449
|
+
* still exists, else 总计. Since v1.2.1 the choice is also persisted to the
|
|
4450
|
+
* plugin settings (the startup bootstrap loads it back), so reopening the
|
|
4451
|
+
* dialog / restarting DSH restores it.
|
|
4388
4452
|
*/
|
|
4389
4453
|
function detailsWindowOf(state, ctx) {
|
|
4390
4454
|
const key = typeof state.detailsWindow === 'string' ? state.detailsWindow : 'total'
|
|
@@ -4415,7 +4479,7 @@ window.__ModuleLoader__.load({
|
|
|
4415
4479
|
return bucket === null ? 0 : bucket.total ?? 0
|
|
4416
4480
|
}
|
|
4417
4481
|
|
|
4418
|
-
/** The window pill row under the 明细 donut title (v1.2.0). */
|
|
4482
|
+
/** The window pill row under the 明细 donut title (v1.2.0; saved since v1.2.1). */
|
|
4419
4483
|
function buildDetailsWindowSwitch(state, ctx, t) {
|
|
4420
4484
|
const active = detailsWindowOf(state, ctx)
|
|
4421
4485
|
const option = (key, label) =>
|
|
@@ -4429,7 +4493,10 @@ window.__ModuleLoader__.load({
|
|
|
4429
4493
|
'data-active': active === key ? 'true' : 'false',
|
|
4430
4494
|
'aria-pressed': active === key ? 'true' : 'false',
|
|
4431
4495
|
onClick: () => {
|
|
4432
|
-
if (active !== key)
|
|
4496
|
+
if (active !== key) {
|
|
4497
|
+
store.set({ detailsWindow: key })
|
|
4498
|
+
persistDetailsWindow()
|
|
4499
|
+
}
|
|
4433
4500
|
},
|
|
4434
4501
|
},
|
|
4435
4502
|
label,
|
|
@@ -5123,7 +5190,7 @@ window.__ModuleLoader__.load({
|
|
|
5123
5190
|
* v1.2.0: a 今日 / 最近7天 / 最近X天 / 总计 window switch under the title picks
|
|
5124
5191
|
* which stats window the three donuts aggregate (default 总计). The pills of
|
|
5125
5192
|
* windows an old Host does not report are hidden; the state lives in the store
|
|
5126
|
-
*
|
|
5193
|
+
* and, since v1.2.1, is persisted to the plugin settings.
|
|
5127
5194
|
*/
|
|
5128
5195
|
function buildStatsDetailsCharts(state, ctx, t) {
|
|
5129
5196
|
const { stats } = ctx
|
|
@@ -6702,15 +6769,18 @@ window.__ModuleLoader__.load({
|
|
|
6702
6769
|
}
|
|
6703
6770
|
|
|
6704
6771
|
/**
|
|
6705
|
-
* Match one hover card to its stats.workspaces entry:
|
|
6706
|
-
* first, then a UNIQUE path suffix (entry path endsWith the card
|
|
6707
|
-
* the card path endsWith the entry basename
|
|
6772
|
+
* Match one hover card to its stats.workspaces entry (v1.2.2): case-insensitive
|
|
6773
|
+
* exact label first, then a UNIQUE path suffix (entry path endsWith the card
|
|
6774
|
+
* path, the card path endsWith the entry basename, or the folder-name suffix
|
|
6775
|
+
* equals the entry label for Hosts whose wire path is empty). Ambiguous → null.
|
|
6708
6776
|
*/
|
|
6709
6777
|
function workspaceHoverEntryOf(stats, title, pathText) {
|
|
6710
6778
|
if (stats === null || typeof stats !== 'object' || !Array.isArray(stats.workspaces)) return null
|
|
6711
|
-
const wantedTitle = typeof title === 'string' ? title.trim() : ''
|
|
6779
|
+
const wantedTitle = typeof title === 'string' ? title.trim().toLowerCase() : ''
|
|
6712
6780
|
if (wantedTitle !== '') {
|
|
6713
|
-
const byLabel = stats.workspaces.filter(
|
|
6781
|
+
const byLabel = stats.workspaces.filter(
|
|
6782
|
+
(entry) => stringOrEmpty(entry?.label).trim().toLowerCase() === wantedTitle,
|
|
6783
|
+
)
|
|
6714
6784
|
if (byLabel.length === 1) return byLabel[0]
|
|
6715
6785
|
}
|
|
6716
6786
|
const cardPath = typeof pathText === 'string' ? pathText.trim().toLowerCase() : ''
|
|
@@ -6718,10 +6788,13 @@ window.__ModuleLoader__.load({
|
|
|
6718
6788
|
if (suffix === '') return null
|
|
6719
6789
|
const byPath = stats.workspaces.filter((entry) => {
|
|
6720
6790
|
const entryPath = stringOrEmpty(entry?.path).trim().toLowerCase()
|
|
6721
|
-
|
|
6722
|
-
if (
|
|
6723
|
-
|
|
6724
|
-
|
|
6791
|
+
const entryLabel = stringOrEmpty(entry?.label).trim().toLowerCase()
|
|
6792
|
+
if (entryPath !== '') {
|
|
6793
|
+
if (workspacePathSuffixOf(entryPath).toLowerCase() === suffix) return true
|
|
6794
|
+
if (cardPath !== '' && (entryPath.endsWith(cardPath) || cardPath.endsWith(entryPath))) return true
|
|
6795
|
+
}
|
|
6796
|
+
// Old Hosts sent no path; the label often carries the folder name.
|
|
6797
|
+
return entryPath === '' && entryLabel !== '' && entryLabel === suffix
|
|
6725
6798
|
})
|
|
6726
6799
|
return byPath.length === 1 ? byPath[0] : null
|
|
6727
6800
|
}
|
package/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { createUsageStats } from './lib/usage-stats.mjs'
|
|
|
25
25
|
import {
|
|
26
26
|
DEFAULT_SETTINGS,
|
|
27
27
|
DETAILS_DEFAULTS,
|
|
28
|
+
DETAILS_WINDOWS,
|
|
28
29
|
DISPLAY_VIEWS,
|
|
29
30
|
LABEL_STYLES,
|
|
30
31
|
STATS_SCOPES,
|
|
@@ -71,7 +72,7 @@ export const inject = []
|
|
|
71
72
|
export const PLUGIN_NAME = 'dsh-tokenlens'
|
|
72
73
|
|
|
73
74
|
/** Plugin version; mirrors package.json (kept here so diagnostics need no fs read). */
|
|
74
|
-
export const PLUGIN_VERSION = '1.2.
|
|
75
|
+
export const PLUGIN_VERSION = '1.2.2'
|
|
75
76
|
|
|
76
77
|
/** Documented defaults; every field of the raw row config is optional. */
|
|
77
78
|
const DEFAULTS = {
|
|
@@ -1290,12 +1291,16 @@ export function apply(ctx, rawConfig) {
|
|
|
1290
1291
|
}
|
|
1291
1292
|
}
|
|
1292
1293
|
if (Array.isArray(out.workspaces)) {
|
|
1293
|
-
|
|
1294
|
+
// Only the basename crosses the wire: the client matches a workspace hover
|
|
1295
|
+
// card by its folder name (the full cwd stays Host-only). v1.2.2 fixes a
|
|
1296
|
+
// double-map bug that always emptied this field, which made hover cards
|
|
1297
|
+
// without an exact title match show no token stats.
|
|
1298
|
+
out.workspaces = out.workspaces.map(({ cwd, path, ...row }) => {
|
|
1299
|
+
const full = typeof path === 'string' && path !== '' ? path : typeof cwd === 'string' ? cwd : ''
|
|
1300
|
+
const parts = full.split(/[\\/]+/).filter((part) => part !== '')
|
|
1301
|
+
return { ...row, path: parts.length === 0 ? '' : parts[parts.length - 1] }
|
|
1302
|
+
})
|
|
1294
1303
|
}
|
|
1295
|
-
out.workspaces = out.workspaces.map((row) => ({
|
|
1296
|
-
...row,
|
|
1297
|
-
path: String(row.path ?? row.cwd ?? '').split(/[\\/]/).filter(Boolean).pop() ?? '',
|
|
1298
|
-
}))
|
|
1299
1304
|
return scrubMessagePaths(out)
|
|
1300
1305
|
}
|
|
1301
1306
|
|
|
@@ -1655,6 +1660,14 @@ export function apply(ctx, rawConfig) {
|
|
|
1655
1660
|
}
|
|
1656
1661
|
next.detailsExpanded = incoming.detailsExpanded
|
|
1657
1662
|
}
|
|
1663
|
+
if (Object.prototype.hasOwnProperty.call(incoming, 'detailsWindow')) {
|
|
1664
|
+
if (!DETAILS_WINDOWS.includes(incoming.detailsWindow)) {
|
|
1665
|
+
throw new UsageError('unsupported-provider', `settings.detailsWindow must be one of ${DETAILS_WINDOWS.join(', ')}`, {
|
|
1666
|
+
retryable: false,
|
|
1667
|
+
})
|
|
1668
|
+
}
|
|
1669
|
+
next.detailsWindow = incoming.detailsWindow
|
|
1670
|
+
}
|
|
1658
1671
|
providerStore = saveProviderStore(providerStoreFile, {
|
|
1659
1672
|
...providerStore,
|
|
1660
1673
|
settings: next,
|
package/lib/provider-store.mjs
CHANGED
|
@@ -29,6 +29,8 @@ export const LABEL_STYLES = ['short', 'full']
|
|
|
29
29
|
export const DISPLAY_VIEWS = ['chart', 'data']
|
|
30
30
|
export const STATS_SCOPES = ['provider', 'all']
|
|
31
31
|
export const DETAILS_DEFAULTS = ['auto', 'expanded', 'collapsed']
|
|
32
|
+
/** The 明细 donut windows of the chart mode (v1.2.1): 今日 / 最近7天 / 最近X天 / 总计. */
|
|
33
|
+
export const DETAILS_WINDOWS = ['today', 'last7Days', 'second', 'total']
|
|
32
34
|
|
|
33
35
|
/** Default display settings (management 插件设置 tab). */
|
|
34
36
|
export const DEFAULT_SETTINGS = {
|
|
@@ -36,6 +38,8 @@ export const DEFAULT_SETTINGS = {
|
|
|
36
38
|
defaultView: 'chart',
|
|
37
39
|
defaultScope: 'provider',
|
|
38
40
|
detailsExpanded: 'auto',
|
|
41
|
+
// v1.2.1 图表明细窗口默认值: 总计 (the last choice is persisted there).
|
|
42
|
+
detailsWindow: 'total',
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/** The default override file: `$DSH_HOME/dsh-tokenlens/providers.json`. */
|
|
@@ -119,6 +123,9 @@ export function sanitizeSettings(value) {
|
|
|
119
123
|
detailsExpanded: DETAILS_DEFAULTS.includes(source.detailsExpanded)
|
|
120
124
|
? source.detailsExpanded
|
|
121
125
|
: DEFAULT_SETTINGS.detailsExpanded,
|
|
126
|
+
detailsWindow: DETAILS_WINDOWS.includes(source.detailsWindow)
|
|
127
|
+
? source.detailsWindow
|
|
128
|
+
: DEFAULT_SETTINGS.detailsWindow,
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tokenlens",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.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",
|