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/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-BpArQi1Q.js";
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 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
5
- "version": "4.9.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",