dsh-connect-workbuddy 2.2.1 → 2.3.0
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 +42 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +286 -78
- package/lib/{host-heartbeat-Dy8fW3G5.js → host-heartbeat-Uh8JCXqk.js} +49 -1
- package/lib/index.d.ts +238 -1
- package/lib/index.js +336 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.0 (2026-09-29)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
#### 模型「测试」:一键查出限量模型的冷却恢复时间
|
|
8
|
+
|
|
9
|
+
**新增模型可用性测试。** 卡片每个模型行新增一个**测试**按钮,模型区顶部新增**测试已勾选**(对 `enabledModelIds` 逐个测)。每个模型只发**一条最小请求**(`max_tokens: 1`),直接回答两件事:**这个模型现在能不能用**,以及**不能用时什么时候恢复**。
|
|
10
|
+
|
|
11
|
+
限量模型的冷却时间过去只能靠反复试错去猜——现在点一下就有结论。
|
|
12
|
+
|
|
13
|
+
**实测(本机真实账号,两个网关)**:CN 16 个与全球 23 个模型各抽样实打,全部 HTTP 200;单次耗时 0.8~3.4s。**成本可忽略**——响应体 `"credit"` 报 0~0.01,其中一次探测前后余额完全不变(933 → 933),而名册上正常一次回答是 0.79x 起。
|
|
14
|
+
|
|
15
|
+
**关于「多久之后可以再次使用」,有一条必须先说清的事实:上游不给这个数据。**
|
|
16
|
+
|
|
17
|
+
探测前先做了取证:200 响应头里**没有 `Retry-After`,也没有 `X-RateLimit-*`**;对同一模型**连续快速打 12 次全部成功**,未触发任何限流。因此本功能**不编造倒计时**,只在两种上游真的说了时间的情形下显示时间:
|
|
18
|
+
|
|
19
|
+
| 情形 | 卡片显示 | 时间来源 |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| 成功 | `✓ 可用(1.2s)` | — |
|
|
22
|
+
| 429 / 限流,上游未给时间 | `✗ 被限流 · 上游未给出何时恢复` | 无(如实说没有) |
|
|
23
|
+
| 429 / 限流,带 `Retry-After` | `✗ 被限流 · {时间} 之后可再用` | `Retry-After` 头(秒数或 HTTP-date 两种形态) |
|
|
24
|
+
| 积分耗尽,且该区域有月周期包 | `✗ 积分不足 · 积分包 {时间} 刷新` | 月周期包的 `refreshAtMs` |
|
|
25
|
+
| 积分耗尽,无月周期包 | `✗ 积分不足 · 上游未给出何时恢复` | 无 |
|
|
26
|
+
| 401/403 凭据被拒 | `✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号` | — |
|
|
27
|
+
|
|
28
|
+
限流**不会**去借月周期包的刷新时间:那会让用户为几秒的限流去等几周。这个区分由 `cooldownOf` 强制,并有专门用例钉住。
|
|
29
|
+
|
|
30
|
+
**为何每个探测请求都必须以 system 消息开头**:国际网关(`www.workbuddy.ai`)对 `[{role:'user'}]` 一律返回 HTTP 400 / 业务码 `11128`(`first message is not system prompt`)。按国内版那样只发一条 user 消息,会把**整个国际版误报成全部不可用**。实测加上 system 消息后立即 200,故探测请求始终带一条 system 消息。
|
|
31
|
+
|
|
32
|
+
- **架构**:探测在 **Host** 侧发起(`src/probe.ts` 构造与解读,`WorkBuddyUpstreamClient.probeChat` 传输,`/plugins/dsh-connect-workbuddy/probe` 路由)。浏览器只发模型 id、只收回结论——**凭据绝不跨到页面**。
|
|
33
|
+
- **串行而非并发**:路由逐模型依次探测。一次并发扇出正是制造「限流」最直接的办法,那样测出来的会是插件自己的负载,而不是上游的限制。有专门用例钉住这一点。
|
|
34
|
+
- **不依赖 `dirty`**:测试按钮不受「有未保存改动」影响——它回答的是「这个模型现在能不能用」,这与是否已保存无关;把它绑在 dirty 上,按钮会在用户正决定取舍时恰好失效。也**不依赖 `canWrite`**(探测不写任何设置),但**依赖已登录**(没凭据只可能是「被拒」)。
|
|
35
|
+
- **不改动运行时**:探测结果不写入设置、不影响 `enabledModelIds` / `imageModelIds`,运行时目录零改动。
|
|
36
|
+
- **错误分类复用**:`outcomeOfFailure` 建立在既有的 `classifyUpstreamError` 之上(中英文额度标记、`soft_rate` / `hard_credit`),使探测与真实 chat 请求对同一上游应答的判定不可能分歧。
|
|
37
|
+
- **失败不抛异常**:单个模型探测失败返回一条结果,不中断整批——一个坏模型不该把其余行的结论一起吞掉。失败正文经 `safeMessage` 同规则脱敏(JWT 与 `token=` 查询参数)。
|
|
38
|
+
|
|
39
|
+
### Tests
|
|
40
|
+
|
|
41
|
+
- `tests/probe.spec.ts`(新增,28 例):请求体必带 system 首条且 `max_tokens: 1` / `stream:true`;`Retry-After` 的秒数与 HTTP-date 两形态、以及 0 / 负数 / 过期时间不被当作冷却;`cooldownOf` 的四种组合(含**限流绝不编造时间**、**限流不借月周期刷新时间**);七类失败分类;成功流不被截断影响;**抛出异常不逃逸**;失败正文脱敏。
|
|
42
|
+
- `tests/web-status.spec.ts`(+10 例):逐模型串行调用(并发变异会红)、region 透传、空列表 / 非数组 / 超限批量一律 400、无能力时 503、非回环来源 403、非 POST 405、未知 region 400、抛错转 500 且不泄露 token。
|
|
43
|
+
- **变异验证**(5 处,逐一实测变红):给限流编造固定倒计时 → 3 例红;删掉 system 首条消息 → 2 例红;让限流借用月周期刷新时间 → 1 例红;把串行批改为并发扇出 → 1 例红;其余对照组全绿。
|
|
44
|
+
|
|
3
45
|
## 2.2.1 (2026-09-29)
|
|
4
46
|
|
|
5
47
|
### Fixes
|
package/README.en.md
CHANGED
|
@@ -25,6 +25,7 @@ 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 and the models header has **Test selected**: each chosen model gets one minimal request (`max_tokens: 1`), and you get a direct answer for whether it works right now and, if not, when it comes back. Measured cost is negligible: the response reports `credit` between 0 and 0.01, and one probe left the balance unchanged (933 → 933), against 0.79x and up for a real reply; each probe took 0.8–3.4s. **On "when can I use this again", the upstream does not provide that data**: measured responses carry no `Retry-After` and no `X-RateLimit-*`, and 12 rapid requests to the same model all succeeded. So the plugin **never invents a countdown** — it shows a time only for the two cases where the upstream actually states one (a `Retry-After` header, or the monthly quota's refresh point when the quota is exhausted) and says plainly that no time was given otherwise. 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.
|
|
28
29
|
- **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
30
|
- **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
31
|
- **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,7 @@
|
|
|
31
31
|
- **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号选择、积分概览与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。
|
|
32
32
|
- **可单独关闭任一版本供应商** —— 每个 tab 右侧有一个勾选框(默认都勾选)。**取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉**(不是只藏起 tab):它的路由与「设置 → 模型」页的目录条目一并摘除,启动时也不再为它发无用请求。账号、积分、目录与勾选**全部保留**,重新勾选即完整恢复。典型用法是「我根本用不到国际版」——关掉它,让选择器干净。注意:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 `NO_ADAPTER`,卡片会在关闭态的 tab 上明确提示这一点,不会静默失败。
|
|
33
33
|
- **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。
|
|
34
|
+
- **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,模型区顶部有「测试已勾选」,对勾选的模型**各发一条最小请求**(`max_tokens: 1`),直接告诉你它此刻能否使用、不能用时何时恢复。实测代价可忽略:`credit` 报 0~0.01,单次探测前后余额不变(933 → 933),而正常一次回答是 0.79x 起;单次耗时 0.8~3.4s。**关于「多久之后可以再次使用」,上游并不提供这个数据**:实测响应头里没有 `Retry-After`、也没有 `X-RateLimit-*`,对同一模型连续快速打 12 次全部成功。因此插件**不编造倒计时**,只在两种上游真的给出时间的情形下显示时间——`Retry-After` 头,或积分耗尽时该区域月周期包的刷新点;两者都没有时如实显示「上游未给出何时恢复」。限流**不会**去借月周期包的刷新时间(那会让人为几秒的限流去等几周)。探测在后台发起,**凭据不会传到页面**;结果不写入设置,也不改变你已保存的勾选。
|
|
34
35
|
- **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
|
|
35
36
|
- **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
|
|
36
37
|
- **只读积分概览** —— 按套餐聚合展示剩余积分,并列出每个模型的积分倍率。查询不消耗积分。
|
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-Uh8JCXqk.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 exactly two situations — a `Retry-After` header, or an
|
|
895
|
+
* exhausted monthly quota whose refresh point it declares — and its 200s and
|
|
896
|
+
* 429s carry NO rate-limit metadata at all (measured: no `Retry-After`, no
|
|
897
|
+
* `X-RateLimit-*`, and 12 rapid requests to one model all succeeded). So a
|
|
898
|
+
* limited result WITHOUT a stated time must say that plainly. Substituting a
|
|
899
|
+
* locally invented countdown would be the single most misleading thing this
|
|
900
|
+
* feature could do: it would look like an upstream answer while being a guess,
|
|
901
|
+
* and the user would 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;
|
|
@@ -1154,6 +1233,8 @@ window.__ModuleLoader__.load({
|
|
|
1154
1233
|
* A bound scope is therefore all that is required to accept an edit.
|
|
1155
1234
|
*/
|
|
1156
1235
|
const canWrite = settingsScope !== void 0;
|
|
1236
|
+
/** Whether any row's probe is in flight, so the batch button reflects it. */
|
|
1237
|
+
const anyProbing = Object.keys(probing).length > 0;
|
|
1157
1238
|
const editDraft = (edit) => {
|
|
1158
1239
|
setDrafts((prev) => ({
|
|
1159
1240
|
...prev,
|
|
@@ -1201,6 +1282,59 @@ window.__ModuleLoader__.load({
|
|
|
1201
1282
|
return next;
|
|
1202
1283
|
});
|
|
1203
1284
|
};
|
|
1285
|
+
/**
|
|
1286
|
+
* Probe one or more models with a single minimal request each.
|
|
1287
|
+
*
|
|
1288
|
+
* The Host owns the request: the browser half sends only model ids and gets
|
|
1289
|
+
* outcomes back, because the credential must never cross to the page. The
|
|
1290
|
+
* route runs the batch sequentially and this waits for the whole answer, which
|
|
1291
|
+
* is what lets the card report every row at once — a per-row response would
|
|
1292
|
+
* race the shared `probing` map and make the batch button's state unknowable.
|
|
1293
|
+
*
|
|
1294
|
+
* Probing is deliberately independent of `dirty`: a test answers "does this
|
|
1295
|
+
* model work right now", which is true of the SAVED selection and the draft
|
|
1296
|
+
* alike, and blocking it on unsaved edits would make the button dead exactly
|
|
1297
|
+
* when the user is deciding what to keep.
|
|
1298
|
+
*/
|
|
1299
|
+
const probeModels = async (modelIds) => {
|
|
1300
|
+
if (modelIds.length === 0) return;
|
|
1301
|
+
setProbeError(void 0);
|
|
1302
|
+
setProbing((previous) => {
|
|
1303
|
+
const next = { ...previous };
|
|
1304
|
+
for (const id of modelIds) next[id] = true;
|
|
1305
|
+
return next;
|
|
1306
|
+
});
|
|
1307
|
+
try {
|
|
1308
|
+
const response = await fetch(withWorkBuddyRegion(WORKBUDDY_PROBE_PATH, activeRegion), {
|
|
1309
|
+
method: "POST",
|
|
1310
|
+
headers: {
|
|
1311
|
+
"content-type": "application/json",
|
|
1312
|
+
accept: "application/json"
|
|
1313
|
+
},
|
|
1314
|
+
credentials: "same-origin",
|
|
1315
|
+
body: JSON.stringify({ modelIds })
|
|
1316
|
+
});
|
|
1317
|
+
const body = await response.json().catch(() => void 0);
|
|
1318
|
+
if (!response.ok || !Array.isArray(body?.results)) throw new Error(body?.error ?? `HTTP ${String(response.status)}`);
|
|
1319
|
+
if (!mounted.current) return;
|
|
1320
|
+
setProbes((previous) => {
|
|
1321
|
+
const regionResults = { ...previous[activeRegion] ?? {} };
|
|
1322
|
+
for (const result of body.results) regionResults[result.modelId] = result;
|
|
1323
|
+
return {
|
|
1324
|
+
...previous,
|
|
1325
|
+
[activeRegion]: regionResults
|
|
1326
|
+
};
|
|
1327
|
+
});
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
if (mounted.current) setProbeError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
1330
|
+
} finally {
|
|
1331
|
+
if (mounted.current) setProbing((previous) => {
|
|
1332
|
+
const next = { ...previous };
|
|
1333
|
+
for (const id of modelIds) delete next[id];
|
|
1334
|
+
return next;
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1204
1338
|
const saveModels = async () => {
|
|
1205
1339
|
if (settingsScope === void 0) return;
|
|
1206
1340
|
if (status.status !== "signed-in") return;
|
|
@@ -1520,92 +1654,132 @@ window.__ModuleLoader__.load({
|
|
|
1520
1654
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1521
1655
|
className: "dsm-workbuddy-models-summary",
|
|
1522
1656
|
children: t("row.modelsSummary", { count: activeEnabledIds.size })
|
|
1523
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1657
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1658
|
+
className: "dsm-workbuddy-models-head-actions",
|
|
1659
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1660
|
+
type: "button",
|
|
1661
|
+
className: "dsm-btn dsm-btn-outline",
|
|
1662
|
+
disabled: activeEnabledIds.size === 0 || anyProbing,
|
|
1663
|
+
title: t("row.probeAllHint"),
|
|
1664
|
+
onClick: () => {
|
|
1665
|
+
probeModels([...activeEnabledIds]);
|
|
1666
|
+
},
|
|
1667
|
+
children: anyProbing ? t("row.probing") : t("row.probeAll")
|
|
1668
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1669
|
+
type: "button",
|
|
1670
|
+
className: "dsm-btn dsm-btn-outline",
|
|
1671
|
+
disabled: busy,
|
|
1672
|
+
onClick: () => {
|
|
1673
|
+
refreshModels();
|
|
1674
|
+
},
|
|
1675
|
+
children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
|
|
1676
|
+
})]
|
|
1531
1677
|
})]
|
|
1532
1678
|
}),
|
|
1679
|
+
probeError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1680
|
+
className: "dsm-workbuddy-model-probe-result dsm-workbuddy-model-probe-result-bad",
|
|
1681
|
+
role: "alert",
|
|
1682
|
+
children: t("row.probeError", { message: probeError })
|
|
1683
|
+
}),
|
|
1533
1684
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1534
1685
|
className: "dsm-workbuddy-model-list",
|
|
1535
1686
|
children: visibleModels.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1536
1687
|
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",
|
|
1688
|
+
children: [
|
|
1689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1690
|
+
className: "dsm-workbuddy-model-head",
|
|
1602
1691
|
children: [
|
|
1603
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1604
|
-
|
|
1605
|
-
|
|
1692
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1693
|
+
className: "dsm-workbuddy-model-enabled",
|
|
1694
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1695
|
+
type: "checkbox",
|
|
1696
|
+
checked: activeEnabledIds.has(model.id),
|
|
1697
|
+
disabled: !canWrite || saving,
|
|
1698
|
+
onChange: () => {
|
|
1699
|
+
toggleModel(model.id);
|
|
1700
|
+
}
|
|
1701
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1702
|
+
className: "dsm-workbuddy-model-copy",
|
|
1703
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1704
|
+
className: "dsm-workbuddy-model-name",
|
|
1705
|
+
children: [model.name, model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1706
|
+
className: "dsm-workbuddy-model-name-rate",
|
|
1707
|
+
children: [
|
|
1708
|
+
"(",
|
|
1709
|
+
model.creditMultiplier.toFixed(2),
|
|
1710
|
+
"x)"
|
|
1711
|
+
]
|
|
1712
|
+
})]
|
|
1713
|
+
})
|
|
1714
|
+
})]
|
|
1715
|
+
}),
|
|
1716
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1717
|
+
className: "dsm-workbuddy-model-image",
|
|
1718
|
+
title: imageCheckboxHint(model, t),
|
|
1719
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1720
|
+
type: "checkbox",
|
|
1721
|
+
checked: activeImageIds.has(model.id),
|
|
1722
|
+
disabled: !canWrite || saving,
|
|
1723
|
+
onChange: () => {
|
|
1724
|
+
toggleImage(model.id);
|
|
1725
|
+
}
|
|
1726
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelImage") })]
|
|
1727
|
+
}),
|
|
1728
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
|
|
1729
|
+
className: "dsm-workbuddy-context-budget",
|
|
1730
|
+
"aria-label": t("row.contextBudget"),
|
|
1731
|
+
children: [model.nativeContextWindow > 2e5 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1732
|
+
type: "radio",
|
|
1733
|
+
name: `context-${model.id}`,
|
|
1734
|
+
checked: (activeContextBudgets[model.id] ?? 2e5) === 2e5,
|
|
1735
|
+
disabled: !canWrite || saving,
|
|
1736
|
+
onChange: () => {
|
|
1737
|
+
setContextBudget(model.id, 2e5);
|
|
1738
|
+
}
|
|
1739
|
+
}), /* @__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", {
|
|
1740
|
+
type: "radio",
|
|
1741
|
+
name: `context-${model.id}`,
|
|
1742
|
+
checked: model.nativeContextWindow <= 2e5 || activeContextBudgets[model.id] === model.nativeContextWindow,
|
|
1743
|
+
disabled: model.nativeContextWindow <= 2e5 || !canWrite || saving,
|
|
1744
|
+
onChange: () => {
|
|
1745
|
+
setContextBudget(model.id, model.nativeContextWindow);
|
|
1746
|
+
}
|
|
1747
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(model.nativeContextWindow, t("row.modelUnknown")) })] })]
|
|
1748
|
+
}),
|
|
1749
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1750
|
+
type: "button",
|
|
1751
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-model-probe",
|
|
1752
|
+
disabled: probing[model.id] === true || status.status !== "signed-in",
|
|
1753
|
+
title: t("row.probeHint"),
|
|
1754
|
+
onClick: () => {
|
|
1755
|
+
probeModels([model.id]);
|
|
1756
|
+
},
|
|
1757
|
+
children: probing[model.id] === true ? t("row.probing") : t("row.probe")
|
|
1758
|
+
})
|
|
1606
1759
|
]
|
|
1607
|
-
})
|
|
1608
|
-
|
|
1760
|
+
}),
|
|
1761
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1762
|
+
className: "dsm-workbuddy-model-details",
|
|
1763
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1764
|
+
className: "dsm-workbuddy-model-meta",
|
|
1765
|
+
children: [
|
|
1766
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelContext", { context: formatCapacity(model.nativeContextWindow, t("row.modelUnknown")) }) }),
|
|
1767
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelOutput", { output: formatCapacity(model.maxTokens, t("row.modelUnknown")) }) }),
|
|
1768
|
+
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(" / ") }) })
|
|
1769
|
+
]
|
|
1770
|
+
})
|
|
1771
|
+
}),
|
|
1772
|
+
(() => {
|
|
1773
|
+
const result = probes[activeRegion]?.[model.id];
|
|
1774
|
+
if (result === void 0) return null;
|
|
1775
|
+
const view = probeResultView(result, t);
|
|
1776
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1777
|
+
className: `dsm-workbuddy-model-probe-result dsm-workbuddy-model-probe-result-${view.tone}`,
|
|
1778
|
+
role: "status",
|
|
1779
|
+
children: [view.text, result.message === void 0 ? null : " · " + result.message]
|
|
1780
|
+
});
|
|
1781
|
+
})()
|
|
1782
|
+
]
|
|
1609
1783
|
}, model.id))
|
|
1610
1784
|
}),
|
|
1611
1785
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -1757,6 +1931,23 @@ window.__ModuleLoader__.load({
|
|
|
1757
1931
|
"row.modelReasoning": "Reasoning: {efforts}",
|
|
1758
1932
|
"row.modelUnknown": "Unknown",
|
|
1759
1933
|
"row.modelCapabilityPending": "Only capabilities advertised by WorkBuddy are shown.",
|
|
1934
|
+
"row.probe": "Test",
|
|
1935
|
+
"row.probing": "Testing…",
|
|
1936
|
+
"row.probeAll": "Test selected",
|
|
1937
|
+
"row.probeAllHint": "Send one minimal request (1 output token) to each enabled model and report which ones answer right now. Measured cost: 0 to 0.01 credits per model, against 0.79+ for a real reply.",
|
|
1938
|
+
"row.probeHint": "Send one minimal request to this model to check it answers right now.",
|
|
1939
|
+
"row.probeOk": "✓ Usable",
|
|
1940
|
+
"row.probeOkMs": "✓ Usable ({ms})",
|
|
1941
|
+
"row.probeRateLimitedUnknown": "✗ Rate limited · the upstream gave no time for when it frees up",
|
|
1942
|
+
"row.probeRateLimitedAt": "✗ Rate limited · usable again after {at}",
|
|
1943
|
+
"row.probeOutOfCreditUnknown": "✗ Not enough credits · the upstream gave no time for when it frees up",
|
|
1944
|
+
"row.probeOutOfCreditAt": "✗ Not enough credits · quota refreshes {at}",
|
|
1945
|
+
"row.probeCredentialRejected": "✗ WorkBuddy refused this account's token — sign in again or pick another account",
|
|
1946
|
+
"row.probeNotFound": "✗ The upstream does not know this model",
|
|
1947
|
+
"row.probeUnavailable": "✗ The upstream could not be reached — this is a network problem, not the model",
|
|
1948
|
+
"row.probeFailed": "✗ Test failed",
|
|
1949
|
+
"row.probeFailedStatus": "✗ Test failed (HTTP {status})",
|
|
1950
|
+
"row.probeError": "Could not run the test: {message}",
|
|
1760
1951
|
"row.cheer": "Star on GitHub"
|
|
1761
1952
|
};
|
|
1762
1953
|
const zh = {
|
|
@@ -1834,6 +2025,23 @@ window.__ModuleLoader__.load({
|
|
|
1834
2025
|
"row.modelReasoning": "推理强度:{efforts}",
|
|
1835
2026
|
"row.modelUnknown": "未知",
|
|
1836
2027
|
"row.modelCapabilityPending": "仅展示 WorkBuddy 接口明确公布的模型能力。",
|
|
2028
|
+
"row.probe": "测试",
|
|
2029
|
+
"row.probing": "测试中…",
|
|
2030
|
+
"row.probeAll": "测试已勾选",
|
|
2031
|
+
"row.probeAllHint": "对每个已勾选的模型各发一条最小请求(只出 1 个 token),报告哪个现在真的能用。实测每个模型消耗 0~0.01 积分,而正常一次回答是 0.79 起。",
|
|
2032
|
+
"row.probeHint": "对该模型发一条最小请求,检查它现在能不能用。",
|
|
2033
|
+
"row.probeOk": "✓ 可用",
|
|
2034
|
+
"row.probeOkMs": "✓ 可用({ms})",
|
|
2035
|
+
"row.probeRateLimitedUnknown": "✗ 被限流 · 上游未给出何时恢复",
|
|
2036
|
+
"row.probeRateLimitedAt": "✗ 被限流 · {at} 之后可再用",
|
|
2037
|
+
"row.probeOutOfCreditUnknown": "✗ 积分不足 · 上游未给出何时恢复",
|
|
2038
|
+
"row.probeOutOfCreditAt": "✗ 积分不足 · 积分包 {at} 刷新",
|
|
2039
|
+
"row.probeCredentialRejected": "✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号",
|
|
2040
|
+
"row.probeNotFound": "✗ 上游不认识这个模型",
|
|
2041
|
+
"row.probeUnavailable": "✗ 连不上上游——这是网络问题,不是模型问题",
|
|
2042
|
+
"row.probeFailed": "✗ 测试失败",
|
|
2043
|
+
"row.probeFailedStatus": "✗ 测试失败(HTTP {status})",
|
|
2044
|
+
"row.probeError": "无法执行测试:{message}",
|
|
1837
2045
|
"row.cheer": "鼓励一下"
|
|
1838
2046
|
};
|
|
1839
2047
|
//#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.0";
|
|
2214
2262
|
//#endregion
|
|
2215
2263
|
//#region src/host-heartbeat.ts
|
|
2216
2264
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -512,6 +512,25 @@ type WorkBuddyChatResult = {
|
|
|
512
512
|
kind: UpstreamErrorKind;
|
|
513
513
|
message: string;
|
|
514
514
|
};
|
|
515
|
+
/**
|
|
516
|
+
* Chat answer that also carries the response headers.
|
|
517
|
+
*
|
|
518
|
+
* Used by the model probe, whose whole point is to ask "is this usable right
|
|
519
|
+
* now, and if not, when?" — and the only place an upstream ever names a
|
|
520
|
+
* cooldown is the `Retry-After` header, which {@link WorkBuddyChatResult}
|
|
521
|
+
* deliberately does not surface. Returning a separate shape keeps the ordinary
|
|
522
|
+
* chat path (and its callers) untouched.
|
|
523
|
+
*/
|
|
524
|
+
interface WorkBuddyProbeAnswer {
|
|
525
|
+
ok: boolean;
|
|
526
|
+
status: number;
|
|
527
|
+
/** Raw `Retry-After`, when the upstream sent one; null otherwise. */
|
|
528
|
+
retryAfter: string | null;
|
|
529
|
+
/** Failure body excerpt, only when `ok` is false. */
|
|
530
|
+
body?: string;
|
|
531
|
+
/** The live stream, only when `ok` is true. The caller drains and discards it. */
|
|
532
|
+
response?: Response;
|
|
533
|
+
}
|
|
515
534
|
/** Classify an upstream failure from its HTTP status and body excerpt. */
|
|
516
535
|
declare function classifyUpstreamError(status: number, body: string): UpstreamErrorKind;
|
|
517
536
|
/**
|
|
@@ -578,6 +597,20 @@ declare function parseUpstreamModel(value: unknown): WorkBuddyUpstreamModel | un
|
|
|
578
597
|
declare class WorkBuddyUpstreamClient {
|
|
579
598
|
/** POST the chat endpoint; a successful answer is the raw SSE response. */
|
|
580
599
|
chatStream(credential: WorkBuddyCredential, bodyJson: string, signal?: AbortSignal): Promise<WorkBuddyChatResult>;
|
|
600
|
+
/**
|
|
601
|
+
* Send one minimal chat request for `bodyJson` and report the RAW answer.
|
|
602
|
+
*
|
|
603
|
+
* Shares {@link chatHeaders} and the chat base with {@link chatStream} on
|
|
604
|
+
* purpose: a probe is only meaningful if it reaches the same endpoint with the
|
|
605
|
+
* same authentication as a real request. The differences are deliberate and
|
|
606
|
+
* narrow — it returns the headers (for `Retry-After`) and the failure body
|
|
607
|
+
* (for classification) instead of a pre-classified error, so the probe module
|
|
608
|
+
* owns the interpretation and the network layer stays a transport.
|
|
609
|
+
*
|
|
610
|
+
* The caller MUST drain {@link WorkBuddyProbeAnswer.response}; an unread body
|
|
611
|
+
* holds the connection open.
|
|
612
|
+
*/
|
|
613
|
+
probeChat(credential: WorkBuddyCredential, bodyJson: string, signal?: AbortSignal): Promise<WorkBuddyProbeAnswer>;
|
|
581
614
|
/** POST the token-refresh endpoint; the caller merges the outcome. */
|
|
582
615
|
refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
|
|
583
616
|
/**
|
|
@@ -1153,6 +1186,151 @@ declare function clearHostHeartbeat(): Promise<void>;
|
|
|
1153
1186
|
/** The npm package version this build was produced from. */
|
|
1154
1187
|
declare const WORKBUDDY_CONNECT_VERSION: string;
|
|
1155
1188
|
//#endregion
|
|
1189
|
+
//#region src/probe.d.ts
|
|
1190
|
+
/**
|
|
1191
|
+
* The system message every probe carries.
|
|
1192
|
+
*
|
|
1193
|
+
* Not decoration: the international gateway rejects a conversation that does not
|
|
1194
|
+
* start with a system message (see the module note). A probe that omitted it
|
|
1195
|
+
* would report every global model as broken.
|
|
1196
|
+
*/
|
|
1197
|
+
declare const PROBE_SYSTEM_PROMPT = "You are a connectivity check. Reply with a single word.";
|
|
1198
|
+
/** The one-word user turn; the smallest thing a chat endpoint accepts. */
|
|
1199
|
+
declare const PROBE_USER_PROMPT = "ping";
|
|
1200
|
+
/**
|
|
1201
|
+
* Output cap for a probe. One token is enough to prove the model answers, and
|
|
1202
|
+
* keeps the cost negligible: live measurements of `max_tokens: 1` probes
|
|
1203
|
+
* reported `credit` between 0 and 0.01, and one such probe left the credit
|
|
1204
|
+
* balance unchanged (933 → 933). For scale, a model on that roster bills 0.79+
|
|
1205
|
+
* per real reply. The point of the feature is to learn whether a model works
|
|
1206
|
+
* while spending as close to nothing as the service allows.
|
|
1207
|
+
*/
|
|
1208
|
+
declare const PROBE_MAX_TOKENS = 1;
|
|
1209
|
+
/**
|
|
1210
|
+
* Build the request body for one probe.
|
|
1211
|
+
*
|
|
1212
|
+
* `stream` is forced because the upstream rejects non-streaming chat requests;
|
|
1213
|
+
* `prepareChatBody` enforces that too, but sending it explicitly keeps this
|
|
1214
|
+
* function's output valid on its own.
|
|
1215
|
+
*/
|
|
1216
|
+
declare function probeRequestBody(modelId: string): string;
|
|
1217
|
+
/**
|
|
1218
|
+
* How one probe ended.
|
|
1219
|
+
*
|
|
1220
|
+
* `rate-limited` is split out from a generic failure because it is the one
|
|
1221
|
+
* outcome the feature exists for: it means "this model works, but not right
|
|
1222
|
+
* now". Everything else is a real, actionable failure or a success.
|
|
1223
|
+
*/
|
|
1224
|
+
type WorkBuddyProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
1225
|
+
/**
|
|
1226
|
+
* One probe result, as the card renders it.
|
|
1227
|
+
*
|
|
1228
|
+
* `retryAtMs` is present ONLY when the upstream actually stated a time (a
|
|
1229
|
+
* `Retry-After` header, or the region's quota refresh point when the failure is
|
|
1230
|
+
* an exhausted quota). Its absence is meaningful and must be rendered as "the
|
|
1231
|
+
* upstream did not say", never as a locally invented countdown.
|
|
1232
|
+
*/
|
|
1233
|
+
interface WorkBuddyProbeResult {
|
|
1234
|
+
modelId: string;
|
|
1235
|
+
outcome: WorkBuddyProbeOutcome;
|
|
1236
|
+
/** Round-trip time in ms, for a successful probe. */
|
|
1237
|
+
elapsedMs?: number;
|
|
1238
|
+
/** HTTP status the upstream answered with; 0 means the request never landed. */
|
|
1239
|
+
status?: number;
|
|
1240
|
+
/** Upstream's own words, redacted. Present on failures. */
|
|
1241
|
+
message?: string;
|
|
1242
|
+
/** A time the upstream named, in ms. Absent when it named none. */
|
|
1243
|
+
retryAtMs?: number;
|
|
1244
|
+
/** Where a supplied `retryAtMs` came from, so the card can say so. */
|
|
1245
|
+
retrySource?: 'retry-after' | 'quota-refresh';
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Parse a `Retry-After` header value into an absolute time.
|
|
1249
|
+
*
|
|
1250
|
+
* Both RFC 9110 forms are accepted: delay-seconds (`120`) and an HTTP-date
|
|
1251
|
+
* (`Wed, 21 Oct 2026 07:28:00 GMT`). Returns undefined for anything else,
|
|
1252
|
+
* including the negative/zero delays some gateways emit — a "retry now" is not
|
|
1253
|
+
* a cooldown and reporting it as one would be worse than saying nothing.
|
|
1254
|
+
*
|
|
1255
|
+
* `nowMs` is injected so the delay-seconds branch is testable without a clock.
|
|
1256
|
+
*/
|
|
1257
|
+
declare function parseRetryAfter(value: string | null, nowMs: number): number | undefined;
|
|
1258
|
+
/**
|
|
1259
|
+
* The cooldown the upstream stated, if it stated one.
|
|
1260
|
+
*
|
|
1261
|
+
* Two sources, in order of authority:
|
|
1262
|
+
*
|
|
1263
|
+
* 1. `Retry-After` on the failure response. This is the upstream naming a time
|
|
1264
|
+
* for THIS request, so it wins.
|
|
1265
|
+
* 2. The region's monthly quota refresh point, but ONLY for an out-of-credit
|
|
1266
|
+
* outcome. A quota that resets at a known time is the one case where "when
|
|
1267
|
+
* can I use this again" has a real answer even without a header — and it is
|
|
1268
|
+
* the answer for the most common real limit on this service.
|
|
1269
|
+
*
|
|
1270
|
+
* A `rate-limited` outcome with neither source returns `{}`, which the card
|
|
1271
|
+
* renders as "the upstream did not say when". That is the honest answer: the
|
|
1272
|
+
* live probes found no rate-limit metadata in any response at all, so inventing
|
|
1273
|
+
* a number here would be pure fiction.
|
|
1274
|
+
*/
|
|
1275
|
+
declare function cooldownOf(input: {
|
|
1276
|
+
outcome: WorkBuddyProbeOutcome;
|
|
1277
|
+
retryAfter: string | null;
|
|
1278
|
+
nowMs: number;
|
|
1279
|
+
/** The region's next monthly quota refresh, when the upstream declares one. */
|
|
1280
|
+
quotaRefreshAtMs?: number;
|
|
1281
|
+
}): Pick<WorkBuddyProbeResult, 'retryAtMs' | 'retrySource'>;
|
|
1282
|
+
/**
|
|
1283
|
+
* Classify one failed probe into the card's outcome vocabulary.
|
|
1284
|
+
*
|
|
1285
|
+
* Built on `classifyUpstreamError` rather than re-testing status codes, so a
|
|
1286
|
+
* probe and a real chat request can never disagree about what the same upstream
|
|
1287
|
+
* answer means. The extra split here is `credential-rejected`, which the
|
|
1288
|
+
* upstream signals with a non-JSON 401/403 edge page and which needs completely
|
|
1289
|
+
* different advice (re-auth, not "wait").
|
|
1290
|
+
*/
|
|
1291
|
+
declare function outcomeOfFailure(status: number, body: string): WorkBuddyProbeOutcome;
|
|
1292
|
+
/**
|
|
1293
|
+
* Read the credit this probe consumed out of an SSE stream, when the upstream
|
|
1294
|
+
* reports it.
|
|
1295
|
+
*
|
|
1296
|
+
* Purely informational — the card shows it so a user can see how little a probe
|
|
1297
|
+
* actually costs rather than having to trust a claim. Absent when the stream
|
|
1298
|
+
* carried no `usage` block.
|
|
1299
|
+
*/
|
|
1300
|
+
declare function creditOfStream(text: string): number | undefined;
|
|
1301
|
+
/** Whether a probe outcome means the model answered. */
|
|
1302
|
+
declare function probeSucceeded(outcome: WorkBuddyProbeOutcome): boolean;
|
|
1303
|
+
/** What one probe needs from the upstream client. */
|
|
1304
|
+
interface WorkBuddyProbeClient {
|
|
1305
|
+
probeChat(credential: WorkBuddyCredential, bodyJson: string, signal?: AbortSignal): Promise<{
|
|
1306
|
+
ok: boolean;
|
|
1307
|
+
status: number;
|
|
1308
|
+
retryAfter: string | null;
|
|
1309
|
+
body?: string;
|
|
1310
|
+
response?: Response;
|
|
1311
|
+
}>;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Run one probe for one model and reduce it to the card's row.
|
|
1315
|
+
*
|
|
1316
|
+
* The successful path drains the SSE stream purely to read the `credit` figure
|
|
1317
|
+
* the upstream reports, then discards the rest: the probe's question is "did
|
|
1318
|
+
* this model answer", and `max_tokens: 1` means there is nothing else in the
|
|
1319
|
+
* stream worth keeping.
|
|
1320
|
+
*
|
|
1321
|
+
* Never throws. A probe is a diagnostic the user asked for, so every failure is
|
|
1322
|
+
* a RESULT — one dead model must not abort the batch and hide the other rows.
|
|
1323
|
+
*/
|
|
1324
|
+
declare function probeModel(input: {
|
|
1325
|
+
client: WorkBuddyProbeClient;
|
|
1326
|
+
credential: WorkBuddyCredential;
|
|
1327
|
+
modelId: string;
|
|
1328
|
+
nowMs: number;
|
|
1329
|
+
/** The region's next monthly quota refresh, when known. */
|
|
1330
|
+
quotaRefreshAtMs?: number;
|
|
1331
|
+
signal?: AbortSignal;
|
|
1332
|
+
}): Promise<WorkBuddyProbeResult>;
|
|
1333
|
+
//#endregion
|
|
1156
1334
|
//#region src/status-paths.d.ts
|
|
1157
1335
|
/**
|
|
1158
1336
|
* Node-free constants and types shared by the Host and browser halves.
|
|
@@ -1175,6 +1353,15 @@ declare const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/mo
|
|
|
1175
1353
|
declare const WORKBUDDY_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/accounts/refresh";
|
|
1176
1354
|
/** Plugin-owned daily check-in action endpoint. */
|
|
1177
1355
|
declare const WORKBUDDY_CHECKIN_PATH = "/plugins/dsh-connect-workbuddy/checkin";
|
|
1356
|
+
/**
|
|
1357
|
+
* Plugin-owned model probe endpoint.
|
|
1358
|
+
*
|
|
1359
|
+
* Sends one minimal chat request per named model so the card can answer "is
|
|
1360
|
+
* this model usable right now, and if it is limited, when can it be used
|
|
1361
|
+
* again?". Lives on the Host because the credential must never reach the
|
|
1362
|
+
* browser half.
|
|
1363
|
+
*/
|
|
1364
|
+
declare const WORKBUDDY_PROBE_PATH = "/plugins/dsh-connect-workbuddy/probe";
|
|
1178
1365
|
/** Query parameter naming the region a card request addresses. */
|
|
1179
1366
|
declare const WORKBUDDY_REGION_PARAM = "region";
|
|
1180
1367
|
/** Every region, in card tab order. */
|
|
@@ -1444,6 +1631,38 @@ type WorkBuddyWebUsage =
|
|
|
1444
1631
|
status: 'error';
|
|
1445
1632
|
message: string;
|
|
1446
1633
|
};
|
|
1634
|
+
/**
|
|
1635
|
+
* Outcome vocabulary of one model probe, shared by the Host and the card.
|
|
1636
|
+
*
|
|
1637
|
+
* Mirrors `WorkBuddyProbeOutcome` in `./probe.ts`; kept as its own declaration
|
|
1638
|
+
* here because this module is the node-free bridge and must not import the
|
|
1639
|
+
* Host-side probe module (which pulls in the credential type).
|
|
1640
|
+
*/
|
|
1641
|
+
type WorkBuddyWebProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
1642
|
+
/** One model's probe result, as the card renders it. */
|
|
1643
|
+
interface WorkBuddyWebProbeResult {
|
|
1644
|
+
modelId: string;
|
|
1645
|
+
outcome: WorkBuddyWebProbeOutcome;
|
|
1646
|
+
/** Round-trip time in ms; present once the request landed. */
|
|
1647
|
+
elapsedMs?: number;
|
|
1648
|
+
/** HTTP status the upstream answered with; 0 means it never landed. */
|
|
1649
|
+
status?: number;
|
|
1650
|
+
/** Upstream's own words, already redacted. */
|
|
1651
|
+
message?: string;
|
|
1652
|
+
/**
|
|
1653
|
+
* A time the upstream named for using this model again, in ms.
|
|
1654
|
+
*
|
|
1655
|
+
* ABSENT means the upstream named none, and the card must say exactly that.
|
|
1656
|
+
* The live probes that shaped this feature found no rate-limit metadata in
|
|
1657
|
+
* any response — no `Retry-After`, no `X-RateLimit-*` — so a client-invented
|
|
1658
|
+
* countdown here would be fiction. Only two sources ever fill this:
|
|
1659
|
+
* a real `Retry-After`, or (for an exhausted quota) the region's own quota
|
|
1660
|
+
* refresh point, which the upstream does declare.
|
|
1661
|
+
*/
|
|
1662
|
+
retryAtMs?: number;
|
|
1663
|
+
/** Which of those two sources supplied {@link retryAtMs}. */
|
|
1664
|
+
retrySource?: 'retry-after' | 'quota-refresh';
|
|
1665
|
+
}
|
|
1447
1666
|
/**
|
|
1448
1667
|
* Deep copy of a settings value with every `{get(): T}` live reference replaced
|
|
1449
1668
|
* by the value it resolves to.
|
|
@@ -1511,6 +1730,24 @@ interface WorkBuddyStatusRouteOptions {
|
|
|
1511
1730
|
* committed settings value rather than a local guess.
|
|
1512
1731
|
*/
|
|
1513
1732
|
regionEnabled(region: WorkBuddyRegion): boolean;
|
|
1733
|
+
/**
|
|
1734
|
+
* Send one minimal request to each named model of a region and report what
|
|
1735
|
+
* came back.
|
|
1736
|
+
*
|
|
1737
|
+
* Optional: it is the only dependency the card's test button needs, so a Host
|
|
1738
|
+
* built without it (or a route mounted for a status-only consumer) simply
|
|
1739
|
+
* answers 503 and the button reports that instead of failing the card.
|
|
1740
|
+
*/
|
|
1741
|
+
probeModels?(region: WorkBuddyRegion, modelIds: readonly string[], signal?: AbortSignal): Promise<readonly WorkBuddyWebProbeResult[]>;
|
|
1742
|
+
/**
|
|
1743
|
+
* The region's next monthly quota refresh, when the upstream declares one.
|
|
1744
|
+
*
|
|
1745
|
+
* Used ONLY to answer "when can I use this again" for an out-of-credit
|
|
1746
|
+
* outcome, which is the one cooldown the service actually states: a monthly
|
|
1747
|
+
* resource that resets at a known time. Every other limited outcome has no
|
|
1748
|
+
* time from any source and must be reported as such.
|
|
1749
|
+
*/
|
|
1750
|
+
quotaRefreshAtMs?(region: WorkBuddyRegion): number | undefined;
|
|
1514
1751
|
/**
|
|
1515
1752
|
* Report whether a region still has at least one local sign-in.
|
|
1516
1753
|
*
|
|
@@ -1739,4 +1976,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
|
|
|
1739
1976
|
*/
|
|
1740
1977
|
declare function apply(ctx: Context, config: Config): void;
|
|
1741
1978
|
//#endregion
|
|
1742
|
-
export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
1979
|
+
export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, PROBE_USER_PROMPT, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-
|
|
1
|
+
import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-Uh8JCXqk.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
@@ -492,6 +492,210 @@ function createWorkBuddyShim(options) {
|
|
|
492
492
|
};
|
|
493
493
|
}
|
|
494
494
|
//#endregion
|
|
495
|
+
//#region src/probe.ts
|
|
496
|
+
/**
|
|
497
|
+
* The system message every probe carries.
|
|
498
|
+
*
|
|
499
|
+
* Not decoration: the international gateway rejects a conversation that does not
|
|
500
|
+
* start with a system message (see the module note). A probe that omitted it
|
|
501
|
+
* would report every global model as broken.
|
|
502
|
+
*/
|
|
503
|
+
const PROBE_SYSTEM_PROMPT = "You are a connectivity check. Reply with a single word.";
|
|
504
|
+
/** The one-word user turn; the smallest thing a chat endpoint accepts. */
|
|
505
|
+
const PROBE_USER_PROMPT = "ping";
|
|
506
|
+
/**
|
|
507
|
+
* Output cap for a probe. One token is enough to prove the model answers, and
|
|
508
|
+
* keeps the cost negligible: live measurements of `max_tokens: 1` probes
|
|
509
|
+
* reported `credit` between 0 and 0.01, and one such probe left the credit
|
|
510
|
+
* balance unchanged (933 → 933). For scale, a model on that roster bills 0.79+
|
|
511
|
+
* per real reply. The point of the feature is to learn whether a model works
|
|
512
|
+
* while spending as close to nothing as the service allows.
|
|
513
|
+
*/
|
|
514
|
+
const PROBE_MAX_TOKENS = 1;
|
|
515
|
+
/**
|
|
516
|
+
* Build the request body for one probe.
|
|
517
|
+
*
|
|
518
|
+
* `stream` is forced because the upstream rejects non-streaming chat requests;
|
|
519
|
+
* `prepareChatBody` enforces that too, but sending it explicitly keeps this
|
|
520
|
+
* function's output valid on its own.
|
|
521
|
+
*/
|
|
522
|
+
function probeRequestBody(modelId) {
|
|
523
|
+
return JSON.stringify({
|
|
524
|
+
model: modelId,
|
|
525
|
+
messages: [{
|
|
526
|
+
role: "system",
|
|
527
|
+
content: PROBE_SYSTEM_PROMPT
|
|
528
|
+
}, {
|
|
529
|
+
role: "user",
|
|
530
|
+
content: PROBE_USER_PROMPT
|
|
531
|
+
}],
|
|
532
|
+
max_tokens: 1,
|
|
533
|
+
stream: true
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Parse a `Retry-After` header value into an absolute time.
|
|
538
|
+
*
|
|
539
|
+
* Both RFC 9110 forms are accepted: delay-seconds (`120`) and an HTTP-date
|
|
540
|
+
* (`Wed, 21 Oct 2026 07:28:00 GMT`). Returns undefined for anything else,
|
|
541
|
+
* including the negative/zero delays some gateways emit — a "retry now" is not
|
|
542
|
+
* a cooldown and reporting it as one would be worse than saying nothing.
|
|
543
|
+
*
|
|
544
|
+
* `nowMs` is injected so the delay-seconds branch is testable without a clock.
|
|
545
|
+
*/
|
|
546
|
+
function parseRetryAfter(value, nowMs) {
|
|
547
|
+
if (value === null) return void 0;
|
|
548
|
+
const trimmed = value.trim();
|
|
549
|
+
if (trimmed === "") return void 0;
|
|
550
|
+
if (/^\d+$/u.test(trimmed)) {
|
|
551
|
+
const seconds = Number(trimmed);
|
|
552
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return void 0;
|
|
553
|
+
return nowMs + seconds * 1e3;
|
|
554
|
+
}
|
|
555
|
+
const parsed = Date.parse(trimmed);
|
|
556
|
+
if (Number.isNaN(parsed)) return void 0;
|
|
557
|
+
if (parsed <= nowMs) return void 0;
|
|
558
|
+
return parsed;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* The cooldown the upstream stated, if it stated one.
|
|
562
|
+
*
|
|
563
|
+
* Two sources, in order of authority:
|
|
564
|
+
*
|
|
565
|
+
* 1. `Retry-After` on the failure response. This is the upstream naming a time
|
|
566
|
+
* for THIS request, so it wins.
|
|
567
|
+
* 2. The region's monthly quota refresh point, but ONLY for an out-of-credit
|
|
568
|
+
* outcome. A quota that resets at a known time is the one case where "when
|
|
569
|
+
* can I use this again" has a real answer even without a header — and it is
|
|
570
|
+
* the answer for the most common real limit on this service.
|
|
571
|
+
*
|
|
572
|
+
* A `rate-limited` outcome with neither source returns `{}`, which the card
|
|
573
|
+
* renders as "the upstream did not say when". That is the honest answer: the
|
|
574
|
+
* live probes found no rate-limit metadata in any response at all, so inventing
|
|
575
|
+
* a number here would be pure fiction.
|
|
576
|
+
*/
|
|
577
|
+
function cooldownOf(input) {
|
|
578
|
+
const fromHeader = parseRetryAfter(input.retryAfter, input.nowMs);
|
|
579
|
+
if (fromHeader !== void 0) return {
|
|
580
|
+
retryAtMs: fromHeader,
|
|
581
|
+
retrySource: "retry-after"
|
|
582
|
+
};
|
|
583
|
+
if (input.outcome === "out-of-credit" && input.quotaRefreshAtMs !== void 0) return {
|
|
584
|
+
retryAtMs: input.quotaRefreshAtMs,
|
|
585
|
+
retrySource: "quota-refresh"
|
|
586
|
+
};
|
|
587
|
+
return {};
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Classify one failed probe into the card's outcome vocabulary.
|
|
591
|
+
*
|
|
592
|
+
* Built on `classifyUpstreamError` rather than re-testing status codes, so a
|
|
593
|
+
* probe and a real chat request can never disagree about what the same upstream
|
|
594
|
+
* answer means. The extra split here is `credential-rejected`, which the
|
|
595
|
+
* upstream signals with a non-JSON 401/403 edge page and which needs completely
|
|
596
|
+
* different advice (re-auth, not "wait").
|
|
597
|
+
*/
|
|
598
|
+
function outcomeOfFailure(status, body) {
|
|
599
|
+
if (status === 401 || status === 403) return "credential-rejected";
|
|
600
|
+
switch (classifyUpstreamError(status, body)) {
|
|
601
|
+
case "soft_rate": return "rate-limited";
|
|
602
|
+
case "hard_credit": return "out-of-credit";
|
|
603
|
+
case "session_dead": return "credential-rejected";
|
|
604
|
+
case "not_found": return "not-found";
|
|
605
|
+
default: return status === 0 || status >= 500 ? "unavailable" : "failed";
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Read the credit this probe consumed out of an SSE stream, when the upstream
|
|
610
|
+
* reports it.
|
|
611
|
+
*
|
|
612
|
+
* Purely informational — the card shows it so a user can see how little a probe
|
|
613
|
+
* actually costs rather than having to trust a claim. Absent when the stream
|
|
614
|
+
* carried no `usage` block.
|
|
615
|
+
*/
|
|
616
|
+
function creditOfStream(text) {
|
|
617
|
+
const match = /"credit"\s*:\s*(-?[0-9]*\.?[0-9]+)/u.exec(text);
|
|
618
|
+
if (match === null) return void 0;
|
|
619
|
+
const value = Number(match[1]);
|
|
620
|
+
return Number.isFinite(value) ? value : void 0;
|
|
621
|
+
}
|
|
622
|
+
/** Whether a probe outcome means the model answered. */
|
|
623
|
+
function probeSucceeded(outcome) {
|
|
624
|
+
return outcome === "ok";
|
|
625
|
+
}
|
|
626
|
+
/** Max characters of upstream text kept for display, after redaction. */
|
|
627
|
+
const PROBE_MESSAGE_LIMIT = 300;
|
|
628
|
+
/**
|
|
629
|
+
* Redact token-shaped content out of upstream text before it is stored or sent.
|
|
630
|
+
*
|
|
631
|
+
* Duplicated from the route layer on purpose rather than imported: this module
|
|
632
|
+
* is the one that reads raw failure bodies, so the redaction belongs at the
|
|
633
|
+
* point of capture. A probe failure body is the one place a raw upstream string
|
|
634
|
+
* from an arbitrary endpoint enters the plugin's data flow.
|
|
635
|
+
*/
|
|
636
|
+
function redact(text) {
|
|
637
|
+
return text.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]").slice(0, PROBE_MESSAGE_LIMIT);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Run one probe for one model and reduce it to the card's row.
|
|
641
|
+
*
|
|
642
|
+
* The successful path drains the SSE stream purely to read the `credit` figure
|
|
643
|
+
* the upstream reports, then discards the rest: the probe's question is "did
|
|
644
|
+
* this model answer", and `max_tokens: 1` means there is nothing else in the
|
|
645
|
+
* stream worth keeping.
|
|
646
|
+
*
|
|
647
|
+
* Never throws. A probe is a diagnostic the user asked for, so every failure is
|
|
648
|
+
* a RESULT — one dead model must not abort the batch and hide the other rows.
|
|
649
|
+
*/
|
|
650
|
+
async function probeModel(input) {
|
|
651
|
+
let answer;
|
|
652
|
+
const startedAt = Date.now();
|
|
653
|
+
try {
|
|
654
|
+
answer = await input.client.probeChat(input.credential, probeRequestBody(input.modelId), input.signal);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
return {
|
|
657
|
+
modelId: input.modelId,
|
|
658
|
+
outcome: "unavailable",
|
|
659
|
+
message: redact(error instanceof Error ? error.message : String(error))
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const elapsedMs = Date.now() - startedAt;
|
|
663
|
+
if (answer.ok) {
|
|
664
|
+
let credit;
|
|
665
|
+
if (answer.response !== void 0) try {
|
|
666
|
+
credit = creditOfStream(await answer.response.text());
|
|
667
|
+
} catch {}
|
|
668
|
+
return {
|
|
669
|
+
modelId: input.modelId,
|
|
670
|
+
outcome: "ok",
|
|
671
|
+
elapsedMs: Date.now() - startedAt,
|
|
672
|
+
status: answer.status,
|
|
673
|
+
...credit === void 0 ? {} : { message: `credit ${credit}` },
|
|
674
|
+
...cooldownOf({
|
|
675
|
+
outcome: "ok",
|
|
676
|
+
retryAfter: answer.retryAfter,
|
|
677
|
+
nowMs: input.nowMs,
|
|
678
|
+
...input.quotaRefreshAtMs === void 0 ? {} : { quotaRefreshAtMs: input.quotaRefreshAtMs }
|
|
679
|
+
})
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
const body = answer.body ?? "";
|
|
683
|
+
const outcome = outcomeOfFailure(answer.status, body);
|
|
684
|
+
return {
|
|
685
|
+
modelId: input.modelId,
|
|
686
|
+
outcome,
|
|
687
|
+
elapsedMs,
|
|
688
|
+
status: answer.status,
|
|
689
|
+
...body === "" ? {} : { message: redact(body) },
|
|
690
|
+
...cooldownOf({
|
|
691
|
+
outcome,
|
|
692
|
+
retryAfter: answer.retryAfter,
|
|
693
|
+
nowMs: input.nowMs,
|
|
694
|
+
...input.quotaRefreshAtMs === void 0 ? {} : { quotaRefreshAtMs: input.quotaRefreshAtMs }
|
|
695
|
+
})
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
//#endregion
|
|
495
699
|
//#region src/status-paths.ts
|
|
496
700
|
/**
|
|
497
701
|
* Node-free constants and types shared by the Host and browser halves.
|
|
@@ -514,6 +718,15 @@ const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/ref
|
|
|
514
718
|
const WORKBUDDY_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/accounts/refresh";
|
|
515
719
|
/** Plugin-owned daily check-in action endpoint. */
|
|
516
720
|
const WORKBUDDY_CHECKIN_PATH = "/plugins/dsh-connect-workbuddy/checkin";
|
|
721
|
+
/**
|
|
722
|
+
* Plugin-owned model probe endpoint.
|
|
723
|
+
*
|
|
724
|
+
* Sends one minimal chat request per named model so the card can answer "is
|
|
725
|
+
* this model usable right now, and if it is limited, when can it be used
|
|
726
|
+
* again?". Lives on the Host because the credential must never reach the
|
|
727
|
+
* browser half.
|
|
728
|
+
*/
|
|
729
|
+
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-connect-workbuddy/probe";
|
|
517
730
|
/** Query parameter naming the region a card request addresses. */
|
|
518
731
|
const WORKBUDDY_REGION_PARAM = "region";
|
|
519
732
|
/** Every region, in card tab order. */
|
|
@@ -684,6 +897,51 @@ function json(res, status, body) {
|
|
|
684
897
|
});
|
|
685
898
|
res.end(payload);
|
|
686
899
|
}
|
|
900
|
+
/**
|
|
901
|
+
* Cap on how many models one probe request may name.
|
|
902
|
+
*
|
|
903
|
+
* A probe is a real (if tiny) request against a metered service, and the batch
|
|
904
|
+
* runs sequentially, so an unbounded list would let one click hold the route for
|
|
905
|
+
* minutes. The card never sends more than the models on its own screen.
|
|
906
|
+
*/
|
|
907
|
+
const PROBE_BATCH_LIMIT = 64;
|
|
908
|
+
/** Request-body cap for the probe route: model ids only, never content. */
|
|
909
|
+
const PROBE_BODY_LIMIT = 65536;
|
|
910
|
+
/**
|
|
911
|
+
* Read a small JSON request body.
|
|
912
|
+
*
|
|
913
|
+
* Bounded, unlike the settings route's reader: this endpoint takes a list of
|
|
914
|
+
* model ids and has no legitimate use for a large payload, so an oversized body
|
|
915
|
+
* is refused rather than buffered.
|
|
916
|
+
*/
|
|
917
|
+
function readJsonBody(req) {
|
|
918
|
+
return new Promise((resolve, reject) => {
|
|
919
|
+
const chunks = [];
|
|
920
|
+
let size = 0;
|
|
921
|
+
req.on("data", (chunk) => {
|
|
922
|
+
size += chunk.length;
|
|
923
|
+
if (size > PROBE_BODY_LIMIT) {
|
|
924
|
+
reject(/* @__PURE__ */ new Error("request body too large"));
|
|
925
|
+
req.destroy();
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
chunks.push(chunk);
|
|
929
|
+
});
|
|
930
|
+
req.on("end", () => {
|
|
931
|
+
try {
|
|
932
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
933
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
934
|
+
reject(/* @__PURE__ */ new Error("request body must be a JSON object"));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
resolve(parsed);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
req.on("error", reject);
|
|
943
|
+
});
|
|
944
|
+
}
|
|
687
945
|
/** Loopback browser origins only; other devices are refused until trusted origins exist. */
|
|
688
946
|
function loopbackOrigin(req) {
|
|
689
947
|
const origin = req.headers.origin;
|
|
@@ -947,6 +1205,28 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
947
1205
|
}
|
|
948
1206
|
}
|
|
949
1207
|
});
|
|
1208
|
+
const disposeProbe = ctx.webServer.register({
|
|
1209
|
+
kind: "exact",
|
|
1210
|
+
path: WORKBUDDY_PROBE_PATH,
|
|
1211
|
+
handler: async (req, res) => {
|
|
1212
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
1213
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
1214
|
+
if (deps.probeModels === void 0) return json(res, 503, { error: "model probe unavailable" });
|
|
1215
|
+
const region = requestRegion(req, res);
|
|
1216
|
+
if (region === void 0) return;
|
|
1217
|
+
try {
|
|
1218
|
+
const body = await readJsonBody(req);
|
|
1219
|
+
const modelIds = Array.isArray(body.modelIds) ? body.modelIds.filter((id) => typeof id === "string" && id !== "") : [];
|
|
1220
|
+
if (modelIds.length === 0) return json(res, 400, { error: "modelIds must be a non-empty array of strings" });
|
|
1221
|
+
if (modelIds.length > PROBE_BATCH_LIMIT) return json(res, 400, { error: `at most ${String(PROBE_BATCH_LIMIT)} models per probe` });
|
|
1222
|
+
const results = [];
|
|
1223
|
+
for (const modelId of modelIds) results.push(...await deps.probeModels(region, [modelId]));
|
|
1224
|
+
json(res, 200, { results });
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
json(res, 500, { error: safeMessage(error) });
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
950
1230
|
const disposeDiagWrite = ctx.webServer.register({
|
|
951
1231
|
kind: "exact",
|
|
952
1232
|
path: "/plugins/dsh-connect-workbuddy/__save",
|
|
@@ -1002,6 +1282,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
1002
1282
|
disposeCheckin();
|
|
1003
1283
|
disposeAccounts();
|
|
1004
1284
|
disposeUsage();
|
|
1285
|
+
disposeProbe();
|
|
1005
1286
|
disposeDiagWrite();
|
|
1006
1287
|
};
|
|
1007
1288
|
}, "dsh-connect-workbuddy: Web status route");
|
|
@@ -1393,10 +1674,63 @@ function apply(ctx, config) {
|
|
|
1393
1674
|
contextBudgets: (region) => regionStateOf(current(), region).contextBudgets ?? {},
|
|
1394
1675
|
discoverModels,
|
|
1395
1676
|
regionEnabled: (region) => regionEnabled(current(), region),
|
|
1677
|
+
/**
|
|
1678
|
+
* One minimal request per named model, through the region's own credential.
|
|
1679
|
+
*
|
|
1680
|
+
* Resolved here rather than in the route so the browser half never touches a
|
|
1681
|
+
* token: the route passes only model ids and receives only outcomes.
|
|
1682
|
+
*
|
|
1683
|
+
* `quotaRefreshAtMs` is the region's nearest monthly refresh point, read from
|
|
1684
|
+
* the SAME credit answer the card displays. It is the only cooldown this
|
|
1685
|
+
* service ever states — an exhausted monthly resource that resets at a known
|
|
1686
|
+
* time — so it is what makes "when can I use this again" answerable for an
|
|
1687
|
+
* out-of-credit result. Every other limited outcome has no time anywhere, and
|
|
1688
|
+
* the probe reports exactly that instead of inventing one.
|
|
1689
|
+
*/
|
|
1690
|
+
async probeModels(region, modelIds, signal) {
|
|
1691
|
+
let credential;
|
|
1692
|
+
try {
|
|
1693
|
+
credential = await stacks[region].store.resolve();
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
return modelIds.map((modelId) => ({
|
|
1696
|
+
modelId,
|
|
1697
|
+
outcome: "credential-rejected",
|
|
1698
|
+
message: error instanceof Error ? error.message.slice(0, 300) : String(error).slice(0, 300)
|
|
1699
|
+
}));
|
|
1700
|
+
}
|
|
1701
|
+
const quotaRefreshAtMs = await quotaRefreshOf(region, credential);
|
|
1702
|
+
const results = [];
|
|
1703
|
+
for (const modelId of modelIds) results.push(await probeModel({
|
|
1704
|
+
client,
|
|
1705
|
+
credential,
|
|
1706
|
+
modelId,
|
|
1707
|
+
nowMs: Date.now(),
|
|
1708
|
+
...quotaRefreshAtMs === void 0 ? {} : { quotaRefreshAtMs },
|
|
1709
|
+
...signal === void 0 ? {} : { signal }
|
|
1710
|
+
}));
|
|
1711
|
+
return results;
|
|
1712
|
+
},
|
|
1396
1713
|
regionUsable(region, usable) {
|
|
1397
1714
|
if (stacks[region].catalog.setRegionUsable(usable)) invalidateCatalog();
|
|
1398
1715
|
}
|
|
1399
1716
|
}));
|
|
1717
|
+
/**
|
|
1718
|
+
* The region's nearest monthly quota refresh, or undefined when unknown.
|
|
1719
|
+
*
|
|
1720
|
+
* The monthly resource (`CapacityType 4`) is the one package that never
|
|
1721
|
+
* expires and resets on a cycle, so its `refreshAtMs` is a real answer to
|
|
1722
|
+
* "when can I use this again" after the quota runs out. The probe passes it
|
|
1723
|
+
* through; a failure to read it is not an error, because a probe without a
|
|
1724
|
+
* cooldown is still a useful probe.
|
|
1725
|
+
*/
|
|
1726
|
+
const quotaRefreshOf = async (region, credential) => {
|
|
1727
|
+
try {
|
|
1728
|
+
const refreshes = (await client.fetchCredits(credential)).packages.filter((pack) => pack.monthly && pack.refreshAtMs !== void 0).map((pack) => pack.refreshAtMs);
|
|
1729
|
+
return refreshes.length === 0 ? void 0 : Math.min(...refreshes);
|
|
1730
|
+
} catch {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1400
1734
|
ctx.inject(["settings"], (sctx) => {
|
|
1401
1735
|
const settings = sctx.settings;
|
|
1402
1736
|
ctx.effect(() => settings.configure({ auto: true }, ctx.fiber));
|
|
@@ -1513,4 +1847,4 @@ function apply(ctx, config) {
|
|
|
1513
1847
|
});
|
|
1514
1848
|
}
|
|
1515
1849
|
//#endregion
|
|
1516
|
-
export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
1850
|
+
export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, PROBE_USER_PROMPT, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
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": "2.
|
|
5
|
+
"version": "2.3.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|