dsh-connect-workbuddy 4.5.0 → 4.5.2
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 +56 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +300 -89
- package/lib/{host-heartbeat-DvcOd-3C.js → host-heartbeat-DcUM0cVn.js} +1 -1
- package/lib/index.d.ts +4 -12
- package/lib/index.js +14 -58
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.5.2 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
> 本节含三件事,**一次发布**:先修了「每行下拉框挤坏布局 + 命名看不懂」,随后维护者判定**把这个下拉框连同逐账号设置整体撤掉**;并**新增两个一键遍历按钮**(对齐同族 trae 插件)——撤掉的是「逐账号指定测试哪一个模型」,加回的是「一次把模型测一遍」,两者不冲突。4.4.6 之后没有任何以 4.5.2 发布过的产物,所以合并为一版。
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **新增两个一键遍历按钮:「一键测试启用模型」与「一键测试所有模型」**(维护者要求在对比同族 `trae` 插件后补齐;`trae` 3.5.0 提供同一对按钮)
|
|
10
|
+
- **为什么这两个按钮与「3.0.0 删掉批量按钮」不矛盾**:3.0.0 删它是因为「一次覆盖整个已勾选列表就是一次不可控支出」,并**在路由层加固**(多于 1 个模型直接 400,防止陈旧 bundle 或手写客户端把按钮变成消费循环)。这一版**保留那道闸门**,把循环放在**卡片端**:一个模型一次请求、串行、每个结果一出来就显示 —— 也就是 `trae` 的做法。路由仍然只接受 1 个模型。
|
|
11
|
+
- **成本按维护者的选择写在提示里(不做二次确认)**:每次探测都发真实体积的请求,真花积分。**本机实测**(默认 25000 token 体积)每次约 **0.01 积分**,目录 16~20 个模型 → 「所有模型」一轮约 **0.16~0.2 积分**。历史实测曾达 0.02~0.72/次(体积不同),所以提示里写明「含未启用的模型,明显更贵」而不是只给一个数。
|
|
12
|
+
- **两个按钮的范围不同,这一点是成本控制而不是措辞**:「启用模型」= 用户勾选的那几个(尊重他刚做的选择,不花在他关掉的模型上);「所有模型」= 本区域 live 目录里的每一个。取的是**可见目录**而不是配置里的任意 id,所以不可能花在只存在于配置文件里的名字上。
|
|
13
|
+
- **串行而非并发**:探测的目的就是发现限流,一次把整个名册打出去是最容易制造限流的做法 —— 与宿主自己那条循环同样的理由。
|
|
14
|
+
- **凭据被拒时立刻停止并明说**:被拒的凭据对剩下每个模型都会给出同一个答案,继续跑只是花钱重复收集同一个答案。**模型级失败(如某模型被限流)不停** —— 那是关于那个模型的信息,停下来会把它藏掉。汇总只统计**实际测到**的数量,所以提前停止的一次遍历不会读成「整册全绿」。
|
|
15
|
+
- **顺带修掉一处过期注释**:`probeModels` 的文档还在描述「宿主端一次跑完整个批量」(那是 3.0.0 之前的形态),与新的事实(路由只收 1 个、由卡片端循环)相反,已更正。
|
|
16
|
+
- **守卫**:`tests/card-model-sweep.spec.tsx`(10 例)钉住三件事:**一个请求一个模型**(断言每次 `/probe` 请求体的 `modelIds` 长度为 1)、**两个按钮范围不同**(「所有」= 全部三个;「启用」= 只有勾选的,且断言**不含**未启用的那个)、**汇总只算测到的**(一个模型被限流时不得读成全绿)。另含:无启用项时「启用」按钮置灰而「所有」仍可用、凭据被拒只发一次请求、模型级失败不中断、跑完按钮恢复可用。**变异验证**:把「所有模型」改成只遍历启用项 → 2 例红;去掉凭据被拒的提前停止 → 2 例红。
|
|
17
|
+
- **布局**:模型区头部现在有三个按钮(刷新 + 两个遍历),中文标签较长(「一键测试所有模型」),所以按钮组允许换行且标签 `nowrap` —— 与之前账号池行那个「测试按钮被拆成两行」是同一类失败,这次先防住。
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Fixes(同一次发布,见上)
|
|
21
|
+
|
|
22
|
+
- **(已随本节撤除)账号池每行那个「测试模型」下拉框把行挤坏了:「测试」按钮被压成一列一个字(「测」/「试」),选项名也让人看不懂。**(维护者看截图发现,属 4.5.0 的 #61 改动引入)
|
|
23
|
+
- **布局**:那一行的动作列是按「只有两个按钮」的宽度设计的网格轨道(有签到列时仅 `.8fr`),4.5.0 往里塞了一个**约 150px、不可再压缩**的下拉框,于是按钮被挤到只剩一个字符宽 —— 每一行都变成两行「测 / 试」。**注意:这个缺陷的修法(加最小宽度轨道等)随后连同下拉框一起被撤掉了** —— 保留下来的是 `white-space:nowrap`,它独立成立:这个动作列的宽度是表格的一个分数轨,一个固定的两字标签(测试)不该被拆行,窄窗口下**裁剪**才是正确的失败方式。
|
|
24
|
+
- **文案**:那个下拉框的第一个选项原本叫**「区域目标」** —— 维护者看到它直接问「这目标区域是做什么用的?」,这本身就是命名失败的证据:它读起来像一个**区域**,而它表达的是「**跟随账号池的目标模型**」。当时先改为**「同账号池」**(en: `same as pool`)并把 `title` 重写为**先回答「它是做什么的」**;随后整个下拉框被撤除,这两个字符串也一并删掉了。**留下的教训是命名那条**:一个选项名如果让人需要问「这是做什么用的」,那它写的就是「值从哪来」而不是「它做什么」。
|
|
25
|
+
- **为什么这件事值得记**:这两处都不是逻辑错,而是**只有人看界面才会发现**的错 —— 自动测试全绿、CI 全绿、发布会成功。它提醒本仓库的验证手段(源码级守卫 + 变异验证)覆盖不了「读起来对不对、排得下排不下」,而这两类恰恰需要真机截图或使用者提问才能暴露。
|
|
26
|
+
- **守卫**:`tests/client-host-contract.spec.ts` 新增一例钉住 `nowrap`;随撤除一并改写为「不再为已移除的下拉框预留宽度」(见下)。**变异验证**:删掉 `nowrap` → 该例变红。
|
|
27
|
+
|
|
28
|
+
### 撤除
|
|
29
|
+
|
|
30
|
+
- **撤掉「逐账号测试模型」(`pool.targetModelIdByAccount`)—— 它是 4.4.6 采纳报告者提议时加进来的,维护者判定应撤。**(issue #61 的后续)
|
|
31
|
+
- **为什么撤**:**池设置里本来就有统一的测试模型**(`pool.targetModelId`),而报告的诉求(「你用 `step5` 就要测这个模型」)用它就能达成 —— 报告者当时把它配成 `hy4-preview-f`(实际在用 `step-5-preview`),**根因是那个统一设置指错了模型,不是缺少逐账号能力**。
|
|
32
|
+
- **更重要的理由**:逐账号让**同一张表的「全绿」不再可比** —— A 的 `ok` 是模型 X 上的、B 的 `ok` 是模型 Y 上的,而「全绿不代表可用」正是这个 issue 抱怨的东西。为了「方便对比某个号」引入一个削弱「整表可比」的设置,方向是反的。
|
|
33
|
+
- **撤掉的东西**:配置字段 `pool.targetModelIdByAccount`(含 schema)、`targetModelPreference()`、批量路由的**逐账号解析器**(`testAllAccounts` 回到「整批一个模型」的 `string` 形态,`WorkBuddyPoolDeps.test` 的联合类型一并收窄)、池文件与 usage 载荷里的逐账号映射、每行那个「测试模型」下拉框(含 `setRowModel`、draft 比较、写入与**写入后校验**、三个 locale 键)、以及为容纳它而加的网格最小宽度。
|
|
34
|
+
- **完整保留的东西(#61 里真正解决问题的那一半,不需要任何新配置)**:
|
|
35
|
+
1. 探针记录**写入 `modelId`**(`probeModel()` 一直返回它,此前落盘被丢掉),所以每行的结论**自带适用范围**;批量与实时聊天失败两条路径都写;
|
|
36
|
+
2. 每行**显示**「模型 X」(`poolProbeModel`),以及未记录时的显式说明;
|
|
37
|
+
3. `resolveTargetModel` 的**陈旧目标如实报告、绝不静默替换**(指向已下架模型时报 `stale`,不换一个用户没选的模型去花积分)。
|
|
38
|
+
- **升级影响**:老配置里若已写了 `targetModelIdByAccount`,升级后**被忽略**(未知字段),池的实际测试模型回到 `targetModelId` —— 也就是「所有号测同一个模型」,这正是撤除的目的。**不影响聊天用哪个模型**(那个设置从来不参与选路)。
|
|
39
|
+
- **守卫**:`tests/issue61-probe-model-scope.spec.ts` 重写为钉住「保留的那一半」+「整批只能一个模型」(`probes every account on the SAME model` 直接断言两次探测拿到同一个 id,若有人把它改回逐账号,这条会红);`tests/pool-route.spec.ts` 删掉逐账号解析器的路由断言与 `askedModel()` 联合类型收窄辅助函数;`tests/client-host-contract.spec.ts` 的布局守卫改写为「不再为已移除的下拉框预留宽度」(并保留 `nowrap`,它与下拉框无关、独立成立)。
|
|
40
|
+
- 全量 **1249 通过 / 2 跳过**。
|
|
41
|
+
|
|
42
|
+
## 4.5.1 (2026-10-14)
|
|
43
|
+
|
|
44
|
+
### Fixes
|
|
45
|
+
|
|
46
|
+
- **`6004`(账号频率超限)在对话里被显示成「当前请求的额度已用尽」,而账号余额还剩 6,788 —— 现在那句话下面补了一句说明。**(issue #63;维护者判定:**保持不进重试**,只补文案)
|
|
47
|
+
- **根因(实测,不是读代码猜的)**:宿主对失败按**码**路由,而插件这条链是 —— shim 的错误响应 → pi-ai 抛出的 message → `classifyPiAiError(message)` → 宿主码 → 客户端文案。三个环节的出货代码取出来直接跑之后:`quota_exceeded` 这个拼写里的下划线**恰好命中** `@deepseek-ai/dsh-llm` 的 `isQuotaExceededError` 正则(`quota[\s_-]+exceeded` 的 `[\s_-]+` 能匹配 `_`),于是归成 `QUOTA`;而 `@deepseek-ai/dsh-client-ui-chat` 对 `QUOTA`/`ACCOUNT_QUOTA` **硬编码**成那句固定文案、并**丢弃**插件自己的 message。**这一版之前我在 #56/#61 里把它说成「插件挑了宿主认识的词」,那个解释是错的。**
|
|
48
|
+
- **为什么不改分类(维护者的决定,也是实测支持的)**:把拼写换成任何一个不再被 quota 正则命中的值,都会落进 `RATE_LIMIT` —— 而它在 `DEFAULT_RETRYABLE_CODES` 里(`EMPTY_RESPONSE`/`RATE_LIMIT`/`SERVER`/`TIMEOUT`/`TRANSPORT`)。按出货默认(`initialDelayMs=500`、指数退避、封顶 10s、`maxRetries=5`)这是**5 次原地重试、标称 15.5 秒退避**,而 `6004` 的恢复点在报文里明写了是**几小时后** —— 退避窗口内不可能恢复。所以「不进重试」保留,「那句话不够准确」用**补充**解决。
|
|
49
|
+
- **修法**:用宿主**专门为此留的扩展点** `shell.quota-notice`(链式插槽,其自有文档写着「the first entry whose selector claims its code takes over the surface」)。本插件注册一条 `select: owner => code === 'QUOTA' ? owner : null` 的链项:
|
|
50
|
+
- **只 claim `QUOTA`** —— 其余码(`AUTH`、`ACCOUNT_SIGNED_OUT`、`ACCOUNT_QUOTA` 等)继续走宿主自己的兜底 Toast,本插件不接管自己说不清的提示;
|
|
51
|
+
- **宿主那句话原样保留**(`matched.message`),补充是**第二行**而不是改写 —— 那句话在别处也显示,重写会让插件与 shell 的用词互相矛盾;
|
|
52
|
+
- 补充文案:**「部分模型另有每日额度限制,与积分余额无关 —— 也可能确实是本账号积分耗尽。账号池那一行会写明上游给出的恢复时刻;也可以先换个账号或换个模型继续。」** 措辞对 `QUOTA` 覆盖的**两种**情况都成立,因为上游是用中文散文区分的,而插件不允许解析它来下政策判断;中英双语都写。
|
|
53
|
+
- 带一个「知道了」按钮,调用宿主自己的 `dismiss`;宿主没给 `dismiss` 时只渲染文字而不崩 —— 这是**全窗口**的提示,在这里抛错会连别的插件的提示一起带走。
|
|
54
|
+
- **组件用本插件自己的 DOM 与样式类**,不引 `@deepseek-ai/dsh-client-ui-primitives`:客户端刻意不依赖它(那条 stale 的 `inject` 曾让整张卡片的到达依赖上宿主最易变的包,见 `tests/client-host-contract.spec.ts` 的说明),所以这里沿用本插件其它组件的做法。
|
|
55
|
+
- **实机核对**:构建后用 Slot 检查器读运行中的插槽树,`shell.quota-notice` 的 occupants 已包含 `dsh-connect-workbuddy-client`(`active: true`),与宿主自己的 `mf` 并列;插槽目录给出的 `ownerProps`(`code`/`message`/`dismiss`/`keepOpen`)与组件读取的字段一致。
|
|
56
|
+
- **守卫**:`tests/issue63-quota-notice.spec.tsx`(7 例)覆盖:claim 恰好是 `QUOTA`、其余码(含 `QUOTA_EXCEEDED`、空串、数字、对象)一律不 claim、宿主句子原样保留且补充可见、dismiss 按钮调用宿主回调、无 `dismiss` 时不崩、宿主没给 message 时回退到自己的标题。**变异验证**:把 claim 扩到 `AUTH`/`ACCOUNT_QUOTA` → 2 例红;删掉补充那一行 → 2 例红。
|
|
57
|
+
- **未做**:没有改任何分类/HTTP 状态,也没有动 `KIND_HOST_CODE` 里 `soft_rate`/`model_busy` → `quota_exceeded` 的翻译(那正是 #56 修复的核心)。
|
|
58
|
+
|
|
3
59
|
## 4.5.0 (2026-10-14)
|
|
4
60
|
|
|
5
61
|
> 版本号是 **minor** 而不是 patch:这一版新增了一个**用户可见的设置**(积分读数的位置)和一个**新的渲染席位**,而且配置 schema 多了一个字段 —— 按语义化版本这是特性,不是修错。`4.4.6` 之后没有任何以 4.4.7 发布过的产物或 Release(tag 已撤回重打)。
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-
|
|
2
|
+
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-DcUM0cVn.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
|
@@ -692,8 +692,6 @@ window.__ModuleLoader__.load({
|
|
|
692
692
|
targetModelId: preferences.targetModelId,
|
|
693
693
|
memberAccountIds: preferences.memberAccountIds
|
|
694
694
|
};
|
|
695
|
-
const overrides = Object.entries(preferences.targetModelIdByAccount ?? {}).filter(([, modelId]) => modelId !== "");
|
|
696
|
-
if (overrides.length > 0) slot["targetModelIdByAccount"] = Object.fromEntries(overrides);
|
|
697
695
|
if (preferences.probeInputTokens !== void 0) slot["probeInputTokens"] = preferences.probeInputTokens;
|
|
698
696
|
const next = nextRegionPool(scope.getSnapshot().value, region, slot)[region];
|
|
699
697
|
await writeField(scope, "regions", region, next, (readBack) => {
|
|
@@ -706,11 +704,7 @@ window.__ModuleLoader__.load({
|
|
|
706
704
|
if (!Array.isArray(stored) || stored.length !== preferences.memberAccountIds.length) return false;
|
|
707
705
|
const wanted = new Set(preferences.memberAccountIds);
|
|
708
706
|
if (!stored.every((id) => typeof id === "string" && wanted.has(id))) return false;
|
|
709
|
-
|
|
710
|
-
if (overrides.length === 0) return storedModels === void 0 || typeof storedModels === "object" && storedModels !== null && !Array.isArray(storedModels) && Object.keys(storedModels).length === 0;
|
|
711
|
-
if (typeof storedModels !== "object" || storedModels === null || Array.isArray(storedModels)) return false;
|
|
712
|
-
const storedMap = storedModels;
|
|
713
|
-
return Object.keys(storedMap).length === overrides.length && overrides.every(([accountId, modelId]) => storedMap[accountId] === modelId);
|
|
707
|
+
return true;
|
|
714
708
|
});
|
|
715
709
|
}
|
|
716
710
|
//#endregion
|
|
@@ -1297,21 +1291,6 @@ window.__ModuleLoader__.load({
|
|
|
1297
1291
|
const wanted = new Set(right);
|
|
1298
1292
|
return left.every((id) => wanted.has(id));
|
|
1299
1293
|
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Whether two per-account test-model maps are equal (issue #61).
|
|
1302
|
-
*
|
|
1303
|
-
* By CONTENT, not by size: moving an account from one model to another keeps the
|
|
1304
|
-
* count and changes the meaning, and clearing an entry is a change a size
|
|
1305
|
-
* comparison would miss entirely when another account was added at the same
|
|
1306
|
-
* time. Order is irrelevant for the same reason it is in {@link sameIds} — the
|
|
1307
|
-
* map is written by key, not by click sequence.
|
|
1308
|
-
*/
|
|
1309
|
-
function sameModelOverrides(left, right) {
|
|
1310
|
-
const leftKeys = Object.keys(left);
|
|
1311
|
-
const rightKeys = Object.keys(right);
|
|
1312
|
-
if (leftKeys.length !== rightKeys.length) return false;
|
|
1313
|
-
return leftKeys.every((key) => left[key] === right[key]);
|
|
1314
|
-
}
|
|
1315
1294
|
/** Whether a probe outcome means the model answered. */
|
|
1316
1295
|
function outcomeOk(outcome) {
|
|
1317
1296
|
return outcome === "ok";
|
|
@@ -1398,8 +1377,7 @@ window.__ModuleLoader__.load({
|
|
|
1398
1377
|
enabled: pool?.enabled ?? false,
|
|
1399
1378
|
targetModelId: pool?.targetModelId || pool?.staleTargetModelId || "",
|
|
1400
1379
|
probeInputTokens: pool?.probeInputTokens ?? 0,
|
|
1401
|
-
memberAccountIds: pool?.memberAccountIds ?? []
|
|
1402
|
-
targetModelIdByAccount: { ...pool?.targetModelIdByAccount }
|
|
1380
|
+
memberAccountIds: pool?.memberAccountIds ?? []
|
|
1403
1381
|
};
|
|
1404
1382
|
const active = draftRegion === region && draft !== null ? draft : saved;
|
|
1405
1383
|
/**
|
|
@@ -1420,7 +1398,7 @@ window.__ModuleLoader__.load({
|
|
|
1420
1398
|
* the pending edit.
|
|
1421
1399
|
*/
|
|
1422
1400
|
const poolRunning = saved.enabled;
|
|
1423
|
-
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !
|
|
1401
|
+
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1424
1402
|
/**
|
|
1425
1403
|
* Membership that actually resolves to a local sign-in — what a batch runs on.
|
|
1426
1404
|
*
|
|
@@ -1581,25 +1559,6 @@ window.__ModuleLoader__.load({
|
|
|
1581
1559
|
};
|
|
1582
1560
|
});
|
|
1583
1561
|
}, [editDraft]);
|
|
1584
|
-
/**
|
|
1585
|
-
* Set ONE account's test model (issue #61).
|
|
1586
|
-
*
|
|
1587
|
-
* `''` REMOVES the entry rather than storing an empty string: "follow the
|
|
1588
|
-
* region target" is the absence of a choice, which is the same shape the host
|
|
1589
|
-
* reads and the same shape an untouched profile has — so nothing about an
|
|
1590
|
-
* older profile changes when this setting is never used.
|
|
1591
|
-
*/
|
|
1592
|
-
const setRowModel = (0, react.useCallback)((accountId, modelId) => {
|
|
1593
|
-
editDraft((current) => {
|
|
1594
|
-
const next = { ...current.targetModelIdByAccount };
|
|
1595
|
-
if (modelId === "") delete next[accountId];
|
|
1596
|
-
else next[accountId] = modelId;
|
|
1597
|
-
return {
|
|
1598
|
-
...current,
|
|
1599
|
-
targetModelIdByAccount: next
|
|
1600
|
-
};
|
|
1601
|
-
});
|
|
1602
|
-
}, [editDraft]);
|
|
1603
1562
|
/** Check every listed account, or none of them. */
|
|
1604
1563
|
const selectAll = (0, react.useCallback)((next) => {
|
|
1605
1564
|
editDraft((current) => ({
|
|
@@ -1920,10 +1879,7 @@ window.__ModuleLoader__.load({
|
|
|
1920
1879
|
},
|
|
1921
1880
|
onRemove: () => {
|
|
1922
1881
|
runRowRemove(account.accountId, account.accountName);
|
|
1923
|
-
}
|
|
1924
|
-
modelOverride: active.targetModelIdByAccount[account.accountId] ?? "",
|
|
1925
|
-
catalog: pool.catalog ?? [],
|
|
1926
|
-
onModelChange: (modelId) => setRowModel(account.accountId, modelId)
|
|
1882
|
+
}
|
|
1927
1883
|
}))), effectiveMembers.length === 0 ? (0, react.createElement)("p", {
|
|
1928
1884
|
className: "dsm-workbuddy-pool-warn",
|
|
1929
1885
|
role: "status"
|
|
@@ -1991,7 +1947,6 @@ window.__ModuleLoader__.load({
|
|
|
1991
1947
|
/** One account's row. */
|
|
1992
1948
|
function renderAccountRow(input) {
|
|
1993
1949
|
const { t, account, checked, canEdit, checkinSupported, onToggle, busy, testDisabled, targetHint, onTest, onRemove } = input;
|
|
1994
|
-
const { modelOverride, catalog, onModelChange } = input;
|
|
1995
1950
|
const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
|
|
1996
1951
|
const excluded = exclusionText(t, account.excludedBy);
|
|
1997
1952
|
const probe = account.probe;
|
|
@@ -2032,20 +1987,7 @@ window.__ModuleLoader__.load({
|
|
|
2032
1987
|
}, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-main" }, probeLine), probeLabel === void 0 && age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-prov" }, probeLabel === void 0 ? null : (0, react.createElement)("span", { className: probeLabel.className }, probeLabel.text), probeLabel === void 0 || age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-age" }, age), probe === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-model" }, probe.modelId === void 0 ? t("row.poolProbeModelUnknown") : t("row.poolProbeModel", { model: probe.modelId })))), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
|
|
2033
1988
|
className: "dsm-workbuddy-pool-unknown",
|
|
2034
1989
|
title: t("row.poolNeverTested")
|
|
2035
|
-
}, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-row-actions" }, (0, react.createElement)("
|
|
2036
|
-
className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-row-model",
|
|
2037
|
-
value: modelOverride,
|
|
2038
|
-
disabled: !canEdit,
|
|
2039
|
-
title: t("row.poolRowModelHint"),
|
|
2040
|
-
"aria-label": t("row.poolRowModelAria", { accountName: name }),
|
|
2041
|
-
onChange: (event) => onModelChange(event.currentTarget.value)
|
|
2042
|
-
}, (0, react.createElement)("option", { value: "" }, t("row.poolRowModelInherit")), modelOverride !== "" && !catalog.some((model) => model.id === modelOverride) ? (0, react.createElement)("option", {
|
|
2043
|
-
value: modelOverride,
|
|
2044
|
-
disabled: true
|
|
2045
|
-
}, t("row.poolTargetStaleOption", { model: modelOverride })) : null, ...catalog.map((model) => (0, react.createElement)("option", {
|
|
2046
|
-
value: model.id,
|
|
2047
|
-
key: model.id
|
|
2048
|
-
}, model.name))), (0, react.createElement)("button", {
|
|
1990
|
+
}, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-row-actions" }, (0, react.createElement)("button", {
|
|
2049
1991
|
type: "button",
|
|
2050
1992
|
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
2051
1993
|
disabled: busy || testDisabled,
|
|
@@ -3081,6 +3023,13 @@ window.__ModuleLoader__.load({
|
|
|
3081
3023
|
/* Its OWN label class, deliberately not the switch's: a query for "the
|
|
3082
3024
|
sidebar-credit switch" must not be able to pick this control up, and the two
|
|
3083
3025
|
are different widgets (a checkbox and a two-way picker). */
|
|
3026
|
+
/* The supplement to the host's fixed quota sentence (issue #63). It sits in the
|
|
3027
|
+
frame-wide overlay seat, so it must read on its own and stay narrow: a column
|
|
3028
|
+
with the host's sentence first (verbatim) and the supplement under it. */
|
|
3029
|
+
.dsm-workbuddy-quota-notice{display:flex;flex-direction:column;gap:4px;max-width:420px;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,#23242a);border:1px solid var(--dsw-alias-border-l1,#2c2d31);font-size:12px;line-height:17px}
|
|
3030
|
+
.dsm-workbuddy-quota-notice-head{font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
3031
|
+
.dsm-workbuddy-quota-notice-body{color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3032
|
+
.dsm-workbuddy-quota-notice-dismiss{align-self:flex-start;margin-top:2px;padding:2px 10px;font-size:12px}
|
|
3084
3033
|
.dsm-workbuddy-credit-position-switch{display:inline-flex;align-items:center;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3085
3034
|
.dsm-workbuddy-credit-position{max-width:150px;padding:2px 6px;font-size:12px}
|
|
3086
3035
|
.dsm-workbuddy-credits-switch-row .dsm-workbuddy-credit-position-switch{margin-left:12px}
|
|
@@ -3153,7 +3102,16 @@ window.__ModuleLoader__.load({
|
|
|
3153
3102
|
meta line, because a cooldown sentence ("rate limited - the upstream gave no
|
|
3154
3103
|
time") does not fit next to the context/output chips, and truncating it would
|
|
3155
3104
|
hide exactly the part the user needs. */
|
|
3156
|
-
|
|
3105
|
+
/* Three buttons now live here (refresh + the two sweeps), and the sweep labels are
|
|
3106
|
+
long in Chinese (一键测试所有模型). flex:none alone let the group run past the
|
|
3107
|
+
header on a narrow card, so the group may WRAP and each label refuses to break
|
|
3108
|
+
mid-word — the same failure the pool row's action buttons had. */
|
|
3109
|
+
.dsm-workbuddy-models-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
|
|
3110
|
+
.dsm-workbuddy-models-head-actions .dsm-btn{white-space:nowrap}
|
|
3111
|
+
/* The sweep buttons keep the outline weight of the refresh button beside them, so
|
|
3112
|
+
the header reads as one group of equal actions. */
|
|
3113
|
+
.dsm-workbuddy-models-sweep{white-space:nowrap}
|
|
3114
|
+
.dsm-workbuddy-models-sweep-summary{margin:6px 0 0;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3157
3115
|
.dsm-workbuddy-model-probe{flex:none;padding:3px 10px;font-size:11px;line-height:16px}
|
|
3158
3116
|
.dsm-workbuddy-model-probe-result{margin:0;font-size:12px;line-height:18px;word-break:break-word}
|
|
3159
3117
|
.dsm-workbuddy-model-probe-result-ok{color:var(--dsw-alias-state-success-primary,#22a06b)}
|
|
@@ -3294,11 +3252,16 @@ window.__ModuleLoader__.load({
|
|
|
3294
3252
|
.dsm-workbuddy-pool-log-info{color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3295
3253
|
.dsm-workbuddy-pool-log-empty{padding:14px;font-size:12px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
3296
3254
|
/* Per-row actions (test / remove), right side of each pool row. */
|
|
3255
|
+
/* Per-row actions (test / remove), right side of each pool row.
|
|
3256
|
+
The nowrap is NOT cosmetic: a two-character label (测试) belongs on one line,
|
|
3257
|
+
and this row's track is a fraction of the table — in a narrow window it let
|
|
3258
|
+
the label break into 测 / 试. Clipping is the right failure for a button
|
|
3259
|
+
whose label is fixed and tiny. */
|
|
3297
3260
|
.dsm-workbuddy-pool-row-actions{display:flex;gap:6px;align-items:center;justify-content:flex-end;flex:none}
|
|
3261
|
+
.dsm-workbuddy-pool-row-actions .dsm-workbuddy-pool-small-btn{white-space:nowrap}
|
|
3298
3262
|
.dsm-workbuddy-pool-row-actions .dsm-workbuddy-pool-small-btn{padding:2px 10px;font-size:12px}
|
|
3299
3263
|
/* This account's own test model (issue #61). Narrower than the section-level
|
|
3300
3264
|
picker so a long model name cannot push the two buttons out of the row. */
|
|
3301
|
-
.dsm-workbuddy-pool-row-model{max-width:150px;padding:2px 6px;font-size:12px}
|
|
3302
3265
|
@media (max-width:760px){.dsm-workbuddy-pool-row{grid-template-columns:1fr 1fr;row-gap:6px}.dsm-workbuddy-pool-select{max-width:180px}}
|
|
3303
3266
|
/* Pool switch, namespaced under .dsm-workbuddy-pool so it cannot collide with host styles. */
|
|
3304
3267
|
.dsm-workbuddy-pool .sw{position:relative;display:inline-block;width:38px;height:22px;flex:none;cursor:pointer}
|
|
@@ -3932,6 +3895,19 @@ window.__ModuleLoader__.load({
|
|
|
3932
3895
|
const [probing, setProbing] = (0, react.useState)({});
|
|
3933
3896
|
/** Probe failure that is not attributable to one model (a dead route, say). */
|
|
3934
3897
|
const [probeError, setProbeError] = (0, react.useState)(void 0);
|
|
3898
|
+
/**
|
|
3899
|
+
* The active region's probe results, mirrored into a ref.
|
|
3900
|
+
*
|
|
3901
|
+
* The sweep loop reads each verdict as soon as it lands, and state read inside
|
|
3902
|
+
* an async loop is the value captured when the loop STARTED — so counting from
|
|
3903
|
+
* `probes` would tally the previous run's answers. The ref is updated in the
|
|
3904
|
+
* same place the state is, so the two cannot drift.
|
|
3905
|
+
*/
|
|
3906
|
+
const probesRef = (0, react.useRef)({});
|
|
3907
|
+
/** Progress of an in-flight sweep, or undefined when none is running. */
|
|
3908
|
+
const [sweeping, setSweeping] = (0, react.useState)(void 0);
|
|
3909
|
+
/** The last sweep's tally, cleared when the next one starts. */
|
|
3910
|
+
const [sweepSummary, setSweepSummary] = (0, react.useState)(void 0);
|
|
3935
3911
|
const mounted = (0, react.useRef)(true);
|
|
3936
3912
|
(0, react.useEffect)(() => {
|
|
3937
3913
|
mounted.current = true;
|
|
@@ -4360,13 +4336,16 @@ window.__ModuleLoader__.load({
|
|
|
4360
4336
|
});
|
|
4361
4337
|
};
|
|
4362
4338
|
/**
|
|
4363
|
-
* Probe
|
|
4339
|
+
* Probe ONE model (the route refuses more than one — see `web-status.ts`,
|
|
4340
|
+
* where the cap is the gate that keeps a whole-roster sweep from becoming a
|
|
4341
|
+
* single unaccounted spend). The HOST owns the request: the browser half sends
|
|
4342
|
+
* only a model id and gets an outcome back, because the credential must never
|
|
4343
|
+
* cross to the page.
|
|
4364
4344
|
*
|
|
4365
|
-
* The
|
|
4366
|
-
*
|
|
4367
|
-
*
|
|
4368
|
-
*
|
|
4369
|
-
* race the shared `probing` map and make the batch button's state unknowable.
|
|
4345
|
+
* The plural name is historical: this used to post a whole list in one request
|
|
4346
|
+
* before 3.0.0 removed the batch button. It is kept because callers that
|
|
4347
|
+
* already hold a list (the two sweep buttons below) can still express "test
|
|
4348
|
+
* these" without a second code path — it just sends them one at a time.
|
|
4370
4349
|
*
|
|
4371
4350
|
* Probing is deliberately independent of `dirty`: a test answers "does this
|
|
4372
4351
|
* model work right now", which is true of the SAVED selection and the draft
|
|
@@ -4394,9 +4373,12 @@ window.__ModuleLoader__.load({
|
|
|
4394
4373
|
const body = await response.json().catch(() => void 0);
|
|
4395
4374
|
if (!response.ok || !Array.isArray(body?.results)) throw new Error(body?.error ?? `HTTP ${String(response.status)}`);
|
|
4396
4375
|
if (!mounted.current) return;
|
|
4376
|
+
const landedResults = body.results;
|
|
4377
|
+
probesRef.current = { ...probesRef.current };
|
|
4378
|
+
for (const result of landedResults) probesRef.current[result.modelId] = result;
|
|
4397
4379
|
setProbes((previous) => {
|
|
4398
4380
|
const regionResults = { ...previous[activeRegion] ?? {} };
|
|
4399
|
-
for (const result of
|
|
4381
|
+
for (const result of landedResults) regionResults[result.modelId] = result;
|
|
4400
4382
|
return {
|
|
4401
4383
|
...previous,
|
|
4402
4384
|
[activeRegion]: regionResults
|
|
@@ -4412,6 +4394,86 @@ window.__ModuleLoader__.load({
|
|
|
4412
4394
|
});
|
|
4413
4395
|
}
|
|
4414
4396
|
};
|
|
4397
|
+
/**
|
|
4398
|
+
* ONE-CLICK availability sweep over several models (issue #61 follow-up, by
|
|
4399
|
+
* request of the maintainer after comparing with the sibling `trae` plugin).
|
|
4400
|
+
*
|
|
4401
|
+
* WHY IT IS A CLIENT-SIDE LOOP. The route accepts exactly ONE model id and
|
|
4402
|
+
* refuses more, and that cap is deliberate: it is what stops a stale bundle or
|
|
4403
|
+
* a hand-rolled client from turning one click into an unaccounted spend over a
|
|
4404
|
+
* whole roster (3.0.0's reason for deleting the original batch button). A
|
|
4405
|
+
* sweep is therefore expressed the way trae expresses it — one model per
|
|
4406
|
+
* request, from here — which keeps that gate intact and has a second benefit
|
|
4407
|
+
* the host-side batch never had: each row's verdict appears the moment it is
|
|
4408
|
+
* known, instead of the card sitting blank until the slowest model answers.
|
|
4409
|
+
*
|
|
4410
|
+
* COST is stated in the button's own hint rather than in a confirmation
|
|
4411
|
+
* dialog: a probe sends a real-volume request, so a sweep over a whole catalog
|
|
4412
|
+
* spends real credit (measured 0.01 per probe at the current default size, on
|
|
4413
|
+
* this machine — see the hint text). The user asked for the hint form, and it
|
|
4414
|
+
* matches the sibling plugin.
|
|
4415
|
+
*
|
|
4416
|
+
* SEQUENTIAL, not parallel: a probe exists to discover rate limits, and firing
|
|
4417
|
+
* a roster's worth of requests at once is the surest way to create one — the
|
|
4418
|
+
* same reason the host's own loop was serial.
|
|
4419
|
+
*
|
|
4420
|
+
* STOPS ON THE FIRST AUTH FAILURE: a rejected credential answers identically
|
|
4421
|
+
* for every remaining model, so continuing would only spend credit to collect
|
|
4422
|
+
* the same answer again. Other failures (a rate limit on one model) do NOT
|
|
4423
|
+
* stop the sweep — that is information about that model, and stopping would
|
|
4424
|
+
* hide it.
|
|
4425
|
+
*
|
|
4426
|
+
* The summary counts what the run actually established, so a stopped or
|
|
4427
|
+
* failed sweep cannot read as a complete green roster.
|
|
4428
|
+
*/
|
|
4429
|
+
const sweepModels = async (modelIds) => {
|
|
4430
|
+
if (modelIds.length === 0) return;
|
|
4431
|
+
setProbeError(void 0);
|
|
4432
|
+
setSweepSummary(void 0);
|
|
4433
|
+
setSweeping({
|
|
4434
|
+
done: 0,
|
|
4435
|
+
total: modelIds.length
|
|
4436
|
+
});
|
|
4437
|
+
let available = 0;
|
|
4438
|
+
let measured = 0;
|
|
4439
|
+
try {
|
|
4440
|
+
for (const [index, modelId] of modelIds.entries()) {
|
|
4441
|
+
if (!mounted.current) return;
|
|
4442
|
+
await probeModels([modelId]);
|
|
4443
|
+
const result = probesRef.current[modelId];
|
|
4444
|
+
measured += 1;
|
|
4445
|
+
if (result?.outcome === "ok") available += 1;
|
|
4446
|
+
if (result?.outcome === "credential-rejected") {
|
|
4447
|
+
if (mounted.current) setProbeError(t("row.modelsTestAuth"));
|
|
4448
|
+
break;
|
|
4449
|
+
}
|
|
4450
|
+
if (mounted.current) setSweeping({
|
|
4451
|
+
done: index + 1,
|
|
4452
|
+
total: modelIds.length
|
|
4453
|
+
});
|
|
4454
|
+
}
|
|
4455
|
+
if (mounted.current) setSweepSummary(t("row.modelsTestDone", {
|
|
4456
|
+
available,
|
|
4457
|
+
total: measured
|
|
4458
|
+
}));
|
|
4459
|
+
} finally {
|
|
4460
|
+
if (mounted.current) setSweeping(void 0);
|
|
4461
|
+
}
|
|
4462
|
+
};
|
|
4463
|
+
/** Sweep the models this region has ENABLED (the current draft's selection). */
|
|
4464
|
+
const sweepEnabledModels = () => {
|
|
4465
|
+
sweepModels(visibleModels.filter((model) => activeEnabledIds.has(model.id)).map((model) => model.id));
|
|
4466
|
+
};
|
|
4467
|
+
/**
|
|
4468
|
+
* Sweep EVERY model in the region's live catalog, enabled or not.
|
|
4469
|
+
*
|
|
4470
|
+
* Deliberately the whole VISIBLE catalog and nothing else: those are the ids
|
|
4471
|
+
* the host can validate against its own catalog, so this cannot spend credit
|
|
4472
|
+
* on an id that only exists in a config file.
|
|
4473
|
+
*/
|
|
4474
|
+
const sweepAllModels = () => {
|
|
4475
|
+
sweepModels(visibleModels.map((model) => model.id));
|
|
4476
|
+
};
|
|
4415
4477
|
const saveModels = async () => {
|
|
4416
4478
|
if (settingsScope === void 0) return;
|
|
4417
4479
|
if (status.status !== "signed-in") return;
|
|
@@ -4724,17 +4786,41 @@ window.__ModuleLoader__.load({
|
|
|
4724
4786
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4725
4787
|
className: "dsm-workbuddy-models-summary",
|
|
4726
4788
|
children: t("row.modelsSummary", { count: activeEnabledIds.size })
|
|
4727
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4789
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4728
4790
|
className: "dsm-workbuddy-models-head-actions",
|
|
4729
|
-
children:
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4791
|
+
children: [
|
|
4792
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4793
|
+
type: "button",
|
|
4794
|
+
className: "dsm-btn dsm-btn-outline",
|
|
4795
|
+
disabled: busy,
|
|
4796
|
+
onClick: () => {
|
|
4797
|
+
refreshModels();
|
|
4798
|
+
},
|
|
4799
|
+
children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
|
|
4800
|
+
}),
|
|
4801
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4802
|
+
type: "button",
|
|
4803
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-models-sweep",
|
|
4804
|
+
disabled: busy || sweeping !== void 0 || activeEnabledIds.size === 0,
|
|
4805
|
+
title: t("row.modelsTestEnabledHint"),
|
|
4806
|
+
onClick: sweepEnabledModels,
|
|
4807
|
+
children: sweeping === void 0 ? t("row.modelsTestEnabled") : t("row.modelsTesting", {
|
|
4808
|
+
done: sweeping.done,
|
|
4809
|
+
total: sweeping.total
|
|
4810
|
+
})
|
|
4811
|
+
}),
|
|
4812
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4813
|
+
type: "button",
|
|
4814
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-models-sweep",
|
|
4815
|
+
disabled: busy || sweeping !== void 0 || visibleModels.length === 0,
|
|
4816
|
+
title: t("row.modelsTestAllHint"),
|
|
4817
|
+
onClick: sweepAllModels,
|
|
4818
|
+
children: sweeping === void 0 ? t("row.modelsTestAll") : t("row.modelsTesting", {
|
|
4819
|
+
done: sweeping.done,
|
|
4820
|
+
total: sweeping.total
|
|
4821
|
+
})
|
|
4822
|
+
})
|
|
4823
|
+
]
|
|
4738
4824
|
})]
|
|
4739
4825
|
}),
|
|
4740
4826
|
probeError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -4742,6 +4828,11 @@ window.__ModuleLoader__.load({
|
|
|
4742
4828
|
role: "alert",
|
|
4743
4829
|
children: t("row.probeError", { message: probeError })
|
|
4744
4830
|
}),
|
|
4831
|
+
sweepSummary === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4832
|
+
className: "dsm-workbuddy-models-sweep-summary",
|
|
4833
|
+
role: "status",
|
|
4834
|
+
children: sweepSummary
|
|
4835
|
+
}),
|
|
4745
4836
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4746
4837
|
className: "dsm-workbuddy-model-list",
|
|
4747
4838
|
children: visibleModels.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5574,6 +5665,77 @@ window.__ModuleLoader__.load({
|
|
|
5574
5665
|
});
|
|
5575
5666
|
}
|
|
5576
5667
|
//#endregion
|
|
5668
|
+
//#region src/client/WorkBuddyQuotaNotice.tsx
|
|
5669
|
+
/**
|
|
5670
|
+
* The supplement this plugin adds to the host's quota notice (issue #63).
|
|
5671
|
+
*
|
|
5672
|
+
* WHY THIS EXISTS. When an upstream failure is classified `QUOTA`, the host
|
|
5673
|
+
* renders ONE fixed sentence — 「当前请求的额度已用尽」 — and (measured on the
|
|
5674
|
+
* shipped client) it DISCARDS the provider's own message to do so:
|
|
5675
|
+
*
|
|
5676
|
+
* function failureMessage(message, code, t) {
|
|
5677
|
+
* if (code === 'QUOTA' || code === 'ACCOUNT_QUOTA') return t('message.failure.quota')
|
|
5678
|
+
* return code === 'AUTH' ? t('message.failure.auth') : message
|
|
5679
|
+
* }
|
|
5680
|
+
*
|
|
5681
|
+
* That sentence is right for a genuinely exhausted account (14018), and wrong
|
|
5682
|
+
* for `6004` — the upstream's own words are 「您的使用量已超出频率限制,将在
|
|
5683
|
+
* <时刻> 重置」, and the reporter's account still held 6,788 credits. The
|
|
5684
|
+
* plugin cannot fix that by re-classifying: measured against the shipped
|
|
5685
|
+
* classifier, every spelling that stops reading as `QUOTA` falls into
|
|
5686
|
+
* `RATE_LIMIT`, which IS in `DEFAULT_RETRYABLE_CODES` — so it would buy an
|
|
5687
|
+
* honest sentence with 5 in-place retries and ~15.5s of backoff on a limit that
|
|
5688
|
+
* resets hours later. Staying out of the retry set is the behaviour we keep
|
|
5689
|
+
* (the maintainer's call on issue #63); the sentence is what we supplement.
|
|
5690
|
+
*
|
|
5691
|
+
* HOW. The host ships a CHAIN seat for exactly this: `shell.quota-notice`, a
|
|
5692
|
+
* frame-wide overlay whose own comment says "a `shell.quota-notice` entry may
|
|
5693
|
+
* replace the generic Toast for codes it claims". The chain hands a claiming
|
|
5694
|
+
* entry the host's own owner object — `{ code, message, dismiss, keepOpen }` —
|
|
5695
|
+
* so this component can keep the host's sentence VERBATIM and append the one
|
|
5696
|
+
* fact the sentence is missing. Claiming only `QUOTA` leaves every other code on
|
|
5697
|
+
* the host's fallback Toast.
|
|
5698
|
+
*
|
|
5699
|
+
* WHY THE HOST'S SENTENCE IS KEPT, not rewritten: it is the string the shell
|
|
5700
|
+
* shows everywhere else for this code, and a plugin that reworded it would
|
|
5701
|
+
* contradict the shell's own vocabulary. The supplement is additive, and it is
|
|
5702
|
+
* written to be TRUE FOR BOTH CASES the code covers — a model's own daily quota
|
|
5703
|
+
* (not credits) and a genuinely empty balance — because the plugin cannot always
|
|
5704
|
+
* tell which one it is: the upstream states it in Chinese prose the plugin is
|
|
5705
|
+
* not allowed to parse for policy.
|
|
5706
|
+
*
|
|
5707
|
+
* Rendered as plain DOM with this plugin's own classes: the client half
|
|
5708
|
+
* deliberately does not depend on `@deepseek-ai/dsh-client-ui-primitives` (see
|
|
5709
|
+
* the note in `src/client/index.tsx` — a stale inject entry there once made the
|
|
5710
|
+
* whole card's arrival depend on the churn-heaviest host package), so the
|
|
5711
|
+
* notice is built from the same primitives every other component here uses.
|
|
5712
|
+
*/
|
|
5713
|
+
/**
|
|
5714
|
+
* Which notice codes this plugin claims.
|
|
5715
|
+
*
|
|
5716
|
+
* `QUOTA` only — it is what the shipped classifier produces for BOTH failures
|
|
5717
|
+
* this plugin raises (`6004` account rate limit and `14018` exhausted credits),
|
|
5718
|
+
* and claiming anything else would take over a notice this plugin knows nothing
|
|
5719
|
+
* about. Exported so the claim is unit-testable without a host.
|
|
5720
|
+
*/
|
|
5721
|
+
const WORKBUDDY_QUOTA_NOTICE_CODES = ["QUOTA"];
|
|
5722
|
+
/** Whether this plugin should take over the notice for one owner object. */
|
|
5723
|
+
function claimsQuotaNotice(code) {
|
|
5724
|
+
return typeof code === "string" && WORKBUDDY_QUOTA_NOTICE_CODES.includes(code);
|
|
5725
|
+
}
|
|
5726
|
+
/** The frame-wide supplement shown under the host's sentence. */
|
|
5727
|
+
function WorkBuddyQuotaNotice(props) {
|
|
5728
|
+
const { matched, t } = props;
|
|
5729
|
+
return (0, react.createElement)("div", {
|
|
5730
|
+
className: "dsm-workbuddy-quota-notice",
|
|
5731
|
+
role: "status"
|
|
5732
|
+
}, (0, react.createElement)("span", { className: "dsm-workbuddy-quota-notice-head" }, matched.message ?? t("quotaNotice.fallbackHead")), (0, react.createElement)("span", { className: "dsm-workbuddy-quota-notice-body" }, t("quotaNotice.supplement")), matched.dismiss === void 0 ? null : (0, react.createElement)("button", {
|
|
5733
|
+
type: "button",
|
|
5734
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-quota-notice-dismiss",
|
|
5735
|
+
onClick: () => matched.dismiss?.()
|
|
5736
|
+
}, t("quotaNotice.dismiss")));
|
|
5737
|
+
}
|
|
5738
|
+
//#endregion
|
|
5577
5739
|
//#region src/client/locales.ts
|
|
5578
5740
|
/**
|
|
5579
5741
|
* Plugin-card copy registered under the settings.workbuddy locale namespace.
|
|
@@ -5661,6 +5823,13 @@ window.__ModuleLoader__.load({
|
|
|
5661
5823
|
"row.modelReasoning": "Reasoning: {efforts}",
|
|
5662
5824
|
"row.modelUnknown": "Unknown",
|
|
5663
5825
|
"row.modelCapabilityPending": "Only capabilities advertised by WorkBuddy are shown.",
|
|
5826
|
+
"row.modelsTestEnabled": "Test enabled models",
|
|
5827
|
+
"row.modelsTestAll": "Test all models",
|
|
5828
|
+
"row.modelsTesting": "Testing {done}/{total}…",
|
|
5829
|
+
"row.modelsTestEnabledHint": "Tests every model you have ENABLED, one request each — each probe sends a real-volume request, so this spends real credit (about 0.01 per model at the default 25000-token size). Rows show their verdict as it lands. Stops early if the credential is rejected.",
|
|
5830
|
+
"row.modelsTestAllHint": "Tests EVERY model in this region's catalog, including the ones you have NOT enabled — so it costs noticeably more than \"test enabled models\". Same per-probe cost, same early stop on a rejected credential.",
|
|
5831
|
+
"row.modelsTestDone": "Done: {available} of {total} available",
|
|
5832
|
+
"row.modelsTestAuth": "Stopped: the credential was rejected, so every remaining model would answer the same way. Sign in again, then re-run.",
|
|
5664
5833
|
"row.probe": "Test",
|
|
5665
5834
|
"row.probing": "Testing…",
|
|
5666
5835
|
"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.",
|
|
@@ -5769,9 +5938,6 @@ window.__ModuleLoader__.load({
|
|
|
5769
5938
|
"row.poolProbeSourceUnknown": "source unknown",
|
|
5770
5939
|
"row.poolProbeModel": "model {model}",
|
|
5771
5940
|
"row.poolProbeModelUnknown": "model not recorded",
|
|
5772
|
-
"row.poolRowModelHint": "Which model this account's test uses. Leave it on \"region target\" to follow the pool's model; pick one when this account must be tested on the model you actually chat with.",
|
|
5773
|
-
"row.poolRowModelAria": "Test model for {accountName}",
|
|
5774
|
-
"row.poolRowModelInherit": "region target",
|
|
5775
5941
|
"row.poolSettingsTitle": "Account pool settings",
|
|
5776
5942
|
"row.poolSettingsHint": "These are preferences: they take effect when you save (same as the model list). Test results are written automatically and are not affected by this button.",
|
|
5777
5943
|
"row.poolEnabled": "Enable the account pool",
|
|
@@ -5838,6 +6004,9 @@ window.__ModuleLoader__.load({
|
|
|
5838
6004
|
"row.creditPositionComposer": "composer toolbar",
|
|
5839
6005
|
"row.creditPositionStats": "below the input box",
|
|
5840
6006
|
"row.creditPositionAria": "Credit readout position for {region}",
|
|
6007
|
+
"quotaNotice.fallbackHead": "The quota for this request is exhausted",
|
|
6008
|
+
"quotaNotice.supplement": "Some models have their own daily limit that is unrelated to your credit balance — check the account pool row for the stated reset time, or switch account or model to continue.",
|
|
6009
|
+
"quotaNotice.dismiss": "Got it",
|
|
5841
6010
|
"row.creditPositionHint": "Where the credit readout sits. \"composer toolbar\" is the default (the row with the model picker). Choose \"below the input box\" — the row carrying the round / step / token statistics — if another plugin occupies the composer toolbar and the readout does not appear.",
|
|
5842
6011
|
"row.showCreditsGlobalHint": "Show the international (WB AI) credit balance in the composer toolbar, but only while this session uses a WB AI model. Click it for per-account balances and a manual refresh (refreshes by itself every 5 minutes).",
|
|
5843
6012
|
"composer.pointsCn": "WB CN",
|
|
@@ -5937,6 +6106,13 @@ window.__ModuleLoader__.load({
|
|
|
5937
6106
|
"row.modelReasoning": "推理强度:{efforts}",
|
|
5938
6107
|
"row.modelUnknown": "未知",
|
|
5939
6108
|
"row.modelCapabilityPending": "仅展示 WorkBuddy 接口明确公布的模型能力。",
|
|
6109
|
+
"row.modelsTestEnabled": "一键测试启用模型",
|
|
6110
|
+
"row.modelsTestAll": "一键测试所有模型",
|
|
6111
|
+
"row.modelsTesting": "测试中 {done}/{total}…",
|
|
6112
|
+
"row.modelsTestEnabledHint": "逐个测试你**已启用**的模型,一个模型一次请求 —— 每次探测都发真实体积的请求,真花积分(默认 25000 token 体积下约 0.01/个)。每行的结果一出来就显示。凭据被拒时提前停止。",
|
|
6113
|
+
"row.modelsTestAllHint": "测试本区域目录里的**每一个**模型,含**未启用**的 —— 所以比「仅测试启用模型」明显更贵。单次探测成本相同,凭据被拒时同样提前停止。",
|
|
6114
|
+
"row.modelsTestDone": "完成:{total} 个里 {available} 个可用",
|
|
6115
|
+
"row.modelsTestAuth": "已停止:凭据被拒,剩下的模型都会得到同一个答案。请重新登录后再跑一次。",
|
|
5940
6116
|
"row.probe": "测试",
|
|
5941
6117
|
"row.probing": "测试中…",
|
|
5942
6118
|
"row.probeHint": "对该模型发一条真实体积的请求(约 25k 输入 token),报告它现在能不能用。上游的限流按请求体积触发——实测约 20k 能过、约 30k 被拒——所以小请求看着正常、长对话里却可能每次都被拒。这也是为什么每次测试都会消耗真实积分(约 0.7),且一次只测一个模型。",
|
|
@@ -6045,9 +6221,6 @@ window.__ModuleLoader__.load({
|
|
|
6045
6221
|
"row.poolProbeSourceUnknown": "来源未知",
|
|
6046
6222
|
"row.poolProbeModel": "模型 {model}",
|
|
6047
6223
|
"row.poolProbeModelUnknown": "未记录模型",
|
|
6048
|
-
"row.poolRowModelHint": "这个账号测试用哪个模型。留「区域目标」就跟随账号池的目标模型;当某个号必须用你实际在聊的模型来测时,在这里单独指定。",
|
|
6049
|
-
"row.poolRowModelAria": "{accountName} 的测试模型",
|
|
6050
|
-
"row.poolRowModelInherit": "区域目标",
|
|
6051
6224
|
"row.poolSettingsTitle": "账号池设置",
|
|
6052
6225
|
"row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
|
|
6053
6226
|
"row.poolEnabled": "启用账号池",
|
|
@@ -6114,6 +6287,9 @@ window.__ModuleLoader__.load({
|
|
|
6114
6287
|
"row.creditPositionComposer": "输入框工具栏",
|
|
6115
6288
|
"row.creditPositionStats": "输入框下方",
|
|
6116
6289
|
"row.creditPositionAria": "{region} 的积分显示位置",
|
|
6290
|
+
"quotaNotice.fallbackHead": "当前请求的额度已用尽",
|
|
6291
|
+
"quotaNotice.supplement": "部分模型另有每日额度限制,与积分余额无关 —— 也可能确实是本账号积分耗尽。账号池那一行会写明上游给出的恢复时刻;也可以先换个账号或换个模型继续。",
|
|
6292
|
+
"quotaNotice.dismiss": "知道了",
|
|
6117
6293
|
"row.creditPositionHint": "积分显示在哪里。「输入框工具栏」是默认位置(与模型选择同一行)。如果装了别的插件后读数不显示,改成「输入框下方」—— 那是「轮次 / 步数 / token」统计所在的那一行。",
|
|
6118
6294
|
"row.showCreditsGlobalHint": "在对话输入框工具栏显示国际版(WB AI)剩余积分,仅当本会话使用 WB AI 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
|
|
6119
6295
|
"composer.pointsCn": "WB CN",
|
|
@@ -6293,6 +6469,40 @@ window.__ModuleLoader__.load({
|
|
|
6293
6469
|
}
|
|
6294
6470
|
};
|
|
6295
6471
|
/**
|
|
6472
|
+
* The frame-wide quota notice, claimed so the supplement can be shown
|
|
6473
|
+
* (issue #63).
|
|
6474
|
+
*
|
|
6475
|
+
* The host renders ONE fixed sentence for a `QUOTA` failure — 「当前请求的
|
|
6476
|
+
* 额度已用尽」 — and discards the provider's message while doing it. For
|
|
6477
|
+
* `6004` that sentence is wrong (the upstream's own words say 「频率限制」,
|
|
6478
|
+
* with a reset time, and the reporting account still held thousands of
|
|
6479
|
+
* credits), but re-classifying is NOT the fix: measured against the shipped
|
|
6480
|
+
* classifier, every spelling that stops reading as `QUOTA` lands in
|
|
6481
|
+
* `RATE_LIMIT`, which is IN the host's retry set — 5 in-place retries and
|
|
6482
|
+
* ~15.5s of backoff on a limit that resets hours later. So the behaviour
|
|
6483
|
+
* (no retry) stays and the sentence gets a SUPPLEMENT.
|
|
6484
|
+
*
|
|
6485
|
+
* `select` claims `QUOTA` only, and the chain is the host's own designed
|
|
6486
|
+
* extension point for this ("a shell.quota-notice entry may replace the
|
|
6487
|
+
* generic Toast for codes it claims") — every other code keeps the host's
|
|
6488
|
+
* fallback Toast untouched.
|
|
6489
|
+
*/
|
|
6490
|
+
const registerQuotaNotice = () => {
|
|
6491
|
+
try {
|
|
6492
|
+
return ctx.slots.inject("shell.quota-notice", () => ctx.slots.register({
|
|
6493
|
+
name: "shell.quota-notice",
|
|
6494
|
+
locale: namespace,
|
|
6495
|
+
select: (owner) => {
|
|
6496
|
+
return claimsQuotaNotice(typeof owner === "object" && owner !== null ? owner.code : void 0) ? owner : null;
|
|
6497
|
+
},
|
|
6498
|
+
inject: () => ({ t })
|
|
6499
|
+
}, WorkBuddyQuotaNotice));
|
|
6500
|
+
} catch (error) {
|
|
6501
|
+
console.error("[dsh-connect-workbuddy] quota notice slot failed to register (host provider unaffected):", error);
|
|
6502
|
+
return;
|
|
6503
|
+
}
|
|
6504
|
+
};
|
|
6505
|
+
/**
|
|
6296
6506
|
* Every slot this plugin contributes, and how to (re)create them.
|
|
6297
6507
|
*
|
|
6298
6508
|
* Kept as one list because the binding of `settingsScope` and the slot
|
|
@@ -6311,7 +6521,8 @@ window.__ModuleLoader__.load({
|
|
|
6311
6521
|
...WORKBUDDY_CARD_KEYS.map((key) => registerCard("plugins.bundle.config", key)),
|
|
6312
6522
|
...WORKBUDDY_CARD_KEYS.map((key) => registerCard("plugins.row.config", `${key}#${key}`)),
|
|
6313
6523
|
registerComposerCredits(),
|
|
6314
|
-
registerStatsCredits()
|
|
6524
|
+
registerStatsCredits(),
|
|
6525
|
+
registerQuotaNotice()
|
|
6315
6526
|
].filter((disposer) => disposer !== void 0);
|
|
6316
6527
|
/**
|
|
6317
6528
|
* Disposers of the slots registered for the CURRENT binding.
|
|
@@ -3825,7 +3825,7 @@ function generationOf(version) {
|
|
|
3825
3825
|
//#endregion
|
|
3826
3826
|
//#region src/version.ts
|
|
3827
3827
|
/** The npm package version this build was produced from. */
|
|
3828
|
-
const WORKBUDDY_CONNECT_VERSION = "4.5.
|
|
3828
|
+
const WORKBUDDY_CONNECT_VERSION = "4.5.2";
|
|
3829
3829
|
//#endregion
|
|
3830
3830
|
//#region src/host-heartbeat.ts
|
|
3831
3831
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -2660,7 +2660,6 @@ interface WorkBuddyWebPool {
|
|
|
2660
2660
|
* an id whose model has left the catalog must survive a save so the user's
|
|
2661
2661
|
* record of what they chose is not silently destroyed.
|
|
2662
2662
|
*/
|
|
2663
|
-
targetModelIdByAccount?: Readonly<Record<string, string>>;
|
|
2664
2663
|
/**
|
|
2665
2664
|
* Where the target came from. `'stale'` means the SAVED id is no longer in
|
|
2666
2665
|
* the catalog — a distinct state from `'none'` (no free model exists),
|
|
@@ -2953,7 +2952,6 @@ interface WorkBuddyPoolDeps {
|
|
|
2953
2952
|
enabled: boolean;
|
|
2954
2953
|
targetModelId: string;
|
|
2955
2954
|
/** Per-account override of `targetModelId` (issue #61). */
|
|
2956
|
-
targetModelIdByAccount?: Readonly<Record<string, string>>;
|
|
2957
2955
|
/**
|
|
2958
2956
|
* Tokens each test probe sends as saved; `0` when the user never chose.
|
|
2959
2957
|
*
|
|
@@ -3051,16 +3049,11 @@ interface WorkBuddyPoolDeps {
|
|
|
3051
3049
|
*/
|
|
3052
3050
|
test?(region: WorkBuddyRegion,
|
|
3053
3051
|
/**
|
|
3054
|
-
*
|
|
3055
|
-
*
|
|
3056
|
-
*
|
|
3057
|
-
* resolver that names no model yields an `unavailable` row for that account
|
|
3058
|
-
* rather than silently testing the region default.
|
|
3052
|
+
* The model the whole batch is measured on — ONE value, by design: every
|
|
3053
|
+
* member of the batch must be probed on the same model or a green batch
|
|
3054
|
+
* stops meaning a single thing (see the removal note in `account-pool-run.ts`).
|
|
3059
3055
|
*/
|
|
3060
|
-
modelId: string
|
|
3061
|
-
modelId?: string;
|
|
3062
|
-
staleModelId?: string;
|
|
3063
|
-
}), onRow?: (row: WorkBuddyPoolTestRow) => void): Promise<readonly WorkBuddyPoolTestRow[]>;
|
|
3056
|
+
modelId: string, onRow?: (row: WorkBuddyPoolTestRow) => void): Promise<readonly WorkBuddyPoolTestRow[]>;
|
|
3064
3057
|
/** The region's live catalog, used to resolve the free target model. */
|
|
3065
3058
|
catalog?(region: WorkBuddyRegion): readonly WorkBuddyModelInfo[];
|
|
3066
3059
|
}
|
|
@@ -3200,7 +3193,6 @@ interface WorkBuddyPoolPreferences {
|
|
|
3200
3193
|
* chatting with. An id that is no longer in the catalog is reported as stale
|
|
3201
3194
|
* rather than replaced, so no credits are spent on a model nobody chose.
|
|
3202
3195
|
*/
|
|
3203
|
-
targetModelIdByAccount?: Record<string, string>;
|
|
3204
3196
|
/**
|
|
3205
3197
|
* How much input a probe sends, in tokens — the pool's biggest single knob.
|
|
3206
3198
|
*
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-
|
|
1
|
+
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-DcUM0cVn.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -2752,22 +2752,7 @@ async function testAllAccounts(targets, modelId, deps, onRow) {
|
|
|
2752
2752
|
const rows = [];
|
|
2753
2753
|
for (const [index, target] of targets.entries()) {
|
|
2754
2754
|
if (index > 0) await (deps.wait ?? defaultWait)(400);
|
|
2755
|
-
const
|
|
2756
|
-
if (wanted.modelId === void 0) {
|
|
2757
|
-
const row = {
|
|
2758
|
-
accountId: target.accountId,
|
|
2759
|
-
accountName: target.accountName,
|
|
2760
|
-
result: {
|
|
2761
|
-
modelId: wanted.staleModelId ?? "",
|
|
2762
|
-
outcome: "unavailable",
|
|
2763
|
-
message: wanted.staleModelId === void 0 ? "this region offers no model to test" : `the test model "${wanted.staleModelId}" is no longer offered in this region`
|
|
2764
|
-
}
|
|
2765
|
-
};
|
|
2766
|
-
rows.push(row);
|
|
2767
|
-
onRow?.(row);
|
|
2768
|
-
continue;
|
|
2769
|
-
}
|
|
2770
|
-
const row = await testOne(target, wanted.modelId, deps);
|
|
2755
|
+
const row = await testOne(target, modelId, deps);
|
|
2771
2756
|
rows.push(row);
|
|
2772
2757
|
onRow?.(row);
|
|
2773
2758
|
}
|
|
@@ -3074,30 +3059,6 @@ function pickFreeModel(catalog) {
|
|
|
3074
3059
|
* That is the safe direction: no credits are spent while the roster is unknown,
|
|
3075
3060
|
* and the card offers to switch back to automatic.
|
|
3076
3061
|
*/
|
|
3077
|
-
/**
|
|
3078
|
-
* The model ONE account's test should target (issue #61).
|
|
3079
|
-
*
|
|
3080
|
-
* The region-wide `targetModelId` is one value, but `6004` is an ACCOUNT × MODEL
|
|
3081
|
-
* condition: the reporter measured two accounts where `hy4-preview-f` answered
|
|
3082
|
-
* `ok` and `step-5-preview` returned `429 / 6004` at the same moment. Testing
|
|
3083
|
-
* only the region's model therefore cannot answer "can this account serve the
|
|
3084
|
-
* model I actually use", which is the question the pool's test button is read as
|
|
3085
|
-
* answering.
|
|
3086
|
-
*
|
|
3087
|
-
* A per-account override is the smallest thing that makes the question
|
|
3088
|
-
* answerable, and it is additive: an account with no entry keeps using
|
|
3089
|
-
* `targetModelId`, so every existing profile behaves identically.
|
|
3090
|
-
*
|
|
3091
|
-
* Returns a PREFERENCE, not a resolved target — `resolveTargetModel` still
|
|
3092
|
-
* validates it against the catalog, so an override pointing at a model this
|
|
3093
|
-
* region no longer offers is reported as stale rather than silently swapped for
|
|
3094
|
-
* one the user did not choose.
|
|
3095
|
-
*/
|
|
3096
|
-
function targetModelPreference(preferences, accountId) {
|
|
3097
|
-
const perAccount = accountId === void 0 ? void 0 : preferences.targetModelIdByAccount?.[accountId];
|
|
3098
|
-
if (perAccount !== void 0 && perAccount !== "") return perAccount;
|
|
3099
|
-
return preferences.targetModelId;
|
|
3100
|
-
}
|
|
3101
3062
|
function resolveTargetModel(catalog, preferredId) {
|
|
3102
3063
|
if (preferredId !== void 0 && preferredId !== "") return catalog.some((model) => model.id === preferredId) ? {
|
|
3103
3064
|
modelId: preferredId,
|
|
@@ -4349,7 +4310,7 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4349
4310
|
...measured.probe.modelId === void 0 ? {} : { modelId: measured.probe.modelId }
|
|
4350
4311
|
} },
|
|
4351
4312
|
...(() => {
|
|
4352
|
-
const perRow = resolveTargetModel(catalog,
|
|
4313
|
+
const perRow = resolveTargetModel(catalog, preferences.targetModelId);
|
|
4353
4314
|
return {
|
|
4354
4315
|
...perRow.modelId === void 0 ? {} : { testModelId: perRow.modelId },
|
|
4355
4316
|
testModelSource: perRow.source,
|
|
@@ -4368,7 +4329,6 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4368
4329
|
...target.modelId === void 0 ? {} : { targetModelId: target.modelId },
|
|
4369
4330
|
...target.staleModelId === void 0 ? {} : { staleTargetModelId: target.staleModelId },
|
|
4370
4331
|
targetModelSource: target.source,
|
|
4371
|
-
targetModelIdByAccount: preferences.targetModelIdByAccount ?? {},
|
|
4372
4332
|
memberAccountIds: preferences.memberAccountIds,
|
|
4373
4333
|
...preferences.probeInputTokens === void 0 ? {} : { probeInputTokens: preferences.probeInputTokens },
|
|
4374
4334
|
effectiveMemberAccountIds: effectiveMembersOf(accounts.map((account) => account.accountId), new Set(preferences.memberAccountIds)),
|
|
@@ -4613,19 +4573,17 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
4613
4573
|
error: "pool test unavailable"
|
|
4614
4574
|
});
|
|
4615
4575
|
const preferences = pool.preferences(region);
|
|
4616
|
-
const catalog = pool.catalog?.(region) ?? [];
|
|
4617
4576
|
/**
|
|
4618
4577
|
* The model ONE member is tested on (issue #61).
|
|
4619
4578
|
*
|
|
4620
|
-
*
|
|
4621
|
-
*
|
|
4622
|
-
*
|
|
4623
|
-
*
|
|
4624
|
-
* `resolveTargetModel`, so a stale
|
|
4625
|
-
* silently swapped.
|
|
4579
|
+
* ONE model for the whole batch, and it is also the gate: if the region
|
|
4580
|
+
* cannot name a target model the card is told to pick one before any
|
|
4581
|
+
* credits are spent. Every member is therefore measured on the SAME
|
|
4582
|
+
* model, which is what makes a green batch mean one thing. Resolution
|
|
4583
|
+
* runs through `resolveTargetModel`, so a stale target is reported,
|
|
4584
|
+
* never silently swapped for another model.
|
|
4626
4585
|
*/
|
|
4627
|
-
const
|
|
4628
|
-
const target = resolveTargetModel(catalog, targetModelPreference(preferences, void 0));
|
|
4586
|
+
const target = resolveTargetModel(pool.catalog?.(region) ?? [], preferences.targetModelId);
|
|
4629
4587
|
if (target.modelId === void 0) return json(res, 409, {
|
|
4630
4588
|
action: "test",
|
|
4631
4589
|
modelId: void 0,
|
|
@@ -4647,7 +4605,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
4647
4605
|
modelId: target.modelId
|
|
4648
4606
|
});
|
|
4649
4607
|
try {
|
|
4650
|
-
await pool.test(region,
|
|
4608
|
+
await pool.test(region, target.modelId, (row) => {
|
|
4651
4609
|
writeLine({ row });
|
|
4652
4610
|
});
|
|
4653
4611
|
writeLine({ done: true });
|
|
@@ -4985,7 +4943,6 @@ const modelConfig = z.object({
|
|
|
4985
4943
|
const poolConfig = z.object({
|
|
4986
4944
|
enabled: z.boolean().default(false).description("Whether this region's account pool is active (opt-in). While on, a failed chat request is retried against the other usable pool members before the error is reported."),
|
|
4987
4945
|
targetModelId: z.string().default("").description("Model id to test; empty means pick a zero-multiplier model from this region's catalog"),
|
|
4988
|
-
targetModelIdByAccount: z.dict(z.string()).default({}).description("Per-account test model, keyed by account id; an account with no entry uses targetModelId. Use this when an account must be tested on the model you actually chat with, because 6004 is an account-by-model condition."),
|
|
4989
4946
|
/**
|
|
4990
4947
|
* Sizing of the test probe, as a token count the user picks from a fixed set.
|
|
4991
4948
|
*
|
|
@@ -5587,7 +5544,7 @@ function apply(ctx, config) {
|
|
|
5587
5544
|
* rather than pretending a test ran.
|
|
5588
5545
|
*/
|
|
5589
5546
|
testOne: async (region, accountId) => {
|
|
5590
|
-
const target = regionTargetModel(region
|
|
5547
|
+
const target = regionTargetModel(region);
|
|
5591
5548
|
if (target === void 0) return void 0;
|
|
5592
5549
|
const store = stacks[region].store;
|
|
5593
5550
|
const credential = await store.credentialFor(accountId).catch(() => void 0);
|
|
@@ -5957,8 +5914,8 @@ function apply(ctx, config) {
|
|
|
5957
5914
|
* issue #61), so a row test and a batch test never measure different models.
|
|
5958
5915
|
* Undefined mirrors the route's 409 states.
|
|
5959
5916
|
*/
|
|
5960
|
-
const regionTargetModel = (region
|
|
5961
|
-
return resolveTargetModel(displayModels(current(), region),
|
|
5917
|
+
const regionTargetModel = (region) => {
|
|
5918
|
+
return resolveTargetModel(displayModels(current(), region), poolPreferencesOf(current(), region).targetModelId).modelId;
|
|
5962
5919
|
};
|
|
5963
5920
|
/** One region's pool preferences, with the schema's defaults applied. */
|
|
5964
5921
|
/**
|
|
@@ -5975,7 +5932,6 @@ function apply(ctx, config) {
|
|
|
5975
5932
|
return {
|
|
5976
5933
|
enabled: pool?.enabled === true,
|
|
5977
5934
|
targetModelId: pool?.targetModelId ?? "",
|
|
5978
|
-
targetModelIdByAccount: { ...pool?.targetModelIdByAccount },
|
|
5979
5935
|
...probeInputTokens === void 0 ? {} : { probeInputTokens },
|
|
5980
5936
|
memberAccountIds: pool?.memberAccountIds ?? []
|
|
5981
5937
|
};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
|
|
5
|
-
"version": "4.5.
|
|
5
|
+
"version": "4.5.2",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|