dsh-plugin-subscription-oauth 0.15.0 → 0.16.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.
@@ -38,6 +38,20 @@ export interface ProviderRoute {
38
38
  streakDays: number;
39
39
  already: boolean;
40
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
+ }>;
41
55
  /** Fast and image variants for one model, when the route's discovery reported them. */
42
56
  modelVariants?(id: string): Promise<ModelVariants | undefined> | ModelVariants | undefined;
43
57
  }
@@ -113,4 +127,18 @@ export declare class SubscriptionRegistry {
113
127
  streakDays: number;
114
128
  already: boolean;
115
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
+ }>;
116
144
  }
@@ -168,4 +168,11 @@ export class SubscriptionRegistry {
168
168
  throw new ProviderError(`${provider} has no check-in`, BAD_REQUEST);
169
169
  return route.checkin(account);
170
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
+ }
171
178
  }
package/lib/auth/rpc.js CHANGED
@@ -169,6 +169,12 @@ async function runEndpoint(registry, endpoint, payload, balance) {
169
169
  throw new ProviderError('checkin requires an account', BAD_REQUEST);
170
170
  return registry.checkin(requiredProvider(payload, endpoint), account);
171
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
+ }
172
178
  case 'deepseek-balance': {
173
179
  // The DeepSeek account's balance, read on the node half through the host's own
174
180
  // account service. The browser cannot call that service directly (it sits behind
@@ -85,7 +85,7 @@ export declare function finishWorkBuddySignIn(start: WorkBuddySignInStart, fetch
85
85
  }>;
86
86
  /** The domain a token names, else the API host. */
87
87
  export declare function workbuddyDomain(token: Pick<WorkBuddyTokenSet, 'domain'>): string;
88
- /** The headers WorkBuddy's desktop app sends with a chat and its account. */
88
+ /** The headers WorkBuddy's chat gateway accepts: its own CLI's, nothing more. */
89
89
  export declare function workbuddyHeaders(token: WorkBuddyTokenSet): Record<string, string>;
90
90
  /** Whether a token is close enough to its end that a request should refresh it. */
91
91
  export declare function workbuddyStale(token: Pick<WorkBuddyTokenSet, 'expiresAt'>): boolean;
@@ -108,6 +108,28 @@ export interface WorkBuddyCheckin {
108
108
  streakDays: number;
109
109
  todayCredit: number;
110
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
+ }>;
111
133
  /** Read the account's daily check-in state, without changing anything. */
112
134
  export declare function readWorkBuddyCheckin(token: WorkBuddyTokenSet, fetchFn: typeof fetch): Promise<WorkBuddyCheckin>;
113
135
  /** Claim today's check-in reward; resolves with the credit granted and the streak. */
@@ -118,6 +140,7 @@ export declare function claimWorkBuddyCheckin(token: WorkBuddyTokenSet, fetchFn:
118
140
  /** The account's precise remaining credits, summed across its live packages. */
119
141
  export declare function readWorkBuddyCredits(token: WorkBuddyTokenSet, fetchFn: typeof fetch): Promise<{
120
142
  remaining: number;
143
+ total: number;
121
144
  nearestExpiryMs?: number;
122
145
  } | null>;
123
146
  /** Read the account's plan and metered credits for the Settings card. */
@@ -214,26 +214,21 @@ async function workbuddyEnterprise(token, who, fetchFn) {
214
214
  const pick = same.length === 1 ? same[0] : last.length === 1 ? last[0] : mine.length === 1 ? mine[0] : undefined;
215
215
  return pick && typeof pick.enterpriseId === 'string' ? pick.enterpriseId.trim() : '';
216
216
  }
217
- /** The headers WorkBuddy's desktop app sends with a chat and its account. */
217
+ /** The headers WorkBuddy's chat gateway accepts: its own CLI's, nothing more. */
218
218
  export function workbuddyHeaders(token) {
219
219
  const headers = {
220
+ 'Accept': 'application/json, text/plain, */*',
221
+ 'X-Requested-With': 'XMLHttpRequest',
222
+ 'Origin': 'https://copilot.tencent.com',
223
+ 'Referer': 'https://copilot.tencent.com/',
224
+ 'User-Agent': 'CLI/2.63.2 CodeBuddy/2.63.2',
225
+ 'Content-Type': 'application/json',
220
226
  'Authorization': `Bearer ${token.accessToken}`,
221
- 'X-User-Id': token.uid ?? '',
227
+ ...token.uid === undefined || token.uid === '' ? { 'X-No-User-Id': '1' } : { 'X-User-Id': token.uid },
228
+ ...token.enterpriseId === undefined || token.enterpriseId === '' ? { 'X-No-Enterprise-Id': '1' } : { 'X-Enterprise-Id': token.enterpriseId },
222
229
  'X-Domain': workbuddyDomain(token),
223
230
  'X-Product': 'SaaS',
224
- 'X-IDE-Type': 'WorkBuddy',
225
- 'User-Agent': `WorkBuddy/${WORKBUDDY_UA_VERSION}`,
226
- 'X-Requested-With': 'XMLHttpRequest',
227
- 'X-Agent-Intent': 'craft',
228
- 'X-Agent-Purpose': 'conversation',
229
- 'X-IDE-Name': 'WorkBuddy',
230
- 'X-IDE-Version': WORKBUDDY_UA_VERSION,
231
231
  };
232
- const enterprise = typeof token.enterpriseId === 'string' ? token.enterpriseId.trim() : '';
233
- if (enterprise !== '') {
234
- headers['X-Enterprise-Id'] = enterprise;
235
- headers['X-Tenant-Id'] = enterprise;
236
- }
237
232
  return headers;
238
233
  }
239
234
  /** Whether a token is close enough to its end that a request should refresh it. */
@@ -263,6 +258,118 @@ function workbuddyEnterpriseHeaders(token) {
263
258
  const enterprise = typeof token.enterpriseId === 'string' ? token.enterpriseId.trim() : '';
264
259
  return enterprise === '' ? {} : { 'X-Enterprise-Id': enterprise, 'X-Tenant-Id': enterprise };
265
260
  }
261
+ /** The chat request headers: the account's, plus the CLI conventions. */
262
+ function chatHeaders(token) {
263
+ return {
264
+ 'Accept': 'application/json, text/plain, */*',
265
+ 'X-Requested-With': 'XMLHttpRequest',
266
+ 'Origin': 'https://copilot.tencent.com',
267
+ 'Referer': 'https://copilot.tencent.com/',
268
+ 'User-Agent': 'CLI/2.63.2 CodeBuddy/2.63.2',
269
+ 'Content-Type': 'application/json',
270
+ 'Authorization': `Bearer ${token.accessToken}`,
271
+ ...token.uid === undefined || token.uid === '' ? { 'X-No-User-Id': '1' } : { 'X-User-Id': token.uid },
272
+ ...token.enterpriseId === undefined || token.enterpriseId === '' ? { 'X-No-Enterprise-Id': '1' } : { 'X-Enterprise-Id': token.enterpriseId },
273
+ 'X-Product': 'SaaS',
274
+ };
275
+ }
276
+ /** Ask the chat host one question, returning the decoded envelope's data. */
277
+ async function chatCall(token, method, path, body, fetchFn) {
278
+ const response = await fetchFn(`https://copilot.tencent.com${path}`, {
279
+ method,
280
+ headers: chatHeaders(token),
281
+ ...body === undefined ? {} : { body: JSON.stringify(body) },
282
+ signal: AbortSignal.timeout(20_000),
283
+ });
284
+ let env = {};
285
+ try {
286
+ env = JSON.parse(await response.text());
287
+ }
288
+ catch {
289
+ env = {};
290
+ }
291
+ const code = Number.parseInt(String(env.code ?? ''), 10) || 0;
292
+ if (!response.ok || code !== 0) {
293
+ throw new Error(`WorkBuddy ${path}: ${String(env.msg ?? `error ${code}`)}`);
294
+ }
295
+ return isRecord(env.data) ? env.data : null;
296
+ }
297
+ /** Read the account's growth tasks, parsed the way the pool plugin parses them. */
298
+ export async function listWorkBuddyTasks(token, fetchFn) {
299
+ const data = await chatCall(token, 'GET', '/v2/activity/growth/tasks', undefined, fetchFn);
300
+ if (data === null)
301
+ return [];
302
+ const raw = Array.isArray(data.tasks) ? data.tasks.filter(isRecord) : [];
303
+ const out = [];
304
+ for (const entry of raw) {
305
+ const taskCode = typeof entry.task_code === 'string' ? entry.task_code : '';
306
+ if (taskCode === '')
307
+ continue;
308
+ const num = (key) => capacity(entry[key]);
309
+ let current = num('current');
310
+ let target = num('target');
311
+ if (isRecord(entry.progress)) {
312
+ const nestedCurrent = capacity(entry.progress.current);
313
+ const nestedTarget = capacity(entry.progress.target);
314
+ if (nestedTarget > 0 || nestedCurrent > 0) {
315
+ current = nestedCurrent;
316
+ target = nestedTarget;
317
+ }
318
+ }
319
+ const acceptStatus = typeof entry.accept_status === 'string' ? entry.accept_status : '';
320
+ const claimed = acceptStatus === 'claimed';
321
+ out.push({
322
+ taskCode,
323
+ title: typeof entry.title === 'string' && entry.title !== '' ? entry.title : taskCode,
324
+ credit: num('reward_credit'),
325
+ energy: num('reward_energy'),
326
+ target,
327
+ current,
328
+ claimable: !claimed && target > 0 && current >= target,
329
+ claimed,
330
+ locked: entry.locked === true,
331
+ acceptStatus,
332
+ });
333
+ }
334
+ return out;
335
+ }
336
+ /** Accept (enrol in) tasks by code; replaying an accepted task is a safe no-op. */
337
+ export async function acceptWorkBuddyTasks(token, taskCodes, fetchFn) {
338
+ if (taskCodes.length === 0)
339
+ return;
340
+ await chatCall(token, 'POST', '/v2/activity/growth/tasks/accept', { task_codes: [...taskCodes] }, fetchFn);
341
+ }
342
+ /** Claim one task's reward; already-claimed answers zero and counts as success. */
343
+ export async function claimWorkBuddyTask(token, taskCode, fetchFn) {
344
+ const headers = {
345
+ ...workbuddyHeaders(token),
346
+ 'Accept': 'application/json, text/plain, */*',
347
+ 'Origin': 'https://www.workbuddy.cn',
348
+ 'Referer': 'https://www.workbuddy.cn/profile/growth-center',
349
+ 'x-client-platform': 'web',
350
+ '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',
351
+ };
352
+ const response = await fetchFn(`https://www.workbuddy.cn/activity/growth/tasks/${encodeURIComponent(taskCode)}/claim`, {
353
+ method: 'POST',
354
+ headers,
355
+ signal: AbortSignal.timeout(20_000),
356
+ });
357
+ let env = {};
358
+ try {
359
+ env = JSON.parse(await response.text());
360
+ }
361
+ catch {
362
+ env = {};
363
+ }
364
+ const code = Number.parseInt(String(env.code ?? ''), 10) || 0;
365
+ if (!response.ok || code !== 0) {
366
+ throw new Error(`WorkBuddy task ${taskCode}: ${String(env.msg ?? `error ${code}`)}`);
367
+ }
368
+ const data = isRecord(env.data) ? env.data : {};
369
+ if (data.already_claimed === true)
370
+ return { credit: 0, energy: 0 };
371
+ return { credit: capacity(data.credit), energy: capacity(data.energy) };
372
+ }
266
373
  /** A capacity the billing API sends, as a string or number; empty is 0. */
267
374
  function capacity(value) {
268
375
  if (value === null || value === undefined || value === '')
@@ -361,12 +468,14 @@ export async function readWorkBuddyCredits(token, fetchFn) {
361
468
  return undefined;
362
469
  };
363
470
  let remaining = 0;
471
+ let totalSize = 0;
364
472
  let nearestExpiryMs;
365
473
  let livePackages = 0;
366
474
  for (const account of accounts) {
367
475
  const num = (key) => capacity(account[key]);
368
476
  const monthly = num('CapacityType') === 4;
369
477
  const remain = monthly ? num('CycleCapacityRemain') : num('CapacityRemain');
478
+ const size = monthly ? num('CycleCapacitySize') : num('CapacitySize');
370
479
  const capped = remain < 0 ? 0 : remain;
371
480
  const cycleEndMs = parseDate(account.CycleEndTime);
372
481
  const expiresAtMs = monthly ? undefined : parseDate(account.ExpiredTime) ?? cycleEndMs;
@@ -374,6 +483,7 @@ export async function readWorkBuddyCredits(token, fetchFn) {
374
483
  continue;
375
484
  livePackages += 1;
376
485
  remaining += capped;
486
+ totalSize += size > 0 ? size : capped;
377
487
  if (expiresAtMs !== undefined && (nearestExpiryMs === undefined || expiresAtMs < nearestExpiryMs)) {
378
488
  nearestExpiryMs = expiresAtMs;
379
489
  }
@@ -382,7 +492,7 @@ export async function readWorkBuddyCredits(token, fetchFn) {
382
492
  // account, say — so the caller falls through to its own meter.
383
493
  if (livePackages === 0)
384
494
  return null;
385
- return { remaining, ...nearestExpiryMs === undefined ? {} : { nearestExpiryMs } };
495
+ return { remaining, total: totalSize, ...nearestExpiryMs === undefined ? {} : { nearestExpiryMs } };
386
496
  }
387
497
  /** The account's credits from its resource summary: the plan's used against granted. */
388
498
  function usageOfSummary(summary, plan) {
@@ -430,10 +540,15 @@ export async function readWorkBuddyUsage(token, plan, fetchFn, spend) {
430
540
  const credits = await readWorkBuddyCredits(token, fetchFn);
431
541
  if (credits !== null) {
432
542
  const window = { label: 'Credits', used: spend?.spent ?? 0 };
433
- if (credits.remaining > 0 || (spend?.spent ?? 0) > 0) {
434
- const limit = credits.remaining + (spend?.spent ?? 0);
435
- window.limit = limit;
436
- window.usedPercent = limit > 0 ? (100 * (spend?.spent ?? 0)) / limit : 0;
543
+ // The bar reads spent against the total the packages grant, not remaining-plus-spent,
544
+ // so a fresh cycle's spend shows small against its full grant.
545
+ if (credits.total > 0) {
546
+ window.limit = credits.total;
547
+ window.usedPercent = (100 * (spend?.spent ?? 0)) / credits.total;
548
+ }
549
+ else if ((spend?.spent ?? 0) > 0) {
550
+ window.limit = spend?.spent ?? 0;
551
+ window.usedPercent = 100;
437
552
  }
438
553
  if (credits.nearestExpiryMs !== undefined)
439
554
  window.resetsAt = credits.nearestExpiryMs;
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * Subscriptions settings page.
4
4
  *
@@ -315,11 +315,15 @@ function windowAmount(window) {
315
315
  }
316
316
  /** One account's metered windows and check-in, rendered under its account row. */
317
317
  function AccountUsage(props) {
318
- const { entry, t, busyAction, onCheckin } = 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: [checkin?.active === true && (_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [t('checkinTitle'), ' · ', checkin.todayCheckedIn ? t('checkinDone', { days: checkin.streakDays }) : t('checkinPending')] }), !checkin.todayCheckedIn && onCheckin !== undefined && (_jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, 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
+ 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') }))] }));
323
327
  }
324
328
  /** The plan-and-usage block for the whole route. */
325
329
  function UsageBlock(props) {
@@ -524,7 +528,7 @@ function ProviderCard(props) {
524
528
  const usageEntry = (status.usage ?? []).find((entry) => entry.account === account.account);
525
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
526
530
  ? t('accountExpired')
527
- : 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); } } })] }, 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));
528
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) => {
529
533
  event.preventDefault();
530
534
  const id = draft.trim();
@@ -646,6 +650,37 @@ export function SubscriptionsSection(props) {
646
650
  setBusyAction(false);
647
651
  }
648
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]);
649
684
  const stopPolling = useCallback(() => {
650
685
  if (timer.current !== undefined) {
651
686
  clearInterval(timer.current);
@@ -889,5 +924,5 @@ export function SubscriptionsSection(props) {
889
924
  }, [act, call]);
890
925
  if (call === undefined)
891
926
  return _jsx("p", { style: styles.intro, children: t('unavailable') });
892
- 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)))] }));
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)))] }));
893
928
  }
