dsh-connect-workbuddy 3.0.1 → 3.0.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 +13 -0
- package/lib/bin.js +1 -1
- package/lib/{host-heartbeat-CfXJht7R.js → host-heartbeat-Dwmfxuyw.js} +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +82 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.2 (2026-09-30)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- **修复 issue #24:宿主 pi-ai 0.87 下所有 WorkBuddy 模型立即失败**(`Cannot read properties of undefined (reading 'length')`)。
|
|
8
|
+
- **根因**:pi-ai 0.87 的 `normalizeContext` 把 system 提示词搬进 `messages`,成了 `{ role: 'system', content: '<字符串>' }`。而 pi-ai 0.85 的 `Message` 联合类型里**根本没有 `system` 变体**(只有 `UserMessage | AssistantMessage | ToolResultMessage`),于是它的 `estimateMessageTokens` 没有对应分支:`for (const block of message.content)` 会把**字符串逐字符**迭代,`block` 变成单个字符,`block.name.length` 抛错。崩溃发生在 `buildBaseOptions → clampMaxTokensToContext → estimateContextTokens`,即**构造请求体之前**——所以每个模型都在十几毫秒内失败,且**没有任何上游请求发出**。
|
|
9
|
+
- **修法**:新增 `adaptLegacyPiAiContext()`,在把 context 交给本插件的 pi-ai 之前,把 `messages` 里的 system 消息折回 `Context.systemPrompt`(0.85 期望的位置),并用 `withLegacyContext()` 包住 `openAICompletionsApi()` 的 `stream`/`streamSimple` 两个入口。
|
|
10
|
+
- **按形状适配,不按版本判断**:判断依据是「context 里有没有 system 消息」,不是「加载的是哪个 pi-ai 版本」。版本判断会在任一侧升级时立刻失效;本插件必须在**两代宿主**下都能活。对原生 0.85 形状(已有 `systemPrompt`)返回**同一个对象**,原路径零改动。
|
|
11
|
+
- **实测**(用插件真正加载的那份 pi-ai 0.85.1):修复前 0.87 形状 → `Cannot read properties of undefined (reading 'length')`;修复后 → `Connection error.`(走到传输层,即请求已构造完成)。旧形状恒等,`system` 用 block 内容时也能正确合并。
|
|
12
|
+
- **守卫**:`tests/adapter.spec.ts` 新增 8 例,其中**接线用例**(`vi.mock` 掉 stream API,断言 provider 收到的 context 已被折平)是必要的——只测纯函数时,把修复从 provider 上摘掉的变异**不会被打红**(已实测),等于留了一个「修复被摘掉而测试全绿」的缺口。变异验证:去掉 `withLegacyContext` → 该用例变红。
|
|
13
|
+
- **顺带核实**:宿主 asar 声明 `@earendil-works/pi-ai ^0.87.1` 且含 `normalizeContext`;磁盘上四份 pi-ai(profiles / 插件嵌套 / 仓库 / 旧安装树)**全是 0.85.1**。issue 建议的「插件包内放一份 0.87.1」绕行在本机并未生效——嵌套副本是 0.85.1,漂移依旧,只是被当前加载的旧 `dsh-llm-pi-ai`(0.1.5-rc.2,不含 `normalizeContext`)掩盖了。
|
|
14
|
+
- **peer 范围放宽**:`@earendil-works/pi-ai` 由 `>=0.85.0 <0.86.0` 改为 `>=0.85.0 <0.88.0`(peer 与 dev 两处)。原范围**不承认 0.87**,与实际已验证的兼容性不符:适配按形状生效,代码在 0.85 与 0.87 下都已验证可用。放宽后 `pnpm install` 报 "Lockfile is up to date",未产生 lockfile 变更。
|
|
15
|
+
|
|
3
16
|
## 3.0.1 (2026-09-30)
|
|
4
17
|
|
|
5
18
|
### Bug Fixes
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { O as workbuddyOwnAuthPath, T as legacyWorkbuddyOwnAuthPath, W as findWorkbuddyAppExecutable, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, g as WORKBUDDY_AUTH_FILE_ENV, j as WorkBuddyUpstreamClient, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, x as defaultDesktopAuthDirs, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-
|
|
2
|
+
import { O as workbuddyOwnAuthPath, T as legacyWorkbuddyOwnAuthPath, W as findWorkbuddyAppExecutable, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, g as WORKBUDDY_AUTH_FILE_ENV, j as WorkBuddyUpstreamClient, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, x as defaultDesktopAuthDirs, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-Dwmfxuyw.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
|
@@ -2286,7 +2286,7 @@ var WorkBuddyCatalog = class {
|
|
|
2286
2286
|
//#endregion
|
|
2287
2287
|
//#region src/version.ts
|
|
2288
2288
|
/** The npm package version this build was produced from. */
|
|
2289
|
-
const WORKBUDDY_CONNECT_VERSION = "3.0.
|
|
2289
|
+
const WORKBUDDY_CONNECT_VERSION = "3.0.2";
|
|
2290
2290
|
//#endregion
|
|
2291
2291
|
//#region src/host-heartbeat.ts
|
|
2292
2292
|
/**
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-
|
|
1
|
+
import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-Dwmfxuyw.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
@@ -213,6 +213,86 @@ function toPiModel(info, baseUrl, providerId) {
|
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
/**
|
|
216
|
+
* Rewrite a pi-ai 0.87-shaped context into the 0.85 shape this plugin's pi-ai
|
|
217
|
+
* understands.
|
|
218
|
+
*
|
|
219
|
+
* WHY THIS EXISTS (issue #24). pi-ai 0.87's `normalizeContext` moves the system
|
|
220
|
+
* prompt INTO `messages` as `{ role: 'system', content: '<string>' }`. pi-ai
|
|
221
|
+
* 0.85's own `Message` union has no `system` variant
|
|
222
|
+
* (`UserMessage | AssistantMessage | ToolResultMessage`), so its
|
|
223
|
+
* `estimateMessageTokens` has no branch for one: `for (const block of
|
|
224
|
+
* message.content)` iterates the CONTENT STRING character by character, `block`
|
|
225
|
+
* is a single character, and `block.name.length` throws
|
|
226
|
+
* `Cannot read properties of undefined (reading 'length')`.
|
|
227
|
+
*
|
|
228
|
+
* That crash happens inside the library, in `buildBaseOptions ->
|
|
229
|
+
* clampMaxTokensToContext -> estimateContextTokens`, i.e. BEFORE any request is
|
|
230
|
+
* built — so every model fails instantly and no upstream traffic is sent. A
|
|
231
|
+
* host running 0.87 hands us the normalized transcript while our own provider
|
|
232
|
+
* is 0.85, and the plugin cannot patch the library. What it CAN do is not hand
|
|
233
|
+
* a 0.87 transcript to a 0.85 API object: fold the system text back into
|
|
234
|
+
* `Context.systemPrompt`, which is where 0.85 expects it.
|
|
235
|
+
*
|
|
236
|
+
* Deliberately SHAPE-based, not version-based: it asks "does this context carry
|
|
237
|
+
* a system message inside `messages`?" rather than "which pi-ai version is
|
|
238
|
+
* loaded?". A version check would be wrong the moment either side moves, and
|
|
239
|
+
* this plugin has to survive both host generations.
|
|
240
|
+
*
|
|
241
|
+
* Returns the SAME object when there is nothing to adapt — in particular when
|
|
242
|
+
* `systemPrompt` is already set (the native 0.85 shape), so the ordinary path
|
|
243
|
+
* is byte-for-byte untouched.
|
|
244
|
+
*/
|
|
245
|
+
function adaptLegacyPiAiContext(context) {
|
|
246
|
+
if (context === null || typeof context !== "object") return context;
|
|
247
|
+
if (context.systemPrompt !== void 0) return context;
|
|
248
|
+
const messages = Array.isArray(context.messages) ? context.messages : void 0;
|
|
249
|
+
if (messages === void 0) return context;
|
|
250
|
+
const systemTexts = [];
|
|
251
|
+
const rest = [];
|
|
252
|
+
let sawSystemMessage = false;
|
|
253
|
+
for (const message of messages) {
|
|
254
|
+
if (message?.role !== "system") {
|
|
255
|
+
rest.push(message);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
sawSystemMessage = true;
|
|
259
|
+
const text = systemTextOf(message);
|
|
260
|
+
if (text !== "") systemTexts.push(text);
|
|
261
|
+
}
|
|
262
|
+
if (!sawSystemMessage) return context;
|
|
263
|
+
return {
|
|
264
|
+
...context,
|
|
265
|
+
...systemTexts.length === 0 ? {} : { systemPrompt: systemTexts.join("\n\n") },
|
|
266
|
+
messages: rest
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** The text of one system message, whether it is a string or text blocks. */
|
|
270
|
+
function systemTextOf(message) {
|
|
271
|
+
const content = message.content;
|
|
272
|
+
if (typeof content === "string") return content;
|
|
273
|
+
if (!Array.isArray(content)) return "";
|
|
274
|
+
const texts = [];
|
|
275
|
+
for (const block of content) {
|
|
276
|
+
if (typeof block !== "object" || block === null) continue;
|
|
277
|
+
const candidate = block;
|
|
278
|
+
if (candidate.type === "text" && typeof candidate.text === "string") texts.push(candidate.text);
|
|
279
|
+
}
|
|
280
|
+
return texts.join("\n\n");
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Wrap one pi-ai API module so both stream entry points receive a context this
|
|
284
|
+
* build can actually consume (see {@link adaptLegacyPiAiContext}).
|
|
285
|
+
*
|
|
286
|
+
* The deferred-fetch entry points take no context and are passed through as-is.
|
|
287
|
+
*/
|
|
288
|
+
function withLegacyContext(api) {
|
|
289
|
+
return {
|
|
290
|
+
...api,
|
|
291
|
+
stream: (model, context, options) => api.stream(model, adaptLegacyPiAiContext(context), options),
|
|
292
|
+
streamSimple: (model, context, options) => api.streamSimple(model, adaptLegacyPiAiContext(context), options)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
216
296
|
* Assemble the adapter. The provider's `getModels` reads the live catalog,
|
|
217
297
|
* and every model's `baseUrl` is re-resolved per read so the shim's
|
|
218
298
|
* ephemeral port applies from the first snapshot after startup.
|
|
@@ -240,7 +320,7 @@ function createWorkBuddyAdapter(options) {
|
|
|
240
320
|
}
|
|
241
321
|
} },
|
|
242
322
|
models: buildModels(),
|
|
243
|
-
api: openAICompletionsApi()
|
|
323
|
+
api: withLegacyContext(openAICompletionsApi())
|
|
244
324
|
}),
|
|
245
325
|
getModels: () => buildModels()
|
|
246
326
|
};
|
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": "3.0.
|
|
5
|
+
"version": "3.0.2",
|
|
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.
|
|
94
|
+
"@earendil-works/pi-ai": ">=0.85.0 <0.88.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.
|
|
113
|
+
"@earendil-works/pi-ai": ">=0.85.0 <0.88.0",
|
|
114
114
|
"@types/node": "^22.20.0",
|
|
115
115
|
"@types/react": "~18.3.31",
|
|
116
116
|
"@types/react-dom": "18.3.7",
|