dsh-connect-workbuddy 5.5.1 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.en.md +7 -5
- package/README.md +7 -5
- package/docs/assets/dsh-connect-workbuddy-account-pool.png +0 -0
- package/docs/assets/dsh-connect-workbuddy-composer-credits.png +0 -0
- package/docs/assets/dsh-connect-workbuddy-model-picker.png +0 -0
- package/docs/assets/dsh-connect-workbuddy-usage-card.png +0 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +77 -14
- package/lib/{host-heartbeat-BhgyeZWV.js → host-heartbeat-DqTNlE6H.js} +158 -3
- package/lib/index.d.ts +102 -5
- package/lib/index.js +115 -38
- package/package.json +1 -1
- package/screenshots.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.6.0 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
> 特性版本:两条都来自 issue 报告——#120(错峰时段的积分倍率没跟着变)与 #124(企业/教育版账号积分恒为 0、姓名显示成登录别名)。**请求路径、provider 路由、渠道门与计费口径一律未变**;改的是「倍率与徽标同源同时钟」和「企业账号读自己那张计费表」。
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **错峰时段的积分倍率跟随打折,卡片与模型选择器一起变(#120)**
|
|
10
|
+
- **报告**:模型行戴着「夜间折扣」徽标,旁边的数字还是原价 `x0.11`;真实折后价要去 WorkBuddy 客户端才看得到。徽标与价格来自**同一份** `/v3/config`、同一条促销条目,但插件只解析了 `badge`,把 `discount` 整个丢掉了。
|
|
11
|
+
- **修法**:`src/promo.ts` 新增 `WorkBuddyPromoDiscount` 与 `parseDiscount()`,读 `discount.discountedCredits`(`"0.06x"` 这种**把单位粘在值里**的字符串,`Number("0.06x")` 是 `NaN`,必须剥掉末尾 `x` 再转)+ `displayMode`;`factor` 只在字符串读不出时兜底,两者不一致时**字符串胜**——它才是用户实际被扣的那个数。
|
|
12
|
+
- **何时生效**:新增 `selectPromoMultiplier()`,只让**此刻正在运行**的促销打折。夜间折扣在上游是**两条条目**:一条带折扣 + 夜间时段,另一条是白天的常驻徽标(同款徽标、互补时段、无折扣)。白天那条只配画徽标、**不许改价**,否则卡片会把夜间价挂一整天。绝对有效期(`validFrom`/`validUntil`)也一并管住,活动过期即恢复原价。
|
|
13
|
+
- **两处渲染**:卡片模型行(`WorkBuddyCard.tsx` 的 `effectiveRate()`)与宿主模型选择器的标签(`WorkBuddyModelLabel.tsx`,同时显示 `· x0.06`)都用同一套判定;选择器原来根本不显示倍率,现在补上了——那一屏正是「现在到底多少钱」最该看到的地方。
|
|
14
|
+
- **README**(中英各一句):模型名里的倍率是**实付价**——活动时段显示折后价,窗口关闭自动回原价,不需要重启。
|
|
15
|
+
- **测试**:新增 `tests/issue120-promo-discount.spec.ts`(10 条)——`"0.06x"` / 裸数字 / `"0x"`(免费不等于缺失)/ `factor` 兜底 / 非有限值与非负值拒绝 / 未知 `displayMode` 忽略 / **白天常驻条目不降价** / 无折扣条目返回 `undefined`(保留目录价)/ 过期恢复 / 多条同时打折取最高优先级。
|
|
16
|
+
|
|
17
|
+
- **企业版(`type: ultimate`)账号读企业计费表,姓名显示真名(#124)**
|
|
18
|
+
- **报告**:企业/教育版账号积分恒为 0,账号名显示成登录别名。
|
|
19
|
+
- **积分修法**:`fetchCredits()` 按 `enterpriseId` 分流到 `/billing/meter/get-enterprise-user-usage`(**没有 `/v2` 前缀**)。个人端点对企业成员回的是 **HTTP 200 + 空 `Accounts` 列表**——一个形状合法但内容为假的「你没有积分」,插件不是被拒绝,是问错了表。企业表回 `{credit, limitNum, cycleResetTime}`,**`credit` 是已用额度**,余额是 `limitNum - credit`;把 `credit` 当余额读会把答案整个反转,比它替换掉的 0 更糟。`cycleResetTime` 不带时区标记,按**本地时间**手动拼(`Date.parse` 对这种形状是实现相关的)。
|
|
20
|
+
- **宁可报错也不报 0**:企业端点若回不出两个数字,抛错而不是返回 `total: 0`——后者正是 #124 报告的症状,等于把 bug 当作修复。
|
|
21
|
+
- **姓名修法**:新增 `preferredAccountName()`,顺序是 `displayName` → `enterpriseUserName` → `nickname`(登录别名垫底),空白串跳过。企业凭证的 `nickname` 是登录别名,把它放第一位会让卡片看起来在管别人的账号。`displayName` / `enterpriseUserName` 从桌面 auth 文档的 `account` 节点读,并随 DSH 凭证一起落盘。
|
|
22
|
+
- **跨次读取不丢**:新增 `mergeCredentialDisplayNames()`,`scanAll()` 合并同账号的两份副本时把姓名**双向带过去**——桌面 App 不总重写这两个字段,整条替换会静默退回别名,让 #124 的症状自己回来。
|
|
23
|
+
- **签到不再浪费请求**:企业网关没有消费者签到(对成员一律回 `10001`)。`WorkBuddyPoolTarget` 新增 `enterprise`,批次直接跳过并在日志里说「企业账号无每日签到」(新增 `skipped` 状态与新文案,中英各一条);跳过的行**不占批次间隔**——间隔是给请求留的。
|
|
24
|
+
- **测试**:新增 `tests/issue124-enterprise-credits.spec.ts`(14 条)——姓名优先级三级 + 空白跳过 + 合并保留/覆盖;企业端点被用而个人端点不被用、`limitNum - credit`、超额不为负、`Data` 包裹也能读、读不出余额即抛错、`cycleResetTime` 本地时间解析、非企业凭证仍走个人表。`tests/account-pool-run.spec.ts` 增 2 条(跳过企业账号、跳过的行不计间隔)。
|
|
25
|
+
|
|
26
|
+
## 5.5.2 (2026-10-23)
|
|
27
|
+
|
|
28
|
+
> 补丁版本:两条修复都来自 issue 报告——#118(`WORKBUDDY_BODY_REPORT` 报告文件无界增长、回填整文件读写导致宿主 OOM)与 #122(界面文案审计)。**请求路径、provider 路由、模型目录、计费与凭据一律未变**;改的只有诊断文件的 I/O 形状、若干句用户可见文案与两处内部注释,外加三张 README 截图换成合成数据。
|
|
29
|
+
|
|
30
|
+
### Fixes
|
|
31
|
+
|
|
32
|
+
- **`WORKBUDDY_BODY_REPORT` 的报告文件不再无界增长,回填不再整文件读写(#118)**
|
|
33
|
+
- **报告**:报告者开着该环境变量长跑,`global.jsonl` 长到 **508 MB**;每次上游回 `usage` 后的回填都会 `readFile` 整文件 → `split('\n')` → `writeFile` 整文件,峰值约为文件大小的 2–3 倍(读串 + 行数组 + 重组串),把宿主进程顶到 OOM。`USAGE_ATTACH_WINDOW = 32` 只裁了**搜索**范围,没裁读写。
|
|
34
|
+
- **修法一:写侧有上限**。`appendBodyReport()` 追加前 `stat` 当前文件,追加会越过 `BODY_REPORT_MAX_BYTES = 32 MB` 时先滚成 `<region>.jsonl.1`(`rm` 旧滚档再 `rename`,只保留一份旧档)再写新行。尺寸检查与追加在**同一个文件锁**里,两个并发请求不会同时判「不用滚」再一起写超。滚档后活文件里是**新**行——报告者对比「最后一行」时不会拿到被滚走的旧捕获。32 MB 的取值:真实会话一行几 KB,32 MB 是几千次请求,远超还有人读它的时点。
|
|
35
|
+
- **修法二:回填只碰尾部窗口**。`attachUsage()` 改成 `open(file, 'r+')` + 读最后 `USAGE_ATTACH_TAIL_BYTES = 1 MB` + 窗口内定位目标行 + 从该行偏移写回 + `truncate` 到新长度;窗口前一个字节都不读不写,500 MB 文件在这里只花 1 MB 堆。窗口起点用**一字节回看**判定行边界:回看字节是 `\n` 说明窗口正好起在行边界、首行完整(必须保留——丢掉等于从文件里删一行);否则首行是上一条的残尾,从搜索与写回里都排除。窗口外的目标行**不再回填**:标注只是 #59 对比的便利,不是正确性要求(发送侧计数早已在盘上)。
|
|
36
|
+
- **README 补警告**(中英各一句):会持续追加、32 MB 滚档、排查完请关闭环境变量并删除报告文件。
|
|
37
|
+
- **测试**:新增 `tests/issue118-body-report-bounded.spec.ts`(6 条)——滚档与「只留一份旧档」、滚档后活文件是新行、不滚空文件与未超上限、大于一个窗口的文件里近期行仍被标注且窗口前逐字节不变、**窗口恰好起在行边界**(用迭代法把追加行尺寸钉到正好 1 MB:`model` 字段被原样抄进行里是唯一的尺寸旋钮,而 `bodyBytes` 的位数会随填充自增,一步算不准;断言三行都在、前两行未被标注也未被删)、目标行被推出窗口后文件不被截断或切碎。原有 `tests/issue59-body-report.spec.ts` 26 条全绿(含「标注只加在原有行上、不新增行」)。
|
|
38
|
+
- **四处用户可见文案与两处内部注释按 #122 审计改正**
|
|
39
|
+
- `row.showCreditsCnHint` / `row.showCreditsGlobalHint`(中英):删掉**不存在**的「手动刷新」——面板头部唯一的按钮是「一键测试」(会真跑一轮测试、真实消耗积分);改成「点击可看各账号余额并一键测试勾选的账号(测试会真实消耗积分)。读数每 5 分钟自动刷新;各账号明细在面板打开时每 60 秒刷新」,把两个周期分开写清(此前只写了 5 分钟那个)。
|
|
40
|
+
- `row.poolEnabledHint`(中英):「失败就换下一个」改成「只有可换号的失败才换下一个」,并点名四类例外(请求本身不合法、模型繁忙、平台排队、渠道校验)与理由(换号只会为同一个拒绝白烧一次真实配额)——这四类在 `isFailoverWorthy` 里被显式排除,旧文案说反了。
|
|
41
|
+
- `row.cliBridgeHint`(中英):`injectResult` **只影响 `/codebuddy` 指令**(结果回注会话、会话空闲时多花一次 DSH 回合),`codebuddy_ask` 工具不读它;并补上三条此前完全没提的限流(每会话每小时 6 回合、每进程每小时 20 回合、同时只跑一个回合)。
|
|
42
|
+
- `row.tabFreeQuotaWarn`(中英):不再声称「时间段内定额 Token 上限」这个插件**读不到**的机制(上游没有这个字段),改成「免费模型并非无限量:上游按单次请求体积与负载限流」;内部注释同步改写——6004 实测是**单次请求体积**触发(`src/probe.ts:71-89`:~25k 通过 / ~30k 被拒),14003 是模型侧负载,两者都不是累计定额。
|
|
43
|
+
- `src/index.ts` 的 `WorkBuddyPoolPreferences.enabled` 注释:旧句「刻意不改变下一个由谁服务」与 `applyPoolSelection()` 每次请求都按排名设服务账号的事实相反;改成「从不改变的是用户**保存的选择**」,并留一句旧注释错在哪的追溯(#122 的审计者指出「用户文案反而比这处注释更准」)。
|
|
44
|
+
- ①④ 三条审计结论无需改动:三条 tab 提醒在 5.5.0 的「能力」分区里已分开表述;5.71 仍是 2026-10-21 的实测常量(无配置项)、生图开关仍 CN-only 且关闭时根本不注册;测试默认 25000 与 1/2/3/5/10 万五档未变。
|
|
45
|
+
|
|
46
|
+
### Docs
|
|
47
|
+
|
|
48
|
+
- **README 产品截图换成真机图并做隐私处理**。旧图停在 5.5.0 之前(「一键签到所有账号」「刷新积分」等已改名的按钮),且带着真实令牌过期日 / 积分包到期日;本版换成维护者实机截的四张:设置卡「模型」页(`dsh-connect-workbuddy-usage-card.png`,含倍率、图片与可关闭思考开关、`拒绝 off` 标记、活动标签)、账号池(`dsh-connect-workbuddy-account-pool.png`,含「只有可换号的失败才换」的新文案与八行成员表)、输入框积分面板(`dsh-connect-workbuddy-composer-credits.png`,一键测试按钮)与模型选择器(`dsh-connect-workbuddy-model-picker.png`,同一枚活动徽标画在模型名之后)。
|
|
49
|
+
- **隐私**:账号列每一行的名字格用不透明色块覆盖(不是可复原的马赛克),并逐行用「色块覆盖区内深色像素数 = 0」的脚本复核;积分、签到状态、排序位次与时间戳保留——它们不含身份。
|
|
50
|
+
- 模型选择器那张只含模型名、倍率与活动文案,没有账号信息。`screenshots.json` 与中英两份 README 的 alt 文本同步。
|
|
51
|
+
|
|
3
52
|
## 5.5.1 (2026-10-23)
|
|
4
53
|
|
|
5
54
|
> 补丁版本:修「账号池状态重启就失效」的根因,以及同一症状的另外三处(手动排序箭头、`regionsMapOf` 跳层读旧副本、卡片保存把脏数据带回去)。**没有新增任何功能**:请求路径、provider 路由、模型目录、计费与凭据一律未变,5.5.0 的全部能力原样保留。
|
package/README.en.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/assets/dsh-connect-workbuddy-usage-card.png" width="640" alt="dsh-connect-workbuddy settings
|
|
2
|
+
<img src="docs/assets/dsh-connect-workbuddy-usage-card.png" width="640" alt="dsh-connect-workbuddy settings card: model directory with multipliers, image and off-thinking switches, promo badges (account names redacted)" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">dsh-connect-workbuddy</h1>
|
|
@@ -33,7 +33,9 @@ It depends on the WorkBuddy client's own endpoints (not a public API), so the pl
|
|
|
33
33
|
- **Tabbed settings card** — the card's top carries a "Domestic / Global" tab bar; each tab holds its own account pool and model management. Account, directory, selection, and unsaved drafts are fully isolated per tab — switching accounts or refreshing models on one tab never touches the other side's runtime catalog or sessions.
|
|
34
34
|
- **Switch either provider off on its own** — each tab carries an on/off checkbox (both checked by default). Unchecking one **withdraws that provider from DSH's model picker entirely** (it is not merely a hidden tab): its adapter route and its "Settings → Models" directory entry are both pulled, and startup no longer sends pointless requests for it. Account, credits, directory, and selection are all kept, and re-checking restores everything. The typical use is "I have no use for the international side" — switch it off and keep the picker clean. Note: a session that had already selected a model from the switched-off provider will report `NO_ADAPTER` on its next call; the card states this plainly on the switched-off tab instead of failing silently.
|
|
35
35
|
- **Model management** — refresh the full catalog from upstream and enable or disable each model individually; refresh is a draft operation that only takes effect on save. Upstream also reports credit multiplier, context/output limits, and reasoning efforts; **image input is decided by a vendor-verified table**: a refresh pre-checks only models the vendor's own documentation calls natively multimodal, leaves documented text-only models unchecked, and treats anything without a vendor source as unverified and unchecked rather than guessing. Upstream's `supportsImages` is a *platform* declaration, not the model's capability, so it does not drive that decision at all. You can still tick any box by hand, and your selection is kept locally. **Every model row also carries a “Can disable” (`off`) checkbox**: it decides whether that model gets the “turn thinking off entirely” level. The default comes from a reviewed built-in table (the 9 models that claim the capability but answer HTTP 400 for it start unticked), and you can change it per model — **untick** for a model you do not need the level on. **The 9 tabled models cannot be ticked at all** (their row is marked "refuses off" with an explanatory tooltip): the level is not merely an extra option — pi-ai uses it as the DEFAULT for every request that names no tier, and a compaction summary is exactly such a request, so one tick made every `/compact` for that model fail with HTTP 400 / code 11133 (issue #83). Models outside the table are still decided entirely by your selection and their own declaration; your ticks are saved on first save and recomputed no further.
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
<img src="docs/assets/dsh-connect-workbuddy-model-picker.png" width="420" alt="DSH model picker: the same campaign badge (night discount / Free now) drawn after the model name, per region (no account data)" />
|
|
38
|
+
- **Campaign badges on model rows** — the upstream model catalog carries campaign information (free trials, night discounts, limited-time free use, subscription-only tiers, and the like). The plugin adds **no extra request** (it reuses the catalog configuration it already fetches when refreshing models) and draws a pill badge right on the model row: the upstream supplies both the colour and the wording, and hovering shows the upstream's campaign text, its start/end dates, and its daily window (e.g. “night 23:00–7:50”). **A badge is a function of the current moment**: it shows only inside the window the upstream declares and disappears on its own when that window closes; a window that crosses midnight is judged in the timezone the upstream declared; when several campaigns hit the same model, only the highest-priority one is drawn. A benefit marker without an end date (such as “subscription only”) stays until upstream withdraws it. **The multiplier shown is the price you actually pay**: while a “night discount” is running, the card and the picker both show the discounted `x0.06` instead of the catalog figure, and both go back on their own when the window closes — no restart needed (issue #120). **A badge is a hint, not behaviour** — it reports the campaign and discount upstream currently advertises, and makes no choice for you. **The same badge now appears in the DSH model picker**: every menu row draws the identical pill after the model name and before the load icon — menu rows use the upstream's short wording ("night", "free"), the composer trigger beside the input uses the full one ("night discount", "exclusive free"); judged from the same data at the same clock, because the screen where the model is chosen is exactly where "is it free right now" matters most. (Both regions' catalogs ship the campaign fields: read as the app, the CN `/v3/config` carries 8 promotions + 1 tier badge and the global one carries 5 + 1; the evidence and the decision rules are written up in [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md).)
|
|
37
39
|
- **Model load on model rows (CN only)** — the upstream answers "how busy is this model right now" from a separate read-only probe (`POST /v2/chat/busy`, up to 50 ids per call, free, no account state touched). The plugin asks it once per catalog refresh and draws the level as a small figure beside the model name: **the number of people = how many are queueing** (healthy `normal` = one yellow figure, `busy` = two orange ones, `congested` = three red ones), while **idle is a green rocket** (nobody is queueing = full speed ahead) — idle is deliberately *not* drawn as "one green person", because at 12px "nobody is waiting" and "one person is waiting" have to be distinguishable at a glance; **unavailable** is a crossed-out figure. The probe's own `unknown` **draws nothing**: **"we were told nothing" is never reported as "all good"**. Colours follow the theme and the `title` tooltip ("极度拥堵") is still there. **The level is not persisted**: the catalog is a fact that survives a restart, a load level is a fact about that minute, so it is stamped onto the runtime catalog only and dies with the process; a failed probe logs one warning and never blocks the refresh. Measured on the CN gateway: the probe answers byte-identically under the CLI and the app identity, the 50-id ceiling is real (62 ids → HTTP 400), and 19 models come back in 186 ms. **The international region shows no load badge, because the international gateway simply does not have this endpoint** — measured, `www.workbuddy.ai` answers **404** for `/v2/chat/busy` (and for the neighbouring `/v3/chat/busy`, `/v2/models/busy` and friends), while the very same credential reads its `/v3/config` fine (28 models) and that document carries **no load or queue field at all**. This is not something the plugin left unimplemented, and it is not a configuration or permission problem: **upstream has never published this data for the international region**. So the international region shows the model name and campaign badges only; the failed probe degrades quietly to "no badge", never blocking the model list, and the plugin never guesses a load level from latency or credits.
|
|
38
40
|
- **Queueing tells you your position** — upstream expresses a queue with three business codes (`6020` waiting, `6021` queue full, `6022` this user already at the maximum). Instead of passing along a raw upstream error, the plugin says "模型排队中:你排在第 8777 位,队列共 9021 个请求;约 7 秒后自动重试". **A queue does not trigger failover**: the line is shared platform-wide and bound to the model, so another account is handed the same position — no account-pool rotation, no extra requests burned. (The upstream's `/v2/chat/queue/status` returns an i18n KEY rather than a sentence, so the plugin never relays its `message` and always uses its own wording.) **The "resend once it is your turn" half is not implemented** — that needs the queue answer's real envelope on the OpenAI-compatible channel, and writing it unmeasured would mean code that never runs.
|
|
39
41
|
- **Tabbed settings card (accounts / models / capabilities / diagnostics)** — the card used to be a single 3,500-pixel column that mixed two things with completely different commit semantics: **operating accounts** (who is serving, how much is left, switch order, check-in log) and **editing the catalog** (ticking models, context budgets, reasoning tiers, turning thinking off). An account change takes effect on the click; a catalog change is a draft that needs Save — so the Save button often sat three screens away from the thing it saves. Now each tab does one thing, and **switching tabs neither refetches nor drops an unsaved draft** (inactive panels stay mounted and are merely `hidden`, so they also leave the accessibility tree). The third tab, Capabilities, holds the **grants**: let the plugin drive the local CodeBuddy CLI, let the model buy an image on its own, where the credit readout is drawn. Each takes effect on the click and never passes through Save, and each is expected to grow sub-options of its own (a working directory for the CLI route, a size or price cap for the image tool) — which is why they no longer share one thin strip above the account table. The fourth tab, Diagnostics, is a read-only fact table: current region, upstream host, the account actually serving (by NAME, never by id), sign-in validity, how many models the catalog holds, how many are enabled, the pool switch, and the **raw text** of the upstream's most recent error (quoted, never translated or summarised) — its only reason to exist is to be pasted into a bug report, so it contains no control that changes a setting.
|
|
@@ -44,7 +46,7 @@ It depends on the WorkBuddy client's own endpoints (not a public API), so the pl
|
|
|
44
46
|
- **Local account switching** — discovers the multiple sign-in credentials WorkBuddy's desktop app leaves behind and lets you switch per region. Tokens are never written to DSH settings.
|
|
45
47
|
- **Credit readout in the composer toolbar (follows the selected model, per region)** — a clickable readout sits at the left of the composer toolbar (e.g. `WB CN · 1,072`) and **switches region automatically with the model this session is using**: pick a WB CN model and it shows the domestic balance, pick a WB AI model and it shows the international one; with any other provider's model it does not appear at all. It refreshes every 5 minutes and can also be refreshed by hand. Clicking it opens a panel: a **table of balances per account**, where clicking a row switches to that account (the same setting the card's account picker writes), and an account that cannot serve right now is **labelled in its row** (e.g. rate-limited, out of credits) using the same wording as the account-pool table. The per-account figures are fetched only while the panel is open, so the 5-minute background refresh never multiplies by the account count. To turn a side off, untick “Show credits in the composer toolbar” under that region's tab on the card; while a region's provider is switched off, the readout disappears and its switch is disabled. The same block carries a “Position” line: by default the readout sits in the composer toolbar (the row with the model picker); choosing “below the input box” moves it to the turn/step/token stats row instead — useful when another plugin has taken the toolbar seat.
|
|
46
48
|
|
|
47
|
-
<img src="docs/assets/dsh-connect-workbuddy-composer-credits.png" width="360" alt="Composer credit panel: a per-account balance table for the domestic region, with a
|
|
49
|
+
<img src="docs/assets/dsh-connect-workbuddy-composer-credits.png" width="360" alt="Composer credit panel: a per-account balance table for the domestic region, with a one-click test button and a last-refreshed time (account names redacted)" />
|
|
48
50
|
- **Account pool: batch actions across several accounts, plus automatic failover** — tick the accounts you want into a pool and you get **one-click check-in for every account**, **one-click testing of every account against a model you choose** (defaulting to the region's zero-multiplier free model), and with **Enable the account pool** switched on, **automatic failover**: a request that fails upstream is retried against the pool's other usable accounts, in order, until one serves it or none is left. Membership starts **empty and is an explicit opt-in**: "signed in on this machine" is not the same as "spend this account's credits and claim its rewards". The pool's **ranking decides who serves every request** (usable first, then expiring-credits-first, higher credits, freshest credential), and on a failure the next usable member is tried. It **never writes back your saved selection** — switch the pool off and your own account applies again immediately. **The switch only decides whether the PLUGIN chooses** — with it off, the plugin stops picking and switching accounts for you, but check-in and testing stay available (they are manual actions). Testing reuses the model rows' real-volume probe, so it **does cost real credits**; **automatic check-in is deliberately not offered** — a check-in claims real rewards, so it only ever runs when you press the button. See [Account pool](#account-pool).
|
|
49
51
|
- **Benefits: a marker for what is unclaimed, plus a per-account Claim button** — the card marks, account by account, anything in the upstream's benefit campaigns (compensation / gifts) that **has not been claimed yet**. The data comes from the upstream's own two status endpoints; in the marking stage the plugin is **read-only** and never calls the two write endpoints. Three display rules all follow the upstream's state: `active && !claimed` shows the row, `claimed` removes it entirely, and an expired campaign (upstream flips `active` to `false`) disappears by itself — the plugin **never compares `end_time` against the local clock**, so a skewed machine cannot argue with the server; `end_time` only drives the "three days left" highlight. `active` alone is not enough to decide: an already-claimed account still reports `active: true`, and reading it alone would leave a claimed campaign hanging there forever. The read reuses the check-in pipeline (once per account, cached for 30 minutes, no request while the account pool is off), and a failing endpoint counts as "unread" rather than caching "nobody has benefits". Each row ends in a solid amber **Claim pill**, and one click claims **one campaign of one account**: there is no "claim everything", because a click spends one irrevocable real entitlement — and the write route **requires `accountId`**, so the request itself cannot batch. Before writing it **re-reads** the status and decides again; anything already claimed or expired returns 409 without touching upstream. After a successful claim it answers with the upstream's **fresh reading** and refreshes the card, with no optimistic update — even a silent upstream failure cannot leave a false "claimed" behind — and it also drops that account's cached benefit state, so the pill cannot linger on a stale status.
|
|
50
52
|
- **Actionable advice when a credential is refused** — when the upstream rejects the selected account's token, the plugin actually **probes** the other local sign-ins: if one still answers, it tells you to switch to it (with a one-click switch) instead of vaguely asking you to sign in again — re-authenticating fixes nothing while you are pinned to a revoked backup. Only when there is genuinely no other local account does it ask you to sign in again.
|
|
@@ -78,7 +80,7 @@ Credentials are read (read-only) from the WorkBuddy desktop app's own auth file.
|
|
|
78
80
|
## Account pool
|
|
79
81
|
|
|
80
82
|
<p align="center">
|
|
81
|
-
<img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy account pool: check
|
|
83
|
+
<img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy account pool: check in the checked accounts, test the checked accounts, manual ordering and check-in state (account names redacted)" />
|
|
82
84
|
</p>
|
|
83
85
|
|
|
84
86
|
A machine usually carries more than one WorkBuddy sign-in. The **account pool** turns that into a single click: tick the accounts you want into the pool, then press **Check in selected** or **Test selected**; switch on **Enable the account pool** and the plugin will carry on through a failure by itself.
|
|
@@ -476,7 +478,7 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
|
|
|
476
478
|
|
|
477
479
|
- **Switching region makes the context "grow", and can hit `code 11115` immediately — the gateway is not over-charging you.** The same history is counted under different buckets in the two regions, and the root cause is how the host's bundled `@earendil-works/pi-ai` replays history. It evaluates `isSameModel` for every assistant message (`transform-messages.js:68`, keyed on provider **and** api **and** model id all being equal): for a message that fails, its thinking is **downgraded to ordinary content** (`:88–96` — not dropped), and content is what the gateways count, while thinking goes out in a different field. This plugin registers the domestic and international editions as **two provider ids** (`workbuddy` / `workbuddy-global`, see [How it works](#how-it-works)) that carry the **same model id** — so one region switch moves **all the other side's thinking into the content bucket at once**. That is the cause of issue #59's "the same conversation history is charged 1.374M by the international gateway and 708K by the domestic one (1.94×)"; switching back is the same formula read the other way, 0.696×.
|
|
478
480
|
- Three consequences you can see: a region switch can move the token count by ±30%–94%, and **how far depends only on the other side's thinking character count, not on total history length**; **switching models inside one region** triggers the same downgrade (the model leg of `isSameModel`), so the count can move without a region switch at all; and that jump lands on top of the host's current compaction threshold (`min(0.8 × window, window − 128000 − 65536)`), so a session that was comfortably under the limit can hit `code 11115 prompt is too long` the moment you switch region.
|
|
479
|
-
- **This is a mismatch between the host and pi-ai, not a bug in this plugin**: not replaying thinking across providers/models is right in itself (another model's train of thought is not context this model should be fed), and the mistake is treating `provider` as a necessary condition for "the same model" when the two provider ids here are **two access regions of one model**. The fix belongs upstream (same api + same model id, providers as an equivalence group); on the plugin side the backstop is `src/context-rescue.ts` since 4.8.0 — **once the upstream confirms an overflow, it folds history in chunks and resends**, instead of replaying the entire range the host meant to compact (which is exactly what makes a compaction request overflow itself and deadlock). To see where it happened: set `WORKBUDDY_BODY_REPORT` to capture a body-composition report — it records counts only, never text.
|
|
481
|
+
- **This is a mismatch between the host and pi-ai, not a bug in this plugin**: not replaying thinking across providers/models is right in itself (another model's train of thought is not context this model should be fed), and the mistake is treating `provider` as a necessary condition for "the same model" when the two provider ids here are **two access regions of one model**. The fix belongs upstream (same api + same model id, providers as an equivalence group); on the plugin side the backstop is `src/context-rescue.ts` since 4.8.0 — **once the upstream confirms an overflow, it folds history in chunks and resends**, instead of replaying the entire range the host meant to compact (which is exactly what makes a compaction request overflow itself and deadlock). To see where it happened: set `WORKBUDDY_BODY_REPORT` to capture a body-composition report — it records counts only, never text. **It appends continuously**: one `<region>.jsonl` per region, rolled to `.1` past 32 MB (one old copy kept); unset the variable and delete the files when you are done — do not leave it on for long sessions.
|
|
480
482
|
|
|
481
483
|
## Disclaimer
|
|
482
484
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/assets/dsh-connect-workbuddy-usage-card.png" width="640" alt="dsh-connect-workbuddy
|
|
2
|
+
<img src="docs/assets/dsh-connect-workbuddy-usage-card.png" width="640" alt="dsh-connect-workbuddy 设置卡:模型目录与倍率、图片与可关闭思考开关、活动标签(账号名已打码)" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">dsh-connect-workbuddy</h1>
|
|
@@ -36,11 +36,13 @@
|
|
|
36
36
|
|
|
37
37
|
## 功能特性
|
|
38
38
|
|
|
39
|
-
- **双供应商并行接入(国内版 + 国际版)** —— 国内版注册为 `workbuddy` provider(`GLM-5.3`、`DeepSeek-V4-Pro`、`Kimi-K3`、`MiniMax-M3`、`Hy3` 等),国际版注册为 `workbuddy-global` provider(`GPT-5.6`、`Gemini-3.5-Flash`、`GLM-5.3`、`Kimi-K3` 等)。**有账号的区域其模型会出现在 DSH 模型选择器里**:两边都有账号时两组同时可选,不同会话可以各选一边,互不干扰。**本机没有账号的区域不列出任何模型**(该区域一个请求也发不出去,列出来只会点到就失败);只要在该区域登录一次,「重新检测账号」或重启后模型就会自动出现——provider 本身始终注册着,因为它的设置卡片正是登录的入口(重新检测在账号池区块里)。模型名内嵌上游积分倍率(如 `GLM-5.3 · x0.79`),与 WorkBuddy
|
|
39
|
+
- **双供应商并行接入(国内版 + 国际版)** —— 国内版注册为 `workbuddy` provider(`GLM-5.3`、`DeepSeek-V4-Pro`、`Kimi-K3`、`MiniMax-M3`、`Hy3` 等),国际版注册为 `workbuddy-global` provider(`GPT-5.6`、`Gemini-3.5-Flash`、`GLM-5.3`、`Kimi-K3` 等)。**有账号的区域其模型会出现在 DSH 模型选择器里**:两边都有账号时两组同时可选,不同会话可以各选一边,互不干扰。**本机没有账号的区域不列出任何模型**(该区域一个请求也发不出去,列出来只会点到就失败);只要在该区域登录一次,「重新检测账号」或重启后模型就会自动出现——provider 本身始终注册着,因为它的设置卡片正是登录的入口(重新检测在账号池区块里)。模型名内嵌上游积分倍率(如 `GLM-5.3 · x0.79`),与 WorkBuddy 自身模型菜单一致;**活动时段显示的是折后价**——「夜间折扣」开着的时候,卡片与选择器上的数字一起变成 `x0.06`,活动一过自动回到原价,不需要重启(#120)。
|
|
40
40
|
- **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号池与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。
|
|
41
41
|
- **可单独关闭任一版本供应商** —— 每个 tab 右侧有一个勾选框(默认都勾选)。**取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉**(不是只藏起 tab):它的路由与「设置 → 模型」页的目录条目一并摘除,启动时也不再为它发无用请求。账号、积分、目录与勾选**全部保留**,重新勾选即完整恢复。典型用法是「我根本用不到国际版」——关掉它,让选择器干净。注意:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 `NO_ADAPTER`,卡片会在关闭态的 tab 上明确提示这一点,不会静默失败。
|
|
42
42
|
- **设置卡片分四栏:账号 / 模型 / 能力 / 诊断** —— 原来这张卡片是一列 3000 多像素的长页,把两类互不相干的事情混排在一起:**运维账号**(谁在服务、还剩多少、切换顺序、签到日志)与**编辑目录**(勾模型、上下文预算、推理档、关闭思考)。它们连提交语义都不同——账号改动点一下即生效,目录改动是草稿、必须点保存——所以「保存」按钮常常离它要保存的东西隔着三屏。现在分栏,一栏只做一件事;**切栏既不会重新拉取,也不会丢掉未保存的目录草稿**(非活动栏留在页面里,只是通过 `hidden` 不可见,因此也不进入无障碍树)。第三栏「能力」放的是**授权类开关**——允许调用本机 CodeBuddy CLI、允许模型自主生图、积分读数显示在哪——它们点一下即生效、从不经过保存,而且每一个都留着长自己子选项的位置(CLI 的工作目录、生图的尺寸或价格上限),所以不挤在账号表格上方那一小条里。第四栏「诊断」是只读的事实表:当前区域、上游域名、真正在服务的账号名、登录有效期、目录里共几个模型、启用了几个、账号池开关,以及上游最近一次错误的**原文**(原样引用,不翻译、不概括)——这一栏存在的唯一理由就是能被整段贴进问题反馈,所以里面没有任何会改设置的控件。
|
|
43
43
|
- **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。**每个模型行还有「可关闭思考」(`off`)勾选框**:决定要不要为该模型提供「完全关闭思考」这一档位。默认由内置复核名单给出(上游声称可关闭、实测却对它返回 HTTP 400 的 9 个模型默认不勾),你可以按模型改勾/取消——**取消**用于当前不需要该档位的模型;**名单内那 9 个模型不可勾选**(行上标「拒绝 off」,悬停有说明):该档位被勾上后不只是多一个选项,pi-ai 会把它当作**所有未指定档位请求的默认值**,压缩摘要请求正属于这一类,勾一次等于让该模型的每一次 `/compact` 都 400 / 11133(issue #83)。名单外的模型仍完全由你的勾选与自身声明决定;首次保存即写入你的勾选,之后不再重算。
|
|
44
|
+
|
|
45
|
+
<img src="docs/assets/dsh-connect-workbuddy-model-picker.png" width="420" alt="DSH 模型选择器:同一枚活动徽标(夜间折扣 / Free now)画在模型名之后(无账号信息)" />
|
|
44
46
|
- **模型行上的活动徽标** —— 上游的模型目录里带着活动信息(免费试用、夜间折扣、限时免费、订阅优先等),插件**不新增任何请求**(复用刷新模型时本来就会拉的那份目录配置),直接在模型行上画一枚胶囊徽标:颜色与文案都由上游给出,悬停能看到上游的活动说明、活动的起止日期与每日生效时段(例如「夜间 23:00–7:50」)。**徽标是「此刻」的函数**:活动只在它自己声明的时间窗内显示,窗口一关徽标就自己消失;跨零点的时段按上游声明的时区判断;同一个模型命中多条活动时只画优先级最高的那条。上游没给结束时间的权益标记(如「订阅优先」)则常驻,直到上游把它下架。**徽标只是提示,不改变模型行为**——它标注的是上游此刻给出的活动与折扣,不替你做选择。**同一枚徽标也出现在 DSH 的模型选择器里**:菜单每一行都在模型名之后、队列图标之前画同样的胶囊 —— 菜单行用上游的短词(「夜间」「限免」),输入框旁那个触发器用完整词(「夜间折扣」「独家限免」);判定用的是同一份数据、同一个时钟,因为选模型的那一屏正好是最该看到「现在免费吗」的地方。(两个区域的上游目录都下发活动字段:国内版按 App 身份读 `/v3/config` 时带 8 条促销 + 1 条等级标记,国际版带 5 条 + 1 条;取证与判定规则见 [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md)。)
|
|
45
47
|
- **模型行上的繁忙程度(仅国内版)** —— 上游另有一条只读探针(`POST /v2/chat/busy`,一次最多 50 个 id,免费、不动账号状态)回答「这个模型此刻多忙」,插件在每次刷新目录时顺带问一次,把等级画成模型名旁边的一枚小人图标:排队人数 = 小人个数(正常 1 个黄人 / 轻微拥堵 2 个橙人 / 极度拥堵 3 个红人),**空闲**则是一枚**绿色小火箭**(没人排队 = 可以全速跑)——空闲不画成「绿色的 1 个人」,因为 12px 下「没人等」与「有 1 个人在等」必须一眼分开;**不可用**是一个划掉的小人。探针没回答的 `unknown` **一律不画**,**「没被告知」不会被当成「一切正常」**;颜色跟随主题,`title` 悬停文字(「极度拥堵」四个字)仍在。**等级不落盘**:目录是能活过重启的事实,负载只是那一分钟的事实,所以等级只盖在运行时目录上、随进程消失,探针失败只记一条 warn,绝不挡住目录刷新。国内版实测这条探针在 CLI 与 App 两种身份下答案逐字节相同,50 个 id 的上限也真实存在(62 个 → HTTP 400),19 个模型 186 ms 返回。**国际版没有这个徽标,因为国际网关根本没有这条接口** —— 实测 `www.workbuddy.ai` 对 `/v2/chat/busy` 回 **404**(`/v3/chat/busy`、`/v2/models/busy` 等同族路径也全是 404),而同一份凭据读它的 `/v3/config` 正常返回 28 个模型、**没有任何负载/排队字段**。这不是本插件没实现,也不是配置或权限问题:**上游国际版从未提供这个数据**。所以国际版只显示模型名与活动徽标,探针那条请求失败后安静退化成「没有徽标」,不会挡住模型列表,也不会用延迟或积分去猜一个「忙不忙」。
|
|
46
48
|
- **排队时说清楚你排在第几位** —— 上游用三个业务码表达排队(`6020` 等待、`6021` 队列满、`6022` 已达上限)。撞上它们时,插件不再给一句上游报错,而是「模型排队中:你排在第 8777 位,队列共 9021 个请求。请稍后重新发送这条消息,或换一个模型」。**排队不会触发换号**:队列跟随的是模型与平台级的队伍,换个账号拿到的还是同一个位次,所以这里既不走账号池也不额外烧请求(上游 `/v2/chat/queue/status` 回的 `message` 是 i18n key 而不是人话,插件因此从不转述它,只用自己的措辞)。**排队不会自动重发,也不会被自动重试**——插件没有排队轮询循环(它需要排队答案在 OpenAI 兼容通道上的真实信封,没测到就不写),并且明确告诉宿主不要重试(`x-should-retry: false`):一次排队在几千位之后,重试十几次也轮不到,只会把真实请求白白花掉。位次报出来,由你决定是重发还是换模型。
|
|
@@ -51,7 +53,7 @@
|
|
|
51
53
|
- **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
|
|
52
54
|
- **对话输入框积分读数(国内版 / 国际版自动切换)** —— 对话输入框左侧工具栏常驻一行可点击的读数(如 `WB CN · 1,072`),**按本会话当前选中的模型自动切换区域**:选中 WB CN 的模型就显示国内版余额,选中 WB AI 的模型就显示国际版余额;用其它 provider 的模型时该读数完全不出现。每 5 分钟自动刷新,也可点开手动刷新。点击读数弹出面板:**按账号列出的余额表格**,点某一行即可切换到该账号(与卡片上的账号选择是同一个设置);**当前不能用的账号会在行内标出原因**(如「被限流」「积分耗尽」),标注口径与账号池表格一致。面板里的余额只在打开时才去查,因此不会让 5 分钟的后台刷新按账号数翻倍。要关掉某一边,就在卡片对应区域的 tab 下取消勾选「在输入框工具栏显示积分读数」;该区域的供应商被关闭时,读数不显示、开关也置灰。同一个开关下面还有「显示位置」这一行:默认在输入框工具栏(与模型选择同一行),改成「输入框下方」后读数落到轮次/步数/token 统计那一行——装了别的插件把工具栏座位占掉时用得上。
|
|
53
55
|
|
|
54
|
-
<img src="docs/assets/dsh-connect-workbuddy-composer-credits.png" width="360" alt="
|
|
56
|
+
<img src="docs/assets/dsh-connect-workbuddy-composer-credits.png" width="360" alt="对话输入框积分面板:国内版账号余额表格、一键测试按钮与最近刷新时间(账号名已打码)" />
|
|
55
57
|
- **账号池:多账号批量操作与自动换号** —— 把要用的账号勾进池子,即可**签到勾选的账号**、**测试勾选的账号 × 你指定的模型**(默认挑该区域倍率为 0 的免费模型);开启**启用账号池**即同时打开**自动换号**——某个请求在上游失败时,插件会依次用池内其他可用账号重试,直到有一个接手、或全部不可用为止,然后才把错误报给你。成员**默认一个都不含**、必须显式勾选(「本机有登录」不等于「要拿它去签到和花积分」);**开启后由池子的排序决定每一次请求用谁**(可用优先,其次快过期先花、积分高、凭据更新的);失败时依次用池内其他可用账号重试。排序**从不写回你保存的选择**——关掉开关,你选定的账号立刻完全生效。只有请求本身不合法(HTTP 400)不换号:同一个请求体换谁都是 400,重试只会白等。**这个开关只管「插件要不要自己决定」**:关掉它,插件不再替你选账号、也不再替你换号,但**签到与测试仍然可用**(它们是手动动作)。测试走的是与模型行同一套真实体积探测,因此**有真实成本**;**不提供自动签到**——签到会真实领取奖励,只在你点按钮时执行。详见[账号池](#账号池)。
|
|
56
58
|
- **福利活动:待领取标记 + 单账号「领取」按钮** —— 卡片会逐账号标出上游活动(补偿 / 赠礼)里**还没领的那一份**,数据来自上游自己的两个状态接口,标记阶段**只读**、两个写接口一次都不调用。显示规则完全由上游状态决定:`active && !claimed` 才显示,`claimed` 整行消失,活动过期(`active` 翻 `false`)自动消失——**不用本地时钟去比 `end_time`**,本机时间偏了也不会与服务端打架;`end_time` 只用于 3 天内临期高亮。`active` 单独看不行:已领取的账号 `active` 仍为 `true`,只看它会让领过的账号永远挂着待领取。读取复用签到的管线(每账号一次、30 分钟缓存、账号池关闭时不发请求),任一接口失败按「未读」处理,不会把一次失败缓存成「全都没福利」。行尾是一个实心琥珀色的**胶囊按钮「领取」**,一次只领**一个账号的一个活动**:没有「全部领取」,因为一次点击花掉的是一份不可撤销的真实权益,写路由也**强制要求 `accountId`**,请求本身无法批量领取。领取前**重新读取**状态再判定,已领取或已过期一律 409 且不调用上游;领取后用上游的**新读数**作答并刷新卡片,不做乐观更新——即使上游静默失败也不会留下「已领取」的假象,成功后同时清掉该账号的福利缓存,按钮不会顶着旧状态赖着不走。
|
|
57
59
|
- **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
|
|
@@ -85,7 +87,7 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
85
87
|
## 账号池
|
|
86
88
|
|
|
87
89
|
<p align="center">
|
|
88
|
-
<img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy
|
|
90
|
+
<img src="docs/assets/dsh-connect-workbuddy-account-pool.png" width="900" alt="dsh-connect-workbuddy 账号池:签到勾选的账号、测试勾选的账号、成员排序与签到状态(账号名已打码)" />
|
|
89
91
|
</p>
|
|
90
92
|
|
|
91
93
|
一台机器上往往登录过多个 WorkBuddy 账号。**账号池**把这件事收成一次点击:把要用的账号勾进池子,然后「签到勾选的账号」或「测试勾选的账号」;再打开**启用账号池**,插件就会在请求失败时自动换号接着跑。
|
|
@@ -489,7 +491,7 @@ else { await envelope.response.text() /* 或者按 SSE 消费 */ }
|
|
|
489
491
|
- **这是宿主与 pi-ai 之间的一处错配,不是本插件的 bug**:不跨 provider/模型重放 thinking 这件事本身是对的(别家模型的思考过程对当前模型没有上下文价值),错在把 `provider` 当作「是不是同一个模型」的必要条件,而这里两个 provider 字符串指的是**同一个模型的两个接入区域**。
|
|
490
492
|
- **本插件已在 4.9.0 起从插件侧绕开它**:两条路由注册的 **model 身份统一为 `workbuddy`**(`src/adapter.ts` 的 `WORKBUDDY_MODEL_IDENTITY`),也就是模型自己声明的 `provider` 字段两边相同,于是 `isSameModel` 成立、换区不再把 thinking 降级成正文。**路由 id 没有合并**——它仍是 `workbuddy` / `workbuddy-global`,各自指向自己的 shim、目录与凭据,所以一侧故障不会带走另一侧,模型选择器也仍是两组。**4.9.0/4.9.1 的这版修复漏了一半,国际版整区不可用(issue #75)**:上面的说法只对 `getModel(provider, id)` 这一条按**参数**查表的路径成立;真正跑聊天的 `models.streamSimple(model, …)` 第一步是 `requireProvider(model)` = `providers.get(model.provider)`(`models.js:351–355`),查的是**模型上盖的那个字段**。于是国内版(路由 `workbuddy`、盖 `workbuddy`)自洽,国际版(路由 `workbuddy-global`、盖 `workbuddy`)在只注册了 `workbuddy-global` 的注册表里查不到,**每一个请求都在发出前就抛 `Unknown provider: workbuddy`**——一次也会失败,且报错里不带任何 WorkBuddy 字样,看起来像宿主坏了。**4.9.2 起改为:每条路由把自己的注册表同时登记上「路由 id」和「共享身份」两个键**(`src/adapter.ts` 的 `modelIdentityKeysOf`),模型上盖的身份仍然两边相同(#59 不回退),而 `requireProvider` 在国际版自己的注册表里也能查到、并拿到**国际版自己的** provider 对象(含它自己的 shim 地址与凭据)。pi-ai 的注册表本来就是 `Map`(`models.js:37` 的 `providers.set(provider.id, provider)`,键取的是 **provider 对象的 `id`**,不是 profiles 映射的键),所以这是注册表原生支持的事;**路由集合、模型选择器、目录与凭据都没有变化**,多出来的那个键不出现在任何 UI 里。回归测试 `tests/issue75-global-provider-dispatch.spec.ts` 走的是宿主的真实构造(`createModels` + `setProvider` + `streamSimple`),并保留一个反证用例固定住旧的失败文案。详见 `docs/audit/ISSUE59-OPTIONS.md` 的「#75 CORRECTION」一节。
|
|
491
493
|
- 升级后仍可能看到计数变化的部分:**在同一区域内换模型**仍会降级(`model` 那条腿是真的换了模型,thinking 签名未必对新模型有效,保留才是错的);**升级之前就录在历史里的消息**带着旧的按区域 id,重放时仍按旧规则降级——这种历史只会越聊越少,不会反过来影响新录的消息。
|
|
492
|
-
- 真正的修法仍在上游(让 `isSameModel` 认「provider 等价组」声明,而不是让插件去伪装自己的身份);插件侧的兜底是 4.8.0 起的 `src/context-rescue.ts`——**一旦上游确认超限,就按块折叠历史后重发**,而不是把宿主要压缩的整段历史原样重放一遍(原样重放正是「压缩请求自己超限 → 压缩死锁」的成因)。想亲眼看到这件事发生在哪条消息上:设 `WORKBUDDY_BODY_REPORT` 抓一次 body
|
|
494
|
+
- 真正的修法仍在上游(让 `isSameModel` 认「provider 等价组」声明,而不是让插件去伪装自己的身份);插件侧的兜底是 4.8.0 起的 `src/context-rescue.ts`——**一旦上游确认超限,就按块折叠历史后重发**,而不是把宿主要压缩的整段历史原样重放一遍(原样重放正是「压缩请求自己超限 → 压缩死锁」的成因)。想亲眼看到这件事发生在哪条消息上:设 `WORKBUDDY_BODY_REPORT` 抓一次 body 组成报告,它只记计数、不记正文。**注意它会持续追加**:每个区一份 `<region>.jsonl`,超过 32 MB 自动滚成 `.1`(只保留一份旧档);排查完请关闭该环境变量并删除报告文件,不要开着它长期跑。
|
|
493
495
|
|
|
494
496
|
## 免责声明
|
|
495
497
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthCandidates, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, Mt as hostPeersInfo, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, vt as WORKBUDDY_APP_EXECUTABLE_ENV, w as defaultDesktopAuthDirs, wt as findWorkbuddyAppExecutableWithSource, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-
|
|
2
|
+
import { C as defaultDesktopAuthCandidates, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, Mt as hostPeersInfo, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, vt as WORKBUDDY_APP_EXECUTABLE_ENV, w as defaultDesktopAuthDirs, wt as findWorkbuddyAppExecutableWithSource, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-DqTNlE6H.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
|
@@ -2051,6 +2051,43 @@ window.__ModuleLoader__.load({
|
|
|
2051
2051
|
* Only ONE badge is drawn per model: a model row is one line wide, and two
|
|
2052
2052
|
* badges would push the model name and the image/think checkboxes out of it.
|
|
2053
2053
|
*/
|
|
2054
|
+
/**
|
|
2055
|
+
* The multiplier to DRAW for a model right now: the promotion's discounted
|
|
2056
|
+
* price when a discount is running, otherwise the catalog's own figure.
|
|
2057
|
+
*
|
|
2058
|
+
* WHY THIS IS SEPARATE FROM THE BADGE. The badge answers "is this model on
|
|
2059
|
+
* offer"; this answers "at what price". They usually agree, but not always —
|
|
2060
|
+
* and when they disagreed the user saw a "night discount" badge next to the
|
|
2061
|
+
* full price (issue #120). The discount lives in the SAME entry that carries
|
|
2062
|
+
* the badge, so the two can be answered from one document and one clock.
|
|
2063
|
+
*
|
|
2064
|
+
* TWO RULES, both about not overstating an offer:
|
|
2065
|
+
*
|
|
2066
|
+
* 1. Only a promotion that is RUNNING at `nowMs` discounts the price. A
|
|
2067
|
+
* standing badge (the daytime half of a night-discount pair) is drawn
|
|
2068
|
+
* outside the discount hours, and it must NOT discount them — that is
|
|
2069
|
+
* exactly the bug this function exists to prevent. The badge admits
|
|
2070
|
+
* standing entries through `selectPromoBadge`; the price does not.
|
|
2071
|
+
* 2. `displayMode` must say `'replace'`, and the value must parse. An entry
|
|
2072
|
+
* with a badge but no readable discount changes nothing, so the catalog's
|
|
2073
|
+
* figure stands.
|
|
2074
|
+
*
|
|
2075
|
+
* The result is `undefined` when the catalog figure should be used unchanged —
|
|
2076
|
+
* the caller then keeps whatever it already draws. Returning the base value
|
|
2077
|
+
* from here would make a caller that has no base figure (a model whose
|
|
2078
|
+
* `credits` was absent) invent one.
|
|
2079
|
+
*/
|
|
2080
|
+
function selectPromoMultiplier(promos, nowMs) {
|
|
2081
|
+
if (promos === void 0 || promos.length === 0) return void 0;
|
|
2082
|
+
const sorted = [...promos].sort((a, b) => b.priority - a.priority || (a.source === b.source ? 0 : a.source === "promotion" ? -1 : 1));
|
|
2083
|
+
for (const promo of sorted) {
|
|
2084
|
+
const discount = promo.discount;
|
|
2085
|
+
if (discount === void 0 || discount.multiplier === void 0) continue;
|
|
2086
|
+
if (discount.displayMode !== "replace") continue;
|
|
2087
|
+
if (!promoActiveAt(promo, nowMs)) continue;
|
|
2088
|
+
return discount.multiplier;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2054
2091
|
function selectPromoBadge(promos, nowMs) {
|
|
2055
2092
|
if (promos === void 0 || promos.length === 0) return void 0;
|
|
2056
2093
|
const sorted = [...promos].sort((a, b) => b.priority - a.priority || (a.source === b.source ? 0 : a.source === "promotion" ? -1 : 1));
|
|
@@ -2696,6 +2733,7 @@ window.__ModuleLoader__.load({
|
|
|
2696
2733
|
credit: String(row.credit ?? 0)
|
|
2697
2734
|
}), "ok");
|
|
2698
2735
|
else if (row.status === "already") appendLog(t("row.poolLogCheckinAlready", { accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName }), "info");
|
|
2736
|
+
else if (row.status === "skipped") appendLog(t("row.poolLogCheckinSkipped", { accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName }), "info");
|
|
2699
2737
|
else appendLog(t("row.poolLogCheckinFailed", {
|
|
2700
2738
|
accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName,
|
|
2701
2739
|
message: row.message ?? t("row.requestFailed")
|
|
@@ -5733,6 +5771,18 @@ window.__ModuleLoader__.load({
|
|
|
5733
5771
|
children: promoBadgeLabel(promo, language)
|
|
5734
5772
|
});
|
|
5735
5773
|
}
|
|
5774
|
+
/**
|
|
5775
|
+
* The multiplier to DRAW for a model right now (issue #120).
|
|
5776
|
+
*
|
|
5777
|
+
* A "night discount" badge used to sit next to the full price: the badge was
|
|
5778
|
+
* resolved against the clock while the number beside it was the catalog's
|
|
5779
|
+
* static figure. The discount lives in the same upstream entry, so both are
|
|
5780
|
+
* answered against the same clock — a promotion that is running and carries a
|
|
5781
|
+
* readable `replace` discount wins; otherwise the catalog figure stands.
|
|
5782
|
+
*/
|
|
5783
|
+
function effectiveRate(model) {
|
|
5784
|
+
return selectPromoMultiplier(model.promos, Date.now()) ?? model.creditMultiplier;
|
|
5785
|
+
}
|
|
5736
5786
|
/** Inject or refresh the shared card CSS for the current client bundle. */
|
|
5737
5787
|
if (typeof document !== "undefined") {
|
|
5738
5788
|
const cssId = "dsh-connect-workbuddy/client.css";
|
|
@@ -7397,9 +7447,9 @@ window.__ModuleLoader__.load({
|
|
|
7397
7447
|
children: [
|
|
7398
7448
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7399
7449
|
className: "dsm-workbuddy-model-name",
|
|
7400
|
-
children: [model.name, model
|
|
7450
|
+
children: [model.name, effectiveRate(model) === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7401
7451
|
className: "dsm-workbuddy-model-name-rate",
|
|
7402
|
-
children: ["x", model.
|
|
7452
|
+
children: ["x", effectiveRate(model).toFixed(2)]
|
|
7403
7453
|
})]
|
|
7404
7454
|
}),
|
|
7405
7455
|
promoChip(model),
|
|
@@ -8680,10 +8730,15 @@ window.__ModuleLoader__.load({
|
|
|
8680
8730
|
if (!response.ok) return void 0;
|
|
8681
8731
|
const body = await response.json();
|
|
8682
8732
|
const promos = {};
|
|
8683
|
-
|
|
8733
|
+
const rates = {};
|
|
8734
|
+
for (const model of body.models ?? []) {
|
|
8735
|
+
if (model.creditMultiplier !== void 0) rates[model.id] = model.creditMultiplier;
|
|
8736
|
+
if (model.promos !== void 0) promos[model.id] = model.promos;
|
|
8737
|
+
}
|
|
8684
8738
|
const snapshot = {
|
|
8685
8739
|
levels: body.busyLevels ?? {},
|
|
8686
|
-
promos
|
|
8740
|
+
promos,
|
|
8741
|
+
rates
|
|
8687
8742
|
};
|
|
8688
8743
|
caches.set(region, {
|
|
8689
8744
|
snapshot,
|
|
@@ -8785,10 +8840,16 @@ window.__ModuleLoader__.load({
|
|
|
8785
8840
|
(0, react.useEffect)(() => subscribeMinute(() => setTick((count) => count + 1)), []);
|
|
8786
8841
|
const level = snapshot?.levels[modelId];
|
|
8787
8842
|
const promo = snapshot === void 0 ? void 0 : selectPromoBadge(snapshot.promos[modelId], Date.now());
|
|
8843
|
+
const baseRate = snapshot?.rates[modelId];
|
|
8844
|
+
const rate = (snapshot === void 0 ? void 0 : selectPromoMultiplier(snapshot.promos[modelId], Date.now())) ?? baseRate;
|
|
8788
8845
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8789
8846
|
className: "dsm-workbuddy-model-label",
|
|
8790
8847
|
children: [
|
|
8791
8848
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }),
|
|
8849
|
+
rate === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8850
|
+
className: "dsm-workbuddy-model-label-rate",
|
|
8851
|
+
children: [" · x", rate.toFixed(2)]
|
|
8852
|
+
}),
|
|
8792
8853
|
promo === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PromoBadge, {
|
|
8793
8854
|
promo,
|
|
8794
8855
|
compact
|
|
@@ -9148,7 +9209,7 @@ window.__ModuleLoader__.load({
|
|
|
9148
9209
|
"row.tabCn": "Domestic",
|
|
9149
9210
|
"row.tabGlobal": "Global",
|
|
9150
9211
|
"row.tabHint": "Each tab is a separate provider (workbuddy / workbuddy-global) with its own account, credits, and models. Both sides are live at once: different sessions can pick from either side, and changes on one tab never touch the other.",
|
|
9151
|
-
"row.tabFreeQuotaWarn": "Free models
|
|
9212
|
+
"row.tabFreeQuotaWarn": "Free models are not unlimited — the upstream limits them by request size and load.",
|
|
9152
9213
|
"row.tabSwitchHint": "Uncheck to switch this provider off. Its models disappear from the DSH model picker; your account, credits, and model settings are kept and return when you check it again.",
|
|
9153
9214
|
"row.tabSwitchAria": "Offer the {region} provider to DSH",
|
|
9154
9215
|
"row.tabOffNotice": "This provider is switched off: none of its models are offered to DSH. Your account, credits, and model settings are kept — check the box above the tab to bring it back.",
|
|
@@ -9388,7 +9449,7 @@ window.__ModuleLoader__.load({
|
|
|
9388
9449
|
"row.poolSettingsTitle": "More settings",
|
|
9389
9450
|
"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.",
|
|
9390
9451
|
"row.poolEnabled": "Enable the account pool",
|
|
9391
|
-
"row.poolEnabledHint": "On: the pool picks the account for every request and moves to the next
|
|
9452
|
+
"row.poolEnabledHint": "On: the pool picks the account for every request, and only a failover-worthy failure moves to the next account — a malformed request, a busy model, a platform queue, or a channel-gate refusal does not (switching accounts would only burn quota for the same refusal). Off: the account you select serves every request, and a failure is reported as it is.",
|
|
9392
9453
|
"row.poolModeAuto": "picking automatically",
|
|
9393
9454
|
"row.poolModeManual": "you pick",
|
|
9394
9455
|
"row.poolManualAccountLabel": "Account in use",
|
|
@@ -9413,6 +9474,7 @@ window.__ModuleLoader__.load({
|
|
|
9413
9474
|
"row.poolLogCheckinStart": "Checking in {count} account(s), one at a time",
|
|
9414
9475
|
"row.poolLogCheckinClaimed": "{accountName}: checked in (+{credit})",
|
|
9415
9476
|
"row.poolLogCheckinAlready": "{accountName}: already checked in today — skipped",
|
|
9477
|
+
"row.poolLogCheckinSkipped": "{accountName}: enterprise account, no daily check-in",
|
|
9416
9478
|
"row.poolLogCheckinFailed": "{accountName}: check-in failed — {message}",
|
|
9417
9479
|
"row.poolLogCheckinDone": "Check-in finished",
|
|
9418
9480
|
"row.poolLogBatchFailed": "Batch failed — nothing ran: {message}",
|
|
@@ -9468,12 +9530,12 @@ window.__ModuleLoader__.load({
|
|
|
9468
9530
|
"row.poolRowRemoving": "Removing the account…",
|
|
9469
9531
|
"row.poolRowRemoveDesktopOnly": "{accountName}: hidden from the list — this account comes from the desktop app's own sign-in, which is left untouched. 「Detect accounts again」 re-adds it.",
|
|
9470
9532
|
"row.cliBridgeLabel": "Let the local CodeBuddy CLI do the work",
|
|
9471
|
-
"row.cliBridgeHint": "Adds the codebuddy_ask tool and the /codebuddy command. Each call spends CodeBuddy quota
|
|
9533
|
+
"row.cliBridgeHint": "Adds the codebuddy_ask tool and the /codebuddy command. Each call spends CodeBuddy quota. injectResult only affects /codebuddy — its result is injected back into the session, costing one DSH turn when the session is idle; tool calls never read it. Rate-limited: 6 turns per session per hour, 20 per process per hour, one turn at a time. Off by default.",
|
|
9472
9534
|
"row.imageToolLabel": "Let the model draw images",
|
|
9473
9535
|
"row.imageToolHint": "Adds the workbuddy_draw_image tool, which the MODEL may call on its own — about 5.71 credits per image, with no per-call confirmation. Off means the tool is not registered at all.",
|
|
9474
9536
|
"row.showCreditsCn": "Show credits in the composer toolbar",
|
|
9475
9537
|
"row.showCreditsGlobal": "Show credits in the composer toolbar",
|
|
9476
|
-
"row.showCreditsCnHint": "Show the domestic (WB CN) credit balance in the composer toolbar, but only while this session uses a WB CN model. Click it for per-account balances and a
|
|
9538
|
+
"row.showCreditsCnHint": "Show the domestic (WB CN) credit balance in the composer toolbar, but only while this session uses a WB CN model. Click it for per-account balances and a one-click test of the checked accounts (the test spends real credits). The readout refreshes by itself every 5 minutes; the per-account table every 60 seconds while it is open.",
|
|
9477
9539
|
"row.creditPositionLabel": "Position",
|
|
9478
9540
|
"row.creditPositionComposer": "composer toolbar",
|
|
9479
9541
|
"row.creditPositionStats": "below the input box",
|
|
@@ -9484,7 +9546,7 @@ window.__ModuleLoader__.load({
|
|
|
9484
9546
|
"quotaNotice.fallbackHead": "The quota for this request is exhausted",
|
|
9485
9547
|
"quotaNotice.supplement": " (some models also have a daily limit that is unrelated to your credits)",
|
|
9486
9548
|
"row.creditPositionHint": "Where the credit readout sits. \"composer toolbar\" is the default (the row with the model picker). Choose \"below the input box\" — the row carrying the round / step / token statistics — if another plugin occupies the composer toolbar and the readout does not appear.",
|
|
9487
|
-
"row.showCreditsGlobalHint": "Show the international (WB AI) credit balance in the composer toolbar, but only while this session uses a WB AI model. Click it for per-account balances and a
|
|
9549
|
+
"row.showCreditsGlobalHint": "Show the international (WB AI) credit balance in the composer toolbar, but only while this session uses a WB AI model. Click it for per-account balances and a one-click test of the checked accounts (the test spends real credits). The readout refreshes by itself every 5 minutes; the per-account table every 60 seconds while it is open.",
|
|
9488
9550
|
"composer.pointsCn": "WB CN",
|
|
9489
9551
|
"composer.pointsGlobal": "WB AI",
|
|
9490
9552
|
"composer.panelTitleCn": "WB CN credits",
|
|
@@ -9526,7 +9588,7 @@ window.__ModuleLoader__.load({
|
|
|
9526
9588
|
"row.tabCn": "国内版",
|
|
9527
9589
|
"row.tabGlobal": "国际版",
|
|
9528
9590
|
"row.tabHint": "每个 tab 是一个独立供应商(workbuddy / workbuddy-global),各有自己的账号、积分与模型。两边同时生效:不同会话可各选一边,一侧的改动不影响另一侧。",
|
|
9529
|
-
"row.tabFreeQuotaWarn": "
|
|
9591
|
+
"row.tabFreeQuotaWarn": "免费模型并非无限量:上游按单次请求体积与负载限流。",
|
|
9530
9592
|
"row.tabSwitchHint": "取消勾选即关闭该供应商:它的模型不再出现在 DSH 模型选择器里;账号、积分与模型设置都会保留,重新勾选即恢复。",
|
|
9531
9593
|
"row.tabSwitchAria": "是否向 DSH 提供{region}供应商",
|
|
9532
9594
|
"row.tabOffNotice": "该供应商已关闭:它的模型不会提供给 DSH。账号、积分与模型设置均已保留——勾选上方复选框即可恢复。",
|
|
@@ -9766,7 +9828,7 @@ window.__ModuleLoader__.load({
|
|
|
9766
9828
|
"row.poolSettingsTitle": "更多设置",
|
|
9767
9829
|
"row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
|
|
9768
9830
|
"row.poolEnabled": "启用账号池",
|
|
9769
|
-
"row.poolEnabledHint": "
|
|
9831
|
+
"row.poolEnabledHint": "开启:由账号池为每次请求挑号;只有可换号的失败才换下一个——请求本身不合法、模型繁忙、平台排队、渠道校验这四类不换(换号只会为同一个拒绝白烧一次配额)。关闭:你选的账号服务每一次请求,失败如实报错。",
|
|
9770
9832
|
"row.poolModeAuto": "自动挑号",
|
|
9771
9833
|
"row.poolModeManual": "手动选号",
|
|
9772
9834
|
"row.poolManualAccountLabel": "使用哪个账号",
|
|
@@ -9791,6 +9853,7 @@ window.__ModuleLoader__.load({
|
|
|
9791
9853
|
"row.poolLogCheckinStart": "开始签到 {count} 个账号(逐个执行)",
|
|
9792
9854
|
"row.poolLogCheckinClaimed": "{accountName}:签到成功 +{credit}",
|
|
9793
9855
|
"row.poolLogCheckinAlready": "{accountName}:今日已签到,跳过",
|
|
9856
|
+
"row.poolLogCheckinSkipped": "{accountName}:企业账号无每日签到",
|
|
9794
9857
|
"row.poolLogCheckinFailed": "{accountName}:签到失败 —— {message}",
|
|
9795
9858
|
"row.poolLogCheckinDone": "签到完成",
|
|
9796
9859
|
"row.poolLogBatchFailed": "批量操作失败 —— 没有执行:{message}",
|
|
@@ -9846,12 +9909,12 @@ window.__ModuleLoader__.load({
|
|
|
9846
9909
|
"row.poolRowRemoving": "正在删除账号…",
|
|
9847
9910
|
"row.poolRowRemoveDesktopOnly": "{accountName}:已从列表中隐藏——这个账号来自桌面 App 自己的登录,该登录未被改动。点「重新检测账号」会重新加入。",
|
|
9848
9911
|
"row.cliBridgeLabel": "允许调用本机 CodeBuddy CLI 干活",
|
|
9849
|
-
"row.cliBridgeHint": "开启后多出 codebuddy_ask 工具与 /codebuddy 指令;每次调用花 CodeBuddy
|
|
9912
|
+
"row.cliBridgeHint": "开启后多出 codebuddy_ask 工具与 /codebuddy 指令;每次调用花 CodeBuddy 配额。injectResult 只影响 /codebuddy:其结果回注会话,会话空闲时多花一次 DSH 回合;工具调用不读它。限流:每会话每小时 6 回合、每进程每小时 20 回合、同时只跑一个回合。默认关闭。",
|
|
9850
9913
|
"row.imageToolLabel": "允许模型自主生图",
|
|
9851
9914
|
"row.imageToolHint": "开启后模型可以自己调用 workbuddy_draw_image 画图——约 5.71 积分/张,没有逐次确认。关闭时该工具根本不会注册。",
|
|
9852
9915
|
"row.showCreditsCn": "在输入框工具栏显示积分读数",
|
|
9853
9916
|
"row.showCreditsGlobal": "在输入框工具栏显示积分读数",
|
|
9854
|
-
"row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN
|
|
9917
|
+
"row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并一键测试勾选的账号(测试会真实消耗积分)。读数每 5 分钟自动刷新;各账号明细在面板打开时每 60 秒刷新。",
|
|
9855
9918
|
"row.creditPositionLabel": "显示位置",
|
|
9856
9919
|
"row.creditPositionComposer": "输入框工具栏",
|
|
9857
9920
|
"row.creditPositionStats": "输入框下方",
|
|
@@ -9862,7 +9925,7 @@ window.__ModuleLoader__.load({
|
|
|
9862
9925
|
"quotaNotice.fallbackHead": "当前请求的额度已用尽",
|
|
9863
9926
|
"quotaNotice.supplement": "(部分模型另有每日额度,与积分无关)",
|
|
9864
9927
|
"row.creditPositionHint": "积分显示在哪里。「输入框工具栏」是默认位置(与模型选择同一行)。如果装了别的插件后读数不显示,改成「输入框下方」—— 那是「轮次 / 步数 / token」统计所在的那一行。",
|
|
9865
|
-
"row.showCreditsGlobalHint": "在对话输入框工具栏显示国际版(WB AI)剩余积分,仅当本会话使用 WB AI
|
|
9928
|
+
"row.showCreditsGlobalHint": "在对话输入框工具栏显示国际版(WB AI)剩余积分,仅当本会话使用 WB AI 模型时出现。点击可看各账号余额并一键测试勾选的账号(测试会真实消耗积分)。读数每 5 分钟自动刷新;各账号明细在面板打开时每 60 秒刷新。",
|
|
9866
9929
|
"composer.pointsCn": "WB CN",
|
|
9867
9930
|
"composer.pointsGlobal": "WB AI",
|
|
9868
9931
|
"composer.panelTitleCn": "WB CN 积分",
|
|
@@ -1546,6 +1546,37 @@ function parseDaily(value) {
|
|
|
1546
1546
|
}
|
|
1547
1547
|
return windows.length === 0 ? void 0 : windows;
|
|
1548
1548
|
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Read a multiplier out of the upstream's `discountedCredits` string.
|
|
1551
|
+
*
|
|
1552
|
+
* The upstream writes the unit INTO the value ("0x", "0.06x", "0.06"), so the
|
|
1553
|
+
* trailing `x` is stripped rather than handed to `Number()`. A value that does
|
|
1554
|
+
* not become a finite non-negative number is refused — a wrong price shown to
|
|
1555
|
+
* the user is worse than the normal price, because the normal price is at least
|
|
1556
|
+
* true.
|
|
1557
|
+
*/
|
|
1558
|
+
function multiplierOfCredits(value) {
|
|
1559
|
+
const raw = text(value);
|
|
1560
|
+
if (raw === void 0) return void 0;
|
|
1561
|
+
const stripped = raw.replace(/x$/i, "").trim();
|
|
1562
|
+
if (stripped === "") return void 0;
|
|
1563
|
+
const parsed = Number(stripped);
|
|
1564
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
1565
|
+
}
|
|
1566
|
+
/** The upstream's `discount` → this plugin's discount shape. */
|
|
1567
|
+
function parseDiscount(value) {
|
|
1568
|
+
const raw = record(value);
|
|
1569
|
+
if (raw === void 0) return void 0;
|
|
1570
|
+
const fromString = multiplierOfCredits(raw["discountedCredits"]);
|
|
1571
|
+
const fromFactor = typeof raw["factor"] === "number" && Number.isFinite(raw["factor"]) && raw["factor"] >= 0 ? raw["factor"] : void 0;
|
|
1572
|
+
const multiplier = fromString ?? fromFactor;
|
|
1573
|
+
const displayMode = raw["displayMode"] === "replace" ? "replace" : void 0;
|
|
1574
|
+
if (multiplier === void 0 && displayMode === void 0) return void 0;
|
|
1575
|
+
return {
|
|
1576
|
+
...multiplier === void 0 ? {} : { multiplier },
|
|
1577
|
+
...displayMode === void 0 ? {} : { displayMode }
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1549
1580
|
/** The upstream's `schedule` → this plugin's schedule shape. */
|
|
1550
1581
|
function parseSchedule(value) {
|
|
1551
1582
|
const raw = record(value);
|
|
@@ -1575,6 +1606,7 @@ function parsePromo(value, source) {
|
|
|
1575
1606
|
if (badge === void 0) return void 0;
|
|
1576
1607
|
const hover = parseHover(raw["hover"]);
|
|
1577
1608
|
const schedule = parseSchedule(raw["schedule"]);
|
|
1609
|
+
const discount = parseDiscount(raw["discount"]);
|
|
1578
1610
|
const priority = typeof raw["priority"] === "number" && Number.isFinite(raw["priority"]) ? raw["priority"] : 0;
|
|
1579
1611
|
return {
|
|
1580
1612
|
id: text(raw["id"]) ?? `${source}:${modelIds.join(",")}`,
|
|
@@ -1583,6 +1615,7 @@ function parsePromo(value, source) {
|
|
|
1583
1615
|
badge,
|
|
1584
1616
|
...hover === void 0 ? {} : { hover },
|
|
1585
1617
|
...schedule === void 0 ? {} : { schedule },
|
|
1618
|
+
...discount === void 0 ? {} : { discount },
|
|
1586
1619
|
priority
|
|
1587
1620
|
};
|
|
1588
1621
|
}
|
|
@@ -3205,6 +3238,24 @@ async function readEnvelope(response) {
|
|
|
3205
3238
|
};
|
|
3206
3239
|
}
|
|
3207
3240
|
/**
|
|
3241
|
+
* Parse the enterprise endpoint's `cycleResetTime` ("2026-11-01 00:00:00").
|
|
3242
|
+
*
|
|
3243
|
+
* The value carries NO timezone marker, and the official client renders it as
|
|
3244
|
+
* local wall-clock time (it is what the user sees in "resets on 11/01 00:00"),
|
|
3245
|
+
* so it is read as LOCAL time rather than as UTC. `Date.parse` on that shape is
|
|
3246
|
+
* implementation-defined, which is exactly why it is assembled by hand.
|
|
3247
|
+
*/
|
|
3248
|
+
function parseEnterpriseInstant(value) {
|
|
3249
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value.trim());
|
|
3250
|
+
if (match === null) {
|
|
3251
|
+
const fallback = Date.parse(value);
|
|
3252
|
+
return Number.isNaN(fallback) ? void 0 : fallback;
|
|
3253
|
+
}
|
|
3254
|
+
const [, y, mo, d, h, mi, se] = match;
|
|
3255
|
+
const parsed = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(se ?? "0")).getTime();
|
|
3256
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3208
3259
|
* Fail an envelope whose business code is non-zero, classified like HTTP errors.
|
|
3209
3260
|
*
|
|
3210
3261
|
* A JSON body on a 401/403 is still a credential refusal: the edge answered in
|
|
@@ -3871,12 +3922,65 @@ var WorkBuddyUpstreamClient = class {
|
|
|
3871
3922
|
};
|
|
3872
3923
|
}
|
|
3873
3924
|
/**
|
|
3925
|
+
* The ENTERPRISE credit table (issue #124).
|
|
3926
|
+
*
|
|
3927
|
+
* `/v2/billing/meter/get-user-resource` answers an enterprise member with
|
|
3928
|
+
* HTTP 200 and an empty `Accounts` list — a legally-shaped "you have no
|
|
3929
|
+
* credits" that is simply false, because the member's credits live under the
|
|
3930
|
+
* enterprise account. The official client branches on the account's
|
|
3931
|
+
* `type: "ultimate"` and calls `/billing/meter/get-enterprise-user-usage`
|
|
3932
|
+
* instead (no `/v2` prefix), which returns:
|
|
3933
|
+
*
|
|
3934
|
+
* { credit: 1234, limitNum: 5000, cycleResetTime: "2026-11-01 00:00:00" }
|
|
3935
|
+
*
|
|
3936
|
+
* What REMAINS is `limitNum - credit`. `credit` there is the amount SPENT,
|
|
3937
|
+
* not the amount left — reading it as a balance inverts the answer, which is
|
|
3938
|
+
* worse than the zero it replaces.
|
|
3939
|
+
*
|
|
3940
|
+
* @returns the credits, or `undefined` when this response carried no
|
|
3941
|
+
* readable balance (the caller turns that into an error rather than
|
|
3942
|
+
* reporting zero).
|
|
3943
|
+
*/
|
|
3944
|
+
async fetchEnterpriseCredits(credential) {
|
|
3945
|
+
const response = await fetch(`${billingBase(credential)}/billing/meter/get-enterprise-user-usage`, {
|
|
3946
|
+
method: "POST",
|
|
3947
|
+
headers: billingHeaders(credential),
|
|
3948
|
+
body: JSON.stringify({}),
|
|
3949
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
3950
|
+
});
|
|
3951
|
+
const envelope = await readEnvelope(response);
|
|
3952
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
3953
|
+
const wrapper = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
3954
|
+
const inner = typeof wrapper["Data"] === "object" && wrapper["Data"] !== null ? wrapper["Data"] : wrapper;
|
|
3955
|
+
const limit = typeof inner["limitNum"] === "number" && Number.isFinite(inner["limitNum"]) ? inner["limitNum"] : void 0;
|
|
3956
|
+
const used = typeof inner["credit"] === "number" && Number.isFinite(inner["credit"]) ? inner["credit"] : void 0;
|
|
3957
|
+
if (limit === void 0 || used === void 0) return void 0;
|
|
3958
|
+
const remain = Math.max(0, limit - used);
|
|
3959
|
+
const refreshAtMs = typeof inner["cycleResetTime"] === "string" ? parseEnterpriseInstant(inner["cycleResetTime"]) : void 0;
|
|
3960
|
+
return {
|
|
3961
|
+
total: remain,
|
|
3962
|
+
packages: [{
|
|
3963
|
+
packageName: "企业积分",
|
|
3964
|
+
remain,
|
|
3965
|
+
size: limit,
|
|
3966
|
+
monthly: true,
|
|
3967
|
+
...refreshAtMs === void 0 ? {} : { refreshAtMs }
|
|
3968
|
+
}],
|
|
3969
|
+
expiringSoon: 0
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
/**
|
|
3874
3973
|
* POST the billing endpoint for the remaining credit, keeping every package
|
|
3875
3974
|
* separate: the card groups monthly-cycle packages itself and lists the
|
|
3876
3975
|
* nearest-expiring one-off packages, so aggregation here would lose the
|
|
3877
3976
|
* dates it needs.
|
|
3878
3977
|
*/
|
|
3879
3978
|
async fetchCredits(credential) {
|
|
3979
|
+
if (credential.enterpriseId !== void 0 && credential.enterpriseId !== "") {
|
|
3980
|
+
const enterprise = await this.fetchEnterpriseCredits(credential);
|
|
3981
|
+
if (enterprise !== void 0) return enterprise;
|
|
3982
|
+
throw new Error("workbuddy upstream enterprise credit: no readable balance in the response");
|
|
3983
|
+
}
|
|
3880
3984
|
const now = /* @__PURE__ */ new Date();
|
|
3881
3985
|
const format = (date) => [
|
|
3882
3986
|
date.getFullYear().toString().padStart(4, "0"),
|
|
@@ -4027,6 +4131,49 @@ var WorkBuddyEncryptedCredentialError = class extends Error {
|
|
|
4027
4131
|
function isEncryptedCredentialError(value) {
|
|
4028
4132
|
return typeof value === "object" && value !== null && value.code === "WORKBUDDY_ENCRYPTED_CREDENTIAL";
|
|
4029
4133
|
}
|
|
4134
|
+
/** One selectable local account, token-free. */
|
|
4135
|
+
/**
|
|
4136
|
+
* The name to SHOW for a credential (issue #124).
|
|
4137
|
+
*
|
|
4138
|
+
* The order matters and is not arbitrary: on an enterprise credential
|
|
4139
|
+
* `nickname` is the LOGIN ALIAS, so putting it first made the card look like it
|
|
4140
|
+
* was managing a stranger's account. The official client prefers the display
|
|
4141
|
+
* name, then the enterprise-side user name, and only then the alias.
|
|
4142
|
+
*
|
|
4143
|
+
* Blank spellings are skipped rather than preferred: the app writes `" "` in
|
|
4144
|
+
* places, and rendering whitespace where a name belongs is worse than falling
|
|
4145
|
+
* through to the alias.
|
|
4146
|
+
*
|
|
4147
|
+
* Exported because the same order has to hold in the store's account list, the
|
|
4148
|
+
* pool rows and the composer panel — three sites that must not drift.
|
|
4149
|
+
*/
|
|
4150
|
+
function preferredAccountName(fields) {
|
|
4151
|
+
for (const candidate of [
|
|
4152
|
+
fields.displayName,
|
|
4153
|
+
fields.enterpriseUserName,
|
|
4154
|
+
fields.nickname
|
|
4155
|
+
]) if (candidate !== void 0 && candidate.trim() !== "") return candidate;
|
|
4156
|
+
return "";
|
|
4157
|
+
}
|
|
4158
|
+
/**
|
|
4159
|
+
* Carry display names forward across a re-read of the same account.
|
|
4160
|
+
*
|
|
4161
|
+
* The desktop auth document is the only source for these fields and it does
|
|
4162
|
+
* not always publish them: a refresh that rewrites the document can drop
|
|
4163
|
+
* `displayName` while keeping the same sign-in. Replacing the whole record
|
|
4164
|
+
* would then silently fall back to the alias — the exact symptom #124 reports,
|
|
4165
|
+
* coming back on its own. So an absent field yields to the previous value and a
|
|
4166
|
+
* PRESENT one wins (it is the newer reading).
|
|
4167
|
+
*/
|
|
4168
|
+
function mergeCredentialDisplayNames(previous, next) {
|
|
4169
|
+
if (previous === void 0) return next;
|
|
4170
|
+
return {
|
|
4171
|
+
...next,
|
|
4172
|
+
...next.displayName === void 0 && previous.displayName !== void 0 ? { displayName: previous.displayName } : {},
|
|
4173
|
+
...next.enterpriseUserName === void 0 && previous.enterpriseUserName !== void 0 ? { enterpriseUserName: previous.enterpriseUserName } : {},
|
|
4174
|
+
...next.nickname === void 0 && previous.nickname !== void 0 ? { nickname: previous.nickname } : {}
|
|
4175
|
+
};
|
|
4176
|
+
}
|
|
4030
4177
|
/** Legacy single-copy basename (pre-dual-provider); kept as migration source. */
|
|
4031
4178
|
const WORKBUDDY_AUTH_FILENAME = ".workbuddy-auth.json";
|
|
4032
4179
|
/** Env variable that overrides the desktop auth-file location. */
|
|
@@ -4099,6 +4246,8 @@ function vaultDocument(credential, accountId, refreshedFrom) {
|
|
|
4099
4246
|
uid: credential.uid,
|
|
4100
4247
|
...credential.enterpriseId === void 0 ? {} : { enterpriseId: credential.enterpriseId },
|
|
4101
4248
|
...credential.nickname === void 0 ? {} : { nickname: credential.nickname },
|
|
4249
|
+
...credential.displayName === void 0 ? {} : { displayName: credential.displayName },
|
|
4250
|
+
...credential.enterpriseUserName === void 0 ? {} : { enterpriseUserName: credential.enterpriseUserName },
|
|
4102
4251
|
...credential.uin === void 0 ? {} : { uin: credential.uin }
|
|
4103
4252
|
}
|
|
4104
4253
|
};
|
|
@@ -4369,6 +4518,8 @@ function parseWorkBuddyAuth(text, filePath, atRestKey) {
|
|
|
4369
4518
|
const lastRefreshAtMs = typeof auth["lastRefreshTime"] === "number" ? expiryToMs(auth["lastRefreshTime"]) : void 0;
|
|
4370
4519
|
const enterpriseId = optionalCredentialField(identity["enterpriseId"], atRestKey);
|
|
4371
4520
|
const nickname = optionalCredentialField(identity["nickname"], atRestKey);
|
|
4521
|
+
const displayName = optionalCredentialField(identity["displayName"], atRestKey);
|
|
4522
|
+
const enterpriseUserName = optionalCredentialField(identity["enterpriseUserName"], atRestKey);
|
|
4372
4523
|
const uin = optionalCredentialField(identity["uin"], atRestKey);
|
|
4373
4524
|
return {
|
|
4374
4525
|
accessToken,
|
|
@@ -4379,6 +4530,8 @@ function parseWorkBuddyAuth(text, filePath, atRestKey) {
|
|
|
4379
4530
|
uid: optionalCredentialField(identity["uid"], atRestKey) ?? "",
|
|
4380
4531
|
...enterpriseId === void 0 ? {} : { enterpriseId },
|
|
4381
4532
|
...nickname === void 0 ? {} : { nickname },
|
|
4533
|
+
...displayName === void 0 ? {} : { displayName },
|
|
4534
|
+
...enterpriseUserName === void 0 ? {} : { enterpriseUserName },
|
|
4382
4535
|
...uin === void 0 ? {} : { uin },
|
|
4383
4536
|
...lastRefreshAtMs === void 0 ? {} : { lastRefreshAtMs },
|
|
4384
4537
|
source: "desktop",
|
|
@@ -4912,7 +5065,8 @@ var WorkBuddyCredentialStore = class {
|
|
|
4912
5065
|
byId.set(id, credential);
|
|
4913
5066
|
continue;
|
|
4914
5067
|
}
|
|
4915
|
-
if (isFresher(credential, existing)) byId.set(id, credential);
|
|
5068
|
+
if (isFresher(credential, existing)) byId.set(id, mergeCredentialDisplayNames(existing, credential));
|
|
5069
|
+
else byId.set(id, mergeCredentialDisplayNames(credential, existing));
|
|
4916
5070
|
}
|
|
4917
5071
|
for (const own of await this.readOwns()) {
|
|
4918
5072
|
if (!this.matchesRegion(own.domain)) continue;
|
|
@@ -5006,8 +5160,9 @@ var WorkBuddyCredentialStore = class {
|
|
|
5006
5160
|
const id = workbuddyAccountId(credential);
|
|
5007
5161
|
return {
|
|
5008
5162
|
id,
|
|
5009
|
-
accountName: credential
|
|
5163
|
+
accountName: preferredAccountName(credential),
|
|
5010
5164
|
domain: credential.domain,
|
|
5165
|
+
...credential.enterpriseId === void 0 || credential.enterpriseId === "" ? {} : { enterprise: true },
|
|
5011
5166
|
source: credential.source,
|
|
5012
5167
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
5013
5168
|
filePath: credential.filePath,
|
|
@@ -5966,7 +6121,7 @@ function generationOf(version) {
|
|
|
5966
6121
|
//#endregion
|
|
5967
6122
|
//#region src/version.ts
|
|
5968
6123
|
/** The npm package version this build was produced from. */
|
|
5969
|
-
const WORKBUDDY_CONNECT_VERSION = "5.
|
|
6124
|
+
const WORKBUDDY_CONNECT_VERSION = "5.6.0";
|
|
5970
6125
|
//#endregion
|
|
5971
6126
|
//#region src/host-heartbeat.ts
|
|
5972
6127
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -15,6 +15,21 @@ interface WorkBuddyCredential {
|
|
|
15
15
|
uid: string;
|
|
16
16
|
enterpriseId?: string;
|
|
17
17
|
nickname?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The account's DISPLAY name, when the app publishes one (issue #124).
|
|
20
|
+
*
|
|
21
|
+
* For an enterprise member `nickname` is the LOGIN ALIAS, not a person's
|
|
22
|
+
* name — a card showing it looks like it is managing somebody else's
|
|
23
|
+
* account. The official client prefers this field, then the enterprise user
|
|
24
|
+
* name, and only then the alias.
|
|
25
|
+
*
|
|
26
|
+
* Read from the desktop auth document when present. It is NOT always there
|
|
27
|
+
* (the app writes only what it has), which is why the read order has three
|
|
28
|
+
* steps rather than two.
|
|
29
|
+
*/
|
|
30
|
+
displayName?: string;
|
|
31
|
+
/** The enterprise-side user name (`enterpriseUserName`), when published. */
|
|
32
|
+
enterpriseUserName?: string;
|
|
18
33
|
uin?: string;
|
|
19
34
|
/** Which auth file this came from; refreshes are always `dsh`. */
|
|
20
35
|
source: 'desktop' | 'dsh';
|
|
@@ -138,7 +153,6 @@ interface WorkBuddyStoreOptions {
|
|
|
138
153
|
*/
|
|
139
154
|
resolveAtRestKey?: () => Promise<Buffer | undefined>;
|
|
140
155
|
}
|
|
141
|
-
/** One selectable local account, token-free. */
|
|
142
156
|
interface WorkBuddyAccountChoice {
|
|
143
157
|
/** Stable id derived from `uin` (or `uid` when uin is absent). */
|
|
144
158
|
id: string;
|
|
@@ -156,6 +170,15 @@ interface WorkBuddyAccountChoice {
|
|
|
156
170
|
/** The auth file this account was read from; newest is preferred. */
|
|
157
171
|
filePath: string;
|
|
158
172
|
selected: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* This account belongs to an enterprise (issue #124).
|
|
175
|
+
*
|
|
176
|
+
* Present only when `enterpriseId` is set. The credits come from the
|
|
177
|
+
* enterprise table and the consumer daily check-in does not exist for it, so
|
|
178
|
+
* callers branch on this FACT rather than on `status.active` (which reads
|
|
179
|
+
* `false` for every enterprise member).
|
|
180
|
+
*/
|
|
181
|
+
enterprise?: boolean;
|
|
159
182
|
}
|
|
160
183
|
/** Legacy single-copy basename (pre-dual-provider); kept as migration source. */
|
|
161
184
|
declare const WORKBUDDY_AUTH_FILENAME = ".workbuddy-auth.json";
|
|
@@ -740,6 +763,41 @@ interface WorkBuddyPromoSchedule {
|
|
|
740
763
|
*/
|
|
741
764
|
dailyUnusable?: true;
|
|
742
765
|
}
|
|
766
|
+
/**
|
|
767
|
+
* The upstream's own discounted price for a promotion, when it publishes one.
|
|
768
|
+
*
|
|
769
|
+
* This is what makes a "night discount" badge more than decoration: the badge
|
|
770
|
+
* says the model is on offer, and this field says HOW MUCH less it costs during
|
|
771
|
+
* the offer. The plugin used to drop it, so a model could wear a "night
|
|
772
|
+
* discount" badge while still showing its full price — the card and the
|
|
773
|
+
* composer readout then disagreed with what the user was actually charged.
|
|
774
|
+
*
|
|
775
|
+
* Shapes observed on the CN feed (2026-10-21, `docs/PROMO_BADGE.md`):
|
|
776
|
+
*
|
|
777
|
+
* { discountedCredits: "0x", displayMode: "replace", factor: 0 }
|
|
778
|
+
*
|
|
779
|
+
* `discountedCredits` is a STRING with the unit glued on ("0x", "0.06x"), not
|
|
780
|
+
* a number: `Number("0.06x")` is `NaN`, so it must be parsed, not coerced.
|
|
781
|
+
* `factor` is a ratio the upstream sometimes supplies (0.545 = "55% of the
|
|
782
|
+
* normal price"); it is used only when `discountedCredits` cannot be read, and
|
|
783
|
+
* the two disagreeing means the string wins — it is the figure the user will
|
|
784
|
+
* be charged.
|
|
785
|
+
*/
|
|
786
|
+
interface WorkBuddyPromoDiscount {
|
|
787
|
+
/**
|
|
788
|
+
* The multiplier the user pays during the promotion, e.g. `0` for free.
|
|
789
|
+
*
|
|
790
|
+
* Parsed from `discountedCredits` ("0x" / "0.06x" / "0.06"), or derived from
|
|
791
|
+
* `factor` when only that is present. `undefined` when neither could be read.
|
|
792
|
+
*/
|
|
793
|
+
multiplier?: number;
|
|
794
|
+
/**
|
|
795
|
+
* The upstream's display instruction. `'replace'` means "show this instead of
|
|
796
|
+
* the normal multiplier"; anything else is not yet observed, so it is not
|
|
797
|
+
* acted on.
|
|
798
|
+
*/
|
|
799
|
+
displayMode?: 'replace';
|
|
800
|
+
}
|
|
743
801
|
/** The upstream's explanatory copy for the badge. */
|
|
744
802
|
interface WorkBuddyPromoHover {
|
|
745
803
|
/** The Chinese wording. */
|
|
@@ -766,6 +824,10 @@ interface WorkBuddyPromo {
|
|
|
766
824
|
* badge matters. Used to pick ONE badge per model when several apply.
|
|
767
825
|
*/
|
|
768
826
|
priority: number;
|
|
827
|
+
/**
|
|
828
|
+
* The discounted price during the promotion, when the upstream publishes one.
|
|
829
|
+
*/
|
|
830
|
+
discount?: WorkBuddyPromoDiscount;
|
|
769
831
|
}
|
|
770
832
|
//#endregion
|
|
771
833
|
//#region src/busy.d.ts
|
|
@@ -1451,6 +1513,27 @@ declare class WorkBuddyUpstreamClient {
|
|
|
1451
1513
|
claimBenefit(credential: WorkBuddyCredential, kind: WorkBuddyBenefit['kind']): Promise<void>;
|
|
1452
1514
|
/** Claim today's check-in reward. The browser route guards this mutation. */
|
|
1453
1515
|
claimDailyCheckin(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinClaim>;
|
|
1516
|
+
/**
|
|
1517
|
+
* The ENTERPRISE credit table (issue #124).
|
|
1518
|
+
*
|
|
1519
|
+
* `/v2/billing/meter/get-user-resource` answers an enterprise member with
|
|
1520
|
+
* HTTP 200 and an empty `Accounts` list — a legally-shaped "you have no
|
|
1521
|
+
* credits" that is simply false, because the member's credits live under the
|
|
1522
|
+
* enterprise account. The official client branches on the account's
|
|
1523
|
+
* `type: "ultimate"` and calls `/billing/meter/get-enterprise-user-usage`
|
|
1524
|
+
* instead (no `/v2` prefix), which returns:
|
|
1525
|
+
*
|
|
1526
|
+
* { credit: 1234, limitNum: 5000, cycleResetTime: "2026-11-01 00:00:00" }
|
|
1527
|
+
*
|
|
1528
|
+
* What REMAINS is `limitNum - credit`. `credit` there is the amount SPENT,
|
|
1529
|
+
* not the amount left — reading it as a balance inverts the answer, which is
|
|
1530
|
+
* worse than the zero it replaces.
|
|
1531
|
+
*
|
|
1532
|
+
* @returns the credits, or `undefined` when this response carried no
|
|
1533
|
+
* readable balance (the caller turns that into an error rather than
|
|
1534
|
+
* reporting zero).
|
|
1535
|
+
*/
|
|
1536
|
+
private fetchEnterpriseCredits;
|
|
1454
1537
|
/**
|
|
1455
1538
|
* POST the billing endpoint for the remaining credit, keeping every package
|
|
1456
1539
|
* separate: the card groups monthly-cycle packages itself and lists the
|
|
@@ -2799,7 +2882,7 @@ interface WorkBuddyPoolMember {
|
|
|
2799
2882
|
interface WorkBuddyPoolCheckinRow {
|
|
2800
2883
|
accountId: string;
|
|
2801
2884
|
accountName: string;
|
|
2802
|
-
status: 'claimed' | 'already' | 'failed';
|
|
2885
|
+
status: 'claimed' | 'already' | 'failed' | 'skipped';
|
|
2803
2886
|
/** Credits gained, when the upstream stated a figure. */
|
|
2804
2887
|
credit?: number;
|
|
2805
2888
|
/** Streak length after the claim, when the upstream stated one. */
|
|
@@ -3159,6 +3242,15 @@ interface WorkBuddyWebAccount {
|
|
|
3159
3242
|
source: 'desktop' | 'dsh';
|
|
3160
3243
|
tokenExpiresAtMs: number;
|
|
3161
3244
|
selected: boolean;
|
|
3245
|
+
/**
|
|
3246
|
+
* This account belongs to an enterprise (issue #124).
|
|
3247
|
+
*
|
|
3248
|
+
* Its credits come from the enterprise table and its check-in is refused by
|
|
3249
|
+
* the upstream (`10001`), so the card must not offer the action for it. The
|
|
3250
|
+
* field is a FACT about the credential (`enterpriseId` is present), not a
|
|
3251
|
+
* guess from `status.active`: that reads `false` for every enterprise member.
|
|
3252
|
+
*/
|
|
3253
|
+
enterprise?: boolean;
|
|
3162
3254
|
}
|
|
3163
3255
|
/**
|
|
3164
3256
|
* One probed candidate path and why it yielded no account.
|
|
@@ -4306,9 +4398,14 @@ interface WorkBuddyPoolPreferences {
|
|
|
4306
4398
|
* plugin behaves exactly as it did before the pool existed: the account the
|
|
4307
4399
|
* user selected serves every request, and a failure is reported as-is.
|
|
4308
4400
|
*
|
|
4309
|
-
* It deliberately does NOT change
|
|
4310
|
-
*
|
|
4311
|
-
*
|
|
4401
|
+
* It deliberately does NOT change the SAVED selection: while on, the pool
|
|
4402
|
+
* picks the serving account for every request (`applyPoolSelection` ranks
|
|
4403
|
+
* the usable members and sets a per-request override), but that override is
|
|
4404
|
+
* per-request borrowing — turning the pool off hands every request back to
|
|
4405
|
+
* the account the user chose, and the card can keep reporting who really
|
|
4406
|
+
* served. (An older version of this comment said the pool "does NOT change
|
|
4407
|
+
* which account serves NEXT", which was wrong the moment `applyPoolSelection`
|
|
4408
|
+
* started ranking on every request; what never changes is the saved choice.)
|
|
4312
4409
|
*/
|
|
4313
4410
|
enabled?: boolean;
|
|
4314
4411
|
/** Model id to test; `''` means auto-pick a free model from this region. */
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $ as prepareChatBody, A as parseWorkBuddyAuth, At as readAtRestKey, B as declaredTools, C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutable, D as isEncryptedCredentialError, Dt as macosBundleExecutable, E as hasEncryptedCredentialFields, Et as isWorkbuddyBundle, F as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, Ft as writeFileAtomic, G as isFirstMessageGateBody, H as isContextTooLongBody, I as WorkBuddyCredentialRejectedError, J as openingEvidenceOfBody, K as knownRegionOf, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, N as CONTEXT_OVERFLOW_MARKER, Nt as resolveDshHome, O as isVaultAccountId, Ot as macosNestedAppBundles, P as CREDENTIAL_REJECTED_CODE, Pt as withFileLock, Q as perturbChannelFingerprints, R as channelGateMemberOf, S as authFileName, St as fetchAtRestKeyPayload, T as defaultDesktopAuthPath, Tt as isEncryptedFieldWrapper, U as isConversationIdGateBody, V as forceNeutralHead, W as isCredentialRejectedError, X as parseReasoning, Y as parseCreditMultiplier, Z as parseUpstreamModel, _ as ENCRYPTED_CREDENTIAL_CODE, _t as nativeModalityOf, a as readHostHeartbeat, at as lastRefusalFor, b as WorkBuddyCredentialStore, bt as deriveAtRestKey, c as WORKBUDDY_CONNECT_VERSION, ct as recordRefusal, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as WorkBuddyImageError, et as regionOf, f as WorkBuddyCatalog, ft as pickImageModel, g as withoutRouters, gt as imageDefaultFor, h as fallbackModelsFor, ht as NATIVE_MODALITY_BY_MODEL_ID, i as processStartTimeMs, it as defaultOffModelIds, j as workbuddyAccountId, jt as workbuddyAppExecutableCandidates, k as legacyWorkbuddyOwnAuthPath, kt as openEncryptedField, l as piAiRuntimeInfo, lt as withEffectiveOff, m as deriveCatalog, mt as queueNoticeText, n as clearHostHeartbeat, nt as attachBusyLevels, o as workbuddyHostHeartbeatPath, ot as offDefaultFor, p as applyContextBudgets, pt as parseQueueAnswer, q as modelIdOfBody, r as isHeartbeatProcessAlive, rt as REFUSES_OFF_MODEL_IDS, s as writeHostHeartbeat, st as offRefused, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as requestEvidenceOf, u as FALLBACK_WORKBUDDY_MODELS, ut as DEFAULT_IMAGE_SIZE, v as WORKBUDDY_AUTH_FILENAME, vt as WORKBUDDY_APP_EXECUTABLE_ENV, w as defaultDesktopAuthDirs, x as WorkBuddyEncryptedCredentialError, xt as deriveAtRestKeyId, y as WORKBUDDY_AUTH_FILE_ENV, yt as clearAtRestKeyCache, z as classifyUpstreamError } from "./host-heartbeat-
|
|
1
|
+
import { $ as prepareChatBody, A as parseWorkBuddyAuth, At as readAtRestKey, B as declaredTools, C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutable, D as isEncryptedCredentialError, Dt as macosBundleExecutable, E as hasEncryptedCredentialFields, Et as isWorkbuddyBundle, F as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, Ft as writeFileAtomic, G as isFirstMessageGateBody, H as isContextTooLongBody, I as WorkBuddyCredentialRejectedError, J as openingEvidenceOfBody, K as knownRegionOf, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, N as CONTEXT_OVERFLOW_MARKER, Nt as resolveDshHome, O as isVaultAccountId, Ot as macosNestedAppBundles, P as CREDENTIAL_REJECTED_CODE, Pt as withFileLock, Q as perturbChannelFingerprints, R as channelGateMemberOf, S as authFileName, St as fetchAtRestKeyPayload, T as defaultDesktopAuthPath, Tt as isEncryptedFieldWrapper, U as isConversationIdGateBody, V as forceNeutralHead, W as isCredentialRejectedError, X as parseReasoning, Y as parseCreditMultiplier, Z as parseUpstreamModel, _ as ENCRYPTED_CREDENTIAL_CODE, _t as nativeModalityOf, a as readHostHeartbeat, at as lastRefusalFor, b as WorkBuddyCredentialStore, bt as deriveAtRestKey, c as WORKBUDDY_CONNECT_VERSION, ct as recordRefusal, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as WorkBuddyImageError, et as regionOf, f as WorkBuddyCatalog, ft as pickImageModel, g as withoutRouters, gt as imageDefaultFor, h as fallbackModelsFor, ht as NATIVE_MODALITY_BY_MODEL_ID, i as processStartTimeMs, it as defaultOffModelIds, j as workbuddyAccountId, jt as workbuddyAppExecutableCandidates, k as legacyWorkbuddyOwnAuthPath, kt as openEncryptedField, l as piAiRuntimeInfo, lt as withEffectiveOff, m as deriveCatalog, mt as queueNoticeText, n as clearHostHeartbeat, nt as attachBusyLevels, o as workbuddyHostHeartbeatPath, ot as offDefaultFor, p as applyContextBudgets, pt as parseQueueAnswer, q as modelIdOfBody, r as isHeartbeatProcessAlive, rt as REFUSES_OFF_MODEL_IDS, s as writeHostHeartbeat, st as offRefused, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as requestEvidenceOf, u as FALLBACK_WORKBUDDY_MODELS, ut as DEFAULT_IMAGE_SIZE, v as WORKBUDDY_AUTH_FILENAME, vt as WORKBUDDY_APP_EXECUTABLE_ENV, w as defaultDesktopAuthDirs, x as WorkBuddyEncryptedCredentialError, xt as deriveAtRestKeyId, y as WORKBUDDY_AUTH_FILE_ENV, yt as clearAtRestKeyCache, z as classifyUpstreamError } from "./host-heartbeat-DqTNlE6H.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
@@ -2142,6 +2142,23 @@ function bodyReportDirectory(env = process.env) {
|
|
|
2142
2142
|
return trimmed;
|
|
2143
2143
|
}
|
|
2144
2144
|
/**
|
|
2145
|
+
* How many bytes from the END of the file {@link attachUsage} reads when it
|
|
2146
|
+
* looks for the line it wrote (issue #118).
|
|
2147
|
+
*
|
|
2148
|
+
* The old implementation read the WHOLE file, split it, rewrote the WHOLE file —
|
|
2149
|
+
* so every streamed answer cost ~3× the file size in heap (the string, the line
|
|
2150
|
+
* array, the re-joined string), which is what OOMed the host at ~500 MB. The
|
|
2151
|
+
* usage callback fires seconds after its own append, so its line is always
|
|
2152
|
+
* near the tail; a bounded tail read makes the cost O(window) instead of
|
|
2153
|
+
* O(file). A line older than the window is simply left un-annotated — the
|
|
2154
|
+
* annotation is best-effort by design (see the header of {@link attachUsage}).
|
|
2155
|
+
*
|
|
2156
|
+
* Exported because the boundary behaviour is exactly what a regression would
|
|
2157
|
+
* break first, and a test that pins it must be able to size a file against it
|
|
2158
|
+
* without reaching into the module.
|
|
2159
|
+
*/
|
|
2160
|
+
const USAGE_ATTACH_TAIL_BYTES = 1048576;
|
|
2161
|
+
/**
|
|
2145
2162
|
* Append one composition to `<dir>/<region>.jsonl`.
|
|
2146
2163
|
*
|
|
2147
2164
|
* One file per region is the whole point: the reporter captures a session, then
|
|
@@ -2151,13 +2168,23 @@ function bodyReportDirectory(env = process.env) {
|
|
|
2151
2168
|
*
|
|
2152
2169
|
* Read-modify-write is avoided on purpose — a long session would rewrite a
|
|
2153
2170
|
* growing file on every step. The lock keeps two concurrent requests from
|
|
2154
|
-
* interleaving a line
|
|
2171
|
+
* interleaving a line, and the size check inside the SAME lock keeps two
|
|
2172
|
+
* concurrent appends from both deciding "no roll needed" and then writing past
|
|
2173
|
+
* the cap together.
|
|
2155
2174
|
*/
|
|
2156
2175
|
async function appendBodyReport(directory, report) {
|
|
2157
2176
|
const file = join(directory, `${report.region}.jsonl`);
|
|
2158
2177
|
await mkdir(directory, { recursive: true });
|
|
2159
2178
|
const line = `${JSON.stringify(report)}\n`;
|
|
2160
|
-
await withFileLock(file, () =>
|
|
2179
|
+
await withFileLock(file, async () => {
|
|
2180
|
+
const size = await stat(file).then((entry) => entry.size).catch(() => 0);
|
|
2181
|
+
if (size > 0 && size + Buffer.byteLength(line, "utf8") > 33554432) {
|
|
2182
|
+
const rolled = `${file}.1`;
|
|
2183
|
+
await rm(rolled, { force: true });
|
|
2184
|
+
await rename(file, rolled);
|
|
2185
|
+
}
|
|
2186
|
+
await appendFile(file, line, "utf8");
|
|
2187
|
+
});
|
|
2161
2188
|
}
|
|
2162
2189
|
/**
|
|
2163
2190
|
* Record one outgoing body, if the report is enabled, and return a handle that
|
|
@@ -2188,16 +2215,6 @@ async function recordBodyReport(region, bodyJson, options = {}) {
|
|
|
2188
2215
|
}
|
|
2189
2216
|
}
|
|
2190
2217
|
/**
|
|
2191
|
-
* How many trailing lines are searched for the line this handle wrote.
|
|
2192
|
-
*
|
|
2193
|
-
* One would be enough for a single-threaded request, but a turn can have several
|
|
2194
|
-
* requests in flight (parallel tool steps) and their lines interleave, so the
|
|
2195
|
-
* target is not necessarily last. A small bounded window keeps the read cheap on
|
|
2196
|
-
* a file that grows to tens of megabytes, while still finding a line that landed
|
|
2197
|
-
* a couple of appends ago.
|
|
2198
|
-
*/
|
|
2199
|
-
const USAGE_ATTACH_WINDOW = 32;
|
|
2200
|
-
/**
|
|
2201
2218
|
* Return a callback that rewrites its own line with the upstream's counts.
|
|
2202
2219
|
*
|
|
2203
2220
|
* Matched by `at` + `region` + `bodyBytes` + `messageCount`, all four being
|
|
@@ -2205,6 +2222,24 @@ const USAGE_ATTACH_WINDOW = 32;
|
|
|
2205
2222
|
* timestamped in the same millisecond, and `bodyBytes` tells apart the two
|
|
2206
2223
|
* different bodies a failover or a retry can produce. A line that is already
|
|
2207
2224
|
* annotated is left alone, so a second call cannot double-apply.
|
|
2225
|
+
*
|
|
2226
|
+
* The rewrite touches ONLY the tail window (issue #118): the file is opened
|
|
2227
|
+
* `r+`, the last {@link USAGE_ATTACH_TAIL_BYTES} are read, the window is split
|
|
2228
|
+
* into lines, the target line is replaced, and the new window is written back
|
|
2229
|
+
* at the SAME offset with the file truncated to the new end. Nothing before the
|
|
2230
|
+
* window is ever read or written, so a 500 MB file costs one megabyte of heap
|
|
2231
|
+
* here instead of three copies of itself.
|
|
2232
|
+
*
|
|
2233
|
+
* Two consequences of the bounded window, both deliberate:
|
|
2234
|
+
*
|
|
2235
|
+
* - a line pushed out of the window by later appends is left un-annotated.
|
|
2236
|
+
* The annotation is a convenience for the #59 diff, never a correctness
|
|
2237
|
+
* requirement — the sent side is already on disk;
|
|
2238
|
+
* - the window must start on a line boundary, so the read takes ONE byte of
|
|
2239
|
+
* lookback before it: when that byte is a newline the window already starts
|
|
2240
|
+
* on a boundary and its first line is complete (kept in search and rewrite);
|
|
2241
|
+
* otherwise the first partial line is dropped from the SEARCH and preserved
|
|
2242
|
+
* byte-for-byte by starting the write at the first complete line's offset.
|
|
2208
2243
|
*/
|
|
2209
2244
|
function attachUsage(directory, report, onError) {
|
|
2210
2245
|
/**
|
|
@@ -2225,28 +2260,58 @@ function attachUsage(directory, report, onError) {
|
|
|
2225
2260
|
(async () => {
|
|
2226
2261
|
try {
|
|
2227
2262
|
await withFileLock(file, async () => {
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2230
|
-
const
|
|
2231
|
-
if (
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2263
|
+
const handle = await open(file, "r+");
|
|
2264
|
+
try {
|
|
2265
|
+
const { size } = await handle.stat();
|
|
2266
|
+
if (size === 0) return;
|
|
2267
|
+
const windowStart = size - Math.min(size, USAGE_ATTACH_TAIL_BYTES);
|
|
2268
|
+
const readStart = windowStart === 0 ? 0 : windowStart - 1;
|
|
2269
|
+
const readLength = size - readStart;
|
|
2270
|
+
const buffer = Buffer.allocUnsafe(readLength);
|
|
2271
|
+
const { bytesRead } = await handle.read(buffer, 0, readLength, readStart);
|
|
2272
|
+
if (bytesRead === 0) return;
|
|
2273
|
+
const raw = buffer.toString("utf8", 0, bytesRead);
|
|
2274
|
+
let contentStart;
|
|
2275
|
+
let text;
|
|
2276
|
+
if (readStart === 0) {
|
|
2277
|
+
contentStart = 0;
|
|
2278
|
+
text = raw;
|
|
2279
|
+
} else if (raw.startsWith("\n")) {
|
|
2280
|
+
contentStart = readStart + 1;
|
|
2281
|
+
text = raw.slice(1);
|
|
2282
|
+
} else {
|
|
2283
|
+
const firstNewline = raw.indexOf("\n");
|
|
2284
|
+
if (firstNewline === -1) return;
|
|
2285
|
+
contentStart = readStart + firstNewline + 1;
|
|
2286
|
+
text = raw.slice(firstNewline + 1);
|
|
2237
2287
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2288
|
+
const lines = text.split("\n");
|
|
2289
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
2290
|
+
const line = lines[index];
|
|
2291
|
+
if (line === void 0 || line === "") continue;
|
|
2292
|
+
let parsed;
|
|
2293
|
+
try {
|
|
2294
|
+
parsed = JSON.parse(line);
|
|
2295
|
+
} catch {
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
if (!isRecord(parsed)) continue;
|
|
2299
|
+
if (parsed["at"] !== report.at) continue;
|
|
2300
|
+
if (parsed["region"] !== report.region) continue;
|
|
2301
|
+
if (parsed["bodyBytes"] !== report.bodyBytes) continue;
|
|
2302
|
+
if (parsed["messageCount"] !== report.messageCount) continue;
|
|
2303
|
+
if (parsed["promptTokens"] !== void 0) return;
|
|
2304
|
+
lines[index] = JSON.stringify({
|
|
2305
|
+
...parsed,
|
|
2306
|
+
...usage
|
|
2307
|
+
});
|
|
2308
|
+
const rewritten = lines.join("\n");
|
|
2309
|
+
await handle.write(rewritten, contentStart, "utf8");
|
|
2310
|
+
await handle.truncate(contentStart + Buffer.byteLength(rewritten, "utf8"));
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
} finally {
|
|
2314
|
+
await handle.close();
|
|
2250
2315
|
}
|
|
2251
2316
|
});
|
|
2252
2317
|
} catch (error) {
|
|
@@ -7302,8 +7367,19 @@ async function measureTps(input) {
|
|
|
7302
7367
|
*/
|
|
7303
7368
|
async function checkinAllAccounts(targets, deps, nowMs = Date.now) {
|
|
7304
7369
|
const rows = [];
|
|
7305
|
-
|
|
7306
|
-
|
|
7370
|
+
let issued = 0;
|
|
7371
|
+
for (const target of targets) {
|
|
7372
|
+
if (target.enterprise === true) {
|
|
7373
|
+
rows.push({
|
|
7374
|
+
accountId: target.accountId,
|
|
7375
|
+
accountName: target.accountName,
|
|
7376
|
+
status: "skipped",
|
|
7377
|
+
message: "enterprise accounts have no daily check-in"
|
|
7378
|
+
});
|
|
7379
|
+
continue;
|
|
7380
|
+
}
|
|
7381
|
+
if (issued > 0) await (deps.wait ?? defaultWait)(400);
|
|
7382
|
+
issued += 1;
|
|
7307
7383
|
rows.push(await checkinOne(target, deps));
|
|
7308
7384
|
}
|
|
7309
7385
|
return rows;
|
|
@@ -11040,7 +11116,8 @@ function apply(ctx, config) {
|
|
|
11040
11116
|
async function poolTargets(region) {
|
|
11041
11117
|
return (await poolMemberAccounts(region)).map((account) => ({
|
|
11042
11118
|
accountId: account.id,
|
|
11043
|
-
accountName: account.accountName
|
|
11119
|
+
accountName: account.accountName,
|
|
11120
|
+
...account.enterprise === true ? { enterprise: true } : {}
|
|
11044
11121
|
}));
|
|
11045
11122
|
}
|
|
11046
11123
|
/**
|
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": "5.
|
|
5
|
+
"version": "5.6.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
package/screenshots.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
[
|
|
2
2
|
"docs/assets/dsh-connect-workbuddy-usage-card.png",
|
|
3
3
|
"docs/assets/dsh-connect-workbuddy-account-pool.png",
|
|
4
|
-
"docs/assets/dsh-connect-workbuddy-composer-credits.png"
|
|
4
|
+
"docs/assets/dsh-connect-workbuddy-composer-credits.png",
|
|
5
|
+
"docs/assets/dsh-connect-workbuddy-model-picker.png"
|
|
5
6
|
]
|