dsh-multi-provider-usage 0.7.2 → 0.7.4

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/README.md CHANGED
@@ -41,14 +41,16 @@ panel settings page — extended from one provider to three.
41
41
  Code's tooltip breaks its amount into purchased + free credits; a credits
42
42
  response without either pool renders a muted **No top-up** badge.
43
43
  - **Auto-update** — on boot and every configured interval the host checks the
44
- GitHub release feed. A local `link:`/`file:` checkout is fast-forwarded when it
45
- is on `main`, clean, and a strict fast-forward. An npm / market / tarball copy
46
- updates itself: the release tarball is unpacked and each packaged file
47
- (`lib/`, `package.json`, `cordis.patch.yml`, `docs/`) is written to a temp file
48
- and renamed over the target, so pnpm's hardlinked store is never edited in
49
- place. Either way the running Host keeps the old module graph, so the
50
- widget/settings show "updated — restart DSH". The plugin never restarts DSH by
51
- itself.
44
+ npm registry, the npmmirror mirror and the GitHub release feed together and
45
+ trusts the newest version any of them reports. A local `link:`/`file:` checkout
46
+ is fast-forwarded when it is on `main`, clean, and a strict fast-forward. An
47
+ npm / market / tarball copy updates itself: the release tarball is downloaded
48
+ from the first source that serves it (registry → mirror → GitHub, in turn) and
49
+ each packaged file (`lib/`, `package.json`, `cordis.patch.yml`, `docs/`) is
50
+ written to a temp file and renamed over the target, so pnpm's hardlinked store
51
+ is never edited in place. Either way the running Host keeps the old module
52
+ graph, so the widget/settings show "updated — restart DSH". The plugin never
53
+ restarts DSH by itself.
52
54
  - **Balance-change animation** — the widget polls on its normal interval; when
53
55
  a provider's balance differs from the previous poll, the number counts from
