dsh-connect-workbuddy 4.9.3 → 4.10.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 +84 -0
- package/README.en.md +4 -3
- package/README.md +3 -2
- package/lib/bin.js +1 -1
- package/lib/client.js +248 -10
- package/lib/{host-heartbeat-BHaBzzOr.js → host-heartbeat-BQHO4oiB.js} +118 -10
- package/lib/index.d.ts +176 -1
- package/lib/index.js +223 -7
- 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-BQHO4oiB.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
|
/**
|
|
@@ -2691,10 +2731,39 @@ function failoverReasonText(failure) {
|
|
|
2691
2731
|
case "policy_reject": return "被服务端策略拒绝";
|
|
2692
2732
|
case "not_found": return "模型不可用";
|
|
2693
2733
|
case "server": return "上游异常";
|
|
2694
|
-
|
|
2734
|
+
case "client": return "请求被上游拒绝";
|
|
2735
|
+
default: return "不可用";
|
|
2695
2736
|
}
|
|
2696
2737
|
}
|
|
2697
2738
|
/**
|
|
2739
|
+
* One upstream sentence, made safe to paste into a single-line error message.
|
|
2740
|
+
*
|
|
2741
|
+
* `result.message` carries the WHOLE upstream body — for a classified failure
|
|
2742
|
+
* that is a JSON envelope 637–777 characters long whose first 400 characters are
|
|
2743
|
+
* the `code`/`requestId`/`extError` skeleton (`param: ""`, `Request: null`). A
|
|
2744
|
+
* plain `slice(0, 400)` therefore cut the one actionable sentence in half: the
|
|
2745
|
+
* reply ended mid-word at `Pleas` and the reader lost `displayTips` entirely
|
|
2746
|
+
* (issue #78).
|
|
2747
|
+
*
|
|
2748
|
+
* Two rules keep that from happening again:
|
|
2749
|
+
* - whitespace collapses to single spaces, because the body is JSON/HTML and its
|
|
2750
|
+
* newlines would otherwise render as a ragged block inside one paragraph;
|
|
2751
|
+
* - the cut prefers a sentence boundary once one exists past the halfway point,
|
|
2752
|
+
* so a clipped sentence reads as "truncated" instead of as broken prose, and
|
|
2753
|
+
* the `…` always says the text continues.
|
|
2754
|
+
*
|
|
2755
|
+
* This is the LAST resort: a body that parsed into a `detail` with `displayMsg`
|
|
2756
|
+
* or `tips` never reaches it, and those are short by construction.
|
|
2757
|
+
*/
|
|
2758
|
+
function clippedUpstreamText(text, limit) {
|
|
2759
|
+
const collapsed = text.replace(/\s+/gu, " ").trim();
|
|
2760
|
+
if (collapsed.length <= limit) return collapsed;
|
|
2761
|
+
const cut = collapsed.slice(0, limit);
|
|
2762
|
+
const stop = Math.max(cut.lastIndexOf("。"), cut.lastIndexOf("!"), cut.lastIndexOf("?"), cut.lastIndexOf(". "), cut.lastIndexOf("! "), cut.lastIndexOf("? "));
|
|
2763
|
+
if (stop >= Math.floor(limit / 2)) return `${cut.slice(0, stop + 1).trimEnd()}…`;
|
|
2764
|
+
return `${cut.trimEnd()}…`;
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2698
2767
|
* The line prepended to an answer that came from a failover account.
|
|
2699
2768
|
*
|
|
2700
2769
|
* Block-quoted and bracketed so it reads as machinery rather than as the model's
|
|
@@ -3094,11 +3163,11 @@ function createWorkBuddyShim(options) {
|
|
|
3094
3163
|
if (!result.ok) {
|
|
3095
3164
|
const attempts = triedAccountIds.length;
|
|
3096
3165
|
const note = attempts > 1 ? `(已尝试 ${attempts} 个账号)` : "";
|
|
3097
|
-
const shortfall = failoverShortfall
|
|
3166
|
+
const shortfall = failoverShortfall !== void 0 && failoverAccount !== void 0 && isFailoverWorthy(result.kind) ? await failoverShortfall().catch(() => void 0) : void 0;
|
|
3098
3167
|
const why = shortfall === void 0 ? "" : `:${shortfall}`;
|
|
3099
3168
|
if (result.kind === "policy_reject") {
|
|
3100
3169
|
const detail = result.detail;
|
|
3101
|
-
const lead = detail?.displayMsg ?? result.message
|
|
3170
|
+
const lead = detail?.displayMsg ?? clippedUpstreamText(result.message, 200);
|
|
3102
3171
|
const meta = ["服务端策略拒绝"];
|
|
3103
3172
|
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
3104
3173
|
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
@@ -3108,7 +3177,7 @@ function createWorkBuddyShim(options) {
|
|
|
3108
3177
|
}
|
|
3109
3178
|
if (result.kind === "model_busy") {
|
|
3110
3179
|
const detail = result.detail;
|
|
3111
|
-
const lead = detail?.displayMsg ?? result.message
|
|
3180
|
+
const lead = detail?.displayMsg ?? clippedUpstreamText(result.message, 200);
|
|
3112
3181
|
const meta = ["模型当前负载较高"];
|
|
3113
3182
|
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
3114
3183
|
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
@@ -3116,7 +3185,15 @@ function createWorkBuddyShim(options) {
|
|
|
3116
3185
|
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, message, hostErrorCode(result.kind));
|
|
3117
3186
|
return;
|
|
3118
3187
|
}
|
|
3119
|
-
|
|
3188
|
+
const detail = result.detail;
|
|
3189
|
+
const meta = [];
|
|
3190
|
+
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
3191
|
+
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
3192
|
+
const stamp = meta.length === 0 ? "" : `(${meta.join(",")})`;
|
|
3193
|
+
const receipt = detail?.tips ?? detail?.displayMsg ?? clippedUpstreamText(result.message, 400);
|
|
3194
|
+
const advice = result.kind === "client" ? "该请求被上游按参数拒绝:与网络、账号、额度都无关,换账号也不会改变结果,请按上面的提示调整后重试" : shortfall === void 0 ? "" : "建议换一个模型继续,或稍后等待该账号的额度恢复";
|
|
3195
|
+
const body = advice === "" ? receipt : `${receipt.replace(/[。.]$/u, "")}。${advice}`;
|
|
3196
|
+
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy 上游失败:${failoverReasonText(result)}(http ${result.status})${note}${why}${stamp}。${body}`, hostErrorCode(result.kind));
|
|
3120
3197
|
return;
|
|
3121
3198
|
}
|
|
3122
3199
|
res.writeHead(200, {
|
|
@@ -4735,6 +4812,15 @@ const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
|
|
|
4735
4812
|
* per-account figures.
|
|
4736
4813
|
*/
|
|
4737
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";
|
|
4738
4824
|
/** Plugin-owned live model refresh endpoint. */
|
|
4739
4825
|
const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
|
|
4740
4826
|
/** Plugin-owned local account rescan endpoint. */
|
|
@@ -5329,6 +5415,7 @@ async function workBuddyWebPool(deps, region, fast = false) {
|
|
|
5329
5415
|
const rankedIds = new Set(ranked.map((row) => row.account.id));
|
|
5330
5416
|
const others = await pool.otherAccounts?.(region) ?? [];
|
|
5331
5417
|
const checkedIn = fast ? pool.cachedCheckedInToday?.(region) ?? {} : await pool.checkedInToday?.(region).catch(() => ({})) ?? {};
|
|
5418
|
+
const claimable = fast ? pool.cachedBenefits?.(region) ?? {} : await pool.benefits?.(region).catch(() => ({})) ?? {};
|
|
5332
5419
|
const rows = [...ranked.map((row) => ({
|
|
5333
5420
|
account: row.account,
|
|
5334
5421
|
member: true,
|
|
@@ -5376,7 +5463,18 @@ async function workBuddyWebPool(deps, region, fast = false) {
|
|
|
5376
5463
|
...row?.excludedBy === void 0 ? {} : { excludedBy: row.excludedBy },
|
|
5377
5464
|
current: account.id === currentAccountId,
|
|
5378
5465
|
member,
|
|
5379
|
-
...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
|
+
})()
|
|
5380
5478
|
};
|
|
5381
5479
|
});
|
|
5382
5480
|
return {
|
|
@@ -5506,6 +5604,43 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
5506
5604
|
}
|
|
5507
5605
|
}
|
|
5508
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
|
+
});
|
|
5509
5644
|
const disposeRefresh = ctx.webServer.register({
|
|
5510
5645
|
kind: "exact",
|
|
5511
5646
|
path: WORKBUDDY_MODELS_REFRESH_PATH,
|
|
@@ -6231,6 +6366,16 @@ function apply(ctx, config) {
|
|
|
6231
6366
|
* immediately instead of waiting out the TTL.
|
|
6232
6367
|
*/
|
|
6233
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 = {};
|
|
6234
6379
|
const settingsNs = settingsNamespaceOf(ctx);
|
|
6235
6380
|
const stacks = {};
|
|
6236
6381
|
for (const region of REGION_KEYS) {
|
|
@@ -6616,6 +6761,77 @@ function apply(ctx, config) {
|
|
|
6616
6761
|
}
|
|
6617
6762
|
return cached;
|
|
6618
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
|
+
},
|
|
6619
6835
|
async currentAccountId(region) {
|
|
6620
6836
|
await applyPoolSelection(region).catch(() => void 0);
|
|
6621
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.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|