dsh-workbuddy-xdpool 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -0
- package/lib/bin.js +49 -20
- package/lib/client.js +597 -416
- package/lib/index.d.ts +9 -1
- package/lib/index.js +138 -26
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,146 @@
|
|
|
4
4
|
|
|
5
5
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
6
6
|
|
|
7
|
+
## 1.7.0 (2026-09-25)
|
|
8
|
+
|
|
9
|
+
一句话:**这一版终于能在 DSH 0.1.7 上活下来了,顺带把设置入口搬进了左栏、把页面重新排了一遍。**
|
|
10
|
+
|
|
11
|
+
以前一个构建只能伺候 0.1.5 那条线,0.1.7 上是真的会出事 —— 不是"卡片不显示"这种小事,
|
|
12
|
+
是整个 DSH 直接退出。现在两条线一个构建通吃。
|
|
13
|
+
|
|
14
|
+
### 一、0.1.7 上一启用插件,DSH 就原地去世
|
|
15
|
+
|
|
16
|
+
先说症状,因为它实在太有迷惑性了:用户装好插件、点启用、**进程直接没了**。
|
|
17
|
+
|
|
18
|
+
桌面端只留下一句 `DSH Host exited (1)`。日志?日志里最后一行还是个无关紧要的
|
|
19
|
+
warning,然后就**戛然而止**,一个 error 都没有。
|
|
20
|
+
|
|
21
|
+
这种"日志被齐刷刷截断"的样子,其实是宿主的一个设计在作祟:DSH 装了 `installFailLoud`,
|
|
22
|
+
进程里**任何**没被捕获的异常、任何没接住的 Promise rejection,都会让它当场 `exit(1)`。
|
|
23
|
+
而那句真正的死因只写进 stderr —— 打包版 Windows 应用压根没有控制台,于是它就
|
|
24
|
+
悄无声息地消失了。查了半天日志找不到错,别怀疑自己,它真的没写进去。
|
|
25
|
+
|
|
26
|
+
那么是谁抛的?**三个坑叠在一起**,而且全在插件这边,内核无辜:
|
|
27
|
+
|
|
28
|
+
**坑 1:宿主那边的 API 换了个名字,老代码还在敲旧门。**
|
|
29
|
+
0.1.7 把 `settings.installSection(...)` 整个删了,换成了 `configure({auto}, owner)`。
|
|
30
|
+
我们的代码虽然做了"先看看有没有"的判断,但问题是 —— 判断失败时它只是 **warn 一声就算了**,
|
|
31
|
+
设置页从此永远不出现;而万一服务真在场,它就**直接裸调**,在 0.1.7 上"啪"地从
|
|
32
|
+
`apply()` 里抛出去。宿主一看:未捕获异常,走你。
|
|
33
|
+
|
|
34
|
+
**坑 2:客户端 `inject` 里写了两个"有你没我"的服务名。**
|
|
35
|
+
0.1.5 提供 `settingsScope`,0.1.7 换成了 `configForms`。而 cordis 的依赖闸门是**硬**的:
|
|
36
|
+
`inject` 里只要有一个服务不存在,`apply` 就**永远不会被执行**,fiber 就永远挂在那里
|
|
37
|
+
PENDING。桌面端报 `renderer boot failed (plugins: dsh-workbuddy-xdpool)`,
|
|
38
|
+
然后拒绝打开。它不报错,它就是不开。
|
|
39
|
+
|
|
40
|
+
**坑 3:卡片注册的那个槽位,0.1.7 里根本不存在。**
|
|
41
|
+
`settings.plugin.item` 是 0.1.5 的专属座位。0.1.7 的设置左栏只认 `settings.section`
|
|
42
|
+
(内置的通用 / 模型 / 插件页都坐那儿)。我们一直往一个不存在的座位上放东西,
|
|
43
|
+
于是卡片安安静静地不显示 —— 不报错、不警告,就是不出现。
|
|
44
|
+
|
|
45
|
+
**怎么修的:** 一律改成"先探路、再走",不猜。
|
|
46
|
+
|
|
47
|
+
- 宿主侧:有 `installSection` 就用它(0.1.5),没有就用 `configure({auto:true})`(0.1.7),
|
|
48
|
+
两个都没有才 warn。写入时的命名空间也得跟着线路换 —— 0.1.5 用自己起的名字,
|
|
49
|
+
0.1.7 得用**宿主给这个插件分配的条目 id**(`ctx.fiber.entry.options.id`)。
|
|
50
|
+
这里猜错的代价是每次保存都报 `No configurable plugin entry`。
|
|
51
|
+
- 客户端:`inject` 只留两条线**都**有的 `slots` / `locale`,设置服务改用 `ctx.get()`
|
|
52
|
+
软探测(服务不在就返回 undefined,不抛)。
|
|
53
|
+
- 顺手声明 `engines.dsh: ">=0.1.5-rc.1 <0.2.0"`,让插件市场把两条线都认成兼容;
|
|
54
|
+
另外把 `dsh.client.inject` 里早就停更的 `dsh-client-runtime` 噪声清掉了。
|
|
55
|
+
|
|
56
|
+
### 二、0.1.7 上保存没反应,或者说"没保存成功"
|
|
57
|
+
|
|
58
|
+
0.1.7 有个硬门禁:**schema 里一个 `volatile` 字段都没标的条目,直接拒收**。
|
|
59
|
+
`write()` 抛 `has no volatile fields`,`describe()` 干脆跳过它 —— 页面就是一片空白。
|
|
60
|
+
我们的 Config 当时一个都没标,于是保存必炸、页面必空。
|
|
61
|
+
|
|
62
|
+
还有个更阴的:0.1.7 把 volatile 字段以**活引用** `{get(): T}` 的形式交回来
|
|
63
|
+
(这样改设置不用重挂载就能生效)。我们直接拿它去比较 —— 拿一个对象比一个字符串,
|
|
64
|
+
结果就是**刚存进去的值,读回来是"没设置"**。
|
|
65
|
+
|
|
66
|
+
对,这就是那个"我明明填了保留积分,重开又变回 0"的元凶。不是没存进去,是读错了。
|
|
67
|
+
|
|
68
|
+
**怎么修的:** Config 全部 12 个字段都过一遍 `asVolatile()` —— schemastery 3.18.3+
|
|
69
|
+
会调 `.volatile()`;0.1.5 那条线是 3.18.2,压根没这个方法,那就退化成什么都不做
|
|
70
|
+
(不能手写 `meta.volatile`,那会绕过 schemastery 自己的摆放校验,塞给 0.1.5 一个
|
|
71
|
+
它看不懂的东西)。读取则统一走 `unwrapVolatileDeep()` 把活引用剥开。
|
|
72
|
+
另外监听 `loader/volatile-update`,保存后运行中的实例立刻重读,不用重启。
|
|
73
|
+
|
|
74
|
+
### 三、设置入口搬进左栏 + 页面重新排了一遍
|
|
75
|
+
|
|
76
|
+
- **入口**:改注册到 `settings.section`,左栏现在多出一行,名字叫 **XD Pool**。
|
|
77
|
+
- **图标**:这个槽位的注册契约只投影 `id` / `order` / `label`,**没有 icon 字段** ——
|
|
78
|
+
宿主对不认识的 id 一律画个齿轮。想换图标只能自己动手:照着 `dshmarket` 的做法,
|
|
79
|
+
按 label 在 DOM 里认出自己那一行、打个标记,再用 CSS 把宿主的 svg 藏起来,
|
|
80
|
+
用 `mask-image` + `currentColor` 把自己的图标盖上去(颜色跟着主题走)。
|
|
81
|
+
外加一个 `MutationObserver` 盯着 —— 切语言、切主题时行元素会被重建,标记会掉。
|
|
82
|
+
- **页面**:以前要点一下才展开,现在**整页直接铺开**,并且重新排成分区卡片:
|
|
83
|
+
页头(图标 / 标题 / 描述,右边放「重新检测账号」「清除冷却」)、
|
|
84
|
+
状态卡片(地区切换 + 状态灯 + 汇总 + 账号使用方式)、
|
|
85
|
+
自动化卡片、账号卡片、模型卡片。
|
|
86
|
+
顺带修了个陈年小毛病:原来有个 `if (!open) return`,意思是**没展开就一次都不去
|
|
87
|
+
拉状态** —— 所以你刚点开永远是空的,得傻等一轮轮询。现在挂载即拉。
|
|
88
|
+
|
|
89
|
+
### 四、顺手修的几件小事(都来自 0.1.7 真机排查)
|
|
90
|
+
|
|
91
|
+
- **取密钥的超时 10 秒 → 30 秒。** 那个子进程是拿 WorkBuddy 的 Electron 二进制当
|
|
92
|
+
普通 Node 跑,冷启动第一次 spawn 实测就要 4.5 秒。碰上同时在跑 pnpm install、
|
|
93
|
+
或者杀毒软件在全盘扫描,10 秒预算直接被击穿 → 所有加密凭据被读成"未登录",
|
|
94
|
+
还要再背 60 秒负缓存。成功结果本来就是进程级缓存,加长预算最多只付一次。
|
|
95
|
+
- **补上盘符根目录。** `D:\workbuddy\`、`D:\workbuddyai\` 是真实安装位置,
|
|
96
|
+
可我们只扫 `Program Files`,一级扫描永远够不着它们。
|
|
97
|
+
- **顶层 IIFE 补 `.catch()`。** 那个 IIFE 里的 `await` 一旦失败就变成 unhandled
|
|
98
|
+
rejection,在 `installFailLoud` 下同样是直接杀宿主。
|
|
99
|
+
|
|
100
|
+
- **保存收益账本失败不再拖垮宿主。** 这条来自社区反馈:以前 scheduler 把「写
|
|
101
|
+
settings」的调用放在 `try` 里但**没 await** —— 而 `try/catch` 只抓得住同步抛错,
|
|
102
|
+
Promise 的 rejection 会直接漏成 unhandled rejection,在 `installFailLoud` 下
|
|
103
|
+
就是 `exit(1)`。那位朋友遇到的正是这个:打开设置页 → 读状态 → 滚动收益账本 →
|
|
104
|
+
写盘失败 → 整个 DSH 起不来。他把原因归到「写入方法名从 `set()` 改成了
|
|
105
|
+
`update()`」,那确实是当时那一次抛错的直接原因(我们 1.6.0 已经改过了),但
|
|
106
|
+
**结构上的隐患当时还在**:只要保存以任何理由失败(服务缺失、文件被锁、校验
|
|
107
|
+
不通过),宿主照样会崩。现在给持久化回调的返回值挂上 rejection 处理,同时保留
|
|
108
|
+
同步抛错的原有兜底 —— 保存失败只会记一条 warning,账本继续在内存里服务卡片,
|
|
109
|
+
绝不影响已经领到的奖励。
|
|
110
|
+
|
|
111
|
+
### 验证
|
|
112
|
+
|
|
113
|
+
- `typecheck` / `typecheck:client` 全绿;vitest **222 用例全绿**(原 198 + 新增 24)。
|
|
114
|
+
- 新增 `tests/host-compat.test.ts`:两条线的注册分支互斥且都不抛、两个 API 都没有时
|
|
115
|
+
只 warn、全部字段都标了 volatile、活引用的浅 / 深解引用、`inject` 不含互斥服务名、
|
|
116
|
+
注册槽位是 `settings.section`、0.1.7 用条目 id / 0.1.5 用 namespace、
|
|
117
|
+
整页无折叠且轮询不再依赖展开状态、左栏名称与图标注入、异步持久化失败不产生
|
|
118
|
+
unhandled rejection(该用例经反向验证:还原成旧写法时它会失败)。
|
|
119
|
+
- 拿**真实的 0.1.7 schemastery 3.18.4** 解析**真实的 Config**(12 个字段全部变成
|
|
120
|
+
活引用、schema 校验无异常);再用假宿主加载真实产物 `lib/index.js` / `lib/client.js`,
|
|
121
|
+
确认 `apply()` 在 0.1.7 / 0.1.5 / 两个服务都没有 这三种情形下都不抛异常。
|
|
122
|
+
- `lib/` 与 `src/` 同步重建(不然从 GitHub 源码装的人拿到的还是旧产物)。
|
|
123
|
+
|
|
124
|
+
### 最后
|
|
125
|
+
|
|
126
|
+
这一版由用户在本机 DSH Desktop 2.0.14(内核 0.1.7-rc.1)实测确认:
|
|
127
|
+
**入口出来了、卡片显示了、名字和图标都对了。**
|
|
128
|
+
|
|
129
|
+
感谢反馈问题的朋友 —— 那个"日志戛然而止"的现象描述得非常关键,否则光看日志
|
|
130
|
+
根本无从下手。
|
|
131
|
+
|
|
132
|
+
## 1.6.1 (2026-09-24)
|
|
133
|
+
|
|
134
|
+
### 同机双桌面 build:凭据按各自 keyId 解开
|
|
135
|
+
|
|
136
|
+
国内 `WorkBuddy.exe` 与国际版 `WorkBuddyAI.exe` 各自独立密钥,每个加密字段的
|
|
137
|
+
envelope 里带 keyId。同机双 build 时,旧 opener 假设"全局唯一一把密钥",
|
|
138
|
+
keyId 不匹配就报 `ciphertext 错误 / 找不到账号`。
|
|
139
|
+
|
|
140
|
+
修法:`encryptedFieldOpener` 先预热密钥缓存,再返回同步闭包,按字段
|
|
141
|
+
`envelope.keyId` 用 `atRestKeyFor` 选对应 build 的密钥;无 build 能提供该
|
|
142
|
+
keyId 时报"此字段无可用密钥"而非静默空串。
|
|
143
|
+
|
|
144
|
+
回归测试 +3(全仓 198):不同 keyId 各用自身密钥解开、无 build 提供时抛错、
|
|
145
|
+
错误密钥在场仍命中正确 key。
|
|
146
|
+
|
|
7
147
|
## 1.6.0 (2026-09-24)
|
|
8
148
|
|
|
9
149
|
这一版修的都是「已装、已登录,插件却说找不到 App / 存不进去」这一类误报。
|
package/lib/bin.js
CHANGED
|
@@ -1640,8 +1640,20 @@ const AAD_DOMAIN = Buffer.from("WB-AAD\0", "ascii");
|
|
|
1640
1640
|
const SYMMETRIC_SCHEME = "sym-v1";
|
|
1641
1641
|
/** Env override pointing at the WorkBuddy desktop executable. */
|
|
1642
1642
|
const WORKBUDDY_APP_EXECUTABLE_ENV = "WORKBUDDY_APP_EXECUTABLE";
|
|
1643
|
-
/**
|
|
1644
|
-
|
|
1643
|
+
/**
|
|
1644
|
+
* How long the app is given to answer with its key payload.
|
|
1645
|
+
*
|
|
1646
|
+
* 30s, not 10s: the child is the WorkBuddy Electron binary running as plain
|
|
1647
|
+
* Node, and its FIRST spawn on a cold machine costs several seconds on its own
|
|
1648
|
+
* (measured 4.5s here) before the endpoint security stack has warmed its scan
|
|
1649
|
+
* cache. Under load — a concurrent `pnpm install` from the market, a running
|
|
1650
|
+
* full-disk scan — that first spawn crosses a 10s budget, the fetch rejects,
|
|
1651
|
+
* `readAtRestKey` returns undefined, and every encrypted credential then reads
|
|
1652
|
+
* as `WorkBuddyEncryptedCredentialError` until the 60s negative cache expires.
|
|
1653
|
+
* A successful fetch is cached for the process lifetime, so the longer budget
|
|
1654
|
+
* is only ever paid once per process, and only when the app is present but slow.
|
|
1655
|
+
*/
|
|
1656
|
+
const KEY_FETCH_TIMEOUT_MS = 3e4;
|
|
1645
1657
|
/**
|
|
1646
1658
|
* Executable file names the desktop app ships under, in probe order.
|
|
1647
1659
|
*
|
|
@@ -1896,6 +1908,7 @@ function windowsFallbackAppPaths(env) {
|
|
|
1896
1908
|
}
|
|
1897
1909
|
roots.add(join(drive, "Program Files"));
|
|
1898
1910
|
roots.add(join(drive, "Program Files (x86)"));
|
|
1911
|
+
roots.add(drive);
|
|
1899
1912
|
}
|
|
1900
1913
|
for (const root of roots) for (const name of APP_EXECUTABLE_NAMES) {
|
|
1901
1914
|
out.push(join(root, "WorkBuddy", name));
|
|
@@ -3830,12 +3843,16 @@ var WorkBuddyScheduler = class {
|
|
|
3830
3843
|
*/
|
|
3831
3844
|
persistEarnings() {
|
|
3832
3845
|
if (this.saveEarningsFn === void 0) return;
|
|
3846
|
+
const accounts = {};
|
|
3847
|
+
for (const [id, entry] of this.earnings) accounts[id] = entry;
|
|
3848
|
+
const ledger = {
|
|
3849
|
+
date: this.earningsDate,
|
|
3850
|
+
accounts
|
|
3851
|
+
};
|
|
3833
3852
|
try {
|
|
3834
|
-
const
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
date: this.earningsDate,
|
|
3838
|
-
accounts
|
|
3853
|
+
const saved = this.saveEarningsFn(ledger);
|
|
3854
|
+
if (saved !== void 0 && typeof saved.then === "function") saved.then(void 0, (error) => {
|
|
3855
|
+
this.logger.warn?.("dsh-workbuddy-xdpool: could not persist automation earnings:", error);
|
|
3839
3856
|
});
|
|
3840
3857
|
} catch (error) {
|
|
3841
3858
|
this.logger.warn?.("dsh-workbuddy-xdpool: could not persist automation earnings:", error);
|
|
@@ -4267,6 +4284,18 @@ function formatRates(status) {
|
|
|
4267
4284
|
* region that has never been saved stays absent so `applyConfigFromSource` can
|
|
4268
4285
|
* fall back to the legacy flat keys.
|
|
4269
4286
|
*/
|
|
4287
|
+
function asVolatile(schema) {
|
|
4288
|
+
const candidate = schema;
|
|
4289
|
+
return typeof candidate.volatile === "function" ? candidate.volatile() : schema;
|
|
4290
|
+
}
|
|
4291
|
+
/**
|
|
4292
|
+
* One region's model-selection schema.
|
|
4293
|
+
*
|
|
4294
|
+
* Every field is optional on purpose: an absent field keeps its documented
|
|
4295
|
+
* meaning ("all enabled" / "follow the upstream image flag" / "no cap"), and a
|
|
4296
|
+
* region that has never been saved stays absent so `applyConfigFromSource` can
|
|
4297
|
+
* fall back to the legacy flat keys.
|
|
4298
|
+
*/
|
|
4270
4299
|
const modelSelectionSchema = z.object({
|
|
4271
4300
|
enabledModelIds: z.array(z.string()).description("Model ids enabled in this region's picker (absent = all)"),
|
|
4272
4301
|
imageModelIds: z.array(z.string()).description("Model ids accepting image input in this region (absent = follow upstream)"),
|
|
@@ -4294,22 +4323,22 @@ const automationSchema = z.object({
|
|
|
4294
4323
|
exhaustCooldownMs: z.number().step(1).min(1e3).description("How long a spent account rests, in milliseconds")
|
|
4295
4324
|
});
|
|
4296
4325
|
z.object({
|
|
4297
|
-
authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app own location)"),
|
|
4298
|
-
cooldownMs: z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds"),
|
|
4299
|
-
distribution: z.union([
|
|
4326
|
+
authFile: asVolatile(z.string().description("WorkBuddy desktop auth file (defaults to the app own location)")),
|
|
4327
|
+
cooldownMs: asVolatile(z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds")),
|
|
4328
|
+
distribution: asVolatile(z.union([
|
|
4300
4329
|
"priority",
|
|
4301
4330
|
"round-robin",
|
|
4302
4331
|
"balanced"
|
|
4303
|
-
]).default("priority").description("How requests are spread: priority (drain one), round-robin (in order), or balanced (idle-weighted random)"),
|
|
4304
|
-
disabledAccountIds: z.array(z.string()).default([]).description("Account ids excluded from the pool (empty = every discovered account participates)"),
|
|
4305
|
-
creditReserves: z.dict(z.number().step(1).min(0)).default({}).description("Per-account credit floor: stop using an account once its balance reaches this value"),
|
|
4306
|
-
enabledModelIds: z.array(z.string()).default([]).description("Legacy shared model-id list; used by a region that has no per-region selection yet"),
|
|
4307
|
-
imageModelIds: z.array(z.string()).default([]).description("Legacy shared image-id list; used by a region that has no per-region selection yet"),
|
|
4308
|
-
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Legacy shared context budgets; used by a region with no per-region selection yet"),
|
|
4309
|
-
modelSelectionCn: modelSelectionSchema.description("Model selection for the domestic gateway"),
|
|
4310
|
-
modelSelectionGlobal: modelSelectionSchema.description("Model selection for the international gateway"),
|
|
4311
|
-
automation: automationSchema.description("Daily points automation (activity report, task claiming, check-in)"),
|
|
4312
|
-
automationEarnings: z.any().description("Automation earnings ledger (written by the scheduler)")
|
|
4332
|
+
]).default("priority").description("How requests are spread: priority (drain one), round-robin (in order), or balanced (idle-weighted random)")),
|
|
4333
|
+
disabledAccountIds: asVolatile(z.array(z.string()).default([]).description("Account ids excluded from the pool (empty = every discovered account participates)")),
|
|
4334
|
+
creditReserves: asVolatile(z.dict(z.number().step(1).min(0)).default({}).description("Per-account credit floor: stop using an account once its balance reaches this value")),
|
|
4335
|
+
enabledModelIds: asVolatile(z.array(z.string()).default([]).description("Legacy shared model-id list; used by a region that has no per-region selection yet")),
|
|
4336
|
+
imageModelIds: asVolatile(z.array(z.string()).default([]).description("Legacy shared image-id list; used by a region that has no per-region selection yet")),
|
|
4337
|
+
contextBudgets: asVolatile(z.dict(z.number().step(1).min(1)).default({}).description("Legacy shared context budgets; used by a region with no per-region selection yet")),
|
|
4338
|
+
modelSelectionCn: asVolatile(modelSelectionSchema.description("Model selection for the domestic gateway")),
|
|
4339
|
+
modelSelectionGlobal: asVolatile(modelSelectionSchema.description("Model selection for the international gateway")),
|
|
4340
|
+
automation: asVolatile(automationSchema.description("Daily points automation (activity report, task claiming, check-in)")),
|
|
4341
|
+
automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)"))
|
|
4313
4342
|
});
|
|
4314
4343
|
/** Assemble the runtime objects without registering anything. */
|
|
4315
4344
|
/**
|