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.
@@ -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);
@@ -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
  }
@@ -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): Promise<{
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
- return { plan: plan ?? 'Enterprise', windows: [enterprise] };
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 { 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
  *
@@ -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
- return (_jsxs("div", { style: styles.usageInline, children: [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') }))] }));
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
  }
@@ -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;
@@ -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}',