dsh-connect-workbuddy 4.4.4 → 4.4.6
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 +61 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +216 -18
- package/lib/{host-heartbeat-BwbDWvzw.js → host-heartbeat-B2jrUDhg.js} +31 -6
- package/lib/index.d.ts +184 -33
- package/lib/index.js +379 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.4.6 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **面板的「测试」只测一个模型,却把结果当成账号级结论:账号池说「可用」,一切到实际在用的模型就撞 `429 / 6004`。**(issue #61 报告者报告,维护者在 issue 下确认「测试的是某一个模型……不能换其他模型」)
|
|
8
|
+
- **现象与证据**:报告者同一时刻、同一脚本测两个国内账号 —— `hy4-preview-f`(当时的 `pool.targetModelId`,面板测的就是它)两个号都可用,而 `step-5-preview`(当时实际在用的模型)两个号都 `429 / 6004`。池文件里这两个号还被更早的一次批量测试记成 `outcome=ok source=test-batch`,而池开着时那份「ok」正是选号依据。**`6004` 是「账号 × 模型」维度的条件**(本项目 10-13 也在三个国际账号上量到 `deepseek-v4.1-flash` 挂、其余模型全通),所以单一模型探测在结构上无法回答「这个账号能不能服务」。
|
|
9
|
+
- **根因(报告者给出代码位置,本地逐条复核属实)**:`resolveTargetModel()` 只解析出**一个** `modelId`(来自区域级 `pool.targetModelId`),一条路径同时服务批量与逐行测试;而 `probeModel()` 的三个返回分支**早就带着 `modelId`**,落盘时被丢掉了 —— 记录里只有 `outcome/atMs/source/retryAtMs/message`,没有任何字段说明这条 `ok` 是**哪个模型**上的。
|
|
10
|
+
- **修法(三处,各自成立)**:
|
|
11
|
+
1. **测量记录记住模型**:`WorkBuddyPoolProbe` 新增可选 `modelId`,两个写入方(批量测试、实时聊天失败)在拿到模型的那一刻写下它。
|
|
12
|
+
2. **每个账号可单独指定测试模型**:新增设置 `pool.targetModelIdByAccount`(账号 id → 模型 id),读取时先查 map、查不到回落 `targetModelId`。**批量测试与逐行测试走同一条解析**(报告者指出这正是「改一处两处同时生效」的地方),所以「测试所有账号」也会按每个号自己的模型去测。解析仍然过 `resolveTargetModel`:指向本区域已下架模型时**报 `stale`、绝不静默换成另一个模型**(换一个等于花积分测一个用户没选的模型,还把它当作这个号的结论)。
|
|
13
|
+
3. **卡片显示适用范围**:池表每行的测量结论旁写上「模型 {model}」(记录里没有该字段的旧数据写「未记录模型」,不猜);每行新增一个测试模型下拉框(留空 = 跟随区域目标),随「保存」一起写入。
|
|
14
|
+
- **为什么 `modelId` 落盘时不继承**:`accountName` 与 `retryAtMs` 描述的是**账号**(后来者看不到的名字仍然是真的,未来的刷新时刻仍然在未来),而模型描述的是**这一次测量**。把旧的 model id 继承到一次新测量上,等于用不是它的模型给它贴标签 —— 正是这个字段要终结的那种混淆。所以**缺失即「范围未知」,不是猜测**。
|
|
15
|
+
- **配置向后兼容**:`targetModelIdByAccount` 默认空对象,且**只有非空时才写入** settings(与 `probeInputTokens` 同一条规则)—— 从未用过这个设置的 profile 写出来的槽位与升级前逐字节相同,升级不会看起来像用户改过设置。
|
|
16
|
+
- **守卫**:`tests/issue61-probe-model-scope.spec.ts`(9 例)覆盖:逐账号优先/回落、空串视为「跟随区域目标」、override 下架时解析为 `stale` 且不带 `modelId`、批量按各账号自己的模型探测、单一模型的旧调用形态不变、解析不出模型时产出 `unavailable` 行并指名那个模型(**没有**回退到别的模型)、批量写入 `modelId`、**不继承**、新测量覆盖旧模型。`tests/pool-route.spec.ts` 新增一条路由级断言:批量路由交给 Host 的是**逐账号解析器**(`a` 用区域目标、`b` 用 `b` 自己的)。**变异验证**:记录不再写 `modelId` → 1 例红;合并改成继承 → 1 例红;批量改回「整批一个模型」→ 1 例红;路由改回传单一 modelId → 路由断言红。
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
- **新增「上游请求体构成」诊断(`WORKBUDDY_BODY_REPORT`),用于定案 issue #59:两份请求体到底是不是同一份历史。**(issue #59)
|
|
21
|
+
- **为什么需要**:报告里同一份历史被国内网关算约 70.8 万 token、国际网关算约 137.4 万(1.94 倍),切区域即 `code 11115 prompt is too long`。两条独立测量已经把范围压到唯一一个问题:报告者在**两个区域**跑本仓库的内容类别探针,逐字节相同的散文/代码/JSON 六格比值全在 **0.98~1.06**(同一段字节两边计价一致);代码链路核对又表明**历史 reasoning 根本没进请求体**(pi-ai 只在 `compat.requiresThinkingAsText` 为真时把 `thinking` 当文本转发,本插件从未设置;`reasoning_details` 那条路需要流式解析从不写出的签名)。**剩下唯一可能是两份请求体不同**,而这件事**不需要任何区域有积分**就能查:请求体是本地拼好的,`14018 Credits exhausted` 是发出去之后才可能发生的。
|
|
22
|
+
- **做法**:`src/body-report.ts` 在 shim 把 body 交给上游**之前**记录它的**构成** —— 每条消息的 `role`、逐块的类型(`system`/`text`/`tool-call`/`tool-result`/`reasoning`/`image`)、字符数与其中**中日韩字符**数(实测中文 0.669、英文 0.224 token/字符,差 3 倍,混在一起看不出来)、`tools` 数组、`messages.length` 与 body 总字节数。按区域写 `<dir>/<region>.jsonl`,一次请求一行(一轮对话有很多次上游调用,**最后一次**才是要比的那份)。
|
|
23
|
+
- **只记计数、不记文本**:不写任何消息内容、账号名或令牌 —— 一个要被贴进公开 issue 的诊断不能有泄漏用户会话的能力。`tests/issue59-body-report.spec.ts` 里有一条专门的断言:用带特征串的 body 生成报告后,序列化结果**不含**该特征串(变异验证:让 `block()` 把 `text` 一起带上 → 该例与 reasoning 那例同时变红)。
|
|
24
|
+
- **开关**:环境变量 `WORKBUDDY_BODY_REPORT` 未设置或为空白即**关闭**;`1`/`true` 写到 `<DSH home>/body-reports/`;其他值按**目录路径**解释。写文件失败只走日志,**绝不影响聊天请求**(有专门断言)。
|
|
25
|
+
- **守卫**:`tests/issue59-body-report.spec.ts`(16 例)。
|
|
26
|
+
- **配套**:`scripts/probe-region-token-accounting.mjs` 新增 `--language en|zh` 与 `--kind prose|code|json`(六种组合确定性填充、跨区域逐字节相同),并补上国内侧实测基线:prose 214.0 / code 287.0 / json 288.3(en),prose 671.0 / code 480.7 / json 397.3(zh),单位 tokens/1k chars。
|
|
27
|
+
|
|
28
|
+
## 4.4.5 (2026-10-14)
|
|
29
|
+
|
|
30
|
+
### Fixes
|
|
31
|
+
|
|
32
|
+
- **积分余额读错了「家族」:月度套餐被按容量池(上限值)计入,国内、国际都偏高一个完整月度额度;同时漏读了小数。这条**推翻** 4.3.3 对 issue #48 的判断。**(issue #58 报告者报告,维护者用官方客户端截图定案)
|
|
33
|
+
- **判据(决定性)**:维护者自己账号的官方客户端截图 —— `总剩余积分 2,196.61 = 套餐基础积分 500 + 平台奖励积分 1,696.61`,而**同一份账单响应**里 `Σ CycleCapacityRemainPrecise = 2,196.61`(分毫不差),`Σ CapacityRemain = 2,196`。**那个「.61」就是证据**:整数 `CapacityRemain` 无论怎么加都加不出小数位,因为小数只存在于 `*Precise` 字段里,而它属于**周期**家族。客户端自己的三个桶(套餐基础 / 平台奖励 / 购买)与(月度周期、gift 周期、购买)一一对应。
|
|
34
|
+
- **为什么池值不是余额**:一个周期已用尽的月度套餐仍返回 `CapacityRemain=500, CapacityUsed=0`(「一点没用、全都还在」),而周期说 `CycleCapacityUsed=500`。本机 **8/8** 个国内账号上,池值与周期值只在**周期未被使用**时相等;周期一旦开始消耗,按池读数就**整整高出一个月度额度**(LaoDing 500、老丁 500、两次实测各差 56 / 115)。
|
|
35
|
+
- **国际侧的独立佐证**:三个国际账号返回 `CapacityRemain=100`,而国际网关**用 `14018 Credits exhausted` 拒绝它们的对话请求** —— 池值在那里不是可花的额度。也就是说 #48 当初认定的「误报 0」其实是真值,报告者看到的 200 来自 gift 包(或宽限路径)。
|
|
36
|
+
- **修法**:
|
|
37
|
+
- 余额一律读**周期**家族(`CycleCapacityRemain`),**两种套餐统一** —— gift 的两套字段实测完全相等,所以统一读周期等于**删掉了那个出错的月度分支**:不再有「哪种包读哪个字段」这种可以搞错的特殊情况。
|
|
38
|
+
- **读 `*Precise`**(字符串,带小数)优先,整数拼写回退。这是 2,196.61 里那个 `.61` 的来源,此前完全没读。
|
|
39
|
+
- `size` 同样取周期家族(`CycleCapacitySize` → `CapacitySize` 回退),这样「已剩 0 / 共 100」读得通。
|
|
40
|
+
- 完全没有周期家族的行(例如只带 `CapacitySize`/`CapacityRemain` 的合成行)回退读池,避免静默报 0。
|
|
41
|
+
- **删除 `cycleRemain` 字段**:余额本身就是周期值了,它成了同义重复;`refreshAtMs`(周期续期点)才是「本周期已用完、什么时候恢复」的表达。gift 的丢弃规则(耗尽/过期)、`expiringSoon`、`nearestExpiryMs` 均未改变。
|
|
42
|
+
- **显示只取整数部分(向下取整)**:余额现在带小数(`2,196.61`),卡片不显示小数位。取**向下取整**而非四舍五入:四舍五入会把 `2,196.61` 印成 `2,197`,而官方客户端显示 `2,196.61` —— 一张刚跟客户端核对过的卡片反而看起来差 1;而且 `0.61` 向上取整会印成 `1`,等于告诉读者他有一个并不存在的积分可花。三处显示(编辑器读数、账号池积分列、模型容量)统一走新增的 `src/client/credit-format.ts`,守卫 `tests/credit-format.spec.ts`(5 例)。
|
|
43
|
+
- **实测核对**(构建后的插件,真实账号):`177****417… total=2196.61` —— 与客户端截图**逐位一致**;`LaoDing 647.01`、`老丁 1360.13`(旧读数为 1147 / 1860)。国际三个账号 `0.00`,与 `14018` 一致。
|
|
44
|
+
- **守卫**:`tests/issue58-credit-balance-cycle.spec.ts`(9 例,替换掉 `tests/issue48-monthly-pool-credits.spec.ts` —— 后者的断言钉的正是被推翻的规则)。覆盖:total == Σ 周期精确值(含小数)、周期耗尽时读周期而非池、国际耗尽行为 0、周期未用时两种读法一致(说明不是「所有数字都变了」)、无周期家族回退、精确/整数两种拼写、gift 丢弃规则不变、负值不进 total、`expiringSoon` 与 total 同源。**变异验证**:把余额改回读池 → 3 例变红;把 `*Precise` 读法去掉 → 4 例变红。
|
|
45
|
+
- **另一处新增工具**:`scripts/probe-credits-raw.mjs` —— 逐包原样打印账单响应(含未被读取的字段)并给出三个候选口径的合计,让「客户端显示的到底是哪个字段」这件事可以被独立复现,而不必相信本文件的叙述。
|
|
46
|
+
|
|
47
|
+
- **账号池的「已不再登录」警告只报数量、不报是谁,而且没有任何一行可以处理。**(issue #60 报告者)
|
|
48
|
+
- **现象**:账号池下方出现 `有 1 个已保存的账号在本机已不再登录……再保存一次即可移除它们。` —— 用户既不知道是哪个账号,界面里也找不到对应的行,更**没有办法执行「再保存一次」**:这句话描述的移除动作,在当时的数据结构下无处可做。
|
|
49
|
+
- **三条独立成因**(报告者逐条给出代码位置,本地逐条复核属实):
|
|
50
|
+
1. `ghostMemberIds()` 只返回**裸 id**,把当时就在手边的名称丢掉了;
|
|
51
|
+
2. 文案模板只有 `{count}`,而同文件的 `row.poolSelectAria` / `row.poolCurrentHeader` 都带 `{accountName}` —— 不是没东西可显示,是这条文案没设计成能显示;
|
|
52
|
+
3. 幽灵账号**没有行**:账号表格渲染的是**本机已登录**的名册,而幽灵恰恰是「不在名册里」的那一个,所以「哪一行该打标记」在旧结构下无解。
|
|
53
|
+
- **修法**(两条,各自成立):
|
|
54
|
+
- **让测量记录记住名称**:`WorkBuddyPoolProbe` 新增可选 `accountName`,由**两个写入方**在拿到账号的那一刻写下(测试批次一直持有该名称;实时聊天那条在取不到名称时**继承**旧值)。选择存在测量文件而非设置里的原因与 4.4.4 同源:**名称只在被测量的那一刻确定可知**,登录消失后别处都查不到;而设置槽会被「草稿 → 保存」覆盖,实测数据不该住在里面。`mergePoolProbe()` 对缺失名称**继承**、对新名称**覆盖**(与 `retryAtMs` 同一口径:**缺失不等于声明**)。
|
|
55
|
+
- **给幽灵一个可见且可处理的位置**:警告句改为指名(`{count}` + `{names}`),下面渲染**每个幽灵一个只读徽标 + ✕ 移除按钮**。移除是**草稿编辑**(与取消勾选一个已列出账号完全同构),仍然由「保存」提交 —— 旧文案承诺的那个动作,现在真的做得到了。
|
|
56
|
+
- **取不到名称时不编造**:徽标退回 **id 前缀**(`e44e62b3…`)。前缀不是好标签,但它是**诚实的**,且足以把两个幽灵分开;换成「第 1 个」这类更好看的位置标签,会把用户指向错误的账号。宿主**只**为名册里没有的 id 下发名称,避免同一件事有两个可能互相矛盾的来源。
|
|
57
|
+
- **守卫**:新增 `tests/issue60-ghost-member-labels.spec.ts`(14 例)—— 命名、空名退回前缀、顺序稳定、三名截断、`mergePoolProbe` 的继承/覆盖/两字段同时继承、`probeUpdatesOf` 写名与空名不写、磁盘往返。**变异验证**:把名称继承改回整体替换 → 2 例变红。
|
|
58
|
+
|
|
59
|
+
### Docs
|
|
60
|
+
|
|
61
|
+
- 新增 `scripts/probe-region-token-accounting.mjs`:同一段英文文本在国内外两个网关下各发一次,比较上游自报的 `prompt_tokens`,并判定该网关的**缓存计账口径**(`prompt_tokens` 是否包含命中缓存的部分)。用于 issue #59 的根因定位;只读、按账号逐个报告失败原因。
|
|
62
|
+
- 新增 `scripts/probe-credits-raw.mjs`:逐包原样打印账单响应并列出三个候选口径的合计,用于核对「官方客户端显示的余额到底是哪个字段」(issue #58);只读,账号名做遮蔽。
|
|
63
|
+
|
|
3
64
|
## 4.4.4 (2026-10-13)
|
|
4
65
|
|
|
5
66
|
### Fixes
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient,
|
|
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-B2jrUDhg.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
|
@@ -661,6 +661,8 @@ window.__ModuleLoader__.load({
|
|
|
661
661
|
targetModelId: preferences.targetModelId,
|
|
662
662
|
memberAccountIds: preferences.memberAccountIds
|
|
663
663
|
};
|
|
664
|
+
const overrides = Object.entries(preferences.targetModelIdByAccount ?? {}).filter(([, modelId]) => modelId !== "");
|
|
665
|
+
if (overrides.length > 0) slot["targetModelIdByAccount"] = Object.fromEntries(overrides);
|
|
664
666
|
if (preferences.probeInputTokens !== void 0) slot["probeInputTokens"] = preferences.probeInputTokens;
|
|
665
667
|
const next = nextRegionPool(scope.getSnapshot().value, region, slot)[region];
|
|
666
668
|
await writeField(scope, "regions", region, next, (readBack) => {
|
|
@@ -672,7 +674,12 @@ window.__ModuleLoader__.load({
|
|
|
672
674
|
const stored = pool["memberAccountIds"];
|
|
673
675
|
if (!Array.isArray(stored) || stored.length !== preferences.memberAccountIds.length) return false;
|
|
674
676
|
const wanted = new Set(preferences.memberAccountIds);
|
|
675
|
-
|
|
677
|
+
if (!stored.every((id) => typeof id === "string" && wanted.has(id))) return false;
|
|
678
|
+
const storedModels = pool["targetModelIdByAccount"];
|
|
679
|
+
if (overrides.length === 0) return storedModels === void 0 || typeof storedModels === "object" && storedModels !== null && !Array.isArray(storedModels) && Object.keys(storedModels).length === 0;
|
|
680
|
+
if (typeof storedModels !== "object" || storedModels === null || Array.isArray(storedModels)) return false;
|
|
681
|
+
const storedMap = storedModels;
|
|
682
|
+
return Object.keys(storedMap).length === overrides.length && overrides.every(([accountId, modelId]) => storedMap[accountId] === modelId);
|
|
676
683
|
});
|
|
677
684
|
}
|
|
678
685
|
//#endregion
|
|
@@ -692,6 +699,42 @@ window.__ModuleLoader__.load({
|
|
|
692
699
|
}
|
|
693
700
|
}
|
|
694
701
|
//#endregion
|
|
702
|
+
//#region src/client/credit-format.ts
|
|
703
|
+
/**
|
|
704
|
+
* How a credit balance is written on screen.
|
|
705
|
+
*
|
|
706
|
+
* `4.4.5` made the balance exact: it now comes from the upstream's `*Precise`
|
|
707
|
+
* family, so the number carries a fraction (`2,196.61`) where it used to be a
|
|
708
|
+
* whole integer. The card does not show that fraction — a narrow row reads
|
|
709
|
+
* better, and the product's own breakdown (套餐基础 / 平台奖励 / 购买) is the
|
|
710
|
+
* place to look for detail.
|
|
711
|
+
*
|
|
712
|
+
* Taking the FLOOR rather than rounding is the one decision here, and it is not
|
|
713
|
+
* cosmetic:
|
|
714
|
+
*
|
|
715
|
+
* - a balance is a quantity you either have or do not. `0.61` rounded UP prints
|
|
716
|
+
* `1`, which tells the reader they can spend a credit they do not have;
|
|
717
|
+
* - the official client shows `2,196.61`, so its integer part is `2,196` — the
|
|
718
|
+
* floor. Rounding would print `2,197` and make a card that had JUST been
|
|
719
|
+
* reconciled against the client look one credit off, which is the exact
|
|
720
|
+
* complaint (issue #58) this display rule is downstream of.
|
|
721
|
+
*
|
|
722
|
+
* Grouping is kept because a four-figure balance is otherwise hard to read at a
|
|
723
|
+
* glance, and it is the only reason this is not `Math.floor(value).toString()`.
|
|
724
|
+
*
|
|
725
|
+
* @module dsh-connect-workbuddy/client/credit-format
|
|
726
|
+
*/
|
|
727
|
+
/**
|
|
728
|
+
* Format a credit balance for display: whole credits, grouped, floored.
|
|
729
|
+
*
|
|
730
|
+
* `Math.floor` before `toLocaleString` (rather than a `maximumFractionDigits`
|
|
731
|
+
* option) is deliberate: the option ROUNDS, so it would print `2,197` for
|
|
732
|
+
* `2,196.61`.
|
|
733
|
+
*/
|
|
734
|
+
function formatCredits(value) {
|
|
735
|
+
return Math.floor(value).toLocaleString("en-US");
|
|
736
|
+
}
|
|
737
|
+
//#endregion
|
|
695
738
|
//#region src/client/BusyOverlay.tsx
|
|
696
739
|
/**
|
|
697
740
|
* The shared busy veil's spinner: an arc that breathes while it turns.
|
|
@@ -1054,6 +1097,42 @@ window.__ModuleLoader__.load({
|
|
|
1054
1097
|
function ghostMemberIds(draftMembers, listedIds) {
|
|
1055
1098
|
return draftMembers.filter((id) => !listedIds.has(id));
|
|
1056
1099
|
}
|
|
1100
|
+
/** How many characters of an id are shown when no name was ever measured. */
|
|
1101
|
+
const GHOST_ID_PREFIX = 8;
|
|
1102
|
+
/**
|
|
1103
|
+
* Label saved member ids that resolve to no local sign-in (issue #60).
|
|
1104
|
+
*
|
|
1105
|
+
* The old warning was a bare count, which named no account and had no
|
|
1106
|
+
* corresponding row — the user could neither identify the member nor act on it,
|
|
1107
|
+
* even though the collected name was in hand at write time. This turns the same
|
|
1108
|
+
* set into something renderable and specific.
|
|
1109
|
+
*
|
|
1110
|
+
* `names` is the Host's last-measured map; a missing entry means no measurement
|
|
1111
|
+
* ever recorded a name, not that the account has none. Ids keep the order the
|
|
1112
|
+
* saved list has, so the rendered list does not reshuffle between polls.
|
|
1113
|
+
*/
|
|
1114
|
+
function ghostMembers(draftMembers, listedIds, names = {}) {
|
|
1115
|
+
return ghostMemberIds(draftMembers, listedIds).map((id) => {
|
|
1116
|
+
const measured = names[id];
|
|
1117
|
+
const named = typeof measured === "string" && measured !== "";
|
|
1118
|
+
return {
|
|
1119
|
+
id,
|
|
1120
|
+
label: named ? measured : `${id.slice(0, GHOST_ID_PREFIX)}…`,
|
|
1121
|
+
named
|
|
1122
|
+
};
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* The names to show in one sentence, capped so the warning stays readable.
|
|
1127
|
+
*
|
|
1128
|
+
* Beyond three the sentence would wrap into a paragraph, so it ends with an
|
|
1129
|
+
* ellipsis — the removable list rendered underneath carries the full set, which
|
|
1130
|
+
* is why truncating here cannot hide a member.
|
|
1131
|
+
*/
|
|
1132
|
+
function ghostNameList(ghosts, limit = 3) {
|
|
1133
|
+
const shown = ghosts.slice(0, limit).map((ghost) => ghost.label).join("、");
|
|
1134
|
+
return ghosts.length <= limit ? shown : `${shown}…`;
|
|
1135
|
+
}
|
|
1057
1136
|
/**
|
|
1058
1137
|
* How many accounts a batch will really touch, as the log announces it.
|
|
1059
1138
|
*
|
|
@@ -1187,6 +1266,21 @@ window.__ModuleLoader__.load({
|
|
|
1187
1266
|
const wanted = new Set(right);
|
|
1188
1267
|
return left.every((id) => wanted.has(id));
|
|
1189
1268
|
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Whether two per-account test-model maps are equal (issue #61).
|
|
1271
|
+
*
|
|
1272
|
+
* By CONTENT, not by size: moving an account from one model to another keeps the
|
|
1273
|
+
* count and changes the meaning, and clearing an entry is a change a size
|
|
1274
|
+
* comparison would miss entirely when another account was added at the same
|
|
1275
|
+
* time. Order is irrelevant for the same reason it is in {@link sameIds} — the
|
|
1276
|
+
* map is written by key, not by click sequence.
|
|
1277
|
+
*/
|
|
1278
|
+
function sameModelOverrides(left, right) {
|
|
1279
|
+
const leftKeys = Object.keys(left);
|
|
1280
|
+
const rightKeys = Object.keys(right);
|
|
1281
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
1282
|
+
return leftKeys.every((key) => left[key] === right[key]);
|
|
1283
|
+
}
|
|
1190
1284
|
/** Whether a probe outcome means the model answered. */
|
|
1191
1285
|
function outcomeOk(outcome) {
|
|
1192
1286
|
return outcome === "ok";
|
|
@@ -1205,7 +1299,7 @@ window.__ModuleLoader__.load({
|
|
|
1205
1299
|
}
|
|
1206
1300
|
}
|
|
1207
1301
|
function formatNumber$1(value) {
|
|
1208
|
-
return
|
|
1302
|
+
return formatCredits(value);
|
|
1209
1303
|
}
|
|
1210
1304
|
/**
|
|
1211
1305
|
* `MM/DD, HH:mm` on a 24-hour clock.
|
|
@@ -1273,7 +1367,8 @@ window.__ModuleLoader__.load({
|
|
|
1273
1367
|
enabled: pool?.enabled ?? false,
|
|
1274
1368
|
targetModelId: pool?.targetModelId || pool?.staleTargetModelId || "",
|
|
1275
1369
|
probeInputTokens: pool?.probeInputTokens ?? 0,
|
|
1276
|
-
memberAccountIds: pool?.memberAccountIds ?? []
|
|
1370
|
+
memberAccountIds: pool?.memberAccountIds ?? [],
|
|
1371
|
+
targetModelIdByAccount: { ...pool?.targetModelIdByAccount }
|
|
1277
1372
|
};
|
|
1278
1373
|
const active = draftRegion === region && draft !== null ? draft : saved;
|
|
1279
1374
|
/**
|
|
@@ -1294,7 +1389,7 @@ window.__ModuleLoader__.load({
|
|
|
1294
1389
|
* the pending edit.
|
|
1295
1390
|
*/
|
|
1296
1391
|
const poolRunning = saved.enabled;
|
|
1297
|
-
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1392
|
+
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameModelOverrides(draft.targetModelIdByAccount, saved.targetModelIdByAccount) || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1298
1393
|
/**
|
|
1299
1394
|
* Membership that actually resolves to a local sign-in — what a batch runs on.
|
|
1300
1395
|
*
|
|
@@ -1314,8 +1409,15 @@ window.__ModuleLoader__.load({
|
|
|
1314
1409
|
listedIds,
|
|
1315
1410
|
dirty
|
|
1316
1411
|
});
|
|
1317
|
-
/**
|
|
1318
|
-
|
|
1412
|
+
/**
|
|
1413
|
+
* Saved ids with no matching sign-in; surfaced rather than silently ignored.
|
|
1414
|
+
*
|
|
1415
|
+
* Named from the Host's last-measured map (issue #60): the id alone told the
|
|
1416
|
+
* user nothing, because nothing on screen shows ids. `pool.memberNames` can
|
|
1417
|
+
* only be absent for an older Host or an account never measured, and the
|
|
1418
|
+
* helper then falls back to an id prefix instead of a blank.
|
|
1419
|
+
*/
|
|
1420
|
+
const ghostMembers$1 = ghostMembers(active.memberAccountIds, listedIds, pool?.memberNames ?? {});
|
|
1319
1421
|
/**
|
|
1320
1422
|
* Pool MEMBERS that can serve right now.
|
|
1321
1423
|
*
|
|
@@ -1420,6 +1522,22 @@ window.__ModuleLoader__.load({
|
|
|
1420
1522
|
settingsScope,
|
|
1421
1523
|
t
|
|
1422
1524
|
]);
|
|
1525
|
+
/**
|
|
1526
|
+
* Drop one ghost member from the draft.
|
|
1527
|
+
*
|
|
1528
|
+
* A DRAFT edit, not a write: membership is one of the preferences the Save
|
|
1529
|
+
* button commits, and a control that wrote straight through would leave the
|
|
1530
|
+
* panel claiming "unsaved changes" for an edit it had already persisted. The
|
|
1531
|
+
* ghost rows therefore behave exactly like unchecking a listed account, which
|
|
1532
|
+
* is the behaviour the old copy ("save again to drop them") described but
|
|
1533
|
+
* gave the user no way to perform — the ids had no rows to uncheck.
|
|
1534
|
+
*/
|
|
1535
|
+
const removeGhost = (0, react.useCallback)((accountId) => {
|
|
1536
|
+
editDraft((current) => ({
|
|
1537
|
+
...current,
|
|
1538
|
+
memberAccountIds: current.memberAccountIds.filter((id) => id !== accountId)
|
|
1539
|
+
}));
|
|
1540
|
+
}, [editDraft]);
|
|
1423
1541
|
/** Check or uncheck one account in this pool. */
|
|
1424
1542
|
const toggleMember = (0, react.useCallback)((accountId, next) => {
|
|
1425
1543
|
editDraft((current) => {
|
|
@@ -1432,6 +1550,25 @@ window.__ModuleLoader__.load({
|
|
|
1432
1550
|
};
|
|
1433
1551
|
});
|
|
1434
1552
|
}, [editDraft]);
|
|
1553
|
+
/**
|
|
1554
|
+
* Set ONE account's test model (issue #61).
|
|
1555
|
+
*
|
|
1556
|
+
* `''` REMOVES the entry rather than storing an empty string: "follow the
|
|
1557
|
+
* region target" is the absence of a choice, which is the same shape the host
|
|
1558
|
+
* reads and the same shape an untouched profile has — so nothing about an
|
|
1559
|
+
* older profile changes when this setting is never used.
|
|
1560
|
+
*/
|
|
1561
|
+
const setRowModel = (0, react.useCallback)((accountId, modelId) => {
|
|
1562
|
+
editDraft((current) => {
|
|
1563
|
+
const next = { ...current.targetModelIdByAccount };
|
|
1564
|
+
if (modelId === "") delete next[accountId];
|
|
1565
|
+
else next[accountId] = modelId;
|
|
1566
|
+
return {
|
|
1567
|
+
...current,
|
|
1568
|
+
targetModelIdByAccount: next
|
|
1569
|
+
};
|
|
1570
|
+
});
|
|
1571
|
+
}, [editDraft]);
|
|
1435
1572
|
/** Check every listed account, or none of them. */
|
|
1436
1573
|
const selectAll = (0, react.useCallback)((next) => {
|
|
1437
1574
|
editDraft((current) => ({
|
|
@@ -1752,14 +1889,32 @@ window.__ModuleLoader__.load({
|
|
|
1752
1889
|
},
|
|
1753
1890
|
onRemove: () => {
|
|
1754
1891
|
runRowRemove(account.accountId, account.accountName);
|
|
1755
|
-
}
|
|
1892
|
+
},
|
|
1893
|
+
modelOverride: active.targetModelIdByAccount[account.accountId] ?? "",
|
|
1894
|
+
catalog: pool.catalog ?? [],
|
|
1895
|
+
onModelChange: (modelId) => setRowModel(account.accountId, modelId)
|
|
1756
1896
|
}))), effectiveMembers.length === 0 ? (0, react.createElement)("p", {
|
|
1757
1897
|
className: "dsm-workbuddy-pool-warn",
|
|
1758
1898
|
role: "status"
|
|
1759
|
-
}, `${t("row.poolNoneSelected")} ${t("row.poolNoneSelectedHint")}`) : null, ghostMembers.length === 0 ? null : (0, react.createElement)("p", {
|
|
1899
|
+
}, `${t("row.poolNoneSelected")} ${t("row.poolNoneSelectedHint")}`) : null, ghostMembers$1.length === 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-ghosts" }, (0, react.createElement)("p", {
|
|
1760
1900
|
className: "dsm-workbuddy-pool-warn",
|
|
1761
1901
|
role: "status"
|
|
1762
|
-
}, t("row.poolGhostMembers", {
|
|
1902
|
+
}, t("row.poolGhostMembers", {
|
|
1903
|
+
count: ghostMembers$1.length,
|
|
1904
|
+
names: ghostNameList(ghostMembers$1)
|
|
1905
|
+
})), (0, react.createElement)("ul", { className: "dsm-workbuddy-pool-ghost-list" }, ghostMembers$1.map((ghost) => (0, react.createElement)("li", {
|
|
1906
|
+
key: ghost.id,
|
|
1907
|
+
className: "dsm-workbuddy-pool-ghost"
|
|
1908
|
+
}, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-ghost-name" }, ghost.label), (0, react.createElement)("button", {
|
|
1909
|
+
type: "button",
|
|
1910
|
+
className: "dsm-workbuddy-pool-ghost-remove",
|
|
1911
|
+
title: t(ghost.named ? "row.poolGhostRemoveNamed" : "row.poolGhostRemoveUnnamed", { account: ghost.label }),
|
|
1912
|
+
"aria-label": t("row.poolGhostRemove", { account: ghost.label }),
|
|
1913
|
+
disabled: saving,
|
|
1914
|
+
onClick: () => {
|
|
1915
|
+
removeGhost(ghost.id);
|
|
1916
|
+
}
|
|
1917
|
+
}, "✕"))))), dirty && !sameIds(active.memberAccountIds, saved.memberAccountIds) ? (0, react.createElement)("p", { className: "dsm-workbuddy-pool-dirty" }, t("row.poolUnsavedMembers")) : null), pool.targetModelSource === "none" ? (0, react.createElement)("p", {
|
|
1763
1918
|
className: "dsm-workbuddy-pool-warn",
|
|
1764
1919
|
role: "status"
|
|
1765
1920
|
}, `${t("row.poolTargetNone")} ${t("row.poolTargetNoneHint")}`) : null, pool.targetModelSource === "stale" ? (0, react.createElement)("div", { className: "dsm-workbuddy-pool-conflict" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-conflict-main" }, (0, react.createElement)("b", null, t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" })), (0, react.createElement)("span", null, t("row.poolTargetStaleHint"))), (0, react.createElement)("button", {
|
|
@@ -1805,6 +1960,7 @@ window.__ModuleLoader__.load({
|
|
|
1805
1960
|
/** One account's row. */
|
|
1806
1961
|
function renderAccountRow(input) {
|
|
1807
1962
|
const { t, account, checked, canEdit, checkinSupported, onToggle, busy, testDisabled, targetHint, onTest, onRemove } = input;
|
|
1963
|
+
const { modelOverride, catalog, onModelChange } = input;
|
|
1808
1964
|
const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
|
|
1809
1965
|
const excluded = exclusionText(t, account.excludedBy);
|
|
1810
1966
|
const probe = account.probe;
|
|
@@ -1842,10 +1998,23 @@ window.__ModuleLoader__.load({
|
|
|
1842
1998
|
}), (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", {
|
|
1843
1999
|
className: "dsm-workbuddy-pool-probe",
|
|
1844
2000
|
...probe?.message === void 0 ? {} : { title: probe.message }
|
|
1845
|
-
}, (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", {
|
|
2001
|
+
}, (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", {
|
|
1846
2002
|
className: "dsm-workbuddy-pool-unknown",
|
|
1847
2003
|
title: t("row.poolNeverTested")
|
|
1848
|
-
}, 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)("
|
|
2004
|
+
}, 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)("select", {
|
|
2005
|
+
className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-row-model",
|
|
2006
|
+
value: modelOverride,
|
|
2007
|
+
disabled: !canEdit,
|
|
2008
|
+
title: t("row.poolRowModelHint"),
|
|
2009
|
+
"aria-label": t("row.poolRowModelAria", { accountName: name }),
|
|
2010
|
+
onChange: (event) => onModelChange(event.currentTarget.value)
|
|
2011
|
+
}, (0, react.createElement)("option", { value: "" }, t("row.poolRowModelInherit")), modelOverride !== "" && !catalog.some((model) => model.id === modelOverride) ? (0, react.createElement)("option", {
|
|
2012
|
+
value: modelOverride,
|
|
2013
|
+
disabled: true
|
|
2014
|
+
}, t("row.poolTargetStaleOption", { model: modelOverride })) : null, ...catalog.map((model) => (0, react.createElement)("option", {
|
|
2015
|
+
value: model.id,
|
|
2016
|
+
key: model.id
|
|
2017
|
+
}, model.name))), (0, react.createElement)("button", {
|
|
1849
2018
|
type: "button",
|
|
1850
2019
|
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
1851
2020
|
disabled: busy || testDisabled,
|
|
@@ -2987,6 +3156,16 @@ window.__ModuleLoader__.load({
|
|
|
2987
3156
|
.dsm-workbuddy-pool-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
2988
3157
|
.dsm-workbuddy-pool-note{margin:0;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
2989
3158
|
.dsm-workbuddy-pool-warn{margin:0;padding:9px 11px;border-radius:9px;font-size:12px;line-height:17px;color:var(--dsw-alias-state-warn-primary,#f59e0b);background:rgba(245,158,11,.09);border:1px solid rgba(245,158,11,.3)}
|
|
3159
|
+
/* Ghost members (issue #60): the warning plus one removable chip per id. The
|
|
3160
|
+
chips are what make the warning actionable — a ghost has no row in the table
|
|
3161
|
+
above, because that roster is the sign-ins this machine still has. */
|
|
3162
|
+
.dsm-workbuddy-pool-ghosts{display:flex;flex-direction:column;gap:6px}
|
|
3163
|
+
.dsm-workbuddy-pool-ghost-list{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}
|
|
3164
|
+
.dsm-workbuddy-pool-ghost{display:inline-flex;align-items:center;gap:4px;padding:2px 4px 2px 9px;border-radius:999px;font-size:12px;line-height:18px;color:var(--dsw-alias-state-warn-primary,#f59e0b);background:rgba(245,158,11,.09);border:1px solid rgba(245,158,11,.3)}
|
|
3165
|
+
.dsm-workbuddy-pool-ghost-name{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3166
|
+
.dsm-workbuddy-pool-ghost-remove{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:0;border-radius:999px;font-size:11px;line-height:1;cursor:pointer;color:inherit;background:transparent}
|
|
3167
|
+
.dsm-workbuddy-pool-ghost-remove:hover:not(:disabled){background:rgba(245,158,11,.2)}
|
|
3168
|
+
.dsm-workbuddy-pool-ghost-remove:disabled{cursor:default;opacity:.5}
|
|
2990
3169
|
.dsm-workbuddy-pool-error{margin:0;font-size:12px;line-height:17px;color:var(--dsw-alias-state-error-primary,#ef4444);white-space:pre-line}
|
|
2991
3170
|
.dsm-workbuddy-pool-table{border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2,#24262c)}
|
|
2992
3171
|
.dsm-workbuddy-pool-row{display:grid;grid-template-columns:1.6fr 1fr 1.3fr .9fr .8fr;gap:10px;align-items:center;padding:10px 14px;border-top:1px solid var(--dsw-alias-border-l1,#2c2d31);font-size:13px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
@@ -3020,6 +3199,10 @@ window.__ModuleLoader__.load({
|
|
|
3020
3199
|
.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)}
|
|
3021
3200
|
.dsm-workbuddy-pool-prov-sep{color:var(--dsw-alias-label-dimmed,#9aa0a6);opacity:.7}
|
|
3022
3201
|
.dsm-workbuddy-pool-prov-age{color:var(--dsw-alias-label-tertiary,#999);white-space:nowrap}
|
|
3202
|
+
/* WHICH model a verdict is about (issue #61). Deliberately quiet: it is scope
|
|
3203
|
+
information on the provenance line, not a status of its own, so an ok row and
|
|
3204
|
+
a rate-limited row must not look different because of it. */
|
|
3205
|
+
.dsm-workbuddy-pool-prov-model{color:var(--dsw-alias-label-tertiary,#999);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px}
|
|
3023
3206
|
.dsm-workbuddy-pool-members{display:flex;flex-direction:column;gap:10px}
|
|
3024
3207
|
.dsm-workbuddy-pool-members-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
3025
3208
|
.dsm-workbuddy-pool-members-title{margin:0 0 3px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
@@ -3073,6 +3256,9 @@ window.__ModuleLoader__.load({
|
|
|
3073
3256
|
/* Per-row actions (test / remove), right side of each pool row. */
|
|
3074
3257
|
.dsm-workbuddy-pool-row-actions{display:flex;gap:6px;align-items:center;justify-content:flex-end;flex:none}
|
|
3075
3258
|
.dsm-workbuddy-pool-row-actions .dsm-workbuddy-pool-small-btn{padding:2px 10px;font-size:12px}
|
|
3259
|
+
/* This account's own test model (issue #61). Narrower than the section-level
|
|
3260
|
+
picker so a long model name cannot push the two buttons out of the row. */
|
|
3261
|
+
.dsm-workbuddy-pool-row-model{max-width:150px;padding:2px 6px;font-size:12px}
|
|
3076
3262
|
@media (max-width:760px){.dsm-workbuddy-pool-row{grid-template-columns:1fr 1fr;row-gap:6px}.dsm-workbuddy-pool-select{max-width:180px}}
|
|
3077
3263
|
/* Pool switch, namespaced under .dsm-workbuddy-pool so it cannot collide with host styles. */
|
|
3078
3264
|
.dsm-workbuddy-pool .sw{position:relative;display:inline-block;width:38px;height:22px;flex:none;cursor:pointer}
|
|
@@ -3542,7 +3728,7 @@ window.__ModuleLoader__.load({
|
|
|
3542
3728
|
function formatNumber(value) {
|
|
3543
3729
|
return new Intl.NumberFormat(void 0, {
|
|
3544
3730
|
minimumFractionDigits: 0,
|
|
3545
|
-
maximumFractionDigits:
|
|
3731
|
+
maximumFractionDigits: 0
|
|
3546
3732
|
}).format(value);
|
|
3547
3733
|
}
|
|
3548
3734
|
/**
|
|
@@ -4865,10 +5051,6 @@ window.__ModuleLoader__.load({
|
|
|
4865
5051
|
const value = usage.showCreditsInMainUi;
|
|
4866
5052
|
return typeof value === "boolean" ? value : void 0;
|
|
4867
5053
|
}
|
|
4868
|
-
/** Group the digits so a four-figure balance stays readable in a narrow row. */
|
|
4869
|
-
function formatCredits(value) {
|
|
4870
|
-
return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
|
|
4871
|
-
}
|
|
4872
5054
|
function formatClock(value) {
|
|
4873
5055
|
return new Date(value).toLocaleTimeString("zh-CN", {
|
|
4874
5056
|
hour: "2-digit",
|
|
@@ -5455,6 +5637,11 @@ window.__ModuleLoader__.load({
|
|
|
5455
5637
|
"row.poolProbeSourceTest": "tested",
|
|
5456
5638
|
"row.poolProbeSourceLive": "hit by a live request",
|
|
5457
5639
|
"row.poolProbeSourceUnknown": "source unknown",
|
|
5640
|
+
"row.poolProbeModel": "model {model}",
|
|
5641
|
+
"row.poolProbeModelUnknown": "model not recorded",
|
|
5642
|
+
"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.",
|
|
5643
|
+
"row.poolRowModelAria": "Test model for {accountName}",
|
|
5644
|
+
"row.poolRowModelInherit": "region target",
|
|
5458
5645
|
"row.poolSettingsTitle": "Account pool settings",
|
|
5459
5646
|
"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.",
|
|
5460
5647
|
"row.poolEnabled": "Enable the account pool",
|
|
@@ -5497,7 +5684,10 @@ window.__ModuleLoader__.load({
|
|
|
5497
5684
|
"row.poolSelectedCount": "{count} of {total} selected",
|
|
5498
5685
|
"row.poolNoneSelected": "No account is checked into this pool.",
|
|
5499
5686
|
"row.poolNoneSelectedHint": "Check at least one account above and save. Until then both buttons stay disabled, because a pool with no members has nothing to run on.",
|
|
5500
|
-
"row.poolGhostMembers": "{count}
|
|
5687
|
+
"row.poolGhostMembers": "{count} pool member(s) are no longer signed in on this machine, so they are not counted and not used: {names}. Remove any you no longer need below.",
|
|
5688
|
+
"row.poolGhostRemove": "Remove {account} from this pool",
|
|
5689
|
+
"row.poolGhostRemoveNamed": "Remove {account} from this pool. This account is no longer signed in on this machine.",
|
|
5690
|
+
"row.poolGhostRemoveUnnamed": "Remove {account} from this pool. No name was ever recorded for this account, so what you see is the start of its id.",
|
|
5501
5691
|
"row.poolSelectAria": "Include {accountName} in this region's account pool",
|
|
5502
5692
|
"row.poolMember": "In pool",
|
|
5503
5693
|
"row.poolCurrentBadge": "In use",
|
|
@@ -5718,6 +5908,11 @@ window.__ModuleLoader__.load({
|
|
|
5718
5908
|
"row.poolProbeSourceTest": "测得",
|
|
5719
5909
|
"row.poolProbeSourceLive": "聊天撞上",
|
|
5720
5910
|
"row.poolProbeSourceUnknown": "来源未知",
|
|
5911
|
+
"row.poolProbeModel": "模型 {model}",
|
|
5912
|
+
"row.poolProbeModelUnknown": "未记录模型",
|
|
5913
|
+
"row.poolRowModelHint": "这个账号测试用哪个模型。留「区域目标」就跟随账号池的目标模型;当某个号必须用你实际在聊的模型来测时,在这里单独指定。",
|
|
5914
|
+
"row.poolRowModelAria": "{accountName} 的测试模型",
|
|
5915
|
+
"row.poolRowModelInherit": "区域目标",
|
|
5721
5916
|
"row.poolSettingsTitle": "账号池设置",
|
|
5722
5917
|
"row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
|
|
5723
5918
|
"row.poolEnabled": "启用账号池",
|
|
@@ -5760,7 +5955,10 @@ window.__ModuleLoader__.load({
|
|
|
5760
5955
|
"row.poolSelectedCount": "已选 {count} / {total}",
|
|
5761
5956
|
"row.poolNoneSelected": "这个池里还没有勾选任何账号。",
|
|
5762
5957
|
"row.poolNoneSelectedHint": "请在上方勾选至少一个账号并保存。在此之前两个按钮都不可用 —— 空池没有可执行的对象。",
|
|
5763
|
-
"row.poolGhostMembers": "有 {count}
|
|
5958
|
+
"row.poolGhostMembers": "有 {count} 个已选入本池的账号在本机已不再登录,因此不计入、也不会被使用:{names}。不需要的可在下方逐个移除。",
|
|
5959
|
+
"row.poolGhostRemove": "把 {account} 移出本池",
|
|
5960
|
+
"row.poolGhostRemoveNamed": "把 {account} 移出本池。该账号在本机已不再登录。",
|
|
5961
|
+
"row.poolGhostRemoveUnnamed": "把 {account} 移出本池。这个账号从未记录过名字,显示的是它 id 的开头几位。",
|
|
5764
5962
|
"row.poolSelectAria": "把 {accountName} 加入本区域的账号池",
|
|
5765
5963
|
"row.poolMember": "已在池中",
|
|
5766
5964
|
"row.poolCurrentBadge": "当前使用",
|
|
@@ -1220,6 +1220,24 @@ function ensureSystemHead(obj) {
|
|
|
1220
1220
|
* `tool_choice` (the upstream's field is a string; object forms return 400),
|
|
1221
1221
|
* and guarantee a leading system message.
|
|
1222
1222
|
*/
|
|
1223
|
+
/**
|
|
1224
|
+
* The model id a prepared body asks for, when it carries one (issue #61).
|
|
1225
|
+
*
|
|
1226
|
+
* Lives beside {@link prepareChatBody} because it reads the same shape: the
|
|
1227
|
+
* serialized OpenAI body, after this plugin's rewrites. Never throws — a
|
|
1228
|
+
* malformed body is the upstream's to reject, not a reason to lose the failure
|
|
1229
|
+
* measurement that this id is needed for.
|
|
1230
|
+
*/
|
|
1231
|
+
function modelIdOfBody(bodyJson) {
|
|
1232
|
+
try {
|
|
1233
|
+
const parsed = JSON.parse(bodyJson);
|
|
1234
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
1235
|
+
const model = parsed["model"];
|
|
1236
|
+
return typeof model === "string" && model !== "" ? model : void 0;
|
|
1237
|
+
} catch {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1223
1241
|
function prepareChatBody(source) {
|
|
1224
1242
|
let body;
|
|
1225
1243
|
try {
|
|
@@ -1829,10 +1847,18 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1829
1847
|
if (typeof raw !== "object" || raw === null) continue;
|
|
1830
1848
|
const account = raw;
|
|
1831
1849
|
const numberField = (key) => typeof account[key] === "number" ? account[key] : 0;
|
|
1850
|
+
const preciseField = (key) => {
|
|
1851
|
+
const raw = account[`${key}Precise`];
|
|
1852
|
+
if (typeof raw === "string" && raw !== "") {
|
|
1853
|
+
const parsed = Number.parseFloat(raw);
|
|
1854
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
1855
|
+
}
|
|
1856
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
1857
|
+
return numberField(key);
|
|
1858
|
+
};
|
|
1832
1859
|
const monthly = numberField("CapacityType") === 4;
|
|
1833
|
-
const size =
|
|
1834
|
-
const remain =
|
|
1835
|
-
const cycleRemain = monthly ? numberField("CycleCapacityRemain") : void 0;
|
|
1860
|
+
const size = preciseField("CycleCapacitySize") || preciseField("CapacitySize");
|
|
1861
|
+
const remain = account["CycleCapacityRemain"] !== void 0 || account["CycleCapacityRemainPrecise"] !== void 0 ? preciseField("CycleCapacityRemain") : preciseField("CapacityRemain");
|
|
1836
1862
|
const cappedRemain = remain < 0 ? 0 : remain;
|
|
1837
1863
|
const cycleEndMs = parseDate(account["CycleEndTime"]);
|
|
1838
1864
|
const expiresAtMs = monthly ? void 0 : parseDate(account["ExpiredTime"]) ?? cycleEndMs;
|
|
@@ -1849,7 +1875,6 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1849
1875
|
remain: cappedRemain,
|
|
1850
1876
|
size,
|
|
1851
1877
|
monthly,
|
|
1852
|
-
...cycleRemain === void 0 ? {} : { cycleRemain: Math.max(0, cycleRemain) },
|
|
1853
1878
|
...refreshAtMs === void 0 ? {} : { refreshAtMs },
|
|
1854
1879
|
...expiresAtMs === void 0 ? {} : { expiresAtMs }
|
|
1855
1880
|
});
|
|
@@ -3800,7 +3825,7 @@ function generationOf(version) {
|
|
|
3800
3825
|
//#endregion
|
|
3801
3826
|
//#region src/version.ts
|
|
3802
3827
|
/** The npm package version this build was produced from. */
|
|
3803
|
-
const WORKBUDDY_CONNECT_VERSION = "4.4.
|
|
3828
|
+
const WORKBUDDY_CONNECT_VERSION = "4.4.6";
|
|
3804
3829
|
//#endregion
|
|
3805
3830
|
//#region src/host-heartbeat.ts
|
|
3806
3831
|
/**
|
|
@@ -3958,4 +3983,4 @@ async function clearHostHeartbeat() {
|
|
|
3958
3983
|
await rm(workbuddyHostHeartbeatPath(), { force: true });
|
|
3959
3984
|
}
|
|
3960
3985
|
//#endregion
|
|
3961
|
-
export {
|
|
3986
|
+
export { isEncryptedFieldWrapper as $, workbuddyAccountId as A, modelIdOfBody as B, defaultDesktopAuthDirs as C, isVaultAccountId as D, isEncryptedCredentialError as E, WorkBuddyUpstreamClient as F, regionOf as G, parseReasoning as H, classifyUpstreamError as I, deriveAtRestKey as J, WORKBUDDY_APP_EXECUTABLE_ENV as K, declaredTools as L, CREDENTIAL_REJECTED_CODE as M, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as N, legacyWorkbuddyOwnAuthPath as O, WorkBuddyCredentialRejectedError as P, findWorkbuddyAppExecutableWithSource as Q, isCredentialRejectedError as R, defaultDesktopAuthCandidates as S, hasEncryptedCredentialFields as T, parseUpstreamModel as U, parseCreditMultiplier as V, prepareChatBody as W, fetchAtRestKeyPayload as X, deriveAtRestKeyId as Y, findWorkbuddyAppExecutable as Z, WORKBUDDY_AUTH_FILENAME as _, readHostHeartbeat as a, workbuddyAppExecutableCandidates as at, WorkBuddyEncryptedCredentialError as b, WORKBUDDY_CONNECT_VERSION as c, withFileLock as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, isWorkbuddyBundle as et, WorkBuddyCatalog as f, ENCRYPTED_CREDENTIAL_CODE as g, fallbackModelsFor as h, processStartTimeMs as i, readAtRestKey as it, workbuddyOwnAuthPath as j, parseWorkBuddyAuth as k, piAiRuntimeInfo as l, writeFileAtomic as lt, deriveCatalog as m, clearHostHeartbeat as n, macosNestedAppBundles as nt, workbuddyHostHeartbeatPath as o, hostPeersInfo as ot, applyContextBudgets as p, clearAtRestKeyCache as q, isHeartbeatProcessAlive as r, openEncryptedField as rt, writeHostHeartbeat as s, resolveDshHome as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, macosBundleExecutable as tt, FALLBACK_WORKBUDDY_MODELS as u, WORKBUDDY_AUTH_FILE_ENV as v, defaultDesktopAuthPath as w, authFileName as x, WorkBuddyCredentialStore as y, knownRegionOf as z };
|