dsh-connect-workbuddy 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.1 (2026-09-29)
4
+
5
+ ### Features
6
+
7
+ #### 模型「测试」改为真实体积:揭穿「测试通过、实际被限」
8
+
9
+ **上游的 6004 限流按请求体积触发**,而此前的探针只发 `max_tokens: 1` + `ping`(约 18 token)——它永远够不到那条线。于是出现一个自相矛盾的现象:**卡片测出来模型可用,真实会话却每次都 429**。这不是探针算错,而是它**问的问题太轻**。
10
+
11
+ 真机量化(2026-09-29,同一账号、同一模型,只改输入体积):
12
+
13
+ | 输入 | 上游计到的 prompt_tokens | 结果 |
14
+ | --- | --- | --- |
15
+ | ~10 | 38 | ok |
16
+ | ~1,000 | 1,138 | ok |
17
+ | ~10,000 | 11,138 | ok |
18
+ | ~18,000 | 20,018 | ok |
19
+ | **~25,000** | **25,023** | ok |
20
+ | ~30,000(干净账号只发这一次) | ~32k | **429 / 6004** |
21
+
22
+ - **阈值在 20k 与 30k 之间,单次大请求即触发**(不是累计),且**每个账号各有自己的重置时间**(实测同一时刻两个账号分别报 `22:20:33` 与 `00:32:51`)。
23
+ - **为什么这是用户会踩到的**:插件向 DSH 宣称的上下文窗口最高 200K(`web-status.ts` 把 1M 的模型按默认预算压到 200K),DSH 因此要到 20 万 token 才压缩——**远超上游 ~20–30k 的节流线**。长会话里请求一路涨过阈值,之后每一次都失败,而小探针始终显示可用。
24
+ - **改动**:**只保留一种测试**,即每个模型行上的「测试」按钮,它现在发一条 **~25k 输入 token** 的真实体积请求。真机验证:请求体 112,713 B,上游计到 `prompt_tokens: 25023`。
25
+ - **删掉了两样东西**:① 原先的最小探针(`probeRequestBody` 的 `ping` 版本、`heavy` 开关、`PROBE_USER_PROMPT`);② 模型区顶部的**「测试已勾选」批量按钮**。后者不是排版调整而是成本决定——每次探测都真花钱(实测 `credit` 约 0.02~0.72),一次覆盖整个已勾选列表就是一次不可控支出。路由相应改为**只接受 1 个模型**(多于 1 个直接 400):卡片只发这一个形状,多模型请求不是卡片发的,拒绝它正是防止陈旧 bundle 或手写客户端把按钮变成消费循环的那道闸。
26
+ - **实测代价**:一次「测试」报 `credit` 0.02(健康账号、25k 输入);被限流时立即 429 并给出重置时间。此前的最小探针是 0~0.01。
27
+
28
+ ### Bug Fixes
29
+
30
+ #### 「上游未给出何时恢复」是错的:恢复时间一直在响应体里
31
+
32
+ 2.3.0 的模型测试上线后,真实使用暴露了一个要命的偏差:**探针说模型可用、用户点「测试」也看到通过,但真正发请求却被限流**;而被限流的那一行显示的是「上游未给出何时恢复」——**上游其实说了,就在响应体里**。
33
+
34
+ - **根因**:`cooldownOf()` 只读两个来源——`Retry-After` 响应头、以及积分耗尽时的月度刷新点。而本服务的 429 **根本没有 `Retry-After` 头**,重置时间写在响应体的 `msg` 文本里。实测抓到的原文(2026-09-29,逐字):
35
+
36
+ ```json
37
+ {"code":6004,"msg":"您的使用量已超出频率限制,将在 2026-09-30 02:30:30 UTC+8 重置,您也可以切换其他模型继续使用。"}
38
+ ```
39
+
40
+ 2.3.0 的注释写「上游不提供限流元数据」,这个结论**是从响应头单独得出的**——就响应头而言是对的,作为结论是错的。
41
+ - **为什么「测试通过、实际被限」不是探针的错**:探针与真实请求的判定本来就一致。用真实凭据实测,同一模型用**四种请求形态**(`max_tokens:1` + `ping`、`max_tokens:2000` + 长 prompt、以及两者交叉)全部返回**同一个 429**;13 个模型里只有 `deepseek-v4.1-flash` 被限,其余 12 个(含默认的 `glm-5.3-flash` 一类)全部 `ok`。所以「测试通过」看到的是**别的模型**,而被限的那个恰好是配置里用着的。限流由账户/模型维度的配额决定,与请求大小无关——这也回答了「加大请求能不能测出来」:不能。
42
+ - **修法**:新增 `parseUpstreamResetAt()`,从失败响应体解析 `将在 <时间> UTC<偏移> 重置`;`cooldownOf()` 增加第三个来源 `upstream-message`(优先级:响应头 > 响应体 > 月度刷新点)。`probeModel()` 把 body 传给它。
43
+ - **时区必须按偏移量算,不能被本机时区左右**:`2026-09-30 02:30:30 UTC+8` 是 UTC 的 `2026-09-29T18:30:30Z`。直接 `Date.parse` 会**按本地时区**解读——在 UTC+8 的机器上恰好正确,换台机器就静默错一个偏移量。因此用 `Date.UTC` 组装后减去声明的偏移,实测 UTC / Asia/Shanghai / America/New_York 三种 `TZ` 下输出一致。
44
+ - **宁可不说,也不说错**:无法解析时返回 `undefined`,卡片继续如实显示「上游未给出何时恢复」。`Date.UTC` 会把 13 月、32 日**静默归一化**成另一个真实时刻,所以解析后逐字段回验,不合法就丢弃;`UTC+99` 这类越界偏移同样拒绝。这些都配了用例。
45
+ - **守卫与变异验证**:`tests/probe.spec.ts`(28 → 38 例)覆盖:真机原文端到端(经 `probeModel`)、响应头优先于响应体、`T` 分隔符与 `UTC+0800` 紧凑写法、负偏移、四种「不该解析出时间」的形态、三种非法日期、越界偏移。**变异验证**:把偏移量那一步去掉(当成 UTC 算)→ **6 例变红**。
46
+ - 文案无需改动:`row.probeRateLimitedAt`(`✗ 被限流 · {at} 之后可再用`)本就存在,此前只是永远走不到。
47
+
48
+ ### Tests
49
+
50
+ - `tests/probe.spec.ts`(28 → 42 例):探测请求体**恒为真实体积**且随目标常量缩放;`parseUpstreamResetAt` 的真机原文、`T` 分隔符、`UTC+0800` 紧凑写法、负偏移、越界偏移、三种非法日期、四种「不该解析出时间」的形态;`cooldownOf` 三级来源的优先级(响应头 > 响应体 > 月度刷新点)。
51
+ - `tests/web-status.spec.ts`(49 → 50 例):路由**只接受单个模型**,多模型请求 400。
52
+ - **变异验证**:把偏移量换算去掉(当成 UTC 算)→ **6 例变红**;去掉单模型强制 → 对应用例变红。
53
+ - 全量:**449 例全绿**(2.3.0 为 434 例),typecheck 与 build 通过。
54
+
3
55
  ## 2.3.0 (2026-09-29)
