dsh-xdpool 2.6.2 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,33 @@
1
1
  # Changelog
2
2
  本项目的所有重要变更都会记录在此文件。
3
3
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+ ## 2.6.3 — 修掉 2.6.2 装上后插件起不来的问题
5
+
6
+ > 2.6.2 从 npm 安装后,插件导入失败(`Cannot find package '@earendil-works/pi-ai'`),
7
+ > 于是它的所有 web 路由都没注册 —— 积分接口 404,用量面板空白,并显示
8
+ > 「网关尚未上报 token 数」。这句话把问题指向了网关,实际是插件自己没加载起来。
9
+
10
+ ### 缺 7 个 peer 依赖
11
+
12
+ 2.6.2 有 7 个 peerDependency(`cordis`、`dsh-llm`、`dsh-llm-pi-ai`、`dsh-credentials`、
13
+ `dsh-settings`、`schemastery`、`pi-ai`),这些由宿主提供。手动复制目录时它们从宿主的
14
+ `node_modules` 可见;改用包管理器安装后,解析链断了。
15
+
16
+ 现在 `postinstall` 会把这些包从宿主链接进 profile,版本与宿主完全一致,不额外占用空间。
17
+ 装完自带一次导入自检,失败会直接把原因写进日志,而不是等用户看到空白面板才发现。
18
+
19
+ ### 今日用量的三个缺陷
20
+
21
+ **日期用了 UTC**:`toISOString()` 取的是 UTC 日期,在 UTC+8 意味着凌晨 0–8 点算成前一天。
22
+ 现在按本地日历取值。
23
+
24
+ **`days: 1` 实际覆盖两天**:窗口下界原先减了整个 `days`,于是「今日」包含了昨天。
25
+ 改成 `days - 1`,标签与口径一致。
26
+
27
+ **重启后归零**:`UsageRecorder` 只存内存,重启即空。现在启动时从渠道包的持久账本
28
+ (保留 90 天)回填当天用量 —— 那些数据一直在磁盘上,只是从没被读回来。
29
+ 回填是尽力而为的:读不到只损失重启前的数字,不影响插件。
30
+
4
31
  ## 2.6.2 — 积分统计、账号备份、输入框积分显示
5
32
 
6
33
  > 这一版把「用了多少积分」「还剩多少」「账号怎么搬走」三件事做完了,并修掉了几个把正确数据说错的地方。
