dsh-connect-workbuddy 4.6.6 → 4.7.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 +89 -0
- package/README.en.md +19 -6
- package/README.md +20 -7
- package/lib/bin.js +1 -1
- package/lib/{host-heartbeat-NHBxcmkQ.js → host-heartbeat-Dd9fsFMR.js} +14 -6
- package/lib/index.d.ts +24 -4
- package/lib/index.js +165 -25
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.7.0 (2026-10-17)
|
|
4
|
+
|
|
5
|
+
> 版本号是 **minor**:这不是只改内部的 patch —— 它**改变了发往上游的请求体**。DSH Desktop 0.2.1-rc.1 随包 pi-ai 1.1.0,本版修的是该组合下「请求静默丢掉 system prompt 与全部工具声明」。若你停留在更早的宿主,行为不变。
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **DSH 0.2.1-rc.1 下所有 workbuddy 模型静默丢失 system prompt 与工具声明(新一轮 #24/#25/#26 同族缺陷)**
|
|
10
|
+
- **症状**:升级到 DSH `0.2.1-rc.1`(随包 `@earendil-works/pi-ai@1.1.0`)后,workbuddy / workbuddy-global 的模型**全部**不再调用工具,界面上**没有任何报错**。模型有时把 DSML 标记当正文吐出来——和 4.4.2 记录的现象一模一样。
|
|
11
|
+
- **根因(三环叠加,缺一不成立)**:
|
|
12
|
+
1. `dsh-llm-pi-ai` **两版都不做归一化**。0.1.7 与 0.2.1 的 `piContext()` 都直接返回 `{systemPrompt, messages, tools}`,两版里 `normalizeContext` 出现次数均为 **0**。
|
|
13
|
+
2. pi-ai **0.85.1 读顶层字段**:`dist/api/openai-completions.js:909` 的 `if (context.systemPrompt)`,工具读 `context.tools`。所以 0.85 能自己兜底,宿主不归一化也看不出问题。
|
|
14
|
+
3. pi-ai **1.1.0 只读 `messages`**:走 `resolveTranscript()` → `collapseSystemMessages()`,顶层 `systemPrompt` / `tools` **从不读取**。
|
|
15
|
+
于是 `withLegacyContext()` 的 **modern 分支原样透传**(当时一行 `return api`),把 0.85 形状的原始信封直接交给 1.1.0 的 api → 请求体里 system prompt 与 tools 双双为空。
|
|
16
|
+
- **为什么 4.4.2 的修复不够**:4.4.2 修的是「1.x 被判成 legacy → 折成 0.85 形状 → 工具丢」。本版是**同一失效模式的反面**——「1.x 判成 modern(判定是对的)→ 不折 → 工具和 prompt 一起丢」。门是对的,空的是那个分支。`generationOf()` 无需改动。
|
|
17
|
+
- **实测证据(真实 `createProvider` + 真实 `openAICompletionsApi` + 本地 SSE 服务器抓线上 body)**:
|
|
18
|
+
|
|
19
|
+
| pi-ai | 线上 `messages[].role` | system 文本 | `body.tools` |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| 0.85.1 | `["system","user"]` | 原样 | 1 ✅ |
|
|
22
|
+
| **1.1.0(修复前)** | **`["user"]`** | **丢失** | **0** |
|
|
23
|
+
| 1.1.0(修复后) | `["system","user"]` | 原样 | 1 ✅ |
|
|
24
|
+
| 1.1.0(已归一化输入) | `["system","user"]` | 原样 | 1 ✅(幂等) |
|
|
25
|
+
|
|
26
|
+
- **改了什么**:新增 `foldModernContext()`,modern 分支改为**归一化**而非透传——把顶层的 `systemPrompt`/`tools` 折进首条 `system` 消息(文本进 `content`、工具进 `toolsAdded`),并**删掉**这两个顶层字段,与 pi-ai 官方 `normalizeContext` 的输出形状一致。
|
|
27
|
+
- **为什么内联而不是调 `normalizeContext`**:该函数在 **0.85.1 里不存在**,静态 import 会在那里变成 `undefined`;而本模块一贯「绝不静态 import pi-ai」(`pi-ai-runtime.ts` 头部教条,正是它让独立 `bin` 能在没装 peer 的 profile 里跑)。内联折叠零新依赖。
|
|
28
|
+
- **幂等**:输入若已归一化(顶层两字段都不存在),直接原样返回——否则会再前置一条**空的** system 消息。
|
|
29
|
+
- **★ 守卫为什么一直没抓到**:`tests/adapter-outgoing-body.spec.ts` 是唯一断言「线上 body 真的带 tools 与 system」的用例,却被 `it.skipIf(piAiRuntimeInfo().generation !== 'legacy')` 限死,**modern 分支零覆盖**;而作者 checkout 解析到 0.85.1,它一直是绿的。`adapter.spec.ts` 也帮不上忙——它 mock 了 api,只断言「交给 wrapper 的 context」,这个断言恰好**认证**了缺陷。
|
|
30
|
+
- **本版把守卫扩成 2×2 矩阵**:{legacy, modern} × {原始信封, 已归一化 transcript} 四格都断言 `body.tools` 与首条 system 文本。在原用例的基础上补了「modern × 原始信封」这一格——**它就是本缺陷的回归钉**。另加幂等用例与「只有 tools、没有 systemPrompt」用例。
|
|
31
|
+
- **本机与 CI 的代际约束(写在用例注释里)**:legacy 两格只能在 0.85 checkout 上跑(1.x 的 api 读不了 0.85 扁平形状);modern 两格只能在 0.87+/1.x 上跑(0.85 的 `estimateMessageTokens` 会对 system 消息的**字符串**逐字符迭代并读 `block.name.length` 而崩溃,即 #24 的崩溃)。用例按**已安装版本**选格,不会让任何一代完全无人看管。
|
|
32
|
+
- **变异验证**:把 modern 分支改回 `return api` → 新增的「FOLDS a raw seam envelope」用例**红**(1 例)。验证后已还原。
|
|
33
|
+
- **实机验证**:临时把 checkout 的 `node_modules/@earendil-works/pi-ai` 换成 1.1.0(连同 1.1.0 新需要的 `typebox`/`partial-json`/`openai`),modern 四格**全绿**;legacy 两格如预期红(1.1.0 读不了扁平形状)。验证后已完整还原 0.85.1 与依赖树。
|
|
34
|
+
|
|
35
|
+
- **「可关闭思考」的模型被锁成不可关闭,不可关闭的反倒可关(`adapter.ts` 既有缺陷,非本次升级引入)**
|
|
36
|
+
- **根因**:`workBuddyThinkingLevelMap()` 的判断写反了 —— `if (canDisableThinking !== true) map.off = null`,而 `null` 在 pi-ai 里的语义是「该档位**不支持**」(`getSupportedThinkingLevels` 按 `mapped === null` 过滤;`openai-completions` 用 `thinkingLevelMap?.off !== null` 决定能否发「关闭思考」的请求)。
|
|
37
|
+
- **实际后果**:上游声明 `canDisableThinking: true` 且用户没取消勾选 → `off` 被置 `null` → 关闭思考**不可选**;上游声明 `false`/未声明 → 不置 `null` → `off` 落成字符串 `'off'` → **反而可选**。与 `offDefaultFor()` 的语义正好相反。
|
|
38
|
+
- **为什么一直没发现**:`tests/adapter.spec.ts` 只探了 `canDisableThinking: false` 那半边,而写反的形式在那半边也恰好通过;另外两条对 `true` 的用例把期望值写成了「没有 off」或「off 缺失」,**把缺陷当成了规格**。
|
|
39
|
+
- **改了什么**:改为 `canDisableThinking === true ? 'off' : null`,并把三条用例的期望值改正,新增「能力未声明时不给 off」一例。
|
|
40
|
+
- **变异验证**:把判断改回 `!== true` → 2 例红。
|
|
41
|
+
|
|
42
|
+
- **DSH 宿主线守卫的 semver 比较器是错的:判 `0.2.1-rc.1` 通过,而 npm 实算是 false**
|
|
43
|
+
- **根因**:`tests/dsh-line.spec.ts` 自写的 `satisfiesRange()` 只做朴素 `>=` / `<` 比较,没有实现 semver 的**预发布悬崖**——预发布版本只有当同一比较器集合里存在**同一 `major.minor.patch` 元组**上的预发布比较符时才被放行。`>=0.1.7-rc.1` 只对 `0.1.7-x` 开口,`<0.3.0-0` 排除整个 `0.3.0` 元组,`0.2.1-rc.1`(元组 `0.2.1`)两条都不满足。
|
|
44
|
+
- **为什么夹具没触发**:原夹具是 `['0.1.7-rc.1','0.1.7-rc.2','0.2.0-rc.1','0.2.0-rc.9','0.2.0','0.2.1','0.2.99']`——恰好跳过真实发布的 `0.2.1-rc.1`;而 `0.2.0-rc.1` / `0.2.0-rc.9` 被断言成 admitted,那本身也是错的(npm 判 false)。
|
|
45
|
+
- **改了什么**:比较器按规范重写(`parseVersion` / `compareParsed` / `sameTuple` + 悬崖规则),夹具拆成「正式版放行」与「已知预发布偏差」两组,并新增 5 条照抄 npm 实际输出的悬崖用例。
|
|
46
|
+
- **新增的「已知偏差」用例**:显式断言 `0.2.1-rc.1` 与 `0.2.0-rc.1` 在现声明下 **不** admitted、而 `0.2.1` GA admitted。于是偏差是**被记录、被解释**的,而不是被错比较器掩盖;将来若改清单,这条会提醒同步更新说明。
|
|
47
|
+
- **为什么可以暂不改清单**:详见下一条。宿主 `evaluatePluginCompatibility()` 只校验 `@deepseek-ai/dsh*` 命名空间,本机 profile 用 `link:` + 默认不自动装 peer,rc 宿主上插件照常加载;等宿主发 `0.2.1` GA,现声明即放行。
|
|
48
|
+
- **变异验证**:去掉悬崖规则 → 2 例红。
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- **`@earendil-works/pi-ai` 的 peer 范围放宽到 1.x:`>=0.85.0 <0.88.0 || >=1.0.0-0 <2.0.0`(peer 与 dev 两处同步)**
|
|
53
|
+
- **为什么必须改**:旧范围 `>=0.85.0 <0.88.0` **不**放行 DSH 0.2.1-rc.1 随包的 1.1.0,而 1.1.0 恰恰是本版修好后要支持的版本 —— 声明与实际支持面自相矛盾。
|
|
54
|
+
- **范围是实算出来的,不是猜的**:`0.85.0 / 0.85.1 / 0.87.1 / 1.0.0 / 1.0.2 / 1.1.0` 均 `true`;`2.0.0` 与 `1.1.0-beta.1` 均 `false`。上界停在 `<2.0.0` 与 `generationOf()` 刻意不认 2.x 的判断保持一致;预发布串不承诺。
|
|
55
|
+
- **注意**:宿主门禁只校验 `@deepseek-ai/dsh*`,所以这条 peer **从不参与**宿主校验(4.4.2 已记录)。改它主要是让**清单说真话**,真正保护用户的是上面那条代码修复。
|
|
56
|
+
- **`dsh-*` 那 9 条 peer 本版未动**:理由见上一条「已知偏差」——任何为 `0.2.x` rc 硬凑的静态范围都会有漏(已逐一实算否证:`|| >=0.2.0-rc.1 <0.2.99`、`|| >=0.3.0-0 <0.3.0`、`>=0.1.7-rc.1-0 <0.3.0-0`、去掉预发布上界、`0.2.x` 通配符,全部失败)。
|
|
57
|
+
|
|
58
|
+
### CI
|
|
59
|
+
|
|
60
|
+
- **新增 `test-pi-ai-next` 作业:在 pi-ai 1.1.0 上跑一遍完整 typecheck + 测试 + 构建**
|
|
61
|
+
- **为什么必须有**:`tests/adapter-outgoing-body.spec.ts` 的矩阵是 2×2,但**一个代际跑不了另一个代际的库**——0.85 checkout 上只能跑 legacy 两格(1.x 的 api 读不了扁平形状),1.x checkout 上只能跑 modern 两格(0.85 的分词器在 system 消息上崩,即 #24)。主 `test` 作业钉的是 0.85.1,**modern 半边从未在 CI 里运行过**,而这正是 0.2.1-rc.1 坏掉的那一半。
|
|
62
|
+
- **怎么切版本**:pi-ai 既是 peer 又是直接 devDependency,`pnpm-workspace.yaml` 的 `overrides` 是官方支持的改法。作业用 `sed` 改那一条 override,再 `--no-frozen-lockfile` 让 pnpm 只为它重解析。
|
|
63
|
+
- **有一道断言防止作业空转**:装完立刻 `node -p` 读实际版本并要求等于矩阵值,否则「跑了一遍绿」可能跑的根本不是 1.1.0。
|
|
64
|
+
- **本作业当场抓到一个本机永远看不到的缺陷**:pi-ai 1.x 给 provider 侧上下文加了**品牌**(`TranscriptContext`,`unique symbol` 字段,声明为「只有 `normalizeContext()` 能产出」)。在 1.x 的**类型**上,`stream`/`streamSimple` 的两条分支都收到品牌类型,于是把普通 `Context` 传进去会 `TS2345`——而 `modern` 分支的 `return api` 也一并被类型系统拒绝。本机 0.85 的类型面是裸 `Context`,**这个错误在 0.85 上不存在**。修法是一处**有注释的** `transcript<T>()` 断言,理由写在函数上:调用点已经完成 `normalizeContext` 的职责,而 `normalizeContext` 在 0.85 不存在、不能导入。**变异验证**:去掉该断言 → 1.1.0 上 `tsc` 报 4 个 `TS2345`。
|
|
65
|
+
- **验证过它真的会响**:把 modern 分支改回 `return api`,1.1.0 作业**红 4 例**(`adapter.spec.ts` 与 `adapter-outgoing-body.spec.ts` 各自的 modern 守卫),而 0.85.1 作业保持全绿。这正是本作业存在的意义。
|
|
66
|
+
- 仅跑 ubuntu:这是库层面的断言,不是平台断言;OS 矩阵已由主 `test` 作业覆盖。
|
|
67
|
+
|
|
68
|
+
### Docs
|
|
69
|
+
|
|
70
|
+
- `src/pi-ai-runtime.ts` 头部更正:peer 范围同步为新值;删去把 1.x 说成「未发布」的过时表述(DSH 0.2.1-rc.1 已随包 1.1.0);补一句「落在 modern 是必要但**不充分**——modern 分支还必须折叠原始信封」。
|
|
71
|
+
- `src/adapter.ts` 的 `adaptLegacyPiAiContext()` 补注 **legacy-only**:它的结果是 0.85 扁平形状,绝不能交给 1.x 的 api(1.x 从 `messages[].toolsAdded` 读工具,会一个都看不到)。
|
|
72
|
+
- 新增 `docs/DSH-0.2.1-RC1-FOLLOWUP.md`:本次升级的完整调研档,含逐包对照表、四项「已核对无需改动」、失败候选与证据来源。
|
|
73
|
+
|
|
74
|
+
## 4.6.7 (2026-10-17)
|
|
75
|
+
|
|
76
|
+
> 版本号是 **patch**:只改账号池的排序规则,没有接口或配置变更。但它是**行为变更**——开启账号池的机器上,「这次请求用谁」可能变了,且新行为正是本版要的。
|
|
77
|
+
|
|
78
|
+
### Fixes
|
|
79
|
+
|
|
80
|
+
- **「快过期的积分优先」从未生效:第 3 个排序键被第 2 个键完全遮蔽,临期积分必然作废(issue #70)**
|
|
81
|
+
- **症状**:开启账号池后,`当前使用` 永远是积分总量最高的那个账号;哪怕另一个账号有积分**当天就要过期**,它也一直排最后,直到临期积分作废。
|
|
82
|
+
- **根因**:`rankPool()` 的排序键是**严格字典序**——先比积分总量(`scoreOf()` = `credits.total`),**只有总量完全相等**才比到期时间。而总量几乎从不恰好相等,所以「快过期优先」这条规则**在数学上不可达**。报告者的真实病例:账号 C 有 **196.4 分当天 17:21 到期**,只因为比 A/B 少约 1,200 分,即永远排最后,那 196.4 分必然烂掉。
|
|
83
|
+
- **为什么这条特别值得记**:README 把这两条写成**并列的第 2、3 条**,读起来像两个同级偏好;代码里却是严格优先级。**一条「不花就作废」的规则被另一条完全压制,等于死代码**,而它从输出上看不出任何异常——顺序永远「有道理」,只是那条规则从不参与。
|
|
84
|
+
- **改了什么**:把临期判据提到总量之前,并拆成三个独立的键——**有临期积分者优先** → **先到期者优先** → **临期量大者优先**,之后才回到积分总量。
|
|
85
|
+
- **一个单位错误顺手修掉**:旧的 `_expiring` 键实际存的是 `nearestExpiryMs`(**时间戳**),而 `expiringSoon`(**积分量**)在排序里从未被读过。现在两者分开:`_expiring` 仍是时间戳(用于「谁先到期」),`_expiringAmount` 才是积分量。直接拿这两个相减会是一个不容易发现的数量级错误。
|
|
86
|
+
- **一个反直觉但正确的后果,写进了代码注释与两份 README**:这条规则会让**临期 1 分**的账号排到**余额 5 万**的账号前面。**这不是缺陷**——积分是通用的,不花那 1 分它**确定**会损失;而余额 5 万的账号下一轮还在。唯一的真实代价是临期账号可能不够跑完一个请求,此时由既有的换号逻辑接手。若按总量优先,则临期积分**确定**作废。
|
|
87
|
+
- **可用性仍然第一**:处于限流冷却/凭据被拒的账号即使有大量临期积分也**不会**被提前——它花不掉,提上去只会让每个请求都撞墙。
|
|
88
|
+
- **测试**:新增 6 例(报告者真实数据 `C → A → B`、无临期时仍按总量、临期 1 分压过 5 万、都在临期时先到期者优先、同到期时量大者优先、不可用账号不被提升)。**变异验证**:① 恢复旧的字典序(总量先判)→ 红 4 例;② 去掉「先到期者优先」→ 红 2;③ `_atRisk` 恒为 false(等价修复前)→ 红 2。
|
|
89
|
+
- **顺带修正一条名不副实的旧测试**:`prefers the account whose credits expire soonest when totals tie` 的两个夹具都只有 `nearestExpiryMs`、`expiringSoon` 为 0,即**两个账号都没有任何临期积分**。它在旧规则下能过,靠的正是「排序看时间戳、不看量」这个缺陷;现已补上真实的 `expiringSoon`,让它真的在测名字里那件事。
|
|
90
|
+
- **README**(中英同步):排序规则把「快过期先花」提到第 2 条、「积分高者优先」降到第 3 条,并就地写明旧行为为何是死代码、以及「临期 1 分压过 5 万」为何是对的。
|
|
91
|
+
|
|
3
92
|
## 4.6.6 (2026-10-17)
|
|
4
93
|
|
|
5
94
|
> 版本号是 **patch**:只动诊断(`WORKBUDDY_BODY_REPORT` 的报告内容),不改请求、响应、计费或界面。**默认行为完全不变**——报告默认仍是关的,关着时这版与 4.6.5 行为等价。
|
package/README.en.md
CHANGED
|
@@ -32,7 +32,7 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
32
32
|
- **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 WB CN / WB AI credits in the composer” in that region's tab on the card; while a region's provider is switched off, the readout disappears and its switch is disabled.
|
|
33
33
|
|
|
34
34
|
<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 refresh button and a last-refreshed time" />
|
|
35
|
-
- **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 higher credits,
|
|
35
|
+
- **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).
|
|
36
36
|
- **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.
|
|
37
37
|
- **Add accounts by QR sign-in, import/export them in bulk, test or delete them one by one** — the WorkBuddy desktop app is no longer a prerequisite: the card's **Add account by QR code** (labelled **Sign in on the web** for the international build — that region uses a browser login page, there is no QR) opens a dialog and takes you **straight to the authorization page** in your browser; if the browser blocks that, the dialog's link, copy and **Open page again** are the fallback. Every credential lands in the plugin's own vault (`$DSH_HOME/workbuddy-vault/<region>/<accountId>.json`, one file per account), and the desktop app's auth files stay **read-only and are never modified**. Each row of the account table gains **Test** and **Remove**: a row test reuses the same probe as the batch run, so the two are comparable; removing deletes only the copy the plugin stored and **hides** the account: it disappears at once and the automatic poll cannot bring it back, while **Detect accounts again** restores it (signing in or importing the same account clears the hide too). **The desktop app's sign-in is never touched** — removing means "this plugin no longer lists it", not "sign out of the app". **Export / import accounts** moves credentials to another machine: pick accounts to export a JSON file → choose a file and read a **redacted preview** (field names and whether a token is present; the token itself never enters the preview) → tick what to import. An import overwrites per account rather than duplicating, and **only a record positively identified as belonging to that region is stored** — a domain the plugin cannot classify is skipped and counted as skipped. See [Account pool](#account-pool).
|
|
38
38
|
- **Credits live in the account pool** — the member table shows each account's remaining credits and nearest expiry, and credits are the ranking's second key (more credits serves first). The queries are **read-only** and consume no credits.
|
|
@@ -118,11 +118,15 @@ The last two rows are deliberate: check-in and testing are MANUAL actions over t
|
|
|
118
118
|
### The ranking (what decides who serves)
|
|
119
119
|
|
|
120
120
|
1. **Usable first** — an account a measurement rules out (limited with its cooldown unexpired, credential rejected, upstream unreachable) sorts last and is not chosen;
|
|
121
|
-
2. **
|
|
122
|
-
3. **
|
|
121
|
+
2. **Expiring credits first** — points lapsing within three days are lost if unspent, so an account holding any comes first, the soonest-lapsing one ahead of the rest, and between two of them the larger amount at risk wins;
|
|
122
|
+
3. **Higher credit balance first.** Among accounts with nothing expiring, the largest balance goes first. Credits take part **only when a fresh reading exists** (what the card's poll, a batch test or a save collected; valid for 10 minutes). The request path **never** fetches credits — that is an upstream call per member, and one page of chat would turn into N extra requests;
|
|
123
123
|
4. **Freshest credential first**;
|
|
124
124
|
5. The account id closes the order, making it **total** and reproducible rather than jittering with the roster's order.
|
|
125
125
|
|
|
126
|
+
> **Why #2 outranks #3** (issue #70): these used to be strict lexicographic keys — total first, expiry consulted only when totals were **exactly** equal. Totals are almost never exactly equal, so the expiry rule **never actually fired**: a "use it or lose it" rule wholly shadowed by another, i.e. dead code. In the reported case an account held **196.4 points lapsing that same day** and still ranked last, merely because it held ~1,200 fewer points than the other two — so all 196.4 were lost.
|
|
127
|
+
>
|
|
128
|
+
> The cost is worth stating plainly: this also puts an account with **1 expiring point** ahead of one holding **50,000**. That is not a defect — credits are fungible, so leaving the 1 point unspent loses it with certainty, whereas the 50,000 account is still there on the next request. The one real cost is a failover round-trip when the expiring account cannot cover a whole request, which the existing failover path absorbs.
|
|
129
|
+
|
|
126
130
|
### Why an account leaves the pool, and when it comes back
|
|
127
131
|
|
|
128
132
|
| Probe outcome | What it means for failover |
|
|
@@ -148,7 +152,7 @@ One class of state in the pool **does not go through a draft**: probe results, c
|
|
|
148
152
|
|
|
149
153
|
## Install
|
|
150
154
|
|
|
151
|
-
> ⚠️ **Version requirement: DSH 0.1.7-rc.1 or newer (current window: `0.1.7-rc.1` – `0.2.x`).** Since v2.1.0 this plugin only supports hosts on DSH 0.1.7-rc.1 and above (older hosts cannot resolve the dependencies to install this version); hosts on 0.1.5 and earlier should stay on v2.0.15. Since v2.1.2 the upper bound is **`<0.3.0-0
|
|
155
|
+
> ⚠️ **Version requirement: DSH 0.1.7-rc.1 or newer (current window: `0.1.7-rc.1` – `0.2.x` GA).** Since v2.1.0 this plugin only supports hosts on DSH 0.1.7-rc.1 and above (older hosts cannot resolve the dependencies to install this version); hosts on 0.1.5 and earlier should stay on v2.0.15. Since v2.1.2 the upper bound is **`<0.3.0-0`**; the 0.3.0 line is explicitly refused. See [Supported host range](#supported-host-range) below.
|
|
152
156
|
|
|
153
157
|
### Supported host range
|
|
154
158
|
|
|
@@ -157,11 +161,20 @@ DSH's shipped gate (`evaluatePluginCompatibility` in `packages/boot/app-boot/src
|
|
|
157
161
|
| Host version | Verdict |
|
|
158
162
|
| --- | --- |
|
|
159
163
|
| `0.1.7-rc.1` – `0.1.7-rc.2` | ✅ supported |
|
|
160
|
-
| `0.2.
|
|
164
|
+
| `0.2.x` **GA** (`0.2.0`, `0.2.1`, …) | ✅ supported (verified from 2.1.2) |
|
|
165
|
+
| `0.2.x` **prerelease** (`0.2.0-rc.1`, `0.2.1-rc.1`, …) | ⚠️ outside the declared range — see below |
|
|
161
166
|
| `0.3.0-0` and above | ❌ explicitly refused (that line is unverified) |
|
|
162
167
|
| `0.1.6` and earlier | ❌ explicitly refused (0.1.5 users should stay on v2.0.15) |
|
|
163
168
|
|
|
164
|
-
>
|
|
169
|
+
> **On `0.2.x` prereleases (corrected in v4.7.0)**: this table previously listed `0.2.0-rc.1` as "✅ supported". That was **wrong**.
|
|
170
|
+
> npm semver admits a prerelease only when a comparator carries a prerelease on the **same `major.minor.patch` tuple**.
|
|
171
|
+
> The declared `>=0.1.7-rc.1 <0.3.0-0` opens only `0.1.7-x`, while `<0.3.0-0` excludes the `0.3.0` tuple — so versions such as
|
|
172
|
+
> **`0.2.1-rc.1` satisfy neither comparator** (`satisfies('0.2.1-rc.1', …) === false`).
|
|
173
|
+
> The practical impact is small: the host gate only evaluates the `@deepseek-ai/dsh*` namespace, and on rc hosts this plugin loads
|
|
174
|
+
> normally via `link:` / a manual install. It is also **not fixable by editing the range** — no static range can cover "any `0.2.x`
|
|
175
|
+
> prerelease"; five candidate formulations were computed and refuted (see CHANGELOG 4.7.0). It resolves itself once the host ships `0.2.1` GA.
|
|
176
|
+
|
|
177
|
+
> The upper bound is written `<0.3.0-0`, not `<0.3.0`: **every DSH tag to date is a prerelease** (`-alpha.N` / `-rc.N`), so `<0.3.0` alone would admit the entire unverified `0.3.0-alpha.1` / `0.3.0-rc.1` line, leaving the guardrail inert — exactly the defect shape 2.1.2 fixes. `tests/dsh-line.spec.ts` guards this contract, including the prerelease cliff.
|
|
165
178
|
|
|
166
179
|
> 🪟 **Platform support: Windows, macOS, and Linux are supported side by side.** **Windows is a first-class target**, not an afterthought: CI runs the full suite on `windows-latest`, and four areas carry Windows-specific branches with their own tests — credential paths, process heartbeat, desktop-app executable lookup, and settings writes. **When fixing a defect that only reproduces on Windows, the Windows reporter's field evidence and an encryption vector captured from a real Windows install are what the fix is judged against** (see the 2.0.6 / 2.0.11 / 2.0.12 sections of [CHANGELOG](CHANGELOG.md)). See the [Platform support](#platform-support) section for the per-platform differences and known limitations.
|
|
167
180
|
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
- **对话输入框积分读数(国内版 / 国际版自动切换)** —— 对话输入框左侧工具栏常驻一行可点击的读数(如 `WB CN · 1,072`),**按本会话当前选中的模型自动切换区域**:选中 WB CN 的模型就显示国内版余额,选中 WB AI 的模型就显示国际版余额;用其它 provider 的模型时该读数完全不出现。每 5 分钟自动刷新,也可点开手动刷新。点击读数弹出面板:**按账号列出的余额表格**,点某一行即可切换到该账号(与卡片上的账号选择是同一个设置);**当前不能用的账号会在行内标出原因**(如「被限流」「积分耗尽」),标注口径与账号池表格一致。面板里的余额只在打开时才去查,因此不会让 5 分钟的后台刷新按账号数翻倍。要关掉某一边,就在卡片对应区域的 tab 里取消勾选「在对话框显示 WB CN / WB AI 积分」;该区域的供应商被关闭时,读数不显示、开关也置灰。
|
|
40
40
|
|
|
41
41
|
<img src="docs/assets/dsh-connect-workbuddy-composer-credits.png" width="360" alt="对话输入框积分面板:国内版账号余额表格、刷新积分按钮与最近刷新时间" />
|
|
42
|
-
- **账号池:多账号批量操作与自动换号** —— 把要用的账号勾进池子,即可**一键签到所有账号**、**一键测试所有账号 × 你指定的模型**(默认挑该区域倍率为 0
|
|
42
|
+
- **账号池:多账号批量操作与自动换号** —— 把要用的账号勾进池子,即可**一键签到所有账号**、**一键测试所有账号 × 你指定的模型**(默认挑该区域倍率为 0 的免费模型);开启**启用账号池**即同时打开**自动换号**——某个请求在上游失败时,插件会依次用池内其他可用账号重试,直到有一个接手、或全部不可用为止,然后才把错误报给你。成员**默认一个都不含**、必须显式勾选(「本机有登录」不等于「要拿它去签到和花积分」);**开启后由池子的排序决定每一次请求用谁**(可用优先,其次快过期先花、积分高、凭据更新的);失败时依次用池内其他可用账号重试。排序**从不写回你保存的选择**——关掉开关,你选定的账号立刻完全生效。只有请求本身不合法(HTTP 400)不换号:同一个请求体换谁都是 400,重试只会白等。**这个开关只管「插件要不要自己决定」**:关掉它,插件不再替你选账号、也不再替你换号,但**签到与测试仍然可用**(它们是手动动作)。测试走的是与模型行同一套真实体积探测,因此**有真实成本**;**不提供自动签到**——签到会真实领取奖励,只在你点按钮时执行。详见[账号池](#账号池)。
|
|
43
43
|
- **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
|
|
44
44
|
- **扫码添加账号、批量导入导出、逐账号测试/删除** —— 不必先装 WorkBuddy 桌面 App:卡片里的「扫码添加账号」(国际版为「网页登录」——那边是浏览器登录页,没有二维码)点开即弹出对话框,并在浏览器里**自动打开**授权页;被浏览器拦截时对话框内提供链接、复制与「重新打开页面」兜底。所有凭据统一落到插件自己的凭据库(`$DSH_HOME/workbuddy-vault/<region>/<accountId>.json`,一账号一文件),**桌面 App 的认证文件始终只读、永不被改动**。账号表格每一行右侧有「测试」与「删除」:测试与批量测试走同一套探测,结果可比;删除只删插件自己存的那份,并把这个账号**暂时隐藏**:删掉后立刻消失,也不会被自动轮询带回来;点「重新检测账号」即可找回(重新登录或导入同一账号也会清除隐藏记录)。**桌面 App 的登录始终不动**——删除是「本插件不再列出它」,不是退出 App 登录。「导出 / 导入账号」可把凭据搬到另一台机器:多选导出 JSON → 选文件后先看**脱敏预览**(只显示字段与是否有 token,明文不进预览)→ 勾选导入。导入按账号去重覆盖,**只有确认属于该区域**的记录才入库,认不出的域名会被跳过并如实计入「已跳过」。详见[账号池](#账号池)。
|
|
45
45
|
- **积分信息在账号池里** —— 成员表逐账号显示剩余积分与最近到期时间,排序的第二键就是积分(谁多谁优先)。查询是**只读**的、不消耗积分。
|
|
@@ -94,7 +94,7 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
94
94
|
|
|
95
95
|
| | 账号池 **开启** | 账号池 **关闭** |
|
|
96
96
|
|---|---|---|
|
|
97
|
-
| 谁服务 | 由排序**自动**选出(可用优先 →
|
|
97
|
+
| 谁服务 | 由排序**自动**选出(可用优先 → 快过期先花 → 积分高 → 凭据新) | **你在账号池里手动选**「使用哪个账号」 |
|
|
98
98
|
| 请求失败 | **自动**换下一个可用账号重试 | 不换号,如实报错 |
|
|
99
99
|
| 一键签到 / 一键测试 | 可用 | **仍然可用** |
|
|
100
100
|
| 目标测试模型 | 可改 | **仍然可改** |
|
|
@@ -108,11 +108,15 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
108
108
|
### 排序规则(开启账号池后决定谁服务)
|
|
109
109
|
|
|
110
110
|
1. **可用优先** —— 被测量判定为不可用(限流未到恢复时间、凭据被拒、连通性失败等)的排到最后,不参与选择;
|
|
111
|
-
2.
|
|
112
|
-
3.
|
|
111
|
+
2. **快过期的积分先花** —— 3 天内到期的积分用不掉就作废,所以**有临期积分的账号排在前面**,**先到期的先排**;两个都在临期时,**临期积分多的优先**;
|
|
112
|
+
3. **积分高者优先** —— 在没有临期积分的账号里,选积分最多的。**积分只在有新鲜读数时参与**(卡片轮询、批量测试或保存时读到的那份,10 分钟内有效):请求路径**不会**为了排序去打上游,否则一页对话就会变成 N 个额外请求;
|
|
113
113
|
4. **凭据更晚过期的优先**;
|
|
114
114
|
5. 最后按账号 id 收尾,保证顺序是**全序**、可复现,不会因为账号表顺序变化而抖动。
|
|
115
115
|
|
|
116
|
+
> **第 2 条为什么排到第 3 条前面**(issue #70):这两条此前是**严格字典序**——先比积分总量,只有总量**完全相等**才比到期时间。而总量几乎从不恰好相等,于是「快过期优先」**实际上从未生效过**:一条「不花就作废」的规则被另一条完全遮蔽,等于死代码。真实病例里,某账号有 **196.4 分当天作废**,只因为比另两个账号少约 1,200 分就永远排在最后,那 196 分必然烂掉。
|
|
117
|
+
>
|
|
118
|
+
> 代价也要说清:这条规则会让**临期 1 分**的账号排到**余额 5 万**的账号前面。这不是缺陷——积分是通用的,不花那 1 分它**确定**会损失;而余额 5 万的账号下一轮还在。唯一的真实代价是临期账号可能不够跑完一个请求,这时由既有的换号逻辑接手。
|
|
119
|
+
|
|
116
120
|
### 两个批量动作
|
|
117
121
|
|
|
118
122
|
- **一键签到所有账号** —— 逐个账号签到。**签到前先查当天状态**,已签到的直接标记为「已签到」而**不发写入请求**(幂等,不重复领取);任何单个账号失败只标记该行,**不中断整批**。
|
|
@@ -156,7 +160,7 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
156
160
|
|
|
157
161
|
## 安装
|
|
158
162
|
|
|
159
|
-
> ⚠️ **版本要求:DSH 0.1.7-rc.1 及以上(当前窗口:`0.1.7-rc.1` ~ `0.2.x
|
|
163
|
+
> ⚠️ **版本要求:DSH 0.1.7-rc.1 及以上(当前窗口:`0.1.7-rc.1` ~ `0.2.x` 正式版)。** 自 v2.1.0 起本插件只支持 DSH 0.1.7-rc.1 及以上的宿主(旧版宿主无法通过依赖解析安装本版);0.1.5 及更早的宿主请停留在 v2.0.15。自 v2.1.2 起上界放宽为 **`<0.3.0-0`**,0.3.0 线被明确拒绝。详见下方[受支持的宿主范围](#受支持的宿主范围)。
|
|
160
164
|
|
|
161
165
|
### 受支持的宿主范围
|
|
162
166
|
|
|
@@ -165,11 +169,20 @@ DSH 的随包门禁(`packages/boot/app-boot/src/plugin-compatibility.ts` 的 `
|
|
|
165
169
|
| 宿主版本 | 判定 |
|
|
166
170
|
| --- | --- |
|
|
167
171
|
| `0.1.7-rc.1` ~ `0.1.7-rc.2` | ✅ 支持 |
|
|
168
|
-
| `0.2.
|
|
172
|
+
| `0.2.x` **正式版**(`0.2.0`、`0.2.1` …) | ✅ 支持(2.1.2 起实测) |
|
|
173
|
+
| `0.2.x` **预发布**(`0.2.0-rc.1`、`0.2.1-rc.1` …) | ⚠️ 不落在声明范围内,见下方说明 |
|
|
169
174
|
| `0.3.0-0` 及以上 | ❌ 明确拒绝(该线尚未验证) |
|
|
170
175
|
| `0.1.6` 及更早 | ❌ 明确拒绝(0.1.5 线请停留在 v2.0.15) |
|
|
171
176
|
|
|
172
|
-
>
|
|
177
|
+
> **关于 `0.2.x` 预发布(v4.7.0 更正)**:此前本表把 `0.2.0-rc.1` 写作「✅ 支持」,那是**错的**。
|
|
178
|
+
> npm semver 规定:预发布版本只有当一个比较器**在同一条 `major.minor.patch` 元组上**带预发布标签时才被放行。
|
|
179
|
+
> 现声明 `>=0.1.7-rc.1 <0.3.0-0` 只对 `0.1.7-x` 开口,`<0.3.0-0` 又排除了 `0.3.0` 元组,因此
|
|
180
|
+
> **`0.2.1-rc.1` 这类版本两条比较器都不满足**(`satisfies('0.2.1-rc.1', …) === false`)。
|
|
181
|
+
> 实际影响很小:宿主门禁只校验 `@deepseek-ai/dsh*` 命名空间,且 rc 宿主上本插件按 `link:` / 手工安装照常加载。
|
|
182
|
+
> 这**不是**靠改范围能解决的问题——任何静态范围都覆盖不了「任意 `0.2.x` 预发布」,已逐一实算否证五种写法
|
|
183
|
+
> (见 CHANGELOG 4.7.0)。等宿主发 `0.2.1` 正式版即自然成立。
|
|
184
|
+
|
|
185
|
+
> 上界写作 `<0.3.0-0` 而非 `<0.3.0`:DSH 至今**全部 tag 都是预发布**(`-alpha.N` / `-rc.N`),只写 `<0.3.0` 会放行整条 `0.3.0-alpha.1` / `0.3.0-rc.1` 这条从未验证过的线,围栏形同虚设——这正是 2.1.2 修掉的缺陷形状。该口径由 `tests/dsh-line.spec.ts` 守卫(含预发布悬崖的用例)。
|
|
173
186
|
|
|
174
187
|
> 🪟 **平台支持:Windows、macOS、Linux 三者并列支持。** **Windows 是一等目标平台**,不是「顺带能跑」:CI 在 `windows-latest` 上跑完整套件,凭据路径、进程心跳、App 可执行文件定位、设置写入四处都有 Windows 专属分支与测试,并且**修复只对 Windows 生效的缺陷时,以 Windows 用户回报的现场与真机取证的加密向量为准**(见 [CHANGELOG](CHANGELOG.md) 的 2.0.6 / 2.0.11 / 2.0.12 各节)。见下方[平台支持](#平台支持)一节了解各平台的具体差异与已知限制。
|
|
175
188
|
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-
|
|
2
|
+
import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-Dd9fsFMR.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
|
@@ -3801,9 +3801,9 @@ var WorkBuddyCatalog = class {
|
|
|
3801
3801
|
* Which `@earendil-works/pi-ai` copy THIS plugin actually resolves at runtime.
|
|
3802
3802
|
*
|
|
3803
3803
|
* WHY THIS EXISTS (issues #24 / #25 / #26). The plugin declares pi-ai as a
|
|
3804
|
-
* peer (`>=0.85.0 <0.88.0`), so the module it imports is
|
|
3805
|
-
* environment provides — and the two generations consume
|
|
3806
|
-
* shapes:
|
|
3804
|
+
* peer (`>=0.85.0 <0.88.0 || >=1.0.0-0 <2.0.0`), so the module it imports is
|
|
3805
|
+
* whatever the host environment provides — and the two generations consume
|
|
3806
|
+
* DIFFERENT context shapes:
|
|
3807
3807
|
*
|
|
3808
3808
|
* - 0.87 hands providers a normalized transcript: `{ messages: [ {role:
|
|
3809
3809
|
* 'system', content, toolsAdded}, … ] }`. The system prompt and the tool
|
|
@@ -3920,8 +3920,9 @@ function readVersion(moduleFile) {
|
|
|
3920
3920
|
* doctor shows it as unreadable, so the guess is visible rather than silent).
|
|
3921
3921
|
*
|
|
3922
3922
|
* The 1.x line MUST land on `modern`, and it is not a hypothetical: DSH
|
|
3923
|
-
* 0.2.1-alpha.2 ships `@earendil-works/pi-ai@1.0.2`,
|
|
3924
|
-
*
|
|
3923
|
+
* 0.2.1-alpha.2 ships `@earendil-works/pi-ai@1.0.2`, and DSH 0.2.1-rc.1 ships
|
|
3924
|
+
* 1.1.0 (which is the version this plugin is now tested against). Both 0.87.1
|
|
3925
|
+
* and 1.0.2/1.1.0 read tools from the same place
|
|
3925
3926
|
* (`context.messages` → the leading system message's `toolsAdded`; see
|
|
3926
3927
|
* pi-ai's `resolveTranscriptTools`). Matching only `0.<minor>.` classified
|
|
3927
3928
|
* 1.0.2 as `legacy`, so `withLegacyContext` folded the transcript into the
|
|
@@ -3937,6 +3938,13 @@ function readVersion(moduleFile) {
|
|
|
3937
3938
|
* observed 1.x line (including current 1.1.0) without assuming an uninspected
|
|
3938
3939
|
* future major such as 2.x keeps the same transcript contract. The declared
|
|
3939
3940
|
* peer range remains the plugin's supported-version boundary.
|
|
3941
|
+
*
|
|
3942
|
+
* ⚠️ Being on `modern` is necessary but NOT sufficient. The modern branch must
|
|
3943
|
+
* also FOLD a raw seam envelope: `dsh-llm-pi-ai` never normalizes, and 1.x
|
|
3944
|
+
* reads only `context.messages`. Between 4.4.2 (which taught this function
|
|
3945
|
+
* about major 1) and the 0.2.1-rc.1 fix, `withLegacyContext` returned the
|
|
3946
|
+
* modern api UNWRAPPED, so every request on pi-ai 1.x went upstream with no
|
|
3947
|
+
* prompt and no tool declarations. See `foldModernContext` in `adapter.ts`.
|
|
3940
3948
|
*/
|
|
3941
3949
|
function generationOf(version) {
|
|
3942
3950
|
if (version === void 0) return "legacy";
|
|
@@ -3949,7 +3957,7 @@ function generationOf(version) {
|
|
|
3949
3957
|
//#endregion
|
|
3950
3958
|
//#region src/version.ts
|
|
3951
3959
|
/** The npm package version this build was produced from. */
|
|
3952
|
-
const WORKBUDDY_CONNECT_VERSION = "4.
|
|
3960
|
+
const WORKBUDDY_CONNECT_VERSION = "4.7.0";
|
|
3953
3961
|
//#endregion
|
|
3954
3962
|
//#region src/host-heartbeat.ts
|
|
3955
3963
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -1200,9 +1200,9 @@ declare function createWorkBuddyShim(options: WorkBuddyShimOptions): WorkBuddySh
|
|
|
1200
1200
|
* Which `@earendil-works/pi-ai` copy THIS plugin actually resolves at runtime.
|
|
1201
1201
|
*
|
|
1202
1202
|
* WHY THIS EXISTS (issues #24 / #25 / #26). The plugin declares pi-ai as a
|
|
1203
|
-
* peer (`>=0.85.0 <0.88.0`), so the module it imports is
|
|
1204
|
-
* environment provides — and the two generations consume
|
|
1205
|
-
* shapes:
|
|
1203
|
+
* peer (`>=0.85.0 <0.88.0 || >=1.0.0-0 <2.0.0`), so the module it imports is
|
|
1204
|
+
* whatever the host environment provides — and the two generations consume
|
|
1205
|
+
* DIFFERENT context shapes:
|
|
1206
1206
|
*
|
|
1207
1207
|
* - 0.87 hands providers a normalized transcript: `{ messages: [ {role:
|
|
1208
1208
|
* 'system', content, toolsAdded}, … ] }`. The system prompt and the tool
|
|
@@ -1300,7 +1300,27 @@ declare function workBuddyModelInput(info: WorkBuddyModelInfo): ('text' | 'image
|
|
|
1300
1300
|
* `x0.00`, matching WorkBuddy's rendering of free models.
|
|
1301
1301
|
*/
|
|
1302
1302
|
declare function workBuddyDisplayName(info: WorkBuddyModelInfo): string;
|
|
1303
|
-
/**
|
|
1303
|
+
/**
|
|
1304
|
+
* Map only levels advertised by WorkBuddy; undeclared DSH levels stay unavailable.
|
|
1305
|
+
*
|
|
1306
|
+
* `off` is offered EXACTLY when `canDisableThinking === true` and withheld
|
|
1307
|
+
* (mapped to `null`) otherwise. `null` is pi-ai's "this level is not
|
|
1308
|
+
* supported": `getSupportedThinkingLevels` filters on `mapped === null`, and
|
|
1309
|
+
* `openai-completions` reads `thinkingLevelMap?.off !== null` to decide whether
|
|
1310
|
+
* it may send a thinking-disabled request.
|
|
1311
|
+
*
|
|
1312
|
+
* This condition used to be inverted (`!== true` set the null), which locked
|
|
1313
|
+
* `off` OFF for precisely the models that CAN disable thinking while leaving it
|
|
1314
|
+
* ON for those that cannot. It stayed invisible because the only test probed
|
|
1315
|
+
* the `canDisableThinking: false` half, which the inverted form also passes.
|
|
1316
|
+
*
|
|
1317
|
+
* The value read here is already the EFFECTIVE one: the runtime catalog is
|
|
1318
|
+
* stamped by `withEffectiveOff()` before it reaches the adapter, so this is the
|
|
1319
|
+
* user's `off` selection when they made one (and the upstream declaration,
|
|
1320
|
+
* minus `REFUSES_OFF_MODEL_IDS`, when they never did). Absence is "unknown" and
|
|
1321
|
+
* stays unoffered, matching how the rest of the plugin treats undeclared
|
|
1322
|
+
* capability.
|
|
1323
|
+
*/
|
|
1304
1324
|
declare function workBuddyThinkingLevelMap(info: WorkBuddyModelInfo): WorkBuddyThinkingLevelMap | undefined;
|
|
1305
1325
|
/**
|
|
1306
1326
|
* Assemble the adapter. The provider's `getModels` reads the live catalog,
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-
|
|
1
|
+
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-Dd9fsFMR.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -184,12 +184,33 @@ function workBuddyModelInput(info) {
|
|
|
184
184
|
function workBuddyDisplayName(info) {
|
|
185
185
|
return info.creditMultiplier === void 0 ? info.name : `${info.name} · x${info.creditMultiplier.toFixed(2)}`;
|
|
186
186
|
}
|
|
187
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Map only levels advertised by WorkBuddy; undeclared DSH levels stay unavailable.
|
|
189
|
+
*
|
|
190
|
+
* `off` is offered EXACTLY when `canDisableThinking === true` and withheld
|
|
191
|
+
* (mapped to `null`) otherwise. `null` is pi-ai's "this level is not
|
|
192
|
+
* supported": `getSupportedThinkingLevels` filters on `mapped === null`, and
|
|
193
|
+
* `openai-completions` reads `thinkingLevelMap?.off !== null` to decide whether
|
|
194
|
+
* it may send a thinking-disabled request.
|
|
195
|
+
*
|
|
196
|
+
* This condition used to be inverted (`!== true` set the null), which locked
|
|
197
|
+
* `off` OFF for precisely the models that CAN disable thinking while leaving it
|
|
198
|
+
* ON for those that cannot. It stayed invisible because the only test probed
|
|
199
|
+
* the `canDisableThinking: false` half, which the inverted form also passes.
|
|
200
|
+
*
|
|
201
|
+
* The value read here is already the EFFECTIVE one: the runtime catalog is
|
|
202
|
+
* stamped by `withEffectiveOff()` before it reaches the adapter, so this is the
|
|
203
|
+
* user's `off` selection when they made one (and the upstream declaration,
|
|
204
|
+
* minus `REFUSES_OFF_MODEL_IDS`, when they never did). Absence is "unknown" and
|
|
205
|
+
* stays unoffered, matching how the rest of the plugin treats undeclared
|
|
206
|
+
* capability.
|
|
207
|
+
*/
|
|
188
208
|
function workBuddyThinkingLevelMap(info) {
|
|
189
209
|
const supported = info.reasoning?.supportedEfforts?.filter((effort) => THINKING_LEVELS.includes(effort));
|
|
190
210
|
if (supported === void 0 || supported.length === 0) return void 0;
|
|
191
211
|
const map = Object.fromEntries(THINKING_LEVELS.map((level) => [level, supported.includes(level) ? level : null]));
|
|
192
|
-
if (info.reasoning?.canDisableThinking
|
|
212
|
+
if (info.reasoning?.canDisableThinking === true) map.off = "off";
|
|
213
|
+
else map.off = null;
|
|
193
214
|
return map;
|
|
194
215
|
}
|
|
195
216
|
/** Build one pi-ai model descriptor pointing at the loopback shim. */
|
|
@@ -267,6 +288,14 @@ function toPiModel(info, baseUrl, providerId) {
|
|
|
267
288
|
* text is empty but which carries `toolsAdded` still folds its TOOLS up; the
|
|
268
289
|
* placeholder for the missing prompt then comes from `ensureSystemHead` at
|
|
269
290
|
* the wire (the last of the three layers).
|
|
291
|
+
*
|
|
292
|
+
* LEGACY-ONLY. This is the inverse direction from {@link foldModernContext}:
|
|
293
|
+
* it produces the FLAT `{systemPrompt, tools, messages}` shape that ONLY
|
|
294
|
+
* pi-ai 0.85 reads. Never hand its result to a 1.x api — 1.x reads tools from
|
|
295
|
+
* `messages[].toolsAdded` and would see none (issues #25/#26). Type note: the
|
|
296
|
+
* parameter is the 0.85 `Context`; pi-ai 1.1.0 types the api's second parameter
|
|
297
|
+
* as `TranscriptContext`, so this function cannot be called with 1.x typings —
|
|
298
|
+
* that is intentional, and it is why the modern branch below does not touch it.
|
|
270
299
|
*/
|
|
271
300
|
function adaptLegacyPiAiContext(context) {
|
|
272
301
|
if (context === null || typeof context !== "object") return context;
|
|
@@ -330,30 +359,117 @@ function systemTextOf(message) {
|
|
|
330
359
|
return texts.join("\n\n");
|
|
331
360
|
}
|
|
332
361
|
/**
|
|
362
|
+
* Fold a RAW context into the normalized transcript the modern generation
|
|
363
|
+
* consumes: a leading `system` message carrying the prompt AND the tool
|
|
364
|
+
* declarations in its `toolsAdded` field.
|
|
365
|
+
*
|
|
366
|
+
* WHY THIS IS NEEDED AT ALL (the 0.2.1-rc.1 regression). This used to be
|
|
367
|
+
* unnecessary: the modern branch returned pi-ai's api UNWRAPPED, on the belief
|
|
368
|
+
* that the modern generation normalized for itself. It does not.
|
|
369
|
+
* `dsh-llm-pi-ai` — both the 0.1.7 and the 0.2.1 lines — hands every provider a
|
|
370
|
+
* RAW envelope (`piContext()` builds `{systemPrompt, messages, tools}` and
|
|
371
|
+
* never calls `normalizeContext`), while:
|
|
372
|
+
*
|
|
373
|
+
* - pi-ai 0.85.x READS that raw shape (its request builder consults
|
|
374
|
+
* `context.systemPrompt` / `context.tools` directly), and
|
|
375
|
+
* - pi-ai 1.x READS ONLY `context.messages` (it resolves the transcript and
|
|
376
|
+
* collapses system messages; the top-level fields are consulted nowhere).
|
|
377
|
+
*
|
|
378
|
+
* On pi-ai 1.x a pass-through request therefore went upstream with NO system
|
|
379
|
+
* prompt and NO tool declarations, silently: the model stopped calling tools,
|
|
380
|
+
* and the domestic gateway tolerated the prompt-less body. Same failure class
|
|
381
|
+
* as issues #24/#25/#26, reached from the opposite side — 4.4.2 fixed "1.x
|
|
382
|
+
* misjudged as legacy and folded"; this fixes "1.x judged modern and NOT
|
|
383
|
+
* folded, though nothing had normalized".
|
|
384
|
+
*
|
|
385
|
+
* Built inline rather than by importing pi-ai's `normalizeContext`, which does
|
|
386
|
+
* not exist in 0.85.x: a static import would be `undefined` there, and this
|
|
387
|
+
* module deliberately never imports pi-ai statically (see `pi-ai-runtime.ts` —
|
|
388
|
+
* that doctrine is what lets the standalone `bin` run in a profile where the
|
|
389
|
+
* peer is never installed). The folded shape matches what `normalizeContext`
|
|
390
|
+
* produces for the same input: `{role:'system', content, toolsAdded, timestamp:0}`.
|
|
391
|
+
*
|
|
392
|
+
* The top-level `systemPrompt`/`tools` fields are DROPPED, exactly as
|
|
393
|
+
* `normalizeContext` drops them, so a raw transcript and an already-normalized
|
|
394
|
+
* one converge on the same object.
|
|
395
|
+
*/
|
|
396
|
+
function foldModernContext(context) {
|
|
397
|
+
const prompt = context.systemPrompt;
|
|
398
|
+
const tools = context.tools;
|
|
399
|
+
const hasPrompt = prompt !== void 0 && prompt !== "";
|
|
400
|
+
const hasTools = Array.isArray(tools) && tools.length > 0;
|
|
401
|
+
if (!hasPrompt && !hasTools) return context;
|
|
402
|
+
const system = {
|
|
403
|
+
role: "system",
|
|
404
|
+
content: prompt ?? "",
|
|
405
|
+
...hasTools ? { toolsAdded: tools } : {},
|
|
406
|
+
timestamp: 0
|
|
407
|
+
};
|
|
408
|
+
const { systemPrompt: _prompt, tools: _tools, ...rest } = context;
|
|
409
|
+
return {
|
|
410
|
+
...rest,
|
|
411
|
+
messages: [system, ...context.messages]
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
333
415
|
* Wrap one pi-ai API module so both stream entry points receive a context this
|
|
334
|
-
*
|
|
335
|
-
* when the module RESOLVED for this plugin is the legacy generation.
|
|
416
|
+
* generation can actually consume.
|
|
336
417
|
*
|
|
337
418
|
* The gate is the plugin's own module, resolved via {@link piAiRuntimeInfo}
|
|
338
|
-
* (manifest version of the copy this plugin resolves — no pi-ai import):
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
419
|
+
* (manifest version of the copy this plugin resolves — no pi-ai import):
|
|
420
|
+
*
|
|
421
|
+
* - `legacy` (0.85) wants the flat `{systemPrompt, tools, messages}` shape, so
|
|
422
|
+
* a normalized transcript is UN-folded by {@link adaptLegacyPiAiContext}.
|
|
423
|
+
* - `modern` (0.87+, incl. 1.x) wants the normalized transcript, so a raw
|
|
424
|
+
* envelope is FOLDED by {@link foldModernContext}. Passing it through
|
|
425
|
+
* unnormalized is what silently deleted the prompt and the tools on pi-ai
|
|
426
|
+
* 1.x — the regression this branch now prevents.
|
|
427
|
+
*
|
|
428
|
+
* Both branches are idempotent, so a future seam that starts normalizing on its
|
|
429
|
+
* own side changes nothing here.
|
|
347
430
|
*
|
|
348
431
|
* The deferred-fetch entry points take no context and are passed through as-is
|
|
349
432
|
* in both branches.
|
|
350
433
|
*/
|
|
434
|
+
/**
|
|
435
|
+
* Present an adapted context as the type the api's parameter currently expects.
|
|
436
|
+
*
|
|
437
|
+
* pi-ai 1.x BRANDS its provider-facing context (`TranscriptContext`, a
|
|
438
|
+
* `unique symbol` field) precisely so a raw `Context` cannot reach provider
|
|
439
|
+
* code by accident; the brand's stated sole producer is `normalizeContext()`.
|
|
440
|
+
* On a 1.x typings surface BOTH branches below receive that branded type, so
|
|
441
|
+
* merely returning a plain `Context` fails `tsc` with TS2345 — an error that
|
|
442
|
+
* only appears when the peer TYPES are 1.x, which is why the 0.85 checkout
|
|
443
|
+
* never saw it.
|
|
444
|
+
*
|
|
445
|
+
* The cast is a deliberate, documented assertion of exactly what the brand
|
|
446
|
+
* protects, and it is true after the call site has done the work:
|
|
447
|
+
*
|
|
448
|
+
* - modern: {@link foldModernContext} has already performed
|
|
449
|
+
* `normalizeContext`'s job — prompt and tools live on the leading system
|
|
450
|
+
* message, top-level fields gone;
|
|
451
|
+
* - legacy: {@link adaptLegacyPiAiContext} produced the FLAT 0.85 shape, which
|
|
452
|
+
* is only ever observed on a 0.85 checkout, where this type is a plain
|
|
453
|
+
* `Context` and the cast is an upcast.
|
|
454
|
+
*
|
|
455
|
+
* Do NOT "fix" this by importing `normalizeContext`: it does not exist in
|
|
456
|
+
* 0.85, so that import would be `undefined` at runtime on the very hosts this
|
|
457
|
+
* code must keep working — the same reason `pi-ai-runtime.ts` documents why
|
|
458
|
+
* this module never imports pi-ai statically.
|
|
459
|
+
*/
|
|
460
|
+
function transcript(context) {
|
|
461
|
+
return context;
|
|
462
|
+
}
|
|
351
463
|
function withLegacyContext(api, generation) {
|
|
352
|
-
if (generation === "modern") return
|
|
464
|
+
if (generation === "modern") return {
|
|
465
|
+
...api,
|
|
466
|
+
stream: (model, context, options) => api.stream(model, transcript(foldModernContext(context)), options),
|
|
467
|
+
streamSimple: (model, context, options) => api.streamSimple(model, transcript(foldModernContext(context)), options)
|
|
468
|
+
};
|
|
353
469
|
return {
|
|
354
470
|
...api,
|
|
355
|
-
stream: (model, context, options) => api.stream(model, adaptLegacyPiAiContext(context), options),
|
|
356
|
-
streamSimple: (model, context, options) => api.streamSimple(model, adaptLegacyPiAiContext(context), options)
|
|
471
|
+
stream: (model, context, options) => api.stream(model, transcript(adaptLegacyPiAiContext(context)), options),
|
|
472
|
+
streamSimple: (model, context, options) => api.streamSimple(model, transcript(adaptLegacyPiAiContext(context)), options)
|
|
357
473
|
};
|
|
358
474
|
}
|
|
359
475
|
/**
|
|
@@ -3281,16 +3397,36 @@ function retryDue(probe, nowMs, fallbackMs = POOL_UNKNOWN_COOLDOWN_MS) {
|
|
|
3281
3397
|
*
|
|
3282
3398
|
* 1. **Usability.** Only accounts that can be billed right now are candidates;
|
|
3283
3399
|
* they always outrank every excluded one.
|
|
3284
|
-
* 2. **
|
|
3285
|
-
*
|
|
3286
|
-
*
|
|
3287
|
-
*
|
|
3288
|
-
*
|
|
3289
|
-
* 4. **
|
|
3290
|
-
*
|
|
3400
|
+
* 2. **Expiring credits first, soonest first.** Points about to expire are
|
|
3401
|
+
* worth exactly zero after they do, so spending them before they lapse is
|
|
3402
|
+
* strictly better than saving them.
|
|
3403
|
+
* 3. **Largest expiring amount.** Between two accounts that both have expiring
|
|
3404
|
+
* points, the one with more at risk goes first.
|
|
3405
|
+
* 4. **Credits, highest first.** Among accounts with nothing expiring, spend the
|
|
3406
|
+
* one with the most, so a nearly-drained account is conserved.
|
|
3407
|
+
* 5. **Freshest credential.** Tie-break so a stale token never wins a draw.
|
|
3408
|
+
* 6. **Account id.** Final tie-break, making the order TOTAL: two accounts
|
|
3291
3409
|
* equal on every ranking key would otherwise keep the caller's array order,
|
|
3292
3410
|
* so the same pool could rotate differently between two runs.
|
|
3293
3411
|
*
|
|
3412
|
+
* WHY EXPIRY OUTRANKS SIZE (issue #70). These used to be strict lexicographic
|
|
3413
|
+
* keys: total, THEN expiry. Since totals are almost never exactly equal, the
|
|
3414
|
+
* expiry rule was unreachable — a live report had an account with 196 points
|
|
3415
|
+
* lapsing that day ranked LAST, behind two accounts that expire weeks later,
|
|
3416
|
+
* simply because it held ~1,200 fewer points overall. It was never selected, so
|
|
3417
|
+
* the 196 points always lapsed. A "use it or lose it" rule that another rule
|
|
3418
|
+
* completely shadows is not a tie-break, it is dead code.
|
|
3419
|
+
*
|
|
3420
|
+
* WHY SPENDING THE SMALL EXPIRING ACCOUNT IS STILL RIGHT. Ordering by expiry
|
|
3421
|
+
* does mean an account with 1 point lapsing outranks one holding 50,000. That
|
|
3422
|
+
* looks wrong and is not: credits are fungible, so using the rich account
|
|
3423
|
+
* instead does not preserve the 1 point — it spends the 1 point's chance and
|
|
3424
|
+
* loses it. Using the expiring account spends a point that was going to be lost
|
|
3425
|
+
* anyway, and the rich account is still there for the next request. The only
|
|
3426
|
+
* real cost is a failover round-trip if the expiring account cannot cover a
|
|
3427
|
+
* whole request, which the exclusion/failover path already absorbs. Ordering by
|
|
3428
|
+
* total instead loses the expiring points with certainty.
|
|
3429
|
+
*
|
|
3294
3430
|
* Excluded accounts are still RETURNED (sorted after the candidates) so the
|
|
3295
3431
|
* card can explain why each is out; they differ from candidates by
|
|
3296
3432
|
* `excludedBy` being set on them, never by being silently dropped.
|
|
@@ -3307,14 +3443,18 @@ function rankPool(members, nowMs) {
|
|
|
3307
3443
|
score: scoreOf(member),
|
|
3308
3444
|
...excludedBy === void 0 ? {} : { excludedBy },
|
|
3309
3445
|
_usable: excludedBy === void 0,
|
|
3446
|
+
_atRisk: (member.credits?.expiringSoon ?? 0) > 0,
|
|
3447
|
+
_expiringAmount: member.credits?.expiringSoon ?? 0,
|
|
3310
3448
|
_expiring: member.credits?.nearestExpiryMs ?? Number.POSITIVE_INFINITY,
|
|
3311
3449
|
_tokenExpiresAtMs: member.tokenExpiresAtMs ?? 0
|
|
3312
3450
|
};
|
|
3313
3451
|
});
|
|
3314
3452
|
rows.sort((left, right) => {
|
|
3315
3453
|
if (left._usable !== right._usable) return left._usable ? -1 : 1;
|
|
3454
|
+
if (left._atRisk !== right._atRisk) return left._atRisk ? -1 : 1;
|
|
3455
|
+
if (left._atRisk && right._atRisk && left._expiring !== right._expiring) return left._expiring - right._expiring;
|
|
3456
|
+
if (left._expiringAmount !== right._expiringAmount) return right._expiringAmount - left._expiringAmount;
|
|
3316
3457
|
if (left.score !== right.score) return right.score - left.score;
|
|
3317
|
-
if (left._expiring !== right._expiring) return left._expiring - right._expiring;
|
|
3318
3458
|
if (left._tokenExpiresAtMs !== right._tokenExpiresAtMs) return right._tokenExpiresAtMs - left._tokenExpiresAtMs;
|
|
3319
3459
|
return left.account.id < right.account.id ? -1 : left.account.id > right.account.id ? 1 : 0;
|
|
3320
3460
|
});
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.7.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.3.0-0",
|
|
92
92
|
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.3.0-0",
|
|
93
93
|
"@deepseek-ai/schemastery": ">=3.18.1-rc.1 <4.0.0 || >=3.18.2 <4.0.0",
|
|
94
|
-
"@earendil-works/pi-ai": ">=0.85.0 <0.88.0",
|
|
94
|
+
"@earendil-works/pi-ai": ">=0.85.0 <0.88.0 || >=1.0.0-0 <2.0.0",
|
|
95
95
|
"react": "^18.2.0"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.3.0-0",
|
|
111
111
|
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.3.0-0",
|
|
112
112
|
"@deepseek-ai/schemastery": ">=3.18.1 <4.0.0 || >=3.18.2 <4.0.0",
|
|
113
|
-
"@earendil-works/pi-ai": ">=0.85.0 <0.88.0",
|
|
113
|
+
"@earendil-works/pi-ai": ">=0.85.0 <0.88.0 || >=1.0.0-0 <2.0.0",
|
|
114
114
|
"@types/node": "^22.20.0",
|
|
115
115
|
"@types/react": "~18.3.31",
|
|
116
116
|
"@types/react-dom": "18.3.7",
|