4
56
 
5
57
  ### Features
@@ -25,6 +77,8 @@
25
77
  | 积分耗尽,无月周期包 | `✗ 积分不足 · 上游未给出何时恢复` | 无 |
26
78
  | 401/403 凭据被拒 | `✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号` | — |
27
79
 
80
+ > ⚠️ **本节的第 2 条来源此后有更正(见 Unreleased)**:本节写「上游不提供这个数据」,那是**只看响应头**得出的结论——响应头里确实没有,但上游把重置时间写在**响应体**里(`将在 … UTC+8 重置`)。「上游未给时间」那一行因此在真实 429 上从不该出现。解析与实测见 Unreleased 的 Bug Fixes 一节。
81
+
28
82
  限流**不会**去借月周期包的刷新时间:那会让用户为几秒的限流去等几周。这个区分由 `cooldownOf` 强制,并有专门用例钉住。
29
83
 
30
84
  **为何每个探测请求都必须以 system 消息开头**:国际网关(`www.workbuddy.ai`)对 `[{role:'user'}]` 一律返回 HTTP 400 / 业务码 `11128`(`first message is not system prompt`)。按国内版那样只发一条 user 消息,会把**整个国际版误报成全部不可用**。实测加上 system 消息后立即 200,故探测请求始终带一条 system 消息。
@@ -38,7 +92,7 @@
38
92
 
39
93
  ### Tests
40
94
 
41
- - `tests/probe.spec.ts`(新增,28 例):请求体必带 system 首条且 `max_tokens: 1` / `stream:true`;`Retry-After` 的秒数与 HTTP-date 两形态、以及 0 / 负数 / 过期时间不被当作冷却;`cooldownOf` 的四种组合(含**限流绝不编造时间**、**限流不借月周期刷新时间**);七类失败分类;成功流不被截断影响;**抛出异常不逃逸**;失败正文脱敏。
95
+ - `tests/probe.spec.ts`(新增,28 例;此后在 Unreleased 增至 38 例):请求体必带 system 首条且 `max_tokens: 1` / `stream:true`;`Retry-After` 的秒数与 HTTP-date 两形态、以及 0 / 负数 / 过期时间不被当作冷却;`cooldownOf` 的四种组合(含**限流绝不编造时间**、**限流不借月周期刷新时间**);七类失败分类;成功流不被截断影响;**抛出异常不逃逸**;失败正文脱敏。
42
96
  - `tests/web-status.spec.ts`(+10 例):逐模型串行调用(并发变异会红)、region 透传、空列表 / 非数组 / 超限批量一律 400、无能力时 503、非回环来源 403、非 POST 405、未知 region 400、抛错转 500 且不泄露 token。
43
97
  - **变异验证**(5 处,逐一实测变红):给限流编造固定倒计时 → 3 例红;删掉 system 首条消息 → 2 例红;让限流借用月周期刷新时间 → 1 例红;把串行批改为并发扇出 → 1 例红;其余对照组全绿。
44
98
 
package/README.en.md CHANGED
@@ -25,7 +25,9 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
25
25
  - **Tabbed settings card** — the card's top carries a "Domestic / Global" tab bar; each tab holds its own account picker, credits overview, and model management. Account, directory, selection, and unsaved drafts are fully isolated per tab — switching accounts or refreshing models on one tab never touches the other side's runtime catalog or sessions.
26
26
  - **Switch either provider off on its own** — each tab carries an on/off checkbox (both checked by default). Unchecking one **withdraws that provider from DSH's model picker entirely** (it is not merely a hidden tab): its adapter route and its "Settings → Models" directory entry are both pulled, and startup no longer sends pointless requests for it. Account, credits, directory, and selection are all kept, and re-checking restores everything. The typical use is "I have no use for the international side" — switch it off and keep the picker clean. Note: a session that had already selected a model from the switched-off provider will report `NO_ADAPTER` on its next call; the card states this plainly on the switched-off tab instead of failing silently.
27
27
  - **Model management** — refresh the full catalog from upstream and enable or disable each model individually; refresh is a draft operation that only takes effect on save. Upstream also reports credit multiplier, context/output limits, and reasoning efforts; **image input is decided by a vendor-verified table**: a refresh pre-checks only models the vendor's own documentation calls natively multimodal, leaves documented text-only models unchecked, and treats anything without a vendor source as unverified and unchecked rather than guessing. Upstream's `supportsImages` is a *platform* declaration, not the model's capability, so it does not drive that decision at all. You can still tick any box by hand, and your selection is kept locally.
