dsh-connect-workbuddy 4.9.0 → 4.9.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 +50 -13
- package/README.md +1 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +19 -2
- package/lib/{host-heartbeat-CS6PxirW.js → host-heartbeat-DSMmEhuD.js} +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.js +64 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 4.
|
|
3
|
+
## 4.9.2 (2026-10-19)
|
|
4
4
|
|
|
5
|
-
> 版本号是 **patch
|
|
5
|
+
> 版本号是 **patch**:只改 `profiles` 里多登记一个注册表键,外加文档与注释;发往上游的请求体、路由 id、模型 id、模型选择器、目录与凭据一律未变。但**它修的是一个整区不可用的故障**——4.9.0/4.9.1 期间国际版(`workbuddy-global`)的每一个请求都在发出前失败。
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### Fixes
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
9
|
+
- **issue #75:国际版整区报 `Unknown provider: workbuddy`,任何请求都不可能成功**
|
|
10
|
+
- **原来错在哪**:4.9.0 为修 issue #59 把两条路由的模型身份统一成 `workbuddy`(`WORKBUDDY_MODEL_IDENTITY`),理由是「只有让 `isSameModel` 判等,跨区的 thinking 才不会降级成正文」。当时的论证是:pi-ai 的 `getModel(provider, id)` 取 `providers.get(provider).getModels()`(`models.js:116–118`),**查表的键**与**模型上盖的 `provider` 字段**是分开的,所以盖上 `workbuddy` 无害。**这条论证只对按参数查表的那条路径成立,漏掉了真正跑聊天的那条**:`models.streamSimple(model, …)` 的第一帧就是 `requireProvider(model)` = `this.providers.get(model.provider)`(`models.js:351–355`),查的正是模型上盖的那个字段。而每条路由的注册表里只登记过它自己的路由 id(宿主 `dsh-llm-pi-ai` 对每个 profile 调一次 `models.setProvider(profile.piProvider)`,键取 **provider 对象的 `id`**)。于是国内版(路由 `workbuddy`、盖 `workbuddy`)自洽,国际版(路由 `workbuddy-global`、盖 `workbuddy`)在一个只有 `workbuddy-global` 的注册表里查 `workbuddy` —— 抛 `ModelsError("provider", "Unknown provider: workbuddy")`,**在请求发出之前**。
|
|
11
|
+
- **为什么测试没抓住**:#59 配的守卫测试断言的是 `getModel(provider, id)` 这种**按参数**的查表,恰好是不看盖上字段的那一种形状;于是它可以全绿,而聊天路径已经整区死掉。报错文案里也不含 "WorkBuddy" 字样,用户看到的是宿主状态一个无关的 provider 名。
|
|
12
|
+
- **现在的形状**:新增 `modelIdentityKeysOf(providerId, identity)`(`src/adapter.ts`),返回一条路由要在自己的注册表里登记的全部键。每条路由把**同一个** shim 的 provider **按两个 id 各建一个对象**登记:路由 id(`workbuddy-global`,含它自己的 shim 地址、目录、凭据)+共享身份(`workbuddy`)。国内版路由 id 与身份相同,去重后仍只登记一个。这段构造抽成 `buildProfiles()`,初始发布与 `invalidate()`(目录刷新后重建)**共用同一份**——旧代码在 `invalidate()` 里按单键重建,刷新一次就会把国际版重新打回故障。
|
|
13
|
+
- **一个必须说破的细节**:`Models.setProvider(provider)` 存的是 `this.providers.set(provider.id, provider)`(`models.js:35–38`),键取的是**provider 对象的 `id`**,不是 `profiles` 映射的键。因此「把同一个 provider 对象发布两次」是**静默无效**的,必须给身份那个键一个 `id` 改为身份的 provider 对象。`ResolvedPiAiProviderProfile.provider` 上游注释称自己就是「`Models` 集合的键」,只有 `piProvider.id === profile.provider` 时才成立——按对象拆开后这个不变式回来了。
|
|
14
|
+
- **顺带覆盖的第二处失败点**:`Models.getAuth(providerOrModel)` 同样用 `providers.get(providerOrModel.provider)` 取 provider(`models.js:275–280`,宿主侧 `:334–341`),修之前国际版即使过了 `requireProvider` 也会以 `Provider is not configured` 倒在取凭据一步;同一个键一并覆盖。
|
|
15
|
+
- **刻意没做的**:报告者提议的一行改法(`modelIdentity = options.modelIdentity ?? providerId`)**不能采纳**——它把身份拆回按路由区分,等于回退 #59(跨区 thinking 又整批降级成正文、计费跳变重现)。也没必要把身份做成「第二条 provider 条目」:`Models.providers` 本来就是 `Map`,多一个键不影响路由集合,模型选择器、目录、凭据、`registerAdapter` 的 routes 数组全都没有变化。上游的真修法仍是让 `isSameModel` 认「provider 等价组」声明(`earendil-works/pi#6167`)。
|
|
16
|
+
- **测试**:新增 `tests/issue75-global-provider-dispatch.spec.ts`,不用 `profiles()` 的字面量断言,而是**照宿主的构造方式**建注册表(`createModels()` + 每个 profile 一次 `setProvider`)再走 `streamSimple`。其中包含:国际版真实服务的模型能在国际版注册表里被 `requireProvider` 解析且拿到**国际版自己的** `baseUrl`(不是国内版的);国内版不被连累;**一个反证用例**手工重建 4.9.1 的单键注册表,断言它仍然失败于 `Unknown provider: workbuddy`,防止上面几条变成空转通过;`invalidate()` 之后上述性质仍在;以及 #59 的两条性质(两边盖同一个身份、跨区 thinking 仍是 `thinking`)不回退。把 `buildProfiles` 临时改回单键重跑,这组测试会以 `ModelsError: Unknown provider: workbuddy` 失败——已实测确认判别力。
|
|
17
|
+
- **文档**:`README.md` 的 issue #59 那一段原文把上述错误论证当成了依据,已改正并写明 #75 的成因与 4.9.2 的做法;`docs/audit/ISSUE59-OPTIONS.md` 的「O1′ SHIPPED」一节补了「#75 CORRECTION」,说明当时为什么论证错了、以及为什么「按你推理的那条查表路径写的测试,无法证伪从同一条路径得出的结论」。
|
|
18
|
+
|
|
19
|
+
## 4.9.1 (2026-10-19)
|
|
20
|
+
|
|
21
|
+
> 版本号是 **patch**:只改账号池里两枚「当前使用」徽章的配色,外加一处 CHANGELOG 排序修正;不改请求、响应、计费、排序规则或任何配置项。发往上游的请求体与 4.9.0 逐字节相同。
|
|
22
|
+
|
|
23
|
+
### Fixes
|
|
24
|
+
|
|
25
|
+
- **账号池的两枚「当前使用」徽章改成实心绿底 + 白字**
|
|
26
|
+
- **原来错在哪**:两枚徽章(`src/client/styles.ts:303` 的区块标题行、`:353` 的表格行内)的颜色由**两个互不相干的来源**拼成 —— 底色是写死的淡蓝 `rgba(86,134,254,.16)`,字色却取 `--dsw-alias-brand-primary`。后者是宿主主题的**对比色**(见同文件约 118 行那条 checkbox 注释:它曾被用来画勾选框,在浅色主题下把格子涂成沉重的黑色药丸),于是同一枚徽章在浅色主题下是「近黑字 + 淡蓝底」,读起来像一条**禁用标签**,而不是「这一行正在服务」。
|
|
27
|
+
- **为什么底色也要一起换**:只把字改成绿色(4.9.0 之后内部试过的一版)仍然是一枚**淡色药丸上的彩色小字**,属于安静的批注。徽章存在的全部意义就是让人一眼找到正在服务的那一行,需要找的东西不算数。所以改成填充态:绿底本身就是底,白字压在上面。
|
|
28
|
+
- **现在的形状**:
|
|
29
|
+
| 片段 | 值 | 说明 |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| 背景 | `var(--dsw-alias-state-success-primary,#22c55e)` | 与右列「已签到」「✓ 可用」**同一个绿**,跟随主题(深色 `#22c55e` / 浅色 `#16a34a`) |
|
|
32
|
+
| 文字 | `#fff` | 直接写死而非取 token:字色与底色各走一路,正是这条故障的成因 |
|
|
33
|
+
| 描边 | `1px solid rgba(34,197,94,.55)` | 主题把 success 改成深色时,防止药丸与行底色糊在一起 |
|
|
34
|
+
- **对比度要说准**:白字在 `#22c55e` 上约 **2.2:1**、在 `#16a34a` 上约 **3.3:1**,低于正文 4.5:1 的门槛。这是有意的取舍,已写在 CSS 注释里:徽章保持 11–12px **粗体**、只承载两个字的短标签,先被当作「形状」看见,再被当作字读到。若要更硬的对比需改深绿底(`#15803d`,白字约 4.9:1),本版没这么做。
|
|
35
|
+
- **刻意没动的**:账号名左侧那条实心色条(`:343`)、手动排序的「第 N 位」排名徽章(`:357`)、↑↓ 按钮的 hover 态(`:359`)、行底色(`:325`/`:329`)仍用 `brand-primary`。它们描摹的是「顺序」与「可操作性」,与「是不是我在用的那个」是两件事 —— 4.8.1 之前那版正因为把一种颜色同时用于多件事才读不出来。
|
|
36
|
+
- **测试**:`tests/client-host-contract.spec.ts` 新增用例,逐个选择器从提取出的 CSS 里断言「绿底 + `color:#fff`」,同时钉住两条**不得回归**:不得再出现 `brand-primary`,不得再出现淡蓝 `rgba(86,134,254,.16)`。按选择器逐条断言而非全文断言,是为了防止其中一枚悄悄改回去而测试仍然通过。
|
|
37
|
+
- **`CHANGELOG.md` 的版本节顺序修正**
|
|
38
|
+
- 4.9.0 上次发布时被插到了 4.8.2 **之后**,最新版本不在最上面。本版把 4.9.0 一节移回 4.8.2 之前;两节正文一字未改。
|
|
19
39
|
|
|
20
40
|
## 4.9.0 (2026-10-18)
|
|
21
41
|
|
|
@@ -39,6 +59,23 @@
|
|
|
39
59
|
- **没有一并「修」掉的**:同区内换模型仍降级(那是真的换了模型,thinking 签名对新模型未必有效,保留才是错的);升级前录进历史的消息带旧的按区域 id,重放时仍按旧规则降级,但只会越聊越少,不影响新录的消息。真正的修法仍在上游:`isSameModel` 应当能读「provider 等价组」声明,而不是让插件去伪装身份。
|
|
40
60
|
- **测试如何证明**:`transformMessages` 上跑了两组对照 —— 旧的一对按区域 id 复现出 `[text, text]`(这正是报告的成因),新的一对给出 `[thinking, text]` 且正文原样保留;另有一条断言**不同 model id 仍然降级**,防止这次修复被误读成「任何模型都算同一个」。本地 pi-ai 0.85.1 与宿主 1.1.0 在这条判据上的行为已分别核对一致。
|
|
41
61
|
|
|
62
|
+
## 4.8.2 (2026-10-18)
|
|
63
|
+
|
|
64
|
+
> 版本号是 **patch**:只改积分面板第二行的文字配色,不改请求、响应、计费、排序或任何配置项。`stateTone` 的判定、圆点的颜色规则、行的顺序都与本版之前完全相同。
|
|
65
|
+
|
|
66
|
+
### Changes
|
|
67
|
+
|
|
68
|
+
- **积分面板第二行的「当前选用」改成绿色**
|
|
69
|
+
- **原来错在哪**:`ComposerCredits.tsx` 把那两个事实拼成**一个** span —— `detailLine(当前选用, 被限流 · 约 6 小时后)`。一个 span 只能有一种颜色,而它挂的是 `.dsm-workbuddy-composer-panel-mark`,颜色是原因用的琥珀色 `--dsw-alias-state-warn-primary`。于是「当前选用」这句陈述事实的话,被涂成了问题的颜色:在一张「被限流」的表格里,它看起来像第三种警告。
|
|
70
|
+
- **现在的形状**:第二行拆成两个 span,各得各的语气 ——
|
|
71
|
+
| 片段 | 类名 | 颜色 | 出现在 |
|
|
72
|
+
| --- | --- | --- | --- |
|
|
73
|
+
| 当前选用 | `.dsm-workbuddy-composer-panel-mark-current` | `--dsw-alias-state-success-primary`(绿) | 正在服务本 tab 的那一行 |
|
|
74
|
+
| 被限流 · 约 X 小时后 | `.dsm-workbuddy-composer-panel-mark-reason` | `--dsw-alias-state-warn-primary`(琥珀,**未变**) | 用不了的行 |
|
|
75
|
+
- **分隔符改由 CSS 画**:两个 span 之间的 ` · ` 来自相邻兄弟选择器 `-current + -reason::before{content:" · "}`,所以把这一行当**纯文本**读出来仍是 `当前选用 · 被限流 · 约 6 小时后` —— 行 `title` 的悬浮提示、`th` 的 `aria-label`、以及既有的字符串断言都不受影响。这也是没有把两段文案在 TS 里拼成一个字符串的原因:拼了就没法给它们分别上色。
|
|
76
|
+
- **刻意没动的**:圆点的颜色规则(能用=绿、不能用=红、未测=灰)与 `stateTone` 的判定一行未改。选哪个颜色回答的是「能不能用」,第二行回答的是「是不是我在用的那个」,这两条规则各自独立 —— 4.8.1 之前那一版正是因为把颜色同时用于两件事才读不出来。
|
|
77
|
+
- **测试**:`tests/composer-credits-render.spec.tsx` 新增用例钉住「选中为独立绿色 span、原因为独立琥珀 span、两者共存仍不合并」,颜色断言直接从 `WORKBUDDY_COMPOSER_CSS` 里读规则,而不是断言类名存在就算过。
|
|
78
|
+
|
|
42
79
|
## 4.8.1 (2026-10-18)
|
|
43
80
|
|
|
44
81
|
> 版本号是 **patch**:只改卡片界面的样式与排版,不改请求、响应、计费或任何配置项。发往上游的请求体与本版之前逐字节相同。
|
package/README.md
CHANGED
|
@@ -369,7 +369,7 @@ else { await envelope.response.text() /* 或者按 SSE 消费 */ }
|
|
|
369
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
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
|
|
372
|
+
- **本插件已在 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」一节。
|
|
373
373
|
- 升级后仍可能看到计数变化的部分:**在同一区域内换模型**仍会降级(`model` 那条腿是真的换了模型,thinking 签名未必对新模型有效,保留才是错的);**升级之前就录在历史里的消息**带着旧的按区域 id,重放时仍按旧规则降级——这种历史只会越聊越少,不会反过来影响新录的消息。
|
|
374
374
|
- 真正的修法仍在上游(让 `isSameModel` 认「provider 等价组」声明,而不是让插件去伪装自己的身份);插件侧的兜底是 4.8.0 起的 `src/context-rescue.ts`——**一旦上游确认超限,就按块折叠历史后重发**,而不是把宿主要压缩的整段历史原样重放一遍(原样重放正是「压缩请求自己超限 → 压缩死锁」的成因)。想亲眼看到这件事发生在哪条消息上:设 `WORKBUDDY_BODY_REPORT` 抓一次 body 组成报告,它只记计数、不记正文。
|
|
375
375
|
|
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-DSMmEhuD.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.9.
|
|
3969
|
+
const WORKBUDDY_CONNECT_VERSION = "4.9.2";
|
|
3970
3970
|
//#endregion
|
|
3971
3971
|
//#region src/host-heartbeat.ts
|
|
3972
3972
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -1284,6 +1284,10 @@ interface WorkBuddyAdapterOptions {
|
|
|
1284
1284
|
* Model identity stamped on every model this route serves; defaults to
|
|
1285
1285
|
* {@link WORKBUDDY_MODEL_IDENTITY}. Tests override it to model a transcript
|
|
1286
1286
|
* recorded by a host that still stamped per-region ids.
|
|
1287
|
+
*
|
|
1288
|
+
* It also decides the extra registry key this adapter publishes alongside
|
|
1289
|
+
* its route (issue #75) — see {@link modelIdentityKeysOf}, which is what
|
|
1290
|
+
* keeps an overridden stamp resolvable exactly like the production one.
|
|
1287
1291
|
*/
|
|
1288
1292
|
modelIdentity?: string;
|
|
1289
1293
|
/** Resolve the durable attachment service at request time, when present. */
|
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-DSMmEhuD.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";
|
|
@@ -119,25 +119,60 @@ const WORKBUDDY_PROVIDERS = {
|
|
|
119
119
|
* byte-identical history, and a compression request that could then never fit
|
|
120
120
|
* the window it was computed for.
|
|
121
121
|
*
|
|
122
|
-
*
|
|
123
|
-
* `
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
122
|
+
* The stamp is NOT inert, and reading it as inert is what broke issue #75.
|
|
123
|
+
* `provider` is the KEY pi-ai dispatches on: `requireProvider` resolves
|
|
124
|
+
* `this.providers.get(model.provider)` and throws
|
|
125
|
+
* `Unknown provider: ${model.provider}` when it is absent (`models.js:410–416`
|
|
126
|
+
* on the host's 1.1.0, `:351–357` on the 0.85.1 this repo resolves), and
|
|
127
|
+
* `getAuth` resolves the same key, answering `Provider is not configured`
|
|
128
|
+
* instead (`:334–341` and `:429`; `:275–280` and `:366` on 0.85.1). Both sit on
|
|
129
|
+
* the LIVE chat path: `dsh-llm-pi-ai` streams through
|
|
130
|
+
* `snapshot.models.streamSimple(model, …)` (`lib/index.js:1983`), which calls
|
|
131
|
+
* `requireProvider` before a byte is sent (`models.js:454–459`). Each region
|
|
132
|
+
* builds its OWN registry from its own adapter — `current()` registers
|
|
133
|
+
* `profile.piProvider` per profile (`lib/index.js:1852–1862`) — so the
|
|
134
|
+
* `workbuddy-global` region registered only `workbuddy-global` and could not
|
|
135
|
+
* dispatch a model stamped `workbuddy`: every request failed with
|
|
136
|
+
* `Unknown provider: workbuddy` before reaching the gateway.
|
|
137
|
+
* {@link modelIdentityKeysOf} is the correction: the identity has to be a
|
|
138
|
+
* registry key of its own in every region, or the stamp cannot be shared.
|
|
139
|
+
*
|
|
140
|
+
* Lookup by ROUTE is a separate question and stays intact: `getModel(provider,
|
|
141
|
+
* id)` is `getModels(provider).find(...)` (`models.js:116–118`), i.e. it goes
|
|
142
|
+
* through the map key, while the STAMPED field is whatever the provider put
|
|
143
|
+
* there. So the route stays `workbuddy`/`workbuddy-global` (each with its own
|
|
144
|
+
* shim, catalog and credential store) while every model it serves advertises
|
|
145
|
+
* THIS id. The openai-completions api reads `model.provider` only for
|
|
146
|
+
* provider-specific quirks (none of which match these ids) and as an
|
|
147
|
+
* error-message label.
|
|
148
|
+
*
|
|
149
|
+
* Deliberately NOT the same string as the global route: a message is only
|
|
136
150
|
* same-model when provider, api AND model id all match, so the shared id must
|
|
137
151
|
* not be reachable as a route a user could pick — it names one model, not one
|
|
138
|
-
* gateway.
|
|
152
|
+
* gateway. It IS reachable as a pi-ai registry key, which is the point: those
|
|
153
|
+
* are internal and never surface as a route, because `registerAdapter` takes
|
|
154
|
+
* the route list explicitly (`dsh-llm/lib/index.js:1842–1861`,
|
|
155
|
+
* `prepareRoutes` `:1867–1885`) and this plugin passes exactly the two region
|
|
156
|
+
* ids (`src/index.ts:2069–2086`).
|
|
139
157
|
*/
|
|
140
158
|
const WORKBUDDY_MODEL_IDENTITY = "workbuddy";
|
|
159
|
+
/**
|
|
160
|
+
* Registry keys an adapter serving `providerId` must publish so that every
|
|
161
|
+
* stamp its models carry resolves inside its own region (issue #75).
|
|
162
|
+
*
|
|
163
|
+
* The route id first, then the model identity, de-duplicated — route-first so
|
|
164
|
+
* `profiles()` opens with the route the adapter actually serves. CN yields one
|
|
165
|
+
* key (`workbuddy` is both its route and the shared identity) and the global
|
|
166
|
+
* region yields two (`workbuddy-global`, `workbuddy`); the two regions
|
|
167
|
+
* therefore publish different key sets, and each must publish the shared one.
|
|
168
|
+
*
|
|
169
|
+
* `identity` is a parameter because {@link WorkBuddyAdapterOptions.modelIdentity}
|
|
170
|
+
* can override the stamp: the keys have to follow the stamp, or a test that
|
|
171
|
+
* stamps a per-region id would get the production keys back.
|
|
172
|
+
*/
|
|
173
|
+
function modelIdentityKeysOf(providerId, identity = WORKBUDDY_MODEL_IDENTITY) {
|
|
174
|
+
return [...new Set([providerId, identity].filter((key) => key !== void 0))];
|
|
175
|
+
}
|
|
141
176
|
/** Region a provider route id belongs to. */
|
|
142
177
|
function regionOfProvider(provider) {
|
|
143
178
|
for (const [region, id] of Object.entries(WORKBUDDY_PROVIDERS)) if (id === provider) return region;
|
|
@@ -250,7 +285,9 @@ function workBuddyThinkingLevelMap(info) {
|
|
|
250
285
|
*
|
|
251
286
|
* `identity` — not the route id — is what `provider` carries; see
|
|
252
287
|
* {@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,
|
|
288
|
+
* model is REACHED by is the registry key the adapter serves, while this
|
|
289
|
+
* stamped field is what pi-ai resolves the provider BY (issue #75) — so every
|
|
290
|
+
* key in {@link modelIdentityKeysOf} must be a key this route registers.
|
|
254
291
|
*/
|
|
255
292
|
function toPiModel(info, baseUrl, identity = WORKBUDDY_MODEL_IDENTITY) {
|
|
256
293
|
const thinkingLevelMap = workBuddyThinkingLevelMap(info);
|
|
@@ -589,17 +626,21 @@ function createWorkBuddyAdapter(options) {
|
|
|
589
626
|
}),
|
|
590
627
|
getModels: () => buildModels()
|
|
591
628
|
};
|
|
592
|
-
const
|
|
593
|
-
provider:
|
|
629
|
+
const profileFor = (key) => ({
|
|
630
|
+
provider: key,
|
|
594
631
|
displayName: providerName,
|
|
595
632
|
streamIdleTimeoutMs: WORKBUDDY_STREAM_IDLE_TIMEOUT_MS,
|
|
596
633
|
retryPolicy: resolveRetryPolicy(void 0, "dsh-connect-workbuddy retryPolicy"),
|
|
597
634
|
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
598
635
|
modelErrors: /* @__PURE__ */ new Map(),
|
|
599
636
|
...REQUEST_IMAGE_BUDGETS,
|
|
600
|
-
piProvider: provider
|
|
601
|
-
|
|
602
|
-
|
|
637
|
+
piProvider: key === providerId ? provider : {
|
|
638
|
+
...provider,
|
|
639
|
+
id: key
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
const buildProfiles = () => new Map(modelIdentityKeysOf(providerId, modelIdentity).map((key) => [key, profileFor(key)]));
|
|
643
|
+
let profiles = buildProfiles();
|
|
603
644
|
return {
|
|
604
645
|
adapter: new PiAiAdapter({
|
|
605
646
|
profiles: () => profiles,
|
|
@@ -608,7 +649,7 @@ function createWorkBuddyAdapter(options) {
|
|
|
608
649
|
...resolveAttachments === void 0 ? {} : { resolveAttachments }
|
|
609
650
|
}),
|
|
610
651
|
invalidate: () => {
|
|
611
|
-
profiles =
|
|
652
|
+
profiles = buildProfiles();
|
|
612
653
|
}
|
|
613
654
|
};
|
|
614
655
|
}
|
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.9.
|
|
5
|
+
"version": "4.9.2",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|