dsh-connect-workbuddy 4.9.4 → 4.10.1
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 +80 -0
- package/README.en.md +4 -3
- package/README.md +3 -2
- package/docs/WINDOWS.md +5 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +248 -10
- package/lib/{host-heartbeat-BpArQi1Q.js → host-heartbeat-Cq4_pJ_l.js} +211 -21
- package/lib/index.d.ts +184 -10
- package/lib/index.js +181 -2
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-
|
|
1
|
+
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-Cq4_pJ_l.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -78,6 +78,46 @@ async function resolveCredentialRecovery(options) {
|
|
|
78
78
|
};
|
|
79
79
|
return { reloginRequired: false };
|
|
80
80
|
}
|
|
81
|
+
/** How the display copy names each campaign. */
|
|
82
|
+
const WORKBUDDY_BENEFIT_TITLES = {
|
|
83
|
+
compensation: "模型服务异常补偿",
|
|
84
|
+
gift: "新人首月赠送"
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Resolve one campaign against the clock.
|
|
88
|
+
*
|
|
89
|
+
* `active` is the gateway's own window verdict and is trusted as-is; the clock is
|
|
90
|
+
* consulted only for the countdown, never to override `active`. An account whose
|
|
91
|
+
* clock is skewed therefore still renders the upstream's answer rather than a
|
|
92
|
+
* locally-invented one.
|
|
93
|
+
*/
|
|
94
|
+
function benefitOf(benefit, nowMs) {
|
|
95
|
+
const daysLeft = benefit.endsAtMs === void 0 ? void 0 : Math.max(0, Math.ceil((benefit.endsAtMs - nowMs) / 864e5));
|
|
96
|
+
const state = benefit.claimed ? "claimed" : benefit.active ? "claimable" : "expired";
|
|
97
|
+
return {
|
|
98
|
+
kind: benefit.kind,
|
|
99
|
+
state,
|
|
100
|
+
creditNum: benefit.creditNum,
|
|
101
|
+
...benefit.endsAtMs === void 0 ? {} : { endsAtMs: benefit.endsAtMs },
|
|
102
|
+
...daysLeft === void 0 ? {} : { daysLeft },
|
|
103
|
+
urgent: state === "claimable" && daysLeft !== void 0 && daysLeft <= 3
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The claimable campaigns of one account, urgent first.
|
|
108
|
+
*
|
|
109
|
+
* Ordered by `daysLeft` ascending so the soonest-expiring offer leads — the whole
|
|
110
|
+
* point of surfacing this is that a 3-day window otherwise lapses unnoticed. A
|
|
111
|
+
* campaign with no stated end sorts last, because "no deadline" is never more
|
|
112
|
+
* urgent than a deadline.
|
|
113
|
+
*
|
|
114
|
+
* `claimed` and `expired` campaigns are dropped here, not in the renderer: the
|
|
115
|
+
* user asked for them to be ABSENT, and encoding that in one tested place beats
|
|
116
|
+
* asking every caller to remember to filter.
|
|
117
|
+
*/
|
|
118
|
+
function claimableBenefits(benefits, nowMs) {
|
|
119
|
+
return benefits.map((benefit) => benefitOf(benefit, nowMs)).filter((view) => view.state === "claimable").sort((left, right) => (left.daysLeft ?? 1e6) - (right.daysLeft ?? 1e6));
|
|
120
|
+
}
|
|
81
121
|
//#endregion
|
|
82
122
|
//#region src/adapter.ts
|
|
83
123
|
/**
|
|
@@ -4772,6 +4812,15 @@ const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
|
|
|
4772
4812
|
* per-account figures.
|
|
4773
4813
|
*/
|
|
4774
4814
|
const WORKBUDDY_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-workbuddy/account-credits";
|
|
4815
|
+
/**
|
|
4816
|
+
* Plugin-owned benefit claim endpoint: POST + loopback, ONE account per call.
|
|
4817
|
+
*
|
|
4818
|
+
* A credit grant is a write on a real account, so it carries the same guards as
|
|
4819
|
+
* check-in, plus `?accountId=` because the pool can hold accounts this plugin
|
|
4820
|
+
* did not pick. There is deliberately no batch action here — see
|
|
4821
|
+
* `claimBenefit` and the route handler.
|
|
4822
|
+
*/
|
|
4823
|
+
const WORKBUDDY_BENEFIT_CLAIM_PATH = "/plugins/dsh-connect-workbuddy/benefit/claim";
|
|
4775
4824
|
/** Plugin-owned live model refresh endpoint. */
|
|
4776
4825
|
const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
|
|
4777
4826
|
/** Plugin-owned local account rescan endpoint. */
|
|
@@ -5366,6 +5415,7 @@ async function workBuddyWebPool(deps, region, fast = false) {
|
|
|
5366
5415
|
const rankedIds = new Set(ranked.map((row) => row.account.id));
|
|
5367
5416
|
const others = await pool.otherAccounts?.(region) ?? [];
|
|
5368
5417
|
const checkedIn = fast ? pool.cachedCheckedInToday?.(region) ?? {} : await pool.checkedInToday?.(region).catch(() => ({})) ?? {};
|
|
5418
|
+
const claimable = fast ? pool.cachedBenefits?.(region) ?? {} : await pool.benefits?.(region).catch(() => ({})) ?? {};
|
|
5369
5419
|
const rows = [...ranked.map((row) => ({
|
|
5370
5420
|
account: row.account,
|
|
5371
5421
|
member: true,
|
|
@@ -5413,7 +5463,18 @@ async function workBuddyWebPool(deps, region, fast = false) {
|
|
|
5413
5463
|
...row?.excludedBy === void 0 ? {} : { excludedBy: row.excludedBy },
|
|
5414
5464
|
current: account.id === currentAccountId,
|
|
5415
5465
|
member,
|
|
5416
|
-
...checkedIn[account.id] === void 0 ? {} : { checkedInToday: checkedIn[account.id] }
|
|
5466
|
+
...checkedIn[account.id] === void 0 ? {} : { checkedInToday: checkedIn[account.id] },
|
|
5467
|
+
...(() => {
|
|
5468
|
+
const views = claimable[account.id];
|
|
5469
|
+
if (views === void 0 || views.length === 0) return {};
|
|
5470
|
+
return { benefits: views.map((view) => ({
|
|
5471
|
+
kind: view.kind,
|
|
5472
|
+
title: WORKBUDDY_BENEFIT_TITLES[view.kind],
|
|
5473
|
+
creditNum: view.creditNum,
|
|
5474
|
+
...view.daysLeft === void 0 ? {} : { daysLeft: view.daysLeft },
|
|
5475
|
+
urgent: view.urgent
|
|
5476
|
+
})) };
|
|
5477
|
+
})()
|
|
5417
5478
|
};
|
|
5418
5479
|
});
|
|
5419
5480
|
return {
|
|
@@ -5543,6 +5604,43 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
5543
5604
|
}
|
|
5544
5605
|
}
|
|
5545
5606
|
});
|
|
5607
|
+
ctx.webServer.register({
|
|
5608
|
+
kind: "exact",
|
|
5609
|
+
path: WORKBUDDY_BENEFIT_CLAIM_PATH,
|
|
5610
|
+
handler: async (req, res) => {
|
|
5611
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5612
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
5613
|
+
const region = requestRegion(req, res);
|
|
5614
|
+
if (region === void 0) return;
|
|
5615
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5616
|
+
const accountId = url.searchParams.get(WORKBUDDY_POOL_ACCOUNT_PARAM);
|
|
5617
|
+
const kind = url.searchParams.get("kind");
|
|
5618
|
+
if (accountId === null || accountId === "") return json(res, 400, { error: "accountId is required" });
|
|
5619
|
+
if (kind !== "compensation" && kind !== "gift") return json(res, 400, { error: "kind must be compensation or gift" });
|
|
5620
|
+
try {
|
|
5621
|
+
const credential = await deps.store(region).credentialFor(accountId);
|
|
5622
|
+
if (credential === void 0) return json(res, 404, { error: "no credential for this account" });
|
|
5623
|
+
const target = (await deps.client.fetchBenefits(credential)).find((benefit) => benefit.kind === kind);
|
|
5624
|
+
if (target === void 0) return json(res, 404, { error: "benefit not available on this account" });
|
|
5625
|
+
if (target.claimed) return json(res, 409, {
|
|
5626
|
+
error: "benefit already claimed",
|
|
5627
|
+
benefit: target
|
|
5628
|
+
});
|
|
5629
|
+
if (!target.active) return json(res, 409, {
|
|
5630
|
+
error: "benefit activity is not active",
|
|
5631
|
+
benefit: target
|
|
5632
|
+
});
|
|
5633
|
+
await deps.client.claimBenefit(credential, kind);
|
|
5634
|
+
deps.pool?.invalidateBenefit?.(region, accountId);
|
|
5635
|
+
json(res, 200, {
|
|
5636
|
+
claimed: true,
|
|
5637
|
+
benefit: (await deps.client.fetchBenefits(credential)).find((benefit) => benefit.kind === kind) ?? target
|
|
5638
|
+
});
|
|
5639
|
+
} catch (error) {
|
|
5640
|
+
json(res, 500, { error: safeMessage(error) });
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
});
|
|
5546
5644
|
const disposeRefresh = ctx.webServer.register({
|
|
5547
5645
|
kind: "exact",
|
|
5548
5646
|
path: WORKBUDDY_MODELS_REFRESH_PATH,
|
|
@@ -6268,6 +6366,16 @@ function apply(ctx, config) {
|
|
|
6268
6366
|
* immediately instead of waiting out the TTL.
|
|
6269
6367
|
*/
|
|
6270
6368
|
const checkinStateCache = {};
|
|
6369
|
+
/**
|
|
6370
|
+
* Per-region, per-account benefit readings (issue #79).
|
|
6371
|
+
*
|
|
6372
|
+
* Same shape and same reasoning as `checkinStateCache`: claim state changes
|
|
6373
|
+
* when a human acts on the web page or in the desktop app, not spontaneously,
|
|
6374
|
+
* so a per-poll read of two endpoints per account would be thousands of
|
|
6375
|
+
* requests a day to learn nothing. Coarser than the check-in TTL because a
|
|
6376
|
+
* benefit window is measured in days, not hours.
|
|
6377
|
+
*/
|
|
6378
|
+
const benefitStateCache = {};
|
|
6271
6379
|
const settingsNs = settingsNamespaceOf(ctx);
|
|
6272
6380
|
const stacks = {};
|
|
6273
6381
|
for (const region of REGION_KEYS) {
|
|
@@ -6653,6 +6761,77 @@ function apply(ctx, config) {
|
|
|
6653
6761
|
}
|
|
6654
6762
|
return cached;
|
|
6655
6763
|
},
|
|
6764
|
+
/**
|
|
6765
|
+
* Claimable promotional benefits per account (issue #79).
|
|
6766
|
+
*
|
|
6767
|
+
* Only campaigns the account can STILL claim are returned — a claimed or
|
|
6768
|
+
* lapsed one is absent, which is what the issue asked for. Absent means
|
|
6769
|
+
* the card shows no benefit line at all.
|
|
6770
|
+
*
|
|
6771
|
+
* Two status reads per account, so it is cached for `BENEFIT_CACHE_MS` and
|
|
6772
|
+
* skipped entirely when the pool is off (same reasoning as
|
|
6773
|
+
* `checkedInToday`: no table, no request).
|
|
6774
|
+
*/
|
|
6775
|
+
async benefits(region) {
|
|
6776
|
+
if (!poolPreferencesOf(current(), region).enabled) return {};
|
|
6777
|
+
const store = stacks[region].store;
|
|
6778
|
+
const accounts = await store.accounts();
|
|
6779
|
+
const now = Date.now();
|
|
6780
|
+
const entries = await Promise.all(accounts.map(async (account) => {
|
|
6781
|
+
const cached = benefitStateCache[region]?.[account.id];
|
|
6782
|
+
if (cached !== void 0 && now - cached.atMs < 18e5) return [account.id, cached.views];
|
|
6783
|
+
const credential = await store.credentialFor(account.id).catch(() => void 0);
|
|
6784
|
+
if (credential === void 0) return void 0;
|
|
6785
|
+
const read = await client.fetchBenefits(credential).catch(() => void 0);
|
|
6786
|
+
if (read === void 0) return void 0;
|
|
6787
|
+
const views = claimableBenefits(read, now);
|
|
6788
|
+
benefitStateCache[region] = {
|
|
6789
|
+
...benefitStateCache[region],
|
|
6790
|
+
[account.id]: {
|
|
6791
|
+
views,
|
|
6792
|
+
atMs: now
|
|
6793
|
+
}
|
|
6794
|
+
};
|
|
6795
|
+
return [account.id, views];
|
|
6796
|
+
}));
|
|
6797
|
+
return Object.fromEntries(entries.filter((entry) => entry !== void 0));
|
|
6798
|
+
},
|
|
6799
|
+
/**
|
|
6800
|
+
* Claimable benefits from the readings already taken this process.
|
|
6801
|
+
*
|
|
6802
|
+
* The first-paint counterpart of `benefits`, mirroring
|
|
6803
|
+
* `cachedCheckedInToday`: no request, so painting never blocks on two
|
|
6804
|
+
* calls per account. An unmeasured account is absent, not empty.
|
|
6805
|
+
*/
|
|
6806
|
+
cachedBenefits(region) {
|
|
6807
|
+
if (!poolPreferencesOf(current(), region).enabled) return {};
|
|
6808
|
+
const now = Date.now();
|
|
6809
|
+
const state = benefitStateCache[region] ?? {};
|
|
6810
|
+
const cached = {};
|
|
6811
|
+
for (const [accountId, entry] of Object.entries(state)) {
|
|
6812
|
+
if (now - entry.atMs >= 18e5) continue;
|
|
6813
|
+
cached[accountId] = entry.views;
|
|
6814
|
+
}
|
|
6815
|
+
return cached;
|
|
6816
|
+
},
|
|
6817
|
+
/**
|
|
6818
|
+
* Drop one account's cached benefit reading (issue #79).
|
|
6819
|
+
*
|
|
6820
|
+
* The claim route writes upstream itself, so this cache is the ONLY thing
|
|
6821
|
+
* that can keep answering "claimable" after a successful claim — and it was
|
|
6822
|
+
* never invalidated: the credits went up, the 领取 pill stayed for up to 30
|
|
6823
|
+
* minutes, and the claim looked like it had silently failed. That is the
|
|
6824
|
+
* exact symptom the user reported. `checkin` already deletes its entries
|
|
6825
|
+
* after a batch; this is the same rule for the claim.
|
|
6826
|
+
*
|
|
6827
|
+
* Deleting (rather than overwriting with the post-claim read) keeps the
|
|
6828
|
+
* ownership where it belongs: the route already made the authoritative
|
|
6829
|
+
* re-read, and the next `benefits()` refills this from live data.
|
|
6830
|
+
*/
|
|
6831
|
+
invalidateBenefit(region, accountId) {
|
|
6832
|
+
const cached = benefitStateCache[region];
|
|
6833
|
+
if (cached !== void 0) delete cached[accountId];
|
|
6834
|
+
},
|
|
6656
6835
|
async currentAccountId(region) {
|
|
6657
6836
|
await applyPoolSelection(region).catch(() => void 0);
|
|
6658
6837
|
const credential = await stacks[region].store.current().catch(() => void 0);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
|
-
"description": "把本机登录的 WorkBuddy
|
|
5
|
-
"version": "4.
|
|
4
|
+
"description": "把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness:每次刷新按上游目录自动读取每个模型的积分倍率,支持逐模型图片输入开关、多账号切换与账号池自动换号,并提供每日签到、卡片与输入框里的胶囊积分读数等只读积分概览。",
|
|
5
|
+
"version": "4.10.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|