dsh-connect-workbuddy 2.0.13 → 2.0.15
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 +57 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +134 -24
- package/lib/{host-heartbeat-FxHiaIn-.js → host-heartbeat-bvp_AX4w.js} +1 -1
- package/lib/index.d.ts +31 -6
- package/lib/index.js +53 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
3
|
+
## 2.0.15 (2026-09-24)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- **不能正确取消(或启用)国际版 / 国内版区域开关**(用户实报:「我发现现在不能正确取消国际或者国内版本」)。**写入其实一直是成功的**——用户机器上的 `settings.yaml` 已经写着 `regions.global.enabled: false`——失败的是**开关控件没能反映这个事实**,所以看起来像「点了没用」。
|
|
8
|
+
|
|
9
|
+
- **根因(两处,都在读取侧)**:
|
|
10
|
+
|
|
11
|
+
1. **开关读的是浏览器镜像,而写入落在 Host。** 2.0.14 把设置写入改为 **Host 端点优先**(那是唯一不会删掉兄弟区域的写入方),而**端点写入不会更新浏览器的 settings 镜像**——这一滞后在本仓库别处早已注明。开关的勾选态却是从镜像读的:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
checked={regionEnabledOf(settingsScope?.getSnapshot().value, region)}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
于是 Host 已经提交了 `enabled: false`,复选框**仍显示为开**。这同时解释了为什么此前「读法」与「写入顺序」两轮修复都没有暴露它:**写入一直是对的,错的是回显**。
|
|
18
|
+
|
|
19
|
+
2. **切换后不重新读取 Host。** `toggleRegion()` 写完就结束,从不重新拉取状态,因此新值根本没有机会进入卡片。
|
|
20
|
+
|
|
21
|
+
- **修法**:
|
|
22
|
+
- 开关改为**以 Host 的值为准**——Host 在状态载荷里已经送回它按自身 config 判定的 `enabled`(`deps.regionEnabled`)。镜像降级为**兜底**:老版本 Host 没这个字段、或状态尚未加载时,仍按存储文档渲染,而不是靠猜。两边用的是**同一条 opt-out 规则**(只有显式 `false` 才算关闭),因此两个来源不可能互相矛盾——只可能其中一个**过期**。
|
|
23
|
+
- `toggleRegion()` 在写入后 **`await refreshUsage(item)`**,重新读取 Host;失败时**显式报错**,而不是让开关静静地不动。
|
|
24
|
+
- 面板打开时**两个区域都拉取**,而不是只拉当前标签页。两个开关**始终**渲染在标签栏上,只拉当前区域会让另一个开关一直停在镜像兜底上——正是本次要摆脱的那个过期来源。
|
|
25
|
+
|
|
26
|
+
- **回归测试**(`tests/volatile-scope.spec.ts`,9 → 13 例):断言 Host 已提交关闭而镜像仍说开时开关显示为**关**(反向同理)、Host 未应答时回落到镜像、以及兜底路径仍能容忍 `{get(): T}` 活引用(否则会退回「永远显示开」的老毛病)。**变异验证**:把读取改回「只看镜像」→ **2 例立即变红**。
|
|
27
|
+
|
|
28
|
+
### Tests
|
|
29
|
+
|
|
30
|
+
- 341 → 345(新增 4 例:区域开关以 Host 真值为准、镜像仅兜底)。
|
|
31
|
+
|
|
32
|
+
## 2.0.14 (2026-09-23)
|
|
33
|
+
|
|
34
|
+
### Bug Fixes
|
|
35
|
+
|
|
36
|
+
- **保存国内版 / 国际版信息时,总有一个区域的配置被清掉**(用户实报:「我保存国际版信息和国内版信息,总会丢失一个」)。这是 2.0.13 那条「活引用被展开成函数」缺陷的**客户端侧同源路径**——服务端(`__save`)那次只修了一半,浏览器侧一直在按错误的方式读同一个值。
|
|
37
|
+
|
|
38
|
+
- **根因**:`regions` / `accounts` 被声明为 `asVolatile(...)`,schemastery 会把这类字段解析成 `{ get(): T }` **活引用**——**这一步发生在 schemastery 内部**(由 schema 的 `meta.volatile` 驱动),因此与 DSH 版本无关:0.1.5 与 0.1.7 上,浏览器侧看到的解析值**都是**活引用。
|
|
39
|
+
- 而活引用**仍然是 `typeof === "object"`**。卡片的每一次写入都是「读出当前区域映射 → 展开 → 覆盖自己那一格」,用来保住**另一个**区域。两处读法都栽在这一点上:
|
|
40
|
+
- `src/client/account-selection.ts` 的 `fieldSnapshotOf()`:`{ ...活引用, [region]: slot }` 得到 `{ get: <函数>, [region]: slot }` —— **另一个区域被整个丢掉**,同时还把引用函数写进了文档。
|
|
41
|
+
- `src/status-paths.ts` 的 `regionsMapOf()`:它检查「是对象且不是数组」就把值当成区域映射返回,而活引用**恰好满足**这个判断。于是映射里每一次查表都是 `undefined`——`regionEnabledOf()` 读出的开关状态**永远是「开」**(这正是该函数注释里警告过的那个历史症状),`nextRegionEnabled()` 的合并基座也变成 `{ get: <函数> }`,**开关一次就丢掉另一个区域**。
|
|
42
|
+
- **修法**。三条独立缺陷,缺一条症状就仍在:
|
|
43
|
+
1. **读法**:`fieldSnapshotOf()` 与 `regionsMapOf()` 都先 `unwrapVolatileDeep()` 再使用。该 helper 原本只存在于 Host 入口,浏览器侧取不到,因此**移入 `src/status-paths.ts`**——那个模块本来就是 host↔client 的无 node 依赖桥梁——并由 `src/index.ts` 继续对外导出,**公共 API 不变**。
|
|
44
|
+
2. **写入顺序**:`writeField()` 改为 **Host 端点优先**。两个写入方的语义**并不相同**,而这一点恰恰会删数据:
|
|
45
|
+
- `__save` 在 Host 进程内把提交的区域合并进该字段的**权威值**,保留其余区域;
|
|
46
|
+
- `scope.set(field, next)` 是把**整个字段**交给 settings 服务,而这个字段是客户端**从浏览器的镜像**自己合并出来的——Host 在这条路径上**不做任何服务端合并**,因此客户端给什么就**原样成为**什么。
|
|
47
|
+
|
|
48
|
+
而镜像**不更新**由端点完成的写入(`contextBudgets` 处早已注明这一滞后),在此前受影响的 0.1.7 部署上该命名空间的镜像更是长期陈旧。于是从镜像构造的合并结果是 `{ [本次区域]: slot }`——**兄弟区域全部缺失**——而 `scope.set` 把它原样存下,区域就此消失。这正是「我保存了国内版,国际版的信息就没有了」。也解释了用户机器上的**混合状态**:`accounts` 两个区域都在(写入时镜像恰好是新的),而 `regions` 少了一个。
|
|
49
|
+
3. **镜像回写**:`__save` 现在**返回它实际存下的那个字段**,客户端据此刷新镜像;此前客户端会用**自己那份陈旧镜像重新合并**再 `scope.set` 回去,**反而把 Host 刚保住兄弟区域的合并结果覆盖掉**。老版本 Host 只回 `{ok:true}`,此时该刷新自动跳过。
|
|
50
|
+
- **回归测试**(新增 `tests/volatile-scope.spec.ts`,9 例):整个套件此前只用**普通对象**模拟 `getSnapshot().value`,而线上真实的形状是活引用——**受影响的路径从未被测过**。新用例按真实形状构造 scope,并**按真实写入路径**建模(Host 端点在服务端合并并回传结果、镜像滞后、`scope.set` 原样存储),断言两个区域在每种交替顺序下都存活、且文档里绝不出现引用函数。另有一例断言客户端镜像的是 **Host 答回的权威值**,而不是它自己重新合并的结果。
|
|
51
|
+
- **变异验证**(分别撤销每一处):`fieldSnapshotOf` 解包 → **5 例红**;`regionsMapOf` 解包 → **2 例红**;镜像回写改回「从自身镜像重合并」→ **仍会删掉兄弟区域**。三处修复各自必需。
|
|
52
|
+
|
|
53
|
+
> 更正两次:① 2.0.13 的说明里我写过「对单 region 写入足够」——**那是错的**,每条写入路径都要靠这次读取来保住另一个区域。② 2.0.14 发布前我判断「读法」是唯一根因——**也不完整**,还有上述第 2、3 条写入路径缺陷。二者均在此订正。
|
|
54
|
+
|
|
55
|
+
### Tests
|
|
56
|
+
|
|
57
|
+
- 332 → 341(新增 9 例:活引用形状下的双区域写入、开关回归,以及「Host 端点为权威、镜像不得删掉兄弟区域」的写入路径回归)。
|
|
58
|
+
|
|
59
|
+
## 2.0.13 (2026-09-23)
|
|
4
60
|
|
|
5
61
|
### Bug Fixes
|
|
6
62
|
|
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-bvp_AX4w.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.
|
|
@@ -768,7 +865,7 @@ window.__ModuleLoader__.load({
|
|
|
768
865
|
(0, react.useEffect)(() => {
|
|
769
866
|
if (!open) return;
|
|
770
867
|
const controller = new AbortController();
|
|
771
|
-
refreshUsage(
|
|
868
|
+
for (const region of WORKBUDDY_REGIONS) refreshUsage(region, controller.signal);
|
|
772
869
|
return () => {
|
|
773
870
|
controller.abort();
|
|
774
871
|
};
|
|
@@ -840,19 +937,29 @@ window.__ModuleLoader__.load({
|
|
|
840
937
|
}
|
|
841
938
|
};
|
|
842
939
|
/**
|
|
843
|
-
* Whether one region's provider is switched on
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
940
|
+
* Whether one region's provider is switched on.
|
|
941
|
+
*
|
|
942
|
+
* The HOST's answer wins, because on this deployment it is the only writer
|
|
943
|
+
* that lands: the switch goes through the plugin's Host save endpoint (that
|
|
944
|
+
* endpoint is the only writer that cannot drop the sibling region), and a
|
|
945
|
+
* write made there does NOT update the browser settings mirror. Reading the
|
|
946
|
+
* mirror therefore left the checkbox stuck ON after a successful disable —
|
|
947
|
+
* "不能正确取消国际版/国内版" — even though `enabled: false` was already in
|
|
948
|
+
* settings.yaml. The Host sends the committed value it derives from its own
|
|
949
|
+
* config (`status.enabled`, see `deps.regionEnabled`), so the card renders
|
|
950
|
+
* that.
|
|
848
951
|
*
|
|
849
|
-
* The
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
* `
|
|
853
|
-
*
|
|
952
|
+
* The mirror stays as the FALLBACK: a host that predates the field, or a
|
|
953
|
+
* status that has not loaded yet, still renders from the stored document
|
|
954
|
+
* rather than guessing. Both rules are the same opt-out rule (only an
|
|
955
|
+
* explicit `false` disables), so the two sources cannot disagree — only one
|
|
956
|
+
* of them can be stale.
|
|
854
957
|
*/
|
|
855
|
-
const regionOn = (item) =>
|
|
958
|
+
const regionOn = (item) => {
|
|
959
|
+
const fromHost = statusByRegion[item]?.enabled;
|
|
960
|
+
if (typeof fromHost === "boolean") return fromHost;
|
|
961
|
+
return regionEnabledOf(settingsScope?.getSnapshot().value, item);
|
|
962
|
+
};
|
|
856
963
|
const activeRegionOn = regionOn(activeRegion);
|
|
857
964
|
/**
|
|
858
965
|
* Switch one region's provider off or on. The write carries the region's
|
|
@@ -873,6 +980,9 @@ window.__ModuleLoader__.load({
|
|
|
873
980
|
try {
|
|
874
981
|
const slot = nextRegionEnabled(settingsScope.getSnapshot().value, item, enabled)[item];
|
|
875
982
|
await writeRegionEnabled(settingsScope, item, enabled, typeof slot === "object" && slot !== null ? slot : {});
|
|
983
|
+
await refreshUsage(item);
|
|
984
|
+
} catch (error) {
|
|
985
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
876
986
|
} finally {
|
|
877
987
|
if (mounted.current) setTogglingRegion(void 0);
|
|
878
988
|
}
|
|
@@ -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.15";
|
|
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-bvp_AX4w.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. */
|
|
@@ -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.15",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|