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.
- package/CHANGELOG.md +37 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +693 -589
- package/lib/{host-heartbeat-4Ao_1fKu.js → host-heartbeat-C1tVU2IN.js} +52 -9
- package/lib/index.js +43 -3
- package/package.json +1 -1
|
@@ -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
|
-
/**
|
|
1532
|
-
|
|
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)
|
|
1541
|
-
|
|
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
|
|
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
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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
|
-
...
|
|
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.
|
|
5
|
+
"version": "5.9.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|