dsh-plugin-subscription-oauth 0.13.0 → 0.16.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/lib/auth/provider-controller.d.ts +7 -0
- package/lib/auth/provider-controller.js +1 -0
- package/lib/auth/registry.d.ts +40 -0
- package/lib/auth/registry.js +14 -0
- package/lib/auth/rpc.js +12 -0
- package/lib/auth/workbuddy-flow.d.ts +45 -1
- package/lib/auth/workbuddy-flow.js +222 -2
- package/lib/client/SubscriptionsSection.js +71 -6
- package/lib/client/locales.d.ts +20 -0
- package/lib/client/locales.js +20 -0
- package/lib/client.js +149 -27
- package/lib/client.js.map +1 -1
- package/lib/protocol.d.ts +50 -1
- package/lib/protocol.js +2 -0
- package/lib/providers/workbuddy-credits.d.ts +23 -0
- package/lib/providers/workbuddy-credits.js +52 -0
- package/lib/providers/workbuddy-provider.js +70 -3
- package/lib/providers/workbuddy.d.ts +4 -0
- package/lib/providers/workbuddy.js +14 -0
- package/package.json +1 -1
|
@@ -77,6 +77,13 @@ export interface ProviderUsageRead {
|
|
|
77
77
|
plan?: string;
|
|
78
78
|
renewsAt?: number;
|
|
79
79
|
windows: readonly UsageWindow[];
|
|
80
|
+
/** Daily check-in state, when the provider states one. */
|
|
81
|
+
checkin?: {
|
|
82
|
+
active: boolean;
|
|
83
|
+
todayCheckedIn: boolean;
|
|
84
|
+
streakDays: number;
|
|
85
|
+
todayCredit: number;
|
|
86
|
+
};
|
|
80
87
|
/**
|
|
81
88
|
* Why these figures are not the provider's primary accounting.
|
|
82
89
|
*
|
|
@@ -496,6 +496,7 @@ export class ProviderController {
|
|
|
496
496
|
fetchedAt: Date.now(),
|
|
497
497
|
...read.plan === undefined ? {} : { plan: read.plan },
|
|
498
498
|
...read.renewsAt === undefined ? {} : { renewsAt: read.renewsAt },
|
|
499
|
+
...read.checkin === undefined ? {} : { checkin: read.checkin },
|
|
499
500
|
...read.usageError === undefined ? {} : { substitute: true },
|
|
500
501
|
};
|
|
501
502
|
this.usageByAccount.set(account, entry);
|
package/lib/auth/registry.d.ts
CHANGED
|
@@ -32,6 +32,26 @@ export interface ProviderRoute {
|
|
|
32
32
|
removeModel(id: string): Promise<ProviderStatus>;
|
|
33
33
|
usage(force?: boolean): Promise<ProviderUsageResult>;
|
|
34
34
|
reload(): Promise<ProviderStatus>;
|
|
35
|
+
/** Claim one account's daily check-in reward, when the route has one. */
|
|
36
|
+
checkin?(account: string): Promise<{
|
|
37
|
+
credit: number;
|
|
38
|
+
streakDays: number;
|
|
39
|
+
already: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
/** Read growth tasks and optionally auto-claim, when the route has them. */
|
|
42
|
+
tasks?(account: string, claim: boolean): Promise<{
|
|
43
|
+
tasks: {
|
|
44
|
+
taskCode: string;
|
|
45
|
+
title: string;
|
|
46
|
+
credit: number;
|
|
47
|
+
target: number;
|
|
48
|
+
current: number;
|
|
49
|
+
state: 'pending' | 'claimable' | 'claimed' | 'locked';
|
|
50
|
+
}[];
|
|
51
|
+
claimed: number;
|
|
52
|
+
claimable: number;
|
|
53
|
+
creditEarned: number;
|
|
54
|
+
}>;
|
|
35
55
|
/** Fast and image variants for one model, when the route's discovery reported them. */
|
|
36
56
|
modelVariants?(id: string): Promise<ModelVariants | undefined> | ModelVariants | undefined;
|
|
37
57
|
}
|
|
@@ -101,4 +121,24 @@ export declare class SubscriptionRegistry {
|
|
|
101
121
|
setAutoOn(enabled: boolean): Promise<{
|
|
102
122
|
enabled: boolean;
|
|
103
123
|
}>;
|
|
124
|
+
/** Claim one account's daily check-in reward, when the route supports it. */
|
|
125
|
+
checkin(provider: string, account: string): Promise<{
|
|
126
|
+
credit: number;
|
|
127
|
+
streakDays: number;
|
|
128
|
+
already: boolean;
|
|
129
|
+
}>;
|
|
130
|
+
/** Read one account's growth tasks and optionally auto-claim the claimable rewards. */
|
|
131
|
+
tasks(provider: string, account: string, claim: boolean): Promise<{
|
|
132
|
+
tasks: {
|
|
133
|
+
taskCode: string;
|
|
134
|
+
title: string;
|
|
135
|
+
credit: number;
|
|
136
|
+
target: number;
|
|
137
|
+
current: number;
|
|
138
|
+
state: 'pending' | 'claimable' | 'claimed' | 'locked';
|
|
139
|
+
}[];
|
|
140
|
+
claimed: number;
|
|
141
|
+
claimable: number;
|
|
142
|
+
creditEarned: number;
|
|
143
|
+
}>;
|
|
104
144
|
}
|
package/lib/auth/registry.js
CHANGED
|
@@ -161,4 +161,18 @@ export class SubscriptionRegistry {
|
|
|
161
161
|
await this.preferences.setAutoOn(enabled);
|
|
162
162
|
return { enabled };
|
|
163
163
|
}
|
|
164
|
+
/** Claim one account's daily check-in reward, when the route supports it. */
|
|
165
|
+
async checkin(provider, account) {
|
|
166
|
+
const route = this.route(provider);
|
|
167
|
+
if (route.checkin === undefined)
|
|
168
|
+
throw new ProviderError(`${provider} has no check-in`, BAD_REQUEST);
|
|
169
|
+
return route.checkin(account);
|
|
170
|
+
}
|
|
171
|
+
/** Read one account's growth tasks and optionally auto-claim the claimable rewards. */
|
|
172
|
+
async tasks(provider, account, claim) {
|
|
173
|
+
const route = this.route(provider);
|
|
174
|
+
if (route.tasks === undefined)
|
|
175
|
+
throw new ProviderError(`${provider} has no tasks`, BAD_REQUEST);
|
|
176
|
+
return route.tasks(account, claim);
|
|
177
|
+
}
|
|
164
178
|
}
|
package/lib/auth/rpc.js
CHANGED
|
@@ -163,6 +163,18 @@ async function runEndpoint(registry, endpoint, payload, balance) {
|
|
|
163
163
|
throw new ProviderError('setAutoOn requires an enabled flag', BAD_REQUEST);
|
|
164
164
|
return registry.setAutoOn(enabled);
|
|
165
165
|
}
|
|
166
|
+
case 'checkin': {
|
|
167
|
+
const account = stringField(payload, 'account');
|
|
168
|
+
if (account === undefined)
|
|
169
|
+
throw new ProviderError('checkin requires an account', BAD_REQUEST);
|
|
170
|
+
return registry.checkin(requiredProvider(payload, endpoint), account);
|
|
171
|
+
}
|
|
172
|
+
case 'tasks': {
|
|
173
|
+
const account = stringField(payload, 'account');
|
|
174
|
+
if (account === undefined)
|
|
175
|
+
throw new ProviderError('tasks requires an account', BAD_REQUEST);
|
|
176
|
+
return registry.tasks(requiredProvider(payload, endpoint), account, booleanField(payload, 'claim') === true);
|
|
177
|
+
}
|
|
166
178
|
case 'deepseek-balance': {
|
|
167
179
|
// The DeepSeek account's balance, read on the node half through the host's own
|
|
168
180
|
// account service. The browser cannot call that service directly (it sits behind
|
|
@@ -101,8 +101,52 @@ export interface WorkBuddyUsageWindow {
|
|
|
101
101
|
limit?: number;
|
|
102
102
|
resetsAt?: number;
|
|
103
103
|
}
|
|
104
|
+
/** The account's check-in state, from the billing activity endpoint. */
|
|
105
|
+
export interface WorkBuddyCheckin {
|
|
106
|
+
active: boolean;
|
|
107
|
+
todayCheckedIn: boolean;
|
|
108
|
+
streakDays: number;
|
|
109
|
+
todayCredit: number;
|
|
110
|
+
}
|
|
111
|
+
/** One growth task, as the activity endpoint frames it. */
|
|
112
|
+
export interface WorkBuddyTask {
|
|
113
|
+
taskCode: string;
|
|
114
|
+
title: string;
|
|
115
|
+
credit: number;
|
|
116
|
+
energy: number;
|
|
117
|
+
target: number;
|
|
118
|
+
current: number;
|
|
119
|
+
claimable: boolean;
|
|
120
|
+
claimed: boolean;
|
|
121
|
+
locked: boolean;
|
|
122
|
+
acceptStatus: string;
|
|
123
|
+
}
|
|
124
|
+
/** Read the account's growth tasks, parsed the way the pool plugin parses them. */
|
|
125
|
+
export declare function listWorkBuddyTasks(token: WorkBuddyTokenSet, fetchFn: typeof fetch): Promise<WorkBuddyTask[]>;
|
|
126
|
+
/** Accept (enrol in) tasks by code; replaying an accepted task is a safe no-op. */
|
|
127
|
+
export declare function acceptWorkBuddyTasks(token: WorkBuddyTokenSet, taskCodes: readonly string[], fetchFn: typeof fetch): Promise<void>;
|
|
128
|
+
/** Claim one task's reward; already-claimed answers zero and counts as success. */
|
|
129
|
+
export declare function claimWorkBuddyTask(token: WorkBuddyTokenSet, taskCode: string, fetchFn: typeof fetch): Promise<{
|
|
130
|
+
credit: number;
|
|
131
|
+
energy: number;
|
|
132
|
+
}>;
|
|
133
|
+
/** Read the account's daily check-in state, without changing anything. */
|
|
134
|
+
export declare function readWorkBuddyCheckin(token: WorkBuddyTokenSet, fetchFn: typeof fetch): Promise<WorkBuddyCheckin>;
|
|
135
|
+
/** Claim today's check-in reward; resolves with the credit granted and the streak. */
|
|
136
|
+
export declare function claimWorkBuddyCheckin(token: WorkBuddyTokenSet, fetchFn: typeof fetch): Promise<{
|
|
137
|
+
credit: number;
|
|
138
|
+
streakDays: number;
|
|
139
|
+
}>;
|
|
140
|
+
/** The account's precise remaining credits, summed across its live packages. */
|
|
141
|
+
export declare function readWorkBuddyCredits(token: WorkBuddyTokenSet, fetchFn: typeof fetch): Promise<{
|
|
142
|
+
remaining: number;
|
|
143
|
+
nearestExpiryMs?: number;
|
|
144
|
+
} | null>;
|
|
104
145
|
/** Read the account's plan and metered credits for the Settings card. */
|
|
105
|
-
export declare function readWorkBuddyUsage(token: WorkBuddyTokenSet, plan: string | undefined, fetchFn: typeof fetch
|
|
146
|
+
export declare function readWorkBuddyUsage(token: WorkBuddyTokenSet, plan: string | undefined, fetchFn: typeof fetch, spend?: {
|
|
147
|
+
spent: number;
|
|
148
|
+
}): Promise<{
|
|
106
149
|
plan: string;
|
|
107
150
|
windows: WorkBuddyUsageWindow[];
|
|
151
|
+
checkin?: WorkBuddyCheckin;
|
|
108
152
|
}>;
|
|
@@ -263,6 +263,118 @@ function workbuddyEnterpriseHeaders(token) {
|
|
|
263
263
|
const enterprise = typeof token.enterpriseId === 'string' ? token.enterpriseId.trim() : '';
|
|
264
264
|
return enterprise === '' ? {} : { 'X-Enterprise-Id': enterprise, 'X-Tenant-Id': enterprise };
|
|
265
265
|
}
|
|
266
|
+
/** The chat request headers: the account's, plus the CLI conventions. */
|
|
267
|
+
function chatHeaders(token) {
|
|
268
|
+
return {
|
|
269
|
+
'Accept': 'application/json, text/plain, */*',
|
|
270
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
271
|
+
'Origin': 'https://copilot.tencent.com',
|
|
272
|
+
'Referer': 'https://copilot.tencent.com/',
|
|
273
|
+
'User-Agent': 'CLI/2.63.2 CodeBuddy/2.63.2',
|
|
274
|
+
'Content-Type': 'application/json',
|
|
275
|
+
'Authorization': `Bearer ${token.accessToken}`,
|
|
276
|
+
...token.uid === undefined || token.uid === '' ? { 'X-No-User-Id': '1' } : { 'X-User-Id': token.uid },
|
|
277
|
+
...token.enterpriseId === undefined || token.enterpriseId === '' ? { 'X-No-Enterprise-Id': '1' } : { 'X-Enterprise-Id': token.enterpriseId },
|
|
278
|
+
'X-Product': 'SaaS',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/** Ask the chat host one question, returning the decoded envelope's data. */
|
|
282
|
+
async function chatCall(token, method, path, body, fetchFn) {
|
|
283
|
+
const response = await fetchFn(`https://copilot.tencent.com${path}`, {
|
|
284
|
+
method,
|
|
285
|
+
headers: chatHeaders(token),
|
|
286
|
+
...body === undefined ? {} : { body: JSON.stringify(body) },
|
|
287
|
+
signal: AbortSignal.timeout(20_000),
|
|
288
|
+
});
|
|
289
|
+
let env = {};
|
|
290
|
+
try {
|
|
291
|
+
env = JSON.parse(await response.text());
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
env = {};
|
|
295
|
+
}
|
|
296
|
+
const code = Number.parseInt(String(env.code ?? ''), 10) || 0;
|
|
297
|
+
if (!response.ok || code !== 0) {
|
|
298
|
+
throw new Error(`WorkBuddy ${path}: ${String(env.msg ?? `error ${code}`)}`);
|
|
299
|
+
}
|
|
300
|
+
return isRecord(env.data) ? env.data : null;
|
|
301
|
+
}
|
|
302
|
+
/** Read the account's growth tasks, parsed the way the pool plugin parses them. */
|
|
303
|
+
export async function listWorkBuddyTasks(token, fetchFn) {
|
|
304
|
+
const data = await chatCall(token, 'GET', '/v2/activity/growth/tasks', undefined, fetchFn);
|
|
305
|
+
if (data === null)
|
|
306
|
+
return [];
|
|
307
|
+
const raw = Array.isArray(data.tasks) ? data.tasks.filter(isRecord) : [];
|
|
308
|
+
const out = [];
|
|
309
|
+
for (const entry of raw) {
|
|
310
|
+
const taskCode = typeof entry.task_code === 'string' ? entry.task_code : '';
|
|
311
|
+
if (taskCode === '')
|
|
312
|
+
continue;
|
|
313
|
+
const num = (key) => capacity(entry[key]);
|
|
314
|
+
let current = num('current');
|
|
315
|
+
let target = num('target');
|
|
316
|
+
if (isRecord(entry.progress)) {
|
|
317
|
+
const nestedCurrent = capacity(entry.progress.current);
|
|
318
|
+
const nestedTarget = capacity(entry.progress.target);
|
|
319
|
+
if (nestedTarget > 0 || nestedCurrent > 0) {
|
|
320
|
+
current = nestedCurrent;
|
|
321
|
+
target = nestedTarget;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const acceptStatus = typeof entry.accept_status === 'string' ? entry.accept_status : '';
|
|
325
|
+
const claimed = acceptStatus === 'claimed';
|
|
326
|
+
out.push({
|
|
327
|
+
taskCode,
|
|
328
|
+
title: typeof entry.title === 'string' && entry.title !== '' ? entry.title : taskCode,
|
|
329
|
+
credit: num('reward_credit'),
|
|
330
|
+
energy: num('reward_energy'),
|
|
331
|
+
target,
|
|
332
|
+
current,
|
|
333
|
+
claimable: !claimed && target > 0 && current >= target,
|
|
334
|
+
claimed,
|
|
335
|
+
locked: entry.locked === true,
|
|
336
|
+
acceptStatus,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
/** Accept (enrol in) tasks by code; replaying an accepted task is a safe no-op. */
|
|
342
|
+
export async function acceptWorkBuddyTasks(token, taskCodes, fetchFn) {
|
|
343
|
+
if (taskCodes.length === 0)
|
|
344
|
+
return;
|
|
345
|
+
await chatCall(token, 'POST', '/v2/activity/growth/tasks/accept', { task_codes: [...taskCodes] }, fetchFn);
|
|
346
|
+
}
|
|
347
|
+
/** Claim one task's reward; already-claimed answers zero and counts as success. */
|
|
348
|
+
export async function claimWorkBuddyTask(token, taskCode, fetchFn) {
|
|
349
|
+
const headers = {
|
|
350
|
+
...workbuddyHeaders(token),
|
|
351
|
+
'Accept': 'application/json, text/plain, */*',
|
|
352
|
+
'Origin': 'https://www.workbuddy.cn',
|
|
353
|
+
'Referer': 'https://www.workbuddy.cn/profile/growth-center',
|
|
354
|
+
'x-client-platform': 'web',
|
|
355
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
|
|
356
|
+
};
|
|
357
|
+
const response = await fetchFn(`https://www.workbuddy.cn/activity/growth/tasks/${encodeURIComponent(taskCode)}/claim`, {
|
|
358
|
+
method: 'POST',
|
|
359
|
+
headers,
|
|
360
|
+
signal: AbortSignal.timeout(20_000),
|
|
361
|
+
});
|
|
362
|
+
let env = {};
|
|
363
|
+
try {
|
|
364
|
+
env = JSON.parse(await response.text());
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
env = {};
|
|
368
|
+
}
|
|
369
|
+
const code = Number.parseInt(String(env.code ?? ''), 10) || 0;
|
|
370
|
+
if (!response.ok || code !== 0) {
|
|
371
|
+
throw new Error(`WorkBuddy task ${taskCode}: ${String(env.msg ?? `error ${code}`)}`);
|
|
372
|
+
}
|
|
373
|
+
const data = isRecord(env.data) ? env.data : {};
|
|
374
|
+
if (data.already_claimed === true)
|
|
375
|
+
return { credit: 0, energy: 0 };
|
|
376
|
+
return { credit: capacity(data.credit), energy: capacity(data.energy) };
|
|
377
|
+
}
|
|
266
378
|
/** A capacity the billing API sends, as a string or number; empty is 0. */
|
|
267
379
|
function capacity(value) {
|
|
268
380
|
if (value === null || value === undefined || value === '')
|
|
@@ -311,6 +423,79 @@ async function workbuddyMeter(token, path, body, fetchFn) {
|
|
|
311
423
|
throw new Error(String(env.msg ?? `error ${code}`));
|
|
312
424
|
return isRecord(env.data) ? env.data : null;
|
|
313
425
|
}
|
|
426
|
+
/** Read the account's daily check-in state, without changing anything. */
|
|
427
|
+
export async function readWorkBuddyCheckin(token, fetchFn) {
|
|
428
|
+
const data = await workbuddyMeter(token, '/billing/meter/checkin-activity-status', {}, fetchFn);
|
|
429
|
+
if (data === null)
|
|
430
|
+
return { active: false, todayCheckedIn: false, streakDays: 0, todayCredit: 0 };
|
|
431
|
+
const num = (key) => typeof data[key] === 'number' ? data[key] : 0;
|
|
432
|
+
return {
|
|
433
|
+
active: data.active === true,
|
|
434
|
+
todayCheckedIn: data.today_checked_in === true,
|
|
435
|
+
streakDays: num('streak_days'),
|
|
436
|
+
todayCredit: num('today_credit'),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/** Claim today's check-in reward; resolves with the credit granted and the streak. */
|
|
440
|
+
export async function claimWorkBuddyCheckin(token, fetchFn) {
|
|
441
|
+
const data = await workbuddyMeter(token, '/billing/meter/daily-checkin', {}, fetchFn);
|
|
442
|
+
if (data === null)
|
|
443
|
+
return { credit: 0, streakDays: 0 };
|
|
444
|
+
const num = (key) => typeof data[key] === 'number' ? data[key] : 0;
|
|
445
|
+
return { credit: num('credit'), streakDays: num('streak_days') };
|
|
446
|
+
}
|
|
447
|
+
/** The account's precise remaining credits, summed across its live packages. */
|
|
448
|
+
export async function readWorkBuddyCredits(token, fetchFn) {
|
|
449
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
450
|
+
const fmt = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
451
|
+
const now = new Date();
|
|
452
|
+
const data = await workbuddyMeter(token, '/billing/meter/get-user-resource', {
|
|
453
|
+
PageNumber: 1,
|
|
454
|
+
PageSize: 100,
|
|
455
|
+
ProductCode: 'p_tcaca',
|
|
456
|
+
Status: [0, 3],
|
|
457
|
+
PackageEndTimeRangeBegin: fmt(now),
|
|
458
|
+
PackageEndTimeRangeEnd: fmt(new Date(now.getTime() + 3185136e6)),
|
|
459
|
+
}, fetchFn);
|
|
460
|
+
if (data === null)
|
|
461
|
+
return null;
|
|
462
|
+
const response = isRecord(data.Response) ? data.Response : {};
|
|
463
|
+
const inner = isRecord(response.Data) ? response.Data : {};
|
|
464
|
+
const accounts = Array.isArray(inner.Accounts) ? inner.Accounts.filter(isRecord) : [];
|
|
465
|
+
const parseDate = (raw) => {
|
|
466
|
+
if (typeof raw === 'number' && raw > 0xe8d4a51000)
|
|
467
|
+
return raw;
|
|
468
|
+
if (typeof raw === 'string' && raw !== '') {
|
|
469
|
+
const parsed = Date.parse(raw);
|
|
470
|
+
if (!Number.isNaN(parsed))
|
|
471
|
+
return parsed;
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
};
|
|
475
|
+
let remaining = 0;
|
|
476
|
+
let nearestExpiryMs;
|
|
477
|
+
let livePackages = 0;
|
|
478
|
+
for (const account of accounts) {
|
|
479
|
+
const num = (key) => capacity(account[key]);
|
|
480
|
+
const monthly = num('CapacityType') === 4;
|
|
481
|
+
const remain = monthly ? num('CycleCapacityRemain') : num('CapacityRemain');
|
|
482
|
+
const capped = remain < 0 ? 0 : remain;
|
|
483
|
+
const cycleEndMs = parseDate(account.CycleEndTime);
|
|
484
|
+
const expiresAtMs = monthly ? undefined : parseDate(account.ExpiredTime) ?? cycleEndMs;
|
|
485
|
+
if (!monthly && (capped <= 0 || (expiresAtMs !== undefined && expiresAtMs <= Date.now())))
|
|
486
|
+
continue;
|
|
487
|
+
livePackages += 1;
|
|
488
|
+
remaining += capped;
|
|
489
|
+
if (expiresAtMs !== undefined && (nearestExpiryMs === undefined || expiresAtMs < nearestExpiryMs)) {
|
|
490
|
+
nearestExpiryMs = expiresAtMs;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// No live package reads as "this account doesn't use the package list" — an enterprise
|
|
494
|
+
// account, say — so the caller falls through to its own meter.
|
|
495
|
+
if (livePackages === 0)
|
|
496
|
+
return null;
|
|
497
|
+
return { remaining, ...nearestExpiryMs === undefined ? {} : { nearestExpiryMs } };
|
|
498
|
+
}
|
|
314
499
|
/** The account's credits from its resource summary: the plan's used against granted. */
|
|
315
500
|
function usageOfSummary(summary, plan) {
|
|
316
501
|
const isPaid = summary?.IsPaidUser === true;
|
|
@@ -349,7 +534,34 @@ function enterpriseUsageOf(data) {
|
|
|
349
534
|
return window;
|
|
350
535
|
}
|
|
351
536
|
/** Read the account's plan and metered credits for the Settings card. */
|
|
352
|
-
export async function readWorkBuddyUsage(token, plan, fetchFn) {
|
|
537
|
+
export async function readWorkBuddyUsage(token, plan, fetchFn, spend) {
|
|
538
|
+
// The precise figures: the live packages' remaining and total size, and the account's
|
|
539
|
+
// accumulated spend from the real per-turn credits. Spent-against-limit reads as the
|
|
540
|
+
// true billed share, not an estimate from tokens.
|
|
541
|
+
try {
|
|
542
|
+
const credits = await readWorkBuddyCredits(token, fetchFn);
|
|
543
|
+
if (credits !== null) {
|
|
544
|
+
const window = { label: 'Credits', used: spend?.spent ?? 0 };
|
|
545
|
+
if (credits.remaining > 0 || (spend?.spent ?? 0) > 0) {
|
|
546
|
+
const limit = credits.remaining + (spend?.spent ?? 0);
|
|
547
|
+
window.limit = limit;
|
|
548
|
+
window.usedPercent = limit > 0 ? (100 * (spend?.spent ?? 0)) / limit : 0;
|
|
549
|
+
}
|
|
550
|
+
if (credits.nearestExpiryMs !== undefined)
|
|
551
|
+
window.resetsAt = credits.nearestExpiryMs;
|
|
552
|
+
const out = { plan: plan ?? 'Free', windows: [window] };
|
|
553
|
+
try {
|
|
554
|
+
out.checkin = await readWorkBuddyCheckin(token, fetchFn);
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
// A check-in read failure never hides the credits.
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// Fall through to the summary when the package list can't be read.
|
|
564
|
+
}
|
|
353
565
|
const summary = await workbuddyMeter(token, '/billing/meter/get-user-resource-summary', {}, fetchFn);
|
|
354
566
|
const out = usageOfSummary(summary, plan);
|
|
355
567
|
// An enterprise account's summary has no packages: its credits are the enterprise meter's.
|
|
@@ -357,12 +569,20 @@ export async function readWorkBuddyUsage(token, plan, fetchFn) {
|
|
|
357
569
|
try {
|
|
358
570
|
const enterprise = enterpriseUsageOf(await workbuddyMeter(token, '/billing/meter/get-enterprise-user-usage', {}, fetchFn));
|
|
359
571
|
if (enterprise !== null) {
|
|
360
|
-
|
|
572
|
+
out.windows = [enterprise];
|
|
573
|
+
out.plan = plan ?? 'Enterprise';
|
|
361
574
|
}
|
|
362
575
|
}
|
|
363
576
|
catch {
|
|
364
577
|
// The personal-account refusal (10001) leaves the card as the summary said.
|
|
365
578
|
}
|
|
366
579
|
}
|
|
580
|
+
// Fold the check-in state in beside the credits: the card shows today's status and streak.
|
|
581
|
+
try {
|
|
582
|
+
out.checkin = await readWorkBuddyCheckin(token, fetchFn);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// A check-in read failure never hides the credits.
|
|
586
|
+
}
|
|
367
587
|
return out;
|
|
368
588
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Subscriptions settings page.
|
|
4
4
|
*
|
|
@@ -313,12 +313,17 @@ function windowAmount(window) {
|
|
|
313
313
|
return `${String(window.used)}/${String(window.limit)}`;
|
|
314
314
|
return '—';
|
|
315
315
|
}
|
|
316
|
-
/** One account's metered windows, rendered under its account row. */
|
|
316
|
+
/** One account's metered windows and check-in, rendered under its account row. */
|
|
317
317
|
function AccountUsage(props) {
|
|
318
|
-
const { entry, t } = props;
|
|
318
|
+
const { entry, t, busyAction, onCheckin, onTasks } = props;
|
|
319
319
|
if (entry === undefined)
|
|
320
320
|
return null;
|
|
321
|
-
|
|
321
|
+
const checkin = entry.checkin;
|
|
322
|
+
return (_jsxs("div", { style: styles.usageInline, children: [_jsxs("div", { style: styles.usageLine, children: [_jsx("span", { style: styles.modelName, children: checkin?.active === true
|
|
323
|
+
? checkin.todayCheckedIn
|
|
324
|
+
? t('checkinDone', { days: checkin.streakDays })
|
|
325
|
+
: t('checkinPending')
|
|
326
|
+
: '' }), _jsxs("span", { style: styles.pushRight, children: [onTasks !== undefined && (_jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: busyAction, onClick: onTasks, children: t('tasksClaim') })), checkin?.active === true && !checkin.todayCheckedIn && onCheckin !== undefined && (_jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: busyAction, onClick: onCheckin, children: t('checkinClaim') }))] })] }), entry.windows.map((window) => (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [windowLabel(t, window), ' · ', windowAmount(window)] }), _jsx("span", { style: styles.modelMeta, children: window.resetsAt === undefined ? '' : formatReset(window.resetsAt, t) })] }), window.usedPercent !== undefined && (_jsx("div", { className: "dsh-subscription-meter", children: _jsx("span", { style: { width: `${String(Math.min(100, Math.max(0, window.usedPercent)))}%` }, "data-exhausted": window.usedPercent >= 90 ? 'true' : 'false' }) }))] }, `${entry.account}-${window.label}`))), entry.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageNone') }))] }));
|
|
322
327
|
}
|
|
323
328
|
/** The plan-and-usage block for the whole route. */
|
|
324
329
|
function UsageBlock(props) {
|
|
@@ -523,7 +528,7 @@ function ProviderCard(props) {
|
|
|
523
528
|
const usageEntry = (status.usage ?? []).find((entry) => entry.account === account.account);
|
|
524
529
|
return (_jsxs("div", { style: styles.accountRow, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("button", { type: "button", style: styles.starButton, title: account.isDefault ? t('defaultBadge') : t('makeDefault'), "aria-label": account.isDefault ? t('defaultBadge') : t('makeDefault'), disabled: busyAction || account.isDefault, onClick: () => { props.onSetDefault(account.account); }, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: account.label }), account.isDefault && _jsx("span", { style: styles.badgeDefault, children: t('defaultBadge') }), account.needsLogin === true && (_jsx("span", { style: styles.badgeWarn, title: t('accountNeedsLoginHint'), children: t('accountNeedsLogin') })), usageEntry?.plan !== undefined && (_jsx("span", { style: styles.badge, children: t('usagePlan', { plan: usageEntry.plan }) })), _jsx("span", { style: styles.statusLine, children: account.expired
|
|
525
530
|
? t('accountExpired')
|
|
526
|
-
: t('accountExpires', { time: formatTime(account.expiresAt) }) }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: () => { props.onLogout(account.account); }, children: t('logout') })] }), _jsx(AccountUsage, { entry: usageEntry, t: t })] }, account.account));
|
|
531
|
+
: t('accountExpires', { time: formatTime(account.expiresAt) }) }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: () => { props.onLogout(account.account); }, children: t('logout') })] }), _jsx(AccountUsage, { entry: usageEntry, t: t, busyAction: busyAction, ...props.onCheckin === undefined ? {} : { onCheckin: () => { props.onCheckin?.(account.account); } }, ...props.onTasks === undefined ? {} : { onTasks: () => { props.onTasks?.(account.account); } } })] }, account.account));
|
|
527
532
|
}), status.loggedIn && (_jsxs("div", { style: styles.catalog, children: [_jsxs("div", { style: styles.catalogHeader, children: [_jsxs("button", { type: "button", className: "dsh-subscription-disclosure", style: styles.disclosure, "aria-expanded": modelsOpen, "aria-controls": `dsh-subscription-models-${status.provider}`, onClick: toggleModelsOpen, children: [_jsx("span", { style: styles.chevron, "aria-hidden": "true", children: modelsOpen ? '▾' : '▸' }), _jsx("span", { style: styles.catalogTitle, children: t('modelsTitle') })] }), _jsxs("span", { style: styles.catalogMeta, children: [t('modelsCount', { count: models.length }), ' · ', liveCatalog ? t('modelsLive') : t('modelsBuiltIn'), hiddenCount === 0 ? '' : ` ${t('modelsHiddenCount', { count: hiddenCount })}`] }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: props.onRefreshModels, children: busyAction ? t('refreshing') : t('refresh') }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: busyAction || !connectable, title: t('modelsAddHint'), onClick: () => { setAdding((open) => !open); }, children: t('modelsAdd') })] }), adding && (_jsxs("form", { style: styles.addRow, onSubmit: (event) => {
|
|
528
533
|
event.preventDefault();
|
|
529
534
|
const id = draft.trim();
|
|
@@ -616,6 +621,66 @@ export function SubscriptionsSection(props) {
|
|
|
616
621
|
setBusyAction(false);
|
|
617
622
|
}
|
|
618
623
|
}, [call, refresh, t]);
|
|
624
|
+
/** Claim one account's daily check-in reward, then refresh the figures. */
|
|
625
|
+
const checkin = useCallback(async (provider, account) => {
|
|
626
|
+
if (call === undefined)
|
|
627
|
+
return;
|
|
628
|
+
setBusyAction(true);
|
|
629
|
+
try {
|
|
630
|
+
const result = await call('checkin', { provider, account });
|
|
631
|
+
if (!mounted.current)
|
|
632
|
+
return;
|
|
633
|
+
setMessages((previous) => ({
|
|
634
|
+
...previous,
|
|
635
|
+
[provider]: {
|
|
636
|
+
tone: 'success',
|
|
637
|
+
text: result.already
|
|
638
|
+
? t('checkinAlready', { days: result.streakDays })
|
|
639
|
+
: t('checkinClaimed', { credit: result.credit, days: result.streakDays }),
|
|
640
|
+
},
|
|
641
|
+
}));
|
|
642
|
+
await refresh();
|
|
643
|
+
}
|
|
644
|
+
catch (caught) {
|
|
645
|
+
if (mounted.current)
|
|
646
|
+
setError(t('actionFailed', { message: messageOf(caught) }));
|
|
647
|
+
}
|
|
648
|
+
finally {
|
|
649
|
+
if (mounted.current)
|
|
650
|
+
setBusyAction(false);
|
|
651
|
+
}
|
|
652
|
+
}, [call, refresh, t]);
|
|
653
|
+
/** Auto-claim one account's growth-task rewards, then refresh the figures. */
|
|
654
|
+
const runTasks = useCallback(async (provider, account) => {
|
|
655
|
+
if (call === undefined)
|
|
656
|
+
return;
|
|
657
|
+
setBusyAction(true);
|
|
658
|
+
try {
|
|
659
|
+
const result = await call('tasks', { provider, account, claim: true });
|
|
660
|
+
if (!mounted.current)
|
|
661
|
+
return;
|
|
662
|
+
setMessages((previous) => ({
|
|
663
|
+
...previous,
|
|
664
|
+
[provider]: {
|
|
665
|
+
tone: 'success',
|
|
666
|
+
text: result.claimed > 0
|
|
667
|
+
? t('tasksClaimed', { count: result.claimed, credit: result.creditEarned })
|
|
668
|
+
: result.claimable > 0
|
|
669
|
+
? t('tasksClaimable', { count: result.claimable })
|
|
670
|
+
: t('tasksNone'),
|
|
671
|
+
},
|
|
672
|
+
}));
|
|
673
|
+
await refresh();
|
|
674
|
+
}
|
|
675
|
+
catch (caught) {
|
|
676
|
+
if (mounted.current)
|
|
677
|
+
setError(t('actionFailed', { message: messageOf(caught) }));
|
|
678
|
+
}
|
|
679
|
+
finally {
|
|
680
|
+
if (mounted.current)
|
|
681
|
+
setBusyAction(false);
|
|
682
|
+
}
|
|
683
|
+
}, [call, refresh, t]);
|
|
619
684
|
const stopPolling = useCallback(() => {
|
|
620
685
|
if (timer.current !== undefined) {
|
|
621
686
|
clearInterval(timer.current);
|
|
@@ -859,5 +924,5 @@ export function SubscriptionsSection(props) {
|
|
|
859
924
|
}, [act, call]);
|
|
860
925
|
if (call === undefined)
|
|
861
926
|
return _jsx("p", { style: styles.intro, children: t('unavailable') });
|
|
862
|
-
return (_jsxs("div", { className: "dsh-subscriptions-settings", style: styles.section, children: [_jsx("style", { children: subscriptionsCss }), _jsxs("div", { style: styles.sectionHeader, children: [_jsx("h2", { style: styles.heading, children: t('nav') }), _jsx("p", { style: styles.intro, children: t('intro') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: error }), status === undefined && _jsx("p", { style: styles.statusLine, children: t('loading') }), orderedProviders.map((provider) => (_jsx(ProviderCard, { status: provider, t: t, busyAction: busyAction, ...messages[provider.provider] === undefined ? {} : { message: messages[provider.provider] }, onLogin: () => { login(provider.provider); }, onCancel: () => { cancel(provider.provider); }, onLogout: (account) => { logout(provider.provider, account); }, onSetDefault: (account) => { setDefault(provider.provider, account); }, onRefreshToken: (account) => { refreshToken(provider.provider, account); }, onRefreshModels: () => { refreshModels(provider.provider); }, onSetModelVisible: (id, visible) => { setModelVisible(provider.provider, id, visible); }, onSetModelOrder: (ids) => { setModelOrder(provider.provider, ids); }, onAddModel: (id) => { addModel(provider.provider, id); }, onRemoveModel: (id) => { removeModel(provider.provider, id); }, onRefreshUsage: () => { refreshUsage(provider.provider); }, onReload: () => { reload(provider.provider); }, ...provider.provider !== 'auto' ? {} : { autoOn, onSetAutoOn: (enabled) => { void setAuto(enabled); } } }, provider.provider)))] }));
|
|
927
|
+
return (_jsxs("div", { className: "dsh-subscriptions-settings", style: styles.section, children: [_jsx("style", { children: subscriptionsCss }), _jsxs("div", { style: styles.sectionHeader, children: [_jsx("h2", { style: styles.heading, children: t('nav') }), _jsx("p", { style: styles.intro, children: t('intro') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: error }), status === undefined && _jsx("p", { style: styles.statusLine, children: t('loading') }), orderedProviders.map((provider) => (_jsx(ProviderCard, { status: provider, t: t, busyAction: busyAction, ...messages[provider.provider] === undefined ? {} : { message: messages[provider.provider] }, onLogin: () => { login(provider.provider); }, onCancel: () => { cancel(provider.provider); }, onLogout: (account) => { logout(provider.provider, account); }, onSetDefault: (account) => { setDefault(provider.provider, account); }, onRefreshToken: (account) => { refreshToken(provider.provider, account); }, onRefreshModels: () => { refreshModels(provider.provider); }, onSetModelVisible: (id, visible) => { setModelVisible(provider.provider, id, visible); }, onSetModelOrder: (ids) => { setModelOrder(provider.provider, ids); }, onAddModel: (id) => { addModel(provider.provider, id); }, onRemoveModel: (id) => { removeModel(provider.provider, id); }, onRefreshUsage: () => { refreshUsage(provider.provider); }, onReload: () => { reload(provider.provider); }, ...provider.provider !== 'auto' ? {} : { autoOn, onSetAutoOn: (enabled) => { void setAuto(enabled); } }, ...provider.provider !== 'workbuddy' ? {} : { onCheckin: (account) => { void checkin(provider.provider, account); } }, ...provider.provider !== 'workbuddy' ? {} : { onTasks: (account) => { void runTasks(provider.provider, account); } } }, provider.provider)))] }));
|
|
863
928
|
}
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -61,6 +61,16 @@ export declare const en: {
|
|
|
61
61
|
readonly modelsAutoChainOff: "Take out of Auto";
|
|
62
62
|
readonly modelsAutoChainEmpty: "No model is enabled, so Auto has nothing to try.";
|
|
63
63
|
readonly autoSwitchLabel: "Offer Auto in the model picker";
|
|
64
|
+
readonly checkinTitle: "Daily check-in";
|
|
65
|
+
readonly checkinPending: "not checked in today";
|
|
66
|
+
readonly checkinDone: "checked in · {days}-day streak";
|
|
67
|
+
readonly checkinClaim: "Check in";
|
|
68
|
+
readonly checkinClaimed: "Checked in, +{credit} credits (streak {days}d)";
|
|
69
|
+
readonly checkinAlready: "Already checked in today (streak {days}d)";
|
|
70
|
+
readonly tasksClaim: "Tasks";
|
|
71
|
+
readonly tasksClaimed: "Claimed {count} task rewards, +{credit} credits";
|
|
72
|
+
readonly tasksClaimable: "{count} tasks ready to claim but not yet earned";
|
|
73
|
+
readonly tasksNone: "No task rewards to claim right now";
|
|
64
74
|
readonly modelsPinned: "pinned";
|
|
65
75
|
readonly usageTitle: "Plan and usage";
|
|
66
76
|
readonly usagePlan: "Plan: {plan}";
|
|
@@ -159,6 +169,16 @@ export declare const zh: {
|
|
|
159
169
|
modelsAutoChainOff: string;
|
|
160
170
|
modelsAutoChainEmpty: string;
|
|
161
171
|
autoSwitchLabel: string;
|
|
172
|
+
checkinTitle: string;
|
|
173
|
+
checkinPending: string;
|
|
174
|
+
checkinDone: string;
|
|
175
|
+
checkinClaim: string;
|
|
176
|
+
checkinClaimed: string;
|
|
177
|
+
checkinAlready: string;
|
|
178
|
+
tasksClaim: string;
|
|
179
|
+
tasksClaimed: string;
|
|
180
|
+
tasksClaimable: string;
|
|
181
|
+
tasksNone: string;
|
|
162
182
|
modelsPinned: string;
|
|
163
183
|
usageTitle: string;
|
|
164
184
|
usagePlan: string;
|
package/lib/client/locales.js
CHANGED
|
@@ -61,6 +61,16 @@ export const en = {
|
|
|
61
61
|
modelsAutoChainOff: 'Take out of Auto',
|
|
62
62
|
modelsAutoChainEmpty: 'No model is enabled, so Auto has nothing to try.',
|
|
63
63
|
autoSwitchLabel: 'Offer Auto in the model picker',
|
|
64
|
+
checkinTitle: 'Daily check-in',
|
|
65
|
+
checkinPending: 'not checked in today',
|
|
66
|
+
checkinDone: 'checked in · {days}-day streak',
|
|
67
|
+
checkinClaim: 'Check in',
|
|
68
|
+
checkinClaimed: 'Checked in, +{credit} credits (streak {days}d)',
|
|
69
|
+
checkinAlready: 'Already checked in today (streak {days}d)',
|
|
70
|
+
tasksClaim: 'Tasks',
|
|
71
|
+
tasksClaimed: 'Claimed {count} task rewards, +{credit} credits',
|
|
72
|
+
tasksClaimable: '{count} tasks ready to claim but not yet earned',
|
|
73
|
+
tasksNone: 'No task rewards to claim right now',
|
|
64
74
|
modelsPinned: 'pinned',
|
|
65
75
|
usageTitle: 'Plan and usage',
|
|
66
76
|
usagePlan: 'Plan: {plan}',
|
|
@@ -157,6 +167,16 @@ export const zh = {
|
|
|
157
167
|
modelsAutoChainOff: '从自动中移除',
|
|
158
168
|
modelsAutoChainEmpty: '没有启用任何模型,自动没有可尝试的模型。',
|
|
159
169
|
autoSwitchLabel: '在模型选择器中提供 Auto',
|
|
170
|
+
checkinTitle: '每日签到',
|
|
171
|
+
checkinPending: '今日未签到',
|
|
172
|
+
checkinDone: '已签到 · 连签 {days} 天',
|
|
173
|
+
checkinClaim: '签到',
|
|
174
|
+
checkinClaimed: '签到成功,+{credit} 积分(连签 {days} 天)',
|
|
175
|
+
checkinAlready: '今日已签到(连签 {days} 天)',
|
|
176
|
+
tasksClaim: '做任务',
|
|
177
|
+
tasksClaimed: '已领取 {count} 个任务奖励,+{credit} 积分',
|
|
178
|
+
tasksClaimable: '有 {count} 个任务可领但还未达成',
|
|
179
|
+
tasksNone: '现在没有可领的任务奖励',
|
|
160
180
|
modelsPinned: '固定',
|
|
161
181
|
usageTitle: '套餐与额度',
|
|
162
182
|
usagePlan: '套餐:{plan}',
|