dsh-xdpool 2.2.0 → 2.3.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 CHANGED
@@ -1,6 +1,27 @@
1
1
  # Changelog
2
2
  本项目的所有重要变更都会记录在此文件。
3
3
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+ ## 2.3.0 — 扫码渠道接入账号池,免密车道修复,数据看板有了真数
5
+ > 这一版收掉三件大事:扫码登录的账号真正归入账号池并按渠道展示;免费模型车道跟上上游的新校验、不再动辄 403;数据看板不再是一排零。
6
+ ### 账号池:扫码渠道按渠道入池、逐模型可控
7
+ **账号池页新增了扫码渠道卡**。此前扫码登录页加的账号游离在账号池之外——账号池只认 WorkBuddy 桌面端登录的账号,扫码加进来的十几个渠道账号在池里没有一席之地,模型开不开、哪个开,全靠猜。现在每家扫码渠道在账号池页有一张自己的卡:账号列表(昵称、启停、到期、余额)、模型列表(逐个勾选启用、免费/失效标签、上下文大小选择)、每日签到与积分领取。改动即时生效,不必点保存。
8
+ 配套的 RPC 通道一并补齐:前端与扫码渠道后端之间的账号、模型、积分、启停等请求全部走同一封装(`channel-rpc.ts`),新增 `model.setDisabledMany`、`model.setContextBudget` 等方法,保证卡片上看到的和后端真实状态一致。
9
+ **重复账号自动清理**:同一渠道反复扫码不再攒出一排半成品账号——按「可用性」保留(长期凭据优先于临期凭据),其余清掉,日志里写明留了谁删了谁。
10
+ ### 停机补偿:自动领取
11
+ WorkBuddy 侧偶发的模型服务异常会按账号补偿积分(在网页「用量」页顶部领,过期作废)。现在自动任务多了一项「停机补偿」,默认每天下午检查一次,有就领、领完记账,账单里能看到「补偿 +N 积分」。
12
+ ### 免费模型车道:跟上上游的客户端校验
13
+ 免费模型上游在 10 月中旬收紧了两道校验,旧版几乎每次调用都撞 403:
14
+ - **会话 id 编码**:上游要求会话 id 按规范的降序时间戳编码(低 12 位落在固定区间),旧实现用哈希摘要当前缀,合规概率只有约 1.5%。现在改为确定性派生规范 id——同一会话稳定映射到同一上游会话,配额不会被反复消耗。
15
+ - **客户端指纹**:请求头、路径、工具声明的形状全部对齐当前 CLI 客户端(v2 形状),并用真实网关验证过:免费模型能正常返回内容,超限时明确报限流而不是 403 拒绝。
16
+ ### 数据看板:不再是一排零
17
+ 免密通道的调用此前从未接入用量记录器,看板上免费模型的调用数恒为 0、重启即清空。现在每次调用(无论成败)都会记录请求数、轮次、首字延迟、生成速度;Token 总量取决于上游是否回传用量帧,不回传时如实显示「未上报」而不是编一个数。
18
+ ### 其他
19
+ - 聚合渠道页的模型按「免费优先、上下文窗口、识图能力」排序;能力未知的模型如实留空,不编造窗口数字。
20
+ - 渠道卡模型行在模型较多时提供搜索框;按钮配色统一为中性色,hover 有扫光动效。
21
+ - 重复的登录入口合并;「已添加 N 个账号」的计数修正为去重后的真实数量。
22
+ - 新增 5 个测试文件(免密车道闸门、扫码账号接入、RPC 方法清单、模型选择、客户端 bundle 纯净度),全套 68 文件 895 用例。
23
+ ## 2.2.1 — README 已知限制描述更正
24
+ 纯文档修正:README「已知限制」里写着池「没法替你发起登录或扫码」——这是改名前的旧描述。现在的 XD Pool 有扫码登录页,可以为自己十三家渠道发起扫码登录;WorkBuddy 本体的 token 仍由桌面 App 的腾讯 SSO 铸造并绑定设备,这条限制保留但改成了准确的说法。代码无变化,发布这一版只为让 npm 页面的 README 跟上。
4
25
  ## 2.2.0 — 账号池重启不再失忆,CLI 修好,快照真正回读
5
26
  > 群友报的三个问题一起收掉:重启 DSH 后账号池只剩当前登录账号;`dsh-xdpool` 命令行任何子命令都报 `ERR_MODULE_NOT_FOUND`;`import` 写进去的快照写了就忘,永远读不回来。
6
27
  ### 重启后账号还在:账号池持久化
