dsh-connect-workbuddy 5.5.1 → 5.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.en.md +6 -4
- package/README.md +6 -4
- 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 +10 -10
- package/lib/{host-heartbeat-BhgyeZWV.js → host-heartbeat-hcH37MkG.js} +1 -1
- package/lib/index.d.ts +8 -3
- package/lib/index.js +100 -35
- package/package.json +1 -1
- package/screenshots.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.5.2 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
> 补丁版本:两条修复都来自 issue 报告——#118(`WORKBUDDY_BODY_REPORT` 报告文件无界增长、回填整文件读写导致宿主 OOM)与 #122(界面文案审计)。**请求路径、provider 路由、模型目录、计费与凭据一律未变**;改的只有诊断文件的 I/O 形状、若干句用户可见文案与两处内部注释,外加三张 README 截图换成合成数据。
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **`WORKBUDDY_BODY_REPORT` 的报告文件不再无界增长,回填不再整文件读写(#118)**
|
|
10
|
+
- **报告**:报告者开着该环境变量长跑,`global.jsonl` 长到 **508 MB**;每次上游回 `usage` 后的回填都会 `readFile` 整文件 → `split('\n')` → `writeFile` 整文件,峰值约为文件大小的 2–3 倍(读串 + 行数组 + 重组串),把宿主进程顶到 OOM。`USAGE_ATTACH_WINDOW = 32` 只裁了**搜索**范围,没裁读写。
|
|
11
|
+
- **修法一:写侧有上限**。`appendBodyReport()` 追加前 `stat` 当前文件,追加会越过 `BODY_REPORT_MAX_BYTES = 32 MB` 时先滚成 `<region>.jsonl.1`(`rm` 旧滚档再 `rename`,只保留一份旧档)再写新行。尺寸检查与追加在**同一个文件锁**里,两个并发请求不会同时判「不用滚」再一起写超。滚档后活文件里是**新**行——报告者对比「最后一行」时不会拿到被滚走的旧捕获。32 MB 的取值:真实会话一行几 KB,32 MB 是几千次请求,远超还有人读它的时点。
|
|
12
|
+
- **修法二:回填只碰尾部窗口**。`attachUsage()` 改成 `open(file, 'r+')` + 读最后 `USAGE_ATTACH_TAIL_BYTES = 1 MB` + 窗口内定位目标行 + 从该行偏移写回 + `truncate` 到新长度;窗口前一个字节都不读不写,500 MB 文件在这里只花 1 MB 堆。窗口起点用**一字节回看**判定行边界:回看字节是 `\n` 说明窗口正好起在行边界、首行完整(必须保留——丢掉等于从文件里删一行);否则首行是上一条的残尾,从搜索与写回里都排除。窗口外的目标行**不再回填**:标注只是 #59 对比的便利,不是正确性要求(发送侧计数早已在盘上)。
|
|
13
|
+
- **README 补警告**(中英各一句):会持续追加、32 MB 滚档、排查完请关闭环境变量并删除报告文件。
|
|
14
|
+
- **测试**:新增 `tests/issue118-body-report-bounded.spec.ts`(6 条)——滚档与「只留一份旧档」、滚档后活文件是新行、不滚空文件与未超上限、大于一个窗口的文件里近期行仍被标注且窗口前逐字节不变、**窗口恰好起在行边界**(用迭代法把追加行尺寸钉到正好 1 MB:`model` 字段被原样抄进行里是唯一的尺寸旋钮,而 `bodyBytes` 的位数会随填充自增,一步算不准;断言三行都在、前两行未被标注也未被删)、目标行被推出窗口后文件不被截断或切碎。原有 `tests/issue59-body-report.spec.ts` 26 条全绿(含「标注只加在原有行上、不新增行」)。
|
|
15
|
+
- **四处用户可见文案与两处内部注释按 #122 审计改正**
|
|
16
|
+
- `row.showCreditsCnHint` / `row.showCreditsGlobalHint`(中英):删掉**不存在**的「手动刷新」——面板头部唯一的按钮是「一键测试」(会真跑一轮测试、真实消耗积分);改成「点击可看各账号余额并一键测试勾选的账号(测试会真实消耗积分)。读数每 5 分钟自动刷新;各账号明细在面板打开时每 60 秒刷新」,把两个周期分开写清(此前只写了 5 分钟那个)。
|
|
17
|
+
- `row.poolEnabledHint`(中英):「失败就换下一个」改成「只有可换号的失败才换下一个」,并点名四类例外(请求本身不合法、模型繁忙、平台排队、渠道校验)与理由(换号只会为同一个拒绝白烧一次真实配额)——这四类在 `isFailoverWorthy` 里被显式排除,旧文案说反了。
|
|
18
|
+
- `row.cliBridgeHint`(中英):`injectResult` **只影响 `/codebuddy` 指令**(结果回注会话、会话空闲时多花一次 DSH 回合),`codebuddy_ask` 工具不读它;并补上三条此前完全没提的限流(每会话每小时 6 回合、每进程每小时 20 回合、同时只跑一个回合)。
|
|
19
|
+
- `row.tabFreeQuotaWarn`(中英):不再声称「时间段内定额 Token 上限」这个插件**读不到**的机制(上游没有这个字段),改成「免费模型并非无限量:上游按单次请求体积与负载限流」;内部注释同步改写——6004 实测是**单次请求体积**触发(`src/probe.ts:71-89`:~25k 通过 / ~30k 被拒),14003 是模型侧负载,两者都不是累计定额。
|
|
20
|
+
- `src/index.ts` 的 `WorkBuddyPoolPreferences.enabled` 注释:旧句「刻意不改变下一个由谁服务」与 `applyPoolSelection()` 每次请求都按排名设服务账号的事实相反;改成「从不改变的是用户**保存的选择**」,并留一句旧注释错在哪的追溯(#122 的审计者指出「用户文案反而比这处注释更准」)。
|
|
21
|
+
- ①④ 三条审计结论无需改动:三条 tab 提醒在 5.5.0 的「能力」分区里已分开表述;5.71 仍是 2026-10-21 的实测常量(无配置项)、生图开关仍 CN-only 且关闭时根本不注册;测试默认 25000 与 1/2/3/5/10 万五档未变。
|
|
22
|
+
|
|
23
|
+
### Docs
|
|
24
|
+
|
|
25
|
+
- **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`,同一枚活动徽标画在模型名之后)。
|
|
26
|
+
- **隐私**:账号列每一行的名字格用不透明色块覆盖(不是可复原的马赛克),并逐行用「色块覆盖区内深色像素数 = 0」的脚本复核;积分、签到状态、排序位次与时间戳保留——它们不含身份。
|
|
27
|
+
- 模型选择器那张只含模型名、倍率与活动文案,没有账号信息。`screenshots.json` 与中英两份 README 的 alt 文本同步。
|
|
28
|
+
|
|
3
29
|
## 5.5.1 (2026-10-23)
|
|
4
30
|
|
|
5
31
|
> 补丁版本:修「账号池状态重启就失效」的根因,以及同一症状的另外三处(手动排序箭头、`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,6 +33,8 @@ 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
|
+
|
|
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)" />
|
|
36
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. **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.
|
|
@@ -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>
|
|
@@ -41,6 +41,8 @@
|
|
|
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-hcH37MkG.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
|
@@ -9148,7 +9148,7 @@ window.__ModuleLoader__.load({
|
|
|
9148
9148
|
"row.tabCn": "Domestic",
|
|
9149
9149
|
"row.tabGlobal": "Global",
|
|
9150
9150
|
"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
|
|
9151
|
+
"row.tabFreeQuotaWarn": "Free models are not unlimited — the upstream limits them by request size and load.",
|
|
9152
9152
|
"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
9153
|
"row.tabSwitchAria": "Offer the {region} provider to DSH",
|
|
9154
9154
|
"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 +9388,7 @@ window.__ModuleLoader__.load({
|
|
|
9388
9388
|
"row.poolSettingsTitle": "More settings",
|
|
9389
9389
|
"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
9390
|
"row.poolEnabled": "Enable the account pool",
|
|
9391
|
-
"row.poolEnabledHint": "On: the pool picks the account for every request and moves to the next
|
|
9391
|
+
"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
9392
|
"row.poolModeAuto": "picking automatically",
|
|
9393
9393
|
"row.poolModeManual": "you pick",
|
|
9394
9394
|
"row.poolManualAccountLabel": "Account in use",
|
|
@@ -9468,12 +9468,12 @@ window.__ModuleLoader__.load({
|
|
|
9468
9468
|
"row.poolRowRemoving": "Removing the account…",
|
|
9469
9469
|
"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
9470
|
"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
|
|
9471
|
+
"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
9472
|
"row.imageToolLabel": "Let the model draw images",
|
|
9473
9473
|
"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
9474
|
"row.showCreditsCn": "Show credits in the composer toolbar",
|
|
9475
9475
|
"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
|
|
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 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
9477
|
"row.creditPositionLabel": "Position",
|
|
9478
9478
|
"row.creditPositionComposer": "composer toolbar",
|
|
9479
9479
|
"row.creditPositionStats": "below the input box",
|
|
@@ -9484,7 +9484,7 @@ window.__ModuleLoader__.load({
|
|
|
9484
9484
|
"quotaNotice.fallbackHead": "The quota for this request is exhausted",
|
|
9485
9485
|
"quotaNotice.supplement": " (some models also have a daily limit that is unrelated to your credits)",
|
|
9486
9486
|
"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
|
|
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 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
9488
|
"composer.pointsCn": "WB CN",
|
|
9489
9489
|
"composer.pointsGlobal": "WB AI",
|
|
9490
9490
|
"composer.panelTitleCn": "WB CN credits",
|
|
@@ -9526,7 +9526,7 @@ window.__ModuleLoader__.load({
|
|
|
9526
9526
|
"row.tabCn": "国内版",
|
|
9527
9527
|
"row.tabGlobal": "国际版",
|
|
9528
9528
|
"row.tabHint": "每个 tab 是一个独立供应商(workbuddy / workbuddy-global),各有自己的账号、积分与模型。两边同时生效:不同会话可各选一边,一侧的改动不影响另一侧。",
|
|
9529
|
-
"row.tabFreeQuotaWarn": "
|
|
9529
|
+
"row.tabFreeQuotaWarn": "免费模型并非无限量:上游按单次请求体积与负载限流。",
|
|
9530
9530
|
"row.tabSwitchHint": "取消勾选即关闭该供应商:它的模型不再出现在 DSH 模型选择器里;账号、积分与模型设置都会保留,重新勾选即恢复。",
|
|
9531
9531
|
"row.tabSwitchAria": "是否向 DSH 提供{region}供应商",
|
|
9532
9532
|
"row.tabOffNotice": "该供应商已关闭:它的模型不会提供给 DSH。账号、积分与模型设置均已保留——勾选上方复选框即可恢复。",
|
|
@@ -9766,7 +9766,7 @@ window.__ModuleLoader__.load({
|
|
|
9766
9766
|
"row.poolSettingsTitle": "更多设置",
|
|
9767
9767
|
"row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
|
|
9768
9768
|
"row.poolEnabled": "启用账号池",
|
|
9769
|
-
"row.poolEnabledHint": "
|
|
9769
|
+
"row.poolEnabledHint": "开启:由账号池为每次请求挑号;只有可换号的失败才换下一个——请求本身不合法、模型繁忙、平台排队、渠道校验这四类不换(换号只会为同一个拒绝白烧一次配额)。关闭:你选的账号服务每一次请求,失败如实报错。",
|
|
9770
9770
|
"row.poolModeAuto": "自动挑号",
|
|
9771
9771
|
"row.poolModeManual": "手动选号",
|
|
9772
9772
|
"row.poolManualAccountLabel": "使用哪个账号",
|
|
@@ -9846,12 +9846,12 @@ window.__ModuleLoader__.load({
|
|
|
9846
9846
|
"row.poolRowRemoving": "正在删除账号…",
|
|
9847
9847
|
"row.poolRowRemoveDesktopOnly": "{accountName}:已从列表中隐藏——这个账号来自桌面 App 自己的登录,该登录未被改动。点「重新检测账号」会重新加入。",
|
|
9848
9848
|
"row.cliBridgeLabel": "允许调用本机 CodeBuddy CLI 干活",
|
|
9849
|
-
"row.cliBridgeHint": "开启后多出 codebuddy_ask 工具与 /codebuddy 指令;每次调用花 CodeBuddy
|
|
9849
|
+
"row.cliBridgeHint": "开启后多出 codebuddy_ask 工具与 /codebuddy 指令;每次调用花 CodeBuddy 配额。injectResult 只影响 /codebuddy:其结果回注会话,会话空闲时多花一次 DSH 回合;工具调用不读它。限流:每会话每小时 6 回合、每进程每小时 20 回合、同时只跑一个回合。默认关闭。",
|
|
9850
9850
|
"row.imageToolLabel": "允许模型自主生图",
|
|
9851
9851
|
"row.imageToolHint": "开启后模型可以自己调用 workbuddy_draw_image 画图——约 5.71 积分/张,没有逐次确认。关闭时该工具根本不会注册。",
|
|
9852
9852
|
"row.showCreditsCn": "在输入框工具栏显示积分读数",
|
|
9853
9853
|
"row.showCreditsGlobal": "在输入框工具栏显示积分读数",
|
|
9854
|
-
"row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN
|
|
9854
|
+
"row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并一键测试勾选的账号(测试会真实消耗积分)。读数每 5 分钟自动刷新;各账号明细在面板打开时每 60 秒刷新。",
|
|
9855
9855
|
"row.creditPositionLabel": "显示位置",
|
|
9856
9856
|
"row.creditPositionComposer": "输入框工具栏",
|
|
9857
9857
|
"row.creditPositionStats": "输入框下方",
|
|
@@ -9862,7 +9862,7 @@ window.__ModuleLoader__.load({
|
|
|
9862
9862
|
"quotaNotice.fallbackHead": "当前请求的额度已用尽",
|
|
9863
9863
|
"quotaNotice.supplement": "(部分模型另有每日额度,与积分无关)",
|
|
9864
9864
|
"row.creditPositionHint": "积分显示在哪里。「输入框工具栏」是默认位置(与模型选择同一行)。如果装了别的插件后读数不显示,改成「输入框下方」—— 那是「轮次 / 步数 / token」统计所在的那一行。",
|
|
9865
|
-
"row.showCreditsGlobalHint": "在对话输入框工具栏显示国际版(WB AI)剩余积分,仅当本会话使用 WB AI
|
|
9865
|
+
"row.showCreditsGlobalHint": "在对话输入框工具栏显示国际版(WB AI)剩余积分,仅当本会话使用 WB AI 模型时出现。点击可看各账号余额并一键测试勾选的账号(测试会真实消耗积分)。读数每 5 分钟自动刷新;各账号明细在面板打开时每 60 秒刷新。",
|
|
9866
9866
|
"composer.pointsCn": "WB CN",
|
|
9867
9867
|
"composer.pointsGlobal": "WB AI",
|
|
9868
9868
|
"composer.panelTitleCn": "WB CN 积分",
|
|
@@ -5966,7 +5966,7 @@ function generationOf(version) {
|
|
|
5966
5966
|
//#endregion
|
|
5967
5967
|
//#region src/version.ts
|
|
5968
5968
|
/** The npm package version this build was produced from. */
|
|
5969
|
-
const WORKBUDDY_CONNECT_VERSION = "5.5.
|
|
5969
|
+
const WORKBUDDY_CONNECT_VERSION = "5.5.2";
|
|
5970
5970
|
//#endregion
|
|
5971
5971
|
//#region src/host-heartbeat.ts
|
|
5972
5972
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -4306,9 +4306,14 @@ interface WorkBuddyPoolPreferences {
|
|
|
4306
4306
|
* plugin behaves exactly as it did before the pool existed: the account the
|
|
4307
4307
|
* user selected serves every request, and a failure is reported as-is.
|
|
4308
4308
|
*
|
|
4309
|
-
* It deliberately does NOT change
|
|
4310
|
-
*
|
|
4311
|
-
*
|
|
4309
|
+
* It deliberately does NOT change the SAVED selection: while on, the pool
|
|
4310
|
+
* picks the serving account for every request (`applyPoolSelection` ranks
|
|
4311
|
+
* the usable members and sets a per-request override), but that override is
|
|
4312
|
+
* per-request borrowing — turning the pool off hands every request back to
|
|
4313
|
+
* the account the user chose, and the card can keep reporting who really
|
|
4314
|
+
* served. (An older version of this comment said the pool "does NOT change
|
|
4315
|
+
* which account serves NEXT", which was wrong the moment `applyPoolSelection`
|
|
4316
|
+
* started ranking on every request; what never changes is the saved choice.)
|
|
4312
4317
|
*/
|
|
4313
4318
|
enabled?: boolean;
|
|
4314
4319
|
/** 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-hcH37MkG.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) {
|
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.
|
|
5
|
+
"version": "5.5.2",
|
|
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
|
]
|