dsh-connect-workbuddy 5.8.5 → 5.9.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.
@@ -1467,8 +1467,27 @@ function text(value) {
1467
1467
  *
1468
1468
  * Any other malformed value is left to the caller to drop — a wrong window
1469
1469
  * would hide a live promotion, and guessing is not this parser's job.
1470
+ *
1471
+ * THE TYPE GUARD IS LOAD-BEARING, NOT DEFENSIVE DECORATION (issue #139). The
1472
+ * upstream feeds a string, and `parseDaily`'s own `text()` would already have
1473
+ * dropped a number — but the SAVED `lastCatalog` is a second, independent
1474
+ * entrance to this function, and nothing on that path type-checks. A promotion
1475
+ * object rides through `toPersistedWorkBuddyModel` untouched (the settings
1476
+ * schema keeps unknown keys), survives `resolveRoster`, and reaches the
1477
+ * renderer's `selectPromoBadge`. A numeric `start` there used to throw
1478
+ * `value.trim is not a function` DURING RENDER, which the host's slot error
1479
+ * boundary turned into a blank configuration section — the page looked as if
1480
+ * the plugin had never installed.
1481
+ *
1482
+ * Refusing a non-string is the same answer the parser already gives every
1483
+ * other unreadable shape: drop the window rather than guess. Interpreting a
1484
+ * number as "minutes since midnight" was considered and rejected — `1380`
1485
+ * happens to mean 23:00, but no upstream has ever published that shape, and
1486
+ * inventing hours for a promotion the upstream never described is exactly the
1487
+ * overstatement this module exists to prevent.
1470
1488
  */
1471
1489
  function minutesOfClock(value) {
1490
+ if (typeof value !== "string") return void 0;
1472
1491
  const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
1473
1492
  if (match === null) return void 0;
1474
1493
  const hours = Number(match[1]);
@@ -1528,8 +1547,27 @@ function parseHover(value) {
1528
1547
  ...textEn === void 0 ? {} : { textEn }
1529
1548
  };
1530
1549
  }
1531
- /** The upstream's `schedule.daily` → the windows this plugin can evaluate. */
1532
- function parseDaily(value) {
1550
+ /**
1551
+ * What an unreadable endpoint actually was, for the one diagnostic line below.
1552
+ *
1553
+ * Deliberately coarse: a NUMBER is the shape a future upstream format change
1554
+ * would plausibly take (minutes since midnight), so it is named separately
1555
+ * from "some other non-string". Reporting the typeof is enough to tell the two
1556
+ * apart in a log without ever echoing upstream content into the console.
1557
+ */
1558
+ function shapeOf(value) {
1559
+ if (typeof value === "number") return "number";
1560
+ if (value === null) return "null";
1561
+ if (Array.isArray(value)) return "array";
1562
+ return typeof value;
1563
+ }
1564
+ /**
1565
+ * The upstream's `schedule.daily` → the windows this plugin can evaluate.
1566
+ *
1567
+ * `label` names the entry for the diagnostic line and is passed in rather than
1568
+ * read from `value`: the entry id lives one level up, in `parsePromo`.
1569
+ */
1570
+ function parseDaily(value, label) {
1533
1571
  if (!Array.isArray(value)) return void 0;
1534
1572
  const windows = [];
1535
1573
  for (const entry of value) {
@@ -1537,8 +1575,12 @@ function parseDaily(value) {
1537
1575
  if (raw === void 0) continue;
1538
1576
  const start = text(raw["start"]);
1539
1577
  const end = text(raw["end"]);
1540
- if (start === void 0 || end === void 0) continue;
1541
- if (minutesOfClock(start) === void 0 || minutesOfClock(end) === void 0) continue;
1578
+ if (start === void 0 || end === void 0 || minutesOfClock(start) === void 0 || minutesOfClock(end) === void 0) {
1579
+ try {
1580
+ console.warn(`[dsh-connect-workbuddy] promotion "${label}": dropped an unreadable daily window (start: ${shapeOf(raw["start"])}, end: ${shapeOf(raw["end"])}); the upstream may have changed the time format`);
1581
+ } catch {}
1582
+ continue;
1583
+ }
1542
1584
  windows.push({
1543
1585
  start,
1544
1586
  end
@@ -1578,13 +1620,13 @@ function parseDiscount(value) {
1578
1620
  };
1579
1621
  }
1580
1622
  /** The upstream's `schedule` → this plugin's schedule shape. */
1581
- function parseSchedule(value) {
1623
+ function parseSchedule(value, label) {
1582
1624
  const raw = record(value);
1583
1625
  if (raw === void 0) return void 0;
1584
1626
  const timezone = text(raw["timezone"]);
1585
1627
  const validFrom = parseInstant(raw["validFrom"]);
1586
1628
  const validUntil = parseInstant(raw["validUntil"]);
1587
- const daily = parseDaily(raw["daily"]);
1629
+ const daily = parseDaily(raw["daily"], label);
1588
1630
  const dailyUnusable = daily === void 0 && Array.isArray(raw["daily"]) && raw["daily"].length > 0;
1589
1631
  if (timezone === void 0 && validFrom === void 0 && validUntil === void 0 && daily === void 0 && !dailyUnusable) return void 0;
1590
1632
  return {
@@ -1605,11 +1647,12 @@ function parsePromo(value, source) {
1605
1647
  const badge = parseBadge(raw["badge"]);
1606
1648
  if (badge === void 0) return void 0;
1607
1649
  const hover = parseHover(raw["hover"]);
1608
- const schedule = parseSchedule(raw["schedule"]);
1650
+ const id = text(raw["id"]) ?? `${source}:${modelIds.join(",")}`;
1651
+ const schedule = parseSchedule(raw["schedule"], id);
1609
1652
  const discount = parseDiscount(raw["discount"]);
1610
1653
  const priority = typeof raw["priority"] === "number" && Number.isFinite(raw["priority"]) ? raw["priority"] : 0;
1611
1654
  return {
1612
- id: text(raw["id"]) ?? `${source}:${modelIds.join(",")}`,
1655
+ id,
1613
1656
  source,
1614
1657
  modelIds,
1615
1658
  badge,
@@ -6940,7 +6983,7 @@ function describeRosterSeenPayload(raw) {
6940
6983
  //#endregion
6941
6984
  //#region src/version.ts
6942
6985
  /** The npm package version this build was produced from. */
6943
- const WORKBUDDY_CONNECT_VERSION = "5.8.5";
6986
+ const WORKBUDDY_CONNECT_VERSION = "5.9.0";
6944
6987
  //#endregion
6945
6988
  //#region src/host-heartbeat.ts
6946
6989
  /**
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as parseCreditMultiplier, A as isEncryptedCredentialError, At as isEncryptedFieldWrapper, B as WorkBuddyCredentialRejectedError, Bt as writeFileAtomic, C as WorkBuddyCredentialStore, Ct as WORKBUDDY_APP_EXECUTABLE_ENV, D as defaultDesktopAuthDirs, Dt as fetchAtRestKeyPayload, E as defaultDesktopAuthCandidates, Et as deriveAtRestKeyId, F as workbuddyAccountId, Ft as readAtRestKey, G as forceNeutralHead, H as channelGateMemberOf, I as workbuddyOwnAuthPath, It as workbuddyAppExecutableCandidates, J as isCredentialRejectedError, K as isContextTooLongBody, L as CONTEXT_OVERFLOW_MARKER, M as legacyWorkbuddyOwnAuthPath, Mt as macosBundleExecutable, N as parseWorkBuddyAuth, Nt as macosNestedAppBundles, O as defaultDesktopAuthPath, Ot as findWorkbuddyAppExecutable, P as preferredAccountName, Pt as openEncryptedField, Q as openingEvidenceOfBody, R as CREDENTIAL_REJECTED_CODE, Rt as resolveDshHome, S as WORKBUDDY_AUTH_FILE_ENV, St as nativeModalityOf, T as authFileName, Tt as deriveAtRestKey, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, W as declaredTools, X as knownRegionOf, Y as isFirstMessageGateBody, Z as modelIdOfBody, _ as deriveCatalog, _t as pickImageModel, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as NATIVE_MODALITY_BY_MODEL_ID, c as WORKBUDDY_CONNECT_VERSION, ct as REFUSES_OFF_MODEL_IDS, dt as offDefaultFor, et as parseReasoning, f as piAiRuntimeInfo, ft as offRefused, g as applyContextBudgets, gt as WorkBuddyImageError, h as WorkBuddyCatalog, ht as DEFAULT_IMAGE_SIZE, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as isWorkbuddyBundle, k as hasEncryptedCredentialFields, l as createFileRosterSeenStore, lt as defaultOffModelIds, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, mt as withEffectiveOff, n as clearHostHeartbeat, nt as perturbChannelFingerprints, o as workbuddyHostHeartbeatPath, ot as rewriteSessionTitleModel, p as FALLBACK_WORKBUDDY_MODELS, pt as recordRefusal, q as isConversationIdGateBody, r as isHeartbeatProcessAlive, rt as prepareChatBody, s as writeHostHeartbeat, st as attachBusyLevels, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as parseUpstreamModel, ut as lastRefusalFor, v as fallbackModelsFor, vt as parseQueueAnswer, w as WorkBuddyEncryptedCredentialError, wt as clearAtRestKeyCache, x as WORKBUDDY_AUTH_FILENAME, xt as imageDefaultFor, y as withoutRouters, yt as queueNoticeText, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as withFileLock } from "./host-heartbeat-4Ao_1fKu.js";
1
+ import { $ as parseCreditMultiplier, A as isEncryptedCredentialError, At as isEncryptedFieldWrapper, B as WorkBuddyCredentialRejectedError, Bt as writeFileAtomic, C as WorkBuddyCredentialStore, Ct as WORKBUDDY_APP_EXECUTABLE_ENV, D as defaultDesktopAuthDirs, Dt as fetchAtRestKeyPayload, E as defaultDesktopAuthCandidates, Et as deriveAtRestKeyId, F as workbuddyAccountId, Ft as readAtRestKey, G as forceNeutralHead, H as channelGateMemberOf, I as workbuddyOwnAuthPath, It as workbuddyAppExecutableCandidates, J as isCredentialRejectedError, K as isContextTooLongBody, L as CONTEXT_OVERFLOW_MARKER, M as legacyWorkbuddyOwnAuthPath, Mt as macosBundleExecutable, N as parseWorkBuddyAuth, Nt as macosNestedAppBundles, O as defaultDesktopAuthPath, Ot as findWorkbuddyAppExecutable, P as preferredAccountName, Pt as openEncryptedField, Q as openingEvidenceOfBody, R as CREDENTIAL_REJECTED_CODE, Rt as resolveDshHome, S as WORKBUDDY_AUTH_FILE_ENV, St as nativeModalityOf, T as authFileName, Tt as deriveAtRestKey, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, W as declaredTools, X as knownRegionOf, Y as isFirstMessageGateBody, Z as modelIdOfBody, _ as deriveCatalog, _t as pickImageModel, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as NATIVE_MODALITY_BY_MODEL_ID, c as WORKBUDDY_CONNECT_VERSION, ct as REFUSES_OFF_MODEL_IDS, dt as offDefaultFor, et as parseReasoning, f as piAiRuntimeInfo, ft as offRefused, g as applyContextBudgets, gt as WorkBuddyImageError, h as WorkBuddyCatalog, ht as DEFAULT_IMAGE_SIZE, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as isWorkbuddyBundle, k as hasEncryptedCredentialFields, l as createFileRosterSeenStore, lt as defaultOffModelIds, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, mt as withEffectiveOff, n as clearHostHeartbeat, nt as perturbChannelFingerprints, o as workbuddyHostHeartbeatPath, ot as rewriteSessionTitleModel, p as FALLBACK_WORKBUDDY_MODELS, pt as recordRefusal, q as isConversationIdGateBody, r as isHeartbeatProcessAlive, rt as prepareChatBody, s as writeHostHeartbeat, st as attachBusyLevels, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as parseUpstreamModel, ut as lastRefusalFor, v as fallbackModelsFor, vt as parseQueueAnswer, w as WorkBuddyEncryptedCredentialError, wt as clearAtRestKeyCache, x as WORKBUDDY_AUTH_FILENAME, xt as imageDefaultFor, y as withoutRouters, yt as queueNoticeText, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as withFileLock } from "./host-heartbeat-C1tVU2IN.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -85,6 +85,10 @@ import { isIP } from "node:net";
85
85
  * `supportsImages` ride along for the same reason: the upstream publishes
86
86
  * them, the saved copy is a snapshot of one moment, and a stale multiplier is
87
87
  * a wrong PRICE shown to the user.
88
+ *
89
+ * `promos` is NOT in this list — see {@link overlayLiveCapabilities} for why it
90
+ * needs a different rule (it must be able to CLEAR a saved value, which this
91
+ * loop cannot express).
88
92
  */
89
93
  const LIVE_OWNED_FIELDS = [
90
94
  "reasoning",
@@ -110,6 +114,31 @@ const LIVE_OWNED_FIELDS = [
110
114
  * and the user's effective choice is stamped over it by `withEffectiveOff`
111
115
  * afterwards. So the overlay restores the declaration, and the user's tick
112
116
  * still wins.
117
+ *
118
+ * `promos` IS LIVE-OWNED TOO, AND IS REPLACED UNCONDITIONALLY. It cannot use
119
+ * the loop above, because that loop only writes a field the live entry HAS —
120
+ * which would keep a saved promotion forever once the upstream withdrew it.
121
+ * Both directions were observed to be wrong before this rule existed:
122
+ *
123
+ * - saved WITHOUT `promos`, live WITH them → the live promotions were
124
+ * dropped entirely, so a model on a night discount drew no badge at all;
125
+ * - saved WITH old `promos`, live with new ones → the saved ones won, so a
126
+ * promotion whose hours the upstream had since changed kept advertising
127
+ * the OLD hours.
128
+ *
129
+ * The card persists `promos` into `lastCatalog` (the settings schema keeps
130
+ * unknown keys), so a user who saved the directory once had their badges
131
+ * frozen at that moment — the exact opposite of what `src/promo.ts` promises
132
+ * ("a badge is a function of the clock, not of the fetch"). The upstream's
133
+ * current answer therefore REPLACES the saved one, including when that answer
134
+ * is "no promotions at all": a badge the upstream no longer publishes must
135
+ * stop being drawn, and a standing entry with no schedule has no absolute
136
+ * window to expire it on its own.
137
+ *
138
+ * The key is DELETED rather than set to `undefined` when the live entry has
139
+ * no promotions, for the same reason `resolveRoster` deletes `busyLevel`: an
140
+ * explicit `undefined` survives `structuredClone` and the settings write
141
+ * path's strict JSON codec rejects the whole save.
113
142
  */
114
143
  function overlayLiveCapabilities(saved, live) {
115
144
  if (live === void 0) return saved;
@@ -118,9 +147,20 @@ function overlayLiveCapabilities(saved, live) {
118
147
  const value = live[field];
119
148
  if (value !== void 0) overlay[field] = value;
120
149
  }
121
- if (Object.keys(overlay).length === 0) return saved;
150
+ const livePromos = live.promos;
151
+ const savedPromos = saved.promos;
152
+ const promosDiffer = livePromos !== savedPromos && (livePromos !== void 0 || savedPromos !== void 0);
153
+ if (Object.keys(overlay).length === 0 && !promosDiffer) return saved;
154
+ if (livePromos !== void 0) {
155
+ overlay["promos"] = livePromos;
156
+ return {
157
+ ...saved,
158
+ ...overlay
159
+ };
160
+ }
161
+ const { promos: _stalePromos, ...rest } = saved;
122
162
  return {
123
- ...saved,
163
+ ...rest,
124
164
  ...overlay
125
165
  };
126
166
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-workbuddy",
3
3
  "displayName": "DSH Connect WorkBuddy",
4
4
  "description": "把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness:每次刷新按上游目录自动读取每个模型的积分倍率,支持逐模型图片输入开关、多账号切换与账号池自动换号,并提供每日签到、卡片与输入框里的胶囊积分读数等只读积分概览。",
5
- "version": "5.8.5",
5
+ "version": "5.9.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",