package/README.en.md CHANGED
@@ -259,7 +259,7 @@ xdpool:
259
259
 
260
260
  ## Known limitations
261
261
 
262
- - **Only accounts on THIS machine's desktop app**: the pool cannot — and will not — sign you in or scan a QR code (tokens are minted by the WorkBuddy desktop app's own Tencent SSO and bound to the device). Adding an account = signing in on the desktop app; XD Pool absorbs it.
262
+ - **The WorkBuddy sign-in itself still goes through the desktop app**: XD Pool can launch QR sign-in for its own thirteen login channels, but WorkBuddy's own token is minted by the desktop app's Tencent SSO and bound to the device — the pool will not sign you in to WorkBuddy. Adding an account = signing in on the desktop app; XD Pool absorbs it.
263
263
 
264
264
  - **A few automation tasks are deliberately not implemented**
265
265
  - `Expert_Philanthropy` (charity expert): requires a real donation; there is no way around it.
package/README.md CHANGED
@@ -272,7 +272,7 @@ xdpool:
272
272
 
273
273
  ## 已知限制
274
274
 
275
- - **只能用本机桌面 App 里的账号**:池不会、也没法替你发起 WorkBuddy 的登录或扫码(token 由 WorkBuddy 桌面 App 自己的腾讯 SSO 铸造并和设备绑定)。加号 = 在桌面 App 里登录 / 切换,XD Pool 自动吸收。
275
+ - **WorkBuddy 主账号的登录仍走桌面 App**:XD Pool 可以为插件自己的十三家扫码登录渠道发起扫码,但 WorkBuddy 本体的 token 由桌面 App 的腾讯 SSO 铸造并和设备绑定,池不会替你登录 WorkBuddy。加号 = 在桌面 App 里登录 / 切换,XD Pool 自动吸收。
276
276
 
277
277
  - **自动化里有几个任务刻意没做**
278
278
  - `Expert_Philanthropy`(公益专家):需要真捐钱,绕不过去。
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
@@ -456,6 +481,16 @@ function isAlreadyCheckin(error) {
456
481
  const message = error instanceof Error ? error.message : String(error);
457
482
  return ALREADY_CHECKIN_MARKERS.some((marker) => message.includes(marker));
458
483
  }
484
+ /**
485
+ * Whether an error means "no compensation offer is running for this account".
486
+ *
487
+ * Callers treat this as a successful no-op: the job did its job and correctly
488
+ * found nothing to collect.
489
+ */
490
+ function isNoCompensation(error) {
491
+ const lower = (error instanceof Error ? error.message : String(error)).toLowerCase();
492
+ return NO_COMPENSATION_MARKERS.some((marker) => lower.includes(marker.toLowerCase()));
493
+ }
459
494
  /** Parse the upstream's `credits` string into a multiplier. */
460
495
  function parseCreditMultiplier(value) {
461
496
  if (typeof value !== "string") return void 0;
@@ -1112,6 +1147,52 @@ var WorkBuddyUpstreamClient = class {
1112
1147
  };
1113
1148
  }
1114
1149
  /**
1150
+ * Read the service-outage compensation offer, if any.
1151
+ *
1152
+ * NOTE the missing `/v2`: unlike the check-in routes, this one lives at
1153
+ * `/billing/meter/compensation-status` and the `/v2` variant answers a plain
1154
+ * 404 (verified on both billing hosts). The web client calls it that way too.
1155
+ */
1156
+ async fetchCompensationStatus(credential) {
1157
+ const response = await this.fetchImpl(`${billingBase(credential)}/billing/meter/compensation-status`, {
1158
+ method: "POST",
1159
+ headers: billingHeaders(credential),
1160
+ body: "{}",
1161
+ signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
1162
+ });
1163
+ const envelope = await readEnvelope(response);
1164
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
1165
+ const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
1166
+ return {
1167
+ active: data["active"] === true,
1168
+ claimed: data["claimed"] === true,
1169
+ creditNum: typeof data["credit_num"] === "number" ? data["credit_num"] : 0,
1170
+ ...typeof data["end_time"] === "string" && data["end_time"] !== "" ? { endTime: data["end_time"] } : {}
1171
+ };
1172
+ }
1173
+ /**
1174
+ * Collect the running compensation offer.
1175
+ *
1176
+ * Idempotent upstream: a repeat claim answers `code: 0` with
1177
+ * `claimed: true` rather than an error, so a second pass in the same day
1178
+ * records as a no-op instead of a failure.
1179
+ */
1180
+ async claimCompensation(credential) {
1181
+ const response = await this.fetchImpl(`${billingBase(credential)}/billing/meter/claim-compensation`, {
1182
+ method: "POST",
1183
+ headers: billingHeaders(credential),
1184
+ body: "{}",
1185
+ signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
1186
+ });
1187
+ const envelope = await readEnvelope(response);
1188
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
1189
+ const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
1190
+ return {
1191
+ claimed: data["claimed"] !== false,
1192
+ creditNum: typeof data["credit_num"] === "number" ? data["credit_num"] : 0
1193
+ };
1194
+ }
1195
+ /**
1115
1196
  * Public fingerprint fields every desktop event carries.
1116
1197
  *
1117
1198
  * The upstream scores a desktop-fingerprinted task only when the event looks
@@ -3120,6 +3201,15 @@ var WorkBuddyAccountPool = class {
3120
3201
  persistent;
3121
3202
  /** Path of the durable store; computed once so tests can pin it. */