@@ -67,6 +67,10 @@ export declare const en: {
67
67
  readonly checkinClaim: "Check in";
68
68
  readonly checkinClaimed: "Checked in, +{credit} credits (streak {days}d)";
69
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";
70
74
  readonly modelsPinned: "pinned";
71
75
  readonly usageTitle: "Plan and usage";
72
76
  readonly usagePlan: "Plan: {plan}";
@@ -171,6 +175,10 @@ export declare const zh: {
171
175
  checkinClaim: string;
172
176
  checkinClaimed: string;
173
177
  checkinAlready: string;
178
+ tasksClaim: string;
179
+ tasksClaimed: string;
180
+ tasksClaimable: string;
181
+ tasksNone: string;
174
182
  modelsPinned: string;
175
183
  usageTitle: string;
176
184
  usagePlan: string;
@@ -67,6 +67,10 @@ export const en = {
67
67
  checkinClaim: 'Check in',
68
68
  checkinClaimed: 'Checked in, +{credit} credits (streak {days}d)',
69
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',
70
74
  modelsPinned: 'pinned',
71
75
  usageTitle: 'Plan and usage',
72
76
  usagePlan: 'Plan: {plan}',
@@ -169,6 +173,10 @@ export const zh = {
169
173
  checkinClaim: '签到',
170
174
  checkinClaimed: '签到成功,+{credit} 积分(连签 {days} 天)',
171
175
  checkinAlready: '今日已签到(连签 {days} 天)',
176
+ tasksClaim: '做任务',
177
+ tasksClaimed: '已领取 {count} 个任务奖励,+{credit} 积分',
178
+ tasksClaimable: '有 {count} 个任务可领但还未达成',
179
+ tasksNone: '现在没有可领的任务奖励',
172
180
  modelsPinned: '固定',
173
181
  usageTitle: '套餐与额度',
174
182
  usagePlan: '套餐:{plan}',
package/lib/client.js CHANGED
@@ -98,6 +98,10 @@ const en = {
98
98
  checkinClaim: "Check in",
99
99
  checkinClaimed: "Checked in, +{credit} credits (streak {days}d)",
100
100
  checkinAlready: "Already checked in today (streak {days}d)",
101
+ tasksClaim: "Tasks",
102
+ tasksClaimed: "Claimed {count} task rewards, +{credit} credits",
103
+ tasksClaimable: "{count} tasks ready to claim but not yet earned",
104
+ tasksNone: "No task rewards to claim right now",
101
105
  modelsPinned: "pinned",
102
106
  usageTitle: "Plan and usage",
103
107
  usagePlan: "Plan: {plan}",
@@ -200,6 +204,10 @@ const zh = {
200
204
  checkinClaim: "签到",
201
205
  checkinClaimed: "签到成功,+{credit} 积分(连签 {days} 天)",
202
206
  checkinAlready: "今日已签到(连签 {days} 天)",
207
+ tasksClaim: "做任务",
208
+ tasksClaimed: "已领取 {count} 个任务奖励,+{credit} 积分",
209
+ tasksClaimable: "有 {count} 个任务可领但还未达成",
210
+ tasksNone: "现在没有可领的任务奖励",
203
211
  modelsPinned: "固定",
204
212
  usageTitle: "套餐与额度",
205
213
  usagePlan: "套餐:{plan}",
@@ -1341,28 +1349,32 @@ function windowAmount$1(window$1) {
1341
1349
  }
1342
1350
  /** One account's metered windows and check-in, rendered under its account row. */
1343
1351
  function AccountUsage(props) {
1344
- const { entry, t, busyAction, onCheckin } = props;
1352
+ const { entry, t, busyAction, onCheckin, onTasks } = props;
1345
1353
  if (entry === void 0) return null;
1346
1354
  const checkin = entry.checkin;
1347
1355
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1348
1356
  style: styles.usageInline,
1349
1357
  children: [
1350
- checkin?.active === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1358
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1351
1359
  style: styles.usageLine,
1352
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1360
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1353
1361
  style: styles.modelName,
1354
- children: [
1355
- t("checkinTitle"),
1356
- " · ",
1357
- checkin.todayCheckedIn ? t("checkinDone", { days: checkin.streakDays }) : t("checkinPending")
1358
- ]
1359
- }), !checkin.todayCheckedIn && onCheckin !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1360
- type: "button",
1361
- className: "dsh-subscription-quiet",
1362
+ children: checkin?.active === true ? checkin.todayCheckedIn ? t("checkinDone", { days: checkin.streakDays }) : t("checkinPending") : ""
1363
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1362
1364
  style: styles.pushRight,
1363
- disabled: busyAction,
1364
- onClick: onCheckin,
1365
- children: t("checkinClaim")
1365
+ children: [onTasks !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1366
+ type: "button",
1367
+ className: "dsh-subscription-quiet",
1368
+ disabled: busyAction,
1369
+ onClick: onTasks,
1370
+ children: t("tasksClaim")
1371
+ }), checkin?.active === true && !checkin.todayCheckedIn && onCheckin !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1372
+ type: "button",
1373
+ className: "dsh-subscription-quiet",
1374
+ disabled: busyAction,
1375
+ onClick: onCheckin,
1376
+ children: t("checkinClaim")
1377
+ })]
1366
1378
  })]
