dsh-connect-workbuddy 4.4.3 → 4.4.4
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 +29 -0
- package/README.en.md +22 -0
- package/README.md +22 -0
- package/lib/bin.js +1 -1
- package/lib/{host-heartbeat-C8imAyWG.js → host-heartbeat-BwbDWvzw.js} +1 -1
- package/lib/index.js +65 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.4.4 (2026-10-13)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **实时聊天路径丢掉了「确定的积分刷新时刻」:同一个「积分耗尽」在一屏上给出两个恢复时间,其中一个差了 18 天。**(issue #56 报告者在核验 4.4.3 时发现)
|
|
8
|
+
- **来源**:报告者核完 4.4.3 的三处修复后,回答我请他判断的那个问题(「`out-of-credit` 无时刻时仍走 30 分钟兜底,是否该改」),并给出了**实测依据**。他先写下、又**自己更正掉**了一个错误结论(「`quotaRefreshAtMs` 没有调用点」—— 实测有 15 处、管道是通的),更正后的结论反而更具体。
|
|
9
|
+
- **现象**:同一个 `out-of-credit` 失败,**按它从哪个门进来**得到两个答案:
|
|
10
|
+
| 路径 | `cooldownOf` 输出 | 池表显示的恢复时刻 |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| 探针(传 `quotaRefreshAtMs`) | `{retryAtMs: 2026-11-01, retrySource: 'quota-refresh'}` | **2026-11-01** |
|
|
13
|
+
| 实时聊天(当时不传) | `{}` → 落到 `POOL_UNKNOWN_COOLDOWN_MS` | **约 30 分钟后** |
|
|
14
|
+
**两条路径相差约 18 天**,而计费侧给出的那个「下月 1 日」才是唯一诚实的答案。
|
|
15
|
+
- **两个独立缺陷,且会叠加**:
|
|
16
|
+
1. **`recordAccountFailure()` 没有传 `quotaRefreshAtMs`** —— 探针路径早已证明这个数据拿得到(`quotaRefreshOf(region, credential)`),实时路径却没接上。
|
|
17
|
+
2. **`writePoolProbes()` 是账户级整体展开**(`{ ...existing, ...updates }`),`updates[accountId]` 这整个**新对象**替换掉旧对象 —— 所以实时聊天那次写入会把探针**已经存好的** `retryAtMs` 一起删掉。只修第 1 条不够:一条不带 `retryAtMs` 的 `live-request` 记录仍会抹掉好值。
|
|
18
|
+
- **修法**:
|
|
19
|
+
- `recordAccountFailure()` 在 `outcome === 'out-of-credit'` 时查一次 `quotaRefreshOf(region, credential)` 并传入 `cooldownOf`。**只对这一个 outcome 查**:`quotaRefreshOf` 要发一次上游请求,而限流或网络抖动**没有刷新点可找** —— 为它付这次请求等于花钱去学「没有答案」。查不到不是错误,兜底照旧。
|
|
20
|
+
- `writePoolProbes()` 改为逐账户调用新增的纯函数 **`mergePoolProbe(existing, update)`**:`outcome` / `atMs` / `source` / `message` 一律归**更新的那次测量**,而 `retryAtMs` 在**更新没给**时**继承**旧值。继承只针对「缺失不等于声明」的这一个字段 —— 更新**给了**时刻仍然覆盖(新的、更短的冷却必须能压过旧的)。另有一条边界:**已经过去的时刻不继承**,否则记录会被钉在一个已经消失的时刻上,行里会读成「<过去的时刻> 之后可再用」。
|
|
21
|
+
- **刻意未改**「无时刻时仍走 30 分钟兜底」—— 报告者明确不主张改这一条(拿不到刷新点时,用短窗口偶尔试试是合理的)。我们反对的只是**在拿得到真值的时候用那个数字**。
|
|
22
|
+
- **守卫**:新增 `tests/issue56-quota-refresh-merge.spec.ts`(11 例),覆盖继承规则、覆盖规则、过期时刻不继承、以及**磁盘上真实的「探针先写、实时聊天后写」序列**仍然保住刷新点;另把 `tests/pool-e2e.spec.ts` 里那条按**源码形状**断言的守卫扩了三条(必须查到 `quotaRefreshOf(`、必须把 `quotaRefreshAtMs` 传进 `cooldownOf`、且只对能有的 outcome 查)—— 因为 `recordAccountFailure` 此前**没有任何测试覆盖到**,把修复还原后全套测试仍然全绿,这正是它会溜掉的原因。变异验证:还原合并改动 → 2 例变红;还原实时路径的入参 → 源码守卫变红。
|
|
23
|
+
|
|
24
|
+
- **两处「下一版补」的文档欠账,本版补上。**
|
|
25
|
+
- **`chatStream()` 返回信封而非可迭代流**(issue #56 报告者最初就提醒过,我当时说了「下一版补」):中英 README 的「开发」一节新增一节,写明要先查 `envelope.ok`,并点名跳过它的典型后果是**把失败全报成成功**(`response` 为 `undefined`,读它会抛错或被吞掉,于是「每个模型都成功」—— 报告者自己第一次写脚本就踩了这个)。
|
|
26
|
+
- **pnpm 新版本等待期会挡住浮动升级**(报告者在核验 4.4.3 时顺手实测):中英 README 的「安装」一节新增告示 —— 版本刚发布时浮动范围(`^4.4.2`)会被**静默压回上一版**,而精确版本(`4.4.3`)立即生效;两种写法都「安装成功」,所以症状是「升级跑完了、版本没变」,**极易被误报成发布失败**。附上他的实测表(隔离目录、pnpm 12.3.4、`--lockfile-only`),并明确建议写精确版本号。
|
|
27
|
+
|
|
28
|
+
### Docs
|
|
29
|
+
|
|
30
|
+
- 无其它。本版含上述修复与两处文档补记。
|
|
31
|
+
|
|
3
32
|
## 4.4.3 (2026-10-13)
|
|
4
33
|
|
|
5
34
|
### Fixes
|
package/README.en.md
CHANGED
|
@@ -179,6 +179,16 @@ npm install dsh-connect-workbuddy
|
|
|
179
179
|
|
|
180
180
|
Restart the DSH process after install/update/uninstall.
|
|
181
181
|
|
|
182
|
+
> ⚠️ **Pin an EXACT version for a fresh release; do not rely on a floating range.** pnpm enforces a release-age window (`minimumReleaseAge`, 24h by default), so shortly after a version ships a floating range is **silently resolved back to the previous one**: `^4.4.2` still resolves to `4.4.2`, while an exact `4.4.3` gets `4.4.3` immediately. Both spellings "install successfully", so the symptom is *"the upgrade command finished and the version did not change"* — easily misread as a failed release. Measured (isolated directory, pnpm 12.3.4, `--lockfile-only`, touching nothing of the user's environment):
|
|
183
|
+
>
|
|
184
|
+
> | Target spelling | Resolved version |
|
|
185
|
+
> | --- | --- |
|
|
186
|
+
> | floating `^4.4.2` | **4.4.2** ⚠️ |
|
|
187
|
+
> | exact `4.4.3` | **4.4.3** |
|
|
188
|
+
> | floating + `--config.minimum-release-age=0` | **4.4.3** |
|
|
189
|
+
>
|
|
190
|
+
> To pick up a new release right away, pin it: `dsh plugin --profile desktop add dsh-connect-workbuddy@4.4.4`. A floating range catches up once the window elapses.
|
|
191
|
+
|
|
182
192
|
The plugin also runs under **Web** and **TUI**; pick the command matching your profile:
|
|
183
193
|
|
|
184
194
|
```sh
|
|
@@ -290,6 +300,18 @@ Local dev via a `link:` install to the desktop profile (restart DSH Desktop afte
|
|
|
290
300
|
dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
|
|
291
301
|
```
|
|
292
302
|
|
|
303
|
+
### Calling `chatStream()` from a script: it returns an **envelope**, not a stream
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
const envelope = await client.chatStream(credential, bodyJson)
|
|
307
|
+
if (!envelope.ok) { /* failure: envelope.status / .kind / .message / .detail */ }
|
|
308
|
+
else { await envelope.response.text() /* or consume it as SSE */ }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Check `envelope.ok` before consuming.** It returns `{ ok: true, response }` or `{ ok: false, status, kind, message, detail }` — not something you can `for await` directly. Skip that check and the usual outcome is **every failure reported as a success** (there is no `response`, reading it throws or is swallowed, and "all models passed"). Recorded here on the strength of the issue #56 reporter's own note, so the next person does not lose the same hour.
|
|
312
|
+
|
|
313
|
+
`kind` on a failed envelope is the failure classification used throughout this document (`soft_rate` / `model_busy` / `hard_credit` / `session_dead` / `policy_reject` / `not_found` / `server` / `client`), and `detail` carries the upstream `upstreamCode` / `requestId` / `displayMsg`.
|
|
314
|
+
|
|
293
315
|
## Marketplace listing
|
|
294
316
|
|
|
295
317
|
The plugin ships an installable `dsh.bundle` manifest and is published to npm. Community marketplaces generally sync entries from the [Awesome DSH Plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) registry; the submission draft lives at [`awesome-dsh-plugin-submission/dingminhua__dsh-connect-workbuddy.yml`](awesome-dsh-plugin-submission/dingminhua__dsh-connect-workbuddy.yml), and the formal submission is PR [#3812](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/3812) (`data/plugins/dingminhua__dsh-connect-workbuddy.yml`, CI and Submission gate green, awaiting maintainer merge).
|
package/README.md
CHANGED
|
@@ -189,6 +189,16 @@ npm install dsh-connect-workbuddy
|
|
|
189
189
|
|
|
190
190
|
安装、更新或卸载 bundle 后,需要重启对应的 DSH 进程。
|
|
191
191
|
|
|
192
|
+
> ⚠️ **刚发布的版本请写「精确版本号」,不要依赖浮动范围。** pnpm 有**新版本等待期**(`minimumReleaseAge`,默认 24 小时),所以一个版本刚发布不久时,浮动范围会被**静默压回上一个版本**:声明 `^4.4.2` 时它仍解析到 `4.4.2`,而写精确的 `4.4.3` 则立即拿到 `4.4.3`。由于**两种写法都「安装成功」**,表现是「升级命令跑完了、版本却没变」,很容易被误判成发布失败。已实测(隔离目录、pnpm 12.3.4、`--lockfile-only`,未触碰用户环境):
|
|
193
|
+
>
|
|
194
|
+
> | 目标写法 | 解析到的版本 |
|
|
195
|
+
> | --- | --- |
|
|
196
|
+
> | 浮动 `^4.4.2` | **4.4.2** ⚠️ |
|
|
197
|
+
> | 精确 `4.4.3` | **4.4.3** |
|
|
198
|
+
> | 浮动 + `--config.minimum-release-age=0` | **4.4.3** |
|
|
199
|
+
>
|
|
200
|
+
> 想立刻用上新版,请写精确版本号:`dsh plugin --profile desktop add dsh-connect-workbuddy@4.4.4`。过了等待期之后,浮动范围会自然跟上。
|
|
201
|
+
|
|
192
202
|
插件在 **Web**、**Desktop**、**TUI** 三种界面下均可运行,按你使用的 profile 选择对应命令:
|
|
193
203
|
|
|
194
204
|
```sh
|
|
@@ -300,6 +310,18 @@ pnpm run check # typecheck + test + build
|
|
|
300
310
|
dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-workbuddy
|
|
301
311
|
```
|
|
302
312
|
|
|
313
|
+
### 写脚本调用 `chatStream()` 时注意:它返回的是**信封**,不是可迭代流
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
const envelope = await client.chatStream(credential, bodyJson)
|
|
317
|
+
if (!envelope.ok) { /* 失败:envelope.status / .kind / .message / .detail */ }
|
|
318
|
+
else { await envelope.response.text() /* 或者按 SSE 消费 */ }
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**先查 `envelope.ok` 再消费** —— 它返回 `{ ok: true, response }` 或 `{ ok: false, status, kind, message, detail }`,而不是一个可以直接 `for await` 的流。跳过这一步的典型后果是**把失败全报成成功**(`response` 是 `undefined`,读它会抛错或被吞掉,于是「每个模型都成功」)。这条来自 issue #56 报告者的实测提醒,写在这里是为了让下一个人少踩一次同样的坑。
|
|
322
|
+
|
|
323
|
+
失败信封里的 `kind` 就是本文提到的失败分类(`soft_rate` / `model_busy` / `hard_credit` / `session_dead` / `policy_reject` / `not_found` / `server` / `client`),`detail` 携带上游的 `upstreamCode` / `requestId` / `displayMsg`。
|
|
324
|
+
|
|
303
325
|
## 市场收录与展示
|
|
304
326
|
|
|
305
327
|
插件已包含可安装的 `dsh.bundle` manifest,并发布到 npm。社区市场通常从 [Awesome DSH Plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 注册表同步条目;仓库内的提交草稿位于 [`awesome-dsh-plugin-submission/dingminhua__dsh-connect-workbuddy.yml`](awesome-dsh-plugin-submission/dingminhua__dsh-connect-workbuddy.yml),正式提交为 PR [#3812](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/3812)(`data/plugins/dingminhua__dsh-connect-workbuddy.yml`,已通过 CI 与 Submission gate,等待维护者合并)。
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, G as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, S as defaultDesktopAuthCandidates, Z as findWorkbuddyAppExecutableWithSource, a as readHostHeartbeat, at as hostPeersInfo, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-
|
|
2
|
+
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, G as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, S as defaultDesktopAuthCandidates, Z as findWorkbuddyAppExecutableWithSource, a as readHostHeartbeat, at as hostPeersInfo, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-BwbDWvzw.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
|
@@ -3800,7 +3800,7 @@ function generationOf(version) {
|
|
|
3800
3800
|
//#endregion
|
|
3801
3801
|
//#region src/version.ts
|
|
3802
3802
|
/** The npm package version this build was produced from. */
|
|
3803
|
-
const WORKBUDDY_CONNECT_VERSION = "4.4.
|
|
3803
|
+
const WORKBUDDY_CONNECT_VERSION = "4.4.4";
|
|
3804
3804
|
//#endregion
|
|
3805
3805
|
//#region src/host-heartbeat.ts
|
|
3806
3806
|
/**
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as isWorkbuddyBundle, A as workbuddyAccountId, B as parseCreditMultiplier, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as WORKBUDDY_APP_EXECUTABLE_ENV, H as parseUpstreamModel, I as classifyUpstreamError, J as deriveAtRestKeyId, K as clearAtRestKeyCache, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, Q as isEncryptedFieldWrapper, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as prepareChatBody, V as parseReasoning, W as regionOf, X as findWorkbuddyAppExecutable, Y as fetchAtRestKeyPayload, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as writeFileAtomic, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as macosBundleExecutable, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as workbuddyAppExecutableCandidates, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, m as deriveCatalog, n as clearHostHeartbeat, nt as openEncryptedField, o as workbuddyHostHeartbeatPath, ot as resolveDshHome, p as applyContextBudgets, q as deriveAtRestKey, r as isHeartbeatProcessAlive, rt as readAtRestKey, s as writeHostHeartbeat, st as withFileLock, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosNestedAppBundles, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-
|
|
1
|
+
import { $ as isWorkbuddyBundle, A as workbuddyAccountId, B as parseCreditMultiplier, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as WORKBUDDY_APP_EXECUTABLE_ENV, H as parseUpstreamModel, I as classifyUpstreamError, J as deriveAtRestKeyId, K as clearAtRestKeyCache, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, Q as isEncryptedFieldWrapper, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as prepareChatBody, V as parseReasoning, W as regionOf, X as findWorkbuddyAppExecutable, Y as fetchAtRestKeyPayload, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as writeFileAtomic, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as macosBundleExecutable, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as workbuddyAppExecutableCandidates, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, m as deriveCatalog, n as clearHostHeartbeat, nt as openEncryptedField, o as workbuddyHostHeartbeatPath, ot as resolveDshHome, p as applyContextBudgets, q as deriveAtRestKey, r as isHeartbeatProcessAlive, rt as readAtRestKey, s as writeHostHeartbeat, st as withFileLock, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosNestedAppBundles, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-BwbDWvzw.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";
|
|
@@ -3383,11 +3383,51 @@ function knownProbeSource(value) {
|
|
|
3383
3383
|
return value === "test-batch" || value === "live-request" ? value : void 0;
|
|
3384
3384
|
}
|
|
3385
3385
|
/**
|
|
3386
|
+
* Merge ONE new measurement over the record already stored for that account.
|
|
3387
|
+
*
|
|
3388
|
+
* A plain `{ ...existing, ...update }` is wrong here, and the damage is silent.
|
|
3389
|
+
* The two writers describe the same account at different moments and carry
|
|
3390
|
+
* different fields:
|
|
3391
|
+
*
|
|
3392
|
+
* - `test-batch` measures the TARGET MODEL deliberately, so it is the only
|
|
3393
|
+
* writer that can produce a refresh point (`retryAtMs` with
|
|
3394
|
+
* `retrySource: 'quota-refresh'`) — a time the billing side states outright.
|
|
3395
|
+
* - `live-request` reports what a real chat request just hit. It often carries
|
|
3396
|
+
* no `retryAtMs` at all (a transport blip names no time), and a spread would
|
|
3397
|
+
* therefore DELETE the refresh point the probe had already resolved.
|
|
3398
|
+
*
|
|
3399
|
+
* Measured consequence of the spread, on a real `14018` account: the probe
|
|
3400
|
+
* recorded "quota refreshes 2026-11-01", the next live failure rewrote the
|
|
3401
|
+
* record without that field, and the pool then reported "retry in about 30
|
|
3402
|
+
* minutes" — putting an exhausted account back into rotation to be refused
|
|
3403
|
+
* again. The correct value was in the file one write earlier.
|
|
3404
|
+
*
|
|
3405
|
+
* So `retryAtMs` is INHERITED when the update does not state one, and every
|
|
3406
|
+
* other field is the update's (the newer measurement owns `outcome`, `atMs`,
|
|
3407
|
+
* `source` and `message`). Inheriting is only about the one field whose absence
|
|
3408
|
+
* is not a statement: an update that DOES carry a time still overrides, which is
|
|
3409
|
+
* what lets a fresh, shorter cooldown supersede a stale longer one.
|
|
3410
|
+
*
|
|
3411
|
+
* Pure and exported so the rule is unit-testable without a filesystem — this is
|
|
3412
|
+
* exactly the kind of "two writers, one slot" rule that this project has been
|
|
3413
|
+
* bitten by before.
|
|
3414
|
+
*/
|
|
3415
|
+
function mergePoolProbe(existing, update) {
|
|
3416
|
+
if (existing === void 0) return update;
|
|
3417
|
+
if (update.retryAtMs !== void 0) return update;
|
|
3418
|
+
if (existing.retryAtMs === void 0 || existing.retryAtMs <= update.atMs) return update;
|
|
3419
|
+
return {
|
|
3420
|
+
...update,
|
|
3421
|
+
retryAtMs: existing.retryAtMs
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3386
3425
|
* Merge measurements into one region's store and write it atomically.
|
|
3387
3426
|
*
|
|
3388
3427
|
* A MERGE rather than a replace, because batches measure one account at a time
|
|
3389
3428
|
* and a partial failure must not erase what earlier runs learned. An account
|
|
3390
|
-
* absent from `updates` keeps its previous measurement
|
|
3429
|
+
* absent from `updates` keeps its previous measurement, and an account present
|
|
3430
|
+
* in both is combined by {@link mergePoolProbe} rather than overwritten.
|
|
3391
3431
|
*
|
|
3392
3432
|
* Never throws: a failed write loses only the newest measurements, and the
|
|
3393
3433
|
* caller's results are still returned to the user. Surfacing a disk error here
|
|
@@ -3399,14 +3439,12 @@ async function writePoolProbes(region, updates) {
|
|
|
3399
3439
|
try {
|
|
3400
3440
|
await withFileLock(path, async () => {
|
|
3401
3441
|
const existing = await readPoolProbes(region);
|
|
3402
|
-
const
|
|
3442
|
+
const merged = { ...existing };
|
|
3443
|
+
for (const [accountId, update] of Object.entries(updates)) merged[accountId] = mergePoolProbe(existing[accountId], update);
|
|
3444
|
+
await writeFileAtomic(path, `${JSON.stringify({
|
|
3403
3445
|
version: STORE_FORMAT_VERSION,
|
|
3404
|
-
probes:
|
|
3405
|
-
|
|
3406
|
-
...updates
|
|
3407
|
-
}
|
|
3408
|
-
};
|
|
3409
|
-
await writeFileAtomic(path, `${JSON.stringify(document, null, 2)}\n`, {
|
|
3446
|
+
probes: merged
|
|
3447
|
+
}, null, 2)}\n`, {
|
|
3410
3448
|
mode: 384,
|
|
3411
3449
|
dirMode: 448
|
|
3412
3450
|
});
|
|
@@ -5420,6 +5458,21 @@ function apply(ctx, config) {
|
|
|
5420
5458
|
* 「将在 … 重置」 — which `cooldownOf` parses. When it states no time, the store's
|
|
5421
5459
|
* own short window applies rather than a guess at a long one.
|
|
5422
5460
|
*
|
|
5461
|
+
* THE QUOTA REFRESH POINT IS PASSED IN, and it is not decoration. This path
|
|
5462
|
+
* used to omit `quotaRefreshAtMs` while the PROBE path supplied it, so one
|
|
5463
|
+
* `out-of-credit` failure got two different answers depending on which door it
|
|
5464
|
+
* arrived through: the probe said "the quota refreshes on the 1st" (a time the
|
|
5465
|
+
* billing side states outright) while a live chat failure said "30 minutes
|
|
5466
|
+
* from now" — a window the account cannot possibly clear, so the pool put it
|
|
5467
|
+
* back and paid another refused round trip. Worse, the write below combines
|
|
5468
|
+
* with the stored record, and the stored refresh point was being LOST in the
|
|
5469
|
+
* process (see {@link mergePoolProbe}).
|
|
5470
|
+
*
|
|
5471
|
+
* Reading it costs one `fetchCredits`, affordable here but not on every
|
|
5472
|
+
* failure: it runs only for `out-of-credit`, where the refresh point is the one
|
|
5473
|
+
* honest answer and there is no cheaper source. Failing to read it is not an
|
|
5474
|
+
* error — the fallback still applies.
|
|
5475
|
+
*
|
|
5423
5476
|
* The upstream's text is persisted alongside the outcome so the card can name
|
|
5424
5477
|
* the reason. It is REDACTED first because this message is a raw upstream body:
|
|
5425
5478
|
* without that, a failure containing a token-shaped string would write it into
|
|
@@ -5431,11 +5484,13 @@ function apply(ctx, config) {
|
|
|
5431
5484
|
const members = poolPreferencesOf(current(), region).memberAccountIds;
|
|
5432
5485
|
if (members.length > 0 && !members.includes(accountId)) return;
|
|
5433
5486
|
const outcome = outcomeOfFailure(failure.status, failure.message);
|
|
5487
|
+
const quotaRefreshAtMs = outcome === "out-of-credit" ? await stacks[region].store.credentialFor(accountId).then((credential) => credential === void 0 ? void 0 : quotaRefreshOf(region, credential)).catch(() => void 0) : void 0;
|
|
5434
5488
|
const { retryAtMs } = cooldownOf({
|
|
5435
5489
|
outcome,
|
|
5436
5490
|
retryAfter: null,
|
|
5437
5491
|
nowMs: Date.now(),
|
|
5438
|
-
body: failure.message
|
|
5492
|
+
body: failure.message,
|
|
5493
|
+
...quotaRefreshAtMs === void 0 ? {} : { quotaRefreshAtMs }
|
|
5439
5494
|
});
|
|
5440
5495
|
const message = failure.message === "" ? "" : redactUpstreamText(failure.message);
|
|
5441
5496
|
await writePoolProbes(region, { [accountId]: {
|
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": "4.4.
|
|
5
|
+
"version": "4.4.4",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|