dsh-connect-workbuddy 3.4.3 → 3.4.5
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 +47 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +122 -7
- package/lib/{host-heartbeat-D5ArLcFf.js → host-heartbeat-CTigiLJK.js} +19 -4
- package/lib/index.d.ts +112 -6
- package/lib/index.js +203 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.5 (2026-10-04)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **保存设置不再把「本该用实时目录」的模型槽写空,正在用的模型不会凭空消失(issue #32)。**
|
|
8
|
+
- **现象**:在 `regions` 从未成功写入过的 profile 上(如 issue #31 所述的环境),第一次保存(哪怕只改了一处偏好)之后,模型选择器里**正在使用的默认模型突然不见了**,其他只在上游目录里、不在内置静态名单里的模型也一起消失,且**没有任何报错**。
|
|
9
|
+
- **根因**(两半,缺一不可):
|
|
10
|
+
1. **写半边**——`__save` 的 `merged` 以宿主解析值为种子。当 `regions` 还是空对象时,`{"cn":{"imageModelIds":[]}}` 这样的载荷会落下**一个没有 `lastCatalog` 的 cn 槽**。槽本身合法(schema 里 `lastCatalog` 默认就是 `[]`),但它缺了这份目录。
|
|
11
|
+
2. **读半边**——`configuredModels` / `displayModels` 把 roster 解析成两步:`state.lastCatalog?.length ? lastCatalog : 静态名单`。「槽存在但没有目录」和「从未配置过」于是被当成同一件事,都退回**内置的 12 条静态名单**;而 `applySelection` 又把这份静态名单 `catalog.set()` 进**实时目录**——实时发现注册的 17 条就此被覆盖。正在用的 `space-bunny` 只在实时目录里、不在静态名单里,当场消失。
|
|
12
|
+
- **修法**:
|
|
13
|
+
- 写半边:`__save` 在合并之后、落盘之前,对本次载荷**真正触及**的区域(只碰 `incoming` 里出现过的键,不动调用方没提到的兄弟区域),若该槽缺目录(`lastCatalog` 缺失或为空数组),就用 `displayModels` 取到的**实时名册**补上。用 `displayModels` 而非上游往返:保存不该依赖网络,也不该因为上游不可用而失败。调用方显式传来的数组(含非空数组)仍然优先,实时名册为空则维持原状、不写空数组。
|
|
14
|
+
- 读半边:roster 解析改成三步 `lastCatalog → 该区域当前已注册的实时目录 → 静态名单`。静态名单退回它本来该待的位置——上游离线、什么都发现不了时的最后兜底。`applySelection` 在 `set()` **之前**读一次当前目录,避免观察到自己正要写的值。
|
|
15
|
+
- 这样「槽存在但缺目录」会**降到实时目录**,永远不会越过它掉到静态名单。
|
|
16
|
+
|
|
17
|
+
- **未显式设置预算的模型不再被强制压到 200K,1M 模型的真实窗口得以保留(issue #33)。**
|
|
18
|
+
- **现象**:上游目录里 17 个模型有 15 个被改报成 200000——`space-bunny`、`deepseek-v4.1-flash`、`deepseek-v4-pro`、`glm-5.3`、`glm-5.3-flash`、`glm-5.2`、`kimi-k3-1`、`kimi-k2.8-preview` 的 1000000、`hy4-preview` 的 960000、`minimax-m3` 的 512000、`auto`/`kimi-k2.6`/`kimi-k2.7` 的 256000 全部变成 200000。
|
|
19
|
+
- **根因**:`applyContextBudgets` 里 `budgets[model.id] ?? 200_000` 的默认值——**未设置预算被当成「设成 200K」**。这条默认值在 CHANGELOG 里给出的理由是「让 DSH 提前压缩以避开上游节流」,但同一份 CHANGELOG 又自述实测节流线是 **2 万~3 万 token 且单次请求即触顶**:200K 比那条线高一个数量级,压到 200K 并没有把压缩提前到安全区。200K 在静态目录里也无出处(`auto` 是 168000、`hy3` 是 192000、`default-model` 是 176000、`primary-model` 是 272000);插件自己的静态目录还把 `glm-5.3` 等声明为 1000000,与钳制自相矛盾。
|
|
20
|
+
- **修法**:`applyContextBudgets` 不再有默认值——**只做上限**:未设置预算时保留模型的原生窗口,设置了就用 `Math.min(原生, 预算)`。`contextBudgets` 的语义恢复成「用户设的上限」,而不是一个隐式的全局改写。(`toWebModel` 里那份重复的 `?? 200_000` 也一并去掉,改为调用同一个函数,避免卡片与运行时目录各说各话。)
|
|
21
|
+
- 想继续压缩的用户仍然可以显式设置 `regions.<region>.contextBudgets`;卡片上原本只在原生窗口 > 200K 时才出现的「DSH 上下文预算」选择器,现在因为不再默认钳制而更有意义。
|
|
22
|
+
|
|
23
|
+
## 3.4.4 (2026-10-03)
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
- **测试请求体积可选(1 万 / 2 万 / 3 万 / 5 万 / 10 万 token),不再固定 2.5 万。**
|
|
28
|
+
- **背景**:上游的频率限制(6004)按**请求体积**触发——实测阈值落在 2 万~3 万 token 之间,**单次请求就足以触顶**(不是累计)。而账号池的「测试」探针此前固定发 25,000 token,于是真实对话体积在 2.5 万~3 万之间时会出现「测试测出可用、一发聊天立刻被限」。
|
|
29
|
+
- **为什么不是把固定值调大**:探针只测一个点,而阈值是一个区间、真实对话体积是连续的——把 2.5 万改成 3 万只是把缝隙上移(3 万以上漏判),且成本线性上升(2.5 万 ≈ 0.72 积分,5 万 ≈ 1.4+)。**体积是用户流量的属性,因此交给用户选。**
|
|
30
|
+
- 新增池子设置「测试请求体积」,默认仍是实测标定的 25K(`0` = 默认);改动立即生效,无需重载。
|
|
31
|
+
- 相应用户可见的说明写在设置项旁:解释 2 万~3 万阈值与「测出可用、一发就被限」的成因。
|
|
32
|
+
|
|
33
|
+
- **换号时在回复正文里说明原因,不再只有日志。**
|
|
34
|
+
- **背景**:池子的 failover 会给同一个请求依次试多个账号,此前只写一行 `logger.warn`——用户完全看不到。于是一次切换过的回复,和「你选的那个账号正常回答」**在界面上完全一样**,而且事后无法从记录区分「刚才真有请求失败」和「状态莫名其妙退回了旧值」。
|
|
35
|
+
- 现在回答开头会附一行(仅当真的发生了切换、且确实有内容送达时):
|
|
36
|
+
|
|
37
|
+
> [dsh-connect-workbuddy] 「13046625578」was rate limited — this answer came from another account
|
|
38
|
+
|
|
39
|
+
- 只报**第一个**被放弃的账号与原因(连续试 4 个账号时不刷屏),原因按用户能采取的行动措辞(`rate limited` / `signed out` / `rejected by policy` / …),而不是内部 kind 名。
|
|
40
|
+
|
|
41
|
+
- **池子记录标注来源与时间,让「结果变了」可解释。**
|
|
42
|
+
- 每条测量现在记录它是**谁写的**:`测得`(用户点「测试」的批测)或 `聊天撞上`(实时聊天失败被记录),以及**多久前**更新。
|
|
43
|
+
- 卡片上分两行显示:主行是结果/恢复时间/原因,次行是来源胶囊 + 相对时间。此前只能靠时间戳分布**猜**来源(批测写同一时间戳、单账号写入时间戳各不同)。
|
|
44
|
+
- 「来源未知」用于本次升级之前写入、没有来源字段的旧记录——不猜,如实标注。
|
|
45
|
+
|
|
46
|
+
### Fixes
|
|
47
|
+
|
|
48
|
+
- **「来源未知」胶囊太淡看不清。** 旧样式同时做了四处削弱(`background:transparent`、`1px dashed`、`label-dimmed #9aa0a6`、`font-weight:400`);实测对比度发现**字色不是主因**(`#9aa0a6` 5.28:1,反而比正常胶囊的 `#999` 4.89:1 更亮),主因是 10px 小字配 400 字重。已改为实底 + 实线边框 + `label-secondary`(8.41:1),并去掉字重覆盖以继承基类的 600。
|
|
49
|
+
|
|
3
50
|
## 3.4.3 (2026-10-03)
|
|
4
51
|
|
|
5
52
|
### Features
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { A as workbuddyOwnAuthPath, C as defaultDesktopAuthDirs, D as legacyWorkbuddyOwnAuthPath, P as WorkBuddyUpstreamClient, S as defaultDesktopAuthCandidates, U as WORKBUDDY_APP_EXECUTABLE_ENV, Y as findWorkbuddyAppExecutableWithSource, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-CTigiLJK.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
|
@@ -513,11 +513,19 @@ window.__ModuleLoader__.load({
|
|
|
513
513
|
* @throws {WorkBuddySettingsWriteError} when neither writer persists the value.
|
|
514
514
|
*/
|
|
515
515
|
async function writePoolPreferences(scope, region, preferences) {
|
|
516
|
-
const
|
|
516
|
+
const slot = {
|
|
517
|
+
enabled: preferences.enabled,
|
|
518
|
+
targetModelId: preferences.targetModelId,
|
|
519
|
+
memberAccountIds: preferences.memberAccountIds
|
|
520
|
+
};
|
|
521
|
+
if (preferences.probeInputTokens !== void 0) slot["probeInputTokens"] = preferences.probeInputTokens;
|
|
522
|
+
const next = nextRegionPool(scope.getSnapshot().value, region, slot)[region];
|
|
517
523
|
await writeField(scope, "regions", region, next, (readBack) => {
|
|
518
524
|
const pool = readBack?.pool;
|
|
519
525
|
if (pool === void 0 || pool === null) return false;
|
|
520
526
|
if (pool["enabled"] !== preferences.enabled || pool["targetModelId"] !== preferences.targetModelId) return false;
|
|
527
|
+
const wantedTokens = preferences.probeInputTokens ?? 0;
|
|
528
|
+
if ((pool["probeInputTokens"] ?? 0) !== wantedTokens) return false;
|
|
521
529
|
const stored = pool["memberAccountIds"];
|
|
522
530
|
if (!Array.isArray(stored) || stored.length !== preferences.memberAccountIds.length) return false;
|
|
523
531
|
const wanted = new Set(preferences.memberAccountIds);
|
|
@@ -619,6 +627,53 @@ window.__ModuleLoader__.load({
|
|
|
619
627
|
return message.replace(/^transport error:\s*/u, "");
|
|
620
628
|
}
|
|
621
629
|
//#endregion
|
|
630
|
+
//#region src/client/probe-age.ts
|
|
631
|
+
const MINUTE_MS$1 = 6e4;
|
|
632
|
+
const HOUR_MS$1 = 60 * MINUTE_MS$1;
|
|
633
|
+
const DAY_MS$1 = 24 * HOUR_MS$1;
|
|
634
|
+
/**
|
|
635
|
+
* How long ago a measurement was taken, coarsely: "刚刚", "N 分钟前",
|
|
636
|
+
* "N 小时前", "N 天前".
|
|
637
|
+
*
|
|
638
|
+
* Coarse on purpose, for the same reason `remainingText` rounds: a figure that
|
|
639
|
+
* changes every second reads as live telemetry rather than as "how stale is
|
|
640
|
+
* this", which is the only question being asked here.
|
|
641
|
+
*
|
|
642
|
+
* A clock skew that puts the measurement in the FUTURE (a stored `atMs` ahead
|
|
643
|
+
* of `nowMs`, from a machine whose clock moved backwards) is reported as
|
|
644
|
+
* "刚刚" rather than as a negative age: the record is real, and the card must
|
|
645
|
+
* not print "-3 分钟前".
|
|
646
|
+
*/
|
|
647
|
+
function probeAgeText(t, atMs, nowMs) {
|
|
648
|
+
const age = nowMs - atMs;
|
|
649
|
+
if (age < MINUTE_MS$1) return t("row.poolProbeJustNow");
|
|
650
|
+
if (age < HOUR_MS$1) return t("row.poolProbeMinutesAgo", { count: Math.round(age / MINUTE_MS$1) });
|
|
651
|
+
if (age < 2 * DAY_MS$1) return t("row.poolProbeHoursAgo", { count: Math.round(age / HOUR_MS$1) });
|
|
652
|
+
return t("row.poolProbeDaysAgo", { count: Math.round(age / DAY_MS$1) });
|
|
653
|
+
}
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/probe.ts
|
|
656
|
+
/**
|
|
657
|
+
* The probe sizes the card offers, in tokens.
|
|
658
|
+
*
|
|
659
|
+
* A closed set, each step a meaningful position relative to the measured 20k~30k
|
|
660
|
+
* threshold: below it (10k/20k) proves an account can serve SHORT requests and
|
|
661
|
+
* is the cheap choice; at it (30k) is the size that finally covers the whole
|
|
662
|
+
* measured window; above it (50k/100k) is for conversations that routinely run
|
|
663
|
+
* long, where a smaller probe would report "usable" for an account that refuses
|
|
664
|
+
* the very next message.
|
|
665
|
+
*
|
|
666
|
+
* Exported so the schema's description, the card's dropdown, and the coercion
|
|
667
|
+
* below cannot drift apart into three different menus.
|
|
668
|
+
*/
|
|
669
|
+
const PROBE_INPUT_TOKEN_CHOICES = [
|
|
670
|
+
1e4,
|
|
671
|
+
2e4,
|
|
672
|
+
3e4,
|
|
673
|
+
5e4,
|
|
674
|
+
1e5
|
|
675
|
+
];
|
|
676
|
+
//#endregion
|
|
622
677
|
//#region src/client/remaining.ts
|
|
623
678
|
const MINUTE_MS = 6e4;
|
|
624
679
|
const HOUR_MS = 60 * MINUTE_MS;
|
|
@@ -989,10 +1044,11 @@ window.__ModuleLoader__.load({
|
|
|
989
1044
|
const saved = {
|
|
990
1045
|
enabled: pool?.enabled ?? false,
|
|
991
1046
|
targetModelId: pool?.targetModelId || pool?.staleTargetModelId || "",
|
|
1047
|
+
probeInputTokens: pool?.probeInputTokens ?? 0,
|
|
992
1048
|
memberAccountIds: pool?.memberAccountIds ?? []
|
|
993
1049
|
};
|
|
994
1050
|
const active = draftRegion === region && draft !== null ? draft : saved;
|
|
995
|
-
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1051
|
+
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
996
1052
|
/**
|
|
997
1053
|
* Membership that actually resolves to a local sign-in — what a batch runs on.
|
|
998
1054
|
*
|
|
@@ -1376,10 +1432,24 @@ window.__ModuleLoader__.load({
|
|
|
1376
1432
|
const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
|
|
1377
1433
|
const excluded = exclusionText(t, account);
|
|
1378
1434
|
const probe = account.probe;
|
|
1435
|
+
const outcomeLine = probe === void 0 ? t("row.poolNeverTested") : probe.retryAtMs !== void 0 ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryAt", { at: formatShort(probe.retryAtMs) })}` : probe.outcome === "rate-limited" || probe.outcome === "out-of-credit" ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}` : outcomeText(t, probe.outcome);
|
|
1436
|
+
const remaining = probe?.retryAtMs === void 0 ? "" : remainingText(t, probe.retryAtMs, Date.now());
|
|
1437
|
+
const reason = probe === void 0 ? void 0 : inlineProbeReason(probe.outcome, probe.message);
|
|
1438
|
+
const age = probe === void 0 ? void 0 : probeAgeText(t, probe.atMs, Date.now());
|
|
1439
|
+
const probeLabel = probe === void 0 ? void 0 : probe.source === void 0 ? {
|
|
1440
|
+
text: t("row.poolProbeSourceUnknown"),
|
|
1441
|
+
className: "dsm-workbuddy-pool-prov-tag dsm-workbuddy-pool-prov-unknown"
|
|
1442
|
+
} : probe.source === "live-request" ? {
|
|
1443
|
+
text: t("row.poolProbeSourceLive"),
|
|
1444
|
+
className: "dsm-workbuddy-pool-prov-tag dsm-workbuddy-pool-prov-live"
|
|
1445
|
+
} : {
|
|
1446
|
+
text: t("row.poolProbeSourceTest"),
|
|
1447
|
+
className: "dsm-workbuddy-pool-prov-tag"
|
|
1448
|
+
};
|
|
1379
1449
|
const probeLine = [
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1450
|
+
outcomeLine,
|
|
1451
|
+
remaining,
|
|
1452
|
+
reason
|
|
1383
1453
|
].filter((part) => part !== void 0 && part !== "").join(" · ");
|
|
1384
1454
|
return (0, react.createElement)("div", {
|
|
1385
1455
|
className: `dsm-workbuddy-pool-row${account.current ? " dsm-workbuddy-pool-row-current" : ""}`,
|
|
@@ -1396,7 +1466,7 @@ window.__ModuleLoader__.load({
|
|
|
1396
1466
|
}), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-account-name" }, (0, react.createElement)("b", null, name), account.current ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-tag" }, t("row.poolCurrentBadge")) : null, (0, react.createElement)("span", { className: checked ? "dsm-workbuddy-pool-member" : "dsm-workbuddy-pool-not-member" }, checked ? t("row.poolMember") : t("row.poolNotMember")), excluded === void 0 ? probe === void 0 ? null : (0, react.createElement)("span", null, t("row.poolTestedAt", { at: formatShort(probe.atMs) })) : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-excluded" }, excluded))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-credits" }, account.credits === void 0 ? (0, react.createElement)("span", null, "—") : (0, react.createElement)("b", null, formatNumber$1(account.credits)), account.expiringSoon !== void 0 && account.expiringSoon > 0 ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon" }, t("row.poolCreditSoon", { count: formatNumber$1(account.expiringSoon) })) : account.nearestExpiryMs === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon-plain" }, t("row.poolCreditNearest", { at: formatShort(account.nearestExpiryMs) }))), (0, react.createElement)("span", {
|
|
1397
1467
|
className: "dsm-workbuddy-pool-probe",
|
|
1398
1468
|
...probe?.message === void 0 ? {} : { title: probe.message }
|
|
1399
|
-
}, probeLine), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
|
|
1469
|
+
}, (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))), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
|
|
1400
1470
|
className: "dsm-workbuddy-pool-unknown",
|
|
1401
1471
|
title: t("row.poolNeverTested")
|
|
1402
1472
|
}, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null);
|
|
@@ -1431,7 +1501,22 @@ window.__ModuleLoader__.load({
|
|
|
1431
1501
|
}, t("row.poolTargetStaleOption", { model: active.targetModelId })) : null, ...(pool.catalog ?? []).map((model) => (0, react.createElement)("option", {
|
|
1432
1502
|
value: model.id,
|
|
1433
1503
|
key: model.id
|
|
1434
|
-
}, model.creditMultiplier === void 0 ? model.name : `${model.name} (x${model.creditMultiplier.toFixed(2)})`)))))
|
|
1504
|
+
}, model.creditMultiplier === void 0 ? model.name : `${model.name} (x${model.creditMultiplier.toFixed(2)})`))))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolProbeSizeLabel")), (0, react.createElement)("span", null, t("row.poolProbeSizeHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("select", {
|
|
1505
|
+
className: "dsm-workbuddy-pool-select",
|
|
1506
|
+
value: String(active.probeInputTokens),
|
|
1507
|
+
disabled: !canEdit,
|
|
1508
|
+
onChange: (event) => {
|
|
1509
|
+
const parsed = Number.parseInt(event.currentTarget.value, 10);
|
|
1510
|
+
const next = Number.isFinite(parsed) ? parsed : 0;
|
|
1511
|
+
onEdit((current) => ({
|
|
1512
|
+
...current,
|
|
1513
|
+
probeInputTokens: next
|
|
1514
|
+
}));
|
|
1515
|
+
}
|
|
1516
|
+
}, (0, react.createElement)("option", { value: "0" }, t("row.poolProbeSizeDefault")), ...PROBE_INPUT_TOKEN_CHOICES.map((tokens) => (0, react.createElement)("option", {
|
|
1517
|
+
value: String(tokens),
|
|
1518
|
+
key: String(tokens)
|
|
1519
|
+
}, t("row.poolProbeSizeOption", { tokens: String(tokens / 1e3) }))))))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-save-bar" }, (0, react.createElement)("a", {
|
|
1435
1520
|
className: "dsm-workbuddy-usage-cheer",
|
|
1436
1521
|
href: WORKBUDDY_GITHUB_URL,
|
|
1437
1522
|
target: "_blank",
|
|
@@ -1705,6 +1790,14 @@ window.__ModuleLoader__.load({
|
|
|
1705
1790
|
.dsm-workbuddy-pool-soon{font-size:11px;line-height:16px;color:var(--dsw-alias-state-warn-primary,#f59e0b)}
|
|
1706
1791
|
.dsm-workbuddy-pool-soon-plain{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
1707
1792
|
.dsm-workbuddy-pool-probe,.dsm-workbuddy-pool-checkin{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary,#999);min-width:0;overflow-wrap:anywhere}
|
|
1793
|
+
.dsm-workbuddy-pool-probe{display:flex;flex-direction:column;gap:4px}
|
|
1794
|
+
.dsm-workbuddy-pool-probe-main{color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
1795
|
+
.dsm-workbuddy-pool-probe-prov{display:flex;align-items:center;gap:5px;font-size:11px;line-height:15px;color:var(--dsw-alias-label-tertiary,#999);min-width:0;flex-wrap:wrap}
|
|
1796
|
+
.dsm-workbuddy-pool-prov-tag{padding:1px 6px;border-radius:999px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-tertiary,#999);font-size:10px;font-weight:600;line-height:15px;white-space:nowrap}
|
|
1797
|
+
.dsm-workbuddy-pool-prov-live{background:rgba(245,158,11,.16);color:var(--dsw-alias-state-warn-primary,#f59e0b)}
|
|
1798
|
+
.dsm-workbuddy-pool-prov-unknown{background:var(--dsw-alias-bg-layer-3,#2a2c33);border:1px solid var(--dsw-alias-border-l1,#2c2d31);color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
1799
|
+
.dsm-workbuddy-pool-prov-sep{color:var(--dsw-alias-label-dimmed,#9aa0a6);opacity:.7}
|
|
1800
|
+
.dsm-workbuddy-pool-prov-age{color:var(--dsw-alias-label-tertiary,#999);white-space:nowrap}
|
|
1708
1801
|
.dsm-workbuddy-pool-members{display:flex;flex-direction:column;gap:10px}
|
|
1709
1802
|
.dsm-workbuddy-pool-members-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
1710
1803
|
.dsm-workbuddy-pool-members-title{margin:0 0 3px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
@@ -3077,6 +3170,13 @@ window.__ModuleLoader__.load({
|
|
|
3077
3170
|
"row.remainingMinutes": "in about {count} min",
|
|
3078
3171
|
"row.remainingHours": "in about {count} h",
|
|
3079
3172
|
"row.remainingDays": "in about {count} d",
|
|
3173
|
+
"row.poolProbeJustNow": "just now",
|
|
3174
|
+
"row.poolProbeMinutesAgo": "{count} min ago",
|
|
3175
|
+
"row.poolProbeHoursAgo": "{count} h ago",
|
|
3176
|
+
"row.poolProbeDaysAgo": "{count} d ago",
|
|
3177
|
+
"row.poolProbeSourceTest": "tested",
|
|
3178
|
+
"row.poolProbeSourceLive": "hit by a live request",
|
|
3179
|
+
"row.poolProbeSourceUnknown": "source unknown",
|
|
3080
3180
|
"row.poolSettingsTitle": "Account pool settings",
|
|
3081
3181
|
"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.",
|
|
3082
3182
|
"row.poolEnabled": "Enable the account pool",
|
|
@@ -3087,6 +3187,10 @@ window.__ModuleLoader__.load({
|
|
|
3087
3187
|
"row.poolTargetLabel": "Target test model",
|
|
3088
3188
|
"row.poolTargetHint": "Auto (default) picks a zero-multiplier model from this region's catalog, preferring the largest context window. The two regions can pick different models. A manual choice always wins.",
|
|
3089
3189
|
"row.poolTargetAutoOption": "Auto — pick a free model",
|
|
3190
|
+
"row.poolProbeSizeLabel": "Test request size",
|
|
3191
|
+
"row.poolProbeSizeHint": "How much text a test sends. The upstream refuses a request once it grows past roughly 20K–30K tokens, so a test smaller than your real conversations answers \"usable\" for an account that would refuse them. Bigger tests are more truthful but cost more credit each.",
|
|
3192
|
+
"row.poolProbeSizeDefault": "Default (25K)",
|
|
3193
|
+
"row.poolProbeSizeOption": "{tokens}K tokens",
|
|
3090
3194
|
"row.poolSaved": "Save",
|
|
3091
3195
|
"row.poolSaving": "Saving…",
|
|
3092
3196
|
"row.poolSaveFailed": "Could not save: {message}",
|
|
@@ -3254,6 +3358,13 @@ window.__ModuleLoader__.load({
|
|
|
3254
3358
|
"row.remainingMinutes": "约 {count} 分钟后",
|
|
3255
3359
|
"row.remainingHours": "约 {count} 小时后",
|
|
3256
3360
|
"row.remainingDays": "约 {count} 天后",
|
|
3361
|
+
"row.poolProbeJustNow": "刚刚",
|
|
3362
|
+
"row.poolProbeMinutesAgo": "{count} 分钟前",
|
|
3363
|
+
"row.poolProbeHoursAgo": "{count} 小时前",
|
|
3364
|
+
"row.poolProbeDaysAgo": "{count} 天前",
|
|
3365
|
+
"row.poolProbeSourceTest": "测得",
|
|
3366
|
+
"row.poolProbeSourceLive": "聊天撞上",
|
|
3367
|
+
"row.poolProbeSourceUnknown": "来源未知",
|
|
3257
3368
|
"row.poolSettingsTitle": "账号池设置",
|
|
3258
3369
|
"row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
|
|
3259
3370
|
"row.poolEnabled": "启用账号池",
|
|
@@ -3264,6 +3375,10 @@ window.__ModuleLoader__.load({
|
|
|
3264
3375
|
"row.poolTargetLabel": "目标测试模型",
|
|
3265
3376
|
"row.poolTargetHint": "「自动」(默认)会从本区域目录里挑一个倍率为 0 的免费模型,多个候选取上下文窗口最大的。国内版与国际版可能挑到不同的模型。手动指定优先于自动。",
|
|
3266
3377
|
"row.poolTargetAutoOption": "自动 —— 挑一个免费模型",
|
|
3378
|
+
"row.poolProbeSizeLabel": "测试请求体积",
|
|
3379
|
+
"row.poolProbeSizeHint": "测试时发送多少文本。上游在请求大约超过 2 万~3 万 token 时会开始拒绝,所以测试体积若小于你真实对话的体积,就会出现「测出可用、实际一发就被限」。体积越大测得越准,但每次测试消耗的积分也越多。",
|
|
3380
|
+
"row.poolProbeSizeDefault": "默认(2.5 万)",
|
|
3381
|
+
"row.poolProbeSizeOption": "{tokens} 万 token",
|
|
3267
3382
|
"row.poolSaved": "保存",
|
|
3268
3383
|
"row.poolSaving": "保存中…",
|
|
3269
3384
|
"row.poolSaveFailed": "保存失败:{message}",
|
|
@@ -2556,11 +2556,26 @@ const FALLBACK_WORKBUDDY_MODELS_GLOBAL = [
|
|
|
2556
2556
|
function fallbackModelsFor(region) {
|
|
2557
2557
|
return region === "global" ? FALLBACK_WORKBUDDY_MODELS_GLOBAL : FALLBACK_WORKBUDDY_MODELS;
|
|
2558
2558
|
}
|
|
2559
|
-
/**
|
|
2559
|
+
/**
|
|
2560
|
+
* Apply the saved local DSH budget. A budget is an UPPER LIMIT the user sets,
|
|
2561
|
+
* never a default: a model without an explicit budget keeps its own window, and
|
|
2562
|
+
* a budget can only ever LOWER a window (`Math.min`), so a value above the
|
|
2563
|
+
* native one is not an error, just a no-op.
|
|
2564
|
+
*
|
|
2565
|
+
* There used to be a `?? 200_000` default here (issue #33), on the rationale
|
|
2566
|
+
* that a lower advertised window makes DSH compact sooner and so keeps a long
|
|
2567
|
+
* session under the upstream throttle. That rationale does not hold: the
|
|
2568
|
+
* measured throttle fires on a SINGLE request past 20k-30k input tokens (the
|
|
2569
|
+
* CHANGELOG's own wording calls 200K "远超上游 ~20–30k 的节流线"), so clamping to
|
|
2570
|
+
* 200K bought none of the safety it claimed while costing every >200K model its
|
|
2571
|
+
* real window — and contradicting the static roster in this very file, which
|
|
2572
|
+
* declares those models as 1M. A user who wants the conservative behaviour sets
|
|
2573
|
+
* a budget explicitly; the card offers that per model.
|
|
2574
|
+
*/
|
|
2560
2575
|
function applyContextBudgets(catalog, budgets = {}) {
|
|
2561
2576
|
return catalog.map((model) => ({
|
|
2562
2577
|
...model,
|
|
2563
|
-
contextWindow: model.
|
|
2578
|
+
contextWindow: budgets[model.id] === void 0 ? model.contextWindow : Math.min(model.contextWindow, budgets[model.id])
|
|
2564
2579
|
}));
|
|
2565
2580
|
}
|
|
2566
2581
|
function deriveCatalog(catalog, enabled, budgets = {}) {
|
|
@@ -2791,7 +2806,7 @@ function generationOf(version) {
|
|
|
2791
2806
|
//#endregion
|
|
2792
2807
|
//#region src/version.ts
|
|
2793
2808
|
/** The npm package version this build was produced from. */
|
|
2794
|
-
const WORKBUDDY_CONNECT_VERSION = "3.4.
|
|
2809
|
+
const WORKBUDDY_CONNECT_VERSION = "3.4.5";
|
|
2795
2810
|
//#endregion
|
|
2796
2811
|
//#region src/host-heartbeat.ts
|
|
2797
2812
|
/**
|
|
@@ -2949,4 +2964,4 @@ async function clearHostHeartbeat() {
|
|
|
2949
2964
|
await rm(workbuddyHostHeartbeatPath(), { force: true });
|
|
2950
2965
|
}
|
|
2951
2966
|
//#endregion
|
|
2952
|
-
export {
|
|
2967
|
+
export { macosNestedAppBundles as $, workbuddyOwnAuthPath as A, parseUpstreamModel as B, defaultDesktopAuthDirs as C, legacyWorkbuddyOwnAuthPath as D, isEncryptedCredentialError as E, classifyUpstreamError as F, deriveAtRestKey as G, regionOf as H, declaredTools as I, findWorkbuddyAppExecutable as J, deriveAtRestKeyId as K, isCredentialRejectedError as L, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as M, WorkBuddyCredentialRejectedError as N, parseWorkBuddyAuth as O, WorkBuddyUpstreamClient as P, macosBundleExecutable as Q, parseCreditMultiplier as R, defaultDesktopAuthCandidates as S, hasEncryptedCredentialFields as T, WORKBUDDY_APP_EXECUTABLE_ENV as U, prepareChatBody as V, clearAtRestKeyCache as W, isEncryptedFieldWrapper as X, findWorkbuddyAppExecutableWithSource as Y, isWorkbuddyBundle as Z, WORKBUDDY_AUTH_FILENAME as _, readHostHeartbeat as a, WorkBuddyEncryptedCredentialError as b, WORKBUDDY_CONNECT_VERSION as c, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, openEncryptedField as et, WorkBuddyCatalog as f, ENCRYPTED_CREDENTIAL_CODE as g, fallbackModelsFor as h, processStartTimeMs as i, CREDENTIAL_REJECTED_CODE as j, workbuddyAccountId as k, piAiRuntimeInfo as l, deriveCatalog as m, clearHostHeartbeat as n, workbuddyAppExecutableCandidates as nt, workbuddyHostHeartbeatPath as o, applyContextBudgets as p, fetchAtRestKeyPayload as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, readAtRestKey as tt, FALLBACK_WORKBUDDY_MODELS as u, WORKBUDDY_AUTH_FILE_ENV as v, defaultDesktopAuthPath as w, authFileName as x, WorkBuddyCredentialStore as y, parseReasoning as z };
|
package/lib/index.d.ts
CHANGED
|
@@ -1351,7 +1351,7 @@ declare const PROBE_SYSTEM_PROMPT = "You are a connectivity check. Reply with a
|
|
|
1351
1351
|
* Output cap for a probe. One token is enough to prove the model answers.
|
|
1352
1352
|
*
|
|
1353
1353
|
* The probe's cost comes from its INPUT, not its output (see
|
|
1354
|
-
* {@link
|
|
1354
|
+
* {@link DEFAULT_PROBE_INPUT_TOKENS}), so paying for a long completion would add money
|
|
1355
1355
|
* without adding signal.
|
|
1356
1356
|
*/
|
|
1357
1357
|
declare const PROBE_MAX_TOKENS = 1;
|
|
@@ -1378,8 +1378,27 @@ declare const PROBE_MAX_TOKENS = 1;
|
|
|
1378
1378
|
* A probe that sent a handful of tokens answered "usable" while every real
|
|
1379
1379
|
* request in a long conversation was refused — the probe was asking too small a
|
|
1380
1380
|
* question. 25k is sized to be a realistic long-conversation payload.
|
|
1381
|
+
*
|
|
1382
|
+
* Renamed from `PROBE_INPUT_TOKENS` to say what it now IS: the DEFAULT, not the
|
|
1383
|
+
* only possible size. The size is a property of the user's own traffic (see
|
|
1384
|
+
* {@link probeRequestBody}), so it became a setting; this constant is what an
|
|
1385
|
+
* unset setting, an older profile, and every existing test mean by "a probe".
|
|
1386
|
+
*/
|
|
1387
|
+
declare const DEFAULT_PROBE_INPUT_TOKENS = 25000;
|
|
1388
|
+
/**
|
|
1389
|
+
* The probe sizes the card offers, in tokens.
|
|
1390
|
+
*
|
|
1391
|
+
* A closed set, each step a meaningful position relative to the measured 20k~30k
|
|
1392
|
+
* threshold: below it (10k/20k) proves an account can serve SHORT requests and
|
|
1393
|
+
* is the cheap choice; at it (30k) is the size that finally covers the whole
|
|
1394
|
+
* measured window; above it (50k/100k) is for conversations that routinely run
|
|
1395
|
+
* long, where a smaller probe would report "usable" for an account that refuses
|
|
1396
|
+
* the very next message.
|
|
1397
|
+
*
|
|
1398
|
+
* Exported so the schema's description, the card's dropdown, and the coercion
|
|
1399
|
+
* below cannot drift apart into three different menus.
|
|
1381
1400
|
*/
|
|
1382
|
-
declare const
|
|
1401
|
+
declare const PROBE_INPUT_TOKEN_CHOICES: readonly number[];
|
|
1383
1402
|
/**
|
|
1384
1403
|
* Build the request body for one probe.
|
|
1385
1404
|
*
|
|
@@ -1387,10 +1406,13 @@ declare const PROBE_INPUT_TOKENS = 25000;
|
|
|
1387
1406
|
* `prepareChatBody` enforces that too, but sending it explicitly keeps this
|
|
1388
1407
|
* function's output valid on its own.
|
|
1389
1408
|
*
|
|
1390
|
-
* Measured on the built artifact: 112,713 bytes of body, and
|
|
1391
|
-
* counted `prompt_tokens: 25023` at a cost of `credit: 0.72`.
|
|
1409
|
+
* Measured on the built artifact at the DEFAULT size: 112,713 bytes of body, and
|
|
1410
|
+
* the upstream counted `prompt_tokens: 25023` at a cost of `credit: 0.72`. The
|
|
1411
|
+
* body bytes and the credit both scale with the size asked for, which is why the
|
|
1412
|
+
* size is a user setting: a larger probe answers more truthfully for someone
|
|
1413
|
+
* whose conversations are long, and costs proportionally more to ask.
|
|
1392
1414
|
*/
|
|
1393
|
-
declare function probeRequestBody(modelId: string): string;
|
|
1415
|
+
declare function probeRequestBody(modelId: string, inputTokens?: number): string;
|
|
1394
1416
|
/**
|
|
1395
1417
|
* How one probe ended.
|
|
1396
1418
|
*
|
|
@@ -1509,12 +1531,35 @@ declare function probeModel(input: {
|
|
|
1509
1531
|
credential: WorkBuddyCredential;
|
|
1510
1532
|
modelId: string;
|
|
1511
1533
|
nowMs: number;
|
|
1534
|
+
/**
|
|
1535
|
+
* How much input this probe sends, in tokens.
|
|
1536
|
+
*
|
|
1537
|
+
* Omitted means {@link DEFAULT_PROBE_INPUT_TOKENS}, so every existing caller
|
|
1538
|
+
* keeps the measured 25k and only the pool's user-facing "test" passes a
|
|
1539
|
+
* chosen size through.
|
|
1540
|
+
*/
|
|
1541
|
+
inputTokens?: number;
|
|
1512
1542
|
/** The region's next monthly quota refresh, when known. */
|
|
1513
1543
|
quotaRefreshAtMs?: number;
|
|
1514
1544
|
signal?: AbortSignal;
|
|
1515
1545
|
}): Promise<WorkBuddyProbeResult>;
|
|
1516
1546
|
//#endregion
|
|
1517
1547
|
//#region src/account-pool.d.ts
|
|
1548
|
+
/**
|
|
1549
|
+
* Which of the pool's writers produced a measurement.
|
|
1550
|
+
*
|
|
1551
|
+
* `test-batch` — the user pressed "test" (or the check-in batch), so the
|
|
1552
|
+
* outcome is a deliberate measurement of the target model.
|
|
1553
|
+
* `live-request` — a real chat request failed and its failure was recorded so
|
|
1554
|
+
* the NEXT request starts from a measurement instead of rediscovering the
|
|
1555
|
+
* same wall.
|
|
1556
|
+
*
|
|
1557
|
+
* The distinction is diagnostic, not behavioural: both feed the same routing
|
|
1558
|
+
* and cooling logic. It exists so a user who watched an account measure `ok`
|
|
1559
|
+
* and later saw it limited can tell whether that was a later live failure
|
|
1560
|
+
* (expected) or an older value winning (a bug).
|
|
1561
|
+
*/
|
|
1562
|
+
type WorkBuddyPoolProbeSource = 'test-batch' | 'live-request';
|
|
1518
1563
|
/** One account's pool entry: what the picker needs, plus its last measurement. */
|
|
1519
1564
|
interface WorkBuddyPoolAccount {
|
|
1520
1565
|
/** Stable account id (the same one `WorkBuddyAccountChoice` carries). */
|
|
@@ -1534,6 +1579,26 @@ interface WorkBuddyPoolProbe {
|
|
|
1534
1579
|
outcome: WorkBuddyProbeOutcome;
|
|
1535
1580
|
/** When this measurement was taken, in epoch ms. */
|
|
1536
1581
|
atMs: number;
|
|
1582
|
+
/**
|
|
1583
|
+
* WHAT WROTE THIS MEASUREMENT.
|
|
1584
|
+
*
|
|
1585
|
+
* Two sites write probes for the same account, and before this field they
|
|
1586
|
+
* were indistinguishable in the file — which made a real confusion
|
|
1587
|
+
* unanswerable: the user presses "test", sees an account measured `ok`, and
|
|
1588
|
+
* later finds it `rate-limited`. Nothing in the record said whether that was
|
|
1589
|
+
* a NEWER measurement (a live request really did hit the limit) or an OLDER
|
|
1590
|
+
* one somehow winning, and the file keeps no history to tell them apart.
|
|
1591
|
+
*
|
|
1592
|
+
* With the source recorded, that question has an answer on sight: a
|
|
1593
|
+
* `live-request` entry that replaced a `test-batch` result is the pool
|
|
1594
|
+
* working as designed (a live failure steers the next request away from the
|
|
1595
|
+
* account); two entries of the SAME source with the OLDER one winning would
|
|
1596
|
+
* be a genuine bug.
|
|
1597
|
+
*
|
|
1598
|
+
* ABSENT for any measurement written before this field existed, so old probe
|
|
1599
|
+
* files stay readable — an absent source means "unknown", never a guess.
|
|
1600
|
+
*/
|
|
1601
|
+
source?: WorkBuddyPoolProbeSource;
|
|
1537
1602
|
/**
|
|
1538
1603
|
* When this account may be usable again, when the upstream stated a time.
|
|
1539
1604
|
*
|
|
@@ -1961,6 +2026,14 @@ interface WorkBuddyWebPoolAccount {
|
|
|
1961
2026
|
probe?: {
|
|
1962
2027
|
outcome: WorkBuddyWebProbeOutcome;
|
|
1963
2028
|
atMs: number;
|
|
2029
|
+
/**
|
|
2030
|
+
* What wrote this measurement, so the card can explain a change the user
|
|
2031
|
+
* watched happen: `test-batch` is a deliberate test, `live-request` is a
|
|
2032
|
+
* real request that failed and overwrote it. ABSENT for records written
|
|
2033
|
+
* before the field existed — the card must then say "unknown" rather than
|
|
2034
|
+
* guess which of the two it was.
|
|
2035
|
+
*/
|
|
2036
|
+
source?: 'test-batch' | 'live-request';
|
|
1964
2037
|
/** Only when the upstream named a time; never invented locally. */
|
|
1965
2038
|
retryAtMs?: number;
|
|
1966
2039
|
/**
|
|
@@ -2030,6 +2103,14 @@ interface WorkBuddyWebPool {
|
|
|
2030
2103
|
targetModelSource: 'preferred' | 'free' | 'none' | 'stale';
|
|
2031
2104
|
/** The account ids the user has checked into this region's pool. */
|
|
2032
2105
|
memberAccountIds?: readonly string[];
|
|
2106
|
+
/**
|
|
2107
|
+
* Tokens each test probe sends, as the user saved it; `0`/absent = default.
|
|
2108
|
+
*
|
|
2109
|
+
* The SAVED value, not the resolved one: the card needs to tell "the user
|
|
2110
|
+
* never chose a size" apart from "the user chose 25000", because the first
|
|
2111
|
+
* must keep showing the default option rather than a number they never picked.
|
|
2112
|
+
*/
|
|
2113
|
+
probeInputTokens?: number;
|
|
2033
2114
|
/**
|
|
2034
2115
|
* The subset of {@link memberAccountIds} that resolves to a real local
|
|
2035
2116
|
* sign-in — i.e. what a batch actually runs on.
|
|
@@ -2188,6 +2269,14 @@ interface WorkBuddyPoolDeps {
|
|
|
2188
2269
|
preferences(region: WorkBuddyRegion): {
|
|
2189
2270
|
enabled: boolean;
|
|
2190
2271
|
targetModelId: string;
|
|
2272
|
+
/**
|
|
2273
|
+
* Tokens each test probe sends as saved; `0` when the user never chose.
|
|
2274
|
+
*
|
|
2275
|
+
* Optional so a caller that has not learnt about the setting still
|
|
2276
|
+
* satisfies this interface, and so its absence reads as "default" — the
|
|
2277
|
+
* same meaning the host gives an older profile.
|
|
2278
|
+
*/
|
|
2279
|
+
probeInputTokens?: number;
|
|
2191
2280
|
memberAccountIds: readonly string[];
|
|
2192
2281
|
};
|
|
2193
2282
|
/** One region's CHECKED accounts, plus their credits and last measurements. */
|
|
@@ -2344,6 +2433,23 @@ interface WorkBuddyPoolPreferences {
|
|
|
2344
2433
|
enabled?: boolean;
|
|
2345
2434
|
/** Model id to test; `''` means auto-pick a free model from this region. */
|
|
2346
2435
|
targetModelId?: string;
|
|
2436
|
+
/**
|
|
2437
|
+
* How much input a probe sends, in tokens — the pool's biggest single knob.
|
|
2438
|
+
*
|
|
2439
|
+
* The upstream's rate limit (6004) fires on request SIZE, and the threshold
|
|
2440
|
+
* was measured to sit between 20k and 30k tokens. The probe has to be sized
|
|
2441
|
+
* near where the user's REAL conversations land: too small and it reports
|
|
2442
|
+
* "usable" while every long conversation is refused (the exact false positive
|
|
2443
|
+
* that made this knob necessary), too large and it costs real credit on every
|
|
2444
|
+
* batch AND excludes accounts that would have served the shorter requests the
|
|
2445
|
+
* user actually sends.
|
|
2446
|
+
*
|
|
2447
|
+
* A fixed value therefore cannot be right for everyone — the right size is a
|
|
2448
|
+
* property of the user's own traffic, which only they know. `undefined` keeps
|
|
2449
|
+
* the measured default ({@link DEFAULT_PROBE_INPUT_TOKENS}), so an existing
|
|
2450
|
+
* profile is unaffected.
|
|
2451
|
+
*/
|
|
2452
|
+
probeInputTokens?: number;
|
|
2347
2453
|
/**
|
|
2348
2454
|
* The account ids checked into this region's pool.
|
|
2349
2455
|
*
|
|
@@ -2498,4 +2604,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
|
|
|
2498
2604
|
*/
|
|
2499
2605
|
declare function apply(ctx: Context, config: Config): void;
|
|
2500
2606
|
//#endregion
|
|
2501
|
-
export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID,
|
|
2607
|
+
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, WorkBuddyPoolPreferences, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as macosNestedAppBundles, A as workbuddyOwnAuthPath, B as parseUpstreamModel, C as defaultDesktopAuthDirs, D as legacyWorkbuddyOwnAuthPath, E as isEncryptedCredentialError, F as classifyUpstreamError, G as deriveAtRestKey, H as regionOf, I as declaredTools, J as findWorkbuddyAppExecutable, K as deriveAtRestKeyId, L as isCredentialRejectedError, M as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, N as WorkBuddyCredentialRejectedError, O as parseWorkBuddyAuth, P as WorkBuddyUpstreamClient, Q as macosBundleExecutable, R as parseCreditMultiplier, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as WORKBUDDY_APP_EXECUTABLE_ENV, V as prepareChatBody, W as clearAtRestKeyCache, X as isEncryptedFieldWrapper, Z as isWorkbuddyBundle, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as openEncryptedField, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, j as CREDENTIAL_REJECTED_CODE, k as workbuddyAccountId, l as piAiRuntimeInfo, m as deriveCatalog, n as clearHostHeartbeat, nt as workbuddyAppExecutableCandidates, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as fetchAtRestKeyPayload, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as readAtRestKey, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as parseReasoning } from "./host-heartbeat-CTigiLJK.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -1342,10 +1342,49 @@ var RecoveryStream = class {
|
|
|
1342
1342
|
*/
|
|
1343
1343
|
nativeCallsSeen = false;
|
|
1344
1344
|
buffer;
|
|
1345
|
-
|
|
1345
|
+
/**
|
|
1346
|
+
* A notice to place at the head of the answer, once, before any prose.
|
|
1347
|
+
*
|
|
1348
|
+
* Set when the request reached this stream only after the pool failed over, so
|
|
1349
|
+
* the user learns WHY the answer came from a different account. Without it a
|
|
1350
|
+
* failover is invisible: the reply looks like it came from the account they
|
|
1351
|
+
* picked, and the only trace is a `logger.warn` they never see. The notice is
|
|
1352
|
+
* emitted ahead of the model's own text so it cannot be mistaken for part of
|
|
1353
|
+
* the answer, and only once — a retry that never delivers text must not leave
|
|
1354
|
+
* a stray line behind.
|
|
1355
|
+
*/
|
|
1356
|
+
notice;
|
|
1357
|
+
constructor(res, gate, logger, notice) {
|
|
1346
1358
|
this.res = res;
|
|
1347
1359
|
this.logger = logger;
|
|
1348
1360
|
this.buffer = gate === void 0 ? void 0 : new DsmlStreamBuffer(gate);
|
|
1361
|
+
this.notice = notice;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Emit the pending failover notice, if any, and forget it.
|
|
1365
|
+
*
|
|
1366
|
+
* Called immediately before the first content reaches the client. Placed
|
|
1367
|
+
* here rather than in the constructor because a stream whose write fails, or
|
|
1368
|
+
* whose attempt is discarded in favour of a retry, must not have already told
|
|
1369
|
+
* the user about a switch it never completed.
|
|
1370
|
+
*/
|
|
1371
|
+
flushNotice() {
|
|
1372
|
+
const notice = this.notice;
|
|
1373
|
+
if (notice === void 0) return;
|
|
1374
|
+
this.notice = void 0;
|
|
1375
|
+
if (!this.res.writable) return;
|
|
1376
|
+
const chunk = { choices: [{
|
|
1377
|
+
index: 0,
|
|
1378
|
+
delta: {},
|
|
1379
|
+
finish_reason: ""
|
|
1380
|
+
}] };
|
|
1381
|
+
const choiceRecord = {
|
|
1382
|
+
index: 0,
|
|
1383
|
+
delta: {},
|
|
1384
|
+
finish_reason: ""
|
|
1385
|
+
};
|
|
1386
|
+
this.windowOpened = true;
|
|
1387
|
+
this.res.write(contentFrame(chunk, choiceRecord, notice));
|
|
1349
1388
|
}
|
|
1350
1389
|
/** True when a call or real prose has reached the client. */
|
|
1351
1390
|
get delivered() {
|
|
@@ -1444,6 +1483,7 @@ var RecoveryStream = class {
|
|
|
1444
1483
|
if (Array.isArray(nativeCalls) && nativeCalls.length > 0) {
|
|
1445
1484
|
this.nativeCallsSeen = true;
|
|
1446
1485
|
this.windowOpened = true;
|
|
1486
|
+
this.flushNotice();
|
|
1447
1487
|
this.send(frame, false);
|
|
1448
1488
|
this.wroteContent = true;
|
|
1449
1489
|
this.closeWindow();
|
|
@@ -1458,6 +1498,7 @@ var RecoveryStream = class {
|
|
|
1458
1498
|
}
|
|
1459
1499
|
if (this.buffer === void 0 || this.nativeCallsSeen) {
|
|
1460
1500
|
this.windowOpened = true;
|
|
1501
|
+
this.flushNotice();
|
|
1461
1502
|
this.closeWindow();
|
|
1462
1503
|
this.res.write(frame);
|
|
1463
1504
|
this.wroteContent = true;
|
|
@@ -1466,6 +1507,7 @@ var RecoveryStream = class {
|
|
|
1466
1507
|
this.windowOpened = true;
|
|
1467
1508
|
const outcome = this.buffer.add(content);
|
|
1468
1509
|
if (outcome.calls !== void 0 && outcome.calls.length > 0) {
|
|
1510
|
+
this.flushNotice();
|
|
1469
1511
|
this.send(callsFrame(chunk, choiceRecord, outcome.calls), false);
|
|
1470
1512
|
this.wroteContent = true;
|
|
1471
1513
|
this.closeWindow();
|
|
@@ -1478,6 +1520,7 @@ var RecoveryStream = class {
|
|
|
1478
1520
|
return;
|
|
1479
1521
|
}
|
|
1480
1522
|
if (outcome.prose) {
|
|
1523
|
+
this.flushNotice();
|
|
1481
1524
|
this.send(contentFrame(chunk, choiceRecord, outcome.text), false);
|
|
1482
1525
|
this.wroteContent = true;
|
|
1483
1526
|
this.closeWindow();
|
|
@@ -1521,6 +1564,7 @@ var RecoveryStream = class {
|
|
|
1521
1564
|
this.windowOpened = true;
|
|
1522
1565
|
const frame = contentFrame(chunk, choiceRecord, text);
|
|
1523
1566
|
if (text.trim() !== "") {
|
|
1567
|
+
this.flushNotice();
|
|
1524
1568
|
this.send(frame, false);
|
|
1525
1569
|
this.wroteContent = true;
|
|
1526
1570
|
this.closeWindow();
|
|
@@ -1598,6 +1642,55 @@ function callsFrame(chunk, choice, calls) {
|
|
|
1598
1642
|
};
|
|
1599
1643
|
return `data: ${JSON.stringify(next)}\n\n`;
|
|
1600
1644
|
}
|
|
1645
|
+
/**
|
|
1646
|
+
* How to name a pool account in a sentence the USER reads.
|
|
1647
|
+
*
|
|
1648
|
+
* Prefers the phone number (`uin`) because that is what the pool table shows and
|
|
1649
|
+
* what a person recognises; the nickname and the opaque account id are fallbacks
|
|
1650
|
+
* so this can never render an empty name. Deliberately NOT the hashed account id
|
|
1651
|
+
* on its own: a 24-character hex digest identifies an account to the plugin, but
|
|
1652
|
+
* to the person reading the answer it identifies nothing.
|
|
1653
|
+
*/
|
|
1654
|
+
function describeAccount(credential) {
|
|
1655
|
+
if (credential.uin !== void 0 && credential.uin !== "") return credential.uin;
|
|
1656
|
+
if (credential.nickname !== void 0 && credential.nickname !== "") return credential.nickname;
|
|
1657
|
+
return workbuddyAccountId(credential);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* One short phrase for why an account was abandoned, for the in-reply notice.
|
|
1661
|
+
*
|
|
1662
|
+
* Kept to the distinction the user can act on rather than the internal kind
|
|
1663
|
+
* name: "rate limited" says wait or add an account, "rejected" says the sign-in
|
|
1664
|
+
* is the problem. The numeric kind would only send them to the log.
|
|
1665
|
+
*/
|
|
1666
|
+
function failoverReasonText(failure) {
|
|
1667
|
+
if (failure.ok) return "unusable";
|
|
1668
|
+
switch (failure.kind) {
|
|
1669
|
+
case "soft_rate":
|
|
1670
|
+
case "hard_credit": return "rate limited";
|
|
1671
|
+
case "session_dead": return "signed out";
|
|
1672
|
+
case "policy_reject": return "rejected by policy";
|
|
1673
|
+
case "not_found": return "model unavailable";
|
|
1674
|
+
case "server": return "erroring upstream";
|
|
1675
|
+
default: return `unusable (http ${failure.status})`;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* The line prepended to an answer that came from a failover account.
|
|
1680
|
+
*
|
|
1681
|
+
* Block-quoted and bracketed so it reads as machinery rather than as the model's
|
|
1682
|
+
* own words, and so a user skimming for the answer can skip it. It states the
|
|
1683
|
+
* reason AND the account left behind, because those are the two facts that make
|
|
1684
|
+
* an unexpected reply explicable.
|
|
1685
|
+
*
|
|
1686
|
+
* The wording avoids "switched TO <account>" on purpose: the account that took
|
|
1687
|
+
* over is the one the user is about to keep talking to, and naming it invites
|
|
1688
|
+
* the reading that they must now manage it. What they need is to know the one
|
|
1689
|
+
* they chose did not answer, and why.
|
|
1690
|
+
*/
|
|
1691
|
+
function failoverNoticeText(from, reason) {
|
|
1692
|
+
return `\n\n> [dsh-connect-workbuddy] 「${from}」was ${reason} — this answer came from another account\n\n`;
|
|
1693
|
+
}
|
|
1601
1694
|
/** One frame carrying replacement text for a frame whose content was consumed. */
|
|
1602
1695
|
function contentFrame(chunk, choice, content) {
|
|
1603
1696
|
const delta = {
|
|
@@ -1726,6 +1819,15 @@ function createWorkBuddyShim(options) {
|
|
|
1726
1819
|
let result = await client.chatStream(credential, prepared, controller.signal);
|
|
1727
1820
|
const triedAccountIds = [];
|
|
1728
1821
|
/**
|
|
1822
|
+
* The account the request STARTED on, and the reason it was abandoned.
|
|
1823
|
+
*
|
|
1824
|
+
* `??=` on the first switch only: if three accounts fail in a row the user
|
|
1825
|
+
* cares that the answer did not come from the one they picked, not with a
|
|
1826
|
+
* blow-by-blow list of every candidate that was also rate limited.
|
|
1827
|
+
*/
|
|
1828
|
+
let switchedFrom;
|
|
1829
|
+
let switchedBecause;
|
|
1830
|
+
/**
|
|
1729
1831
|
* Tell the plugin about a failed attempt so it can store the measurement.
|
|
1730
1832
|
*
|
|
1731
1833
|
* Every failed attempt is reported exactly once, INCLUDING the last one — an
|
|
@@ -1780,6 +1882,8 @@ function createWorkBuddyShim(options) {
|
|
|
1780
1882
|
});
|
|
1781
1883
|
if (controller.signal.aborted) break;
|
|
1782
1884
|
logger?.warn(`dsh-connect-workbuddy: retrying chat on another pool account after ${result.kind} (http ${result.status})`);
|
|
1885
|
+
switchedFrom ??= describeAccount(credential);
|
|
1886
|
+
switchedBecause ??= failoverReasonText(result);
|
|
1783
1887
|
credential = next;
|
|
1784
1888
|
result = await client.chatStream(credential, prepared, controller.signal);
|
|
1785
1889
|
}
|
|
@@ -1813,7 +1917,8 @@ function createWorkBuddyShim(options) {
|
|
|
1813
1917
|
requiredParameters: declared.requiredParameters,
|
|
1814
1918
|
...declared.pinnedToolName === void 0 ? {} : { pinnedToolName: declared.pinnedToolName }
|
|
1815
1919
|
};
|
|
1816
|
-
const
|
|
1920
|
+
const notice = switchedFrom === void 0 || switchedBecause === void 0 ? void 0 : failoverNoticeText(switchedFrom, switchedBecause);
|
|
1921
|
+
const writer = new RecoveryStream(res, gate, logger, notice);
|
|
1817
1922
|
await writer.consume(result.response.body);
|
|
1818
1923
|
if (gate !== void 0 && !writer.delivered && writer.residueSeen && !controller.signal.aborted) {
|
|
1819
1924
|
logger?.warn("dsh-connect-workbuddy: the turn produced DSML markup only; retrying it once");
|
|
@@ -1868,7 +1973,7 @@ const PROBE_SYSTEM_PROMPT = "You are a connectivity check. Reply with a single w
|
|
|
1868
1973
|
* Output cap for a probe. One token is enough to prove the model answers.
|
|
1869
1974
|
*
|
|
1870
1975
|
* The probe's cost comes from its INPUT, not its output (see
|
|
1871
|
-
* {@link
|
|
1976
|
+
* {@link DEFAULT_PROBE_INPUT_TOKENS}), so paying for a long completion would add money
|
|
1872
1977
|
* without adding signal.
|
|
1873
1978
|
*/
|
|
1874
1979
|
const PROBE_MAX_TOKENS = 1;
|
|
@@ -1895,8 +2000,44 @@ const PROBE_MAX_TOKENS = 1;
|
|
|
1895
2000
|
* A probe that sent a handful of tokens answered "usable" while every real
|
|
1896
2001
|
* request in a long conversation was refused — the probe was asking too small a
|
|
1897
2002
|
* question. 25k is sized to be a realistic long-conversation payload.
|
|
2003
|
+
*
|
|
2004
|
+
* Renamed from `PROBE_INPUT_TOKENS` to say what it now IS: the DEFAULT, not the
|
|
2005
|
+
* only possible size. The size is a property of the user's own traffic (see
|
|
2006
|
+
* {@link probeRequestBody}), so it became a setting; this constant is what an
|
|
2007
|
+
* unset setting, an older profile, and every existing test mean by "a probe".
|
|
2008
|
+
*/
|
|
2009
|
+
const DEFAULT_PROBE_INPUT_TOKENS = 25e3;
|
|
2010
|
+
/**
|
|
2011
|
+
* The probe sizes the card offers, in tokens.
|
|
2012
|
+
*
|
|
2013
|
+
* A closed set, each step a meaningful position relative to the measured 20k~30k
|
|
2014
|
+
* threshold: below it (10k/20k) proves an account can serve SHORT requests and
|
|
2015
|
+
* is the cheap choice; at it (30k) is the size that finally covers the whole
|
|
2016
|
+
* measured window; above it (50k/100k) is for conversations that routinely run
|
|
2017
|
+
* long, where a smaller probe would report "usable" for an account that refuses
|
|
2018
|
+
* the very next message.
|
|
2019
|
+
*
|
|
2020
|
+
* Exported so the schema's description, the card's dropdown, and the coercion
|
|
2021
|
+
* below cannot drift apart into three different menus.
|
|
1898
2022
|
*/
|
|
1899
|
-
const
|
|
2023
|
+
const PROBE_INPUT_TOKEN_CHOICES = [
|
|
2024
|
+
1e4,
|
|
2025
|
+
2e4,
|
|
2026
|
+
3e4,
|
|
2027
|
+
5e4,
|
|
2028
|
+
1e5
|
|
2029
|
+
];
|
|
2030
|
+
/**
|
|
2031
|
+
* A requested probe size reduced to a size this build will actually send.
|
|
2032
|
+
*
|
|
2033
|
+
* Anything outside {@link PROBE_INPUT_TOKEN_CHOICES} — `undefined` from an older
|
|
2034
|
+
* profile, `0` from an unset field, a hand-edited odd number — becomes the
|
|
2035
|
+
* measured default. Coercing rather than rejecting keeps a hand-edited profile
|
|
2036
|
+
* working instead of turning one odd value into a failed test batch.
|
|
2037
|
+
*/
|
|
2038
|
+
function resolveProbeInputTokens(requested) {
|
|
2039
|
+
return requested !== void 0 && PROBE_INPUT_TOKEN_CHOICES.includes(requested) ? requested : DEFAULT_PROBE_INPUT_TOKENS;
|
|
2040
|
+
}
|
|
1900
2041
|
/**
|
|
1901
2042
|
* Characters per token for the filler text, measured.
|
|
1902
2043
|
*
|
|
@@ -1915,11 +2056,14 @@ const FILLER_LINE = "The quick brown fox jumps over the lazy dog. ";
|
|
|
1915
2056
|
* `prepareChatBody` enforces that too, but sending it explicitly keeps this
|
|
1916
2057
|
* function's output valid on its own.
|
|
1917
2058
|
*
|
|
1918
|
-
* Measured on the built artifact: 112,713 bytes of body, and
|
|
1919
|
-
* counted `prompt_tokens: 25023` at a cost of `credit: 0.72`.
|
|
2059
|
+
* Measured on the built artifact at the DEFAULT size: 112,713 bytes of body, and
|
|
2060
|
+
* the upstream counted `prompt_tokens: 25023` at a cost of `credit: 0.72`. The
|
|
2061
|
+
* body bytes and the credit both scale with the size asked for, which is why the
|
|
2062
|
+
* size is a user setting: a larger probe answers more truthfully for someone
|
|
2063
|
+
* whose conversations are long, and costs proportionally more to ask.
|
|
1920
2064
|
*/
|
|
1921
|
-
function probeRequestBody(modelId) {
|
|
1922
|
-
const targetChars = Math.ceil(
|
|
2065
|
+
function probeRequestBody(modelId, inputTokens = DEFAULT_PROBE_INPUT_TOKENS) {
|
|
2066
|
+
const targetChars = Math.ceil(resolveProbeInputTokens(inputTokens) * CHARS_PER_TOKEN);
|
|
1923
2067
|
const filler = FILLER_LINE.repeat(Math.ceil(targetChars / 45));
|
|
1924
2068
|
return JSON.stringify({
|
|
1925
2069
|
model: modelId,
|
|
@@ -2117,7 +2261,7 @@ async function probeModel(input) {
|
|
|
2117
2261
|
let answer;
|
|
2118
2262
|
const startedAt = Date.now();
|
|
2119
2263
|
try {
|
|
2120
|
-
answer = await input.client.probeChat(input.credential, probeRequestBody(input.modelId), input.signal);
|
|
2264
|
+
answer = await input.client.probeChat(input.credential, probeRequestBody(input.modelId, input.inputTokens), input.signal);
|
|
2121
2265
|
} catch (error) {
|
|
2122
2266
|
return {
|
|
2123
2267
|
modelId: input.modelId,
|
|
@@ -2310,6 +2454,7 @@ function probeUpdatesOf(rows) {
|
|
|
2310
2454
|
updates[row.accountId] = {
|
|
2311
2455
|
outcome: row.result.outcome,
|
|
2312
2456
|
atMs,
|
|
2457
|
+
source: "test-batch",
|
|
2313
2458
|
...row.result.retryAtMs === void 0 ? {} : { retryAtMs: row.result.retryAtMs },
|
|
2314
2459
|
...row.result.message === void 0 ? {} : { message: row.result.message }
|
|
2315
2460
|
};
|
|
@@ -2574,7 +2719,7 @@ const POOL_STORE_PREFIX = ".workbuddy-pool";
|
|
|
2574
2719
|
function isProbe(value) {
|
|
2575
2720
|
if (typeof value !== "object" || value === null) return false;
|
|
2576
2721
|
const candidate = value;
|
|
2577
|
-
return typeof candidate["outcome"] === "string" && typeof candidate["atMs"] === "number" && (candidate["retryAtMs"] === void 0 || typeof candidate["retryAtMs"] === "number") && (candidate["message"] === void 0 || typeof candidate["message"] === "string");
|
|
2722
|
+
return typeof candidate["outcome"] === "string" && typeof candidate["atMs"] === "number" && (candidate["retryAtMs"] === void 0 || typeof candidate["retryAtMs"] === "number") && (candidate["message"] === void 0 || typeof candidate["message"] === "string") && (candidate["source"] === void 0 || typeof candidate["source"] === "string");
|
|
2578
2723
|
}
|
|
2579
2724
|
/**
|
|
2580
2725
|
* One region's path for measured pool facts.
|
|
@@ -2621,6 +2766,7 @@ async function readPoolProbes(region) {
|
|
|
2621
2766
|
probes[accountId] = {
|
|
2622
2767
|
outcome: probe.outcome,
|
|
2623
2768
|
atMs: probe.atMs,
|
|
2769
|
+
...knownProbeSource(probe.source) === void 0 ? {} : { source: knownProbeSource(probe.source) },
|
|
2624
2770
|
...probe.retryAtMs === void 0 ? {} : { retryAtMs: probe.retryAtMs },
|
|
2625
2771
|
...probe.message === void 0 ? {} : { message: probe.message }
|
|
2626
2772
|
};
|
|
@@ -2628,6 +2774,17 @@ async function readPoolProbes(region) {
|
|
|
2628
2774
|
return probes;
|
|
2629
2775
|
}
|
|
2630
2776
|
/**
|
|
2777
|
+
* A stored source string reduced to a source this build understands.
|
|
2778
|
+
*
|
|
2779
|
+
* `isProbe` accepts any string so a newer build's label cannot destroy the
|
|
2780
|
+
* measurement it describes; this narrows it at the point of use. An unknown
|
|
2781
|
+
* value becomes `undefined`, which the card renders as "unknown" — the honest
|
|
2782
|
+
* answer, rather than mislabelling it as one of the two kinds we do know.
|
|
2783
|
+
*/
|
|
2784
|
+
function knownProbeSource(value) {
|
|
2785
|
+
return value === "test-batch" || value === "live-request" ? value : void 0;
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2631
2788
|
* Merge measurements into one region's store and write it atomically.
|
|
2632
2789
|
*
|
|
2633
2790
|
* A MERGE rather than a replace, because batches measure one account at a time
|
|
@@ -2936,10 +3093,11 @@ function toCredits(answer) {
|
|
|
2936
3093
|
}
|
|
2937
3094
|
/** Project a model into the card's row, dropping empty optional fields. */
|
|
2938
3095
|
function toWebModel(model, budgets) {
|
|
3096
|
+
const budgeted = applyContextBudgets([model], budgets)[0] ?? model;
|
|
2939
3097
|
return {
|
|
2940
3098
|
id: model.id,
|
|
2941
3099
|
name: model.name,
|
|
2942
|
-
contextWindow:
|
|
3100
|
+
contextWindow: budgeted.contextWindow,
|
|
2943
3101
|
nativeContextWindow: model.contextWindow,
|
|
2944
3102
|
maxTokens: model.maxTokens,
|
|
2945
3103
|
...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
|
|
@@ -3127,6 +3285,7 @@ async function workBuddyWebPool(deps, region) {
|
|
|
3127
3285
|
...measured?.probe === void 0 ? {} : { probe: {
|
|
3128
3286
|
outcome: measured.probe.outcome,
|
|
3129
3287
|
atMs: measured.probe.atMs,
|
|
3288
|
+
...measured.probe.source === void 0 ? {} : { source: measured.probe.source },
|
|
3130
3289
|
...measured.probe.retryAtMs === void 0 ? {} : { retryAtMs: measured.probe.retryAtMs },
|
|
3131
3290
|
...measured.probe.message === void 0 ? {} : { message: measured.probe.message }
|
|
3132
3291
|
} },
|
|
@@ -3143,6 +3302,7 @@ async function workBuddyWebPool(deps, region) {
|
|
|
3143
3302
|
...target.staleModelId === void 0 ? {} : { staleTargetModelId: target.staleModelId },
|
|
3144
3303
|
targetModelSource: target.source,
|
|
3145
3304
|
memberAccountIds: preferences.memberAccountIds,
|
|
3305
|
+
...preferences.probeInputTokens === void 0 ? {} : { probeInputTokens: preferences.probeInputTokens },
|
|
3146
3306
|
effectiveMemberAccountIds: effectiveMembersOf(accounts.map((account) => account.accountId), new Set(preferences.memberAccountIds)),
|
|
3147
3307
|
accounts,
|
|
3148
3308
|
catalog: deps.displayModels(region).map((model) => ({
|
|
@@ -3392,6 +3552,14 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
3392
3552
|
...slot
|
|
3393
3553
|
} : slot;
|
|
3394
3554
|
}
|
|
3555
|
+
if (field === "regions") for (const key of Object.keys(incoming)) {
|
|
3556
|
+
const slot = merged[key];
|
|
3557
|
+
if (!isPlainRecord(slot) || key !== "cn" && key !== "global") continue;
|
|
3558
|
+
if (Array.isArray(slot.lastCatalog) && slot.lastCatalog.length > 0) continue;
|
|
3559
|
+
const live = deps.displayModels(key);
|
|
3560
|
+
if (live.length === 0) continue;
|
|
3561
|
+
slot.lastCatalog = live;
|
|
3562
|
+
}
|
|
3395
3563
|
await settings.mutate(row.ns, [{
|
|
3396
3564
|
op: "set",
|
|
3397
3565
|
path: [field],
|
|
@@ -3522,6 +3690,16 @@ const modelConfig = z.object({
|
|
|
3522
3690
|
const poolConfig = z.object({
|
|
3523
3691
|
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."),
|
|
3524
3692
|
targetModelId: z.string().default("").description("Model id to test; empty means pick a zero-multiplier model from this region's catalog"),
|
|
3693
|
+
/**
|
|
3694
|
+
* Sizing of the test probe, as a token count the user picks from a fixed set.
|
|
3695
|
+
*
|
|
3696
|
+
* A closed set rather than a free number: each step is a meaningful position
|
|
3697
|
+
* relative to the measured 20k~30k threshold, and an arbitrary value would
|
|
3698
|
+
* invite a size that is neither safely under nor clearly over it. `0` (and any
|
|
3699
|
+
* unknown value) falls back to the measured default, so a profile written by
|
|
3700
|
+
* an older build — and one hand-edited to something odd — behaves identically.
|
|
3701
|
+
*/
|
|
3702
|
+
probeInputTokens: z.number().step(1).default(0).description("Input tokens each test probe sends: 10000 / 20000 / 30000 / 50000 / 100000. Bigger probes catch a rate limit real conversations would hit, but cost more credit per test and may mark an account unusable for large requests only. 0 uses the measured default (25000)"),
|
|
3525
3703
|
memberAccountIds: z.array(z.string()).default([]).description("Account ids checked into this pool (opt-in; empty means the pool covers nothing)")
|
|
3526
3704
|
});
|
|
3527
3705
|
const regionStateConfig = z.object({
|
|
@@ -3757,13 +3935,16 @@ function apply(ctx, config) {
|
|
|
3757
3935
|
...model,
|
|
3758
3936
|
...images.has(model.id) ? { multimodal: true } : { multimodal: false }
|
|
3759
3937
|
}));
|
|
3760
|
-
const configuredModels = (value, region) => {
|
|
3938
|
+
const configuredModels = (value, region, liveModels = []) => {
|
|
3761
3939
|
const state = regionStateOf(value, region);
|
|
3762
|
-
|
|
3940
|
+
const roster = state.lastCatalog?.length ? state.lastCatalog : liveModels.length ? liveModels : fallbackModelsFor(region);
|
|
3941
|
+
return withImageSelection(deriveCatalog(roster, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? []));
|
|
3763
3942
|
};
|
|
3764
3943
|
const displayModels = (value, region) => {
|
|
3765
3944
|
const state = regionStateOf(value, region);
|
|
3766
|
-
|
|
3945
|
+
if (state.lastCatalog?.length) return state.lastCatalog;
|
|
3946
|
+
const live = stacks[region]?.catalog?.current();
|
|
3947
|
+
return live?.length ? live : fallbackModelsFor(region);
|
|
3767
3948
|
};
|
|
3768
3949
|
let current = () => config;
|
|
3769
3950
|
let invalidateCatalog = () => {};
|
|
@@ -3786,7 +3967,7 @@ function apply(ctx, config) {
|
|
|
3786
3967
|
for (const region of REGION_KEYS) {
|
|
3787
3968
|
stacks[region].store.setDesktopPath(authFile);
|
|
3788
3969
|
stacks[region].store.selectAccount(effectiveAccountFor(region, value));
|
|
3789
|
-
stacks[region].catalog.set(configuredModels(value, region));
|
|
3970
|
+
stacks[region].catalog.set(configuredModels(value, region, stacks[region].catalog.current()));
|
|
3790
3971
|
}
|
|
3791
3972
|
invalidateCatalog();
|
|
3792
3973
|
syncRegionRegistration(value);
|
|
@@ -4138,6 +4319,7 @@ function apply(ctx, config) {
|
|
|
4138
4319
|
await writePoolProbes(region, { [accountId]: {
|
|
4139
4320
|
outcome,
|
|
4140
4321
|
atMs: Date.now(),
|
|
4322
|
+
source: "live-request",
|
|
4141
4323
|
...retryAtMs === void 0 ? {} : { retryAtMs },
|
|
4142
4324
|
...message === "" ? {} : { message }
|
|
4143
4325
|
} });
|
|
@@ -4228,9 +4410,11 @@ function apply(ctx, config) {
|
|
|
4228
4410
|
/** One region's pool preferences, with the schema's defaults applied. */
|
|
4229
4411
|
const poolPreferencesOf = (config, region) => {
|
|
4230
4412
|
const pool = regionStateOf(config, region).pool;
|
|
4413
|
+
const probeInputTokens = pool?.probeInputTokens;
|
|
4231
4414
|
return {
|
|
4232
4415
|
enabled: pool?.enabled === true,
|
|
4233
4416
|
targetModelId: pool?.targetModelId ?? "",
|
|
4417
|
+
...probeInputTokens === void 0 ? {} : { probeInputTokens },
|
|
4234
4418
|
memberAccountIds: pool?.memberAccountIds ?? []
|
|
4235
4419
|
};
|
|
4236
4420
|
};
|
|
@@ -4276,11 +4460,13 @@ function apply(ctx, config) {
|
|
|
4276
4460
|
claimDailyCheckin: (credential) => client.claimDailyCheckin(credential),
|
|
4277
4461
|
probe: async (credential, modelId) => {
|
|
4278
4462
|
const quotaRefreshAtMs = await quotaRefreshOf(region, credential);
|
|
4463
|
+
const inputTokens = resolveProbeInputTokens(poolPreferencesOf(current(), region).probeInputTokens);
|
|
4279
4464
|
return probeModel({
|
|
4280
4465
|
client,
|
|
4281
4466
|
credential,
|
|
4282
4467
|
modelId,
|
|
4283
4468
|
nowMs: Date.now(),
|
|
4469
|
+
inputTokens,
|
|
4284
4470
|
...quotaRefreshAtMs === void 0 ? {} : { quotaRefreshAtMs }
|
|
4285
4471
|
});
|
|
4286
4472
|
}
|
|
@@ -4402,4 +4588,4 @@ function apply(ctx, config) {
|
|
|
4402
4588
|
});
|
|
4403
4589
|
}
|
|
4404
4590
|
//#endregion
|
|
4405
|
-
export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID,
|
|
4591
|
+
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
|
|
5
|
-
"version": "3.4.
|
|
5
|
+
"version": "3.4.5",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|