3122
3203
  storePath;
3204
+ /**
3205
+ * Optional second source of credentials: the channel pack's account pool,
3206
+ * which holds the accounts the user signed in through the scan-login page.
3207
+ *
3208
+ * A function rather than a list because that pool is mounted asynchronously
3209
+ * and can be re-read on demand; `undefined` means "no such source", which is
3210
+ * the state every test and every pre-mount startup is in.
3211
+ */
3212
+ externalSource;
3123
3213
  constructor(options = {}) {
3124
3214
  this.logger = options.logger;
3125
3215
  this.authDirs = options.authDirs ?? candidateAuthDirs();
@@ -3146,6 +3236,14 @@ var WorkBuddyAccountPool = class {
3146
3236
  if (options.creditReserves !== void 0) this.setCreditReserves(options.creditReserves);
3147
3237
  }
3148
3238
  /**
3239
+ * Register (or clear, with `undefined`) the extra credential source that
3240
+ * `scan()` merges alongside the auth directories. Rescanning is the caller's
3241
+ * job: this only changes where the next scan looks.
3242
+ */
3243
+ applyExternalSource(source) {
3244
+ this.externalSource = source;
3245
+ }
3246
+ /**
3149
3247
  * Replace the permanent ignore list.
3150
3248
  *
3151
3249
  * Also drops any already-discovered account that is now ignored, so the change
@@ -3202,6 +3300,15 @@ var WorkBuddyAccountPool = class {
3202
3300
  }
3203
3301
  this.skippedFiles = skipped;
3204
3302
  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("; "));
3303
+ if (this.externalSource !== void 0) try {
3304
+ const external = await this.externalSource();
3305
+ for (const credential of external) {
3306
+ if (this.ignoredIds.size > 0 && this.ignoredIds.has(workbuddyAccountId(credential))) continue;
3307
+ found.push(credential);
3308
+ }
3309
+ } catch (error) {
3310
+ this.logger?.warn?.("dsh-xdpool: could not read channel-pack accounts; desktop accounts only", error);
3311
+ }
3205
3312
  const byId = /* @__PURE__ */ new Map();
3206
3313
  for (const account of this.accounts) byId.set(account.id, account);
3207
3314
  if (this.persistent && this.accounts.length === 0) {
@@ -4198,7 +4305,8 @@ const DEFAULT_AUTOMATION_HOURS = {
4198
4305
  report: [10],
4199
4306
  tasks: [11],
4200
4307
  streak: [12],
4201
- travel: [9, 21]
4308
+ travel: [9, 21],
4309
+ compensation: [13]
4202
4310
  };
4203
4311
  //#endregion
4204
4312
  //#region src/channels.ts
@@ -9156,7 +9264,7 @@ Object.freeze({
9156
9264
  "openai-responses": "/responses",
9157
9265
  "anthropic-messages": "/messages"
9158
9266
  });
9159
- process.env.XDPOOL_FREE_BASE;
9267
+ typeof process === "undefined" || process.env.XDPOOL_FREE_BASE;
9160
9268
  Object.freeze([
9161
9269
  "muse-spark-1.3-contributor-free",
9162
9270
  "muse-spark-1.2-contributor-free",
@@ -9615,7 +9723,8 @@ const JOB_KINDS = [
9615
9723
  "report",
9616
9724
  "tasks",
9617
9725
  "streak",
9618
- "travel"
9726
+ "travel",
9727
+ "compensation"
9619
9728
  ];
9620
9729
  /**
9621
9730
  * Pick the hour list to run on: the configured one, or the default.
@@ -9662,9 +9771,10 @@ function diffEarnings(before, after) {
9662
9771
  claimed: entry.claimed - (was?.claimed ?? 0),
9663
9772
  checkinCredit: entry.checkinCredit - (was?.checkinCredit ?? 0),
9664
9773
  bonusCredit: entry.bonusCredit - (was?.bonusCredit ?? 0),
9665
- travelCredit: entry.travelCredit - (was?.travelCredit ?? 0)
9774
+ travelCredit: entry.travelCredit - (was?.travelCredit ?? 0),
9775
+ compensationCredit: entry.compensationCredit - (was?.compensationCredit ?? 0)
9666
9776
  };
9667
- if (gain.credit === 0 && gain.energy === 0 && gain.claimed === 0 && gain.checkinCredit === 0 && gain.bonusCredit === 0 && gain.travelCredit === 0) continue;
9777
+ if (gain.credit === 0 && gain.energy === 0 && gain.claimed === 0 && gain.checkinCredit === 0 && gain.bonusCredit === 0 && gain.travelCredit === 0 && gain.compensationCredit === 0) continue;
9668
9778
  accounts[id] = gain;
9669
9779
  credit += gain.credit;
9670
9780
  energy += gain.energy;
@@ -9758,6 +9868,7 @@ var WorkBuddyScheduler = class {
9758
9868
  reportHours;
9759
9869
  streakHours;
9760
9870
  travelHours;
9871
+ compensationHours;
9761
9872
  timer;
9762
9873
  running = false;
9763
9874
  /** Guards against a slow run overlapping the next tick. */
@@ -9777,7 +9888,8 @@ var WorkBuddyScheduler = class {
9777
9888
  report: { ...EMPTY_JOB_STATE },
9778
9889
  tasks: { ...EMPTY_JOB_STATE },
9779
9890
  streak: { ...EMPTY_JOB_STATE },
9780
- travel: { ...EMPTY_JOB_STATE }
9891
+ travel: { ...EMPTY_JOB_STATE },
9892
+ compensation: { ...EMPTY_JOB_STATE }
9781
9893
  };
9782
9894
  claimableSeen = 0;
9783
9895
  /**
@@ -9814,6 +9926,7 @@ var WorkBuddyScheduler = class {
9814
9926
  this.taskHours = hoursOrDefault(options.taskHours, DEFAULT_AUTOMATION_HOURS.tasks);
9815
9927
  this.streakHours = hoursOrDefault(options.streakHours, DEFAULT_AUTOMATION_HOURS.streak);
9816
9928
  this.travelHours = hoursOrDefault(options.travelHours, DEFAULT_AUTOMATION_HOURS.travel);
9929
+ this.compensationHours = hoursOrDefault(options.compensationHours, DEFAULT_AUTOMATION_HOURS.compensation);
9817
9930
  }
9818
9931
  /** Apply a new configuration; safe to call while running. */
9819
9932
  /**
@@ -9846,6 +9959,7 @@ var WorkBuddyScheduler = class {
9846
9959
  if (options.taskHours !== void 0) this.taskHours = hoursOrDefault(options.taskHours, DEFAULT_AUTOMATION_HOURS.tasks);
9847
9960
  if (options.streakHours !== void 0) this.streakHours = hoursOrDefault(options.streakHours, DEFAULT_AUTOMATION_HOURS.streak);
9848
9961
  if (options.travelHours !== void 0) this.travelHours = hoursOrDefault(options.travelHours, DEFAULT_AUTOMATION_HOURS.travel);
9962
+ if (options.compensationHours !== void 0) this.compensationHours = hoursOrDefault(options.compensationHours, DEFAULT_AUTOMATION_HOURS.compensation);
9849
9963
  }
9850
9964
  /** Hours for one job, used by the loop and the status document. */
9851
9965
  hoursOf(kind) {
@@ -9855,6 +9969,7 @@ var WorkBuddyScheduler = class {
9855
9969
  case "tasks": return this.taskHours;
9856
9970
  case "streak": return this.streakHours;
9857
9971
  case "travel": return this.travelHours;
9972
+ case "compensation": return this.compensationHours;
9858
9973
  }
9859
9974
  }
9860
9975
  /** Start the loop. Idempotent. */
@@ -9968,6 +10083,7 @@ var WorkBuddyScheduler = class {
9968
10083
  reportHours: this.reportHours,
9969
10084
  streakHours: this.streakHours,
9970
10085
  travelHours: this.travelHours,
10086
+ compensationHours: this.compensationHours,
9971
10087
  jobs,
9972
10088
  claimableSeen: this.claimableSeen,
9973
10089
  earningsToday: this.earningsSnapshot(),
@@ -10080,7 +10196,8 @@ var WorkBuddyScheduler = class {
10080
10196
  const checkinCredit = delta.checkinCredit ?? 0;
10081
10197
  const bonusCredit = delta.bonusCredit ?? 0;
10082
10198
  const travelCredit = delta.travelCredit ?? 0;
10083
- if (credit === 0 && energy === 0 && claimed === 0 && checkinCredit === 0 && bonusCredit === 0 && travelCredit === 0) return;
10199
+ const compensationCredit = delta.compensationCredit ?? 0;
10200
+ if (credit === 0 && energy === 0 && claimed === 0 && checkinCredit === 0 && bonusCredit === 0 && travelCredit === 0 && compensationCredit === 0) return;
10084
10201
  const existing = this.earnings.get(accountId);
10085
10202
  this.earnings.set(accountId, {
10086
10203
  credit: (existing?.credit ?? 0) + credit,
@@ -10089,6 +10206,7 @@ var WorkBuddyScheduler = class {
10089
10206
  checkinCredit: (existing?.checkinCredit ?? 0) + checkinCredit,
10090
10207
  bonusCredit: (existing?.bonusCredit ?? 0) + bonusCredit,
10091
10208
  travelCredit: (existing?.travelCredit ?? 0) + travelCredit,
10209
+ compensationCredit: (existing?.compensationCredit ?? 0) + compensationCredit,
10092
10210
  date: today
10093
10211
  });
10094
10212
  this.persistEarnings();
@@ -10196,7 +10314,19 @@ var WorkBuddyScheduler = class {
10196
10314
  if (progress !== void 0) progressNote = progress;
10197
10315
  break;
10198
10316
  }
10199
- case "travel": await this.runTravel(account);
10317
+ case "travel":
10318
+ await this.runTravel(account);
10319
+ break;
10320
+ case "compensation": try {
10321
+ const outcome = await this.runCompensation(account);
10322
+ if (outcome.credit > 0) {
10323
+ detail.push(`补偿 +${outcome.credit}`);
10324
+ this.logger.info?.(`automation compensation ${account.label}: +${outcome.credit} credit`);
10325
+ }
10326
+ } catch (error) {
10327
+ if (!isNoCompensation(error)) throw error;
10328
+ this.logger.info?.(`automation compensation ${account.label}: no offer to collect`);
10329
+ }
10200
10330
  }
10201
10331
  ok++;
10202
10332
  } catch (error) {
@@ -10525,6 +10655,27 @@ var WorkBuddyScheduler = class {
10525
10655
  }
10526
10656
  this.logger.info?.(`automation travel ${account.label}: nothing to do (state=${travel.state})`);
10527
10657
  }
10658
+ /**
10659
+ * Collect the running service-outage compensation offer, if any.
10660
+ *
10661
+ * Read-then-claim, rather than claim-blind: a claim costs one mutation the
10662
+ * upstream may reject, and the status call is a free read that also tells us
10663
+ * the size on offer so the ledger records the real number instead of
10664
+ * assuming the usual 500.
10665
+ *
10666
+ * Returns the credits collected — 0 when there was nothing to collect, which
10667
+ * is the normal case for most of the year.
10668
+ */
10669
+ async runCompensation(account) {
10670
+ const credential = account.credential;
10671
+ const status = await this.client.fetchCompensationStatus(credential);
10672
+ if (!status.active || status.claimed || status.creditNum <= 0) return { credit: 0 };
10673
+ const claim = await this.client.claimCompensation(credential);
10674
+ if (!claim.claimed) return { credit: 0 };
10675
+ const credit = claim.creditNum > 0 ? claim.creditNum : status.creditNum;
10676
+ this.recordEarnings(account.id, dayKey(this.now()), { compensationCredit: credit });
10677
+ return { credit };
10678
+ }
10528
10679
  };
10529
10680
  Object.freeze({
10530
10681
  "agnes": "推理内容以 think 标签返回",
@@ -10769,6 +10920,7 @@ const automationSchema = z.object({
10769
10920
  taskHours: z.array(z.number().step(1).min(0).max(23)).description("Local hours for task enrolment and claiming"),
10770
10921
  streakHours: z.array(z.number().step(1).min(0).max(23)).description("Local hours for streak redemption"),
10771
10922
  travelHours: z.array(z.number().step(1).min(0).max(23)).description("Local hours for the buddy travel loop"),
10923
+ compensationHours: z.array(z.number().step(1).min(0).max(23)).description("Local hours for the outage-compensation claim"),
10772
10924
  exhaustCooldownMs: z.number().step(1).min(1e3).description("How long a spent account rests, in milliseconds")
10773
10925
  });
10774
10926
  z.object({