dsh-plugin-subscription-oauth 0.13.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/auth/provider-controller.d.ts +7 -0
- package/lib/auth/provider-controller.js +1 -0
- package/lib/auth/registry.d.ts +40 -0
- package/lib/auth/registry.js +14 -0
- package/lib/auth/rpc.js +12 -0
- package/lib/auth/workbuddy-flow.d.ts +45 -1
- package/lib/auth/workbuddy-flow.js +222 -2
- package/lib/client/SubscriptionsSection.js +71 -6
- package/lib/client/locales.d.ts +20 -0
- package/lib/client/locales.js +20 -0
- package/lib/client.js +149 -27
- package/lib/client.js.map +1 -1
- package/lib/protocol.d.ts +50 -1
- package/lib/protocol.js +2 -0
- package/lib/providers/workbuddy-credits.d.ts +23 -0
- package/lib/providers/workbuddy-credits.js +52 -0
- package/lib/providers/workbuddy-provider.js +70 -3
- package/lib/providers/workbuddy.d.ts +4 -0
- package/lib/providers/workbuddy.js +14 -0
- package/package.json +1 -1
package/lib/protocol.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
/** Prefix of every RPC method this plugin serves. */
|
|
13
13
|
export declare const RPC_PREFIX = "subscription-oauth.";
|
|
14
14
|
/** Endpoints the Settings page may call. */
|
|
15
|
-
export declare const RPC_ENDPOINTS: readonly ["status", "login", "cancel", "logout", "setDefault", "refresh", "discover", "setModelVisible", "setModelOrder", "addModel", "removeModel", "usage", "reload", "auto-state", "setAutoOn", "deepseek-balance", "ccswitch-balance", "model-variants"];
|
|
15
|
+
export declare const RPC_ENDPOINTS: readonly ["status", "login", "cancel", "logout", "setDefault", "refresh", "discover", "setModelVisible", "setModelOrder", "addModel", "removeModel", "usage", "reload", "auto-state", "setAutoOn", "checkin", "tasks", "deepseek-balance", "ccswitch-balance", "model-variants"];
|
|
16
16
|
/** One endpoint name. */
|
|
17
17
|
export type RpcEndpoint = typeof RPC_ENDPOINTS[number];
|
|
18
18
|
/** Client → host envelope. */
|
|
@@ -146,6 +146,13 @@ export interface ProviderUsage {
|
|
|
146
146
|
renewsAt?: number;
|
|
147
147
|
/** Metered allowances, most relevant first. */
|
|
148
148
|
windows: readonly UsageWindow[];
|
|
149
|
+
/** Daily check-in state, when the provider states one. */
|
|
150
|
+
checkin?: {
|
|
151
|
+
active: boolean;
|
|
152
|
+
todayCheckedIn: boolean;
|
|
153
|
+
streakDays: number;
|
|
154
|
+
todayCredit: number;
|
|
155
|
+
};
|
|
149
156
|
/** Epoch milliseconds these figures were read. */
|
|
150
157
|
fetchedAt: number;
|
|
151
158
|
/**
|
|
@@ -323,6 +330,14 @@ export interface RpcMap {
|
|
|
323
330
|
payload: SetAutoOnRequest;
|
|
324
331
|
value: AutoStateResult;
|
|
325
332
|
};
|
|
333
|
+
checkin: {
|
|
334
|
+
payload: CheckinRequest;
|
|
335
|
+
value: CheckinResult;
|
|
336
|
+
};
|
|
337
|
+
tasks: {
|
|
338
|
+
payload: TasksRequest;
|
|
339
|
+
value: TasksResult;
|
|
340
|
+
};
|
|
326
341
|
'deepseek-balance': {
|
|
327
342
|
payload: DeepSeekBalanceRequest;
|
|
328
343
|
value: DeepSeekBalanceResult;
|
|
@@ -398,6 +413,40 @@ export interface SetAutoOnRequest {
|
|
|
398
413
|
export interface AutoStateResult {
|
|
399
414
|
enabled: boolean;
|
|
400
415
|
}
|
|
416
|
+
/** Payload of `checkin`: which account claims today's reward. */
|
|
417
|
+
export interface CheckinRequest {
|
|
418
|
+
provider: string;
|
|
419
|
+
account: string;
|
|
420
|
+
}
|
|
421
|
+
/** Result of `checkin`: the credit granted and the streak after claiming. */
|
|
422
|
+
export interface CheckinResult {
|
|
423
|
+
credit: number;
|
|
424
|
+
streakDays: number;
|
|
425
|
+
already: boolean;
|
|
426
|
+
}
|
|
427
|
+
/** Payload of `tasks`: which account to read and auto-claim. */
|
|
428
|
+
export interface TasksRequest {
|
|
429
|
+
provider: string;
|
|
430
|
+
account: string;
|
|
431
|
+
/** Claim every claimable reward; false only lists. */
|
|
432
|
+
claim?: boolean;
|
|
433
|
+
}
|
|
434
|
+
/** One task's state for the card. */
|
|
435
|
+
export interface WorkBuddyTaskState {
|
|
436
|
+
taskCode: string;
|
|
437
|
+
title: string;
|
|
438
|
+
credit: number;
|
|
439
|
+
target: number;
|
|
440
|
+
current: number;
|
|
441
|
+
state: 'pending' | 'claimable' | 'claimed' | 'locked';
|
|
442
|
+
}
|
|
443
|
+
/** Result of `tasks`: the task list and what auto-claiming earned. */
|
|
444
|
+
export interface TasksResult {
|
|
445
|
+
tasks: WorkBuddyTaskState[];
|
|
446
|
+
claimed: number;
|
|
447
|
+
claimable: number;
|
|
448
|
+
creditEarned: number;
|
|
449
|
+
}
|
|
401
450
|
/** Payload of `deepseek-balance`. */
|
|
402
451
|
export interface DeepSeekBalanceRequest {
|
|
403
452
|
/** Client metadata the host's account service expects. */
|
package/lib/protocol.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-account credit spend tracking for the WorkBuddy route.
|
|
3
|
+
*
|
|
4
|
+
* The gateway states the real billed figure for a turn — `usage.credit` on the last
|
|
5
|
+
* usage frame of the stream — so the spent total is a sum of those, never an estimate
|
|
6
|
+
* from tokens and a rate. Spends accumulate per account in a small JSON file beside the
|
|
7
|
+
* credentials, and fold with the live packages' remaining allowance into one precise
|
|
8
|
+
* spent-against-limit window.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-subscription-oauth/providers/workbuddy-credits
|
|
11
|
+
*/
|
|
12
|
+
/** One account's accumulated spend. */
|
|
13
|
+
interface AccountSpend {
|
|
14
|
+
spent: number;
|
|
15
|
+
calls: number;
|
|
16
|
+
}
|
|
17
|
+
/** The spend ledger, keyed by account id. */
|
|
18
|
+
export type SpendLedger = Record<string, AccountSpend>;
|
|
19
|
+
/** Read the ledger, tolerating a missing or corrupt file. */
|
|
20
|
+
export declare function readSpendLedger(file: string): Promise<SpendLedger>;
|
|
21
|
+
/** Add one turn's billed credit to an account's spend, returning the new totals. */
|
|
22
|
+
export declare function recordSpend(file: string, account: string, credit: number): Promise<AccountSpend>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-account credit spend tracking for the WorkBuddy route.
|
|
3
|
+
*
|
|
4
|
+
* The gateway states the real billed figure for a turn — `usage.credit` on the last
|
|
5
|
+
* usage frame of the stream — so the spent total is a sum of those, never an estimate
|
|
6
|
+
* from tokens and a rate. Spends accumulate per account in a small JSON file beside the
|
|
7
|
+
* credentials, and fold with the live packages' remaining allowance into one precise
|
|
8
|
+
* spent-against-limit window.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-subscription-oauth/providers/workbuddy-credits
|
|
11
|
+
*/
|
|
12
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
/** Read the ledger, tolerating a missing or corrupt file. */
|
|
15
|
+
export async function readSpendLedger(file) {
|
|
16
|
+
try {
|
|
17
|
+
const raw = JSON.parse(await readFile(file, 'utf8'));
|
|
18
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
19
|
+
return {};
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
22
|
+
if (typeof value !== 'object' || value === null)
|
|
23
|
+
continue;
|
|
24
|
+
const record = value;
|
|
25
|
+
const spent = typeof record.spent === 'number' && Number.isFinite(record.spent) ? record.spent : 0;
|
|
26
|
+
const calls = typeof record.calls === 'number' && Number.isFinite(record.calls) ? record.calls : 0;
|
|
27
|
+
out[key] = { spent: Math.max(0, spent), calls: Math.max(0, calls) };
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Add one turn's billed credit to an account's spend, returning the new totals. */
|
|
36
|
+
export async function recordSpend(file, account, credit) {
|
|
37
|
+
const ledger = await readSpendLedger(file);
|
|
38
|
+
const previous = ledger[account] ?? { spent: 0, calls: 0 };
|
|
39
|
+
const next = {
|
|
40
|
+
spent: Math.round((previous.spent + Math.max(0, credit)) * 10000) / 10000,
|
|
41
|
+
calls: previous.calls + 1,
|
|
42
|
+
};
|
|
43
|
+
ledger[account] = next;
|
|
44
|
+
try {
|
|
45
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
46
|
+
await writeFile(file, JSON.stringify(ledger, null, 2), 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// A failed write keeps the in-memory figure; the next turn retries.
|
|
50
|
+
}
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
@@ -8,7 +8,9 @@ import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
|
8
8
|
import { TokenStore } from '../auth/store.js';
|
|
9
9
|
import { TokenManager } from '../auth/tokens.js';
|
|
10
10
|
import { LOGIN_CANCELLED_MESSAGE } from '../auth/device-flow.js';
|
|
11
|
-
import
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { readSpendLedger } from './workbuddy-credits.js';
|
|
13
|
+
import { finishWorkBuddySignIn, parseWorkBuddySession, readWorkBuddyUsage, refreshWorkBuddyToken, startWorkBuddySignIn, workbuddyRefreshable, workbuddyStale, claimWorkBuddyCheckin, listWorkBuddyTasks, acceptWorkBuddyTasks, claimWorkBuddyTask, WORKBUDDY_EARLY_MS, } from '../auth/workbuddy-flow.js';
|
|
12
14
|
import { WorkBuddyAdapter, WORKBUDDY_CODE_URL, WORKBUDDY_CATALOG, WORKBUDDY_DEFAULT_MODEL, WORKBUDDY_PROVIDER_ID, WORKBUDDY_PROVIDER_NAME, } from './workbuddy.js';
|
|
13
15
|
import { ProviderController } from '../auth/provider-controller.js';
|
|
14
16
|
import { proxiedFetch } from '../http.js';
|
|
@@ -129,9 +131,11 @@ export function createWorkBuddyProvider(options) {
|
|
|
129
131
|
}
|
|
130
132
|
},
|
|
131
133
|
});
|
|
134
|
+
const spendFile = path.join(path.dirname(options.credentialFile), 'workbuddy-spend.json');
|
|
132
135
|
const adapter = new WorkBuddyAdapter({
|
|
133
136
|
streamIdleTimeoutMs: options.streamIdleTimeoutMs,
|
|
134
137
|
tokens,
|
|
138
|
+
spendFile,
|
|
135
139
|
...options.onWarn === undefined ? {} : { onWarn: options.onWarn },
|
|
136
140
|
hiddenModels: () => options.preferences.hiddenIds(WORKBUDDY_PROVIDER_ID),
|
|
137
141
|
userModels: () => options.preferences.extraIds(WORKBUDDY_PROVIDER_ID),
|
|
@@ -164,8 +168,10 @@ export function createWorkBuddyProvider(options) {
|
|
|
164
168
|
const held = await store.get(account);
|
|
165
169
|
if (held === undefined)
|
|
166
170
|
return undefined;
|
|
167
|
-
const
|
|
168
|
-
|
|
171
|
+
const ledger = await readSpendLedger(spendFile);
|
|
172
|
+
const spend = ledger[account];
|
|
173
|
+
const read = await readWorkBuddyUsage(tokenOf(held), held.accountId, fetchFn, spend === undefined ? undefined : { spent: spend.spent });
|
|
174
|
+
return { plan: read.plan, windows: read.windows, ...read.checkin === undefined ? {} : { checkin: read.checkin } };
|
|
169
175
|
},
|
|
170
176
|
details: () => [
|
|
171
177
|
{ key: 'provider', value: WORKBUDDY_PROVIDER_NAME },
|
|
@@ -201,5 +207,66 @@ export function createWorkBuddyProvider(options) {
|
|
|
201
207
|
removeModel: (id) => controller.removeModel(id),
|
|
202
208
|
usage: (force) => controller.usage(force),
|
|
203
209
|
reload: () => controller.reload(),
|
|
210
|
+
checkin: async (account) => {
|
|
211
|
+
const held = await store.get(account);
|
|
212
|
+
if (held === undefined)
|
|
213
|
+
throw new ProviderError('WorkBuddy account not signed in', REAUTH_REQUIRED);
|
|
214
|
+
try {
|
|
215
|
+
const claim = await claimWorkBuddyCheckin(tokenOf(held), fetchFn);
|
|
216
|
+
return { credit: claim.credit, streakDays: claim.streakDays, already: claim.credit === 0 };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
// An already-claimed check-in is a normal answer, not a failure.
|
|
221
|
+
if (/already|已签到/i.test(message))
|
|
222
|
+
return { credit: 0, streakDays: 0, already: true };
|
|
223
|
+
throw new ProviderError(`WorkBuddy check-in failed: ${message}`, REAUTH_REQUIRED);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
tasks: async (account, claim) => {
|
|
227
|
+
const held = await store.get(account);
|
|
228
|
+
if (held === undefined)
|
|
229
|
+
throw new ProviderError('WorkBuddy account not signed in', REAUTH_REQUIRED);
|
|
230
|
+
const token = tokenOf(held);
|
|
231
|
+
// Enrol in every open task first: accepting is the sign-up half, and replaying it is
|
|
232
|
+
// a safe no-op, so progress a chat or an activity report lights is never missed.
|
|
233
|
+
const tasks = await listWorkBuddyTasks(token, fetchFn);
|
|
234
|
+
const open = tasks.filter((task) => !task.claimed && !task.locked && task.acceptStatus !== 'claimed').map((task) => task.taskCode);
|
|
235
|
+
try {
|
|
236
|
+
await acceptWorkBuddyTasks(token, open, fetchFn);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Accepting is best-effort; a failure here does not block reading or claiming.
|
|
240
|
+
}
|
|
241
|
+
const fresh = await listWorkBuddyTasks(token, fetchFn);
|
|
242
|
+
let creditEarned = 0;
|
|
243
|
+
let claimedCount = 0;
|
|
244
|
+
if (claim) {
|
|
245
|
+
for (const task of fresh.filter((entry) => entry.claimable && !entry.locked)) {
|
|
246
|
+
try {
|
|
247
|
+
const reward = await claimWorkBuddyTask(token, task.taskCode, fetchFn);
|
|
248
|
+
creditEarned += reward.credit;
|
|
249
|
+
claimedCount += 1;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// One task's refusal does not stop the others.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const stateOf = (task) => task.claimed ? 'claimed' : task.locked ? 'locked' : task.claimable ? 'claimable' : 'pending';
|
|
257
|
+
return {
|
|
258
|
+
tasks: fresh.map((task) => ({
|
|
259
|
+
taskCode: task.taskCode,
|
|
260
|
+
title: task.title,
|
|
261
|
+
credit: task.credit,
|
|
262
|
+
target: task.target,
|
|
263
|
+
current: task.current,
|
|
264
|
+
state: stateOf(task),
|
|
265
|
+
})),
|
|
266
|
+
claimed: claimedCount,
|
|
267
|
+
claimable: fresh.filter((task) => task.claimable && !task.locked).length,
|
|
268
|
+
creditEarned,
|
|
269
|
+
};
|
|
270
|
+
},
|
|
204
271
|
};
|
|
205
272
|
}
|
|
@@ -43,6 +43,8 @@ export interface WorkBuddyAdapterOptions {
|
|
|
43
43
|
onWarn?: (message: string) => void;
|
|
44
44
|
hiddenModels?: () => Promise<ReadonlySet<string>>;
|
|
45
45
|
userModels?: () => Promise<readonly string[]>;
|
|
46
|
+
/** Where per-account credit spends accumulate; beside the credential file. */
|
|
47
|
+
spendFile?: string;
|
|
46
48
|
fetchFn?: FetchFn;
|
|
47
49
|
}
|
|
48
50
|
/** The WorkBuddy China wire adapter serving the `workbuddy` route. */
|
|
@@ -60,6 +62,8 @@ export declare class WorkBuddyAdapter extends LlmAdapter {
|
|
|
60
62
|
resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
61
63
|
providerRetryPolicy(): undefined;
|
|
62
64
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
65
|
+
/** The current turn's billed credit, read off the last usage frame. */
|
|
66
|
+
private turnCredit;
|
|
63
67
|
/** The stored session's fields as the token the request signs with. */
|
|
64
68
|
private tokenOf;
|
|
65
69
|
private requestBody;
|
|
@@ -13,6 +13,7 @@ import { idleWatchdog, mapFetchFailure } from './common.js';
|
|
|
13
13
|
import { proxiedFetch } from '../http.js';
|
|
14
14
|
import { findEntry } from './catalog.js';
|
|
15
15
|
import { WORKBUDDY_ENDPOINT, WORKBUDDY_UA_VERSION, workbuddyHeaders } from '../auth/workbuddy-flow.js';
|
|
16
|
+
import { recordSpend } from './workbuddy-credits.js';
|
|
16
17
|
/** The `workbuddy` provider route key (China build). */
|
|
17
18
|
export const WORKBUDDY_PROVIDER_ID = 'workbuddy';
|
|
18
19
|
/** Human name for the route and its cards. */
|
|
@@ -277,6 +278,9 @@ export class WorkBuddyAdapter extends LlmAdapter {
|
|
|
277
278
|
// Capture the tool-call history so unsafe tool names in past turns can be renamed to
|
|
278
279
|
// the safe ones the model now sees, keeping the conversation consistent.
|
|
279
280
|
this.historyMessages = options.messages.filter((message) => message.role === 'assistant' || message.role === 'tool');
|
|
281
|
+
// The last usage frame carries the turn's real billed credit; record it so the spend
|
|
282
|
+
// total is a sum of real figures, not an estimate.
|
|
283
|
+
this.turnCredit = 0;
|
|
280
284
|
try {
|
|
281
285
|
const body = this.requestBody(options);
|
|
282
286
|
const response = await this.fetchFn(WORKBUDDY_API, {
|
|
@@ -308,8 +312,13 @@ export class WorkBuddyAdapter extends LlmAdapter {
|
|
|
308
312
|
}
|
|
309
313
|
finally {
|
|
310
314
|
watchdog.stop();
|
|
315
|
+
if (this.turnCredit > 0 && this.options.spendFile !== undefined) {
|
|
316
|
+
void recordSpend(this.options.spendFile, held.account, this.turnCredit).catch(() => { });
|
|
317
|
+
}
|
|
311
318
|
}
|
|
312
319
|
}
|
|
320
|
+
/** The current turn's billed credit, read off the last usage frame. */
|
|
321
|
+
turnCredit = 0;
|
|
313
322
|
/** The stored session's fields as the token the request signs with. */
|
|
314
323
|
tokenOf(session) {
|
|
315
324
|
return {
|
|
@@ -564,6 +573,11 @@ export class WorkBuddyAdapter extends LlmAdapter {
|
|
|
564
573
|
if (usage !== undefined && usage !== null) {
|
|
565
574
|
const input = usage.prompt_tokens ?? 0;
|
|
566
575
|
const output = usage.completion_tokens ?? 0;
|
|
576
|
+
// The last usage frame carries the real billed credit; keep the latest, so a
|
|
577
|
+
// mid-stream retry does not double-count.
|
|
578
|
+
if (typeof usage.credit === 'number' && Number.isFinite(usage.credit)) {
|
|
579
|
+
this.turnCredit = usage.credit;
|
|
580
|
+
}
|
|
567
581
|
yield {
|
|
568
582
|
type: 'usage',
|
|
569
583
|
usage: { inputTokens: input, outputTokens: output, totalTokens: usage.total_tokens ?? input + output },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscription-oauth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Use subscription accounts as DeepSeek Harness LLM providers — Kimi Code (OAuth device flow), Grok / X Premium (OIDC + PKCE), Google Antigravity (Google OAuth + Cloud Code Assist) and GLM / BigModel (ZCode's browser sign-in) — with per-model visibility, drag-to-reorder Auto fallback and plan quotas from the web Settings page",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "EricFL1998",
|