dsh-xdpool 2.2.1 → 2.4.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 +44 -0
- package/lib/bin.js +248 -19
- package/lib/client.js +7395 -231
- package/lib/index.d.ts +159 -8
- package/lib/index.js +3382 -2466
- package/package.json +1 -1
- package/vendor/channel-pack/pack.js +530 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
本项目的所有重要变更都会记录在此文件。
|
|
3
3
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
|
+
## 2.4.0 — 会话身份头、清单不再冻结、中文路由可存
|
|
5
|
+
> 这一版收的是「用着用着就报错」的那一类问题:调用被网关判成非法渠道(11128)、国际版模型清单永远停在启动那一刻、中文名字的路由点了保存却没存下、上下文选择一选就弹回去。
|
|
6
|
+
### 会话身份头:修掉 400 / 11128「Illegal API invocation from an unapproved channel」
|
|
7
|
+
网关对「这一轮对话属于谁」有一组身份头,缺一个就整轮拒绝,报 `11128 Illegal API invocation from an unapproved channel`,看起来像密钥失效,其实是新会话没自报身份。补齐官方客户端的契约:
|
|
8
|
+
- **会话稳定三头**(同一会话内不变):`X-Conversation-ID`、`X-Conversation-Request-ID`、`acp-connection-id`。
|
|
9
|
+
- **每请求两头**(逐次随机):`X-Conversation-Message-ID`、`X-Request-ID`。
|
|
10
|
+
- 另附 `X-Agent-Intent`、`X-Agent-Purpose`、`x-codebuddy-request`、`X-Private-Data`、`X-Product` 与 W3C/B3 追踪族。
|
|
11
|
+
|
|
12
|
+
实测口径:三个稳定头缺任一个即 400 11128,齐全即 200;值本身不校验(非 UUID 字符串也过)。池车道(WorkBuddy 国内/国际)与扫码渠道的 `send` 两条路径都已接上。
|
|
13
|
+
顺带修掉同源的一处:`role: "developer"` 的消息在国内网关同样触发 11128,现统一上送为 `system`。
|
|
14
|
+
### 国际版模型清单:不再冻结在启动那一刻
|
|
15
|
+
`createProvider` 的 `models` 是在创建 provider 时快照下来的数组,而原来的 `invalidate()` 只重排了 profile、复用了同一个 provider 对象,宿主按对象身份记忆化 `current()`,于是**新拿到的模型清单永远送不到选择器**——表现是「重新获取模型」点了没反应、国际版清单停在开机那一刻。现在每次 `invalidate()` 重建 provider 与 profile 两个对象,并通过 adapter handle 的 `replace()` 重新登记路由,由宿主自己发出 `llm/adapters-updated`。
|
|
16
|
+
(附带核查:国际版本来就有独立的 `modelSelectionGlobal` 键,与国内白名单分开读写,这一处无需改动。)
|
|
17
|
+
### 中文路由名现在存得下
|
|
18
|
+
链名 slug 原来是 `[^a-z0-9]+ → '-'`,中文名整串被压成空 id,规范化时被当作无效条目静默丢弃——于是「保存成功」但什么都没存。改为 Unicode 感知的 slug(`\p{L}`/`\p{N}`),拉丁名形态完全不变,已存的路由 id 继续可解析。
|
|
19
|
+
### 上下文大小:选中态不再一选就弹回
|
|
20
|
+
渠道卡把「从未保存过预算」当成了「已收窄到 200K」显示,用户点「完整」清空预算后该行又算回 200K,选中态立刻跳回第一项。现在未保存过预算就显示完整窗口;清空预算时同时保留 `nativeContextWindow`,radio 组不会在指针底下消失。
|
|
21
|
+
### 思考档位:不再只认一档
|
|
22
|
+
pack 兜底表把十几个模型的 `reasoningEfforts` 写成了单档(`["medium"]` / `["high"]`),于是国际版 `Deepseek-V4.1-Flash` 只能选 High。逐模型实测(两网关、五档全跑)后按真实能力补齐:凡是能调通的模型五档都接受、且各档 `reasoning_content` 长度不同。`gpt-5.3-codex` 五档皆 200 但 reasoning 恒空——档位合法而不可观测,已经在表里注明;`gemini-3.5-flash` 与 `primary-model` 的 400 是账号套餐权限(`model [X] is not allowed for user type [personal]`),与档位无关,未动。
|
|
23
|
+
### 重复账号:同一个人不再显示成两个
|
|
24
|
+
桌面端凭据同时带数字 `uin` 与 UUID `uid`,扫码渠道只带 `uid`,而账号 id 是按 `uin` 优先哈希的——同一个人走两条登录路径就整出两个池条目,各自带一部分模型。现在按 `uid` 建次级索引合并到同一个池条目,既不改哈希(否则全部冷却、失效标记、忽略名单、持久化快照一次性作废),也与凭据到达顺序无关。
|
|
25
|
+
### 「重新检测账号」不再静默失败
|
|
26
|
+
`listAllAccounts` 漏了 `await`,拿到的是一个 Promise,随后的 `.filter` 抛 `is not a function` 并被外层 try/catch 吞掉,表现是扫码账号全部并进池、重新检测永远没结果。
|
|
27
|
+
### 其他
|
|
28
|
+
- 测试:新增会话身份头与中文路由名的回归用例,全套 68 文件 910 用例。
|
|
29
|
+
## 2.3.0 — 扫码渠道接入账号池,免密车道修复,数据看板有了真数
|
|
30
|
+
> 这一版收掉三件大事:扫码登录的账号真正归入账号池并按渠道展示;免费模型车道跟上上游的新校验、不再动辄 403;数据看板不再是一排零。
|
|
31
|
+
### 账号池:扫码渠道按渠道入池、逐模型可控
|
|
32
|
+
**账号池页新增了扫码渠道卡**。此前扫码登录页加的账号游离在账号池之外——账号池只认 WorkBuddy 桌面端登录的账号,扫码加进来的十几个渠道账号在池里没有一席之地,模型开不开、哪个开,全靠猜。现在每家扫码渠道在账号池页有一张自己的卡:账号列表(昵称、启停、到期、余额)、模型列表(逐个勾选启用、免费/失效标签、上下文大小选择)、每日签到与积分领取。改动即时生效,不必点保存。
|
|
33
|
+
配套的 RPC 通道一并补齐:前端与扫码渠道后端之间的账号、模型、积分、启停等请求全部走同一封装(`channel-rpc.ts`),新增 `model.setDisabledMany`、`model.setContextBudget` 等方法,保证卡片上看到的和后端真实状态一致。
|
|
34
|
+
**重复账号自动清理**:同一渠道反复扫码不再攒出一排半成品账号——按「可用性」保留(长期凭据优先于临期凭据),其余清掉,日志里写明留了谁删了谁。
|
|
35
|
+
### 停机补偿:自动领取
|
|
36
|
+
WorkBuddy 侧偶发的模型服务异常会按账号补偿积分(在网页「用量」页顶部领,过期作废)。现在自动任务多了一项「停机补偿」,默认每天下午检查一次,有就领、领完记账,账单里能看到「补偿 +N 积分」。
|
|
37
|
+
### 免费模型车道:跟上上游的客户端校验
|
|
38
|
+
免费模型上游在 10 月中旬收紧了两道校验,旧版几乎每次调用都撞 403:
|
|
39
|
+
- **会话 id 编码**:上游要求会话 id 按规范的降序时间戳编码(低 12 位落在固定区间),旧实现用哈希摘要当前缀,合规概率只有约 1.5%。现在改为确定性派生规范 id——同一会话稳定映射到同一上游会话,配额不会被反复消耗。
|
|
40
|
+
- **客户端指纹**:请求头、路径、工具声明的形状全部对齐当前 CLI 客户端(v2 形状),并用真实网关验证过:免费模型能正常返回内容,超限时明确报限流而不是 403 拒绝。
|
|
41
|
+
### 数据看板:不再是一排零
|
|
42
|
+
免密通道的调用此前从未接入用量记录器,看板上免费模型的调用数恒为 0、重启即清空。现在每次调用(无论成败)都会记录请求数、轮次、首字延迟、生成速度;Token 总量取决于上游是否回传用量帧,不回传时如实显示「未上报」而不是编一个数。
|
|
43
|
+
### 其他
|
|
44
|
+
- 聚合渠道页的模型按「免费优先、上下文窗口、识图能力」排序;能力未知的模型如实留空,不编造窗口数字。
|
|
45
|
+
- 渠道卡模型行在模型较多时提供搜索框;按钮配色统一为中性色,hover 有扫光动效。
|
|
46
|
+
- 重复的登录入口合并;「已添加 N 个账号」的计数修正为去重后的真实数量。
|
|
47
|
+
- 新增 5 个测试文件(免密车道闸门、扫码账号接入、RPC 方法清单、模型选择、客户端 bundle 纯净度),全套 68 文件 895 用例。
|
|
4
48
|
## 2.2.1 — README 已知限制描述更正
|
|
5
49
|
纯文档修正:README「已知限制」里写着池「没法替你发起登录或扫码」——这是改名前的旧描述。现在的 XD Pool 有扫码登录页,可以为自己十三家渠道发起扫码登录;WorkBuddy 本体的 token 仍由桌面 App 的腾讯 SSO 铸造并绑定设备,这条限制保留但改成了准确的说法。代码无变化,发布这一版只为让 npm 页面的 README 跟上。
|
|
6
50
|
## 2.2.0 — 账号池重启不再失忆,CLI 修好,快照真正回读
|
package/lib/bin.js
CHANGED
|
@@ -161,6 +161,31 @@ const SESSION_DEAD_MARKERS = [
|
|
|
161
161
|
*/
|
|
162
162
|
const ALREADY_CHECKIN_MARKERS = ["已签到", "already"];
|
|
163
163
|
/**
|
|
164
|
+
* Markers for "there is no compensation offer to collect".
|
|
165
|
+
*
|
|
166
|
+
* The offer is seasonal — an outage announcement opens one, and it closes on a
|
|
167
|
+
* deadline — so for most of the year every account answers a claim with a
|
|
168
|
+
* refusal. That is a successful no-op, not a failure, for the same reason
|
|
169
|
+
* `isAlreadyCheckin` exists.
|
|
170
|
+
*/
|
|
171
|
+
const NO_COMPENSATION_MARKERS = [
|
|
172
|
+
"没有可领取",
|
|
173
|
+
"暂无可领取",
|
|
174
|
+
"无可领取",
|
|
175
|
+
"不可领取",
|
|
176
|
+
"未开启",
|
|
177
|
+
"没有活动",
|
|
178
|
+
"已领取",
|
|
179
|
+
"已过期",
|
|
180
|
+
"活动已结束",
|
|
181
|
+
"已结束",
|
|
182
|
+
"not eligible",
|
|
183
|
+
"no compensation",
|
|
184
|
+
"already claimed",
|
|
185
|
+
"not found",
|
|
186
|
+
"expired"
|
|
187
|
+
];
|
|
188
|
+
/**
|
|
164
189
|
* Hosts the international product answers on, once each has been stripped of a
|
|
165
190
|
* leading label. The WorkBuddy AI desktop app signs in at `workbuddy.ai` (and
|
|
166
191
|
* the desktop client itself lists `workbuddy.cc` alongside it); the CodeBuddy
|
|
@@ -252,6 +277,80 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
|
|
|
252
277
|
"X-IDE-Type": "WorkBuddy",
|
|
253
278
|
"X-IDE-Version": "5.5.6"
|
|
254
279
|
});
|
|
280
|
+
/**
|
|
281
|
+
* The conversation-identity headers the gateway's channel check demands.
|
|
282
|
+
*
|
|
283
|
+
* WHY THIS EXISTS
|
|
284
|
+
*
|
|
285
|
+
* `www.workbuddy.ai` turned "channel verification" from a soft check into a
|
|
286
|
+
* hard one. A `POST /v2/chat/completions` missing ANY of `X-Conversation-ID`,
|
|
287
|
+
* `X-Conversation-Request-ID` or `X-Conversation-Message-ID` is rejected with
|
|
288
|
+
* business code 11128 (`msg: "request illegal"` on the international gateway,
|
|
289
|
+
* `msg: "conversation id missing"` in the earlier wording). Verified by
|
|
290
|
+
* elimination against real credentials: delete any one of the three and the
|
|
291
|
+
* call 400s; send all three and it 200s with normal SSE. Every other header the
|
|
292
|
+
* desktop sends (`acp-connection-id`, `X-Request-ID`, `X-Agent-Intent`,
|
|
293
|
+
* `X-Private-Data`, the W3C/B3 tracing family) can be dropped without effect,
|
|
294
|
+
* so only these three are load-bearing.
|
|
295
|
+
*
|
|
296
|
+
* The values are NOT validated — plain non-UUID strings pass — so the gateway
|
|
297
|
+
* is checking for presence and consistency, not for a signature. That lets us
|
|
298
|
+
* derive them from the conversation instead of registering state with upstream.
|
|
299
|
+
*
|
|
300
|
+
* 11128 IS MULTIPLEXED. The domestic gateway returns the same code with
|
|
301
|
+
* `msg: "Illegal API invocation from an unapproved channel"` for a different
|
|
302
|
+
* cause entirely: a `developer`-role message. `prepareChatBody()` already
|
|
303
|
+
* rewrites that role; see its comment. Never branch on the business code alone.
|
|
304
|
+
*/
|
|
305
|
+
const CONVERSATION_STABLE_SALT = "conversation";
|
|
306
|
+
/**
|
|
307
|
+
* The three stable conversation ids for one logical conversation.
|
|
308
|
+
*
|
|
309
|
+
* Stable across every call and every account in a conversation, because the
|
|
310
|
+
* upstream treats the triple as one session: regenerating them per request
|
|
311
|
+
* would read as a new conversation on each turn. Derived rather than stored so
|
|
312
|
+
* the identity survives a restart and needs no registry — the same reasoning
|
|
313
|
+
* `deriveDeviceId` uses for the device id.
|
|
314
|
+
*
|
|
315
|
+
* Falls back to a random seed when the caller has no key (a one-shot compaction
|
|
316
|
+
* outside any conversation), which is safe because presence is all that is
|
|
317
|
+
* checked.
|
|
318
|
+
*/
|
|
319
|
+
function conversationIdentityOf(conversationKey, seed = randomUUID) {
|
|
320
|
+
const base = conversationKey === void 0 || conversationKey === "" ? seed() : conversationKey;
|
|
321
|
+
const hash = (suffix) => createHash("sha256").update(`${CONVERSATION_STABLE_SALT}:${suffix}:${base}`).digest("hex");
|
|
322
|
+
const asUuid = (hex) => `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
323
|
+
return {
|
|
324
|
+
conversationId: asUuid(hash("id")),
|
|
325
|
+
conversationRequestId: asUuid(hash("request")),
|
|
326
|
+
acpConnectionId: asUuid(hash("acp"))
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Full header set for one chat completion.
|
|
331
|
+
*
|
|
332
|
+
* `chatHeaders()` stays untouched on purpose: it is shared with the
|
|
333
|
+
* points-automation endpoints, which have never been shown to carry
|
|
334
|
+
* conversation identity, and adding it there would change how that traffic is
|
|
335
|
+
* attributed. The per-request ids are fresh UUIDs every call; only the triple
|
|
336
|
+
* from `conversationIdentityOf` is stable.
|
|
337
|
+
*/
|
|
338
|
+
function chatCompletionHeaders(credential, conversationKey) {
|
|
339
|
+
const identity = conversationIdentityOf(conversationKey);
|
|
340
|
+
return {
|
|
341
|
+
...chatHeaders(credential),
|
|
342
|
+
...CHAT_IDENTITY_HEADERS,
|
|
343
|
+
"X-Conversation-ID": identity.conversationId,
|
|
344
|
+
"X-Conversation-Request-ID": identity.conversationRequestId,
|
|
345
|
+
"acp-connection-id": identity.acpConnectionId,
|
|
346
|
+
"X-Conversation-Message-ID": randomUUID(),
|
|
347
|
+
"X-Request-ID": randomUUID(),
|
|
348
|
+
"X-Agent-Intent": "craft",
|
|
349
|
+
"X-Agent-Purpose": "conversation",
|
|
350
|
+
"X-Private-Data": "true",
|
|
351
|
+
"x-codebuddy-request": "1"
|
|
352
|
+
};
|
|
353
|
+
}
|
|
255
354
|
/** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
|
|
256
355
|
function refreshHeaders(credential) {
|
|
257
356
|
const headers = {
|
|
@@ -456,6 +555,16 @@ function isAlreadyCheckin(error) {
|
|
|
456
555
|
const message = error instanceof Error ? error.message : String(error);
|
|
457
556
|
return ALREADY_CHECKIN_MARKERS.some((marker) => message.includes(marker));
|
|
458
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Whether an error means "no compensation offer is running for this account".
|
|
560
|
+
*
|
|
561
|
+
* Callers treat this as a successful no-op: the job did its job and correctly
|
|
562
|
+
* found nothing to collect.
|
|
563
|
+
*/
|
|
564
|
+
function isNoCompensation(error) {
|
|
565
|
+
const lower = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
566
|
+
return NO_COMPENSATION_MARKERS.some((marker) => lower.includes(marker.toLowerCase()));
|
|
567
|
+
}
|
|
459
568
|
/** Parse the upstream's `credits` string into a multiplier. */
|
|
460
569
|
function parseCreditMultiplier(value) {
|
|
461
570
|
if (typeof value !== "string") return void 0;
|
|
@@ -844,13 +953,10 @@ var WorkBuddyUpstreamClient = class {
|
|
|
844
953
|
* single string. Throws on any failure: the compactor then falls back to
|
|
845
954
|
* plain truncation rather than failing the user's turn.
|
|
846
955
|
*/
|
|
847
|
-
async completeChat(credential, prepared, signal) {
|
|
956
|
+
async completeChat(credential, prepared, signal, conversationKey) {
|
|
848
957
|
const response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
|
|
849
958
|
method: "POST",
|
|
850
|
-
headers:
|
|
851
|
-
...chatHeaders(credential),
|
|
852
|
-
...CHAT_IDENTITY_HEADERS
|
|
853
|
-
},
|
|
959
|
+
headers: chatCompletionHeaders(credential, conversationKey),
|
|
854
960
|
body: prepared,
|
|
855
961
|
...signal === void 0 ? {} : { signal }
|
|
856
962
|
});
|
|
@@ -862,15 +968,12 @@ var WorkBuddyUpstreamClient = class {
|
|
|
862
968
|
return await readCompletionText(response.body);
|
|
863
969
|
}
|
|
864
970
|
/** Forward one chat completion. Never throws for upstream failures. */
|
|
865
|
-
async chatStream(credential, prepared, signal) {
|
|
971
|
+
async chatStream(credential, prepared, signal, conversationKey) {
|
|
866
972
|
let response;
|
|
867
973
|
try {
|
|
868
974
|
response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
|
|
869
975
|
method: "POST",
|
|
870
|
-
headers:
|
|
871
|
-
...chatHeaders(credential),
|
|
872
|
-
...CHAT_IDENTITY_HEADERS
|
|
873
|
-
},
|
|
976
|
+
headers: chatCompletionHeaders(credential, conversationKey),
|
|
874
977
|
body: prepared,
|
|
875
978
|
...signal === void 0 ? {} : { signal }
|
|
876
979
|
});
|
|
@@ -1112,6 +1215,52 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1112
1215
|
};
|
|
1113
1216
|
}
|
|
1114
1217
|
/**
|
|
1218
|
+
* Read the service-outage compensation offer, if any.
|
|
1219
|
+
*
|
|
1220
|
+
* NOTE the missing `/v2`: unlike the check-in routes, this one lives at
|
|
1221
|
+
* `/billing/meter/compensation-status` and the `/v2` variant answers a plain
|
|
1222
|
+
* 404 (verified on both billing hosts). The web client calls it that way too.
|
|
1223
|
+
*/
|
|
1224
|
+
async fetchCompensationStatus(credential) {
|
|
1225
|
+
const response = await this.fetchImpl(`${billingBase(credential)}/billing/meter/compensation-status`, {
|
|
1226
|
+
method: "POST",
|
|
1227
|
+
headers: billingHeaders(credential),
|
|
1228
|
+
body: "{}",
|
|
1229
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
1230
|
+
});
|
|
1231
|
+
const envelope = await readEnvelope(response);
|
|
1232
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
1233
|
+
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
1234
|
+
return {
|
|
1235
|
+
active: data["active"] === true,
|
|
1236
|
+
claimed: data["claimed"] === true,
|
|
1237
|
+
creditNum: typeof data["credit_num"] === "number" ? data["credit_num"] : 0,
|
|
1238
|
+
...typeof data["end_time"] === "string" && data["end_time"] !== "" ? { endTime: data["end_time"] } : {}
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Collect the running compensation offer.
|
|
1243
|
+
*
|
|
1244
|
+
* Idempotent upstream: a repeat claim answers `code: 0` with
|
|
1245
|
+
* `claimed: true` rather than an error, so a second pass in the same day
|
|
1246
|
+
* records as a no-op instead of a failure.
|
|
1247
|
+
*/
|
|
1248
|
+
async claimCompensation(credential) {
|
|
1249
|
+
const response = await this.fetchImpl(`${billingBase(credential)}/billing/meter/claim-compensation`, {
|
|
1250
|
+
method: "POST",
|
|
1251
|
+
headers: billingHeaders(credential),
|
|
1252
|
+
body: "{}",
|
|
1253
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
1254
|
+
});
|
|
1255
|
+
const envelope = await readEnvelope(response);
|
|
1256
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
1257
|
+
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
1258
|
+
return {
|
|
1259
|
+
claimed: data["claimed"] !== false,
|
|
1260
|
+
creditNum: typeof data["credit_num"] === "number" ? data["credit_num"] : 0
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1115
1264
|
* Public fingerprint fields every desktop event carries.
|
|
1116
1265
|
*
|
|
1117
1266
|
* The upstream scores a desktop-fingerprinted task only when the event looks
|
|
@@ -3120,6 +3269,15 @@ var WorkBuddyAccountPool = class {
|
|
|
3120
3269
|
persistent;
|
|
3121
3270
|
/** Path of the durable store; computed once so tests can pin it. */
|
|
3122
3271
|
storePath;
|
|
3272
|
+
/**
|
|
3273
|
+
* Optional second source of credentials: the channel pack's account pool,
|
|
3274
|
+
* which holds the accounts the user signed in through the scan-login page.
|
|
3275
|
+
*
|
|
3276
|
+
* A function rather than a list because that pool is mounted asynchronously
|
|
3277
|
+
* and can be re-read on demand; `undefined` means "no such source", which is
|
|
3278
|
+
* the state every test and every pre-mount startup is in.
|
|
3279
|
+
*/
|
|
3280
|
+
externalSource;
|
|
3123
3281
|
constructor(options = {}) {
|
|
3124
3282
|
this.logger = options.logger;
|
|
3125
3283
|
this.authDirs = options.authDirs ?? candidateAuthDirs();
|
|
@@ -3146,6 +3304,14 @@ var WorkBuddyAccountPool = class {
|
|
|
3146
3304
|
if (options.creditReserves !== void 0) this.setCreditReserves(options.creditReserves);
|
|
3147
3305
|
}
|
|
3148
3306
|
/**
|
|
3307
|
+
* Register (or clear, with `undefined`) the extra credential source that
|
|
3308
|
+
* `scan()` merges alongside the auth directories. Rescanning is the caller's
|
|
3309
|
+
* job: this only changes where the next scan looks.
|
|
3310
|
+
*/
|
|
3311
|
+
applyExternalSource(source) {
|
|
3312
|
+
this.externalSource = source;
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3149
3315
|
* Replace the permanent ignore list.
|
|
3150
3316
|
*
|
|
3151
3317
|
* Also drops any already-discovered account that is now ignored, so the change
|
|
@@ -3202,6 +3368,15 @@ var WorkBuddyAccountPool = class {
|
|
|
3202
3368
|
}
|
|
3203
3369
|
this.skippedFiles = skipped;
|
|
3204
3370
|
if (skipped.length > 0) this.logger?.warn?.(`dsh-xdpool: ${skipped.length} credential file(s) could not be read; ${found.length} account(s) still loaded. Reasons: ` + [...new Set(skipped.map((s) => s.reason))].join("; "));
|
|
3371
|
+
if (this.externalSource !== void 0) try {
|
|
3372
|
+
const external = await this.externalSource();
|
|
3373
|
+
for (const credential of external) {
|
|
3374
|
+
if (this.ignoredIds.size > 0 && this.ignoredIds.has(workbuddyAccountId(credential))) continue;
|
|
3375
|
+
found.push(credential);
|
|
3376
|
+
}
|
|
3377
|
+
} catch (error) {
|
|
3378
|
+
this.logger?.warn?.("dsh-xdpool: could not read channel-pack accounts; desktop accounts only", error);
|
|
3379
|
+
}
|
|
3205
3380
|
const byId = /* @__PURE__ */ new Map();
|
|
3206
3381
|
for (const account of this.accounts) byId.set(account.id, account);
|
|
3207
3382
|
if (this.persistent && this.accounts.length === 0) {
|
|
@@ -3218,8 +3393,17 @@ var WorkBuddyAccountPool = class {
|
|
|
3218
3393
|
});
|
|
3219
3394
|
}
|
|
3220
3395
|
}
|
|
3396
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
3397
|
+
for (const account of byId.values()) {
|
|
3398
|
+
const uid = account.credential.uid;
|
|
3399
|
+
if (typeof uid === "string" && uid !== "" && !byUid.has(uid)) byUid.set(uid, account.id);
|
|
3400
|
+
}
|
|
3221
3401
|
for (const credential of found) {
|
|
3222
|
-
const
|
|
3402
|
+
const byIdentity = workbuddyAccountId(credential);
|
|
3403
|
+
const uid = credential.uid;
|
|
3404
|
+
const knownId = typeof uid === "string" && uid !== "" ? byUid.get(uid) : void 0;
|
|
3405
|
+
const id = knownId ?? byIdentity;
|
|
3406
|
+
if (knownId === void 0 && typeof uid === "string" && uid !== "") byUid.set(uid, id);
|
|
3223
3407
|
const existing = byId.get(id);
|
|
3224
3408
|
if (existing === void 0) {
|
|
3225
3409
|
byId.set(id, {
|
|
@@ -4198,7 +4382,8 @@ const DEFAULT_AUTOMATION_HOURS = {
|
|
|
4198
4382
|
report: [10],
|
|
4199
4383
|
tasks: [11],
|
|
4200
4384
|
streak: [12],
|
|
4201
|
-
travel: [9, 21]
|
|
4385
|
+
travel: [9, 21],
|
|
4386
|
+
compensation: [13]
|
|
4202
4387
|
};
|
|
4203
4388
|
//#endregion
|
|
4204
4389
|
//#region src/channels.ts
|
|
@@ -9156,7 +9341,7 @@ Object.freeze({
|
|
|
9156
9341
|
"openai-responses": "/responses",
|
|
9157
9342
|
"anthropic-messages": "/messages"
|
|
9158
9343
|
});
|
|
9159
|
-
process.env.XDPOOL_FREE_BASE;
|
|
9344
|
+
typeof process === "undefined" || process.env.XDPOOL_FREE_BASE;
|
|
9160
9345
|
Object.freeze([
|
|
9161
9346
|
"muse-spark-1.3-contributor-free",
|
|
9162
9347
|
"muse-spark-1.2-contributor-free",
|
|
@@ -9615,7 +9800,8 @@ const JOB_KINDS = [
|
|
|
9615
9800
|
"report",
|
|
9616
9801
|
"tasks",
|
|
9617
9802
|
"streak",
|
|
9618
|
-
"travel"
|
|
9803
|
+
"travel",
|
|
9804
|
+
"compensation"
|
|
9619
9805
|
];
|
|
9620
9806
|
/**
|
|
9621
9807
|
* Pick the hour list to run on: the configured one, or the default.
|
|
@@ -9662,9 +9848,10 @@ function diffEarnings(before, after) {
|
|
|
9662
9848
|
claimed: entry.claimed - (was?.claimed ?? 0),
|
|
9663
9849
|
checkinCredit: entry.checkinCredit - (was?.checkinCredit ?? 0),
|
|
9664
9850
|
bonusCredit: entry.bonusCredit - (was?.bonusCredit ?? 0),
|
|
9665
|
-
travelCredit: entry.travelCredit - (was?.travelCredit ?? 0)
|
|
9851
|
+
travelCredit: entry.travelCredit - (was?.travelCredit ?? 0),
|
|
9852
|
+
compensationCredit: entry.compensationCredit - (was?.compensationCredit ?? 0)
|
|
9666
9853
|
};
|
|
9667
|
-
if (gain.credit === 0 && gain.energy === 0 && gain.claimed === 0 && gain.checkinCredit === 0 && gain.bonusCredit === 0 && gain.travelCredit === 0) continue;
|
|
9854
|
+
if (gain.credit === 0 && gain.energy === 0 && gain.claimed === 0 && gain.checkinCredit === 0 && gain.bonusCredit === 0 && gain.travelCredit === 0 && gain.compensationCredit === 0) continue;
|
|
9668
9855
|
accounts[id] = gain;
|
|
9669
9856
|
credit += gain.credit;
|
|
9670
9857
|
energy += gain.energy;
|
|
@@ -9758,6 +9945,7 @@ var WorkBuddyScheduler = class {
|
|
|
9758
9945
|
reportHours;
|
|
9759
9946
|
streakHours;
|
|
9760
9947
|
travelHours;
|
|
9948
|
+
compensationHours;
|
|
9761
9949
|
timer;
|
|
9762
9950
|
running = false;
|
|
9763
9951
|
/** Guards against a slow run overlapping the next tick. */
|
|
@@ -9777,7 +9965,8 @@ var WorkBuddyScheduler = class {
|
|
|
9777
9965
|
report: { ...EMPTY_JOB_STATE },
|
|
9778
9966
|
tasks: { ...EMPTY_JOB_STATE },
|
|
9779
9967
|
streak: { ...EMPTY_JOB_STATE },
|
|
9780
|
-
travel: { ...EMPTY_JOB_STATE }
|
|
9968
|
+
travel: { ...EMPTY_JOB_STATE },
|
|
9969
|
+
compensation: { ...EMPTY_JOB_STATE }
|
|
9781
9970
|
};
|
|
9782
9971
|
claimableSeen = 0;
|
|
9783
9972
|
/**
|
|
@@ -9814,6 +10003,7 @@ var WorkBuddyScheduler = class {
|
|
|
9814
10003
|
this.taskHours = hoursOrDefault(options.taskHours, DEFAULT_AUTOMATION_HOURS.tasks);
|
|
9815
10004
|
this.streakHours = hoursOrDefault(options.streakHours, DEFAULT_AUTOMATION_HOURS.streak);
|
|
9816
10005
|
this.travelHours = hoursOrDefault(options.travelHours, DEFAULT_AUTOMATION_HOURS.travel);
|
|
10006
|
+
this.compensationHours = hoursOrDefault(options.compensationHours, DEFAULT_AUTOMATION_HOURS.compensation);
|
|
9817
10007
|
}
|
|
9818
10008
|
/** Apply a new configuration; safe to call while running. */
|
|
9819
10009
|
/**
|
|
@@ -9846,6 +10036,7 @@ var WorkBuddyScheduler = class {
|
|
|
9846
10036
|
if (options.taskHours !== void 0) this.taskHours = hoursOrDefault(options.taskHours, DEFAULT_AUTOMATION_HOURS.tasks);
|
|
9847
10037
|
if (options.streakHours !== void 0) this.streakHours = hoursOrDefault(options.streakHours, DEFAULT_AUTOMATION_HOURS.streak);
|
|
9848
10038
|
if (options.travelHours !== void 0) this.travelHours = hoursOrDefault(options.travelHours, DEFAULT_AUTOMATION_HOURS.travel);
|
|
10039
|
+
if (options.compensationHours !== void 0) this.compensationHours = hoursOrDefault(options.compensationHours, DEFAULT_AUTOMATION_HOURS.compensation);
|
|
9849
10040
|
}
|
|
9850
10041
|
/** Hours for one job, used by the loop and the status document. */
|
|
9851
10042
|
hoursOf(kind) {
|
|
@@ -9855,6 +10046,7 @@ var WorkBuddyScheduler = class {
|
|
|
9855
10046
|
case "tasks": return this.taskHours;
|
|
9856
10047
|
case "streak": return this.streakHours;
|
|
9857
10048
|
case "travel": return this.travelHours;
|
|
10049
|
+
case "compensation": return this.compensationHours;
|
|
9858
10050
|
}
|
|
9859
10051
|
}
|
|
9860
10052
|
/** Start the loop. Idempotent. */
|
|
@@ -9968,6 +10160,7 @@ var WorkBuddyScheduler = class {
|
|
|
9968
10160
|
reportHours: this.reportHours,
|
|
9969
10161
|
streakHours: this.streakHours,
|
|
9970
10162
|
travelHours: this.travelHours,
|
|
10163
|
+
compensationHours: this.compensationHours,
|
|
9971
10164
|
jobs,
|
|
9972
10165
|
claimableSeen: this.claimableSeen,
|
|
9973
10166
|
earningsToday: this.earningsSnapshot(),
|
|
@@ -10080,7 +10273,8 @@ var WorkBuddyScheduler = class {
|
|
|
10080
10273
|
const checkinCredit = delta.checkinCredit ?? 0;
|
|
10081
10274
|
const bonusCredit = delta.bonusCredit ?? 0;
|
|
10082
10275
|
const travelCredit = delta.travelCredit ?? 0;
|
|
10083
|
-
|
|
10276
|
+
const compensationCredit = delta.compensationCredit ?? 0;
|
|
10277
|
+
if (credit === 0 && energy === 0 && claimed === 0 && checkinCredit === 0 && bonusCredit === 0 && travelCredit === 0 && compensationCredit === 0) return;
|
|
10084
10278
|
const existing = this.earnings.get(accountId);
|
|
10085
10279
|
this.earnings.set(accountId, {
|
|
10086
10280
|
credit: (existing?.credit ?? 0) + credit,
|
|
@@ -10089,6 +10283,7 @@ var WorkBuddyScheduler = class {
|
|
|
10089
10283
|
checkinCredit: (existing?.checkinCredit ?? 0) + checkinCredit,
|
|
10090
10284
|
bonusCredit: (existing?.bonusCredit ?? 0) + bonusCredit,
|
|
10091
10285
|
travelCredit: (existing?.travelCredit ?? 0) + travelCredit,
|
|
10286
|
+
compensationCredit: (existing?.compensationCredit ?? 0) + compensationCredit,
|
|
10092
10287
|
date: today
|
|
10093
10288
|
});
|
|
10094
10289
|
this.persistEarnings();
|
|
@@ -10196,7 +10391,19 @@ var WorkBuddyScheduler = class {
|
|
|
10196
10391
|
if (progress !== void 0) progressNote = progress;
|
|
10197
10392
|
break;
|
|
10198
10393
|
}
|
|
10199
|
-
case "travel":
|
|
10394
|
+
case "travel":
|
|
10395
|
+
await this.runTravel(account);
|
|
10396
|
+
break;
|
|
10397
|
+
case "compensation": try {
|
|
10398
|
+
const outcome = await this.runCompensation(account);
|
|
10399
|
+
if (outcome.credit > 0) {
|
|
10400
|
+
detail.push(`补偿 +${outcome.credit}`);
|
|
10401
|
+
this.logger.info?.(`automation compensation ${account.label}: +${outcome.credit} credit`);
|
|
10402
|
+
}
|
|
10403
|
+
} catch (error) {
|
|
10404
|
+
if (!isNoCompensation(error)) throw error;
|
|
10405
|
+
this.logger.info?.(`automation compensation ${account.label}: no offer to collect`);
|
|
10406
|
+
}
|
|
10200
10407
|
}
|
|
10201
10408
|
ok++;
|
|
10202
10409
|
} catch (error) {
|
|
@@ -10525,6 +10732,27 @@ var WorkBuddyScheduler = class {
|
|
|
10525
10732
|
}
|
|
10526
10733
|
this.logger.info?.(`automation travel ${account.label}: nothing to do (state=${travel.state})`);
|
|
10527
10734
|
}
|
|
10735
|
+
/**
|
|
10736
|
+
* Collect the running service-outage compensation offer, if any.
|
|
10737
|
+
*
|
|
10738
|
+
* Read-then-claim, rather than claim-blind: a claim costs one mutation the
|
|
10739
|
+
* upstream may reject, and the status call is a free read that also tells us
|
|
10740
|
+
* the size on offer so the ledger records the real number instead of
|
|
10741
|
+
* assuming the usual 500.
|
|
10742
|
+
*
|
|
10743
|
+
* Returns the credits collected — 0 when there was nothing to collect, which
|
|
10744
|
+
* is the normal case for most of the year.
|
|
10745
|
+
*/
|
|
10746
|
+
async runCompensation(account) {
|
|
10747
|
+
const credential = account.credential;
|
|
10748
|
+
const status = await this.client.fetchCompensationStatus(credential);
|
|
10749
|
+
if (!status.active || status.claimed || status.creditNum <= 0) return { credit: 0 };
|
|
10750
|
+
const claim = await this.client.claimCompensation(credential);
|
|
10751
|
+
if (!claim.claimed) return { credit: 0 };
|
|
10752
|
+
const credit = claim.creditNum > 0 ? claim.creditNum : status.creditNum;
|
|
10753
|
+
this.recordEarnings(account.id, dayKey(this.now()), { compensationCredit: credit });
|
|
10754
|
+
return { credit };
|
|
10755
|
+
}
|
|
10528
10756
|
};
|
|
10529
10757
|
Object.freeze({
|
|
10530
10758
|
"agnes": "推理内容以 think 标签返回",
|
|
@@ -10769,6 +10997,7 @@ const automationSchema = z.object({
|
|
|
10769
10997
|
taskHours: z.array(z.number().step(1).min(0).max(23)).description("Local hours for task enrolment and claiming"),
|
|
10770
10998
|
streakHours: z.array(z.number().step(1).min(0).max(23)).description("Local hours for streak redemption"),
|
|
10771
10999
|
travelHours: z.array(z.number().step(1).min(0).max(23)).description("Local hours for the buddy travel loop"),
|
|
11000
|
+
compensationHours: z.array(z.number().step(1).min(0).max(23)).description("Local hours for the outage-compensation claim"),
|
|
10772
11001
|
exhaustCooldownMs: z.number().step(1).min(1e3).description("How long a spent account rests, in milliseconds")
|
|
10773
11002
|
});
|
|
10774
11003
|
z.object({
|