package/lib/client.js CHANGED
@@ -13460,7 +13460,7 @@ window.__ModuleLoader__.load({
13460
13460
  *
13461
13461
  * @module dsh-xdpool/version
13462
13462
  */
13463
- const POOL_VERSION = "2.6.2";
13463
+ const POOL_VERSION = "2.6.3";
13464
13464
  //#endregion
13465
13465
  //#region src/client/PoolCard.tsx
13466
13466
  /**
package/lib/index.d.ts CHANGED
@@ -2987,6 +2987,28 @@ declare class UsageRecorder {
2987
2987
  size(): number;
2988
2988
  /** Forget every retained call. */
2989
2989
  reset(): void;
2990
+ /**
2991
+ * Refill today's samples from the pack's own ledger.
2992
+ *
2993
+ * WHY THIS EXISTS
2994
+ *
2995
+ * `samples` is process memory, so every restart emptied the usage panel and the pool
2996
+ * card — "网关尚未上报 token 数" after a restart was not the gateway declining to
2997
+ * answer, it was this recorder having forgotten. The pack, meanwhile, persists a 90-day
2998
+ * day-aggregated ledger on disk, so the same period was already recorded and simply not
2999
+ * read.
3000
+ *
3001
+ * Called ONCE at startup, before the first request. It REPLACES rather than appends:
3002
+ * whatever is in memory at that moment is either empty (a fresh process) or belongs to
3003
+ * the same ledger entries, and adding both would double every figure.
3004
+ *
3005
+ * The rows are synthesised at DAY granularity — one `UsageCall` per (channel, model) with
3006
+ * the day's totals — because that is the resolution the ledger keeps. Per-call timestamps
3007
+ * are not recoverable, so the panel's "recent calls" list stays empty after a restart
3008
+ * while its totals are right; inventing timestamps to fill the list would be worse than
3009
+ * an empty list.
3010
+ */
3011
+ backfill(calls: readonly UsageCall[]): void;
2990
3012
  /** Aggregate the retained window. */
2991
3013
  summary(): UsageSummary;
2992
3014
  /**
package/lib/index.js CHANGED
@@ -11862,6 +11862,31 @@ var UsageRecorder = class {
11862
11862
  reset() {
11863
11863
  this.samples = [];
11864
11864
  }
11865
+ /**
11866
+ * Refill today's samples from the pack's own ledger.
11867
+ *
11868
+ * WHY THIS EXISTS
11869
+ *
11870
+ * `samples` is process memory, so every restart emptied the usage panel and the pool
11871
+ * card — "网关尚未上报 token 数" after a restart was not the gateway declining to
11872
+ * answer, it was this recorder having forgotten. The pack, meanwhile, persists a 90-day
11873
+ * day-aggregated ledger on disk, so the same period was already recorded and simply not
11874
+ * read.
11875
+ *
11876
+ * Called ONCE at startup, before the first request. It REPLACES rather than appends:
11877
+ * whatever is in memory at that moment is either empty (a fresh process) or belongs to
11878
+ * the same ledger entries, and adding both would double every figure.
11879
+ *
11880
+ * The rows are synthesised at DAY granularity — one `UsageCall` per (channel, model) with
11881
+ * the day's totals — because that is the resolution the ledger keeps. Per-call timestamps
11882
+ * are not recoverable, so the panel's "recent calls" list stays empty after a restart
11883
+ * while its totals are right; inventing timestamps to fill the list would be worse than
11884
+ * an empty list.
11885
+ */
11886
+ backfill(calls) {
11887
+ this.samples = [];
11888
+ for (const call of calls) this.record(call);
11889
+ }
11865
11890
  /** Aggregate the retained window. */
11866
11891
  summary() {
11867
11892
  const rows = /* @__PURE__ */ new Map();
@@ -21172,10 +21197,25 @@ function numberOf(value) {
21172
21197
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
21173
21198
  }
21174
21199
  /** `2026-11-07` minus N days, as the same string shape. */
21200
+ /**
21201
+ * A `YYYY-MM-DD` key for `days` ago, in the LOCAL calendar.
21202
+ *
21203
+ * Not `toISOString()`: that is UTC, and the ledger's day keys are whatever the machine's
21204
+ * clock says when the pack writes them — i.e. local. Mixing the two shifts the window by
21205
+ * the UTC offset, so a user in UTC+8 reading "today" between midnight and 08:00 was shown
21206
+ * the previous day's totals. The bug is invisible for most of the day and wrong for
21207
+ * exactly the hours someone checks whether a long run has spent much.
21208
+ *
21209
+ * Built from local parts rather than by subtracting milliseconds, because a DST boundary
21210
+ * would otherwise land on the wrong day.
21211
+ */
21175
21212
  function daysAgoKey(days, now = /* @__PURE__ */ new Date()) {
21176
21213
  const copy = new Date(now.getTime());
21177
- copy.setUTCDate(copy.getUTCDate() - days);
21178
- return copy.toISOString().slice(0, 10);
21214
+ copy.setDate(copy.getDate() - days);
21215
+ const year = copy.getFullYear();
21216
+ const month = String(copy.getMonth() + 1).padStart(2, "0");
21217
+ const day = String(copy.getDate()).padStart(2, "0");
21218
+ return `${String(year)}-${month}-${day}`;
21179
21219
  }
21180
21220
  /** Split `mode|channel|account|model`, refusing anything that is not four parts. */
21181
21221
  function parseLedgerKey(key) {
@@ -21224,7 +21264,15 @@ async function readCreditSpend(input) {
21224
21264
  }
21225
21265
  const daysNode = parsed.days;
21226
21266
  if (daysNode === null || typeof daysNode !== "object") return empty;
21227
- const earliest = input.days === void 0 ? void 0 : daysAgoKey(input.days, input.now);
21267
+ /**
21268
+ * `days: 1` means TODAY ALONE, not "today plus yesterday".
21269
+ *
21270
+ * The old arithmetic subtracted the window size before taking the bound
21271
+ * (`daysAgoKey(days)`), so a one-day window covered today AND yesterday: a strip labelled
21272
+ * "today" reported a figure that included yesterday evening's work. `days - 1` makes the
21273
+ * label true — one day back from today is the start of today.
21274
+ */
21275
+ const earliest = input.days === void 0 ? void 0 : daysAgoKey(Math.max(0, input.days - 1), input.now);
21228
21276
  const acc = /* @__PURE__ */ new Map();
21229
21277
  let unreadable = 0;
21230
21278
  let gatewayRequests = 0;
@@ -21292,6 +21340,86 @@ async function readCreditSpend(input) {
21292
21340
  empty: !sawAnyDay
21293
21341
  };
21294
21342
  }
21343
+ /**
21344
+ * Refill a `UsageRecorder` with TODAY's totals from the pack ledger.
21345
+ *
21346
+ * The recorder keeps process memory only, so every restart emptied the usage panel and the
21347
+ * pool card printed "the gateway reported no token counts" — an accusation aimed at the
21348
+ * gateway for something this plugin had forgotten. The pack already persists a 90-day
21349
+ * ledger, so the same period was on disk the whole time.
21350
+ *
21351
+ * Day granularity on purpose: the ledger aggregates per (channel, account, model) per day
21352
+ * and keeps no per-call timestamps, so one `UsageCall` is synthesised per row carrying the
21353
+ * day's totals. That makes the TOTALS correct after a restart while the panel's
21354
+ * recent-calls list stays empty — inventing timestamps to fill a list would be worse than
21355
+ * an empty list.
21356
+ *
21357
+ * Returns how many rows were restored, so a startup note can say whether it worked.
21358
+ */
21359
+ async function backfillUsageFromLedger(home, recorder, logger) {
21360
+ let text;
21361
+ try {
21362
+ text = await readFile(join(home, LEDGER_FILE), "utf8");
21363
+ } catch {
21364
+ return 0;
21365
+ }
21366
+ let parsed;
21367
+ try {
21368
+ parsed = JSON.parse(text);
21369
+ } catch {
21370
+ logger?.warn?.("dsh-xdpool: token ledger is not readable JSON; usage starts empty");
21371
+ return 0;
21372
+ }
21373
+ const daysNode = parsed.days;
21374
+ if (daysNode === null || typeof daysNode !== "object") return 0;
21375
+ const today = daysKey(/* @__PURE__ */ new Date());
21376
+ const day = daysNode[today];
21377
+ if (day === null || day === void 0 || typeof day !== "object") return 0;
21378
+ const calls = [];
21379
+ for (const [key, raw] of Object.entries(day)) {
21380
+ const named = parseLedgerKey(key);
21381
+ if (named === void 0 || named.mode !== POOL_MODE) continue;
21382
+ if (raw === null || typeof raw !== "object") continue;
21383
+ const entry = raw;
21384
+ const requests = numberOf(entry.requests);
21385
+ const input = numberOf(entry.inputTokens);
21386
+ const cache = numberOf(entry.cacheReadTokens);
21387
+ const output = numberOf(entry.outputTokens);
21388
+ calls.push({
21389
+ channel: named.channel,
21390
+ region: named.channel === "workbuddy" ? "global" : named.channel === "buddy" ? "cn" : "",
21391
+ model: named.model,
21392
+ account: "",
21393
+ turnId: `backfill:${today}:${named.channel}:${named.model}`,
21394
+ at: startOfLocalDay(/* @__PURE__ */ new Date()).getTime(),
21395
+ ok: requests > numberOf(entry.errors),
21396
+ usage: {
21397
+ inputTokens: input,
21398
+ outputTokens: output,
21399
+ cachedTokens: cache,
21400
+ reasoningTokens: numberOf(entry.reasoningTokens)
21401
+ }
21402
+ });
21403
+ }
21404
+ if (calls.length === 0) return 0;
21405
+ recorder.reset();
21406
+ for (const call of calls) recorder.record(call);
21407
+ logger?.info?.(`dsh-xdpool: restored ${String(calls.length)} usage row(s) for ${today} from the ledger`);
21408
+ return calls.length;
21409
+ }
21410
+ /** Local `YYYY-MM-DD`, matching how the pack names its day buckets. */
21411
+ function daysKey(now) {
21412
+ const year = now.getFullYear();
21413
+ const month = String(now.getMonth() + 1).padStart(2, "0");
21414
+ const day = String(now.getDate()).padStart(2, "0");
21415
+ return `${String(year)}-${month}-${day}`;
21416
+ }
21417
+ /** Midnight at the start of `now`'s local day. */
21418
+ function startOfLocalDay(now) {
21419
+ const copy = new Date(now.getTime());
21420
+ copy.setHours(0, 0, 0, 0);
21421
+ return copy;
21422
+ }
21295
21423
  //#endregion
21296
21424
  //#region src/relay.ts
21297
21425
  /**
@@ -23301,6 +23429,21 @@ function apply(ctx, config = {}) {
23301
23429
  recordStartupNote(`registration FAILED: ${describeError(error)}`);
23302
23430
  return;
23303
23431
  }
23432
+ /**
23433
+ * Refill today's usage from the pack's ledger before anything is served.
23434
+ *
23435
+ * `UsageRecorder` holds process memory, so a restart used to empty the usage panel
23436
+ * and the pool card — the card then printed "网关尚未上报 token 数", which was never
23437
+ * true: nothing had asked yet. The pack keeps a 90-day ledger on disk, so the same
23438
+ * day was already recorded and simply not read back.
23439
+ *
23440
+ * Best-effort and fire-and-forget: a ledger that cannot be read costs the panel its
23441
+ * pre-restart totals, never the plugin. It is deliberately not awaited — the first
23442
+ * request must not wait on a file read.
23443
+ */
23444
+ backfillUsageFromLedger(dshHome(), usageRecorder, ctx.logger).catch((error) => {
23445
+ ctx.logger.warn?.("dsh-xdpool: usage backfill failed", error);
23446
+ });
23304
23447
  if (stopped) return;
23305
23448
  core.pool.scan().then((accounts) => {
23306
23449
  ctx.logger.info?.(`dsh-xdpool: ${accounts.length} WorkBuddy account(s) in rotation`);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-xdpool",
3
3
  "displayName": "DSH XD Pool",
4
4
  "description": "One DSH model pool for every free quota you have: merge all locally signed-in WorkBuddy accounts with auto-failover, sign in to 13 vendors by QR/web, aggregate 76 vendors' free tiers, plus a usage dashboard, model routing, a local OpenAI-compatible gateway and a points-automation suite.",
5
- "version": "2.6.2",
5
+ "version": "2.6.3",
6
6
  "license": "MIT",
7
7
  "author": "XDTrees",
8
8
  "homepage": "https://github.com/XDTrees/dsh-xdpool#readme",