dsh-connect-workbuddy 5.12.1 → 5.12.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 CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.12.2 (2026-10-27)
4
+
5
+ > 两处修复:**① 装完重启插件消失**(#151)——把宿主提供的 `@earendil-works/pi-ai` 标成 optional
6
+ > peer,使安装期不再因「闭包里解析不到它」而拒绝整个 generation;**② DSML 工具名一字之差**
7
+ > (#152)——`pwdsh` → `pwsh` 这类与本次声明工具只差一个字符的名字,现在按那个工具恢复,
8
+ > 不再整段回退成正文。两处都保持 fail-closed 的默认:候选集只来自请求自己的 `body.tools`,
9
+ > 不凭空造名。
10
+
11
+ ### Fixes
12
+
13
+ - **安装成功但重启后插件消失:generation peer validation 拒绝了安装(#151)**
14
+ - **现象**:插件市场安装显示 `generation-install: promoted to …`,接着
15
+ `generation peer validation failed: @earendil-works/pi-ai does not resolve from the generation or
16
+ installation closure`,`exit=1`;重启后该 generation 被扫掉(`swept N unreferenced plugin generation(s)`),
17
+ 用户得重装。
18
+ - **根因不是"插件依赖没装",而是"它不可能被装上"**:pi-ai 由**宿主**提供 —— `dsh-llm-pi-ai` 把它列为自己的
19
+ `dependencies`(`^1.1.0`),宿主用运行期拦截层应答这个裸包名;而 profile 是按 `autoInstallPeers: false` 建的,
20
+ peer 从不进入 profile 的 `node_modules`。所以一份「必需 peer」在安装闭包里**永远解析不到**,校验必然失败。
21
+ - **修法**:`package.json` 增加 `peerDependenciesMeta`,把 `@earendil-works/pi-ai` 标为 `optional: true`。
22
+ **范围一个字符都没动**(仍是 `>=0.85.0 <0.88.0 || >=1.0.0-0 <2.0.0`)——`optional` 说的是「不要
23
+ 求我自己带一份」,不是「我不支持这些版本」;运行期版本探测(`pi-ai-runtime.ts`)与语义边界仍以该范围为准。
24
+ - 为什么不是干脆删掉这条 peer:插件**静态 import** 了 pi-ai(`adapter.ts:19–21`),它不是可选依赖,
25
+ 声明必须留着;只有"安装期强制"这一层需要让开。
26
+ - 报告里那句「`@deepseek-ai/cordis` 已有同样处理,照此办理即可」**不成立** —— 本仓库此前
27
+ `peerDependenciesMeta` 是空的,cordis 也从未标 optional。修法本身没问题,但依据是上面那条,不是先例。
28
+
29
+ - **DSML 泄漏时工具名一字之差,整段标记回退成正文(#152)**
30
+ - **现象**:模型把工具调用写进正文时把名字写成 `pwdsh`,而请求声明的是 `pwsh`。第二道闸按名字拒绝
31
+ (这是对的:那个名字从未被提供过),于是整块按原文显示 —— 用户看到一堵乱码墙。
32
+ - **新证据**:幻觉名与真实名之间存在**可预测的一字变形**(`pwdsh` = `pwsh` 中间多插一个 `d`,
33
+ 编辑距离 1)。这跟此前记录过的 `validate` 那种「名字整段被换掉」不同,是可以低成本兜底的。
34
+ - **修法**:第二道闸在拒绝之前先试一次受限纠正(`repairNearMissName`,由 shim 开启):
35
+ - 候选集**只来自本次请求的 `body.tools`** —— 永远不会选中一个没被提供的名字,更不会凭空造名;
36
+ - 写出名与候选名的**编辑距离必须 ≤ 1**,且**恰好一个**候选落在这个距离内(两个就放弃);
37
+ - 双方长度都 **≥ 4**(`cat`/`cut`、`bash`/`dash` 这类三字母的一字之差不是证据);
38
+ - 写出名若**已精确命中**声明名,直接用它,绝不"纠正"到邻居;
39
+ - **只纠正 `invoke name=`**;`parameter name=` 一律不动(改参数名等于改调用语义,比显示正文更糟);
40
+ - `tool_choice` 的钉选、第四道闸(必需参数缺失即截断)**都在纠正后的名字上照旧生效**。
41
+ - 默认仍是 fail-closed:`repairNearMissName` 不传就是关闭,只有 shim 的请求路径打开它。
42
+
43
+ ### Tests
44
+
45
+ - `tests/dsml-recovery.spec.ts` 新增 10 条(#152):真实捕获形态 `pwdsh` → `pwsh` 被恢复;不开关时仍拒绝;
46
+ 两个候选在距离内则放弃;短名(写出名 3 字 / 候选 3 字)两种方向都拒绝;已精确命中的名字不被"纠正";
47
+ 不在声明集里的近邻不被凭空选中;钉选与必需参数两道闸在纠正后仍生效;**参数名不被纠正**。
48
+ - `tests/shim-recovery.spec.ts` 新增 1 条(#152 的**接线**):走真实 shim 与真实响应流,断言 `pwdsh`
49
+ 在声明 `pwsh` 时被恢复成 `pwsh`、在只声明 `bash` 时**仍回退成正文**。这条单独存在的理由与 #149 相同:
50
+ 纯函数用例自己传开关,摘掉 `shim.ts` 里的 `repairNearMissName: true` 时它们全绿 —— 实测该变异只让这条变红。
51
+ - `tests/dsh-line.spec.ts` 新增 1 条(#151):断言 `peerDependenciesMeta['@earendil-works/pi-ai'].optional === true`,
52
+ 且 peer **范围**未因此改动。
53
+ - 变异验证(每条都确认过会让对应用例变红):去掉编辑距离判断 → 1 条红;去掉唯一性判断 → 1 条红;
54
+ 去掉两个长度下限 → 2 条红;忽略 opt-in(默认开启)→ 1 条红;摘掉 shim 的接线 → 1 条红;移除 `optional` → 1 条红。
55
+ - 全量:`tsc` 双配置 0 错误,**143 文件 / 2491 通过(2 条按平台跳过)**,`tsdown` 构建通过。
56
+
3
57
  ## 5.12.1 (2026-10-26)
4
58
 
5
59
  > 修正 5.12.0 的 `dropForeignThinking` 漏判的一条腿:`isSameModel` 判据是 provider **且** api
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as WorkBuddyCredentialStore, D as defaultDesktopAuthDirs, E as defaultDesktopAuthCandidates, I as workbuddyOwnAuthPath, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, S as WORKBUDDY_AUTH_FILE_ENV, Ut as findWorkbuddyAppExecutableWithSource, V as WorkBuddyUpstreamClient, Zt as hostPeersInfo, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as rosterSeenPath, f as piAiRuntimeInfo, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, o as workbuddyHostHeartbeatPath, p as FALLBACK_WORKBUDDY_MODELS, r as isHeartbeatProcessAlive, u as describeRosterSeenPayload } from "./host-heartbeat-DKKM63s5.js";
2
+ import { C as WorkBuddyCredentialStore, D as defaultDesktopAuthDirs, E as defaultDesktopAuthCandidates, I as workbuddyOwnAuthPath, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, S as WORKBUDDY_AUTH_FILE_ENV, Ut as findWorkbuddyAppExecutableWithSource, V as WorkBuddyUpstreamClient, Zt as hostPeersInfo, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as rosterSeenPath, f as piAiRuntimeInfo, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, o as workbuddyHostHeartbeatPath, p as FALLBACK_WORKBUDDY_MODELS, r as isHeartbeatProcessAlive, u as describeRosterSeenPayload } from "./host-heartbeat-BdTV4Aoi.js";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { realpathSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
@@ -7566,7 +7566,7 @@ function describeRosterSeenPayload(raw) {
7566
7566
  //#endregion
7567
7567
  //#region src/version.ts
7568
7568
  /** The npm package version this build was produced from. */
7569
- const WORKBUDDY_CONNECT_VERSION = "5.12.1";
7569
+ const WORKBUDDY_CONNECT_VERSION = "5.12.2";
7570
7570
  //#endregion
7571
7571
  //#region src/host-heartbeat.ts
7572
7572
  /**
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as parseCreditMultiplier, $t as withFileLock, A as isEncryptedCredentialError, At as pickMediaModel, B as WorkBuddyCredentialRejectedError, Bt as deriveAtRestKeyId, C as WorkBuddyCredentialStore, Ct as IMAGE_TAG_TEXT_TO_IMAGE, D as defaultDesktopAuthDirs, Dt as imageMimeForPath, E as defaultDesktopAuthCandidates, Et as imageDataUrl, F as workbuddyAccountId, Ft as imageDefaultFor, G as forceNeutralHead, Gt as isWorkbuddyBundle, H as channelGateMemberOf, Ht as findWorkbuddyAppExecutable, I as workbuddyOwnAuthPath, It as nativeModalityOf, J as isCredentialRejectedError, Jt as openEncryptedField, K as isContextTooLongBody, Kt as macosBundleExecutable, L as CONTEXT_OVERFLOW_MARKER, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, Mt as parseQueueAnswer, N as parseWorkBuddyAuth, Nt as queueNoticeText, O as defaultDesktopAuthPath, Ot as isBareBase64Image, P as preferredAccountName, Pt as NATIVE_MODALITY_BY_MODEL_ID, Q as openingEvidenceOfBody, Qt as resolveDshHome, R as CREDENTIAL_REJECTED_CODE, Rt as clearAtRestKeyCache, S as WORKBUDDY_AUTH_FILE_ENV, St as IMAGE_TAG_IMAGE_TO_IMAGE, T as authFileName, Tt as WorkBuddyImageError, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, Vt as fetchAtRestKeyPayload, W as declaredTools, Wt as isEncryptedFieldWrapper, X as knownRegionOf, Xt as workbuddyAppExecutableCandidates, Y as isFirstMessageGateBody, Yt as readAtRestKey, Z as modelIdOfBody, _ as deriveCatalog, _t as WorkBuddyVideoError, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as resolveVideoParams, c as WORKBUDDY_CONNECT_VERSION, ct as REFUSES_OFF_MODEL_IDS, dt as offDefaultFor, en as writeFileAtomic, et as parseReasoning, f as piAiRuntimeInfo, ft as offRefused, g as applyContextBudgets, gt as VIDEO_TAG_TEXT_TO_VIDEO, h as WorkBuddyCatalog, ht as VIDEO_TAG_IMAGE_TO_VIDEO, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as sniffImageMime, k as hasEncryptedCredentialFields, kt as parseImageDataUrl, l as createFileRosterSeenStore, lt as defaultOffModelIds, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, mt as withEffectiveOff, n as clearHostHeartbeat, nt as perturbChannelFingerprints, o as workbuddyHostHeartbeatPath, ot as rewriteSessionTitleModel, p as FALLBACK_WORKBUDDY_MODELS, pt as recordRefusal, q as isConversationIdGateBody, qt as macosNestedAppBundles, r as isHeartbeatProcessAlive, rt as prepareChatBody, s as writeHostHeartbeat, st as attachBusyLevels, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as parseUpstreamModel, ut as lastRefusalFor, v as fallbackModelsFor, vt as parseSupportedSecondsRange, w as WorkBuddyEncryptedCredentialError, wt as MAX_INPUT_IMAGE_BYTES, x as WORKBUDDY_AUTH_FILENAME, xt as DEFAULT_IMAGE_SIZE, y as withoutRouters, yt as pickVideoModel, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as deriveAtRestKey } from "./host-heartbeat-DKKM63s5.js";
1
+ import { $ as parseCreditMultiplier, $t as withFileLock, A as isEncryptedCredentialError, At as pickMediaModel, B as WorkBuddyCredentialRejectedError, Bt as deriveAtRestKeyId, C as WorkBuddyCredentialStore, Ct as IMAGE_TAG_TEXT_TO_IMAGE, D as defaultDesktopAuthDirs, Dt as imageMimeForPath, E as defaultDesktopAuthCandidates, Et as imageDataUrl, F as workbuddyAccountId, Ft as imageDefaultFor, G as forceNeutralHead, Gt as isWorkbuddyBundle, H as channelGateMemberOf, Ht as findWorkbuddyAppExecutable, I as workbuddyOwnAuthPath, It as nativeModalityOf, J as isCredentialRejectedError, Jt as openEncryptedField, K as isContextTooLongBody, Kt as macosBundleExecutable, L as CONTEXT_OVERFLOW_MARKER, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, Mt as parseQueueAnswer, N as parseWorkBuddyAuth, Nt as queueNoticeText, O as defaultDesktopAuthPath, Ot as isBareBase64Image, P as preferredAccountName, Pt as NATIVE_MODALITY_BY_MODEL_ID, Q as openingEvidenceOfBody, Qt as resolveDshHome, R as CREDENTIAL_REJECTED_CODE, Rt as clearAtRestKeyCache, S as WORKBUDDY_AUTH_FILE_ENV, St as IMAGE_TAG_IMAGE_TO_IMAGE, T as authFileName, Tt as WorkBuddyImageError, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, Vt as fetchAtRestKeyPayload, W as declaredTools, Wt as isEncryptedFieldWrapper, X as knownRegionOf, Xt as workbuddyAppExecutableCandidates, Y as isFirstMessageGateBody, Yt as readAtRestKey, Z as modelIdOfBody, _ as deriveCatalog, _t as WorkBuddyVideoError, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as resolveVideoParams, c as WORKBUDDY_CONNECT_VERSION, ct as REFUSES_OFF_MODEL_IDS, dt as offDefaultFor, en as writeFileAtomic, et as parseReasoning, f as piAiRuntimeInfo, ft as offRefused, g as applyContextBudgets, gt as VIDEO_TAG_TEXT_TO_VIDEO, h as WorkBuddyCatalog, ht as VIDEO_TAG_IMAGE_TO_VIDEO, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as sniffImageMime, k as hasEncryptedCredentialFields, kt as parseImageDataUrl, l as createFileRosterSeenStore, lt as defaultOffModelIds, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, mt as withEffectiveOff, n as clearHostHeartbeat, nt as perturbChannelFingerprints, o as workbuddyHostHeartbeatPath, ot as rewriteSessionTitleModel, p as FALLBACK_WORKBUDDY_MODELS, pt as recordRefusal, q as isConversationIdGateBody, qt as macosNestedAppBundles, r as isHeartbeatProcessAlive, rt as prepareChatBody, s as writeHostHeartbeat, st as attachBusyLevels, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as parseUpstreamModel, ut as lastRefusalFor, v as fallbackModelsFor, vt as parseSupportedSecondsRange, w as WorkBuddyEncryptedCredentialError, wt as MAX_INPUT_IMAGE_BYTES, x as WORKBUDDY_AUTH_FILENAME, xt as DEFAULT_IMAGE_SIZE, y as withoutRouters, yt as pickVideoModel, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as deriveAtRestKey } from "./host-heartbeat-BdTV4Aoi.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -2793,6 +2793,56 @@ const MARKER_ONLY_TAG_NAMES = /* @__PURE__ */ new Set(["tool-calls", "toolcalls"
2793
2793
  /** Fence markers at the start of a line. */
2794
2794
  const FENCE_MARKERS = ["```", "~~~"];
2795
2795
  /**
2796
+ * Shortest tool name a near-miss repair may act on (issue #152).
2797
+ *
2798
+ * Below this, "one edit apart" stops being evidence: `dash` and `bash` are one
2799
+ * edit apart, and so are any two four-letter names sharing a prefix. At 4 the
2800
+ * reported `pwsh` → `pwdsh` still qualifies (both are 4+), which is the shape
2801
+ * this exists for.
2802
+ */
2803
+ const NEAR_NAME_MIN_LENGTH = 4;
2804
+ /**
2805
+ * Whether `a` and `b` are at most one edit apart (insertion, deletion, or
2806
+ * substitution).
2807
+ *
2808
+ * Only ever asked for a ceiling of 1, where the general Levenshtein matrix is
2809
+ * unnecessary: the lengths must differ by at most one, and after skipping the
2810
+ * first position where they differ, the remaining tails must still agree.
2811
+ *
2812
+ * The equal-length case is a SUBSTITUTION, not an insertion, so it is checked
2813
+ * separately — skipping the differing character on one side only would leave
2814
+ * tails of different length and silently miss every one-character typo of the
2815
+ * same length (`pwdsh`/`pwdss`, `bash`/`dash`), which is the whole point here.
2816
+ */
2817
+ function withinOneEdit(a, b) {
2818
+ if (a === b) return true;
2819
+ const [short, long] = a.length <= b.length ? [a, b] : [b, a];
2820
+ if (long.length - short.length > 1) return false;
2821
+ let i = 0;
2822
+ while (i < short.length && short[i] === long[i]) i++;
2823
+ if (short.length === long.length) return short.slice(i + 1) === long.slice(i + 1);
2824
+ return long.slice(i + 1) === short.slice(i);
2825
+ }
2826
+ /**
2827
+ * The one declared tool within a single edit of `written`, or `undefined`.
2828
+ *
2829
+ * Refuses when the name is already declared (nothing to repair), when either
2830
+ * side is shorter than {@link NEAR_NAME_MIN_LENGTH}, and when more than one
2831
+ * declared tool is in reach — ambiguity is exactly what gate 2 exists to stop.
2832
+ */
2833
+ function nearMissNameFor(written, declaredNames) {
2834
+ if (declaredNames.has(written)) return void 0;
2835
+ if (written.length < NEAR_NAME_MIN_LENGTH) return void 0;
2836
+ let fit;
2837
+ for (const candidate of declaredNames) {
2838
+ if (candidate.length < NEAR_NAME_MIN_LENGTH) continue;
2839
+ if (!withinOneEdit(written, candidate)) continue;
2840
+ if (fit !== void 0) return void 0;
2841
+ fit = candidate;
2842
+ }
2843
+ return fit;
2844
+ }
2845
+ /**
2796
2846
  * Find every span that must not be scanned for markup.
2797
2847
  *
2798
2848
  * One pass, not a predicate call per character: the reference asks
@@ -3208,8 +3258,11 @@ function parseInvoke(open, close, block, gate) {
3208
3258
  const parameters = parseParameters(block.slice(open.end + 1, close.start));
3209
3259
  let name;
3210
3260
  if (declared !== void 0 && declared !== "") {
3211
- if (!gate.declaredNames.has(declared)) return void 0;
3212
- name = declared;
3261
+ if (!gate.declaredNames.has(declared)) {
3262
+ const repaired = gate.repairNearMissName === true ? nearMissNameFor(declared, gate.declaredNames) : void 0;
3263
+ if (repaired === void 0) return void 0;
3264
+ name = repaired;
3265
+ } else name = declared;
3213
3266
  } else if (open.attributes.trim() === "") {
3214
3267
  name = inferNameFromSchema(parameters, gate);
3215
3268
  if (name === void 0) return void 0;
@@ -4995,6 +5048,7 @@ function createWorkBuddyShim(options) {
4995
5048
  requiredParameters: declared.requiredParameters,
4996
5049
  acceptedParameters: declared.acceptedParameters,
4997
5050
  inferFromSchema: true,
5051
+ repairNearMissName: true,
4998
5052
  ...declared.pinnedToolName === void 0 ? {} : { pinnedToolName: declared.pinnedToolName }
4999
5053
  };
5000
5054
  const notice = switchedFrom === void 0 || switchedBecause === void 0 ? void 0 : failoverNoticeText(switchedFrom, switchedBecause);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-workbuddy",
3
3
  "displayName": "DSH Connect WorkBuddy",
4
4
  "description": "把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness:每次刷新按上游目录自动读取每个模型的积分倍率,支持逐模型图片输入开关、多账号切换与账号池自动换号,并提供每日签到、卡片与输入框里的胶囊积分读数等只读积分概览。",
5
- "version": "5.12.1",
5
+ "version": "5.12.2",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",
@@ -96,6 +96,11 @@
96
96
  "@earendil-works/pi-ai": ">=0.85.0 <0.88.0 || >=1.0.0-0 <2.0.0",
97
97
  "react": "^18.2.0"
98
98
  },
99
+ "peerDependenciesMeta": {
100
+ "@earendil-works/pi-ai": {
101
+ "optional": true
102
+ }
103
+ },
99
104
  "devDependencies": {
100
105
  "@deepseek-ai/cordis": ">=4.0.1 <5.0.0 || >=4.0.2 <5.0.0",
101
106
  "@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.3.0-0",