dsh-multi-provider-usage 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/README.zh.md +7 -6
- package/lib/auto-update.js +176 -25
- package/lib/client.js +4 -2
- package/package.json +1 -1
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
@@ -267,7 +269,7 @@ the settings page and the auto-generated form hot-apply.
|
|
|
267
269
|
| `glmRegion` | `"auto"` | `auto`, `zai`, `bigmodel`, `custom` |
|
|
268
270
|
| `cacheMs` | `30000` | Per-provider host cache TTL |
|
|
269
271
|
| `updateCheck` | `true` | npm update check (24 h) |
|
|
270
|
-
| `autoUpdate` | `true` | Check GitHub
|
|
272
|
+
| `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
273
|
| `autoUpdateCheckHours` | `6` | Hours between auto-update checks (1–168) |
|
|
272
274
|
| `injectSessionHeader` | `true` | OpenCode GO session-header fix |
|
|
273
275
|
| `providerOrder` | all three ids | Provider display order in the widget; drag the cards in the plugin settings to reorder |
|
package/README.zh.md
CHANGED
|
@@ -34,11 +34,12 @@
|
|
|
34
34
|
设置页内为分步引导:可复制的登录命令、控制台网页链接与「重新检测」按钮。
|
|
35
35
|
Command Code 的悬停提示会拆成「购买 + 免费」;credits 响应没有这两个池时显示
|
|
36
36
|
灰色「无充值额度」。
|
|
37
|
-
- **自动更新** —
|
|
38
|
-
`link:`/`file:`
|
|
39
|
-
`git pull`;npm / 市场 / tarball
|
|
40
|
-
|
|
41
|
-
|
|
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`)
|
|
@@ -225,7 +226,7 @@ Loader 直接读取导出的 `Config` schema;所有字段都是 `.volatile()`
|
|
|
225
226
|
| `glmRegion` | `"auto"` | `auto`、`zai`、`bigmodel`、`custom` |
|
|
226
227
|
| `cacheMs` | `30000` | 单个服务商的宿主缓存 TTL |
|
|
227
228
|
| `updateCheck` | `true` | npm 新版本检查(24 小时) |
|
|
228
|
-
| `autoUpdate` | `true` |
|
|
229
|
+
| `autoUpdate` | `true` | 同时查询 npm / npmmirror / GitHub Release;干净 checkout 快进,安装包按可达的源依次下载 tarball 原地替换 |
|
|
229
230
|
| `autoUpdateCheckHours` | `6` | 自动更新检查间隔(1–168 小时) |
|
|
230
231
|
| `injectSessionHeader` | `true` | OpenCode GO 会话头修复 |
|
|
231
232
|
| `providerOrder` | 三个 id 全集 | 组件中的显示顺序;在插件设置里拖动卡片调整 |
|
package/lib/auto-update.js
CHANGED
|
@@ -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):
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
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
|
|
515
|
-
// it is restarted, which the
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
@@ -633,8 +633,10 @@ window.__ModuleLoader__.load({
|
|
|
633
633
|
if (state.status === "updated" && state.restartRequired === true) {
|
|
634
634
|
return t("autoUpdate.updated", { v: state.version || state.latestVersion || "?" });
|
|
635
635
|
}
|
|
636
|
-
|
|
637
|
-
|
|
636
|
+
// The host only reports "available" after a successful check, so the
|
|
637
|
+
// version is real; the guard keeps a stale "v?" off the widget anyway.
|
|
638
|
+
if (state.status === "available" && typeof state.latestVersion === "string" && state.latestVersion.length > 0) {
|
|
639
|
+
return t("autoUpdate.available", { v: state.latestVersion });
|
|
638
640
|
}
|
|
639
641
|
return "";
|
|
640
642
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-multi-provider-usage",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|