54
56
  the old value to the refreshed one while a red/green delta chip (for example
@@ -170,15 +172,11 @@ When the balance is not configured (or the console login is fixable), the
170
172
  OpenCode GO card in the plugin settings renders a step-by-step **setup guide**
171
173
  in place of the static hint. The lead-in names the state-specific fix (nothing
172
174
  configured, an API-key login, or an expired sign-in). Step ① shows the
173
- `opencode auth login` CLI command with a **Copy command** button; step ② is the
174
- manual path — copy the console token and store it as the `OPENCODE_CONSOLE_TOKEN`
175
- credential — with a **Copy credential ref** button and a link to the OpenCode
176
- console website (derived from `opencodeConsoleBaseUrl`, ending in `/console`);
177
- step ③ offers a **Re-check** button that re-reads the sign-in state and forces
178
- one upstream usage read, so a fresh login shows the balance without restarting
179
- DSH (unsaved form edits are never discarded by the re-check). The
180
- `/provider-usage` command gives the same state-specific guidance as text for
181
- all three fixable states.
175
+ `opencode auth login` CLI command with a **Copy command** button; step ② offers
176
+ a **Re-check** button that re-reads the sign-in state and forces one upstream
177
+ usage read, so a fresh login shows the balance without restarting DSH (unsaved
178
+ form edits are never discarded by the re-check). The `/provider-usage` command
179
+ gives the same state-specific guidance as text for all three fixable states.
182
180
 
183
181
  The optional `opencodeConsoleAuthFile` overrides the auth file (and its
184
182
  directory is used to locate `opencode.db`), `opencodeConsoleOrgId` supplies the
@@ -267,7 +265,7 @@ the settings page and the auto-generated form hot-apply.
267
265
  | `glmRegion` | `"auto"` | `auto`, `zai`, `bigmodel`, `custom` |
268
266
  | `cacheMs` | `30000` | Per-provider host cache TTL |
269
267
  | `updateCheck` | `true` | npm update check (24 h) |
270
- | `autoUpdate` | `true` | Check GitHub releases; fast-forward a clean checkout, otherwise replace an installed package |
268
+ | `autoUpdate` | `true` | Check npm, npmmirror and GitHub together; fast-forward a clean checkout, otherwise replace an installed package from the first source that serves the tarball |
271
269
  | `autoUpdateCheckHours` | `6` | Hours between auto-update checks (1–168) |
272
270
  | `injectSessionHeader` | `true` | OpenCode GO session-header fix |
273
271
  | `providerOrder` | all three ids | Provider display order in the widget; drag the cards in the plugin settings to reorder |
@@ -421,7 +419,7 @@ network and no real keys. They cover the schema, all three adapters, OpenCode
421
419
  GO service integration/fallback, aggregation and error isolation, caching and
422
420
  forced refresh, settings conflicts/whitelisting, the client bundle contract,
423
421
  top-up balance discovery (credential and local auth.json) and failure
424
- isolation, and the session-header injection. 98 tests, all offline.
422
+ isolation, and the session-header injection. 102 tests, all offline.
425
423
 
426
424
  ## License
427
425
 
package/README.zh.md CHANGED
@@ -34,11 +34,12 @@
34
34
  设置页内为分步引导:可复制的登录命令、控制台网页链接与「重新检测」按钮。
35
35
  Command Code 的悬停提示会拆成「购买 + 免费」;credits 响应没有这两个池时显示
36
36
  灰色「无充值额度」。
37
- - **自动更新** — 宿主启动后以及按配置的间隔检查 GitHub Release。本地
38
- `link:`/`file:` 目录检出在「main 分支、工作区干净、可严格 fast-forward」时直接
39
- `git pull`;npm / 市场 / tarball 安装的副本则下载该 Release 的 tarball,先写临时
40
- 文件再 `rename` 覆盖 `lib/`、`package.json`、`cordis.patch.yml` 等打包文件——不会
41
- 就地改写 pnpm store 的硬链接。两种方式完成后,运行中的宿主仍持有旧模块,所以
37
+ - **自动更新** — 宿主启动后以及按配置的间隔,同时向 npm registry、npmmirror 镜像与
38
+ GitHub Release 查询,以其中报告的最新版本为准。本地 `link:`/`file:` 目录检出在
39
+ 「main 分支、工作区干净、可严格 fast-forward」时直接 `git pull`;npm / 市场 / tarball
40
+ 安装的副本按「npm → npmmirror → GitHub」依次尝试下载 tarball,先写临时文件再
41
+ `rename` 覆盖 `lib/`、`package.json`、`cordis.patch.yml` 等打包文件——不会就地
42
+ 改写 pnpm store 的硬链接。两种方式完成后,运行中的宿主仍持有旧模块,所以
42
43
  组件/设置页显示“已更新到 vX,重启 DSH 生效”;插件不会自行重启 DSH。
43
44
  - **余额变化动画** — 组件按正常轮询周期刷新;某个服务商的余额相比上一次轮询
44
45
  发生变化时,数字会从旧值滚动到新值,同时一个红色/绿色差值(例如 `-0.1`)
@@ -139,11 +140,9 @@ Plugins 面板中的设置页,悬停提示修复方式:运行 `opencode auth
139
140
 
140
141
  余额未配置或控制台登录可修复时,插件设置页的 OpenCode GO 卡片不再给一句静态
141
142
  提示,而是渲染分步**引导**:引言按当前状态指明修复方式(未配置 / API Key 登录 /
142
- 登录已过期)。第①步给出 `opencode auth login` 命令与「复制命令」按钮;第②步是
143
- 手动路径——从控制台网页复制令牌、保存为 `OPENCODE_CONSOLE_TOKEN` 凭据——带
144
- 「复制凭据名」按钮和打开 OpenCode 控制台网页的链接(由 `opencodeConsoleBaseUrl`
145
- 推导,以 `/console` 结尾);第③步提供「重新检测」按钮,重读登录状态并强制刷新
146
- 一次用量,新登录无需重启 DSH 即可显示余额(重新检测不会丢弃表单里未保存的修改)。
143
+ 登录已过期)。第①步给出 `opencode auth login` 命令与「复制命令」按钮;第②步提供
144
+ 「重新检测」按钮,重读登录状态并强制刷新一次用量,新登录无需重启 DSH 即可显示
145
+ 余额(重新检测不会丢弃表单里未保存的修改)。
147
146
  `/provider-usage` 命令对三种可修复状态都会给出同样的文字引导。
148
147
 
149
148
  可用 `opencodeConsoleAuthFile` 覆盖 auth.json 路径(其目录也用于定位
@@ -225,7 +224,7 @@ Loader 直接读取导出的 `Config` schema;所有字段都是 `.volatile()`
225
224
  | `glmRegion` | `"auto"` | `auto`、`zai`、`bigmodel`、`custom` |
226
225
  | `cacheMs` | `30000` | 单个服务商的宿主缓存 TTL |
227
226
  | `updateCheck` | `true` | npm 新版本检查(24 小时) |
228
- | `autoUpdate` | `true` | 检查 GitHub Release;干净 checkout 快进,安装包用 Release tarball 原地替换 |
227
+ | `autoUpdate` | `true` | 同时查询 npm / npmmirror / GitHub Release;干净 checkout 快进,安装包按可达的源依次下载 tarball 原地替换 |
229
228
  | `autoUpdateCheckHours` | `6` | 自动更新检查间隔(1–168 小时) |
230
229
  | `injectSessionHeader` | `true` | OpenCode GO 会话头修复 |
231
230
  | `providerOrder` | 三个 id 全集 | 组件中的显示顺序;在插件设置里拖动卡片调整 |
@@ -370,7 +369,7 @@ npm test
370
369
  测试是封闭的:伪造 `globalThis.fetch` + stub Cordis 上下文,不访问网络、不使用真实
371
370
  密钥。覆盖 schema、三个适配器、OpenCode GO 服务集成/回退、聚合与故障隔离、缓存与
372
371
  强制刷新、设置冲突/白名单、客户端 bundle 契约、充值余额发现(凭据 / 本地
373
- auth.json)与失败隔离、会话头注入、余额变化动画;共 98 个测试。
372
+ auth.json)与失败隔离、会话头注入、余额变化动画;共 102 个测试。
374
373
 
375
374
  ## 许可证
376
375
 
@@ -6,10 +6,12 @@
6
6
  *
7
7
  * - `checkout` (`link:` / `file:` directory): fast-forward the git checkout,
8
8
  * but only when it is on `main`, clean, and can fast-forward `origin/main`.
9
- * - `package` (npm / market / tarball install): download the GitHub release
10
- * tarball and replace the packaged files in place. Every file is written
11
- * to a temp file inside the package directory first and then renamed over
12
- * the target, which keeps pnpm's hardlinked store untouched.
9
+ * - `package` (npm / market / tarball install): pick the newest release
10
+ * known to npm, the npmmirror mirror and GitHub together, then download
11
+ * the release tarball from the first source that serves it and replace
12
+ * the packaged files in place. Every file is written to a temp file
13
+ * inside the package directory first and then renamed over the target,
14
+ * which keeps pnpm's hardlinked store untouched.
13
15
  *
14
16
  * Neither strategy restarts the Host: the running module graph is stale once
15
17
  * the files land, so the UI reports `restartRequired`. The updater never
@@ -28,6 +30,10 @@ import { gunzipSync } from "node:zlib";
28
30
 
29
31
  export const AUTO_UPDATE_REPO = "wjw1-Evan/dsh-provider-usage";
30
32
  export const AUTO_UPDATE_RELEASE_URL = `https://api.github.com/repos/${AUTO_UPDATE_REPO}/releases/latest`;
33
+ /** npm registry — the canonical published package (also the `updateCheck` source). */
34
+ export const NPM_REGISTRY_URL = "https://registry.npmjs.org";
35
+ /** npmmirror mirror of the registry; reachable where npm/GitHub are not. */
36
+ export const NPM_MIRROR_URL = "https://registry.npmmirror.com";
31
37
  /** Delay before the first background check, so activation is never blocked. */
32
38
  const FIRST_CHECK_DELAY_MS = 15_000;
33
39
  /** Hourly tick; the configured check interval gates actual network calls. */
@@ -390,6 +396,124 @@ async function fetchLatestRelease(fetchImpl) {
390
396
  };
391
397
  }