28
- - **Model availability testing, including cooldown recovery times** — working out when a limited model frees up used to mean guessing through repeated failures. Now every model row has a **Test** button and the models header has **Test selected**: each chosen model gets one minimal request (`max_tokens: 1`), and you get a direct answer for whether it works right now and, if not, when it comes back. Measured cost is negligible: the response reports `credit` between 0 and 0.01, and one probe left the balance unchanged (933 → 933), against 0.79x and up for a real reply; each probe took 0.8–3.4s. **On "when can I use this again", the upstream does not provide that data**: measured responses carry no `Retry-After` and no `X-RateLimit-*`, and 12 rapid requests to the same model all succeeded. So the plugin **never invents a countdown** — it shows a time only for the two cases where the upstream actually states one (a `Retry-After` header, or the monthly quota's refresh point when the quota is exhausted) and says plainly that no time was given otherwise. A rate limit deliberately does **not** borrow the monthly refresh time, which would tell you to wait weeks for a limit that clears in seconds. Probes run in the Host, so **the credential never reaches the page**, and results are not written to settings — your saved selection is untouched.
28
+ - **Model availability testing, including cooldown recovery times** — working out when a limited model frees up used to mean guessing through repeated failures. Now every model row has a **Test** button that gives you a direct answer for whether the model works right now and, if not, when it comes back. **It sends a real-volume request (about 25k input tokens)**, rather than "ping it and see if it answers" — because the upstream throttles on request **SIZE** (measured: about 20k tokens pass, about 30k are refused), so a small request looks fine while every request in a long conversation may be refused. That is exactly where "the test passes but I am limited" came from. **Where the recovery time comes from**: the upstream's 429 carries **no** `Retry-After` header (and no `X-RateLimit-*`), but it writes the reset time into the response **body** — `{"code":6004,"msg":"…将在 2026-09-30 02:30:30 UTC+8 重置…"}`. The plugin parses that time and converts it using the UTC offset the message states, so the result does not depend on your machine's timezone; the card then shows “✗ Rate limited · usable again after 2026/9/30 02:30:30”. When none of the sources states a time, the plugin **never invents a countdown** and says plainly that no time was given. A rate limit deliberately does **not** borrow the monthly refresh time, which would tell you to wait weeks for a limit that clears in seconds. Probes run in the Host, so **the credential never reaches the page**, and results are not written to settings — your saved selection is untouched.
29
+
30
+ **Two things to know.** ① **It really does spend credits** — one measured probe reported `credit` 0.02 (a refused one 429s and costs almost nothing). So testing is **manual and per-model only**: there is deliberately no “Test selected” batch button, because sweeping the whole list from one click is an uncontrolled spend — and the Host enforces the same rule, **accepting a single model only** and refusing multi-model requests. ② The upstream advertises a 1M context yet throttles far below it, and the plugin advertises up to 200K to DSH, so DSH compacts very late — hitting the throttle in a long session follows directly from that.
29
31
  - **Local account switching** — discovers the multiple sign-in credentials WorkBuddy's desktop app leaves behind and lets you switch per region. Tokens are never written to DSH settings.
30
32
  - **Actionable advice when a credential is refused** — when the upstream rejects the selected account's token, the plugin actually **probes** the other local sign-ins: if one still answers, it tells you to switch to it (with a one-click switch) instead of vaguely asking you to sign in again — re-authenticating fixes nothing while you are pinned to a revoked backup. Only when there is genuinely no other local account does it ask you to sign in again.
31
33
  - **Read-only credits overview** — remaining credit aggregated per package, plus each model's credit multiplier. Queries consume no credits.
package/README.md CHANGED
@@ -31,7 +31,9 @@
31
31
  - **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号选择、积分概览与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。
32
32
  - **可单独关闭任一版本供应商** —— 每个 tab 右侧有一个勾选框(默认都勾选)。**取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉**(不是只藏起 tab):它的路由与「设置 → 模型」页的目录条目一并摘除,启动时也不再为它发无用请求。账号、积分、目录与勾选**全部保留**,重新勾选即完整恢复。典型用法是「我根本用不到国际版」——关掉它,让选择器干净。注意:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 `NO_ADAPTER`,卡片会在关闭态的 tab 上明确提示这一点,不会静默失败。
33
33
  - **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。
34
- - **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,模型区顶部有「测试已勾选」,对勾选的模型**各发一条最小请求**(`max_tokens: 1`),直接告诉你它此刻能否使用、不能用时何时恢复。实测代价可忽略:`credit` 报 0~0.01,单次探测前后余额不变(933 → 933),而正常一次回答是 0.79x 起;单次耗时 0.8~3.4s。**关于「多久之后可以再次使用」,上游并不提供这个数据**:实测响应头里没有 `Retry-After`、也没有 `X-RateLimit-*`,对同一模型连续快速打 12 次全部成功。因此插件**不编造倒计时**,只在两种上游真的给出时间的情形下显示时间——`Retry-After` 头,或积分耗尽时该区域月周期包的刷新点;两者都没有时如实显示「上游未给出何时恢复」。限流**不会**去借月周期包的刷新时间(那会让人为几秒的限流去等几周)。探测在后台发起,**凭据不会传到页面**;结果不写入设置,也不改变你已保存的勾选。
34
+ - **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,直接告诉你它此刻能否使用、不能用时何时恢复。**它发的是一条真实体积的请求(约 25k 输入 token)**,而不是「发个 ping 看通不通」——因为上游的限流**按请求体积触发**(实测约 20k 能过、约 30k 被拒),小请求看着正常、长对话里却可能每次都被拒,那正是「测试通过、实际被限」的来源。**恢复时间的来源**:上游的 429 **不带** `Retry-After` 响应头(也没有 `X-RateLimit-*`),但它把重置时间写在**响应体**里——`{"code":6004,"msg":"…将在 2026-09-30 02:30:30 UTC+8 重置…"}`。插件从响应体解析该时间(并按其声明的 UTC 偏移换算,不受你本机时区影响),因此卡片能直接显示「✗ 被限流 · 2026/9/30 02:30:30 之后可再用」。三处来源都没说时,插件**不编造倒计时**,如实显示「上游未给出何时恢复」。限流**不会**去借月周期包的刷新时间(那会让人为几秒的限流去等几周)。探测在后台发起,**凭据不会传到页面**;结果不写入设置,也不改变你已保存的勾选。
35
+
36
+ **两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件向 DSH 宣称的窗口最高 200K,所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。
35
37
  - **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
36
38
  - **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
37
39
  - **只读积分概览** —— 按套餐聚合展示剩余积分,并列出每个模型的积分倍率。查询不消耗积分。
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { O as workbuddyOwnAuthPath, T as legacyWorkbuddyOwnAuthPath, W as findWorkbuddyAppExecutable, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, g as WORKBUDDY_AUTH_FILE_ENV, j as WorkBuddyUpstreamClient, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, x as defaultDesktopAuthDirs, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-Uh8JCXqk.js";
2
+ import { O as workbuddyOwnAuthPath, T as legacyWorkbuddyOwnAuthPath, W as findWorkbuddyAppExecutable, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, g as WORKBUDDY_AUTH_FILE_ENV, j as WorkBuddyUpstreamClient, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, x as defaultDesktopAuthDirs, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-CDZRWbFv.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
package/lib/client.js CHANGED
@@ -891,14 +891,14 @@ window.__ModuleLoader__.load({
891
891
  * The sentence a probe result gets, and how it should be coloured.
892
892
  *
893
893
  * The cooldown wording is the part that had to be got right. The upstream names
894
- * a "use again" time in exactly two situations — a `Retry-After` header, or an
895
- * exhausted monthly quota whose refresh point it declares — and its 200s and
896
- * 429s carry NO rate-limit metadata at all (measured: no `Retry-After`, no
897
- * `X-RateLimit-*`, and 12 rapid requests to one model all succeeded). So a
898
- * limited result WITHOUT a stated time must say that plainly. Substituting a
899
- * locally invented countdown would be the single most misleading thing this
900
- * feature could do: it would look like an upstream answer while being a guess,
901
- * and the user would wait for a moment that means nothing.
894
+ * a "use again" time in three situations: a `Retry-After` header, the reset
895
+ * sentence it writes into its own failure BODY (where this service actually
896
+ * puts it — its 429s carry no rate-limit header at all), or an exhausted
897
+ * monthly quota whose refresh point it declares. So a limited result WITHOUT
898
+ * any stated time must say that plainly. Substituting a locally invented
899
+ * countdown would be the single most misleading thing this feature could do: it
900
+ * would look like an upstream answer while being a guess, and the user would
901
+ * wait for a moment that means nothing.
902
902
  */
903
903
  function probeResultView(result, t) {
904
904
  switch (result.outcome) {
@@ -1233,8 +1233,6 @@ window.__ModuleLoader__.load({
1233
1233
  * A bound scope is therefore all that is required to accept an edit.
1234
1234
  */
1235
1235
  const canWrite = settingsScope !== void 0;
1236
- /** Whether any row's probe is in flight, so the batch button reflects it. */
1237
- const anyProbing = Object.keys(probing).length > 0;
1238
1236
  const editDraft = (edit) => {
1239
1237
  setDrafts((prev) => ({
1240
1238
  ...prev,
@@ -1654,18 +1652,9 @@ window.__ModuleLoader__.load({
1654
1652
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1655
1653
  className: "dsm-workbuddy-models-summary",
1656
1654
  children: t("row.modelsSummary", { count: activeEnabledIds.size })
1657
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1655
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1658
1656
  className: "dsm-workbuddy-models-head-actions",
1659
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1660
- type: "button",
1661
- className: "dsm-btn dsm-btn-outline",
1662
- disabled: activeEnabledIds.size === 0 || anyProbing,
1663
- title: t("row.probeAllHint"),
1664
- onClick: () => {
1665
- probeModels([...activeEnabledIds]);
1666
- },
1667
- children: anyProbing ? t("row.probing") : t("row.probeAll")
1668
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1657
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1669
1658
  type: "button",
1670
1659
  className: "dsm-btn dsm-btn-outline",
1671
1660
  disabled: busy,
@@ -1673,7 +1662,7 @@ window.__ModuleLoader__.load({
1673
1662
  refreshModels();
1674
1663
  },
1675
1664
  children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
1676
- })]
1665
+ })
1677
1666
  })]
1678
1667
  }),
1679
1668
  probeError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1746,15 +1735,18 @@ window.__ModuleLoader__.load({
1746
1735
  }
1747
1736
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(model.nativeContextWindow, t("row.modelUnknown")) })] })]
1748
1737
  }),
