dsh-xdpool 0.0.0-stage → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1012 -0
- package/LICENSE +21 -0
- package/README.en.md +294 -0
- package/README.md +306 -2
- package/assets/account-credits.png +0 -0
- package/assets/account-pool.png +0 -0
- package/assets/aggregate-channels.png +0 -0
- package/assets/automation-panel.png +0 -0
- package/assets/login-channels.png +0 -0
- package/assets/model-picker.png +0 -0
- package/assets/region-tabs.png +0 -0
- package/cordis.patch.yml +48 -0
- package/lib/bin.js +11186 -0
- package/lib/client.js +8636 -0
- package/lib/index.d.ts +3835 -0
- package/lib/index.js +19545 -0
- package/package.json +135 -4
- package/screenshots.json +8 -0
- package/vendor/channel-pack/LICENSE +21 -0
- package/vendor/channel-pack/NOTICE.md +53 -0
- package/vendor/channel-pack/pack.js +68531 -0
- package/vendor/channel-pack/qoder-auth-wasm.wasm +0 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,3835 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { Api, Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
4
|
+
import { Context, Context as Context$1 } from "@deepseek-ai/cordis";
|
|
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
|
|
167
|
+
//#region src/context-budget.d.ts
|
|
168
|
+
/**
|
|
169
|
+
* Context-window budgeting for the WorkBuddy shim.
|
|
170
|
+
*
|
|
171
|
+
* The upstream answers `context_length_exceeded` (business code 11115) when a
|
|
172
|
+
* request overruns the model's window. Rather than bouncing that back to the
|
|
173
|
+
* user as a dead turn, the shim compacts the conversation on the fly:
|
|
174
|
+
*
|
|
175
|
+
* 1. estimate the prompt cost locally (cheap, no round trip);
|
|
176
|
+
* 2. drop the oldest turns while keeping `system` + the newest exchange;
|
|
177
|
+
* 3. if that still overruns, ask the model itself to summarise the middle of
|
|
178
|
+
* the conversation and splice that summary back in as a system message.
|
|
179
|
+
*
|
|
180
|
+
* Everything here is pure and synchronous-free except `summarizeMessages`,
|
|
181
|
+
* which the caller drives through an injected chat function so this module
|
|
182
|
+
* stays testable without a network.
|
|
183
|
+
*
|
|
184
|
+
* @module dsh-xdpool/context-budget
|
|
185
|
+
*/
|
|
186
|
+
/** One OpenAI chat message, narrowed to the fields we must preserve. */
|
|
187
|
+
interface ChatMessage {
|
|
188
|
+
role: string;
|
|
189
|
+
content: unknown;
|
|
190
|
+
[key: string]: unknown;
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/upstream.d.ts
|
|
194
|
+
/** Upstream failure classes the shim maps onto distinct HTTP answers. */
|
|
195
|
+
type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'not_found' | 'server' | 'client';
|
|
196
|
+
/** Token-refresh answer; fields the upstream omits stay absent. */
|
|
197
|
+
interface WorkBuddyRefreshOutcome {
|
|
198
|
+
accessToken: string;
|
|
199
|
+
refreshToken?: string;
|
|
200
|
+
expiresInSec?: number;
|
|
201
|
+
domain?: string;
|
|
202
|
+
}
|
|
203
|
+
/** One CLI-usable model, carrying what the plugin card displays. */
|
|
204
|
+
interface WorkBuddyUpstreamModel {
|
|
205
|
+
id: string;
|
|
206
|
+
name: string;
|
|
207
|
+
contextWindow: number;
|
|
208
|
+
maxTokens: number;
|
|
209
|
+
creditMultiplier?: number;
|
|
210
|
+
/** Upstream image-input flag. Both gateways spell it `supportsImages`; some entries
|
|
211
|
+
* also carry `disabledMultimodal`, the negative spelling. Reading anything else
|
|
212
|
+
* reported every model as text-only, which made a vision shim register a second
|
|
213
|
+
* route under the same display name and split the model picker group.
|
|
214
|
+
*/
|
|
215
|
+
supportsImages?: boolean;
|
|
216
|
+
reasoning?: {
|
|
217
|
+
supportedEfforts?: readonly string[];
|
|
218
|
+
defaultEffort?: string;
|
|
219
|
+
canDisableThinking?: boolean;
|
|
220
|
+
};
|
|
221
|
+
descriptionZh?: string;
|
|
222
|
+
descriptionEn?: string;
|
|
223
|
+
supportsToolCall?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Promo tags the upstream attaches to a model.
|
|
226
|
+
*
|
|
227
|
+
* Both gateways send a `tags` array, but the vocabularies are NOT the same
|
|
228
|
+
* and neither uses the words this plugin used to expect:
|
|
229
|
+
*
|
|
230
|
+
* - Global sends `["craft"]`, `["text-to-image"]`, `["text-to-video"]`, `[]`
|
|
231
|
+
* — capability/grouping labels, never `free`.
|
|
232
|
+
* - Zero cost is expressed as `credits: "x0.00"` instead.
|
|
233
|
+
*
|
|
234
|
+
* So this field is a passthrough of what the upstream really said, and
|
|
235
|
+
* `free` is DERIVED from the multiplier rather than awaited as a tag (see
|
|
236
|
+
* {@link isFreeModel}).
|
|
237
|
+
*/
|
|
238
|
+
tags?: readonly string[];
|
|
239
|
+
}
|
|
240
|
+
/** One billing package, already normalised. */
|
|
241
|
+
interface WorkBuddyCreditPackage {
|
|
242
|
+
packageName: string;
|
|
243
|
+
remain: number;
|
|
244
|
+
size: number;
|
|
245
|
+
monthly: boolean;
|
|
246
|
+
refreshAtMs?: number;
|
|
247
|
+
expiresAtMs?: number;
|
|
248
|
+
}
|
|
249
|
+
/** Aggregated credit answer for one credential. */
|
|
250
|
+
interface WorkBuddyCredits {
|
|
251
|
+
total: number;
|
|
252
|
+
packages: readonly WorkBuddyCreditPackage[];
|
|
253
|
+
expiringSoon: number;
|
|
254
|
+
nearestExpiryMs?: number;
|
|
255
|
+
}
|
|
256
|
+
/** Daily check-in activity state. */
|
|
257
|
+
interface WorkBuddyCheckinStatus {
|
|
258
|
+
active: boolean;
|
|
259
|
+
todayCheckedIn: boolean;
|
|
260
|
+
streakDays: number;
|
|
261
|
+
dailyCredit: number;
|
|
262
|
+
todayCredit: number;
|
|
263
|
+
isStreakDay: boolean;
|
|
264
|
+
nextStreakDay: number;
|
|
265
|
+
streakBonusDays: number;
|
|
266
|
+
streakBonusCredit: number;
|
|
267
|
+
/** Upstream-supplied button label; the card falls back to its own copy. */
|
|
268
|
+
claimButtonText?: string;
|
|
269
|
+
}
|
|
270
|
+
/** Daily check-in claim result. */
|
|
271
|
+
interface WorkBuddyCheckinClaim {
|
|
272
|
+
credit: number;
|
|
273
|
+
streakDays: number;
|
|
274
|
+
isStreakDay: boolean;
|
|
275
|
+
}
|
|
276
|
+
/** Result of one upstream chat attempt. */
|
|
277
|
+
type ChatStreamResult = {
|
|
278
|
+
ok: true;
|
|
279
|
+
response: Response;
|
|
280
|
+
} | {
|
|
281
|
+
ok: false;
|
|
282
|
+
kind: UpstreamErrorKind;
|
|
283
|
+
status: number;
|
|
284
|
+
message: string;
|
|
285
|
+
};
|
|
286
|
+
interface UpstreamClientOptions {
|
|
287
|
+
/** Injectable fetch, primarily for tests. */
|
|
288
|
+
fetchImpl?: typeof fetch;
|
|
289
|
+
/** Client version string sent to the upstream. */
|
|
290
|
+
clientVersion?: string;
|
|
291
|
+
/**
|
|
292
|
+
* Override the catalog-fetch backoff, in milliseconds.
|
|
293
|
+
*
|
|
294
|
+
* Tests set this to `[]` (or tiny values) so a retry case does not spend real
|
|
295
|
+
* seconds sleeping. Production uses {@link CATALOG_RETRY_BACKOFF_MS}.
|
|
296
|
+
*/
|
|
297
|
+
catalogRetryBackoffMs?: readonly number[];
|
|
298
|
+
}
|
|
299
|
+
/** Region for a login domain; an empty domain means CN (matching upstream tooling). */
|
|
300
|
+
/** The two gateways WorkBuddy serves: the domestic one and the international one. */
|
|
301
|
+
type WorkBuddyRegion = 'cn' | 'global';
|
|
302
|
+
/**
|
|
303
|
+
* Classify an upstream failure from its HTTP status and body excerpt.
|
|
304
|
+
* Body markers win over status, because the upstream reuses 400/200 for
|
|
305
|
+
* several distinct conditions.
|
|
306
|
+
*/
|
|
307
|
+
export declare function classifyUpstreamError(status: number, body: string): UpstreamErrorKind;
|
|
308
|
+
/**
|
|
309
|
+
* Parse the reset time the upstream reports for a rate limit, when present.
|
|
310
|
+
* Recognises an epoch-millisecond field and the Chinese-localised sentence
|
|
311
|
+
* form, so the pool can resume exactly when the window reopens.
|
|
312
|
+
*/
|
|
313
|
+
export declare function parseRateLimitReset(body: string): number | undefined;
|
|
314
|
+
/** One growth-centre task, flattened from the upstream's loosely-shaped entry. */
|
|
315
|
+
/** One streak tier and what redeeming it pays. */
|
|
316
|
+
interface WorkBuddyStreakTier {
|
|
317
|
+
/** Tier key, e.g. `7d`. */
|
|
318
|
+
tier: string;
|
|
319
|
+
/** Login days the tier needs. */
|
|
320
|
+
days: number;
|
|
321
|
+
credit: number;
|
|
322
|
+
energy: number;
|
|
323
|
+
cards: number;
|
|
324
|
+
/** Lottery draws the tier grants. */
|
|
325
|
+
chances: number;
|
|
326
|
+
/** `locked` / `unlocked` / `claimed`. */
|
|
327
|
+
status: string;
|
|
328
|
+
}
|
|
329
|
+
/** The growth streak as a whole: progress, tiers, makeup cards. */
|
|
330
|
+
interface WorkBuddyStreakStatus {
|
|
331
|
+
/** Current consecutive active days. */
|
|
332
|
+
days: number;
|
|
333
|
+
/** Days active this month. */
|
|
334
|
+
monthTotalDays: number;
|
|
335
|
+
/** Next tier key, e.g. `7d`. */
|
|
336
|
+
nextTier: string;
|
|
337
|
+
/** Days still needed for `nextTier`. */
|
|
338
|
+
nextTierRemaining: number;
|
|
339
|
+
/** Makeup cards in hand. */
|
|
340
|
+
makeupCards: number;
|
|
341
|
+
tiers: readonly WorkBuddyStreakTier[];
|
|
342
|
+
}
|
|
343
|
+
/** One buddy trip state. */
|
|
344
|
+
interface WorkBuddyTravelState {
|
|
345
|
+
/** `idle` (can depart) / `traveling` / `arrived` (can claim). */
|
|
346
|
+
state: string;
|
|
347
|
+
/** Trip id, required to claim an arrived trip. */
|
|
348
|
+
recordId: number;
|
|
349
|
+
/** The once-a-day depart limit has been used. */
|
|
350
|
+
dailyLimitReached: boolean;
|
|
351
|
+
/** Credits an arrived trip pays. */
|
|
352
|
+
rewardCredit: number;
|
|
353
|
+
}
|
|
354
|
+
interface WorkBuddyTask {
|
|
355
|
+
/** Upstream task code; the claim path is built from it. */
|
|
356
|
+
taskCode: string;
|
|
357
|
+
title: string;
|
|
358
|
+
/** Reward in credits, when the task declares one. */
|
|
359
|
+
credit: number;
|
|
360
|
+
/** Reward in energy, when the task declares one. */
|
|
361
|
+
energy: number;
|
|
362
|
+
/** Whether the task carries any reward at all. */
|
|
363
|
+
hasReward: boolean;
|
|
364
|
+
/** Progress target; 0 is a valid value (a task with no counter). */
|
|
365
|
+
target: number;
|
|
366
|
+
/** Current progress; 0 is a valid value. */
|
|
367
|
+
current: number;
|
|
368
|
+
/** Upstream enrolment state: not_accepted / accepted / claimed. */
|
|
369
|
+
acceptStatus: string;
|
|
370
|
+
/** Upstream task state, e.g. `complete`. */
|
|
371
|
+
status: string;
|
|
372
|
+
/** Progress reached its target and the reward is still outstanding. */
|
|
373
|
+
claimable: boolean;
|
|
374
|
+
/** Reward already collected. */
|
|
375
|
+
claimed: boolean;
|
|
376
|
+
/** Upstream marked the task locked (not yet reachable). */
|
|
377
|
+
locked: boolean;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Flatten one upstream task entry.
|
|
381
|
+
*
|
|
382
|
+
* Progress is reported two ways depending on the task: flat `current`/`target`
|
|
383
|
+
* fields, or a nested `progress: {current, target}`. The nested form wins when
|
|
384
|
+
* it carries anything, because a task that reports both puts the live counter
|
|
385
|
+
* there. Entries without a usable `task_code` are dropped — without one the
|
|
386
|
+
* claim path cannot be built.
|
|
387
|
+
*/
|
|
388
|
+
/**
|
|
389
|
+
* The event chain that scores the two Buddy-app tasks.
|
|
390
|
+
*
|
|
391
|
+
* Both tasks accept the same chain, and the chain is a pure value: building it
|
|
392
|
+
* needs no client, so callers (and tests) can hold one without a live upstream.
|
|
393
|
+
*
|
|
394
|
+
* The task text says "upgrade to the desktop client and open it from the app
|
|
395
|
+
* launcher". The scorer does not watch the UI — it watches this event sequence
|
|
396
|
+
* with the desktop fingerprint, which is why the sequence is what gets sent.
|
|
397
|
+
*
|
|
398
|
+
* Measured against the live upstream: progress 0/1 → 1/1 claimable in ~8s.
|
|
399
|
+
*/
|
|
400
|
+
/**
|
|
401
|
+
* A complete "desktop client ran a request successfully" event chain.
|
|
402
|
+
*
|
|
403
|
+
* Six events in the order the real client emits them: task created, message
|
|
404
|
+
* send, request send, message response, message status, request response.
|
|
405
|
+
* Several tasks are scored off this chain (or one that embeds it), because what
|
|
406
|
+
* they measure is "a real request completed", which the client only proves
|
|
407
|
+
* through this exact sequence.
|
|
408
|
+
*
|
|
409
|
+
* Measured: this chain alone lights up `RichMeow_Chat`.
|
|
410
|
+
*/
|
|
411
|
+
export declare function desktopChatEvents(conversationId: string, requestId: string, messageId: string, modelId?: string, modelName?: string): Record<string, unknown>[];
|
|
412
|
+
/**
|
|
413
|
+
* A chat chain plus the two canvas events that score `create_canvas`.
|
|
414
|
+
*
|
|
415
|
+
* Worth +300, the joint largest task on the board. The canvas events ride
|
|
416
|
+
* the same metrics channel as everything else, so no real canvas is needed.
|
|
417
|
+
*
|
|
418
|
+
* Measured: three accounts scored 1/1 from this sequence.
|
|
419
|
+
*/
|
|
420
|
+
export declare function desktopCanvasEvents(conversationId: string, requestId: string): Record<string, unknown>[];
|
|
421
|
+
/**
|
|
422
|
+
* The single event that scores `automation_1` (a scheduled task was created).
|
|
423
|
+
*
|
|
424
|
+
* Measured: two accounts lit it with this event alone.
|
|
425
|
+
*/
|
|
426
|
+
export declare function desktopAutomationCreatedEvent(name: string): Record<string, unknown>;
|
|
427
|
+
export declare function buddyAppEvents(buddyId: string, buddyName: string): Record<string, unknown>[];
|
|
428
|
+
export declare class WorkBuddyUpstreamClient {
|
|
429
|
+
private readonly fetchImpl;
|
|
430
|
+
private readonly clientVersion;
|
|
431
|
+
/** Backoff ladder for `fetchModels`; empty means a single attempt. */
|
|
432
|
+
private readonly catalogRetryBackoffMs;
|
|
433
|
+
/**
|
|
434
|
+
* Optional logger for retry notices.
|
|
435
|
+
*
|
|
436
|
+
* Set by the host so a retry is visible in the log with its attempt count —
|
|
437
|
+
* without it, a retry that eventually succeeds is invisible, and an operator
|
|
438
|
+
* debugging "why was the catalog slow" has nothing to look at.
|
|
439
|
+
*/
|
|
440
|
+
logger: {
|
|
441
|
+
warn?(...args: unknown[]): void;
|
|
442
|
+
info?(...args: unknown[]): void;
|
|
443
|
+
} | undefined;
|
|
444
|
+
constructor(options?: UpstreamClientOptions);
|
|
445
|
+
/**
|
|
446
|
+
* Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
|
|
447
|
+
* force `stream: true` (the upstream rejects non-streaming), convert the
|
|
448
|
+
* DSH `developer` role into `system` (upstream rejects `developer` with
|
|
449
|
+
* business code 11128), and flatten `tool_choice` into its string form.
|
|
450
|
+
*/
|
|
451
|
+
prepareChatBody(raw: string): string;
|
|
452
|
+
/**
|
|
453
|
+
* Parse a raw OpenAI chat body without normalising it.
|
|
454
|
+
*
|
|
455
|
+
* The compactor needs the message array as objects, while `chatStream` only
|
|
456
|
+
* accepts the serialised string form.
|
|
457
|
+
*/
|
|
458
|
+
parseChatBody(raw: string): Record<string, unknown> | undefined;
|
|
459
|
+
/** Re-serialise `base` with a rewritten `messages` array, still normalised. */
|
|
460
|
+
buildChatBody(base: Record<string, unknown>, messages: readonly ChatMessage[]): string;
|
|
461
|
+
/**
|
|
462
|
+
* Run one NON-streaming completion and return the assistant text.
|
|
463
|
+
*
|
|
464
|
+
* Used only for internal compaction (summarising dropped turns). The chat
|
|
465
|
+
* endpoint itself always streams, so this reassembles the SSE frames into a
|
|
466
|
+
* single string. Throws on any failure: the compactor then falls back to
|
|
467
|
+
* plain truncation rather than failing the user's turn.
|
|
468
|
+
*/
|
|
469
|
+
completeChat(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal): Promise<string>;
|
|
470
|
+
/** Forward one chat completion. Never throws for upstream failures. */
|
|
471
|
+
chatStream(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal): Promise<ChatStreamResult>;
|
|
472
|
+
/** POST the token-refresh endpoint; the caller merges the outcome. */
|
|
473
|
+
refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
|
|
474
|
+
/**
|
|
475
|
+
* Fetch the model catalog, keeping the `cli` agent's models only.
|
|
476
|
+
*
|
|
477
|
+
* The two gateways are read differently, because they answer differently:
|
|
478
|
+
*
|
|
479
|
+
* - **CN** serves the roster at `/v2/enterprises/personal/models` and expects
|
|
480
|
+
* the CLI client spelling.
|
|
481
|
+
* - **Global** serves it as part of the product config at `/v3/config`, and
|
|
482
|
+
* only to the DESKTOP client channel. Asking the global host with the CLI UA
|
|
483
|
+
* yields a truncated roster, and the CN path answers HTTP 500 there — which
|
|
484
|
+
* is what left the international provider on its static fallback.
|
|
485
|
+
*
|
|
486
|
+
* Both documents share the `{ models, agents }` entry shape, so the parsing
|
|
487
|
+
* below is common to the two branches.
|
|
488
|
+
*/
|
|
489
|
+
fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
|
|
490
|
+
/**
|
|
491
|
+
* One catalog attempt, without retries.
|
|
492
|
+
*
|
|
493
|
+
* Split out so {@link fetchModels} can retry it: the failure this guards
|
|
494
|
+
* against is a startup network hiccup, where several independent components
|
|
495
|
+
* see `fetch failed` inside the same second and the very next request
|
|
496
|
+
* succeeds — exactly the case a single attempt turns into "the user's model
|
|
497
|
+
* list is missing half its entries for the rest of the session".
|
|
498
|
+
*/
|
|
499
|
+
private fetchModelsOnce;
|
|
500
|
+
/** Read-only credits query, aggregated by package. Does not consume credits. */
|
|
501
|
+
fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
|
|
502
|
+
/** Query today's check-in status without changing account state. */
|
|
503
|
+
fetchCheckinStatus(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinStatus>;
|
|
504
|
+
/** Claim today's check-in reward. The browser route guards this mutation. */
|
|
505
|
+
claimDailyCheckin(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinClaim>;
|
|
506
|
+
/**
|
|
507
|
+
* Public fingerprint fields every desktop event carries.
|
|
508
|
+
*
|
|
509
|
+
* The upstream scores a desktop-fingerprinted task only when the event looks
|
|
510
|
+
* like it came from the desktop client: the same field set, the same stable
|
|
511
|
+
* device ids, the same build. A partial map is accepted with 200 and scores
|
|
512
|
+
* nothing, so these are copied wholesale rather than trimmed.
|
|
513
|
+
*
|
|
514
|
+
* `machineId`/`sessionId` are DERIVED from the account uid, never random: a
|
|
515
|
+
* new device id on every call is itself a signal that the traffic is not a
|
|
516
|
+
* real client.
|
|
517
|
+
*/
|
|
518
|
+
desktopFingerprint(credential: WorkBuddyCredential): Record<string, unknown>;
|
|
519
|
+
/**
|
|
520
|
+
* Send desktop-fingerprinted events to the growth system.
|
|
521
|
+
*
|
|
522
|
+
* The body is an ARRAY of events, and every event carries the full desktop
|
|
523
|
+
* fingerprint plus its own business fields. Different tasks recognise
|
|
524
|
+
* different fingerprint families (CLI / desktop / web), which is why this is
|
|
525
|
+
* separate from {@link reportActivity}: they are not interchangeable.
|
|
526
|
+
*
|
|
527
|
+
* Business fields win over the fingerprint, so a caller can override a device
|
|
528
|
+
* id to align with a real install.
|
|
529
|
+
*/
|
|
530
|
+
reportDesktopEvents(credential: WorkBuddyCredential, events: readonly Record<string, unknown>[]): Promise<void>;
|
|
531
|
+
/**
|
|
532
|
+
* Report one chat-activity event to the growth system.
|
|
533
|
+
*
|
|
534
|
+
* The body is an ARRAY holding a single `chat_request_send` event, and every
|
|
535
|
+
* field is filled in: a three-field minimal event is accepted with 200 and
|
|
536
|
+
* then silently dropped, so the full shape is load-bearing rather than
|
|
537
|
+
* cosmetic. `userId` is the one field the server actually keys on — without
|
|
538
|
+
* it the request still answers 200 and scores nothing.
|
|
539
|
+
*
|
|
540
|
+
* One report per account per day is the quota the reference panel settled on;
|
|
541
|
+
* a single report lights the growth streak and unlocks the `first_buddy`
|
|
542
|
+
* family, which is why this runs before the task-centre pass.
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Send a WEB-fingerprinted event.
|
|
546
|
+
*
|
|
547
|
+
* A third fingerprint family, alongside CLI and desktop: a browser shape
|
|
548
|
+
* posted to the web origin with x-client-platform: web. Page-behaviour
|
|
549
|
+
* tasks such as `Library_read` are scored on it. Measured:
|
|
550
|
+
* `library_doc_intro_click` scored about four seconds after landing.
|
|
551
|
+
*/
|
|
552
|
+
reportWebEvent(credential: WorkBuddyCredential, eventCode: string, pageUrl: string, elementId: string, elementName: string): Promise<void>;
|
|
553
|
+
/**
|
|
554
|
+
* Apply an appearance theme on the account.
|
|
555
|
+
*
|
|
556
|
+
* The theme task is scored on the `appearance_skin_apply` event, not on this
|
|
557
|
+
* call — but the event alone is not enough either. The pair is what a real
|
|
558
|
+
* client produces: it PATCHes the account's selected skin, then reports the
|
|
559
|
+
* event as the settings page closes. Measured on the reference panel after
|
|
560
|
+
* the earlier "the API alone does not score" reading was corrected.
|
|
561
|
+
*/
|
|
562
|
+
setAppearanceTheme(credential: WorkBuddyCredential, resourceKey: string): Promise<void>;
|
|
563
|
+
/**
|
|
564
|
+
* The platform's expert marketplace.
|
|
565
|
+
*
|
|
566
|
+
* Needed before any expert can be summoned: the scorer verifies that the
|
|
567
|
+
* expert id exists on the platform, so a made-up id scores nothing. The
|
|
568
|
+
* response carries the display fields the summon events replay.
|
|
569
|
+
*/
|
|
570
|
+
marketExpertList(credential: WorkBuddyCredential, expertType?: 'agent' | 'team' | ''): Promise<readonly MarketExpert[]>;
|
|
571
|
+
/**
|
|
572
|
+
* Open a REAL desktop conversation and return the ids the server assigned.
|
|
573
|
+
*
|
|
574
|
+
* The expert and skill tasks join their events to a conversation the server
|
|
575
|
+
* has actually seen, so a locally invented `requestId` scores nothing. This
|
|
576
|
+
* starts a chat, reads the server's id out of the SSE stream, then drops the
|
|
577
|
+
* rest of the stream — the answer itself is irrelevant, only its identity is.
|
|
578
|
+
*
|
|
579
|
+
* Returns `undefined` instead of throwing when the conversation cannot be
|
|
580
|
+
* opened or carries no recognisable id, because every caller is a best-effort
|
|
581
|
+
* task chain.
|
|
582
|
+
*/
|
|
583
|
+
openConversation(credential: WorkBuddyCredential, expertId?: string, signal?: AbortSignal): Promise<{
|
|
584
|
+
conversationId: string;
|
|
585
|
+
requestId: string;
|
|
586
|
+
} | undefined>;
|
|
587
|
+
/**
|
|
588
|
+
* Report one chat-activity event to the growth system.
|
|
589
|
+
*
|
|
590
|
+
* The body is an ARRAY holding a single chat_request_send event, and every
|
|
591
|
+
* field is filled in: a three-field minimal event is accepted with 200 and
|
|
592
|
+
* then silently dropped, so the full shape is load-bearing rather than
|
|
593
|
+
* cosmetic. userId is the one field the server actually keys on.
|
|
594
|
+
*
|
|
595
|
+
* One report per account per day is the quota the reference panel settled
|
|
596
|
+
* on; a single report lights the growth streak and unlocks the first_buddy
|
|
597
|
+
* family, which is why this runs before the task-centre pass.
|
|
598
|
+
*/
|
|
599
|
+
reportActivity(credential: WorkBuddyCredential, conversationId?: string): Promise<void>;
|
|
600
|
+
/**
|
|
601
|
+
* Read back the growth streak in days.
|
|
602
|
+
*
|
|
603
|
+
* This is the read-only oracle for {@link reportActivity}: a report that
|
|
604
|
+
* returned 200 yet left the streak untouched was silently dropped (a missing
|
|
605
|
+
* `userId` is the usual cause), so callers verify instead of trusting the
|
|
606
|
+
* status code.
|
|
607
|
+
*
|
|
608
|
+
* Two shape traps, both measured against the live upstream:
|
|
609
|
+
*
|
|
610
|
+
* - The path carries NO `/v2` prefix, unlike its sibling task endpoints under
|
|
611
|
+
* `/v2/activity/growth/*`. Asking for the `/v2` form does not 404; it
|
|
612
|
+
* answers with a body that carries no `streak` object at all.
|
|
613
|
+
* - The counter is nested as `data.streak.days`, not `data.days`. Reading the
|
|
614
|
+
* flat field yields a constant 0, which would make every successful report
|
|
615
|
+
* look like a silent drop.
|
|
616
|
+
*
|
|
617
|
+
* Returns 0 only when the field is genuinely absent.
|
|
618
|
+
*/
|
|
619
|
+
growthStreakDays(credential: WorkBuddyCredential): Promise<number>;
|
|
620
|
+
/**
|
|
621
|
+
* The full streak picture: days, tier unlock state, and what each tier pays.
|
|
622
|
+
*
|
|
623
|
+
* Read before redeeming, because the tier state is the only honest answer to
|
|
624
|
+
* "is there anything to claim": the redeem endpoint answers 403 for a locked
|
|
625
|
+
* tier, which is indistinguishable from a real failure once the response is
|
|
626
|
+
* just an error.
|
|
627
|
+
*/
|
|
628
|
+
growthStreakFull(credential: WorkBuddyCredential): Promise<WorkBuddyStreakStatus>;
|
|
629
|
+
/**
|
|
630
|
+
* Redeem one unlocked streak tier.
|
|
631
|
+
*
|
|
632
|
+
* A locked tier answers 403 ("连续登录天数不足"); callers check the status from
|
|
633
|
+
* {@link growthStreakFull} first, so this only throws for genuine failures.
|
|
634
|
+
* The client token is the upstream's idempotency key — a fresh one per attempt
|
|
635
|
+
* keeps a retry from being read as a duplicate of the last one.
|
|
636
|
+
*/
|
|
637
|
+
redeemStreakTier(credential: WorkBuddyCredential, tier: string): Promise<void>;
|
|
638
|
+
/** How many lottery draws are available right now. */
|
|
639
|
+
lotteryChances(credential: WorkBuddyCredential): Promise<number>;
|
|
640
|
+
/**
|
|
641
|
+
* Draw the lottery once.
|
|
642
|
+
*
|
|
643
|
+
* Returns the raw prize payload: its shape is set by the running campaign, so
|
|
644
|
+
* it is passed through rather than modelled.
|
|
645
|
+
*/
|
|
646
|
+
lotteryDraw(credential: WorkBuddyCredential): Promise<unknown>;
|
|
647
|
+
/**
|
|
648
|
+
* The buddy profile, or undefined when the account has no buddy yet.
|
|
649
|
+
*
|
|
650
|
+
* `data.buddy` is null / absent / an empty object depending on how far the
|
|
651
|
+
* account got, and all three mean the same thing to a caller: adopt first.
|
|
652
|
+
*/
|
|
653
|
+
buddyInfo(credential: WorkBuddyCredential): Promise<{
|
|
654
|
+
instanceId: number;
|
|
655
|
+
name: string;
|
|
656
|
+
} | undefined>;
|
|
657
|
+
/** Agree to the buddy terms. Idempotent upstream. */
|
|
658
|
+
buddyAgree(credential: WorkBuddyCredential): Promise<void>;
|
|
659
|
+
/**
|
|
660
|
+
* Adopt the first buddy.
|
|
661
|
+
*
|
|
662
|
+
* Gated upstream on having reported activity that day: without it the answer
|
|
663
|
+
* is 400 "first_buddy task not completed yet". Callers treat that as "not yet"
|
|
664
|
+
* rather than an error, which is why it is thrown as-is for them to classify.
|
|
665
|
+
*/
|
|
666
|
+
buddyAdoptFirst(credential: WorkBuddyCredential): Promise<void>;
|
|
667
|
+
/** Current travel state for the account's buddy. */
|
|
668
|
+
buddyTravelStatus(credential: WorkBuddyCredential): Promise<WorkBuddyTravelState>;
|
|
669
|
+
/**
|
|
670
|
+
* Send the buddy travelling.
|
|
671
|
+
*
|
|
672
|
+
* The location is always 4 (古镇客栈): the four locations have identical
|
|
673
|
+
* reward and duration ranges, so there is nothing to optimise.
|
|
674
|
+
*/
|
|
675
|
+
buddyTravelDepart(credential: WorkBuddyCredential, locationId?: number): Promise<void>;
|
|
676
|
+
/**
|
|
677
|
+
* Collect an arrived trip's reward.
|
|
678
|
+
*
|
|
679
|
+
* `recordId` is required and comes from the status read; the upstream rejects
|
|
680
|
+
* a claim without it.
|
|
681
|
+
*/
|
|
682
|
+
buddyTravelClaim(credential: WorkBuddyCredential, recordId: number): Promise<number>;
|
|
683
|
+
/** Whether yesterday is a gap in the activity heatmap. */
|
|
684
|
+
heatmapYesterdayMissed(credential: WorkBuddyCredential): Promise<boolean>;
|
|
685
|
+
/** Spend one makeup card on a date. Idempotent for an already-filled date. */
|
|
686
|
+
useMakeupCard(credential: WorkBuddyCredential, date: string): Promise<void>;
|
|
687
|
+
/**
|
|
688
|
+
* Call a growth-domain endpoint and return its unwrapped `data`.
|
|
689
|
+
*
|
|
690
|
+
* These endpoints live on the chat host with the billing header set, and
|
|
691
|
+
* carry the same envelope as everything else. Centralised here because every
|
|
692
|
+
* growth call needs the identical envelope check.
|
|
693
|
+
*/
|
|
694
|
+
private growthJson;
|
|
695
|
+
/** Legacy thin wrapper kept for `status`/`doctor`: returns raw envelope data. */
|
|
696
|
+
credits(credential: WorkBuddyCredential): Promise<{
|
|
697
|
+
ok: true;
|
|
698
|
+
data: unknown;
|
|
699
|
+
} | {
|
|
700
|
+
ok: false;
|
|
701
|
+
message: string;
|
|
702
|
+
}>;
|
|
703
|
+
/**
|
|
704
|
+
* Fetch the growth task list for one account.
|
|
705
|
+
*
|
|
706
|
+
* The upstream answers `data.tasks[]`, and `claimable` is derived locally —
|
|
707
|
+
* the upstream does not mark it. Only a task whose progress reached its
|
|
708
|
+
* target and that is not already claimed counts as eligible.
|
|
709
|
+
*/
|
|
710
|
+
listTasks(credential: WorkBuddyCredential): Promise<readonly WorkBuddyTask[]>;
|
|
711
|
+
/**
|
|
712
|
+
* Accept (enrol in) tasks by code.
|
|
713
|
+
*
|
|
714
|
+
* Accepting is the "sign up" half: it produces no progress by itself, and the
|
|
715
|
+
* upstream answers success for an already-accepted task, so replaying this is
|
|
716
|
+
* safe. Progress is lit by real activity (a chat, an activity report).
|
|
717
|
+
*/
|
|
718
|
+
acceptTasks(credential: WorkBuddyCredential, taskCodes: readonly string[]): Promise<void>;
|
|
719
|
+
/**
|
|
720
|
+
* Claim one task's reward.
|
|
721
|
+
*
|
|
722
|
+
* Two details differ from list/accept and are load-bearing:
|
|
723
|
+
*
|
|
724
|
+
* - The task code rides the PATH, not the body.
|
|
725
|
+
* - It is served by the web origin, not the chat host, and only when the
|
|
726
|
+
* request carries the growth-centre Origin/Referer plus
|
|
727
|
+
* `x-client-platform: web`. The chat host's `/reward/claim` path does not
|
|
728
|
+
* exist and answers 400 "task not completed".
|
|
729
|
+
*
|
|
730
|
+
* A repeat claim answers `already_claimed` with zero credit, which is treated
|
|
731
|
+
* as success so the caller can stay idempotent.
|
|
732
|
+
*/
|
|
733
|
+
claimTaskReward(credential: WorkBuddyCredential, taskCode: string): Promise<{
|
|
734
|
+
credit: number;
|
|
735
|
+
energy: number;
|
|
736
|
+
}>;
|
|
737
|
+
}
|
|
738
|
+
//#endregion
|
|
739
|
+
//#region src/accounts.d.ts
|
|
740
|
+
/** Minimal upstream surface the pool needs to refresh a token (no circular import). */
|
|
741
|
+
interface TokenRefresher {
|
|
742
|
+
refreshToken(credential: WorkBuddyCredential): Promise<{
|
|
743
|
+
accessToken: string;
|
|
744
|
+
refreshToken?: string;
|
|
745
|
+
expiresInSec?: number;
|
|
746
|
+
domain?: string;
|
|
747
|
+
}>;
|
|
748
|
+
}
|
|
749
|
+
/** Live auth file name the WorkBuddy desktop app writes. */
|
|
750
|
+
export declare const WORKBUDDY_LIVE_FILENAME = "workbuddy-desktop.info";
|
|
751
|
+
/** Snapshot files left behind by previous logins share this prefix. */
|
|
752
|
+
/** Env override for the auth file or its directory. */
|
|
753
|
+
export declare const WORKBUDDY_AUTH_FILE_ENV = "WORKBUDDY_AUTH_FILE";
|
|
754
|
+
/** One parsed WorkBuddy credential. */
|
|
755
|
+
/**
|
|
756
|
+
* A credential file the pool could not turn into an account.
|
|
757
|
+
*
|
|
758
|
+
* Reported (not swallowed) because the count is otherwise a lie: a directory
|
|
759
|
+
* holding four files that yields two accounts looks like two accounts were
|
|
760
|
+
* deleted, when in fact two files were unreadable. Naming the file and the
|
|
761
|
+
* reason is what makes the difference visible.
|
|
762
|
+
*/
|
|
763
|
+
interface WorkBuddySkippedFile {
|
|
764
|
+
path: string;
|
|
765
|
+
reason: 'encrypted' | 'unreadable' | 'malformed';
|
|
766
|
+
}
|
|
767
|
+
interface WorkBuddyCredential {
|
|
768
|
+
accessToken: string;
|
|
769
|
+
refreshToken: string;
|
|
770
|
+
expiresAtMs: number;
|
|
771
|
+
refreshExpiresAtMs?: number;
|
|
772
|
+
/**
|
|
773
|
+
* When the upstream says it issued this token (`auth.lastRefreshTime`).
|
|
774
|
+
*
|
|
775
|
+
* This, not `expiresAtMs`, is the reliable freshness signal: the upstream
|
|
776
|
+
* never rewrites a stored expiry when it revokes a token, so a long-dead
|
|
777
|
+
* backup can claim to expire later than the token that actually works.
|
|
778
|
+
* Absent on documents the desktop app did not write (the plugin's own
|
|
779
|
+
* refreshed copy, older builds).
|
|
780
|
+
*/
|
|
781
|
+
lastRefreshAtMs?: number;
|
|
782
|
+
nickname?: string;
|
|
783
|
+
uin?: string;
|
|
784
|
+
uid?: string;
|
|
785
|
+
enterpriseId?: string;
|
|
786
|
+
domain: string;
|
|
787
|
+
/** Where this credential came from, for diagnostics. */
|
|
788
|
+
sourcePath: string;
|
|
789
|
+
}
|
|
790
|
+
/** An account is a credential plus pool bookkeeping. */
|
|
791
|
+
interface WorkBuddyAccount {
|
|
792
|
+
/** Stable pool key: sha256 of the billing identity. */
|
|
793
|
+
id: string;
|
|
794
|
+
/** Short human label, e.g. `青楫渡` or `青楫渡#29890334`. */
|
|
795
|
+
label: string;
|
|
796
|
+
credential: WorkBuddyCredential;
|
|
797
|
+
/**
|
|
798
|
+
* Epoch ms until which this account is skipped for EVERY model. Only set by
|
|
799
|
+
* account-wide cooldowns (callers that penalize without a model id). The
|
|
800
|
+
* upstream rate limit is actually per-model ("可切换其他模型继续使用"), so
|
|
801
|
+
* routine 429s are tracked in {@link modelCooldowns} instead and never ban a
|
|
802
|
+
* whole account.
|
|
803
|
+
*/
|
|
804
|
+
cooldownUntilMs: number;
|
|
805
|
+
/**
|
|
806
|
+
* Per-model cooldowns, keyed by upstream model id → epoch ms until that model
|
|
807
|
+
* on THIS account is skipped. A 429 on `hy4-preview` cools only that model
|
|
808
|
+
* here; `hy3`/`glm-*` on the same account keep serving.
|
|
809
|
+
*/
|
|
810
|
+
modelCooldowns: Record<string, number>;
|
|
811
|
+
/** Consecutive rate-limit hits, for diagnostics. */
|
|
812
|
+
rateLimitHits: number;
|
|
813
|
+
/**
|
|
814
|
+
* Epoch ms until which this account is skipped because the upstream REJECTED
|
|
815
|
+
* its sign-in (401/403), not because of a rate limit.
|
|
816
|
+
*
|
|
817
|
+
* A revoked session is invisible from the credential file: the upstream does
|
|
818
|
+
* not rewrite `expiresAtMs` when it kills a refresh token, so a dead account
|
|
819
|
+
* looks perfectly fresh and, without this field, gets picked again on the very
|
|
820
|
+
* next attempt. With `maxAttempts` retries that meant one dead account could
|
|
821
|
+
* absorb an entire request while healthy accounts were never tried.
|
|
822
|
+
*
|
|
823
|
+
* Only a fresh sign-in in the desktop app clears it, so the cooldown is long;
|
|
824
|
+
* `0`/undefined means "not known to be dead".
|
|
825
|
+
*/
|
|
826
|
+
credentialDeadUntilMs?: number;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Platform-default directories holding the desktop app's auth files.
|
|
830
|
+
* Windows probes Local before Roaming; a redirected profile still resolves
|
|
831
|
+
* through the env location.
|
|
832
|
+
*/
|
|
833
|
+
export declare function defaultDesktopAuthDirs(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string[];
|
|
834
|
+
/**
|
|
835
|
+
* Parse a WorkBuddy auth document. Accepts the nested desktop shape
|
|
836
|
+
* `{"auth":{...},"account":{...}}` and the flat panel shape; returns undefined
|
|
837
|
+
* when there is no usable access token.
|
|
838
|
+
*
|
|
839
|
+
* `decrypt` opens the desktop app's `$wbEncrypted` field wrapper (5.6.0+, both
|
|
840
|
+
* platforms). Absent means "plain-string builds only", which is what every
|
|
841
|
+
* caller without an at-rest key should pass.
|
|
842
|
+
*/
|
|
843
|
+
export declare function parseWorkBuddyAuth(text: string, sourcePath: string, decrypt?: (field: unknown) => string): WorkBuddyCredential | undefined;
|
|
844
|
+
export declare function workbuddyAccountId(credential: Pick<WorkBuddyCredential, 'uin' | 'uid' | 'nickname'>): string;
|
|
845
|
+
/** Every directory the pool should scan, in probe order. */
|
|
846
|
+
export declare function candidateAuthDirs(env?: NodeJS.ProcessEnv): string[];
|
|
847
|
+
/** How the pool chooses which account serves the next request. */
|
|
848
|
+
type AccountDistribution = 'priority' | 'round-robin' | 'balanced' | 'sticky' | 'expiry';
|
|
849
|
+
interface AccountPoolOptions {
|
|
850
|
+
/** Logger for discovery and rotation events. */
|
|
851
|
+
logger?: {
|
|
852
|
+
info?(...args: unknown[]): void;
|
|
853
|
+
warn(...args: unknown[]): void;
|
|
854
|
+
error?(...args: unknown[]): void;
|
|
855
|
+
};
|
|
856
|
+
/** Override the directories scanned (tests). */
|
|
857
|
+
authDirs?: readonly string[];
|
|
858
|
+
/** How long a rate-limited account stays out of rotation. */
|
|
859
|
+
cooldownMs?: number;
|
|
860
|
+
/** How long an account rests after its credits run out (default 30 minutes). */
|
|
861
|
+
exhaustCooldownMs?: number;
|
|
862
|
+
/**
|
|
863
|
+
* How long an account rests after the upstream rejects its sign-in
|
|
864
|
+
* (default 30 minutes). Only a fresh sign-in in the desktop app clears it.
|
|
865
|
+
*/
|
|
866
|
+
credentialDeadCooldownMs?: number;
|
|
867
|
+
/** Upstream client used to refresh near-expiry tokens. */
|
|
868
|
+
client?: TokenRefresher;
|
|
869
|
+
/** Refresh this long before actual expiry; default five minutes. */
|
|
870
|
+
refreshMarginMs?: number;
|
|
871
|
+
/**
|
|
872
|
+
* How requests are spread across the pool.
|
|
873
|
+
*
|
|
874
|
+
* - `priority` (default): one account serves every request until it is
|
|
875
|
+
* rate-limited, then the next in order takes over. Credits drain one
|
|
876
|
+
* account at a time, and a cooled account resumes at the head of the
|
|
877
|
+
* queue the moment its window resets.
|
|
878
|
+
* - `round-robin`: consecutive requests rotate through the pool so the
|
|
879
|
+
* spend spreads evenly.
|
|
880
|
+
* - `sticky`: one account per conversation, and a new conversation moves to
|
|
881
|
+
* the next account in order. Keeps the upstream prompt cache warm inside a
|
|
882
|
+
* conversation while still spreading spend across conversations.
|
|
883
|
+
* - `expiry`: the account whose one-off credit packs expire soonest serves
|
|
884
|
+
* first, so use-it-or-lose-it credits are spent before they die. Accounts
|
|
885
|
+
* with no known expiry sort last, so a pool that has never been probed
|
|
886
|
+
* behaves like `priority`.
|
|
887
|
+
*/
|
|
888
|
+
distribution?: AccountDistribution;
|
|
889
|
+
}
|
|
890
|
+
export declare class WorkBuddyAccountPool {
|
|
891
|
+
private readonly logger;
|
|
892
|
+
private authDirs;
|
|
893
|
+
private cooldownMs;
|
|
894
|
+
/**
|
|
895
|
+
* How long an account stays out of rotation after the upstream reports its
|
|
896
|
+
* credits are spent. Credit packs reset on their own schedule rather than on a
|
|
897
|
+
* rate-limit window, so this is much longer than `cooldownMs`.
|
|
898
|
+
*/
|
|
899
|
+
private exhaustCooldownMs;
|
|
900
|
+
/**
|
|
901
|
+
* How long an account stays out of rotation after the upstream rejected its
|
|
902
|
+
* sign-in. Cleared by a newer credential file or an explicit sign-in.
|
|
903
|
+
*/
|
|
904
|
+
private credentialDeadCooldownMs;
|
|
905
|
+
private readonly client;
|
|
906
|
+
private readonly refreshMarginMs;
|
|
907
|
+
private accounts;
|
|
908
|
+
/**
|
|
909
|
+
* Files the last scan could not read, with the reason.
|
|
910
|
+
*
|
|
911
|
+
* Surfaced so "2 accounts" can be told apart from "4 files, 2 unreadable" —
|
|
912
|
+
* the difference between accounts being gone and files being unopenable.
|
|
913
|
+
*/
|
|
914
|
+
private skippedFiles;
|
|
915
|
+
private distribution;
|
|
916
|
+
/** Cursor for round-robin mode; unused under priority distribution. */
|
|
917
|
+
private cursor;
|
|
918
|
+
/**
|
|
919
|
+
* `sticky` mode: conversation key → account id.
|
|
920
|
+
*
|
|
921
|
+
* A conversation that keeps the same account also keeps that account's
|
|
922
|
+
* upstream prompt cache warm — the cache is per tenant, so rotating accounts
|
|
923
|
+
* mid-conversation pays full prompt cost on every turn. Insertion order is
|
|
924
|
+
* the LRU order: re-binding deletes then re-inserts.
|
|
925
|
+
*/
|
|
926
|
+
private readonly affinity;
|
|
927
|
+
private lastScanAtMs;
|
|
928
|
+
/**
|
|
929
|
+
* The account id the user pinned, or undefined for none.
|
|
930
|
+
*
|
|
931
|
+
* Field rather than a public property so that the only way in is
|
|
932
|
+
* {@link WorkBuddyPool.prefer} and the only way out is {@link WorkBuddyPool.preferredId}:
|
|
933
|
+
* a mutable public field would let a caller pin an account without the pool
|
|
934
|
+
* knowing, and a getter alone would let a caller read a pin the pool has
|
|
935
|
+
* already forgotten.
|
|
936
|
+
*/
|
|
937
|
+
private preferredAccountId;
|
|
938
|
+
/**
|
|
939
|
+
* Account ids the user switched off on the card.
|
|
940
|
+
*
|
|
941
|
+
* Disabling is a user preference rather than a property of the credential:
|
|
942
|
+
* `scan()` rebuilds every account object from the auth files, so the set
|
|
943
|
+
* lives on the pool and is re-applied from settings after each scan.
|
|
944
|
+
*/
|
|
945
|
+
private disabledIds;
|
|
946
|
+
/**
|
|
947
|
+
* Account ids the user threw out of the pool for good.
|
|
948
|
+
*
|
|
949
|
+
* Enforced BEFORE the credential is parsed: `scan()` skips a file whose
|
|
950
|
+
* identity is already ignored, so an ignored account costs no at-rest key
|
|
951
|
+
* lookup (which spawns the desktop app on 5.6.0+) and cannot re-enter the pool
|
|
952
|
+
* when the app writes a fresh sign-in for it. That is the difference from
|
|
953
|
+
* {@link disabledIds}, which only filters at pick time and leaves the account
|
|
954
|
+
* listed, readable and re-discoverable.
|
|
955
|
+
*
|
|
956
|
+
* The set is supplied by the host from the plugin's own ignore file, and is
|
|
957
|
+
* replaced wholesale on every {@link applyIgnored} so removing an entry takes
|
|
958
|
+
* effect on the next scan without a restart.
|
|
959
|
+
*/
|
|
960
|
+
private ignoredIds;
|
|
961
|
+
/**
|
|
962
|
+
* Per-account credit floor, keyed by account id. 0 (or absent) means "spend
|
|
963
|
+
* it all".
|
|
964
|
+
*
|
|
965
|
+
* A reserved balance is protection, not a hard limit the upstream knows
|
|
966
|
+
* about: the pool simply stops picking that account once its last known
|
|
967
|
+
* balance is at or below the floor, so the user keeps a cushion instead of
|
|
968
|
+
* draining every account to zero.
|
|
969
|
+
*/
|
|
970
|
+
private creditReserves;
|
|
971
|
+
/**
|
|
972
|
+
* Last known credit balance per account, epoch ms aside.
|
|
973
|
+
*
|
|
974
|
+
* Refreshed in the background after a successful request, so a pick can
|
|
975
|
+
* consult it. An account with no reading is treated as usable: refusing to
|
|
976
|
+
* pick an account just because its balance has not been checked yet would
|
|
977
|
+
* strand a healthy pool, and the first 402 still cools it as before.
|
|
978
|
+
*/
|
|
979
|
+
private creditBalances;
|
|
980
|
+
/**
|
|
981
|
+
* Nearest expiry among each account's one-off credit packs, epoch ms.
|
|
982
|
+
*
|
|
983
|
+
* Credit packs are use-it-or-lose-it, so "which account should serve next"
|
|
984
|
+
* has a second honest answer besides "which one is idle": the one whose
|
|
985
|
+
* credits die soonest. Only one-off packs count — monthly packs refresh on
|
|
986
|
+
* their own cycle and are therefore never urgent — which is why this is
|
|
987
|
+
* written from the same `fetchCredits` reading that fills `creditBalances`.
|
|
988
|
+
*
|
|
989
|
+
* An absent entry means "never probed, or nothing is about to expire". Both
|
|
990
|
+
* are the same thing to `expiry` mode: not urgent, so it sorts last.
|
|
991
|
+
*/
|
|
992
|
+
private creditExpiry;
|
|
993
|
+
/**
|
|
994
|
+
* Last time each account served a request, epoch ms. Drives the idle term
|
|
995
|
+
* of the priority-mode weighting below: an account that just served loses to
|
|
996
|
+
* one that has been idle, so a small pool stops hammering a single account.
|
|
997
|
+
*
|
|
998
|
+
* In-memory on purpose: it only biases the next pick, so a cold start that
|
|
999
|
+
* treats every account as idle is the right default. Not keyed by id lookup
|
|
1000
|
+
* misses because a removed account simply disappears from the map on re-scan.
|
|
1001
|
+
*/
|
|
1002
|
+
private lastUsedAt;
|
|
1003
|
+
private refreshInflight;
|
|
1004
|
+
constructor(options?: AccountPoolOptions);
|
|
1005
|
+
/**
|
|
1006
|
+
* Re-apply configuration that only affects discovery and cooldown policy,
|
|
1007
|
+
* without rebuilding the pool. A later `scan()` uses the new auth dirs and
|
|
1008
|
+
* cooldown window; existing accounts keep their in-memory state.
|
|
1009
|
+
*/
|
|
1010
|
+
applyConfig(options: {
|
|
1011
|
+
authDirs?: readonly string[];
|
|
1012
|
+
cooldownMs?: number;
|
|
1013
|
+
exhaustCooldownMs?: number;
|
|
1014
|
+
distribution?: AccountDistribution;
|
|
1015
|
+
disabledAccountIds?: readonly string[];
|
|
1016
|
+
/** Per-account credit floor, keyed by account id. Absent keeps the current map. */
|
|
1017
|
+
creditReserves?: Readonly<Record<string, number>>;
|
|
1018
|
+
}): void;
|
|
1019
|
+
/**
|
|
1020
|
+
* Replace the permanent ignore list.
|
|
1021
|
+
*
|
|
1022
|
+
* Also drops any already-discovered account that is now ignored, so the change
|
|
1023
|
+
* is visible without waiting for the next scan: the card refreshes its status
|
|
1024
|
+
* document right after the write, and an account still sitting in `accounts`
|
|
1025
|
+
* would keep showing up there.
|
|
1026
|
+
*/
|
|
1027
|
+
applyIgnored(ids: Iterable<string>): void;
|
|
1028
|
+
/** Whether this account has been thrown out of the pool for good. */
|
|
1029
|
+
isIgnored(accountId: string): boolean;
|
|
1030
|
+
/** Every ignored id currently in force, in insertion order. */
|
|
1031
|
+
ignoredIdsInOrder(): string[];
|
|
1032
|
+
/**
|
|
1033
|
+
* Credential files the last scan could not read, with the reason.
|
|
1034
|
+
*
|
|
1035
|
+
* Exposed because a short account list is otherwise indistinguishable from a
|
|
1036
|
+
* broken one: with this, the card can say "2 accounts, 2 files unreadable"
|
|
1037
|
+
* instead of silently showing half a pool.
|
|
1038
|
+
*/
|
|
1039
|
+
skippedFilesInOrder(): readonly WorkBuddySkippedFile[];
|
|
1040
|
+
/** Rescan the auth directories and merge newly discovered accounts. */
|
|
1041
|
+
scan(): Promise<WorkBuddyAccount[]>;
|
|
1042
|
+
/** All accounts, cooldown state included. */
|
|
1043
|
+
list(region?: WorkBuddyRegion): readonly WorkBuddyAccount[];
|
|
1044
|
+
/**
|
|
1045
|
+
* Accounts currently eligible to serve a request.
|
|
1046
|
+
*
|
|
1047
|
+
* With a `modelId`, an account is eligible when it is not account-wide cooled
|
|
1048
|
+
* AND that model is not cooling on it — so a 429 on `hy4-preview` only keeps
|
|
1049
|
+
* that model out while `hy3` on the same account stays usable. Without a
|
|
1050
|
+
* model id the legacy account-wide check applies (callers that cannot name a
|
|
1051
|
+
* model, e.g. CLI diagnostics).
|
|
1052
|
+
*/
|
|
1053
|
+
private available;
|
|
1054
|
+
/**
|
|
1055
|
+
* Why no account is available right now, for an accurate error.
|
|
1056
|
+
*
|
|
1057
|
+
* The pool can be empty for reasons that need OPPOSITE remedies: nobody is
|
|
1058
|
+
* signed in (the user must sign in), every account is rate-limited (the user
|
|
1059
|
+
* must wait, and retrying later works), or every account was switched off /
|
|
1060
|
+
* ignored (the user must re-enable one). Reporting all of them as "no
|
|
1061
|
+
* credential, sign in" sent users to re-authenticate over a temporary 429 —
|
|
1062
|
+
* observed as an "API key invalid" panel for a model that was merely cooling.
|
|
1063
|
+
*
|
|
1064
|
+
* Counts are over the region's accounts, since a provider only ever sees its
|
|
1065
|
+
* own gateway.
|
|
1066
|
+
*/
|
|
1067
|
+
unavailableReason(modelId?: string, region?: WorkBuddyRegion): {
|
|
1068
|
+
total: number;
|
|
1069
|
+
cooling: number;
|
|
1070
|
+
disabled: number;
|
|
1071
|
+
/** Accounts skipped because the upstream rejected their sign-in. */
|
|
1072
|
+
dead: number;
|
|
1073
|
+
reason: 'empty' | 'cooling' | 'disabled' | 'reserve' | 'session_dead' | 'none';
|
|
1074
|
+
};
|
|
1075
|
+
/** Round-robin: the legacy cursor walk, kept for the distribution that asks for it. */
|
|
1076
|
+
private pickRoundRobin;
|
|
1077
|
+
/** Remember which account a conversation is bound to, keeping LRU order. */
|
|
1078
|
+
private bindAffinity;
|
|
1079
|
+
/**
|
|
1080
|
+
* `sticky`: the account this conversation already used, when it can still
|
|
1081
|
+
* serve the model being asked for.
|
|
1082
|
+
*
|
|
1083
|
+
* Returns `undefined` both when there is no binding and when the binding is
|
|
1084
|
+
* no longer eligible (cooling for this model, disabled, out of credits) — the
|
|
1085
|
+
* caller then rebinds, which is what makes a rate-limited conversation hop to
|
|
1086
|
+
* a fresh account instead of failing.
|
|
1087
|
+
*/
|
|
1088
|
+
private affinityAccount;
|
|
1089
|
+
/** Bindings currently remembered; exposed for tests and diagnostics. */
|
|
1090
|
+
affinitySize(): number;
|
|
1091
|
+
/** Forget every conversation binding (tests, and a settings change). */
|
|
1092
|
+
clearAffinity(): void;
|
|
1093
|
+
/**
|
|
1094
|
+
* Priority mode: weighted random over the eligible accounts.
|
|
1095
|
+
*
|
|
1096
|
+
* The weight is an idle bonus — `1 + min(idleHours * perHour, max)` — so an
|
|
1097
|
+
* account that has never served (or has been idle for a while) outranks one
|
|
1098
|
+
* that just answered. Reference panel logic drops its success-rate term
|
|
1099
|
+
* entirely because a lifetime error counter penalises an account forever;
|
|
1100
|
+
* instantaneous health is already handled by cooldowns, which is why those
|
|
1101
|
+
* accounts never reach this list.
|
|
1102
|
+
*
|
|
1103
|
+
* A pool with no idle history (fresh process) hashes to equal weights, which
|
|
1104
|
+
* spreads the very first picks instead of always returning index 0.
|
|
1105
|
+
*/
|
|
1106
|
+
private pickByWeight;
|
|
1107
|
+
/**
|
|
1108
|
+
* `expiry` mode: the account whose one-off credit packs die soonest.
|
|
1109
|
+
*
|
|
1110
|
+
* Credit packs are use-it-or-lose-it, so spending the dying ones first is
|
|
1111
|
+
* strictly better than spreading the spend: an account that expires with
|
|
1112
|
+
* credits left is money burnt, while an account whose packs have no deadline
|
|
1113
|
+
* loses nothing by waiting. Two accounts expiring at the same instant fall
|
|
1114
|
+
* back to the pool order, and an account with no known expiry sorts last —
|
|
1115
|
+
* "we have not looked" must never outrank a real deadline.
|
|
1116
|
+
*
|
|
1117
|
+
* A pool where nobody has a known expiry therefore behaves exactly like
|
|
1118
|
+
* priority, which is the honest degradation: without a reading there is
|
|
1119
|
+
* nothing to sort by.
|
|
1120
|
+
*/
|
|
1121
|
+
private pickByExpiry;
|
|
1122
|
+
/**
|
|
1123
|
+
* Pick the account to serve a request.
|
|
1124
|
+
*
|
|
1125
|
+
* Two distributions, chosen by the `distribution` setting:
|
|
1126
|
+
*
|
|
1127
|
+
* - **priority** (default, and what the card ships with): one account serves
|
|
1128
|
+
* every request until it is rate-limited, then the next in order takes over.
|
|
1129
|
+
* Credits drain one account at a time, and a cooling account returns to the
|
|
1130
|
+
* head of the queue the moment its window resets — it was never consumed, so
|
|
1131
|
+
* it resumes straight away.
|
|
1132
|
+
* - **round-robin**: consecutive requests rotate through the pool so spend
|
|
1133
|
+
* spreads evenly across every account.
|
|
1134
|
+
* - **sticky**: one account per conversation, and a new conversation moves to
|
|
1135
|
+
* the next account in order. Keeps the upstream prompt cache warm within a
|
|
1136
|
+
* conversation while still spreading spend across conversations.
|
|
1137
|
+
* - **expiry**: the account whose one-off credit packs expire soonest serves
|
|
1138
|
+
* first, so use-it-or-lose-it credits are spent before they vanish.
|
|
1139
|
+
*
|
|
1140
|
+
* In every mode an explicit user selection (`prefer`) heads the list, a
|
|
1141
|
+
* cooling account is skipped for that model only, and an unrecognised setting
|
|
1142
|
+
* falls back to priority.
|
|
1143
|
+
*
|
|
1144
|
+
* Scans on first use, and rescans when every known account is cooling down: a
|
|
1145
|
+
* fresh desktop login is the usual way out of an exhausted pool.
|
|
1146
|
+
*
|
|
1147
|
+
* `conversationKey` is only consulted under `sticky`; other modes ignore it.
|
|
1148
|
+
*/
|
|
1149
|
+
acquire(modelId?: string, region?: WorkBuddyRegion, conversationKey?: string): Promise<WorkBuddyAccount | undefined>;
|
|
1150
|
+
/** How the pool currently spreads requests. Shown on the card. */
|
|
1151
|
+
currentDistribution(): AccountDistribution;
|
|
1152
|
+
/**
|
|
1153
|
+
* The account the user asked to be tried first, whatever the distribution says.
|
|
1154
|
+
*
|
|
1155
|
+
* Pins identity, not the credential: the account still has to pass every gate
|
|
1156
|
+
* the strategy applies (fresh token, out of cooldown, above its credit reserve,
|
|
1157
|
+
* not switched off), and when it fails one of them the pool falls back to the
|
|
1158
|
+
* strategy untouched. That is the whole promise — "try this first", never "only
|
|
1159
|
+
* ever use this one" — and it is why a stale pin is harmless.
|
|
1160
|
+
*
|
|
1161
|
+
* Returns undefined when nothing is pinned, which is also how the user undoes a
|
|
1162
|
+
* pin. The value survives {@link WorkBuddyPool.scan}, because a rescan discovers
|
|
1163
|
+
* accounts and must not quietly revise a choice the user made about one of them.
|
|
1164
|
+
*/
|
|
1165
|
+
prefer(accountId: string | undefined): void;
|
|
1166
|
+
/**
|
|
1167
|
+
* Which account is pinned, for the card and the status document.
|
|
1168
|
+
*
|
|
1169
|
+
* A reader is required rather than optional politeness: without one the status
|
|
1170
|
+
* document would have to report a preference it cannot see, and the only
|
|
1171
|
+
* truthful way to render "you asked for this one" on a row is to ask the pool.
|
|
1172
|
+
* Callers that need to know whether the pin is still *live* (pinned AND present
|
|
1173
|
+
* in the pool) must do that presence check themselves — this answers "what did
|
|
1174
|
+
* the user choose", not "is it usable right now".
|
|
1175
|
+
*/
|
|
1176
|
+
preferredId(): string | undefined;
|
|
1177
|
+
/** Whether the user switched this account off on the card. */
|
|
1178
|
+
isDisabled(accountId: string): boolean;
|
|
1179
|
+
/** Every account id the user switched off, in discovery order. */
|
|
1180
|
+
disabledIdsInOrder(): string[];
|
|
1181
|
+
/**
|
|
1182
|
+
* Record that an account actually served a request.
|
|
1183
|
+
*
|
|
1184
|
+
* Called by the shim once the upstream answers 200 — only then is the account
|
|
1185
|
+
* the one the user is really being served by. `balanced` mode reads the same map
|
|
1186
|
+
* for its idle weighting, so a request that failed over to another account must
|
|
1187
|
+
* not count as used for the account that was merely tried.
|
|
1188
|
+
*/
|
|
1189
|
+
noteServed(accountId: string): void;
|
|
1190
|
+
/**
|
|
1191
|
+
* Record an account latest known credit balance.
|
|
1192
|
+
*
|
|
1193
|
+
* Called after a request and by the card balance refresh, so the reserve
|
|
1194
|
+
* check has something to compare against. A reading for an unknown account is
|
|
1195
|
+
* dropped: `scan()` rebuilds the account list and a stale id would otherwise
|
|
1196
|
+
* accumulate forever.
|
|
1197
|
+
*
|
|
1198
|
+
* `nearestExpiryMs` is the same reading's nearest one-off pack deadline, or
|
|
1199
|
+
* undefined when nothing is about to expire. It is written through, not
|
|
1200
|
+
* merged: a pack that has been spent or has died disappears from the next
|
|
1201
|
+
* reading, and keeping the stale deadline would pin the pool to an account
|
|
1202
|
+
* whose credits are already gone — the opposite of what `expiry` mode wants.
|
|
1203
|
+
*/
|
|
1204
|
+
noteCredits(accountId: string, balance: number, nearestExpiryMs?: number): void;
|
|
1205
|
+
/** Nearest one-off pack expiry for one account, or undefined when none known. */
|
|
1206
|
+
creditExpiryOf(accountId: string): number | undefined;
|
|
1207
|
+
/** Last known balance for one account, or undefined when never read. */
|
|
1208
|
+
creditsOf(accountId: string): number | undefined;
|
|
1209
|
+
/** The credit floor the user set for one account; 0 when unset. */
|
|
1210
|
+
creditReserveOf(accountId: string): number;
|
|
1211
|
+
/**
|
|
1212
|
+
* Replace every reserve. Called from settings on each apply, so the map
|
|
1213
|
+
* mirrors the saved document exactly instead of accumulating old keys.
|
|
1214
|
+
*/
|
|
1215
|
+
setCreditReserves(reserves: Readonly<Record<string, number>>): void;
|
|
1216
|
+
/** Every reserve currently in force, keyed by account id. */
|
|
1217
|
+
creditReservesInOrder(): Record<string, number>;
|
|
1218
|
+
/**
|
|
1219
|
+
* Whether an account is held back only by its reserve.
|
|
1220
|
+
*
|
|
1221
|
+
* Separates "resting to protect credits" from every other reason an account
|
|
1222
|
+
* is out of rotation, which is what the card shows the user.
|
|
1223
|
+
*/
|
|
1224
|
+
isReserved(accountId: string): boolean;
|
|
1225
|
+
/**
|
|
1226
|
+
* The account that served the most recent request, if any.
|
|
1227
|
+
*
|
|
1228
|
+
* Distinct from "who would serve the next one": this is a record of what
|
|
1229
|
+
* actually happened, which is what the card needs to answer "which account am
|
|
1230
|
+
* I using right now?". Under `balanced` there is no deterministic next account
|
|
1231
|
+
* at all, so a recorded fact is the only honest answer.
|
|
1232
|
+
*
|
|
1233
|
+
* Returns undefined before the first request of the process, and after every
|
|
1234
|
+
* known account has been re-scanned away (a login swapped out under us).
|
|
1235
|
+
*/
|
|
1236
|
+
lastServedId(): string | undefined;
|
|
1237
|
+
/** Best-effort refresh of one account after a session-dead upstream answer. */
|
|
1238
|
+
refreshAccount(accountId: string, options?: {
|
|
1239
|
+
force?: boolean;
|
|
1240
|
+
}): Promise<boolean>;
|
|
1241
|
+
/**
|
|
1242
|
+
* Cool a whole account because the upstream REJECTED its sign-in (401/403).
|
|
1243
|
+
*
|
|
1244
|
+
* Called with direct evidence (the request just came back `session_dead`), so
|
|
1245
|
+
* it does not need to guess: without this, the next retry of the SAME request
|
|
1246
|
+
* picks the same account again — its credential file still claims to be valid
|
|
1247
|
+
* — and a request with 8 attempts spends all 8 on one dead account while
|
|
1248
|
+
* healthy accounts are never tried.
|
|
1249
|
+
*/
|
|
1250
|
+
penalizeCredentialDead(accountId: string): void;
|
|
1251
|
+
/** Put an account back in rotation after its sign-in was proven good again. */
|
|
1252
|
+
clearCredentialDead(accountId: string): void;
|
|
1253
|
+
/** Accounts currently kept out of rotation because their sign-in was rejected. */
|
|
1254
|
+
deadCredentials(): readonly WorkBuddyAccount[];
|
|
1255
|
+
/**
|
|
1256
|
+
* Refresh the account's access token when it is within the margin (or already
|
|
1257
|
+
* expired), in-flight de-duped per account. A failed refresh keeps the
|
|
1258
|
+
* existing token when it has not yet expired, so an unreachable refresh
|
|
1259
|
+
* endpoint never takes down a working session.
|
|
1260
|
+
*
|
|
1261
|
+
* `force` skips the "is it expiring?" gate. That gate is a cost optimisation,
|
|
1262
|
+
* not a correctness rule: it exists so a healthy token is not re-fetched on
|
|
1263
|
+
* every acquire. After the upstream has ALREADY rejected the token, the gate
|
|
1264
|
+
* is actively wrong — a revoked token keeps its future `expiresAtMs` — and the
|
|
1265
|
+
* caller needs the refresh attempted so the credential can be proven dead.
|
|
1266
|
+
*
|
|
1267
|
+
* Returns true when the token is known good afterwards (refreshed, or still
|
|
1268
|
+
* valid), false when the credential was proven dead.
|
|
1269
|
+
*/
|
|
1270
|
+
private ensureFresh;
|
|
1271
|
+
/**
|
|
1272
|
+
* Cool a whole account after the upstream reports its credits are spent.
|
|
1273
|
+
*
|
|
1274
|
+
* Credit exhaustion is an ACCOUNT condition, unlike a model rate limit: every
|
|
1275
|
+
* model on that account is unusable until the quota resets, so this cools the
|
|
1276
|
+
* account as a whole (no `modelId`) for the configured exhaustion window. The
|
|
1277
|
+
* shim then rotates to a different account instead of failing the request.
|
|
1278
|
+
*/
|
|
1279
|
+
penalizeExhausted(accountId: string): void;
|
|
1280
|
+
/**
|
|
1281
|
+
* Mark an account (or one of its models) rate-limited.
|
|
1282
|
+
*
|
|
1283
|
+
* With `modelId`, only that model on the account is cooled — the account's
|
|
1284
|
+
* other models stay in rotation, matching the upstream's per-model rate
|
|
1285
|
+
* limit ("可切换其他模型继续使用"). Without a model id the whole account is
|
|
1286
|
+
* cooled, which callers should reserve for limits that truly span every model.
|
|
1287
|
+
*/
|
|
1288
|
+
penalize(accountId: string, resetAtMs?: number, modelId?: string): void;
|
|
1289
|
+
/** Clear all cooldowns (account-wide and per-model), e.g. from a reset command. */
|
|
1290
|
+
resetCooldowns(): void;
|
|
1291
|
+
/** Diagnostics snapshot. Account-wide cooling count (per-model cooling excluded:
|
|
1292
|
+
* the account as a whole stays usable when only one model is limited). */
|
|
1293
|
+
status(): {
|
|
1294
|
+
count: number;
|
|
1295
|
+
cooling: number;
|
|
1296
|
+
dead: number;
|
|
1297
|
+
lastScanAtMs: number;
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
//#endregion
|
|
1301
|
+
//#region src/status-paths.d.ts
|
|
1302
|
+
/**
|
|
1303
|
+
* Node-free constants and types shared by the Host and browser halves of the
|
|
1304
|
+
* XD Pool settings card.
|
|
1305
|
+
*
|
|
1306
|
+
* Pool's runtime state already lives in `src/status.ts` (`buildStatus` /
|
|
1307
|
+
* `WorkBuddyStatus`); this module only carves the cross-domain (Host→browser)
|
|
1308
|
+
* JSON document into a shape that stays token-free and matches what the
|
|
1309
|
+
* browser card renders. Route paths are plugin-owned and mounted on the Host's
|
|
1310
|
+
* same-origin web server (see `src/web-status.ts`).
|
|
1311
|
+
*
|
|
1312
|
+
* @module dsh-xdpool/status-paths
|
|
1313
|
+
*/
|
|
1314
|
+
/** Plugin-owned read-only pool status endpoint (account rows + models + shim). */
|
|
1315
|
+
export declare const POOL_STATUS_PATH = "/plugins/dsh-xdpool/status";
|
|
1316
|
+
/** Plugin-owned local account rescan endpoint (re-read desktop snapshots). */
|
|
1317
|
+
export declare const POOL_RESCAN_PATH = "/plugins/dsh-xdpool/accounts/rescan";
|
|
1318
|
+
/** Plugin-owned cooldown reset endpoint (clear all 429 cooldowns). */
|
|
1319
|
+
export declare const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-xdpool/cooldowns/reset";
|
|
1320
|
+
/** Plugin-owned daily check-in action endpoint (claim today's reward). */
|
|
1321
|
+
export declare const POOL_CHECKIN_PATH = "/plugins/dsh-xdpool/checkin";
|
|
1322
|
+
/** Plugin-owned model-selection save endpoint (writes the settings section). */
|
|
1323
|
+
export declare const POOL_MODELS_SAVE_PATH = "/plugins/dsh-xdpool/models/save";
|
|
1324
|
+
/**
|
|
1325
|
+
* Throw one account out of the pool for good, or take it back.
|
|
1326
|
+
*
|
|
1327
|
+
* Separate from the disable route because the semantics differ: disabling is a
|
|
1328
|
+
* rotation preference the account survives, ignoring survives the account.
|
|
1329
|
+
*/
|
|
1330
|
+
export declare const POOL_ACCOUNT_IGNORE_PATH = "/plugins/dsh-xdpool/accounts/ignored";
|
|
1331
|
+
/** Run one automation job immediately, so the card can verify it on demand. */
|
|
1332
|
+
export declare const POOL_AUTOMATION_RUN_PATH = "/plugins/dsh-xdpool/automation/run";
|
|
1333
|
+
/** Set or clear one account's reserved-credit floor. */
|
|
1334
|
+
export declare const POOL_CREDIT_RESERVE_PATH = "/plugins/dsh-xdpool/accounts/credit-reserve";
|
|
1335
|
+
/**
|
|
1336
|
+
* One model's facts, exactly as the host's catalogue spells them.
|
|
1337
|
+
*
|
|
1338
|
+
* Every optional field is absent when the host does not know it, which is the
|
|
1339
|
+
* common case: modalities are only reported by adapters that publish them, and a
|
|
1340
|
+
* missing window is missing rather than a default.
|
|
1341
|
+
*/
|
|
1342
|
+
interface PoolWebChannelModel {
|
|
1343
|
+
id: string;
|
|
1344
|
+
name: string;
|
|
1345
|
+
contextWindow?: number;
|
|
1346
|
+
maxTokens?: number;
|
|
1347
|
+
inputModalities?: readonly string[];
|
|
1348
|
+
description?: string;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* One billion-context session, reduced to what a dashboard may show.
|
|
1352
|
+
*
|
|
1353
|
+
* Counts, sizes and timestamps only. The proxy's own rows carry titles and
|
|
1354
|
+
* first-block hints, which are the user's words; those stay on the machine that
|
|
1355
|
+
* produced them and are not relayed.
|
|
1356
|
+
*/
|
|
1357
|
+
interface PoolWebBillionSession {
|
|
1358
|
+
/** The proxy's session id. Safe to show: it is a generated handle. */
|
|
1359
|
+
id: string;
|
|
1360
|
+
/** Which wire the session speaks; `openai` is what DSH's shim uses. */
|
|
1361
|
+
protocol: string;
|
|
1362
|
+
/** Whether a request hit this session in the last few seconds. */
|
|
1363
|
+
live: boolean;
|
|
1364
|
+
requests: number;
|
|
1365
|
+
/** The compacted context size the proxy is holding, 0 when unknown. */
|
|
1366
|
+
contextTokens: number;
|
|
1367
|
+
/** Which measurement the context size came from; `usage` is the honest one. */
|
|
1368
|
+
contextTokensSource: string;
|
|
1369
|
+
/** Net tokens the proxy says compression saved. */
|
|
1370
|
+
tokensSaved: number;
|
|
1371
|
+
inputTokens: number;
|
|
1372
|
+
cachedTokens: number;
|
|
1373
|
+
outputTokens: number;
|
|
1374
|
+
/** Cache hit rate 0-100, or -1 when no input tokens were ever counted. */
|
|
1375
|
+
cacheHitPct: number;
|
|
1376
|
+
/** How many compressed blocks this session holds. */
|
|
1377
|
+
blocks: number;
|
|
1378
|
+
/** The window the proxy believes the model has, 0 when unknown. */
|
|
1379
|
+
contextWindow: number;
|
|
1380
|
+
/** ISO timestamp of the last request, or '' when the session never had one. */
|
|
1381
|
+
lastSeen: string;
|
|
1382
|
+
/** The last model the session saw, or '' before the first request. */
|
|
1383
|
+
lastModel: string;
|
|
1384
|
+
}
|
|
1385
|
+
/** The document {@link POOL_BILLION_PATH} answers with. */
|
|
1386
|
+
interface PoolWebBillionStatus {
|
|
1387
|
+
/**
|
|
1388
|
+
* Whether a proxy was found and answered.
|
|
1389
|
+
*
|
|
1390
|
+
* `missing` is a normal, expected state, not an error: billion-context is a
|
|
1391
|
+
* separate plugin, and a machine without it must get a page that explains
|
|
1392
|
+
* that rather than a 503 or a blank panel.
|
|
1393
|
+
*/
|
|
1394
|
+
state: 'ready' | 'missing' | 'unreachable';
|
|
1395
|
+
/** Human text for a non-ready state; absent when ready. */
|
|
1396
|
+
reason?: string;
|
|
1397
|
+
/**
|
|
1398
|
+
* The proxy's own loopback origin, absent only when none was recorded.
|
|
1399
|
+
*
|
|
1400
|
+
* The card needs this to hand the user a browser link: billion-context ships
|
|
1401
|
+
* its own dashboard, including a per-session detail view, and duplicating
|
|
1402
|
+
* those screens here would be a second and divergent copy of another
|
|
1403
|
+
* plugin's interface. Linking out keeps one answer.
|
|
1404
|
+
*
|
|
1405
|
+
* This is a service address, not conversation material, and it is safe to
|
|
1406
|
+
* publish for the same reason the route that carries it is loopback-only: a
|
|
1407
|
+
* caller who can read this document can already reach that listener directly.
|
|
1408
|
+
* It is present on `unreachable` too — a proxy that recorded its port and
|
|
1409
|
+
* then went quiet is exactly the case where the URL is the only handle.
|
|
1410
|
+
*/
|
|
1411
|
+
origin?: string;
|
|
1412
|
+
/** The proxy's version, from its own manifest. */
|
|
1413
|
+
version?: string;
|
|
1414
|
+
/** When the proxy process started (epoch ms), 0 when unknown. */
|
|
1415
|
+
startedAt?: number;
|
|
1416
|
+
/** Sessions newest-first, capped by the route. */
|
|
1417
|
+
sessions: readonly PoolWebBillionSession[];
|
|
1418
|
+
/** How many sessions the proxy holds in total, including capped-out ones. */
|
|
1419
|
+
totalSessions: number;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Where a registry provider's key goes: `apiKey`, `baseUrl`, or `enabled`.
|
|
1423
|
+
*/
|
|
1424
|
+
type PoolWebAggregatePatch = {
|
|
1425
|
+
kind: 'provider';
|
|
1426
|
+
providerId: string;
|
|
1427
|
+
enabled?: boolean;
|
|
1428
|
+
apiKey?: string;
|
|
1429
|
+
baseUrl?: string;
|
|
1430
|
+
} |
|
|
1431
|
+
/** Flip the lane's own master switch; `false` contributes no models at all. */
|
|
1432
|
+
{
|
|
1433
|
+
kind: 'lane';
|
|
1434
|
+
enabled: boolean;
|
|
1435
|
+
} |
|
|
1436
|
+
/** Forget one provider's stored setting entirely, key included. */
|
|
1437
|
+
{
|
|
1438
|
+
kind: 'clear';
|
|
1439
|
+
providerId: string;
|
|
1440
|
+
};
|
|
1441
|
+
/** One provider as the card renders it. Never carries the key itself. */
|
|
1442
|
+
interface PoolWebAggregateProvider {
|
|
1443
|
+
id: string;
|
|
1444
|
+
name: string;
|
|
1445
|
+
category: string;
|
|
1446
|
+
authKind: string;
|
|
1447
|
+
format: string;
|
|
1448
|
+
/** Where the user obtains a key, for the "get one" link. */
|
|
1449
|
+
authUrl?: string;
|
|
1450
|
+
website?: string;
|
|
1451
|
+
/** True when this lane can serve the provider with a pasted key. */
|
|
1452
|
+
supported: boolean;
|
|
1453
|
+
/** True when a key is present (or none is needed). */
|
|
1454
|
+
configured: boolean;
|
|
1455
|
+
/**
|
|
1456
|
+
* True when this vendor contributes models right now.
|
|
1457
|
+
*
|
|
1458
|
+
* Not simply "the user did not switch it off": with no explicit flag the
|
|
1459
|
+
* answer follows the credential, so an unconfigured vendor reads as off. The
|
|
1460
|
+
* roster is a directory to choose from, and a page that drew every one of the
|
|
1461
|
+
* seventy-six with its switch already on could not show what was configured.
|
|
1462
|
+
*/
|
|
1463
|
+
enabled: boolean;
|
|
1464
|
+
/**
|
|
1465
|
+
* The vendor's own one-line description, from the registry.
|
|
1466
|
+
*
|
|
1467
|
+
* This is the row's small print. The registry carries no per-model blurb —
|
|
1468
|
+
* `models[]` entries are id/name/contextWindow/vision/reasoning only — so the
|
|
1469
|
+
* description belongs to the vendor, and the card must not imply otherwise.
|
|
1470
|
+
*/
|
|
1471
|
+
notes?: string;
|
|
1472
|
+
/** A base-URL override the user typed, if any. */
|
|
1473
|
+
baseUrl?: string;
|
|
1474
|
+
/** True when the provider declares a model listing this lane can read. */
|
|
1475
|
+
hasListing: boolean;
|
|
1476
|
+
/** Model ids currently exposed: the chosen set, or the registry's own. */
|
|
1477
|
+
models: readonly string[];
|
|
1478
|
+
/** True when the exposed set came from a live listing rather than the registry. */
|
|
1479
|
+
discovered: boolean;
|
|
1480
|
+
/** When the provider was first configured. */
|
|
1481
|
+
addedAt?: string;
|
|
1482
|
+
}
|
|
1483
|
+
/** The document {@link POOL_AGGREGATE_PATH} answers with. */
|
|
1484
|
+
interface PoolWebAggregateStatus {
|
|
1485
|
+
/** The lane's own master switch. */
|
|
1486
|
+
enabled: boolean;
|
|
1487
|
+
/** How many providers hold a setting at all. */
|
|
1488
|
+
configured: number;
|
|
1489
|
+
/** How many model rows the lane currently offers. */
|
|
1490
|
+
modelCount: number;
|
|
1491
|
+
/** The rows, in provider-then-model order. */
|
|
1492
|
+
models: readonly {
|
|
1493
|
+
id: string;
|
|
1494
|
+
name: string;
|
|
1495
|
+
providerId: string;
|
|
1496
|
+
modelId: string;
|
|
1497
|
+
contextWindow: number;
|
|
1498
|
+
maxOutputTokens: number;
|
|
1499
|
+
supportsImages: boolean;
|
|
1500
|
+
source: 'static' | 'discovered';
|
|
1501
|
+
}[];
|
|
1502
|
+
/** The whole registry, in registry order. */
|
|
1503
|
+
providers: readonly PoolWebAggregateProvider[];
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* One login channel as the card renders it.
|
|
1507
|
+
*
|
|
1508
|
+
* `loginKind` is presentation only: `browser` means "open a link and sign in
|
|
1509
|
+
* there". The card never learns which protocol a channel speaks — that stays
|
|
1510
|
+
* inside the pack, which hands back a `loginUrl` per sign-in attempt.
|
|
1511
|
+
*/
|
|
1512
|
+
interface PoolWebChannel {
|
|
1513
|
+
id: string;
|
|
1514
|
+
name: string;
|
|
1515
|
+
/** Who runs it. */
|
|
1516
|
+
org: string;
|
|
1517
|
+
/**
|
|
1518
|
+
* The vendor's published API host, when it publishes one.
|
|
1519
|
+
*
|
|
1520
|
+
* Absent for eleven of the thirteen: the pack names `apiDomain` only for the
|
|
1521
|
+
* two WorkBuddy lanes, and everything else would be a host we guessed. The
|
|
1522
|
+
* card falls back to `org` when this is missing, so the subtitle is never
|
|
1523
|
+
* blank and never invented.
|
|
1524
|
+
*/
|
|
1525
|
+
domain?: string;
|
|
1526
|
+
/** Accent colour, for the channel's chip. */
|
|
1527
|
+
accent: string;
|
|
1528
|
+
/** One Chinese line on what the free allowance actually is. */
|
|
1529
|
+
note: string;
|
|
1530
|
+
loginKind: 'browser';
|
|
1531
|
+
/** True when this plugin's own account pool already serves this channel. */
|
|
1532
|
+
ownedByPool: boolean;
|
|
1533
|
+
}
|
|
1534
|
+
/** The document {@link POOL_CHANNELS_PATH} answers with. */
|
|
1535
|
+
interface PoolWebChannelsStatus {
|
|
1536
|
+
/** Whether the vendored pack mounted, and if not, why. */
|
|
1537
|
+
state: 'pending' | 'ready' | 'unavailable';
|
|
1538
|
+
/** Human text for a non-ready state; absent when ready. */
|
|
1539
|
+
reason?: string;
|
|
1540
|
+
channels: readonly PoolWebChannel[];
|
|
1541
|
+
}
|
|
1542
|
+
/** One model a chain member may name, from either lane. */
|
|
1543
|
+
interface PoolWebComboCandidate {
|
|
1544
|
+
id: string;
|
|
1545
|
+
name: string;
|
|
1546
|
+
/** `workbuddy` for a pool model, the provider id for a third-party one. */
|
|
1547
|
+
owner: string;
|
|
1548
|
+
/** True when a third-party model already has a usable key. */
|
|
1549
|
+
usable: boolean;
|
|
1550
|
+
contextWindow: number;
|
|
1551
|
+
supportsImages: boolean;
|
|
1552
|
+
}
|
|
1553
|
+
/** One chain, resolved against the live roster. */
|
|
1554
|
+
interface PoolWebCombo {
|
|
1555
|
+
/** `combo:<slug>`, always. */
|
|
1556
|
+
id: string;
|
|
1557
|
+
name: string;
|
|
1558
|
+
/** Members that exist right now, in order. Empty means the chain is dead. */
|
|
1559
|
+
models: readonly string[];
|
|
1560
|
+
/** Members the user configured but which the roster no longer serves. */
|
|
1561
|
+
missing: readonly string[];
|
|
1562
|
+
strategy: 'fallback' | 'round-robin';
|
|
1563
|
+
stickyLimit: number;
|
|
1564
|
+
enabled: boolean;
|
|
1565
|
+
contextWindow: number;
|
|
1566
|
+
supportsImages: boolean;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* The document {@link POOL_COMBOS_PATH} answers with.
|
|
1570
|
+
*
|
|
1571
|
+
* `candidates` is the whole roster rather than only what is chainable, because
|
|
1572
|
+
* a chain that cannot add a new model is not an editor.
|
|
1573
|
+
*/
|
|
1574
|
+
interface PoolWebCombosStatus {
|
|
1575
|
+
combos: readonly PoolWebCombo[];
|
|
1576
|
+
candidates: readonly PoolWebComboCandidate[];
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* What a relay patch can change.
|
|
1580
|
+
*
|
|
1581
|
+
* `key: ''` rotates rather than clears: the card sends an empty string when the
|
|
1582
|
+
* user asks for a new key, and "no key" is not a state the relay can serve — a
|
|
1583
|
+
* keyless relay would have to accept everyone.
|
|
1584
|
+
*/
|
|
1585
|
+
interface PoolWebRelayPatch {
|
|
1586
|
+
enabled?: boolean;
|
|
1587
|
+
host?: string;
|
|
1588
|
+
port?: number;
|
|
1589
|
+
key?: string;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* What a key probe answered.
|
|
1593
|
+
*
|
|
1594
|
+
* `modelIds` is present only when the provider's listing was readable, and the
|
|
1595
|
+
* "discover models" action turns it into a save. `committed` reports how many
|
|
1596
|
+
* ids that action actually wrote — a listing that came back empty is a real
|
|
1597
|
+
* answer (the provider serves nothing) and must not be reported as a failure.
|
|
1598
|
+
*/
|
|
1599
|
+
interface PoolWebAggregateProbe {
|
|
1600
|
+
ok: boolean;
|
|
1601
|
+
/** The provider's HTTP status, or 0 when it was never reached. */
|
|
1602
|
+
status: number;
|
|
1603
|
+
message: string;
|
|
1604
|
+
modelCount?: number;
|
|
1605
|
+
/** Ids the provider listed, when it declared a listing and answered. */
|
|
1606
|
+
modelIds?: readonly string[];
|
|
1607
|
+
/** How many ids the discover action wrote back. Absent on a plain probe. */
|
|
1608
|
+
committed?: number;
|
|
1609
|
+
}
|
|
1610
|
+
/** One account's row, token-free. */
|
|
1611
|
+
interface PoolWebAccount {
|
|
1612
|
+
id: string;
|
|
1613
|
+
label: string;
|
|
1614
|
+
nickname?: string;
|
|
1615
|
+
domain: string;
|
|
1616
|
+
/** ISO timestamp; absent when the credential carries no expiry. */
|
|
1617
|
+
expiresAt?: string;
|
|
1618
|
+
/** Account-wide cooldown (every model blocked); only after a no-model penalize. */
|
|
1619
|
+
cooling: boolean;
|
|
1620
|
+
/** ISO timestamp when the account-wide 429 cooldown lifts; only while cooling. */
|
|
1621
|
+
cooldownUntil?: string;
|
|
1622
|
+
/**
|
|
1623
|
+
* The upstream REJECTED this account's sign-in (401/403) — the credential file
|
|
1624
|
+
* still looks valid because the upstream never rewrites its expiry when it
|
|
1625
|
+
* revokes a token. Distinct from `cooling`: waiting does not fix it, only
|
|
1626
|
+
* signing in again in the desktop app does.
|
|
1627
|
+
*/
|
|
1628
|
+
credentialDead?: boolean;
|
|
1629
|
+
/** ISO timestamp when the dead mark expires and the account is retried; only while `credentialDead`. */
|
|
1630
|
+
credentialDeadUntil?: string;
|
|
1631
|
+
/**
|
|
1632
|
+
* Per-model cooldowns currently active. The account is NOT `cooling` while a
|
|
1633
|
+
* model is limited — its other models still serve — but each entry tells the
|
|
1634
|
+
* card which model is out until when (e.g. `hy4-preview` cooling to 10:14,
|
|
1635
|
+
* `hy3` normal).
|
|
1636
|
+
*/
|
|
1637
|
+
modelCooldowns?: ReadonlyArray<{
|
|
1638
|
+
modelId: string;
|
|
1639
|
+
until: string;
|
|
1640
|
+
}>;
|
|
1641
|
+
/**
|
|
1642
|
+
* Whether the user switched this account off. A disabled account never
|
|
1643
|
+
* serves a request, but it stays listed so the card can switch it back on.
|
|
1644
|
+
*/
|
|
1645
|
+
disabled: boolean;
|
|
1646
|
+
/**
|
|
1647
|
+
* Whether the user pinned this account to be tried first.
|
|
1648
|
+
*
|
|
1649
|
+
* Distinct from `disabled`, and the card has to tell them apart: a disabled
|
|
1650
|
+
* account is skipped entirely, a preferred one is merely first in line. It
|
|
1651
|
+
* stays `false` when nothing is pinned, so the row never borrows a mark that
|
|
1652
|
+
* describes a global choice rather than this account.
|
|
1653
|
+
*/
|
|
1654
|
+
preferred: boolean;
|
|
1655
|
+
rateLimitHits: number;
|
|
1656
|
+
/**
|
|
1657
|
+
* Credits the user asked to keep for this account. The pool stops picking the
|
|
1658
|
+
* account once its balance reaches the reserve, so this many credits survive.
|
|
1659
|
+
* 0 means the account may be spent down as before.
|
|
1660
|
+
*/
|
|
1661
|
+
creditReserve: number;
|
|
1662
|
+
/**
|
|
1663
|
+
* Whether the account is held back purely by its reserve right now. Kept
|
|
1664
|
+
* distinct from `cooling`: a reserved account is healthy and simply
|
|
1665
|
+
* protected, which is a different thing to tell the user than rate-limited.
|
|
1666
|
+
*/
|
|
1667
|
+
reserved: boolean;
|
|
1668
|
+
/**
|
|
1669
|
+
* What the automation earned for this account today. Absent when it earned
|
|
1670
|
+
* nothing (or the automation never ran for it), so the card can stay quiet
|
|
1671
|
+
* instead of printing a row of zeroes.
|
|
1672
|
+
*/
|
|
1673
|
+
automationToday?: PoolWebAutomationEarnings;
|
|
1674
|
+
/** ISO timestamp of the last successful use (best-effort pool bookkeeping). */
|
|
1675
|
+
lastUsedAt?: string;
|
|
1676
|
+
/** Aggregated credit summary for the account, read-only. */
|
|
1677
|
+
credits?: PoolWebCredits;
|
|
1678
|
+
creditsError?: string;
|
|
1679
|
+
/**
|
|
1680
|
+
* Today's check-in state for this account, read-only. Present only when the
|
|
1681
|
+
* per-account check-in probe succeeded and the program is active. The card
|
|
1682
|
+
* renders one claim button per account, so a multi-account pool can collect
|
|
1683
|
+
* every account's daily reward without switching accounts by hand.
|
|
1684
|
+
*/
|
|
1685
|
+
checkin?: PoolWebCheckin;
|
|
1686
|
+
checkinError?: string;
|
|
1687
|
+
}
|
|
1688
|
+
/** One credit package (as surfaced by the pool's upstream client), node-free. */
|
|
1689
|
+
interface PoolWebCreditPackage {
|
|
1690
|
+
packageName: string;
|
|
1691
|
+
remain?: number;
|
|
1692
|
+
size?: number;
|
|
1693
|
+
/** CapacityType 4 — refreshed each cycle and never expires. */
|
|
1694
|
+
monthly?: boolean;
|
|
1695
|
+
/** Next cycle refresh point, ms. */
|
|
1696
|
+
cycleRefreshMs?: number;
|
|
1697
|
+
/** One-off expiry, ms. */
|
|
1698
|
+
expiresAtMs?: number;
|
|
1699
|
+
}
|
|
1700
|
+
/** Aggregated credit answer the card renders under one account. */
|
|
1701
|
+
interface PoolWebCredits {
|
|
1702
|
+
total?: number;
|
|
1703
|
+
packages: readonly PoolWebCreditPackage[];
|
|
1704
|
+
/** Credits expiring within 3 days. */
|
|
1705
|
+
expiringSoon?: number;
|
|
1706
|
+
/** When the nearest package expires, ms. */
|
|
1707
|
+
nearestExpiryMs?: number;
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Daily check-in state the card renders under one account's credits. Mirrors
|
|
1711
|
+
* the upstream activity endpoint, minus anything the browser does not need.
|
|
1712
|
+
*/
|
|
1713
|
+
interface PoolWebCheckin {
|
|
1714
|
+
/** The activity is running; a claim button is offered only while true. */
|
|
1715
|
+
active: boolean;
|
|
1716
|
+
/** Already collected today — the button renders as a done state. */
|
|
1717
|
+
todayCheckedIn: boolean;
|
|
1718
|
+
/** Consecutive days checked in. */
|
|
1719
|
+
streakDays: number;
|
|
1720
|
+
/** Credits a single day grants. */
|
|
1721
|
+
dailyCredit: number;
|
|
1722
|
+
/** Credits collected today (0 before claiming). */
|
|
1723
|
+
todayCredit: number;
|
|
1724
|
+
/** Today is a streak milestone day. */
|
|
1725
|
+
isStreakDay: boolean;
|
|
1726
|
+
/** The day count the next milestone lands on. */
|
|
1727
|
+
nextStreakDay: number;
|
|
1728
|
+
/** Bonus credits granted on a milestone day. */
|
|
1729
|
+
streakBonusCredit: number;
|
|
1730
|
+
}
|
|
1731
|
+
/** Result of one claim, so the card can confirm what was collected. */
|
|
1732
|
+
interface PoolWebCheckinClaim {
|
|
1733
|
+
credit: number;
|
|
1734
|
+
streakDays: number;
|
|
1735
|
+
isStreakDay: boolean;
|
|
1736
|
+
}
|
|
1737
|
+
/** One model the pool exposes to DSH, with cost / free tags. */
|
|
1738
|
+
interface PoolWebModel {
|
|
1739
|
+
id: string;
|
|
1740
|
+
name: string;
|
|
1741
|
+
/** Relative credit cost, e.g. 0.79 for x0.79. */
|
|
1742
|
+
multiplier?: number;
|
|
1743
|
+
/** Upstream tags: free / limited-free / night-discount. */
|
|
1744
|
+
tags?: readonly string[];
|
|
1745
|
+
/** Effective image support after the user's per-model toggle. */
|
|
1746
|
+
supportsImages: boolean;
|
|
1747
|
+
/** Effective context window after the user's budget cap. */
|
|
1748
|
+
contextWindow: number;
|
|
1749
|
+
/** The window the upstream advertises, before any cap. */
|
|
1750
|
+
nativeContextWindow: number;
|
|
1751
|
+
/** Upstream output ceiling, so the card can show both limits. */
|
|
1752
|
+
maxOutputTokens: number;
|
|
1753
|
+
/** Thinking levels the upstream declares, when it declares any. */
|
|
1754
|
+
supportedEfforts?: readonly string[];
|
|
1755
|
+
/** Whether this model is currently enabled in the picker. */
|
|
1756
|
+
enabled: boolean;
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* What one connectivity probe measured.
|
|
1760
|
+
*
|
|
1761
|
+
* Deliberately three numbers and nothing else. The card asks "can this model
|
|
1762
|
+
* answer at all", so a probe that reported token counts or a verdict about
|
|
1763
|
+
* quality would be answering a question nobody asked, and one that reported
|
|
1764
|
+
* "0 output tokens" for a refusal would be reporting a bug as a fact.
|
|
1765
|
+
*
|
|
1766
|
+
* `outputChars` is a character count, not a token count: this plugin cannot
|
|
1767
|
+
* tokenize for a model it does not own, and a character count answers "did it
|
|
1768
|
+
* say anything" without pretending to know more.
|
|
1769
|
+
*/
|
|
1770
|
+
interface PoolWebModelProbeResult {
|
|
1771
|
+
/** True when the model produced at least one character of output. */
|
|
1772
|
+
ok: boolean;
|
|
1773
|
+
/** Milliseconds until the first output character arrived. Absent when none did. */
|
|
1774
|
+
firstFrameMs?: number;
|
|
1775
|
+
/** Characters of output produced before the probe stopped reading. */
|
|
1776
|
+
outputChars: number;
|
|
1777
|
+
/**
|
|
1778
|
+
* Why the probe failed, when it did.
|
|
1779
|
+
*
|
|
1780
|
+
* Rendered verbatim, and it is a failure to SHOW rather than to hide: a
|
|
1781
|
+
* connectivity test that swallows its error teaches the user the button is
|
|
1782
|
+
* broken.
|
|
1783
|
+
*/
|
|
1784
|
+
error?: string;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Body of the account ignore/unignore route: exactly one account per request.
|
|
1788
|
+
*
|
|
1789
|
+
* `ignored: true` throws the account out of the pool for good (its credential is
|
|
1790
|
+
* not even read on the next scan, and a fresh desktop sign-in will not bring it
|
|
1791
|
+
* back). `false` restores it, at which point the next scan discovers it again.
|
|
1792
|
+
*/
|
|
1793
|
+
interface PoolWebAccountIgnore {
|
|
1794
|
+
/** Pool account id, as reported in `PoolWebAccount.id`. */
|
|
1795
|
+
accountId: string;
|
|
1796
|
+
/** `true` ignores the account permanently; `false` takes it back. */
|
|
1797
|
+
ignored: boolean;
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* One account the user has thrown out of the pool.
|
|
1801
|
+
*
|
|
1802
|
+
* Kept on the status document so the card can list what was ignored and offer a
|
|
1803
|
+
* way back: without that, "ignored" is a one-way door the user cannot inspect or
|
|
1804
|
+
* undo from the UI, which is how a hidden list becomes a support burden.
|
|
1805
|
+
*/
|
|
1806
|
+
interface PoolWebIgnoredAccount {
|
|
1807
|
+
/** Pool account id, the same key `PoolWebAccount.id` uses. */
|
|
1808
|
+
id: string;
|
|
1809
|
+
/** Human label captured at ignore time, so the row reads without a rescan. */
|
|
1810
|
+
label: string;
|
|
1811
|
+
/** ISO timestamp of when it was ignored. */
|
|
1812
|
+
ignoredAt: string;
|
|
1813
|
+
}
|
|
1814
|
+
interface PoolWebModelSelection {
|
|
1815
|
+
/** Absent = every model is enabled. */
|
|
1816
|
+
enabledModelIds?: readonly string[];
|
|
1817
|
+
/** Absent = each model follows its upstream image capability. */
|
|
1818
|
+
imageModelIds?: readonly string[];
|
|
1819
|
+
/** Per-model context-window cap, keyed by model id. */
|
|
1820
|
+
contextBudgets?: Readonly<Record<string, number | undefined>>;
|
|
1821
|
+
}
|
|
1822
|
+
/** The JSON document the pool card renders. */
|
|
1823
|
+
interface PoolWebStatus {
|
|
1824
|
+
ok: boolean;
|
|
1825
|
+
accounts: readonly PoolWebAccount[];
|
|
1826
|
+
/** The next account the pool would use (rotation cursor). */
|
|
1827
|
+
activeAccountId?: string;
|
|
1828
|
+
/**
|
|
1829
|
+
* The account the user pinned to be tried first, when one is pinned and it is
|
|
1830
|
+
* still in the pool.
|
|
1831
|
+
*
|
|
1832
|
+
* Reported at document level rather than as a per-row flag because it is a
|
|
1833
|
+
* single global choice, and omitting it when the pinned account has been
|
|
1834
|
+
* scanned away keeps the card from drawing a "preferred" badge on nothing.
|
|
1835
|
+
*/
|
|
1836
|
+
preferredAccountId?: string;
|
|
1837
|
+
cooling: number;
|
|
1838
|
+
models: readonly PoolWebModel[];
|
|
1839
|
+
/** The saved selection the card diffs its draft against. */
|
|
1840
|
+
selection: PoolWebModelSelection;
|
|
1841
|
+
/**
|
|
1842
|
+
* How the pool spreads requests: `priority` drains one account before
|
|
1843
|
+
* moving on, `round-robin` splits the spend evenly.
|
|
1844
|
+
*/
|
|
1845
|
+
distribution: PoolDistribution;
|
|
1846
|
+
/** Which region this document describes. */
|
|
1847
|
+
region: PoolRegion;
|
|
1848
|
+
/** Every region holding at least one account, in display order. */
|
|
1849
|
+
regions: readonly PoolRegion[];
|
|
1850
|
+
shim: {
|
|
1851
|
+
running: boolean;
|
|
1852
|
+
baseUrl?: string;
|
|
1853
|
+
};
|
|
1854
|
+
/** Daily-points automation state, so the card can show what ran and when. */
|
|
1855
|
+
automation: PoolWebAutomation;
|
|
1856
|
+
/** Per-account credit floors currently in force, keyed by account id. */
|
|
1857
|
+
creditReserves: Readonly<Record<string, number>>;
|
|
1858
|
+
/**
|
|
1859
|
+
* Accounts thrown out of the pool, in the order they were ignored.
|
|
1860
|
+
*
|
|
1861
|
+
* Reported so the card can show the list and offer a way back. These accounts
|
|
1862
|
+
* are NOT in `accounts`: they are filtered out before their credentials are
|
|
1863
|
+
* read, which is the whole point of the feature.
|
|
1864
|
+
*/
|
|
1865
|
+
ignored: readonly PoolWebIgnoredAccount[];
|
|
1866
|
+
/**
|
|
1867
|
+
* Where THIS region's model list came from.
|
|
1868
|
+
*
|
|
1869
|
+
* `live` = fetched from the gateway (or a cached fetch survived a restart).
|
|
1870
|
+
* `fallback` = the built-in static table, which means the startup fetch
|
|
1871
|
+
* failed and the user is looking at a SHORTER, possibly stale roster — models
|
|
1872
|
+
* they had enabled can be missing from it entirely.
|
|
1873
|
+
*
|
|
1874
|
+
* Surfaced because the failure used to be log-only: the picker quietly lost
|
|
1875
|
+
* half its entries and nothing on screen said why, so it read as "the plugin
|
|
1876
|
+
* deleted my models".
|
|
1877
|
+
*/
|
|
1878
|
+
catalogSource?: PoolWebCatalogSource;
|
|
1879
|
+
/** When the live catalog was last successfully fetched, ISO. */
|
|
1880
|
+
catalogUpdatedAt?: string;
|
|
1881
|
+
/** Why the last fetch failed, when it did. Redacted and length-capped. */
|
|
1882
|
+
catalogError?: string;
|
|
1883
|
+
/**
|
|
1884
|
+
* Credential files on disk that did NOT become accounts.
|
|
1885
|
+
*
|
|
1886
|
+
* Reported so the account count can be trusted: without this, a directory
|
|
1887
|
+
* holding four files that yields two accounts looks like two accounts were
|
|
1888
|
+
* deleted, when really two files could not be opened (most often an encrypted
|
|
1889
|
+
* credential the desktop app was not running to unlock).
|
|
1890
|
+
*/
|
|
1891
|
+
skippedFiles?: readonly PoolWebSkippedFile[];
|
|
1892
|
+
}
|
|
1893
|
+
/** A credential file the pool could not read. */
|
|
1894
|
+
interface PoolWebSkippedFile {
|
|
1895
|
+
/** Basename only; the full path lives in the desktop app's auth directory. */
|
|
1896
|
+
file: string;
|
|
1897
|
+
reason: 'encrypted' | 'unreadable' | 'malformed';
|
|
1898
|
+
}
|
|
1899
|
+
/** How a region's model list was obtained. */
|
|
1900
|
+
type PoolWebCatalogSource = 'live' | 'fallback';
|
|
1901
|
+
/** One automation job's last run, as shown on the card. */
|
|
1902
|
+
interface PoolWebAutomationJob {
|
|
1903
|
+
/** `YYYY-MM-DD` of the last run in this process, if it has run. */
|
|
1904
|
+
lastRunDate?: string;
|
|
1905
|
+
/**
|
|
1906
|
+
* Epoch ms of the last run, so the card can show the TIME.
|
|
1907
|
+
*
|
|
1908
|
+
* Carried because a date-only stamp cannot tell one run from eight: every
|
|
1909
|
+
* repeat inside the same day rendered as the identical `2026-09-28 · 2`,
|
|
1910
|
+
* which is what kept a "re-runs every hour" defect invisible on the card.
|
|
1911
|
+
*/
|
|
1912
|
+
lastRunAtMs?: number;
|
|
1913
|
+
/**
|
|
1914
|
+
* Configured slots consumed today, as `YYYY-MM-DDTHH`.
|
|
1915
|
+
*
|
|
1916
|
+
* Shown so "which of today's hours already ran" is answerable at a glance
|
|
1917
|
+
* rather than inferred from a counter.
|
|
1918
|
+
*/
|
|
1919
|
+
firedSlots?: readonly string[];
|
|
1920
|
+
/** Accounts that finished without error on the last run. */
|
|
1921
|
+
ok: number;
|
|
1922
|
+
/** Accounts that failed on the last run (each one skipped, the run continued). */
|
|
1923
|
+
failed: number;
|
|
1924
|
+
/** Credits claimed by the task job on the last run. */
|
|
1925
|
+
credit: number;
|
|
1926
|
+
/** Energy claimed by the task job on the last run. */
|
|
1927
|
+
energy: number;
|
|
1928
|
+
/** Tasks claimed by the task job on the last run. */
|
|
1929
|
+
claimed: number;
|
|
1930
|
+
/** One-line summary of the last run. */
|
|
1931
|
+
message?: string;
|
|
1932
|
+
/**
|
|
1933
|
+
* What the last run actually did, in the words of the task board.
|
|
1934
|
+
*
|
|
1935
|
+
* `message` is a count; this is the list a person can check off, which is
|
|
1936
|
+
* what turns a row from "it ran" into "it did the things I care about".
|
|
1937
|
+
*/
|
|
1938
|
+
detail?: readonly string[];
|
|
1939
|
+
/** A pending milestone worth naming, e.g. the next streak tier countdown. */
|
|
1940
|
+
progress?: string;
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Automation block on the status document.
|
|
1944
|
+
*
|
|
1945
|
+
* Carries the schedule and each job's last outcome so the card can answer
|
|
1946
|
+
* "is it on, when does it run, and what did it last do" without reaching into
|
|
1947
|
+
* the scheduler itself.
|
|
1948
|
+
*/
|
|
1949
|
+
interface PoolWebAutomation {
|
|
1950
|
+
/** Master switch, mirrored from the saved config. */
|
|
1951
|
+
enabled: boolean;
|
|
1952
|
+
/** Whether the loop is currently running. */
|
|
1953
|
+
running: boolean;
|
|
1954
|
+
/** Configured hours per job, so the card can show the schedule. */
|
|
1955
|
+
checkinHours: readonly number[];
|
|
1956
|
+
reportHours: readonly number[];
|
|
1957
|
+
taskHours: readonly number[];
|
|
1958
|
+
streakHours: readonly number[];
|
|
1959
|
+
travelHours: readonly number[];
|
|
1960
|
+
jobs: {
|
|
1961
|
+
checkin: PoolWebAutomationJob;
|
|
1962
|
+
report: PoolWebAutomationJob;
|
|
1963
|
+
tasks: PoolWebAutomationJob;
|
|
1964
|
+
streak: PoolWebAutomationJob;
|
|
1965
|
+
travel: PoolWebAutomationJob;
|
|
1966
|
+
};
|
|
1967
|
+
/** Claimable tasks seen on the most recent task pass, across accounts. */
|
|
1968
|
+
claimableSeen: number;
|
|
1969
|
+
/**
|
|
1970
|
+
* Whether a manual run is in flight. The card polls this to know when to
|
|
1971
|
+
* stop showing progress and report the result.
|
|
1972
|
+
*/
|
|
1973
|
+
runInProgress: boolean;
|
|
1974
|
+
/**
|
|
1975
|
+
* Per-account credits/energy/tasks the automation earned TODAY, keyed by
|
|
1976
|
+
* account id. An account that earned nothing is simply absent, so the card
|
|
1977
|
+
* can say "nothing yet" instead of showing a bare zero.
|
|
1978
|
+
*/
|
|
1979
|
+
earningsToday: Readonly<Record<string, PoolWebAutomationEarnings>>;
|
|
1980
|
+
}
|
|
1981
|
+
/** Today's automation take for one account. */
|
|
1982
|
+
interface PoolWebAutomationEarnings {
|
|
1983
|
+
/** Credits claimed from the task centre today. */
|
|
1984
|
+
credit: number;
|
|
1985
|
+
/** Energy claimed from the task centre today. */
|
|
1986
|
+
energy: number;
|
|
1987
|
+
/** Tasks claimed today. */
|
|
1988
|
+
claimed: number;
|
|
1989
|
+
/** Credits collected from check-in today. */
|
|
1990
|
+
checkinCredit: number;
|
|
1991
|
+
/** Credits from streak redemption and the lottery today. */
|
|
1992
|
+
bonusCredit: number;
|
|
1993
|
+
/** Credits from buddy adoption and the travel loop today. */
|
|
1994
|
+
travelCredit: number;
|
|
1995
|
+
/** Local date the counters belong to (YYYY-MM-DD). */
|
|
1996
|
+
date: string;
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* The two gateways, matching the provider ids the host registers. `cn` is the
|
|
2000
|
+
* domestic gateway (`copilot.tencent.com` / `codebuddy.cn`); `global` is the
|
|
2001
|
+
* international one (`workbuddy.ai`).
|
|
2002
|
+
*/
|
|
2003
|
+
type PoolRegion = 'cn' | 'global';
|
|
2004
|
+
/** How the pool spreads requests across its accounts. */
|
|
2005
|
+
type PoolDistribution = 'priority' | 'round-robin' | 'balanced' | 'sticky' | 'expiry';
|
|
2006
|
+
/**
|
|
2007
|
+
* The schedule every automation job falls back to.
|
|
2008
|
+
*
|
|
2009
|
+
* Shared by both halves on purpose. The host uses it when a configured hour
|
|
2010
|
+
* list arrives empty (the settings schema materializes "never configured" into
|
|
2011
|
+
* `[]`), and the card uses it when it writes the `automation` block back, so a
|
|
2012
|
+
* document that already holds an empty list is healed instead of being saved
|
|
2013
|
+
* back as an unrunnable schedule.
|
|
2014
|
+
*
|
|
2015
|
+
* This lives here rather than in `scheduler.ts` because the browser half cannot
|
|
2016
|
+
* import the host module: `scheduler.ts` pulls in `node:crypto` and the whole
|
|
2017
|
+
* upstream client, none of which exists in the browser bundle. Two hand-written
|
|
2018
|
+
* copies would drift, and the drift is invisible — the card would write a
|
|
2019
|
+
* schedule the scheduler does not run.
|
|
2020
|
+
*/
|
|
2021
|
+
export declare const DEFAULT_AUTOMATION_HOURS: {
|
|
2022
|
+
readonly checkin: readonly [9];
|
|
2023
|
+
readonly report: readonly [10];
|
|
2024
|
+
readonly tasks: readonly [11];
|
|
2025
|
+
readonly streak: readonly [12];
|
|
2026
|
+
readonly travel: readonly [9, 21];
|
|
2027
|
+
};
|
|
2028
|
+
//#endregion
|
|
2029
|
+
//#region src/catalog.d.ts
|
|
2030
|
+
/** One model the provider exposes. */
|
|
2031
|
+
interface WorkBuddyModelInfo {
|
|
2032
|
+
id: string;
|
|
2033
|
+
/** Display name; the multiplier is appended for the picker. */
|
|
2034
|
+
name: string;
|
|
2035
|
+
contextWindow: number;
|
|
2036
|
+
maxOutputTokens: number;
|
|
2037
|
+
/** Relative credit cost, e.g. 0.79 for `x0.79`. */
|
|
2038
|
+
multiplier?: number;
|
|
2039
|
+
/** Upstream-declared thinking levels. */
|
|
2040
|
+
supportedEfforts?: readonly string[];
|
|
2041
|
+
supportsImages: boolean;
|
|
2042
|
+
/** Upstream tags: free / limited-free / night-discount. */
|
|
2043
|
+
tags?: readonly string[];
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Static fallback used before the first live catalog fetch, and whenever the
|
|
2047
|
+
* upstream cannot be reached.
|
|
2048
|
+
*
|
|
2049
|
+
* This table is the DOMESTIC (CN) roster, and it is kept in step with what
|
|
2050
|
+
* `copilot.tencent.com` actually advertises. It matters more than a "just in
|
|
2051
|
+
* case" list usually would: a failed startup fetch falls back to it, and the
|
|
2052
|
+
* user sees that as "half my models were deleted" — `deepseek-v4.1-flash`
|
|
2053
|
+
* and friends simply vanishing from the picker, with no visible explanation.
|
|
2054
|
+
*
|
|
2055
|
+
* Every row below was captured from the live endpoint, INCLUDING the window
|
|
2056
|
+
* sizes: the previous version carried a 32K window for `hy3` when the gateway
|
|
2057
|
+
* says 192K, and listed `kimi-k3` under an id the gateway no longer uses
|
|
2058
|
+
* (`kimi-k3-1`). A stale fallback is worse than a short one — it looks
|
|
2059
|
+
* authoritative while being wrong.
|
|
2060
|
+
*
|
|
2061
|
+
* `multiplier: 0` is the gateways' own spelling of "free" (`credits: "x0.00"`),
|
|
2062
|
+
* which is what turns on the free badge.
|
|
2063
|
+
*/
|
|
2064
|
+
export declare const FALLBACK_WORKBUDDY_MODELS: readonly WorkBuddyModelInfo[];
|
|
2065
|
+
/** Which gateway a catalog describes. Mirrors `WorkBuddyRegion` in `upstream.ts`. */
|
|
2066
|
+
type CatalogRegion = 'cn' | 'global';
|
|
2067
|
+
/** Live catalog with a static fallback behind it. */
|
|
2068
|
+
export declare class WorkBuddyCatalog {
|
|
2069
|
+
/**
|
|
2070
|
+
* Which gateway this catalog serves, and therefore which static table it falls
|
|
2071
|
+
* back to.
|
|
2072
|
+
*
|
|
2073
|
+
* Optional in the constructor so every existing `new WorkBuddyCatalog()`
|
|
2074
|
+
* (tests, and the plugin's own default wiring) keeps compiling; it defaults to
|
|
2075
|
+
* `cn` because that is the table this plugin has always shipped.
|
|
2076
|
+
*/
|
|
2077
|
+
private readonly region;
|
|
2078
|
+
private models;
|
|
2079
|
+
constructor(region?: CatalogRegion);
|
|
2080
|
+
private listeners;
|
|
2081
|
+
/** User's model selection. Empty object = follow the catalog unfiltered. */
|
|
2082
|
+
private selection;
|
|
2083
|
+
/**
|
|
2084
|
+
* Whether `models` came from the gateway or from the static table.
|
|
2085
|
+
*
|
|
2086
|
+
* Tracked so the card can SAY which it is showing. A failed fetch used to be
|
|
2087
|
+
* invisible on screen: the picker simply held fewer models than before, which
|
|
2088
|
+
* the user reasonably read as "the plugin deleted my models" rather than "the
|
|
2089
|
+
* network hiccuped at startup".
|
|
2090
|
+
*/
|
|
2091
|
+
private source;
|
|
2092
|
+
/** When the live list last landed. */
|
|
2093
|
+
private lastUpdatedAt;
|
|
2094
|
+
/** Why the last fetch failed, for display. */
|
|
2095
|
+
private lastError;
|
|
2096
|
+
current(): readonly WorkBuddyModelInfo[];
|
|
2097
|
+
/**
|
|
2098
|
+
* The models DSH should actually offer, after applying the user's selection:
|
|
2099
|
+
* disabled models are dropped, an explicit image list overrides the upstream
|
|
2100
|
+
* capability flag, and a per-model budget caps the advertised window.
|
|
2101
|
+
*
|
|
2102
|
+
* An absent `enabledModelIds` means "everything" — a fresh install with no
|
|
2103
|
+
* saved selection must not present an empty picker.
|
|
2104
|
+
*/
|
|
2105
|
+
visible(): readonly WorkBuddyModelInfo[];
|
|
2106
|
+
/** Replace the catalog and notify the adapter to rebuild its model list. */
|
|
2107
|
+
update(models: readonly WorkBuddyModelInfo[]): void;
|
|
2108
|
+
/** Restore the static fallback, e.g. when the upstream stops answering. */
|
|
2109
|
+
reset(): void;
|
|
2110
|
+
/** Replace the user's selection; the adapter rebuilds from `visible()`. */
|
|
2111
|
+
applySelection(selection: ModelSelection): void;
|
|
2112
|
+
/** The selection currently in force, for the card's save round-trip. */
|
|
2113
|
+
currentSelection(): ModelSelection;
|
|
2114
|
+
/**
|
|
2115
|
+
* Whether this catalog is serving live data or the built-in table.
|
|
2116
|
+
*
|
|
2117
|
+
* `fallback` is not an error state, but it IS a degraded one: the user is
|
|
2118
|
+
* looking at a shorter roster than the gateway offers, so the card says so
|
|
2119
|
+
* and offers a retry instead of letting them wonder where the models went.
|
|
2120
|
+
*/
|
|
2121
|
+
currentSource(): PoolWebCatalogSource;
|
|
2122
|
+
/**
|
|
2123
|
+
* When the live list last landed, if it ever did.
|
|
2124
|
+
*
|
|
2125
|
+
* Named `catalogUpdatedAt()` rather than `updatedAt()` because the class
|
|
2126
|
+
* already had an `updatedAt` member; two members of the same name is a
|
|
2127
|
+
* compile error, and the awkwardness is a useful signal that the concept is
|
|
2128
|
+
* "when THIS catalog was refreshed", not a generic timestamp.
|
|
2129
|
+
*/
|
|
2130
|
+
catalogUpdatedAt(): string | undefined;
|
|
2131
|
+
/** Why the last fetch failed, if it did. */
|
|
2132
|
+
lastFetchError(): string | undefined;
|
|
2133
|
+
onChange(listener: () => void): () => void;
|
|
2134
|
+
find(id: string): WorkBuddyModelInfo | undefined;
|
|
2135
|
+
/** Replace the catalog from the live upstream list; keeps the fallback if empty. */
|
|
2136
|
+
updateFromUpstream(models: readonly WorkBuddyUpstreamModel[]): void;
|
|
2137
|
+
/**
|
|
2138
|
+
* Record that a fetch attempt failed, leaving the current list in place.
|
|
2139
|
+
*
|
|
2140
|
+
* The list is deliberately NOT reset here: a refresh that fails should keep
|
|
2141
|
+
* whatever working catalog is already loaded, rather than demoting a healthy
|
|
2142
|
+
* session to the static table because one retry ran out.
|
|
2143
|
+
*/
|
|
2144
|
+
noteFetchFailure(message: string): void;
|
|
2145
|
+
private notify;
|
|
2146
|
+
}
|
|
2147
|
+
/** The user's model selection, as stored in the settings section. */
|
|
2148
|
+
interface ModelSelection {
|
|
2149
|
+
/** Absent = every model in the catalog is offered. */
|
|
2150
|
+
enabledModelIds?: readonly string[];
|
|
2151
|
+
/** Absent = each model follows its upstream image capability. */
|
|
2152
|
+
imageModelIds?: readonly string[];
|
|
2153
|
+
/** Per-model context-window cap, keyed by model id. */
|
|
2154
|
+
contextBudgets?: Readonly<Record<string, number | undefined>>;
|
|
2155
|
+
}
|
|
2156
|
+
//#endregion
|
|
2157
|
+
//#region src/providers.d.ts
|
|
2158
|
+
/** One provider's saved settings: on/off, its key, and any discovered models. */
|
|
2159
|
+
interface ProviderSetting {
|
|
2160
|
+
/**
|
|
2161
|
+
* The explicit switch. Absent means "follow the credential": a provider the
|
|
2162
|
+
* user has actually configured is on, and the seventy-five they have never
|
|
2163
|
+
* touched are off.
|
|
2164
|
+
*
|
|
2165
|
+
* The card used to draw every one of the seventy-six vendors with its switch
|
|
2166
|
+
* already on, which read as "everything is already in the pool" and made the
|
|
2167
|
+
* one vendor that WAS configured indistinguishable from the rest.
|
|
2168
|
+
*/
|
|
2169
|
+
enabled?: boolean;
|
|
2170
|
+
apiKey?: string;
|
|
2171
|
+
/** User override for the base URL, for providers whose host is per-deployment. */
|
|
2172
|
+
baseUrl?: string;
|
|
2173
|
+
/** Model ids the user chose to expose, when narrower than the registry's. */
|
|
2174
|
+
models?: readonly string[];
|
|
2175
|
+
/** Model ids a listing round discovered, kept so a later save is not a guess. */
|
|
2176
|
+
discovered?: readonly string[];
|
|
2177
|
+
addedAt?: string;
|
|
2178
|
+
}
|
|
2179
|
+
/** The whole aggregate section of the plugin settings. */
|
|
2180
|
+
interface AggregateSettings {
|
|
2181
|
+
/** Master switch: off contributes no model rows at all. */
|
|
2182
|
+
enabled?: boolean;
|
|
2183
|
+
providers?: Readonly<Record<string, ProviderSetting>>;
|
|
2184
|
+
}
|
|
2185
|
+
//#endregion
|
|
2186
|
+
//#region src/combos.d.ts
|
|
2187
|
+
/** Chain strategies. Anything else on disk falls back to `fallback`. */
|
|
2188
|
+
declare const COMBO_STRATEGIES: readonly ['fallback', 'round-robin'];
|
|
2189
|
+
type ComboStrategy = (typeof COMBO_STRATEGIES)[number];
|
|
2190
|
+
/** A chain as the user configured it, before the catalogs are consulted. */
|
|
2191
|
+
interface ComboSetting {
|
|
2192
|
+
/** `combo:<slug>`; derived from the name when absent or malformed. */
|
|
2193
|
+
id?: string;
|
|
2194
|
+
name: string;
|
|
2195
|
+
/** Member model ids, in the order the user put them. */
|
|
2196
|
+
models: readonly string[];
|
|
2197
|
+
/** How the chain advances; `fallback` restarts from the top each turn. */
|
|
2198
|
+
strategy?: ComboStrategy;
|
|
2199
|
+
/** Consecutive turns one member serves before `round-robin` advances. */
|
|
2200
|
+
stickyLimit?: number;
|
|
2201
|
+
/** Absent is treated as on; an explicit false hides the row. */
|
|
2202
|
+
enabled?: boolean;
|
|
2203
|
+
}
|
|
2204
|
+
/** One picker row standing for a chain. */
|
|
2205
|
+
interface ComboModelRow {
|
|
2206
|
+
/** Always `combo:<slug>`. */
|
|
2207
|
+
id: string;
|
|
2208
|
+
name: string;
|
|
2209
|
+
/** Members that exist RIGHT NOW, in order. Never empty — see below. */
|
|
2210
|
+
models: readonly string[];
|
|
2211
|
+
strategy: ComboStrategy;
|
|
2212
|
+
stickyLimit: number;
|
|
2213
|
+
/** Smallest window across members; 0 when no member declares one. */
|
|
2214
|
+
contextWindow: number;
|
|
2215
|
+
maxOutputTokens: number;
|
|
2216
|
+
supportsImages: boolean;
|
|
2217
|
+
/**
|
|
2218
|
+
* Discriminator. Aggregate rows carry `providerId`, pool rows carry neither,
|
|
2219
|
+
* and only this shape is `true`.
|
|
2220
|
+
*/
|
|
2221
|
+
combo: true;
|
|
2222
|
+
}
|
|
2223
|
+
//#endregion
|
|
2224
|
+
//#region src/store.d.ts
|
|
2225
|
+
/**
|
|
2226
|
+
* The aggregate section as it lives on disk.
|
|
2227
|
+
*
|
|
2228
|
+
* Combos live in the same file rather than a second one: they only mean
|
|
2229
|
+
* anything in terms of the models the providers beside them serve, so a user who
|
|
2230
|
+
* clears a key and leaves a chain behind would be editing a dangling reference
|
|
2231
|
+
* in a file that no longer explains it. One document, one delete to undo.
|
|
2232
|
+
*
|
|
2233
|
+
* The LAN relay's settings live here too, for the same "one file to delete"
|
|
2234
|
+
* reason and because they hold a shared secret.
|
|
2235
|
+
*/
|
|
2236
|
+
type AggregateStore = AggregateSettings & {
|
|
2237
|
+
combos?: readonly ComboSetting[];
|
|
2238
|
+
relay?: RelaySetting;
|
|
2239
|
+
};
|
|
2240
|
+
/**
|
|
2241
|
+
* The LAN relay's own settings.
|
|
2242
|
+
*
|
|
2243
|
+
* Stored beside the aggregate keys rather than in the settings document for the
|
|
2244
|
+
* same reason the keys are: this holds a credential. The relay key is shown to
|
|
2245
|
+
* the user once so it can be pasted into another device, and the settings
|
|
2246
|
+
* document is echoed to the browser by the host's own form renderer — we would
|
|
2247
|
+
* rather that path never carried a shared secret.
|
|
2248
|
+
*/
|
|
2249
|
+
interface RelaySetting {
|
|
2250
|
+
enabled?: boolean;
|
|
2251
|
+
/** Bind address; only `127.0.0.1` and `0.0.0.0` are honoured. */
|
|
2252
|
+
host?: string;
|
|
2253
|
+
port?: number;
|
|
2254
|
+
/** URL-safe shared secret LAN clients authenticate with. */
|
|
2255
|
+
key?: string;
|
|
2256
|
+
}
|
|
2257
|
+
//#endregion
|
|
2258
|
+
//#region src/aggregate.d.ts
|
|
2259
|
+
/** Registry `format` → the wire vocabulary this module speaks. */
|
|
2260
|
+
type AggregateWire = 'chat' | 'responses' | 'messages';
|
|
2261
|
+
/** One model the aggregate lane offers. */
|
|
2262
|
+
interface AggregateModelRow {
|
|
2263
|
+
/** Composite id: `providerId/modelId`, which is also the picker id. */
|
|
2264
|
+
id: string;
|
|
2265
|
+
/** Display name: the provider's own name plus the row's label. */
|
|
2266
|
+
name: string;
|
|
2267
|
+
providerId: string;
|
|
2268
|
+
modelId: string;
|
|
2269
|
+
contextWindow: number;
|
|
2270
|
+
maxOutputTokens: number;
|
|
2271
|
+
supportsImages: boolean;
|
|
2272
|
+
wire: AggregateWire;
|
|
2273
|
+
/** `static` from the registry, `discovered` from a live listing. */
|
|
2274
|
+
source: 'static' | 'discovered';
|
|
2275
|
+
}
|
|
2276
|
+
/** One stored aggregate row set, refreshed on change, for the shim and adapter. */
|
|
2277
|
+
declare class AggregateCatalog {
|
|
2278
|
+
private readonly read;
|
|
2279
|
+
private readonly poolRows;
|
|
2280
|
+
private rows;
|
|
2281
|
+
private chainRows;
|
|
2282
|
+
/**
|
|
2283
|
+
* @param read The settings document.
|
|
2284
|
+
* @param poolRows The pool's own rows, needed only to resolve a chain member
|
|
2285
|
+
* that names a WorkBuddy model. A chain may mix pool models with aggregate
|
|
2286
|
+
* ones, so without this a perfectly valid chain would lose its pool half.
|
|
2287
|
+
*/
|
|
2288
|
+
constructor(read: () => AggregateStore | Promise<AggregateStore>, poolRows?: () => readonly WorkBuddyModelInfo[]);
|
|
2289
|
+
/**
|
|
2290
|
+
* Every row this catalog offers: third-party rows first, then chain rows.
|
|
2291
|
+
*
|
|
2292
|
+
* Chains come last so a picker listing stays readable — a chain is a
|
|
2293
|
+
* convenience over models the user already has listed, not a new vendor.
|
|
2294
|
+
*/
|
|
2295
|
+
current(): readonly (AggregateModelRow | ComboModelRow)[];
|
|
2296
|
+
/** Only the third-party rows, which is what the aggregate resolver needs. */
|
|
2297
|
+
providerRows(): readonly AggregateModelRow[];
|
|
2298
|
+
/** Only the chain rows. */
|
|
2299
|
+
comboRows(): readonly ComboModelRow[];
|
|
2300
|
+
/** Re-read the store and notify listeners when the roster actually changed. */
|
|
2301
|
+
refresh(): Promise<readonly (AggregateModelRow | ComboModelRow)[]>;
|
|
2302
|
+
private listeners;
|
|
2303
|
+
onChange(listener: () => void): () => void;
|
|
2304
|
+
/** One third-party row by composite id, or undefined. */
|
|
2305
|
+
find(id: string): AggregateModelRow | undefined;
|
|
2306
|
+
/** One chain row by combo id, or undefined. */
|
|
2307
|
+
findCombo(id: string): ComboModelRow | undefined;
|
|
2308
|
+
/** Whether any provider or chain is serving anything right now. */
|
|
2309
|
+
hasRows(): boolean;
|
|
2310
|
+
}
|
|
2311
|
+
//#endregion
|
|
2312
|
+
//#region src/stats.d.ts
|
|
2313
|
+
/**
|
|
2314
|
+
* Per-call usage accounting for the chat path.
|
|
2315
|
+
*
|
|
2316
|
+
* The gateway can answer `usage` when the request asks for it
|
|
2317
|
+
* (`stream_options.include_usage`), but the shim forwards the response body
|
|
2318
|
+
* verbatim and threw every number away. This module is where those numbers
|
|
2319
|
+
* stop being thrown away: a bounded ring of samples in, one aggregate
|
|
2320
|
+
* document out. Nothing here touches Node or HTTP, so the same code is
|
|
2321
|
+
* exercised by unit tests and by the browser half.
|
|
2322
|
+
*
|
|
2323
|
+
* Two counts are kept apart on purpose:
|
|
2324
|
+
*
|
|
2325
|
+
* - a LOGICAL TURN is one user request, however many accounts it visited;
|
|
2326
|
+
* - a PHYSICAL CALL is one upstream HTTP round trip.
|
|
2327
|
+
*
|
|
2328
|
+
* They answer different questions. "How many turns failed" must not inflate
|
|
2329
|
+
* because the pool rotated three accounts before succeeding, and "how much
|
|
2330
|
+
* did this model cost" must not be diluted by recording the turn once.
|
|
2331
|
+
* Collapsing them into a single `calls` number is exactly what makes a panel
|
|
2332
|
+
* disagree with the person reading it.
|
|
2333
|
+
*
|
|
2334
|
+
* Usage is a BONUS, never a guarantee: a gateway may ignore
|
|
2335
|
+
* `stream_options` entirely. A window with no reported usage reports zeroes
|
|
2336
|
+
* AND says so, so the panel can print "not reported" instead of a confident
|
|
2337
|
+
* "0 tokens".
|
|
2338
|
+
*
|
|
2339
|
+
* @module dsh-xdpool/stats
|
|
2340
|
+
*/
|
|
2341
|
+
/** Tokens one upstream call reported. Every field is a non-negative count. */
|
|
2342
|
+
interface TokenUsage {
|
|
2343
|
+
inputTokens: number;
|
|
2344
|
+
outputTokens: number;
|
|
2345
|
+
/** A subset of `outputTokens` the model spent thinking. */
|
|
2346
|
+
reasoningTokens: number;
|
|
2347
|
+
/** A subset of `inputTokens` served from the prompt cache. */
|
|
2348
|
+
cachedTokens: number;
|
|
2349
|
+
}
|
|
2350
|
+
/** One upstream HTTP round trip, successful or not. */
|
|
2351
|
+
interface UsageCall {
|
|
2352
|
+
/** Which gateway served it: `cn` or `global`. */
|
|
2353
|
+
region: string;
|
|
2354
|
+
/** The model id the request asked for. */
|
|
2355
|
+
model: string;
|
|
2356
|
+
/**
|
|
2357
|
+
* The pool account that served this physical call, as its display label.
|
|
2358
|
+
*
|
|
2359
|
+
* Carried because "which account earned this turn" is a question the ledger
|
|
2360
|
+
* has to answer from the samples themselves: the call is filed from inside
|
|
2361
|
+
* the stream forwarder, long after the choice of account has scrolled out of
|
|
2362
|
+
* whatever picked it. Without it the per-account view could only be a guess
|
|
2363
|
+
* reconstructed from turn ordering, which is exactly the kind of plausible
|
|
2364
|
+
* fiction this module refuses to publish.
|
|
2365
|
+
*/
|
|
2366
|
+
account?: string;
|
|
2367
|
+
/**
|
|
2368
|
+
* Identifies one user request.
|
|
2369
|
+
*
|
|
2370
|
+
* Calls sharing a `turnId` are retries of the SAME logical turn - the pool
|
|
2371
|
+
* cooling an account and taking the next one. That is what lets the panel
|
|
2372
|
+
* separate "3 calls" from "1 turn that rotated".
|
|
2373
|
+
*/
|
|
2374
|
+
turnId: string;
|
|
2375
|
+
/** When the call finished, in epoch milliseconds. */
|
|
2376
|
+
at: number;
|
|
2377
|
+
ok: boolean;
|
|
2378
|
+
/** Request sent to first byte. Absent when the call failed before any byte. */
|
|
2379
|
+
ttftMs?: number;
|
|
2380
|
+
/** First byte to `[DONE]`. Absent when the stream never completed. */
|
|
2381
|
+
decodeMs?: number;
|
|
2382
|
+
usage?: TokenUsage;
|
|
2383
|
+
}
|
|
2384
|
+
/** One row of the per-model aggregate. */
|
|
2385
|
+
interface UsageRow {
|
|
2386
|
+
region: string;
|
|
2387
|
+
model: string;
|
|
2388
|
+
/** Physical upstream calls. */
|
|
2389
|
+
calls: number;
|
|
2390
|
+
/** Logical turns that reached this model. */
|
|
2391
|
+
turns: number;
|
|
2392
|
+
/** Calls that failed. */
|
|
2393
|
+
failed: number;
|
|
2394
|
+
/** Turns where a failure was followed by a success on another account. */
|
|
2395
|
+
recovered: number;
|
|
2396
|
+
inputTokens: number;
|
|
2397
|
+
outputTokens: number;
|
|
2398
|
+
reasoningTokens: number;
|
|
2399
|
+
cachedTokens: number;
|
|
2400
|
+
/** Output tokens per second while decoding, or `null` when unsound. */
|
|
2401
|
+
tps: number | null;
|
|
2402
|
+
/** Mean time to first byte, or `null` when no call reported one. */
|
|
2403
|
+
avgTtftMs: number | null;
|
|
2404
|
+
/** Most recent completion, for "last used". */
|
|
2405
|
+
lastUsedAt: number;
|
|
2406
|
+
}
|
|
2407
|
+
/** One row of the per-account aggregate. */
|
|
2408
|
+
interface UsageAccountRow {
|
|
2409
|
+
/** Display label of the account, as the pool prints it. */
|
|
2410
|
+
account: string;
|
|
2411
|
+
/** Region the account belongs to; an account serves one gateway. */
|
|
2412
|
+
region: string;
|
|
2413
|
+
/** Physical calls this account served. */
|
|
2414
|
+
calls: number;
|
|
2415
|
+
/** Logical turns this account saw through. */
|
|
2416
|
+
turns: number;
|
|
2417
|
+
/** Calls this account refused. */
|
|
2418
|
+
failed: number;
|
|
2419
|
+
inputTokens: number;
|
|
2420
|
+
outputTokens: number;
|
|
2421
|
+
cachedTokens: number;
|
|
2422
|
+
/** Most recent completion, for "last used". */
|
|
2423
|
+
lastUsedAt: number;
|
|
2424
|
+
/** The two most-used models here, for a compact "what it usually runs" column. */
|
|
2425
|
+
topModels: readonly string[];
|
|
2426
|
+
}
|
|
2427
|
+
/** One calendar day, local time. */
|
|
2428
|
+
interface UsageDay {
|
|
2429
|
+
day: string;
|
|
2430
|
+
calls: number;
|
|
2431
|
+
failedTurns: number;
|
|
2432
|
+
inputTokens: number;
|
|
2433
|
+
outputTokens: number;
|
|
2434
|
+
}
|
|
2435
|
+
/** The document the usage panel renders. */
|
|
2436
|
+
interface UsageSummary {
|
|
2437
|
+
/** Samples still retained, so the panel can say "last N". */
|
|
2438
|
+
samples: number;
|
|
2439
|
+
/** Logical turns in the retained window. */
|
|
2440
|
+
turns: number;
|
|
2441
|
+
/** Turns where every call failed. */
|
|
2442
|
+
failedTurns: number;
|
|
2443
|
+
/** Turns that failed and then succeeded on another account. */
|
|
2444
|
+
recoveredTurns: number;
|
|
2445
|
+
/** Physical upstream calls; above `turns` when the pool rotated. */
|
|
2446
|
+
calls: number;
|
|
2447
|
+
/**
|
|
2448
|
+
* False when no retained sample carried usage.
|
|
2449
|
+
*
|
|
2450
|
+
* The panel must distinguish "used no tokens" from "the gateway told us
|
|
2451
|
+
* nothing", and only this flag tells them apart.
|
|
2452
|
+
*/
|
|
2453
|
+
usageReported: boolean;
|
|
2454
|
+
/**
|
|
2455
|
+
* Retained calls, newest first.
|
|
2456
|
+
*
|
|
2457
|
+
* Published so the ledger's "recent detail" table can be rendered from the
|
|
2458
|
+
* same numbers the aggregates above are computed from. Two derivations of
|
|
2459
|
+
* one fact set are two chances to disagree; handing the panel the raw
|
|
2460
|
+
* samples removes the second derivation entirely.
|
|
2461
|
+
*
|
|
2462
|
+
* Newest first because that is the order a reader wants, and `record` only
|
|
2463
|
+
* ever appends - so reversing here costs one copy rather than a second
|
|
2464
|
+
* insertion order to keep in sync.
|
|
2465
|
+
*/
|
|
2466
|
+
entries: readonly UsageCall[];
|
|
2467
|
+
rows: UsageRow[];
|
|
2468
|
+
accounts: UsageAccountRow[];
|
|
2469
|
+
days: UsageDay[];
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* A bounded window of recent calls, summarised on demand.
|
|
2473
|
+
*
|
|
2474
|
+
* In-memory by design: this is a live panel, and persisting every prompt's
|
|
2475
|
+
* token counts would turn a convenience into a record of what the user asked.
|
|
2476
|
+
* A restart therefore starts the chart again, which the panel states rather
|
|
2477
|
+
* than hides.
|
|
2478
|
+
*/
|
|
2479
|
+
declare class UsageRecorder {
|
|
2480
|
+
private samples;
|
|
2481
|
+
private readonly limit;
|
|
2482
|
+
constructor(limit?: number);
|
|
2483
|
+
/** Retain one call, dropping the oldest once the window is full. */
|
|
2484
|
+
record(call: UsageCall): void;
|
|
2485
|
+
/** Samples currently retained. */
|
|
2486
|
+
size(): number;
|
|
2487
|
+
/** Forget every retained call. */
|
|
2488
|
+
reset(): void;
|
|
2489
|
+
/** Aggregate the retained window. */
|
|
2490
|
+
summary(): UsageSummary;
|
|
2491
|
+
/**
|
|
2492
|
+
* One row per account, busiest first.
|
|
2493
|
+
*
|
|
2494
|
+
* Samples that never named an account are grouped under an empty label
|
|
2495
|
+
* rather than dropped: `account` is optional on `UsageCall`, so a caller that
|
|
2496
|
+
* files a sample without one - a failure raised before any account was ever
|
|
2497
|
+
* picked, say - would otherwise vanish from this view, and the account table
|
|
2498
|
+
* would add up to less than the call total with nothing on screen to explain
|
|
2499
|
+
* the gap. Every shim record site names its account; this branch exists so
|
|
2500
|
+
* that a future one cannot quietly break the totals.
|
|
2501
|
+
*
|
|
2502
|
+
* `topModels` keeps the two most-used ids rather than every one: this column
|
|
2503
|
+
* answers "what does this account usually run", and a list of nine ids is not
|
|
2504
|
+
* an answer to that question, it is just the other table again.
|
|
2505
|
+
*/
|
|
2506
|
+
private accountRows;
|
|
2507
|
+
/**
|
|
2508
|
+
* The daily series, oldest first, limited to the most recent days.
|
|
2509
|
+
*
|
|
2510
|
+
* Sorted by key rather than insertion order because a retained window can
|
|
2511
|
+
* straddle a month boundary, and a chart whose axis jumps backwards reads as
|
|
2512
|
+
* corrupted data rather than as history.
|
|
2513
|
+
*
|
|
2514
|
+
* `failedTurns` is counted by DISTINCT turn id, not by summing per-call
|
|
2515
|
+
* failures: a turn that rotated three accounts and failed on all three is
|
|
2516
|
+
* still one failed turn, and the whole reason this module keeps turns and
|
|
2517
|
+
* calls apart is so that number stays honest in every place it appears.
|
|
2518
|
+
*/
|
|
2519
|
+
private days;
|
|
2520
|
+
}
|
|
2521
|
+
//#endregion
|
|
2522
|
+
//#region src/shim.d.ts
|
|
2523
|
+
interface ShimLogger {
|
|
2524
|
+
info?(...args: unknown[]): void;
|
|
2525
|
+
warn(...args: unknown[]): void;
|
|
2526
|
+
error(...args: unknown[]): void;
|
|
2527
|
+
}
|
|
2528
|
+
interface WorkBuddyShim {
|
|
2529
|
+
ready: Promise<void>;
|
|
2530
|
+
baseUrl(): string;
|
|
2531
|
+
token(): string;
|
|
2532
|
+
close(): Promise<void>;
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Everything the aggregate lane needs to answer one request.
|
|
2536
|
+
*
|
|
2537
|
+
* Optional on {@link WorkBuddyShimOptions} because most shims are pool-only: the
|
|
2538
|
+
* CLI and the tests never build an aggregate catalog, and making them supply one
|
|
2539
|
+
* would mean threading an argument nobody reads.
|
|
2540
|
+
*/
|
|
2541
|
+
interface AggregateLaneOptions {
|
|
2542
|
+
/** The rows currently offered, keyed by composite `providerId/modelId`. */
|
|
2543
|
+
catalog: AggregateCatalog;
|
|
2544
|
+
/** Where the stored settings come from, re-read per request. */
|
|
2545
|
+
readStore: () => AggregateStore;
|
|
2546
|
+
}
|
|
2547
|
+
interface ExtraLaneOptions {
|
|
2548
|
+
/**
|
|
2549
|
+
* The免密 free lane: which rows exist right now, and where to send a turn.
|
|
2550
|
+
*
|
|
2551
|
+
* The row reader is consulted per request, exactly like the pool's catalog —
|
|
2552
|
+
* a roster that refreshed after startup must be servable without a restart.
|
|
2553
|
+
*/
|
|
2554
|
+
free: ExtraLaneSource;
|
|
2555
|
+
}
|
|
2556
|
+
interface ExtraLaneSource {
|
|
2557
|
+
current: () => readonly {
|
|
2558
|
+
modelId: string;
|
|
2559
|
+
id: string;
|
|
2560
|
+
}[];
|
|
2561
|
+
/**
|
|
2562
|
+
* Run one turn, handing back the frames to write.
|
|
2563
|
+
*
|
|
2564
|
+
* The source owns the whole upstream conversation — the session identity, the
|
|
2565
|
+
* wire conversion, the tool bookkeeping — and returns chat-wire SSE payloads,
|
|
2566
|
+
* because that is the only wire this shim's caller speaks.
|
|
2567
|
+
*/
|
|
2568
|
+
run: (options: {
|
|
2569
|
+
modelId: string;
|
|
2570
|
+
body: string;
|
|
2571
|
+
signal: AbortSignal;
|
|
2572
|
+
onFrame: (payload: string) => void;
|
|
2573
|
+
}) => Promise<unknown>;
|
|
2574
|
+
}
|
|
2575
|
+
interface WorkBuddyShimOptions {
|
|
2576
|
+
pool: WorkBuddyAccountPool;
|
|
2577
|
+
client: WorkBuddyUpstreamClient;
|
|
2578
|
+
catalog: WorkBuddyCatalog;
|
|
2579
|
+
logger?: ShimLogger;
|
|
2580
|
+
/**
|
|
2581
|
+
* Restrict this shim to one gateway. Two shims run side by side — one
|
|
2582
|
+
* per region — and each must only ever draw accounts that belong to its
|
|
2583
|
+
* own gateway. Absent means "every account" (a single-region deployment).
|
|
2584
|
+
*/
|
|
2585
|
+
region?: WorkBuddyRegion;
|
|
2586
|
+
/** Max accounts to try per request before giving up. */
|
|
2587
|
+
maxAttempts?: number;
|
|
2588
|
+
/**
|
|
2589
|
+
* Where per-call usage goes.
|
|
2590
|
+
*
|
|
2591
|
+
* Optional so the CLI and the tests can run a shim with no dashboard: they
|
|
2592
|
+
* assemble a pool without a panel, and forcing a recorder on them would mean
|
|
2593
|
+
* threading an argument nobody reads.
|
|
2594
|
+
*/
|
|
2595
|
+
usageRecorder?: UsageRecorder;
|
|
2596
|
+
/**
|
|
2597
|
+
* The second lane: providers the user holds a key for.
|
|
2598
|
+
*
|
|
2599
|
+
* Attached to the CN shim only. The aggregate lane is gateway-independent — it
|
|
2600
|
+
* has no region to filter by — so running it on both shims would expose the
|
|
2601
|
+
* same models twice under two provider ids, and a user who picked one would
|
|
2602
|
+
* have no way to tell which they were using.
|
|
2603
|
+
*/
|
|
2604
|
+
aggregate?: AggregateLaneOptions;
|
|
2605
|
+
/**
|
|
2606
|
+
* The third and fourth lanes: the pooled免密 gateway and the sealed co-paid
|
|
2607
|
+
* relay.
|
|
2608
|
+
*
|
|
2609
|
+
* Also CN-shim-only, for the aggregate lane's reason plus one of their own: a
|
|
2610
|
+
* second copy under the global provider id would list every model twice and
|
|
2611
|
+
* give the user two names for one lane.
|
|
2612
|
+
*/
|
|
2613
|
+
extra?: ExtraLaneOptions;
|
|
2614
|
+
}
|
|
2615
|
+
export declare function createWorkBuddyShim(options: WorkBuddyShimOptions): WorkBuddyShim;
|
|
2616
|
+
//#endregion
|
|
2617
|
+
//#region src/free-messages.d.ts
|
|
2618
|
+
/**
|
|
2619
|
+
* Message and frame translation between the OpenAI chat wire the plugin speaks
|
|
2620
|
+
* downstream and the three wires the免密 free gateway serves upstream.
|
|
2621
|
+
*
|
|
2622
|
+
* Downstream there is ONE wire. The pool, the third-party aggregate and the
|
|
2623
|
+
* co-paid lane all answer OpenAI chat completions, so the harness adapter is
|
|
2624
|
+
* configured once with `openAICompletionsApi()` and this file never has to be
|
|
2625
|
+
* reached from there. Upstream there are THREE: `/chat/completions`,
|
|
2626
|
+
* `/responses` and `/messages`, chosen per model. Everything about that
|
|
2627
|
+
* asymmetry lives here — request bodies go chat to responses/messages, and
|
|
2628
|
+
* answers come back responses/messages to chat.
|
|
2629
|
+
*
|
|
2630
|
+
* WHAT IS NOT IN HERE
|
|
2631
|
+
*
|
|
2632
|
+
* Images. A harness image arrives as an attachment descriptor, and turning one
|
|
2633
|
+
* into bytes the gateway can read is the adapter's job (it owns the attachment
|
|
2634
|
+
* plane). When no resolver is supplied an image block is dropped and a warning
|
|
2635
|
+
* is recorded, because silently forwarding a placeholder URL would make the
|
|
2636
|
+
* model answer about a picture it never saw. Everything else — text, tool
|
|
2637
|
+
* calls, tool results, reasoning — round-trips.
|
|
2638
|
+
*
|
|
2639
|
+
* The tool-call index mapping is the subtle part. Anthropic addresses blocks by
|
|
2640
|
+
* position in one flat array and Responses by output item, while OpenAI clients
|
|
2641
|
+
* expect a per-request sequential index, and a block may stream its argument
|
|
2642
|
+
* fragments BEFORE it names itself. So a block is classified the moment it names
|
|
2643
|
+
* itself, its argument fragments are held until then, and a held block whose
|
|
2644
|
+
* verdict turns out to be "not executable" is dropped instead of handed to a
|
|
2645
|
+
* client that cannot run it.
|
|
2646
|
+
*
|
|
2647
|
+
* @module src/free-messages
|
|
2648
|
+
*/
|
|
2649
|
+
/** The wire shapes this file translates between. */
|
|
2650
|
+
type FreeWire = 'chat' | 'responses' | 'messages';
|
|
2651
|
+
//#endregion
|
|
2652
|
+
//#region src/free-catalog.d.ts
|
|
2653
|
+
/** One free-lane row, as the adapter and the shim both read it. */
|
|
2654
|
+
interface FreeModelRow {
|
|
2655
|
+
/** Published id, WITHOUT the `free:` prefix. */
|
|
2656
|
+
id: string;
|
|
2657
|
+
/** The id the picker and the wire both carry. */
|
|
2658
|
+
modelId: string;
|
|
2659
|
+
name: string;
|
|
2660
|
+
wire: FreeWire;
|
|
2661
|
+
vision: boolean;
|
|
2662
|
+
reasoning: boolean;
|
|
2663
|
+
contextWindow: number;
|
|
2664
|
+
maxOutput: number;
|
|
2665
|
+
canDisableThinking: boolean;
|
|
2666
|
+
regionSensitive: boolean;
|
|
2667
|
+
}
|
|
2668
|
+
//#endregion
|
|
2669
|
+
//#region src/adapter.d.ts
|
|
2670
|
+
/** Provider route this bundle owns. */
|
|
2671
|
+
/** Provider route this bundle owns for the domestic (CN) gateway. */
|
|
2672
|
+
export declare const XDPOOL_PROVIDER = "xdpool";
|
|
2673
|
+
interface WorkBuddyAdapterOptions {
|
|
2674
|
+
shim: WorkBuddyShim;
|
|
2675
|
+
catalog: WorkBuddyCatalog | AggregateCatalog;
|
|
2676
|
+
/** Plugin context; the pi-ai adapter reads `attachments`/`fs` from it. */
|
|
2677
|
+
ctx: Context$1;
|
|
2678
|
+
providerId?: string;
|
|
2679
|
+
displayName?: string;
|
|
2680
|
+
/**
|
|
2681
|
+
* The rows this provider serves, when it is a LATER lane rather than a pool.
|
|
2682
|
+
*
|
|
2683
|
+
* A fourth route registers its own provider id, so it owns its whole roster
|
|
2684
|
+
* and `catalog` is never consulted for it — passing a pool catalog there is not
|
|
2685
|
+
* a silent default, it is ignored on purpose. The roster is read per call,
|
|
2686
|
+
* exactly like the pool's, so a refresh that lands after startup is visible
|
|
2687
|
+
* without a restart.
|
|
2688
|
+
*/
|
|
2689
|
+
extraRows?: (() => readonly FreeModelRow[]) | undefined;
|
|
2690
|
+
}
|
|
2691
|
+
/** What {@link createWorkBuddyAdapter} hands back. */
|
|
2692
|
+
interface WorkBuddyAdapter {
|
|
2693
|
+
providerId: string;
|
|
2694
|
+
displayName: string;
|
|
2695
|
+
adapter: PiAiAdapter;
|
|
2696
|
+
/** Rebuild the pi-ai model list from the current catalog. */
|
|
2697
|
+
buildModels: () => Model<Api>[];
|
|
2698
|
+
/** Rebuild the adapter's provider snapshot; call after a catalog update. */
|
|
2699
|
+
invalidate: () => void;
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Assemble the adapter. `getModels` re-reads the live catalog, and every
|
|
2703
|
+
* model's `baseUrl` is re-resolved per read so the shim's ephemeral port
|
|
2704
|
+
* applies from the first snapshot after startup. Call only after `shim.ready`.
|
|
2705
|
+
*
|
|
2706
|
+
* `catalog` accepts the aggregate catalog too; {@link isAggregateCatalog} tells
|
|
2707
|
+
* the two apart by shape rather than by a flag, because the aggregate rows are a
|
|
2708
|
+
* different kind of record, not a differently-populated pool row.
|
|
2709
|
+
*/
|
|
2710
|
+
export declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
|
|
2711
|
+
//#endregion
|
|
2712
|
+
//#region src/scheduler.d.ts
|
|
2713
|
+
/** How often the loop wakes to look for a due job. */
|
|
2714
|
+
/**
|
|
2715
|
+
* How long to wait for event scoring to land before re-reading the task list.
|
|
2716
|
+
*
|
|
2717
|
+
* Scoring is asynchronous on the upstream side, so an immediate re-read still
|
|
2718
|
+
* shows the old progress and the claim pass would skip a task that is in fact
|
|
2719
|
+
* now claimable. Measured: the chain is reflected by about eight seconds.
|
|
2720
|
+
*/
|
|
2721
|
+
export declare const EVENT_SCORE_WAIT_MS = 9000;
|
|
2722
|
+
export declare const AUTOMATION_TICK_MS = 60000;
|
|
2723
|
+
/** Which jobs the automation runs, and when. */
|
|
2724
|
+
/**
|
|
2725
|
+
* The persisted daily earnings ledger.
|
|
2726
|
+
*
|
|
2727
|
+
* `date` is the local day the counters belong to: a ledger from a previous day
|
|
2728
|
+
* is discarded on load rather than carried forward as "today".
|
|
2729
|
+
*/
|
|
2730
|
+
interface AutomationLedger {
|
|
2731
|
+
date: string;
|
|
2732
|
+
accounts: Record<string, AutomationAccountEarnings>;
|
|
2733
|
+
}
|
|
2734
|
+
interface AutomationOptions {
|
|
2735
|
+
/** Master switch; false stops every job. */
|
|
2736
|
+
enabled?: boolean;
|
|
2737
|
+
/** Hour list for the daily check-in job. */
|
|
2738
|
+
checkinHours?: readonly number[];
|
|
2739
|
+
/** Hour list for the task-centre job (accept + claim). */
|
|
2740
|
+
taskHours?: readonly number[];
|
|
2741
|
+
/** Hour list for the activity-report job. */
|
|
2742
|
+
reportHours?: readonly number[];
|
|
2743
|
+
/** Hour list for the streak-redemption job. */
|
|
2744
|
+
streakHours?: readonly number[];
|
|
2745
|
+
/** Hour list for the buddy travel job. */
|
|
2746
|
+
travelHours?: readonly number[];
|
|
2747
|
+
/** Per-account serial delay, in milliseconds. */
|
|
2748
|
+
accountDelayMs?: number;
|
|
2749
|
+
/** Override the gap between expert chains, in milliseconds (tests use 0). */
|
|
2750
|
+
expertGapMs?: number;
|
|
2751
|
+
/** Override the event-scoring wait, in milliseconds (tests use 0). */
|
|
2752
|
+
eventScoreWaitMs?: number;
|
|
2753
|
+
/** Override the clock, for tests. */
|
|
2754
|
+
now?: () => Date;
|
|
2755
|
+
/** Logger; defaults to a no-op so tests stay quiet. */
|
|
2756
|
+
logger?: SchedulerLogger;
|
|
2757
|
+
/**
|
|
2758
|
+
* Persist the daily earnings ledger, and restore it on construction.
|
|
2759
|
+
*
|
|
2760
|
+
* The ledger cannot live only in memory: a host restart mid-day would wipe
|
|
2761
|
+
* what the automation already earned, and the card would show nothing for
|
|
2762
|
+
* rewards that really were collected.
|
|
2763
|
+
*/
|
|
2764
|
+
loadEarnings?: () => AutomationLedger | undefined;
|
|
2765
|
+
saveEarnings?: (ledger: AutomationLedger) => void;
|
|
2766
|
+
}
|
|
2767
|
+
/** Logger surface, kept structural so any host logger fits. */
|
|
2768
|
+
interface SchedulerLogger {
|
|
2769
|
+
info?(...args: unknown[]): void;
|
|
2770
|
+
warn?(...args: unknown[]): void;
|
|
2771
|
+
}
|
|
2772
|
+
/** One job's last-run record, surfaced on the status document. */
|
|
2773
|
+
interface AutomationJobState {
|
|
2774
|
+
/** `YYYY-MM-DD` of the last completed run, or undefined if it never ran. */
|
|
2775
|
+
lastRunDate?: string;
|
|
2776
|
+
/**
|
|
2777
|
+
* The scheduled SLOT of the last run, as `YYYY-MM-DDTHH`.
|
|
2778
|
+
*
|
|
2779
|
+
* The tick de-duplicates on this rather than on the date: keying on the date
|
|
2780
|
+
* alone caps every job at one run a day, which is wrong for a job with two
|
|
2781
|
+
* time points — blocking the cat loop's second pass would leave the cat out
|
|
2782
|
+
* until tomorrow. Per slot a job runs once in each configured hour, while a
|
|
2783
|
+
* repeat tick inside the same hour is still refused.
|
|
2784
|
+
*/
|
|
2785
|
+
lastRunSlot?: string;
|
|
2786
|
+
/**
|
|
2787
|
+
* Every configured slot consumed TODAY, as `YYYY-MM-DDTHH`.
|
|
2788
|
+
*
|
|
2789
|
+
* This is what actually gates a re-run, and it is a SET rather than a single
|
|
2790
|
+
* slot because one field could not express the contract: a job configured for
|
|
2791
|
+
* `[9, 21]` runs twice, and a single "last slot" value can only remember one
|
|
2792
|
+
* of them — the second run erased the first, so the 9 o'clock candidate looked
|
|
2793
|
+
* unconsumed again and the job re-fired every hour after 21:00.
|
|
2794
|
+
*
|
|
2795
|
+
* The values are the CONFIGURED hours that were spent, never the clock time
|
|
2796
|
+
* the run happened to finish at. Storing the finish time was the original
|
|
2797
|
+
* defect: the reader compared it against a configured candidate, so the two
|
|
2798
|
+
* almost never matched and the gate stayed open all day.
|
|
2799
|
+
*
|
|
2800
|
+
* In-memory like the rest of `states`: this is a per-process record, and the
|
|
2801
|
+
* catch-up design deliberately re-runs an hour that passed while DSH was
|
|
2802
|
+
* closed. See `automationEarnings` for the ledger that DOES persist.
|
|
2803
|
+
*/
|
|
2804
|
+
firedSlots?: readonly string[];
|
|
2805
|
+
/**
|
|
2806
|
+
* The clock slot the last run STARTED in (`YYYY-MM-DDTHH`).
|
|
2807
|
+
*
|
|
2808
|
+
* Separate from {@link firedSlots} on purpose: this is a throttle ("do not
|
|
2809
|
+
* start twice inside the same hour"), while `firedSlots` is the schedule
|
|
2810
|
+
* ledger. Conflating the two is what let a catch-up run at 13:00 erase the
|
|
2811
|
+
* record of the 9 o'clock slot.
|
|
2812
|
+
*/
|
|
2813
|
+
lastFiredHour?: string;
|
|
2814
|
+
/** Epoch ms of the last completed run. */
|
|
2815
|
+
lastRunAtMs?: number;
|
|
2816
|
+
/** Accounts that completed without throwing. */
|
|
2817
|
+
ok: number;
|
|
2818
|
+
/** Accounts that threw (each one skipped, the run continued). */
|
|
2819
|
+
failed: number;
|
|
2820
|
+
/** Credits claimed by the task job on the last run. */
|
|
2821
|
+
credit: number;
|
|
2822
|
+
/** Energy claimed by the task job on the last run. */
|
|
2823
|
+
energy: number;
|
|
2824
|
+
/** Tasks claimed by the task job on the last run. */
|
|
2825
|
+
claimed: number;
|
|
2826
|
+
/** Human-readable summary of the last run. */
|
|
2827
|
+
/**
|
|
2828
|
+
* What this run actually did, in the words of the task board.
|
|
2829
|
+
*
|
|
2830
|
+
* `message` is a count ("3 accounts, 5 tasks claimed"); this is the list a
|
|
2831
|
+
* person can check off — the reward titles the pass collected. A row showing
|
|
2832
|
+
* only a bare number cannot answer "did it do the thing I care about", which
|
|
2833
|
+
* is the question the panel exists to answer.
|
|
2834
|
+
*/
|
|
2835
|
+
detail?: readonly string[];
|
|
2836
|
+
/**
|
|
2837
|
+
* A pending milestone worth naming, when there is one.
|
|
2838
|
+
*
|
|
2839
|
+
* Streak tiers are why this exists: every tier reads `locked` until enough
|
|
2840
|
+
* consecutive days accumulate, and "locked" on its own reads as "broken"
|
|
2841
|
+
* rather than "come back in four days".
|
|
2842
|
+
*/
|
|
2843
|
+
progress?: string;
|
|
2844
|
+
message?: string;
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* What the automation earned for ONE account today.
|
|
2848
|
+
*
|
|
2849
|
+
* Reset at the local day boundary alongside the per-job "already ran today"
|
|
2850
|
+
* guard, so the card shows today rather than a running total that never
|
|
2851
|
+
* answers "did it do anything for this account recently".
|
|
2852
|
+
*/
|
|
2853
|
+
interface AutomationAccountEarnings {
|
|
2854
|
+
/** Credits the automation claimed from the task centre today. */
|
|
2855
|
+
credit: number;
|
|
2856
|
+
/** Energy claimed from the task centre today. */
|
|
2857
|
+
energy: number;
|
|
2858
|
+
/** Tasks claimed today. */
|
|
2859
|
+
claimed: number;
|
|
2860
|
+
/**
|
|
2861
|
+
* Credits the automation collected from check-in today.
|
|
2862
|
+
*
|
|
2863
|
+
* Kept separate from `credit` because they are different achievements and the
|
|
2864
|
+
* card shows them on their own lines: "the automation claimed 3 tasks" and
|
|
2865
|
+
* "the automation checked in" are not the same claim to the user.
|
|
2866
|
+
*/
|
|
2867
|
+
checkinCredit: number;
|
|
2868
|
+
/** Credits from streak redemption + lottery today. */
|
|
2869
|
+
bonusCredit: number;
|
|
2870
|
+
/** Credits from the buddy adoption / travel loop today. */
|
|
2871
|
+
travelCredit: number;
|
|
2872
|
+
/** Local date the counters belong to. */
|
|
2873
|
+
date: string;
|
|
2874
|
+
}
|
|
2875
|
+
/** What ONE account gained during a single run. */
|
|
2876
|
+
interface AutomationAccountGain {
|
|
2877
|
+
credit: number;
|
|
2878
|
+
energy: number;
|
|
2879
|
+
claimed: number;
|
|
2880
|
+
checkinCredit: number;
|
|
2881
|
+
bonusCredit: number;
|
|
2882
|
+
travelCredit: number;
|
|
2883
|
+
}
|
|
2884
|
+
/** Totals from running the whole ordered pass at once. */
|
|
2885
|
+
interface AutomationRunSummary {
|
|
2886
|
+
/** How many jobs actually ran (a job with no hour is skipped). */
|
|
2887
|
+
jobsRun: number;
|
|
2888
|
+
/** Accounts that finished without error, summed across jobs. */
|
|
2889
|
+
okCount: number;
|
|
2890
|
+
/** Accounts that failed, summed across jobs. */
|
|
2891
|
+
failed: number;
|
|
2892
|
+
credit: number;
|
|
2893
|
+
energy: number;
|
|
2894
|
+
claimed: number;
|
|
2895
|
+
/**
|
|
2896
|
+
* What each account gained during THIS run, keyed by account id. Only
|
|
2897
|
+
* accounts that gained something appear.
|
|
2898
|
+
*/
|
|
2899
|
+
accounts: Readonly<Record<string, AutomationAccountGain>>;
|
|
2900
|
+
}
|
|
2901
|
+
/** Automation snapshot for the status document and the card. */
|
|
2902
|
+
interface AutomationStatus {
|
|
2903
|
+
enabled: boolean;
|
|
2904
|
+
/** Whether the loop is running. */
|
|
2905
|
+
running: boolean;
|
|
2906
|
+
checkinHours: readonly number[];
|
|
2907
|
+
taskHours: readonly number[];
|
|
2908
|
+
reportHours: readonly number[];
|
|
2909
|
+
streakHours: readonly number[];
|
|
2910
|
+
travelHours: readonly number[];
|
|
2911
|
+
jobs: {
|
|
2912
|
+
checkin: AutomationJobState;
|
|
2913
|
+
report: AutomationJobState;
|
|
2914
|
+
tasks: AutomationJobState;
|
|
2915
|
+
streak: AutomationJobState;
|
|
2916
|
+
travel: AutomationJobState;
|
|
2917
|
+
};
|
|
2918
|
+
/** Claimable tasks seen on the most recent task pass, across accounts. */
|
|
2919
|
+
claimableSeen: number;
|
|
2920
|
+
/**
|
|
2921
|
+
* Per-account totals for today, keyed by account id. Only accounts that
|
|
2922
|
+
* actually earned something appear, so the card can render "no earnings"
|
|
2923
|
+
* as an absence rather than a zero it has to explain.
|
|
2924
|
+
*/
|
|
2925
|
+
earningsToday: Readonly<Record<string, AutomationAccountEarnings>>;
|
|
2926
|
+
/**
|
|
2927
|
+
* Whether a manual run is in flight right now.
|
|
2928
|
+
*
|
|
2929
|
+
* A run takes tens of seconds (one upstream call per account per job, plus
|
|
2930
|
+
* the scoring wait), which is far too long for the card to hold a request
|
|
2931
|
+
* open. The button starts a run and the panel polls this flag instead.
|
|
2932
|
+
*/
|
|
2933
|
+
runInProgress: boolean;
|
|
2934
|
+
}
|
|
2935
|
+
/** The three plus one job kinds, in a stable order. */
|
|
2936
|
+
type AutomationJobKind = 'checkin' | 'tasks' | 'report' | 'streak' | 'travel';
|
|
2937
|
+
/** The four jobs in the order a tick runs them: report before tasks, always. */
|
|
2938
|
+
export declare const AUTOMATION_JOB_KINDS: readonly AutomationJobKind[];
|
|
2939
|
+
/** Reject anything that is not a job kind, so a route cannot name an unknown job. */
|
|
2940
|
+
export declare function isAutomationJobKind(value: unknown): value is AutomationJobKind;
|
|
2941
|
+
export declare function dayKey(date: Date, timeZone?: string): string;
|
|
2942
|
+
/**
|
|
2943
|
+
* Whether `now`'s local hour is one of `hours`.
|
|
2944
|
+
*
|
|
2945
|
+
* The reference panel computes a `nextFire` instant and sleeps until it; this
|
|
2946
|
+
* loop instead wakes every minute and asks "is any job due now". Both fire at
|
|
2947
|
+
* the top of a configured hour, but the polling form cannot miss a slot to a
|
|
2948
|
+
* suspended process — a laptop that slept through 10:00 still runs the job the
|
|
2949
|
+
* moment it wakes, on the same day.
|
|
2950
|
+
*/
|
|
2951
|
+
export declare function isFireHour(now: Date, hours: readonly number[]): boolean;
|
|
2952
|
+
/**
|
|
2953
|
+
* The points automation.
|
|
2954
|
+
*
|
|
2955
|
+
* Owns a single timer loop. Construction is inert — nothing runs until
|
|
2956
|
+
* {@link start}, and {@link stop} is idempotent so a plugin teardown that fires
|
|
2957
|
+
* twice is harmless.
|
|
2958
|
+
*/
|
|
2959
|
+
export declare class WorkBuddyScheduler {
|
|
2960
|
+
private readonly pool;
|
|
2961
|
+
private readonly client;
|
|
2962
|
+
private readonly logger;
|
|
2963
|
+
private readonly now;
|
|
2964
|
+
private readonly delayMs;
|
|
2965
|
+
/**
|
|
2966
|
+
* How long to wait for event scoring before re-reading the task list.
|
|
2967
|
+
* Tests set 0 so a pass does not spend nine real seconds per account.
|
|
2968
|
+
*/
|
|
2969
|
+
private readonly eventScoreWaitMs;
|
|
2970
|
+
/**
|
|
2971
|
+
* Gap between two expert summon chains.
|
|
2972
|
+
* Tests set 0 so a pass does not spend six real seconds per expert.
|
|
2973
|
+
*/
|
|
2974
|
+
private readonly expertGapMs;
|
|
2975
|
+
private enabled;
|
|
2976
|
+
private checkinHours;
|
|
2977
|
+
private taskHours;
|
|
2978
|
+
private reportHours;
|
|
2979
|
+
private streakHours;
|
|
2980
|
+
private travelHours;
|
|
2981
|
+
private timer;
|
|
2982
|
+
private running;
|
|
2983
|
+
/** Guards against a slow run overlapping the next tick. */
|
|
2984
|
+
private busy;
|
|
2985
|
+
/** True while a manual run is in flight, so the card can poll it. */
|
|
2986
|
+
private runInFlight;
|
|
2987
|
+
/**
|
|
2988
|
+
* Set once {@link stop} is called.
|
|
2989
|
+
*
|
|
2990
|
+
* Deliberately false before `start`: the loop is not running yet, but a
|
|
2991
|
+
* manual `tick` must still work. `stop` is what makes a run abandon the
|
|
2992
|
+
* accounts it has not reached yet.
|
|
2993
|
+
*/
|
|
2994
|
+
private stopped;
|
|
2995
|
+
private readonly states;
|
|
2996
|
+
private claimableSeen;
|
|
2997
|
+
/**
|
|
2998
|
+
* Credits/energy/tasks earned per account TODAY, keyed by account id.
|
|
2999
|
+
*
|
|
3000
|
+
* Cleared whenever the day key rolls over, so the card always answers
|
|
3001
|
+
* "what did the automation get for THIS account today".
|
|
3002
|
+
*/
|
|
3003
|
+
private earnings;
|
|
3004
|
+
/** Day key the counters above belong to. */
|
|
3005
|
+
private earningsDate;
|
|
3006
|
+
/** Host hooks that persist the ledger across restarts. */
|
|
3007
|
+
private readonly loadEarnings;
|
|
3008
|
+
private saveEarningsFn;
|
|
3009
|
+
constructor(pool: WorkBuddyAccountPool, client: WorkBuddyUpstreamClient, options?: AutomationOptions);
|
|
3010
|
+
/** Apply a new configuration; safe to call while running. */
|
|
3011
|
+
/**
|
|
3012
|
+
* Install the persistence hook once the host settings service is available.
|
|
3013
|
+
*
|
|
3014
|
+
* Separate from the constructor because the scheduler is built with the pool,
|
|
3015
|
+
* long before the settings section exists; a ledger written before that point
|
|
3016
|
+
* would have nowhere to go.
|
|
3017
|
+
*/
|
|
3018
|
+
setEarningsPersistence(save: (ledger: AutomationLedger) => void | Promise<void>): void;
|
|
3019
|
+
/**
|
|
3020
|
+
* Fold a previously persisted ledger back in, when it belongs to today.
|
|
3021
|
+
*
|
|
3022
|
+
* Used after the settings document becomes readable, which happens after
|
|
3023
|
+
* construction; a ledger from an earlier day is ignored so the counters never
|
|
3024
|
+
* claim yesterday as today.
|
|
3025
|
+
*/
|
|
3026
|
+
applyEarningsLedger(ledger: AutomationLedger): void;
|
|
3027
|
+
applyConfig(options: AutomationOptions): void;
|
|
3028
|
+
/** Hours for one job, used by the loop and the status document. */
|
|
3029
|
+
private hoursOf;
|
|
3030
|
+
/** Start the loop. Idempotent. */
|
|
3031
|
+
start(): void;
|
|
3032
|
+
/** Stop the loop. Idempotent, and safe before `start`. */
|
|
3033
|
+
stop(): void;
|
|
3034
|
+
/** Snapshot for the status document. */
|
|
3035
|
+
/**
|
|
3036
|
+
* Run one job immediately, regardless of the clock.
|
|
3037
|
+
*
|
|
3038
|
+
* Exists so the automation can be verified from the card without waiting for
|
|
3039
|
+
* its hour. A manual run is recorded exactly like a scheduled one, so the
|
|
3040
|
+
* timer will not repeat it later the same day: every job is idempotent, but a
|
|
3041
|
+
* second pass would still be wasted upstream calls.
|
|
3042
|
+
*
|
|
3043
|
+
* `force` ignores the already-ran-today guard, which is what pressing the
|
|
3044
|
+
* button a second time means.
|
|
3045
|
+
*/
|
|
3046
|
+
runNow(kind: AutomationJobKind, force?: boolean): Promise<AutomationJobState>;
|
|
3047
|
+
/**
|
|
3048
|
+
* Run every job once, in the scheduled order.
|
|
3049
|
+
*
|
|
3050
|
+
* Order matters and is not configurable: the activity report has to land
|
|
3051
|
+
* before the task pass reads task progress, or the pass sees counters the
|
|
3052
|
+
* report would have moved. This is what the card's single button calls.
|
|
3053
|
+
*/
|
|
3054
|
+
/**
|
|
3055
|
+
* Start a full pass in the background and return immediately.
|
|
3056
|
+
*
|
|
3057
|
+
* A pass takes tens of seconds - one upstream round trip per account per job,
|
|
3058
|
+
* plus the scoring wait - which is far too long to hold the card request open:
|
|
3059
|
+
* the browser or the host web server would time out, and the user would see
|
|
3060
|
+
* a hung button for a run that is actually working.
|
|
3061
|
+
*
|
|
3062
|
+
* Returns whether a run started. A second call while one is in flight is
|
|
3063
|
+
* ignored rather than queued: pressing the button twice means hurry up, and
|
|
3064
|
+
* the run already under way covers it.
|
|
3065
|
+
*/
|
|
3066
|
+
startRunAll(): boolean;
|
|
3067
|
+
runAll(): Promise<AutomationRunSummary>;
|
|
3068
|
+
status(): AutomationStatus;
|
|
3069
|
+
/**
|
|
3070
|
+
* One poll: run every due job, serially.
|
|
3071
|
+
*
|
|
3072
|
+
* Serial by design — the jobs share the same accounts and the upstream
|
|
3073
|
+
* rate-limits per account, so overlapping passes would only trip that limit.
|
|
3074
|
+
* A job that throws is recorded and the loop continues.
|
|
3075
|
+
*/
|
|
3076
|
+
/**
|
|
3077
|
+
* Whether `kind` is due at `now`: its earliest configured hour has passed in
|
|
3078
|
+
* the scheduling timezone, and no hour of today has been consumed yet.
|
|
3079
|
+
*
|
|
3080
|
+
* Hours are consumed per SLOT (one entry per configured hour), so a job with
|
|
3081
|
+
* two hours still runs twice a day — but a job whose hour passed while DSH was
|
|
3082
|
+
* closed runs immediately on the next tick instead of waiting for tomorrow.
|
|
3083
|
+
*/
|
|
3084
|
+
/**
|
|
3085
|
+
* The configured slot `kind` should consume at `now`, or undefined when none.
|
|
3086
|
+
*
|
|
3087
|
+
* Returns the SLOT STRING (not a boolean) because the caller must record the
|
|
3088
|
+
* same value it acted on. Returning a boolean was the original bug's enabler:
|
|
3089
|
+
* the tick asked "is it due", then `runJob` independently wrote "what time is
|
|
3090
|
+
* it now" — and those two answers were almost never equal.
|
|
3091
|
+
*
|
|
3092
|
+
* CATCH-UP semantics are deliberate: a candidate fires once its hour has
|
|
3093
|
+
* PASSED and its slot is still unconsumed, so a laptop that slept through
|
|
3094
|
+
* 10:00 still runs the job when it wakes, on the same day.
|
|
3095
|
+
*
|
|
3096
|
+
* The EARLIEST unconsumed candidate wins, which is what keeps a two-hour job
|
|
3097
|
+
* (`travelHours: [9, 21]`) whole: consuming the earliest due slot leaves the
|
|
3098
|
+
* later one for its own hour.
|
|
3099
|
+
*/
|
|
3100
|
+
private dueSlot;
|
|
3101
|
+
/** Today's consumed slots for one job, as a set. */
|
|
3102
|
+
private firedSlotsOf;
|
|
3103
|
+
/** Record one consumed slot on a job's state. */
|
|
3104
|
+
private consumeSlot;
|
|
3105
|
+
private tick;
|
|
3106
|
+
/** Run one job against every eligible account and record the outcome. */
|
|
3107
|
+
/**
|
|
3108
|
+
* Add one account's take to today's counters, resetting first if the day
|
|
3109
|
+
* rolled over. Called from the task pass, which is the only job that earns.
|
|
3110
|
+
*/
|
|
3111
|
+
/**
|
|
3112
|
+
* Today's per-account earnings, as a plain object for the status document.
|
|
3113
|
+
*
|
|
3114
|
+
* Rolls the day first so a status read just after midnight does not report
|
|
3115
|
+
* yesterday's totals under today's date.
|
|
3116
|
+
*/
|
|
3117
|
+
private earningsSnapshot;
|
|
3118
|
+
/**
|
|
3119
|
+
* Add one account take to today counters, resetting first if the day rolled
|
|
3120
|
+
* over. Every source is tracked separately so the card can show what earned
|
|
3121
|
+
* what, rather than one opaque total.
|
|
3122
|
+
*/
|
|
3123
|
+
private recordEarnings;
|
|
3124
|
+
/** Clear the per-account counters when the local day changes. */
|
|
3125
|
+
private rollEarnings;
|
|
3126
|
+
/**
|
|
3127
|
+
* Write the ledger through the host hook, when one was supplied.
|
|
3128
|
+
*
|
|
3129
|
+
* Best effort on purpose: a failed save must never abort a run that has
|
|
3130
|
+
* already collected rewards, and the in-memory ledger keeps serving the card
|
|
3131
|
+
* for the rest of the session either way.
|
|
3132
|
+
*/
|
|
3133
|
+
private persistEarnings;
|
|
3134
|
+
/**
|
|
3135
|
+
* Run one job against every eligible account and record the outcome.
|
|
3136
|
+
*
|
|
3137
|
+
* `consumedSlot` is the configured slot this run spends (`dueSlot`'s answer).
|
|
3138
|
+
* It is passed IN rather than recomputed here so the value recorded is exactly
|
|
3139
|
+
* the value the schedule decided on — the defect this replaces wrote
|
|
3140
|
+
* `slotKey(now)` instead, i.e. the clock time the run finished at, which
|
|
3141
|
+
* almost never equals the configured candidate the gate had compared against.
|
|
3142
|
+
*
|
|
3143
|
+
* The task job runs in TWO passes. The first sends the event chains that light
|
|
3144
|
+
* up client-scored tasks; the second collects rewards. They are separate
|
|
3145
|
+
* because scoring lands asynchronously — a chain sent and claimed within the
|
|
3146
|
+
* same breath finds the task still un-scored — and because sending is fast
|
|
3147
|
+
* while claiming wants the whole pool to have been lit up first. Splitting
|
|
3148
|
+
* them costs one shared wait instead of one wait per account.
|
|
3149
|
+
*/
|
|
3150
|
+
private runJob;
|
|
3151
|
+
/** Compose the one-line summary shown on the card. */
|
|
3152
|
+
private summarise;
|
|
3153
|
+
/**
|
|
3154
|
+
* Accounts to run against, in pool order.
|
|
3155
|
+
*
|
|
3156
|
+
* Disabled accounts are excluded here rather than filtered by the caller so a
|
|
3157
|
+
* card switch takes effect on the next pass without any event plumbing.
|
|
3158
|
+
*/
|
|
3159
|
+
private accountsInOrder;
|
|
3160
|
+
/**
|
|
3161
|
+
* Send one activity report, then verify it landed.
|
|
3162
|
+
*
|
|
3163
|
+
* The upstream answers 200 even when it drops the event, so the streak is
|
|
3164
|
+
* read back as the oracle: `days > 0` means it counted. A failed read-back is
|
|
3165
|
+
* logged and treated as a suspicious result, never as a retry — the report is
|
|
3166
|
+
* idempotent per day, and hammering it is exactly what the one-a-day quota
|
|
3167
|
+
* exists to avoid.
|
|
3168
|
+
*/
|
|
3169
|
+
private reportOne;
|
|
3170
|
+
private sendEventChains;
|
|
3171
|
+
/**
|
|
3172
|
+
* Send one chain on the channel it was built for.
|
|
3173
|
+
*
|
|
3174
|
+
* The transport is not a detail of the sender: the scorer keys different
|
|
3175
|
+
* tasks to different fingerprint families, so a web-scored event posted as a
|
|
3176
|
+
* desktop event is accepted and then ignored.
|
|
3177
|
+
*/
|
|
3178
|
+
private sendChain;
|
|
3179
|
+
/**
|
|
3180
|
+
* Build every chain that scores one task.
|
|
3181
|
+
*
|
|
3182
|
+
* Most tasks need a single chain; `template_5` needs five, because the scorer
|
|
3183
|
+
* counts distinct `template_used` events rather than a boolean. The two tasks
|
|
3184
|
+
* that join a conversation (skill, expert) open a real one first, which is why
|
|
3185
|
+
* this is async.
|
|
3186
|
+
*/
|
|
3187
|
+
private chainsFor;
|
|
3188
|
+
/**
|
|
3189
|
+
* The summon-and-use chains for the expert tasks.
|
|
3190
|
+
*
|
|
3191
|
+
* Two steps per expert, and both are load-bearing: the summon events alone are
|
|
3192
|
+
* impressions, and a use event on its own scores nothing because the scorer
|
|
3193
|
+
* looks the conversation up. Only a real chat with `X-Expert-Id` produces an
|
|
3194
|
+
* id it will accept.
|
|
3195
|
+
*/
|
|
3196
|
+
private expertChains;
|
|
3197
|
+
/**
|
|
3198
|
+
* The 腾讯轻量云 expert chain.
|
|
3199
|
+
*
|
|
3200
|
+
* Structurally the same as the expert task, with two differences the scorer
|
|
3201
|
+
* checks: `agent_task_created` has to name the expert, and the use event has
|
|
3202
|
+
* to report `mode: 'LOCAL'` with an empty type and zero cost — that is what
|
|
3203
|
+
* the lighthouse criterion looks for.
|
|
3204
|
+
*/
|
|
3205
|
+
private lighthouseChains;
|
|
3206
|
+
/**
|
|
3207
|
+
* The task-centre pass for one account.
|
|
3208
|
+
*
|
|
3209
|
+
* Order matters: enrich first (enrol in everything open), then claim. Both
|
|
3210
|
+
* halves are idempotent — accepting an already-accepted task succeeds, and a
|
|
3211
|
+
* repeat claim answers `already_claimed` — so a pass that dies halfway is
|
|
3212
|
+
* safe to replay on the next tick.
|
|
3213
|
+
*/
|
|
3214
|
+
private runTasks;
|
|
3215
|
+
/**
|
|
3216
|
+
* Streak redemption plus the lottery it unlocks.
|
|
3217
|
+
*
|
|
3218
|
+
* Tiers unlock on consecutive active days (7/14/28). Redeeming one pays
|
|
3219
|
+
* credits, energy, a makeup card and — the part nothing else grants — lottery
|
|
3220
|
+
* draws, so the draw runs straight after and only for the chances in hand.
|
|
3221
|
+
*
|
|
3222
|
+
* Everything here is idempotent: a tier already claimed is skipped by its
|
|
3223
|
+
* status, and a draw consumes one chance, so a replay cannot double-spend.
|
|
3224
|
+
*/
|
|
3225
|
+
private redeemStreak;
|
|
3226
|
+
/**
|
|
3227
|
+
* One trip through the buddy travel loop for an account.
|
|
3228
|
+
*
|
|
3229
|
+
* A single pass advances the state machine by at most one step: a trip
|
|
3230
|
+
* that has arrived is collected, and an idle buddy is sent out. A buddy
|
|
3231
|
+
* already travelling is left alone — there is nothing to do until it lands.
|
|
3232
|
+
*
|
|
3233
|
+
* Measured against the live upstream: the departed trip reports
|
|
3234
|
+
* `dailyLimitReached` immediately, so the once-a-day limit needs no local
|
|
3235
|
+
* bookkeeping.
|
|
3236
|
+
*/
|
|
3237
|
+
private runTravel;
|
|
3238
|
+
}
|
|
3239
|
+
//#endregion
|
|
3240
|
+
//#region src/status.d.ts
|
|
3241
|
+
/** One account's status row. */
|
|
3242
|
+
interface AccountStatus {
|
|
3243
|
+
id: string;
|
|
3244
|
+
label: string;
|
|
3245
|
+
nickname?: string;
|
|
3246
|
+
domain: string;
|
|
3247
|
+
/** ISO timestamp when the access token expires. */
|
|
3248
|
+
expiresAt?: string;
|
|
3249
|
+
/** Account-wide cooldown (every model blocked). */
|
|
3250
|
+
cooling: boolean;
|
|
3251
|
+
cooldownUntil?: string;
|
|
3252
|
+
/** Per-model cooldowns active right now (modelId → ISO until); the account
|
|
3253
|
+
* itself is not `cooling` while only some models are limited. */
|
|
3254
|
+
modelCooldowns?: readonly {
|
|
3255
|
+
modelId: string;
|
|
3256
|
+
until: string;
|
|
3257
|
+
}[];
|
|
3258
|
+
rateLimitHits: number;
|
|
3259
|
+
/** Read-only aggregated credit summary for the account. */
|
|
3260
|
+
credits?: WorkBuddyCredits;
|
|
3261
|
+
creditsError?: string;
|
|
3262
|
+
sourcePath: string;
|
|
3263
|
+
}
|
|
3264
|
+
/** Whole-plugin status document. */
|
|
3265
|
+
interface WorkBuddyStatus {
|
|
3266
|
+
ok: boolean;
|
|
3267
|
+
accounts: AccountStatus[];
|
|
3268
|
+
activeAccountId?: string;
|
|
3269
|
+
cooling: number;
|
|
3270
|
+
models: {
|
|
3271
|
+
id: string;
|
|
3272
|
+
name: string;
|
|
3273
|
+
multiplier?: number;
|
|
3274
|
+
tags?: readonly string[];
|
|
3275
|
+
}[];
|
|
3276
|
+
shim: {
|
|
3277
|
+
running: boolean;
|
|
3278
|
+
baseUrl?: string;
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
interface StatusOptions {
|
|
3282
|
+
pool: WorkBuddyAccountPool;
|
|
3283
|
+
/** The catalog to report. Regional callers pass their own region's. */
|
|
3284
|
+
catalog: WorkBuddyCatalog;
|
|
3285
|
+
client: WorkBuddyUpstreamClient;
|
|
3286
|
+
shim?: {
|
|
3287
|
+
running: boolean;
|
|
3288
|
+
baseUrl?: string;
|
|
3289
|
+
};
|
|
3290
|
+
/** Query credits per account. Off for cheap diagnostics runs. */
|
|
3291
|
+
includeCredits?: boolean;
|
|
3292
|
+
}
|
|
3293
|
+
/** Build the status document. Never throws. */
|
|
3294
|
+
export declare function buildStatus(options: StatusOptions): Promise<WorkBuddyStatus>;
|
|
3295
|
+
/** Format the status document for a terminal. */
|
|
3296
|
+
export declare function formatStatus(status: WorkBuddyStatus): string;
|
|
3297
|
+
/** Format the per-model credit multipliers. */
|
|
3298
|
+
export declare function formatRates(status: WorkBuddyStatus): string;
|
|
3299
|
+
//#endregion
|
|
3300
|
+
//#region src/ignored.d.ts
|
|
3301
|
+
/** Directory holding this plugin's own state (imported snapshots, ignore list). */
|
|
3302
|
+
export declare const PLUGIN_DATA_DIR_NAME = ".xdpool";
|
|
3303
|
+
/** File holding the permanent ignore list, inside {@link pluginDataDir}. */
|
|
3304
|
+
export declare const IGNORED_FILE_NAME = "ignored.json";
|
|
3305
|
+
/** One ignored account, as stored on disk and shown on the card. */
|
|
3306
|
+
type IgnoredAccount = PoolWebIgnoredAccount;
|
|
3307
|
+
/**
|
|
3308
|
+
* The DSH home directory, honouring the same override the host uses.
|
|
3309
|
+
*
|
|
3310
|
+
* Shared by the CLI and the host so both halves resolve the same file: an
|
|
3311
|
+
* `ignore` written from the terminal has to be visible to the running plugin,
|
|
3312
|
+
* which is only true if they agree on where "home" is.
|
|
3313
|
+
*/
|
|
3314
|
+
export declare function dshHome(env?: NodeJS.ProcessEnv): string;
|
|
3315
|
+
/** This plugin's own state directory. */
|
|
3316
|
+
export declare function pluginDataDir(env?: NodeJS.ProcessEnv): string;
|
|
3317
|
+
/** Absolute path of the ignore list. */
|
|
3318
|
+
export declare function ignoredIdsPath(env?: NodeJS.ProcessEnv): string;
|
|
3319
|
+
/**
|
|
3320
|
+
* Read the ignore list, tolerating every "no list yet" shape.
|
|
3321
|
+
*
|
|
3322
|
+
* A missing file, unreadable file, or invalid JSON all mean the same thing to
|
|
3323
|
+
* the caller — nothing is ignored — so none of them throws. The pool must be
|
|
3324
|
+
* able to start on a machine that has never ignored anything.
|
|
3325
|
+
*/
|
|
3326
|
+
export declare function readIgnoredAccounts(path?: string): Promise<IgnoredAccount[]>;
|
|
3327
|
+
/**
|
|
3328
|
+
* Synchronous read, for startup.
|
|
3329
|
+
*
|
|
3330
|
+
* The host applies the ignore list from inside `apply()`, which is synchronous,
|
|
3331
|
+
* and doing it there removes a startup race: an async load could resolve AFTER
|
|
3332
|
+
* the first account scan, which would let an ignored account slip into the pool
|
|
3333
|
+
* once per boot. The file is a few hundred bytes, so a blocking read at startup
|
|
3334
|
+
* costs nothing measurable.
|
|
3335
|
+
*/
|
|
3336
|
+
export declare function readIgnoredAccountsSync(path?: string): IgnoredAccount[];
|
|
3337
|
+
/**
|
|
3338
|
+
* Replace the ignore list, atomically.
|
|
3339
|
+
*
|
|
3340
|
+
* Written to a sibling temp file and renamed over the target so a crash (or a
|
|
3341
|
+
* concurrent reader) can never observe a half-written document — the ignore
|
|
3342
|
+
* list is the only thing standing between a dead account and the rotation, and
|
|
3343
|
+
* a truncated file reads as "nothing is ignored", which would quietly put every
|
|
3344
|
+
* discarded account back in the pool.
|
|
3345
|
+
*/
|
|
3346
|
+
export declare function writeIgnoredAccounts(accounts: readonly IgnoredAccount[], path?: string): Promise<void>;
|
|
3347
|
+
/**
|
|
3348
|
+
* Add one account to the ignore list, preserving the rest.
|
|
3349
|
+
*
|
|
3350
|
+
* A read-modify-write rather than a wholesale replace: the card and the CLI can
|
|
3351
|
+
* both be open, and each request names exactly one account, so re-writing the
|
|
3352
|
+
* whole list from a stale view would drop the other side's edits.
|
|
3353
|
+
*/
|
|
3354
|
+
export declare function ignoreAccount(account: {
|
|
3355
|
+
id: string;
|
|
3356
|
+
label?: string;
|
|
3357
|
+
}, path?: string): Promise<IgnoredAccount[]>;
|
|
3358
|
+
/** Drop one account from the ignore list. Returns the resulting list. */
|
|
3359
|
+
export declare function unignoreAccount(accountId: string, path?: string): Promise<IgnoredAccount[]>;
|
|
3360
|
+
//#endregion
|
|
3361
|
+
//#region src/preferred.d.ts
|
|
3362
|
+
/** Absolute path of the preferred-account file. */
|
|
3363
|
+
export declare function preferredIdPath(env?: NodeJS.ProcessEnv): string;
|
|
3364
|
+
/**
|
|
3365
|
+
* Read the pinned id, tolerating every "no pin yet" shape.
|
|
3366
|
+
*
|
|
3367
|
+
* A missing file is the normal state on a fresh install, so it is not an error.
|
|
3368
|
+
*/
|
|
3369
|
+
export declare function readPreferredId(path?: string): Promise<string | undefined>;
|
|
3370
|
+
/**
|
|
3371
|
+
* Synchronous read, for startup.
|
|
3372
|
+
*
|
|
3373
|
+
* The host pins the account from inside `apply()`, which is synchronous, and
|
|
3374
|
+
* doing it there removes a startup race: an async load could resolve AFTER the
|
|
3375
|
+
* first account scan, so the first request of the process would be served by
|
|
3376
|
+
* whichever account the strategy picked rather than the one the user asked for.
|
|
3377
|
+
* The file is tens of bytes, so a blocking read at startup costs nothing
|
|
3378
|
+
* measurable.
|
|
3379
|
+
*/
|
|
3380
|
+
export declare function readPreferredIdSync(path?: string): string | undefined;
|
|
3381
|
+
/**
|
|
3382
|
+
* Write (or clear) the pinned id, atomically.
|
|
3383
|
+
*
|
|
3384
|
+
* Written to a sibling temp file and renamed over the target so a crash or a
|
|
3385
|
+
* concurrent reader can never observe a half-written document. A truncated file
|
|
3386
|
+
* reads as "nothing is pinned", which is survivable — the strategy takes over —
|
|
3387
|
+
* but a half-written file that happens to parse could pin an account nobody
|
|
3388
|
+
* chose, and that is the failure worth designing out.
|
|
3389
|
+
*
|
|
3390
|
+
* `accountId: undefined` DELETES the file rather than writing `{"accountId":
|
|
3391
|
+
* null}`. Deleting makes "no pin" the same state as "never pinned", so there is
|
|
3392
|
+
* exactly one way for the next read to come back empty, and the directory stays
|
|
3393
|
+
* free of documents that describe the absence of a decision.
|
|
3394
|
+
*/
|
|
3395
|
+
export declare function writePreferredId(accountId: string | undefined, path?: string): Promise<void>;
|
|
3396
|
+
//#endregion
|
|
3397
|
+
//#region src/relay.d.ts
|
|
3398
|
+
interface PoolRelayStatus {
|
|
3399
|
+
enabled: boolean;
|
|
3400
|
+
/** Whether a listener is actually bound right now. */
|
|
3401
|
+
listening: boolean;
|
|
3402
|
+
host: string;
|
|
3403
|
+
port: number;
|
|
3404
|
+
/** Where a client should point; '' until something is listening. */
|
|
3405
|
+
endpoint: string;
|
|
3406
|
+
/**
|
|
3407
|
+
* The relay key in full.
|
|
3408
|
+
*
|
|
3409
|
+
* Returned on purpose: the card has to show it once so the user can copy it
|
|
3410
|
+
* into the other device. This is the key WE minted for LAN clients, not the
|
|
3411
|
+
* shim secret, and it grants nothing beyond reaching the pool.
|
|
3412
|
+
*/
|
|
3413
|
+
key: string;
|
|
3414
|
+
/** Why nothing is bound; '' when there is nothing to explain. */
|
|
3415
|
+
error: string;
|
|
3416
|
+
}
|
|
3417
|
+
//#endregion
|
|
3418
|
+
//#region src/web-status.d.ts
|
|
3419
|
+
/** Constructor dependencies — a narrow slice of the pool runtime. */
|
|
3420
|
+
interface PoolStatusRouteOptions {
|
|
3421
|
+
pool: WorkBuddyAccountPool;
|
|
3422
|
+
/** One catalog per region; the card reads the one for its active tab. */
|
|
3423
|
+
catalogs: Readonly<Record<PoolRegion, WorkBuddyCatalog>>;
|
|
3424
|
+
client: WorkBuddyUpstreamClient;
|
|
3425
|
+
/** Lazily resolve the running loopback shim, when it has bound a port. */
|
|
3426
|
+
shim?: () => {
|
|
3427
|
+
running: boolean;
|
|
3428
|
+
baseUrl?: string;
|
|
3429
|
+
};
|
|
3430
|
+
/**
|
|
3431
|
+
* The daily-points automation, when the host half has one.
|
|
3432
|
+
*
|
|
3433
|
+
* Optional so these routes still mount on a profile that assembled a pool
|
|
3434
|
+
* without a scheduler (the CLI and the tests do exactly that); the card then
|
|
3435
|
+
* reports the automation as off instead of showing a broken panel.
|
|
3436
|
+
*/
|
|
3437
|
+
scheduler?: () => AutomationStatus;
|
|
3438
|
+
/**
|
|
3439
|
+
* Start a manual pass, for the card's "run now" button.
|
|
3440
|
+
*
|
|
3441
|
+
* Returns whether a run STARTED, not its result: a pass takes tens of
|
|
3442
|
+
* seconds, so it runs in the background and the card polls the scheduler
|
|
3443
|
+
* status for progress and earnings.
|
|
3444
|
+
*/
|
|
3445
|
+
runAutomation?: (job: string, force: boolean) => boolean;
|
|
3446
|
+
/**
|
|
3447
|
+
* Persist one account's reserved-credit floor. Same settings document as every
|
|
3448
|
+
* other card write, so it survives a restart and is re-applied after a scan.
|
|
3449
|
+
* Absent without a settings service: the route then answers 503.
|
|
3450
|
+
*/
|
|
3451
|
+
setCreditReserve?: (accountId: string, reserve: number) => Promise<void> | void;
|
|
3452
|
+
/**
|
|
3453
|
+
* Persist the user's model selection. Provided by the host half, which owns
|
|
3454
|
+
* the settings section; absent when the plugin runs without a settings
|
|
3455
|
+
* service (the save route then reports 503 rather than pretending to work).
|
|
3456
|
+
*/
|
|
3457
|
+
/**
|
|
3458
|
+
* Persist one region's selection. The region travels with the payload: the two
|
|
3459
|
+
* gateways advertise different rosters, so the domestic tab and the
|
|
3460
|
+
* international tab each own their list and must never overwrite each other.
|
|
3461
|
+
*/
|
|
3462
|
+
saveSelection?: (region: PoolRegion, selection: PoolWebModelSelection) => Promise<void> | void;
|
|
3463
|
+
/**
|
|
3464
|
+
* Persist one account switch. Same settings document as every other card
|
|
3465
|
+
* write, so it survives a restart and is re-applied after each re-scan.
|
|
3466
|
+
* Absent without a settings service: the route then answers 503.
|
|
3467
|
+
*/
|
|
3468
|
+
setAccountDisabled?: (accountId: string, disabled: boolean) => Promise<void> | void;
|
|
3469
|
+
/**
|
|
3470
|
+
* Throw one account out of the pool for good, or take it back.
|
|
3471
|
+
*
|
|
3472
|
+
* Backed by the plugin's own ignore file rather than the settings document,
|
|
3473
|
+
* because the CLI writes the same list and has no settings service. Absent
|
|
3474
|
+
* when the host did not wire it: the route then answers 503.
|
|
3475
|
+
*/
|
|
3476
|
+
setAccountIgnored?: (accountId: string, ignored: boolean) => Promise<void> | void;
|
|
3477
|
+
/**
|
|
3478
|
+
* Name the account to try first, or clear the pin with a null id.
|
|
3479
|
+
*
|
|
3480
|
+
* Backed by the plugin's own preferred-account file rather than the settings
|
|
3481
|
+
* document, for the same reason as {@link setAccountIgnored}: the CLI's
|
|
3482
|
+
* `prefer` command writes the same file and has no settings service, so a
|
|
3483
|
+
* settings-backed pin would only ever be writable from the card.
|
|
3484
|
+
*
|
|
3485
|
+
* Called by the handler and required to have taken effect in the pool before
|
|
3486
|
+
* it resolves: the card re-polls straight after, and a pin that is persisted
|
|
3487
|
+
* but not yet visible in the pool would make the next request ignore the
|
|
3488
|
+
* choice the user just made. Absent when the host did not wire it: the route
|
|
3489
|
+
* then answers 503.
|
|
3490
|
+
*/
|
|
3491
|
+
setPreferredAccount?: (accountId: string | null) => Promise<void> | void;
|
|
3492
|
+
/**
|
|
3493
|
+
* The accounts currently ignored, for the card's "ignored" list.
|
|
3494
|
+
*
|
|
3495
|
+
* A thunk rather than a snapshot so the document always reflects the file on
|
|
3496
|
+
* disk, including edits made by the CLI while the card is open.
|
|
3497
|
+
*/
|
|
3498
|
+
ignoredAccounts?: () => readonly PoolWebIgnoredAccount[];
|
|
3499
|
+
/**
|
|
3500
|
+
* Re-fetch the model catalog for BOTH regions and report what landed.
|
|
3501
|
+
*
|
|
3502
|
+
* Absent when the host did not wire it: the route then answers 503 rather
|
|
3503
|
+
* than pretending the refresh happened.
|
|
3504
|
+
*/
|
|
3505
|
+
refreshCatalog?: () => Promise<{
|
|
3506
|
+
regions: Readonly<Record<PoolRegion, {
|
|
3507
|
+
source: PoolWebCatalogSource;
|
|
3508
|
+
models: number;
|
|
3509
|
+
error?: string;
|
|
3510
|
+
}>>;
|
|
3511
|
+
}>;
|
|
3512
|
+
/**
|
|
3513
|
+
* The usage panel's aggregate, produced by whatever served the calls.
|
|
3514
|
+
*
|
|
3515
|
+
* A thunk rather than a snapshot so the panel always sees the newest calls,
|
|
3516
|
+
* and optional because a profile assembled without a recorder still serves
|
|
3517
|
+
* chat normally - it simply has no usage to show, and the route reports that
|
|
3518
|
+
* instead of pretending the panel is broken.
|
|
3519
|
+
*/
|
|
3520
|
+
usage?: () => UsageSummary;
|
|
3521
|
+
/**
|
|
3522
|
+
* The aggregate lane's stored settings, for the read route.
|
|
3523
|
+
*
|
|
3524
|
+
* A thunk rather than a snapshot so the document always reflects the file on
|
|
3525
|
+
* disk, including a CLI edit made while the card is open — the same contract
|
|
3526
|
+
* `ignoredAccounts` documents for the ignore list.
|
|
3527
|
+
*/
|
|
3528
|
+
aggregateStore?: () => AggregateStore;
|
|
3529
|
+
/**
|
|
3530
|
+
* The whole aggregate document, built host-side.
|
|
3531
|
+
*
|
|
3532
|
+
* One dep instead of `aggregateStore` plus a model count because the two must
|
|
3533
|
+
* be read together: the store says which providers are configured, and the
|
|
3534
|
+
* running catalog says which rows that actually produces. Two separate reads
|
|
3535
|
+
* could straddle a save and show a provider as configured with no rows.
|
|
3536
|
+
*/
|
|
3537
|
+
aggregateDocument?: () => Promise<PoolWebAggregateStatus> | PoolWebAggregateStatus;
|
|
3538
|
+
/**
|
|
3539
|
+
* Save one aggregate patch and return the document the card should re-read.
|
|
3540
|
+
*
|
|
3541
|
+
* The HOST half owns the write because it is the only side that also knows how
|
|
3542
|
+
* to refresh the running catalog: a key saved on the card has to change the
|
|
3543
|
+
* model picker without a DSH restart, which is the whole point of the card.
|
|
3544
|
+
*/
|
|
3545
|
+
saveAggregate?: (patch: PoolWebAggregatePatch) => Promise<PoolWebAggregateStatus>;
|
|
3546
|
+
/**
|
|
3547
|
+
* Verify one provider's key. Never throws: the answer is what the card shows.
|
|
3548
|
+
*/
|
|
3549
|
+
probeAggregate?: (providerId: string) => Promise<PoolWebAggregateProbe>;
|
|
3550
|
+
/**
|
|
3551
|
+
* Send one real streaming call and report whether the model answered.
|
|
3552
|
+
*
|
|
3553
|
+
* Never throws: the measurement is the answer, and a probe that rejected
|
|
3554
|
+
* would leave the button spinning forever instead of telling the user what
|
|
3555
|
+
* went wrong.
|
|
3556
|
+
*
|
|
3557
|
+
* The HOST half owns this because only the host can reach `ctx.llm.stream`,
|
|
3558
|
+
* which is the one surface that resolves a provider/model pair through the
|
|
3559
|
+
* real adapter registry — the plugin's own lanes do not know which credential
|
|
3560
|
+
* a given provider id ends up using.
|
|
3561
|
+
*/
|
|
3562
|
+
probeModel?: (provider: string, model: string, signal?: AbortSignal) => Promise<PoolWebModelProbeResult>;
|
|
3563
|
+
/**
|
|
3564
|
+
* What models each login channel contributes, as the host's catalogue spells it.
|
|
3565
|
+
*
|
|
3566
|
+
* Optional because a build without a login-channel pack genuinely cannot answer
|
|
3567
|
+
* — the route reports that rather than pretending every channel is empty.
|
|
3568
|
+
* Providers the host cannot answer for are omitted from the returned record;
|
|
3569
|
+
* see {@link PoolWebChannelModels} for why that distinction is load-bearing.
|
|
3570
|
+
*/
|
|
3571
|
+
channelModels?: (providers: readonly string[]) => Promise<Readonly<Record<string, readonly PoolWebChannelModel[]>>>;
|
|
3572
|
+
/**
|
|
3573
|
+
* Fetch one provider's live model list, write it back as its discovered set,
|
|
3574
|
+
* and refresh the catalog.
|
|
3575
|
+
*/
|
|
3576
|
+
discoverAggregate?: (providerId: string) => Promise<PoolWebAggregateProbe>;
|
|
3577
|
+
/**
|
|
3578
|
+
* The login-channel list, built host-side.
|
|
3579
|
+
*
|
|
3580
|
+
* `state` is the load-bearing part: a card that silently rendered an empty
|
|
3581
|
+
* channel list would look exactly like "this host has no channels", which is
|
|
3582
|
+
* the one conclusion the user cannot verify for themselves.
|
|
3583
|
+
*/
|
|
3584
|
+
channelsDocument?: () => PoolWebChannelsStatus;
|
|
3585
|
+
/**
|
|
3586
|
+
* The chains plus the roster their members are drawn from, built host-side.
|
|
3587
|
+
*
|
|
3588
|
+
* One dep because the two halves are the same question asked twice — "what can
|
|
3589
|
+
* a chain name, and does what it names still exist" — and answering them from
|
|
3590
|
+
* two snapshots could show a member that the chains no longer resolve.
|
|
3591
|
+
*/
|
|
3592
|
+
combosDocument?: () => PoolWebCombosStatus;
|
|
3593
|
+
/**
|
|
3594
|
+
* Replace the chain table and refresh the running catalog, then re-answer the
|
|
3595
|
+
* document. The catalog refresh is what makes a new chain pickable at once.
|
|
3596
|
+
*/
|
|
3597
|
+
saveCombos?: (raw: unknown) => Promise<PoolWebCombosStatus>;
|
|
3598
|
+
/**
|
|
3599
|
+
* What the locally running billion-context proxy is doing, read-only.
|
|
3600
|
+
*
|
|
3601
|
+
* Absent when the module could not be wired: the route then answers 503
|
|
3602
|
+
* rather than pretending a third-party proxy reported nothing. It never
|
|
3603
|
+
* rejects — a missing proxy is a normal state with its own `reason`, because
|
|
3604
|
+
* billion-context is a separate optional plugin.
|
|
3605
|
+
*/
|
|
3606
|
+
billionDocument?: () => Promise<PoolWebBillionStatus>;
|
|
3607
|
+
/**
|
|
3608
|
+
* The relay's current state, or undefined when this build has no relay.
|
|
3609
|
+
*
|
|
3610
|
+
* Reading the key back out of here is deliberate — the card has to show it so
|
|
3611
|
+
* the user can paste it into another device — but it is the RELAY key, which
|
|
3612
|
+
* the plugin minted for LAN clients. The shim's own per-process secret is never
|
|
3613
|
+
* exposed by any route.
|
|
3614
|
+
*/
|
|
3615
|
+
relayDocument?: () => Promise<PoolRelayStatus>;
|
|
3616
|
+
/** Apply a relay patch, re-evaluate the listener, and answer with the new state. */
|
|
3617
|
+
saveRelay?: (patch: PoolWebRelayPatch) => Promise<PoolRelayStatus>;
|
|
3618
|
+
}
|
|
3619
|
+
/**
|
|
3620
|
+
* Assemble the card's status document. Per-account credits and check-in state
|
|
3621
|
+
* are queried live; a failing query degrades to `creditsError` / `checkinError`
|
|
3622
|
+
* rather than failing the whole document. Never throws.
|
|
3623
|
+
*/
|
|
3624
|
+
export declare function poolWebStatus(deps: PoolStatusRouteOptions, region?: PoolRegion): Promise<PoolWebStatus>;
|
|
3625
|
+
/**
|
|
3626
|
+
* Mount the read-only routes on a context where `webServer` is available. The
|
|
3627
|
+
* caller uses `ctx.inject(['webServer'], ...)` so Desktop startup order cannot
|
|
3628
|
+
* make this registration disappear.
|
|
3629
|
+
*/
|
|
3630
|
+
export declare function registerPoolStatusRoute(ctx: Context$1, deps: PoolStatusRouteOptions): void;
|
|
3631
|
+
//#endregion
|
|
3632
|
+
//#region src/index.d.ts
|
|
3633
|
+
/** Stable Cordis plugin name. */
|
|
3634
|
+
export declare const name = "llm-xdpool";
|
|
3635
|
+
/** The model registry required before the provider can register. */
|
|
3636
|
+
export declare const inject: string[];
|
|
3637
|
+
/**
|
|
3638
|
+
* Settings namespace for the XD Pool card. Registering a section here
|
|
3639
|
+
* is what makes the provider appear on the Models settings page and causes the
|
|
3640
|
+
* Host to mount the plugin's client card under Plugin configuration — exactly
|
|
3641
|
+
* the mechanism the single-account connector uses.
|
|
3642
|
+
*/
|
|
3643
|
+
export declare const XDPOOL_SETTINGS_NS: SettingsNamespace;
|
|
3644
|
+
/** Plugin configuration. */
|
|
3645
|
+
export interface Config {
|
|
3646
|
+
/** Explicit WorkBuddy desktop auth-file path override. */
|
|
3647
|
+
authFile?: string;
|
|
3648
|
+
/** Rate-limit cooldown per account, milliseconds. */
|
|
3649
|
+
cooldownMs?: number;
|
|
3650
|
+
/**
|
|
3651
|
+
* How the pool spreads requests across accounts.
|
|
3652
|
+
*
|
|
3653
|
+
* - `priority` (default) drains one account before moving to the next, which
|
|
3654
|
+
* is what a pool of your own accounts is for.
|
|
3655
|
+
* - `round-robin` walks the pool in order, so the spend splits evenly.
|
|
3656
|
+
* - `balanced` draws at random, weighting whichever account has been idle
|
|
3657
|
+
* longest. Spend still spreads, but without a fixed order, so one unhealthy
|
|
3658
|
+
* account cannot pin the pool to itself.
|
|
3659
|
+
* - `sticky` gives each conversation one account and moves the next new
|
|
3660
|
+
* conversation to the next account in order. Rotating accounts inside a
|
|
3661
|
+
* conversation throws away the upstream prompt cache (it is per tenant), so
|
|
3662
|
+
* this keeps the cache warm while still spreading spend across
|
|
3663
|
+
* conversations.
|
|
3664
|
+
* - `expiry` picks the account whose one-off credit packs expire soonest, so
|
|
3665
|
+
* use-it-or-lose-it credits are spent before they die. Accounts with no
|
|
3666
|
+
* known expiry sort last, so an unprobed pool degrades to `priority`.
|
|
3667
|
+
*
|
|
3668
|
+
* Absent reads as `priority`.
|
|
3669
|
+
*/
|
|
3670
|
+
distribution?: 'priority' | 'round-robin' | 'balanced' | 'sticky' | 'expiry';
|
|
3671
|
+
/**
|
|
3672
|
+
* Account ids switched off on the card. A disabled account is never picked
|
|
3673
|
+
* to serve a request, but it stays in the pool and on the card so it can be
|
|
3674
|
+
* switched back on. Ids are the pool's stable per-credential keys, which
|
|
3675
|
+
* survive re-scans (see WorkBuddyAccountPool.disabledIds).
|
|
3676
|
+
*/
|
|
3677
|
+
disabledAccountIds?: string[];
|
|
3678
|
+
/**
|
|
3679
|
+
* Per-account credit floor, keyed by account id. The pool stops picking an
|
|
3680
|
+
* account once its last known balance reaches this value, so the reserved
|
|
3681
|
+
* credits survive. Absent or 0 spends the account down as before.
|
|
3682
|
+
*/
|
|
3683
|
+
creditReserves?: Record<string, number>;
|
|
3684
|
+
/**
|
|
3685
|
+
* Model ids enabled in the picker. Absent means "every model the catalog
|
|
3686
|
+
* advertises" — an unconfigured install should never present an empty model
|
|
3687
|
+
* list just because the key is missing.
|
|
3688
|
+
*/
|
|
3689
|
+
enabledModelIds?: string[];
|
|
3690
|
+
/**
|
|
3691
|
+
* Model ids that additionally accept image input. Absent means "follow the
|
|
3692
|
+
* upstream capability flag"; an explicit list is authoritative for the models
|
|
3693
|
+
* it mentions and leaves the rest to the catalog.
|
|
3694
|
+
*/
|
|
3695
|
+
imageModelIds?: string[];
|
|
3696
|
+
/**
|
|
3697
|
+
* Per-model context-window override, keyed by model id. The upstream can
|
|
3698
|
+
* advertise more than DSH wants to hand a single turn, so the card lets the
|
|
3699
|
+
* user cap a model without touching the catalog.
|
|
3700
|
+
*/
|
|
3701
|
+
contextBudgets?: Record<string, number>;
|
|
3702
|
+
/**
|
|
3703
|
+
* Per-region model selection. The two gateways advertise different rosters, so
|
|
3704
|
+
* one shared list would let a save on one tab silently rewrite the other tab's
|
|
3705
|
+
* picker. Each region owns its own copy; a region with no entry falls back to
|
|
3706
|
+
* the legacy flat keys above, so an upgrade keeps the list already in use.
|
|
3707
|
+
*/
|
|
3708
|
+
modelSelectionCn?: ModelSelectionConfig;
|
|
3709
|
+
modelSelectionGlobal?: ModelSelectionConfig;
|
|
3710
|
+
/**
|
|
3711
|
+
* Daily-points automation. Absent means off: the scheduler makes upstream
|
|
3712
|
+
* calls on the user behalf, so it stays opt-in rather than surprising a
|
|
3713
|
+
* fresh install with background traffic.
|
|
3714
|
+
*/
|
|
3715
|
+
automation?: AutomationConfig;
|
|
3716
|
+
/**
|
|
3717
|
+
* The automation's daily earnings ledger, written by the scheduler itself.
|
|
3718
|
+
*
|
|
3719
|
+
* It lives in settings rather than only in memory so a host restart mid-day
|
|
3720
|
+
* does not wipe what the automation already earned.
|
|
3721
|
+
*/
|
|
3722
|
+
automationEarnings?: AutomationLedger;
|
|
3723
|
+
}
|
|
3724
|
+
/** One region's saved model selection. */
|
|
3725
|
+
export interface ModelSelectionConfig {
|
|
3726
|
+
enabledModelIds?: string[];
|
|
3727
|
+
imageModelIds?: string[];
|
|
3728
|
+
contextBudgets?: Record<string, number>;
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* Daily-points automation.
|
|
3732
|
+
*
|
|
3733
|
+
* Absent means off: the scheduler makes upstream calls on the user behalf, so
|
|
3734
|
+
* it stays opt-in rather than surprising a fresh install with background
|
|
3735
|
+
* traffic. Each job carries its own hour list so the passes can be spread out
|
|
3736
|
+
* (or pushed off-peak) without disabling any of them.
|
|
3737
|
+
*
|
|
3738
|
+
* Ordering note: the report job must run before the task job. A report is what
|
|
3739
|
+
* lights the growth streak and unlocks the `first_buddy` family, so a task pass
|
|
3740
|
+
* that ran first would read counters before they could have moved.
|
|
3741
|
+
*/
|
|
3742
|
+
export interface AutomationConfig {
|
|
3743
|
+
/** Master switch for every automation job. Absent reads as false. */
|
|
3744
|
+
enabled?: boolean;
|
|
3745
|
+
/** Hours (local, 0-23) at which the daily check-in runs. */
|
|
3746
|
+
checkinHours?: number[];
|
|
3747
|
+
/** Hours at which the activity report runs. Keep ahead of `taskHours`. */
|
|
3748
|
+
reportHours?: number[];
|
|
3749
|
+
/** Hours at which tasks are enrolled in and claimed. */
|
|
3750
|
+
taskHours?: number[];
|
|
3751
|
+
/** Hours at which streak redemption runs. */
|
|
3752
|
+
streakHours?: number[];
|
|
3753
|
+
/** Hours at which the buddy travel loop runs. */
|
|
3754
|
+
travelHours?: number[];
|
|
3755
|
+
/** How long an account rests after its credits run out, in milliseconds. */
|
|
3756
|
+
exhaustCooldownMs?: number;
|
|
3757
|
+
}
|
|
3758
|
+
/** Upper bound the card offers as the "default" context window, in tokens. */
|
|
3759
|
+
export declare const DEFAULT_CONTEXT_BUDGET = 200000;
|
|
3760
|
+
/**
|
|
3761
|
+
* Fold a saved automation block into scheduler options.
|
|
3762
|
+
*
|
|
3763
|
+
* Absent means off, stated once here so every caller agrees: the card writes
|
|
3764
|
+
* `enabled` as a real boolean, and a config that never touched the section must
|
|
3765
|
+
* not accidentally arm background upstream traffic.
|
|
3766
|
+
*/
|
|
3767
|
+
export declare function automationOptions(automation: AutomationConfig | undefined): AutomationOptions;
|
|
3768
|
+
/** Settings key holding one region's saved selection. */
|
|
3769
|
+
export declare const modelSelectionKeyFor: (region: 'cn' | 'global') => string;
|
|
3770
|
+
/**
|
|
3771
|
+
* Plugin configuration schema.
|
|
3772
|
+
*
|
|
3773
|
+
* Mirrors the shape the settings section stores. Every field carries a default
|
|
3774
|
+
* so a config that never touched the card still folds cleanly: a field whose
|
|
3775
|
+
* schema declares no default is read as absent by the settings fold. That is
|
|
3776
|
+
* also why `contextBudgets` is a real dictionary (`z.dict`) - an open object
|
|
3777
|
+
* schema reads as "an object with no fields" and the fold then throws while
|
|
3778
|
+
* the provider row is rendered.
|
|
3779
|
+
*
|
|
3780
|
+
* Every field is wrapped in {@link asVolatile}: on the 0.1.7 line the settings
|
|
3781
|
+
* write gate refuses an entry whose schema declares no volatile field at all
|
|
3782
|
+
* ("Plugin entry ... has no volatile fields") and `describe()` skips such an
|
|
3783
|
+
* entry — so an unmarked schema means the card can neither render nor save. On
|
|
3784
|
+
* the 0.1.5 line the wrapper degrades to an identity no-op (see its JSDoc), and
|
|
3785
|
+
* the value the running instance reads is a plain value either way once
|
|
3786
|
+
* unwrapped.
|
|
3787
|
+
*/
|
|
3788
|
+
export declare const Config: z<Config>;
|
|
3789
|
+
/** Everything the CLI needs from a live plugin instance. */
|
|
3790
|
+
export interface WorkBuddyPoolApi {
|
|
3791
|
+
pool: WorkBuddyAccountPool;
|
|
3792
|
+
/** One catalog per region, matching the two registered providers. */
|
|
3793
|
+
catalogs: Readonly<Record<WorkBuddyRegion, WorkBuddyCatalog>>;
|
|
3794
|
+
client: WorkBuddyUpstreamClient;
|
|
3795
|
+
shim: WorkBuddyShim;
|
|
3796
|
+
adapter: WorkBuddyAdapter | undefined;
|
|
3797
|
+
rescan(): Promise<number>;
|
|
3798
|
+
status(includeCredits?: boolean): Promise<Awaited<ReturnType<typeof buildStatus>>>;
|
|
3799
|
+
resetCooldowns(): void;
|
|
3800
|
+
/** Daily-points automation; assembled with the core, inert until started. */
|
|
3801
|
+
scheduler: WorkBuddyScheduler;
|
|
3802
|
+
}
|
|
3803
|
+
/** The live API, or undefined when the plugin has not applied yet. */
|
|
3804
|
+
export declare function currentApi(): WorkBuddyPoolApi | undefined;
|
|
3805
|
+
/** Test seam: install an API instance without booting cordis. */
|
|
3806
|
+
export declare function setApi(next: WorkBuddyPoolApi | undefined): void;
|
|
3807
|
+
/** Assemble the runtime objects without registering anything. */
|
|
3808
|
+
/**
|
|
3809
|
+
* Assemble the runtime objects without registering anything.
|
|
3810
|
+
*
|
|
3811
|
+
* One catalog per region, mirroring the two shims: the CN and global gateways
|
|
3812
|
+
* do not advertise the same roster, and a shared catalog meant the picker showed
|
|
3813
|
+
* whichever list happened to be fetched first (always the CN one, since the
|
|
3814
|
+
* seeding step read `accounts[0]`).
|
|
3815
|
+
*/
|
|
3816
|
+
export declare function createCore(logger?: {
|
|
3817
|
+
warn(...args: unknown[]): void;
|
|
3818
|
+
info?(...args: unknown[]): void;
|
|
3819
|
+
}): {
|
|
3820
|
+
pool: WorkBuddyAccountPool;
|
|
3821
|
+
catalogs: {
|
|
3822
|
+
readonly cn: WorkBuddyCatalog;
|
|
3823
|
+
readonly global: WorkBuddyCatalog;
|
|
3824
|
+
};
|
|
3825
|
+
client: WorkBuddyUpstreamClient;
|
|
3826
|
+
scheduler: WorkBuddyScheduler;
|
|
3827
|
+
};
|
|
3828
|
+
/**
|
|
3829
|
+
* Start the loopback endpoint, register the `xdpool` provider, and
|
|
3830
|
+
* discover accounts. The provider registers only after `shim.ready` resolves,
|
|
3831
|
+
* because its models read the shim origin at construction time.
|
|
3832
|
+
*/
|
|
3833
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
|
3834
|
+
//#endregion
|
|
3835
|
+
export type { AccountStatus, AutomationLedger, AutomationRunSummary, AutomationStatus, Context, ExpertUseMode, IgnoredAccount, MarketExpert, ModelSelection, PoolStatusRouteOptions, PoolWebAccountIgnore, PoolWebCheckin, PoolWebCheckinClaim, PoolWebIgnoredAccount, PoolWebModel, PoolWebModelSelection, PoolWebStatus, SchedulerLogger, TaskEventChain, TaskEventTransport, UpstreamErrorKind, WorkBuddyAccount, WorkBuddyAdapter, WorkBuddyCredential, WorkBuddyModelInfo, WorkBuddyShim, WorkBuddyStatus };
|