dsh-connect-workbuddy 4.6.7 → 4.8.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 +96 -0
- package/README.en.md +12 -3
- package/README.md +12 -3
- package/lib/bin.js +1 -1
- package/lib/{host-heartbeat-D7a100yu.js → host-heartbeat--S6JVSci.js} +14 -6
- package/lib/index.d.ts +42 -4
- package/lib/index.js +315 -30
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,101 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.8.0 (2026-10-18)
|
|
4
|
+
|
|
5
|
+
> 版本号是 **minor**:这不是只改内部的 patch —— 它**会改动会话历史**。跨区/跨窗口切换后原本必然卡死的那一类会话,本版能把上下文折回去继续用。没有溢出时行为完全不变。
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **切到窗口更小的路由后,会话永久卡在 `400 code 11115 prompt is too long`(#59 后半)**
|
|
10
|
+
- **症状**:会话在一个 1M 窗口的模型上跑得很长,切成国际区(`workbuddy-global`,窗口 1,048,576)后,**每一条**请求都返回 `prompt is too long: 1374460 tokens > 1048576 maximum`。换回原区域能用,再切过去还是卡死;手动压缩同样 400。会话实质报废。
|
|
11
|
+
- **根因(宿主侧,两处叠加)**:
|
|
12
|
+
1. **压力阈值按「当前模型」窗口现算**,`compaction-basic/src/config.ts:188-192` 的 `thresholdTokens = min(contextWindow × thresholdRatio, messageBudgetTokens)`。1M 窗口下阈值是 800,000,而 707,801 未到 —— 会话在**切区那一刻就已经超了新模型的窗口**,可自动压缩的触发点还在旧模型的尺度上,永远等不到。
|
|
13
|
+
2. **溢出兜底把整段历史塞进一次摘要请求**,`compaction-basic/src/index.ts:299` 的 `selectCompactableRange(session, measurement, 0)` 保留量为 0 → `region.ts:150-154` 选出「除最后一条外的全部历史」;`summarizer.ts:141-151` 又把这份摘要发给**当前那条小窗口路由**。于是救援请求和它要救援的请求一样溢出 —— 死锁。
|
|
14
|
+
宿主侧已按 deepseek-ai/deepseek-harness#9814 提交,上面两处即该 issue 的 A、B 两点。
|
|
15
|
+
- **本版做的事**:适配器在溢出错误**仍然持有 session id** 的那一刻接手,用宿主**自己的**压缩事务(`ctx.compaction.compactRegion`)把历史折回去,而不是另发一次摘要请求。
|
|
16
|
+
- 分块上限 = 模型窗口的 **35%**,因此摘要请求本身**按构造**一定装得下(宿主默认那一次是 131%)。
|
|
17
|
+
- 折叠目标是窗口的 **60%**,逐块折叠直到低于目标;块边界来自宿主自己的 `tokenMeter`。
|
|
18
|
+
- **绝不折掉当前请求携带的那条消息**(`nodes.at(-1)` 永远保留)。
|
|
19
|
+
- 宿主拒绝某个区间(切断了 tool call 与 result 的配对、或正有压缩在跑)时,把尾边向内收一格重试;仍是拒绝就放弃,绝不空转。
|
|
20
|
+
- 折完仍降不下去(单条消息本身就超过窗口)时立刻停手 —— 这是结构性问题,重试只会烧掉重试预算。
|
|
21
|
+
- **实测(真实 GL 400 载荷)**:`isContextOverflow` 对 `prompt is too long: 1374460 tokens > 1048576 maximum` 与 `{"code":11115,...,"type":"CONTEXT_WINDOW_EXCEEDED"}` 均判定为真,对 `rate limit exceeded` 判定为假 —— 只在真溢出时动手。
|
|
22
|
+
- **降级路径**:宿主没装压缩包(`compaction` 服务不存在)、或请求没带 `sessionId`(非主循环调用)时,适配器原样返回,行为与本版之前**逐字节相同**。
|
|
23
|
+
|
|
24
|
+
### 已知未修
|
|
25
|
+
|
|
26
|
+
- **跨区 prompt token 计数漂移**(#59 前半):同一份 3.6 MB 请求体,CN 计 642,800、GL 计 447,306。这不是网关数错 —— 历史里的 thinking 块按上游当初实际返回的字段名(`reasoning` / `reasoning_content`)原样回放(宿主 `pi-ai` 0.85.1 `openai-completions.js:428` → `:1006`),同一批字节因此落进不同计费桶。归属宿主,本插件没有改写历史的权限,详见 #59 与 `src/body-report.ts` 头部说明。
|
|
27
|
+
|
|
28
|
+
## 4.7.0 (2026-10-17)
|
|
29
|
+
|
|
30
|
+
> 版本号是 **minor**:这不是只改内部的 patch —— 它**改变了发往上游的请求体**。DSH Desktop 0.2.1-rc.1 随包 pi-ai 1.1.0,本版修的是该组合下「请求静默丢掉 system prompt 与全部工具声明」。若你停留在更早的宿主,行为不变。
|
|
31
|
+
|
|
32
|
+
### Fixes
|
|
33
|
+
|
|
34
|
+
- **DSH 0.2.1-rc.1 下所有 workbuddy 模型静默丢失 system prompt 与工具声明(新一轮 #24/#25/#26 同族缺陷)**
|
|
35
|
+
- **症状**:升级到 DSH `0.2.1-rc.1`(随包 `@earendil-works/pi-ai@1.1.0`)后,workbuddy / workbuddy-global 的模型**全部**不再调用工具,界面上**没有任何报错**。模型有时把 DSML 标记当正文吐出来——和 4.4.2 记录的现象一模一样。
|
|
36
|
+
- **根因(三环叠加,缺一不成立)**:
|
|
37
|
+
1. `dsh-llm-pi-ai` **两版都不做归一化**。0.1.7 与 0.2.1 的 `piContext()` 都直接返回 `{systemPrompt, messages, tools}`,两版里 `normalizeContext` 出现次数均为 **0**。
|
|
38
|
+
2. pi-ai **0.85.1 读顶层字段**:`dist/api/openai-completions.js:909` 的 `if (context.systemPrompt)`,工具读 `context.tools`。所以 0.85 能自己兜底,宿主不归一化也看不出问题。
|
|
39
|
+
3. pi-ai **1.1.0 只读 `messages`**:走 `resolveTranscript()` → `collapseSystemMessages()`,顶层 `systemPrompt` / `tools` **从不读取**。
|
|
40
|
+
于是 `withLegacyContext()` 的 **modern 分支原样透传**(当时一行 `return api`),把 0.85 形状的原始信封直接交给 1.1.0 的 api → 请求体里 system prompt 与 tools 双双为空。
|
|
41
|
+
- **为什么 4.4.2 的修复不够**:4.4.2 修的是「1.x 被判成 legacy → 折成 0.85 形状 → 工具丢」。本版是**同一失效模式的反面**——「1.x 判成 modern(判定是对的)→ 不折 → 工具和 prompt 一起丢」。门是对的,空的是那个分支。`generationOf()` 无需改动。
|
|
42
|
+
- **实测证据(真实 `createProvider` + 真实 `openAICompletionsApi` + 本地 SSE 服务器抓线上 body)**:
|
|
43
|
+
|
|
44
|
+
| pi-ai | 线上 `messages[].role` | system 文本 | `body.tools` |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| 0.85.1 | `["system","user"]` | 原样 | 1 ✅ |
|
|
47
|
+
| **1.1.0(修复前)** | **`["user"]`** | **丢失** | **0** |
|
|
48
|
+
| 1.1.0(修复后) | `["system","user"]` | 原样 | 1 ✅ |
|
|
49
|
+
| 1.1.0(已归一化输入) | `["system","user"]` | 原样 | 1 ✅(幂等) |
|
|
50
|
+
|
|
51
|
+
- **改了什么**:新增 `foldModernContext()`,modern 分支改为**归一化**而非透传——把顶层的 `systemPrompt`/`tools` 折进首条 `system` 消息(文本进 `content`、工具进 `toolsAdded`),并**删掉**这两个顶层字段,与 pi-ai 官方 `normalizeContext` 的输出形状一致。
|
|
52
|
+
- **为什么内联而不是调 `normalizeContext`**:该函数在 **0.85.1 里不存在**,静态 import 会在那里变成 `undefined`;而本模块一贯「绝不静态 import pi-ai」(`pi-ai-runtime.ts` 头部教条,正是它让独立 `bin` 能在没装 peer 的 profile 里跑)。内联折叠零新依赖。
|
|
53
|
+
- **幂等**:输入若已归一化(顶层两字段都不存在),直接原样返回——否则会再前置一条**空的** system 消息。
|
|
54
|
+
- **★ 守卫为什么一直没抓到**:`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」,这个断言恰好**认证**了缺陷。
|
|
55
|
+
- **本版把守卫扩成 2×2 矩阵**:{legacy, modern} × {原始信封, 已归一化 transcript} 四格都断言 `body.tools` 与首条 system 文本。在原用例的基础上补了「modern × 原始信封」这一格——**它就是本缺陷的回归钉**。另加幂等用例与「只有 tools、没有 systemPrompt」用例。
|
|
56
|
+
- **本机与 CI 的代际约束(写在用例注释里)**:legacy 两格只能在 0.85 checkout 上跑(1.x 的 api 读不了 0.85 扁平形状);modern 两格只能在 0.87+/1.x 上跑(0.85 的 `estimateMessageTokens` 会对 system 消息的**字符串**逐字符迭代并读 `block.name.length` 而崩溃,即 #24 的崩溃)。用例按**已安装版本**选格,不会让任何一代完全无人看管。
|
|
57
|
+
- **变异验证**:把 modern 分支改回 `return api` → 新增的「FOLDS a raw seam envelope」用例**红**(1 例)。验证后已还原。
|
|
58
|
+
- **实机验证**:临时把 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 与依赖树。
|
|
59
|
+
|
|
60
|
+
- **「可关闭思考」的模型被锁成不可关闭,不可关闭的反倒可关(`adapter.ts` 既有缺陷,非本次升级引入)**
|
|
61
|
+
- **根因**:`workBuddyThinkingLevelMap()` 的判断写反了 —— `if (canDisableThinking !== true) map.off = null`,而 `null` 在 pi-ai 里的语义是「该档位**不支持**」(`getSupportedThinkingLevels` 按 `mapped === null` 过滤;`openai-completions` 用 `thinkingLevelMap?.off !== null` 决定能否发「关闭思考」的请求)。
|
|
62
|
+
- **实际后果**:上游声明 `canDisableThinking: true` 且用户没取消勾选 → `off` 被置 `null` → 关闭思考**不可选**;上游声明 `false`/未声明 → 不置 `null` → `off` 落成字符串 `'off'` → **反而可选**。与 `offDefaultFor()` 的语义正好相反。
|
|
63
|
+
- **为什么一直没发现**:`tests/adapter.spec.ts` 只探了 `canDisableThinking: false` 那半边,而写反的形式在那半边也恰好通过;另外两条对 `true` 的用例把期望值写成了「没有 off」或「off 缺失」,**把缺陷当成了规格**。
|
|
64
|
+
- **改了什么**:改为 `canDisableThinking === true ? 'off' : null`,并把三条用例的期望值改正,新增「能力未声明时不给 off」一例。
|
|
65
|
+
- **变异验证**:把判断改回 `!== true` → 2 例红。
|
|
66
|
+
|
|
67
|
+
- **DSH 宿主线守卫的 semver 比较器是错的:判 `0.2.1-rc.1` 通过,而 npm 实算是 false**
|
|
68
|
+
- **根因**:`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`)两条都不满足。
|
|
69
|
+
- **为什么夹具没触发**:原夹具是 `['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)。
|
|
70
|
+
- **改了什么**:比较器按规范重写(`parseVersion` / `compareParsed` / `sameTuple` + 悬崖规则),夹具拆成「正式版放行」与「已知预发布偏差」两组,并新增 5 条照抄 npm 实际输出的悬崖用例。
|
|
71
|
+
- **新增的「已知偏差」用例**:显式断言 `0.2.1-rc.1` 与 `0.2.0-rc.1` 在现声明下 **不** admitted、而 `0.2.1` GA admitted。于是偏差是**被记录、被解释**的,而不是被错比较器掩盖;将来若改清单,这条会提醒同步更新说明。
|
|
72
|
+
- **为什么可以暂不改清单**:详见下一条。宿主 `evaluatePluginCompatibility()` 只校验 `@deepseek-ai/dsh*` 命名空间,本机 profile 用 `link:` + 默认不自动装 peer,rc 宿主上插件照常加载;等宿主发 `0.2.1` GA,现声明即放行。
|
|
73
|
+
- **变异验证**:去掉悬崖规则 → 2 例红。
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **`@earendil-works/pi-ai` 的 peer 范围放宽到 1.x:`>=0.85.0 <0.88.0 || >=1.0.0-0 <2.0.0`(peer 与 dev 两处同步)**
|
|
78
|
+
- **为什么必须改**:旧范围 `>=0.85.0 <0.88.0` **不**放行 DSH 0.2.1-rc.1 随包的 1.1.0,而 1.1.0 恰恰是本版修好后要支持的版本 —— 声明与实际支持面自相矛盾。
|
|
79
|
+
- **范围是实算出来的,不是猜的**:`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 的判断保持一致;预发布串不承诺。
|
|
80
|
+
- **注意**:宿主门禁只校验 `@deepseek-ai/dsh*`,所以这条 peer **从不参与**宿主校验(4.4.2 已记录)。改它主要是让**清单说真话**,真正保护用户的是上面那条代码修复。
|
|
81
|
+
- **`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` 通配符,全部失败)。
|
|
82
|
+
|
|
83
|
+
### CI
|
|
84
|
+
|
|
85
|
+
- **新增 `test-pi-ai-next` 作业:在 pi-ai 1.1.0 上跑一遍完整 typecheck + 测试 + 构建**
|
|
86
|
+
- **为什么必须有**:`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 坏掉的那一半。
|
|
87
|
+
- **怎么切版本**:pi-ai 既是 peer 又是直接 devDependency,`pnpm-workspace.yaml` 的 `overrides` 是官方支持的改法。作业用 `sed` 改那一条 override,再 `--no-frozen-lockfile` 让 pnpm 只为它重解析。
|
|
88
|
+
- **有一道断言防止作业空转**:装完立刻 `node -p` 读实际版本并要求等于矩阵值,否则「跑了一遍绿」可能跑的根本不是 1.1.0。
|
|
89
|
+
- **本作业当场抓到一个本机永远看不到的缺陷**: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`。
|
|
90
|
+
- **验证过它真的会响**:把 modern 分支改回 `return api`,1.1.0 作业**红 4 例**(`adapter.spec.ts` 与 `adapter-outgoing-body.spec.ts` 各自的 modern 守卫),而 0.85.1 作业保持全绿。这正是本作业存在的意义。
|
|
91
|
+
- 仅跑 ubuntu:这是库层面的断言,不是平台断言;OS 矩阵已由主 `test` 作业覆盖。
|
|
92
|
+
|
|
93
|
+
### Docs
|
|
94
|
+
|
|
95
|
+
- `src/pi-ai-runtime.ts` 头部更正:peer 范围同步为新值;删去把 1.x 说成「未发布」的过时表述(DSH 0.2.1-rc.1 已随包 1.1.0);补一句「落在 modern 是必要但**不充分**——modern 分支还必须折叠原始信封」。
|
|
96
|
+
- `src/adapter.ts` 的 `adaptLegacyPiAiContext()` 补注 **legacy-only**:它的结果是 0.85 扁平形状,绝不能交给 1.x 的 api(1.x 从 `messages[].toolsAdded` 读工具,会一个都看不到)。
|
|
97
|
+
- 新增 `docs/DSH-0.2.1-RC1-FOLLOWUP.md`:本次升级的完整调研档,含逐包对照表、四项「已核对无需改动」、失败候选与证据来源。
|
|
98
|
+
|
|
3
99
|
## 4.6.7 (2026-10-17)
|
|
4
100
|
|
|
5
101
|
> 版本号是 **patch**:只改账号池的排序规则,没有接口或配置变更。但它是**行为变更**——开启账号池的机器上,「这次请求用谁」可能变了,且新行为正是本版要的。
|
package/README.en.md
CHANGED
|
@@ -152,7 +152,7 @@ One class of state in the pool **does not go through a draft**: probe results, c
|
|
|
152
152
|
|
|
153
153
|
## Install
|
|
154
154
|
|
|
155
|
-
> ⚠️ **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.
|
|
156
156
|
|
|
157
157
|
### Supported host range
|
|
158
158
|
|
|
@@ -161,11 +161,20 @@ DSH's shipped gate (`evaluatePluginCompatibility` in `packages/boot/app-boot/src
|
|
|
161
161
|
| Host version | Verdict |
|
|
162
162
|
| --- | --- |
|
|
163
163
|
| `0.1.7-rc.1` – `0.1.7-rc.2` | ✅ supported |
|
|
164
|
-
| `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 |
|
|
165
166
|
| `0.3.0-0` and above | ❌ explicitly refused (that line is unverified) |
|
|
166
167
|
| `0.1.6` and earlier | ❌ explicitly refused (0.1.5 users should stay on v2.0.15) |
|
|
167
168
|
|
|
168
|
-
>
|
|
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.
|
|
169
178
|
|
|
170
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.
|
|
171
180
|
|
package/README.md
CHANGED
|
@@ -160,7 +160,7 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
160
160
|
|
|
161
161
|
## 安装
|
|
162
162
|
|
|
163
|
-
> ⚠️ **版本要求: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 线被明确拒绝。详见下方[受支持的宿主范围](#受支持的宿主范围)。
|
|
164
164
|
|
|
165
165
|
### 受支持的宿主范围
|
|
166
166
|
|
|
@@ -169,11 +169,20 @@ DSH 的随包门禁(`packages/boot/app-boot/src/plugin-compatibility.ts` 的 `
|
|
|
169
169
|
| 宿主版本 | 判定 |
|
|
170
170
|
| --- | --- |
|
|
171
171
|
| `0.1.7-rc.1` ~ `0.1.7-rc.2` | ✅ 支持 |
|
|
172
|
-
| `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` …) | ⚠️ 不落在声明范围内,见下方说明 |
|
|
173
174
|
| `0.3.0-0` 及以上 | ❌ 明确拒绝(该线尚未验证) |
|
|
174
175
|
| `0.1.6` 及更早 | ❌ 明确拒绝(0.1.5 线请停留在 v2.0.15) |
|
|
175
176
|
|
|
176
|
-
>
|
|
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` 守卫(含预发布悬崖的用例)。
|
|
177
186
|
|
|
178
187
|
> 🪟 **平台支持:Windows、macOS、Linux 三者并列支持。** **Windows 是一等目标平台**,不是「顺带能跑」:CI 在 `windows-latest` 上跑完整套件,凭据路径、进程心跳、App 可执行文件定位、设置写入四处都有 Windows 专属分支与测试,并且**修复只对 Windows 生效的缺陷时,以 Windows 用户回报的现场与真机取证的加密向量为准**(见 [CHANGELOG](CHANGELOG.md) 的 2.0.6 / 2.0.11 / 2.0.12 各节)。见下方[平台支持](#平台支持)一节了解各平台的具体差异与已知限制。
|
|
179
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--S6JVSci.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.8.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
|
|
@@ -1246,6 +1246,18 @@ declare function createWorkBuddyShim(options: WorkBuddyShimOptions): WorkBuddySh
|
|
|
1246
1246
|
/** The resolved pi-ai generation, by context-shape capability. */
|
|
1247
1247
|
type PiAiGeneration = 'modern' | 'legacy';
|
|
1248
1248
|
//#endregion
|
|
1249
|
+
//#region src/context-rescue.d.ts
|
|
1250
|
+
interface ContextRescue {
|
|
1251
|
+
/**
|
|
1252
|
+
* Fold history until the next request fits the given window.
|
|
1253
|
+
*
|
|
1254
|
+
* @param sessionId - session whose history should be folded.
|
|
1255
|
+
* @param window - context window of the route that just overflowed.
|
|
1256
|
+
* @returns whether any history was folded.
|
|
1257
|
+
*/
|
|
1258
|
+
rescue(sessionId: string, window: number): Promise<boolean>;
|
|
1259
|
+
}
|
|
1260
|
+
//#endregion
|
|
1249
1261
|
//#region src/adapter.d.ts
|
|
1250
1262
|
/** Provider route this bundle owns for the domestic (CN) gateway. */
|
|
1251
1263
|
declare const WORKBUDDY_PROVIDER = "workbuddy";
|
|
@@ -1270,6 +1282,12 @@ interface WorkBuddyAdapterOptions {
|
|
|
1270
1282
|
displayName?: string;
|
|
1271
1283
|
/** Resolve the durable attachment service at request time, when present. */
|
|
1272
1284
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
1285
|
+
/**
|
|
1286
|
+
* Resolve the host-side context-overflow rescue, when this host exposes one.
|
|
1287
|
+
* Omitted (or resolving to `undefined`) leaves the adapter exactly as it was:
|
|
1288
|
+
* a provider overflow is forwarded and the host's own recovery decides.
|
|
1289
|
+
*/
|
|
1290
|
+
resolveContextRescue?: () => ContextRescue | undefined;
|
|
1273
1291
|
/**
|
|
1274
1292
|
* Override the pi-ai generation gate (see {@link piAiRuntimeInfo}); tests
|
|
1275
1293
|
* inject both branches because this checkout only ever RESOLVES one of
|
|
@@ -1300,7 +1318,27 @@ declare function workBuddyModelInput(info: WorkBuddyModelInfo): ('text' | 'image
|
|
|
1300
1318
|
* `x0.00`, matching WorkBuddy's rendering of free models.
|
|
1301
1319
|
*/
|
|
1302
1320
|
declare function workBuddyDisplayName(info: WorkBuddyModelInfo): string;
|
|
1303
|
-
/**
|
|
1321
|
+
/**
|
|
1322
|
+
* Map only levels advertised by WorkBuddy; undeclared DSH levels stay unavailable.
|
|
1323
|
+
*
|
|
1324
|
+
* `off` is offered EXACTLY when `canDisableThinking === true` and withheld
|
|
1325
|
+
* (mapped to `null`) otherwise. `null` is pi-ai's "this level is not
|
|
1326
|
+
* supported": `getSupportedThinkingLevels` filters on `mapped === null`, and
|
|
1327
|
+
* `openai-completions` reads `thinkingLevelMap?.off !== null` to decide whether
|
|
1328
|
+
* it may send a thinking-disabled request.
|
|
1329
|
+
*
|
|
1330
|
+
* This condition used to be inverted (`!== true` set the null), which locked
|
|
1331
|
+
* `off` OFF for precisely the models that CAN disable thinking while leaving it
|
|
1332
|
+
* ON for those that cannot. It stayed invisible because the only test probed
|
|
1333
|
+
* the `canDisableThinking: false` half, which the inverted form also passes.
|
|
1334
|
+
*
|
|
1335
|
+
* The value read here is already the EFFECTIVE one: the runtime catalog is
|
|
1336
|
+
* stamped by `withEffectiveOff()` before it reaches the adapter, so this is the
|
|
1337
|
+
* user's `off` selection when they made one (and the upstream declaration,
|
|
1338
|
+
* minus `REFUSES_OFF_MODEL_IDS`, when they never did). Absence is "unknown" and
|
|
1339
|
+
* stays unoffered, matching how the rest of the plugin treats undeclared
|
|
1340
|
+
* capability.
|
|
1341
|
+
*/
|
|
1304
1342
|
declare function workBuddyThinkingLevelMap(info: WorkBuddyModelInfo): WorkBuddyThinkingLevelMap | undefined;
|
|
1305
1343
|
/**
|
|
1306
1344
|
* Assemble the adapter. The provider's `getModels` reads the live catalog,
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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--S6JVSci.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";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { createProvider } from "@earendil-works/pi-ai";
|
|
6
|
+
import { createAssistantMessageEventStream, createProvider, isContextOverflow } from "@earendil-works/pi-ai";
|
|
7
7
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
8
8
|
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
9
9
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
@@ -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,162 @@ 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)
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Layer the host-side context-overflow rescue onto a provider's streams.
|
|
477
|
+
*
|
|
478
|
+
* pi-ai reports a provider failure as an `error` event rather than a throw, so
|
|
479
|
+
* the rescue runs while the failing assistant message is still in hand: history
|
|
480
|
+
* is folded before the event continues on its way, and the host's retry then
|
|
481
|
+
* rebuilds its request against a smaller surface. Nothing is swallowed — the
|
|
482
|
+
* error event is forwarded unchanged, because deciding whether to retry is the
|
|
483
|
+
* host's policy, not this transport's.
|
|
484
|
+
*
|
|
485
|
+
* Both gates are deliberate. Without `sessionId` there is no session to fold,
|
|
486
|
+
* and without a declared `contextWindow` there is no budget to fold against;
|
|
487
|
+
* either absence leaves the stream untouched.
|
|
488
|
+
*/
|
|
489
|
+
function withContextRescue(api, resolve) {
|
|
490
|
+
if (resolve === void 0) return api;
|
|
491
|
+
const wrap = (inner) => (model, context, options) => {
|
|
492
|
+
const source = inner(model, context, options);
|
|
493
|
+
const rescue = resolve();
|
|
494
|
+
const sessionId = options?.sessionId;
|
|
495
|
+
const window = model.contextWindow;
|
|
496
|
+
if (rescue === void 0 || sessionId === void 0 || window === void 0) return source;
|
|
497
|
+
const out = createAssistantMessageEventStream();
|
|
498
|
+
let final;
|
|
499
|
+
(async () => {
|
|
500
|
+
try {
|
|
501
|
+
for await (const event of source) {
|
|
502
|
+
if (event.type === "done") final = event.message;
|
|
503
|
+
if (event.type === "error") final = event.error;
|
|
504
|
+
if (event.type === "error" && isContextOverflow(event.error)) await rescue.rescue(sessionId, window);
|
|
505
|
+
out.push(event);
|
|
506
|
+
}
|
|
507
|
+
out.end(final);
|
|
508
|
+
} catch {
|
|
509
|
+
out.end(final);
|
|
510
|
+
}
|
|
511
|
+
})();
|
|
512
|
+
return out;
|
|
513
|
+
};
|
|
514
|
+
return {
|
|
515
|
+
...api,
|
|
516
|
+
stream: wrap(api.stream),
|
|
517
|
+
streamSimple: wrap(api.streamSimple)
|
|
357
518
|
};
|
|
358
519
|
}
|
|
359
520
|
/**
|
|
@@ -385,7 +546,7 @@ function createWorkBuddyAdapter(options) {
|
|
|
385
546
|
}
|
|
386
547
|
} },
|
|
387
548
|
models: buildModels(),
|
|
388
|
-
api: withLegacyContext(openAICompletionsApi(), generation)
|
|
549
|
+
api: withContextRescue(withLegacyContext(openAICompletionsApi(), generation), options.resolveContextRescue)
|
|
389
550
|
}),
|
|
390
551
|
getModels: () => buildModels()
|
|
391
552
|
};
|
|
@@ -413,6 +574,104 @@ function createWorkBuddyAdapter(options) {
|
|
|
413
574
|
};
|
|
414
575
|
}
|
|
415
576
|
//#endregion
|
|
577
|
+
//#region src/context-rescue.ts
|
|
578
|
+
/** Share of the window one fold chunk may consume, leaving room for the prompt. */
|
|
579
|
+
const CHUNK_FRACTION = .35;
|
|
580
|
+
/** Post-fold measurement target: comfortably below the window the route allows. */
|
|
581
|
+
const TARGET_FRACTION = .6;
|
|
582
|
+
/** A session is never folded more than this many times by one rescue. */
|
|
583
|
+
const MAX_ROUNDS = 8;
|
|
584
|
+
/** How many times one chunk may shrink its tail edge before giving up. */
|
|
585
|
+
const MAX_SHRINK = 24;
|
|
586
|
+
/** A single fold is abandoned after this long. */
|
|
587
|
+
const FOLD_TIMEOUT_MS = 18e4;
|
|
588
|
+
/**
|
|
589
|
+
* Create the rescue bound to a live host.
|
|
590
|
+
*
|
|
591
|
+
* @param resolve - resolves host services at call time.
|
|
592
|
+
* @returns the rescue handle the adapter calls on a provider overflow.
|
|
593
|
+
*/
|
|
594
|
+
function createContextRescue(resolve) {
|
|
595
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
596
|
+
return { async rescue(sessionId, window) {
|
|
597
|
+
if (inflight.has(sessionId)) return false;
|
|
598
|
+
inflight.add(sessionId);
|
|
599
|
+
try {
|
|
600
|
+
return await foldUntilFits(resolve(), sessionId, window);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
resolve().log?.("dsh-connect-workbuddy: context rescue failed", error);
|
|
603
|
+
return false;
|
|
604
|
+
} finally {
|
|
605
|
+
inflight.delete(sessionId);
|
|
606
|
+
}
|
|
607
|
+
} };
|
|
608
|
+
}
|
|
609
|
+
async function foldUntilFits(deps, sessionId, window) {
|
|
610
|
+
const { meter, compaction, agents } = deps;
|
|
611
|
+
if (meter === void 0 || compaction === void 0 || agents === void 0) return false;
|
|
612
|
+
const agent = agents.get(sessionId);
|
|
613
|
+
if (agent === void 0) return false;
|
|
614
|
+
const target = Math.max(1, Math.floor(window * TARGET_FRACTION));
|
|
615
|
+
let foldedAny = false;
|
|
616
|
+
let previous = Number.POSITIVE_INFINITY;
|
|
617
|
+
for (let round = 0; round < MAX_ROUNDS; round += 1) {
|
|
618
|
+
const measurement = meter.measure(agent.session);
|
|
619
|
+
if (measurement.totalTokens <= target) return foldedAny;
|
|
620
|
+
if (measurement.totalTokens >= previous) return foldedAny;
|
|
621
|
+
previous = measurement.totalTokens;
|
|
622
|
+
if (!await foldOnce(deps, agent, measurement, window, target)) return foldedAny;
|
|
623
|
+
foldedAny = true;
|
|
624
|
+
}
|
|
625
|
+
return foldedAny;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Fold the oldest chunk that fits, shrinking its tail edge when the host
|
|
629
|
+
* refuses the range.
|
|
630
|
+
*
|
|
631
|
+
* The host rejects a range whose edges split a tool call from its result, and
|
|
632
|
+
* it rejects an active or missing range before appending anything — so a
|
|
633
|
+
* refused attempt costs no durable state and the edge can simply move inward.
|
|
634
|
+
*
|
|
635
|
+
* @returns whether one chunk was folded.
|
|
636
|
+
*/
|
|
637
|
+
async function foldOnce(deps, agent, measurement, window, target) {
|
|
638
|
+
const nodes = measurement.nodes;
|
|
639
|
+
if (nodes.length < 2) return false;
|
|
640
|
+
const budget = Math.floor(window * CHUNK_FRACTION);
|
|
641
|
+
const excess = measurement.totalTokens - target;
|
|
642
|
+
const chunk = Math.max(1, Math.min(budget, excess));
|
|
643
|
+
const head = nodes[0];
|
|
644
|
+
const compaction = deps.compaction;
|
|
645
|
+
if (head === void 0 || compaction === void 0) return false;
|
|
646
|
+
let end = -1;
|
|
647
|
+
let accumulated = 0;
|
|
648
|
+
for (let index = 0; index < nodes.length - 1; index += 1) {
|
|
649
|
+
const node = nodes[index];
|
|
650
|
+
if (node === void 0) break;
|
|
651
|
+
accumulated += node.tokens;
|
|
652
|
+
end = index;
|
|
653
|
+
if (accumulated >= chunk) break;
|
|
654
|
+
}
|
|
655
|
+
if (end < 0) return false;
|
|
656
|
+
const signal = AbortSignal.timeout(FOLD_TIMEOUT_MS);
|
|
657
|
+
const floor = Math.max(0, end - MAX_SHRINK);
|
|
658
|
+
for (; end >= floor; end -= 1) {
|
|
659
|
+
const tail = nodes[end];
|
|
660
|
+
if (tail === void 0) break;
|
|
661
|
+
try {
|
|
662
|
+
await compaction.compactRegion(head.seq, tail.seq, agent, signal);
|
|
663
|
+
return true;
|
|
664
|
+
} catch (error) {
|
|
665
|
+
if (signal.aborted) {
|
|
666
|
+
deps.log?.("dsh-connect-workbuddy: context rescue timed out", error);
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
deps.log?.("dsh-connect-workbuddy: context rescue found no foldable range");
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
//#endregion
|
|
416
675
|
//#region src/body-report.ts
|
|
417
676
|
/**
|
|
418
677
|
* Body-composition report: WHAT was sent upstream, in counts (issue #59).
|
|
@@ -425,16 +684,32 @@ function createWorkBuddyAdapter(options) {
|
|
|
425
684
|
* - the gateways count identical bytes differently (upstream's business); or
|
|
426
685
|
* - the two requests did not carry the same messages (ours to fix).
|
|
427
686
|
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
687
|
+
* Neither world, it turned out — the answer was a third one, and this report is
|
|
688
|
+
* what produced it. A paired capture in one session (4.6.7) measured
|
|
689
|
+
* `bodyBytes` 3,628,068 vs 3,626,374, a ratio of 0.9995: both requests carried
|
|
690
|
+
* the same bytes. What differed was the BUCKET each byte was counted in — CN
|
|
691
|
+
* text 741,952 vs GL 184,821, CN reasoning 72,270 vs GL 627,994, with a
|
|
692
|
+
* complementary residual of 0.3%. The gateways did not miscount; the same
|
|
693
|
+
* history was labelled differently on the two sides.
|
|
694
|
+
*
|
|
695
|
+
* The labelling is pi-ai's replay, not this plugin's. `openai-completions.js`
|
|
696
|
+
* records the reasoning field name the upstream stream actually used into
|
|
697
|
+
* `thinkingSignature` (`:428`), and on the way back out writes
|
|
698
|
+
* `assistantMsg[<that name>] = <the thinking text>` (`:1006`), so a
|
|
699
|
+
* transcript produced against a gateway that says `reasoning` is replayed as
|
|
700
|
+
* `reasoning` and one that says `reasoning_content` as `reasoning_content`.
|
|
701
|
+
* This plugin sets only `compat.supportsReasoningEffort` (`src/adapter.ts:200`),
|
|
702
|
+
* so the `else` branch is taken and the field goes out raw rather than folded
|
|
703
|
+
* into text.
|
|
704
|
+
*
|
|
705
|
+
* An earlier revision of this comment claimed the opposite — that historical
|
|
706
|
+
* `reasoning` never reaches the wire, because `requiresThinkingAsText` is unset
|
|
707
|
+
* and the `reasoning_details` route needs a signature the parser never writes.
|
|
708
|
+
* Both halves are wrong under pi-ai 0.85.1: the unset flag is precisely what
|
|
709
|
+
* routes thinking out as a raw field, and the parser does write the
|
|
710
|
+
* `reasoning_details` signature (`:188–193`). The correction matters because it
|
|
711
|
+
* turns `reasoning` into a bucket that can be non-zero — and diffing it across
|
|
712
|
+
* regions is the direct measurement of the drift.
|
|
438
713
|
*
|
|
439
714
|
* This module exists to settle exactly that, and it can do it WITHOUT either
|
|
440
715
|
* region having credit: the body is assembled locally, before the upstream ever
|
|
@@ -6493,6 +6768,15 @@ function apply(ctx, config) {
|
|
|
6493
6768
|
Promise.all(REGION_KEYS.map((region) => stacks[region].shim.ready)).then(async () => {
|
|
6494
6769
|
if (stopped) return;
|
|
6495
6770
|
const adapters = {};
|
|
6771
|
+
const hostService = (name) => ctx.get(name);
|
|
6772
|
+
const contextRescue = createContextRescue(() => ({
|
|
6773
|
+
meter: hostService("tokenMeter"),
|
|
6774
|
+
compaction: hostService("compaction"),
|
|
6775
|
+
agents: hostService("agents"),
|
|
6776
|
+
log: (message, error) => {
|
|
6777
|
+
ctx.logger.warn(message, error);
|
|
6778
|
+
}
|
|
6779
|
+
}));
|
|
6496
6780
|
try {
|
|
6497
6781
|
for (const region of REGION_KEYS) adapters[region] = createWorkBuddyAdapter({
|
|
6498
6782
|
shim: stacks[region].shim,
|
|
@@ -6500,7 +6784,8 @@ function apply(ctx, config) {
|
|
|
6500
6784
|
catalog: stacks[region].catalog,
|
|
6501
6785
|
provider: WORKBUDDY_PROVIDERS[region],
|
|
6502
6786
|
displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES[region],
|
|
6503
|
-
resolveAttachments: () => ctx.get("attachments")
|
|
6787
|
+
resolveAttachments: () => ctx.get("attachments"),
|
|
6788
|
+
resolveContextRescue: () => contextRescue
|
|
6504
6789
|
});
|
|
6505
6790
|
invalidateCatalog = () => {
|
|
6506
6791
|
for (const region of REGION_KEYS) adapters[region].invalidate();
|
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.8.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",
|