dsh-workbuddy-xdpool 1.0.1 → 1.2.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/index.d.ts CHANGED
@@ -3,6 +3,167 @@ import { Api, Model } from "@earendil-works/pi-ai";
3
3
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
4
4
  import { Context, Context as Context$1 } from "@deepseek-ai/cordis";
5
5
  import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
6
+ //#region src/task-events.d.ts
7
+ /**
8
+ * The event chains that light up the client-scored daily tasks.
9
+ *
10
+ * The task board advertises things like "open an app", "use five templates" or
11
+ * "summon a platform expert". Those read as actions only a human at the desktop
12
+ * app can take, but the scorer does not check the app — it checks the event
13
+ * stream. Replaying the same events, with the same desktop fingerprint, scores
14
+ * the task. Every chain here was measured against the live upstream.
15
+ *
16
+ * Kept apart from the scheduler because a chain is pure data: the scheduler
17
+ * decides *when* to send one, this module decides *what* one is. A few chains
18
+ * need a real server-side id first (the expert family), which is why the builder
19
+ * is allowed to be async and to call the upstream.
20
+ */
21
+ /**
22
+ * How a chain reaches the upstream.
23
+ *
24
+ * There are three fingerprint families and the scorer keys different tasks to
25
+ * different ones, so the transport is part of the chain rather than a detail of
26
+ * the sender: `Buddy_App` needs the desktop fingerprint, while `Library_read` is
27
+ * only scored when it arrives with the WEB fingerprint.
28
+ */
29
+ type TaskEventTransport = 'desktop' | 'web';
30
+ /** A ready-to-send chain, plus the channel it must go out on. */
31
+ interface TaskEventChain {
32
+ transport: TaskEventTransport;
33
+ /** Desktop chains: the event array. */
34
+ events?: readonly Record<string, unknown>[];
35
+ /** Web chains: the single page event to report. */
36
+ web?: {
37
+ eventCode: string;
38
+ pageUrl: string;
39
+ elementId: string;
40
+ elementName: string;
41
+ };
42
+ }
43
+ /**
44
+ * The app the buddy chain enters.
45
+ *
46
+ * One chain lights up two tasks: `Buddy_App` (open any app) and `Buddy_App_QQ`
47
+ * (the QQ-specific one), because this is a QQ-hosted app. Measured 0/1 → 1/1 on
48
+ * both from a single chain.
49
+ */
50
+ export declare const BUDDY_APP_ID = "cb_y5Dy46tPQGGWtueMxXbe";
51
+ export declare const BUDDY_APP_NAME = "企鹅教师助手";
52
+ /**
53
+ * The buddy-app chain (two tasks).
54
+ *
55
+ * Five clicks in the order a user would make them: discover the app, see it,
56
+ * enter it, confirm the account link, skip the second binding step.
57
+ */
58
+ export declare function buddyAppChain(): TaskEventChain;
59
+ /**
60
+ * The design-canvas chain (`create_canvas`, +300 — the joint largest reward).
61
+ *
62
+ * The two canvas events ride the same metrics channel as everything else, so no
63
+ * real canvas is ever created; the chat chain in front supplies the ids they
64
+ * reference. Measured 1/1 on three accounts.
65
+ */
66
+ export declare function canvasChain(): TaskEventChain;
67
+ /**
68
+ * The scheduled-task event (`automation_1`).
69
+ *
70
+ * One event is the whole chain — measured 1/1 on two accounts. The name is only
71
+ * for the server's own records, so a generated one is fine.
72
+ */
73
+ export declare function automationChain(): TaskEventChain;
74
+ /**
75
+ * The plain chat chain (`RichMeow_Chat`, and the base of the template chain).
76
+ *
77
+ * Measured: this chain alone lights `RichMeow_Chat`.
78
+ */
79
+ export declare function chatChain(): TaskEventChain;
80
+ /**
81
+ * The "same as this case" chain (`playbook_prompt`).
82
+ *
83
+ * The scorer watches `playbook_prompt_send` — sending the prompt that the
84
+ * inspiration case pre-fills — not the card impression or the button click, so
85
+ * the whole click path is replayed for realism but the send is what counts.
86
+ */
87
+ export declare function playbookChain(caseId?: string, caseName?: string): TaskEventChain;
88
+ /** The inspiration case the reference panel sends a prompt for. */
89
+ export declare const PLAYBOOK_CASE_ID = "pm-gtm-launch-plan";
90
+ export declare const PLAYBOOK_CASE_NAME = "新产品上市 GTM 发布计划一页纸";
91
+ /**
92
+ * The five templates the reference panel cycles through, as `[id, name]`.
93
+ *
94
+ * The upstream does not check that these templates exist — only that five
95
+ * distinct `template_used` events arrive — so they are the reference set.
96
+ */
97
+ export declare const TEMPLATE_PRESETS: readonly (readonly [string, string])[];
98
+ /**
99
+ * One "created a task from a template" chain (`template_5`, +100 for five).
100
+ *
101
+ * Each group is a chat chain (which supplies the ids the template events join
102
+ * on) plus `agent_task_created_with_template` and `template_used`. Measured:
103
+ * five groups in one report scored 5/5.
104
+ */
105
+ export declare function templateChain(templateId: string, templateName: string): TaskEventChain;
106
+ /** Every template group, ready to send in order. */
107
+ export declare function templateChains(): readonly TaskEventChain[];
108
+ /**
109
+ * The library-introduction click (`Library_read`).
110
+ *
111
+ * Scored on the WEB fingerprint — the same event posted with the desktop
112
+ * fingerprint scores nothing — so this chain returns a web transport and the
113
+ * scheduler routes it through `reportWebEvent`.
114
+ */
115
+ export declare function libraryReadChain(): TaskEventChain;
116
+ /** The document the library click is reported against. */
117
+ export declare const LIBRARY_DOC_URL = "https://www.workbuddy.cn/space/d/o0KWYeynteVv06UnAZqIFm";
118
+ /** The theme key `Hp_Appearance` is scored on (和平精英激战金秋). */
119
+ export declare const APPEARANCE_THEME_KEY = "theme-tkmw7j";
120
+ /** The skill `skill_1` is scored on. */
121
+ export declare const SKILL_ID = "skill_2097350077599879168";
122
+ export declare const SKILL_NAME = "润泽小馆·日报撰写";
123
+ /** The 腾讯轻量云 expert `Expert_lighthouse` is scored on. */
124
+ export declare const LIGHTHOUSE_EXPERT_ID = "ex_2cvvUZQhDyeJ";
125
+ /**
126
+ * Build the `skill_1` chain from a REAL conversation.
127
+ *
128
+ * Unlike the template and canvas chains, this one is verified against the
129
+ * conversation it names, so the caller must first open a real chat and hand the
130
+ * server-side ids in.
131
+ */
132
+ export declare function skillChain(conversationId: string, requestId: string): TaskEventChain;
133
+ /** The theme-apply event `Hp_Appearance` is scored on. */
134
+ export declare function appearanceChain(themeKey?: string): TaskEventChain;
135
+ /** One expert from the platform's marketplace, as the summon chains need it. */
136
+ interface MarketExpert {
137
+ expertId: string;
138
+ expertType: string;
139
+ displayName: string;
140
+ profession: string;
141
+ version: string;
142
+ categories: readonly string[];
143
+ }
144
+ /**
145
+ * The three "summon an expert" events (`expert_summon_click` and friends).
146
+ *
147
+ * Paid before the conversation, in the order the app emits them.
148
+ */
149
+ export declare function expertSummonEvents(expert: MarketExpert): Record<string, unknown>[];
150
+ /** `mode` for the `expert_actual_use` payload; the scorer checks it. */
151
+ type ExpertUseMode = 'craft' | 'LOCAL';
152
+ /**
153
+ * The "an expert really answered" event, which is what the expert tasks count.
154
+ *
155
+ * The `requestId` must be the SERVER's id for a real chat: a made-up one scores
156
+ * nothing, because the scorer looks the conversation up.
157
+ */
158
+ export declare function expertActualUseEvent(expert: MarketExpert, conversationId: string, requestId: string, mode?: ExpertUseMode): Record<string, unknown>;
159
+ /**
160
+ * The chat chain for an expert conversation.
161
+ *
162
+ * `agent_task_created` carries the expert fields the scorer reads to attribute
163
+ * the conversation to that expert.
164
+ */
165
+ export declare function expertChatEvents(expert: MarketExpert, conversationId: string, requestId: string): Record<string, unknown>[];
166
+ //#endregion
6
167
  //#region src/upstream.d.ts
