dsh-tokenlens 1.3.3 → 1.3.5
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 +12 -2
- package/PROTOCOL.md +11 -6
- package/README.en.md +3 -3
- package/README.md +8 -7
- package/client.js +120 -14
- package/index.js +27 -5
- package/lib/provider-store.mjs +15 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
**版本记录约定**:同一发布日只保留一个章节,标题取当日**最后一个**已发布版本号;当日多次发布的改动合并到该章节,并在标题下注明当日实际发布过的版本号(npm 上仍可安装这些独立版本)。若当日已有发布,后续小改动不再单独增加版本号,而是并入下一篇日志。这样日志按天规整、不出现同日多个小版本。
|
|
6
6
|
|
|
7
|
-
## 1.3.
|
|
7
|
+
## 1.3.5 — 2026-11-02
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- feat(ui): 供应商管理页可为 **Open Design** 填写「工作区 ID」并持久化到插件配置——此前 workspaceId 只能在 profile patch 或 `OPENDESIGN_WORKSPACE_ID` 里设置,用推理 Key 配好供应商后额度卡片仍报错却无处可改
|
|
10
|
+
- feat(ui): Open Design 额度失败时的引导——详情弹窗的错误条说明「额度查询需要工作区 ID 与 Control Key(不是推理 Key)」,并提供「去配置」按钮直接跳到供应商管理页
|
|
11
|
+
- feat(host): Host 侧错误文案更具体(distinguish 缺 workspaceId / 缺 Control Key,并指向管理页);`providerUpdate` 接受 `workspaceId`,校验为 1–120 位标识符字符(字母、数字、`. _ : @ -`),路径穿越/空格/超长一律拒绝
|
|
12
|
+
- feat(wire): `providers[]` 仅对声明了 workspace 的适配器(Open Design)下发 `workspaceId` 字段;其余供应商不下发,客户端据此决定是否渲染输入框(旧 Host 无此字段 ⇒ 不渲染,自然降级)
|
|
13
|
+
- feat(ui): Open Design 缺配置时 chip 不再按 `missing-key` 静默隐藏,而是显示「待配置」并可点进详情看引导——否则用户既看不到提示也没有入口
|
|
14
|
+
- 测试:280 → 285(workspace ID 校验/持久化/清空、管理页字段渲染与保存、缺 workspaceId 的 Host 报错与引导按钮、待配置 chip)
|
|
15
|
+
|
|
16
|
+
## 1.3.4 — 2026-11-01
|
|
17
|
+
|
|
18
|
+
(当日已发布版本:v1.2.2 / v1.3.0 / v1.3.1 / v1.3.3 / v1.3.4;v1.3.2 因仓库迁移未发布)
|
|
10
19
|
|
|
11
20
|
- feat(update): 「启动时检查更新」改为**默认开启**(可在插件设置关闭);启动时向 npm registry 查询一次 latest 版本号,不携带凭据 / 账号 / 用量,失败只记录错误、不阻塞启动
|
|
12
21
|
- feat(ui): 详情弹窗(统计首页)右下角的**插件版本处**显示更新状态——有新版本时显示「有新版本 vX」并可点击打开 npm;无新版本时显示「已是最新版本」;检查失败或关闭检查时不显示
|
|
@@ -16,6 +25,7 @@
|
|
|
16
25
|
- chore(repo): 仓库迁移到 **senyingxidi/dsh-tokenlens**(原作者账号改名);同步更新 `package.json` 的 repository/homepage/bugs、README 徽章与 7 张截图、`market/index.json`、CONTRIBUTING / SECURITY / CODE_OF_CONDUCT / Issue 模板与 LICENSE 署名;旧 GitHub 地址会 301 重定向,旧市场索引 raw 地址继续可用
|
|
17
26
|
- fix(ui): 插件版本处自愈——页面在插件更新前加载时,`hello` 的旧版本号不再覆盖新的 Host 版本(取 hello / providers / 更新检查 `current` 中最新的版本),无需刷新页面即可显示正确版本
|
|
18
27
|
- docs: README 增加「用 `link:` 安装的插件不要从插件市场重复安装/升级」的 Windows pnpm EPERM 排错说明;CHANGELOG 改为按发布日合并
|
|
28
|
+
- test(ci): 修复 Windows CI 偶发失败——倒计时测试 fixture 增加 30 秒余量,慢机器上「2 小时 30 分后重置」不再跨成 29 分(CI #26 / #28 的偶发红均属这类跨分钟/小时边界)
|
|
19
29
|
- 测试:280 个自动化用例(版本比较/检查/并发、设置开关、状态行、图表下钻与窗口、工作区 wire 回归)
|
|
20
30
|
|
|
21
31
|
## 1.2.1 — 2026-10-31
|
package/PROTOCOL.md
CHANGED
|
@@ -305,6 +305,9 @@ with `[path]`.
|
|
|
305
305
|
Client behaviour:
|
|
306
306
|
|
|
307
307
|
- `missing-key`, `unsupported-provider`, `disabled` → hide the chip (silent).
|
|
308
|
+
The one exception (v1.3.5): a provider whose Host payload carries a `workspaceId`
|
|
309
|
+
field (Open Design) keeps a 「待配置 / Needs setup」 chip on `missing-key`, because
|
|
310
|
+
hiding it would leave no way to reach the dialog's configuration guidance.
|
|
308
311
|
- everything else → show the chip in a warning state with a retry action.
|
|
309
312
|
|
|
310
313
|
## Provider selection (client side)
|
|
@@ -327,13 +330,15 @@ needs no provider catalog of its own.
|
|
|
327
330
|
|
|
328
331
|
The dialog 「供应商管理」 page talks to three additional actions:
|
|
329
332
|
|
|
330
|
-
- `{action:"providers"}` → `{ok:true, version, 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,updateCheck}, update:<UpdateInfo|null>, updatePackageUrl, storeFile, storeFileDisplay}`. `storeFile` is the real path (local tooling); `storeFileDisplay` replaces the user's home directory with `~` and is what the management page renders. `version` (v1.3.2) is the running Host version, so a page loaded before an in-place plugin update can refresh a stale `hello` label without a reload. `update` (v1.3.0) is the last update-check result and `updatePackageUrl` the npm page the management UI links to.
|
|
331
|
-
- `{action:"providerUpdate", providerId, update:{enabled?,apiKey?,apiKeyRef?,baseURL?,consoleUrl?,label?,shortLabel?}}` → same list payload;
|
|
333
|
+
- `{action:"providers"}` → `{ok:true, version, providers:[{providerId,label,shortLabel,kind,enabled,hasKey,keySource,apiKeyRef,baseURL,consoleUrl,workspaceId?,builtin,dshManaged,dshProviderId}], dshProviders:[{providerId,label,supported,usageProviderId}], settings:{labelStyle,defaultView,defaultScope,detailsExpanded,detailsWindow,updateCheck}, update:<UpdateInfo|null>, updatePackageUrl, storeFile, storeFileDisplay}`. `storeFile` is the real path (local tooling); `storeFileDisplay` replaces the user's home directory with `~` and is what the management page renders. `version` (v1.3.2) is the running Host version, so a page loaded before an in-place plugin update can refresh a stale `hello` label without a reload. `update` (v1.3.0) is the last update-check result and `updatePackageUrl` the npm page the management UI links to. `workspaceId` (v1.3.5) is present only on adapters whose defaults declare one (currently `open-design`); its mere presence is the capability flag for the management page's workspace-ID input, so a provider without it renders no such field.
|
|
334
|
+
- `{action:"providerUpdate", providerId, update:{enabled?,apiKey?,apiKeyRef?,baseURL?,workspaceId?,consoleUrl?,label?,shortLabel?}}` → same list payload;
|
|
332
335
|
empty strings clear a stored value (falling back to the profile/default). `label` (全名, ≤40 chars) and
|
|
333
|
-
`shortLabel` (短标签, ≤12 chars) let the management page rename a provider.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
336
|
+
`shortLabel` (短标签, ≤12 chars) let the management page rename a provider. `workspaceId` (v1.3.5, ≤120 chars,
|
|
337
|
+
charset `[A-Za-z0-9._:@-]`) is the workspace-scoped identifier Open Design's read-only billing API needs; a
|
|
338
|
+
value that is not identifier-shaped (slashes, whitespace, path traversal) answers `parse` and is never persisted.
|
|
339
|
+
The shipped management page no longer edits API keys or base URLs: every provider inherits them from DSH model
|
|
340
|
+
settings, and the page only saves enabled/consoleUrl/workspaceId/label/shortLabel. Overrides are persisted
|
|
341
|
+
atomically to `$DSH_HOME/dsh-tokenlens/providers.json` (plugin-owned, not the profile patch); the same file also
|
|
337
342
|
- `{action:"providerTest", providerId}` → the normal snapshot envelope (`{ok,snapshot}` or `{ok:false,error}`), forcing one upstream read.
|
|
338
343
|
- `{action:"settingsUpdate", settings:{labelStyle?:"short"|"full", defaultView?:"chart"|"data", defaultScope?:"provider"|"all", detailsExpanded?:"auto"|"expanded"|"collapsed", detailsWindow?:"today"|"last7Days"|"second"|"total", updateCheck?:boolean}}` → 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); `updateCheck` (v1.3.0, default `true` since v1.3.1) enables the startup update check — the off→on transition runs one check immediately. Invalid values answer `unsupported-provider`.
|
|
339
344
|
|
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.3.
|
|
12
|
+
**dsh-TokenLens** is a usage plugin for DSH (DeepSeek Harness), version v1.3.4. 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.
|
|
@@ -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.3.
|
|
69
|
+
{ "dependencies": { "dsh-tokenlens": "^1.3.4" },
|
|
70
70
|
"dsh": { "profile": { "bundles": ["…", "dsh-tokenlens"] } } }
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -107,7 +107,7 @@ Every option is optional. Override the `dsh-tokenlens` row in `$DSH_HOME/profile
|
|
|
107
107
|
|
|
108
108
|
Credentials come from DSH itself: providers configured in DSH inherit their `apiKeyEnv`, or you can set environment variables such as `OPENCODE_GO_API_KEY`, `COMMANDCODE_API_KEY` and `DEEPSEEK_API_KEY`.
|
|
109
109
|
|
|
110
|
-
Open Design (OpenDesign) is the exception: its read-only billing API needs a **workspace ID** plus a **Control Key** (`OPENDESIGN_CONTROL_KEY` or `VELA_CONTROL_KEY`), which is *not* the inference API key from the Cloud API Keys page. Set `providers.open-design.workspaceId
|
|
110
|
+
Open Design (OpenDesign) is the exception: its read-only billing API needs a **workspace ID** plus a **Control Key** (`OPENDESIGN_CONTROL_KEY` or `VELA_CONTROL_KEY`), which is *not* the inference API key from the Cloud API Keys page. Set the workspace ID right on the provider management page's Open Design card (v1.3.5, stored in the plugin's own config and kept across restarts), under `providers.open-design.workspaceId`, or via `OPENDESIGN_WORKSPACE_ID`; provide the Control Key through the environment, DSH credentials, or the management page's key field. When either is missing the detail dialog names the missing piece and offers a 「去配置 / Configure」 button instead of a generic missing-key error.
|
|
111
111
|
|
|
112
112
|
See the [Chinese README](README.md) for the full configuration reference, preview and provider tables.
|
|
113
113
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
> **一眼看清你的用量**:在 DSH 输入框下方显示当前供应商的套餐容量,并汇总本机跨 Harness 的 Token 消耗。
|
|
11
11
|
|
|
12
|
-
**dsh-TokenLens(词元透镜)** 是 DSH(DeepSeek Harness)的用量插件(v1.3.
|
|
12
|
+
**dsh-TokenLens(词元透镜)** 是 DSH(DeepSeek Harness)的用量插件(v1.3.4,公开版本)。它把两类数字**按不同口径**分开呈现:
|
|
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
|
+
- **供应商管理页**:「已配置 / 内置 / 全部」筛选、启用开关、凭据状态、控制台链接、短标签与全名编辑、测试连接、已订阅徽章;Open Design 额外提供「工作区 ID」输入(v1.3.5);另有「插件设置」可配置标签样式、默认展示模式、默认统计范围、明细展开状态、图表「明细」环形图的默认窗口(今日 / 最近 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.3.
|
|
109
|
+
"dsh-tokenlens": "^1.3.4"
|
|
110
110
|
},
|
|
111
111
|
"dsh": {
|
|
112
112
|
"profile": {
|
|
@@ -192,7 +192,8 @@ dsh plugin --profile <profile> remove dsh-tokenlens
|
|
|
192
192
|
- 在 DSH「设置 → 模型服务」中配置过的供应商会被自动识别,插件按路由 id 或 baseURL 主机匹配内置适配器,并继承该路由的 `apiKeyEnv` 凭据。
|
|
193
193
|
- 也可以直接用环境变量提供,例如 `OPENCODE_GO_API_KEY`、`COMMANDCODE_API_KEY`、`DEEPSEEK_API_KEY`、`MOONSHOT_API_KEY` 等(每家供应商的引用见 `lib/providers.mjs`)。
|
|
194
194
|
- 或写入 `$DSH_HOME/.credentials.yaml` 的 `refs:` 映射(与 DSH 凭据服务同一份文档)。
|
|
195
|
-
- **Open Design(OpenDesign)** 的额度查询需要 OpenDesign Cloud 的 **workspace ID + Control Key**(`OPENDESIGN_CONTROL_KEY` 或 `VELA_CONTROL_KEY`),**不是** Cloud API Keys 页的推理 Key(推理 Key 只能调用模型)。workspace ID
|
|
195
|
+
- **Open Design(OpenDesign)** 的额度查询需要 OpenDesign Cloud 的 **workspace ID + Control Key**(`OPENDESIGN_CONTROL_KEY` 或 `VELA_CONTROL_KEY`),**不是** Cloud API Keys 页的推理 Key(推理 Key 只能调用模型)。workspace ID 可直接在[供应商管理页](#供应商管理)的 Open Design 卡片里填写(v1.3.5,保存在插件自己的配置里,重启后仍在),或写在 provider 配置里,或用 `OPENDESIGN_WORKSPACE_ID` 环境变量;Control Key 通过环境变量 / DSH 凭据 / 管理页的 Key 输入框提供。
|
|
196
|
+
- 配置不全时,详情弹窗会说明缺哪一项并提供「去配置」按钮,而不是只显示一个笼统的「没有找到该供应商的 API Key」。
|
|
196
197
|
|
|
197
198
|
解析顺序(先命中先用):DSH 凭据服务 → 环境变量 → `$DSH_HOME/.credentials.yaml` → OpenCode CLI 的 `auth.json`(仅默认的 `OPENCODE_GO_API_KEY` 引用)。
|
|
198
199
|
|
|
@@ -252,7 +253,7 @@ dsh plugin --profile <profile> remove dsh-tokenlens
|
|
|
252
253
|
|
|
253
254
|
- 同 id 的配置行**整体替换**插件的默认配置,因此覆盖时请重述所有想保留的字段;
|
|
254
255
|
- `providers.<id>` 与 `stats.<zcode|opencode|workbuddy|qoder>` 内部按字段合并到默认值上;
|
|
255
|
-
-
|
|
256
|
+
- 供应商管理页保存的覆盖(启用状态、名称、控制台链接、Open Design 的工作区 ID 等)写入插件自己的 `$DSH_HOME/dsh-tokenlens/providers.json`(原子写入、仅本人可读),既不进 profile patch,也不会覆盖 DSH 模型设置;读取优先级为 **管理页覆盖 > profile 配置 > 内置默认**。
|
|
256
257
|
- `enabled: false` 会卸载 Host 半(bridge、Agent 工具与轮询一并停止),适合临时停用插件。
|
|
257
258
|
|
|
258
259
|
## 支持的供应商
|
|
@@ -280,7 +281,7 @@ dsh plugin --profile <profile> remove dsh-tokenlens
|
|
|
280
281
|
|
|
281
282
|
- OpenCode 的额度定义见[官方文档](https://opencode.ai/docs/go/#usage-limits)(5h = 月度额度的 20%、周 = 50%、月 = 100%);接口只返回百分比,插件也只画百分比。
|
|
282
283
|
- Command Code 的 `/alpha/*` 是官方 CLI 自用的私有计费路由;免费 / 无方案账户返回空窗口(显示 `—`),插件不会用方案目录猜额度。
|
|
283
|
-
- Open Design 的窗口按账户实际返回的时长渲染:Go 同时有 5 小时和 7 天窗口,Plus / Pro / Max 只有 7 天窗口;钱包余额是独立于窗口的 USD 额度,用尽后继续扣钱包。查询要求工作区权限,鉴权失败会明确显示错误而不是 0
|
|
284
|
+
- Open Design 的窗口按账户实际返回的时长渲染:Go 同时有 5 小时和 7 天窗口,Plus / Pro / Max 只有 7 天窗口;钱包余额是独立于窗口的 USD 额度,用尽后继续扣钱包。查询要求工作区权限,鉴权失败会明确显示错误而不是 0;工作区 ID 与 Control Key 缺任一项时,弹窗会指出缺哪一项并给出「去配置」入口(v1.3.5)。
|
|
284
285
|
- 未配置凭据的供应商 chip 静默隐藏;`zai`、`minimax`、`deepinfra` 属 best-effort 适配(未公开接口,失败时按可重试错误优雅降级)。
|
|
285
286
|
- 各供应商的元数据(`kind` / `label` / `shortLabel` / `baseURL` / `apiKeyRef` / `consoleUrl`)集中在 `lib/providers.mjs` 的 `PROVIDER_DEFAULTS` 与 `ADAPTERS` 中,新增适配器只需在该文件登记并补测试。
|
|
286
287
|
|
|
@@ -392,7 +393,7 @@ Host 半注册 `tokenlens_usage`,参数 `action`(默认 `snapshot`)与可
|
|
|
392
393
|
|
|
393
394
|
## 验证
|
|
394
395
|
|
|
395
|
-
- `node --test`:**
|
|
396
|
+
- `node --test`:**285 个自动化用例**,覆盖 Host 数据层、供应商适配器、统计折叠、各 harness 读取、插件集成与浏览器端渲染 / 交互。
|
|
396
397
|
- 在真实环境中验证过 **5 个 harness** 的数据源;本机统计与独立的 SQLite 查询、独立的日志解析逐项对账(差额为 0),跨 harness 合并与供应商映射算术自洽。
|
|
397
398
|
- 供应商接口按各官方文档与参考客户端行为验证;未配置凭据时 chip 静默隐藏,接口异常时按可重试错误降级。
|
|
398
399
|
|
package/client.js
CHANGED
|
@@ -157,6 +157,7 @@ window.__ModuleLoader__.load({
|
|
|
157
157
|
title: '词元透镜(dsh-TokenLens)',
|
|
158
158
|
chipHint: '当前模型供应商的套餐用量,点击查看详情',
|
|
159
159
|
chipUnavailable: '用量读取失败',
|
|
160
|
+
chipNeedsConfig: '待配置',
|
|
160
161
|
chipNoUsage: '暂无用量',
|
|
161
162
|
chipRolling: '5时',
|
|
162
163
|
chipWeekly: '周',
|
|
@@ -328,6 +329,12 @@ window.__ModuleLoader__.load({
|
|
|
328
329
|
presetCollapse: '收起',
|
|
329
330
|
providerConsoleURL: '控制台地址(Console URL)',
|
|
330
331
|
providerConsoleURLPlaceholder: '留空则使用内置默认地址',
|
|
332
|
+
providerWorkspaceId: '工作区 ID(Workspace ID)',
|
|
333
|
+
providerWorkspaceIdHint: 'Open Design Cloud 工作区标识;额度查询需要',
|
|
334
|
+
providerWorkspaceIdPlaceholder: '留空则使用环境变量 OPENDESIGN_WORKSPACE_ID',
|
|
335
|
+
providerWorkspaceIdTooLong: '工作区 ID 最多 120 个字符',
|
|
336
|
+
configureProvider: '去配置',
|
|
337
|
+
errorOpenDesignConfig: 'Open Design 额度查询需要工作区 ID 与 Control Key(不是推理 Key)',
|
|
331
338
|
providerSave: '保存',
|
|
332
339
|
providerSaving: '保存中…',
|
|
333
340
|
providerTest: '测试连接',
|
|
@@ -401,6 +408,7 @@ window.__ModuleLoader__.load({
|
|
|
401
408
|
title: 'dsh-TokenLens',
|
|
402
409
|
chipHint: 'Plan usage for the selected provider — click for details',
|
|
403
410
|
chipUnavailable: 'Usage unavailable',
|
|
411
|
+
chipNeedsConfig: 'Needs setup',
|
|
404
412
|
chipNoUsage: 'No usage',
|
|
405
413
|
chipRolling: '5h',
|
|
406
414
|
chipWeekly: 'wk',
|
|
@@ -572,6 +580,12 @@ window.__ModuleLoader__.load({
|
|
|
572
580
|
presetExpand: 'Expand',
|
|
573
581
|
presetCollapse: 'Collapse',
|
|
574
582
|
providerConsoleURL: 'Console URL',
|
|
583
|
+
providerWorkspaceId: 'Workspace ID',
|
|
584
|
+
providerWorkspaceIdHint: 'The Open Design Cloud workspace identifier the quota query is scoped to',
|
|
585
|
+
providerWorkspaceIdPlaceholder: 'Empty uses the OPENDESIGN_WORKSPACE_ID environment variable',
|
|
586
|
+
providerWorkspaceIdTooLong: 'The workspace ID may be at most 120 characters',
|
|
587
|
+
configureProvider: 'Configure',
|
|
588
|
+
errorOpenDesignConfig: 'Open Design quota needs a workspace ID and a Control Key (not an inference key)',
|
|
575
589
|
providerConsoleURLPlaceholder: 'Leave empty for the built-in default',
|
|
576
590
|
providerSave: 'Save',
|
|
577
591
|
providerSaving: 'Saving…',
|
|
@@ -1083,6 +1097,10 @@ window.__ModuleLoader__.load({
|
|
|
1083
1097
|
// how to add that route instead of offering any credential input.
|
|
1084
1098
|
const wireDshProviderId = (stringOrEmpty(raw.dshProviderId) || '').trim()
|
|
1085
1099
|
const dshManaged = raw.dshManaged === true && wireDshProviderId !== ''
|
|
1100
|
+
// v1.3.5: workspace-scoped adapters (Open Design) carry the workspace id a
|
|
1101
|
+
// user may set on this page. Absent on every other provider (and on an
|
|
1102
|
+
// older Host), where it normalizes to '' and no field renders.
|
|
1103
|
+
const workspaceId = (stringOrEmpty(raw.workspaceId) || '').trim()
|
|
1086
1104
|
// v0.16.0: user-declared custom adapters are flagged `custom:true` (and are
|
|
1087
1105
|
// no longer builtin). The Host currently returns only the display/credential
|
|
1088
1106
|
// hints, so the optional declaration tail is normalized defensively: a Host
|
|
@@ -1114,6 +1132,10 @@ window.__ModuleLoader__.load({
|
|
|
1114
1132
|
windows: normalizeCustomWindows(raw.windows),
|
|
1115
1133
|
dshManaged,
|
|
1116
1134
|
dshProviderId: dshManaged ? wireDshProviderId : '',
|
|
1135
|
+
// v1.3.5: the Host only sends this field for adapters that use it; its
|
|
1136
|
+
// mere presence is the capability flag that renders the input.
|
|
1137
|
+
workspaceId,
|
|
1138
|
+
supportsWorkspaceId: Object.prototype.hasOwnProperty.call(raw, 'workspaceId'),
|
|
1117
1139
|
}
|
|
1118
1140
|
}
|
|
1119
1141
|
|
|
@@ -2306,6 +2328,7 @@ window.__ModuleLoader__.load({
|
|
|
2306
2328
|
update !== null && typeof update === 'object' && Object.prototype.hasOwnProperty.call(update, field)
|
|
2307
2329
|
const clearedDraft = hasField('consoleUrl')
|
|
2308
2330
|
const resetNames = !clearedDraft && (hasField('shortLabel') || hasField('label'))
|
|
2331
|
+
const resetWorkspace = hasField('workspaceId')
|
|
2309
2332
|
const rowTest = { ...current.rowTest }
|
|
2310
2333
|
delete rowTest[providerId]
|
|
2311
2334
|
// Adopt the answered list; a Host that omits it must not blank the page.
|
|
@@ -2313,17 +2336,26 @@ window.__ModuleLoader__.load({
|
|
|
2313
2336
|
let nextDrafts = current.drafts
|
|
2314
2337
|
if (clearedDraft) {
|
|
2315
2338
|
nextDrafts = mapWithout(current.drafts, providerId)
|
|
2316
|
-
} else if (resetNames) {
|
|
2339
|
+
} else if (resetNames || resetWorkspace) {
|
|
2317
2340
|
const draft = current.drafts[providerId]
|
|
2318
2341
|
const answered = adoptsList
|
|
2319
2342
|
? parsed.providers.find((entry) => entry.providerId === providerId)
|
|
2320
2343
|
: undefined
|
|
2321
2344
|
if (draft !== undefined && draft !== null && typeof draft === 'object') {
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
shortLabel
|
|
2325
|
-
label
|
|
2326
|
-
}
|
|
2345
|
+
const next = { ...draft }
|
|
2346
|
+
if (resetNames) {
|
|
2347
|
+
next.shortLabel = answered !== undefined ? answered.shortLabel : draft.shortLabel
|
|
2348
|
+
next.label = answered !== undefined ? answered.label : draft.label
|
|
2349
|
+
}
|
|
2350
|
+
// v1.3.5: a saved workspace id re-seeds from the answered list,
|
|
2351
|
+
// so a normalized (trimmed) value replaces the raw draft.
|
|
2352
|
+
if (resetWorkspace) {
|
|
2353
|
+
next.workspaceId =
|
|
2354
|
+
answered !== undefined && typeof answered.workspaceId === 'string'
|
|
2355
|
+
? answered.workspaceId
|
|
2356
|
+
: draft.workspaceId
|
|
2357
|
+
}
|
|
2358
|
+
nextDrafts = mapWith(current.drafts, providerId, next)
|
|
2327
2359
|
}
|
|
2328
2360
|
}
|
|
2329
2361
|
patchManagement({
|
|
@@ -2810,6 +2842,7 @@ window.__ModuleLoader__.load({
|
|
|
2810
2842
|
consoleUrl: provider.consoleUrl,
|
|
2811
2843
|
shortLabel: provider.shortLabel,
|
|
2812
2844
|
label: provider.label,
|
|
2845
|
+
workspaceId: typeof provider.workspaceId === 'string' ? provider.workspaceId : '',
|
|
2813
2846
|
}
|
|
2814
2847
|
}
|
|
2815
2848
|
|
|
@@ -2830,6 +2863,13 @@ window.__ModuleLoader__.load({
|
|
|
2830
2863
|
if (shortLabel !== provider.shortLabel) update.shortLabel = shortLabel
|
|
2831
2864
|
const label = typeof source.label === 'string' ? source.label.trim() : provider.label
|
|
2832
2865
|
if (label !== provider.label) update.label = label
|
|
2866
|
+
// v1.3.5: the workspace id of a workspace-scoped adapter; an empty string
|
|
2867
|
+
// clears the override so the Host falls back to its env var.
|
|
2868
|
+
if (provider.supportsWorkspaceId === true) {
|
|
2869
|
+
const wireWorkspaceId = typeof provider.workspaceId === 'string' ? provider.workspaceId : ''
|
|
2870
|
+
const workspaceId = typeof source.workspaceId === 'string' ? source.workspaceId.trim() : wireWorkspaceId
|
|
2871
|
+
if (workspaceId !== wireWorkspaceId) update.workspaceId = workspaceId
|
|
2872
|
+
}
|
|
2833
2873
|
return update
|
|
2834
2874
|
}
|
|
2835
2875
|
|
|
@@ -2852,13 +2892,15 @@ window.__ModuleLoader__.load({
|
|
|
2852
2892
|
consoleUrl: provider.consoleUrl,
|
|
2853
2893
|
shortLabel: provider.shortLabel,
|
|
2854
2894
|
label: provider.label,
|
|
2895
|
+
workspaceId: typeof provider.workspaceId === 'string' ? provider.workspaceId : '',
|
|
2855
2896
|
}),
|
|
2856
2897
|
})
|
|
2857
2898
|
}
|
|
2858
2899
|
|
|
2859
|
-
/** Record one typed editor value (names
|
|
2900
|
+
/** Record one typed editor value (names, the display-only console URL, the workspace id). */
|
|
2860
2901
|
function updateProviderDraft(provider, field, value) {
|
|
2861
|
-
if (field !== 'consoleUrl' && field !== 'shortLabel' && field !== 'label') return
|
|
2902
|
+
if (field !== 'consoleUrl' && field !== 'shortLabel' && field !== 'label' && field !== 'workspaceId') return
|
|
2903
|
+
if (field === 'workspaceId' && provider.supportsWorkspaceId !== true) return
|
|
2862
2904
|
if (provider === null || provider === undefined) return
|
|
2863
2905
|
const current = store.get().management
|
|
2864
2906
|
const draft = providerDraftOf(provider, current)
|
|
@@ -3201,6 +3243,9 @@ window.__ModuleLoader__.load({
|
|
|
3201
3243
|
.dsh-tokenlens-banner{border:1px solid var(--dsw-alias-border-subtle,rgba(127,127,127,.24));border-radius:var(--dsw-radius-sm);padding:8px 10px;display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.06))}
|
|
3202
3244
|
.dsh-tokenlens-banner-title{color:var(--dsw-alias-label-error,#d9534f);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.5;font-weight:600}
|
|
3203
3245
|
.dsh-tokenlens-banner-detail{color:var(--dsw-alias-label-tertiary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.5;overflow-wrap:anywhere}
|
|
3246
|
+
.dsh-tokenlens-banner-hint{align-items:center;display:flex;gap:8px;flex-wrap:wrap;color:var(--dsw-alias-label-secondary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.5}
|
|
3247
|
+
.dsh-tokenlens-banner-hint-text{min-width:0;overflow-wrap:anywhere}
|
|
3248
|
+
.dsh-tokenlens-configure-provider{flex:none}
|
|
3204
3249
|
.dsh-tokenlens-banner-actions{align-items:center;display:flex;gap:8px}
|
|
3205
3250
|
.dsh-tokenlens-meta{align-items:baseline;gap:8px;display:flex;justify-content:space-between;color:var(--dsw-alias-label-tertiary);font-size:var(--dsw-alias-font-size-sm,12px);line-height:1.5}
|
|
3206
3251
|
.dsh-tokenlens-stale{color:#d98a1f;white-space:nowrap}
|
|
@@ -3507,9 +3552,23 @@ window.__ModuleLoader__.load({
|
|
|
3507
3552
|
// The compositor dock chip.
|
|
3508
3553
|
// ---------------------------------------------------------------------
|
|
3509
3554
|
|
|
3555
|
+
/**
|
|
3556
|
+
* Whether this state is a workspace-scoped provider (Open Design) that is
|
|
3557
|
+
* missing its configuration. Such a provider keeps a chip as the only entry
|
|
3558
|
+
* point to the guidance in the dialog (v1.3.5), because 「没有 API Key」 hides
|
|
3559
|
+
* the chip by protocol and would leave the user with no way in.
|
|
3560
|
+
*/
|
|
3561
|
+
function needsWorkspaceConfigEntry(state) {
|
|
3562
|
+
if (normalizeSnapshot(state.snapshot) !== null) return false
|
|
3563
|
+
if (state.error === null || typeof state.error !== 'object' || state.error.code !== 'missing-key') return false
|
|
3564
|
+
const rawId = state.providerId !== null ? state.providerId : (state.snapshot?.provider?.id ?? null)
|
|
3565
|
+
return canonicalProviderId(rawId) === 'open-design'
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3510
3568
|
/** Whether the chip must stay silent for this state. */
|
|
3511
3569
|
function chipHidden(state) {
|
|
3512
|
-
|
|
3570
|
+
if (!isHidden(state.error)) return false
|
|
3571
|
+
return needsWorkspaceConfigEntry(state) !== true
|
|
3513
3572
|
}
|
|
3514
3573
|
|
|
3515
3574
|
/** Severity of the chip dot: worst window, balance/unknown mapped to normal. */
|
|
@@ -3614,25 +3673,30 @@ window.__ModuleLoader__.load({
|
|
|
3614
3673
|
/** The chip when the route has failed and there is no data yet. */
|
|
3615
3674
|
function buildUnavailableChip(state, t, sessionId) {
|
|
3616
3675
|
const brand = providerBrand(state, null)
|
|
3676
|
+
// A workspace-scoped provider missing its configuration reads as 「待配置」
|
|
3677
|
+
// instead of a read failure: nothing upstream failed, setup is incomplete.
|
|
3678
|
+
const needsConfig = needsWorkspaceConfigEntry(state)
|
|
3679
|
+
const label = needsConfig ? t('chipNeedsConfig') : t('chipUnavailable')
|
|
3617
3680
|
return h(
|
|
3618
3681
|
'span',
|
|
3619
3682
|
{ className: 'dsh-tokenlens-anchor', 'data-composer-stat': 'tokenlens' },
|
|
3620
3683
|
h(
|
|
3621
3684
|
Tooltip,
|
|
3622
|
-
{ label
|
|
3685
|
+
{ label, side: 'top' },
|
|
3623
3686
|
h(
|
|
3624
3687
|
'button',
|
|
3625
3688
|
{
|
|
3626
3689
|
type: 'button',
|
|
3627
3690
|
className: 'dsh-tokenlens-chip',
|
|
3628
|
-
title:
|
|
3629
|
-
'aria-label':
|
|
3691
|
+
title: label,
|
|
3692
|
+
'aria-label': label,
|
|
3630
3693
|
'aria-haspopup': 'dialog',
|
|
3694
|
+
...(needsConfig ? { 'data-needs-config': 'true' } : {}),
|
|
3631
3695
|
onClick: () => openDialog(sessionId),
|
|
3632
3696
|
},
|
|
3633
3697
|
h('span', { className: 'dsh-tokenlens-dot', 'data-level': 'warning', 'aria-hidden': true }),
|
|
3634
3698
|
h('span', { className: 'dsh-tokenlens-brand' }, brand),
|
|
3635
|
-
h('span', { className: 'dsh-tokenlens-metric' },
|
|
3699
|
+
h('span', { className: 'dsh-tokenlens-metric' }, label),
|
|
3636
3700
|
),
|
|
3637
3701
|
),
|
|
3638
3702
|
)
|
|
@@ -3880,7 +3944,7 @@ window.__ModuleLoader__.load({
|
|
|
3880
3944
|
}
|
|
3881
3945
|
|
|
3882
3946
|
/** The error banner, with a retry button unless the Host says it cannot help. */
|
|
3883
|
-
function buildErrorBanner(error, t, onRetry, key) {
|
|
3947
|
+
function buildErrorBanner(error, t, onRetry, key, extraChildren) {
|
|
3884
3948
|
const localized = describeError(error, t)
|
|
3885
3949
|
const detail = typeof error?.message === 'string' ? error.message.trim() : ''
|
|
3886
3950
|
const showDetail = detail !== '' && detail !== localized
|
|
@@ -3890,15 +3954,41 @@ window.__ModuleLoader__.load({
|
|
|
3890
3954
|
h(Button, { key: 'retry', variant: 'outline', className: 'dsh-tokenlens-retry', onClick: onRetry }, t('retry')),
|
|
3891
3955
|
)
|
|
3892
3956
|
}
|
|
3957
|
+
const extras = Array.isArray(extraChildren) ? extraChildren.filter((child) => child !== null && child !== undefined) : []
|
|
3893
3958
|
return h(
|
|
3894
3959
|
'div',
|
|
3895
3960
|
{ key: key ?? 'error', className: 'dsh-tokenlens-banner', 'data-kind': 'error', role: 'alert' },
|
|
3896
3961
|
h('div', { className: 'dsh-tokenlens-banner-title' }, localized),
|
|
3897
3962
|
showDetail ? h('div', { className: 'dsh-tokenlens-banner-detail' }, detail) : null,
|
|
3963
|
+
extras.length > 0 ? h('div', { className: 'dsh-tokenlens-banner-hint' }, extras) : null,
|
|
3898
3964
|
actions.length > 0 ? h('div', { className: 'dsh-tokenlens-banner-actions' }, actions) : null,
|
|
3899
3965
|
)
|
|
3900
3966
|
}
|
|
3901
3967
|
|
|
3968
|
+
/**
|
|
3969
|
+
* The extra guidance a workspace-scoped provider (Open Design) gets when its
|
|
3970
|
+
* configuration is incomplete: the reason in product terms plus a jump to the
|
|
3971
|
+
* management page, where the workspace id and the Control Key hint live.
|
|
3972
|
+
*/
|
|
3973
|
+
function buildWorkspaceConfigHint(state, error, t) {
|
|
3974
|
+
if (error === null || typeof error !== 'object' || error.code !== 'missing-key') return []
|
|
3975
|
+
const rawId = state.providerId !== null ? state.providerId : (state.snapshot?.provider?.id ?? null)
|
|
3976
|
+
if (canonicalProviderId(rawId) !== 'open-design') return []
|
|
3977
|
+
return [
|
|
3978
|
+
h('span', { key: 'text', className: 'dsh-tokenlens-banner-hint-text' }, t('errorOpenDesignConfig')),
|
|
3979
|
+
h(
|
|
3980
|
+
Button,
|
|
3981
|
+
{
|
|
3982
|
+
key: 'configure',
|
|
3983
|
+
variant: 'outline',
|
|
3984
|
+
className: 'dsh-tokenlens-configure-provider',
|
|
3985
|
+
onClick: () => openManagement(),
|
|
3986
|
+
},
|
|
3987
|
+
t('configureProvider'),
|
|
3988
|
+
),
|
|
3989
|
+
]
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3902
3992
|
/** The usage "last updated" line plus a stale marker. */
|
|
3903
3993
|
function buildMeta(state, snapshot, now, t) {
|
|
3904
3994
|
if (snapshot === null) return null
|
|
@@ -5121,6 +5211,7 @@ window.__ModuleLoader__.load({
|
|
|
5121
5211
|
void requestUsage('refresh')
|
|
5122
5212
|
},
|
|
5123
5213
|
'usage-error',
|
|
5214
|
+
buildWorkspaceConfigHint(state, state.error, t),
|
|
5124
5215
|
),
|
|
5125
5216
|
)
|
|
5126
5217
|
}
|
|
@@ -6012,6 +6103,21 @@ window.__ModuleLoader__.load({
|
|
|
6012
6103
|
label: t('providerConsoleURL'),
|
|
6013
6104
|
placeholder: t('providerConsoleURLPlaceholder'),
|
|
6014
6105
|
}),
|
|
6106
|
+
// v1.3.5: only workspace-scoped adapters (Open Design) ask for a
|
|
6107
|
+
// workspace id; every other provider renders no field at all.
|
|
6108
|
+
provider.supportsWorkspaceId === true
|
|
6109
|
+
? buildProviderNameField(provider, management, t, {
|
|
6110
|
+
key: 'workspaceId',
|
|
6111
|
+
className: 'dsh-tokenlens-workspaceid-input',
|
|
6112
|
+
dataAttr: 'data-provider-workspaceid',
|
|
6113
|
+
field: 'workspaceId',
|
|
6114
|
+
label: t('providerWorkspaceId'),
|
|
6115
|
+
hint: t('providerWorkspaceIdHint'),
|
|
6116
|
+
placeholder: t('providerWorkspaceIdPlaceholder'),
|
|
6117
|
+
maxLength: 120,
|
|
6118
|
+
tooLongText: t('providerWorkspaceIdTooLong'),
|
|
6119
|
+
})
|
|
6120
|
+
: null,
|
|
6015
6121
|
]
|
|
6016
6122
|
const actions = h(
|
|
6017
6123
|
'div',
|
package/index.js
CHANGED
|
@@ -29,7 +29,9 @@ import {
|
|
|
29
29
|
DISPLAY_VIEWS,
|
|
30
30
|
LABEL_STYLES,
|
|
31
31
|
STATS_SCOPES,
|
|
32
|
+
WORKSPACE_ID_MAX,
|
|
32
33
|
defaultProviderStoreFile,
|
|
34
|
+
isWorkspaceId,
|
|
33
35
|
loadProviderStore,
|
|
34
36
|
mergeProviderOverride,
|
|
35
37
|
sanitizeSettings,
|
|
@@ -74,7 +76,7 @@ export const inject = []
|
|
|
74
76
|
export const PLUGIN_NAME = 'dsh-tokenlens'
|
|
75
77
|
|
|
76
78
|
/** Plugin version; mirrors package.json (kept here so diagnostics need no fs read). */
|
|
77
|
-
export const PLUGIN_VERSION = '1.3.
|
|
79
|
+
export const PLUGIN_VERSION = '1.3.5'
|
|
78
80
|
|
|
79
81
|
/** Documented defaults; every field of the raw row config is optional. */
|
|
80
82
|
const DEFAULTS = {
|
|
@@ -636,6 +638,9 @@ export function apply(ctx, rawConfig) {
|
|
|
636
638
|
})
|
|
637
639
|
key = { ...key, hasKey: key.hasKey && typeof secret.value === 'string' && secret.value !== '' }
|
|
638
640
|
}
|
|
641
|
+
// v1.3.5: only adapters whose defaults declare a workspace (Open Design)
|
|
642
|
+
// advertise the field, so the management page renders it exactly there.
|
|
643
|
+
const supportsWorkspaceId = Object.prototype.hasOwnProperty.call(PROVIDER_DEFAULTS[id], 'workspaceId')
|
|
639
644
|
providers.push({
|
|
640
645
|
providerId: id,
|
|
641
646
|
label: provider.label,
|
|
@@ -647,6 +652,9 @@ export function apply(ctx, rawConfig) {
|
|
|
647
652
|
apiKeyRef: provider.apiKeyRef ?? '',
|
|
648
653
|
baseURL: provider.baseURL ?? '',
|
|
649
654
|
consoleUrl: provider.consoleUrl ?? '',
|
|
655
|
+
...(supportsWorkspaceId
|
|
656
|
+
? { workspaceId: typeof provider.workspaceId === 'string' ? provider.workspaceId : '' }
|
|
657
|
+
: {}),
|
|
650
658
|
builtin: true,
|
|
651
659
|
// Configured through DSH model settings: the management page should
|
|
652
660
|
// inherit the key/base URL instead of asking for them again.
|
|
@@ -759,6 +767,17 @@ export function apply(ctx, rawConfig) {
|
|
|
759
767
|
if (typeof update?.enabled === 'boolean') patch.enabled = update.enabled
|
|
760
768
|
if (typeof update?.apiKey === 'string') patch.apiKey = update.apiKey.trim()
|
|
761
769
|
if (typeof update?.apiKeyRef === 'string') patch.apiKeyRef = update.apiKeyRef.trim()
|
|
770
|
+
if (typeof update?.workspaceId === 'string') {
|
|
771
|
+
const workspaceId = update.workspaceId.trim()
|
|
772
|
+
if (workspaceId !== '' && !isWorkspaceId(workspaceId)) {
|
|
773
|
+
throw new UsageError(
|
|
774
|
+
'parse',
|
|
775
|
+
`provider workspaceId must be 1-${WORKSPACE_ID_MAX} identifier characters (letters, digits, . _ : @ -)`,
|
|
776
|
+
{ retryable: false },
|
|
777
|
+
)
|
|
778
|
+
}
|
|
779
|
+
patch.workspaceId = workspaceId
|
|
780
|
+
}
|
|
762
781
|
if (typeof update?.baseURL === 'string') {
|
|
763
782
|
const rawBaseURL = update.baseURL.trim()
|
|
764
783
|
patch.baseURL = rawBaseURL === '' ? '' : sanitizeUsageBaseURL(rawBaseURL)
|
|
@@ -1019,14 +1038,17 @@ export function apply(ctx, rawConfig) {
|
|
|
1019
1038
|
if (workspaceId === '') {
|
|
1020
1039
|
throw new UsageError(
|
|
1021
1040
|
'missing-key',
|
|
1022
|
-
'OpenDesign quota needs a workspace id (
|
|
1041
|
+
'OpenDesign quota needs a workspace id: set it on the plugin management page (open-design → 工作区 ID), ' +
|
|
1042
|
+
'in providers.open-design.workspaceId, or in OPENDESIGN_WORKSPACE_ID',
|
|
1023
1043
|
{ retryable: false },
|
|
1024
1044
|
)
|
|
1025
1045
|
}
|
|
1026
1046
|
if (controlKey === '') {
|
|
1027
|
-
throw new UsageError(
|
|
1028
|
-
|
|
1029
|
-
|
|
1047
|
+
throw new UsageError(
|
|
1048
|
+
'missing-key',
|
|
1049
|
+
`OpenDesign quota needs a Control Key (${refs.join(' / ')}); the Cloud API Keys inference key cannot read billing`,
|
|
1050
|
+
{ retryable: false },
|
|
1051
|
+
)
|
|
1030
1052
|
}
|
|
1031
1053
|
provider = { ...provider, workspaceId, controlKey }
|
|
1032
1054
|
keyInfo = { ref: controlRef, source: controlSource, detail: controlDetail }
|
package/lib/provider-store.mjs
CHANGED
|
@@ -134,6 +134,18 @@ export function sanitizeSettings(value) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/** The longest workspace id the management page accepts (identifiers are short). */
|
|
138
|
+
export const WORKSPACE_ID_MAX = 120
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Whether a typed workspace id is safe to persist and interpolate into an API
|
|
142
|
+
* path: opaque identifier characters only (uuid / slug shaped).
|
|
143
|
+
*/
|
|
144
|
+
export function isWorkspaceId(value) {
|
|
145
|
+
return typeof value === 'string' && value.trim() !== '' && value.trim().length <= WORKSPACE_ID_MAX
|
|
146
|
+
&& /^[A-Za-z0-9._:@-]+$/.test(value.trim())
|
|
147
|
+
}
|
|
148
|
+
|
|
137
149
|
/** Only the fields the management page may set, with type guards. */
|
|
138
150
|
export function sanitizeOverride(value) {
|
|
139
151
|
const out = {}
|
|
@@ -148,6 +160,9 @@ export function sanitizeOverride(value) {
|
|
|
148
160
|
for (const key of ['baseURL', 'consoleUrl', 'usagePath', 'balancePath', 'creditsPath', 'subscriptionsPath', 'whoamiPath']) {
|
|
149
161
|
if (typeof source[key] === 'string' && source[key].trim() !== '') out[key] = source[key].trim()
|
|
150
162
|
}
|
|
163
|
+
// v1.3.5: Open Design's read-only billing API is workspace-scoped, so the
|
|
164
|
+
// management page persists the workspace id here (never a secret).
|
|
165
|
+
if (isWorkspaceId(source.workspaceId)) out.workspaceId = source.workspaceId.trim()
|
|
151
166
|
return out
|
|
152
167
|
}
|
|
153
168
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tokenlens",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
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",
|