392
398
 
399
+ // ── package release sources ─────────────────────────────────────────────
400
+ // An installed package updates by downloading a release tarball. GitHub is
401
+ // the canonical source, but the registry serves the identical packaged
402
+ // surface and is reachable where GitHub is not (and vice versa), so the
403
+ // checker asks npm, the npmmirror mirror and GitHub together and trusts the
404
+ // newest version any of them reports.
405
+
406
+ /** Build the registry tarball URL for a version. */
407
+ function registryTarballUrl(baseUrl, packageName, version) {
408
+ return `${baseUrl}/${packageName}/-/${packageName}-${version}.tgz`;
409
+ }
410
+
411
+ /** Fetch the latest published version from one npm registry endpoint. */
412
+ async function fetchLatestFromRegistry(baseUrl, packageName, fetchImpl) {
413
+ const res = await fetchImpl(`${baseUrl}/${packageName}/latest`, {
414
+ headers: {
415
+ Accept: "application/vnd.npm.install-v1+json",
416
+ "User-Agent": "dsh-multi-provider-usage"
417
+ },
418
+ signal: AbortSignal.timeout(10_000)
419
+ });
420
+ if (!res.ok) throw new Error(`npm registry check ${res.status}`);
421
+ const doc = await res.json();
422
+ const version = typeof doc?.version === "string" ? doc.version : "";
423
+ if (parseVersion(version) === null) throw new Error("npm registry has no usable version");
424
+ const dist = doc?.dist?.tarball;
425
+ return {
426
+ version,
427
+ tarballUrl: typeof dist === "string" && dist.length > 0 ? dist : registryTarballUrl(baseUrl, packageName, version)
428
+ };
429
+ }
430
+
431
+ /**
432
+ * Pick the newest release known to any package source: npm registry, the
433
+ * npmmirror mirror and the GitHub release. One source lagging or failing is
434
+ * tolerated while any other answers; only when every source fails does the
435
+ * check itself fail. The winning source's tarball URL is the first download
436
+ * candidate.
437
+ */
438
+ async function resolvePackageRelease({ packageName, fetchImpl }) {
439
+ const candidates = [
440
+ { label: "npm", load: () => fetchLatestFromRegistry(NPM_REGISTRY_URL, packageName, fetchImpl) },
441
+ { label: "npmmirror", load: () => fetchLatestFromRegistry(NPM_MIRROR_URL, packageName, fetchImpl) },
442
+ { label: "GitHub", load: () => fetchLatestRelease(fetchImpl) }
443
+ ];
444
+ const results = await Promise.allSettled(candidates.map((candidate) => candidate.load()));
445
+ /** @type {{ version: string, tarballUrl: string }[]} */
446
+ const successes = [];
447
+ /** @type {{ label: string, error: unknown }[]} */
448
+ const failures = [];
449
+ results.forEach((result, index) => {
450
+ if (result.status === "fulfilled") successes.push(result.value);
451
+ else failures.push({ label: candidates[index].label, error: result.reason });
452
+ });
453
+ if (successes.length === 0) {
454
+ throw new Error(`release check failed from all sources — ${describeFailures(failures)}`);
455
+ }
456
+ let best = successes[0];
457
+ for (const candidate of successes.slice(1)) {
458
+ if (isNewerVersion(candidate.version, best.version)) best = candidate;
459
+ }
460
+ return {
461
+ version: best.version,
462
+ // Each answering source's tarball URL, newest-version source first: a
463
+ // source that answered the check is also the likeliest download.
464
+ tarballUrls: [
465
+ best.tarballUrl,
466
+ ...successes.filter((candidate) => candidate !== best).map((candidate) => candidate.tarballUrl)
467
+ ]
468
+ };
469
+ }
470
+
471
+ /** Tarball URLs to try for a resolved release: answering sources first, then
472
+ * the canonical URLs for each source in case the check was partial. */
473
+ function downloadCandidates(release, packageName) {
474
+ const urls = [];
475
+ const add = (url) => {
476
+ if (typeof url === "string" && url.length > 0 && !urls.includes(url)) urls.push(url);
477
+ };
478
+ for (const url of release.tarballUrls ?? []) add(url);
479
+ add(registryTarballUrl(NPM_REGISTRY_URL, packageName, release.version));
480
+ add(registryTarballUrl(NPM_MIRROR_URL, packageName, release.version));
481
+ add(`https://github.com/${AUTO_UPDATE_REPO}/archive/refs/tags/v${release.version}.tar.gz`);
482
+ return urls;
483
+ }
484
+
485
+ /** Short source label for a tarball URL, used in failure messages. */
486
+ function sourceLabelOf(url) {
487
+ const text = String(url);
488
+ if (text.includes("registry.npmjs.org")) return "npm";
489
+ if (text.includes("registry.npmmirror.com")) return "npmmirror";
490
+ if (text.includes("github.com") || text.includes("githubusercontent.com") || text.includes("codeload")) return "GitHub";
491
+ return "release";
492
+ }
493
+
494
+ /** True when the failure is local, so another download source cannot help. */
495
+ function isLocalWriteError(error) {
496
+ const code = typeof error?.code === "string" ? error.code : "";
497
+ if (/^(EACCES|EPERM|EROFS|EXDEV|EBUSY|ENOENT|ENOTDIR|EISDIR)$/.test(code)) return true;
498
+ return /is a symlink|permission denied|not writable/i.test(String(error?.message ?? error));
499
+ }
500
+
501
+ /** Attach an actionable hint to a filesystem-permission failure. */
502
+ function withWriteHint(error) {
503
+ const message = String(error?.message ?? error);
504
+ if (!/^(EACCES|EPERM|EROFS)$/.test(typeof error?.code === "string" ? error.code : "")) return message;
505
+ return `${message} — the package directory is not writable; fix permissions or reinstall the package`;
506
+ }
507
+
508
+ /** Compact, bounded "source: error" summary for combined failure messages.
509
+ * Per-error bounds stay loose enough that an appended hint survives. */
510
+ function describeFailures(failures) {
511
+ return failures
512
+ .map(({ label, error }) => `${label}: ${String(error?.message ?? error).slice(0, 240)}`)
513
+ .join("; ")
514
+ .slice(0, 800);
515
+ }
516
+
393
517
  /**
394
518
  * Start the background checker.
395
519
  *
@@ -462,14 +586,20 @@ export function startAutoUpdate(options) {
462
586
  lastCheckAt = now();
463
587
  patch({ status: "checking", message: null });
464
588
  inFlight = (async () => {
589
+ // A failed release check is an "error" (nothing new is known); a failed
590
+ // apply is "available" (the release is known, the download/write is
591
+ // not). The UI toasts only the latter.
592
+ let phase = "check";
465
593
  try {
466
- const release = await fetchLatestRelease(fetchImpl);
467
594
  const install = detect({ packageDir, profileDir, packageName });
595
+ const release = install.kind === "checkout"
596
+ ? await fetchLatestRelease(fetchImpl)
597
+ : await resolvePackageRelease({ packageName, fetchImpl });
468
598
  patch({
469
599
  latestVersion: release.version,
470
600
  checkedAt: now(),
471
601
  installKind: install.kind,
472
- releaseUrl: release.url
602
+ releaseUrl: release.url ?? null
473
603
  });
474
604
  // `currentVersion` never changes in a running host, so a release
475
605
  // this session already applied would be re-downloaded every cycle
@@ -510,29 +640,50 @@ export function startAutoUpdate(options) {
510
640
  message: "checkout updated; restart DSH to apply"
511
641
  });
512
642
  }
643
+ phase = "apply";
513
644
  // Installed package (npm / market / tarball): replace the packaged
514
- // files in place. The running Host keeps the old module graph until
515
- // it is restarted, which the UI reports.
516
- const result = await applyPackageUpdate({
517
- packageDir,
518
- tarballUrl: release.tarballUrl,
519
- fetchImpl,
520
- packageName,
521
- expectVersion: release.version,
522
- logger
523
- });
524
- logger.info?.(`dsh-multi-provider-usage: self-updated package to ${result.version}; restart DSH to apply`);
525
- return patch({
526
- status: "updated",
527
- version: result.version,
528
- updatedAt: now(),
529
- restartRequired: true,
530
- message: "package files replaced; restart DSH to apply"
531
- });
645
+ // files in place, trying each download source in turn. The running
646
+ // Host keeps the old module graph until it is restarted, which the
647
+ // UI reports.
648
+ /** @type {{ label: string, error: unknown }[]} */
649
+ const failures = [];
650
+ const candidates = downloadCandidates(release, packageName);
651
+ for (const url of candidates) {
652
+ const label = sourceLabelOf(url);
653
+ try {
654
+ const result = await applyPackageUpdate({
655
+ packageDir,
656
+ tarballUrl: url,
657
+ fetchImpl,
658
+ packageName,
659
+ expectVersion: release.version,
660
+ logger
661
+ });
662
+ logger.info?.(`dsh-multi-provider-usage: self-updated package to ${result.version} from ${label}; restart DSH to apply`);
663
+ return patch({
664
+ status: "updated",
665
+ version: result.version,
666
+ updatedAt: now(),
667
+ restartRequired: true,
668
+ message: `package files replaced from ${label}; restart DSH to apply`
669
+ });
670
+ } catch (error) {
671
+ if (isLocalWriteError(error)) {
672
+ // No other download source can fix a local filesystem problem.
673
+ failures.push({ label, error: withWriteHint(error) });
674
+ break;
675
+ }
676
+ failures.push({ label, error });
677
+ }
678
+ }
679
+ const prefix = failures.length === candidates.length
680
+ ? "update download failed from all sources"
681
+ : "update failed";
682
+ throw new Error(`${prefix} — ${describeFailures(failures)}`);
532
683
  } catch (error) {
533
684
  const message = error instanceof Error ? error.message : String(error);
534
685
  logger.warn?.(`dsh-multi-provider-usage: auto-update failed: ${message}`);
535
- return patch({ status: "available", message, restartRequired: false });
686
+ return patch({ status: phase === "check" ? "error" : "available", message, restartRequired: false });
536
687
  } finally {
537
688
  inFlight = null;
538
689
  }