7
168
  /** Upstream failure classes the shim maps onto distinct HTTP answers. */
8
169
  type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'not_found' | 'server' | 'client';
@@ -102,6 +263,120 @@ export declare function classifyUpstreamError(status: number, body: string): Ups
102
263
  * form, so the pool can resume exactly when the window reopens.
103
264
  */
104
265
  export declare function parseRateLimitReset(body: string): number | undefined;
266
+ /** One growth-centre task, flattened from the upstream's loosely-shaped entry. */
267
+ /** One streak tier and what redeeming it pays. */
268
+ interface WorkBuddyStreakTier {
269
+ /** Tier key, e.g. `7d`. */
270
+ tier: string;
271
+ /** Login days the tier needs. */
272
+ days: number;
273
+ credit: number;
274
+ energy: number;
275
+ cards: number;
276
+ /** Lottery draws the tier grants. */
277
+ chances: number;
278
+ /** `locked` / `unlocked` / `claimed`. */
279
+ status: string;
280
+ }
281
+ /** The growth streak as a whole: progress, tiers, makeup cards. */
282
+ interface WorkBuddyStreakStatus {
283
+ /** Current consecutive active days. */
284
+ days: number;
285
+ /** Days active this month. */
286
+ monthTotalDays: number;
287
+ /** Next tier key, e.g. `7d`. */
288
+ nextTier: string;
289
+ /** Days still needed for `nextTier`. */
290
+ nextTierRemaining: number;
291
+ /** Makeup cards in hand. */
292
+ makeupCards: number;
293
+ tiers: readonly WorkBuddyStreakTier[];
294
+ }
295
+ /** One buddy trip state. */
296
+ interface WorkBuddyTravelState {
297
+ /** `idle` (can depart) / `traveling` / `arrived` (can claim). */
298
+ state: string;
299
+ /** Trip id, required to claim an arrived trip. */
300
+ recordId: number;
301
+ /** The once-a-day depart limit has been used. */
302
+ dailyLimitReached: boolean;
303
+ /** Credits an arrived trip pays. */
304
+ rewardCredit: number;
305
+ }
306
+ interface WorkBuddyTask {
307
+ /** Upstream task code; the claim path is built from it. */
308
+ taskCode: string;
309
+ title: string;
310
+ /** Reward in credits, when the task declares one. */
311
+ credit: number;
312
+ /** Reward in energy, when the task declares one. */
313
+ energy: number;
314
+ /** Whether the task carries any reward at all. */
315
+ hasReward: boolean;
316
+ /** Progress target; 0 is a valid value (a task with no counter). */
317
+ target: number;
318
+ /** Current progress; 0 is a valid value. */
319
+ current: number;
320
+ /** Upstream enrolment state: not_accepted / accepted / claimed. */
321
+ acceptStatus: string;
322
+ /** Upstream task state, e.g. `complete`. */
323
+ status: string;
324
+ /** Progress reached its target and the reward is still outstanding. */
325
+ claimable: boolean;
326
+ /** Reward already collected. */
327
+ claimed: boolean;
328
+ /** Upstream marked the task locked (not yet reachable). */
329
+ locked: boolean;
330
+ }
331
+ /**
332
+ * Flatten one upstream task entry.
333
+ *
334
+ * Progress is reported two ways depending on the task: flat `current`/`target`
335
+ * fields, or a nested `progress: {current, target}`. The nested form wins when
336
+ * it carries anything, because a task that reports both puts the live counter
337
+ * there. Entries without a usable `task_code` are dropped — without one the
338
+ * claim path cannot be built.
339
+ */
340
+ /**
341
+ * The event chain that scores the two Buddy-app tasks.
342
+ *
343
+ * Both tasks accept the same chain, and the chain is a pure value: building it
344
+ * needs no client, so callers (and tests) can hold one without a live upstream.
345
+ *
346
+ * The task text says "upgrade to the desktop client and open it from the app
347
+ * launcher". The scorer does not watch the UI — it watches this event sequence
348
+ * with the desktop fingerprint, which is why the sequence is what gets sent.
349
+ *
350
+ * Measured against the live upstream: progress 0/1 → 1/1 claimable in ~8s.
351
+ */
352
+ /**
353
+ * A complete "desktop client ran a request successfully" event chain.
354
+ *
355
+ * Six events in the order the real client emits them: task created, message
356
+ * send, request send, message response, message status, request response.
357
+ * Several tasks are scored off this chain (or one that embeds it), because what
358
+ * they measure is "a real request completed", which the client only proves
359
+ * through this exact sequence.
360
+ *
361
+ * Measured: this chain alone lights up `RichMeow_Chat`.
362
+ */
363
+ export declare function desktopChatEvents(conversationId: string, requestId: string, messageId: string, modelId?: string, modelName?: string): Record<string, unknown>[];
364
+ /**
365
+ * A chat chain plus the two canvas events that score `create_canvas`.
366
+ *
367
+ * Worth +300, the joint largest task on the board. The canvas events ride
368
+ * the same metrics channel as everything else, so no real canvas is needed.
369
+ *
370
+ * Measured: three accounts scored 1/1 from this sequence.
371
+ */
372
+ export declare function desktopCanvasEvents(conversationId: string, requestId: string): Record<string, unknown>[];
373
+ /**
374
+ * The single event that scores `automation_1` (a scheduled task was created).
375
+ *
376
+ * Measured: two accounts lit it with this event alone.
377
+ */
378
+ export declare function desktopAutomationCreatedEvent(name: string): Record<string, unknown>;
379
+ export declare function buddyAppEvents(buddyId: string, buddyName: string): Record<string, unknown>[];
105
380
  export declare class WorkBuddyUpstreamClient {
106
381
  private readonly fetchImpl;
107
382
  private readonly clientVersion;
@@ -139,6 +414,195 @@ export declare class WorkBuddyUpstreamClient {
139
414
  fetchCheckinStatus(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinStatus>;
140
415
  /** Claim today's check-in reward. The browser route guards this mutation. */
141
416
  claimDailyCheckin(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinClaim>;
417
+ /**
418
+ * Public fingerprint fields every desktop event carries.
419
+ *
420
+ * The upstream scores a desktop-fingerprinted task only when the event looks
421
+ * like it came from the desktop client: the same field set, the same stable
422
+ * device ids, the same build. A partial map is accepted with 200 and scores
423
+ * nothing, so these are copied wholesale rather than trimmed.
424
+ *
425
+ * `machineId`/`sessionId` are DERIVED from the account uid, never random: a
426
+ * new device id on every call is itself a signal that the traffic is not a
427
+ * real client.
428
+ */
429
+ desktopFingerprint(credential: WorkBuddyCredential): Record<string, unknown>;
430
+ /**
431
+ * Send desktop-fingerprinted events to the growth system.
432
+ *
433
+ * The body is an ARRAY of events, and every event carries the full desktop
434
+ * fingerprint plus its own business fields. Different tasks recognise
435
+ * different fingerprint families (CLI / desktop / web), which is why this is
436
+ * separate from {@link reportActivity}: they are not interchangeable.
437
+ *
438
+ * Business fields win over the fingerprint, so a caller can override a device
439
+ * id to align with a real install.
440
+ */
441
+ reportDesktopEvents(credential: WorkBuddyCredential, events: readonly Record<string, unknown>[]): Promise<void>;
442
+ /**
443
+ * Report one chat-activity event to the growth system.
444
+ *
445
+ * The body is an ARRAY holding a single `chat_request_send` event, and every
446
+ * field is filled in: a three-field minimal event is accepted with 200 and
447
+ * then silently dropped, so the full shape is load-bearing rather than
448
+ * cosmetic. `userId` is the one field the server actually keys on — without
449
+ * it the request still answers 200 and scores nothing.
450
+ *
451
+ * One report per account per day is the quota the reference panel settled on;
452
+ * a single report lights the growth streak and unlocks the `first_buddy`
453
+ * family, which is why this runs before the task-centre pass.
454
+
455
+ /**
456
+ * Send a WEB-fingerprinted event.
457
+ *
458
+ * A third fingerprint family, alongside CLI and desktop: a browser shape
459
+ * posted to the web origin with x-client-platform: web. Page-behaviour
460
+ * tasks such as `Library_read` are scored on it. Measured:
461
+ * `library_doc_intro_click` scored about four seconds after landing.
462
+ */
463
+ reportWebEvent(credential: WorkBuddyCredential, eventCode: string, pageUrl: string, elementId: string, elementName: string): Promise<void>;
464
+ /**
465
+ * Apply an appearance theme on the account.
466
+ *
467
+ * The theme task is scored on the `appearance_skin_apply` event, not on this
468
+ * call — but the event alone is not enough either. The pair is what a real
469
+ * client produces: it PATCHes the account's selected skin, then reports the
470
+ * event as the settings page closes. Measured on the reference panel after
471
+ * the earlier "the API alone does not score" reading was corrected.
472
+ */
473
+ setAppearanceTheme(credential: WorkBuddyCredential, resourceKey: string): Promise<void>;
474
+ /**
475
+ * The platform's expert marketplace.
476
+ *
477
+ * Needed before any expert can be summoned: the scorer verifies that the
478
+ * expert id exists on the platform, so a made-up id scores nothing. The
479
+ * response carries the display fields the summon events replay.
480
+ */
481
+ marketExpertList(credential: WorkBuddyCredential, expertType?: 'agent' | 'team' | ''): Promise<readonly MarketExpert[]>;
482
+ /**
483
+ * Open a REAL desktop conversation and return the ids the server assigned.
484
+ *
485
+ * The expert and skill tasks join their events to a conversation the server
486
+ * has actually seen, so a locally invented `requestId` scores nothing. This
487
+ * starts a chat, reads the server's id out of the SSE stream, then drops the
488
+ * rest of the stream — the answer itself is irrelevant, only its identity is.
489
+ *
490
+ * Returns `undefined` instead of throwing when the conversation cannot be
491
+ * opened or carries no recognisable id, because every caller is a best-effort
492
+ * task chain.
493
+ */
494
+ openConversation(credential: WorkBuddyCredential, expertId?: string, signal?: AbortSignal): Promise<{
495
+ conversationId: string;
496
+ requestId: string;
497
+ } | undefined>;
498
+ /**
499
+ * Report one chat-activity event to the growth system.
500
+ *
501
+ * The body is an ARRAY holding a single chat_request_send event, and every
502
+ * field is filled in: a three-field minimal event is accepted with 200 and
503
+ * then silently dropped, so the full shape is load-bearing rather than
504
+ * cosmetic. userId is the one field the server actually keys on.
505
+ *
506
+ * One report per account per day is the quota the reference panel settled
507
+ * on; a single report lights the growth streak and unlocks the first_buddy
508
+ * family, which is why this runs before the task-centre pass.
509
+ */
510
+ reportActivity(credential: WorkBuddyCredential, conversationId?: string): Promise<void>;
511
+ /**
512
+ * Read back the growth streak in days.
513
+ *
514
+ * This is the read-only oracle for {@link reportActivity}: a report that
515
+ * returned 200 yet left the streak untouched was silently dropped (a missing
516
+ * `userId` is the usual cause), so callers verify instead of trusting the
517
+ * status code.
518
+ *
519
+ * Two shape traps, both measured against the live upstream:
520
+ *
521
+ * - The path carries NO `/v2` prefix, unlike its sibling task endpoints under
522
+ * `/v2/activity/growth/*`. Asking for the `/v2` form does not 404; it
523
+ * answers with a body that carries no `streak` object at all.
524
+ * - The counter is nested as `data.streak.days`, not `data.days`. Reading the
525
+ * flat field yields a constant 0, which would make every successful report
526
+ * look like a silent drop.
527
+ *
528
+ * Returns 0 only when the field is genuinely absent.
529
+ */
530
+ growthStreakDays(credential: WorkBuddyCredential): Promise<number>;
531
+ /**
532
+ * The full streak picture: days, tier unlock state, and what each tier pays.
533
+ *
534
+ * Read before redeeming, because the tier state is the only honest answer to
535
+ * "is there anything to claim": the redeem endpoint answers 403 for a locked
536
+ * tier, which is indistinguishable from a real failure once the response is
537
+ * just an error.
538
+ */
539
+ growthStreakFull(credential: WorkBuddyCredential): Promise<WorkBuddyStreakStatus>;
540
+ /**
541
+ * Redeem one unlocked streak tier.
542
+ *
543
+ * A locked tier answers 403 ("连续登录天数不足"); callers check the status from
544
+ * {@link growthStreakFull} first, so this only throws for genuine failures.
545
+ * The client token is the upstream's idempotency key — a fresh one per attempt
546
+ * keeps a retry from being read as a duplicate of the last one.
547
+ */
548
+ redeemStreakTier(credential: WorkBuddyCredential, tier: string): Promise<void>;
549
+ /** How many lottery draws are available right now. */
550
+ lotteryChances(credential: WorkBuddyCredential): Promise<number>;
551
+ /**
552
+ * Draw the lottery once.
553
+ *
554
+ * Returns the raw prize payload: its shape is set by the running campaign, so
555
+ * it is passed through rather than modelled.
556
+ */
557
+ lotteryDraw(credential: WorkBuddyCredential): Promise<unknown>;
558
+ /**
559
+ * The buddy profile, or undefined when the account has no buddy yet.
560
+ *
561
+ * `data.buddy` is null / absent / an empty object depending on how far the
562
+ * account got, and all three mean the same thing to a caller: adopt first.
563
+ */
564
+ buddyInfo(credential: WorkBuddyCredential): Promise<{
565
+ instanceId: number;
566
+ name: string;
567
+ } | undefined>;
568
+ /** Agree to the buddy terms. Idempotent upstream. */
569
+ buddyAgree(credential: WorkBuddyCredential): Promise<void>;
570
+ /**
571
+ * Adopt the first buddy.
572
+ *
573
+ * Gated upstream on having reported activity that day: without it the answer
574
+ * is 400 "first_buddy task not completed yet". Callers treat that as "not yet"
575
+ * rather than an error, which is why it is thrown as-is for them to classify.
576
+ */
577
+ buddyAdoptFirst(credential: WorkBuddyCredential): Promise<void>;
578
+ /** Current travel state for the account's buddy. */
579
+ buddyTravelStatus(credential: WorkBuddyCredential): Promise<WorkBuddyTravelState>;
580
+ /**
581
+ * Send the buddy travelling.
582
+ *
583
+ * The location is always 4 (古镇客栈): the four locations have identical
584
+ * reward and duration ranges, so there is nothing to optimise.
585
+ */
586
+ buddyTravelDepart(credential: WorkBuddyCredential, locationId?: number): Promise<void>;
587
+ /**
588
+ * Collect an arrived trip's reward.
589
+ *
590
+ * `recordId` is required and comes from the status read; the upstream rejects
591
+ * a claim without it.
592
+ */
593
+ buddyTravelClaim(credential: WorkBuddyCredential, recordId: number): Promise<number>;
594
+ /** Whether yesterday is a gap in the activity heatmap. */
595
+ heatmapYesterdayMissed(credential: WorkBuddyCredential): Promise<boolean>;
596
+ /** Spend one makeup card on a date. Idempotent for an already-filled date. */
597
+ useMakeupCard(credential: WorkBuddyCredential, date: string): Promise<void>;
598
+ /**
599
+ * Call a growth-domain endpoint and return its unwrapped `data`.
600
+ *
601
+ * These endpoints live on the chat host with the billing header set, and
602
+ * carry the same envelope as everything else. Centralised here because every
603
+ * growth call needs the identical envelope check.
604
+ */
605
+ private growthJson;
142
606
  /** Legacy thin wrapper kept for `status`/`doctor`: returns raw envelope data. */
143
607
  credits(credential: WorkBuddyCredential): Promise<{
144
608
  ok: true;
@@ -147,6 +611,40 @@ export declare class WorkBuddyUpstreamClient {
147
611
  ok: false;
148
612
  message: string;
149
613
  }>;
614
+ /**
615
+ * Fetch the growth task list for one account.
616
+ *
617
+ * The upstream answers `data.tasks[]`, and `claimable` is derived locally —
618
+ * the upstream does not mark it. Only a task whose progress reached its
619
+ * target and that is not already claimed counts as eligible.
620
+ */
621
+ listTasks(credential: WorkBuddyCredential): Promise<readonly WorkBuddyTask[]>;
622
+ /**
623
+ * Accept (enrol in) tasks by code.
624
+ *
625
+ * Accepting is the "sign up" half: it produces no progress by itself, and the
626
+ * upstream answers success for an already-accepted task, so replaying this is
627
+ * safe. Progress is lit by real activity (a chat, an activity report).
628
+ */
629
+ acceptTasks(credential: WorkBuddyCredential, taskCodes: readonly string[]): Promise<void>;
630
+ /**
631
+ * Claim one task's reward.
632
+ *
633
+ * Two details differ from list/accept and are load-bearing:
634
+ *
635
+ * - The task code rides the PATH, not the body.
636
+ * - It is served by the web origin, not the chat host, and only when the
637
+ * request carries the growth-centre Origin/Referer plus
638
+ * `x-client-platform: web`. The chat host's `/reward/claim` path does not
639
+ * exist and answers 400 "task not completed".
640
+ *
641
+ * A repeat claim answers `already_claimed` with zero credit, which is treated
642
+ * as success so the caller can stay idempotent.
643
+ */
644
+ claimTaskReward(credential: WorkBuddyCredential, taskCode: string): Promise<{
645
+ credit: number;
646
+ energy: number;
647
+ }>;
150
648
  }
151
649
  //#endregion
152
650
  //#region src/accounts.d.ts
@@ -284,6 +782,25 @@ export declare class WorkBuddyAccountPool {
284
782
  * lives on the pool and is re-applied from settings after each scan.
285
783
  */
286
784
  private disabledIds;
785
+ /**
786
+ * Per-account credit floor, keyed by account id. 0 (or absent) means "spend
787
+ * it all".
788
+ *
789
+ * A reserved balance is protection, not a hard limit the upstream knows
790
+ * about: the pool simply stops picking that account once its last known
791
+ * balance is at or below the floor, so the user keeps a cushion instead of
792
+ * draining every account to zero.
793
+ */
794
+ private creditReserves;
795
+ /**
796
+ * Last known credit balance per account, epoch ms aside.
797
+ *
798
+ * Refreshed in the background after a successful request, so a pick can
799
+ * consult it. An account with no reading is treated as usable: refusing to
800
+ * pick an account just because its balance has not been checked yet would
801
+ * strand a healthy pool, and the first 402 still cools it as before.
802
+ */
803
+ private creditBalances;
287
804
  /**
288
805
  * Last time each account served a request, epoch ms. Drives the idle term
289
806
  * of the priority-mode weighting below: an account that just served loses to
@@ -307,6 +824,8 @@ export declare class WorkBuddyAccountPool {
307
824
  exhaustCooldownMs?: number;
308
825
  distribution?: AccountDistribution;
309
826
  disabledAccountIds?: readonly string[];
827
+ /** Per-account credit floor, keyed by account id. Absent keeps the current map. */
828
+ creditReserves?: Readonly<Record<string, number>>;
310
829
  }): void;
311
830
  /** Rescan the auth directories and merge newly discovered accounts. */
312
831
  scan(): Promise<WorkBuddyAccount[]>;
@@ -376,6 +895,33 @@ export declare class WorkBuddyAccountPool {
376
895
  * not count as used for the account that was merely tried.
377
896
  */
378
897
  noteServed(accountId: string): void;
898
+ /**
899
+ * Record an account latest known credit balance.
900
+ *
901
+ * Called after a request and by the card balance refresh, so the reserve
902
+ * check has something to compare against. A reading for an unknown account is
903
+ * dropped: `scan()` rebuilds the account list and a stale id would otherwise
904
+ * accumulate forever.
905
+ */
906
+ noteCredits(accountId: string, balance: number): void;
907
+ /** Last known balance for one account, or undefined when never read. */
908
+ creditsOf(accountId: string): number | undefined;
909
+ /** The credit floor the user set for one account; 0 when unset. */
910
+ creditReserveOf(accountId: string): number;
911
+ /**
912
+ * Replace every reserve. Called from settings on each apply, so the map
913
+ * mirrors the saved document exactly instead of accumulating old keys.
914
+ */
915
+ setCreditReserves(reserves: Readonly<Record<string, number>>): void;
916
+ /** Every reserve currently in force, keyed by account id. */
917
+ creditReservesInOrder(): Record<string, number>;
918
+ /**
919
+ * Whether an account is held back only by its reserve.
920
+ *
921
+ * Separates "resting to protect credits" from every other reason an account
922
+ * is out of rotation, which is what the card shows the user.
923
+ */
924
+ isReserved(accountId: string): boolean;
379
925
  /**
380
926
  * The account that served the most recent request, if any.
381
927
  *
@@ -541,6 +1087,444 @@ interface WorkBuddyAdapter {
541
1087
  */
542
1088
  export declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
543
1089
  //#endregion
1090
+ //#region src/scheduler.d.ts
1091
+ /** How often the loop wakes to look for a due job. */
1092
+ /**
1093
+ * How long to wait for event scoring to land before re-reading the task list.
1094
+ *
1095
+ * Scoring is asynchronous on the upstream side, so an immediate re-read still
1096
+ * shows the old progress and the claim pass would skip a task that is in fact
1097
+ * now claimable. Measured: the chain is reflected by about eight seconds.
1098
+ */
1099
+ export declare const EVENT_SCORE_WAIT_MS = 9000;
1100
+ export declare const AUTOMATION_TICK_MS = 60000;
1101
+ /** Which jobs the automation runs, and when. */
1102
+ /**
1103
+ * The persisted daily earnings ledger.
1104
+ *
1105
+ * `date` is the local day the counters belong to: a ledger from a previous day
1106
+ * is discarded on load rather than carried forward as "today".
1107
+ */
1108
+ interface AutomationLedger {
1109
+ date: string;
1110
+ accounts: Record<string, AutomationAccountEarnings>;
1111
+ }
1112
+ interface AutomationOptions {
1113
+ /** Master switch; false stops every job. */
1114
+ enabled?: boolean;
1115
+ /** Hour list for the daily check-in job. */
1116
+ checkinHours?: readonly number[];
1117
+ /** Hour list for the task-centre job (accept + claim). */
1118
+ taskHours?: readonly number[];
1119
+ /** Hour list for the activity-report job. */
1120
+ reportHours?: readonly number[];
1121
+ /** Hour list for the streak-redemption job. */
1122
+ streakHours?: readonly number[];
1123
+ /** Hour list for the buddy travel job. */
1124
+ travelHours?: readonly number[];
1125
+ /** Per-account serial delay, in milliseconds. */
1126
+ accountDelayMs?: number;
1127
+ /** Override the gap between expert chains, in milliseconds (tests use 0). */
1128
+ expertGapMs?: number;
1129
+ /** Override the event-scoring wait, in milliseconds (tests use 0). */
1130
+ eventScoreWaitMs?: number;
1131
+ /** Override the clock, for tests. */
1132
+ now?: () => Date;
1133
+ /** Logger; defaults to a no-op so tests stay quiet. */
1134
+ logger?: SchedulerLogger;
1135
+ /**
1136
+ * Persist the daily earnings ledger, and restore it on construction.
1137
+ *
1138
+ * The ledger cannot live only in memory: a host restart mid-day would wipe
1139
+ * what the automation already earned, and the card would show nothing for
1140
+ * rewards that really were collected.
1141
+ */
1142
+ loadEarnings?: () => AutomationLedger | undefined;
1143
+ saveEarnings?: (ledger: AutomationLedger) => void;
1144
+ }
1145
+ /** Logger surface, kept structural so any host logger fits. */
1146
+ interface SchedulerLogger {
1147
+ info?(...args: unknown[]): void;
1148
+ warn?(...args: unknown[]): void;
1149
+ }
1150
+ /** One job's last-run record, surfaced on the status document. */
1151
+ interface AutomationJobState {
1152
+ /** `YYYY-MM-DD` of the last completed run, or undefined if it never ran. */
1153
+ lastRunDate?: string;
1154
+ /** Epoch ms of the last completed run. */
1155
+ lastRunAtMs?: number;
1156
+ /** Accounts that completed without throwing. */
1157
+ ok: number;
1158
+ /** Accounts that threw (each one skipped, the run continued). */
1159
+ failed: number;
1160
+ /** Credits claimed by the task job on the last run. */
1161
+ credit: number;
1162
+ /** Energy claimed by the task job on the last run. */
1163
+ energy: number;
1164
+ /** Tasks claimed by the task job on the last run. */
1165
+ claimed: number;
1166
+ /** Human-readable summary of the last run. */
1167
+ message?: string;
1168
+ }
1169
+ /**
1170
+ * What the automation earned for ONE account today.
1171
+ *
1172
+ * Reset at the local day boundary alongside the per-job "already ran today"
1173
+ * guard, so the card shows today rather than a running total that never
1174
+ * answers "did it do anything for this account recently".
1175
+ */
1176
+ interface AutomationAccountEarnings {
1177
+ /** Credits the automation claimed from the task centre today. */
1178
+ credit: number;
1179
+ /** Energy claimed from the task centre today. */
1180
+ energy: number;
1181
+ /** Tasks claimed today. */
1182
+ claimed: number;
1183
+ /**
1184
+ * Credits the automation collected from check-in today.
1185
+ *
1186
+ * Kept separate from `credit` because they are different achievements and the
1187
+ * card shows them on their own lines: "the automation claimed 3 tasks" and
1188
+ * "the automation checked in" are not the same claim to the user.
1189
+ */
1190
+ checkinCredit: number;
1191
+ /** Credits from streak redemption + lottery today. */
1192
+ bonusCredit: number;
1193
+ /** Credits from the buddy adoption / travel loop today. */
1194
+ travelCredit: number;
1195
+ /** Local date the counters belong to. */
1196
+ date: string;
1197
+ }
1198
+ /** What ONE account gained during a single run. */
1199
+ interface AutomationAccountGain {
1200
+ credit: number;
1201
+ energy: number;
1202
+ claimed: number;
1203
+ checkinCredit: number;
1204
+ bonusCredit: number;
1205
+ travelCredit: number;
1206
+ }
1207
+ /** Totals from running the whole ordered pass at once. */
1208
+ interface AutomationRunSummary {
1209
+ /** How many jobs actually ran (a job with no hour is skipped). */
1210
+ jobsRun: number;
1211
+ /** Accounts that finished without error, summed across jobs. */
1212
+ okCount: number;
1213
+ /** Accounts that failed, summed across jobs. */
1214
+ failed: number;
1215
+ credit: number;
1216
+ energy: number;
1217
+ claimed: number;
1218
+ /**
1219
+ * What each account gained during THIS run, keyed by account id. Only
1220
+ * accounts that gained something appear.
1221
+ */
1222
+ accounts: Readonly<Record<string, AutomationAccountGain>>;
1223
+ }
1224
+ /** Automation snapshot for the status document and the card. */
1225
+ interface AutomationStatus {
1226
+ enabled: boolean;
1227
+ /** Whether the loop is running. */
1228
+ running: boolean;
1229
+ checkinHours: readonly number[];
1230
+ taskHours: readonly number[];
1231
+ reportHours: readonly number[];
1232
+ streakHours: readonly number[];
1233
+ travelHours: readonly number[];
1234
+ jobs: {
1235
+ checkin: AutomationJobState;
1236
+ report: AutomationJobState;
1237
+ tasks: AutomationJobState;
1238
+ streak: AutomationJobState;
1239
+ travel: AutomationJobState;
1240
+ };
1241
+ /** Claimable tasks seen on the most recent task pass, across accounts. */
1242
+ claimableSeen: number;
1243
+ /**
1244
+ * Per-account totals for today, keyed by account id. Only accounts that
1245
+ * actually earned something appear, so the card can render "no earnings"
1246
+ * as an absence rather than a zero it has to explain.
1247
+ */
1248
+ earningsToday: Readonly<Record<string, AutomationAccountEarnings>>;
1249
+ /**
1250
+ * Whether a manual run is in flight right now.
1251
+ *
1252
+ * A run takes tens of seconds (one upstream call per account per job, plus
1253
+ * the scoring wait), which is far too long for the card to hold a request
1254
+ * open. The button starts a run and the panel polls this flag instead.
1255
+ */
1256
+ runInProgress: boolean;
1257
+ }
1258
+ /** The three plus one job kinds, in a stable order. */
1259
+ type AutomationJobKind = 'checkin' | 'tasks' | 'report' | 'streak' | 'travel';
1260
+ /** The four jobs in the order a tick runs them: report before tasks, always. */
1261
+ export declare const AUTOMATION_JOB_KINDS: readonly AutomationJobKind[];
1262
+ /** Reject anything that is not a job kind, so a route cannot name an unknown job. */
1263
+ export declare function isAutomationJobKind(value: unknown): value is AutomationJobKind;
1264
+ export declare function dayKey(date: Date): string;
1265
+ /**
1266
+ * Whether `now`'s local hour is one of `hours`.
1267
+ *
1268
+ * The reference panel computes a `nextFire` instant and sleeps until it; this
1269
+ * loop instead wakes every minute and asks "is any job due now". Both fire at
1270
+ * the top of a configured hour, but the polling form cannot miss a slot to a
1271
+ * suspended process — a laptop that slept through 10:00 still runs the job the
1272
+ * moment it wakes, on the same day.
1273
+ */
1274
+ export declare function isFireHour(now: Date, hours: readonly number[]): boolean;
1275
+ /**
1276
+ * The points automation.
1277
+ *
1278
+ * Owns a single timer loop. Construction is inert — nothing runs until
1279
+ * {@link start}, and {@link stop} is idempotent so a plugin teardown that fires
1280
+ * twice is harmless.
1281
+ */
1282
+ export declare class WorkBuddyScheduler {
1283
+ private readonly pool;
1284
+ private readonly client;
1285
+ private readonly logger;
1286
+ private readonly now;
1287
+ private readonly delayMs;
1288
+ /**
1289
+ * How long to wait for event scoring before re-reading the task list.
1290
+ * Tests set 0 so a pass does not spend nine real seconds per account.
1291
+ */
1292
+ private readonly eventScoreWaitMs;
1293
+ /**
1294
+ * Gap between two expert summon chains.
1295
+ * Tests set 0 so a pass does not spend six real seconds per expert.
1296
+ */
1297
+ private readonly expertGapMs;
1298
+ private enabled;
1299
+ private checkinHours;
1300
+ private taskHours;
1301
+ private reportHours;
1302
+ private streakHours;
1303
+ private travelHours;
1304
+ private timer;
1305
+ private running;
1306
+ /** Guards against a slow run overlapping the next tick. */
1307
+ private busy;
1308
+ /** True while a manual run is in flight, so the card can poll it. */
1309
+ private runInFlight;
1310
+ /**
1311
+ * Set once {@link stop} is called.
1312
+ *
1313
+ * Deliberately false before `start`: the loop is not running yet, but a
1314
+ * manual `tick` must still work. `stop` is what makes a run abandon the
1315
+ * accounts it has not reached yet.
1316
+ */
1317
+ private stopped;
1318
+ private readonly states;
1319
+ private claimableSeen;
1320
+ /**
1321
+ * Credits/energy/tasks earned per account TODAY, keyed by account id.
1322
+ *
1323
+ * Cleared whenever the day key rolls over, so the card always answers
1324
+ * "what did the automation get for THIS account today".
1325
+ */
1326
+ private earnings;
1327
+ /** Day key the counters above belong to. */
1328
+ private earningsDate;
1329
+ /** Host hooks that persist the ledger across restarts. */
1330
+ private readonly loadEarnings;
1331
+ private saveEarningsFn;
1332
+ constructor(pool: WorkBuddyAccountPool, client: WorkBuddyUpstreamClient, options?: AutomationOptions);
1333
+ /** Apply a new configuration; safe to call while running. */
1334
+ /**
1335
+ * Install the persistence hook once the host settings service is available.
1336
+ *
1337
+ * Separate from the constructor because the scheduler is built with the pool,
1338
+ * long before the settings section exists; a ledger written before that point
1339
+ * would have nowhere to go.
1340
+ */
1341
+ setEarningsPersistence(save: (ledger: AutomationLedger) => void): void;
1342
+ /**
1343
+ * Fold a previously persisted ledger back in, when it belongs to today.
1344
+ *
1345
+ * Used after the settings document becomes readable, which happens after
1346
+ * construction; a ledger from an earlier day is ignored so the counters never
1347
+ * claim yesterday as today.
1348
+ */
1349
+ applyEarningsLedger(ledger: AutomationLedger): void;
1350
+ applyConfig(options: AutomationOptions): void;
1351
+ /** Hours for one job, used by the loop and the status document. */
1352
+ private hoursOf;
1353
+ /** Start the loop. Idempotent. */
1354
+ start(): void;
1355
+ /** Stop the loop. Idempotent, and safe before `start`. */
1356
+ stop(): void;
1357
+ /** Snapshot for the status document. */
1358
+ /**
1359
+ * Run one job immediately, regardless of the clock.
1360
+ *
1361
+ * Exists so the automation can be verified from the card without waiting for
1362
+ * its hour. A manual run is recorded exactly like a scheduled one, so the
1363
+ * timer will not repeat it later the same day: every job is idempotent, but a
1364
+ * second pass would still be wasted upstream calls.
1365
+ *
1366
+ * `force` ignores the already-ran-today guard, which is what pressing the
1367
+ * button a second time means.
1368
+ */
1369
+ runNow(kind: AutomationJobKind, force?: boolean): Promise<AutomationJobState>;
1370
+ /**
1371
+ * Run every job once, in the scheduled order.
1372
+ *
1373
+ * Order matters and is not configurable: the activity report has to land
1374
+ * before the task pass reads task progress, or the pass sees counters the
1375
+ * report would have moved. This is what the card's single button calls.
1376
+ */
1377
+ /**
1378
+ * Start a full pass in the background and return immediately.
1379
+ *
1380
+ * A pass takes tens of seconds - one upstream round trip per account per job,
1381
+ * plus the scoring wait - which is far too long to hold the card request open:
1382
+ * the browser or the host web server would time out, and the user would see
1383
+ * a hung button for a run that is actually working.
1384
+ *
1385
+ * Returns whether a run started. A second call while one is in flight is
1386
+ * ignored rather than queued: pressing the button twice means hurry up, and
1387
+ * the run already under way covers it.
1388
+ */
1389
+ startRunAll(): boolean;
1390
+ runAll(): Promise<AutomationRunSummary>;
1391
+ status(): AutomationStatus;
1392
+ /**
1393
+ * One poll: run every due job, serially.
1394
+ *
1395
+ * Serial by design — the jobs share the same accounts and the upstream
1396
+ * rate-limits per account, so overlapping passes would only trip that limit.
1397
+ * A job that throws is recorded and the loop continues.
1398
+ */
1399
+ private tick;
1400
+ /** Run one job against every eligible account and record the outcome. */
1401
+ /**
1402
+ * Add one account's take to today's counters, resetting first if the day
1403
+ * rolled over. Called from the task pass, which is the only job that earns.
1404
+ */
1405
+ /**
1406
+ * Today's per-account earnings, as a plain object for the status document.
1407
+ *
1408
+ * Rolls the day first so a status read just after midnight does not report
1409
+ * yesterday's totals under today's date.
1410
+ */
1411
+ private earningsSnapshot;
1412
+ /**
1413
+ * Add one account take to today counters, resetting first if the day rolled
1414
+ * over. Every source is tracked separately so the card can show what earned
1415
+ * what, rather than one opaque total.
1416
+ */
1417
+ private recordEarnings;
1418
+ /** Clear the per-account counters when the local day changes. */
1419
+ private rollEarnings;
1420
+ /**
1421
+ * Write the ledger through the host hook, when one was supplied.
1422
+ *
1423
+ * Best effort on purpose: a failed save must never abort a run that has
1424
+ * already collected rewards, and the in-memory ledger keeps serving the card
1425
+ * for the rest of the session either way.
1426
+ */
1427
+ private persistEarnings;
1428
+ /**
1429
+ * Run one job against every eligible account and record the outcome.
1430
+ *
1431
+ * The task job runs in TWO passes. The first sends the event chains that light
1432
+ * up client-scored tasks; the second collects rewards. They are separate
1433
+ * because scoring lands asynchronously — a chain sent and claimed within the
1434
+ * same breath finds the task still un-scored — and because sending is fast
1435
+ * while claiming wants the whole pool to have been lit up first. Splitting
1436
+ * them costs one shared wait instead of one wait per account.
1437
+ */
1438
+ private runJob;
1439
+ /** Compose the one-line summary shown on the card. */
1440
+ private summarise;
1441
+ /**
1442
+ * Accounts to run against, in pool order.
1443
+ *
1444
+ * Disabled accounts are excluded here rather than filtered by the caller so a
1445
+ * card switch takes effect on the next pass without any event plumbing.
1446
+ */
1447
+ private accountsInOrder;
1448
+ /**
1449
+ * Send one activity report, then verify it landed.
1450
+ *
1451
+ * The upstream answers 200 even when it drops the event, so the streak is
1452
+ * read back as the oracle: `days > 0` means it counted. A failed read-back is
1453
+ * logged and treated as a suspicious result, never as a retry — the report is
1454
+ * idempotent per day, and hammering it is exactly what the one-a-day quota
1455
+ * exists to avoid.
1456
+ */
1457
+ private reportOne;
1458
+ private sendEventChains;
1459
+ /**
1460
+ * Send one chain on the channel it was built for.
1461
+ *
1462
+ * The transport is not a detail of the sender: the scorer keys different
1463
+ * tasks to different fingerprint families, so a web-scored event posted as a
1464
+ * desktop event is accepted and then ignored.
1465
+ */
1466
+ private sendChain;
1467
+ /**
1468
+ * Build every chain that scores one task.
1469
+ *
1470
+ * Most tasks need a single chain; `template_5` needs five, because the scorer
1471
+ * counts distinct `template_used` events rather than a boolean. The two tasks
1472
+ * that join a conversation (skill, expert) open a real one first, which is why
1473
+ * this is async.
1474
+ */
1475
+ private chainsFor;
1476
+ /**
1477
+ * The summon-and-use chains for the expert tasks.
1478
+ *
1479
+ * Two steps per expert, and both are load-bearing: the summon events alone are
1480
+ * impressions, and a use event on its own scores nothing because the scorer
1481
+ * looks the conversation up. Only a real chat with `X-Expert-Id` produces an
1482
+ * id it will accept.
1483
+ */
1484
+ private expertChains;
1485
+ /**
1486
+ * The 腾讯轻量云 expert chain.
1487
+ *
1488
+ * Structurally the same as the expert task, with two differences the scorer
1489
+ * checks: `agent_task_created` has to name the expert, and the use event has
1490
+ * to report `mode: 'LOCAL'` with an empty type and zero cost — that is what
1491
+ * the lighthouse criterion looks for.
1492
+ */
1493
+ private lighthouseChains;
1494
+ /**
1495
+ * The task-centre pass for one account.
1496
+ *
1497
+ * Order matters: enrich first (enrol in everything open), then claim. Both
1498
+ * halves are idempotent — accepting an already-accepted task succeeds, and a
1499
+ * repeat claim answers `already_claimed` — so a pass that dies halfway is
1500
+ * safe to replay on the next tick.
1501
+ */
1502
+ private runTasks;
1503
+ /**
1504
+ * Streak redemption plus the lottery it unlocks.
1505
+ *
1506
+ * Tiers unlock on consecutive active days (7/14/28). Redeeming one pays
1507
+ * credits, energy, a makeup card and — the part nothing else grants — lottery
1508
+ * draws, so the draw runs straight after and only for the chances in hand.
1509
+ *
1510
+ * Everything here is idempotent: a tier already claimed is skipped by its
1511
+ * status, and a draw consumes one chance, so a replay cannot double-spend.
1512
+ */
1513
+ private redeemStreak;
1514
+ /**
1515
+ * One trip through the buddy travel loop for an account.
1516
+ *
1517
+ * A single pass advances the state machine by at most one step: a trip
1518
+ * that has arrived is collected, and an idle buddy is sent out. A buddy
1519
+ * already travelling is left alone — there is nothing to do until it lands.
1520
+ *
1521
+ * Measured against the live upstream: the departed trip reports
1522
+ * `dailyLimitReached` immediately, so the once-a-day limit needs no local
1523
+ * bookkeeping.
1524
+ */
1525
+ private runTravel;
1526
+ }
1527
+ //#endregion
544
1528
  //#region src/status.d.ts
545
1529
  /** One account's status row. */
546
1530
  interface AccountStatus {
@@ -624,6 +1608,10 @@ export declare const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/c
624
1608
  export declare const POOL_CHECKIN_PATH = "/plugins/dsh-workbuddy-xdpool/checkin";
625
1609
  /** Plugin-owned model-selection save endpoint (writes the settings section). */
626
1610
  export declare const POOL_MODELS_SAVE_PATH = "/plugins/dsh-workbuddy-xdpool/models/save";
1611
+ /** Run one automation job immediately, so the card can verify it on demand. */
1612
+ export declare const POOL_AUTOMATION_RUN_PATH = "/plugins/dsh-workbuddy-xdpool/automation/run";
1613
+ /** Set or clear one account's reserved-credit floor. */
1614
+ export declare const POOL_CREDIT_RESERVE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/credit-reserve";
627
1615
  /** One pool account's row, token-free. */
628
1616
  interface PoolWebAccount {
629
1617
  id: string;
@@ -652,6 +1640,24 @@ interface PoolWebAccount {
652
1640
  */
653
1641
  disabled: boolean;
654
1642
  rateLimitHits: number;
1643
+ /**
1644
+ * Credits the user asked to keep for this account. The pool stops picking the
1645
+ * account once its balance reaches the reserve, so this many credits survive.
1646
+ * 0 means the account may be spent down as before.
1647
+ */
1648
+ creditReserve: number;
1649
+ /**
1650
+ * Whether the account is held back purely by its reserve right now. Kept
1651
+ * distinct from `cooling`: a reserved account is healthy and simply
1652
+ * protected, which is a different thing to tell the user than rate-limited.
1653
+ */
1654
+ reserved: boolean;
1655
+ /**
1656
+ * What the automation earned for this account today. Absent when it earned
1657
+ * nothing (or the automation never ran for it), so the card can stay quiet
1658
+ * instead of printing a row of zeroes.
1659
+ */
1660
+ automationToday?: PoolWebAutomationEarnings;
655
1661
  /** ISO timestamp of the last successful use (best-effort pool bookkeeping). */
656
1662
  lastUsedAt?: string;
657
1663
  /** Aggregated credit summary for the account, read-only. */
@@ -767,6 +1773,82 @@ interface PoolWebStatus {
767
1773
  running: boolean;
768
1774
  baseUrl?: string;
769
1775
  };
1776
+ /** Daily-points automation state, so the card can show what ran and when. */
1777
+ automation: PoolWebAutomation;
1778
+ /** Per-account credit floors currently in force, keyed by account id. */
1779
+ creditReserves: Readonly<Record<string, number>>;
1780
+ }
1781
+ /** One automation job's last run, as shown on the card. */
1782
+ interface PoolWebAutomationJob {
1783
+ /** `YYYY-MM-DD` of the last run in this process, if it has run. */
1784
+ lastRunDate?: string;
1785
+ /** Accounts that finished without error on the last run. */
1786
+ ok: number;
1787
+ /** Accounts that failed on the last run (each one skipped, the run continued). */
1788
+ failed: number;
1789
+ /** Credits claimed by the task job on the last run. */
1790
+ credit: number;
1791
+ /** Energy claimed by the task job on the last run. */
1792
+ energy: number;
1793
+ /** Tasks claimed by the task job on the last run. */
1794
+ claimed: number;
1795
+ /** One-line summary of the last run. */
1796
+ message?: string;
1797
+ }
1798
+ /**
1799
+ * Automation block on the status document.
1800
+ *
1801
+ * Carries the schedule and each job's last outcome so the card can answer
1802
+ * "is it on, when does it run, and what did it last do" without reaching into
1803
+ * the scheduler itself.
1804
+ */
1805
+ interface PoolWebAutomation {
1806
+ /** Master switch, mirrored from the saved config. */
1807
+ enabled: boolean;
1808
+ /** Whether the loop is currently running. */
1809
+ running: boolean;
1810
+ /** Configured hours per job, so the card can show the schedule. */
1811
+ checkinHours: readonly number[];
1812
+ reportHours: readonly number[];
1813
+ taskHours: readonly number[];
1814
+ streakHours: readonly number[];
1815
+ jobs: {
1816
+ checkin: PoolWebAutomationJob;
1817
+ report: PoolWebAutomationJob;
1818
+ tasks: PoolWebAutomationJob;
1819
+ streak: PoolWebAutomationJob;
1820
+ travel: PoolWebAutomationJob;
1821
+ };
1822
+ /** Claimable tasks seen on the most recent task pass, across accounts. */
1823
+ claimableSeen: number;
1824
+ /**
1825
+ * Whether a manual run is in flight. The card polls this to know when to
1826
+ * stop showing progress and report the result.
1827
+ */
1828
+ runInProgress: boolean;
1829
+ /**
1830
+ * Per-account credits/energy/tasks the automation earned TODAY, keyed by
1831
+ * account id. An account that earned nothing is simply absent, so the card
1832
+ * can say "nothing yet" instead of showing a bare zero.
1833
+ */
1834
+ earningsToday: Readonly<Record<string, PoolWebAutomationEarnings>>;
1835
+ }
1836
+ /** Today's automation take for one account. */
1837
+ interface PoolWebAutomationEarnings {
1838
+ /** Credits claimed from the task centre today. */
1839
+ credit: number;
1840
+ /** Energy claimed from the task centre today. */
1841
+ energy: number;
1842
+ /** Tasks claimed today. */
1843
+ claimed: number;
1844
+ /** Credits collected from check-in today. */
1845
+ checkinCredit: number;
1846
+ /** Credits from streak redemption and the lottery today. */
1847
+ bonusCredit: number;
1848
+ /** Credits from buddy adoption and the travel loop today. */
1849
+ travelCredit: number;
1850
+ /** Local date the counters belong to (YYYY-MM-DD). */
1851
+ date: string;
770
1852
  }
771
1853
  /**
772
1854
  * The two gateways, matching the provider ids the host registers. `cn` is the
@@ -777,6 +1859,72 @@ type PoolRegion = 'cn' | 'global';
777
1859
  /** How the pool spreads requests across its accounts. */
778
1860
  type PoolDistribution = 'priority' | 'round-robin' | 'balanced';
779
1861
  //#endregion
1862
+ //#region src/web-status.d.ts
1863
+ /** Constructor dependencies — a narrow slice of the pool runtime. */
1864
+ interface PoolStatusRouteOptions {
1865
+ pool: WorkBuddyAccountPool;
1866
+ /** One catalog per region; the card reads the one for its active tab. */
1867
+ catalogs: Readonly<Record<PoolRegion, WorkBuddyCatalog>>;
1868
+ client: WorkBuddyUpstreamClient;
1869
+ /** Lazily resolve the running loopback shim, when it has bound a port. */
1870
+ shim?: () => {
1871
+ running: boolean;
1872
+ baseUrl?: string;
1873
+ };
1874
+ /**
1875
+ * The daily-points automation, when the host half has one.
1876
+ *
1877
+ * Optional so these routes still mount on a profile that assembled a pool
1878
+ * without a scheduler (the CLI and the tests do exactly that); the card then
1879
+ * reports the automation as off instead of showing a broken panel.
1880
+ */
1881
+ scheduler?: () => AutomationStatus;
1882
+ /**
1883
+ /**
1884
+ * Start a manual pass, for the card's "run now" button.
1885
+ *
1886
+ * Returns whether a run STARTED, not its result: a pass takes tens of
1887
+ * seconds, so it runs in the background and the card polls the scheduler
1888
+ * status for progress and earnings.
1889
+ */
1890
+ runAutomation?: (job: string, force: boolean) => boolean;
1891
+ /**
1892
+ * Persist one account's reserved-credit floor. Same settings document as every
1893
+ * other card write, so it survives a restart and is re-applied after a scan.
1894
+ * Absent without a settings service: the route then answers 503.
1895
+ */
1896
+ setCreditReserve?: (accountId: string, reserve: number) => Promise<void> | void;
1897
+ /**
1898
+ * Persist the user's model selection. Provided by the host half, which owns
1899
+ * the settings section; absent when the plugin runs without a settings
1900
+ * service (the save route then reports 503 rather than pretending to work).
1901
+ */
1902
+ /**
1903
+ * Persist one region's selection. The region travels with the payload: the two
1904
+ * gateways advertise different rosters, so the domestic tab and the
1905
+ * international tab each own their list and must never overwrite each other.
1906
+ */
1907
+ saveSelection?: (region: PoolRegion, selection: PoolWebModelSelection) => Promise<void> | void;
1908
+ /**
1909
+ * Persist one account switch. Same settings document as every other card
1910
+ * write, so it survives a restart and is re-applied after each re-scan.
1911
+ * Absent without a settings service: the route then answers 503.
1912
+ */
1913
+ setAccountDisabled?: (accountId: string, disabled: boolean) => Promise<void> | void;
1914
+ }
1915
+ /**
1916
+ * Assemble the card's status document. Per-account credits and check-in state
1917
+ * are queried live; a failing query degrades to `creditsError` / `checkinError`
1918
+ * rather than failing the whole document. Never throws.
1919
+ */
1920
+ export declare function poolWebStatus(deps: PoolStatusRouteOptions, region?: PoolRegion): Promise<PoolWebStatus>;
1921
+ /**
1922
+ * Mount the read-only routes on a context where `webServer` is available. The
1923
+ * caller uses `ctx.inject(['webServer'], ...)` so Desktop startup order cannot
1924
+ * make this registration disappear.
1925
+ */
1926
+ export declare function registerPoolStatusRoute(ctx: Context$1, deps: PoolStatusRouteOptions): void;
1927
+ //#endregion
780
1928
  //#region src/index.d.ts
781
1929
  /** Stable Cordis plugin name. */
782
1930
  export declare const name = "llm-workbuddy-xdpool";
@@ -798,11 +1946,16 @@ export interface Config {
798
1946
  /**
799
1947
  * How the pool spreads requests across accounts.
800
1948
  *
801
- * `priority` (default) drains one account before moving to the next, which
802
- * is what a pool of your own accounts is for. `round-robin` splits the
803
- * spend evenly instead. Absent reads as `priority`.
1949
+ * - `priority` (default) drains one account before moving to the next, which
1950
+ * is what a pool of your own accounts is for.
1951
+ * - `round-robin` walks the pool in order, so the spend splits evenly.
1952
+ * - `balanced` draws at random, weighting whichever account has been idle
1953
+ * longest. Spend still spreads, but without a fixed order, so one unhealthy
1954
+ * account cannot pin the pool to itself.
1955
+ *
1956
+ * Absent reads as `priority`.
804
1957
  */
805
- distribution?: 'priority' | 'round-robin';
1958
+ distribution?: 'priority' | 'round-robin' | 'balanced';
806
1959
  /**
807
1960
  * Account ids switched off on the card. A disabled account is never picked
808
1961
  * to serve a request, but it stays in the pool and on the card so it can be
@@ -810,6 +1963,12 @@ export interface Config {
810
1963
  * survive re-scans (see WorkBuddyAccountPool.disabledIds).
811
1964
  */
812
1965
  disabledAccountIds?: string[];
1966
+ /**
1967
+ * Per-account credit floor, keyed by account id. The pool stops picking an
1968
+ * account once its last known balance reaches this value, so the reserved
1969
+ * credits survive. Absent or 0 spends the account down as before.
1970
+ */
1971
+ creditReserves?: Record<string, number>;
813
1972
  /**
814
1973
  * Model ids enabled in the picker. Absent means "every model the catalog
815
1974
  * advertises" — an unconfigured install should never present an empty model
@@ -836,6 +1995,19 @@ export interface Config {
836
1995
  */
837
1996
  modelSelectionCn?: ModelSelectionConfig;
838
1997
  modelSelectionGlobal?: ModelSelectionConfig;
1998
+ /**
1999
+ * Daily-points automation. Absent means off: the scheduler makes upstream
2000
+ * calls on the user behalf, so it stays opt-in rather than surprising a
2001
+ * fresh install with background traffic.
2002
+ */
2003
+ automation?: AutomationConfig;
2004
+ /**
2005
+ * The automation's daily earnings ledger, written by the scheduler itself.
2006
+ *
2007
+ * It lives in settings rather than only in memory so a host restart mid-day
2008
+ * does not wipe what the automation already earned.
2009
+ */
2010
+ automationEarnings?: AutomationLedger;
839
2011
  }
840
2012
  /** One region's saved model selection. */
841
2013
  export interface ModelSelectionConfig {
@@ -843,8 +2015,44 @@ export interface ModelSelectionConfig {
843
2015
  imageModelIds?: string[];
844
2016
  contextBudgets?: Record<string, number>;
845
2017
  }
2018
+ /**
2019
+ * Daily-points automation.
2020
+ *
2021
+ * Absent means off: the scheduler makes upstream calls on the user behalf, so
2022
+ * it stays opt-in rather than surprising a fresh install with background
2023
+ * traffic. Each job carries its own hour list so the passes can be spread out
2024
+ * (or pushed off-peak) without disabling any of them.
2025
+ *
2026
+ * Ordering note: the report job must run before the task job. A report is what
2027
+ * lights the growth streak and unlocks the `first_buddy` family, so a task pass
2028
+ * that ran first would read counters before they could have moved.
2029
+ */
2030
+ export interface AutomationConfig {
2031
+ /** Master switch for every automation job. Absent reads as false. */
2032
+ enabled?: boolean;
2033
+ /** Hours (local, 0-23) at which the daily check-in runs. */
2034
+ checkinHours?: number[];
2035
+ /** Hours at which the activity report runs. Keep ahead of `taskHours`. */
2036
+ reportHours?: number[];
2037
+ /** Hours at which tasks are enrolled in and claimed. */
2038
+ taskHours?: number[];
2039
+ /** Hours at which streak redemption runs. */
2040
+ streakHours?: number[];
2041
+ /** Hours at which the buddy travel loop runs. */
2042
+ travelHours?: number[];
2043
+ /** How long an account rests after its credits run out, in milliseconds. */
2044
+ exhaustCooldownMs?: number;
2045
+ }
846
2046
  /** Upper bound the card offers as the "default" context window, in tokens. */
847
2047
  export declare const DEFAULT_CONTEXT_BUDGET = 200000;
2048
+ /**
2049
+ * Fold a saved automation block into scheduler options.
2050
+ *
2051
+ * Absent means off, stated once here so every caller agrees: the card writes
2052
+ * `enabled` as a real boolean, and a config that never touched the section must
2053
+ * not accidentally arm background upstream traffic.
2054
+ */
2055
+ export declare function automationOptions(automation: AutomationConfig | undefined): AutomationOptions;
848
2056
  /** Settings key holding one region's saved selection. */
849
2057
  export declare const modelSelectionKeyFor: (region: 'cn' | 'global') => string;
850
2058
  /**
@@ -869,6 +2077,8 @@ export interface WorkBuddyPoolApi {
869
2077
  rescan(): Promise<number>;
870
2078
  status(includeCredits?: boolean): Promise<Awaited<ReturnType<typeof buildStatus>>>;
871
2079
  resetCooldowns(): void;
2080
+ /** Daily-points automation; assembled with the core, inert until started. */
2081
+ scheduler: WorkBuddyScheduler;
872
2082
  }
873
2083
  /** The live API, or undefined when the plugin has not applied yet. */
874
2084
  export declare function currentApi(): WorkBuddyPoolApi | undefined;
@@ -885,6 +2095,7 @@ export declare function setApi(next: WorkBuddyPoolApi | undefined): void;
885
2095
  */
886
2096
  export declare function createCore(logger?: {
887
2097
  warn(...args: unknown[]): void;
2098
+ info?(...args: unknown[]): void;
888
2099
  }): {
889
2100
  pool: WorkBuddyAccountPool;
890
2101
  catalogs: {
@@ -892,6 +2103,7 @@ export declare function createCore(logger?: {
892
2103
  readonly global: WorkBuddyCatalog;
893
2104
  };
894
2105
  client: WorkBuddyUpstreamClient;
2106
+ scheduler: WorkBuddyScheduler;
895
2107
  };
896
2108
  /**
897
2109
  * Start the loopback endpoint, register the `workbuddy-xdpool` provider, and
@@ -900,4 +2112,4 @@ export declare function createCore(logger?: {
900
2112
  */
901
2113
  export declare function apply(ctx: Context, config?: Config): void;
902
2114
  //#endregion
903
- export type { AccountStatus, Context, ModelSelection, PoolWebCheckin, PoolWebCheckinClaim, PoolWebModel, PoolWebModelSelection, PoolWebStatus, UpstreamErrorKind, WorkBuddyAccount, WorkBuddyAdapter, WorkBuddyCredential, WorkBuddyModelInfo, WorkBuddyShim, WorkBuddyStatus };
2115
+ export type { AccountStatus, AutomationLedger, AutomationRunSummary, AutomationStatus, Context, ExpertUseMode, MarketExpert, ModelSelection, PoolStatusRouteOptions, PoolWebCheckin, PoolWebCheckinClaim, PoolWebModel, PoolWebModelSelection, PoolWebStatus, SchedulerLogger, TaskEventChain, TaskEventTransport, UpstreamErrorKind, WorkBuddyAccount, WorkBuddyAdapter, WorkBuddyCredential, WorkBuddyModelInfo, WorkBuddyShim, WorkBuddyStatus };