dsh-connect-workbuddy 2.2.1 → 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 +96 -0
- package/README.en.md +3 -0
- package/README.md +3 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +274 -78
- package/lib/{host-heartbeat-Dy8fW3G5.js → host-heartbeat-CDZRWbFv.js} +49 -1
- package/lib/index.d.ts +273 -1
- package/lib/index.js +422 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,101 @@
|
|
|
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
|
+
|
|
55
|
+
## 2.3.0 (2026-09-29)
|
|
56
|
+
|
|
57
|
+
### Features
|
|
58
|
+
|
|
59
|
+
#### 模型「测试」:一键查出限量模型的冷却恢复时间
|
|
60
|
+
|
|
61
|
+
**新增模型可用性测试。** 卡片每个模型行新增一个**测试**按钮,模型区顶部新增**测试已勾选**(对 `enabledModelIds` 逐个测)。每个模型只发**一条最小请求**(`max_tokens: 1`),直接回答两件事:**这个模型现在能不能用**,以及**不能用时什么时候恢复**。
|
|
62
|
+
|
|
63
|
+
限量模型的冷却时间过去只能靠反复试错去猜——现在点一下就有结论。
|
|
64
|
+
|
|
65
|
+
**实测(本机真实账号,两个网关)**:CN 16 个与全球 23 个模型各抽样实打,全部 HTTP 200;单次耗时 0.8~3.4s。**成本可忽略**——响应体 `"credit"` 报 0~0.01,其中一次探测前后余额完全不变(933 → 933),而名册上正常一次回答是 0.79x 起。
|
|
66
|
+
|
|
67
|
+
**关于「多久之后可以再次使用」,有一条必须先说清的事实:上游不给这个数据。**
|
|
68
|
+
|
|
69
|
+
探测前先做了取证:200 响应头里**没有 `Retry-After`,也没有 `X-RateLimit-*`**;对同一模型**连续快速打 12 次全部成功**,未触发任何限流。因此本功能**不编造倒计时**,只在两种上游真的说了时间的情形下显示时间:
|
|
70
|
+
|
|
71
|
+
| 情形 | 卡片显示 | 时间来源 |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| 成功 | `✓ 可用(1.2s)` | — |
|
|
74
|
+
| 429 / 限流,上游未给时间 | `✗ 被限流 · 上游未给出何时恢复` | 无(如实说没有) |
|
|
75
|
+
| 429 / 限流,带 `Retry-After` | `✗ 被限流 · {时间} 之后可再用` | `Retry-After` 头(秒数或 HTTP-date 两种形态) |
|
|
76
|
+
| 积分耗尽,且该区域有月周期包 | `✗ 积分不足 · 积分包 {时间} 刷新` | 月周期包的 `refreshAtMs` |
|
|
77
|
+
| 积分耗尽,无月周期包 | `✗ 积分不足 · 上游未给出何时恢复` | 无 |
|
|
78
|
+
| 401/403 凭据被拒 | `✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号` | — |
|
|
79
|
+
|
|
80
|
+
> ⚠️ **本节的第 2 条来源此后有更正(见 Unreleased)**:本节写「上游不提供这个数据」,那是**只看响应头**得出的结论——响应头里确实没有,但上游把重置时间写在**响应体**里(`将在 … UTC+8 重置`)。「上游未给时间」那一行因此在真实 429 上从不该出现。解析与实测见 Unreleased 的 Bug Fixes 一节。
|
|
81
|
+
|
|
82
|
+
限流**不会**去借月周期包的刷新时间:那会让用户为几秒的限流去等几周。这个区分由 `cooldownOf` 强制,并有专门用例钉住。
|
|
83
|
+
|
|
84
|
+
**为何每个探测请求都必须以 system 消息开头**:国际网关(`www.workbuddy.ai`)对 `[{role:'user'}]` 一律返回 HTTP 400 / 业务码 `11128`(`first message is not system prompt`)。按国内版那样只发一条 user 消息,会把**整个国际版误报成全部不可用**。实测加上 system 消息后立即 200,故探测请求始终带一条 system 消息。
|
|
85
|
+
|
|
86
|
+
- **架构**:探测在 **Host** 侧发起(`src/probe.ts` 构造与解读,`WorkBuddyUpstreamClient.probeChat` 传输,`/plugins/dsh-connect-workbuddy/probe` 路由)。浏览器只发模型 id、只收回结论——**凭据绝不跨到页面**。
|
|
87
|
+
- **串行而非并发**:路由逐模型依次探测。一次并发扇出正是制造「限流」最直接的办法,那样测出来的会是插件自己的负载,而不是上游的限制。有专门用例钉住这一点。
|
|
88
|
+
- **不依赖 `dirty`**:测试按钮不受「有未保存改动」影响——它回答的是「这个模型现在能不能用」,这与是否已保存无关;把它绑在 dirty 上,按钮会在用户正决定取舍时恰好失效。也**不依赖 `canWrite`**(探测不写任何设置),但**依赖已登录**(没凭据只可能是「被拒」)。
|
|
89
|
+
- **不改动运行时**:探测结果不写入设置、不影响 `enabledModelIds` / `imageModelIds`,运行时目录零改动。
|
|
90
|
+
- **错误分类复用**:`outcomeOfFailure` 建立在既有的 `classifyUpstreamError` 之上(中英文额度标记、`soft_rate` / `hard_credit`),使探测与真实 chat 请求对同一上游应答的判定不可能分歧。
|
|
91
|
+
- **失败不抛异常**:单个模型探测失败返回一条结果,不中断整批——一个坏模型不该把其余行的结论一起吞掉。失败正文经 `safeMessage` 同规则脱敏(JWT 与 `token=` 查询参数)。
|
|
92
|
+
|
|
93
|
+
### Tests
|
|
94
|
+
|
|
95
|
+
- `tests/probe.spec.ts`(新增,28 例;此后在 Unreleased 增至 38 例):请求体必带 system 首条且 `max_tokens: 1` / `stream:true`;`Retry-After` 的秒数与 HTTP-date 两形态、以及 0 / 负数 / 过期时间不被当作冷却;`cooldownOf` 的四种组合(含**限流绝不编造时间**、**限流不借月周期刷新时间**);七类失败分类;成功流不被截断影响;**抛出异常不逃逸**;失败正文脱敏。
|
|
96
|
+
- `tests/web-status.spec.ts`(+10 例):逐模型串行调用(并发变异会红)、region 透传、空列表 / 非数组 / 超限批量一律 400、无能力时 503、非回环来源 403、非 POST 405、未知 region 400、抛错转 500 且不泄露 token。
|
|
97
|
+
- **变异验证**(5 处,逐一实测变红):给限流编造固定倒计时 → 3 例红;删掉 system 首条消息 → 2 例红;让限流借用月周期刷新时间 → 1 例红;把串行批改为并发扇出 → 1 例红;其余对照组全绿。
|
|
98
|
+
|
|
3
99
|
## 2.2.1 (2026-09-29)
|
|
4
100
|
|
|
5
101
|
### Fixes
|
package/README.en.md
CHANGED
|
@@ -25,6 +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 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.
|
|
28
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.
|
|
29
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.
|
|
30
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,6 +31,9 @@
|
|
|
31
31
|
- **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号选择、积分概览与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。
|
|
32
32
|
- **可单独关闭任一版本供应商** —— 每个 tab 右侧有一个勾选框(默认都勾选)。**取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉**(不是只藏起 tab):它的路由与「设置 → 模型」页的目录条目一并摘除,启动时也不再为它发无用请求。账号、积分、目录与勾选**全部保留**,重新勾选即完整恢复。典型用法是「我根本用不到国际版」——关掉它,让选择器干净。注意:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 `NO_ADAPTER`,卡片会在关闭态的 tab 上明确提示这一点,不会静默失败。
|
|
33
33
|
- **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。
|
|
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 很晚才压缩对话——长会话撞限流是这条机制的直接结果。
|
|
34
37
|
- **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
|
|
35
38
|
- **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
|
|
36
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-
|
|
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
|
@@ -28,6 +28,15 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
const WORKBUDDY_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/accounts/refresh";
|
|
29
29
|
/** Plugin-owned daily check-in action endpoint. */
|
|
30
30
|
const WORKBUDDY_CHECKIN_PATH = "/plugins/dsh-connect-workbuddy/checkin";
|
|
31
|
+
/**
|
|
32
|
+
* Plugin-owned model probe endpoint.
|
|
33
|
+
*
|
|
34
|
+
* Sends one minimal chat request per named model so the card can answer "is
|
|
35
|
+
* this model usable right now, and if it is limited, when can it be used
|
|
36
|
+
* again?". Lives on the Host because the credential must never reach the
|
|
37
|
+
* browser half.
|
|
38
|
+
*/
|
|
39
|
+
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-connect-workbuddy/probe";
|
|
31
40
|
/** Query parameter naming the region a card request addresses. */
|
|
32
41
|
const WORKBUDDY_REGION_PARAM = "region";
|
|
33
42
|
/** Every region, in card tab order. */
|
|
@@ -592,6 +601,16 @@ window.__ModuleLoader__.load({
|
|
|
592
601
|
.dsm-workbuddy-context-budget label{display:inline-flex;align-items:center;gap:4px;cursor:pointer}
|
|
593
602
|
.dsm-workbuddy-context-budget input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
594
603
|
.dsm-workbuddy-model-capability-note{margin:0;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
|
|
604
|
+
/* Model probe (test button + result). The result line is its own row under the
|
|
605
|
+
meta line, because a cooldown sentence ("rate limited - the upstream gave no
|
|
606
|
+
time") does not fit next to the context/output chips, and truncating it would
|
|
607
|
+
hide exactly the part the user needs. */
|
|
608
|
+
.dsm-workbuddy-models-head-actions{display:flex;align-items:center;gap:8px;flex:none}
|
|
609
|
+
.dsm-workbuddy-model-probe{flex:none;padding:3px 10px;font-size:11px;line-height:16px}
|
|
610
|
+
.dsm-workbuddy-model-probe-result{margin:0;font-size:12px;line-height:18px;word-break:break-word}
|
|
611
|
+
.dsm-workbuddy-model-probe-result-ok{color:var(--dsw-alias-state-success-primary,#22a06b)}
|
|
612
|
+
.dsm-workbuddy-model-probe-result-warn{color:var(--dsw-alias-state-warn-primary,#f59e0b)}
|
|
613
|
+
.dsm-workbuddy-model-probe-result-bad{color:var(--dsw-alias-state-error-primary,#ef4444)}
|
|
595
614
|
.dsm-workbuddy-model-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);padding-top:12px}
|
|
596
615
|
.dsm-workbuddy-model-save-error{flex:1;min-width:0;color:var(--dsw-alias-state-error-primary,#ef4444);font-size:12px;line-height:16px;text-align:right}
|
|
597
616
|
.dsm-workbuddy-model-actions-buttons{display:flex;align-items:center;justify-content:flex-end;gap:8px}
|
|
@@ -868,6 +887,54 @@ window.__ModuleLoader__.load({
|
|
|
868
887
|
function dotStyle(status) {
|
|
869
888
|
return { background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)" };
|
|
870
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* The sentence a probe result gets, and how it should be coloured.
|
|
892
|
+
*
|
|
893
|
+
* The cooldown wording is the part that had to be got right. The upstream names
|
|
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
|
+
*/
|
|
903
|
+
function probeResultView(result, t) {
|
|
904
|
+
switch (result.outcome) {
|
|
905
|
+
case "ok": {
|
|
906
|
+
const elapsed = result.elapsedMs === void 0 ? void 0 : `${String(result.elapsedMs / 1e3)}s`;
|
|
907
|
+
return {
|
|
908
|
+
text: elapsed === void 0 ? t("row.probeOk") : t("row.probeOkMs", { ms: elapsed }),
|
|
909
|
+
tone: "ok"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
case "rate-limited": return {
|
|
913
|
+
text: result.retryAtMs === void 0 ? t("row.probeRateLimitedUnknown") : t("row.probeRateLimitedAt", { at: formatDate(result.retryAtMs) }),
|
|
914
|
+
tone: "warn"
|
|
915
|
+
};
|
|
916
|
+
case "out-of-credit": return {
|
|
917
|
+
text: result.retryAtMs === void 0 ? t("row.probeOutOfCreditUnknown") : t("row.probeOutOfCreditAt", { at: formatDate(result.retryAtMs) }),
|
|
918
|
+
tone: "bad"
|
|
919
|
+
};
|
|
920
|
+
case "credential-rejected": return {
|
|
921
|
+
text: t("row.probeCredentialRejected"),
|
|
922
|
+
tone: "bad"
|
|
923
|
+
};
|
|
924
|
+
case "not-found": return {
|
|
925
|
+
text: t("row.probeNotFound"),
|
|
926
|
+
tone: "bad"
|
|
927
|
+
};
|
|
928
|
+
case "unavailable": return {
|
|
929
|
+
text: t("row.probeUnavailable"),
|
|
930
|
+
tone: "bad"
|
|
931
|
+
};
|
|
932
|
+
default: return {
|
|
933
|
+
text: result.status === void 0 ? t("row.probeFailed") : t("row.probeFailedStatus", { status: String(result.status) }),
|
|
934
|
+
tone: "bad"
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
}
|
|
871
938
|
/** Render WorkBuddy sign-in state, credits, and model selection as one card. */
|
|
872
939
|
function WorkBuddyCard({ t, settingsScope, view }) {
|
|
873
940
|
if (t === void 0) throw new Error("WorkBuddy plugin card requires its translation function");
|
|
@@ -909,6 +976,18 @@ window.__ModuleLoader__.load({
|
|
|
909
976
|
const [checkinActionError, setCheckinActionError] = (0, react.useState)(void 0);
|
|
910
977
|
/** Region whose on/off checkbox write is in flight, so its box can't race. */
|
|
911
978
|
const [togglingRegion, setTogglingRegion] = (0, react.useState)(void 0);
|
|
979
|
+
/**
|
|
980
|
+
* Probe results, keyed by region then model id.
|
|
981
|
+
*
|
|
982
|
+
* Kept per region for the same reason drafts are: the two tabs are separate
|
|
983
|
+
* provider stacks, so a test run on one tab must not be clobbered — or shown —
|
|
984
|
+
* as the other tab's answer.
|
|
985
|
+
*/
|
|
986
|
+
const [probes, setProbes] = (0, react.useState)({});
|
|
987
|
+
/** Model ids with a probe in flight, so each row's button can disable itself. */
|
|
988
|
+
const [probing, setProbing] = (0, react.useState)({});
|
|
989
|
+
/** Probe failure that is not attributable to one model (a dead route, say). */
|
|
990
|
+
const [probeError, setProbeError] = (0, react.useState)(void 0);
|
|
912
991
|
const mounted = (0, react.useRef)(true);
|
|
913
992
|
(0, react.useEffect)(() => {
|
|
914
993
|
mounted.current = true;
|
|
@@ -1201,6 +1280,59 @@ window.__ModuleLoader__.load({
|
|
|
1201
1280
|
return next;
|
|
1202
1281
|
});
|
|
1203
1282
|
};
|
|
1283
|
+
/**
|
|
1284
|
+
* Probe one or more models with a single minimal request each.
|
|
1285
|
+
*
|
|
1286
|
+
* The Host owns the request: the browser half sends only model ids and gets
|
|
1287
|
+
* outcomes back, because the credential must never cross to the page. The
|
|
1288
|
+
* route runs the batch sequentially and this waits for the whole answer, which
|
|
1289
|
+
* is what lets the card report every row at once — a per-row response would
|
|
1290
|
+
* race the shared `probing` map and make the batch button's state unknowable.
|
|
1291
|
+
*
|
|
1292
|
+
* Probing is deliberately independent of `dirty`: a test answers "does this
|
|
1293
|
+
* model work right now", which is true of the SAVED selection and the draft
|
|
1294
|
+
* alike, and blocking it on unsaved edits would make the button dead exactly
|
|
1295
|
+
* when the user is deciding what to keep.
|
|
1296
|
+
*/
|
|
1297
|
+
const probeModels = async (modelIds) => {
|
|
1298
|
+
if (modelIds.length === 0) return;
|
|
1299
|
+
setProbeError(void 0);
|
|
1300
|
+
setProbing((previous) => {
|
|
1301
|
+
const next = { ...previous };
|
|
1302
|
+
for (const id of modelIds) next[id] = true;
|
|
1303
|
+
return next;
|
|
1304
|
+
});
|
|
1305
|
+
try {
|
|
1306
|
+
const response = await fetch(withWorkBuddyRegion(WORKBUDDY_PROBE_PATH, activeRegion), {
|
|
1307
|
+
method: "POST",
|
|
1308
|
+
headers: {
|
|
1309
|
+
"content-type": "application/json",
|
|
1310
|
+
accept: "application/json"
|
|
1311
|
+
},
|
|
1312
|
+
credentials: "same-origin",
|
|
1313
|
+
body: JSON.stringify({ modelIds })
|
|
1314
|
+
});
|
|
1315
|
+
const body = await response.json().catch(() => void 0);
|
|
1316
|
+
if (!response.ok || !Array.isArray(body?.results)) throw new Error(body?.error ?? `HTTP ${String(response.status)}`);
|
|
1317
|
+
if (!mounted.current) return;
|
|
1318
|
+
setProbes((previous) => {
|
|
1319
|
+
const regionResults = { ...previous[activeRegion] ?? {} };
|
|
1320
|
+
for (const result of body.results) regionResults[result.modelId] = result;
|
|
1321
|
+
return {
|
|
1322
|
+
...previous,
|
|
1323
|
+
[activeRegion]: regionResults
|
|
1324
|
+
};
|
|
1325
|
+
});
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
if (mounted.current) setProbeError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
1328
|
+
} finally {
|
|
1329
|
+
if (mounted.current) setProbing((previous) => {
|
|
1330
|
+
const next = { ...previous };
|
|
1331
|
+
for (const id of modelIds) delete next[id];
|
|
1332
|
+
return next;
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1204
1336
|
const saveModels = async () => {
|
|
1205
1337
|
if (settingsScope === void 0) return;
|
|
1206
1338
|
if (status.status !== "signed-in") return;
|
|
@@ -1520,92 +1652,126 @@ window.__ModuleLoader__.load({
|
|
|
1520
1652
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1521
1653
|
className: "dsm-workbuddy-models-summary",
|
|
1522
1654
|
children: t("row.modelsSummary", { count: activeEnabledIds.size })
|
|
1523
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1655
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1656
|
+
className: "dsm-workbuddy-models-head-actions",
|
|
1657
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1658
|
+
type: "button",
|
|
1659
|
+
className: "dsm-btn dsm-btn-outline",
|
|
1660
|
+
disabled: busy,
|
|
1661
|
+
onClick: () => {
|
|
1662
|
+
refreshModels();
|
|
1663
|
+
},
|
|
1664
|
+
children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
|
|
1665
|
+
})
|
|
1531
1666
|
})]
|
|
1532
1667
|
}),
|
|
1668
|
+
probeError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1669
|
+
className: "dsm-workbuddy-model-probe-result dsm-workbuddy-model-probe-result-bad",
|
|
1670
|
+
role: "alert",
|
|
1671
|
+
children: t("row.probeError", { message: probeError })
|
|
1672
|
+
}),
|
|
1533
1673
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1534
1674
|
className: "dsm-workbuddy-model-list",
|
|
1535
1675
|
children: visibleModels.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1536
1676
|
className: `dsm-workbuddy-model${activeEnabledIds.has(model.id) ? "" : " dsm-workbuddy-model-disabled"}`,
|
|
1537
|
-
children: [
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1541
|
-
className: "dsm-workbuddy-model-enabled",
|
|
1542
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1543
|
-
type: "checkbox",
|
|
1544
|
-
checked: activeEnabledIds.has(model.id),
|
|
1545
|
-
disabled: !canWrite || saving,
|
|
1546
|
-
onChange: () => {
|
|
1547
|
-
toggleModel(model.id);
|
|
1548
|
-
}
|
|
1549
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1550
|
-
className: "dsm-workbuddy-model-copy",
|
|
1551
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1552
|
-
className: "dsm-workbuddy-model-name",
|
|
1553
|
-
children: [model.name, model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1554
|
-
className: "dsm-workbuddy-model-name-rate",
|
|
1555
|
-
children: [
|
|
1556
|
-
"(",
|
|
1557
|
-
model.creditMultiplier.toFixed(2),
|
|
1558
|
-
"x)"
|
|
1559
|
-
]
|
|
1560
|
-
})]
|
|
1561
|
-
})
|
|
1562
|
-
})]
|
|
1563
|
-
}),
|
|
1564
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1565
|
-
className: "dsm-workbuddy-model-image",
|
|
1566
|
-
title: imageCheckboxHint(model, t),
|
|
1567
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1568
|
-
type: "checkbox",
|
|
1569
|
-
checked: activeImageIds.has(model.id),
|
|
1570
|
-
disabled: !canWrite || saving,
|
|
1571
|
-
onChange: () => {
|
|
1572
|
-
toggleImage(model.id);
|
|
1573
|
-
}
|
|
1574
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelImage") })]
|
|
1575
|
-
}),
|
|
1576
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
1577
|
-
className: "dsm-workbuddy-context-budget",
|
|
1578
|
-
"aria-label": t("row.contextBudget"),
|
|
1579
|
-
children: [model.nativeContextWindow > 2e5 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1580
|
-
type: "radio",
|
|
1581
|
-
name: `context-${model.id}`,
|
|
1582
|
-
checked: (activeContextBudgets[model.id] ?? 2e5) === 2e5,
|
|
1583
|
-
disabled: !canWrite || saving,
|
|
1584
|
-
onChange: () => {
|
|
1585
|
-
setContextBudget(model.id, 2e5);
|
|
1586
|
-
}
|
|
1587
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "200K" })] }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1588
|
-
type: "radio",
|
|
1589
|
-
name: `context-${model.id}`,
|
|
1590
|
-
checked: model.nativeContextWindow <= 2e5 || activeContextBudgets[model.id] === model.nativeContextWindow,
|
|
1591
|
-
disabled: model.nativeContextWindow <= 2e5 || !canWrite || saving,
|
|
1592
|
-
onChange: () => {
|
|
1593
|
-
setContextBudget(model.id, model.nativeContextWindow);
|
|
1594
|
-
}
|
|
1595
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(model.nativeContextWindow, t("row.modelUnknown")) })] })]
|
|
1596
|
-
})
|
|
1597
|
-
]
|
|
1598
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1599
|
-
className: "dsm-workbuddy-model-details",
|
|
1600
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1601
|
-
className: "dsm-workbuddy-model-meta",
|
|
1677
|
+
children: [
|
|
1678
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1679
|
+
className: "dsm-workbuddy-model-head",
|
|
1602
1680
|
children: [
|
|
1603
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1604
|
-
|
|
1605
|
-
|
|
1681
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1682
|
+
className: "dsm-workbuddy-model-enabled",
|
|
1683
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1684
|
+
type: "checkbox",
|
|
1685
|
+
checked: activeEnabledIds.has(model.id),
|
|
1686
|
+
disabled: !canWrite || saving,
|
|
1687
|
+
onChange: () => {
|
|
1688
|
+
toggleModel(model.id);
|
|
1689
|
+
}
|
|
1690
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1691
|
+
className: "dsm-workbuddy-model-copy",
|
|
1692
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1693
|
+
className: "dsm-workbuddy-model-name",
|
|
1694
|
+
children: [model.name, model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1695
|
+
className: "dsm-workbuddy-model-name-rate",
|
|
1696
|
+
children: [
|
|
1697
|
+
"(",
|
|
1698
|
+
model.creditMultiplier.toFixed(2),
|
|
1699
|
+
"x)"
|
|
1700
|
+
]
|
|
1701
|
+
})]
|
|
1702
|
+
})
|
|
1703
|
+
})]
|
|
1704
|
+
}),
|
|
1705
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1706
|
+
className: "dsm-workbuddy-model-image",
|
|
1707
|
+
title: imageCheckboxHint(model, t),
|
|
1708
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1709
|
+
type: "checkbox",
|
|
1710
|
+
checked: activeImageIds.has(model.id),
|
|
1711
|
+
disabled: !canWrite || saving,
|
|
1712
|
+
onChange: () => {
|
|
1713
|
+
toggleImage(model.id);
|
|
1714
|
+
}
|
|
1715
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelImage") })]
|
|
1716
|
+
}),
|
|
1717
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
1718
|
+
className: "dsm-workbuddy-context-budget",
|
|
1719
|
+
"aria-label": t("row.contextBudget"),
|
|
1720
|
+
children: [model.nativeContextWindow > 2e5 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1721
|
+
type: "radio",
|
|
1722
|
+
name: `context-${model.id}`,
|
|
1723
|
+
checked: (activeContextBudgets[model.id] ?? 2e5) === 2e5,
|
|
1724
|
+
disabled: !canWrite || saving,
|
|
1725
|
+
onChange: () => {
|
|
1726
|
+
setContextBudget(model.id, 2e5);
|
|
1727
|
+
}
|
|
1728
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "200K" })] }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1729
|
+
type: "radio",
|
|
1730
|
+
name: `context-${model.id}`,
|
|
1731
|
+
checked: model.nativeContextWindow <= 2e5 || activeContextBudgets[model.id] === model.nativeContextWindow,
|
|
1732
|
+
disabled: model.nativeContextWindow <= 2e5 || !canWrite || saving,
|
|
1733
|
+
onChange: () => {
|
|
1734
|
+
setContextBudget(model.id, model.nativeContextWindow);
|
|
1735
|
+
}
|
|
1736
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(model.nativeContextWindow, t("row.modelUnknown")) })] })]
|
|
1737
|
+
}),
|
|
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
|
+
})
|
|
1750
|
+
})
|
|
1606
1751
|
]
|
|
1607
|
-
})
|
|
1608
|
-
|
|
1752
|
+
}),
|
|
1753
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1754
|
+
className: "dsm-workbuddy-model-details",
|
|
1755
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1756
|
+
className: "dsm-workbuddy-model-meta",
|
|
1757
|
+
children: [
|
|
1758
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelContext", { context: formatCapacity(model.nativeContextWindow, t("row.modelUnknown")) }) }),
|
|
1759
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelOutput", { output: formatCapacity(model.maxTokens, t("row.modelUnknown")) }) }),
|
|
1760
|
+
model.reasoning === void 0 || model.reasoning.supportedEfforts === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelReasoning", { efforts: model.reasoning.supportedEfforts.join(" / ") }) })
|
|
1761
|
+
]
|
|
1762
|
+
})
|
|
1763
|
+
}),
|
|
1764
|
+
(() => {
|
|
1765
|
+
const result = probes[activeRegion]?.[model.id];
|
|
1766
|
+
if (result === void 0) return null;
|
|
1767
|
+
const view = probeResultView(result, t);
|
|
1768
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1769
|
+
className: `dsm-workbuddy-model-probe-result dsm-workbuddy-model-probe-result-${view.tone}`,
|
|
1770
|
+
role: "status",
|
|
1771
|
+
children: [view.text, result.message === void 0 ? null : " · " + result.message]
|
|
1772
|
+
});
|
|
1773
|
+
})()
|
|
1774
|
+
]
|
|
1609
1775
|
}, model.id))
|
|
1610
1776
|
}),
|
|
1611
1777
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -1757,6 +1923,21 @@ window.__ModuleLoader__.load({
|
|
|
1757
1923
|
"row.modelReasoning": "Reasoning: {efforts}",
|
|
1758
1924
|
"row.modelUnknown": "Unknown",
|
|
1759
1925
|
"row.modelCapabilityPending": "Only capabilities advertised by WorkBuddy are shown.",
|
|
1926
|
+
"row.probe": "Test",
|
|
1927
|
+
"row.probing": "Testing…",
|
|
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.",
|
|
1929
|
+
"row.probeOk": "✓ Usable",
|
|
1930
|
+
"row.probeOkMs": "✓ Usable ({ms})",
|
|
1931
|
+
"row.probeRateLimitedUnknown": "✗ Rate limited · the upstream gave no time for when it frees up",
|
|
1932
|
+
"row.probeRateLimitedAt": "✗ Rate limited · usable again after {at}",
|
|
1933
|
+
"row.probeOutOfCreditUnknown": "✗ Not enough credits · the upstream gave no time for when it frees up",
|
|
1934
|
+
"row.probeOutOfCreditAt": "✗ Not enough credits · quota refreshes {at}",
|
|
1935
|
+
"row.probeCredentialRejected": "✗ WorkBuddy refused this account's token — sign in again or pick another account",
|
|
1936
|
+
"row.probeNotFound": "✗ The upstream does not know this model",
|
|
1937
|
+
"row.probeUnavailable": "✗ The upstream could not be reached — this is a network problem, not the model",
|
|
1938
|
+
"row.probeFailed": "✗ Test failed",
|
|
1939
|
+
"row.probeFailedStatus": "✗ Test failed (HTTP {status})",
|
|
1940
|
+
"row.probeError": "Could not run the test: {message}",
|
|
1760
1941
|
"row.cheer": "Star on GitHub"
|
|
1761
1942
|
};
|
|
1762
1943
|
const zh = {
|
|
@@ -1834,6 +2015,21 @@ window.__ModuleLoader__.load({
|
|
|
1834
2015
|
"row.modelReasoning": "推理强度:{efforts}",
|
|
1835
2016
|
"row.modelUnknown": "未知",
|
|
1836
2017
|
"row.modelCapabilityPending": "仅展示 WorkBuddy 接口明确公布的模型能力。",
|
|
2018
|
+
"row.probe": "测试",
|
|
2019
|
+
"row.probing": "测试中…",
|
|
2020
|
+
"row.probeHint": "对该模型发一条**真实体积**的请求(约 25k 输入 token),报告它现在能不能用。上游的限流按**请求体积**触发——实测约 20k 能过、约 30k 被拒——所以小请求看着正常、长对话里却可能每次都被拒。这也是为什么每次测试都会消耗真实积分(约 0.7),且一次只测一个模型。",
|
|
2021
|
+
"row.probeOk": "✓ 可用",
|
|
2022
|
+
"row.probeOkMs": "✓ 可用({ms})",
|
|
2023
|
+
"row.probeRateLimitedUnknown": "✗ 被限流 · 上游未给出何时恢复",
|
|
2024
|
+
"row.probeRateLimitedAt": "✗ 被限流 · {at} 之后可再用",
|
|
2025
|
+
"row.probeOutOfCreditUnknown": "✗ 积分不足 · 上游未给出何时恢复",
|
|
2026
|
+
"row.probeOutOfCreditAt": "✗ 积分不足 · 积分包 {at} 刷新",
|
|
2027
|
+
"row.probeCredentialRejected": "✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号",
|
|
2028
|
+
"row.probeNotFound": "✗ 上游不认识这个模型",
|
|
2029
|
+
"row.probeUnavailable": "✗ 连不上上游——这是网络问题,不是模型问题",
|
|
2030
|
+
"row.probeFailed": "✗ 测试失败",
|
|
2031
|
+
"row.probeFailedStatus": "✗ 测试失败(HTTP {status})",
|
|
2032
|
+
"row.probeError": "无法执行测试:{message}",
|
|
1837
2033
|
"row.cheer": "鼓励一下"
|
|
1838
2034
|
};
|
|
1839
2035
|
//#endregion
|
|
@@ -827,6 +827,54 @@ var WorkBuddyUpstreamClient = class {
|
|
|
827
827
|
message: text
|
|
828
828
|
};
|
|
829
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* Send one minimal chat request for `bodyJson` and report the RAW answer.
|
|
832
|
+
*
|
|
833
|
+
* Shares {@link chatHeaders} and the chat base with {@link chatStream} on
|
|
834
|
+
* purpose: a probe is only meaningful if it reaches the same endpoint with the
|
|
835
|
+
* same authentication as a real request. The differences are deliberate and
|
|
836
|
+
* narrow — it returns the headers (for `Retry-After`) and the failure body
|
|
837
|
+
* (for classification) instead of a pre-classified error, so the probe module
|
|
838
|
+
* owns the interpretation and the network layer stays a transport.
|
|
839
|
+
*
|
|
840
|
+
* The caller MUST drain {@link WorkBuddyProbeAnswer.response}; an unread body
|
|
841
|
+
* holds the connection open.
|
|
842
|
+
*/
|
|
843
|
+
async probeChat(credential, bodyJson, signal) {
|
|
844
|
+
let response;
|
|
845
|
+
try {
|
|
846
|
+
response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
|
|
847
|
+
method: "POST",
|
|
848
|
+
headers: {
|
|
849
|
+
...chatHeaders(credential),
|
|
850
|
+
"Authorization": `Bearer ${credential.accessToken}`
|
|
851
|
+
},
|
|
852
|
+
body: bodyJson,
|
|
853
|
+
...signal === void 0 ? {} : { signal }
|
|
854
|
+
});
|
|
855
|
+
} catch (error) {
|
|
856
|
+
return {
|
|
857
|
+
ok: false,
|
|
858
|
+
status: 0,
|
|
859
|
+
retryAfter: null,
|
|
860
|
+
body: `transport error: ${String(error)}`
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
const retryAfter = response.headers.get("retry-after");
|
|
864
|
+
if (response.ok) return {
|
|
865
|
+
ok: true,
|
|
866
|
+
status: response.status,
|
|
867
|
+
retryAfter,
|
|
868
|
+
response
|
|
869
|
+
};
|
|
870
|
+
const body = (await response.text()).slice(0, ERROR_BODY_LIMIT);
|
|
871
|
+
return {
|
|
872
|
+
ok: false,
|
|
873
|
+
status: response.status,
|
|
874
|
+
retryAfter,
|
|
875
|
+
body
|
|
876
|
+
};
|
|
877
|
+
}
|
|
830
878
|
/** POST the token-refresh endpoint; the caller merges the outcome. */
|
|
831
879
|
async refreshToken(credential) {
|
|
832
880
|
const response = await fetch(`${chatBase(credential)}/v2/plugin/auth/token/refresh`, {
|
|
@@ -2210,7 +2258,7 @@ var WorkBuddyCatalog = class {
|
|
|
2210
2258
|
//#endregion
|
|
2211
2259
|
//#region src/version.ts
|
|
2212
2260
|
/** The npm package version this build was produced from. */
|
|
2213
|
-
const WORKBUDDY_CONNECT_VERSION = "2.
|
|
2261
|
+
const WORKBUDDY_CONNECT_VERSION = "2.3.1";
|
|
2214
2262
|
//#endregion
|
|
2215
2263
|
//#region src/host-heartbeat.ts
|
|
2216
2264
|
/**
|