package/lib/client.js CHANGED
@@ -179,11 +179,8 @@ window.__ModuleLoader__.load({
179
179
  "cfg.console.expired": "控制台登录已过期,请重新运行 opencode auth login 后再刷新。",
180
180
  "cfg.console.guide.title": "如何显示充值余额",
181
181
  "cfg.console.guide.step1": "在终端运行以下命令,选择 OpenCode Console 账号登录:",
182
- "cfg.console.guide.step2": "或从控制台网页复制令牌,保存为 DSH 凭据:",
183
- "cfg.console.guide.step3": "完成后点击重新检测,插件会重读登录状态并立即刷新余额,无需重启 DSH。",
184
- "cfg.console.guide.link": "打开 OpenCode 控制台 ↗",
182
+ "cfg.console.guide.step2": "完成后点击重新检测,插件会重读登录状态并立即刷新余额,无需重启 DSH。",
185
183
  "cfg.console.copy.command": "复制命令",
186
- "cfg.console.copy.ref": "复制凭据名",
187
184
  "cfg.console.recheck": "重新检测",
188
185
  "cfg.console.rechecking": "检测中…",
189
186
  "cfg.console.copied": "已复制",
@@ -307,11 +304,8 @@ window.__ModuleLoader__.load({
307
304
  "cfg.console.expired": "The console sign-in expired; run opencode auth login again and refresh.",
308
305
  "cfg.console.guide.title": "How to show the top-up balance",
309
306
  "cfg.console.guide.step1": "Run this command in a terminal and choose the OpenCode Console account:",
310
- "cfg.console.guide.step2": "Or copy the token from the console website and store it as a DSH credential:",
311
- "cfg.console.guide.step3": "When done, click Re-check — the plugin re-reads the sign-in state and refreshes the balance immediately; no DSH restart needed.",
312
- "cfg.console.guide.link": "Open the OpenCode console ↗",
307
+ "cfg.console.guide.step2": "When done, click Re-check — the plugin re-reads the sign-in state and refreshes the balance immediately; no DSH restart needed.",
313
308
  "cfg.console.copy.command": "Copy command",
314
- "cfg.console.copy.ref": "Copy credential ref",
315
309
  "cfg.console.recheck": "Re-check",
316
310
  "cfg.console.rechecking": "Checking…",
317
311
  "cfg.console.copied": "Copied",
@@ -633,8 +627,10 @@ window.__ModuleLoader__.load({
633
627
  if (state.status === "updated" && state.restartRequired === true) {
634
628
  return t("autoUpdate.updated", { v: state.version || state.latestVersion || "?" });
635
629
  }
636
- if (state.status === "available") {
637
- return t("autoUpdate.available", { v: state.latestVersion || "?" });
630
+ // The host only reports "available" after a successful check, so the
631
+ // version is real; the guard keeps a stale "v?" off the widget anyway.
632
+ if (state.status === "available" && typeof state.latestVersion === "string" && state.latestVersion.length > 0) {
633
+ return t("autoUpdate.available", { v: state.latestVersion });
638
634
  }
639
635
  return "";
640
636
  }
@@ -1478,11 +1474,10 @@ window.__ModuleLoader__.load({
1478
1474
  /**
1479
1475
  * The balance setup guide on the OpenCode GO card, shown whenever the
1480
1476
  * console read is not configured or names a fixable reason (API-key
1481
- * login, expired sign-in). Three steps, each with an actionable row:
1482
- * the CLI sign-in command (copyable), the manual credential path with
1483
- * a console link, and a re-check that re-reads the sign-in state and
1484
- * refreshes the balance without restarting DSH. The lead-in names the
1485
- * state-specific fix.
1477
+ * login, expired sign-in). Two steps, each with an actionable row:
1478
+ * the CLI sign-in command (copyable), and a re-check that re-reads
1479
+ * the sign-in state and refreshes the balance without restarting DSH.
1480
+ * The lead-in names the state-specific fix.
1486
1481
  */
1487
1482
  function ConsoleGuide(props) {
1488
1483
  const t = props.t;
@@ -1499,9 +1494,6 @@ window.__ModuleLoader__.load({
1499
1494
  const leadIn = props.reason === "api-key"
1500
1495
  ? t("cfg.console.apiKey")
1501
1496
  : props.reason === "expired" ? t("cfg.console.expired") : t("cfg.console.hint");
1502
- const credentialRef = typeof props.credentialRef === "string" && props.credentialRef !== ""
1503
- ? props.credentialRef
1504
- : "OPENCODE_CONSOLE_TOKEN";
1505
1497
  const command = "opencode auth login";
1506
1498
  const copyKeys = { copiedKey: "cfg.console.copied", failedKey: "cfg.console.copyFailed" };
1507
1499
  const stepRow = (key, text) => react_jsx_runtime.jsx("p", { style: PS.hint, children: text }, key);
@@ -1527,30 +1519,6 @@ window.__ModuleLoader__.load({
1527
1519
  ] }, "guide-step1"),
1528
1520
  react_jsx_runtime.jsx("div", { style: PS.guideStep, children: [
1529
1521
  stepRow("guide-step2-text", t("cfg.console.guide.step2")),
1530
- react_jsx_runtime.jsx("div", {
1531
- style: PS.inviteRow,
1532
- children: [
1533
- react_jsx_runtime.jsx("code", { style: PS.guideCode, children: credentialRef }, "guide-ref"),
1534
- react_jsx_runtime.jsx(CopyButton, {
1535
- t: t,
1536
- value: credentialRef,
1537
- label: t("cfg.console.copy.ref"),
1538
- ...copyKeys
1539
- }, "guide-step2-copy")
1540
- ]
1541
- }, "guide-step2-row"),
1542
- typeof props.consoleUrl === "string" && props.consoleUrl !== ""
1543
- ? react_jsx_runtime.jsx("a", {
1544
- style: PS.inviteLink,
1545
- href: props.consoleUrl,
1546
- target: "_blank",
1547
- rel: "noreferrer",
1548
- children: t("cfg.console.guide.link")
1549
- }, "guide-link")
1550
- : null
1551
- ] }, "guide-step2"),
1552
- react_jsx_runtime.jsx("div", { style: PS.guideStep, children: [
1553
- stepRow("guide-step3-text", t("cfg.console.guide.step3")),
1554
1522
  react_jsx_runtime.jsx("button", {
1555
1523
  type: "button",
1556
1524
  style: checking || props.disabled === true
@@ -1560,7 +1528,7 @@ window.__ModuleLoader__.load({
1560
1528
  onClick: recheck,
1561
1529
  children: checking ? t("cfg.console.rechecking") : t("cfg.console.recheck")
1562
1530
  }, "guide-recheck")
1563
- ] }, "guide-step3")
1531
+ ] }, "guide-step2")
1564
1532
  ]
1565
1533
  });
1566
1534
  }
@@ -1855,15 +1823,6 @@ window.__ModuleLoader__.load({
1855
1823
  const server = formFromValue(state.value || {});
1856
1824
  const dirty = Object.keys(form).some((key) => !fieldEquals(form[key], server[key]));
1857
1825
 
1858
- // The balance guide's manual path: the configured console credential
1859
- // ref (default OPENCODE_CONSOLE_TOKEN) and the console website
1860
- // derived from the configured console origin.
1861
- const consoleValue = state.value && typeof state.value === "object" ? state.value : {};
1862
- const consoleRef = typeof consoleValue.opencodeConsoleApiKeyEnv === "string" && consoleValue.opencodeConsoleApiKeyEnv !== ""
1863
- ? consoleValue.opencodeConsoleApiKeyEnv
1864
- : "OPENCODE_CONSOLE_TOKEN";
1865
- const consoleLink = String(consoleValue.opencodeConsoleBaseUrl || "https://opencode.ai").replace(/\/+$/, "") + "/console";
1866
-
1867
1826
  /**
1868
1827
  * Re-check the console balance: re-read the detection report
1869
1828
  * (configured / reason) and force one upstream usage read, so a
@@ -2029,8 +1988,6 @@ window.__ModuleLoader__.load({
2029
1988
  // session) names the fix in the guide's lead-in
2030
1989
  // instead of the generic hint.
2031
1990
  reason: consoleFixable ? consoleInfo.reason : "",
2032
- credentialRef: consoleRef,
2033
- consoleUrl: consoleLink,
2034
1991
  disabled: disabled,
2035
1992
  onRecheck: recheckBalance
2036
1993
  }, "console-guide"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-multi-provider-usage",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "DSH (DeepSeek Harness) plugin: one collapsible sidebar widget for OpenCode GO, Command Code and GLM Code Plan quotas plus top-up balances, a periodic balance-change animation, automatic checkout and installed-package updates, same-origin usage proxies, a Plugins-panel settings page and a /provider-usage chat command (DSH 0.1.7+ and 0.2.0 compatible)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",