dsh-connect-workbuddy 4.8.2 → 4.9.1
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 +43 -0
- package/README.md +5 -2
- package/lib/bin.js +1 -1
- package/lib/client.js +19 -2
- package/lib/{host-heartbeat-B-4nBogJ.js → host-heartbeat-DahwuDGO.js} +1 -1
- package/lib/index.d.ts +6 -0
- package/lib/index.js +45 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.9.1 (2026-10-19)
|
|
4
|
+
|
|
5
|
+
> 版本号是 **patch**:只改账号池里两枚「当前使用」徽章的配色,外加一处 CHANGELOG 排序修正;不改请求、响应、计费、排序规则或任何配置项。发往上游的请求体与 4.9.0 逐字节相同。
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **账号池的两枚「当前使用」徽章改成实心绿底 + 白字**
|
|
10
|
+
- **原来错在哪**:两枚徽章(`src/client/styles.ts:303` 的区块标题行、`:353` 的表格行内)的颜色由**两个互不相干的来源**拼成 —— 底色是写死的淡蓝 `rgba(86,134,254,.16)`,字色却取 `--dsw-alias-brand-primary`。后者是宿主主题的**对比色**(见同文件约 118 行那条 checkbox 注释:它曾被用来画勾选框,在浅色主题下把格子涂成沉重的黑色药丸),于是同一枚徽章在浅色主题下是「近黑字 + 淡蓝底」,读起来像一条**禁用标签**,而不是「这一行正在服务」。
|
|
11
|
+
- **为什么底色也要一起换**:只把字改成绿色(4.9.0 之后内部试过的一版)仍然是一枚**淡色药丸上的彩色小字**,属于安静的批注。徽章存在的全部意义就是让人一眼找到正在服务的那一行,需要找的东西不算数。所以改成填充态:绿底本身就是底,白字压在上面。
|
|
12
|
+
- **现在的形状**:
|
|
13
|
+
| 片段 | 值 | 说明 |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| 背景 | `var(--dsw-alias-state-success-primary,#22c55e)` | 与右列「已签到」「✓ 可用」**同一个绿**,跟随主题(深色 `#22c55e` / 浅色 `#16a34a`) |
|
|
16
|
+
| 文字 | `#fff` | 直接写死而非取 token:字色与底色各走一路,正是这条故障的成因 |
|
|
17
|
+
| 描边 | `1px solid rgba(34,197,94,.55)` | 主题把 success 改成深色时,防止药丸与行底色糊在一起 |
|
|
18
|
+
- **对比度要说准**:白字在 `#22c55e` 上约 **2.2:1**、在 `#16a34a` 上约 **3.3:1**,低于正文 4.5:1 的门槛。这是有意的取舍,已写在 CSS 注释里:徽章保持 11–12px **粗体**、只承载两个字的短标签,先被当作「形状」看见,再被当作字读到。若要更硬的对比需改深绿底(`#15803d`,白字约 4.9:1),本版没这么做。
|
|
19
|
+
- **刻意没动的**:账号名左侧那条实心色条(`:343`)、手动排序的「第 N 位」排名徽章(`:357`)、↑↓ 按钮的 hover 态(`:359`)、行底色(`:325`/`:329`)仍用 `brand-primary`。它们描摹的是「顺序」与「可操作性」,与「是不是我在用的那个」是两件事 —— 4.8.1 之前那版正因为把一种颜色同时用于多件事才读不出来。
|
|
20
|
+
- **测试**:`tests/client-host-contract.spec.ts` 新增用例,逐个选择器从提取出的 CSS 里断言「绿底 + `color:#fff`」,同时钉住两条**不得回归**:不得再出现 `brand-primary`,不得再出现淡蓝 `rgba(86,134,254,.16)`。按选择器逐条断言而非全文断言,是为了防止其中一枚悄悄改回去而测试仍然通过。
|
|
21
|
+
- **`CHANGELOG.md` 的版本节顺序修正**
|
|
22
|
+
- 4.9.0 上次发布时被插到了 4.8.2 **之后**,最新版本不在最上面。本版把 4.9.0 一节移回 4.8.2 之前;两节正文一字未改。
|
|
23
|
+
|
|
24
|
+
## 4.9.0 (2026-10-18)
|
|
25
|
+
|
|
26
|
+
> 版本号是 **minor**:这不是只改内部或只改文案的 patch —— 它改了请求里每个模型声明的身份,因此**会改变发往同一模型的历史消息的重放形状**(thinking 不再被降级成正文)。计费前提本身没有改;不跨区、不换模型的会话,其请求体与 4.8.2 逐字节相同。
|
|
27
|
+
|
|
28
|
+
### Changes
|
|
29
|
+
|
|
30
|
+
- **issue #59 计费前提定案:`reasoning` 不计费;同时撤回我们自己一个说法、改正报告者两个数**
|
|
31
|
+
- **定案的是什么**:`docs/reasoning-investigation.md` §10.1 的 (ii)「上游忽不忽略 `reasoning`」自 4.6.x 起一直记**尚未测定**,`docs/audit/ISSUE59-VERIFY.md` 的 C7 也记 UNVERIFIABLE-LOCALLY。现在可以写清楚了,依据两条互不依赖的算式:
|
|
32
|
+
1. **同区同模型增量**(报告者的机器实测):国际侧两次相邻请求之间 `promptTokens` +416,`reasoning` 字符 +1,290、其余块 +892。计费读法算出来是 **0.1907 tok/char**,不计费读法是 **0.4664**;后者与我们自己实测的 `tool-result` 系数 **455.5 tok/1k** 只差 2.4%,而那一步新增的正是 `assistant(tool-call)` + `tool(result)`。
|
|
33
|
+
2. **国际侧自己的账**(纯本仓库系数):tool 块 959,283 字符 × 0.4555 = **436,953 tok**,已是它 447,306 总量的 **97.7%**;剩下的 10,353 tok 要分给另外 822,343 字符(0.0126 tok/char)—— 只要 `reasoning` 也计费,这笔账就凑不出来。
|
|
34
|
+
- **性质必须说准**:这是「**报告者实测 + 本仓库系数复算佐证**」,**不是本机实测**。本机没有 `global.jsonl`,`cn.jsonl` 685 条里 `promptTokens` 命中 0(都是 4.6.6 之前写的记录)。要升级成「本机独立复现」,还差一份独立账号的捕获或一次受控的 reasoning 开关 A/B。
|
|
35
|
+
- **我们撤回了什么**:4.8.0/4.8.1 的文档里那句「若 `reasoning` 计费,预测比值 0.697」是**量纲串了** —— 0.697 是两边非 reasoning 密度的比值,不是计费假设的预测。计费假设下两区计费字符几乎相等(差 0.15%),预测比值应 **≈1.00**,而实测 **1.4370**:计费假设是**被证伪**,不是被吻合。报告者的这条批评成立,`docs/audit/ISSUE59-VERIFY.md` 的追记里已写明。
|
|
36
|
+
- **改正了两个数**:报告者撤回 `text` 741,952 → **741,687**(原数重复计了 265 字符),代码注释与测试文件头随之更正;他给过的隐含系数 **0.3518 tok/char 作废**(分母串了,真分母是 555,724),该数从未进过公开文档,C7 当初「无法复现」的疑问至此解释完毕。
|
|
37
|
+
- **不变的部分**:`isSameModel` 判据、两个 provider id 的成因、`context-rescue` 的兜底行为,一条都没改 —— 本次只改文档与注释,不动任何运行时行为。
|
|
38
|
+
|
|
39
|
+
- **issue #59 插件侧修复:两条路由的 model 身份统一,换区不再把 thinking 降级成正文**
|
|
40
|
+
- **原来错在哪**:pi-ai 只把历史里的 thinking 当 thinking 重放,条件是产生它的消息与本次请求的 provider、api、model id **三者全等**(`transform-messages.js:68`);不等就**降级成普通正文**(`:88–96`)。本插件把国内版与国际版注册成两条独立路由(`workbuddy` / `workbuddy-global`),而它们是**同一个 model id** —— 于是换一次区,另一侧产生的全部 thinking 整批从 reasoning 桶挪进正文桶。报告者实测:同一份字节相同的历史,一侧 708K、另一侧 1.374M(1.94×),为其中一侧算出的压缩请求因此永远塞不进另一侧的窗口。
|
|
41
|
+
- **改了什么**:新增 `WORKBUDDY_MODEL_IDENTITY = 'workbuddy'`(`src/adapter.ts`),`toPiModel` 盖在模型上的 `provider` 字段两边都用它,`isSameModel` 于是成立。**路由 id 一个都没合并** —— 注册键仍是 `workbuddy` / `workbuddy-global`,各自指向自己的 shim、目录与凭据;`tests/issue59-cross-region-thinking.spec.ts` 断言「同一个 model 身份 + 各自的 baseUrl + 各自的路由键」三件事同时成立。
|
|
42
|
+
- **为什么能这样做而不出问题**:pi-ai 的查表本来就分两件事 —— `getModel(provider, id)` 是 `providers.get(provider).getModels()` 里按 id 找(`models.js:116–118`),**键**是路由 id,**盖在返回对象上的 `provider` 字段**是 provider 自己写的。本插件不注册 `fetchModels`,没有任何下游从该字段反推路由;openai-completions 的 api 只用它取厂商特例(都不匹配这两个 id)和错误信息里的标签。
|
|
43
|
+
- **没有一并「修」掉的**:同区内换模型仍降级(那是真的换了模型,thinking 签名对新模型未必有效,保留才是错的);升级前录进历史的消息带旧的按区域 id,重放时仍按旧规则降级,但只会越聊越少,不影响新录的消息。真正的修法仍在上游:`isSameModel` 应当能读「provider 等价组」声明,而不是让插件去伪装身份。
|
|
44
|
+
- **测试如何证明**:`transformMessages` 上跑了两组对照 —— 旧的一对按区域 id 复现出 `[text, text]`(这正是报告的成因),新的一对给出 `[thinking, text]` 且正文原样保留;另有一条断言**不同 model id 仍然降级**,防止这次修复被误读成「任何模型都算同一个」。本地 pi-ai 0.85.1 与宿主 1.1.0 在这条判据上的行为已分别核对一致。
|
|
45
|
+
|
|
3
46
|
## 4.8.2 (2026-10-18)
|
|
4
47
|
|
|
5
48
|
> 版本号是 **patch**:只改积分面板第二行的文字配色,不改请求、响应、计费、排序或任何配置项。`stateTone` 的判定、圆点的颜色规则、行的顺序都与本版之前完全相同。
|
package/README.md
CHANGED
|
@@ -366,9 +366,12 @@ else { await envelope.response.text() /* 或者按 SSE 消费 */ }
|
|
|
366
366
|
- **「未登录」与「登录信息读不出来」是两件事**:加密文件读不出时,插件给出的建议是「安装桌面 App / 用 `WORKBUDDY_APP_EXECUTABLE` 指定其位置」,而**不是**「重新登录一次」——后者对这种情况无效(凭据本身是好的,缺的是密钥)。
|
|
367
367
|
- **403「request illegal」(code 11140)是服务端策略拒绝,不是登录问题**:聊天网关有时会按内容策略直接拒绝一次请求,响应体形如 `{"code":11140,"msg":"request illegal","requestId":"…","displayMsg":{"zh":"内容未通过安全审核,请调整后重试。"}}`。此前插件把它归为通用 `client` 错误返回 400,DSH 侧又显示成 AUTH 异常——看起来像「登录坏了」,但**重新登录解决不了它**:凭据本身是好的,被拒的是这一次请求。现在这类拒绝按 403 原样透传,消息里带官方文案、`code 11140` 和 `requestId`(联系官方支持时提供)。写回宿主的 `error.code` 是 `policy_reject` 这个稳定类别(而非数值 `11140`):宿主按类别决定要不要原地重试,而策略拒绝是确定性的——重试只会白烧预算,`11140` 也不是宿主认得的词;模型行的「测试」也显示「服务端按内容策略拒绝」而非「token 被拒」。遇到时的两个出口:在 WorkBuddy 桌面端用同一账号发同样的内容验证(桌面端同样被拒,说明限制在账号/内容侧);或切换区域/账号后重试。
|
|
368
368
|
|
|
369
|
-
- **切换区域会让上下文「变大」,甚至一换区就 `code 11115` 超限——不是网关多算你的钱**:同一份历史在两个区域的计费口径不同,根子在宿主内置的 `@earendil-works/pi-ai` 重放历史的方式。它给每条 assistant 消息算一次 `isSameModel`(`transform-messages.js:68`,判据是 provider **且** api **且** model id 三者全等):不等的消息,其 thinking 会被**降级成普通正文**(`:88–96`,并不是丢弃),而正文是网关会数的,thinking
|
|
369
|
+
- **切换区域会让上下文「变大」,甚至一换区就 `code 11115` 超限——不是网关多算你的钱**:同一份历史在两个区域的计费口径不同,根子在宿主内置的 `@earendil-works/pi-ai` 重放历史的方式。它给每条 assistant 消息算一次 `isSameModel`(`transform-messages.js:68`,判据是 provider **且** api **且** model id 三者全等):不等的消息,其 thinking 会被**降级成普通正文**(`:88–96`,并不是丢弃),而正文是网关会数的,thinking 走的是另一个字段(字段名两边都是 `reasoning_content`)。**这一点已由 issue #59 报告者的同区增量实测证实**:`reasoning` 不计费,且同一侧自己的工具结果块按本插件实测的 455.5 tok/1k 就吃掉了它全部计费量的 97.7%。本插件把国内版与国际版注册成**两条 provider 路由**(`workbuddy` / `workbuddy-global`,见[工作原理](#工作原理)),而它们是同一个 model id——于是**换一次区,另一侧产生的全部 thinking 就整批从 thinking 桶挪进正文桶**。这就是 issue #59 里「同一份对话历史,国际网关算 1.374M、国内网关算 708K(1.94×)」的成因;反方向切回来则是同一公式的 0.696×。
|
|
370
370
|
- 由此带来三个用户可见的后果:换区后 token 计数可能跳 ±30%~94%,**跳多少只取决于另一侧 thinking 的字符数,与历史总长无关**;在**同一个区域内换模型**也会触发同样的降级(`isSameModel` 的 model 那条腿),所以不换区也可能看到计数变化;跳变叠加在宿主当前的压缩阈值(`min(0.8×窗口, 窗口−128000−65536)`)上,就会让本来不超限的会话一换区立刻撞上 `code 11115 prompt is too long`。
|
|
371
|
-
- **这是宿主与 pi-ai 之间的一处错配,不是本插件的 bug**:不跨 provider/模型重放 thinking 这件事本身是对的(别家模型的思考过程对当前模型没有上下文价值),错在把 `provider` 当作「是不是同一个模型」的必要条件,而这里两个 provider
|
|
371
|
+
- **这是宿主与 pi-ai 之间的一处错配,不是本插件的 bug**:不跨 provider/模型重放 thinking 这件事本身是对的(别家模型的思考过程对当前模型没有上下文价值),错在把 `provider` 当作「是不是同一个模型」的必要条件,而这里两个 provider 字符串指的是**同一个模型的两个接入区域**。
|
|
372
|
+
- **本插件已在 4.9.0 起从插件侧绕开它**:两条路由注册的 **model 身份统一为 `workbuddy`**(`src/adapter.ts` 的 `WORKBUDDY_MODEL_IDENTITY`),也就是模型自己声明的 `provider` 字段两边相同,于是 `isSameModel` 成立、换区不再把 thinking 降级成正文。**路由 id 没有合并**——它仍是 `workbuddy` / `workbuddy-global`,各自指向自己的 shim、目录与凭据,所以一侧故障不会带走另一侧,模型选择器也仍是两组。依据是 pi-ai 自己的查表形状:`getModel(provider, id)` 取的是 `providers.get(provider).getModels()`(`models.js:116–118`),**查表用的键**与**模型上盖的 provider 字段**是分开的两件事。
|
|
373
|
+
- 升级后仍可能看到计数变化的部分:**在同一区域内换模型**仍会降级(`model` 那条腿是真的换了模型,thinking 签名未必对新模型有效,保留才是错的);**升级之前就录在历史里的消息**带着旧的按区域 id,重放时仍按旧规则降级——这种历史只会越聊越少,不会反过来影响新录的消息。
|
|
374
|
+
- 真正的修法仍在上游(让 `isSameModel` 认「provider 等价组」声明,而不是让插件去伪装自己的身份);插件侧的兜底是 4.8.0 起的 `src/context-rescue.ts`——**一旦上游确认超限,就按块折叠历史后重发**,而不是把宿主要压缩的整段历史原样重放一遍(原样重放正是「压缩请求自己超限 → 压缩死锁」的成因)。想亲眼看到这件事发生在哪条消息上:设 `WORKBUDDY_BODY_REPORT` 抓一次 body 组成报告,它只记计数、不记正文。
|
|
372
375
|
|
|
373
376
|
## 免责声明
|
|
374
377
|
|
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-DahwuDGO.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
|
@@ -3822,7 +3822,24 @@ window.__ModuleLoader__.load({
|
|
|
3822
3822
|
* space-between, so it stays on the right edge even if a future change adds a
|
|
3823
3823
|
* child or removes one. */
|
|
3824
3824
|
.dsm-workbuddy-pool-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
3825
|
-
|
|
3825
|
+
/* SOLID badge: white ink on the success green itself, plus a matching hairline.
|
|
3826
|
+
*
|
|
3827
|
+
* This is deliberately a stronger stop than a tinted pill. Green TEXT on the old
|
|
3828
|
+
* pale-blue tint still read as a quiet annotation — and a comment that has to be
|
|
3829
|
+
* looked for is not doing its job when the badge's whole point is "THIS row is
|
|
3830
|
+
* the one serving". Filled also matches how the host marks primary affordances.
|
|
3831
|
+
*
|
|
3832
|
+
* Both halves now come from the SAME token, so they cannot drift apart the way
|
|
3833
|
+
* they did when the ink was --dsw-alias-brand-primary — the host's CONTRAST
|
|
3834
|
+
* colour, which painted near-BLACK on light themes (see the checkbox note at
|
|
3835
|
+
* line ~118). Theme-following: #22c55e dark / #16a34a light.
|
|
3836
|
+
*
|
|
3837
|
+
* Contrast: white on #22c55e ≈ 2.2:1, on #16a34a ≈ 3.3:1 — under the 4.5:1 bar
|
|
3838
|
+
* for body text, which is why the badge stays 11-12px and BOLD and never carries
|
|
3839
|
+
* a sentence: it is read as a shape first, a word second. The border keeps the
|
|
3840
|
+
* pill from bleeding into the row tint if a theme remaps success to something
|
|
3841
|
+
* dark. */
|
|
3842
|
+
.dsm-workbuddy-pool-current-badge{margin-left:auto;padding:2px 10px;border-radius:999px;background:var(--dsw-alias-state-success-primary,#22c55e);border:1px solid rgba(34,197,94,.55);color:#fff;font-size:12px;font-weight:600;line-height:18px;white-space:nowrap}
|
|
3826
3843
|
/* Sits under the header block, before the batch buttons: account discovery is
|
|
3827
3844
|
the first step of using the pool, so it reads above the actions it enables. */
|
|
3828
3845
|
/* Manual-mode account picker: shares the settings row layout (.pool-set), and
|
|
@@ -3869,7 +3886,7 @@ window.__ModuleLoader__.load({
|
|
|
3869
3886
|
.dsm-workbuddy-pool-account-name span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
3870
3887
|
/* Declared AFTER the blanket span rule so it wins: the badge must not inherit
|
|
3871
3888
|
the muted 11px tertiary treatment the membership line uses. */
|
|
3872
|
-
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-current-tag{align-self:flex-start;margin-top:2px;padding:1px 6px;border-radius:999px;background:
|
|
3889
|
+
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-current-tag{align-self:flex-start;margin-top:2px;padding:1px 6px;border-radius:999px;background:var(--dsw-alias-state-success-primary,#22c55e);border:1px solid rgba(34,197,94,.55);color:#fff;font-size:11px;font-weight:600;line-height:16px;white-space:nowrap}
|
|
3873
3890
|
/* Manual-priority chrome (order badge + up/down arrows). The rank badge is a SPAN,
|
|
3874
3891
|
not a button: it only DESCRIBES the current order and must not read as a
|
|
3875
3892
|
control (nor join the row's button list). The solid vs dashed distinction is
|
|
@@ -3966,7 +3966,7 @@ function generationOf(version) {
|
|
|
3966
3966
|
//#endregion
|
|
3967
3967
|
//#region src/version.ts
|
|
3968
3968
|
/** The npm package version this build was produced from. */
|
|
3969
|
-
const WORKBUDDY_CONNECT_VERSION = "4.
|
|
3969
|
+
const WORKBUDDY_CONNECT_VERSION = "4.9.1";
|
|
3970
3970
|
//#endregion
|
|
3971
3971
|
//#region src/host-heartbeat.ts
|
|
3972
3972
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -1280,6 +1280,12 @@ interface WorkBuddyAdapterOptions {
|
|
|
1280
1280
|
provider?: string;
|
|
1281
1281
|
/** pi-ai provider name and profile display name; defaults to the CN name. */
|
|
1282
1282
|
displayName?: string;
|
|
1283
|
+
/**
|
|
1284
|
+
* Model identity stamped on every model this route serves; defaults to
|
|
1285
|
+
* {@link WORKBUDDY_MODEL_IDENTITY}. Tests override it to model a transcript
|
|
1286
|
+
* recorded by a host that still stamped per-region ids.
|
|
1287
|
+
*/
|
|
1288
|
+
modelIdentity?: string;
|
|
1283
1289
|
/** Resolve the durable attachment service at request time, when present. */
|
|
1284
1290
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
1285
1291
|
/**
|
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-DahwuDGO.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";
|
|
@@ -106,6 +106,38 @@ const WORKBUDDY_PROVIDERS = {
|
|
|
106
106
|
cn: WORKBUDDY_PROVIDER,
|
|
107
107
|
global: WORKBUDDY_GLOBAL_PROVIDER
|
|
108
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* Model identity BOTH regions stamp on their models — the fix for issue #59.
|
|
111
|
+
*
|
|
112
|
+
* pi-ai keeps a historical thinking block as `thinking` only when the message
|
|
113
|
+
* that carried it names the SAME provider, api and model id as the request in
|
|
114
|
+
* flight (`transform-messages.js:68`); every other block is downgraded to plain
|
|
115
|
+
* `text` (`:88–96`) — downgraded, not dropped, which is why no byte count ever
|
|
116
|
+
* explained the 1.94× regional jump. The two gateways are two provider STRINGS
|
|
117
|
+
* for ONE model, so a transcript recorded in one region was replayed as text in
|
|
118
|
+
* the other, and issue #59's reporter measured exactly that: 708K → 1.374M on
|
|
119
|
+
* byte-identical history, and a compression request that could then never fit
|
|
120
|
+
* the window it was computed for.
|
|
121
|
+
*
|
|
122
|
+
* `provider` is also the KEY pi-ai dispatches on (`models.js:410–416`
|
|
123
|
+
* `requireProvider`), so the identity cannot simply be the shared string: the
|
|
124
|
+
* registry has to keep resolving per region. Both halves of that are already
|
|
125
|
+
* satisfied by pi-ai's own lookup shape — `getModel(provider, id)` is
|
|
126
|
+
* `getModels(provider).find(...)` (`models.js:116–118`), i.e. the KEY is the
|
|
127
|
+
* route id while the STAMPED field is whatever the provider put there — so the
|
|
128
|
+
* route stays `workbuddy`/`workbuddy-global` (each with its own shim, catalog
|
|
129
|
+
* and credential store) while every model it serves advertises THIS id. This
|
|
130
|
+
* plugin's provider registers no `fetchModels`, so nothing downstream derives
|
|
131
|
+
* anything from the stamped field; the openai-completions api uses
|
|
132
|
+
* `model.provider` only for provider-specific quirks (none of which match
|
|
133
|
+
* these ids) and as an error-message label.
|
|
134
|
+
*
|
|
135
|
+
* Deliberately NOT the same string as either route: a message is only
|
|
136
|
+
* same-model when provider, api AND model id all match, so the shared id must
|
|
137
|
+
* not be reachable as a route a user could pick — it names one model, not one
|
|
138
|
+
* gateway.
|
|
139
|
+
*/
|
|
140
|
+
const WORKBUDDY_MODEL_IDENTITY = "workbuddy";
|
|
109
141
|
/** Region a provider route id belongs to. */
|
|
110
142
|
function regionOfProvider(provider) {
|
|
111
143
|
for (const [region, id] of Object.entries(WORKBUDDY_PROVIDERS)) if (id === provider) return region;
|
|
@@ -213,14 +245,20 @@ function workBuddyThinkingLevelMap(info) {
|
|
|
213
245
|
else map.off = null;
|
|
214
246
|
return map;
|
|
215
247
|
}
|
|
216
|
-
/**
|
|
217
|
-
|
|
248
|
+
/**
|
|
249
|
+
* Build one pi-ai model descriptor pointing at the loopback shim.
|
|
250
|
+
*
|
|
251
|
+
* `identity` — not the route id — is what `provider` carries; see
|
|
252
|
+
* {@link WORKBUDDY_MODEL_IDENTITY} for why the two must differ. The route the
|
|
253
|
+
* model is REACHED by is the registry key the adapter serves, not this field.
|
|
254
|
+
*/
|
|
255
|
+
function toPiModel(info, baseUrl, identity = WORKBUDDY_MODEL_IDENTITY) {
|
|
218
256
|
const thinkingLevelMap = workBuddyThinkingLevelMap(info);
|
|
219
257
|
return {
|
|
220
258
|
id: info.id,
|
|
221
259
|
name: workBuddyDisplayName(info),
|
|
222
260
|
api: "openai-completions",
|
|
223
|
-
provider:
|
|
261
|
+
provider: identity,
|
|
224
262
|
baseUrl,
|
|
225
263
|
input: workBuddyModelInput(info),
|
|
226
264
|
cost: NO_COST,
|
|
@@ -526,10 +564,11 @@ function createWorkBuddyAdapter(options) {
|
|
|
526
564
|
const { shim, store, catalog, resolveAttachments } = options;
|
|
527
565
|
const providerId = options.provider ?? "workbuddy";
|
|
528
566
|
const providerName = options.displayName ?? "WorkBuddy";
|
|
567
|
+
const modelIdentity = options.modelIdentity ?? "workbuddy";
|
|
529
568
|
const generation = options.piAiGeneration ?? piAiRuntimeInfo().generation;
|
|
530
569
|
const buildModels = () => {
|
|
531
570
|
const baseUrl = `${shim.baseUrl()}/v1`;
|
|
532
|
-
return catalog.current().map((info) => toPiModel(info, baseUrl,
|
|
571
|
+
return catalog.current().map((info) => toPiModel(info, baseUrl, modelIdentity));
|
|
533
572
|
};
|
|
534
573
|
const provider = {
|
|
535
574
|
...createProvider({
|
|
@@ -688,7 +727,7 @@ async function foldOnce(deps, agent, measurement, window, target) {
|
|
|
688
727
|
* what produced it. A paired capture in one session (4.6.7) measured
|
|
689
728
|
* `bodyBytes` 3,628,068 vs 3,626,374, a ratio of 0.9995: both requests carried
|
|
690
729
|
* the same bytes. What differed was the BUCKET each byte was counted in — CN
|
|
691
|
-
* text 741,
|
|
730
|
+
* text 741,687 vs GL 184,821, CN reasoning 72,270 vs GL 627,994, with a
|
|
692
731
|
* complementary residual of 0.3%. The gateways did not miscount; the same
|
|
693
732
|
* history was labelled differently on the two sides.
|
|
694
733
|
*
|
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.9.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|