dsh-connect-workbuddy 3.4.0 → 3.4.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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.1 (2026-10-02)
4
+
5
+ ### Fixes
6
+
7
+ - **「要等几小时才恢复」的配额错误不再被宿主当成「等两秒就好」的瞬时抖动(issue #28)。**
8
+ - **现象**:上游 429 / code 6004 的原文是「您的使用量已超出频率限制,**将在 13:37:03 重置**」——要等几小时。本插件把它正确分类成 `soft_rate`(`src/upstream.ts`),但写回宿主时 `type` 与 `code` 都用了 `soft_rate` 这个内部名字。
9
+ - **根因**:宿主决定「这个失败要不要在原地重试」靠的是从 `type + code + message` 里匹配**几个英文措辞**(`@deepseek-ai/dsh-llm` 的 `isQuotaExceededError`)。`soft_rate` 不在那份名单里,上游原话又是中文,于是宿主只能按 `429` 的默认含义理解——进入 `RATE_LIMIT`,而 `RATE_LIMIT` **在**宿主的重试集合里(`DEFAULT_RETRYABLE_CODES`),`QUOTA` **不在**。宿主于是原地重试 5 次、指数退避,把预算烧光;而真正能救场的跨供应商故障转移(`dsh-subagent-default-model` 的 `failoverEnabled`)根本没被叫到——实测一次会话 1228 个 step、只切换过 1 次供应商、以 429 收场,`models` 里其它供应商全程闲置。
10
+ - **修法**:只翻译写回宿主的 `code`——`soft_rate` → `quota_exceeded`(宿主认得这个词,且它符合上游实际在说的事)。**HTTP 状态码仍是 429**(那才是上游返回的东西,改成 402 是谎报),上游原话仍原样留在 `message` 里给人看。`type` 保持插件自己的内部名字,便于读日志与报错。
11
+ - **只翻译这一处**:其余 kind 本来就落在重试集合之外(`hard_credit` 402 → 按状态就是 QUOTA,`session_dead` 401 → AUTH,502/400 → OTHER),所以**不做多余翻译**——没有人需要的翻译只是等着漂移的谎言。插件**自己的**错误(`unauthorized` / `not_found` / `host_not_allowed` / `internal`)也一律不翻译:把自身的 401/404 说成「配额用尽」,会让宿主去为一个换供应商也治不好的错误做故障转移。
12
+ - 测试直接调用**宿主自己的** `isQuotaExceededError`(`@deepseek-ai/dsh-llm` 导出)来钉住这件事,而不是在测试里抄一份正则——抄一份只能证明两份副本今天一致。三次变异验证:撤回翻译 / 把翻译扩大到所有 kind / 让本地错误也翻译,均会变红。
13
+
3
14
  ## 3.4.0 (2026-10-02)
4
15
 
5
16
  ### Fixes
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { E as legacyWorkbuddyOwnAuthPath, H as WORKBUDDY_APP_EXECUTABLE_ENV, N as WorkBuddyUpstreamClient, S as defaultDesktopAuthDirs, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, q as findWorkbuddyAppExecutable, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, x as defaultDesktopAuthCandidates } from "./host-heartbeat-C2_Zmuqq.js";
2
+ import { E as legacyWorkbuddyOwnAuthPath, H as WORKBUDDY_APP_EXECUTABLE_ENV, N as WorkBuddyUpstreamClient, S as defaultDesktopAuthDirs, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, q as findWorkbuddyAppExecutable, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, x as defaultDesktopAuthCandidates } from "./host-heartbeat-DSO-07lc.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
@@ -2552,7 +2552,7 @@ function generationOf(version) {
2552
2552
  //#endregion
2553
2553
  //#region src/version.ts
2554
2554
  /** The npm package version this build was produced from. */
2555
- const WORKBUDDY_CONNECT_VERSION = "3.4.0";
2555
+ const WORKBUDDY_CONNECT_VERSION = "3.4.1";
2556
2556
  //#endregion
2557
2557
  //#region src/host-heartbeat.ts
2558
2558
  /**
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as readAtRestKey, A as CREDENTIAL_REJECTED_CODE, B as prepareChatBody, C as defaultDesktopAuthPath, D as parseWorkBuddyAuth, E as legacyWorkbuddyOwnAuthPath, F as declaredTools, G as deriveAtRestKeyId, H as WORKBUDDY_APP_EXECUTABLE_ENV, I as isCredentialRejectedError, J as isEncryptedFieldWrapper, K as fetchAtRestKeyPayload, L as parseCreditMultiplier, M as WorkBuddyCredentialRejectedError, N as WorkBuddyUpstreamClient, O as workbuddyAccountId, P as classifyUpstreamError, Q as openEncryptedField, R as parseReasoning, S as defaultDesktopAuthDirs, T as isEncryptedCredentialError, U as clearAtRestKeyCache, V as regionOf, W as deriveAtRestKey, X as macosBundleExecutable, Y as isWorkbuddyBundle, Z as macosNestedAppBundles, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, b as authFileName, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as workbuddyAppExecutableCandidates, f as WorkBuddyCatalog, g as WORKBUDDY_AUTH_FILENAME, h as ENCRYPTED_CREDENTIAL_CODE, i as processStartTimeMs, j as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, m as fallbackModelsFor, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as deriveCatalog, q as findWorkbuddyAppExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, w as hasEncryptedCredentialFields, x as defaultDesktopAuthCandidates, y as WorkBuddyEncryptedCredentialError, z as parseUpstreamModel } from "./host-heartbeat-C2_Zmuqq.js";
1
+ import { $ as readAtRestKey, A as CREDENTIAL_REJECTED_CODE, B as prepareChatBody, C as defaultDesktopAuthPath, D as parseWorkBuddyAuth, E as legacyWorkbuddyOwnAuthPath, F as declaredTools, G as deriveAtRestKeyId, H as WORKBUDDY_APP_EXECUTABLE_ENV, I as isCredentialRejectedError, J as isEncryptedFieldWrapper, K as fetchAtRestKeyPayload, L as parseCreditMultiplier, M as WorkBuddyCredentialRejectedError, N as WorkBuddyUpstreamClient, O as workbuddyAccountId, P as classifyUpstreamError, Q as openEncryptedField, R as parseReasoning, S as defaultDesktopAuthDirs, T as isEncryptedCredentialError, U as clearAtRestKeyCache, V as regionOf, W as deriveAtRestKey, X as macosBundleExecutable, Y as isWorkbuddyBundle, Z as macosNestedAppBundles, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, b as authFileName, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as workbuddyAppExecutableCandidates, f as WorkBuddyCatalog, g as WORKBUDDY_AUTH_FILENAME, h as ENCRYPTED_CREDENTIAL_CODE, i as processStartTimeMs, j as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, m as fallbackModelsFor, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as deriveCatalog, q as findWorkbuddyAppExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, w as hasEncryptedCredentialFields, x as defaultDesktopAuthCandidates, y as WorkBuddyEncryptedCredentialError, z as parseUpstreamModel } from "./host-heartbeat-DSO-07lc.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -1171,6 +1171,46 @@ const KIND_STATUS = {
1171
1171
  server: 502,
1172
1172
  client: 400
1173
1173
  };
1174
+ /**
1175
+ * The `code` spelling the HOST is expected to classify each upstream failure by.
1176
+ *
1177
+ * The host decides whether a failed request may be retried IN PLACE (5 attempts
1178
+ * with backoff) or must be handed to cross-provider failover by reading English
1179
+ * phrases out of `type + " " + code + " " + message` — see
1180
+ * `isQuotaExceededError` in `@deepseek-ai/dsh-llm`. Only `RATE_LIMIT`, `SERVER`,
1181
+ * `TIMEOUT`, `TRANSPORT` and `EMPTY_RESPONSE` are in its retry set, so the
1182
+ * spelling decides which of two very different things happens next.
1183
+ *
1184
+ * `soft_rate` is the one kind that spelling got wrong. The upstream's 429 says
1185
+ * "usage exceeds the frequency limit, and it resets at 13:37" — hours away, not
1186
+ * two seconds. Sent as `soft_rate` it reads as an ordinary rate limit, enters
1187
+ * the in-place retry set, and spends the whole retry budget on an endpoint that
1188
+ * cannot recover within it; by the time cross-provider failover is offered, the
1189
+ * turn has already failed. The upstream's own words are Chinese, so the
1190
+ * phrase-based classifier cannot see the distinction either — the label is the
1191
+ * only channel left to carry it.
1192
+ *
1193
+ * `quota_exceeded` is the minimal honest translation: it is the WORDS the host
1194
+ * recognises, and it matches what the upstream is actually saying. The HTTP
1195
+ * status stays 429 (that is what the upstream returned — relabelling it 402
1196
+ * would misreport the upstream), and the upstream's own message is still passed
1197
+ * through verbatim for a human to read.
1198
+ *
1199
+ * Every other kind already lands outside the retry set by its status alone
1200
+ * (`hard_credit` 402 → quota, `session_dead` 401 → auth, 502/400 → other), so
1201
+ * none of them is listed: a translation nobody needs is a lie waiting to drift.
1202
+ */
1203
+ const KIND_HOST_CODE = { soft_rate: "quota_exceeded" };
1204
+ /**
1205
+ * The `code` to write for an upstream failure, given its classified kind.
1206
+ *
1207
+ * `type` deliberately stays the plugin's own kind: it is what a human reads in
1208
+ * a log or a bug report, and it keeps the two fields individually meaningful.
1209
+ * Only `code` carries the host-facing translation.
1210
+ */
1211
+ function hostErrorCode(kind) {
1212
+ return KIND_HOST_CODE[kind] ?? kind;
1213
+ }
1174
1214
  function writeJson(res, status, body) {
1175
1215
  const payload = JSON.stringify(body);
1176
1216
  res.writeHead(status, {
@@ -1179,11 +1219,19 @@ function writeJson(res, status, body) {
1179
1219
  });
1180
1220
  res.end(payload);
1181
1221
  }
1182
- function writeOpenAIError(res, status, kind, message) {
1222
+ /**
1223
+ * Write a JSON error the way an OpenAI-compatible client expects.
1224
+ *
1225
+ * `code` defaults to `type`, which is right for every error THIS shim raises on
1226
+ * its own (`unauthorized`, `not_found`, …): those are not upstream failures and
1227
+ * must not be disguised as one. The upstream failure path passes an explicit
1228
+ * `code` so the host classifies it correctly — see {@link KIND_HOST_CODE}.
1229
+ */
1230
+ function writeOpenAIError(res, status, kind, message, code) {
1183
1231
  writeJson(res, status, { error: {
1184
1232
  message,
1185
1233
  type: kind,
1186
- code: kind
1234
+ code: code ?? kind
1187
1235
  } });
1188
1236
  }
1189
1237
  /** Read a request body with a size cap; over-limit bodies fail the request. */
@@ -1706,7 +1754,7 @@ function createWorkBuddyShim(options) {
1706
1754
  if (!result.ok) {
1707
1755
  const attempts = triedAccountIds.length;
1708
1756
  const note = attempts > 1 ? ` (after trying ${attempts} accounts)` : "";
1709
- writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy upstream ${result.kind} (http ${result.status})${note}: ${result.message.slice(0, 400)}`);
1757
+ writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy upstream ${result.kind} (http ${result.status})${note}: ${result.message.slice(0, 400)}`, hostErrorCode(result.kind));
1710
1758
  return;
1711
1759
  }
1712
1760
  res.writeHead(200, {
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": "3.4.0",
5
+ "version": "3.4.1",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",