dsh-connect-workbuddy 5.2.1 → 5.2.3

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,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.2.3 (2026-10-22)
4
+
5
+ ### Fixes
6
+
7
+ - **「一键测速启用模型」按钮永远点不动(5.2.0 引入,截图发现)** —— 那个按钮的 `disabled` 条件里写的是 `tpsProbing !== undefined`,而 `tpsProbing` 是 `useState<Record<string, boolean>>({})`:**一个永远是对象的记录,永远不会 `undefined`**。于是比较恒为真,按钮永久禁用,整个测速功能从界面上根本进不去,而且没有任何提示说明为什么。
8
+ - **为什么会写错**:旁边两个可用性按钮比较的是 `sweeping !== undefined`,在那个变量上是对的 —— `sweeping` 真的是 `{done,total} | undefined`,`undefined` 确实表示「没在跑」。我照抄了那个表达式的形状,却没带上让它成立的事实:不同的状态变量,可取的值集合不同。**对一个 map 来说,「忙」是「有键」,不是「已定义」。**
9
+ - 修法:新增派生的 `tpsBusy = Object.keys(tpsProbing).length > 0`,用它替代恒真的比较。
10
+
11
+ ### Tests
12
+
13
+ - `tests/card-tps-button.spec.tsx`(5 条,新增):**断言的是 DOM 上真实的 `disabled` 布尔值,不是 JSX 文本** —— bug 就住在那条表达式里,文本级断言会和它一起说谎。
14
+ - 有启用模型且没在跑时**不可禁用**(这一条在修复前必红)
15
+ - 点击后真的发出请求,且只测启用的模型
16
+ - **一次只测一个模型**(路由的上限,防止一次点击变成对整份名册的无账花费)
17
+ - **一个都没启用时确实禁用**(防止修复把按钮变成无条件可点)
18
+ - 点击后**行上真的出现数字**(只发请求不渲染的按钮会骗过前四条)
19
+ - 已验证:把修复临时撤回,其中 4 条立刻转红。
20
+ - 全量:**112 文件 / 1812 条全绿**;两个 tsconfig 均 exit 0;build 成功。
21
+
22
+ ## 5.2.2 (2026-10-22)
23
+
24
+ ### Fixes
25
+
26
+ - **排队不再承诺、也不再触发自动重试(按维护者要求)** —— 两半都得改,因为它们是同一件事。
27
+ - **文案**:原来结尾是「约 N 秒后自动重试」。**没有任何东西在重试**:插件没有排队轮询循环(`src/queue.ts` 的 "WHAT THIS MODULE DOES NOT DO YET" 写明了这一点),也不会重发。现在改成「请稍后重新发送这条消息,或换一个模型」——位次照常报,但把决定权交回给用户。
28
+ - **请求本身**:那句谎言还有成本。`@deepseek-ai/dsh-llm` 把 HTTP 429 映射成 `RATE_LIMIT`,而 `RATE_LIMIT` 在它的 `DEFAULT_RETRYABLE_CODES` 里 —— 所以宿主会跑 **5 次** 原地重试(0.5s→8s 退避,实测合计约 15 秒)。一次排队在几千位之后,15 秒不可能排到,这 5 次全是**真实请求**,每次都只为再听一遍同一个位次。现在该响应带上 `x-should-retry: false`(两个客户端都**先**看这个头,再看状态码)与 `retry-after: 0`。
29
+ - **不转发上游的 `retry_after`**:7 秒也清不掉 9021 位的队,转发它只会让客户端先等一会再收到同一个答复。
30
+ - **`retryAfterSec` 字段一并删除**:它只用来喂那句倒计时,现在没人读。留着解析但不用的数据,等于给这个承诺留了条回头的路。
31
+ - **其他 429 仍然可以重试**:`model_busy`(14003)是模型级限流,确实约一分钟就恢复,宿主重试正是对的处置。只把排队这一类标成终局,不是一刀切。
32
+
33
+ ### Tests
34
+
35
+ - `tests/queue-no-retry.spec.ts`(6 条,新增):`x-should-retry: false`、`retry-after: 0`、不转发上游 `retry_after`、位次与队列长度仍在、文案里没有「自动重试」、**其他 429 不受影响**(最后这条防止把可恢复的限流也变成即时失败)。
36
+ - `tests/queue.spec.ts`:`retryAfterSec` 相关断言改写为「不携带倒计时」。
37
+ - 全量:**111 文件 / 1807 条全绿**;两个 tsconfig 均 exit 0;build 成功。
38
+
3
39
  ## 5.2.1 (2026-10-22)
