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 +30 -0
- package/lib/bin.js +61 -2
- package/lib/client.js +147 -121
- package/lib/index.d.ts +39 -0
- package/lib/index.js +88 -6
- package/package.json +1 -1
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;
|
|
136
|
-
.
|
|
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{
|
|
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;
|
|
143
|
-
.dsm-workbuddy-xdpool-account-toggle
|
|
144
|
-
.dsm-workbuddy-xdpool-account-toggle:
|
|
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.
|
|
156
|
-
.dsm-workbuddy-xdpool-panel{display:flex;flex-direction:column;min-width:0;gap:7px;padding:14px
|
|
157
|
-
.dsm-workbuddy-xdpool-panel-title{color:var(--dsw-alias-label-tertiary,#999);font-size:
|
|
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:
|
|
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
|
|
537
|
-
|
|
538
|
-
|
|
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 }) ??
|
|
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: [
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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,
|
|
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 =
|
|
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-
|
|
979
|
+
className: "dsm-workbuddy-xdpool-account-head",
|
|
954
980
|
children: [
|
|
955
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
956
|
-
className: "dsm-workbuddy-xdpool-account-
|
|
957
|
-
children:
|
|
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
|
-
|
|
981
|
-
className:
|
|
982
|
-
children:
|
|
985
|
+
tag === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
986
|
+
className: tag.cls,
|
|
987
|
+
children: tag.text
|
|
983
988
|
}),
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
]
|
|
995
|
-
})
|
|
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.
|
|
1008
|
-
account,
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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.
|
|
1258
|
-
"row.
|
|
1259
|
-
"row.
|
|
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.
|
|
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
|
|
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)
|
|
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
|
|
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.
|
|
5
|
+
"version": "1.0.1",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "XDTrees",
|
|
8
8
|
"homepage": "https://github.com/XDTrees/dsh-workbuddy-xdpool#readme",
|