dsh-workbuddy-xdpool 1.0.0 → 1.0.1

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
@@ -4,6 +4,36 @@
4
4
 
5
5
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
6
6
 
7
+ ## 1.0.1 (2026-09-23)
8
+
9
+ 修复两个用号逻辑上的缺陷,并把账号卡片重新排了一遍。
10
+
11
+ ### 一、「当前使用」显示的账号不对
12
+
13
+ 卡片上显示的账号,是你**最近一次实际在用**的那个吗?之前不是 —— 它显示的是「**准备尝试**」的账号。
14
+
15
+ 举个例子:某个账号积分用光了,插件去试它、失败、自动换到另一个号继续服务。但你确实在用(换号成功了),卡片却还显示那个已经废掉的号。
16
+
17
+ 现在改成**只有请求真正成功**才记录,所以显示的永远是你实际在用的那个。
18
+
19
+ ### 二、账号积分用尽时,会直接把错误抛给你
20
+
21
+ 上游有两种"不让用":
22
+
23
+ | 情况 | 含义 | 之前的表现 |
24
+ | --- | --- | --- |
25
+ | **频率限制** | 这个号暂时被限流,等会儿能恢复 | 正确:冷却这个号,**自动换号** |
26
+ | **额度用尽** | 这个号这个周期的积分花完了 | ❌ **直接报错给你**,不换号 |
27
+
28
+ 现在额度用尽也会**自动换到有积分的账号**,并把那个号整体停用 30 分钟(不是只停用某个模型)。请求不会再因此失败。
29
+
30
+ ### 三、账号卡片排版重做
31
+
32
+ - 每个账号的**开关**做成了胶囊按钮(和「账号使用方式」同一套样式),一眼能看出是开还是关
33
+ - 账号卡**分层更清楚**:标题行(名字|状态|开关)→ 分隔线 → 信息与积分两栏
34
+ - 去掉了多余的边框和重复信息(不再显示 `www.workbuddy.cn` 这类域名 —— 上面的区域标签已经说明了)
35
+ - 新增顶部「**当前使用**」提示条,一眼看到正在用哪个号
36
+
7
37
  ## 1.0.0 (2026-09-21)
8
38
 
9
39
  这一版把「账号」和「用号方式」都交给你控制。
package/lib/bin.js CHANGED
@@ -778,10 +778,18 @@ function idleWeight(lastUsedAt, now) {
778
778
  const hours = (now - lastUsedAt) / 36e5;
779
779
  return 1 + Math.min(Math.max(hours, 0) * IDLE_WEIGHT_PER_HOUR, IDLE_WEIGHT_MAX);
780
780
  }
