dsh-workbuddy-xdpool 1.0.1 → 1.1.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/client.js CHANGED
@@ -243,6 +243,21 @@ window.__ModuleLoader__.load({
243
243
  .dsm-workbuddy-xdpool-checkin-meta{align-items:flex-start}
244
244
  .dsm-workbuddy-xdpool-checkin-bonus{text-align:left}
245
245
  }
246
+
247
+ /* Automation panel: schedule and last-run summary, collapsed behind a switch. */
248
+ .dsm-workbuddy-xdpool-auto{margin-top:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:10px;background:var(--dsw-alias-bg-module-platform,#202126)}
249
+ .dsm-workbuddy-xdpool-auto-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
250
+ .dsm-workbuddy-xdpool-auto-title{color:var(--dsw-alias-label-primary,#e8e8ea);font-size:12px;font-weight:600}
251
+ .dsm-workbuddy-xdpool-auto-switch{padding:3px 12px;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:999px;background:transparent;color:var(--dsw-alias-label-dimmed,#8a97b5);font-size:11px;cursor:pointer;transition:color .16s,border-color .16s,background .16s}
252
+ .dsm-workbuddy-xdpool-auto-switch:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,#777);color:var(--dsw-alias-label-primary,#e8e8ea)}
253
+ .dsm-workbuddy-xdpool-auto-switch:disabled{opacity:.5;cursor:not-allowed}
254
+ .dsm-workbuddy-xdpool-auto-switch-on{border-color:#28c8b4;color:#28c8b4;background:rgba(40,200,180,.12)}
255
+ .dsm-workbuddy-xdpool-auto-hint{margin:6px 0 0;color:var(--dsw-alias-label-dimmed,#8a97b5);font-size:11px;line-height:1.5}
256
+ .dsm-workbuddy-xdpool-auto-jobs{margin-top:8px;display:flex;flex-direction:column;gap:4px}
257
+ .dsm-workbuddy-xdpool-auto-job{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dsw-alias-label-dimmed,#8a97b5)}
258
+ .dsm-workbuddy-xdpool-auto-job-name{flex:0 0 72px;color:var(--dsw-alias-label-primary,#e8e8ea)}
259
+ .dsm-workbuddy-xdpool-auto-job-when{flex:1 1 auto;font-variant-numeric:tabular-nums}
260
+ .dsm-workbuddy-xdpool-auto-job-last{flex:0 0 auto;text-align:right;font-variant-numeric:tabular-nums}
246
261
  `.trim();
247
262
  //#endregion
248
263
  //#region src/client/PoolCard.tsx
@@ -303,6 +318,35 @@ window.__ModuleLoader__.load({
303
318
  minute: "2-digit"
304
319
  }).format(new Date(ms));
305
320
  }
321
+ /**
322
+ * The automation jobs, in the order they run.
323
+ *
324
+ * The order is the contract: the report has to land before the task pass, or
325
+ * the task pass reads progress the report would have lit. `hoursOf` reads the
326
+ * matching hour list off the status document so the panel stays in step with
327
+ * whatever schedule the scheduler is actually running on.
328
+ */
329
+ const AUTOMATION_JOBS = [
330
+ "report",
331
+ "tasks",
332
+ "checkin",
333
+ "streak"
334
+ ];
335
+ /** Read one job's configured hours off the status document. */
336
+ function automationHours(status, kind) {
337
+ const automation = status.automation;
338
+ if (automation === void 0) return [];
339
+ switch (kind) {
340
+ case "report": return automation.reportHours;
341
+ case "tasks": return automation.taskHours;
342
+ case "checkin": return automation.checkinHours;
343
+ case "streak": return automation.streakHours;
344
+ }
345
+ }
346
+ /** Read one job's last-run record off the status document. */
347
+ function automationJob(status, kind) {
348
+ return status.automation?.jobs[kind];
349
+ }
306
350
  function dotColor(status) {
307
351
  return status === "ok" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #ef4444)" : "var(--dsw-alias-label-dimmed, #9aa0a6)";
308
352
  }
@@ -392,6 +436,7 @@ window.__ModuleLoader__.load({
392
436
  const [error, setError] = (0, react.useState)(void 0);
393
437
  const [busy, setBusy] = (0, react.useState)(false);
394
438
  const [cooldownBusy, setCooldownBusy] = (0, react.useState)(false);
439
+ const [automationBusy, setAutomationBusy] = (0, react.useState)(false);
395
440
  const [flash, setFlash] = (0, react.useState)(void 0);
396
441
  /** Account id whose daily claim is currently in flight. */
397
442
  const [checkinBusyId, setCheckinBusyId] = (0, react.useState)(void 0);
@@ -646,6 +691,39 @@ window.__ModuleLoader__.load({
646
691
  if (mounted.current) setError(String(cause));
647
692
  }
648
693
  };
694
+ /**
695
+ * Switch the daily-points automation on or off.
696
+ *
697
+ * The whole `automation` object is written as one key, because that is how the
698
+ * settings document stores it: the schedule fields must be carried along, or a
699
+ * save would drop the hour lists the scheduler is running on.
700
+ */
701
+ const setAutomationEnabled = async (enabled) => {
702
+ const write = settingsScope?.set;
703
+ if (write === void 0) {
704
+ setError(t?.("row.modelsSaveError", { message: "settings scope is read-only" }) ?? "settings scope is read-only");
705
+ return;
706
+ }
707
+ const existing = status?.automation;
708
+ setAutomationBusy(true);
709
+ setFlash(void 0);
710
+ try {
711
+ await write.call(settingsScope, "automation", {
712
+ ...existing === void 0 ? {} : {
713
+ checkinHours: [...existing.checkinHours],
714
+ reportHours: [...existing.reportHours],
715
+ taskHours: [...existing.taskHours],
716
+ streakHours: [...existing.streakHours]
717
+ },
718
+ enabled
719
+ });
720
+ await refresh(activeRegion);
721
+ } catch (cause) {
722
+ if (mounted.current) setError(String(cause));
723
+ } finally {
724
+ if (mounted.current) setAutomationBusy(false);
725
+ }
726
+ };
649
727
  const saveModels = async () => {
650
728
  if (draft === void 0 || status === void 0) return;
651
729
  if (enabledCount === 0) {
@@ -818,6 +896,58 @@ window.__ModuleLoader__.load({
818
896
  }) : null]
819
897
  })]
820
898
  }),
899
+ status?.automation === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
900
+ className: "dsm-workbuddy-xdpool-auto",
901
+ "aria-label": t?.("row.autoTitle") ?? "Automation",
902
+ children: [
903
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
904
+ className: "dsm-workbuddy-xdpool-auto-head",
905
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
906
+ className: "dsm-workbuddy-xdpool-auto-title",
907
+ children: t?.("row.autoTitle") ?? "Automation"
908
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
909
+ type: "button",
910
+ role: "switch",
911
+ "aria-checked": status.automation.enabled,
912
+ disabled: !settingsWritable || automationBusy,
913
+ className: `dsm-workbuddy-xdpool-auto-switch${status.automation.enabled ? " dsm-workbuddy-xdpool-auto-switch-on" : ""}`,
914
+ onClick: () => {
915
+ setAutomationEnabled(!status.automation.enabled);
916
+ },
917
+ children: automationBusy ? t?.("row.autoBusy") ?? "Saving…" : status.automation.enabled ? t?.("row.autoOn") ?? "On" : t?.("row.autoOff") ?? "Off"
918
+ })]
919
+ }),
920
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
921
+ className: "dsm-workbuddy-xdpool-auto-hint",
922
+ children: status.automation.enabled ? t?.("row.autoHintOn") ?? "Reports activity, claims task rewards and checks in once a day." : t?.("row.autoHintOff") ?? "Off: no background requests are made for you."
923
+ }),
924
+ status.automation.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
925
+ className: "dsm-workbuddy-xdpool-auto-jobs",
926
+ children: AUTOMATION_JOBS.map((kind) => {
927
+ const job = automationJob(status, kind);
928
+ const hours = automationHours(status, kind);
929
+ const label = t?.(`row.autoJob_${kind}`) ?? kind;
930
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
931
+ className: "dsm-workbuddy-xdpool-auto-job",
932
+ children: [
933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
934
+ className: "dsm-workbuddy-xdpool-auto-job-name",
935
+ children: label
936
+ }),
937
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
938
+ className: "dsm-workbuddy-xdpool-auto-job-when",
939
+ children: hours.length === 0 ? t?.("row.autoHourNone") ?? "not scheduled" : hours.map((hour) => `${String(hour).padStart(2, "0")}:00`).join(" · ")
940
+ }),
941
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
942
+ className: "dsm-workbuddy-xdpool-auto-job-last",
943
+ children: job?.lastRunDate === void 0 ? t?.("row.autoNever") ?? "not run yet" : `${job.lastRunDate} · ${job.ok}${job.failed > 0 ? `/${job.failed}` : ""}`
944
+ })
945
+ ]
946
+ }, kind);
947
+ })
948
+ }) : null
949
+ ]
950
+ }),
821
951
  flash === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
822
952
  className: "dsm-workbuddy-xdpool-note",
823
953
  children: flash
@@ -1343,7 +1473,19 @@ window.__ModuleLoader__.load({
1343
1473
  "row.resetCooldownsBusy": "Clearing…",
1344
1474
  "row.resetCooldownsDone": "Cooldowns cleared",
1345
1475
  "row.accountsRescanned": "Detected {count} account(s)",
1346
- "row.error": "Pool status unavailable: {message}"
1476
+ "row.error": "Pool status unavailable: {message}",
1477
+ "row.autoTitle": "Automation",
1478
+ "row.autoOn": "On",
1479
+ "row.autoOff": "Off",
1480
+ "row.autoBusy": "Saving…",
1481
+ "row.autoHintOn": "Reports activity, claims task rewards and checks in once a day.",
1482
+ "row.autoHintOff": "Off: no background requests are made for you.",
1483
+ "row.autoJob_report": "Activity report",
1484
+ "row.autoJob_tasks": "Task rewards",
1485
+ "row.autoJob_checkin": "Daily check-in",
1486
+ "row.autoJob_streak": "Streak bonus",
1487
+ "row.autoHourNone": "not scheduled",
1488
+ "row.autoNever": "not run yet"
1347
1489
  };
1348
1490
  const zh = {
1349
1491
  "row.title": "WorkBuddy 池(dsh-workbuddy-xdpool)",
@@ -1436,7 +1578,19 @@ window.__ModuleLoader__.load({
1436
1578
  "row.resetCooldownsBusy": "正在清除…",
1437
1579
  "row.resetCooldownsDone": "冷却已清除",
1438
1580
  "row.accountsRescanned": "检测到 {count} 个账号",
1439
- "row.error": "池状态不可用:{message}"
1581
+ "row.error": "池状态不可用:{message}",
1582
+ "row.autoTitle": "积分自动化",
1583
+ "row.autoOn": "已开启",
1584
+ "row.autoOff": "已关闭",
1585
+ "row.autoBusy": "保存中…",
1586
+ "row.autoHintOn": "每天自动活跃上报、领取任务奖励并签到。",
1587
+ "row.autoHintOff": "已关闭:不会替你发起任何后台请求。",
1588
+ "row.autoJob_report": "活跃上报",
1589
+ "row.autoJob_tasks": "任务奖励",
1590
+ "row.autoJob_checkin": "每日签到",
1591
+ "row.autoJob_streak": "连登奖励",
1592
+ "row.autoHourNone": "未排程",
1593
+ "row.autoNever": "尚未执行"
1440
1594
  };
1441
1595
  //#endregion
1442
1596
  //#region src/client/index.tsx
package/lib/index.d.ts CHANGED
@@ -102,6 +102,32 @@ export declare function classifyUpstreamError(status: number, body: string): Ups
102
102
  * form, so the pool can resume exactly when the window reopens.
103
103
  */
104
104
  export declare function parseRateLimitReset(body: string): number | undefined;
105
+ /** One growth-centre task, flattened from the upstream's loosely-shaped entry. */
106
+ interface WorkBuddyTask {
107
+ /** Upstream task code; the claim path is built from it. */
108
+ taskCode: string;
109
+ title: string;
110
+ /** Reward in credits, when the task declares one. */
111
+ credit: number;
112
+ /** Reward in energy, when the task declares one. */
113
+ energy: number;
114
+ /** Whether the task carries any reward at all. */
115
+ hasReward: boolean;
116
+ /** Progress target; 0 is a valid value (a task with no counter). */
117
+ target: number;
118
+ /** Current progress; 0 is a valid value. */
119
+ current: number;
120
+ /** Upstream enrolment state: not_accepted / accepted / claimed. */
121
+ acceptStatus: string;
122
+ /** Upstream task state, e.g. `complete`. */
123
+ status: string;
124
+ /** Progress reached its target and the reward is still outstanding. */
125
+ claimable: boolean;
126
+ /** Reward already collected. */
127
+ claimed: boolean;
128
+ /** Upstream marked the task locked (not yet reachable). */
129
+ locked: boolean;
130
+ }
105
131
  export declare class WorkBuddyUpstreamClient {
106
132
  private readonly fetchImpl;
107
133
  private readonly clientVersion;
@@ -139,6 +165,40 @@ export declare class WorkBuddyUpstreamClient {
139
165
  fetchCheckinStatus(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinStatus>;
140
166
  /** Claim today's check-in reward. The browser route guards this mutation. */
141
167
  claimDailyCheckin(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinClaim>;
168
+ /**
169
+ * Report one chat-activity event to the growth system.
170
+ *
171
+ * The body is an ARRAY holding a single `chat_request_send` event, and every
172
+ * field is filled in: a three-field minimal event is accepted with 200 and
173
+ * then silently dropped, so the full shape is load-bearing rather than
174
+ * cosmetic. `userId` is the one field the server actually keys on — without
175
+ * it the request still answers 200 and scores nothing.
176
+ *
177
+ * One report per account per day is the quota the reference panel settled on;
178
+ * a single report lights the growth streak and unlocks the `first_buddy`
179
+ * family, which is why this runs before the task-centre pass.
180
+ */
181
+ reportActivity(credential: WorkBuddyCredential, conversationId?: string): Promise<void>;
182
+ /**
183
+ * Read back the growth streak in days.
184
+ *
185
+ * This is the read-only oracle for {@link reportActivity}: a report that
186
+ * returned 200 yet left the streak untouched was silently dropped (a missing
187
+ * `userId` is the usual cause), so callers verify instead of trusting the
188
+ * status code.
189
+ *
190
+ * Two shape traps, both measured against the live upstream:
191
+ *
192
+ * - The path carries NO `/v2` prefix, unlike its sibling task endpoints under
193
+ * `/v2/activity/growth/*`. Asking for the `/v2` form does not 404; it
194
+ * answers with a body that carries no `streak` object at all.
195
+ * - The counter is nested as `data.streak.days`, not `data.days`. Reading the
196
+ * flat field yields a constant 0, which would make every successful report
197
+ * look like a silent drop.
198
+ *
199
+ * Returns 0 only when the field is genuinely absent.
200
+ */
201
+ growthStreakDays(credential: WorkBuddyCredential): Promise<number>;
142
202
  /** Legacy thin wrapper kept for `status`/`doctor`: returns raw envelope data. */
143
203
  credits(credential: WorkBuddyCredential): Promise<{
144
204
  ok: true;
@@ -147,6 +207,40 @@ export declare class WorkBuddyUpstreamClient {
147
207
  ok: false;
148
208
  message: string;
149
209
  }>;
210
+ /**
211
+ * Fetch the growth task list for one account.
212
+ *
213
+ * The upstream answers `data.tasks[]`, and `claimable` is derived locally —
214
+ * the upstream does not mark it. Only a task whose progress reached its
215
+ * target and that is not already claimed counts as eligible.
216
+ */
217
+ listTasks(credential: WorkBuddyCredential): Promise<readonly WorkBuddyTask[]>;
218
+ /**
219
+ * Accept (enrol in) tasks by code.
220
+ *
221
+ * Accepting is the "sign up" half: it produces no progress by itself, and the
222
+ * upstream answers success for an already-accepted task, so replaying this is
223
+ * safe. Progress is lit by real activity (a chat, an activity report).
224
+ */
225
+ acceptTasks(credential: WorkBuddyCredential, taskCodes: readonly string[]): Promise<void>;
226
+ /**
227
+ * Claim one task's reward.
228
+ *
229
+ * Two details differ from list/accept and are load-bearing:
230
+ *
231
+ * - The task code rides the PATH, not the body.
232
+ * - It is served by the web origin, not the chat host, and only when the
233
+ * request carries the growth-centre Origin/Referer plus
234
+ * `x-client-platform: web`. The chat host's `/reward/claim` path does not
235
+ * exist and answers 400 "task not completed".
236
+ *
237
+ * A repeat claim answers `already_claimed` with zero credit, which is treated
238
+ * as success so the caller can stay idempotent.
239
+ */
240
+ claimTaskReward(credential: WorkBuddyCredential, taskCode: string): Promise<{
241
+ credit: number;
242
+ energy: number;
243
+ }>;
150
244
  }
151
245
  //#endregion
152
246
  //#region src/accounts.d.ts
@@ -541,6 +635,161 @@ interface WorkBuddyAdapter {
541
635
  */
542
636
  export declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
543
637
  //#endregion
638
+ //#region src/scheduler.d.ts
639
+ /** Which jobs the automation runs, and when. */
640
+ interface AutomationOptions {
641
+ /** Master switch; false stops every job. */
642
+ enabled?: boolean;
643
+ /** Hour list for the daily check-in job. */
644
+ checkinHours?: readonly number[];
645
+ /** Hour list for the task-centre job (accept + claim). */
646
+ taskHours?: readonly number[];
647
+ /** Hour list for the activity-report job. */
648
+ reportHours?: readonly number[];
649
+ /** Hour list for the streak-redemption job. */
650
+ streakHours?: readonly number[];
651
+ /** Per-account serial delay, in milliseconds. */
652
+ accountDelayMs?: number;
653
+ /** Override the clock, for tests. */
654
+ now?: () => Date;
655
+ /** Logger; defaults to a no-op so tests stay quiet. */
656
+ logger?: SchedulerLogger;
657
+ }
658
+ /** Logger surface, kept structural so any host logger fits. */
659
+ interface SchedulerLogger {
660
+ info?(...args: unknown[]): void;
661
+ warn?(...args: unknown[]): void;
662
+ }
663
+ /** One job's last-run record, surfaced on the status document. */
664
+ interface AutomationJobState {
665
+ /** `YYYY-MM-DD` of the last completed run, or undefined if it never ran. */
666
+ lastRunDate?: string;
667
+ /** Epoch ms of the last completed run. */
668
+ lastRunAtMs?: number;
669
+ /** Accounts that completed without throwing. */
670
+ ok: number;
671
+ /** Accounts that threw (each one skipped, the run continued). */
672
+ failed: number;
673
+ /** Credits claimed by the task job on the last run. */
674
+ credit: number;
675
+ /** Energy claimed by the task job on the last run. */
676
+ energy: number;
677
+ /** Tasks claimed by the task job on the last run. */
678
+ claimed: number;
679
+ /** Human-readable summary of the last run. */
680
+ message?: string;
681
+ }
682
+ /** Automation snapshot for the status document and the card. */
683
+ interface AutomationStatus {
684
+ enabled: boolean;
685
+ /** Whether the loop is running. */
686
+ running: boolean;
687
+ checkinHours: readonly number[];
688
+ taskHours: readonly number[];
689
+ reportHours: readonly number[];
690
+ streakHours: readonly number[];
691
+ jobs: {
692
+ checkin: AutomationJobState;
693
+ report: AutomationJobState;
694
+ tasks: AutomationJobState;
695
+ streak: AutomationJobState;
696
+ };
697
+ /** Claimable tasks seen on the most recent task pass, across accounts. */
698
+ claimableSeen: number;
699
+ }
700
+ /**
701
+ * The points automation.
702
+ *
703
+ * Owns a single timer loop. Construction is inert — nothing runs until
704
+ * {@link start}, and {@link stop} is idempotent so a plugin teardown that fires
705
+ * twice is harmless.
706
+ */
707
+ declare class WorkBuddyScheduler {
708
+ private readonly pool;
709
+ private readonly client;
710
+ private readonly logger;
711
+ private readonly now;
712
+ private readonly delayMs;
713
+ private enabled;
714
+ private checkinHours;
715
+ private taskHours;
716
+ private reportHours;
717
+ private streakHours;
718
+ private timer;
719
+ private running;
720
+ /** Guards against a slow run overlapping the next tick. */
721
+ private busy;
722
+ /**
723
+ * Set once {@link stop} is called.
724
+ *
725
+ * Deliberately false before `start`: the loop is not running yet, but a
726
+ * manual `tick` must still work. `stop` is what makes a run abandon the
727
+ * accounts it has not reached yet.
728
+ */
729
+ private stopped;
730
+ private readonly states;
731
+ private claimableSeen;
732
+ constructor(pool: WorkBuddyAccountPool, client: WorkBuddyUpstreamClient, options?: AutomationOptions);
733
+ /** Apply a new configuration; safe to call while running. */
734
+ applyConfig(options: AutomationOptions): void;
735
+ /** Hours for one job, used by the loop and the status document. */
736
+ private hoursOf;
737
+ /** Start the loop. Idempotent. */
738
+ start(): void;
739
+ /** Stop the loop. Idempotent, and safe before `start`. */
740
+ stop(): void;
741
+ /** Snapshot for the status document. */
742
+ status(): AutomationStatus;
743
+ /**
744
+ * One poll: run every due job, serially.
745
+ *
746
+ * Serial by design — the jobs share the same accounts and the upstream
747
+ * rate-limits per account, so overlapping passes would only trip that limit.
748
+ * A job that throws is recorded and the loop continues.
749
+ */
750
+ private tick;
751
+ /** Run one job against every eligible account and record the outcome. */
752
+ private runJob;
753
+ /** Compose the one-line summary shown on the card. */
754
+ private summarise;
755
+ /**
756
+ * Accounts to run against, in pool order.
757
+ *
758
+ * Disabled accounts are excluded here rather than filtered by the caller so a
759
+ * card switch takes effect on the next pass without any event plumbing.
760
+ */
761
+ private accountsInOrder;
762
+ /**
763
+ * Send one activity report, then verify it landed.
764
+ *
765
+ * The upstream answers 200 even when it drops the event, so the streak is
766
+ * read back as the oracle: `days > 0` means it counted. A failed read-back is
767
+ * logged and treated as a suspicious result, never as a retry — the report is
768
+ * idempotent per day, and hammering it is exactly what the one-a-day quota
769
+ * exists to avoid.
770
+ */
771
+ private reportOne;
772
+ /**
773
+ * The task-centre pass for one account.
774
+ *
775
+ * Order matters: enrich first (enrol in everything open), then claim. Both
776
+ * halves are idempotent — accepting an already-accepted task succeeds, and a
777
+ * repeat claim answers `already_claimed` — so a pass that dies halfway is
778
+ * safe to replay on the next tick.
779
+ */
780
+ private runTasks;
781
+ /**
782
+ * Streak redemption and lottery.
783
+ *
784
+ * Left as a deliberate no-op placeholder: the tier/lottery endpoints need
785
+ * their own round of probing against the live upstream before they can be
786
+ * wired safely, and a wrong call here could burn a redemption. The job slot,
787
+ * scheduling and status plumbing already exist, so filling it in is a
788
+ * self-contained change.
789
+ */
790
+ private redeemStreak;
791
+ }
792
+ //#endregion
544
793
  //#region src/status.d.ts
545
794
  /** One account's status row. */
546
795
  interface AccountStatus {
@@ -767,6 +1016,51 @@ interface PoolWebStatus {
767
1016
  running: boolean;
768
1017
  baseUrl?: string;
769
1018
  };
1019
+ /** Daily-points automation state, so the card can show what ran and when. */
1020
+ automation: PoolWebAutomation;
1021
+ }
1022
+ /** One automation job's last run, as shown on the card. */
1023
+ interface PoolWebAutomationJob {
1024
+ /** `YYYY-MM-DD` of the last run in this process, if it has run. */
1025
+ lastRunDate?: string;
1026
+ /** Accounts that finished without error on the last run. */
1027
+ ok: number;
1028
+ /** Accounts that failed on the last run (each one skipped, the run continued). */
1029
+ failed: number;
1030
+ /** Credits claimed by the task job on the last run. */
1031
+ credit: number;
1032
+ /** Energy claimed by the task job on the last run. */
1033
+ energy: number;
1034
+ /** Tasks claimed by the task job on the last run. */
1035
+ claimed: number;
1036
+ /** One-line summary of the last run. */
1037
+ message?: string;
1038
+ }
1039
+ /**
1040
+ * Automation block on the status document.
1041
+ *
1042
+ * Carries the schedule and each job's last outcome so the card can answer
1043
+ * "is it on, when does it run, and what did it last do" without reaching into
1044
+ * the scheduler itself.
1045
+ */
1046
+ interface PoolWebAutomation {
1047
+ /** Master switch, mirrored from the saved config. */
1048
+ enabled: boolean;
1049
+ /** Whether the loop is currently running. */
1050
+ running: boolean;
1051
+ /** Configured hours per job, so the card can show the schedule. */
1052
+ checkinHours: readonly number[];
1053
+ reportHours: readonly number[];
1054
+ taskHours: readonly number[];
1055
+ streakHours: readonly number[];
1056
+ jobs: {
1057
+ checkin: PoolWebAutomationJob;
1058
+ report: PoolWebAutomationJob;
1059
+ tasks: PoolWebAutomationJob;
1060
+ streak: PoolWebAutomationJob;
1061
+ };
1062
+ /** Claimable tasks seen on the most recent task pass, across accounts. */
1063
+ claimableSeen: number;
770
1064
  }
771
1065
  /**
772
1066
  * The two gateways, matching the provider ids the host registers. `cn` is the
@@ -836,6 +1130,12 @@ export interface Config {
836
1130
  */
837
1131
  modelSelectionCn?: ModelSelectionConfig;
838
1132
  modelSelectionGlobal?: ModelSelectionConfig;
1133
+ /**
1134
+ * Daily-points automation. Absent means off: the scheduler makes upstream
1135
+ * calls on the user behalf, so it stays opt-in rather than surprising a
1136
+ * fresh install with background traffic.
1137
+ */
1138
+ automation?: AutomationConfig;
839
1139
  }
840
1140
  /** One region's saved model selection. */
841
1141
  export interface ModelSelectionConfig {
@@ -843,8 +1143,42 @@ export interface ModelSelectionConfig {
843
1143
  imageModelIds?: string[];
844
1144
  contextBudgets?: Record<string, number>;
845
1145
  }
1146
+ /**
1147
+ * Daily-points automation.
1148
+ *
1149
+ * Absent means off: the scheduler makes upstream calls on the user behalf, so
1150
+ * it stays opt-in rather than surprising a fresh install with background
1151
+ * traffic. Each job carries its own hour list so the passes can be spread out
1152
+ * (or pushed off-peak) without disabling any of them.
1153
+ *
1154
+ * Ordering note: the report job must run before the task job. A report is what
1155
+ * lights the growth streak and unlocks the `first_buddy` family, so a task pass
1156
+ * that ran first would read counters before they could have moved.
1157
+ */
1158
+ export interface AutomationConfig {
1159
+ /** Master switch for every automation job. Absent reads as false. */
1160
+ enabled?: boolean;
1161
+ /** Hours (local, 0-23) at which the daily check-in runs. */
1162
+ checkinHours?: number[];
1163
+ /** Hours at which the activity report runs. Keep ahead of `taskHours`. */
1164
+ reportHours?: number[];
1165
+ /** Hours at which tasks are enrolled in and claimed. */
1166
+ taskHours?: number[];
1167
+ /** Hours at which streak redemption runs. */
1168
+ streakHours?: number[];
1169
+ /** How long an account rests after its credits run out, in milliseconds. */
1170
+ exhaustCooldownMs?: number;
1171
+ }
846
1172
  /** Upper bound the card offers as the "default" context window, in tokens. */
847
1173
  export declare const DEFAULT_CONTEXT_BUDGET = 200000;
1174
+ /**
1175
+ * Fold a saved automation block into scheduler options.
1176
+ *
1177
+ * Absent means off, stated once here so every caller agrees: the card writes
1178
+ * `enabled` as a real boolean, and a config that never touched the section must
1179
+ * not accidentally arm background upstream traffic.
1180
+ */
1181
+ export declare function automationOptions(automation: AutomationConfig | undefined): AutomationOptions;
848
1182
  /** Settings key holding one region's saved selection. */
849
1183
  export declare const modelSelectionKeyFor: (region: 'cn' | 'global') => string;
850
1184
  /**
@@ -869,6 +1203,8 @@ export interface WorkBuddyPoolApi {
869
1203
  rescan(): Promise<number>;
870
1204
  status(includeCredits?: boolean): Promise<Awaited<ReturnType<typeof buildStatus>>>;
871
1205
  resetCooldowns(): void;
1206
+ /** Daily-points automation; assembled with the core, inert until started. */
1207
+ scheduler: WorkBuddyScheduler;
872
1208
  }
873
1209
  /** The live API, or undefined when the plugin has not applied yet. */
874
1210
  export declare function currentApi(): WorkBuddyPoolApi | undefined;
@@ -885,6 +1221,7 @@ export declare function setApi(next: WorkBuddyPoolApi | undefined): void;
885
1221
  */
886
1222
  export declare function createCore(logger?: {
887
1223
  warn(...args: unknown[]): void;
1224
+ info?(...args: unknown[]): void;
888
1225
  }): {
889
1226
  pool: WorkBuddyAccountPool;
890
1227
  catalogs: {
@@ -892,6 +1229,7 @@ export declare function createCore(logger?: {
892
1229
  readonly global: WorkBuddyCatalog;
893
1230
  };
894
1231
  client: WorkBuddyUpstreamClient;
1232
+ scheduler: WorkBuddyScheduler;
895
1233
  };
896
1234
  /**
897
1235
  * Start the loopback endpoint, register the `workbuddy-xdpool` provider, and