1749
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1750
- type: "button",
1751
- className: "dsm-btn dsm-btn-outline dsm-workbuddy-model-probe",
1752
- disabled: probing[model.id] === true || status.status !== "signed-in",
1753
- title: t("row.probeHint"),
1754
- onClick: () => {
1755
- probeModels([model.id]);
1756
- },
1757
- children: probing[model.id] === true ? t("row.probing") : t("row.probe")
1738
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1739
+ className: "dsm-workbuddy-model-actions-buttons",
1740
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1741
+ type: "button",
1742
+ className: "dsm-btn dsm-btn-outline dsm-workbuddy-model-probe",
1743
+ disabled: probing[model.id] === true || status.status !== "signed-in",
1744
+ title: t("row.probeHint"),
1745
+ onClick: () => {
1746
+ probeModels([model.id]);
1747
+ },
1748
+ children: probing[model.id] === true ? t("row.probing") : t("row.probe")
1749
+ })
1758
1750
  })
1759
1751
  ]
1760
1752
  }),
@@ -1933,9 +1925,7 @@ window.__ModuleLoader__.load({
1933
1925
  "row.modelCapabilityPending": "Only capabilities advertised by WorkBuddy are shown.",
1934
1926
  "row.probe": "Test",
1935
1927
  "row.probing": "Testing…",
1936
- "row.probeAll": "Test selected",
1937
- "row.probeAllHint": "Send one minimal request (1 output token) to each enabled model and report which ones answer right now. Measured cost: 0 to 0.01 credits per model, against 0.79+ for a real reply.",
1938
- "row.probeHint": "Send one minimal request to this model to check it answers right now.",
1928
+ "row.probeHint": "Send a real-volume request (~25k input tokens) to this model and report whether it answers right now. The upstream rate limit fires on request SIZE — measured: about 20k tokens pass, about 30k are refused — so a small request can look fine while every long-conversation request is refused. This is why every test costs real credits (about 0.7) and runs one model at a time.",
1939
1929
  "row.probeOk": "✓ Usable",
1940
1930
  "row.probeOkMs": "✓ Usable ({ms})",
1941
1931
  "row.probeRateLimitedUnknown": "✗ Rate limited · the upstream gave no time for when it frees up",
@@ -2027,9 +2017,7 @@ window.__ModuleLoader__.load({
2027
2017
  "row.modelCapabilityPending": "仅展示 WorkBuddy 接口明确公布的模型能力。",
2028
2018
  "row.probe": "测试",
2029
2019
  "row.probing": "测试中…",
2030
- "row.probeAll": "测试已勾选",
2031
- "row.probeAllHint": "对每个已勾选的模型各发一条最小请求(只出 1 个 token),报告哪个现在真的能用。实测每个模型消耗 0~0.01 积分,而正常一次回答是 0.79 起。",
2032
- "row.probeHint": "对该模型发一条最小请求,检查它现在能不能用。",
2020
+ "row.probeHint": "对该模型发一条**真实体积**的请求(约 25k 输入 token),报告它现在能不能用。上游的限流按**请求体积**触发——实测约 20k 能过、约 30k 被拒——所以小请求看着正常、长对话里却可能每次都被拒。这也是为什么每次测试都会消耗真实积分(约 0.7),且一次只测一个模型。",
2033
2021
  "row.probeOk": "✓ 可用",
2034
2022
  "row.probeOkMs": "✓ 可用({ms})",
2035
2023
  "row.probeRateLimitedUnknown": "✗ 被限流 · 上游未给出何时恢复",
@@ -2258,7 +2258,7 @@ var WorkBuddyCatalog = class {
2258
2258
  //#endregion
2259
2259
  //#region src/version.ts
2260
2260
  /** The npm package version this build was produced from. */
2261
- const WORKBUDDY_CONNECT_VERSION = "2.3.0";
2261
+ const WORKBUDDY_CONNECT_VERSION = "2.3.1";
2262
2262
  //#endregion
2263
2263
  //#region src/host-heartbeat.ts
2264
2264
  /**
package/lib/index.d.ts CHANGED
@@ -1195,23 +1195,48 @@ declare const WORKBUDDY_CONNECT_VERSION: string;
1195
1195
  * would report every global model as broken.
1196
1196
  */
1197
1197
  declare const PROBE_SYSTEM_PROMPT = "You are a connectivity check. Reply with a single word.";
1198
- /** The one-word user turn; the smallest thing a chat endpoint accepts. */
1199
- declare const PROBE_USER_PROMPT = "ping";
1200
1198
  /**
1201
- * Output cap for a probe. One token is enough to prove the model answers, and
1202
- * keeps the cost negligible: live measurements of `max_tokens: 1` probes
1203
- * reported `credit` between 0 and 0.01, and one such probe left the credit
1204
- * balance unchanged (933 → 933). For scale, a model on that roster bills 0.79+
1205
- * per real reply. The point of the feature is to learn whether a model works
1206
- * while spending as close to nothing as the service allows.
1199
+ * Output cap for a probe. One token is enough to prove the model answers.
1200
+ *
1201
+ * The probe's cost comes from its INPUT, not its output (see
1202
+ * {@link PROBE_INPUT_TOKENS}), so paying for a long completion would add money
1203
+ * without adding signal.
1207
1204
  */
1208
1205
  declare const PROBE_MAX_TOKENS = 1;
1206
+ /**
1207
+ * How much input a probe sends, in tokens.
1208
+ *
1209
+ * This is the whole point of the probe, and it is NOT a free choice. The
1210
+ * upstream's rate limit (business code 6004) fires on request SIZE, measured on
1211
+ * 2026-09-29 by varying only the input against one account and one model:
1212
+ *
1213
+ * | input | prompt_tokens the upstream counted | result |
1214
+ * | --- | --- | --- |
1215
+ * | ~10 | 38 | ok |
1216
+ * | ~1,000 | 1,138 | ok |
1217
+ * | ~10,000 | 11,138 | ok |
1218
+ * | ~18,000 | 20,018 | ok |
1219
+ * | ~25,000 | 25,023 | ok |
1220
+ * | ~30,000 | ~32k | **429 / 6004** |
1221
+ *
1222
+ * So the threshold sits between 20k and 30k, a SINGLE large request is enough to
1223
+ * trip it (it is not a running total), and each account reports its own reset
1224
+ * time (two accounts at the same moment: `22:20:33` and `00:32:51`).
1225
+ *
1226
+ * A probe that sent a handful of tokens answered "usable" while every real
1227
+ * request in a long conversation was refused — the probe was asking too small a
1228
+ * question. 25k is sized to be a realistic long-conversation payload.
1229
+ */
1230
+ declare const PROBE_INPUT_TOKENS = 25000;
1209
1231
  /**
1210
1232
  * Build the request body for one probe.
1211
1233
  *
1212
1234
  * `stream` is forced because the upstream rejects non-streaming chat requests;
1213
1235
  * `prepareChatBody` enforces that too, but sending it explicitly keeps this
1214
1236
  * function's output valid on its own.
1237
+ *
1238
+ * Measured on the built artifact: 112,713 bytes of body, and the upstream
1239
+ * counted `prompt_tokens: 25023` at a cost of `credit: 0.72`.
1215
1240
  */
1216
1241
  declare function probeRequestBody(modelId: string): string;
1217
1242
  /**
@@ -1242,7 +1267,7 @@ interface WorkBuddyProbeResult {
1242
1267
  /** A time the upstream named, in ms. Absent when it named none. */
1243
1268
  retryAtMs?: number;
1244
1269
  /** Where a supplied `retryAtMs` came from, so the card can say so. */
1245
- retrySource?: 'retry-after' | 'quota-refresh';
1270
+ retrySource?: 'retry-after' | 'upstream-message' | 'quota-refresh';
1246
1271
  }
1247
1272
  /**
1248
1273
  * Parse a `Retry-After` header value into an absolute time.
@@ -1258,24 +1283,29 @@ declare function parseRetryAfter(value: string | null, nowMs: number): number |
1258
1283
  /**
1259
1284
  * The cooldown the upstream stated, if it stated one.
1260
1285
  *
1261
- * Two sources, in order of authority:
1286
+ * Three sources, in order of authority:
1262
1287
  *
1263
1288
  * 1. `Retry-After` on the failure response. This is the upstream naming a time
1264
1289
  * for THIS request, so it wins.
1265
- * 2. The region's monthly quota refresh point, but ONLY for an out-of-credit
1290
+ * 2. The reset time the upstream writes into its own failure body (see
1291
+ * {@link parseUpstreamResetAt}). This is the case that actually fires on
1292
+ * this service: a 429 from the Chinese gateway carries code 6004 and a
1293
+ * `将在 … 重置` sentence, with no `Retry-After` header at all.
1294
+ * 3. The region's monthly quota refresh point, but ONLY for an out-of-credit
1266
1295
  * outcome. A quota that resets at a known time is the one case where "when
1267
1296
  * can I use this again" has a real answer even without a header — and it is
1268
1297
  * the answer for the most common real limit on this service.
1269
1298
  *
1270
- * A `rate-limited` outcome with neither source returns `{}`, which the card
1271
- * renders as "the upstream did not say when". That is the honest answer: the
1272
- * live probes found no rate-limit metadata in any response at all, so inventing
1273
- * a number here would be pure fiction.
1299
+ * A limited outcome with none of the three returns `{}`, which the card renders
1300
+ * as "the upstream did not say when". That remains the honest answer for a
1301
+ * genuinely timeless failure; inventing a number here would be pure fiction.
1274
1302
  */
1275
1303
  declare function cooldownOf(input: {
1276
1304
  outcome: WorkBuddyProbeOutcome;
1277
1305
  retryAfter: string | null;
1278
1306
  nowMs: number;
1307
+ /** The upstream's own failure text, where a reset sentence may live. */
1308
+ body?: string;
1279
1309
  /** The region's next monthly quota refresh, when the upstream declares one. */
1280
1310
  quotaRefreshAtMs?: number;
1281
1311
  }): Pick<WorkBuddyProbeResult, 'retryAtMs' | 'retrySource'>;
@@ -1314,9 +1344,9 @@ interface WorkBuddyProbeClient {
1314
1344
  * Run one probe for one model and reduce it to the card's row.
1315
1345
  *
1316
1346
  * The successful path drains the SSE stream purely to read the `credit` figure
1317
- * the upstream reports, then discards the rest: the probe's question is "did
1318
- * this model answer", and `max_tokens: 1` means there is nothing else in the
1319
- * stream worth keeping.
1347
+ * the upstream reports, then discards the rest: the question is whether this
1348
+ * model answers a real-sized request, and `max_tokens: 1` means there is nothing
1349
+ * else in the stream worth keeping.
1320
1350
  *
1321
1351
  * Never throws. A probe is a diagnostic the user asked for, so every failure is
1322
1352
  * a RESULT — one dead model must not abort the batch and hide the other rows.
@@ -1653,15 +1683,15 @@ interface WorkBuddyWebProbeResult {
1653
1683
  * A time the upstream named for using this model again, in ms.
1654
1684
  *
1655
1685
  * ABSENT means the upstream named none, and the card must say exactly that.
1656
- * The live probes that shaped this feature found no rate-limit metadata in
1657
- * any response — no `Retry-After`, no `X-RateLimit-*` — so a client-invented
1658
- * countdown here would be fiction. Only two sources ever fill this:
1659
- * a real `Retry-After`, or (for an exhausted quota) the region's own quota
1660
- * refresh point, which the upstream does declare.
1686
+ * A client-invented countdown here would be fiction. Three sources fill it:
1687
+ * a real `Retry-After` header; the reset sentence the upstream writes into
1688
+ * its own failure BODY (`将在 … UTC+8 重置`, which is where this service
1689
+ * actually puts it — its 429s carry no rate-limit header at all); or, for an
1690
+ * exhausted quota, the region's own quota refresh point.
1661
1691
  */
1662
1692
  retryAtMs?: number;
1663
- /** Which of those two sources supplied {@link retryAtMs}. */
1664
- retrySource?: 'retry-after' | 'quota-refresh';
1693
+ /** Which of those three sources supplied {@link retryAtMs}. */
1694
+ retrySource?: 'retry-after' | 'upstream-message' | 'quota-refresh';
1665
1695
  }
1666
1696
  /**
1667
1697
  * Deep copy of a settings value with every `{get(): T}` live reference replaced
@@ -1731,14 +1761,19 @@ interface WorkBuddyStatusRouteOptions {
1731
1761
  */
1732
1762
  regionEnabled(region: WorkBuddyRegion): boolean;
1733
1763
  /**
1734
- * Send one minimal request to each named model of a region and report what
1764
+ * Send one real-volume request to each named model of a region and report what
1735
1765
  * came back.
1736
1766
  *
1737
1767
  * Optional: it is the only dependency the card's test button needs, so a Host
1738
1768
  * built without it (or a route mounted for a status-only consumer) simply
1739
1769
  * answers 503 and the button reports that instead of failing the card.
1770
+ *
1771
+ * Every request costs real credits (measured `credit: 0.72` for one probe), so
1772
+ * the card drives this ONE model per click and never as a batch.
1740
1773
  */
1741
- probeModels?(region: WorkBuddyRegion, modelIds: readonly string[], signal?: AbortSignal): Promise<readonly WorkBuddyWebProbeResult[]>;
1774
+ probeModels?(region: WorkBuddyRegion, modelIds: readonly string[], options?: {
1775
+ signal?: AbortSignal;
1776
+ }): Promise<readonly WorkBuddyWebProbeResult[]>;
1742
1777
  /**
1743
1778
  * The region's next monthly quota refresh, when the upstream declares one.
1744
1779
  *
@@ -1976,4 +2011,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
1976
2011
  */
1977
2012
  declare function apply(ctx: Context, config: Config): void;
1978
2013
  //#endregion
1979
- export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, PROBE_USER_PROMPT, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
2014
+ export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKENS, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-Uh8JCXqk.js";
1
+ import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-CDZRWbFv.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { createProvider } from "@earendil-works/pi-ai";
@@ -501,25 +501,63 @@ function createWorkBuddyShim(options) {
501
501
  * would report every global model as broken.
502
502
  */
503
503
  const PROBE_SYSTEM_PROMPT = "You are a connectivity check. Reply with a single word.";
504
- /** The one-word user turn; the smallest thing a chat endpoint accepts. */
505
- const PROBE_USER_PROMPT = "ping";
506
504
  /**
507
- * Output cap for a probe. One token is enough to prove the model answers, and
508
- * keeps the cost negligible: live measurements of `max_tokens: 1` probes
509
- * reported `credit` between 0 and 0.01, and one such probe left the credit
510
- * balance unchanged (933 → 933). For scale, a model on that roster bills 0.79+
511
- * per real reply. The point of the feature is to learn whether a model works
512
- * while spending as close to nothing as the service allows.
505
+ * Output cap for a probe. One token is enough to prove the model answers.
506
+ *
507
+ * The probe's cost comes from its INPUT, not its output (see
508
+ * {@link PROBE_INPUT_TOKENS}), so paying for a long completion would add money
509
+ * without adding signal.
513
510
  */
514
511
  const PROBE_MAX_TOKENS = 1;
515
512
  /**
513
+ * How much input a probe sends, in tokens.
514
+ *
515
+ * This is the whole point of the probe, and it is NOT a free choice. The
516
+ * upstream's rate limit (business code 6004) fires on request SIZE, measured on
517
+ * 2026-09-29 by varying only the input against one account and one model:
518
+ *
519
+ * | input | prompt_tokens the upstream counted | result |
520
+ * | --- | --- | --- |
521
+ * | ~10 | 38 | ok |
522
+ * | ~1,000 | 1,138 | ok |
523
+ * | ~10,000 | 11,138 | ok |
524
+ * | ~18,000 | 20,018 | ok |
525
+ * | ~25,000 | 25,023 | ok |
526
+ * | ~30,000 | ~32k | **429 / 6004** |
527
+ *
528
+ * So the threshold sits between 20k and 30k, a SINGLE large request is enough to
529
+ * trip it (it is not a running total), and each account reports its own reset
530
+ * time (two accounts at the same moment: `22:20:33` and `00:32:51`).
531
+ *
532
+ * A probe that sent a handful of tokens answered "usable" while every real
533
+ * request in a long conversation was refused — the probe was asking too small a
534
+ * question. 25k is sized to be a realistic long-conversation payload.
535
+ */
536
+ const PROBE_INPUT_TOKENS = 25e3;
537
+ /**
538
+ * Characters per token for the filler text, measured.
539
+ *
540
+ * Filling with a fixed English sentence, the upstream counted about 4.5
541
+ * characters per token. An estimate rather than a bundled tokenizer: the body
542
+ * only needs to be the right SIZE CLASS, and the target has ample margin over
543
+ * the measured threshold, so estimate error cannot change the verdict.
544
+ */
545
+ const CHARS_PER_TOKEN = 4.5;
546
+ /** Neutral filler; carries no user content, only volume. */
547
+ const FILLER_LINE = "The quick brown fox jumps over the lazy dog. ";
548
+ /**
516
549
  * Build the request body for one probe.
517
550
  *
518
551
  * `stream` is forced because the upstream rejects non-streaming chat requests;
519
552
  * `prepareChatBody` enforces that too, but sending it explicitly keeps this
520
553
  * function's output valid on its own.
554
+ *
555
+ * Measured on the built artifact: 112,713 bytes of body, and the upstream
556
+ * counted `prompt_tokens: 25023` at a cost of `credit: 0.72`.
521
557
  */
522
558
  function probeRequestBody(modelId) {
559
+ const targetChars = Math.ceil(PROBE_INPUT_TOKENS * CHARS_PER_TOKEN);
560
+ const filler = FILLER_LINE.repeat(Math.ceil(targetChars / 45));
523
561
  return JSON.stringify({
524
562
  model: modelId,
525
563
  messages: [{
@@ -527,7 +565,7 @@ function probeRequestBody(modelId) {
527
565
  content: PROBE_SYSTEM_PROMPT
528
566
  }, {
529
567
  role: "user",
530
- content: PROBE_USER_PROMPT
568
+ content: `${filler}Reply with a single word.`
531
569
  }],
532
570
  max_tokens: 1,
533
571
  stream: true
@@ -558,21 +596,71 @@ function parseRetryAfter(value, nowMs) {
558
596
  return parsed;
559
597
  }
560
598
  /**
599
+ * The upstream's reset sentence, as its Chinese gateway writes it.
600
+ *
601
+ * Measured on a live 429 (code 6004), verbatim:
602
+ *
603
+ * `您的使用量已超出频率限制,将在 2026-09-30 02:30:30 UTC+8 重置,您也可以切换其他模型继续使用。`
604
+ *
605
+ * The two halves are captured separately because the second one is the whole
606
+ * reason this parser exists: the upstream DOES name a time, and the plugin was
607
+ * showing "the upstream gave no time" while the answer was sitting right there
608
+ * in the body. The earlier note in this module ("the upstream provides no
609
+ * rate-limit metadata") was drawn from the RESPONSE HEADERS alone — true as far
610
+ * as it went, and wrong as a conclusion, because the time is in the body text.
611
+ *
612
+ * Deliberately loose about the surrounding wording (`[\s\S]*?` on both sides)
613
+ * and strict about the parts that must not be guessed: the keyword 重置, the
614
+ * timestamp shape, and an explicit UTC offset. A message that merely mentions
615
+ * 重置 without a parsable offset yields no time, which is the honest answer.
616
+ */
617
+ const UPSTREAM_RESET_PATTERN = /将在?\s*(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})\s*UTC\s*([+-])(\d{1,2})(?::?(\d{2}))?[\s\S]*?重置/u;
618
+ /**
619
+ * Parse the reset time out of an upstream failure message.
620
+ *
621
+ * Returns epoch ms, or undefined when the message names no parsable time — the
622
+ * caller must then say "the upstream gave no time", never invent one.
623
+ *
624
+ * THE OFFSET IS HONOURED, NOT ASSUMED. `2026-09-30 02:30:30 UTC+8` is 18:30:30
625
+ * UTC the previous day, and `Date.parse` on the bare string would read it as
626
+ * LOCAL time — on a machine set to UTC+8 that happens to be right, and on any
627
+ * other machine it is silently wrong by the offset. So the components are
628
+ * assembled with `Date.UTC` and the stated offset subtracted, which is correct
629
+ * on every host regardless of its own zone.
630
+ *
631
+ * A whole-second resolution is what the upstream prints; sub-second precision
632
+ * would be fiction in the other direction.
633
+ */
634
+ function parseUpstreamResetAt(message) {
635
+ const match = UPSTREAM_RESET_PATTERN.exec(message);
636
+ if (match === null) return void 0;
637
+ const [, year, month, day, hour, minute, second, sign, offsetHours, offsetMinutes] = match;
638
+ const offsetTotalMinutes = (sign === "-" ? -1 : 1) * (Number(offsetHours) * 60 + Number(offsetMinutes ?? "0"));
639
+ const utcMs = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
640
+ const parsed = new Date(utcMs);
641
+ if (parsed.getUTCFullYear() !== Number(year) || parsed.getUTCMonth() !== Number(month) - 1 || parsed.getUTCDate() !== Number(day) || parsed.getUTCHours() !== Number(hour) || parsed.getUTCMinutes() !== Number(minute) || parsed.getUTCSeconds() !== Number(second)) return;
642
+ if (Math.abs(offsetTotalMinutes) > 840) return void 0;
643
+ return utcMs - offsetTotalMinutes * 6e4;
644
+ }
645
+ /**
561
646
  * The cooldown the upstream stated, if it stated one.
562
647
  *
563
- * Two sources, in order of authority:
648
+ * Three sources, in order of authority:
564
649
  *
565
650
  * 1. `Retry-After` on the failure response. This is the upstream naming a time
566
651
  * for THIS request, so it wins.
567
- * 2. The region's monthly quota refresh point, but ONLY for an out-of-credit
652
+ * 2. The reset time the upstream writes into its own failure body (see
653
+ * {@link parseUpstreamResetAt}). This is the case that actually fires on
654
+ * this service: a 429 from the Chinese gateway carries code 6004 and a
655
+ * `将在 … 重置` sentence, with no `Retry-After` header at all.
656
+ * 3. The region's monthly quota refresh point, but ONLY for an out-of-credit
568
657
  * outcome. A quota that resets at a known time is the one case where "when
569
658
  * can I use this again" has a real answer even without a header — and it is
570
659
  * the answer for the most common real limit on this service.
571
660
  *
572
- * A `rate-limited` outcome with neither source returns `{}`, which the card
573
- * renders as "the upstream did not say when". That is the honest answer: the
574
- * live probes found no rate-limit metadata in any response at all, so inventing
575
- * a number here would be pure fiction.
661
+ * A limited outcome with none of the three returns `{}`, which the card renders
662
+ * as "the upstream did not say when". That remains the honest answer for a
663
+ * genuinely timeless failure; inventing a number here would be pure fiction.
576
664
  */
577
665
  function cooldownOf(input) {
578
666
  const fromHeader = parseRetryAfter(input.retryAfter, input.nowMs);
@@ -580,6 +668,11 @@ function cooldownOf(input) {
580
668
  retryAtMs: fromHeader,
581
669
  retrySource: "retry-after"
582
670
  };
671
+ const fromBody = input.body === void 0 ? void 0 : parseUpstreamResetAt(input.body);
672
+ if (fromBody !== void 0) return {
673
+ retryAtMs: fromBody,
674
+ retrySource: "upstream-message"
675
+ };
583
676
  if (input.outcome === "out-of-credit" && input.quotaRefreshAtMs !== void 0) return {
584
677
  retryAtMs: input.quotaRefreshAtMs,
585
678
  retrySource: "quota-refresh"
@@ -640,9 +733,9 @@ function redact(text) {
640
733
  * Run one probe for one model and reduce it to the card's row.
641
734
  *
642
735
  * The successful path drains the SSE stream purely to read the `credit` figure
643
- * the upstream reports, then discards the rest: the probe's question is "did
644
- * this model answer", and `max_tokens: 1` means there is nothing else in the
645
- * stream worth keeping.
736
+ * the upstream reports, then discards the rest: the question is whether this
737
+ * model answers a real-sized request, and `max_tokens: 1` means there is nothing
738
+ * else in the stream worth keeping.
646
739
  *
647
740
  * Never throws. A probe is a diagnostic the user asked for, so every failure is
648
741
  * a RESULT — one dead model must not abort the batch and hide the other rows.
@@ -691,6 +784,7 @@ async function probeModel(input) {
691
784
  outcome,
692
785
  retryAfter: answer.retryAfter,
693
786
  nowMs: input.nowMs,
787
+ body,
694
788
  ...input.quotaRefreshAtMs === void 0 ? {} : { quotaRefreshAtMs: input.quotaRefreshAtMs }
695
789
  })
696
790
  };
@@ -897,14 +991,6 @@ function json(res, status, body) {
897
991
  });
898
992
  res.end(payload);
899
993
  }
900
- /**
901
- * Cap on how many models one probe request may name.
902
- *
903
- * A probe is a real (if tiny) request against a metered service, and the batch
904
- * runs sequentially, so an unbounded list would let one click hold the route for
905
- * minutes. The card never sends more than the models on its own screen.
906
- */
907
- const PROBE_BATCH_LIMIT = 64;
908
994
  /** Request-body cap for the probe route: model ids only, never content. */
909
995
  const PROBE_BODY_LIMIT = 65536;
910
996
  /**
@@ -1218,7 +1304,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
1218
1304
  const body = await readJsonBody(req);
1219
1305
  const modelIds = Array.isArray(body.modelIds) ? body.modelIds.filter((id) => typeof id === "string" && id !== "") : [];
1220
1306
  if (modelIds.length === 0) return json(res, 400, { error: "modelIds must be a non-empty array of strings" });
1221
- if (modelIds.length > PROBE_BATCH_LIMIT) return json(res, 400, { error: `at most ${String(PROBE_BATCH_LIMIT)} models per probe` });
1307
+ if (modelIds.length > 1) return json(res, 400, { error: "a probe accepts exactly one model" });
1222
1308
  const results = [];
1223
1309
  for (const modelId of modelIds) results.push(...await deps.probeModels(region, [modelId]));
1224
1310
  json(res, 200, { results });
@@ -1687,7 +1773,7 @@ function apply(ctx, config) {
1687
1773
  * out-of-credit result. Every other limited outcome has no time anywhere, and
1688
1774
  * the probe reports exactly that instead of inventing one.
1689
1775
  */
1690
- async probeModels(region, modelIds, signal) {
1776
+ async probeModels(region, modelIds, options) {
1691
1777
  let credential;
1692
1778
  try {
1693
1779
  credential = await stacks[region].store.resolve();
@@ -1706,7 +1792,7 @@ function apply(ctx, config) {
1706
1792
  modelId,
1707
1793
  nowMs: Date.now(),
1708
1794
  ...quotaRefreshAtMs === void 0 ? {} : { quotaRefreshAtMs },
1709
- ...signal === void 0 ? {} : { signal }
1795
+ ...options?.signal === void 0 ? {} : { signal: options.signal }
1710
1796
  }));
1711
1797
  return results;
1712
1798
  },
@@ -1847,4 +1933,4 @@ function apply(ctx, config) {
1847
1933
  });
1848
1934
  }
1849
1935
  //#endregion
1850
- export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, PROBE_USER_PROMPT, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
1936
+ export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKENS, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-workbuddy",
3
3
  "displayName": "DSH Connect WorkBuddy",
4
4
  "description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
5
- "version": "2.3.0",
5
+ "version": "2.3.1",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",