781
+ /** Default rest for an account whose credits ran out (packs reset on their own schedule). */
782
+ const EXHAUST_COOLDOWN_MS = 18e5;
781
783
  var WorkBuddyAccountPool = class {
782
784
  logger;
783
785
  authDirs;
784
786
  cooldownMs;
787
+ /**
788
+ * How long an account stays out of rotation after the upstream reports its
789
+ * credits are spent. Credit packs reset on their own schedule rather than on a
790
+ * rate-limit window, so this is much longer than `cooldownMs`.
791
+ */
792
+ exhaustCooldownMs;
785
793
  client;
786
794
  refreshMarginMs;
787
795
  accounts = [];
@@ -813,6 +821,7 @@ var WorkBuddyAccountPool = class {
813
821
  this.logger = options.logger;
814
822
  this.authDirs = options.authDirs ?? candidateAuthDirs();
815
823
  this.cooldownMs = options.cooldownMs ?? 6e4;
824
+ this.exhaustCooldownMs = options.exhaustCooldownMs ?? EXHAUST_COOLDOWN_MS;
816
825
  this.client = options.client;
817
826
  this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
818
827
  this.distribution = options.distribution ?? "priority";
@@ -825,6 +834,7 @@ var WorkBuddyAccountPool = class {
825
834
  applyConfig(options) {
826
835
  if (options.authDirs !== void 0 && options.authDirs.length > 0) this.authDirs = options.authDirs;
827
836
  if (options.cooldownMs !== void 0 && options.cooldownMs >= 1e3) this.cooldownMs = options.cooldownMs;
837
+ if (options.exhaustCooldownMs !== void 0 && options.exhaustCooldownMs >= 1e3) this.exhaustCooldownMs = options.exhaustCooldownMs;
828
838
  if (options.distribution !== void 0) this.distribution = options.distribution;
829
839
  if (options.disabledAccountIds !== void 0) this.disabledIds = new Set(options.disabledAccountIds);
830
840
  }
@@ -951,14 +961,12 @@ var WorkBuddyAccountPool = class {
951
961
  if (this.preferredId !== void 0) {
952
962
  const preferred = pool.find((account) => account.id === this.preferredId);
953
963
  if (preferred !== void 0) {
954
- this.lastUsedAt.set(preferred.id, Date.now());
955
964
  await this.ensureFresh(preferred);
956
965
  return preferred;
957
966
  }
958
967
  }
959
968
  const account = this.distribution === "round-robin" ? this.pickRoundRobin(pool) : this.distribution === "balanced" ? this.pickByWeight(pool) : pool[0];
960
969
  if (account === void 0) return void 0;
961
- this.lastUsedAt.set(account.id, Date.now());
962
970
  await this.ensureFresh(account);
963
971
  return account;
964
972
  }
@@ -978,6 +986,40 @@ var WorkBuddyAccountPool = class {
978
986
  disabledIdsInOrder() {
979
987
  return this.accounts.filter((account) => this.disabledIds.has(account.id)).map((account) => account.id);
980
988
  }
989
+ /**
990
+ * Record that an account actually served a request.
991
+ *
992
+ * Called by the shim once the upstream answers 200 — only then is the account
993
+ * the one the user is really being served by. `balanced` mode reads the same map
994
+ * for its idle weighting, so a request that failed over to another account must
995
+ * not count as used for the account that was merely tried.
996
+ */
997
+ noteServed(accountId) {
998
+ if (!this.accounts.some((account) => account.id === accountId)) return;
999
+ this.lastUsedAt.set(accountId, Date.now());
1000
+ }
1001
+ /**
1002
+ * The account that served the most recent request, if any.
1003
+ *
1004
+ * Distinct from "who would serve the next one": this is a record of what
1005
+ * actually happened, which is what the card needs to answer "which account am
1006
+ * I using right now?". Under `balanced` there is no deterministic next account
1007
+ * at all, so a recorded fact is the only honest answer.
1008
+ *
1009
+ * Returns undefined before the first request of the process, and after every
1010
+ * known account has been re-scanned away (a login swapped out under us).
1011
+ */
1012
+ lastServedId() {
1013
+ let newest;
1014
+ for (const [id, at] of this.lastUsedAt) {
1015
+ if (!this.accounts.some((account) => account.id === id)) continue;
1016
+ if (newest === void 0 || at > newest.at) newest = {
1017
+ id,
1018
+ at
1019
+ };
1020
+ }
1021
+ return newest?.id;
1022
+ }
981
1023
  /** Best-effort refresh of one account after a session-dead upstream answer. */
982
1024
  async refreshAccount(accountId) {
983
1025
  const account = this.accounts.find((item) => item.id === accountId);
@@ -1028,6 +1070,23 @@ var WorkBuddyAccountPool = class {
1028
1070
  }
1029
1071
  }
1030
1072
  /**
1073
+ * Cool a whole account after the upstream reports its credits are spent.
1074
+ *
1075
+ * Credit exhaustion is an ACCOUNT condition, unlike a model rate limit: every
1076
+ * model on that account is unusable until the quota resets, so this cools the
1077
+ * account as a whole (no `modelId`) for the configured exhaustion window. The
1078
+ * shim then rotates to a different account instead of failing the request.
1079
+ */
1080
+ penalizeExhausted(accountId) {
1081
+ const until = Date.now() + this.exhaustCooldownMs;
1082
+ this.penalize(accountId, until);
1083
+ this.logger?.warn(`
1084
+ dsh-workbuddy-xdpool: account credits exhausted; cooling the whole account until
1085
+
1086
+ ${new Date(until).toISOString()}
1087
+ `);
1088
+ }
1089
+ /**
1031
1090
  * Mark an account (or one of its models) rate-limited.
1032
1091
  *
1033
1092
  * With `modelId`, only that model on the account is cooled — the account's
package/lib/client.js CHANGED
@@ -28,8 +28,6 @@ window.__ModuleLoader__.load({
28
28
  const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/cooldowns/reset";
29
29
  /** Plugin-owned daily check-in action endpoint (claim today's reward). */
30
30
  const POOL_CHECKIN_PATH = "/plugins/dsh-workbuddy-xdpool/checkin";
31
- /** Switch one account in or out of the pool (card toggle). */
32
- const POOL_ACCOUNT_DISABLE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/disabled";
33
31
  //#endregion
34
32
  //#region src/client/icon.ts
35
33
  /**
@@ -130,18 +128,32 @@ window.__ModuleLoader__.load({
130
128
  /* Account list (each account = a labeled subpanel, same as dingminhua). */
131
129
  .dsm-workbuddy-xdpool-accounts{display:flex;flex-direction:column;gap:14px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);padding-top:14px}
132
130
  .dsm-workbuddy-xdpool-accounts-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
131
+ /* "In use now": answers which account is serving without scanning every row. */
132
+ /* A hairline + tint reads as status; a filled bar would read as a call to action. */
133
+ .dsm-workbuddy-xdpool-current{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin:10px 0 0;padding:9px 13px;border:1px solid color-mix(in oklab, var(--dsw-alias-state-success-primary,#22a06b) 32%, transparent);border-radius:12px;background:color-mix(in oklab, var(--dsw-alias-state-success-primary,#22a06b) 9%, transparent)}
134
+ .dsm-workbuddy-xdpool-current-dot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dsw-alias-state-success-primary,#22a06b);box-shadow:0 0 0 3px color-mix(in oklab, var(--dsw-alias-state-success-primary,#22a06b) 18%, transparent)}
135
+ .dsm-workbuddy-xdpool-current-label{color:var(--dsw-alias-state-success-primary,#22a06b);font-size:11.5px;line-height:18px;font-weight:600;letter-spacing:.02em;flex:none}
136
+ .dsm-workbuddy-xdpool-current-name{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:13px;line-height:18px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
137
+ .dsm-workbuddy-xdpool-current-note{color:var(--dsw-alias-state-warning-primary,#d97706);font-size:11px;line-height:16px}
133
138
  .dsm-workbuddy-xdpool-accounts-title{margin:0;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:14px;font-weight:600;line-height:20px}
134
139
  .dsm-workbuddy-xdpool-accounts-summary{margin:2px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
135
- .dsm-workbuddy-xdpool-account{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:14px;background:var(--dsw-alias-bg-layer-2,#24262c)}
136
- .dsm-workbuddy-xdpool-account-copy{display:flex;flex-direction:column;gap:3px;min-width:0}
140
+ .dsm-workbuddy-xdpool-account{display:flex;flex-direction:column;gap:0;padding:0;border:1px solid color-mix(in oklab, var(--dsw-alias-border-l2,#3a3d45) 55%, transparent);border-radius:16px;background:var(--dsw-alias-bg-layer-2,#24262c);box-shadow:0 1px 3px rgba(0,0,0,.04);overflow:hidden}
141
+ /* Body row inside a card: identity on the left, credit panels on the right. */
142
+ /* flex-wrap lets the panels drop below on a narrow card instead of squeezing both. */
143
+ .dsm-workbuddy-xdpool-account-body{display:flex;align-items:stretch;gap:0;flex-wrap:wrap}
144
+ .dsm-workbuddy-xdpool-account-copy{display:flex;flex-direction:column;align-items:flex-start;gap:6px;flex:1 1 200px;min-width:0;padding:14px 16px;justify-content:center}
137
145
  .dsm-workbuddy-xdpool-account-label{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:14px;font-weight:600;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
138
- .dsm-workbuddy-xdpool-account-head{display:flex;align-items:center;gap:8px;min-width:0}
146
+ .dsm-workbuddy-xdpool-account-head .dsm-workbuddy-xdpool-account-toggle{margin-left:auto}
147
+ .dsm-workbuddy-xdpool-account-head{display:flex;align-items:center;gap:10px;min-width:0;padding:12px 16px;border-bottom:1px solid color-mix(in oklab, var(--dsw-alias-border-l2,#3a3d45) 45%, transparent)}
139
148
  /* Account switched off on the card: still listed (so it can be turned back on) but visually muted. */
140
149
  .dsm-workbuddy-xdpool-account-off{opacity:.55}
141
150
  /* Small pill switch: "in rotation" vs "off". Native checkbox styled by the label. */
142
- .dsm-workbuddy-xdpool-account-toggle{display:inline-flex;align-items:center;gap:5px;flex:none;cursor:pointer;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:11px;line-height:16px;white-space:nowrap}
143
- .dsm-workbuddy-xdpool-account-toggle input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
144
- .dsm-workbuddy-xdpool-account-toggle:hover{color:var(--dsw-alias-label-primary,#e6e6e6)}
151
+ .dsm-workbuddy-xdpool-account-toggle{appearance:none;font:inherit;display:inline-flex;align-items:center;gap:5px;cursor:pointer;flex:none;border:1px solid color-mix(in oklab, var(--dsw-alias-border-l2,#3a3d45) 70%, transparent);border-radius:999px;padding:3px 10px;font-size:11px;line-height:17px;background:transparent;color:var(--dsw-alias-label-tertiary,#9aa0a8);transition:color .16s,border-color .16s,background .16s}
152
+ .dsm-workbuddy-xdpool-account-toggle:hover:not(:disabled){color:var(--dsw-alias-label-primary,#e6e6e6);border-color:var(--dsw-alias-label-dimmed,#777)}
153
+ .dsm-workbuddy-xdpool-account-toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
154
+ .dsm-workbuddy-xdpool-account-toggle:disabled{cursor:default;opacity:.6}
155
+ .dsm-workbuddy-xdpool-account-toggle-on{background:color-mix(in oklab, var(--dsw-alias-state-success-primary,#22a06b) 14%, transparent);border-color:color-mix(in oklab, var(--dsw-alias-state-success-primary,#22a06b) 55%, transparent);color:var(--dsw-alias-state-success-primary,#22a06b);font-weight:600}
156
+ .dsm-workbuddy-xdpool-account-toggle-dot{width:6px;height:6px;border-radius:50%;flex:none;background:currentColor;opacity:.9}
145
157
  .dsm-workbuddy-xdpool-account-tags{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
146
158
  .dsm-workbuddy-xdpool-account-tag{padding:1px 8px;border-radius:999px;font-size:11px;line-height:18px;background:var(--dsw-alias-state-success-subtle,rgba(34,160,107,.12));color:var(--dsw-alias-state-success-primary,#22a06b)}
147
159
  .dsm-workbuddy-xdpool-account-tag-cooling{background:var(--dsw-alias-state-warning-subtle,rgba(217,119,6,.15));color:var(--dsw-alias-state-warning-primary,#d97706)}
@@ -152,9 +164,9 @@ window.__ModuleLoader__.load({
152
164
  .dsm-workbuddy-xdpool-account-error{margin:0;color:var(--dsw-alias-state-error-primary,#ef4444);font-size:13px;line-height:20px}
153
165
 
154
166
  /* Two-column stats: packages on the left, total + check-in on the right. */
155
- .dsm-workbuddy-xdpool-stats{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(170px,.85fr);gap:10px;margin-top:10px}
156
- .dsm-workbuddy-xdpool-panel{display:flex;flex-direction:column;min-width:0;gap:7px;padding:14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#24262c)}
157
- .dsm-workbuddy-xdpool-panel-title{color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
167
+ .dsm-workbuddy-xdpool-stats{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(168px,.85fr);gap:0;flex:1 1 420px;min-width:0;max-width:660px;border-left:1px solid color-mix(in oklab, var(--dsw-alias-border-l2,#3a3d45) 45%, transparent)}
168
+ .dsm-workbuddy-xdpool-panel{display:flex;flex-direction:column;min-width:0;gap:7px;padding:14px 16px}
169
+ .dsm-workbuddy-xdpool-panel-title{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px;letter-spacing:.03em;text-transform:uppercase;font-weight:600}
158
170
  .dsm-workbuddy-xdpool-panel-empty{color:var(--dsw-alias-label-tertiary,#999);font-size:14px;line-height:20px}
159
171
  .dsm-workbuddy-xdpool-panel-error{color:var(--dsw-alias-state-error-primary,#ef4444);font-size:12px;line-height:18px;word-break:break-word}
160
172
  .dsm-workbuddy-xdpool-panel-foot{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-top:9px;padding-top:9px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
@@ -172,9 +184,9 @@ window.__ModuleLoader__.load({
172
184
  fold onto their own line so a long package name cannot squeeze them out. */
173
185
  .dsm-workbuddy-xdpool-packages-when{grid-column:1/-1;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:11px;line-height:15px;font-variant-numeric:tabular-nums}
174
186
  .dsm-workbuddy-xdpool-packages-when-soon{color:var(--dsw-alias-state-warning-primary,#d97706)}
175
- .dsm-workbuddy-xdpool-panel-total{position:relative;align-items:center;text-align:center;overflow:hidden}
187
+ .dsm-workbuddy-xdpool-panel-total{position:relative;align-items:center;text-align:center;justify-content:center;overflow:hidden;background:color-mix(in oklab, var(--dsw-alias-state-success-primary,#22a06b) 5%, transparent)}
176
188
  .dsm-workbuddy-xdpool-panel-total::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;opacity:.9;background:var(--dsw-alias-state-success-primary,#22a06b)}
177
- .dsm-workbuddy-xdpool-total-value{color:var(--dsw-alias-state-success-primary,#22a06b);font-size:30px;line-height:34px;font-weight:700;letter-spacing:-.5px;white-space:nowrap;font-variant-numeric:tabular-nums}
189
+ .dsm-workbuddy-xdpool-total-value{color:var(--dsw-alias-state-success-primary,#22a06b);font-size:28px;line-height:32px;font-weight:800;letter-spacing:-.02em;white-space:nowrap;font-variant-numeric:tabular-nums}
178
190
 
179
191
  /* Model directory list. */
180
192
  .dsm-workbuddy-xdpool-models{display:flex;flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);padding-top:14px}
@@ -530,27 +542,21 @@ window.__ModuleLoader__.load({
530
542
  * restart and is re-applied after every re-scan.
531
543
  */
532
544
  const toggleAccountDisabled = async (accountId, disabled) => {
545
+ const write = settingsScope?.set;
546
+ if (write === void 0) {
547
+ setError(t?.("row.modelsSaveError", { message: "settings scope is read-only" }) ?? "settings scope is read-only");
548
+ return;
549
+ }
533
550
  setAccountBusyId(accountId);
534
551
  setFlash(void 0);
535
552
  try {
536
- const response = await fetch(POOL_ACCOUNT_DISABLE_PATH, {
537
- method: "POST",
538
- headers: {
539
- accept: "application/json",
540
- "content-type": "application/json"
541
- },
542
- credentials: "same-origin",
543
- body: JSON.stringify({
544
- accountId,
545
- disabled
546
- })
547
- });
548
- const body = await response.json().catch(() => void 0);
549
- if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`);
553
+ const current = (status?.accounts ?? []).filter((account) => account.disabled === true).map((account) => account.id);
554
+ const next = disabled ? current.includes(accountId) ? current : [...current, accountId] : current.filter((id) => id !== accountId);
555
+ await write.call(settingsScope, "disabledAccountIds", next);
550
556
  await refresh(activeRegion);
551
557
  } catch (cause) {
552
558
  const message = cause instanceof Error ? cause.message : String(cause);
553
- if (mounted.current) setError(t?.("row.accountToggleError", { message }) ?? `Could not switch the account: ${message}`);
559
+ if (mounted.current) setError(t?.("row.accountToggleError", { message }) ?? "Could not switch the account: " + message);
554
560
  } finally {
555
561
  if (mounted.current) setAccountBusyId(void 0);
556
562
  }
@@ -851,31 +857,55 @@ window.__ModuleLoader__.load({
851
857
  accountCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
852
858
  className: "dsm-workbuddy-xdpool-accounts",
853
859
  "aria-label": t?.("row.accountsTitle") ?? "Accounts",
854
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
855
- className: "dsm-workbuddy-xdpool-accounts-head",
856
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
857
- className: "dsm-workbuddy-xdpool-accounts-title",
858
- children: t?.("row.accountsTitle") ?? "Accounts in the pool"
859
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
860
- className: "dsm-workbuddy-xdpool-accounts-summary",
861
- children: t?.("row.accountsSummary", {
862
- count: accountCount,
863
- cooling
864
- }) ?? `${accountCount} account(s) · ${cooling} cooling`
865
- })]
866
- }), status?.accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountBlock, {
867
- account,
868
- ...status.activeAccountId === void 0 ? {} : { activeAccountId: status.activeAccountId },
869
- ...checkinBusyId === void 0 ? {} : { checkinBusyId },
870
- onClaimCheckin: (accountId) => {
871
- claimCheckin(accountId);
872
- },
873
- onToggleDisabled: (accountId, disabled) => {
874
- toggleAccountDisabled(accountId, disabled);
875
- },
876
- ...accountBusyId === void 0 ? {} : { accountBusyId },
877
- t
878
- }, account.id))]
860
+ children: [
861
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
862
+ className: "dsm-workbuddy-xdpool-accounts-head",
863
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
864
+ className: "dsm-workbuddy-xdpool-accounts-title",
865
+ children: t?.("row.accountsTitle") ?? "Accounts in the pool"
866
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
867
+ className: "dsm-workbuddy-xdpool-accounts-summary",
868
+ children: t?.("row.accountsSummary", {
869
+ count: accountCount,
870
+ cooling
871
+ }) ?? `${accountCount} account(s) · ${cooling} cooling`
872
+ })]
873
+ }),
874
+ (() => {
875
+ const active = status?.activeAccountId === void 0 ? void 0 : status.accounts.find((account) => account.id === status.activeAccountId);
876
+ if (active === void 0) return null;
877
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
878
+ className: "dsm-workbuddy-xdpool-current",
879
+ children: [
880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsm-workbuddy-xdpool-current-dot" }),
881
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
882
+ className: "dsm-workbuddy-xdpool-current-label",
883
+ children: t?.("row.currentAccount") ?? "In use now"
884
+ }),
885
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
886
+ className: "dsm-workbuddy-xdpool-current-name",
887
+ children: active.label
888
+ }),
889
+ active.disabled === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
890
+ className: "dsm-workbuddy-xdpool-current-note",
891
+ children: t?.("row.currentAccountDisabled") ?? "disabled — will switch on the next request"
892
+ }) : null
893
+ ]
894
+ });
895
+ })(),
896
+ status?.accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountBlock, {
897
+ account,
898
+ ...checkinBusyId === void 0 ? {} : { checkinBusyId },
899
+ onClaimCheckin: (accountId) => {
900
+ claimCheckin(accountId);
901
+ },
902
+ onToggleDisabled: (accountId, disabled) => {
903
+ toggleAccountDisabled(accountId, disabled);
904
+ },
905
+ ...accountBusyId === void 0 ? {} : { accountBusyId },
906
+ t
907
+ }, account.id))
908
+ ]
879
909
  }) : null,
880
910
  (status?.models.length ?? 0) > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
881
911
  className: "dsm-workbuddy-xdpool-models",
@@ -934,81 +964,75 @@ window.__ModuleLoader__.load({
934
964
  });
935
965
  }
936
966
  /** One account block: label + status tag + meta + optional credit panels. */
937
- function AccountBlock({ account, activeAccountId, t, checkinBusyId, onClaimCheckin, accountBusyId, onToggleDisabled }) {
938
- const isActive = account.id === activeAccountId;
967
+ function AccountBlock({ account, t, checkinBusyId, onClaimCheckin, accountBusyId, onToggleDisabled }) {
939
968
  const isDisabled = account.disabled === true;
940
969
  const isCooling = account.cooling === true;
941
970
  const cooldownUntil = account.cooldownUntil !== void 0 ? Date.parse(account.cooldownUntil) : void 0;
942
971
  const modelCooldowns = account.modelCooldowns ?? [];
943
- const tag = isActive ? {
944
- text: t?.("row.accountNext") ?? "Next up",
945
- cls: "dsm-workbuddy-xdpool-account-tag"
946
- } : isCooling ? {
972
+ const tag = isCooling ? {
947
973
  text: t?.("row.cooling") ?? "Cooling",
948
974
  cls: "dsm-workbuddy-xdpool-account-tag dsm-workbuddy-xdpool-account-tag-cooling"
949
975
  } : null;
950
976
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
951
977
  className: isDisabled ? "dsm-workbuddy-xdpool-account dsm-workbuddy-xdpool-account-off" : "dsm-workbuddy-xdpool-account",
952
978
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
953
- className: "dsm-workbuddy-xdpool-account-copy",
979
+ className: "dsm-workbuddy-xdpool-account-head",
954
980
  children: [
955
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
956
- className: "dsm-workbuddy-xdpool-account-head",
957
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
958
- className: "dsm-workbuddy-xdpool-account-label",
959
- children: account.label
960
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
961
- className: "dsm-workbuddy-xdpool-account-toggle",
962
- title: t?.("row.accountToggleHint") ?? "Include this account in the pool",
963
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
964
- type: "checkbox",
965
- checked: !isDisabled,
966
- disabled: accountBusyId === account.id,
967
- onChange: (event) => {
968
- onToggleDisabled(account.id, !event.target.checked);
969
- }
970
- }), isDisabled ? t?.("row.accountOff") ?? "Off" : t?.("row.accountInRotation") ?? "In rotation"]
971
- })]
972
- }),
973
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
974
- className: "dsm-workbuddy-xdpool-account-tags",
975
- children: tag === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
976
- className: tag.cls,
977
- children: tag.text
978
- })
981
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
982
+ className: "dsm-workbuddy-xdpool-account-label",
983
+ children: account.label
979
984
  }),
980
- account.domain !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
981
- className: "dsm-workbuddy-xdpool-account-meta",
982
- children: account.domain
985
+ tag === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
986
+ className: tag.cls,
987
+ children: tag.text
983
988
  }),
984
- account.expiresAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
985
- className: "dsm-workbuddy-xdpool-account-meta",
986
- children: t?.("row.tokenExpiry", { time: formatDateTime(account.expiresAt) }) ?? `token ${formatDateTime(account.expiresAt)}`
987
- }) : null,
988
- isCooling && cooldownUntil !== void 0 && !Number.isNaN(cooldownUntil) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
989
- className: "dsm-workbuddy-xdpool-account-meta",
990
- children: [
991
- t?.("row.cooldownUntil", { time: formatTime(cooldownUntil) }) ?? `until ${formatTime(cooldownUntil)}`,
992
- " · ",
993
- t?.("row.cooldownHits", { hits: account.rateLimitHits ?? 0 }) ?? `${account.rateLimitHits ?? 0} hit(s)`
994
- ]
995
- }) : null,
996
- modelCooldowns.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
997
- className: "dsm-workbuddy-xdpool-account-modelcool",
998
- children: modelCooldowns.map((mc) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
999
- className: "dsm-workbuddy-xdpool-account-modelcool-chip",
1000
- children: t?.("row.modelCooling", {
1001
- model: mc.modelId,
1002
- time: formatDateTime(mc.until)
1003
- }) ?? `${mc.modelId} cooling to ${formatDateTime(mc.until)}`
1004
- }, mc.modelId))
1005
- }) : null
989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
990
+ type: "button",
991
+ role: "switch",
992
+ "aria-checked": !isDisabled,
993
+ className: isDisabled ? "dsm-workbuddy-xdpool-account-toggle" : "dsm-workbuddy-xdpool-account-toggle dsm-workbuddy-xdpool-account-toggle-on",
994
+ title: t?.("row.accountToggleHint") ?? "Enable this account (uncheck to keep it out of the pool)",
995
+ disabled: accountBusyId === account.id,
996
+ onClick: () => {
997
+ onToggleDisabled(account.id, !isDisabled);
998
+ },
999
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsm-workbuddy-xdpool-account-toggle-dot" }), isDisabled ? t?.("row.accountOff") ?? "Disabled" : t?.("row.accountInRotation") ?? "Enabled"]
1000
+ })
1006
1001
  ]
1007
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountStats, {
1008
- account,
1009
- t,
1010
- checkinBusy: checkinBusyId === account.id,
1011
- onClaim: onClaimCheckin
1002
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1003
+ className: "dsm-workbuddy-xdpool-account-body",
1004
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1005
+ className: "dsm-workbuddy-xdpool-account-copy",
1006
+ children: [
1007
+ account.expiresAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1008
+ className: "dsm-workbuddy-xdpool-account-meta",
1009
+ children: t?.("row.tokenExpiry", { time: formatDateTime(account.expiresAt) }) ?? `token ${formatDateTime(account.expiresAt)}`
1010
+ }) : null,
1011
+ isCooling && cooldownUntil !== void 0 && !Number.isNaN(cooldownUntil) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1012
+ className: "dsm-workbuddy-xdpool-account-meta",
1013
+ children: [
1014
+ t?.("row.cooldownUntil", { time: formatTime(cooldownUntil) }) ?? `until ${formatTime(cooldownUntil)}`,
1015
+ " · ",
1016
+ t?.("row.cooldownHits", { hits: account.rateLimitHits ?? 0 }) ?? `${account.rateLimitHits ?? 0} hit(s)`
1017
+ ]
1018
+ }) : null,
1019
+ modelCooldowns.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1020
+ className: "dsm-workbuddy-xdpool-account-modelcool",
1021
+ children: modelCooldowns.map((mc) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1022
+ className: "dsm-workbuddy-xdpool-account-modelcool-chip",
1023
+ children: t?.("row.modelCooling", {
1024
+ model: mc.modelId,
1025
+ time: formatDateTime(mc.until)
1026
+ }) ?? `${mc.modelId} cooling to ${formatDateTime(mc.until)}`
1027
+ }, mc.modelId))
1028
+ }) : null
1029
+ ]
1030
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountStats, {
1031
+ account,
1032
+ t,
1033
+ checkinBusy: checkinBusyId === account.id,
1034
+ onClaim: onClaimCheckin
1035
+ })]
1012
1036
  })]
1013
1037
  });
1014
1038
  }
@@ -1254,11 +1278,12 @@ window.__ModuleLoader__.load({
1254
1278
  "row.accountsSummary": "{count} account(s) · {cooling} cooling",
1255
1279
  "row.ok": "Healthy — requests auto-rotate across accounts",
1256
1280
  "row.allCooling": "Every account is rate-limited right now; requests pause until a cooldown lifts.",
1257
- "row.accountNext": "Next up",
1258
- "row.accountInRotation": "In rotation",
1259
- "row.accountOff": "Off",
1260
- "row.accountToggleHint": "Include this account in the pool",
1281
+ "row.accountInRotation": "Enabled",
1282
+ "row.accountOff": "Disabled",
1283
+ "row.accountToggleHint": "Enable this account (uncheck to keep it out of the pool)",
1261
1284
  "row.accountToggleError": "Could not switch the account: {message}",
1285
+ "row.currentAccount": "In use now",
1286
+ "row.currentAccountDisabled": "switched off — the next request moves on",
1262
1287
  "row.cooling": "Cooling (rate-limited)",
1263
1288
  "row.cooldownHits": "{hits} hit(s)",
1264
1289
  "row.cooldownUntil": "until {time}",
@@ -1346,11 +1371,12 @@ window.__ModuleLoader__.load({
1346
1371
  "row.accountsSummary": "{count} 个账号 · {cooling} 个冷却中",
1347
1372
  "row.ok": "运行健康 —— 请求会在各账号间自动轮换",
1348
1373
  "row.allCooling": "当前所有账号都处于限流冷却,请求会暂停直到某个冷却结束。",
1349
- "row.accountNext": "下一个",
1350
- "row.accountInRotation": "参与轮换",
1374
+ "row.accountInRotation": "已启用",
1351
1375
  "row.accountOff": "已停用",
1352
- "row.accountToggleHint": "把这个账号计入池子",
1376
+ "row.accountToggleHint": "启用该账号(取消勾选则不参与池子)",
1353
1377
  "row.accountToggleError": "切换账号失败:{message}",
1378
+ "row.currentAccount": "当前使用",
1379
+ "row.currentAccountDisabled": "已停用 —— 下次请求会换号",
1354
1380
  "row.cooling": "冷却中(被限流)",
1355
1381
  "row.cooldownHits": "触发 {hits} 次",
1356
1382
  "row.cooldownUntil": "至 {time}",
package/lib/index.d.ts CHANGED
@@ -240,6 +240,8 @@ interface AccountPoolOptions {
240
240
  authDirs?: readonly string[];
241
241
  /** How long a rate-limited account stays out of rotation. */
242
242
  cooldownMs?: number;
243
+ /** How long an account rests after its credits run out (default 30 minutes). */
244
+ exhaustCooldownMs?: number;
243
245
  /** Upstream client used to refresh near-expiry tokens. */
244
246
  client?: TokenRefresher;
245
247
  /** Refresh this long before actual expiry; default five minutes. */
@@ -260,6 +262,12 @@ export declare class WorkBuddyAccountPool {
260
262
  private readonly logger;
261
263
  private authDirs;
262
264
  private cooldownMs;
265
+ /**
266
+ * How long an account stays out of rotation after the upstream reports its
267
+ * credits are spent. Credit packs reset on their own schedule rather than on a
268
+ * rate-limit window, so this is much longer than `cooldownMs`.
269
+ */
270
+ private exhaustCooldownMs;
263
271
  private readonly client;
264
272
  private readonly refreshMarginMs;
265
273
  private accounts;
@@ -296,6 +304,7 @@ export declare class WorkBuddyAccountPool {
296
304
  applyConfig(options: {
297
305
  authDirs?: readonly string[];
298
306
  cooldownMs?: number;
307
+ exhaustCooldownMs?: number;
299
308
  distribution?: AccountDistribution;
300
309
  disabledAccountIds?: readonly string[];
301
310
  }): void;
@@ -358,6 +367,27 @@ export declare class WorkBuddyAccountPool {
358
367
  isDisabled(accountId: string): boolean;
359
368
  /** Every account id the user switched off, in discovery order. */
360
369
  disabledIdsInOrder(): string[];
370
+ /**
371
+ * Record that an account actually served a request.
372
+ *
373
+ * Called by the shim once the upstream answers 200 — only then is the account
374
+ * the one the user is really being served by. `balanced` mode reads the same map
375
+ * for its idle weighting, so a request that failed over to another account must
376
+ * not count as used for the account that was merely tried.
377
+ */
378
+ noteServed(accountId: string): void;
379
+ /**
380
+ * The account that served the most recent request, if any.
381
+ *
382
+ * Distinct from "who would serve the next one": this is a record of what
383
+ * actually happened, which is what the card needs to answer "which account am
384
+ * I using right now?". Under `balanced` there is no deterministic next account
385
+ * at all, so a recorded fact is the only honest answer.
386
+ *
387
+ * Returns undefined before the first request of the process, and after every
388
+ * known account has been re-scanned away (a login swapped out under us).
389
+ */
390
+ lastServedId(): string | undefined;
361
391
  /** Best-effort refresh of one account after a session-dead upstream answer. */
362
392
  refreshAccount(accountId: string): Promise<void>;
363
393
  /**
@@ -367,6 +397,15 @@ export declare class WorkBuddyAccountPool {
367
397
  * endpoint never takes down a working session.
368
398
  */
369
399
  private ensureFresh;
400
+ /**
401
+ * Cool a whole account after the upstream reports its credits are spent.
402
+ *
403
+ * Credit exhaustion is an ACCOUNT condition, unlike a model rate limit: every
404
+ * model on that account is unusable until the quota resets, so this cools the
405
+ * account as a whole (no `modelId`) for the configured exhaustion window. The
406
+ * shim then rotates to a different account instead of failing the request.
407
+ */
408
+ penalizeExhausted(accountId: string): void;
370
409
  /**
371
410
  * Mark an account (or one of its models) rate-limited.
372
411
  *
package/lib/index.js CHANGED
@@ -794,10 +794,18 @@ function idleWeight(lastUsedAt, now) {
794
794
  const hours = (now - lastUsedAt) / 36e5;
795
795
  return 1 + Math.min(Math.max(hours, 0) * IDLE_WEIGHT_PER_HOUR, IDLE_WEIGHT_MAX);
796
796
  }
797
+ /** Default rest for an account whose credits ran out (packs reset on their own schedule). */
798
+ const EXHAUST_COOLDOWN_MS = 18e5;
797
799
  var WorkBuddyAccountPool = class {
798
800
  logger;
799
801
  authDirs;
800
802
  cooldownMs;
803
+ /**
804
+ * How long an account stays out of rotation after the upstream reports its
805
+ * credits are spent. Credit packs reset on their own schedule rather than on a
806
+ * rate-limit window, so this is much longer than `cooldownMs`.
807
+ */
808
+ exhaustCooldownMs;
801
809
  client;
802
810
  refreshMarginMs;
803
811
  accounts = [];
@@ -829,6 +837,7 @@ var WorkBuddyAccountPool = class {
829
837
  this.logger = options.logger;
830
838
  this.authDirs = options.authDirs ?? candidateAuthDirs();
831
839
  this.cooldownMs = options.cooldownMs ?? 6e4;
840
+ this.exhaustCooldownMs = options.exhaustCooldownMs ?? EXHAUST_COOLDOWN_MS;
832
841
  this.client = options.client;
833
842
  this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
834
843
  this.distribution = options.distribution ?? "priority";
@@ -841,6 +850,7 @@ var WorkBuddyAccountPool = class {
841
850
  applyConfig(options) {
842
851
  if (options.authDirs !== void 0 && options.authDirs.length > 0) this.authDirs = options.authDirs;
843
852
  if (options.cooldownMs !== void 0 && options.cooldownMs >= 1e3) this.cooldownMs = options.cooldownMs;
853
+ if (options.exhaustCooldownMs !== void 0 && options.exhaustCooldownMs >= 1e3) this.exhaustCooldownMs = options.exhaustCooldownMs;
844
854
  if (options.distribution !== void 0) this.distribution = options.distribution;
845
855
  if (options.disabledAccountIds !== void 0) this.disabledIds = new Set(options.disabledAccountIds);
846
856
  }
@@ -967,14 +977,12 @@ var WorkBuddyAccountPool = class {
967
977
  if (this.preferredId !== void 0) {
968
978
  const preferred = pool.find((account) => account.id === this.preferredId);
969
979
  if (preferred !== void 0) {
970
- this.lastUsedAt.set(preferred.id, Date.now());
971
980
  await this.ensureFresh(preferred);
972
981
  return preferred;
973
982
  }
974
983
  }
975
984
  const account = this.distribution === "round-robin" ? this.pickRoundRobin(pool) : this.distribution === "balanced" ? this.pickByWeight(pool) : pool[0];
976
985
  if (account === void 0) return void 0;
977
- this.lastUsedAt.set(account.id, Date.now());
978
986
  await this.ensureFresh(account);
979
987
  return account;
980
988
  }
@@ -994,6 +1002,40 @@ var WorkBuddyAccountPool = class {
994
1002
  disabledIdsInOrder() {
995
1003
  return this.accounts.filter((account) => this.disabledIds.has(account.id)).map((account) => account.id);
996
1004
  }
1005
+ /**
1006
+ * Record that an account actually served a request.
1007
+ *
1008
+ * Called by the shim once the upstream answers 200 — only then is the account
1009
+ * the one the user is really being served by. `balanced` mode reads the same map
1010
+ * for its idle weighting, so a request that failed over to another account must
1011
+ * not count as used for the account that was merely tried.
1012
+ */
1013
+ noteServed(accountId) {
1014
+ if (!this.accounts.some((account) => account.id === accountId)) return;
1015
+ this.lastUsedAt.set(accountId, Date.now());
1016
+ }
1017
+ /**
1018
+ * The account that served the most recent request, if any.
1019
+ *
1020
+ * Distinct from "who would serve the next one": this is a record of what
1021
+ * actually happened, which is what the card needs to answer "which account am
1022
+ * I using right now?". Under `balanced` there is no deterministic next account
1023
+ * at all, so a recorded fact is the only honest answer.
1024
+ *
1025
+ * Returns undefined before the first request of the process, and after every
1026
+ * known account has been re-scanned away (a login swapped out under us).
1027
+ */
1028
+ lastServedId() {
1029
+ let newest;
1030
+ for (const [id, at] of this.lastUsedAt) {
1031
+ if (!this.accounts.some((account) => account.id === id)) continue;
1032
+ if (newest === void 0 || at > newest.at) newest = {
1033
+ id,
1034
+ at
1035
+ };
1036
+ }
1037
+ return newest?.id;
1038
+ }
997
1039
  /** Best-effort refresh of one account after a session-dead upstream answer. */
998
1040
  async refreshAccount(accountId) {
999
1041
  const account = this.accounts.find((item) => item.id === accountId);
@@ -1044,6 +1086,23 @@ var WorkBuddyAccountPool = class {
1044
1086
  }
1045
1087
  }
1046
1088
  /**
1089
+ * Cool a whole account after the upstream reports its credits are spent.
1090
+ *
1091
+ * Credit exhaustion is an ACCOUNT condition, unlike a model rate limit: every
1092
+ * model on that account is unusable until the quota resets, so this cools the
1093
+ * account as a whole (no `modelId`) for the configured exhaustion window. The
1094
+ * shim then rotates to a different account instead of failing the request.
1095
+ */
1096
+ penalizeExhausted(accountId) {
1097
+ const until = Date.now() + this.exhaustCooldownMs;
1098
+ this.penalize(accountId, until);
1099
+ this.logger?.warn(`
1100
+ dsh-workbuddy-xdpool: account credits exhausted; cooling the whole account until
1101
+
1102
+ ${new Date(until).toISOString()}
1103
+ `);
1104
+ }
1105
+ /**
1047
1106
  * Mark an account (or one of its models) rate-limited.
1048
1107
  *
1049
1108
  * With `modelId`, only that model on the account is cooled — the account's
@@ -1655,6 +1714,7 @@ function createWorkBuddyShim(options) {
1655
1714
  const result = await client.chatStream(account.credential, prepared, controller.signal);
1656
1715
  if (result.ok) {
1657
1716
  logger?.info?.(`dsh-workbuddy-xdpool: served by ${account.label}`);
1717
+ pool.noteServed(account.id);
1658
1718
  res.writeHead(200, {
1659
1719
  "Content-Type": "text/event-stream",
1660
1720
  "Cache-Control": "no-cache",
@@ -1683,6 +1743,11 @@ function createWorkBuddyShim(options) {
1683
1743
  await pool.refreshAccount(account.id);
1684
1744
  continue;
1685
1745
  }
1746
+ if (result.kind === "hard_credit") {
1747
+ pool.penalizeExhausted(account.id);
1748
+ logger?.warn(`dsh-workbuddy-xdpool: ${account.label} has no credits left (attempt ${attempt + 1}/${maxAttempts}); rotating`);
1749
+ continue;
1750
+ }
1686
1751
  if (result.kind !== "soft_rate") break;
1687
1752
  exhaustedByRateLimit = true;
1688
1753
  pool.penalize(account.id, parseRateLimitReset(result.message), modelId);
@@ -2014,7 +2079,8 @@ async function poolWebStatus(deps, region = "cn") {
2014
2079
  rows.push(row);
2015
2080
  }
2016
2081
  const cooling = rows.filter((row) => row.cooling).length;
2017
- const firstUsable = accounts.find((account) => account.cooldownUntilMs <= now);
2082
+ const lastServed = deps.pool.lastServedId();
2083
+ const firstUsable = lastServed !== void 0 ? accounts.find((account) => account.id === lastServed) : accounts.find((account) => account.cooldownUntilMs <= now);
2018
2084
  let shim;
2019
2085
  if (deps.shim === void 0) shim = { running: false };
2020
2086
  else try {
@@ -2319,14 +2385,30 @@ function apply(ctx, config = {}) {
2319
2385
  * the three model-selection keys, so the settings file cannot be steered from
2320
2386
  * the browser; the catalog re-reads through `onChange` either way.
2321
2387
  */
2388
+ /**
2389
+ * Write one key of the plugin's own settings section. Only ever called with the
2390
+ * model-selection keys, the distribution, and the disabled-account list, so the
2391
+ * settings file cannot be steered from the browser; the catalogs and the pool
2392
+ * re-read through `onChange` either way.
2393
+ *
2394
+ * `set` returns a promise, so a rejection must be caught explicitly: a bare
2395
+ * `void write.call(...)` swallows it, and the card then looks like it saved
2396
+ * while the value never reached the settings file.
2397
+ */
2322
2398
  const setSetting = (key, value) => {
2323
2399
  if (value === void 0) return;
2324
2400
  const write = settingsService?.set;
2325
- if (write === void 0) return;
2401
+ if (write === void 0) {
2402
+ ctx.logger.warn?.(`dsh-workbuddy-xdpool: no settings writer; ` + key + ` was not saved`);
2403
+ return;
2404
+ }
2326
2405
  try {
2327
- write.call(settingsService, key, value);
2406
+ const result = write.call(settingsService, key, value);
2407
+ if (result !== void 0 && typeof result.then === "function") result.catch((error) => {
2408
+ ctx.logger.warn?.(`dsh-workbuddy-xdpool: failed to persist ` + key, error);
2409
+ });
2328
2410
  } catch (error) {
2329
- ctx.logger.warn?.(`dsh-workbuddy-xdpool: failed to persist ${key}`, error);
2411
+ ctx.logger.warn?.(`dsh-workbuddy-xdpool: failed to persist ` + key, error);
2330
2412
  }
2331
2413
  };
2332
2414
  const shims = {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-workbuddy-xdpool",
3
3
  "displayName": "DSH WorkBuddy XD Pool",
4
4
  "description": "Merge every locally signed-in WorkBuddy account into DeepSeek Harness as one auto-failing-over model pool (multi-account rotation, live credits, daily check-in and model catalog).",
5
- "version": "1.0.0",
5
+ "version": "1.0.1",
6
6
  "license": "MIT",
7
7
  "author": "XDTrees",
8
8
  "homepage": "https://github.com/XDTrees/dsh-workbuddy-xdpool#readme",