@jxgame2020/dsh-token-quota 0.1.40 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +467 -126
- package/lib/index.js +136 -3
- package/lib/types/client/TokenQuotaPanel.d.ts +4 -2
- package/lib/types/client/TokenQuotaPanel.js +4 -3
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/index.js +50 -1
- package/lib/types/client/locales.d.ts +26 -0
- package/lib/types/client/locales.js +26 -0
- package/lib/types/client/mic.d.ts +19 -0
- package/lib/types/client/mic.js +158 -0
- package/lib/types/client/store.d.ts +5 -1
- package/lib/types/client/store.js +2 -0
- package/lib/types/index.d.ts +10 -0
- package/lib/types/index.js +124 -2
- package/lib/types/types.d.ts +22 -0
- package/lib/types/types.js +6 -0
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
|
|
11
11
|
import type { ModelProviderGroup, ModelSelection } from '@deepseek-ai/dsh-api-session-controller/types';
|
|
12
|
-
import type { TokenQuotaFullAction, TokenQuotaLog, TokenQuotaBalance, TokenQuotaMusicStyle, TokenQuotaNote, TokenQuotaReset, TokenQuotaSnapshot, TokenQuotaUpgrade } from '@jxgame2020/dsh-token-quota/types';
|
|
12
|
+
import type { TokenQuotaFullAction, TokenQuotaLog, TokenQuotaBalance, TokenQuotaMusicStyle, TokenQuotaNote, TokenQuotaReset, TokenQuotaSnapshot, TokenQuotaTranscribeSettings, TokenQuotaUpgrade } from '@jxgame2020/dsh-token-quota/types';
|
|
13
13
|
/** One rendered model row: directory identity merged with the quota snapshot. */
|
|
14
14
|
export interface ModelQuotaRow {
|
|
15
15
|
/** Stable key `provider/model`. */
|
|
@@ -74,6 +74,8 @@ export interface TokenQuotaPanelState {
|
|
|
74
74
|
musicVolume: number;
|
|
75
75
|
musicStyle: TokenQuotaMusicStyle;
|
|
76
76
|
musicOnlyCurrentSession: boolean;
|
|
77
|
+
/** Voice transcription settings (mirrored from the settings document). */
|
|
78
|
+
transcribe: TokenQuotaTranscribeSettings;
|
|
77
79
|
/** Latest upgrade availability from the Host; null = up to date or unknown. */
|
|
78
80
|
upgrade: TokenQuotaUpgrade | null;
|
|
79
81
|
/** Whether the one-shot upgrade banner was dismissed for the current latest version. */
|
|
@@ -128,6 +130,8 @@ export type TokenQuotaPanelActions = {
|
|
|
128
130
|
setMusicVolume: (d: TokenQuotaPanelState, volume: number) => void;
|
|
129
131
|
setMusicStyle: (d: TokenQuotaPanelState, style: TokenQuotaMusicStyle) => void;
|
|
130
132
|
setMusicOnlyCurrentSession: (d: TokenQuotaPanelState, only: boolean) => void;
|
|
133
|
+
/** Adopt the voice transcription settings. */
|
|
134
|
+
setTranscribeSettings: (d: TokenQuotaPanelState, settings: TokenQuotaTranscribeSettings) => void;
|
|
131
135
|
setUpgrade: (d: TokenQuotaPanelState, upgrade: TokenQuotaUpgrade | null) => void;
|
|
132
136
|
setUpgradeDismissed: (d: TokenQuotaPanelState, dismissed: boolean) => void;
|
|
133
137
|
setCheckingUpdates: (d: TokenQuotaPanelState, checking: boolean) => void;
|
|
@@ -38,6 +38,7 @@ export function createTokenQuotaPanelStore() {
|
|
|
38
38
|
musicVolume: 0.5,
|
|
39
39
|
musicStyle: 'pentatonic',
|
|
40
40
|
musicOnlyCurrentSession: true,
|
|
41
|
+
transcribe: { enabled: false, baseURL: 'https://api.siliconflow.cn/v1', apiKeyEnv: 'SILICONFLOW_API_KEY', model: 'XingChenAGI/XingChenASR-V3.2-Ultra' },
|
|
41
42
|
upgrade: null,
|
|
42
43
|
upgradeDismissed: false,
|
|
43
44
|
checkingUpdates: false,
|
|
@@ -71,6 +72,7 @@ export function createTokenQuotaPanelStore() {
|
|
|
71
72
|
setMusicVolume: (d, volume) => { d.musicVolume = volume; },
|
|
72
73
|
setMusicStyle: (d, style) => { d.musicStyle = style; },
|
|
73
74
|
setMusicOnlyCurrentSession: (d, only) => { d.musicOnlyCurrentSession = only; },
|
|
75
|
+
setTranscribeSettings: (d, settings) => { d.transcribe = settings; },
|
|
74
76
|
setUpgrade: (d, upgrade) => {
|
|
75
77
|
// Guard against host/client version skew (e.g. the host process still
|
|
76
78
|
// runs an older bundle after an upgrade): when the reported "latest"
|
package/lib/types/index.d.ts
CHANGED
|
@@ -85,6 +85,8 @@ export declare class TokenQuotaService extends Service {
|
|
|
85
85
|
private disposeRouteClear;
|
|
86
86
|
/** Disposer for the optional balance-refresh route (`POST /token-quota/refresh-balance`). */
|
|
87
87
|
private disposeRouteBalance;
|
|
88
|
+
/** Disposer for the voice transcription route (`POST /token-quota/transcribe`). */
|
|
89
|
+
private disposeRouteTranscribe;
|
|
88
90
|
/** Disposer for the optional music-event route (`GET /token-quota/events`, SSE). */
|
|
89
91
|
private disposeRouteEvents;
|
|
90
92
|
/** Live SSE subscribers receiving streamed music actions. */
|
|
@@ -129,6 +131,14 @@ export declare class TokenQuotaService extends Service {
|
|
|
129
131
|
* type. A failed fetch keeps the previous value and marks it as an error.
|
|
130
132
|
*/
|
|
131
133
|
private refreshProviderBalance;
|
|
134
|
+
/**
|
|
135
|
+
* Forward a recorded audio blob to the configured OpenAI-compatible
|
|
136
|
+
* `/audio/transcriptions` endpoint and return the recognised text. The API
|
|
137
|
+
* key resolves through the credential seam (Models page) with an environment
|
|
138
|
+
* fallback; the request body is the raw audio and the content type carries
|
|
139
|
+
* the filename for the multipart form.
|
|
140
|
+
*/
|
|
141
|
+
private handleTranscribe;
|
|
132
142
|
/** Read the full per-cycle usage history (log dialog data). */
|
|
133
143
|
readLog(): TokenQuotaLog;
|
|
134
144
|
/**
|
package/lib/types/index.js
CHANGED
|
@@ -38,7 +38,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
38
38
|
// the service listens to without importing any runtime value.
|
|
39
39
|
import '@deepseek-ai/dsh-agent';
|
|
40
40
|
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
41
|
-
import { TOKEN_QUOTA_EXCEEDED_CODE, TOKEN_QUOTA_NAMESPACE, tokenQuotaKey, TOKEN_QUOTA_DEFAULT_MUSIC, TOKEN_QUOTA_MAX_NOTES, TOKEN_QUOTA_DEFAULT_BALANCE, } from "./types.js";
|
|
41
|
+
import { TOKEN_QUOTA_EXCEEDED_CODE, TOKEN_QUOTA_NAMESPACE, tokenQuotaKey, TOKEN_QUOTA_DEFAULT_MUSIC, TOKEN_QUOTA_MAX_NOTES, TOKEN_QUOTA_DEFAULT_BALANCE, TOKEN_QUOTA_DEFAULT_TRANSCRIBE, } from "./types.js";
|
|
42
42
|
import { assertTokenQuotaLimit, splitTokenQuotaKey } from "./invariant.js";
|
|
43
43
|
/** The plugin's package name, used to locate the profile dependency and the registry endpoint. */
|
|
44
44
|
const PACKAGE_NAME = '@jxgame2020/dsh-token-quota';
|
|
@@ -232,6 +232,14 @@ const TOKEN_QUOTA_SETTINGS_SCHEMA = z.object({
|
|
|
232
232
|
enabled: z.boolean().default(TOKEN_QUOTA_DEFAULT_BALANCE.enabled),
|
|
233
233
|
pollMinutes: z.number().min(1).max(120).default(TOKEN_QUOTA_DEFAULT_BALANCE.pollMinutes),
|
|
234
234
|
}).default({ ...TOKEN_QUOTA_DEFAULT_BALANCE }),
|
|
235
|
+
// Voice transcription: the browser records and the host forwards the audio
|
|
236
|
+
// to an OpenAI-compatible endpoint.
|
|
237
|
+
transcribe: z.object({
|
|
238
|
+
enabled: z.boolean().default(TOKEN_QUOTA_DEFAULT_TRANSCRIBE.enabled),
|
|
239
|
+
baseURL: z.string().default(TOKEN_QUOTA_DEFAULT_TRANSCRIBE.baseURL),
|
|
240
|
+
apiKeyEnv: z.string().default(TOKEN_QUOTA_DEFAULT_TRANSCRIBE.apiKeyEnv),
|
|
241
|
+
model: z.string().default(TOKEN_QUOTA_DEFAULT_TRANSCRIBE.model),
|
|
242
|
+
}).default({ ...TOKEN_QUOTA_DEFAULT_TRANSCRIBE }),
|
|
235
243
|
});
|
|
236
244
|
/** Two-digit zero-pad helper. */
|
|
237
245
|
function pad2(value) {
|
|
@@ -288,7 +296,7 @@ export class TokenQuotaService extends Service {
|
|
|
288
296
|
history = {};
|
|
289
297
|
limits = {};
|
|
290
298
|
monitored = undefined;
|
|
291
|
-
settingsSource = () => ({ limits: {}, monitored: [], onFull: 'stop', checkUpdates: true, dimWhenIdle: false, music: { ...TOKEN_QUOTA_DEFAULT_MUSIC }, order: [], notes: [], balance: { ...TOKEN_QUOTA_DEFAULT_BALANCE } });
|
|
299
|
+
settingsSource = () => ({ limits: {}, monitored: [], onFull: 'stop', checkUpdates: true, dimWhenIdle: false, music: { ...TOKEN_QUOTA_DEFAULT_MUSIC }, order: [], notes: [], balance: { ...TOKEN_QUOTA_DEFAULT_BALANCE }, transcribe: { ...TOKEN_QUOTA_DEFAULT_TRANSCRIBE } });
|
|
292
300
|
/** Whether update checks are enabled (mirrors the settings document). */
|
|
293
301
|
checkUpdates = true;
|
|
294
302
|
/** Cached upgrade availability; recomputed by {@link refreshUpgrade}. */
|
|
@@ -325,6 +333,8 @@ export class TokenQuotaService extends Service {
|
|
|
325
333
|
disposeRouteClear;
|
|
326
334
|
/** Disposer for the optional balance-refresh route (`POST /token-quota/refresh-balance`). */
|
|
327
335
|
disposeRouteBalance;
|
|
336
|
+
/** Disposer for the voice transcription route (`POST /token-quota/transcribe`). */
|
|
337
|
+
disposeRouteTranscribe;
|
|
328
338
|
/** Disposer for the optional music-event route (`GET /token-quota/events`, SSE). */
|
|
329
339
|
disposeRouteEvents;
|
|
330
340
|
/** Live SSE subscribers receiving streamed music actions. */
|
|
@@ -369,6 +379,18 @@ export class TokenQuotaService extends Service {
|
|
|
369
379
|
? Math.max(1, Math.min(120, Math.round(doc.balance.pollMinutes)))
|
|
370
380
|
: TOKEN_QUOTA_DEFAULT_BALANCE.pollMinutes,
|
|
371
381
|
},
|
|
382
|
+
transcribe: {
|
|
383
|
+
enabled: doc.transcribe?.enabled ?? TOKEN_QUOTA_DEFAULT_TRANSCRIBE.enabled,
|
|
384
|
+
baseURL: typeof doc.transcribe?.baseURL === 'string' && doc.transcribe.baseURL !== ''
|
|
385
|
+
? doc.transcribe.baseURL.replace(/\/+$/, '')
|
|
386
|
+
: TOKEN_QUOTA_DEFAULT_TRANSCRIBE.baseURL,
|
|
387
|
+
apiKeyEnv: typeof doc.transcribe?.apiKeyEnv === 'string' && doc.transcribe.apiKeyEnv !== ''
|
|
388
|
+
? doc.transcribe.apiKeyEnv
|
|
389
|
+
: TOKEN_QUOTA_DEFAULT_TRANSCRIBE.apiKeyEnv,
|
|
390
|
+
model: typeof doc.transcribe?.model === 'string' && doc.transcribe.model !== ''
|
|
391
|
+
? doc.transcribe.model
|
|
392
|
+
: TOKEN_QUOTA_DEFAULT_TRANSCRIBE.model,
|
|
393
|
+
},
|
|
372
394
|
notes: Array.isArray(doc.notes)
|
|
373
395
|
? doc.notes.slice(0, TOKEN_QUOTA_MAX_NOTES).flatMap((note) => {
|
|
374
396
|
if (typeof note.id !== 'string')
|
|
@@ -549,6 +571,30 @@ export class TokenQuotaService extends Service {
|
|
|
549
571
|
});
|
|
550
572
|
},
|
|
551
573
|
});
|
|
574
|
+
this.disposeRouteTranscribe = server.register({
|
|
575
|
+
kind: 'exact',
|
|
576
|
+
path: '/token-quota/transcribe',
|
|
577
|
+
handler: (req, res) => {
|
|
578
|
+
if (req.method !== 'POST') {
|
|
579
|
+
res.writeHead(405);
|
|
580
|
+
res.end();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
void this.handleTranscribe(req).then((result) => {
|
|
584
|
+
if (result.ok) {
|
|
585
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
586
|
+
res.end(JSON.stringify({ text: result.text }));
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
res.writeHead(result.status);
|
|
590
|
+
res.end(result.message);
|
|
591
|
+
}
|
|
592
|
+
}, (error) => {
|
|
593
|
+
res.writeHead(500);
|
|
594
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
595
|
+
});
|
|
596
|
+
},
|
|
597
|
+
});
|
|
552
598
|
this.disposeRouteClear = server.register({
|
|
553
599
|
kind: 'exact',
|
|
554
600
|
path: '/token-quota/clear-log',
|
|
@@ -895,6 +941,80 @@ export class TokenQuotaService extends Service {
|
|
|
895
941
|
this.balanceFetches.set(provider, promise);
|
|
896
942
|
return promise;
|
|
897
943
|
}
|
|
944
|
+
/**
|
|
945
|
+
* Forward a recorded audio blob to the configured OpenAI-compatible
|
|
946
|
+
* `/audio/transcriptions` endpoint and return the recognised text. The API
|
|
947
|
+
* key resolves through the credential seam (Models page) with an environment
|
|
948
|
+
* fallback; the request body is the raw audio and the content type carries
|
|
949
|
+
* the filename for the multipart form.
|
|
950
|
+
*/
|
|
951
|
+
async handleTranscribe(req) {
|
|
952
|
+
const settings = this.settingsSource().transcribe;
|
|
953
|
+
if (!settings.enabled) {
|
|
954
|
+
return { ok: false, status: 403, message: 'transcription is disabled' };
|
|
955
|
+
}
|
|
956
|
+
const chunks = [];
|
|
957
|
+
for await (const chunk of req) {
|
|
958
|
+
chunks.push(Buffer.from(chunk));
|
|
959
|
+
}
|
|
960
|
+
const audio = Buffer.concat(chunks);
|
|
961
|
+
if (audio.length === 0) {
|
|
962
|
+
return { ok: false, status: 400, message: 'empty audio payload' };
|
|
963
|
+
}
|
|
964
|
+
const credentials = this.ctx.get('credentials');
|
|
965
|
+
let key;
|
|
966
|
+
if (credentials !== undefined) {
|
|
967
|
+
try {
|
|
968
|
+
key = (await credentials.resolve(settings.apiKeyEnv))?.value;
|
|
969
|
+
}
|
|
970
|
+
catch {
|
|
971
|
+
// Seam unreachable: fall through to the environment.
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (key === undefined || key === '')
|
|
975
|
+
key = process.env[settings.apiKeyEnv];
|
|
976
|
+
if (key === undefined || key === '') {
|
|
977
|
+
return { ok: false, status: 400, message: `no API key for credential reference "${settings.apiKeyEnv}"` };
|
|
978
|
+
}
|
|
979
|
+
const contentType = req.headers['content-type'] ?? 'application/octet-stream';
|
|
980
|
+
const disposition = typeof req.headers['content-disposition'] === 'string'
|
|
981
|
+
? req.headers['content-disposition']
|
|
982
|
+
: '';
|
|
983
|
+
const filenameMatch = /filename="([^"]+)"/.exec(disposition);
|
|
984
|
+
const filename = filenameMatch?.[1] ?? 'recording.webm';
|
|
985
|
+
const form = new FormData();
|
|
986
|
+
form.append('model', settings.model);
|
|
987
|
+
form.append('file', new Blob([new Uint8Array(audio)], { type: contentType }), filename);
|
|
988
|
+
try {
|
|
989
|
+
const controller = new AbortController();
|
|
990
|
+
const timer = setTimeout(() => { controller.abort(); }, 60_000);
|
|
991
|
+
let res;
|
|
992
|
+
try {
|
|
993
|
+
res = await fetch(`${settings.baseURL}/audio/transcriptions`, {
|
|
994
|
+
method: 'POST',
|
|
995
|
+
headers: { authorization: `Bearer ${key}`, accept: 'application/json' },
|
|
996
|
+
body: form,
|
|
997
|
+
signal: controller.signal,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
finally {
|
|
1001
|
+
clearTimeout(timer);
|
|
1002
|
+
}
|
|
1003
|
+
if (!res.ok) {
|
|
1004
|
+
const detail = await res.text().catch(() => '');
|
|
1005
|
+
return { ok: false, status: res.status, message: `transcription endpoint HTTP ${res.status}${detail !== '' ? `: ${detail.slice(0, 300)}` : ''}` };
|
|
1006
|
+
}
|
|
1007
|
+
const data = (await res.json());
|
|
1008
|
+
const text = typeof data?.text === 'string' ? data.text : '';
|
|
1009
|
+
if (text === '') {
|
|
1010
|
+
return { ok: false, status: 502, message: 'transcription endpoint returned no text' };
|
|
1011
|
+
}
|
|
1012
|
+
return { ok: true, text };
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
return { ok: false, status: 502, message: error instanceof Error ? error.message : String(error) };
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
898
1018
|
/** Read the full per-cycle usage history (log dialog data). */
|
|
899
1019
|
readLog() {
|
|
900
1020
|
this.rollCycleIfNeeded();
|
|
@@ -1231,6 +1351,8 @@ export class TokenQuotaService extends Service {
|
|
|
1231
1351
|
this.disposeRouteClear();
|
|
1232
1352
|
if (this.disposeRouteBalance !== undefined)
|
|
1233
1353
|
this.disposeRouteBalance();
|
|
1354
|
+
if (this.disposeRouteTranscribe !== undefined)
|
|
1355
|
+
this.disposeRouteTranscribe();
|
|
1234
1356
|
if (this.balanceTimer !== undefined) {
|
|
1235
1357
|
clearTimeout(this.balanceTimer);
|
|
1236
1358
|
this.balanceTimer = undefined;
|
package/lib/types/types.d.ts
CHANGED
|
@@ -55,6 +55,23 @@ export interface TokenQuotaBalanceSettings {
|
|
|
55
55
|
pollMinutes: number;
|
|
56
56
|
}
|
|
57
57
|
export declare const TOKEN_QUOTA_DEFAULT_BALANCE: TokenQuotaBalanceSettings;
|
|
58
|
+
/**
|
|
59
|
+
* Voice transcription settings: when enabled the browser shows a mic button
|
|
60
|
+
* inside the chat composer; a press records the voice, a second press stops
|
|
61
|
+
* and sends the audio to an OpenAI-compatible `/audio/transcriptions`
|
|
62
|
+
* endpoint, and the returned text is inserted into the composer.
|
|
63
|
+
*/
|
|
64
|
+
export interface TokenQuotaTranscribeSettings {
|
|
65
|
+
/** Whether the mic button appears in the composer and transcription is active. */
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
/** API base URL of the OpenAI-compatible transcription endpoint. */
|
|
68
|
+
baseURL: string;
|
|
69
|
+
/** Credential reference (environment-variable name) holding the API key. */
|
|
70
|
+
apiKeyEnv: string;
|
|
71
|
+
/** Speech-to-text model name sent as the `model` field. */
|
|
72
|
+
model: string;
|
|
73
|
+
}
|
|
74
|
+
export declare const TOKEN_QUOTA_DEFAULT_TRANSCRIBE: TokenQuotaTranscribeSettings;
|
|
58
75
|
/** Fetch state of the provider account balance. */
|
|
59
76
|
export type TokenQuotaBalanceStatus = 'ok' | 'unconfigured' | 'error';
|
|
60
77
|
/**
|
|
@@ -211,6 +228,11 @@ export interface TokenQuotaSettings {
|
|
|
211
228
|
* often the host polls the balance endpoint.
|
|
212
229
|
*/
|
|
213
230
|
balance: TokenQuotaBalanceSettings;
|
|
231
|
+
/**
|
|
232
|
+
* Voice transcription: mic in the composer plus the OpenAI-compatible
|
|
233
|
+
* endpoint used to turn a recording into text. Disabled by default.
|
|
234
|
+
*/
|
|
235
|
+
transcribe: TokenQuotaTranscribeSettings;
|
|
214
236
|
/**
|
|
215
237
|
* Floating scratchpad notes owned by the panel. At most
|
|
216
238
|
* {@link TOKEN_QUOTA_MAX_NOTES} are kept; content, title, placement, size
|
package/lib/types/types.js
CHANGED
|
@@ -33,6 +33,12 @@ export const TOKEN_QUOTA_DEFAULT_BALANCE = {
|
|
|
33
33
|
enabled: true,
|
|
34
34
|
pollMinutes: 5,
|
|
35
35
|
};
|
|
36
|
+
export const TOKEN_QUOTA_DEFAULT_TRANSCRIBE = {
|
|
37
|
+
enabled: false,
|
|
38
|
+
baseURL: 'https://api.siliconflow.cn/v1',
|
|
39
|
+
apiKeyEnv: 'SILICONFLOW_API_KEY',
|
|
40
|
+
model: 'XingChenAGI/XingChenASR-V3.2-Ultra',
|
|
41
|
+
};
|
|
36
42
|
/** Default music settings: silent until the user flips the panel switch. */
|
|
37
43
|
export const TOKEN_QUOTA_DEFAULT_MUSIC = {
|
|
38
44
|
enabled: false,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxgame2020/dsh-token-quota",
|
|
3
3
|
"description": "Daily per-model token quota accounting, enforcement, and a floating quota panel for the DeepSeek Harness — a fully self-contained plugin (no core-Harness wiring, no generated RPC contract)",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.41",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|