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
|
@@ -863,21 +863,93 @@ function fetchAtRestKeyPayload(executable) {
|
|
|
863
863
|
let cachedKey;
|
|
864
864
|
let inflightKey;
|
|
865
865
|
/**
|
|
866
|
+
* When the last FAILED lookup finished, epoch ms; 0 means "no recent failure".
|
|
867
|
+
*
|
|
868
|
+
* Issue #77: only a SUCCESS was remembered, so a machine where the key cannot
|
|
869
|
+
* be obtained re-ran the whole discovery on every single probe. `scanAll()`
|
|
870
|
+
* probes one auth file at a time, and a usage request scans several times, so
|
|
871
|
+
* one page open re-ran the discovery dozens of times — each run being three
|
|
872
|
+
* SYNCHRONOUS `reg query` child processes on the host's main thread when the
|
|
873
|
+
* desktop app is not installed (measured 15.3 s of a 19.1 s request, 135
|
|
874
|
+
* `reg.exe` spawns). A failure is therefore remembered for
|
|
875
|
+
* {@link AT_REST_KEY_FAILURE_TTL_MS}: the answer cannot change within one page
|
|
876
|
+
* open, while a user who installs or launches the app still sees it pick up
|
|
877
|
+
* within that window instead of only after a restart.
|
|
878
|
+
*/
|
|
879
|
+
let failedKeyAtMs = 0;
|
|
880
|
+
/** The discovery result behind the current key (or failure); see {@link discoverAppExecutable}. */
|
|
881
|
+
let cachedAppExecutable;
|
|
882
|
+
/**
|
|
883
|
+
* How long a failed key lookup is remembered. Long enough to collapse the
|
|
884
|
+
* dozens of probes one request makes, short enough that installing (or
|
|
885
|
+
* launching) the desktop app takes effect without a restart.
|
|
886
|
+
*/
|
|
887
|
+
const AT_REST_KEY_FAILURE_TTL_MS = 6e4;
|
|
888
|
+
/**
|
|
889
|
+
* How long a NEGATIVE executable discovery is remembered.
|
|
890
|
+
*
|
|
891
|
+
* The negative answer is the expensive one: every candidate path misses, then
|
|
892
|
+
* the Windows fallback shells out to `reg query` once per uninstall hive. The
|
|
893
|
+
* positive answer is cached for the process lifetime instead — its path is
|
|
894
|
+
* retried with `existsSync` on every call anyway, so an app that was moved or
|
|
895
|
+
* removed still falls through to a fresh discovery.
|
|
896
|
+
*/
|
|
897
|
+
const APP_EXECUTABLE_MISS_TTL_MS = 6e4;
|
|
898
|
+
/**
|
|
899
|
+
* Locate the desktop executable through {@link findWorkbuddyAppExecutable},
|
|
900
|
+
* remembering a MISS for {@link APP_EXECUTABLE_MISS_TTL_MS}.
|
|
901
|
+
*
|
|
902
|
+
* Only this module's own lookup is cached, and only when it goes through the
|
|
903
|
+
* real seams: `findWorkbuddyAppExecutable` stays a pure function of its
|
|
904
|
+
* arguments for `doctor` and for the injected-platform tests, which must keep
|
|
905
|
+
* seeing the machine as it is right now.
|
|
906
|
+
*/
|
|
907
|
+
function discoverAppExecutable() {
|
|
908
|
+
const now = Date.now();
|
|
909
|
+
if (cachedAppExecutable !== void 0) {
|
|
910
|
+
if (now - cachedAppExecutable.foundAtMs < APP_EXECUTABLE_MISS_TTL_MS) return cachedAppExecutable.executable;
|
|
911
|
+
cachedAppExecutable = void 0;
|
|
912
|
+
}
|
|
913
|
+
const executable = findWorkbuddyAppExecutable();
|
|
914
|
+
if (executable === void 0) {
|
|
915
|
+
cachedAppExecutable = {
|
|
916
|
+
executable: "",
|
|
917
|
+
foundAtMs: now
|
|
918
|
+
};
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
cachedAppExecutable = {
|
|
922
|
+
executable,
|
|
923
|
+
foundAtMs: Number.POSITIVE_INFINITY
|
|
924
|
+
};
|
|
925
|
+
return executable;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
866
928
|
* The desktop app's at-rest field key, or undefined when it cannot be obtained
|
|
867
929
|
* (app not installed, an older build without the native module, or a future
|
|
868
|
-
* build that rotates the payload).
|
|
869
|
-
*
|
|
870
|
-
*
|
|
930
|
+
* build that rotates the payload).
|
|
931
|
+
*
|
|
932
|
+
* Both outcomes are cached: a success for the process lifetime (the app is
|
|
933
|
+
* spawned at most once), a failure for {@link AT_REST_KEY_FAILURE_TTL_MS}. The
|
|
934
|
+
* failure TTL is what keeps a machine WITHOUT the app from paying the
|
|
935
|
+
* synchronous registry probe on every credential file — see {@link failedKeyAtMs}.
|
|
871
936
|
*/
|
|
872
937
|
function readAtRestKey() {
|
|
873
938
|
if (cachedKey !== void 0) return Promise.resolve(cachedKey);
|
|
874
|
-
inflightKey
|
|
875
|
-
|
|
939
|
+
if (inflightKey !== void 0) return inflightKey;
|
|
940
|
+
if (failedKeyAtMs !== 0 && Date.now() - failedKeyAtMs < AT_REST_KEY_FAILURE_TTL_MS) return Promise.resolve(void 0);
|
|
941
|
+
inflightKey = (async () => {
|
|
942
|
+
const executable = discoverAppExecutable();
|
|
876
943
|
if (executable === void 0) return void 0;
|
|
877
|
-
|
|
944
|
+
return deriveAtRestKey(await fetchAtRestKeyPayload(executable));
|
|
945
|
+
})().then((key) => {
|
|
878
946
|
cachedKey = key;
|
|
947
|
+
failedKeyAtMs = key === void 0 ? Date.now() : 0;
|
|
879
948
|
return key;
|
|
880
|
-
}
|
|
949
|
+
}, (error) => {
|
|
950
|
+
failedKeyAtMs = Date.now();
|
|
951
|
+
throw error;
|
|
952
|
+
}).finally(() => {
|
|
881
953
|
inflightKey = void 0;
|
|
882
954
|
});
|
|
883
955
|
return inflightKey;
|
|
@@ -886,6 +958,8 @@ function readAtRestKey() {
|
|
|
886
958
|
function clearAtRestKeyCache() {
|
|
887
959
|
cachedKey = void 0;
|
|
888
960
|
inflightKey = void 0;
|
|
961
|
+
failedKeyAtMs = 0;
|
|
962
|
+
cachedAppExecutable = void 0;
|
|
889
963
|
}
|
|
890
964
|
//#endregion
|
|
891
965
|
//#region src/upstream.ts
|
|
@@ -917,6 +991,19 @@ function clearAtRestKeyCache() {
|
|
|
917
991
|
*
|
|
918
992
|
* @module dsh-connect-workbuddy/upstream
|
|
919
993
|
*/
|
|
994
|
+
/**
|
|
995
|
+
* The write half of each campaign: the endpoint that actually grants the
|
|
996
|
+
* credits.
|
|
997
|
+
*
|
|
998
|
+
* Kept beside the status paths because they must move together: the pair is
|
|
999
|
+
* what the 「套餐与积分」 page itself uses (`xp`/`vp` in its bundle), and a
|
|
1000
|
+
* campaign whose status is read from one path can only be claimed through its
|
|
1001
|
+
* matching claim path. Note the same absence of a `/v2` prefix.
|
|
1002
|
+
*/
|
|
1003
|
+
const BENEFIT_CLAIM_PATHS = {
|
|
1004
|
+
compensation: "/billing/meter/claim-compensation",
|
|
1005
|
+
gift: "/billing/meter/claim-gift"
|
|
1006
|
+
};
|
|
920
1007
|
const CN_CHAT_BASE = "https://copilot.tencent.com";
|
|
921
1008
|
const CN_BILLING_BASE = "https://www.codebuddy.cn";
|
|
922
1009
|
const GLOBAL_BASE = "https://www.workbuddy.ai";
|
|
@@ -1921,6 +2008,90 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1921
2008
|
...typeof data["claim_button_text"] === "string" && data["claim_button_text"] !== "" ? { claimButtonText: data["claim_button_text"] } : {}
|
|
1922
2009
|
};
|
|
1923
2010
|
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Read every promotional benefit this account can still see, WITHOUT claiming
|
|
2013
|
+
* anything.
|
|
2014
|
+
*
|
|
2015
|
+
* The campaign list is closed and small on purpose: the gateway exposes one
|
|
2016
|
+
* status endpoint per campaign, so "discover benefits" is a fixed fan-out, not
|
|
2017
|
+
* a search. A new campaign type means a new `/billing/meter/*-status` endpoint
|
|
2018
|
+
* added here — the plugin never guesses at undocumented ones.
|
|
2019
|
+
*
|
|
2020
|
+
* Every endpoint is read in parallel because they are independent, and a
|
|
2021
|
+
* failure is per-endpoint: one campaign answering 5xx must not blank out the
|
|
2022
|
+
* other. The caller gets only what was actually read.
|
|
2023
|
+
*
|
|
2024
|
+
* Note the paths carry NO `/v2` prefix — these are not the `/v2/billing/meter`
|
|
2025
|
+
* family the credit and check-in calls use. Probing `/v2/...` answers 404, which
|
|
2026
|
+
* is how this distinction was found.
|
|
2027
|
+
*/
|
|
2028
|
+
async fetchBenefits(credential) {
|
|
2029
|
+
const read = async (kind, path) => {
|
|
2030
|
+
try {
|
|
2031
|
+
return await this.readBenefitStatus(kind, credential, path);
|
|
2032
|
+
} catch {
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
const [compensation, gift] = await Promise.all([read("compensation", "/billing/meter/compensation-status"), read("gift", "/billing/meter/check-gift-claimed")]);
|
|
2037
|
+
return [compensation, gift].filter((benefit) => benefit !== void 0);
|
|
2038
|
+
}
|
|
2039
|
+
/** One benefit status endpoint. Throws on anything that is not a clean `code: 0`. */
|
|
2040
|
+
async readBenefitStatus(kind, credential, path) {
|
|
2041
|
+
const response = await fetch(`${billingBase(credential)}${path}`, {
|
|
2042
|
+
method: "POST",
|
|
2043
|
+
headers: billingHeaders(credential),
|
|
2044
|
+
body: "{}",
|
|
2045
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
2046
|
+
});
|
|
2047
|
+
const envelope = await readEnvelope(response);
|
|
2048
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
2049
|
+
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
2050
|
+
const numberField = (key) => typeof data[key] === "number" ? data[key] : 0;
|
|
2051
|
+
/** The gateway states times as `YYYY-MM-DD HH:MM:SS` in local time. */
|
|
2052
|
+
const timeField = (key) => {
|
|
2053
|
+
const raw = data[key];
|
|
2054
|
+
if (typeof raw !== "string" || raw.trim() === "") return void 0;
|
|
2055
|
+
const parsed = Date.parse(raw.replace(" ", "T"));
|
|
2056
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
2057
|
+
};
|
|
2058
|
+
const endsAtMs = timeField("end_time");
|
|
2059
|
+
const startsAtMs = timeField("start_time");
|
|
2060
|
+
const claimedAtMs = timeField("claimed_at");
|
|
2061
|
+
return {
|
|
2062
|
+
kind,
|
|
2063
|
+
active: data["active"] === true,
|
|
2064
|
+
claimed: data["claimed"] === true,
|
|
2065
|
+
creditNum: numberField("credit_num"),
|
|
2066
|
+
...endsAtMs === void 0 ? {} : { endsAtMs },
|
|
2067
|
+
...startsAtMs === void 0 ? {} : { startsAtMs },
|
|
2068
|
+
...claimedAtMs === void 0 ? {} : { claimedAtMs }
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Claim one benefit campaign on ONE account (issue #79).
|
|
2073
|
+
*
|
|
2074
|
+
* This is a real credit grant on a real account, so it takes no account
|
|
2075
|
+
* argument other than the credential it is handed: the caller decides which
|
|
2076
|
+
* single account, and there is deliberately no batch form of this method. The
|
|
2077
|
+
* browser route is the only caller, and it guards the mutation with the same
|
|
2078
|
+
* POST + loopback checks the check-in route carries.
|
|
2079
|
+
*
|
|
2080
|
+
* The response is not parsed: the documented envelope of these two endpoints
|
|
2081
|
+
* is an empty success, and the caller re-reads the status afterwards. That
|
|
2082
|
+
* re-read, not this answer, is what the user is shown — which is why a claim
|
|
2083
|
+
* that silently did nothing still cannot leave the card saying 待领取.
|
|
2084
|
+
*/
|
|
2085
|
+
async claimBenefit(credential, kind) {
|
|
2086
|
+
const response = await fetch(`${billingBase(credential)}${BENEFIT_CLAIM_PATHS[kind]}`, {
|
|
2087
|
+
method: "POST",
|
|
2088
|
+
headers: billingHeaders(credential),
|
|
2089
|
+
body: "{}",
|
|
2090
|
+
signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
|
|
2091
|
+
});
|
|
2092
|
+
const envelope = await readEnvelope(response);
|
|
2093
|
+
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
2094
|
+
}
|
|
1924
2095
|
/** Claim today's check-in reward. The browser route guards this mutation. */
|
|
1925
2096
|
async claimDailyCheckin(credential) {
|
|
1926
2097
|
const response = await fetch(`${billingBase(credential)}/v2/billing/meter/daily-checkin`, {
|
|
@@ -2766,8 +2937,17 @@ var WorkBuddyCredentialStore = class {
|
|
|
2766
2937
|
* Undefined means "no scan in flight and nothing cached".
|
|
2767
2938
|
*/
|
|
2768
2939
|
scanInflight;
|
|
2769
|
-
/**
|
|
2770
|
-
|
|
2940
|
+
/**
|
|
2941
|
+
* When the cached scan FINISHED, epoch ms; 0 means "not finished yet".
|
|
2942
|
+
*
|
|
2943
|
+
* Deliberately the finish time, not the start time (issue #77). The window
|
|
2944
|
+
* used to be measured from the START, so a scan that took longer than the
|
|
2945
|
+
* window was already outside it the moment it landed: the very next `readAll`
|
|
2946
|
+
* started a fresh scan, and the sharing this cache exists for never happened.
|
|
2947
|
+
* On the machine in that report one scan took 1.4–2.1 s against a 1000 ms
|
|
2948
|
+
* window, so every one of a page open's ~6 reads re-scanned the filesystem.
|
|
2949
|
+
*/
|
|
2950
|
+
scanFinishedAtMs = 0;
|
|
2771
2951
|
/**
|
|
2772
2952
|
* A RUNTIME account override from the pool's rotation, or undefined.
|
|
2773
2953
|
*
|
|
@@ -2924,28 +3104,38 @@ var WorkBuddyCredentialStore = class {
|
|
|
2924
3104
|
/**
|
|
2925
3105
|
* The account list, sharing one scan across concurrent and repeated reads.
|
|
2926
3106
|
*
|
|
2927
|
-
* See {@link scanInflight}: the whole performance fix lives in these
|
|
2928
|
-
*
|
|
2929
|
-
* `
|
|
2930
|
-
*
|
|
3107
|
+
* See {@link scanInflight}: the whole performance fix lives in these lines.
|
|
3108
|
+
* Every caller keeps its OWN semantics (`current`, `accounts`, `status`,
|
|
3109
|
+
* `selectionLost`), because this returns the same array they used to build
|
|
3110
|
+
* themselves — only the filesystem work is shared.
|
|
3111
|
+
*
|
|
3112
|
+
* The window runs from the FINISH time ({@link scanFinishedAtMs}), so a scan
|
|
3113
|
+
* slower than the window still serves the reads that follow it. A scan in
|
|
3114
|
+
* flight is always joined, however long it has been running.
|
|
2931
3115
|
*
|
|
2932
3116
|
* A failed scan is NOT cached: the rejection clears the slot so the next read
|
|
2933
3117
|
* retries, rather than pinning every later caller to one transient failure.
|
|
2934
3118
|
*/
|
|
2935
3119
|
async readAll() {
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
this.
|
|
2940
|
-
|
|
3120
|
+
if (this.scanInflight !== void 0) {
|
|
3121
|
+
if (this.scanFinishedAtMs === 0 || Date.now() - this.scanFinishedAtMs < SCAN_SHARE_WINDOW_MS) return this.scanInflight;
|
|
3122
|
+
}
|
|
3123
|
+
this.scanFinishedAtMs = 0;
|
|
3124
|
+
const scan = this.scanAll().then((credentials) => {
|
|
3125
|
+
this.scanFinishedAtMs = Date.now();
|
|
3126
|
+
return credentials;
|
|
3127
|
+
}, (error) => {
|
|
3128
|
+
if (this.scanInflight === scan) this.scanInflight = void 0;
|
|
3129
|
+
this.scanFinishedAtMs = 0;
|
|
2941
3130
|
throw error;
|
|
2942
3131
|
});
|
|
2943
|
-
|
|
3132
|
+
this.scanInflight = scan;
|
|
3133
|
+
return scan;
|
|
2944
3134
|
}
|
|
2945
3135
|
/** Drop the shared scan so the next read re-reads the filesystem. */
|
|
2946
3136
|
invalidateScan() {
|
|
2947
3137
|
this.scanInflight = void 0;
|
|
2948
|
-
this.
|
|
3138
|
+
this.scanFinishedAtMs = 0;
|
|
2949
3139
|
}
|
|
2950
3140
|
/** The one real scan: read every candidate file, then merge the vault. */
|
|
2951
3141
|
async scanAll() {
|
|
@@ -3977,7 +4167,7 @@ function generationOf(version) {
|
|
|
3977
4167
|
//#endregion
|
|
3978
4168
|
//#region src/version.ts
|
|
3979
4169
|
/** The npm package version this build was produced from. */
|
|
3980
|
-
const WORKBUDDY_CONNECT_VERSION = "4.
|
|
4170
|
+
const WORKBUDDY_CONNECT_VERSION = "4.10.1";
|
|
3981
4171
|
//#endregion
|
|
3982
4172
|
//#region src/host-heartbeat.ts
|
|
3983
4173
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -267,8 +267,17 @@ declare class WorkBuddyCredentialStore {
|
|
|
267
267
|
* Undefined means "no scan in flight and nothing cached".
|
|
268
268
|
*/
|
|
269
269
|
private scanInflight;
|
|
270
|
-
/**
|
|
271
|
-
|
|
270
|
+
/**
|
|
271
|
+
* When the cached scan FINISHED, epoch ms; 0 means "not finished yet".
|
|
272
|
+
*
|
|
273
|
+
* Deliberately the finish time, not the start time (issue #77). The window
|
|
274
|
+
* used to be measured from the START, so a scan that took longer than the
|
|
275
|
+
* window was already outside it the moment it landed: the very next `readAll`
|
|
276
|
+
* started a fresh scan, and the sharing this cache exists for never happened.
|
|
277
|
+
* On the machine in that report one scan took 1.4–2.1 s against a 1000 ms
|
|
278
|
+
* window, so every one of a page open's ~6 reads re-scanned the filesystem.
|
|
279
|
+
*/
|
|
280
|
+
private scanFinishedAtMs;
|
|
272
281
|
/**
|
|
273
282
|
* A RUNTIME account override from the pool's rotation, or undefined.
|
|
274
283
|
*
|
|
@@ -356,10 +365,14 @@ declare class WorkBuddyCredentialStore {
|
|
|
356
365
|
/**
|
|
357
366
|
* The account list, sharing one scan across concurrent and repeated reads.
|
|
358
367
|
*
|
|
359
|
-
* See {@link scanInflight}: the whole performance fix lives in these
|
|
360
|
-
*
|
|
361
|
-
* `
|
|
362
|
-
*
|
|
368
|
+
* See {@link scanInflight}: the whole performance fix lives in these lines.
|
|
369
|
+
* Every caller keeps its OWN semantics (`current`, `accounts`, `status`,
|
|
370
|
+
* `selectionLost`), because this returns the same array they used to build
|
|
371
|
+
* themselves — only the filesystem work is shared.
|
|
372
|
+
*
|
|
373
|
+
* The window runs from the FINISH time ({@link scanFinishedAtMs}), so a scan
|
|
374
|
+
* slower than the window still serves the reads that follow it. A scan in
|
|
375
|
+
* flight is always joined, however long it has been running.
|
|
363
376
|
*
|
|
364
377
|
* A failed scan is NOT cached: the rejection clears the slot so the next read
|
|
365
378
|
* retries, rather than pinning every later caller to one transient failure.
|
|
@@ -706,6 +719,47 @@ interface WorkBuddyCheckinClaim {
|
|
|
706
719
|
streakDays: number;
|
|
707
720
|
isStreakDay: boolean;
|
|
708
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* One promotional benefit the account may or may not still be entitled to.
|
|
724
|
+
*
|
|
725
|
+
* Read from the billing gateway's OWN status endpoints — the same two the
|
|
726
|
+
* 「套餐与积分」 page calls before it decides whether to render a claim card
|
|
727
|
+
* (`/billing/meter/compensation-status`, `/billing/meter/check-gift-claimed`).
|
|
728
|
+
* Reading them is what makes the card honest: the plugin shows a benefit only
|
|
729
|
+
* while the gateway itself would still show one.
|
|
730
|
+
*
|
|
731
|
+
* The two flags answer DIFFERENT questions and neither can be derived from the
|
|
732
|
+
* other — this is the single most important fact about this shape, measured on
|
|
733
|
+
* a real account pool:
|
|
734
|
+
*
|
|
735
|
+
* - `active` is about the WINDOW, not about the reader. An account that has
|
|
736
|
+
* already claimed still reports `active: true` (observed: `claimed: true`
|
|
737
|
+
* together with `active: true` on a claimed gift), so `active` alone cannot
|
|
738
|
+
* mean "claimable".
|
|
739
|
+
* - `claimed` is about THIS account, and it survives the window: once the
|
|
740
|
+
* campaign lapses the endpoint keeps answering `claimed: false` with
|
|
741
|
+
* `active: false` (observed on every international account, whose campaign
|
|
742
|
+
* ended months ago).
|
|
743
|
+
*
|
|
744
|
+
* Hence a benefit is claimable exactly when {@link active} AND NOT
|
|
745
|
+
* {@link claimed}. See `benefitOf` in `src/benefits.ts`.
|
|
746
|
+
*/
|
|
747
|
+
interface WorkBuddyBenefit {
|
|
748
|
+
/** Which campaign this is; the plugin renders one row per kind. */
|
|
749
|
+
kind: 'compensation' | 'gift';
|
|
750
|
+
/** The campaign is inside its window (empty, see: already claimed ≠ inactive). */
|
|
751
|
+
active: boolean;
|
|
752
|
+
/** This account has already taken it — permanently, even after the window. */
|
|
753
|
+
claimed: boolean;
|
|
754
|
+
/** Credits on offer, as the gateway states them. */
|
|
755
|
+
creditNum: number;
|
|
756
|
+
/** Window end, epoch ms. Absent when the gateway sent none it could parse. */
|
|
757
|
+
endsAtMs?: number;
|
|
758
|
+
/** Window start, epoch ms. Compensation omits it. */
|
|
759
|
+
startsAtMs?: number;
|
|
760
|
+
/** When this account claimed it, epoch ms. Only meaningful with `claimed`. */
|
|
761
|
+
claimedAtMs?: number;
|
|
762
|
+
}
|
|
709
763
|
/** Token refresh answer; fields the upstream omits stay absent. */
|
|
710
764
|
interface WorkBuddyRefreshOutcome {
|
|
711
765
|
accessToken: string;
|
|
@@ -934,6 +988,41 @@ declare class WorkBuddyUpstreamClient {
|
|
|
934
988
|
fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
|
|
935
989
|
/** Query today's check-in status without changing account state. */
|
|
936
990
|
fetchCheckinStatus(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinStatus>;
|
|
991
|
+
/**
|
|
992
|
+
* Read every promotional benefit this account can still see, WITHOUT claiming
|
|
993
|
+
* anything.
|
|
994
|
+
*
|
|
995
|
+
* The campaign list is closed and small on purpose: the gateway exposes one
|
|
996
|
+
* status endpoint per campaign, so "discover benefits" is a fixed fan-out, not
|
|
997
|
+
* a search. A new campaign type means a new `/billing/meter/*-status` endpoint
|
|
998
|
+
* added here — the plugin never guesses at undocumented ones.
|
|
999
|
+
*
|
|
1000
|
+
* Every endpoint is read in parallel because they are independent, and a
|
|
1001
|
+
* failure is per-endpoint: one campaign answering 5xx must not blank out the
|
|
1002
|
+
* other. The caller gets only what was actually read.
|
|
1003
|
+
*
|
|
1004
|
+
* Note the paths carry NO `/v2` prefix — these are not the `/v2/billing/meter`
|
|
1005
|
+
* family the credit and check-in calls use. Probing `/v2/...` answers 404, which
|
|
1006
|
+
* is how this distinction was found.
|
|
1007
|
+
*/
|
|
1008
|
+
fetchBenefits(credential: WorkBuddyCredential): Promise<readonly WorkBuddyBenefit[]>;
|
|
1009
|
+
/** One benefit status endpoint. Throws on anything that is not a clean `code: 0`. */
|
|
1010
|
+
private readBenefitStatus;
|
|
1011
|
+
/**
|
|
1012
|
+
* Claim one benefit campaign on ONE account (issue #79).
|
|
1013
|
+
*
|
|
1014
|
+
* This is a real credit grant on a real account, so it takes no account
|
|
1015
|
+
* argument other than the credential it is handed: the caller decides which
|
|
1016
|
+
* single account, and there is deliberately no batch form of this method. The
|
|
1017
|
+
* browser route is the only caller, and it guards the mutation with the same
|
|
1018
|
+
* POST + loopback checks the check-in route carries.
|
|
1019
|
+
*
|
|
1020
|
+
* The response is not parsed: the documented envelope of these two endpoints
|
|
1021
|
+
* is an empty success, and the caller re-reads the status afterwards. That
|
|
1022
|
+
* re-read, not this answer, is what the user is shown — which is why a claim
|
|
1023
|
+
* that silently did nothing still cannot leave the card saying 待领取.
|
|
1024
|
+
*/
|
|
1025
|
+
claimBenefit(credential: WorkBuddyCredential, kind: WorkBuddyBenefit['kind']): Promise<void>;
|
|
937
1026
|
/** Claim today's check-in reward. The browser route guards this mutation. */
|
|
938
1027
|
claimDailyCheckin(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinClaim>;
|
|
939
1028
|
/**
|
|
@@ -1638,9 +1727,12 @@ declare function fetchAtRestKeyPayload(executable: string): Promise<string>;
|
|
|
1638
1727
|
/**
|
|
1639
1728
|
* The desktop app's at-rest field key, or undefined when it cannot be obtained
|
|
1640
1729
|
* (app not installed, an older build without the native module, or a future
|
|
1641
|
-
* build that rotates the payload).
|
|
1642
|
-
*
|
|
1643
|
-
*
|
|
1730
|
+
* build that rotates the payload).
|
|
1731
|
+
*
|
|
1732
|
+
* Both outcomes are cached: a success for the process lifetime (the app is
|
|
1733
|
+
* spawned at most once), a failure for {@link AT_REST_KEY_FAILURE_TTL_MS}. The
|
|
1734
|
+
* failure TTL is what keeps a machine WITHOUT the app from paying the
|
|
1735
|
+
* synchronous registry probe on every credential file — see {@link failedKeyAtMs}.
|
|
1644
1736
|
*/
|
|
1645
1737
|
declare function readAtRestKey(): Promise<Buffer | undefined>;
|
|
1646
1738
|
/** Drop the cached key; tests and diagnostics only. */
|
|
@@ -2011,6 +2103,29 @@ declare function probeModel(input: {
|
|
|
2011
2103
|
signal?: AbortSignal;
|
|
2012
2104
|
}): Promise<WorkBuddyProbeResult>;
|
|
2013
2105
|
//#endregion
|
|
2106
|
+
//#region src/benefits.d.ts
|
|
2107
|
+
/** What the account card shows for one campaign. */
|
|
2108
|
+
type WorkBuddyBenefitState =
|
|
2109
|
+
/** Offered, inside its window, not taken yet. The only state with a claim button. */
|
|
2110
|
+
'claimable' |
|
|
2111
|
+
/** This account already took it. Rendered as nothing at all. */
|
|
2112
|
+
'claimed' |
|
|
2113
|
+
/** The window is over and it was never taken — the offer is gone. */
|
|
2114
|
+
'expired';
|
|
2115
|
+
/** One campaign, resolved for display. */
|
|
2116
|
+
interface WorkBuddyBenefitView {
|
|
2117
|
+
kind: WorkBuddyBenefit['kind'];
|
|
2118
|
+
state: WorkBuddyBenefitState;
|
|
2119
|
+
/** Credits on offer, as the gateway stated them. */
|
|
2120
|
+
creditNum: number;
|
|
2121
|
+
/** Window end, epoch ms. */
|
|
2122
|
+
endsAtMs?: number;
|
|
2123
|
+
/** Whole days until the window closes, floored at 0. Absent without `endsAtMs`. */
|
|
2124
|
+
daysLeft?: number;
|
|
2125
|
+
/** True while the window closes within {@link URGENT_DAYS} days. */
|
|
2126
|
+
urgent: boolean;
|
|
2127
|
+
}
|
|
2128
|
+
//#endregion
|
|
2014
2129
|
//#region src/account-pool.d.ts
|
|
2015
2130
|
/**
|
|
2016
2131
|
* Which of the pool's writers produced a measurement.
|
|
@@ -2651,6 +2766,24 @@ type WorkBuddyWebPoolExclusion = 'unusable' | 'rate-limited' |
|
|
|
2651
2766
|
* does not say (issue #56).
|
|
2652
2767
|
*/
|
|
2653
2768
|
'model-busy' | 'out-of-credit' | 'credential-rejected' | 'not-found' | 'unavailable' | 'failed';
|
|
2769
|
+
/** One claimable benefit, as the card renders it (issue #79). */
|
|
2770
|
+
interface WorkBuddyWebBenefit {
|
|
2771
|
+
/** Which campaign: compensation (crash补偿) or gift (新人首月赠送). */
|
|
2772
|
+
kind: 'compensation' | 'gift';
|
|
2773
|
+
/** Display name for the campaign, resolved Host-side so both clients agree. */
|
|
2774
|
+
title: string;
|
|
2775
|
+
/** Credits on offer. */
|
|
2776
|
+
creditNum: number;
|
|
2777
|
+
/**
|
|
2778
|
+
* Whole days until the window closes, floored at 0.
|
|
2779
|
+
*
|
|
2780
|
+
* ABSENT when the gateway stated no end time — the card must then omit the
|
|
2781
|
+
* countdown rather than print a number nobody knows.
|
|
2782
|
+
*/
|
|
2783
|
+
daysLeft?: number;
|
|
2784
|
+
/** True when the window closes within 3 days, so the card can say 即将到期. */
|
|
2785
|
+
urgent: boolean;
|
|
2786
|
+
}
|
|
2654
2787
|
/**
|
|
2655
2788
|
* One account's row in the pool, as the card renders it.
|
|
2656
2789
|
*
|
|
@@ -2727,6 +2860,16 @@ interface WorkBuddyWebPoolAccount {
|
|
|
2727
2860
|
* result the user just watched succeed.
|
|
2728
2861
|
*/
|
|
2729
2862
|
checkedInToday?: boolean;
|
|
2863
|
+
/**
|
|
2864
|
+
* Promotional benefits this account can STILL claim (issue #79).
|
|
2865
|
+
*
|
|
2866
|
+
* Never carries a claimed or lapsed campaign: the issue's rule is that those
|
|
2867
|
+
* do not appear at all, so their absence here IS the "hide it" instruction.
|
|
2868
|
+
* ABSENT means not read (or nothing claimable), and the card renders no line
|
|
2869
|
+
* either way — a benefit line is only ever added to say "there is something
|
|
2870
|
+
* here for you", never to announce its own absence.
|
|
2871
|
+
*/
|
|
2872
|
+
benefits?: readonly WorkBuddyWebBenefit[];
|
|
2730
2873
|
/**
|
|
2731
2874
|
* The model a TEST of this row would use (issue #61).
|
|
2732
2875
|
*
|
|
@@ -2884,7 +3027,7 @@ declare const WORKBUDDY_SETTINGS_NS: "workbuddy";
|
|
|
2884
3027
|
interface WorkBuddyStatusRouteOptions {
|
|
2885
3028
|
/** The region-scoped credential store backing each region's requests. */
|
|
2886
3029
|
store(region: WorkBuddyRegion): WorkBuddyCredentialStore;
|
|
2887
|
-
client: Pick<WorkBuddyUpstreamClient, 'fetchCredits' | 'fetchCheckinStatus' | 'claimDailyCheckin'>;
|
|
3030
|
+
client: Pick<WorkBuddyUpstreamClient, 'fetchCredits' | 'fetchCheckinStatus' | 'claimDailyCheckin' | 'fetchBenefits' | 'claimBenefit'>;
|
|
2888
3031
|
/**
|
|
2889
3032
|
* The requested region's last-refreshed model directory (unfiltered) for
|
|
2890
3033
|
* card display. Region-scoped because the CN and international apps expose
|
|
@@ -3170,6 +3313,37 @@ interface WorkBuddyPoolDeps {
|
|
|
3170
3313
|
* per-account read in the full document.
|
|
3171
3314
|
*/
|
|
3172
3315
|
cachedCheckedInToday?(region: WorkBuddyRegion): Readonly<Record<string, boolean>>;
|
|
3316
|
+
/**
|
|
3317
|
+
* Claimable promotional benefits per account id (issue #79).
|
|
3318
|
+
*
|
|
3319
|
+
* Only campaigns the account can STILL claim are present; a claimed or lapsed
|
|
3320
|
+
* campaign is absent on purpose, so the card renders nothing for it. Absent
|
|
3321
|
+
* (or a missing entry) also means "not read yet", which the card treats the
|
|
3322
|
+
* same way — no line, rather than a guessed state.
|
|
3323
|
+
*/
|
|
3324
|
+
benefits?(region: WorkBuddyRegion): Promise<Readonly<Record<string, readonly WorkBuddyBenefitView[]>>>;
|
|
3325
|
+
/**
|
|
3326
|
+
* Claimable benefits, from the readings already taken this process.
|
|
3327
|
+
*
|
|
3328
|
+
* The first-paint counterpart of {@link benefits}: no request, so painting
|
|
3329
|
+
* never waits on two upstream reads per account.
|
|
3330
|
+
*/
|
|
3331
|
+
cachedBenefits?(region: WorkBuddyRegion): Readonly<Record<string, readonly WorkBuddyBenefitView[]>>;
|
|
3332
|
+
/**
|
|
3333
|
+
* Forget this account's cached benefit reading so the next status read is live.
|
|
3334
|
+
*
|
|
3335
|
+
* The claim route writes to upstream directly, so nothing else in the plugin
|
|
3336
|
+
* knows the campaign just changed. Without this the card kept showing 领取 for
|
|
3337
|
+
* up to `BENEFIT_CACHE_MS` (30 min) after a SUCCESSFUL claim: the credits went
|
|
3338
|
+
* up, the pill stayed, and the only remaining explanation was "the claim
|
|
3339
|
+
* silently failed" — which invited a second click the route then refused as
|
|
3340
|
+
* already-claimed. `checkin` has the same rule and invalidates its own cache;
|
|
3341
|
+
* this is the benefit counterpart.
|
|
3342
|
+
*
|
|
3343
|
+
* Absent is legal (a Host from before this hook) and means "the click refreshes
|
|
3344
|
+
* the card, but the host-side cache may still answer stale for a while".
|
|
3345
|
+
*/
|
|
3346
|
+
invalidateBenefit?(region: WorkBuddyRegion, accountId: string): void;
|
|
3173
3347
|
/**
|
|
3174
3348
|
* Every local sign-in the pool does NOT cover, so the card can offer them to
|
|
3175
3349
|
* check in. Absent means the card lists only pool members.
|