dsh-workbuddy-xdpool 0.4.5 → 1.0.0
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 +31 -0
- package/lib/bin.js +90 -7
- package/lib/client.js +90 -9
- package/lib/index.d.ts +53 -7
- package/lib/index.js +145 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@
|
|
|
4
4
|
|
|
5
5
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
6
6
|
|
|
7
|
+
## 1.0.0 (2026-09-21)
|
|
8
|
+
|
|
9
|
+
这一版把「账号」和「用号方式」都交给你控制。
|
|
10
|
+
|
|
11
|
+
### 一、可以停用不想用的账号了
|
|
12
|
+
|
|
13
|
+
账号列表里每个账号右侧新增一个开关,关掉它这个账号就**不再参与请求**。
|
|
14
|
+
|
|
15
|
+
- 关掉的账号仍然显示在列表里(变灰),随时可以再打开
|
|
16
|
+
- 设置会被保存,**重启 DSH 后依然有效**
|
|
17
|
+
- 只影响路由,不会删除或改动你的登录凭据
|
|
18
|
+
|
|
19
|
+
### 二、新增「均衡」用号模式
|
|
20
|
+
|
|
21
|
+
原来的两种方式都保留,另加一种:
|
|
22
|
+
|
|
23
|
+
| 模式 | 行为 |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| **优先**(默认,行为不变) | 一直用同一个账号,直到它被限流才换下一个 |
|
|
26
|
+
| **均衡**(新增) | 每个可用账号都参与抽取,**闲置越久权重越高** |
|
|
27
|
+
| **轮换** | 严格轮流,把额度摊平 |
|
|
28
|
+
|
|
29
|
+
**为什么需要「均衡」**:默认的「优先」会死磕第一个账号,把它打到限流才换 —— 如果你有 5 个号,其余 4 个基本闲着。切到「均衡」后请求会自然分散到各个账号,**更不容易撞限流**,可用时间更长。
|
|
30
|
+
|
|
31
|
+
在卡片顶部「账号使用方式」里切换,即时生效。
|
|
32
|
+
|
|
33
|
+
### 其他
|
|
34
|
+
|
|
35
|
+
- 显式选定的账号(如卡片上「下一个」标记的那个)在三种模式下**都会被优先使用**
|
|
36
|
+
- 处于冷却中的账号在「均衡」模式下**一定不会被选中**,冷却结束自动回到抽取池
|
|
37
|
+
|
|
7
38
|
## 0.4.5 (2026-09-21)
|
|
8
39
|
|
|
9
40
|
本版**功能与 0.4.4 完全相同**,只是换了一条发布通道。
|
package/lib/bin.js
CHANGED
|
@@ -758,6 +758,26 @@ function candidateAuthDirs(env = process.env) {
|
|
|
758
758
|
* Read-only pool of every discovered WorkBuddy account, with rate-limit
|
|
759
759
|
* cooldown and round-robin failover.
|
|
760
760
|
*/
|
|
761
|
+
/** Idle bonus per hour an account has been unused (reference-panel default). */
|
|
762
|
+
const IDLE_WEIGHT_PER_HOUR = .5;
|
|
763
|
+
/** Ceiling for the idle bonus, so an idle account cannot dominate forever. */
|
|
764
|
+
const IDLE_WEIGHT_MAX = 5;
|
|
765
|
+
/**
|
|
766
|
+
* Weight one account by how long it has been idle.
|
|
767
|
+
*
|
|
768
|
+
* The base of 1 keeps every eligible account in play: an account that served a
|
|
769
|
+
* moment ago still has a small chance, so a single unhealthy account cannot pin
|
|
770
|
+
* the pool to itself, and the weights never sum to zero.
|
|
771
|
+
*
|
|
772
|
+
* `lastUsedAt === undefined` means "never used in this process", which earns the
|
|
773
|
+
* full bonus: on a fresh start every account ties, and the weighted draw spreads
|
|
774
|
+
* the first requests instead of always picking the first entry.
|
|
775
|
+
*/
|
|
776
|
+
function idleWeight(lastUsedAt, now) {
|
|
777
|
+
if (lastUsedAt === void 0) return 6;
|
|
778
|
+
const hours = (now - lastUsedAt) / 36e5;
|
|
779
|
+
return 1 + Math.min(Math.max(hours, 0) * IDLE_WEIGHT_PER_HOUR, IDLE_WEIGHT_MAX);
|
|
780
|
+
}
|
|
761
781
|
var WorkBuddyAccountPool = class {
|
|
762
782
|
logger;
|
|
763
783
|
authDirs;
|
|
@@ -770,6 +790,24 @@ var WorkBuddyAccountPool = class {
|
|
|
770
790
|
cursor = 0;
|
|
771
791
|
lastScanAtMs = 0;
|
|
772
792
|
preferredId;
|
|
793
|
+
/**
|
|
794
|
+
* Account ids the user switched off on the card.
|
|
795
|
+
*
|
|
796
|
+
* Disabling is a user preference rather than a property of the credential:
|
|
797
|
+
* `scan()` rebuilds every account object from the auth files, so the set
|
|
798
|
+
* lives on the pool and is re-applied from settings after each scan.
|
|
799
|
+
*/
|
|
800
|
+
disabledIds = /* @__PURE__ */ new Set();
|
|
801
|
+
/**
|
|
802
|
+
* Last time each account served a request, epoch ms. Drives the idle term
|
|
803
|
+
* of the priority-mode weighting below: an account that just served loses to
|
|
804
|
+
* one that has been idle, so a small pool stops hammering a single account.
|
|
805
|
+
*
|
|
806
|
+
* In-memory on purpose: it only biases the next pick, so a cold start that
|
|
807
|
+
* treats every account as idle is the right default. Not keyed by id lookup
|
|
808
|
+
* misses because a removed account simply disappears from the map on re-scan.
|
|
809
|
+
*/
|
|
810
|
+
lastUsedAt = /* @__PURE__ */ new Map();
|
|
773
811
|
refreshInflight = /* @__PURE__ */ new Map();
|
|
774
812
|
constructor(options = {}) {
|
|
775
813
|
this.logger = options.logger;
|
|
@@ -788,6 +826,7 @@ var WorkBuddyAccountPool = class {
|
|
|
788
826
|
if (options.authDirs !== void 0 && options.authDirs.length > 0) this.authDirs = options.authDirs;
|
|
789
827
|
if (options.cooldownMs !== void 0 && options.cooldownMs >= 1e3) this.cooldownMs = options.cooldownMs;
|
|
790
828
|
if (options.distribution !== void 0) this.distribution = options.distribution;
|
|
829
|
+
if (options.disabledAccountIds !== void 0) this.disabledIds = new Set(options.disabledAccountIds);
|
|
791
830
|
}
|
|
792
831
|
/** Rescan the auth directories and merge newly discovered accounts. */
|
|
793
832
|
async scan() {
|
|
@@ -840,12 +879,47 @@ var WorkBuddyAccountPool = class {
|
|
|
840
879
|
*/
|
|
841
880
|
available(now, modelId, region) {
|
|
842
881
|
return this.accounts.filter((account) => {
|
|
882
|
+
if (this.disabledIds.has(account.id)) return false;
|
|
843
883
|
if (account.cooldownUntilMs > now) return false;
|
|
844
884
|
if (modelId !== void 0 && (account.modelCooldowns[modelId] ?? 0) > now) return false;
|
|
845
885
|
if (region !== void 0 && regionOf(account.credential.domain) !== region) return false;
|
|
846
886
|
return true;
|
|
847
887
|
});
|
|
848
888
|
}
|
|
889
|
+
/** Round-robin: the legacy cursor walk, kept for the distribution that asks for it. */
|
|
890
|
+
pickRoundRobin(pool) {
|
|
891
|
+
const index = this.cursor % pool.length;
|
|
892
|
+
const account = pool[index];
|
|
893
|
+
if (account === void 0) return void 0;
|
|
894
|
+
this.cursor = (index + 1) % pool.length;
|
|
895
|
+
return account;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Priority mode: weighted random over the eligible accounts.
|
|
899
|
+
*
|
|
900
|
+
* The weight is an idle bonus — `1 + min(idleHours * perHour, max)` — so an
|
|
901
|
+
* account that has never served (or has been idle for a while) outranks one
|
|
902
|
+
* that just answered. Reference panel logic drops its success-rate term
|
|
903
|
+
* entirely because a lifetime error counter penalises an account forever;
|
|
904
|
+
* instantaneous health is already handled by cooldowns, which is why those
|
|
905
|
+
* accounts never reach this list.
|
|
906
|
+
*
|
|
907
|
+
* A pool with no idle history (fresh process) hashes to equal weights, which
|
|
908
|
+
* spreads the very first picks instead of always returning index 0.
|
|
909
|
+
*/
|
|
910
|
+
pickByWeight(pool) {
|
|
911
|
+
if (pool.length === 1) return pool[0];
|
|
912
|
+
const now = Date.now();
|
|
913
|
+
const weights = pool.map((account) => idleWeight(this.lastUsedAt.get(account.id), now));
|
|
914
|
+
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
|
915
|
+
if (!Number.isFinite(total) || total <= 0) return pool[0];
|
|
916
|
+
let roll = Math.random() * total;
|
|
917
|
+
for (let index = 0; index < pool.length; index += 1) {
|
|
918
|
+
roll -= weights[index] ?? 0;
|
|
919
|
+
if (roll < 0) return pool[index];
|
|
920
|
+
}
|
|
921
|
+
return pool[pool.length - 1];
|
|
922
|
+
}
|
|
849
923
|
/**
|
|
850
924
|
* Pick the account to serve a request.
|
|
851
925
|
*
|
|
@@ -875,16 +949,16 @@ var WorkBuddyAccountPool = class {
|
|
|
875
949
|
}
|
|
876
950
|
if (pool.length === 0) return void 0;
|
|
877
951
|
if (this.preferredId !== void 0) {
|
|
878
|
-
const
|
|
879
|
-
if (
|
|
880
|
-
|
|
881
|
-
|
|
952
|
+
const preferred = pool.find((account) => account.id === this.preferredId);
|
|
953
|
+
if (preferred !== void 0) {
|
|
954
|
+
this.lastUsedAt.set(preferred.id, Date.now());
|
|
955
|
+
await this.ensureFresh(preferred);
|
|
956
|
+
return preferred;
|
|
882
957
|
}
|
|
883
958
|
}
|
|
884
|
-
const
|
|
885
|
-
const account = pool[index];
|
|
959
|
+
const account = this.distribution === "round-robin" ? this.pickRoundRobin(pool) : this.distribution === "balanced" ? this.pickByWeight(pool) : pool[0];
|
|
886
960
|
if (account === void 0) return void 0;
|
|
887
|
-
|
|
961
|
+
this.lastUsedAt.set(account.id, Date.now());
|
|
888
962
|
await this.ensureFresh(account);
|
|
889
963
|
return account;
|
|
890
964
|
}
|
|
@@ -896,6 +970,14 @@ var WorkBuddyAccountPool = class {
|
|
|
896
970
|
prefer(accountId) {
|
|
897
971
|
this.preferredId = accountId;
|
|
898
972
|
}
|
|
973
|
+
/** Whether the user switched this account off on the card. */
|
|
974
|
+
isDisabled(accountId) {
|
|
975
|
+
return this.disabledIds.has(accountId);
|
|
976
|
+
}
|
|
977
|
+
/** Every account id the user switched off, in discovery order. */
|
|
978
|
+
disabledIdsInOrder() {
|
|
979
|
+
return this.accounts.filter((account) => this.disabledIds.has(account.id)).map((account) => account.id);
|
|
980
|
+
}
|
|
899
981
|
/** Best-effort refresh of one account after a session-dead upstream answer. */
|
|
900
982
|
async refreshAccount(accountId) {
|
|
901
983
|
const account = this.accounts.find((item) => item.id === accountId);
|
|
@@ -1173,6 +1255,7 @@ z.object({
|
|
|
1173
1255
|
authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app own location)"),
|
|
1174
1256
|
cooldownMs: z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds"),
|
|
1175
1257
|
distribution: z.union(["priority", "round-robin"]).default("priority").description("How requests are spread: priority (drain one) or round-robin"),
|
|
1258
|
+
disabledAccountIds: z.array(z.string()).default([]).description("Account ids excluded from the pool (empty = every discovered account participates)"),
|
|
1176
1259
|
enabledModelIds: z.array(z.string()).default([]).description("Legacy shared model-id list; used by a region that has no per-region selection yet"),
|
|
1177
1260
|
imageModelIds: z.array(z.string()).default([]).description("Legacy shared image-id list; used by a region that has no per-region selection yet"),
|
|
1178
1261
|
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Legacy shared context budgets; used by a region with no per-region selection yet"),
|
package/lib/client.js
CHANGED
|
@@ -28,6 +28,8 @@ 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";
|
|
31
33
|
//#endregion
|
|
32
34
|
//#region src/client/icon.ts
|
|
33
35
|
/**
|
|
@@ -133,6 +135,13 @@ window.__ModuleLoader__.load({
|
|
|
133
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)}
|
|
134
136
|
.dsm-workbuddy-xdpool-account-copy{display:flex;flex-direction:column;gap:3px;min-width:0}
|
|
135
137
|
.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}
|
|
139
|
+
/* Account switched off on the card: still listed (so it can be turned back on) but visually muted. */
|
|
140
|
+
.dsm-workbuddy-xdpool-account-off{opacity:.55}
|
|
141
|
+
/* 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)}
|
|
136
145
|
.dsm-workbuddy-xdpool-account-tags{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
137
146
|
.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)}
|
|
138
147
|
.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)}
|
|
@@ -374,6 +383,8 @@ window.__ModuleLoader__.load({
|
|
|
374
383
|
const [flash, setFlash] = (0, react.useState)(void 0);
|
|
375
384
|
/** Account id whose daily claim is currently in flight. */
|
|
376
385
|
const [checkinBusyId, setCheckinBusyId] = (0, react.useState)(void 0);
|
|
386
|
+
/** Account id whose enable/disable switch is in flight, if any. */
|
|
387
|
+
const [accountBusyId, setAccountBusyId] = (0, react.useState)(void 0);
|
|
377
388
|
/**
|
|
378
389
|
* Draft model selection. `undefined` means "no local edits"; once a checkbox
|
|
379
390
|
* is touched the draft takes over and is what the Save button posts. Discard
|
|
@@ -509,6 +520,41 @@ window.__ModuleLoader__.load({
|
|
|
509
520
|
if (mounted.current) setCheckinBusyId(void 0);
|
|
510
521
|
}
|
|
511
522
|
};
|
|
523
|
+
/**
|
|
524
|
+
* Switch one account in or out of the pool.
|
|
525
|
+
*
|
|
526
|
+
* The id travels in the body and the host validates it against the accounts it
|
|
527
|
+
* really knows, so a stale tab cannot write an orphan id. Disabling only stops
|
|
528
|
+
* the account from being picked — it stays listed so it can be turned back on —
|
|
529
|
+
* and the change is saved through the settings section, so it survives a
|
|
530
|
+
* restart and is re-applied after every re-scan.
|
|
531
|
+
*/
|
|
532
|
+
const toggleAccountDisabled = async (accountId, disabled) => {
|
|
533
|
+
setAccountBusyId(accountId);
|
|
534
|
+
setFlash(void 0);
|
|
535
|
+
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}`);
|
|
550
|
+
await refresh(activeRegion);
|
|
551
|
+
} catch (cause) {
|
|
552
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
553
|
+
if (mounted.current) setError(t?.("row.accountToggleError", { message }) ?? `Could not switch the account: ${message}`);
|
|
554
|
+
} finally {
|
|
555
|
+
if (mounted.current) setAccountBusyId(void 0);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
512
558
|
/** Keep the draft in step with the server copy while nothing is dirty. */
|
|
513
559
|
const modelDraft = draft ?? (status === void 0 ? {} : draftFromStatus(status));
|
|
514
560
|
/** Model edits need a writable settings scope; otherwise the rows are read-only. */
|
|
@@ -722,17 +768,21 @@ window.__ModuleLoader__.load({
|
|
|
722
768
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
723
769
|
className: "dsm-workbuddy-xdpool-dist-title",
|
|
724
770
|
children: t?.("row.distTitle") ?? "Account usage"
|
|
725
|
-
}), [
|
|
771
|
+
}), [
|
|
772
|
+
"priority",
|
|
773
|
+
"balanced",
|
|
774
|
+
"round-robin"
|
|
775
|
+
].map((option) => {
|
|
726
776
|
const active = (status.distribution ?? "priority") === option;
|
|
727
|
-
const label = option === "priority" ? t?.("row.distPriority") ?? "Priority" : t?.("row.distRoundRobin") ?? "Round-robin";
|
|
728
|
-
const hint = option === "priority" ? t?.("row.distPriorityHint") ?? "" : t?.("row.distRoundRobinHint") ?? "";
|
|
777
|
+
const label = option === "priority" ? t?.("row.distPriority") ?? "Priority" : option === "balanced" ? t?.("row.distBalanced") ?? "Balanced" : t?.("row.distRoundRobin") ?? "Round-robin";
|
|
778
|
+
const hint = option === "priority" ? t?.("row.distPriorityHint") ?? "" : option === "balanced" ? t?.("row.distBalancedHint") ?? "" : t?.("row.distRoundRobinHint") ?? "";
|
|
729
779
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
730
780
|
type: "button",
|
|
731
781
|
role: "radio",
|
|
732
782
|
"aria-checked": active,
|
|
733
783
|
title: hint,
|
|
734
784
|
disabled: !modelsEditable,
|
|
735
|
-
className:
|
|
785
|
+
className: "dsm-workbuddy-xdpool-dist-option" + (active ? " dsm-workbuddy-xdpool-dist-option-active" : ""),
|
|
736
786
|
onClick: () => {
|
|
737
787
|
setDistribution(option);
|
|
738
788
|
},
|
|
@@ -820,6 +870,10 @@ window.__ModuleLoader__.load({
|
|
|
820
870
|
onClaimCheckin: (accountId) => {
|
|
821
871
|
claimCheckin(accountId);
|
|
822
872
|
},
|
|
873
|
+
onToggleDisabled: (accountId, disabled) => {
|
|
874
|
+
toggleAccountDisabled(accountId, disabled);
|
|
875
|
+
},
|
|
876
|
+
...accountBusyId === void 0 ? {} : { accountBusyId },
|
|
823
877
|
t
|
|
824
878
|
}, account.id))]
|
|
825
879
|
}) : null,
|
|
@@ -880,8 +934,9 @@ window.__ModuleLoader__.load({
|
|
|
880
934
|
});
|
|
881
935
|
}
|
|
882
936
|
/** One account block: label + status tag + meta + optional credit panels. */
|
|
883
|
-
function AccountBlock({ account, activeAccountId, t, checkinBusyId, onClaimCheckin }) {
|
|
937
|
+
function AccountBlock({ account, activeAccountId, t, checkinBusyId, onClaimCheckin, accountBusyId, onToggleDisabled }) {
|
|
884
938
|
const isActive = account.id === activeAccountId;
|
|
939
|
+
const isDisabled = account.disabled === true;
|
|
885
940
|
const isCooling = account.cooling === true;
|
|
886
941
|
const cooldownUntil = account.cooldownUntil !== void 0 ? Date.parse(account.cooldownUntil) : void 0;
|
|
887
942
|
const modelCooldowns = account.modelCooldowns ?? [];
|
|
@@ -893,13 +948,27 @@ window.__ModuleLoader__.load({
|
|
|
893
948
|
cls: "dsm-workbuddy-xdpool-account-tag dsm-workbuddy-xdpool-account-tag-cooling"
|
|
894
949
|
} : null;
|
|
895
950
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
896
|
-
className: "dsm-workbuddy-xdpool-account",
|
|
951
|
+
className: isDisabled ? "dsm-workbuddy-xdpool-account dsm-workbuddy-xdpool-account-off" : "dsm-workbuddy-xdpool-account",
|
|
897
952
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
898
953
|
className: "dsm-workbuddy-xdpool-account-copy",
|
|
899
954
|
children: [
|
|
900
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
901
|
-
className: "dsm-workbuddy-xdpool-account-
|
|
902
|
-
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
|
+
})]
|
|
903
972
|
}),
|
|
904
973
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
905
974
|
className: "dsm-workbuddy-xdpool-account-tags",
|
|
@@ -1186,6 +1255,10 @@ window.__ModuleLoader__.load({
|
|
|
1186
1255
|
"row.ok": "Healthy — requests auto-rotate across accounts",
|
|
1187
1256
|
"row.allCooling": "Every account is rate-limited right now; requests pause until a cooldown lifts.",
|
|
1188
1257
|
"row.accountNext": "Next up",
|
|
1258
|
+
"row.accountInRotation": "In rotation",
|
|
1259
|
+
"row.accountOff": "Off",
|
|
1260
|
+
"row.accountToggleHint": "Include this account in the pool",
|
|
1261
|
+
"row.accountToggleError": "Could not switch the account: {message}",
|
|
1189
1262
|
"row.cooling": "Cooling (rate-limited)",
|
|
1190
1263
|
"row.cooldownHits": "{hits} hit(s)",
|
|
1191
1264
|
"row.cooldownUntil": "until {time}",
|
|
@@ -1238,6 +1311,8 @@ window.__ModuleLoader__.load({
|
|
|
1238
1311
|
"row.distPriorityHint": "Drain one account before moving to the next",
|
|
1239
1312
|
"row.distRoundRobin": "Round-robin",
|
|
1240
1313
|
"row.distRoundRobinHint": "Spread the spend evenly across accounts",
|
|
1314
|
+
"row.distBalanced": "Balanced",
|
|
1315
|
+
"row.distBalancedHint": "Draw from every account, favouring whichever has been idle longest",
|
|
1241
1316
|
"row.accountsScanning": "Detecting…",
|
|
1242
1317
|
"row.resetCooldowns": "Clear all cooldowns",
|
|
1243
1318
|
"row.resetCooldownsBusy": "Clearing…",
|
|
@@ -1272,6 +1347,10 @@ window.__ModuleLoader__.load({
|
|
|
1272
1347
|
"row.ok": "运行健康 —— 请求会在各账号间自动轮换",
|
|
1273
1348
|
"row.allCooling": "当前所有账号都处于限流冷却,请求会暂停直到某个冷却结束。",
|
|
1274
1349
|
"row.accountNext": "下一个",
|
|
1350
|
+
"row.accountInRotation": "参与轮换",
|
|
1351
|
+
"row.accountOff": "已停用",
|
|
1352
|
+
"row.accountToggleHint": "把这个账号计入池子",
|
|
1353
|
+
"row.accountToggleError": "切换账号失败:{message}",
|
|
1275
1354
|
"row.cooling": "冷却中(被限流)",
|
|
1276
1355
|
"row.cooldownHits": "触发 {hits} 次",
|
|
1277
1356
|
"row.cooldownUntil": "至 {time}",
|
|
@@ -1324,6 +1403,8 @@ window.__ModuleLoader__.load({
|
|
|
1324
1403
|
"row.distPriorityHint": "优先用完一个账号再换下一个",
|
|
1325
1404
|
"row.distRoundRobin": "轮换模式",
|
|
1326
1405
|
"row.distRoundRobinHint": "积分平均分摊到各账号",
|
|
1406
|
+
"row.distBalanced": "均衡",
|
|
1407
|
+
"row.distBalancedHint": "每个账号都参与抽取,闲置越久权重越高",
|
|
1327
1408
|
"row.accountsScanning": "正在检测…",
|
|
1328
1409
|
"row.resetCooldowns": "清除所有冷却",
|
|
1329
1410
|
"row.resetCooldownsBusy": "正在清除…",
|
package/lib/index.d.ts
CHANGED
|
@@ -228,7 +228,7 @@ export declare function workbuddyAccountId(credential: Pick<WorkBuddyCredential,
|
|
|
228
228
|
/** Every directory the pool should scan, in probe order. */
|
|
229
229
|
export declare function candidateAuthDirs(env?: NodeJS.ProcessEnv): string[];
|
|
230
230
|
/** How the pool chooses which account serves the next request. */
|
|
231
|
-
type AccountDistribution = 'priority' | 'round-robin';
|
|
231
|
+
type AccountDistribution = 'priority' | 'round-robin' | 'balanced';
|
|
232
232
|
interface AccountPoolOptions {
|
|
233
233
|
/** Logger for discovery and rotation events. */
|
|
234
234
|
logger?: {
|
|
@@ -256,10 +256,6 @@ interface AccountPoolOptions {
|
|
|
256
256
|
*/
|
|
257
257
|
distribution?: AccountDistribution;
|
|
258
258
|
}
|
|
259
|
-
/**
|
|
260
|
-
* Read-only pool of every discovered WorkBuddy account, with rate-limit
|
|
261
|
-
* cooldown and round-robin failover.
|
|
262
|
-
*/
|
|
263
259
|
export declare class WorkBuddyAccountPool {
|
|
264
260
|
private readonly logger;
|
|
265
261
|
private authDirs;
|
|
@@ -272,6 +268,24 @@ export declare class WorkBuddyAccountPool {
|
|
|
272
268
|
private cursor;
|
|
273
269
|
private lastScanAtMs;
|
|
274
270
|
private preferredId;
|
|
271
|
+
/**
|
|
272
|
+
* Account ids the user switched off on the card.
|
|
273
|
+
*
|
|
274
|
+
* Disabling is a user preference rather than a property of the credential:
|
|
275
|
+
* `scan()` rebuilds every account object from the auth files, so the set
|
|
276
|
+
* lives on the pool and is re-applied from settings after each scan.
|
|
277
|
+
*/
|
|
278
|
+
private disabledIds;
|
|
279
|
+
/**
|
|
280
|
+
* Last time each account served a request, epoch ms. Drives the idle term
|
|
281
|
+
* of the priority-mode weighting below: an account that just served loses to
|
|
282
|
+
* one that has been idle, so a small pool stops hammering a single account.
|
|
283
|
+
*
|
|
284
|
+
* In-memory on purpose: it only biases the next pick, so a cold start that
|
|
285
|
+
* treats every account as idle is the right default. Not keyed by id lookup
|
|
286
|
+
* misses because a removed account simply disappears from the map on re-scan.
|
|
287
|
+
*/
|
|
288
|
+
private lastUsedAt;
|
|
275
289
|
private refreshInflight;
|
|
276
290
|
constructor(options?: AccountPoolOptions);
|
|
277
291
|
/**
|
|
@@ -283,6 +297,7 @@ export declare class WorkBuddyAccountPool {
|
|
|
283
297
|
authDirs?: readonly string[];
|
|
284
298
|
cooldownMs?: number;
|
|
285
299
|
distribution?: AccountDistribution;
|
|
300
|
+
disabledAccountIds?: readonly string[];
|
|
286
301
|
}): void;
|
|
287
302
|
/** Rescan the auth directories and merge newly discovered accounts. */
|
|
288
303
|
scan(): Promise<WorkBuddyAccount[]>;
|
|
@@ -298,6 +313,22 @@ export declare class WorkBuddyAccountPool {
|
|
|
298
313
|
* model, e.g. CLI diagnostics).
|
|
299
314
|
*/
|
|
300
315
|
private available;
|
|
316
|
+
/** Round-robin: the legacy cursor walk, kept for the distribution that asks for it. */
|
|
317
|
+
private pickRoundRobin;
|
|
318
|
+
/**
|
|
319
|
+
* Priority mode: weighted random over the eligible accounts.
|
|
320
|
+
*
|
|
321
|
+
* The weight is an idle bonus — `1 + min(idleHours * perHour, max)` — so an
|
|
322
|
+
* account that has never served (or has been idle for a while) outranks one
|
|
323
|
+
* that just answered. Reference panel logic drops its success-rate term
|
|
324
|
+
* entirely because a lifetime error counter penalises an account forever;
|
|
325
|
+
* instantaneous health is already handled by cooldowns, which is why those
|
|
326
|
+
* accounts never reach this list.
|
|
327
|
+
*
|
|
328
|
+
* A pool with no idle history (fresh process) hashes to equal weights, which
|
|
329
|
+
* spreads the very first picks instead of always returning index 0.
|
|
330
|
+
*/
|
|
331
|
+
private pickByWeight;
|
|
301
332
|
/**
|
|
302
333
|
* Pick the account to serve a request.
|
|
303
334
|
*
|
|
@@ -323,6 +354,10 @@ export declare class WorkBuddyAccountPool {
|
|
|
323
354
|
/** How the pool currently spreads requests. Shown on the card. */
|
|
324
355
|
currentDistribution(): AccountDistribution;
|
|
325
356
|
prefer(accountId: string | undefined): void;
|
|
357
|
+
/** Whether the user switched this account off on the card. */
|
|
358
|
+
isDisabled(accountId: string): boolean;
|
|
359
|
+
/** Every account id the user switched off, in discovery order. */
|
|
360
|
+
disabledIdsInOrder(): string[];
|
|
326
361
|
/** Best-effort refresh of one account after a session-dead upstream answer. */
|
|
327
362
|
refreshAccount(accountId: string): Promise<void>;
|
|
328
363
|
/**
|
|
@@ -572,6 +607,11 @@ interface PoolWebAccount {
|
|
|
572
607
|
modelId: string;
|
|
573
608
|
until: string;
|
|
574
609
|
}>;
|
|
610
|
+
/**
|
|
611
|
+
* Whether the user switched this account off. A disabled account never
|
|
612
|
+
* serves a request, but it stays listed so the card can switch it back on.
|
|
613
|
+
*/
|
|
614
|
+
disabled: boolean;
|
|
575
615
|
rateLimitHits: number;
|
|
576
616
|
/** ISO timestamp of the last successful use (best-effort pool bookkeeping). */
|
|
577
617
|
lastUsedAt?: string;
|
|
@@ -657,7 +697,6 @@ interface PoolWebModel {
|
|
|
657
697
|
/** Whether this model is currently enabled in the picker. */
|
|
658
698
|
enabled: boolean;
|
|
659
699
|
}
|
|
660
|
-
/** The user's saved model selection, echoed back so the card can diff a draft. */
|
|
661
700
|
interface PoolWebModelSelection {
|
|
662
701
|
/** Absent = every model is enabled. */
|
|
663
702
|
enabledModelIds?: readonly string[];
|
|
@@ -697,7 +736,7 @@ interface PoolWebStatus {
|
|
|
697
736
|
*/
|
|
698
737
|
type PoolRegion = 'cn' | 'global';
|
|
699
738
|
/** How the pool spreads requests across its accounts. */
|
|
700
|
-
type PoolDistribution = 'priority' | 'round-robin';
|
|
739
|
+
type PoolDistribution = 'priority' | 'round-robin' | 'balanced';
|
|
701
740
|
//#endregion
|
|
702
741
|
//#region src/index.d.ts
|
|
703
742
|
/** Stable Cordis plugin name. */
|
|
@@ -725,6 +764,13 @@ export interface Config {
|
|
|
725
764
|
* spend evenly instead. Absent reads as `priority`.
|
|
726
765
|
*/
|
|
727
766
|
distribution?: 'priority' | 'round-robin';
|
|
767
|
+
/**
|
|
768
|
+
* Account ids switched off on the card. A disabled account is never picked
|
|
769
|
+
* to serve a request, but it stays in the pool and on the card so it can be
|
|
770
|
+
* switched back on. Ids are the pool's stable per-credential keys, which
|
|
771
|
+
* survive re-scans (see WorkBuddyAccountPool.disabledIds).
|
|
772
|
+
*/
|
|
773
|
+
disabledAccountIds?: string[];
|
|
728
774
|
/**
|
|
729
775
|
* Model ids enabled in the picker. Absent means "every model the catalog
|
|
730
776
|
* advertises" — an unconfigured install should never present an empty model
|
package/lib/index.js
CHANGED
|
@@ -774,6 +774,26 @@ function candidateAuthDirs(env = process.env) {
|
|
|
774
774
|
* Read-only pool of every discovered WorkBuddy account, with rate-limit
|
|
775
775
|
* cooldown and round-robin failover.
|
|
776
776
|
*/
|
|
777
|
+
/** Idle bonus per hour an account has been unused (reference-panel default). */
|
|
778
|
+
const IDLE_WEIGHT_PER_HOUR = .5;
|
|
779
|
+
/** Ceiling for the idle bonus, so an idle account cannot dominate forever. */
|
|
780
|
+
const IDLE_WEIGHT_MAX = 5;
|
|
781
|
+
/**
|
|
782
|
+
* Weight one account by how long it has been idle.
|
|
783
|
+
*
|
|
784
|
+
* The base of 1 keeps every eligible account in play: an account that served a
|
|
785
|
+
* moment ago still has a small chance, so a single unhealthy account cannot pin
|
|
786
|
+
* the pool to itself, and the weights never sum to zero.
|
|
787
|
+
*
|
|
788
|
+
* `lastUsedAt === undefined` means "never used in this process", which earns the
|
|
789
|
+
* full bonus: on a fresh start every account ties, and the weighted draw spreads
|
|
790
|
+
* the first requests instead of always picking the first entry.
|
|
791
|
+
*/
|
|
792
|
+
function idleWeight(lastUsedAt, now) {
|
|
793
|
+
if (lastUsedAt === void 0) return 6;
|
|
794
|
+
const hours = (now - lastUsedAt) / 36e5;
|
|
795
|
+
return 1 + Math.min(Math.max(hours, 0) * IDLE_WEIGHT_PER_HOUR, IDLE_WEIGHT_MAX);
|
|
796
|
+
}
|
|
777
797
|
var WorkBuddyAccountPool = class {
|
|
778
798
|
logger;
|
|
779
799
|
authDirs;
|
|
@@ -786,6 +806,24 @@ var WorkBuddyAccountPool = class {
|
|
|
786
806
|
cursor = 0;
|
|
787
807
|
lastScanAtMs = 0;
|
|
788
808
|
preferredId;
|
|
809
|
+
/**
|
|
810
|
+
* Account ids the user switched off on the card.
|
|
811
|
+
*
|
|
812
|
+
* Disabling is a user preference rather than a property of the credential:
|
|
813
|
+
* `scan()` rebuilds every account object from the auth files, so the set
|
|
814
|
+
* lives on the pool and is re-applied from settings after each scan.
|
|
815
|
+
*/
|
|
816
|
+
disabledIds = /* @__PURE__ */ new Set();
|
|
817
|
+
/**
|
|
818
|
+
* Last time each account served a request, epoch ms. Drives the idle term
|
|
819
|
+
* of the priority-mode weighting below: an account that just served loses to
|
|
820
|
+
* one that has been idle, so a small pool stops hammering a single account.
|
|
821
|
+
*
|
|
822
|
+
* In-memory on purpose: it only biases the next pick, so a cold start that
|
|
823
|
+
* treats every account as idle is the right default. Not keyed by id lookup
|
|
824
|
+
* misses because a removed account simply disappears from the map on re-scan.
|
|
825
|
+
*/
|
|
826
|
+
lastUsedAt = /* @__PURE__ */ new Map();
|
|
789
827
|
refreshInflight = /* @__PURE__ */ new Map();
|
|
790
828
|
constructor(options = {}) {
|
|
791
829
|
this.logger = options.logger;
|
|
@@ -804,6 +842,7 @@ var WorkBuddyAccountPool = class {
|
|
|
804
842
|
if (options.authDirs !== void 0 && options.authDirs.length > 0) this.authDirs = options.authDirs;
|
|
805
843
|
if (options.cooldownMs !== void 0 && options.cooldownMs >= 1e3) this.cooldownMs = options.cooldownMs;
|
|
806
844
|
if (options.distribution !== void 0) this.distribution = options.distribution;
|
|
845
|
+
if (options.disabledAccountIds !== void 0) this.disabledIds = new Set(options.disabledAccountIds);
|
|
807
846
|
}
|
|
808
847
|
/** Rescan the auth directories and merge newly discovered accounts. */
|
|
809
848
|
async scan() {
|
|
@@ -856,12 +895,47 @@ var WorkBuddyAccountPool = class {
|
|
|
856
895
|
*/
|
|
857
896
|
available(now, modelId, region) {
|
|
858
897
|
return this.accounts.filter((account) => {
|
|
898
|
+
if (this.disabledIds.has(account.id)) return false;
|
|
859
899
|
if (account.cooldownUntilMs > now) return false;
|
|
860
900
|
if (modelId !== void 0 && (account.modelCooldowns[modelId] ?? 0) > now) return false;
|
|
861
901
|
if (region !== void 0 && regionOf(account.credential.domain) !== region) return false;
|
|
862
902
|
return true;
|
|
863
903
|
});
|
|
864
904
|
}
|
|
905
|
+
/** Round-robin: the legacy cursor walk, kept for the distribution that asks for it. */
|
|
906
|
+
pickRoundRobin(pool) {
|
|
907
|
+
const index = this.cursor % pool.length;
|
|
908
|
+
const account = pool[index];
|
|
909
|
+
if (account === void 0) return void 0;
|
|
910
|
+
this.cursor = (index + 1) % pool.length;
|
|
911
|
+
return account;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Priority mode: weighted random over the eligible accounts.
|
|
915
|
+
*
|
|
916
|
+
* The weight is an idle bonus — `1 + min(idleHours * perHour, max)` — so an
|
|
917
|
+
* account that has never served (or has been idle for a while) outranks one
|
|
918
|
+
* that just answered. Reference panel logic drops its success-rate term
|
|
919
|
+
* entirely because a lifetime error counter penalises an account forever;
|
|
920
|
+
* instantaneous health is already handled by cooldowns, which is why those
|
|
921
|
+
* accounts never reach this list.
|
|
922
|
+
*
|
|
923
|
+
* A pool with no idle history (fresh process) hashes to equal weights, which
|
|
924
|
+
* spreads the very first picks instead of always returning index 0.
|
|
925
|
+
*/
|
|
926
|
+
pickByWeight(pool) {
|
|
927
|
+
if (pool.length === 1) return pool[0];
|
|
928
|
+
const now = Date.now();
|
|
929
|
+
const weights = pool.map((account) => idleWeight(this.lastUsedAt.get(account.id), now));
|
|
930
|
+
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
|
931
|
+
if (!Number.isFinite(total) || total <= 0) return pool[0];
|
|
932
|
+
let roll = Math.random() * total;
|
|
933
|
+
for (let index = 0; index < pool.length; index += 1) {
|
|
934
|
+
roll -= weights[index] ?? 0;
|
|
935
|
+
if (roll < 0) return pool[index];
|
|
936
|
+
}
|
|
937
|
+
return pool[pool.length - 1];
|
|
938
|
+
}
|
|
865
939
|
/**
|
|
866
940
|
* Pick the account to serve a request.
|
|
867
941
|
*
|
|
@@ -891,16 +965,16 @@ var WorkBuddyAccountPool = class {
|
|
|
891
965
|
}
|
|
892
966
|
if (pool.length === 0) return void 0;
|
|
893
967
|
if (this.preferredId !== void 0) {
|
|
894
|
-
const
|
|
895
|
-
if (
|
|
896
|
-
|
|
897
|
-
|
|
968
|
+
const preferred = pool.find((account) => account.id === this.preferredId);
|
|
969
|
+
if (preferred !== void 0) {
|
|
970
|
+
this.lastUsedAt.set(preferred.id, Date.now());
|
|
971
|
+
await this.ensureFresh(preferred);
|
|
972
|
+
return preferred;
|
|
898
973
|
}
|
|
899
974
|
}
|
|
900
|
-
const
|
|
901
|
-
const account = pool[index];
|
|
975
|
+
const account = this.distribution === "round-robin" ? this.pickRoundRobin(pool) : this.distribution === "balanced" ? this.pickByWeight(pool) : pool[0];
|
|
902
976
|
if (account === void 0) return void 0;
|
|
903
|
-
|
|
977
|
+
this.lastUsedAt.set(account.id, Date.now());
|
|
904
978
|
await this.ensureFresh(account);
|
|
905
979
|
return account;
|
|
906
980
|
}
|
|
@@ -912,6 +986,14 @@ var WorkBuddyAccountPool = class {
|
|
|
912
986
|
prefer(accountId) {
|
|
913
987
|
this.preferredId = accountId;
|
|
914
988
|
}
|
|
989
|
+
/** Whether the user switched this account off on the card. */
|
|
990
|
+
isDisabled(accountId) {
|
|
991
|
+
return this.disabledIds.has(accountId);
|
|
992
|
+
}
|
|
993
|
+
/** Every account id the user switched off, in discovery order. */
|
|
994
|
+
disabledIdsInOrder() {
|
|
995
|
+
return this.accounts.filter((account) => this.disabledIds.has(account.id)).map((account) => account.id);
|
|
996
|
+
}
|
|
915
997
|
/** Best-effort refresh of one account after a session-dead upstream answer. */
|
|
916
998
|
async refreshAccount(accountId) {
|
|
917
999
|
const account = this.accounts.find((item) => item.id === accountId);
|
|
@@ -1333,10 +1415,10 @@ function createWorkBuddyAdapter(options) {
|
|
|
1333
1415
|
streamIdleTimeoutMs: WORKBUDDY_STREAM_IDLE_TIMEOUT_MS,
|
|
1334
1416
|
retryPolicy: resolveRetryPolicy(void 0, "dsh-workbuddy-xdpool retryPolicy"),
|
|
1335
1417
|
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
1336
|
-
modelErrors: /* @__PURE__ */ new Map(),
|
|
1337
1418
|
...REQUEST_IMAGE_BUDGETS,
|
|
1338
1419
|
piProvider: provider
|
|
1339
1420
|
};
|
|
1421
|
+
Object.assign(profile, { modelErrors: /* @__PURE__ */ new Map() });
|
|
1340
1422
|
let profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
|
|
1341
1423
|
return {
|
|
1342
1424
|
providerId,
|
|
@@ -1739,6 +1821,8 @@ const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/cooldowns/reset"
|
|
|
1739
1821
|
const POOL_CHECKIN_PATH = "/plugins/dsh-workbuddy-xdpool/checkin";
|
|
1740
1822
|
/** Plugin-owned model-selection save endpoint (writes the settings section). */
|
|
1741
1823
|
const POOL_MODELS_SAVE_PATH = "/plugins/dsh-workbuddy-xdpool/models/save";
|
|
1824
|
+
/** Switch one account in or out of the pool (card toggle). */
|
|
1825
|
+
const POOL_ACCOUNT_DISABLE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/disabled";
|
|
1742
1826
|
//#endregion
|
|
1743
1827
|
//#region src/web-status.ts
|
|
1744
1828
|
/** Redact token-like content before it crosses to the browser. */
|
|
@@ -1830,7 +1914,25 @@ function parseSelection(body) {
|
|
|
1830
1914
|
}
|
|
1831
1915
|
return out;
|
|
1832
1916
|
}
|
|
1833
|
-
|
|
1917
|
+
/**
|
|
1918
|
+
* Validate the account-toggle body. Returns undefined for anything malformed so
|
|
1919
|
+
* the route answers 400 instead of writing a half-applied switch.
|
|
1920
|
+
*
|
|
1921
|
+
* The id must name a currently known account: accepting an arbitrary string
|
|
1922
|
+
* would let a stale tab (or a renamed credential) leave orphan ids in the
|
|
1923
|
+
* settings file that no card can ever switch back off.
|
|
1924
|
+
*/
|
|
1925
|
+
function parseAccountToggle(body, known) {
|
|
1926
|
+
const accountId = typeof body["accountId"] === "string" ? body["accountId"].trim() : "";
|
|
1927
|
+
const disabled = body["disabled"];
|
|
1928
|
+
if (accountId === "" || typeof disabled !== "boolean") return void 0;
|
|
1929
|
+
if (!known(accountId)) return void 0;
|
|
1930
|
+
return {
|
|
1931
|
+
accountId,
|
|
1932
|
+
disabled
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
function toWebAccount(account, disabled) {
|
|
1834
1936
|
const now = Date.now();
|
|
1835
1937
|
const cooling = account.cooldownUntilMs > now;
|
|
1836
1938
|
const modelCooldowns = Object.entries(account.modelCooldowns).filter(([, until]) => until > now).sort((a, b) => a[1] - b[1]).map(([modelId, until]) => ({
|
|
@@ -1846,6 +1948,7 @@ function toWebAccount(account) {
|
|
|
1846
1948
|
cooling,
|
|
1847
1949
|
...cooling ? { cooldownUntil: new Date(account.cooldownUntilMs).toISOString() } : {},
|
|
1848
1950
|
...modelCooldowns.length === 0 ? {} : { modelCooldowns },
|
|
1951
|
+
disabled,
|
|
1849
1952
|
rateLimitHits: account.rateLimitHits
|
|
1850
1953
|
};
|
|
1851
1954
|
}
|
|
@@ -1878,7 +1981,7 @@ async function poolWebStatus(deps, region = "cn") {
|
|
|
1878
1981
|
const rows = [];
|
|
1879
1982
|
const now = Date.now();
|
|
1880
1983
|
for (const account of accounts) {
|
|
1881
|
-
const row = toWebAccount(account);
|
|
1984
|
+
const row = toWebAccount(account, deps.pool.isDisabled(account.id));
|
|
1882
1985
|
if (!row.cooling) {
|
|
1883
1986
|
try {
|
|
1884
1987
|
const credits = await deps.client.fetchCredits(account.credential);
|
|
@@ -2040,8 +2143,31 @@ function registerPoolStatusRoute(ctx, deps) {
|
|
|
2040
2143
|
}
|
|
2041
2144
|
}
|
|
2042
2145
|
});
|
|
2146
|
+
const disposeAccountDisable = ctx.webServer.register({
|
|
2147
|
+
kind: "exact",
|
|
2148
|
+
path: POOL_ACCOUNT_DISABLE_PATH,
|
|
2149
|
+
handler: async (req, res) => {
|
|
2150
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
2151
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
2152
|
+
if (deps.setAccountDisabled === void 0) return json(res, 503, { error: "settings service unavailable; the account switch cannot be saved" });
|
|
2153
|
+
try {
|
|
2154
|
+
const body = await readJsonBody(req);
|
|
2155
|
+
const known = new Set(deps.pool.list().map((account) => account.id));
|
|
2156
|
+
const toggle = parseAccountToggle(body, (id) => known.has(id));
|
|
2157
|
+
if (toggle === void 0) return json(res, 400, { error: "invalid account toggle payload" });
|
|
2158
|
+
await deps.setAccountDisabled(toggle.accountId, toggle.disabled);
|
|
2159
|
+
json(res, 200, {
|
|
2160
|
+
ok: true,
|
|
2161
|
+
...toggle
|
|
2162
|
+
});
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
json(res, 500, { error: safeMessage(error) });
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
2043
2168
|
return () => {
|
|
2044
2169
|
disposeCheckin();
|
|
2170
|
+
disposeAccountDisable();
|
|
2045
2171
|
disposeModelsSave();
|
|
2046
2172
|
disposeReset();
|
|
2047
2173
|
disposeRescan();
|
|
@@ -2100,6 +2226,7 @@ const Config = z.object({
|
|
|
2100
2226
|
authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app own location)"),
|
|
2101
2227
|
cooldownMs: z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds"),
|
|
2102
2228
|
distribution: z.union(["priority", "round-robin"]).default("priority").description("How requests are spread: priority (drain one) or round-robin"),
|
|
2229
|
+
disabledAccountIds: z.array(z.string()).default([]).description("Account ids excluded from the pool (empty = every discovered account participates)"),
|
|
2103
2230
|
enabledModelIds: z.array(z.string()).default([]).description("Legacy shared model-id list; used by a region that has no per-region selection yet"),
|
|
2104
2231
|
imageModelIds: z.array(z.string()).default([]).description("Legacy shared image-id list; used by a region that has no per-region selection yet"),
|
|
2105
2232
|
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Legacy shared context budgets; used by a region with no per-region selection yet"),
|
|
@@ -2169,11 +2296,12 @@ function apply(ctx, config = {}) {
|
|
|
2169
2296
|
}
|
|
2170
2297
|
};
|
|
2171
2298
|
const applyConfigFromSource = () => {
|
|
2172
|
-
const { authFile, cooldownMs, distribution, enabledModelIds, imageModelIds, contextBudgets, modelSelectionCn, modelSelectionGlobal } = current();
|
|
2299
|
+
const { authFile, cooldownMs, distribution, disabledAccountIds, enabledModelIds, imageModelIds, contextBudgets, modelSelectionCn, modelSelectionGlobal } = current();
|
|
2173
2300
|
core.pool.applyConfig({
|
|
2174
2301
|
...authFile === void 0 ? {} : { authDirs: [dirname(authFile)] },
|
|
2175
2302
|
...cooldownMs === void 0 ? {} : { cooldownMs },
|
|
2176
|
-
distribution: distribution ?? "priority"
|
|
2303
|
+
distribution: distribution ?? "priority",
|
|
2304
|
+
...disabledAccountIds === void 0 ? {} : { disabledAccountIds }
|
|
2177
2305
|
});
|
|
2178
2306
|
const legacySelection = {
|
|
2179
2307
|
...enabledModelIds === void 0 ? {} : { enabledModelIds },
|
|
@@ -2246,6 +2374,11 @@ function apply(ctx, config = {}) {
|
|
|
2246
2374
|
...selection.imageModelIds === void 0 ? {} : { imageModelIds: [...selection.imageModelIds] },
|
|
2247
2375
|
...selection.contextBudgets === void 0 ? {} : { contextBudgets: { ...selection.contextBudgets } }
|
|
2248
2376
|
});
|
|
2377
|
+
},
|
|
2378
|
+
setAccountDisabled: (accountId, disabled) => {
|
|
2379
|
+
const currentIds = current().disabledAccountIds ?? [];
|
|
2380
|
+
const next = disabled ? currentIds.includes(accountId) ? currentIds : [...currentIds, accountId] : currentIds.filter((id) => id !== accountId);
|
|
2381
|
+
setSetting("disabledAccountIds", next);
|
|
2249
2382
|
}
|
|
2250
2383
|
}));
|
|
2251
2384
|
api = {
|
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": "0.
|
|
5
|
+
"version": "1.0.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "XDTrees",
|
|
8
8
|
"homepage": "https://github.com/XDTrees/dsh-workbuddy-xdpool#readme",
|