dsh-workbuddy-xdpool 1.7.6 → 1.7.8
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 +134 -0
- package/README.md +8 -4
- package/lib/bin.js +64 -0
- package/lib/client.js +44 -2
- package/lib/index.d.ts +19 -0
- package/lib/index.js +116 -7
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,140 @@
|
|
|
4
4
|
|
|
5
5
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
6
6
|
|
|
7
|
+
## 1.7.8 — 说清楚卡片在哪儿、以及我们支持到哪个版本
|
|
8
|
+
|
|
9
|
+
> 这次没有功能问题,全在「声明」上:一处把你引到不存在的路径,一处让你以为装错了版本。
|
|
10
|
+
|
|
11
|
+
### README 的入口路径是 0.1.x 时代的,0.2.0 上按它找不到
|
|
12
|
+
|
|
13
|
+
README 一直写着「设置 → 插件 → DSH WorkBuddy XD Pool」。**0.2.0 起没有这条二层路径了。**
|
|
14
|
+
|
|
15
|
+
实际位置是:**打开设置面板 → 顶部平级标签里的「XD Pool」**(排在 `通用设置 / 模型 / 内置插件 / Agent 预设` 后面)。开设置面板的方式是**侧边栏左下角的齿轮**,或快捷键 `Ctrl+Alt+,`。
|
|
16
|
+
|
|
17
|
+
已把 README 三处改掉,并且**补上了开面板的方法**——只写「切到 XD Pool」而不说怎么开设置,等于没写。老路径保留一句「0.1.x 时代叫这个」,方便升级上来的人理解自己的习惯为什么突然不灵了。
|
|
18
|
+
|
|
19
|
+
### `engines.dsh` 写着 `<0.2.0`,可它在 0.2.0 上跑得好好的
|
|
20
|
+
|
|
21
|
+
这个声明的问题不在于"保守",而在于**它会把人带偏**:任何人按 `engines` 做兼容判断,第一反应都是"这插件不支持 0.2.0",然后往错误的方向排查。反馈里就有一位因此走了一大圈。
|
|
22
|
+
|
|
23
|
+
现在放宽到 `<0.3.0`,并且**把三个 host peer 依赖(`dsh-llm` / `dsh-llm-pi-ai` / `dsh-settings`)一起对齐**——它们约束的是同一批宿主包,只改一个反而制造新的不一致。
|
|
24
|
+
|
|
25
|
+
放宽不等于"什么都接受":上界仍然卡在 `<0.3.0`,**没测过的大版本不该被默认声称支持**。
|
|
26
|
+
|
|
27
|
+
### 导航图标匹配不上时,现在会说话
|
|
28
|
+
|
|
29
|
+
`installNavIcon()` 靠选择器找设置面板里那一行。**找不到时它完全静默** —— 表现就是"图标没出来",没有任何线索。这次排查里它不是故障点,但它是最贵的一环:**看起来什么都没发生**。
|
|
30
|
+
|
|
31
|
+
现在连续匹配失败会 `console.warn` 一次,带上选择器、期望的标签、以及**当前导航里实际有什么**:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
[dsh-workbuddy-xdpool] the settings nav row could not be found, so the icon was not applied.
|
|
35
|
+
selector="[role=\"dialog\"] nav button" expectedLabel="XD Pool" rowsPresent=["通用设置","模型",...]
|
|
36
|
+
This is cosmetic only — the card itself still works. Please report this line…
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**不是每次都喊**:设置面板没打开时找不到是**正常的**(那一行压根不在 DOM 里),每次都喊只会让人学会忽略它。所以只在"面板开着、却连续 N 次找不到"时才报一次,且每轮只报一次。
|
|
40
|
+
|
|
41
|
+
### 关于那份"0.2.0 删了 slots 服务"的报告
|
|
42
|
+
|
|
43
|
+
反馈者**自己撤回了**那个结论,我更正一下:**不成立**。
|
|
44
|
+
|
|
45
|
+
他后来实测到:`__DSH_BOOT__.entries` 含本插件、`importErrors` 为空、客户端 fiber 状态是 ACTIVE、卡片 DOM 完整渲染、`installNavIcon` 正常生效。而且**把 `dsh.client.inject` 改掉再测,行为完全一致** —— 所以那两个包的声明**不会阻断加载,不需要改**。
|
|
46
|
+
|
|
47
|
+
他复盘出的两个方法论坑,我认为值得原样记下来,因为它们非常容易重犯:
|
|
48
|
+
- 只用正则扫「谁 `provide("name")` 了服务」,漏掉别的声明写法,就宣布服务被删了 —— **典型的假阴性**;
|
|
49
|
+
- 在**会话页**(设置面板没开)dump DOM 搜插件节点,0 命中当成故障证据 —— 而 `settings.section` **只在面板打开时才进 DOM**,0 命中是必然的。
|
|
50
|
+
|
|
51
|
+
`slots` 和 `settings.section` 在 0.2.0 里都健在,渲染方是 `dsh-client-ui-settings-general` 的 `renderSlot("settings.section", …)`。
|
|
52
|
+
|
|
53
|
+
### 验证
|
|
54
|
+
|
|
55
|
+
- 类型检查全绿,测试 **332 项全绿**(+7)。
|
|
56
|
+
- 新增 `tests/compatibility-declaration.test.ts`:`engines.dsh` 必须容纳 0.2.x、**上界仍要卡住没测过的大版本**、三个 host peer 与 `engines.dsh` 保持一致、README **不得再把老路径当指引**(但允许作为历史说明出现)、必须写出开设置面板的方法。
|
|
57
|
+
- **反向验证**:把 `engines` 退回 `<0.2.0` → 2 项变红;把 README 退回老路径 → 1 项变红。
|
|
58
|
+
- 本机核对宿主代码:`renderSlot("settings.section", { close }, { only: active })` 确为**平级渲染**,插件注册 `order: 440`(排最后),与反馈的实测描述一致。
|
|
59
|
+
|
|
60
|
+
### 一点说明
|
|
61
|
+
|
|
62
|
+
我本机是 DSH Desktop 2.0.15(内核 0.1.7-rc.2),**不是 0.2.0-rc.2**,所以我**没法在本地复现 0.2.0 的渲染行为**。本次改动依据的是反馈者提供的实测数据,外加我对本机宿主代码里 `renderSlot` 调用的核对。放宽 `engines` 这个结论我是采信的,但没有在 0.2.0 上亲手验过。
|
|
63
|
+
|
|
64
|
+
## 1.7.7 — 别再让你为了一个「等一会儿」去重新登录
|
|
65
|
+
|
|
66
|
+
> 报错说「API 密钥无效」,于是你跑去重新登录 —— 但密钥根本没问题,只是那个模型暂时限流了。
|
|
67
|
+
|
|
68
|
+
### 问题一:限流被说成了「密钥无效」
|
|
69
|
+
|
|
70
|
+
**你会看到**:国际版用 hy4 时,偶尔弹出「API 密钥无效」。你重新登录、换账号,都不一定管用。
|
|
71
|
+
|
|
72
|
+
**为什么**:看日志就清楚了(时间连着三行):
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
14:50:22 qingche526 rate-limited on hy4-preview-f → cooling + rotating
|
|
76
|
+
14:50:23 aosi526 rate-limited on hy4-preview-f → cooling + rotating
|
|
77
|
+
14:50:25 401: "no WorkBuddy credential found; sign in on the desktop app"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
两个账号的 hy4-preview-f 都被限流**冷却**了(一小时),于是池子里暂时没有可用账号。可插件判断「有没有被限流」用的是**本次请求内**的临时记录 —— 上次请求造成的冷却,它看不见。于是它以为「一个账号都没有 → 你没登录」→ 报 **401**。
|
|
81
|
+
|
|
82
|
+
DSH 把 401 显示成「API 密钥无效」。**你被派去修一个根本没坏的东西。**
|
|
83
|
+
|
|
84
|
+
**怎么修的**:不再靠本次请求的临时记录猜,而是**直接问账号池「为什么现在没有可用账号」**,然后按真实原因报对应的错:
|
|
85
|
+
|
|
86
|
+
| 真实情况 | 以前 | 现在 |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| 模型限流中 | 401 密钥无效 | **429 该模型限流中**(等一会儿即可)|
|
|
89
|
+
| 账号全被关/移出 | 401 密钥无效 | **403 账号全部已关闭**(去卡片打开)|
|
|
90
|
+
| 积分低于保留线 | 401 密钥无效 | **402 积分不足** |
|
|
91
|
+
| 确实没登录 | 401 | 401(这个才是对的)|
|
|
92
|
+
|
|
93
|
+
四种情况**需要完全相反的处理**,以前却都推给你「重新登录」。现在只有真的该重新登录时才这么说。
|
|
94
|
+
|
|
95
|
+
### 问题二:上下文「压缩」把目标设得比原文还大
|
|
96
|
+
|
|
97
|
+
日志里抓到的:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
context overrun on deepseek-v4.1-flash (~791793 tokens); compacting to ~797952
|
|
101
|
+
↑ 原文 79.1 万 ↑ 目标 79.7 万
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**压缩目标比要压缩的东西还大** —— 等于什么都没压,重试当然又超限。而日志看起来还挺像"我处理过了"。
|
|
105
|
+
|
|
106
|
+
**为什么**:目标值是按**目录里写的窗口**算的(`deepseek-v4.1-flash` 标称 1M),取 80% → `797952`。但**标称 1M 是宣传值,不是真实生效上限** —— 否则它就不会被拒了。
|
|
107
|
+
|
|
108
|
+
**怎么修的**:**正在超限的那个请求本身就是证据**。既然它太大了,目标就必须明显小于它。现在取「按窗口算」和「按原文一半」的较小值:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
原文 791793 → 目标 395896 (真正压掉一半)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 顺便:这类报错终于有线索了
|
|
115
|
+
|
|
116
|
+
上游的 `model_param_invalid` 里 **`param` 字段是空的** —— 它只说「参数不符合要求」,不说是哪个参数。你之前贴的那条 11133 就是这样,等于没有线索。
|
|
117
|
+
|
|
118
|
+
现在遇到 400 时,日志会记下**请求的形状**(顶层参数名、各角色的消息条数、工具数量、内容字符数):
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
rejected the request as invalid (model deepseek-v4.1-flash) —
|
|
122
|
+
keys=[max_tokens messages model reasoning_effort stream temperature tool_choice tools top_k top_p]
|
|
123
|
+
messages=122 (system:1 user:61 assistant:30 tool:30) tools=77 contentChars=481233
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**只有形状,没有内容**(对话是你的,不该进日志)。下次再遇到,日志里就能看出是哪个参数/哪个维度不对。
|
|
127
|
+
|
|
128
|
+
### 我排查时的事
|
|
129
|
+
|
|
130
|
+
我一开始想直接复现 11133,试了:采样参数(`top_k`/`repetition_penalty`/`temperature`/`top_p`)、推理档位(`minimal` 到 `max`,还试了无效档位)、各种消息形状(回放 `reasoning_content`、工具调用链)、消息数量(最多 362 条)、77 个工具 + 长会话、超长 prompt ——**国内版和国际版全部返回 200**。
|
|
131
|
+
|
|
132
|
+
**我没能复现它**,所以没有针对 11133 写"修复"(那会是猜测)。改的都是**从日志里坐实的**问题:那两个是确凿的。这个 11133 加上刚加的形状日志,下次出现时就能定位了。
|
|
133
|
+
|
|
134
|
+
### 验证
|
|
135
|
+
|
|
136
|
+
- 类型检查全绿,测试 **325 项全绿**(+12)。
|
|
137
|
+
- 新增 `tests/pool-availability.test.ts`(7 项):池能正确区分「无人登录 / 模型限流 / 账号全关 / 积分保留 / 可用」、**限流时返回 429 且文案不含「sign in」**、真没登录时仍是 401、**不把另一个 region 的账号算进来**、过期冷却自动视为可用。
|
|
138
|
+
- 新增 `tests/context-budget-budget.test.ts`(5 项):**目录窗口远大于原文时目标必须更小**(用日志里那组真实数字)、窗口未知时减半、窗口小于原文时也不放大、目标不为零。
|
|
139
|
+
- **反向验证**:把 429 分支禁用、退回原来的 401 → 测试变红。
|
|
140
|
+
|
|
7
141
|
## 1.7.6 — 一个读不出来的凭据文件,不再带走整个账号池
|
|
8
142
|
|
|
9
143
|
> 你切了 4 个账号登录,界面显示 4 个;重启之后只剩 2 个 —— 不是账号没了,是它们被一个文件拖累了。
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
## 先看几张图
|
|
21
21
|
|
|
22
|
-
**插件配置卡片(设置 →
|
|
22
|
+
**插件配置卡片(设置 → 「XD Pool」一节)**
|
|
23
23
|
|
|
24
24
|

|
|
25
25
|
|
|
@@ -83,9 +83,13 @@
|
|
|
83
83
|
|
|
84
84
|
## 安装
|
|
85
85
|
|
|
86
|
-
前置条件:装好 WorkBuddy 桌面 App 并登录(插件复用它存在本机的登录状态)。多账号 = 在桌面 App
|
|
86
|
+
前置条件:装好 WorkBuddy 桌面 App 并登录(插件复用它存在本机的登录状态)。多账号 = 在桌面 App 里换着登几次,每次登录都会被自动吸进池里。
|
|
87
87
|
|
|
88
|
-
> 兼容 host `0.1.1-rc.2`
|
|
88
|
+
> 兼容 host `0.1.1-rc.2` 起的整个 0.1.x 系列,以及 `0.2.0-rc.2`。安装设置节时会按 host 能力自动选择 `settings.installSection`(0.1.2-rc.1+)或更早的自由函数写法;卡片在 0.2.0 上作为设置面板里一个**平级标签页**渲染。
|
|
89
|
+
|
|
90
|
+
> **卡片在哪儿打开**:DSH 设置面板本身。开法是**侧边栏左下角的齿轮**,或者快捷键 `Ctrl+Alt+,`。
|
|
91
|
+
> 面板顶部是平级标签:`通用设置 / 模型 / 内置插件 / Agent 预设 / XD Pool` —— 本插件就是最后那个 **XD Pool**。
|
|
92
|
+
> (0.1.x 时代它叫「设置 → 插件 → DSH WorkBuddy XD Pool」,0.2.0 起改成了平级标签,按老路径找不到。)
|
|
89
93
|
|
|
90
94
|
**方式一:从 npm 安装(推荐)**
|
|
91
95
|
|
|
@@ -129,7 +133,7 @@ pnpm typecheck:client # 客户端类型检查
|
|
|
129
133
|
|
|
130
134
|
> 注意:`pnpm install` 要用 pnpm 11(`npx pnpm@11`),必要时加 `--config.confirmModulesPurge=false --config.minimumReleaseAge=0`——pnpm 11 默认的 `minimumReleaseAge` 供应链年龄策略会拦下刚发布的 rc 包。
|
|
131
135
|
|
|
132
|
-
装完以后:模型选择器里会出现 **WorkBuddy XD Pool**
|
|
136
|
+
装完以后:模型选择器里会出现 **WorkBuddy XD Pool** 分组;打开设置面板(左下角齿轮或 `Ctrl+Alt+,`)切到 **XD Pool** 一栏,能看池健康、各账号的令牌 / 积分 / 签到 / 冷却状态,还有「重新检测账号」「清除所有冷却」按钮和每个账号的签到按钮。
|
|
133
137
|
|
|
134
138
|
Web / TUI profile 也能用(`--profile web` / `--profile dsh-tui`)。
|
|
135
139
|
|
package/lib/bin.js
CHANGED
|
@@ -2846,6 +2846,70 @@ var WorkBuddyAccountPool = class {
|
|
|
2846
2846
|
return true;
|
|
2847
2847
|
});
|
|
2848
2848
|
}
|
|
2849
|
+
/**
|
|
2850
|
+
* Why no account is available right now, for an accurate error.
|
|
2851
|
+
*
|
|
2852
|
+
* The pool can be empty for reasons that need OPPOSITE remedies: nobody is
|
|
2853
|
+
* signed in (the user must sign in), every account is rate-limited (the user
|
|
2854
|
+
* must wait, and retrying later works), or every account was switched off /
|
|
2855
|
+
* ignored (the user must re-enable one). Reporting all of them as "no
|
|
2856
|
+
* credential, sign in" sent users to re-authenticate over a temporary 429 —
|
|
2857
|
+
* observed as an "API key invalid" panel for a model that was merely cooling.
|
|
2858
|
+
*
|
|
2859
|
+
* Counts are over the region's accounts, since a provider only ever sees its
|
|
2860
|
+
* own gateway.
|
|
2861
|
+
*/
|
|
2862
|
+
unavailableReason(modelId, region) {
|
|
2863
|
+
const now = Date.now();
|
|
2864
|
+
const inRegion = this.accounts.filter((account) => region === void 0 || regionOf(account.credential.domain) === region);
|
|
2865
|
+
if (inRegion.length === 0) return {
|
|
2866
|
+
total: 0,
|
|
2867
|
+
cooling: 0,
|
|
2868
|
+
disabled: 0,
|
|
2869
|
+
reason: "empty"
|
|
2870
|
+
};
|
|
2871
|
+
let cooling = 0;
|
|
2872
|
+
let disabled = 0;
|
|
2873
|
+
for (const account of inRegion) {
|
|
2874
|
+
if (this.disabledIds.has(account.id)) {
|
|
2875
|
+
disabled += 1;
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
const modelCooling = modelId !== void 0 && (account.modelCooldowns[modelId] ?? 0) > now;
|
|
2879
|
+
if (account.cooldownUntilMs > now || modelCooling) {
|
|
2880
|
+
cooling += 1;
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
const reserve = this.creditReserves.get(account.id);
|
|
2884
|
+
if (reserve !== void 0 && reserve > 0) {
|
|
2885
|
+
const balance = this.creditBalances.get(account.id);
|
|
2886
|
+
if (balance !== void 0 && balance <= reserve) return {
|
|
2887
|
+
total: inRegion.length,
|
|
2888
|
+
cooling,
|
|
2889
|
+
disabled,
|
|
2890
|
+
reason: "reserve"
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
if (cooling > 0) return {
|
|
2895
|
+
total: inRegion.length,
|
|
2896
|
+
cooling,
|
|
2897
|
+
disabled,
|
|
2898
|
+
reason: "cooling"
|
|
2899
|
+
};
|
|
2900
|
+
if (disabled > 0) return {
|
|
2901
|
+
total: inRegion.length,
|
|
2902
|
+
cooling,
|
|
2903
|
+
disabled,
|
|
2904
|
+
reason: "disabled"
|
|
2905
|
+
};
|
|
2906
|
+
return {
|
|
2907
|
+
total: inRegion.length,
|
|
2908
|
+
cooling,
|
|
2909
|
+
disabled,
|
|
2910
|
+
reason: "none"
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2849
2913
|
/** Round-robin: the legacy cursor walk, kept for the distribution that asks for it. */
|
|
2850
2914
|
pickRoundRobin(pool) {
|
|
2851
2915
|
const index = this.cursor % pool.length;
|
package/lib/client.js
CHANGED
|
@@ -2536,6 +2536,15 @@ window.__ModuleLoader__.load({
|
|
|
2536
2536
|
*/
|
|
2537
2537
|
const NAV_ROW_SELECTOR = "[role=\"dialog\"] nav button";
|
|
2538
2538
|
/**
|
|
2539
|
+
* How many consecutive in-dialog passes may miss the nav row before the
|
|
2540
|
+
* diagnostic fires.
|
|
2541
|
+
*
|
|
2542
|
+
* Above 1 because a single miss is routine — the shell can re-render between
|
|
2543
|
+
* our mutation callback and the query. Small enough that a genuinely broken
|
|
2544
|
+
* selector is reported within a frame or two of opening the settings panel.
|
|
2545
|
+
*/
|
|
2546
|
+
const NAV_ICON_MISS_THRESHOLD = 3;
|
|
2547
|
+
/**
|
|
2539
2548
|
* Draw this page's own glyph in its Settings nav row.
|
|
2540
2549
|
*
|
|
2541
2550
|
* The `settings.section` contract carries no icon: the shell decides the glyph
|
|
@@ -2576,13 +2585,46 @@ window.__ModuleLoader__.load({
|
|
|
2576
2585
|
document.head.appendChild(tag);
|
|
2577
2586
|
let disposed = false;
|
|
2578
2587
|
let scheduled = false;
|
|
2588
|
+
/**
|
|
2589
|
+
* Consecutive passes that found no matching row.
|
|
2590
|
+
*
|
|
2591
|
+
* A miss is NORMAL most of the time: the settings dialog is closed, so its
|
|
2592
|
+
* nav is not in the DOM at all, and the icon stays unapplied by design.
|
|
2593
|
+
* Warning on the first miss would fire constantly and train everyone to
|
|
2594
|
+
* ignore it.
|
|
2595
|
+
*
|
|
2596
|
+
* What is worth reporting is a PERSISTENT miss while the dialog IS open —
|
|
2597
|
+
* that means the shell's nav markup changed and the selector no longer
|
|
2598
|
+
* matches, which is exactly the failure that looks like "nothing happened"
|
|
2599
|
+
* and cost hours to find. So the threshold is a burst of misses, and the
|
|
2600
|
+
* warning is emitted once per burst rather than once per mutation.
|
|
2601
|
+
*/
|
|
2602
|
+
let missStreak = 0;
|
|
2603
|
+
let warned = false;
|
|
2579
2604
|
const sync = () => {
|
|
2580
2605
|
scheduled = false;
|
|
2581
2606
|
if (disposed) return;
|
|
2582
2607
|
const wanted = String(resolveLabel() ?? "").trim();
|
|
2583
2608
|
if (wanted === "") return;
|
|
2584
|
-
|
|
2585
|
-
|
|
2609
|
+
let matched = 0;
|
|
2610
|
+
for (const row of document.querySelectorAll(NAV_ROW_SELECTOR)) if (String(row.textContent ?? "").trim() === wanted) {
|
|
2611
|
+
row.setAttribute(NAV_ICON_MARKER, "");
|
|
2612
|
+
matched += 1;
|
|
2613
|
+
} else row.removeAttribute(NAV_ICON_MARKER);
|
|
2614
|
+
if (matched > 0) {
|
|
2615
|
+
missStreak = 0;
|
|
2616
|
+
warned = false;
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
if (document.querySelector(NAV_ROW_SELECTOR) === null) {
|
|
2620
|
+
missStreak = 0;
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
missStreak += 1;
|
|
2624
|
+
if (missStreak < NAV_ICON_MISS_THRESHOLD || warned) return;
|
|
2625
|
+
warned = true;
|
|
2626
|
+
const present = [...document.querySelectorAll(NAV_ROW_SELECTOR)].map((row) => String(row.textContent ?? "").trim()).filter((text) => text !== "");
|
|
2627
|
+
console.warn(`[dsh-workbuddy-xdpool] the settings nav row could not be found, so the icon was not applied. selector=${JSON.stringify(NAV_ROW_SELECTOR)} expectedLabel=${JSON.stringify(wanted)} rowsPresent=${JSON.stringify(present)}. This is cosmetic only — the card itself still works. Please report this line so the selector can be updated.`);
|
|
2586
2628
|
};
|
|
2587
2629
|
const schedule = () => {
|
|
2588
2630
|
if (scheduled || disposed) return;
|
package/lib/index.d.ts
CHANGED
|
@@ -989,6 +989,25 @@ export declare class WorkBuddyAccountPool {
|
|
|
989
989
|
* model, e.g. CLI diagnostics).
|
|
990
990
|
*/
|
|
991
991
|
private available;
|
|
992
|
+
/**
|
|
993
|
+
* Why no account is available right now, for an accurate error.
|
|
994
|
+
*
|
|
995
|
+
* The pool can be empty for reasons that need OPPOSITE remedies: nobody is
|
|
996
|
+
* signed in (the user must sign in), every account is rate-limited (the user
|
|
997
|
+
* must wait, and retrying later works), or every account was switched off /
|
|
998
|
+
* ignored (the user must re-enable one). Reporting all of them as "no
|
|
999
|
+
* credential, sign in" sent users to re-authenticate over a temporary 429 —
|
|
1000
|
+
* observed as an "API key invalid" panel for a model that was merely cooling.
|
|
1001
|
+
*
|
|
1002
|
+
* Counts are over the region's accounts, since a provider only ever sees its
|
|
1003
|
+
* own gateway.
|
|
1004
|
+
*/
|
|
1005
|
+
unavailableReason(modelId?: string, region?: WorkBuddyRegion): {
|
|
1006
|
+
total: number;
|
|
1007
|
+
cooling: number;
|
|
1008
|
+
disabled: number;
|
|
1009
|
+
reason: 'empty' | 'cooling' | 'disabled' | 'reserve' | 'none';
|
|
1010
|
+
};
|
|
992
1011
|
/** Round-robin: the legacy cursor walk, kept for the distribution that asks for it. */
|
|
993
1012
|
private pickRoundRobin;
|
|
994
1013
|
/**
|
package/lib/index.js
CHANGED
|
@@ -2954,6 +2954,70 @@ var WorkBuddyAccountPool = class {
|
|
|
2954
2954
|
return true;
|
|
2955
2955
|
});
|
|
2956
2956
|
}
|
|
2957
|
+
/**
|
|
2958
|
+
* Why no account is available right now, for an accurate error.
|
|
2959
|
+
*
|
|
2960
|
+
* The pool can be empty for reasons that need OPPOSITE remedies: nobody is
|
|
2961
|
+
* signed in (the user must sign in), every account is rate-limited (the user
|
|
2962
|
+
* must wait, and retrying later works), or every account was switched off /
|
|
2963
|
+
* ignored (the user must re-enable one). Reporting all of them as "no
|
|
2964
|
+
* credential, sign in" sent users to re-authenticate over a temporary 429 —
|
|
2965
|
+
* observed as an "API key invalid" panel for a model that was merely cooling.
|
|
2966
|
+
*
|
|
2967
|
+
* Counts are over the region's accounts, since a provider only ever sees its
|
|
2968
|
+
* own gateway.
|
|
2969
|
+
*/
|
|
2970
|
+
unavailableReason(modelId, region) {
|
|
2971
|
+
const now = Date.now();
|
|
2972
|
+
const inRegion = this.accounts.filter((account) => region === void 0 || regionOf(account.credential.domain) === region);
|
|
2973
|
+
if (inRegion.length === 0) return {
|
|
2974
|
+
total: 0,
|
|
2975
|
+
cooling: 0,
|
|
2976
|
+
disabled: 0,
|
|
2977
|
+
reason: "empty"
|
|
2978
|
+
};
|
|
2979
|
+
let cooling = 0;
|
|
2980
|
+
let disabled = 0;
|
|
2981
|
+
for (const account of inRegion) {
|
|
2982
|
+
if (this.disabledIds.has(account.id)) {
|
|
2983
|
+
disabled += 1;
|
|
2984
|
+
continue;
|
|
2985
|
+
}
|
|
2986
|
+
const modelCooling = modelId !== void 0 && (account.modelCooldowns[modelId] ?? 0) > now;
|
|
2987
|
+
if (account.cooldownUntilMs > now || modelCooling) {
|
|
2988
|
+
cooling += 1;
|
|
2989
|
+
continue;
|
|
2990
|
+
}
|
|
2991
|
+
const reserve = this.creditReserves.get(account.id);
|
|
2992
|
+
if (reserve !== void 0 && reserve > 0) {
|
|
2993
|
+
const balance = this.creditBalances.get(account.id);
|
|
2994
|
+
if (balance !== void 0 && balance <= reserve) return {
|
|
2995
|
+
total: inRegion.length,
|
|
2996
|
+
cooling,
|
|
2997
|
+
disabled,
|
|
2998
|
+
reason: "reserve"
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
if (cooling > 0) return {
|
|
3003
|
+
total: inRegion.length,
|
|
3004
|
+
cooling,
|
|
3005
|
+
disabled,
|
|
3006
|
+
reason: "cooling"
|
|
3007
|
+
};
|
|
3008
|
+
if (disabled > 0) return {
|
|
3009
|
+
total: inRegion.length,
|
|
3010
|
+
cooling,
|
|
3011
|
+
disabled,
|
|
3012
|
+
reason: "disabled"
|
|
3013
|
+
};
|
|
3014
|
+
return {
|
|
3015
|
+
total: inRegion.length,
|
|
3016
|
+
cooling,
|
|
3017
|
+
disabled,
|
|
3018
|
+
reason: "none"
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
2957
3021
|
/** Round-robin: the legacy cursor walk, kept for the distribution that asks for it. */
|
|
2958
3022
|
pickRoundRobin(pool) {
|
|
2959
3023
|
const index = this.cursor % pool.length;
|
|
@@ -5548,6 +5612,40 @@ function isContextTooLong(body) {
|
|
|
5548
5612
|
if (/exceeds?\s+(the\s+)?(model\s+)?context\s+(window|limit)/iu.test(body)) return true;
|
|
5549
5613
|
return false;
|
|
5550
5614
|
}
|
|
5615
|
+
/**
|
|
5616
|
+
* A content-free description of a request, for the log when the upstream
|
|
5617
|
+
* rejects it.
|
|
5618
|
+
*
|
|
5619
|
+
* `model_param_invalid` arrives with an EMPTY `param` field, so the error alone
|
|
5620
|
+
* says nothing about which parameter was rejected — the user is left with "the
|
|
5621
|
+
* request parameters do not meet the current model requirements" and no way to
|
|
5622
|
+
* act. Recording the shape (top-level keys, message count by role, tool count,
|
|
5623
|
+
* approximate prompt size) makes such a rejection diagnosable from the log.
|
|
5624
|
+
*
|
|
5625
|
+
* Deliberately SHAPE ONLY: no message content, no tool schemas, no tokens. It
|
|
5626
|
+
* ends up in a log file, and the conversation it describes is the user's.
|
|
5627
|
+
*/
|
|
5628
|
+
function requestShape(raw) {
|
|
5629
|
+
try {
|
|
5630
|
+
const parsed = JSON.parse(raw);
|
|
5631
|
+
const keys = Object.keys(parsed).sort();
|
|
5632
|
+
const messages = Array.isArray(parsed["messages"]) ? parsed["messages"] : [];
|
|
5633
|
+
const byRole = /* @__PURE__ */ new Map();
|
|
5634
|
+
let chars = 0;
|
|
5635
|
+
for (const entry of messages) {
|
|
5636
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
5637
|
+
const message = entry;
|
|
5638
|
+
const role = typeof message["role"] === "string" ? message["role"] : "?";
|
|
5639
|
+
byRole.set(role, (byRole.get(role) ?? 0) + 1);
|
|
5640
|
+
chars += JSON.stringify(message["content"] ?? "").length;
|
|
5641
|
+
}
|
|
5642
|
+
const roles = [...byRole.entries()].map(([role, n]) => `${role}:${n}`).join(" ");
|
|
5643
|
+
const tools = Array.isArray(parsed["tools"]) ? parsed["tools"].length : 0;
|
|
5644
|
+
return `keys=[${keys.join(" ")}] messages=${messages.length}${roles === "" ? "" : ` (${roles})`} tools=${tools} contentChars=${chars}`;
|
|
5645
|
+
} catch {
|
|
5646
|
+
return "body was not parseable JSON";
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5551
5649
|
function readBody(req) {
|
|
5552
5650
|
return new Promise((resolve, reject) => {
|
|
5553
5651
|
const chunks = [];
|
|
@@ -5686,17 +5784,26 @@ function createWorkBuddyShim(options) {
|
|
|
5686
5784
|
}
|
|
5687
5785
|
const tried = [];
|
|
5688
5786
|
let last;
|
|
5689
|
-
let exhaustedByRateLimit = false;
|
|
5690
5787
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
5691
5788
|
if (controller.signal.aborted) return;
|
|
5692
5789
|
const account = await pool.acquire(modelId, region);
|
|
5693
5790
|
if (account === void 0) {
|
|
5694
|
-
|
|
5791
|
+
const why = pool.unavailableReason(modelId, region);
|
|
5792
|
+
if (why.reason === "cooling") {
|
|
5695
5793
|
const subject = modelId === void 0 ? "every WorkBuddy account is rate-limited" : `every account is rate-limited for model ${modelId}`;
|
|
5696
|
-
|
|
5794
|
+
const upstream = last === void 0 ? "" : ` — ${last.message.slice(0, 200)}`;
|
|
5795
|
+
writeOpenAIError(res, 429, "soft_rate", `${subject} (${why.cooling}/${why.total} cooling, tried ${tried.length})${tried.length === 0 ? "" : `: ${tried.join(" → ")}`}${upstream}`);
|
|
5796
|
+
return;
|
|
5797
|
+
}
|
|
5798
|
+
if (why.reason === "disabled") {
|
|
5799
|
+
writeOpenAIError(res, 403, "no_enabled_account", `all ${why.total} WorkBuddy account(s) for this gateway are switched off or ignored; re-enable one on the pool card`);
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
if (why.reason === "reserve") {
|
|
5803
|
+
writeOpenAIError(res, 402, "reserve_floor", `every WorkBuddy account is at or below its credit reserve (${why.total} account(s)); lower the reserve on the pool card or top up credits`);
|
|
5697
5804
|
return;
|
|
5698
5805
|
}
|
|
5699
|
-
writeOpenAIError(res, 401, "not_signed_in", "no WorkBuddy credential found; sign in on the desktop app (or set WORKBUDDY_AUTH_FILE)");
|
|
5806
|
+
writeOpenAIError(res, 401, "not_signed_in", why.total === 0 ? "no WorkBuddy credential found for this gateway; sign in on the desktop app (or set WORKBUDDY_AUTH_FILE)" : `no usable WorkBuddy credential among ${why.total} account(s) for this gateway; sign in again on the desktop app`);
|
|
5700
5807
|
return;
|
|
5701
5808
|
}
|
|
5702
5809
|
tried.push(account.label);
|
|
@@ -5720,8 +5827,8 @@ function createWorkBuddyShim(options) {
|
|
|
5720
5827
|
logger?.warn(`dsh-workbuddy-xdpool: ${account.label} has no credits left (attempt ${attempt + 1}/${maxAttempts}); rotating`);
|
|
5721
5828
|
continue;
|
|
5722
5829
|
}
|
|
5830
|
+
if (result.kind === "client") logger?.warn(`dsh-workbuddy-xdpool: ${account.label} rejected the request as invalid (model ${modelId ?? "(none)"}) — ${requestShape(raw)}; upstream: ${result.message.slice(0, 300)}`);
|
|
5723
5831
|
if (result.kind !== "soft_rate") break;
|
|
5724
|
-
exhaustedByRateLimit = true;
|
|
5725
5832
|
pool.penalize(account.id, parseRateLimitReset(result.message), modelId);
|
|
5726
5833
|
logger?.warn(`dsh-workbuddy-xdpool: ${account.label} rate-limited on ${modelId ?? "(no model)"} (attempt ${attempt + 1}/${maxAttempts}); rotating`);
|
|
5727
5834
|
}
|
|
@@ -5844,8 +5951,10 @@ async function recoverFromContextOverrun(options) {
|
|
|
5844
5951
|
};
|
|
5845
5952
|
const overrunTokens = estimateMessagesTokens(messages);
|
|
5846
5953
|
const realWindow = options.contextWindow;
|
|
5847
|
-
const
|
|
5848
|
-
|
|
5954
|
+
const fromWindow = realWindow !== void 0 && realWindow > 0 ? Math.max(512, Math.floor(realWindow * .8) - 2048) : Number.POSITIVE_INFINITY;
|
|
5955
|
+
const fromOverrun = Math.max(512, Math.floor(overrunTokens / 2));
|
|
5956
|
+
const budget = Math.min(fromWindow, fromOverrun);
|
|
5957
|
+
logger?.warn(`dsh-workbuddy-xdpool: context overrun on ${modelId ?? "(no model)"} (~${overrunTokens} tokens, catalog window ${realWindow ?? "unknown"}); compacting to ~${budget} and retrying once`);
|
|
5849
5958
|
let summary;
|
|
5850
5959
|
let compacted = messages;
|
|
5851
5960
|
let compactionDetail = "";
|
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.7.
|
|
5
|
+
"version": "1.7.8",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "XDTrees",
|
|
8
8
|
"homepage": "https://github.com/XDTrees/dsh-workbuddy-xdpool#readme",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"type": "module",
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": "^22.19.0 || >=24.0.0",
|
|
28
|
-
"dsh": ">=0.1.5-rc.1 <0.
|
|
28
|
+
"dsh": ">=0.1.5-rc.1 <0.3.0"
|
|
29
29
|
},
|
|
30
30
|
"main": "lib/index.js",
|
|
31
31
|
"types": "lib/index.d.ts",
|
|
@@ -99,9 +99,9 @@
|
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
101
|
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
102
|
-
"@deepseek-ai/dsh-llm": ">=0.1.1-rc.1 <0.
|
|
103
|
-
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.1-rc.1 <0.
|
|
104
|
-
"@deepseek-ai/dsh-settings": ">=0.1.1-rc.1 <0.
|
|
102
|
+
"@deepseek-ai/dsh-llm": ">=0.1.1-rc.1 <0.3.0",
|
|
103
|
+
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.1-rc.1 <0.3.0",
|
|
104
|
+
"@deepseek-ai/dsh-settings": ">=0.1.1-rc.1 <0.3.0",
|
|
105
105
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
106
106
|
"@earendil-works/pi-ai": ">=0.82.1"
|
|
107
107
|
},
|