4
40
 
5
41
  ### Fixes
package/README.md CHANGED
@@ -43,7 +43,7 @@
43
43
  - **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。**每个模型行还有「可关闭思考」(`off`)勾选框**:决定要不要为该模型提供「完全关闭思考」这一档位。默认由内置复核名单给出(上游声称可关闭、实测却对它返回 HTTP 400 的 9 个模型默认不勾),你可以按模型改勾/取消——**取消**用于当前不需要该档位的模型;**名单内那 9 个模型不可勾选**(行上标「拒绝 off」,悬停有说明):该档位被勾上后不只是多一个选项,pi-ai 会把它当作**所有未指定档位请求的默认值**,压缩摘要请求正属于这一类,勾一次等于让该模型的每一次 `/compact` 都 400 / 11133(issue #83)。名单外的模型仍完全由你的勾选与自身声明决定;首次保存即写入你的勾选,之后不再重算。
44
44
  - **模型行上的活动徽标** —— 上游的模型目录里带着活动信息(免费试用、夜间折扣、限时免费、订阅优先等),插件**不新增任何请求**(复用刷新模型时本来就会拉的那份目录配置),直接在模型行上画一枚胶囊徽标:颜色与文案都由上游给出,悬停能看到上游的活动说明、活动的起止日期与每日生效时段(例如「夜间 23:00–7:50」)。**徽标是「此刻」的函数**:活动只在它自己声明的时间窗内显示,窗口一关徽标就自己消失;跨零点的时段按上游声明的时区判断;同一个模型命中多条活动时只画优先级最高的那条。上游没给结束时间的权益标记(如「订阅优先」)则常驻,直到上游把它下架。**徽标只是提示,不改变模型行为**——它标注的是上游此刻给出的活动与折扣,不替你做选择。(两个区域的上游目录都下发活动字段:国内版按 App 身份读 `/v3/config` 时带 8 条促销 + 1 条等级标记,国际版带 5 条 + 1 条;取证与判定规则见 [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md)。)
45
45
  - **模型行上的繁忙程度** —— 上游另有一条只读探针(`POST /v2/chat/busy`,一次最多 50 个 id,免费、不动账号状态)回答「这个模型此刻多忙」,插件在每次刷新目录时顺带问一次,把等级拼在倍率后面:`Hy4 preview · x0.29 · 极度拥堵`。**只画 `busy` / `congested` / `unavailable`**(轻微拥堵 / 极度拥堵 / 不可用):`idle` 有词但不画——实测 20 个模型里 7 个空闲,每行都挂就成了装饰而不是警告;`normal` 与探针没回答的 `unknown` 一律不画,**「没被告知」不会被当成「一切正常」**。**等级不落盘**:目录是能活过重启的事实,负载只是那一分钟的事实,所以等级只盖在运行时目录上、随进程消失,探针失败只记一条 warn,绝不挡住目录刷新。实测这条探针在 CLI 与 App 两种身份下答案逐字节相同,50 个 id 的上限也真实存在(62 个 → HTTP 400),19 个模型 186 ms 返回。
46
- - **排队时说清楚你排在第几位** —— 上游用三个业务码表达排队(`6020` 等待、`6021` 队列满、`6022` 已达上限)。撞上它们时,插件不再给一句上游报错,而是「模型排队中:你排在第 8777 位,队列共 9021 个请求;约 7 秒后自动重试」。**排队不会触发换号**:队列跟随的是模型与平台级的队伍,换个账号拿到的还是同一个位次,所以这里既不走账号池也不烧额外请求(上游 `/v2/chat/queue/status` 回的 `message` 是 i18n key 而不是人话,插件因此从不转述它,只用自己的措辞)。**「排到之后自动重发」这一半没有实现**——它需要排队答案在 OpenAI 兼容通道上的真实信封,没测到就不写。
46
+ - **排队时说清楚你排在第几位** —— 上游用三个业务码表达排队(`6020` 等待、`6021` 队列满、`6022` 已达上限)。撞上它们时,插件不再给一句上游报错,而是「模型排队中:你排在第 8777 位,队列共 9021 个请求。请稍后重新发送这条消息,或换一个模型」。**排队不会触发换号**:队列跟随的是模型与平台级的队伍,换个账号拿到的还是同一个位次,所以这里既不走账号池也不额外烧请求(上游 `/v2/chat/queue/status` 回的 `message` 是 i18n key 而不是人话,插件因此从不转述它,只用自己的措辞)。**排队不会自动重发,也不会被自动重试**——插件没有排队轮询循环(它需要排队答案在 OpenAI 兼容通道上的真实信封,没测到就不写),并且明确告诉宿主不要重试(`x-should-retry: false`):一次排队在几千位之后,重试十几次也轮不到,只会把真实请求白白花掉。位次报出来,由你决定是重发还是换模型。
47
47
  - **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,直接告诉你它此刻能否使用、不能用时何时恢复。**它发的是一条真实体积的请求(约 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 之后可再用」。三处来源都没说时,插件**不编造倒计时**,如实显示「上游未给出何时恢复」。限流**不会**去借月周期包的刷新时间(那会让人为几秒的限流去等几周)。探测在后台发起,**凭据不会传到页面**;结果不写入设置,也不改变你已保存的勾选。
48
48
 
49
49
  **两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件按模型的**原生窗口**向 DSH 宣称(3.5.0 起不再默认压到 200K),所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。想让 DSH 更早压缩,就在模型行上手动设一个「DSH 上下文预算」上限(可选 200K / 500K / 模型原生窗口)。
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutableWithSource, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, _t as WORKBUDDY_APP_EXECUTABLE_ENV, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, jt as hostPeersInfo, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, w as defaultDesktopAuthDirs, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-Icz67WtG.js";
2
+ import { C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutableWithSource, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, _t as WORKBUDDY_APP_EXECUTABLE_ENV, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, jt as hostPeersInfo, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, w as defaultDesktopAuthDirs, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-CgYyUwpM.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
@@ -6139,6 +6139,17 @@ window.__ModuleLoader__.load({
6139
6139
  }
6140
6140
  };
6141
6141
  /**
6142
+ * Whether ANY speed run is in flight, for the sweep button's disabled state.
6143
+ *
6144
+ * NOT `tpsProbing !== undefined`. `tpsProbing` is a record that is ALWAYS an
6145
+ * object, so that comparison is permanently true and the button was
6146
+ * permanently disabled — the defect this corrects. The other two sweep buttons
6147
+ * compare `sweeping !== undefined` because `sweeping` really is an
6148
+ * `{done,total} | undefined` pair; this map has no such absent state, so the
6149
+ * emptiness of its keys is what "busy" means.
6150
+ */
6151
+ const tpsBusy = Object.keys(tpsProbing).length > 0;
6152
+ /**
6142
6153
  * Sweep speed for the models this region has ENABLED.
6143
6154
  *
6144
6155
  * Enabled-only rather than the whole catalog: a speed run is three real
@@ -6614,7 +6625,7 @@ window.__ModuleLoader__.load({
6614
6625
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6615
6626
  type: "button",
6616
6627
  className: "dsm-btn dsm-btn-outline dsm-workbuddy-models-sweep",
6617
- disabled: busy || tpsSweeping !== void 0 || tpsProbing !== void 0 || activeEnabledIds.size === 0,
6628
+ disabled: busy || tpsSweeping !== void 0 || tpsBusy || activeEnabledIds.size === 0,
6618
6629
  title: t("row.tpsSweepEnabledHint"),
6619
6630
  onClick: sweepTpsEnabledModels,
6620
6631
  children: tpsSweeping === void 0 ? t("row.tpsSweepEnabled") : t("row.tpsTesting", {
@@ -1045,8 +1045,6 @@ const WORKBUDDY_QUEUE_CODES = [
1045
1045
  6021,
1046
1046
  6022
1047
1047
  ];
1048
- /** The app's default when the upstream sends no `retry_after` (`?? 5`). */
1049
- const WORKBUDDY_QUEUE_DEFAULT_RETRY_SEC = 5;
1050
1048
  /** True when a business code is one of the app's three queue codes. */
1051
1049
  function isQueueCode(value) {
1052
1050
  return typeof value === "number" && WORKBUDDY_QUEUE_CODES.includes(value);
@@ -1058,12 +1056,10 @@ function reportFrom(code, data) {
1058
1056
  const numberField = (key) => typeof body[key] === "number" && Number.isFinite(body[key]) ? body[key] : void 0;
1059
1057
  const position = numberField("queue_position");
1060
1058
  const size = numberField("queue_size");
1061
- const retryAfter = numberField("retry_after");
1062
1059
  return {
1063
1060
  code,
1064
1061
  ...position === void 0 ? {} : { position },
1065
- ...size === void 0 ? {} : { size },
1066
- retryAfterSec: retryAfter !== void 0 && retryAfter > 0 ? retryAfter : WORKBUDDY_QUEUE_DEFAULT_RETRY_SEC
1062
+ ...size === void 0 ? {} : { size }
1067
1063
  };
1068
1064
  }
1069
1065
  /**
@@ -1121,9 +1117,22 @@ function readEnvelope$1(value) {
1121
1117
  *
1122
1118
  * `waiting` names both numbers because they answer different questions: the
1123
1119
  * POSITION is how long the wait is, the SIZE is whether it is moving at all.
1120
+ *
1121
+ * IT NO LONGER PROMISES "自动重试" (removed 2026-10-22, at the maintainer's
1122
+ * request). The sentence used to end with 「约 N 秒后自动重试」, and nothing
1123
+ * retried: this plugin does not poll `/v2/chat/queue/status` and does not resend
1124
+ * (see "WHAT THIS MODULE DOES NOT DO YET" above). The only retry that ever
1125
+ * happened was the HOST's own bounded in-place retry on a 429 — two attempts,
1126
+ * 0.5s then 1s, about 1.5 seconds — which is not "N seconds" and gives up long
1127
+ * before a real queue of thousands moves.
1128
+ *
1129
+ * So the sentence now states the WAIT and leaves the next move to the reader.
1130
+ * Reporting the position stays honest and useful — knowing you are 8,777th is
1131
+ * the answer to "wait or switch" — but claiming a recovery the plugin does not
1132
+ * perform turned a diagnostic into a promise it could not keep.
1124
1133
  */
1125
1134
  function queueNoticeText(report) {
1126
- if (report.code === 6020) return `模型排队中:你排在第 ${report.position === void 0 ? "未知" : `${report.position}`} 位${report.size === void 0 ? "" : `,队列共 ${report.size} 个请求`};约 ${report.retryAfterSec} 秒后自动重试`;
1135
+ if (report.code === 6020) return `模型排队中:你排在第 ${report.position === void 0 ? "未知" : `${report.position}`} 位${report.size === void 0 ? "" : `,队列共 ${report.size} 个请求`}。请稍后重新发送这条消息,或换一个模型`;
1127
1136
  if (report.code === 6021) return `模型队列已满${report.size === void 0 ? "" : `(共 ${report.size} 个请求排队)`},现在无法排队;请稍后重试或换一个模型`;
1128
1137
  return `你的排队任务已达上限${report.size === void 0 ? "" : `(共 ${report.size} 个请求排队)`},请等待已排队请求完成后再试`;
1129
1138
  }
@@ -5852,7 +5861,7 @@ function generationOf(version) {
5852
5861
  //#endregion
5853
5862
  //#region src/version.ts
5854
5863
  /** The npm package version this build was produced from. */
5855
- const WORKBUDDY_CONNECT_VERSION = "5.2.1";
5864
+ const WORKBUDDY_CONNECT_VERSION = "5.2.3";
5856
5865
  //#endregion
5857
5866
  //#region src/host-heartbeat.ts
5858
5867
  /**
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as regionOf, A as parseWorkBuddyAuth, At as workbuddyAppExecutableCandidates, B as forceNeutralHead, C as defaultDesktopAuthCandidates, D as isEncryptedCredentialError, Dt as macosNestedAppBundles, E as hasEncryptedCredentialFields, Et as macosBundleExecutable, F as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, G as knownRegionOf, H as isConversationIdGateBody, I as WorkBuddyCredentialRejectedError, J as parseCreditMultiplier, K as modelIdOfBody, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, Mt as resolveDshHome, N as CONTEXT_OVERFLOW_MARKER, Nt as withFileLock, O as isVaultAccountId, Ot as openEncryptedField, P as CREDENTIAL_REJECTED_CODE, Pt as writeFileAtomic, Q as prepareChatBody, R as classifyUpstreamError, S as authFileName, St as findWorkbuddyAppExecutable, T as defaultDesktopAuthPath, Tt as isWorkbuddyBundle, U as isCredentialRejectedError, V as isContextTooLongBody, W as isFirstMessageGateBody, X as parseUpstreamModel, Y as parseReasoning, Z as perturbChannelFingerprints, _ as ENCRYPTED_CREDENTIAL_CODE, _t as WORKBUDDY_APP_EXECUTABLE_ENV, a as readHostHeartbeat, at as offDefaultFor, b as WorkBuddyCredentialStore, bt as deriveAtRestKeyId, c as WORKBUDDY_CONNECT_VERSION, ct as withEffectiveOff, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as pickImageModel, et as attachBusyLevels, f as WorkBuddyCatalog, ft as parseQueueAnswer, g as withoutRouters, gt as nativeModalityOf, h as fallbackModelsFor, ht as imageDefaultFor, i as processStartTimeMs, it as lastRefusalFor, j as workbuddyAccountId, k as legacyWorkbuddyOwnAuthPath, kt as readAtRestKey, l as piAiRuntimeInfo, lt as DEFAULT_IMAGE_SIZE, m as deriveCatalog, mt as NATIVE_MODALITY_BY_MODEL_ID, n as clearHostHeartbeat, nt as REFUSES_OFF_MODEL_IDS, o as workbuddyHostHeartbeatPath, ot as offRefused, p as applyContextBudgets, pt as queueNoticeText, q as openingEvidenceOfBody, r as isHeartbeatProcessAlive, rt as defaultOffModelIds, s as writeHostHeartbeat, st as recordRefusal, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as busyDisplaySuffix, u as FALLBACK_WORKBUDDY_MODELS, ut as WorkBuddyImageError, v as WORKBUDDY_AUTH_FILENAME, vt as clearAtRestKeyCache, w as defaultDesktopAuthDirs, wt as isEncryptedFieldWrapper, x as WorkBuddyEncryptedCredentialError, xt as fetchAtRestKeyPayload, y as WORKBUDDY_AUTH_FILE_ENV, yt as deriveAtRestKey, z as declaredTools } from "./host-heartbeat-Icz67WtG.js";
1
+ import { $ as regionOf, A as parseWorkBuddyAuth, At as workbuddyAppExecutableCandidates, B as forceNeutralHead, C as defaultDesktopAuthCandidates, D as isEncryptedCredentialError, Dt as macosNestedAppBundles, E as hasEncryptedCredentialFields, Et as macosBundleExecutable, F as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, G as knownRegionOf, H as isConversationIdGateBody, I as WorkBuddyCredentialRejectedError, J as parseCreditMultiplier, K as modelIdOfBody, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, Mt as resolveDshHome, N as CONTEXT_OVERFLOW_MARKER, Nt as withFileLock, O as isVaultAccountId, Ot as openEncryptedField, P as CREDENTIAL_REJECTED_CODE, Pt as writeFileAtomic, Q as prepareChatBody, R as classifyUpstreamError, S as authFileName, St as findWorkbuddyAppExecutable, T as defaultDesktopAuthPath, Tt as isWorkbuddyBundle, U as isCredentialRejectedError, V as isContextTooLongBody, W as isFirstMessageGateBody, X as parseUpstreamModel, Y as parseReasoning, Z as perturbChannelFingerprints, _ as ENCRYPTED_CREDENTIAL_CODE, _t as WORKBUDDY_APP_EXECUTABLE_ENV, a as readHostHeartbeat, at as offDefaultFor, b as WorkBuddyCredentialStore, bt as deriveAtRestKeyId, c as WORKBUDDY_CONNECT_VERSION, ct as withEffectiveOff, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as pickImageModel, et as attachBusyLevels, f as WorkBuddyCatalog, ft as parseQueueAnswer, g as withoutRouters, gt as nativeModalityOf, h as fallbackModelsFor, ht as imageDefaultFor, i as processStartTimeMs, it as lastRefusalFor, j as workbuddyAccountId, k as legacyWorkbuddyOwnAuthPath, kt as readAtRestKey, l as piAiRuntimeInfo, lt as DEFAULT_IMAGE_SIZE, m as deriveCatalog, mt as NATIVE_MODALITY_BY_MODEL_ID, n as clearHostHeartbeat, nt as REFUSES_OFF_MODEL_IDS, o as workbuddyHostHeartbeatPath, ot as offRefused, p as applyContextBudgets, pt as queueNoticeText, q as openingEvidenceOfBody, r as isHeartbeatProcessAlive, rt as defaultOffModelIds, s as writeHostHeartbeat, st as recordRefusal, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as busyDisplaySuffix, u as FALLBACK_WORKBUDDY_MODELS, ut as WorkBuddyImageError, v as WORKBUDDY_AUTH_FILENAME, vt as clearAtRestKeyCache, w as defaultDesktopAuthDirs, wt as isEncryptedFieldWrapper, x as WorkBuddyEncryptedCredentialError, xt as fetchAtRestKeyPayload, y as WORKBUDDY_AUTH_FILE_ENV, yt as deriveAtRestKey, z as declaredTools } from "./host-heartbeat-CgYyUwpM.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -2434,11 +2434,12 @@ const KIND_HOST_CODE = { soft_rate: "quota_exceeded" };
2434
2434
  function hostErrorCode(kind) {
2435
2435
  return KIND_HOST_CODE[kind] ?? kind;
2436
2436
  }
2437
- function writeJson(res, status, body) {
2437
+ function writeJson(res, status, body, headers) {
2438
2438
  const payload = JSON.stringify(body);
2439
2439
  res.writeHead(status, {
2440
2440
  "Content-Type": "application/json",
2441
- "Content-Length": Buffer.byteLength(payload)
2441
+ "Content-Length": Buffer.byteLength(payload),
2442
+ ...headers
2442
2443
  });
2443
2444
  res.end(payload);
2444
2445
  }
@@ -2450,13 +2451,38 @@ function writeJson(res, status, body) {
2450
2451
  * must not be disguised as one. The upstream failure path passes an explicit
2451
2452
  * `code` so the host classifies it correctly — see {@link KIND_HOST_CODE}.
2452
2453
  */
2453
- function writeOpenAIError(res, status, kind, message, code) {
2454
+ function writeOpenAIError(res, status, kind, message, code, headers) {
2454
2455
  writeJson(res, status, { error: {
2455
2456
  message,
2456
2457
  type: kind,
2457
2458
  code: code ?? kind
2458
- } });
2459
+ } }, headers);
2459
2460
  }
2461
+ /**
2462
+ * Tell every retrying client NOT to retry this failure (2026-10-22, at the
2463
+ * maintainer's request: the queue's automatic retry is removed).
2464
+ *
2465
+ * `x-should-retry` is a non-standard header that both clients in this stack
2466
+ * check BEFORE their status-based rules — the pinned OpenAI SDK's `shouldRetry`
2467
+ * and pi-ai's `isRetryableProviderError` each return false on "false".
2468
+ *
2469
+ * WHY THE QUEUE MUST NOT BE RETRIED. Without this header a 429 is retried by
2470
+ * construction: `@deepseek-ai/dsh-llm` maps status 429 to the `RATE_LIMIT` code
2471
+ * and `RATE_LIMIT` is in `DEFAULT_RETRYABLE_CODES`, so the HOST runs five
2472
+ * in-place attempts with 0.5s→8s backoff. Measured, that budget totals roughly
2473
+ * 15 seconds — against a queue the upstream reports in thousands. Every one of
2474
+ * those attempts is a real request spent to be told the same position again, and
2475
+ * none of them can clear the queue.
2476
+ *
2477
+ * So the queue is now reported ONCE, with the position intact, and the reader
2478
+ * decides whether to resend or switch models. That is exactly what
2479
+ * {@link queueNoticeText} tells them to do — the two changes are one decision: a
2480
+ * diagnostic that declares itself final must actually be final.
2481
+ */
2482
+ const NO_RETRY_HEADERS = Object.freeze({
2483
+ "x-should-retry": "false",
2484
+ "retry-after": "0"
2485
+ });
2460
2486
  /** Read a request body with a size cap; over-limit bodies fail the request. */
2461
2487
  function readBody(req) {
2462
2488
  return new Promise((resolve, reject) => {
@@ -3570,7 +3596,7 @@ function createWorkBuddyShim(options) {
3570
3596
  if (refusedReport !== void 0) reportRefusedTier(refusedReport);
3571
3597
  const queue = result.kind === "model_queue" ? parseQueueAnswer(result.message) : void 0;
3572
3598
  if (queue !== void 0) {
3573
- writeOpenAIError(res, 429, "model_queue", `${queueNoticeText(queue)}(上游 code ${queue.code})${note}`);
3599
+ writeOpenAIError(res, 429, "model_queue", `${queueNoticeText(queue)}(上游 code ${queue.code})${note}`, void 0, NO_RETRY_HEADERS);
3574
3600
  return;
3575
3601
  }
3576
3602
  if (result.kind === "policy_reject") {
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": "5.2.1",
5
+ "version": "5.2.3",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",