dsh-connect-workbuddy 4.5.1 → 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 +39 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +180 -88
- package/lib/{host-heartbeat-Vd2tNdsv.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,44 @@
|
|
|
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
|
+
|
|
3
42
|
## 4.5.1 (2026-10-14)
|
|
4
43
|
|
|
5
44
|
### Fixes
|
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,
|
|
@@ -3160,7 +3102,16 @@ window.__ModuleLoader__.load({
|
|
|
3160
3102
|
meta line, because a cooldown sentence ("rate limited - the upstream gave no
|
|
3161
3103
|
time") does not fit next to the context/output chips, and truncating it would
|
|
3162
3104
|
hide exactly the part the user needs. */
|
|
3163
|
-
|
|
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)}
|
|
3164
3115
|
.dsm-workbuddy-model-probe{flex:none;padding:3px 10px;font-size:11px;line-height:16px}
|
|
3165
3116
|
.dsm-workbuddy-model-probe-result{margin:0;font-size:12px;line-height:18px;word-break:break-word}
|
|
3166
3117
|
.dsm-workbuddy-model-probe-result-ok{color:var(--dsw-alias-state-success-primary,#22a06b)}
|
|
@@ -3301,11 +3252,16 @@ window.__ModuleLoader__.load({
|
|
|
3301
3252
|
.dsm-workbuddy-pool-log-info{color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3302
3253
|
.dsm-workbuddy-pool-log-empty{padding:14px;font-size:12px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
3303
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. */
|
|
3304
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}
|
|
3305
3262
|
.dsm-workbuddy-pool-row-actions .dsm-workbuddy-pool-small-btn{padding:2px 10px;font-size:12px}
|
|
3306
3263
|
/* This account's own test model (issue #61). Narrower than the section-level
|
|
3307
3264
|
picker so a long model name cannot push the two buttons out of the row. */
|
|
3308
|
-
.dsm-workbuddy-pool-row-model{max-width:150px;padding:2px 6px;font-size:12px}
|
|
3309
3265
|
@media (max-width:760px){.dsm-workbuddy-pool-row{grid-template-columns:1fr 1fr;row-gap:6px}.dsm-workbuddy-pool-select{max-width:180px}}
|
|
3310
3266
|
/* Pool switch, namespaced under .dsm-workbuddy-pool so it cannot collide with host styles. */
|
|
3311
3267
|
.dsm-workbuddy-pool .sw{position:relative;display:inline-block;width:38px;height:22px;flex:none;cursor:pointer}
|
|
@@ -3939,6 +3895,19 @@ window.__ModuleLoader__.load({
|
|
|
3939
3895
|
const [probing, setProbing] = (0, react.useState)({});
|
|
3940
3896
|
/** Probe failure that is not attributable to one model (a dead route, say). */
|
|
3941
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);
|
|
3942
3911
|
const mounted = (0, react.useRef)(true);
|
|
3943
3912
|
(0, react.useEffect)(() => {
|
|
3944
3913
|
mounted.current = true;
|
|
@@ -4367,13 +4336,16 @@ window.__ModuleLoader__.load({
|
|
|
4367
4336
|
});
|
|
4368
4337
|
};
|
|
4369
4338
|
/**
|
|
4370
|
-
* 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.
|
|
4371
4344
|
*
|
|
4372
|
-
* The
|
|
4373
|
-
*
|
|
4374
|
-
*
|
|
4375
|
-
*
|
|
4376
|
-
* 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.
|
|
4377
4349
|
*
|
|
4378
4350
|
* Probing is deliberately independent of `dirty`: a test answers "does this
|
|
4379
4351
|
* model work right now", which is true of the SAVED selection and the draft
|
|
@@ -4401,9 +4373,12 @@ window.__ModuleLoader__.load({
|
|
|
4401
4373
|
const body = await response.json().catch(() => void 0);
|
|
4402
4374
|
if (!response.ok || !Array.isArray(body?.results)) throw new Error(body?.error ?? `HTTP ${String(response.status)}`);
|
|
4403
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;
|
|
4404
4379
|
setProbes((previous) => {
|
|
4405
4380
|
const regionResults = { ...previous[activeRegion] ?? {} };
|
|
4406
|
-
for (const result of
|
|
4381
|
+
for (const result of landedResults) regionResults[result.modelId] = result;
|
|
4407
4382
|
return {
|
|
4408
4383
|
...previous,
|
|
4409
4384
|
[activeRegion]: regionResults
|
|
@@ -4419,6 +4394,86 @@ window.__ModuleLoader__.load({
|
|
|
4419
4394
|
});
|
|
4420
4395
|
}
|
|
4421
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
|
+
};
|
|
4422
4477
|
const saveModels = async () => {
|
|
4423
4478
|
if (settingsScope === void 0) return;
|
|
4424
4479
|
if (status.status !== "signed-in") return;
|
|
@@ -4731,17 +4786,41 @@ window.__ModuleLoader__.load({
|
|
|
4731
4786
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4732
4787
|
className: "dsm-workbuddy-models-summary",
|
|
4733
4788
|
children: t("row.modelsSummary", { count: activeEnabledIds.size })
|
|
4734
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4789
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4735
4790
|
className: "dsm-workbuddy-models-head-actions",
|
|
4736
|
-
children:
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
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
|
+
]
|
|
4745
4824
|
})]
|
|
4746
4825
|
}),
|
|
4747
4826
|
probeError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -4749,6 +4828,11 @@ window.__ModuleLoader__.load({
|
|
|
4749
4828
|
role: "alert",
|
|
4750
4829
|
children: t("row.probeError", { message: probeError })
|
|
4751
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
|
+
}),
|
|
4752
4836
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4753
4837
|
className: "dsm-workbuddy-model-list",
|
|
4754
4838
|
children: visibleModels.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5739,6 +5823,13 @@ window.__ModuleLoader__.load({
|
|
|
5739
5823
|
"row.modelReasoning": "Reasoning: {efforts}",
|
|
5740
5824
|
"row.modelUnknown": "Unknown",
|
|
5741
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.",
|
|
5742
5833
|
"row.probe": "Test",
|
|
5743
5834
|
"row.probing": "Testing…",
|
|
5744
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.",
|
|
@@ -5847,9 +5938,6 @@ window.__ModuleLoader__.load({
|
|
|
5847
5938
|
"row.poolProbeSourceUnknown": "source unknown",
|
|
5848
5939
|
"row.poolProbeModel": "model {model}",
|
|
5849
5940
|
"row.poolProbeModelUnknown": "model not recorded",
|
|
5850
|
-
"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.",
|
|
5851
|
-
"row.poolRowModelAria": "Test model for {accountName}",
|
|
5852
|
-
"row.poolRowModelInherit": "region target",
|
|
5853
5941
|
"row.poolSettingsTitle": "Account pool settings",
|
|
5854
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.",
|
|
5855
5943
|
"row.poolEnabled": "Enable the account pool",
|
|
@@ -6018,6 +6106,13 @@ window.__ModuleLoader__.load({
|
|
|
6018
6106
|
"row.modelReasoning": "推理强度:{efforts}",
|
|
6019
6107
|
"row.modelUnknown": "未知",
|
|
6020
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": "已停止:凭据被拒,剩下的模型都会得到同一个答案。请重新登录后再跑一次。",
|
|
6021
6116
|
"row.probe": "测试",
|
|
6022
6117
|
"row.probing": "测试中…",
|
|
6023
6118
|
"row.probeHint": "对该模型发一条真实体积的请求(约 25k 输入 token),报告它现在能不能用。上游的限流按请求体积触发——实测约 20k 能过、约 30k 被拒——所以小请求看着正常、长对话里却可能每次都被拒。这也是为什么每次测试都会消耗真实积分(约 0.7),且一次只测一个模型。",
|
|
@@ -6126,9 +6221,6 @@ window.__ModuleLoader__.load({
|
|
|
6126
6221
|
"row.poolProbeSourceUnknown": "来源未知",
|
|
6127
6222
|
"row.poolProbeModel": "模型 {model}",
|
|
6128
6223
|
"row.poolProbeModelUnknown": "未记录模型",
|
|
6129
|
-
"row.poolRowModelHint": "这个账号测试用哪个模型。留「区域目标」就跟随账号池的目标模型;当某个号必须用你实际在聊的模型来测时,在这里单独指定。",
|
|
6130
|
-
"row.poolRowModelAria": "{accountName} 的测试模型",
|
|
6131
|
-
"row.poolRowModelInherit": "区域目标",
|
|
6132
6224
|
"row.poolSettingsTitle": "账号池设置",
|
|
6133
6225
|
"row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
|
|
6134
6226
|
"row.poolEnabled": "启用账号池",
|
|
@@ -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",
|