dsh-connect-workbuddy 2.0.12 → 2.0.14
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 +43 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +109 -12
- package/lib/{host-heartbeat-ChtMu37E.js → host-heartbeat-SQNmYRNj.js} +1 -1
- package/lib/index.d.ts +31 -6
- package/lib/index.js +54 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.14 (2026-09-23)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- **保存国内版 / 国际版信息时,总有一个区域的配置被清掉**(用户实报:「我保存国际版信息和国内版信息,总会丢失一个」)。这是 2.0.13 那条「活引用被展开成函数」缺陷的**客户端侧同源路径**——服务端(`__save`)那次只修了一半,浏览器侧一直在按错误的方式读同一个值。
|
|
8
|
+
|
|
9
|
+
- **根因**:`regions` / `accounts` 被声明为 `asVolatile(...)`,schemastery 会把这类字段解析成 `{ get(): T }` **活引用**——**这一步发生在 schemastery 内部**(由 schema 的 `meta.volatile` 驱动),因此与 DSH 版本无关:0.1.5 与 0.1.7 上,浏览器侧看到的解析值**都是**活引用。
|
|
10
|
+
- 而活引用**仍然是 `typeof === "object"`**。卡片的每一次写入都是「读出当前区域映射 → 展开 → 覆盖自己那一格」,用来保住**另一个**区域。两处读法都栽在这一点上:
|
|
11
|
+
- `src/client/account-selection.ts` 的 `fieldSnapshotOf()`:`{ ...活引用, [region]: slot }` 得到 `{ get: <函数>, [region]: slot }` —— **另一个区域被整个丢掉**,同时还把引用函数写进了文档。
|
|
12
|
+
- `src/status-paths.ts` 的 `regionsMapOf()`:它检查「是对象且不是数组」就把值当成区域映射返回,而活引用**恰好满足**这个判断。于是映射里每一次查表都是 `undefined`——`regionEnabledOf()` 读出的开关状态**永远是「开」**(这正是该函数注释里警告过的那个历史症状),`nextRegionEnabled()` 的合并基座也变成 `{ get: <函数> }`,**开关一次就丢掉另一个区域**。
|
|
13
|
+
- **修法**。三条独立缺陷,缺一条症状就仍在:
|
|
14
|
+
1. **读法**:`fieldSnapshotOf()` 与 `regionsMapOf()` 都先 `unwrapVolatileDeep()` 再使用。该 helper 原本只存在于 Host 入口,浏览器侧取不到,因此**移入 `src/status-paths.ts`**——那个模块本来就是 host↔client 的无 node 依赖桥梁——并由 `src/index.ts` 继续对外导出,**公共 API 不变**。
|
|
15
|
+
2. **写入顺序**:`writeField()` 改为 **Host 端点优先**。两个写入方的语义**并不相同**,而这一点恰恰会删数据:
|
|
16
|
+
- `__save` 在 Host 进程内把提交的区域合并进该字段的**权威值**,保留其余区域;
|
|
17
|
+
- `scope.set(field, next)` 是把**整个字段**交给 settings 服务,而这个字段是客户端**从浏览器的镜像**自己合并出来的——Host 在这条路径上**不做任何服务端合并**,因此客户端给什么就**原样成为**什么。
|
|
18
|
+
|
|
19
|
+
而镜像**不更新**由端点完成的写入(`contextBudgets` 处早已注明这一滞后),在此前受影响的 0.1.7 部署上该命名空间的镜像更是长期陈旧。于是从镜像构造的合并结果是 `{ [本次区域]: slot }`——**兄弟区域全部缺失**——而 `scope.set` 把它原样存下,区域就此消失。这正是「我保存了国内版,国际版的信息就没有了」。也解释了用户机器上的**混合状态**:`accounts` 两个区域都在(写入时镜像恰好是新的),而 `regions` 少了一个。
|
|
20
|
+
3. **镜像回写**:`__save` 现在**返回它实际存下的那个字段**,客户端据此刷新镜像;此前客户端会用**自己那份陈旧镜像重新合并**再 `scope.set` 回去,**反而把 Host 刚保住兄弟区域的合并结果覆盖掉**。老版本 Host 只回 `{ok:true}`,此时该刷新自动跳过。
|
|
21
|
+
- **回归测试**(新增 `tests/volatile-scope.spec.ts`,9 例):整个套件此前只用**普通对象**模拟 `getSnapshot().value`,而线上真实的形状是活引用——**受影响的路径从未被测过**。新用例按真实形状构造 scope,并**按真实写入路径**建模(Host 端点在服务端合并并回传结果、镜像滞后、`scope.set` 原样存储),断言两个区域在每种交替顺序下都存活、且文档里绝不出现引用函数。另有一例断言客户端镜像的是 **Host 答回的权威值**,而不是它自己重新合并的结果。
|
|
22
|
+
- **变异验证**(分别撤销每一处):`fieldSnapshotOf` 解包 → **5 例红**;`regionsMapOf` 解包 → **2 例红**;镜像回写改回「从自身镜像重合并」→ **仍会删掉兄弟区域**。三处修复各自必需。
|
|
23
|
+
|
|
24
|
+
> 更正两次:① 2.0.13 的说明里我写过「对单 region 写入足够」——**那是错的**,每条写入路径都要靠这次读取来保住另一个区域。② 2.0.14 发布前我判断「读法」是唯一根因——**也不完整**,还有上述第 2、3 条写入路径缺陷。二者均在此订正。
|
|
25
|
+
|
|
26
|
+
### Tests
|
|
27
|
+
|
|
28
|
+
- 332 → 341(新增 9 例:活引用形状下的双区域写入、开关回归,以及「Host 端点为权威、镜像不得删掉兄弟区域」的写入路径回归)。
|
|
29
|
+
|
|
30
|
+
## 2.0.13 (2026-09-23)
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
- **上下文预算(或任何 regions/accounts 字段)保存失败:`settings mutate … must contain only JSON-compatible data (found a function at $.ops[0].value.get)`**(用户实报)。这是把「易失标记一生效」那条缺陷(2.0.12 修的)的**另一条受害者路径**补上了。
|
|
35
|
+
|
|
36
|
+
- **根因**:`__save` 端点(卡片保存的 Host 兜底路径)在合并时直接读取 `row.value[field]`,而 `row.value` 是 settings 服务解析后的 config——某 volatile 字段(`regions` / `accounts`)在那里是一个 `{ get(): T }` **活引用**。`{ ...这种对象 }` 展开后变成 `{ get: <函数> }`,于是 `settings.mutate` 的严格 JSON 兼容校验拒绝它,报 `found a function at $.ops[0].value.get`。保存**完全失败**(不是静默失败,而是直接报错),因为真实保存流程里 `scope.set()` 被判为未落盘后回退到了这个端点。
|
|
37
|
+
- **修法**:`__save` 在合并前先 `unwrapVolatileDeep(row.value?.[field])` 把活引用剥净,让 `mutate` 收到的 payload 只含 JSON 兼容数据。`unwrapVolatileDeep` 是 2.0.12 为 `installSection` 引入的同一 helper,这里复用。
|
|
38
|
+
- **回归测试**(`tests/web-status.spec.ts`):构造一个 resolved config 含 `{ get() }` 活引用的 settings 服务,经真实 `__save` 路由请求,断言 `mutate` 收到的是一个能通过 JSON 往返的 payload(无函数),且上下文预算字段完好。**变异验证**:把 `unwrapVolatileDeep` 那行去掉,该用例立即变红。
|
|
39
|
+
|
|
40
|
+
> 说明:`__save` 会先把 `row.value[field]` 解包、再用「浅展开 + incoming 覆盖」合并到该字段。对单 region 写入(本次场景)足够;若日后要一次写多个 region 槽位,应改为深合并——届时再扩。
|
|
41
|
+
|
|
42
|
+
### Tests
|
|
43
|
+
|
|
44
|
+
- 331 → 332(新增 1 例:live-reference 配置下的 `__save` 保存回归)。
|
|
45
|
+
|
|
3
46
|
## 2.0.12 (2026-09-23)
|
|
4
47
|
|
|
5
48
|
### Features
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { O as workbuddyOwnAuthPath, T as legacyWorkbuddyOwnAuthPath, W as findWorkbuddyAppExecutable, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, g as WORKBUDDY_AUTH_FILE_ENV, j as WorkBuddyUpstreamClient, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, x as defaultDesktopAuthDirs, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-
|
|
2
|
+
import { O as workbuddyOwnAuthPath, T as legacyWorkbuddyOwnAuthPath, W as findWorkbuddyAppExecutable, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, g as WORKBUDDY_AUTH_FILE_ENV, j as WorkBuddyUpstreamClient, l as FALLBACK_WORKBUDDY_MODELS, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, x as defaultDesktopAuthDirs, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-SQNmYRNj.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
package/lib/client.js
CHANGED
|
@@ -47,10 +47,18 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
* project — the lookup then read `section['cn']` (absent), so the card's
|
|
48
48
|
* checkbox reported `true` forever and clicking it appeared to do nothing even
|
|
49
49
|
* though the write succeeded.
|
|
50
|
+
*
|
|
51
|
+
* The resolved section delivers `regions` as a `{get(): T}` LIVE reference, and
|
|
52
|
+
* a live reference is `typeof === 'object'` and not an array — so it passes a
|
|
53
|
+
* naive object check and gets returned as if it were the map. Every lookup on
|
|
54
|
+
* it is then `undefined`: the enabled flag reads back as "on" forever (the very
|
|
55
|
+
* symptom described above), and a merge that spreads this map silently drops
|
|
56
|
+
* the region it was not editing. Unwrap before narrowing.
|
|
50
57
|
*/
|
|
51
58
|
function regionsMapOf(value) {
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
const unwrapped = unwrapVolatileDeep(value);
|
|
60
|
+
if (typeof unwrapped !== "object" || unwrapped === null || Array.isArray(unwrapped)) return {};
|
|
61
|
+
const record = unwrapped;
|
|
54
62
|
const nested = record["regions"];
|
|
55
63
|
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) return nested;
|
|
56
64
|
return record;
|
|
@@ -118,9 +126,60 @@ window.__ModuleLoader__.load({
|
|
|
118
126
|
contextWindow: nativeContextWindow
|
|
119
127
|
};
|
|
120
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Deep copy of a settings value with every `{get(): T}` live reference replaced
|
|
131
|
+
* by the value it resolves to.
|
|
132
|
+
*
|
|
133
|
+
* `regions` and `accounts` are declared `asVolatile(...)`, and schemastery
|
|
134
|
+
* resolves a volatile field to a live reference. That resolution happens in
|
|
135
|
+
* schemastery itself, driven by the schema's `meta.volatile`, so it is
|
|
136
|
+
* independent of the DSH line — the resolved section looks like this to the
|
|
137
|
+
* browser half on BOTH 0.1.5 and 0.1.7.
|
|
138
|
+
*
|
|
139
|
+
* A live reference is still `typeof === 'object'`, so `{ ...reference }` does
|
|
140
|
+
* NOT read the field: it produces `{ get: <function> }`. Any caller that
|
|
141
|
+
* spreads the resolved field to preserve its siblings — the card's
|
|
142
|
+
* "write one region, keep the other" merge — would otherwise DROP every
|
|
143
|
+
* sibling and leak a function into the document. Both halves therefore unwrap
|
|
144
|
+
* before touching a resolved field.
|
|
145
|
+
*
|
|
146
|
+
* Lives here, not in the Host entry, because the browser half needs it too and
|
|
147
|
+
* this module is the node-free bridge between the two.
|
|
148
|
+
*
|
|
149
|
+
* Non-reference values are recursed into so a nested volatile field is caught
|
|
150
|
+
* too — arrays and objects are rebuilt rather than mutated, so the caller's
|
|
151
|
+
* value is never touched.
|
|
152
|
+
*/
|
|
153
|
+
function unwrapVolatileDeep(value) {
|
|
154
|
+
if (value === null || typeof value !== "object") return value;
|
|
155
|
+
if (typeof value.get === "function") return unwrapVolatileDeep(value.get());
|
|
156
|
+
if (Array.isArray(value)) return value.map((entry) => unwrapVolatileDeep(entry));
|
|
157
|
+
const source = value;
|
|
158
|
+
const out = {};
|
|
159
|
+
for (const key of Object.keys(source)) out[key] = unwrapVolatileDeep(source[key]);
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
121
162
|
//#endregion
|
|
122
163
|
//#region src/client/account-selection.ts
|
|
123
164
|
/**
|
|
165
|
+
* Verified writes of the per-region account slot.
|
|
166
|
+
*
|
|
167
|
+
* 参考:DSH 自带插件卡片(`@deepseek-ai/dsh-client-ui-settings-plugins`
|
|
168
|
+
* 的 `CardForm.store()`)— 「写入后回读用户层并与写入值比对」这一校验形态,
|
|
169
|
+
* 是本模块 `writeAccountSlot()` 的出发点。
|
|
170
|
+
* 改动:针对账号槽位重写而非照搬 —— 回读的是**解析后的 section**而非 user 层
|
|
171
|
+
* (Host 的 legacy 归属也读它,两者必须同源),并用精确比较区分 `''`(哨兵清除)
|
|
172
|
+
* 与 `undefined`(键缺失),因为后者会让区域重新回退到旧版 `accountId`。
|
|
173
|
+
*
|
|
174
|
+
* Kept out of `WorkBuddyCard.tsx` — and free of any browser-only import — so
|
|
175
|
+
* the write contract can be unit-tested directly. The card is a `.tsx`
|
|
176
|
+
* component whose module graph pulls DSH's browser packages, which cannot load
|
|
177
|
+
* in the Node test environment; the check below is exactly the part that must
|
|
178
|
+
* not go untested.
|
|
179
|
+
*
|
|
180
|
+
* @module dsh-connect-workbuddy/client/account-selection
|
|
181
|
+
*/
|
|
182
|
+
/**
|
|
124
183
|
* A settings write that did not take effect.
|
|
125
184
|
*
|
|
126
185
|
* Distinct from a rejected `set()`: this is thrown when the write reported
|
|
@@ -135,9 +194,22 @@ window.__ModuleLoader__.load({
|
|
|
135
194
|
this.field = field;
|
|
136
195
|
}
|
|
137
196
|
};
|
|
138
|
-
/**
|
|
197
|
+
/**
|
|
198
|
+
* Read one settings field's current object value from the scope snapshot.
|
|
199
|
+
*
|
|
200
|
+
* The snapshot holds the RESOLVED section, where a volatile field
|
|
201
|
+
* (`regions` / `accounts`) is a `{get(): T}` LIVE reference rather than the
|
|
202
|
+
* plain object it resembles. Unwrapping first is not cosmetic: a live reference
|
|
203
|
+
* is still `typeof === 'object'`, so without this the caller spreads it into
|
|
204
|
+
* `{ get: <function> }` — which DROPS every sibling key (the other region) and
|
|
205
|
+
* leaks a reference function into the document.
|
|
206
|
+
*
|
|
207
|
+
* That was the "saving the international region loses the domestic one" report:
|
|
208
|
+
* every write here preserves the region it is not touching by spreading this
|
|
209
|
+
* value, so reading it wrong silently deleted the untouched region.
|
|
210
|
+
*/
|
|
139
211
|
function fieldSnapshotOf(scope, field) {
|
|
140
|
-
const value = scope.getSnapshot().value?.[field];
|
|
212
|
+
const value = unwrapVolatileDeep(scope.getSnapshot().value?.[field]);
|
|
141
213
|
return typeof value === "object" && value !== null ? value : {};
|
|
142
214
|
}
|
|
143
215
|
/**
|
|
@@ -167,17 +239,32 @@ window.__ModuleLoader__.load({
|
|
|
167
239
|
const reason = `${String(detail.errorName ?? "")} ${String(detail.error ?? "")}`.trim();
|
|
168
240
|
throw new WorkBuddySettingsWriteError(field, `Host save refused: ${reason === "" ? String(detail.error) : reason}`);
|
|
169
241
|
}
|
|
242
|
+
return (await response.json().catch(() => void 0))?.value;
|
|
170
243
|
}
|
|
171
244
|
/**
|
|
172
245
|
* Write one volatile field, then confirm the value actually landed.
|
|
173
246
|
*
|
|
174
|
-
*
|
|
175
|
-
* the
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
247
|
+
* ORDER MATTERS, and getting it wrong DELETES DATA. The two writers do not have
|
|
248
|
+
* the same semantics:
|
|
249
|
+
*
|
|
250
|
+
* - The Host save endpoint merges the posted region into the field's
|
|
251
|
+
* AUTHORITATIVE value inside the Host process, preserving every other region.
|
|
252
|
+
* - `scope.set(field, next)` hands the settings service the WHOLE field, merged
|
|
253
|
+
* on the CLIENT out of the browser mirror. The Host performs no server-side
|
|
254
|
+
* merge on this path, so whatever the client built REPLACES the field.
|
|
255
|
+
*
|
|
256
|
+
* The mirror is not a reliable merge base: a write made through the Host
|
|
257
|
+
* endpoint does not update it (that lag is documented on `contextBudgets`
|
|
258
|
+
* below), and on the affected 0.1.7 deployment the mirror for this namespace
|
|
259
|
+
* stays stale outright. Building the merge from it therefore produced
|
|
260
|
+
* `{ [region]: slot }` with every SIBLING region missing — and `scope.set`
|
|
261
|
+
* stores that verbatim, deleting them. That was the report "I saved the
|
|
262
|
+
* domestic region and the international one was gone": `accounts` survived
|
|
263
|
+
* (written while the mirror happened to be fresh) while `regions` lost a slot.
|
|
264
|
+
*
|
|
265
|
+
* So the Host endpoint goes FIRST — it is the only writer that cannot delete a
|
|
266
|
+
* sibling. `scope.set` is then attempted purely as a mirror refresh, and only
|
|
267
|
+
* when it delivers is the write considered done without the endpoint.
|
|
181
268
|
*
|
|
182
269
|
* `landed` decides whether a value read back from the scope is the value that
|
|
183
270
|
* was written; it is per-field because `''` (a cleared account slot) and an
|
|
@@ -187,6 +274,16 @@ window.__ModuleLoader__.load({
|
|
|
187
274
|
* @throws {WorkBuddySettingsWriteError} when neither path persists the value.
|
|
188
275
|
*/
|
|
189
276
|
async function writeField(scope, field, region, value, landed) {
|
|
277
|
+
let hostError;
|
|
278
|
+
try {
|
|
279
|
+
const authoritative = await saveViaHostEndpoint(field, region, value);
|
|
280
|
+
if (authoritative !== void 0) try {
|
|
281
|
+
await scope.set(field, authoritative);
|
|
282
|
+
} catch {}
|
|
283
|
+
return;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
hostError = error;
|
|
286
|
+
}
|
|
190
287
|
let scopeDelivered = false;
|
|
191
288
|
try {
|
|
192
289
|
scopeDelivered = await scope.set(field, {
|
|
@@ -197,7 +294,7 @@ window.__ModuleLoader__.load({
|
|
|
197
294
|
scopeDelivered = false;
|
|
198
295
|
}
|
|
199
296
|
if (scopeDelivered && landed(fieldSnapshotOf(scope, field)[region])) return;
|
|
200
|
-
|
|
297
|
+
throw hostError instanceof Error ? hostError : new WorkBuddySettingsWriteError(field, "neither the Host save endpoint nor the settings scope persisted the value");
|
|
201
298
|
}
|
|
202
299
|
/**
|
|
203
300
|
* Write one region's account slot, then confirm the value actually landed.
|
|
@@ -2208,7 +2208,7 @@ var WorkBuddyCatalog = class {
|
|
|
2208
2208
|
//#endregion
|
|
2209
2209
|
//#region src/version.ts
|
|
2210
2210
|
/** The npm package version this build was produced from. */
|
|
2211
|
-
const WORKBUDDY_CONNECT_VERSION = "2.0.
|
|
2211
|
+
const WORKBUDDY_CONNECT_VERSION = "2.0.14";
|
|
2212
2212
|
//#endregion
|
|
2213
2213
|
//#region src/host-heartbeat.ts
|
|
2214
2214
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -1247,7 +1247,6 @@ interface WorkBuddyWebAccount {
|
|
|
1247
1247
|
tokenExpiresAtMs: number;
|
|
1248
1248
|
selected: boolean;
|
|
1249
1249
|
}
|
|
1250
|
-
type WorkBuddyWebPackage = WorkBuddyWebCreditPackage;
|
|
1251
1250
|
/**
|
|
1252
1251
|
* One probed candidate path and why it yielded no account.
|
|
1253
1252
|
*
|
|
@@ -1366,6 +1365,31 @@ type WorkBuddyWebUsage =
|
|
|
1366
1365
|
status: 'error';
|
|
1367
1366
|
message: string;
|
|
1368
1367
|
};
|
|
1368
|
+
/**
|
|
1369
|
+
* Deep copy of a settings value with every `{get(): T}` live reference replaced
|
|
1370
|
+
* by the value it resolves to.
|
|
1371
|
+
*
|
|
1372
|
+
* `regions` and `accounts` are declared `asVolatile(...)`, and schemastery
|
|
1373
|
+
* resolves a volatile field to a live reference. That resolution happens in
|
|
1374
|
+
* schemastery itself, driven by the schema's `meta.volatile`, so it is
|
|
1375
|
+
* independent of the DSH line — the resolved section looks like this to the
|
|
1376
|
+
* browser half on BOTH 0.1.5 and 0.1.7.
|
|
1377
|
+
*
|
|
1378
|
+
* A live reference is still `typeof === 'object'`, so `{ ...reference }` does
|
|
1379
|
+
* NOT read the field: it produces `{ get: <function> }`. Any caller that
|
|
1380
|
+
* spreads the resolved field to preserve its siblings — the card's
|
|
1381
|
+
* "write one region, keep the other" merge — would otherwise DROP every
|
|
1382
|
+
* sibling and leak a function into the document. Both halves therefore unwrap
|
|
1383
|
+
* before touching a resolved field.
|
|
1384
|
+
*
|
|
1385
|
+
* Lives here, not in the Host entry, because the browser half needs it too and
|
|
1386
|
+
* this module is the node-free bridge between the two.
|
|
1387
|
+
*
|
|
1388
|
+
* Non-reference values are recursed into so a nested volatile field is caught
|
|
1389
|
+
* too — arrays and objects are rebuilt rather than mutated, so the caller's
|
|
1390
|
+
* value is never touched.
|
|
1391
|
+
*/
|
|
1392
|
+
declare function unwrapVolatileDeep<T>(value: T): T;
|
|
1369
1393
|
//#endregion
|
|
1370
1394
|
//#region src/web-status.d.ts
|
|
1371
1395
|
/** Constructor dependencies. */
|
|
@@ -1524,11 +1548,12 @@ declare function asVolatile<T>(schema: z<T>): z<T>;
|
|
|
1524
1548
|
* live references, and every field it validated threw. The namespace never
|
|
1525
1549
|
* registered, so the card's settings silently disappeared.
|
|
1526
1550
|
*
|
|
1527
|
-
*
|
|
1528
|
-
*
|
|
1529
|
-
*
|
|
1551
|
+
* The implementation now lives in the node-free host<->client bridge, because
|
|
1552
|
+
* the browser half needs the SAME unwrap before it spreads a resolved field —
|
|
1553
|
+
* spreading a live reference is `{get: <function>}`, which drops every sibling
|
|
1554
|
+
* region. It is imported and re-exported from here so the Host entry keeps
|
|
1555
|
+
* exposing it as part of its public API.
|
|
1530
1556
|
*/
|
|
1531
|
-
declare function unwrapVolatileDeep<T>(value: T): T;
|
|
1532
1557
|
declare const Config: z<Config>;
|
|
1533
1558
|
/**
|
|
1534
1559
|
* One region's saved model state. A config written before the region split has
|
|
@@ -1599,4 +1624,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
|
|
|
1599
1624
|
*/
|
|
1600
1625
|
declare function apply(ctx: Context, config: Config): void;
|
|
1601
1626
|
//#endregion
|
|
1602
|
-
export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions,
|
|
1627
|
+
export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nextRegionEnabled, nextRegionSlots, openEncryptedField, parseCreditMultiplier, parseReasoning, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-
|
|
1
|
+
import { A as WorkBuddyCredentialRejectedError, B as clearAtRestKeyCache, C as hasEncryptedCredentialFields, D as workbuddyAccountId, E as parseWorkBuddyAuth, F as parseReasoning, G as isEncryptedFieldWrapper, H as deriveAtRestKeyId, I as parseUpstreamModel, J as macosNestedAppBundles, K as isWorkbuddyBundle, L as prepareChatBody, M as classifyUpstreamError, N as isCredentialRejectedError, O as workbuddyOwnAuthPath, P as parseCreditMultiplier, R as regionOf, S as defaultDesktopAuthPath, T as legacyWorkbuddyOwnAuthPath, U as fetchAtRestKeyPayload, V as deriveAtRestKey, W as findWorkbuddyAppExecutable, X as readAtRestKey, Y as openEncryptedField, Z as workbuddyAppExecutableCandidates, _ as WorkBuddyCredentialStore, a as readHostHeartbeat, b as defaultDesktopAuthCandidates, c as WORKBUDDY_CONNECT_VERSION, d as WorkBuddyCatalog, f as deriveCatalog, g as WORKBUDDY_AUTH_FILE_ENV, h as WORKBUDDY_AUTH_FILENAME, i as processStartTimeMs, j as WorkBuddyUpstreamClient, k as CREDENTIAL_REJECTED_CODE, l as FALLBACK_WORKBUDDY_MODELS, m as ENCRYPTED_CREDENTIAL_CODE, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as fallbackModelsFor, q as macosBundleExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS_GLOBAL, v as WorkBuddyEncryptedCredentialError, w as isEncryptedCredentialError, x as defaultDesktopAuthDirs, y as authFileName, z as WORKBUDDY_APP_EXECUTABLE_ENV } from "./host-heartbeat-SQNmYRNj.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
@@ -544,10 +544,18 @@ function regionOfStatusUrl(url) {
|
|
|
544
544
|
* project — the lookup then read `section['cn']` (absent), so the card's
|
|
545
545
|
* checkbox reported `true` forever and clicking it appeared to do nothing even
|
|
546
546
|
* though the write succeeded.
|
|
547
|
+
*
|
|
548
|
+
* The resolved section delivers `regions` as a `{get(): T}` LIVE reference, and
|
|
549
|
+
* a live reference is `typeof === 'object'` and not an array — so it passes a
|
|
550
|
+
* naive object check and gets returned as if it were the map. Every lookup on
|
|
551
|
+
* it is then `undefined`: the enabled flag reads back as "on" forever (the very
|
|
552
|
+
* symptom described above), and a merge that spreads this map silently drops
|
|
553
|
+
* the region it was not editing. Unwrap before narrowing.
|
|
547
554
|
*/
|
|
548
555
|
function regionsMapOf(value) {
|
|
549
|
-
|
|
550
|
-
|
|
556
|
+
const unwrapped = unwrapVolatileDeep(value);
|
|
557
|
+
if (typeof unwrapped !== "object" || unwrapped === null || Array.isArray(unwrapped)) return {};
|
|
558
|
+
const record = unwrapped;
|
|
551
559
|
const nested = record["regions"];
|
|
552
560
|
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) return nested;
|
|
553
561
|
return record;
|
|
@@ -615,6 +623,39 @@ function toPersistedWorkBuddyModel(model) {
|
|
|
615
623
|
contextWindow: nativeContextWindow
|
|
616
624
|
};
|
|
617
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* Deep copy of a settings value with every `{get(): T}` live reference replaced
|
|
628
|
+
* by the value it resolves to.
|
|
629
|
+
*
|
|
630
|
+
* `regions` and `accounts` are declared `asVolatile(...)`, and schemastery
|
|
631
|
+
* resolves a volatile field to a live reference. That resolution happens in
|
|
632
|
+
* schemastery itself, driven by the schema's `meta.volatile`, so it is
|
|
633
|
+
* independent of the DSH line — the resolved section looks like this to the
|
|
634
|
+
* browser half on BOTH 0.1.5 and 0.1.7.
|
|
635
|
+
*
|
|
636
|
+
* A live reference is still `typeof === 'object'`, so `{ ...reference }` does
|
|
637
|
+
* NOT read the field: it produces `{ get: <function> }`. Any caller that
|
|
638
|
+
* spreads the resolved field to preserve its siblings — the card's
|
|
639
|
+
* "write one region, keep the other" merge — would otherwise DROP every
|
|
640
|
+
* sibling and leak a function into the document. Both halves therefore unwrap
|
|
641
|
+
* before touching a resolved field.
|
|
642
|
+
*
|
|
643
|
+
* Lives here, not in the Host entry, because the browser half needs it too and
|
|
644
|
+
* this module is the node-free bridge between the two.
|
|
645
|
+
*
|
|
646
|
+
* Non-reference values are recursed into so a nested volatile field is caught
|
|
647
|
+
* too — arrays and objects are rebuilt rather than mutated, so the caller's
|
|
648
|
+
* value is never touched.
|
|
649
|
+
*/
|
|
650
|
+
function unwrapVolatileDeep(value) {
|
|
651
|
+
if (value === null || typeof value !== "object") return value;
|
|
652
|
+
if (typeof value.get === "function") return unwrapVolatileDeep(value.get());
|
|
653
|
+
if (Array.isArray(value)) return value.map((entry) => unwrapVolatileDeep(entry));
|
|
654
|
+
const source = value;
|
|
655
|
+
const out = {};
|
|
656
|
+
for (const key of Object.keys(source)) out[key] = unwrapVolatileDeep(source[key]);
|
|
657
|
+
return out;
|
|
658
|
+
}
|
|
618
659
|
//#endregion
|
|
619
660
|
//#region src/web-status.ts
|
|
620
661
|
/** Redact token-like content before it crosses to the browser. */
|
|
@@ -916,7 +957,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
916
957
|
if (field !== "regions" && field !== "accounts") return json(res, 400, { error: "field must be regions or accounts" });
|
|
917
958
|
const row = settings.describe().find((r) => String(r.ns).includes("workbuddy"));
|
|
918
959
|
if (row === void 0) return json(res, 503, { error: "workbuddy namespace missing from describe()" });
|
|
919
|
-
const current = row.value?.[field] ?? {};
|
|
960
|
+
const current = unwrapVolatileDeep(row.value?.[field] ?? {});
|
|
920
961
|
const incoming = body.value ?? {};
|
|
921
962
|
const merged = {
|
|
922
963
|
...current,
|
|
@@ -927,7 +968,10 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
927
968
|
path: [field],
|
|
928
969
|
value: merged
|
|
929
970
|
}], void 0);
|
|
930
|
-
return json(res, 200, {
|
|
971
|
+
return json(res, 200, {
|
|
972
|
+
ok: true,
|
|
973
|
+
value: merged
|
|
974
|
+
});
|
|
931
975
|
} catch (error) {
|
|
932
976
|
const err = error;
|
|
933
977
|
return json(res, 500, {
|
|
@@ -1011,19 +1055,12 @@ function unwrapVolatile(value) {
|
|
|
1011
1055
|
* live references, and every field it validated threw. The namespace never
|
|
1012
1056
|
* registered, so the card's settings silently disappeared.
|
|
1013
1057
|
*
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1058
|
+
* The implementation now lives in the node-free host<->client bridge, because
|
|
1059
|
+
* the browser half needs the SAME unwrap before it spreads a resolved field —
|
|
1060
|
+
* spreading a live reference is `{get: <function>}`, which drops every sibling
|
|
1061
|
+
* region. It is imported and re-exported from here so the Host entry keeps
|
|
1062
|
+
* exposing it as part of its public API.
|
|
1017
1063
|
*/
|
|
1018
|
-
function unwrapVolatileDeep(value) {
|
|
1019
|
-
if (value === null || typeof value !== "object") return value;
|
|
1020
|
-
if (typeof value.get === "function") return unwrapVolatileDeep(value.get());
|
|
1021
|
-
if (Array.isArray(value)) return value.map((entry) => unwrapVolatileDeep(entry));
|
|
1022
|
-
const source = value;
|
|
1023
|
-
const out = {};
|
|
1024
|
-
for (const key of Object.keys(source)) out[key] = unwrapVolatileDeep(source[key]);
|
|
1025
|
-
return out;
|
|
1026
|
-
}
|
|
1027
1064
|
const Config = z.object({
|
|
1028
1065
|
authFile: asVolatile(z.string().description("WorkBuddy desktop auth file (defaults to the app's own location)")),
|
|
1029
1066
|
accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "Connect locally signed-in WorkBuddy models to DeepSeek Harness with a read-only credits overview and model management.",
|
|
5
|
-
"version": "2.0.
|
|
5
|
+
"version": "2.0.14",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|