1367
1379
  }),
1368
1380
  entry.windows.map((window$1) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1741,6 +1753,9 @@ function ProviderCard(props) {
1741
1753
  busyAction,
1742
1754
  ...props.onCheckin === void 0 ? {} : { onCheckin: () => {
1743
1755
  props.onCheckin?.(account.account);
1756
+ } },
1757
+ ...props.onTasks === void 0 ? {} : { onTasks: () => {
1758
+ props.onTasks?.(account.account);
1744
1759
  } }
1745
1760
  })]
1746
1761
  }, account.account);
@@ -2181,6 +2196,38 @@ function SubscriptionsSection(props) {
2181
2196
  refresh,
2182
2197
  t
2183
2198
  ]);
2199
+ /** Auto-claim one account's growth-task rewards, then refresh the figures. */
2200
+ const runTasks = (0, react.useCallback)(async (provider, account) => {
2201
+ if (call === void 0) return;
2202
+ setBusyAction(true);
2203
+ try {
2204
+ const result = await call("tasks", {
2205
+ provider,
2206
+ account,
2207
+ claim: true
2208
+ });
2209
+ if (!mounted.current) return;
2210
+ setMessages((previous) => ({
2211
+ ...previous,
2212
+ [provider]: {
2213
+ tone: "success",
2214
+ text: result.claimed > 0 ? t("tasksClaimed", {
2215
+ count: result.claimed,
2216
+ credit: result.creditEarned
2217
+ }) : result.claimable > 0 ? t("tasksClaimable", { count: result.claimable }) : t("tasksNone")
2218
+ }
2219
+ }));
2220
+ await refresh();
2221
+ } catch (caught) {
2222
+ if (mounted.current) setError(t("actionFailed", { message: messageOf(caught) }));
2223
+ } finally {
2224
+ if (mounted.current) setBusyAction(false);
2225
+ }
2226
+ }, [
2227
+ call,
2228
+ refresh,
2229
+ t
2230
+ ]);
2184
2231
  const stopPolling = (0, react.useCallback)(() => {
2185
2232
  if (timer.current !== void 0) {
2186
2233
  clearInterval(timer.current);
@@ -2503,6 +2550,9 @@ function SubscriptionsSection(props) {
2503
2550
  },
2504
2551
  ...provider.provider !== "workbuddy" ? {} : { onCheckin: (account) => {
2505
2552
  checkin(provider.provider, account);
2553
+ } },
2554
+ ...provider.provider !== "workbuddy" ? {} : { onTasks: (account) => {
2555
+ runTasks(provider.provider, account);
2506
2556
  } }
2507
2557
  }, provider.provider))
2508
2558
  ]