dsh-connect-workbuddy 5.6.1 → 5.6.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 +37 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +3 -1
- package/lib/{host-heartbeat-CEYnXXF3.js → host-heartbeat-r_icwyw3.js} +2 -2
- package/lib/index.d.ts +30 -5
- package/lib/index.js +14 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.6.3 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
> 补丁版本:修「免费模型测出来可用,却显示 8 天后才能用」。**请求路径、provider 路由、计费与凭据一律未变。**
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **成功的探测被当成「要等到下个月」,免费模型显示「约 8 天后」**
|
|
10
|
+
- **症状**(用户报告 + 真机记录):倍率 `x0.00` 的免费模型,测试结果是 `✓ 可用`,同一行却写着 `11/01, 00:00 之后可再用 · 约 8 天后`,读起来就是「这个免费模型要等 8 天」。
|
|
11
|
+
- **根因**:`cooldownOf()` 把**任何**存在的 `Retry-After` 头都当成「你在此之前不能用」,不区分响应是 200 还是 429。而这个网关在**成功的 200** 上也会带 `Retry-After`,其值是**账号月度积分包的刷新时刻**(本机 `.workbuddy-pool.global.json` 里三条记录逐字为 `outcome:"ok"` + `retryAtMs:2026-10-31T16:00:00Z` + `message:"credit 0"`)。于是「额度什么时候补」被渲染成了「模型什么时候解禁」。
|
|
12
|
+
- **修法**:把「成功没有冷却」这条规则**放在 `cooldownOf()` 里**(`outcome === 'ok'` 直接返回 `{}`),而不是只在调用点去掉。理由是这类头在成功响应上确实存在,任何未来的调用方都可能再把头透传进来;规则只有一处,就无法被绕过。`probeModel()` 的成功分支同步不再请求冷却,注释写明为什么。
|
|
13
|
+
- **旧记录一起治**:已经落盘的 `ok` + `retryAtMs` 记录,由 `retryAtMsOf()` 在**读取时**丢弃那个时间(这是所有消费者的唯一入口:卡片、输入框面板、换号路由),下一次 `writePoolProbes` 把修好的形状写回文件。`mergePoolProbe()` 同步拒绝把旧记录的 `retryAtMs` 继承到新的 `ok` 测量上——否则每次重测都会把那个八天重新捡回来。**测量本身不丢**:`modelId`、`atMs`、`source` 都保留,只是不再附一个不存在的等待。
|
|
14
|
+
- **文案层也设了一道闸**:`availabilityLine()` / `availabilityMark()`(卡片与输入框面板共用的那一条规则)遇到 `outcome: 'ok'` 一律只输出判定,不再拼「之后可再用」。这样即使将来又有写入方带进一个时间,用户看到的仍然只是 `✓ 可用`。
|
|
15
|
+
- **没有动的地方**:真实限流(`rate-limited`)、模型繁忙、积分耗尽三条路径的恢复时间**完全不变**——它们的时间是上游明确说的「什么时候能再用」,是有用信息。本次只摘掉「成功」上的时间。
|
|
16
|
+
- **测试**:`tests/probe.spec.ts` 增 2 条(`cooldownOf` 对 `ok` 忽略 `Retry-After` 与 quota-refresh;`probeModel` 端到端在 200 带 `Retry-After` 时不产出 `retryAtMs`);`tests/account-pool-store.spec.ts` 增 3 条(旧 `ok`+时间被读时治愈、新 `ok` 不继承旧时间、仍限流的账号照旧继承);`tests/account-state.spec.ts` 增 3 条(卡片与面板都只印判定、限流行仍印时间)。
|
|
17
|
+
|
|
18
|
+
## 5.6.2 (2026-10-23)
|
|
19
|
+
|
|
20
|
+
> 补丁版本:修 5.6.0/5.6.1 里「折后价读不出来」的原因。**请求路径、provider 路由、计费与凭据一律未变。**
|
|
21
|
+
|
|
22
|
+
### Fixes
|
|
23
|
+
|
|
24
|
+
- **真实促销的 `displayMode` 是 `strikethrough`,只认 `replace` 等于把折扣全丢了(#120,第二次报告)**
|
|
25
|
+
- **症状**:徽标亮着,价格不动。模型行显示 `DeepSeek-V4.1-Flash · x0.11` 配「夜间折扣」。
|
|
26
|
+
- **查证方式**:直接把国内区 `/v3/config` 的 8 条 `modelPromotions` 原文 dump 出来逐条比对,而不是继续照着此前文档里的样例推断(样例只出现过 `replace`)。
|
|
27
|
+
- **真实数据**(2026-10-23 实测,8 条):
|
|
28
|
+
| id | 标签 | `discount` | `displayMode` |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `glm-52-night-discount-202607` | 夜间折扣 | `"0.50x"` | **`strikethrough`** |
|
|
31
|
+
| `hy4-night-discount-daytime-badge-202609` | 夜间免费 | `"0.00x"` | **`strikethrough`** |
|
|
32
|
+
| `step-5-discount-daytime-badge-202610` | 独家限免 | `"0.00x"` | **`strikethrough`** |
|
|
33
|
+
| `hy3-free-trial-202608` | 限时免费 | `"0x"` | `replace` |
|
|
34
|
+
| `glm-52-…-daytime-badge`、`hy4-…-badge`、`ds-discount-daytime-badge-202608`、`space-bunny-discount-202610` | — | 无 | — |
|
|
35
|
+
- **根因**:原实现把 `displayMode` 硬写成 `'replace'`(唯一的样例来源),解析时把 `strikethrough` 当成未知值丢弃,判定时也只放行 `replace`。于是**八条里三条带价格的折扣一条都不生效**,只有 hy3 的 `replace` 能过。
|
|
36
|
+
- **修法**:两种观测到的拼写都认。语义上它们都是「折后价才是实付价」,区别只在客户端是否同时把原价划掉——本插件每行只画一个数字,所以两者等价处理。**仍不认没标 `displayMode` 的 `discount`**:字段缺失不说明该怎么读那个数,猜成 `replace` 就是凭猜改价。
|
|
37
|
+
- **顺带确认不是 bug 的一条**:`ds-discount-daytime-badge-202608`(`deepseek-v4.1-flash` / `deepseek-v4-pro`)的 `discount` 就是 **`null`**,时段 `0:00→23:59` 几乎全天。所以这两个模型「徽标亮着但价格是目录价」是**上游没给折后价**,不是插件漏读。这条单独写了测试钉住:没有数字就**不能**编一个。
|
|
38
|
+
- **测试**:`tests/issue120-promo-discount.spec.ts` 增至 14 条,新增四条用的是**真实原文**(不是样例形状)——`strikethrough` 生效(`0.50x` → 0.50)、`0.00x` 免费算作 0 而不是「没有」、**only-badge 条目(deepseek 那条)徽标有、价格不动**、仍拒绝没有 `displayMode` 的折扣。原「未知 displayMode」那条改成用一个真正不存在的值,避免把已观测值钉成未知。
|
|
39
|
+
|
|
3
40
|
## 5.6.1 (2026-10-23)
|
|
4
41
|
|
|
5
42
|
> 补丁版本:修 5.6.0 自己引入的一处显示回退。**请求路径、provider 路由、计费与凭据一律未变。**
|
package/README.en.md
CHANGED
|
@@ -39,7 +39,7 @@ It depends on the WorkBuddy client's own endpoints (not a public API), so the pl
|
|
|
39
39
|
- **Model load on model rows (CN only)** — the upstream answers "how busy is this model right now" from a separate read-only probe (`POST /v2/chat/busy`, up to 50 ids per call, free, no account state touched). The plugin asks it once per catalog refresh and draws the level as a small figure beside the model name: **the number of people = how many are queueing** (healthy `normal` = one yellow figure, `busy` = two orange ones, `congested` = three red ones), while **idle is a green rocket** (nobody is queueing = full speed ahead) — idle is deliberately *not* drawn as "one green person", because at 12px "nobody is waiting" and "one person is waiting" have to be distinguishable at a glance; **unavailable** is a crossed-out figure. The probe's own `unknown` **draws nothing**: **"we were told nothing" is never reported as "all good"**. Colours follow the theme and the `title` tooltip ("极度拥堵") is still there. **The level is not persisted**: the catalog is a fact that survives a restart, a load level is a fact about that minute, so it is stamped onto the runtime catalog only and dies with the process; a failed probe logs one warning and never blocks the refresh. Measured on the CN gateway: the probe answers byte-identically under the CLI and the app identity, the 50-id ceiling is real (62 ids → HTTP 400), and 19 models come back in 186 ms. **The international region shows no load badge, because the international gateway simply does not have this endpoint** — measured, `www.workbuddy.ai` answers **404** for `/v2/chat/busy` (and for the neighbouring `/v3/chat/busy`, `/v2/models/busy` and friends), while the very same credential reads its `/v3/config` fine (28 models) and that document carries **no load or queue field at all**. This is not something the plugin left unimplemented, and it is not a configuration or permission problem: **upstream has never published this data for the international region**. So the international region shows the model name and campaign badges only; the failed probe degrades quietly to "no badge", never blocking the model list, and the plugin never guesses a load level from latency or credits.
|
|
40
40
|
- **Queueing tells you your position** — upstream expresses a queue with three business codes (`6020` waiting, `6021` queue full, `6022` this user already at the maximum). Instead of passing along a raw upstream error, the plugin says "模型排队中:你排在第 8777 位,队列共 9021 个请求;约 7 秒后自动重试". **A queue does not trigger failover**: the line is shared platform-wide and bound to the model, so another account is handed the same position — no account-pool rotation, no extra requests burned. (The upstream's `/v2/chat/queue/status` returns an i18n KEY rather than a sentence, so the plugin never relays its `message` and always uses its own wording.) **The "resend once it is your turn" half is not implemented** — that needs the queue answer's real envelope on the OpenAI-compatible channel, and writing it unmeasured would mean code that never runs.
|
|
41
41
|
- **Tabbed settings card (accounts / models / capabilities / diagnostics)** — the card used to be a single 3,500-pixel column that mixed two things with completely different commit semantics: **operating accounts** (who is serving, how much is left, switch order, check-in log) and **editing the catalog** (ticking models, context budgets, reasoning tiers, turning thinking off). An account change takes effect on the click; a catalog change is a draft that needs Save — so the Save button often sat three screens away from the thing it saves. Now each tab does one thing, and **switching tabs neither refetches nor drops an unsaved draft** (inactive panels stay mounted and are merely `hidden`, so they also leave the accessibility tree). The third tab, Capabilities, holds the **grants**: let the plugin drive the local CodeBuddy CLI, let the model buy an image on its own, where the credit readout is drawn. Each takes effect on the click and never passes through Save, and each is expected to grow sub-options of its own (a working directory for the CLI route, a size or price cap for the image tool) — which is why they no longer share one thin strip above the account table. The fourth tab, Diagnostics, is a read-only fact table: current region, upstream host, the account actually serving (by NAME, never by id), sign-in validity, how many models the catalog holds, how many are enabled, the pool switch, and the **raw text** of the upstream's most recent error (quoted, never translated or summarised) — its only reason to exist is to be pasted into a bug report, so it contains no control that changes a setting.
|
|
42
|
-
- **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.
|
|
42
|
+
- **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. **A success never has a cooldown**: this gateway also attaches `Retry-After` to a 200, and its value is the account's monthly quota reset (measured: `2026-11-01 00:00 +08:00`) — not a wait. So a successful row shows “✓ Usable” and never appends “usable again after”, and records written by older builds with that stray time are healed on read. Probes run in the Host, so **the credential never reaches the page**, and results are not written to settings — your saved selection is untouched.
|
|
43
43
|
- **Image generation tool `workbuddy_draw_image` (CN only, OFF by default)** — hands WorkBuddy's text-to-image capability to the agent: say "draw me a cat sunning itself on a windowsill" in the conversation and the model calls the tool itself, then posts the image back. **It is off by default**, because it is the one capability here where the MODEL decides to spend: about 5.71 credits an image, with no per-call confirmation available in the host. Tick 「Let the model draw images」 under the card's Accounts tab to enable it; while it is off the tool is **not registered at all**, so the model never sees it and cannot attempt a call that would fail. **It is CN-only** — the international catalog lists no image model at all (not unimplemented; upstream has none). **Endpoint and request body are copied from the official CLI's ImageService** (`POST /v2/images/generations`, model `hunyuan-image-alpha`), and the product is a 24-hour COS signed URL the plugin tells you to download in time. **It really costs money**: about **5.71 credits per image** measured, so the tool description states the price outright, a failure carries a 30-second cooldown (while a bad parameter, a missing sign-in or an unknown model costs nothing and does not cool down), and there is no "draw four at once" parameter that could spend uncontrollably. The definition is hand-written raw JSON Schema, **adding no dependency**. See [docs/IMAGE_GEN.md](docs/IMAGE_GEN.md) for the evidence, protocol and live run.
|
|
44
44
|
|
|
45
45
|
**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 each model's **native** window to DSH (no longer clamped to 200K as of 3.5.0), so DSH compacts very late — hitting the throttle in a long session follows directly from that. To make DSH compact sooner, set a “DSH context budget” cap on the model's row (200K / 500K / the model's native window).
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
- **模型行上的活动徽标** —— 上游的模型目录里带着活动信息(免费试用、夜间折扣、限时免费、订阅优先等),插件**不新增任何请求**(复用刷新模型时本来就会拉的那份目录配置),直接在模型行上画一枚胶囊徽标:颜色与文案都由上游给出,悬停能看到上游的活动说明、活动的起止日期与每日生效时段(例如「夜间 23:00–7:50」)。**徽标是「此刻」的函数**:活动只在它自己声明的时间窗内显示,窗口一关徽标就自己消失;跨零点的时段按上游声明的时区判断;同一个模型命中多条活动时只画优先级最高的那条。上游没给结束时间的权益标记(如「订阅优先」)则常驻,直到上游把它下架。**徽标只是提示,不改变模型行为**——它标注的是上游此刻给出的活动与折扣,不替你做选择。**同一枚徽标也出现在 DSH 的模型选择器里**:菜单每一行都在模型名之后、队列图标之前画同样的胶囊 —— 菜单行用上游的短词(「夜间」「限免」),输入框旁那个触发器用完整词(「夜间折扣」「独家限免」);判定用的是同一份数据、同一个时钟,因为选模型的那一屏正好是最该看到「现在免费吗」的地方。(两个区域的上游目录都下发活动字段:国内版按 App 身份读 `/v3/config` 时带 8 条促销 + 1 条等级标记,国际版带 5 条 + 1 条;取证与判定规则见 [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md)。)
|
|
47
47
|
- **模型行上的繁忙程度(仅国内版)** —— 上游另有一条只读探针(`POST /v2/chat/busy`,一次最多 50 个 id,免费、不动账号状态)回答「这个模型此刻多忙」,插件在每次刷新目录时顺带问一次,把等级画成模型名旁边的一枚小人图标:排队人数 = 小人个数(正常 1 个黄人 / 轻微拥堵 2 个橙人 / 极度拥堵 3 个红人),**空闲**则是一枚**绿色小火箭**(没人排队 = 可以全速跑)——空闲不画成「绿色的 1 个人」,因为 12px 下「没人等」与「有 1 个人在等」必须一眼分开;**不可用**是一个划掉的小人。探针没回答的 `unknown` **一律不画**,**「没被告知」不会被当成「一切正常」**;颜色跟随主题,`title` 悬停文字(「极度拥堵」四个字)仍在。**等级不落盘**:目录是能活过重启的事实,负载只是那一分钟的事实,所以等级只盖在运行时目录上、随进程消失,探针失败只记一条 warn,绝不挡住目录刷新。国内版实测这条探针在 CLI 与 App 两种身份下答案逐字节相同,50 个 id 的上限也真实存在(62 个 → HTTP 400),19 个模型 186 ms 返回。**国际版没有这个徽标,因为国际网关根本没有这条接口** —— 实测 `www.workbuddy.ai` 对 `/v2/chat/busy` 回 **404**(`/v3/chat/busy`、`/v2/models/busy` 等同族路径也全是 404),而同一份凭据读它的 `/v3/config` 正常返回 28 个模型、**没有任何负载/排队字段**。这不是本插件没实现,也不是配置或权限问题:**上游国际版从未提供这个数据**。所以国际版只显示模型名与活动徽标,探针那条请求失败后安静退化成「没有徽标」,不会挡住模型列表,也不会用延迟或积分去猜一个「忙不忙」。
|
|
48
48
|
- **排队时说清楚你排在第几位** —— 上游用三个业务码表达排队(`6020` 等待、`6021` 队列满、`6022` 已达上限)。撞上它们时,插件不再给一句上游报错,而是「模型排队中:你排在第 8777 位,队列共 9021 个请求。请稍后重新发送这条消息,或换一个模型」。**排队不会触发换号**:队列跟随的是模型与平台级的队伍,换个账号拿到的还是同一个位次,所以这里既不走账号池也不额外烧请求(上游 `/v2/chat/queue/status` 回的 `message` 是 i18n key 而不是人话,插件因此从不转述它,只用自己的措辞)。**排队不会自动重发,也不会被自动重试**——插件没有排队轮询循环(它需要排队答案在 OpenAI 兼容通道上的真实信封,没测到就不写),并且明确告诉宿主不要重试(`x-should-retry: false`):一次排队在几千位之后,重试十几次也轮不到,只会把真实请求白白花掉。位次报出来,由你决定是重发还是换模型。
|
|
49
|
-
- **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,直接告诉你它此刻能否使用、不能用时何时恢复。**它发的是一条真实体积的请求(约 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
|
|
49
|
+
- **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,直接告诉你它此刻能否使用、不能用时何时恢复。**它发的是一条真实体积的请求(约 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 之后可再用」。三处来源都没说时,插件**不编造倒计时**,如实显示「上游未给出何时恢复」。限流**不会**去借月周期包的刷新时间(那会让人为几秒的限流去等几周)。**成功即无冷却**:这个网关在 200 响应上也会带 `Retry-After`,值是账号月度积分包的刷新时刻(实测 `2026-11-01 00:00 +08:00`),它不是「你在此之前不能用」——所以成功的行只显示「✓ 可用」,绝不附「之后可再用」;旧版本已经写进文件的那类记录会在读取时自动治愈。探测在后台发起,**凭据不会传到页面**;结果不写入设置,也不改变你已保存的勾选。
|
|
50
50
|
|
|
51
51
|
**两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件按模型的**原生窗口**向 DSH 宣称(3.5.0 起不再默认压到 200K),所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。想让 DSH 更早压缩,就在模型行上手动设一个「DSH 上下文预算」上限(可选 200K / 500K / 模型原生窗口)。
|
|
52
52
|
- **生图工具 `workbuddy_draw_image`(国内版,默认关闭)** —— 把 WorkBuddy 的文生图能力交给 agent:你在对话里说「帮我画一只在窗台上晒太阳的猫」,模型自己就会调用这个工具,画完把图贴回来。**默认不开启**,因为这是本插件里唯一一项「由模型自己决定花钱」的能力:约 5.71 积分一张,而宿主没有「调用前先确认」的机制。想用就去卡片「账号」栏勾上「允许模型自主生图」;不勾的时候这个工具**根本不会注册**,模型看不到它,也就不会发起注定失败的调用。**它只在国内版可用**——国际版的模型目录里一条生图模型都没有(不是没做,是上游没有)。**端点与请求体照抄官方 CLI 的 ImageService**(`POST /v2/images/generations`,`hunyuan-image-alpha` 模型),产物是 24 小时有效的 COS 签名 URL,插件会提醒你及时下载。**它真的花钱**:实测约 **5.71 积分/张**,所以工具描述里直接写了单价,失败后有 30 秒冷却(但参数写错、没登录、模型不存在这三类一分钱没花,不进冷却),也不提供「一次画四张」这类不可控支出的参数。工具定义是手写的 raw JSON Schema,**没有引入任何新依赖**。取证、协议与联调记录见 [docs/IMAGE_GEN.md](docs/IMAGE_GEN.md)。
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthCandidates, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, Mt as hostPeersInfo, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, vt as WORKBUDDY_APP_EXECUTABLE_ENV, w as defaultDesktopAuthDirs, wt as findWorkbuddyAppExecutableWithSource, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-
|
|
2
|
+
import { C as defaultDesktopAuthCandidates, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, Mt as hostPeersInfo, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, vt as WORKBUDDY_APP_EXECUTABLE_ENV, w as defaultDesktopAuthDirs, wt as findWorkbuddyAppExecutableWithSource, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-r_icwyw3.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
|
@@ -1375,6 +1375,7 @@ window.__ModuleLoader__.load({
|
|
|
1375
1375
|
function availabilityLine(t, account, nowMs = Date.now()) {
|
|
1376
1376
|
const probe = account.probe;
|
|
1377
1377
|
if (probe !== void 0) {
|
|
1378
|
+
if (probe.outcome === "ok") return outcomeText(t, probe.outcome);
|
|
1378
1379
|
if (probe.retryAtMs !== void 0) {
|
|
1379
1380
|
const left = remainingText(t, probe.retryAtMs, nowMs);
|
|
1380
1381
|
const at = t("row.poolRetryAt", { at: formatShortClock(probe.retryAtMs) });
|
|
@@ -1403,6 +1404,7 @@ window.__ModuleLoader__.load({
|
|
|
1403
1404
|
function availabilityMark(t, account, nowMs = Date.now()) {
|
|
1404
1405
|
const probe = account.probe;
|
|
1405
1406
|
if (probe !== void 0) {
|
|
1407
|
+
if (probe.outcome === "ok") return outcomeMarkText(t, probe.outcome);
|
|
1406
1408
|
if (probe.retryAtMs !== void 0) {
|
|
1407
1409
|
const left = recoversInText(t, probe.retryAtMs, nowMs);
|
|
1408
1410
|
return left === "" ? outcomeMarkText(t, probe.outcome) : `${outcomeMarkText(t, probe.outcome)} · ${left}`;
|
|
@@ -2083,7 +2085,7 @@ window.__ModuleLoader__.load({
|
|
|
2083
2085
|
for (const promo of sorted) {
|
|
2084
2086
|
const discount = promo.discount;
|
|
2085
2087
|
if (discount === void 0 || discount.multiplier === void 0) continue;
|
|
2086
|
-
if (discount.displayMode !== "replace") continue;
|
|
2088
|
+
if (discount.displayMode !== "replace" && discount.displayMode !== "strikethrough") continue;
|
|
2087
2089
|
if (!promoActiveAt(promo, nowMs)) continue;
|
|
2088
2090
|
return discount.multiplier;
|
|
2089
2091
|
}
|
|
@@ -1570,7 +1570,7 @@ function parseDiscount(value) {
|
|
|
1570
1570
|
const fromString = multiplierOfCredits(raw["discountedCredits"]);
|
|
1571
1571
|
const fromFactor = typeof raw["factor"] === "number" && Number.isFinite(raw["factor"]) && raw["factor"] >= 0 ? raw["factor"] : void 0;
|
|
1572
1572
|
const multiplier = fromString ?? fromFactor;
|
|
1573
|
-
const displayMode = raw["displayMode"] === "replace" ? "
|
|
1573
|
+
const displayMode = raw["displayMode"] === "replace" || raw["displayMode"] === "strikethrough" ? raw["displayMode"] : void 0;
|
|
1574
1574
|
if (multiplier === void 0 && displayMode === void 0) return void 0;
|
|
1575
1575
|
return {
|
|
1576
1576
|
...multiplier === void 0 ? {} : { multiplier },
|
|
@@ -6121,7 +6121,7 @@ function generationOf(version) {
|
|
|
6121
6121
|
//#endregion
|
|
6122
6122
|
//#region src/version.ts
|
|
6123
6123
|
/** The npm package version this build was produced from. */
|
|
6124
|
-
const WORKBUDDY_CONNECT_VERSION = "5.6.
|
|
6124
|
+
const WORKBUDDY_CONNECT_VERSION = "5.6.3";
|
|
6125
6125
|
//#endregion
|
|
6126
6126
|
//#region src/host-heartbeat.ts
|
|
6127
6127
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -792,11 +792,30 @@ interface WorkBuddyPromoDiscount {
|
|
|
792
792
|
*/
|
|
793
793
|
multiplier?: number;
|
|
794
794
|
/**
|
|
795
|
-
* The upstream's display instruction
|
|
796
|
-
* the
|
|
797
|
-
*
|
|
798
|
-
|
|
799
|
-
|
|
795
|
+
* The upstream's display instruction — how the discounted figure relates to
|
|
796
|
+
* the catalog one. BOTH spellings below are observed on the live CN feed
|
|
797
|
+
* (2026-10-23, eight entries dumped from `/v3/config`):
|
|
798
|
+
*
|
|
799
|
+
* `'replace'` — show the discounted figure INSTEAD of the normal one.
|
|
800
|
+
* Used by `hy3-free-trial-202608` (`"0x"`).
|
|
801
|
+
* `'strikethrough'` — the upstream's own client strike out the normal price
|
|
802
|
+
* and print the discounted one. Used by
|
|
803
|
+
* `glm-52-night-discount-202607` (`"0.50x"`),
|
|
804
|
+
* `hy4-night-discount-daytime-badge-202609` (`"0.00x"`)
|
|
805
|
+
* and `step-5-discount-daytime-badge-202610`.
|
|
806
|
+
*
|
|
807
|
+
* An earlier version accepted only `'replace'`, which silently dropped every
|
|
808
|
+
* real CN discount except the hy3 trial — the price never moved while the
|
|
809
|
+
* badge was lit (issue #120, second report). Both modes mean "the discounted
|
|
810
|
+
* figure is what you pay"; they differ only in whether the old figure is also
|
|
811
|
+
* shown on screen, which this plugin does not do either way (one number per
|
|
812
|
+
* row), so both are honoured identically here.
|
|
813
|
+
*
|
|
814
|
+
* `undefined` means the entry carries a discount the upstream did not label.
|
|
815
|
+
* That is NOT acted on: the field's absence says nothing about how to read
|
|
816
|
+
* the figure, and guessing "replace" would change a price on a guess.
|
|
817
|
+
*/
|
|
818
|
+
displayMode?: 'replace' | 'strikethrough';
|
|
800
819
|
}
|
|
801
820
|
/** The upstream's explanatory copy for the badge. */
|
|
802
821
|
interface WorkBuddyPromoHover {
|
|
@@ -2630,6 +2649,12 @@ declare function parseRetryAfter(value: string | null, nowMs: number): number |
|
|
|
2630
2649
|
* as "the upstream did not say when". That remains the honest answer for a
|
|
2631
2650
|
* genuinely timeless failure; inventing a number here would be pure fiction.
|
|
2632
2651
|
*
|
|
2652
|
+
* An `ok` outcome ALWAYS returns `{}`, whatever headers or body came with it.
|
|
2653
|
+
* A successful probe has already answered "can this account serve right now?"
|
|
2654
|
+
* with yes, so there is no wait to state; and on this gateway a 200 can carry a
|
|
2655
|
+
* `Retry-After` whose value is the account's monthly quota reset, which must
|
|
2656
|
+
* never be rendered as a lockout. See the guard at the top of the function.
|
|
2657
|
+
*
|
|
2633
2658
|
* `model-busy` is the ONE exception, and it is not an invented countdown: the
|
|
2634
2659
|
* upstream states a magnitude in prose ("about a minute") rather than a
|
|
2635
2660
|
* timestamp, and the card's own vocabulary has no way to render "about". A
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as prepareChatBody, A as parseWorkBuddyAuth, At as readAtRestKey, B as declaredTools, C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutable, D as isEncryptedCredentialError, Dt as macosBundleExecutable, E as hasEncryptedCredentialFields, Et as isWorkbuddyBundle, F as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, Ft as writeFileAtomic, G as isFirstMessageGateBody, H as isContextTooLongBody, I as WorkBuddyCredentialRejectedError, J as openingEvidenceOfBody, K as knownRegionOf, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, N as CONTEXT_OVERFLOW_MARKER, Nt as resolveDshHome, O as isVaultAccountId, Ot as macosNestedAppBundles, P as CREDENTIAL_REJECTED_CODE, Pt as withFileLock, Q as perturbChannelFingerprints, R as channelGateMemberOf, S as authFileName, St as fetchAtRestKeyPayload, T as defaultDesktopAuthPath, Tt as isEncryptedFieldWrapper, U as isConversationIdGateBody, V as forceNeutralHead, W as isCredentialRejectedError, X as parseReasoning, Y as parseCreditMultiplier, Z as parseUpstreamModel, _ as ENCRYPTED_CREDENTIAL_CODE, _t as nativeModalityOf, a as readHostHeartbeat, at as lastRefusalFor, b as WorkBuddyCredentialStore, bt as deriveAtRestKey, c as WORKBUDDY_CONNECT_VERSION, ct as recordRefusal, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as WorkBuddyImageError, et as regionOf, f as WorkBuddyCatalog, ft as pickImageModel, g as withoutRouters, gt as imageDefaultFor, h as fallbackModelsFor, ht as NATIVE_MODALITY_BY_MODEL_ID, i as processStartTimeMs, it as defaultOffModelIds, j as workbuddyAccountId, jt as workbuddyAppExecutableCandidates, k as legacyWorkbuddyOwnAuthPath, kt as openEncryptedField, l as piAiRuntimeInfo, lt as withEffectiveOff, m as deriveCatalog, mt as queueNoticeText, n as clearHostHeartbeat, nt as attachBusyLevels, o as workbuddyHostHeartbeatPath, ot as offDefaultFor, p as applyContextBudgets, pt as parseQueueAnswer, q as modelIdOfBody, r as isHeartbeatProcessAlive, rt as REFUSES_OFF_MODEL_IDS, s as writeHostHeartbeat, st as offRefused, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as requestEvidenceOf, u as FALLBACK_WORKBUDDY_MODELS, ut as DEFAULT_IMAGE_SIZE, v as WORKBUDDY_AUTH_FILENAME, vt as WORKBUDDY_APP_EXECUTABLE_ENV, w as defaultDesktopAuthDirs, x as WorkBuddyEncryptedCredentialError, xt as deriveAtRestKeyId, y as WORKBUDDY_AUTH_FILE_ENV, yt as clearAtRestKeyCache, z as classifyUpstreamError } from "./host-heartbeat-
|
|
1
|
+
import { $ as prepareChatBody, A as parseWorkBuddyAuth, At as readAtRestKey, B as declaredTools, C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutable, D as isEncryptedCredentialError, Dt as macosBundleExecutable, E as hasEncryptedCredentialFields, Et as isWorkbuddyBundle, F as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, Ft as writeFileAtomic, G as isFirstMessageGateBody, H as isContextTooLongBody, I as WorkBuddyCredentialRejectedError, J as openingEvidenceOfBody, K as knownRegionOf, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, N as CONTEXT_OVERFLOW_MARKER, Nt as resolveDshHome, O as isVaultAccountId, Ot as macosNestedAppBundles, P as CREDENTIAL_REJECTED_CODE, Pt as withFileLock, Q as perturbChannelFingerprints, R as channelGateMemberOf, S as authFileName, St as fetchAtRestKeyPayload, T as defaultDesktopAuthPath, Tt as isEncryptedFieldWrapper, U as isConversationIdGateBody, V as forceNeutralHead, W as isCredentialRejectedError, X as parseReasoning, Y as parseCreditMultiplier, Z as parseUpstreamModel, _ as ENCRYPTED_CREDENTIAL_CODE, _t as nativeModalityOf, a as readHostHeartbeat, at as lastRefusalFor, b as WorkBuddyCredentialStore, bt as deriveAtRestKey, c as WORKBUDDY_CONNECT_VERSION, ct as recordRefusal, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as WorkBuddyImageError, et as regionOf, f as WorkBuddyCatalog, ft as pickImageModel, g as withoutRouters, gt as imageDefaultFor, h as fallbackModelsFor, ht as NATIVE_MODALITY_BY_MODEL_ID, i as processStartTimeMs, it as defaultOffModelIds, j as workbuddyAccountId, jt as workbuddyAppExecutableCandidates, k as legacyWorkbuddyOwnAuthPath, kt as openEncryptedField, l as piAiRuntimeInfo, lt as withEffectiveOff, m as deriveCatalog, mt as queueNoticeText, n as clearHostHeartbeat, nt as attachBusyLevels, o as workbuddyHostHeartbeatPath, ot as offDefaultFor, p as applyContextBudgets, pt as parseQueueAnswer, q as modelIdOfBody, r as isHeartbeatProcessAlive, rt as REFUSES_OFF_MODEL_IDS, s as writeHostHeartbeat, st as offRefused, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as requestEvidenceOf, u as FALLBACK_WORKBUDDY_MODELS, ut as DEFAULT_IMAGE_SIZE, v as WORKBUDDY_AUTH_FILENAME, vt as WORKBUDDY_APP_EXECUTABLE_ENV, w as defaultDesktopAuthDirs, x as WorkBuddyEncryptedCredentialError, xt as deriveAtRestKeyId, y as WORKBUDDY_AUTH_FILE_ENV, yt as clearAtRestKeyCache, z as classifyUpstreamError } from "./host-heartbeat-r_icwyw3.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -7008,6 +7008,12 @@ function parseUpstreamResetAt(message) {
|
|
|
7008
7008
|
* as "the upstream did not say when". That remains the honest answer for a
|
|
7009
7009
|
* genuinely timeless failure; inventing a number here would be pure fiction.
|
|
7010
7010
|
*
|
|
7011
|
+
* An `ok` outcome ALWAYS returns `{}`, whatever headers or body came with it.
|
|
7012
|
+
* A successful probe has already answered "can this account serve right now?"
|
|
7013
|
+
* with yes, so there is no wait to state; and on this gateway a 200 can carry a
|
|
7014
|
+
* `Retry-After` whose value is the account's monthly quota reset, which must
|
|
7015
|
+
* never be rendered as a lockout. See the guard at the top of the function.
|
|
7016
|
+
*
|
|
7011
7017
|
* `model-busy` is the ONE exception, and it is not an invented countdown: the
|
|
7012
7018
|
* upstream states a magnitude in prose ("about a minute") rather than a
|
|
7013
7019
|
* timestamp, and the card's own vocabulary has no way to render "about". A
|
|
@@ -7017,6 +7023,7 @@ function parseUpstreamResetAt(message) {
|
|
|
7017
7023
|
* an account.
|
|
7018
7024
|
*/
|
|
7019
7025
|
function cooldownOf(input) {
|
|
7026
|
+
if (input.outcome === "ok") return {};
|
|
7020
7027
|
const fromHeader = parseRetryAfter(input.retryAfter, input.nowMs);
|
|
7021
7028
|
if (fromHeader !== void 0) return {
|
|
7022
7029
|
retryAtMs: fromHeader,
|
|
@@ -7136,13 +7143,7 @@ async function probeModel(input) {
|
|
|
7136
7143
|
outcome: "ok",
|
|
7137
7144
|
elapsedMs: Date.now() - startedAt,
|
|
7138
7145
|
status: answer.status,
|
|
7139
|
-
...credit === void 0 ? {} : { message: `credit ${credit}` }
|
|
7140
|
-
...cooldownOf({
|
|
7141
|
-
outcome: "ok",
|
|
7142
|
-
retryAfter: answer.retryAfter,
|
|
7143
|
-
nowMs: input.nowMs,
|
|
7144
|
-
...input.quotaRefreshAtMs === void 0 ? {} : { quotaRefreshAtMs: input.quotaRefreshAtMs }
|
|
7145
|
-
})
|
|
7146
|
+
...credit === void 0 ? {} : { message: `credit ${credit}` }
|
|
7146
7147
|
};
|
|
7147
7148
|
}
|
|
7148
7149
|
const body = answer.body ?? "";
|
|
@@ -8357,6 +8358,7 @@ function workbuddyPoolStorePath(region) {
|
|
|
8357
8358
|
* would render a waiting time on an account that is not waiting for anything.
|
|
8358
8359
|
*/
|
|
8359
8360
|
function retryAtMsOf(probe) {
|
|
8361
|
+
if (probe.outcome === "ok") return void 0;
|
|
8360
8362
|
if (probe.retryAtMs !== void 0) return probe.retryAtMs;
|
|
8361
8363
|
if (probe.outcome !== "rate-limited" && probe.outcome !== "out-of-credit") return void 0;
|
|
8362
8364
|
if (probe.message === void 0) return void 0;
|
|
@@ -8455,6 +8457,10 @@ function mergePoolProbe(existing, update) {
|
|
|
8455
8457
|
...update,
|
|
8456
8458
|
accountName: existing.accountName
|
|
8457
8459
|
} : update;
|
|
8460
|
+
if (update.outcome === "ok") {
|
|
8461
|
+
const { retryAtMs: _dropped, ...rest } = merged;
|
|
8462
|
+
return rest;
|
|
8463
|
+
}
|
|
8458
8464
|
if (update.retryAtMs !== void 0) return merged;
|
|
8459
8465
|
if (existing.retryAtMs === void 0 || existing.retryAtMs <= update.atMs) return merged;
|
|
8460
8466
|
return {
|
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.6.
|
|
5
|
+
"version": "5.6.3",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|