dsh-connect-workbuddy 5.12.1 → 5.12.3
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 +153 -0
- package/lib/bin.js +1 -1
- package/lib/{host-heartbeat-DKKM63s5.js → host-heartbeat-D1N-f8Q6.js} +147 -39
- package/lib/index.js +215 -4
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,158 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.12.3 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
> issue #155 的**两个独立 Bug**:**① 业务码按字节匹配 JSON**——`11128`/`14003`/`14018` 用
|
|
6
|
+
> `body.includes('"code":11128')` 判定,于是同一个信封 `{"code":11128}` 认得、多一个空格的
|
|
7
|
+
> `{"code": 11128}` 和写成字符串的 `{"code":"11128"}` 都不认,渠道闸被静默判成 `client`,
|
|
8
|
+
> **整条救援阶梯不再运行**(#113 的修复等于被一个空格关掉);**② 删账号只清内存、不清持久化选择**——
|
|
9
|
+
> 磁盘上 `accounts.cn` 仍写着死 id,下次 `applySelection` 把它读回来,CN 整块变「已保存的账号在本机
|
|
10
|
+
> 已不存在」且账号列表一条不渲染。
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
- **渠道闸/限流/耗尽三族业务码按字节匹配,一个空格就漏判(#155)**
|
|
15
|
+
- **现象**:报告者贴回来的那条消息里**自相矛盾**——正文写着 `code 11128`,同一句却带
|
|
16
|
+
`"type":"client"`,收尾是「请按上面的提示调整后重试」。两半出自同一个信封、同一份 body。
|
|
17
|
+
- **根因**:分类器判族用 `body.includes('"code":11128')`——对**一种序列化**做字节测试;上游 JSON 里
|
|
18
|
+
多一个空格、或把码写成字符串,判据就落空。而 `code 11128` 那半走 `parseUpstreamErrorDetail`
|
|
19
|
+
(`JSON.parse`),照旧读得到。**以下矩阵是把每行都跑在 HEAD 代码上测出来的**,不是推理:
|
|
20
|
+
|
|
21
|
+
| body 写法 | HEAD classify | HEAD detail.upstreamCode |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `{"code":11128}` | `channel_reject` | 11128 |
|
|
24
|
+
| `{"code": 11128}` | `client` ← 漏 | 11128 |
|
|
25
|
+
| `{"code":"11128"}` | `client` ← 漏 | `undefined` ← 也漏 |
|
|
26
|
+
| `{"error":{"code":11128}}` | `channel_reject` | `undefined` ← **反向**不一致 |
|
|
27
|
+
| `{"CODE":11128}` | `client` ← 漏 | `undefined` |
|
|
28
|
+
- **两种不同的错位**,报告者只能贴出第一种。第 2 行就是 #155 那条消息:分类器按 substring 说
|
|
29
|
+
`client`,展示那半按 `JSON.parse` 读到**顶层数字** `11128` 照旧打印 —— 一条回复里,诊断和它自己的
|
|
30
|
+
证据互相打脸。第 3–5 行对支持更糟、对用户不可见:不但两个读取器不一致,**连要求用户提供的那个码都
|
|
31
|
+
整个消失**,`channel_reject` 贴出来却没有 `11128` 可锚。第 4 行方向相反(substring 恰好命中嵌套的
|
|
32
|
+
`"code":11128`,解析器当时只认 `error.data.code`),说明这不是「换成解析器就完了」—— 两侧都得读同一个
|
|
33
|
+
事实,才可能一致。
|
|
34
|
+
- **代价不只是文案**:`client` 是**唯一不进渠道救援阶梯**的类别(`src/shim.ts` 的门是
|
|
35
|
+
`result.kind === 'channel_reject'`),于是 `forceNeutralHead` 与两级扰动重试一次都不会跑 —— 用户被
|
|
36
|
+
要求「调整请求后重试」,而插件根本没试过救援。`14003` 同族实测降级目标**随 HTTP 状态而变**:
|
|
37
|
+
`429 {"code": 14003}` → `soft_rate`、`400` → `client`,而 `model_busy` 是三个**不换号**的类别之一
|
|
38
|
+
(`isFailoverWorthy`)—— 落到 `soft_rate` 就是让整个账号池挨个烧一次真实请求去学一个模型级的事实。
|
|
39
|
+
`14018` spaced 单独看会掉成 `client`,实测只有当 `msg` 里带上了 `Credits exhausted` 那句话时才被
|
|
40
|
+
措辞标记救回 `hard_credit` —— 措辞一改就失效,正是本仓库反复记录的那类脆弱判据。
|
|
41
|
+
- **修法**:新增 `statesBusinessCode(body, code)`,`CHANNEL_REJECT_CODE`/`MODEL_BUSY_CODE`/
|
|
42
|
+
`CREDITS_EXHAUSTED_CODE` 三个常量从字符串形态改成数字,6 处家族判断统一走它。**三段读取,顺序即安全性**:
|
|
43
|
+
① 信封自己声明的码是最终答案,**绝不再做文本扫描** —— 否则 `extError` 里嵌套的 `"code":11128` 会盖掉外层
|
|
44
|
+
`14003` 的自判,正好是 #127 反过来要的精度;② 解析成功却没在已知三种形态里声明码的 body,仍走带引号锚点
|
|
45
|
+
的文本扫描 —— 这条是**严格扩量**:#127 只留解析器,反而把 `{"error":{"code":11140}}` 这种旧文本匹配本来
|
|
46
|
+
认得的写法**收窄**掉了;③ 扫描锚在 `"code":<n>` 而非裸数字,十六进制 requestId 恰好含 `11128` 仍不能把
|
|
47
|
+
无关 body 提上来(原文本形态存在的理由,一并保留)。
|
|
48
|
+
- **一处先前例**:`CONTEXT_TOO_LONG_CODE`(`11115`,#101)早就写成 `/"code"\s*:\s*11115(?!\d)/u` —— 本仓库
|
|
49
|
+
**第三次**独立撞上同一个坑,前两次各修各的族,这就是为什么这次收成一个共享读取器,而不是第四处正则。
|
|
50
|
+
- **顺带修两处同源缺陷**:`envelopeError()` 已把 `code` 解析成数字,却只把 `msg` 交给分类器 —— 而 `msg`
|
|
51
|
+
永远不含 `"code":…`,于是每个「靠业务码判定」的族在这里都不可见,答案一律落到 `client`(#144 那条
|
|
52
|
+
`workbuddy upstream client (http 400): 请求被拦截,请重新发送` 就是一个插件已经握有其码的信封);改为按
|
|
53
|
+
规范形重建 body 再分类。`parseUpstreamErrorDetail()` 原先只认**顶层**且 `typeof === 'number'` 的 `code`,
|
|
54
|
+
现与分类器共用 `businessCodeOfDocument`(补上 `{"error":{"code":…}}`,排在 `error.data.code` 之后以免
|
|
55
|
+
细节盖掉判定)—— 不再出现「按码判了类、却不打印码」。
|
|
56
|
+
- **删掉一个账号后,该区域的持久化选择复活成死 id(#155 第二份报告)**
|
|
57
|
+
- **现象**:「删了一个账号就这样了」——CN 整块变「未登录」,提示「已保存的账号在本机已不存在」,而上方账号
|
|
58
|
+
列表**一条都不渲染**;`usage?region=cn&detail=fast` 答 `status=signed-out selectionLost=true`,但同一份
|
|
59
|
+
响应里明明还有 3 个 token 未过期的账号,国际版正常。报告者自己定位到 `cordis.patch.yml` 里
|
|
60
|
+
`accounts: cn: <本机已不存在的 id>`,并靠手发 `POST /__save {"field":"accounts","value":{"cn":""}}`
|
|
61
|
+
恢复的 —— 本条就是把那个手动动作变成自动。
|
|
62
|
+
- **根因是「只清了一半」**:`removeAccount()` 确实清了内存里的 `this.accountId`,所以**本次会话内一切正常**、
|
|
63
|
+
删除也回报成功;但设置文档里的死 id 一个字节都没动,下一次 `applySelection`(重启,或**任何一次设置保存**)
|
|
64
|
+
把它读回来重新 pin,区域随即掉进死胡同:`current()` 找不到 → `resolve()` 抛 no signed-in account →
|
|
65
|
+
走 `signed-out` 分支,而该状态下卡片的建议指向一个它自己并不渲染的列表。会话内看不出问题、故障延后到重启才
|
|
66
|
+
现形,正是这两步之间的缝隙让报告者和「删账号」连不起来。**只存在于内存的忘记不是修复,是拖延。**
|
|
67
|
+
- **修法**:`removeOne` 之后调用 `clearRegionAccountSelection()`,**只在被删账号正是本区持久化 pin 时**
|
|
68
|
+
写 `accounts[region] = ''`。判定与写入同在一个函数里,调用方无法只挑一半。四条约束都是这条修复的一部分:
|
|
69
|
+
- **写 `''`,绝不删键**:`''` 是「跟随 App 当前登录」的哨兵(也是全新安装的默认值,所以这是**恢复**而非
|
|
70
|
+
发明状态);删掉的键含义是「从未配置」,会回落到旧版扁平 `accountId`,把用户**悄悄切到另一个账号**。
|
|
71
|
+
- **只清 pin 了该号的区域**:无关的删除必须零次写入 —— 内存里 `current()` 都拒绝回退到别的账号,
|
|
72
|
+
删除不能绕过它、从设置文件里做同样的事(换账号等于换人计费)。
|
|
73
|
+
- **整字段合并**:`accounts` 是两个区域共用的一张平表,按调用方视角重建会抹掉兄弟区域的 pin;判定与写入
|
|
74
|
+
都读 `describe()` 的权威文档,且先 `unwrapVolatileDeep` —— 把 volatile 的 `{get}` 活引用写进 payload,
|
|
75
|
+
settings 服务会整次拒绝(`found a function at $.ops[0].value.get`)。
|
|
76
|
+
- **同时覆盖旧版扁平 `accountId`**:pre-split 配置的 `accounts` 是空的、只靠扁平字段 pin,
|
|
77
|
+
`regionPinsAccount` 认这条;而哨兵会**终止**那次回落,所以一次写入同时 settle 两种写法。
|
|
78
|
+
- **fail-soft**:写设置失败只记 warn,绝不回报「删除失败」—— 凭证文件此刻已经真删了,说它还在是第二个谎。
|
|
79
|
+
|
|
80
|
+
### Tests
|
|
81
|
+
|
|
82
|
+
- `tests/issue155-envelope-spelling.spec.ts` 新增 23 条(#155):紧凑/带空格/字符串码/嵌套四种写法 ×
|
|
83
|
+
11128/14003/14018/11140 四族;`{"CODE":11128}` 归族(这一半是**恢复**不是保留:member 层先前对 body
|
|
84
|
+
做了 `toLowerCase()` 因而认大写、`tests/issue98-channel-gate` 钉的是 member,而 family 层测的是原始 body、
|
|
85
|
+
不认 —— 现在一个读取器同时回答两侧);十六进制 requestId 含 `11128` 不得被提上来;嵌套 `11128` 不得盖掉外层
|
|
86
|
+
`14003` 的自判;**分类器与展示给用户的那条码不许互相矛盾**。
|
|
87
|
+
- `tests/issue155-account-removal.spec.ts` 新增 17 条(#155):`regionPinsAccount` 四个方向(本区 pin 了=清 /
|
|
88
|
+
兄弟区 pin 了别的=不清 / 已在跟随 App=不写 / 只靠扁平字段 pin=清,且扁平字段只归它被归属的那个区);
|
|
89
|
+
`nextAccountsSelection` 保兄弟区、只答 `accounts` 字段而非整个 section、`''` 作为数据保留(区别于缺键);
|
|
90
|
+
volatile 活引用不得漏进 payload;无服务/无命名空间时零写入。
|
|
91
|
+
- `tests/issue155-removal-e2e.spec.ts` 新增 5 条(#155 的**接线**):真实 `apply()` + 真实路由 + 真实 config
|
|
92
|
+
对象(settings 双按引用答 `describe()`、`mutate` 原地提交并广播 `loader/volatile-update`),断言
|
|
93
|
+
「删除 → 触发那次重读(等价于重启或任何一次设置保存的 `applySelection`)→ 区域仍 `signed-in` 且
|
|
94
|
+
`selectionLost` 消失」这条旧代码做不到的完整链条;另测兄弟区 pin 存活、无关删除零次写入、写失败时删除仍回报
|
|
95
|
+
200。单独存在的理由与 `tests/pool-e2e.spec.ts` 相同:纯 helper 用例对着我自己写的 double,摘掉 `index.ts`
|
|
96
|
+
里的接线它们全绿。
|
|
97
|
+
- 变异验证(每条都确认会让对应用例变红):分类器退回 `body.includes('"code":N')` → **11 条红**;删掉
|
|
98
|
+
「本区是否 pin 了该号」的判定 → **4 条红**;写整个 section 而非 `accounts` 字段 → **4 条红**;
|
|
99
|
+
`removeOne` 退回只清内存 → **1 条红**(正是那条延后到重启才现形的链)。
|
|
100
|
+
- 全量:`tsc` 双配置 0 错误,**146 文件 / 2536 通过(2 条按平台跳过)**,`tsdown` 构建通过。
|
|
101
|
+
|
|
102
|
+
## 5.12.2 (2026-10-27)
|
|
103
|
+
|
|
104
|
+
> 两处修复:**① 装完重启插件消失**(#151)——把宿主提供的 `@earendil-works/pi-ai` 标成 optional
|
|
105
|
+
> peer,使安装期不再因「闭包里解析不到它」而拒绝整个 generation;**② DSML 工具名一字之差**
|
|
106
|
+
> (#152)——`pwdsh` → `pwsh` 这类与本次声明工具只差一个字符的名字,现在按那个工具恢复,
|
|
107
|
+
> 不再整段回退成正文。两处都保持 fail-closed 的默认:候选集只来自请求自己的 `body.tools`,
|
|
108
|
+
> 不凭空造名。
|
|
109
|
+
|
|
110
|
+
### Fixes
|
|
111
|
+
|
|
112
|
+
- **安装成功但重启后插件消失:generation peer validation 拒绝了安装(#151)**
|
|
113
|
+
- **现象**:插件市场安装显示 `generation-install: promoted to …`,接着
|
|
114
|
+
`generation peer validation failed: @earendil-works/pi-ai does not resolve from the generation or
|
|
115
|
+
installation closure`,`exit=1`;重启后该 generation 被扫掉(`swept N unreferenced plugin generation(s)`),
|
|
116
|
+
用户得重装。
|
|
117
|
+
- **根因不是"插件依赖没装",而是"它不可能被装上"**:pi-ai 由**宿主**提供 —— `dsh-llm-pi-ai` 把它列为自己的
|
|
118
|
+
`dependencies`(`^1.1.0`),宿主用运行期拦截层应答这个裸包名;而 profile 是按 `autoInstallPeers: false` 建的,
|
|
119
|
+
peer 从不进入 profile 的 `node_modules`。所以一份「必需 peer」在安装闭包里**永远解析不到**,校验必然失败。
|
|
120
|
+
- **修法**:`package.json` 增加 `peerDependenciesMeta`,把 `@earendil-works/pi-ai` 标为 `optional: true`。
|
|
121
|
+
**范围一个字符都没动**(仍是 `>=0.85.0 <0.88.0 || >=1.0.0-0 <2.0.0`)——`optional` 说的是「不要
|
|
122
|
+
求我自己带一份」,不是「我不支持这些版本」;运行期版本探测(`pi-ai-runtime.ts`)与语义边界仍以该范围为准。
|
|
123
|
+
- 为什么不是干脆删掉这条 peer:插件**静态 import** 了 pi-ai(`adapter.ts:19–21`),它不是可选依赖,
|
|
124
|
+
声明必须留着;只有"安装期强制"这一层需要让开。
|
|
125
|
+
- 报告里那句「`@deepseek-ai/cordis` 已有同样处理,照此办理即可」**不成立** —— 本仓库此前
|
|
126
|
+
`peerDependenciesMeta` 是空的,cordis 也从未标 optional。修法本身没问题,但依据是上面那条,不是先例。
|
|
127
|
+
|
|
128
|
+
- **DSML 泄漏时工具名一字之差,整段标记回退成正文(#152)**
|
|
129
|
+
- **现象**:模型把工具调用写进正文时把名字写成 `pwdsh`,而请求声明的是 `pwsh`。第二道闸按名字拒绝
|
|
130
|
+
(这是对的:那个名字从未被提供过),于是整块按原文显示 —— 用户看到一堵乱码墙。
|
|
131
|
+
- **新证据**:幻觉名与真实名之间存在**可预测的一字变形**(`pwdsh` = `pwsh` 中间多插一个 `d`,
|
|
132
|
+
编辑距离 1)。这跟此前记录过的 `validate` 那种「名字整段被换掉」不同,是可以低成本兜底的。
|
|
133
|
+
- **修法**:第二道闸在拒绝之前先试一次受限纠正(`repairNearMissName`,由 shim 开启):
|
|
134
|
+
- 候选集**只来自本次请求的 `body.tools`** —— 永远不会选中一个没被提供的名字,更不会凭空造名;
|
|
135
|
+
- 写出名与候选名的**编辑距离必须 ≤ 1**,且**恰好一个**候选落在这个距离内(两个就放弃);
|
|
136
|
+
- 双方长度都 **≥ 4**(`cat`/`cut`、`bash`/`dash` 这类三字母的一字之差不是证据);
|
|
137
|
+
- 写出名若**已精确命中**声明名,直接用它,绝不"纠正"到邻居;
|
|
138
|
+
- **只纠正 `invoke name=`**;`parameter name=` 一律不动(改参数名等于改调用语义,比显示正文更糟);
|
|
139
|
+
- `tool_choice` 的钉选、第四道闸(必需参数缺失即截断)**都在纠正后的名字上照旧生效**。
|
|
140
|
+
- 默认仍是 fail-closed:`repairNearMissName` 不传就是关闭,只有 shim 的请求路径打开它。
|
|
141
|
+
|
|
142
|
+
### Tests
|
|
143
|
+
|
|
144
|
+
- `tests/dsml-recovery.spec.ts` 新增 10 条(#152):真实捕获形态 `pwdsh` → `pwsh` 被恢复;不开关时仍拒绝;
|
|
145
|
+
两个候选在距离内则放弃;短名(写出名 3 字 / 候选 3 字)两种方向都拒绝;已精确命中的名字不被"纠正";
|
|
146
|
+
不在声明集里的近邻不被凭空选中;钉选与必需参数两道闸在纠正后仍生效;**参数名不被纠正**。
|
|
147
|
+
- `tests/shim-recovery.spec.ts` 新增 1 条(#152 的**接线**):走真实 shim 与真实响应流,断言 `pwdsh`
|
|
148
|
+
在声明 `pwsh` 时被恢复成 `pwsh`、在只声明 `bash` 时**仍回退成正文**。这条单独存在的理由与 #149 相同:
|
|
149
|
+
纯函数用例自己传开关,摘掉 `shim.ts` 里的 `repairNearMissName: true` 时它们全绿 —— 实测该变异只让这条变红。
|
|
150
|
+
- `tests/dsh-line.spec.ts` 新增 1 条(#151):断言 `peerDependenciesMeta['@earendil-works/pi-ai'].optional === true`,
|
|
151
|
+
且 peer **范围**未因此改动。
|
|
152
|
+
- 变异验证(每条都确认过会让对应用例变红):去掉编辑距离判断 → 1 条红;去掉唯一性判断 → 1 条红;
|
|
153
|
+
去掉两个长度下限 → 2 条红;忽略 opt-in(默认开启)→ 1 条红;摘掉 shim 的接线 → 1 条红;移除 `optional` → 1 条红。
|
|
154
|
+
- 全量:`tsc` 双配置 0 错误,**143 文件 / 2491 通过(2 条按平台跳过)**,`tsdown` 构建通过。
|
|
155
|
+
|
|
3
156
|
## 5.12.1 (2026-10-26)
|
|
4
157
|
|
|
5
158
|
> 修正 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-
|
|
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-D1N-f8Q6.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -2430,14 +2430,14 @@ const HARD_CREDIT_MARKERS = [
|
|
|
2430
2430
|
* this guards against. Marker tables rot in a way codes do not: the gateway can
|
|
2431
2431
|
* reword its sentence without changing the contract it advertises, and the
|
|
2432
2432
|
* failure is silent (the branch simply stops matching). A code cannot drift like
|
|
2433
|
-
* that, and
|
|
2434
|
-
* contain the digits from promoting an unrelated body.
|
|
2433
|
+
* that, and reading it (see {@link statesBusinessCode}) keeps a request id that
|
|
2434
|
+
* happens to contain the digits from promoting an unrelated body.
|
|
2435
2435
|
*
|
|
2436
2436
|
* ONLY a corroborating signal, never a substitute: the code is checked in
|
|
2437
2437
|
* addition to the markers, so a body that names the code AND reads as a policy
|
|
2438
2438
|
* refusal still resolves by the marker table's order.
|
|
2439
2439
|
*/
|
|
2440
|
-
const CREDITS_EXHAUSTED_CODE =
|
|
2440
|
+
const CREDITS_EXHAUSTED_CODE = 14018;
|
|
2441
2441
|
/** Session-invalidation markers that mean "sign in again in the WorkBuddy app". */
|
|
2442
2442
|
const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
|
|
2443
2443
|
/**
|
|
@@ -2474,7 +2474,21 @@ function businessCodeOf(body) {
|
|
|
2474
2474
|
return match?.[1] === void 0 ? void 0 : Number(match[1]);
|
|
2475
2475
|
}
|
|
2476
2476
|
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
2477
|
-
|
|
2477
|
+
return businessCodeOfDocument(parsed);
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* The business code a PARSED failure document states, or `undefined`.
|
|
2481
|
+
*
|
|
2482
|
+
* ONE READER FOR TWO CALLERS (issue #155's second half). The classifier needs
|
|
2483
|
+
* the code to decide a kind; `parseUpstreamErrorDetail` needs it to print
|
|
2484
|
+
* `code 11128` in the reply the user pastes into an issue. The display half used
|
|
2485
|
+
* to read only a top-level `code` that was `typeof === 'number'`, so for two of
|
|
2486
|
+
* the spellings below the SAME body produced a diagnosis from the code while
|
|
2487
|
+
* showing no code at all — and `client`/`channel_reject` is exactly the pair a
|
|
2488
|
+
* reporter needs to be able to cross-check. Extracted so the two cannot drift
|
|
2489
|
+
* again; `businessCodeOf` keeps the unparseable-body fallback on top of it.
|
|
2490
|
+
*/
|
|
2491
|
+
function businessCodeOfDocument(raw) {
|
|
2478
2492
|
const direct = asBusinessCode(raw["code"]);
|
|
2479
2493
|
if (direct !== void 0) return direct;
|
|
2480
2494
|
const error = raw["error"];
|
|
@@ -2484,6 +2498,8 @@ function businessCodeOf(body) {
|
|
|
2484
2498
|
const nested = asBusinessCode(data["code"]);
|
|
2485
2499
|
if (nested !== void 0) return nested;
|
|
2486
2500
|
}
|
|
2501
|
+
const direct = asBusinessCode(error["code"]);
|
|
2502
|
+
if (direct !== void 0) return direct;
|
|
2487
2503
|
}
|
|
2488
2504
|
const data = raw["data"];
|
|
2489
2505
|
if (typeof data === "object" && data !== null) return asBusinessCode(data["code"]);
|
|
@@ -2493,21 +2509,97 @@ function asBusinessCode(value) {
|
|
|
2493
2509
|
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
2494
2510
|
if (typeof value === "string" && /^\d{4,6}$/u.test(value.trim())) return Number(value.trim());
|
|
2495
2511
|
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Whether a failure body states this business code, in ANY of its spellings.
|
|
2514
|
+
*
|
|
2515
|
+
* THE BUG THIS REMOVES (issue #155). Three of the four code families below were
|
|
2516
|
+
* recognised by `body.includes('"code":11128')` — a byte test on ONE
|
|
2517
|
+
* serialization. Issue #127 had already proven that test unreliable, but it
|
|
2518
|
+
* converted `11140` ONLY: measured on `6bdba2a`, `businessCodeOf` has exactly
|
|
2519
|
+
* one call site, despite that commit's message claiming `11128` as well. So the
|
|
2520
|
+
* SAME envelope answered one thing to the parser and another to the classifier.
|
|
2521
|
+
* Measured row by row against the shipped code:
|
|
2522
|
+
*
|
|
2523
|
+
* body classify detail.upstreamCode
|
|
2524
|
+
* {"code":11128} channel_reject 11128
|
|
2525
|
+
* {"code": 11128} client ← 漏 11128
|
|
2526
|
+
* {"code":"11128"} client ← 漏 undefined ← 也漏
|
|
2527
|
+
* {"error":{"code":11128}} channel_reject undefined ← 反向不一致
|
|
2528
|
+
* {"CODE":11128} client ← 漏 undefined
|
|
2529
|
+
*
|
|
2530
|
+
* TWO DIFFERENT mismatches. Row 2 is what the reporter pasted into issue #155:
|
|
2531
|
+
* `code 11128` in the text (`parseUpstreamErrorDetail` reads a top-level NUMBER,
|
|
2532
|
+
* which JSON.parse delivers even for a spaced body) beside `"type":"client"`
|
|
2533
|
+
* (decided by the substring) — a diagnosis disagreeing with its own evidence
|
|
2534
|
+
* inside one message. Rows 3–5 are worse for support and invisible to the user:
|
|
2535
|
+
* the readers disagree AND the code support asks for is missing entirely. Row 4
|
|
2536
|
+
* runs the OTHER way (the substring happens to hit the nested `"code":11128`
|
|
2537
|
+
* while the parser only knew `error.data.code`), which is why this could not be
|
|
2538
|
+
* fixed by "just use the parser" — both readers must consult ONE fact.
|
|
2539
|
+
* That is not cosmetic — `client` is the one kind that does NOT
|
|
2540
|
+
* enter the channel rescue ladder (`src/shim.ts`, gated on
|
|
2541
|
+
* `kind === 'channel_reject'`), so `forceNeutralHead` and both perturbation
|
|
2542
|
+
* rungs never fired, and the user was told to "adjust the request" for a
|
|
2543
|
+
* refusal the plugin had not actually tried to rescue. Issue #113's whole fix
|
|
2544
|
+
* silently switches off depending on a space.
|
|
2545
|
+
*
|
|
2546
|
+
* THREE READS, IN THIS ORDER, and the ordering is the safety property:
|
|
2547
|
+
*
|
|
2548
|
+
* 1. A code the envelope STATES is final. `businessCodeOf` found one ⇒ that is
|
|
2549
|
+
* the answer, and the text scan below is never consulted. Without this rule,
|
|
2550
|
+
* OR-ing the two reads would let a nested sub-object's `"code":11128`
|
|
2551
|
+
* (inside `extError`, say) reclassify an envelope whose own verdict is
|
|
2552
|
+
* `14003` — promoting a detail over the verdict, the reverse of the precision
|
|
2553
|
+
* issue #127 bought.
|
|
2554
|
+
* 2. A body that PARSES but states no code in the three shapes `businessCodeOf`
|
|
2555
|
+
* knows still gets the quoted-key text scan. That is where this fix strictly
|
|
2556
|
+
* widens the old behaviour: an undocumented nesting (`{"error":{"code":…}}`)
|
|
2557
|
+
* used to be recognised by the substring, and dropping it here would be a
|
|
2558
|
+
* regression dressed up as a refactor.
|
|
2559
|
+
* 3. The scan anchors on `"code":<n>`, never the bare digits, so a hexadecimal
|
|
2560
|
+
* request id containing `11128` cannot promote an unrelated body — the hazard
|
|
2561
|
+
* the original quoted-key form existed to avoid, kept intact. The scan is
|
|
2562
|
+
* applied case-insensitively. This half RESTORES rather than preserves a
|
|
2563
|
+
* behaviour, and the two halves of the family disagreed about it: the member
|
|
2564
|
+
* predicates lower-cased the body before testing it (so `{"CODE":11128,…}`
|
|
2565
|
+
* matched, pinned by `tests/issue98-channel-gate`), while the family
|
|
2566
|
+
* predicate tested the RAW body and did not. One reader now answers both.
|
|
2567
|
+
*
|
|
2568
|
+
* The text form is derived from the number rather than declared beside it: two
|
|
2569
|
+
* constants for one code are two truths that drift, and a stale text form would
|
|
2570
|
+
* silently reinstate the bug above for exactly the spelling this function exists
|
|
2571
|
+
* to catch.
|
|
2572
|
+
*/
|
|
2573
|
+
function statesBusinessCode(body, code) {
|
|
2574
|
+
const stated = businessCodeOf(body);
|
|
2575
|
+
if (stated !== void 0) return stated === code;
|
|
2576
|
+
return body.toLowerCase().includes(`"code":${code}`);
|
|
2577
|
+
}
|
|
2496
2578
|
/** The `11140` content-policy family, whatever the envelope's JSON spelling is. */
|
|
2497
2579
|
const CONTENT_POLICY_CODE = 11140;
|
|
2498
2580
|
/**
|
|
2499
|
-
* Content-policy refusal markers from the chat gateway. The
|
|
2500
|
-
*
|
|
2501
|
-
*
|
|
2581
|
+
* Content-policy refusal markers from the chat gateway. The code is READ rather
|
|
2582
|
+
* than text-matched so a request id cannot contain `11140` by coincidence and
|
|
2583
|
+
* promote an unrelated body — and, since issue #155, so the ONE read is shared
|
|
2584
|
+
* with every other family here instead of being a second, subtly different rule.
|
|
2502
2585
|
*
|
|
2503
2586
|
* `request illegal` is deliberately absent: the live `11128` channel-gate body
|
|
2504
2587
|
* uses the SAME `msg`, so this marker used to run first and classify that gate
|
|
2505
2588
|
* as `policy_reject`. The consequence was severe — the channel rescue ladder
|
|
2506
2589
|
* never ran, the reply changed 400 to 403, and users were told to switch
|
|
2507
2590
|
* accounts for a request-side refusal (issue #127).
|
|
2591
|
+
*
|
|
2592
|
+
* THE NESTING GAP #127 LEFT BEHIND, and why this now reads through
|
|
2593
|
+
* {@link statesBusinessCode}. Issue #127 replaced the text match with the parser
|
|
2594
|
+
* alone, which is exact for the three shapes that parser knows (`code`,
|
|
2595
|
+
* `error.data.code`, `data.code`) — and therefore silently NARROWED coverage for
|
|
2596
|
+
* a body whose code sits anywhere else, such as `{"error":{"code":11140}}`, which
|
|
2597
|
+
* the old text match did catch. The helper keeps both: a code the envelope states
|
|
2598
|
+
* wins outright (the precision #127 was after), and the text scan runs only when
|
|
2599
|
+
* nothing was stated, where it can only restore a match the parser could not see.
|
|
2508
2600
|
*/
|
|
2509
2601
|
function isContentPolicyBody(body) {
|
|
2510
|
-
return
|
|
2602
|
+
return statesBusinessCode(body, CONTENT_POLICY_CODE);
|
|
2511
2603
|
}
|
|
2512
2604
|
/**
|
|
2513
2605
|
* Whether a failure body is the account-restriction member of the `11140`
|
|
@@ -2607,25 +2699,32 @@ function isUndecodablePolicyBody(body) {
|
|
|
2607
2699
|
/**
|
|
2608
2700
|
* Business code of the MODEL-scoped throttle (`14003`, "too many requests").
|
|
2609
2701
|
*
|
|
2610
|
-
*
|
|
2611
|
-
*
|
|
2612
|
-
*
|
|
2613
|
-
*
|
|
2702
|
+
* A number rather than the quoted-key text form: the text match was exact for
|
|
2703
|
+
* one serialization only, so a spaced or string-quoted envelope lost the
|
|
2704
|
+
* `model_busy` kind and fell through to whatever its HTTP status says — measured
|
|
2705
|
+
* at HEAD, `429 {"code": 14003}` became `soft_rate` and `400` became `client`.
|
|
2706
|
+
* That is not a cosmetic rename: `model_busy` is one of the three NON-failover
|
|
2707
|
+
* kinds (`isFailoverWorthy` in `src/shim.ts`), because the throttle is a fact
|
|
2708
|
+
* about the MODEL and the next credential joins the same one — so `soft_rate`
|
|
2709
|
+
* walks the whole pool burning a real request per account to learn nothing,
|
|
2710
|
+
* while `client` prints neither the kind's advice nor its code (issue #155). See
|
|
2711
|
+
* {@link statesBusinessCode}.
|
|
2614
2712
|
*/
|
|
2615
|
-
const MODEL_BUSY_CODE =
|
|
2713
|
+
const MODEL_BUSY_CODE = 14003;
|
|
2616
2714
|
/**
|
|
2617
2715
|
* Business code of the gateway's CHANNEL gate (`11128`, "Illegal API invocation
|
|
2618
2716
|
* from an unapproved channel").
|
|
2619
2717
|
*
|
|
2620
|
-
*
|
|
2621
|
-
*
|
|
2718
|
+
* A NUMBER, not the quoted-key text form it used to be. Matching the text was
|
|
2719
|
+
* exact only for one serialization, which is the whole of issue #155 — see
|
|
2720
|
+
* {@link statesBusinessCode}.
|
|
2622
2721
|
*
|
|
2623
2722
|
* A DIFFERENT gate from the one {@link conversationHeaders} works around, which
|
|
2624
2723
|
* also answers `11128` (`conversation id missing`). The code is the same
|
|
2625
2724
|
* because the gateway reuses it for every rule in that family; the BODY is what
|
|
2626
2725
|
* tells them apart, so {@link isChannelRejectBody} requires both halves.
|
|
2627
2726
|
*/
|
|
2628
|
-
const CHANNEL_REJECT_CODE =
|
|
2727
|
+
const CHANNEL_REJECT_CODE = 11128;
|
|
2629
2728
|
/**
|
|
2630
2729
|
* Business code for "the request exceeded the model's context window" (issue #101).
|
|
2631
2730
|
*
|
|
@@ -2723,24 +2822,23 @@ const FIRST_MESSAGE_GATE_MARKER = "first message is not system prompt";
|
|
|
2723
2822
|
* SAME second. A 6004, by contrast, carries a `将在 … UTC+8 重置` sentence and
|
|
2724
2823
|
* is per-account.
|
|
2725
2824
|
*
|
|
2726
|
-
* So the criterion is the CODE alone,
|
|
2727
|
-
* request id that happens to contain the digits cannot promote
|
|
2728
|
-
* the probe and the chat path cannot drift into two different
|
|
2729
|
-
* body.
|
|
2825
|
+
* So the criterion is the CODE alone, read by {@link statesBusinessCode} so a
|
|
2826
|
+
* request id that happens to contain the digits cannot promote an unrelated
|
|
2827
|
+
* body. Exported so the probe and the chat path cannot drift into two different
|
|
2828
|
+
* answers about one body.
|
|
2730
2829
|
*/
|
|
2731
2830
|
function isModelBusyBody(body) {
|
|
2732
|
-
return body
|
|
2831
|
+
return statesBusinessCode(body, MODEL_BUSY_CODE);
|
|
2733
2832
|
}
|
|
2734
2833
|
/**
|
|
2735
2834
|
* Whether a failure body belongs to the `11128` gate family AT ALL.
|
|
2736
2835
|
*
|
|
2737
|
-
* The code is
|
|
2738
|
-
*
|
|
2739
|
-
*
|
|
2740
|
-
* promoted into a diagnosis.
|
|
2836
|
+
* The code is READ, not text-matched (issue #155): a spaced or string-quoted
|
|
2837
|
+
* envelope used to fall out of the family here, and out of the rescue ladder
|
|
2838
|
+
* with it. {@link statesBusinessCode} keeps both reads.
|
|
2741
2839
|
*/
|
|
2742
2840
|
function isChannelGateFamilyBody(body) {
|
|
2743
|
-
return body
|
|
2841
|
+
return statesBusinessCode(body, CHANNEL_REJECT_CODE);
|
|
2744
2842
|
}
|
|
2745
2843
|
/**
|
|
2746
2844
|
* Whether a failure body is the gateway's CHANNEL gate — i.e. whether it
|
|
@@ -2791,14 +2889,12 @@ function isChannelRejectBody(body) {
|
|
|
2791
2889
|
* `msg` can. Hence the wording is the criterion and the code is the
|
|
2792
2890
|
* corroboration, never the other way round.
|
|
2793
2891
|
*
|
|
2794
|
-
* BOTH halves are required, and the code is
|
|
2795
|
-
*
|
|
2796
|
-
*
|
|
2797
|
-
* failure body is how a red herring gets promoted into a diagnosis.
|
|
2892
|
+
* BOTH halves are required, and the code is read by {@link statesBusinessCode}
|
|
2893
|
+
* rather than text-matched, so a spaced envelope names its member too (issue
|
|
2894
|
+
* #155). The wording half stays case-insensitive: it is prose.
|
|
2798
2895
|
*/
|
|
2799
2896
|
function isConversationIdGateBody(body) {
|
|
2800
|
-
|
|
2801
|
-
return lower.includes(CHANNEL_REJECT_CODE) && lower.includes(CONVERSATION_ID_GATE_MARKER);
|
|
2897
|
+
return statesBusinessCode(body, CHANNEL_REJECT_CODE) && body.toLowerCase().includes(CONVERSATION_ID_GATE_MARKER);
|
|
2802
2898
|
}
|
|
2803
2899
|
/**
|
|
2804
2900
|
* Whether a failure body is the FIRST-MESSAGE gate (`11128`).
|
|
@@ -2811,11 +2907,11 @@ function isConversationIdGateBody(body) {
|
|
|
2811
2907
|
*
|
|
2812
2908
|
* Excluded from the prompt diagnosis for the same reason as
|
|
2813
2909
|
* {@link isConversationIdGateBody}, and named by the same rule: the CODE plus
|
|
2814
|
-
* the WORDING,
|
|
2910
|
+
* the WORDING, the code read rather than text-matched (issue #155), the wording
|
|
2911
|
+
* case-insensitively.
|
|
2815
2912
|
*/
|
|
2816
2913
|
function isFirstMessageGateBody(body) {
|
|
2817
|
-
|
|
2818
|
-
return lower.includes(CHANNEL_REJECT_CODE) && lower.includes(FIRST_MESSAGE_GATE_MARKER);
|
|
2914
|
+
return statesBusinessCode(body, CHANNEL_REJECT_CODE) && body.toLowerCase().includes(FIRST_MESSAGE_GATE_MARKER);
|
|
2819
2915
|
}
|
|
2820
2916
|
/**
|
|
2821
2917
|
* Which member of the `11128` family a body is, decided by the plugin's OWN
|
|
@@ -2885,7 +2981,7 @@ function classifyUpstreamError(status, body) {
|
|
|
2885
2981
|
if (status === 402) return "hard_credit";
|
|
2886
2982
|
const lower = body.toLowerCase();
|
|
2887
2983
|
for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
|
|
2888
|
-
if (body
|
|
2984
|
+
if (statesBusinessCode(body, CREDITS_EXHAUSTED_CODE)) return "hard_credit";
|
|
2889
2985
|
for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
|
|
2890
2986
|
if (isAccountRestrictedBody(body)) return "account_restricted";
|
|
2891
2987
|
if (isContentPolicyBody(body)) return "policy_reject";
|
|
@@ -2931,7 +3027,8 @@ function parseUpstreamErrorDetail(body) {
|
|
|
2931
3027
|
const displayMsg = localizedText(raw.displayMsg) ?? (typeof raw.msg === "string" ? raw.msg : void 0);
|
|
2932
3028
|
const tips = localizedText(raw.displayTips);
|
|
2933
3029
|
const detail = {};
|
|
2934
|
-
|
|
3030
|
+
const statedCode = businessCodeOfDocument(raw);
|
|
3031
|
+
if (statedCode !== void 0) detail.upstreamCode = statedCode;
|
|
2935
3032
|
if (typeof raw.requestId === "string") detail.requestId = raw.requestId;
|
|
2936
3033
|
if (typeof raw.msg === "string" && raw.msg !== "") detail.rawMsg = raw.msg;
|
|
2937
3034
|
if (displayMsg !== void 0) detail.displayMsg = displayMsg;
|
|
@@ -4321,10 +4418,21 @@ function parseEnterpriseInstant(value) {
|
|
|
4321
4418
|
* A JSON body on a 401/403 is still a credential refusal: the edge answered in
|
|
4322
4419
|
* the business shape, but the token is just as unusable, so it maps to the same
|
|
4323
4420
|
* actionable error instead of a generic "client" failure.
|
|
4421
|
+
*
|
|
4422
|
+
* THE CLASSIFIER IS HANDED THE CODE, NOT JUST THE SENTENCE (issue #155's sibling).
|
|
4423
|
+
* `readEnvelope` has already parsed `code` as a number, and this call used to
|
|
4424
|
+
* rebuild its diagnosis from `msg` alone — a sentence that never contains
|
|
4425
|
+
* `"code":…`. Every family that decides on its business code was therefore
|
|
4426
|
+
* invisible here, and the answer fell through to `client`: the report filed as
|
|
4427
|
+
* `workbuddy upstream client (http 400): 请求被拦截,请重新发送` (#144) is one
|
|
4428
|
+
* envelope whose code the Host was already holding. Re-stating the envelope in
|
|
4429
|
+
* its canonical form is what lets the shared classifier see both halves it is
|
|
4430
|
+
* written to read: the code, and the same `msg` wording it saw before. Strictly
|
|
4431
|
+
* more information than the text-only call, never less.
|
|
4324
4432
|
*/
|
|
4325
4433
|
function envelopeError(status, envelope) {
|
|
4326
4434
|
if (status === 401 || status === 403) return new WorkBuddyCredentialRejectedError(status);
|
|
4327
|
-
const kind = classifyUpstreamError(status, envelope.msg);
|
|
4435
|
+
const kind = classifyUpstreamError(status, `{"code":${envelope.code},"msg":${JSON.stringify(envelope.msg)}}`);
|
|
4328
4436
|
return /* @__PURE__ */ new Error(`workbuddy upstream ${kind} (http ${status}): ${envelope.msg.slice(0, 160)}`);
|
|
4329
4437
|
}
|
|
4330
4438
|
/**
|
|
@@ -7566,7 +7674,7 @@ function describeRosterSeenPayload(raw) {
|
|
|
7566
7674
|
//#endregion
|
|
7567
7675
|
//#region src/version.ts
|
|
7568
7676
|
/** The npm package version this build was produced from. */
|
|
7569
|
-
const WORKBUDDY_CONNECT_VERSION = "5.12.
|
|
7677
|
+
const WORKBUDDY_CONNECT_VERSION = "5.12.3";
|
|
7570
7678
|
//#endregion
|
|
7571
7679
|
//#region src/host-heartbeat.ts
|
|
7572
7680
|
/**
|
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-
|
|
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-D1N-f8Q6.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";
|
|
@@ -715,6 +715,82 @@ function nextRegionEnabled(value, region, enabled) {
|
|
|
715
715
|
});
|
|
716
716
|
}
|
|
717
717
|
/**
|
|
718
|
+
* The per-region account SELECTIONS map (`Config.accounts`), as plain data.
|
|
719
|
+
*
|
|
720
|
+
* `value` may be the whole settings SECTION or the `accounts` FIELD: `describe()`
|
|
721
|
+
* hands back the section with volatile fields as live references, and a blind
|
|
722
|
+
* spread of that leaks the `get()` function into a write payload — the settings
|
|
723
|
+
* service then refuses the whole save with `found a function at $.ops[0].value.get`
|
|
724
|
+
* (measured at the card's save endpoint, `src/web-status.ts`). Unwrapping first is
|
|
725
|
+
* what makes the merge carry data and not behaviour.
|
|
726
|
+
*
|
|
727
|
+
* The map is recognised by its KEYS, not by its shape: only region ids can appear
|
|
728
|
+
* in it, while a section always carries at least one non-region key (`authFile`,
|
|
729
|
+
* `regions`, `cliBridge`). Same direction as {@link regionsMapOf} chose for the
|
|
730
|
+
* reason documented there — a nested copy is a DIFFERENT, older value, so the
|
|
731
|
+
* outer object's own keys decide what it is.
|
|
732
|
+
*/
|
|
733
|
+
function accountsMapOf(value) {
|
|
734
|
+
const unwrapped = unwrapVolatileDeep(value);
|
|
735
|
+
if (!isRecord$1(unwrapped)) return {};
|
|
736
|
+
if (hasRegionKey(unwrapped)) return unwrapped;
|
|
737
|
+
const nested = unwrapVolatileDeep(unwrapped["accounts"]);
|
|
738
|
+
return isRecord$1(nested) ? nested : {};
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Build the next `accounts` FIELD for one region's selection.
|
|
742
|
+
*
|
|
743
|
+
* ONLY the target region's key changes; the sibling region is carried over
|
|
744
|
+
* verbatim. `accounts` is a flat two-key map that BOTH regions write, so a
|
|
745
|
+
* whole-field write that rebuilt it from one caller's view would drop the other
|
|
746
|
+
* region's pin — the failure mode every other `next*` helper in this file exists
|
|
747
|
+
* to prevent, in a field that is easier to clobber because it looks trivial.
|
|
748
|
+
*
|
|
749
|
+
* The value is written as given, including `''`. That distinction is load-bearing
|
|
750
|
+
* and not a detail: `''` is the Clear sentinel ("this region follows the app's
|
|
751
|
+
* current sign-in"), while an ABSENT key means "never configured" and still falls
|
|
752
|
+
* back to the legacy flat `accountId` — see `selectAccountFor`. A helper that
|
|
753
|
+
* deleted the key to "clear" a region would silently re-bind it to whichever
|
|
754
|
+
* account the legacy field names.
|
|
755
|
+
*/
|
|
756
|
+
function nextAccountsSelection(value, region, accountId) {
|
|
757
|
+
return {
|
|
758
|
+
...accountsMapOf(value),
|
|
759
|
+
[region]: accountId
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Whether one region's selection is PERSISTENTLY bound to this account id.
|
|
764
|
+
*
|
|
765
|
+
* THE QUESTION {@link nextAccountsSelection} MUST ANSWER BEFORE WRITING. Two
|
|
766
|
+
* persisted sources can pin an account, and clearing only the obvious one leaves
|
|
767
|
+
* the other in place:
|
|
768
|
+
*
|
|
769
|
+
* 1. `accounts[region]` — the per-region slot, the current spelling.
|
|
770
|
+
* 2. the deprecated flat `accountId`, which `selectAccountFor` falls back to for
|
|
771
|
+
* the ONE region the startup scan attributed it to (`legacyAccountRegion`) and
|
|
772
|
+
* only when that region's slot is ABSENT. A pre-split profile therefore pins
|
|
773
|
+
* its account through field 2 while field 1 holds nothing at all — and that
|
|
774
|
+
* is precisely a profile whose user can remove an account and inherit an
|
|
775
|
+
* orphaned pin.
|
|
776
|
+
*
|
|
777
|
+
* `accounts[region] === ''` answers `false` even when the id matches nothing: the
|
|
778
|
+
* sentinel means "this region follows the app's current sign-in", which already
|
|
779
|
+
* cannot point at a dead account, so there is nothing to clear and a write would
|
|
780
|
+
* be noise (and a needless settings round trip).
|
|
781
|
+
*
|
|
782
|
+
* @param value the resolved settings section, or the `accounts` field.
|
|
783
|
+
* @param region the region whose pin is being questioned.
|
|
784
|
+
* @param accountId the account about to be removed.
|
|
785
|
+
* @param legacyAccountId the deprecated flat `accountId`, read as persisted.
|
|
786
|
+
* @param legacyRegion which region the startup scan attributed that flat id to.
|
|
787
|
+
*/
|
|
788
|
+
function regionPinsAccount(value, region, accountId, legacyAccountId, legacyRegion) {
|
|
789
|
+
const configured = accountsMapOf(value)[region];
|
|
790
|
+
if (configured !== void 0) return configured === accountId;
|
|
791
|
+
return legacyAccountId !== void 0 && legacyAccountId === accountId && legacyRegion === region;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
718
794
|
* Project one card row into its persisted `lastCatalog` shape: the native
|
|
719
795
|
* context window becomes the stored `contextWindow`, and the card-only
|
|
720
796
|
* presentation fields (`nativeContextWindow`, `multimodal`) are removed BY
|
|
@@ -897,6 +973,64 @@ async function healRegionsField(service, ns) {
|
|
|
897
973
|
}], void 0);
|
|
898
974
|
return true;
|
|
899
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Drop one region's PERSISTED account selection, and only if it pinned this
|
|
978
|
+
* account.
|
|
979
|
+
*
|
|
980
|
+
* THE HOST-SIDE HALF OF ISSUE #155'S SECOND REPORT. A user removed one account
|
|
981
|
+
* and the whole CN tab turned into「已保存的账号在本机已不存在」, with no way back:
|
|
982
|
+
* the card's advice points at a list it does not render in that state, so the
|
|
983
|
+
* reporter escaped by hand-posting `{"field":"accounts","value":{"cn":""}}` to
|
|
984
|
+
* the save endpoint. This is that write, made automatic.
|
|
985
|
+
*
|
|
986
|
+
* WHY A WRITE AT ALL — the store already forgets. `removeAccount` clears the
|
|
987
|
+
* in-memory selection, which is why the region keeps working for the rest of the
|
|
988
|
+
* session. But the settings document still names the dead id, so the next
|
|
989
|
+
* `applySelection` — a restart, or any settings save — re-reads it, re-pins it,
|
|
990
|
+
* and the region falls back into the same dead end. An in-memory forget that is
|
|
991
|
+
* never persisted is not a fix; it is a delay.
|
|
992
|
+
*
|
|
993
|
+
* THE DECISION LIVES HERE, WITH THE WRITE, because the two must never be
|
|
994
|
+
* separated by a caller's guess. Clearing a region that did NOT pin this account
|
|
995
|
+
* would move the user off the account they chose, and「换账号等于换人计费」is the
|
|
996
|
+
* rule every writer in this plugin obeys: this function writes for exactly the
|
|
997
|
+
* region whose persisted pin names the account being removed, and no others.
|
|
998
|
+
*
|
|
999
|
+
* WRITES `''`, NEVER A DELETED KEY. The sentinel means "follow the app's current
|
|
1000
|
+
* sign-in" — the plugin's documented default, which is what a fresh install does,
|
|
1001
|
+
* so this restores a state rather than inventing one. An absent key means "never
|
|
1002
|
+
* configured" and falls through to the legacy flat `accountId` (`selectAccountFor`),
|
|
1003
|
+
* which would resurrect whichever account that field names: a silent switch
|
|
1004
|
+
* through another door. The sentinel also TERMINATES that lookup, which is why one
|
|
1005
|
+
* write settles both spellings — a pre-split profile pinned through the flat
|
|
1006
|
+
* field is cleared by this same write.
|
|
1007
|
+
*
|
|
1008
|
+
* FAIL-SOFT BY CONTRACT: a service that cannot answer, or a region that pinned
|
|
1009
|
+
* nothing, costs no write. The caller logs a failed mutation rather than failing
|
|
1010
|
+
* the removal, because the credential file is already gone by then and reporting
|
|
1011
|
+
* "could not remove" would be the lie.
|
|
1012
|
+
*
|
|
1013
|
+
* @param service The Host's settings service, or `undefined` to no-op.
|
|
1014
|
+
* @param ns The namespace this plugin's settings live under.
|
|
1015
|
+
* @param region The region being checked; the sibling is preserved by the merge.
|
|
1016
|
+
* @param accountId The account about to be removed.
|
|
1017
|
+
* @param legacyAccountId The deprecated flat `accountId`, as persisted.
|
|
1018
|
+
* @param legacyRegion Which region the startup scan attributed that field to.
|
|
1019
|
+
* @returns whether a write happened.
|
|
1020
|
+
*/
|
|
1021
|
+
async function clearRegionAccountSelection(service, ns, region, accountId, legacyAccountId, legacyRegion) {
|
|
1022
|
+
if (service === void 0) return false;
|
|
1023
|
+
const row = service.describe().find((entry) => entry.ns === ns);
|
|
1024
|
+
if (row === void 0) return false;
|
|
1025
|
+
const section = row.value ?? {};
|
|
1026
|
+
if (!regionPinsAccount(section, region, accountId, legacyAccountId, legacyRegion)) return false;
|
|
1027
|
+
await service.mutate(ns, [{
|
|
1028
|
+
op: "set",
|
|
1029
|
+
path: ["accounts"],
|
|
1030
|
+
value: nextAccountsSelection(section, region, "")
|
|
1031
|
+
}], void 0);
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
900
1034
|
//#endregion
|
|
901
1035
|
//#region src/credential-recovery.ts
|
|
902
1036
|
/** How long a verdict stays fresh by default. */
|
|
@@ -2793,6 +2927,56 @@ const MARKER_ONLY_TAG_NAMES = /* @__PURE__ */ new Set(["tool-calls", "toolcalls"
|
|
|
2793
2927
|
/** Fence markers at the start of a line. */
|
|
2794
2928
|
const FENCE_MARKERS = ["```", "~~~"];
|
|
2795
2929
|
/**
|
|
2930
|
+
* Shortest tool name a near-miss repair may act on (issue #152).
|
|
2931
|
+
*
|
|
2932
|
+
* Below this, "one edit apart" stops being evidence: `dash` and `bash` are one
|
|
2933
|
+
* edit apart, and so are any two four-letter names sharing a prefix. At 4 the
|
|
2934
|
+
* reported `pwsh` → `pwdsh` still qualifies (both are 4+), which is the shape
|
|
2935
|
+
* this exists for.
|
|
2936
|
+
*/
|
|
2937
|
+
const NEAR_NAME_MIN_LENGTH = 4;
|
|
2938
|
+
/**
|
|
2939
|
+
* Whether `a` and `b` are at most one edit apart (insertion, deletion, or
|
|
2940
|
+
* substitution).
|
|
2941
|
+
*
|
|
2942
|
+
* Only ever asked for a ceiling of 1, where the general Levenshtein matrix is
|
|
2943
|
+
* unnecessary: the lengths must differ by at most one, and after skipping the
|
|
2944
|
+
* first position where they differ, the remaining tails must still agree.
|
|
2945
|
+
*
|
|
2946
|
+
* The equal-length case is a SUBSTITUTION, not an insertion, so it is checked
|
|
2947
|
+
* separately — skipping the differing character on one side only would leave
|
|
2948
|
+
* tails of different length and silently miss every one-character typo of the
|
|
2949
|
+
* same length (`pwdsh`/`pwdss`, `bash`/`dash`), which is the whole point here.
|
|
2950
|
+
*/
|
|
2951
|
+
function withinOneEdit(a, b) {
|
|
2952
|
+
if (a === b) return true;
|
|
2953
|
+
const [short, long] = a.length <= b.length ? [a, b] : [b, a];
|
|
2954
|
+
if (long.length - short.length > 1) return false;
|
|
2955
|
+
let i = 0;
|
|
2956
|
+
while (i < short.length && short[i] === long[i]) i++;
|
|
2957
|
+
if (short.length === long.length) return short.slice(i + 1) === long.slice(i + 1);
|
|
2958
|
+
return long.slice(i + 1) === short.slice(i);
|
|
2959
|
+
}
|
|
2960
|
+
/**
|
|
2961
|
+
* The one declared tool within a single edit of `written`, or `undefined`.
|
|
2962
|
+
*
|
|
2963
|
+
* Refuses when the name is already declared (nothing to repair), when either
|
|
2964
|
+
* side is shorter than {@link NEAR_NAME_MIN_LENGTH}, and when more than one
|
|
2965
|
+
* declared tool is in reach — ambiguity is exactly what gate 2 exists to stop.
|
|
2966
|
+
*/
|
|
2967
|
+
function nearMissNameFor(written, declaredNames) {
|
|
2968
|
+
if (declaredNames.has(written)) return void 0;
|
|
2969
|
+
if (written.length < NEAR_NAME_MIN_LENGTH) return void 0;
|
|
2970
|
+
let fit;
|
|
2971
|
+
for (const candidate of declaredNames) {
|
|
2972
|
+
if (candidate.length < NEAR_NAME_MIN_LENGTH) continue;
|
|
2973
|
+
if (!withinOneEdit(written, candidate)) continue;
|
|
2974
|
+
if (fit !== void 0) return void 0;
|
|
2975
|
+
fit = candidate;
|
|
2976
|
+
}
|
|
2977
|
+
return fit;
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2796
2980
|
* Find every span that must not be scanned for markup.
|
|
2797
2981
|
*
|
|
2798
2982
|
* One pass, not a predicate call per character: the reference asks
|
|
@@ -3208,8 +3392,11 @@ function parseInvoke(open, close, block, gate) {
|
|
|
3208
3392
|
const parameters = parseParameters(block.slice(open.end + 1, close.start));
|
|
3209
3393
|
let name;
|
|
3210
3394
|
if (declared !== void 0 && declared !== "") {
|
|
3211
|
-
if (!gate.declaredNames.has(declared))
|
|
3212
|
-
|
|
3395
|
+
if (!gate.declaredNames.has(declared)) {
|
|
3396
|
+
const repaired = gate.repairNearMissName === true ? nearMissNameFor(declared, gate.declaredNames) : void 0;
|
|
3397
|
+
if (repaired === void 0) return void 0;
|
|
3398
|
+
name = repaired;
|
|
3399
|
+
} else name = declared;
|
|
3213
3400
|
} else if (open.attributes.trim() === "") {
|
|
3214
3401
|
name = inferNameFromSchema(parameters, gate);
|
|
3215
3402
|
if (name === void 0) return void 0;
|
|
@@ -4995,6 +5182,7 @@ function createWorkBuddyShim(options) {
|
|
|
4995
5182
|
requiredParameters: declared.requiredParameters,
|
|
4996
5183
|
acceptedParameters: declared.acceptedParameters,
|
|
4997
5184
|
inferFromSchema: true,
|
|
5185
|
+
repairNearMissName: true,
|
|
4998
5186
|
...declared.pinnedToolName === void 0 ? {} : { pinnedToolName: declared.pinnedToolName }
|
|
4999
5187
|
};
|
|
5000
5188
|
const notice = switchedFrom === void 0 || switchedBecause === void 0 ? void 0 : failoverNoticeText(switchedFrom, switchedBecause);
|
|
@@ -11963,8 +12151,31 @@ function apply(ctx, config) {
|
|
|
11963
12151
|
* Forget ONE account's plugin-stored credential (the vault entry; the
|
|
11964
12152
|
* desktop app's files are never touched). The card rescans afterwards,
|
|
11965
12153
|
* so the row disappears the moment the write lands.
|
|
12154
|
+
*
|
|
12155
|
+
* AND DROP THE REGION'S PERSISTED PIN, when this was the pinned account
|
|
12156
|
+
* (issue #155's second report). `removeAccount` forgets the selection in
|
|
12157
|
+
* memory only, so the next `applySelection` — a restart, or any settings
|
|
12158
|
+
* save — re-reads the dead id out of `accounts.<region>`, re-pins it, and
|
|
12159
|
+
* the tab returns to「已保存的账号在本机已不存在」with the card's advice
|
|
12160
|
+
* pointing at a list that state does not render. The clear is decided from
|
|
12161
|
+
* the settings document rather than from the store, and is scoped to the
|
|
12162
|
+
* one region that pinned this account: an unrelated removal must not move
|
|
12163
|
+
* anybody off the account they chose.
|
|
12164
|
+
*
|
|
12165
|
+
* FAIL-SOFT, like the field repair above: the credential is already gone by
|
|
12166
|
+
* the time this runs, so a settings write that cannot land must not be
|
|
12167
|
+
* reported as a failed removal — that would tell the user the account is
|
|
12168
|
+
* still there when it is not.
|
|
11966
12169
|
*/
|
|
11967
|
-
removeOne: async (region, accountId) =>
|
|
12170
|
+
removeOne: async (region, accountId) => {
|
|
12171
|
+
const removed = await stacks[region].store.removeAccount(accountId);
|
|
12172
|
+
try {
|
|
12173
|
+
await clearRegionAccountSelection(hostSettings(), settingsNs, region, accountId, unwrapVolatile(current().accountId), legacyAccountRegion);
|
|
12174
|
+
} catch (error) {
|
|
12175
|
+
ctx.logger.warn("dsh-connect-workbuddy: the account was removed, but the region could not be un-pinned from settings; the selection may reappear as orphaned after a restart", error);
|
|
12176
|
+
}
|
|
12177
|
+
return removed;
|
|
12178
|
+
}
|
|
11968
12179
|
},
|
|
11969
12180
|
/**
|
|
11970
12181
|
* OAuth QR sign-in for this card. The session lives in `src/oauth.ts`
|
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.
|
|
5
|
+
"version": "5.12.3",
|
|
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",
|