@jxgame2020/dsh-token-quota 0.1.22 → 0.1.23
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 +503 -77
- package/lib/index.js +123 -18
- package/lib/types/client/TokenQuotaPanel.d.ts +8 -2
- package/lib/types/client/TokenQuotaPanel.js +7 -4
- package/lib/types/client/index.js +54 -0
- package/lib/types/client/locales.d.ts +14 -0
- package/lib/types/client/locales.js +14 -0
- package/lib/types/client/music.d.ts +54 -0
- package/lib/types/client/music.js +231 -0
- package/lib/types/client/store.d.ts +8 -1
- package/lib/types/client/store.js +6 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/index.js +92 -18
- package/lib/types/types.d.ts +54 -0
- package/lib/types/types.js +8 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -23,6 +23,12 @@ import { LlmError } from "@deepseek-ai/dsh-llm";
|
|
|
23
23
|
const TOKEN_QUOTA_NAMESPACE = "token-quota";
|
|
24
24
|
/** Stable machine code thrown from `agent/request` when a model is over its daily cap. */
|
|
25
25
|
const TOKEN_QUOTA_EXCEEDED_CODE = "TOKEN_QUOTA_EXCEEDED";
|
|
26
|
+
/** Default music settings: silent until the user flips the panel switch. */
|
|
27
|
+
const TOKEN_QUOTA_DEFAULT_MUSIC = {
|
|
28
|
+
enabled: false,
|
|
29
|
+
volume: .5,
|
|
30
|
+
style: "major"
|
|
31
|
+
};
|
|
26
32
|
/** Build the stable per-model key shared by the counter, settings, and snapshot. */
|
|
27
33
|
function tokenQuotaKey(provider, model) {
|
|
28
34
|
return `${provider}/${model}`;
|
|
@@ -207,7 +213,16 @@ const TOKEN_QUOTA_SETTINGS_SCHEMA = z.object({
|
|
|
207
213
|
]).default("stop"),
|
|
208
214
|
checkUpdates: z.boolean().default(true),
|
|
209
215
|
dimWhenIdle: z.boolean().default(false),
|
|
210
|
-
reset: z.any().default(null)
|
|
216
|
+
reset: z.any().default(null),
|
|
217
|
+
music: z.object({
|
|
218
|
+
enabled: z.boolean().default(TOKEN_QUOTA_DEFAULT_MUSIC.enabled),
|
|
219
|
+
volume: z.number().min(0).max(1).default(TOKEN_QUOTA_DEFAULT_MUSIC.volume),
|
|
220
|
+
style: z.union([
|
|
221
|
+
"major",
|
|
222
|
+
"minor",
|
|
223
|
+
"pentatonic"
|
|
224
|
+
]).default(TOKEN_QUOTA_DEFAULT_MUSIC.style)
|
|
225
|
+
}).default({ ...TOKEN_QUOTA_DEFAULT_MUSIC })
|
|
211
226
|
});
|
|
212
227
|
/** Two-digit zero-pad helper. */
|
|
213
228
|
function pad2(value) {
|
|
@@ -263,7 +278,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
263
278
|
monitored: [],
|
|
264
279
|
onFull: "stop",
|
|
265
280
|
checkUpdates: true,
|
|
266
|
-
dimWhenIdle: false
|
|
281
|
+
dimWhenIdle: false,
|
|
282
|
+
music: { ...TOKEN_QUOTA_DEFAULT_MUSIC }
|
|
267
283
|
});
|
|
268
284
|
/** Whether update checks are enabled (mirrors the settings document). */
|
|
269
285
|
checkUpdates = true;
|
|
@@ -291,6 +307,10 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
291
307
|
disposeRouteCheck;
|
|
292
308
|
/** Disposer for the optional clear-log route (`POST /token-quota/clear-log`). */
|
|
293
309
|
disposeRouteClear;
|
|
310
|
+
/** Disposer for the optional music-event route (`GET /token-quota/events`, SSE). */
|
|
311
|
+
disposeRouteEvents;
|
|
312
|
+
/** Live SSE subscribers receiving streamed music actions. */
|
|
313
|
+
musicClients = /* @__PURE__ */ new Set();
|
|
294
314
|
/** Disposer for the webServer-arrival watcher when the service mounts later. */
|
|
295
315
|
disposeRouteWatcher;
|
|
296
316
|
/**
|
|
@@ -312,7 +332,12 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
312
332
|
onFull: doc.onFull === "switchPriority" ? "switchAll" : doc.onFull ?? "stop",
|
|
313
333
|
checkUpdates: doc.checkUpdates ?? true,
|
|
314
334
|
dimWhenIdle: doc.dimWhenIdle ?? false,
|
|
315
|
-
reset: doc.reset ?? void 0
|
|
335
|
+
reset: doc.reset ?? void 0,
|
|
336
|
+
music: {
|
|
337
|
+
enabled: doc.music?.enabled ?? TOKEN_QUOTA_DEFAULT_MUSIC.enabled,
|
|
338
|
+
volume: doc.music?.volume ?? TOKEN_QUOTA_DEFAULT_MUSIC.volume,
|
|
339
|
+
style: doc.music?.style ?? TOKEN_QUOTA_DEFAULT_MUSIC.style
|
|
340
|
+
}
|
|
316
341
|
};
|
|
317
342
|
};
|
|
318
343
|
},
|
|
@@ -439,6 +464,32 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
439
464
|
});
|
|
440
465
|
}
|
|
441
466
|
});
|
|
467
|
+
this.disposeRouteEvents = server.register({
|
|
468
|
+
kind: "exact",
|
|
469
|
+
path: "/token-quota/events",
|
|
470
|
+
handler: (req, res) => {
|
|
471
|
+
res.writeHead(200, {
|
|
472
|
+
"content-type": "text/event-stream",
|
|
473
|
+
"cache-control": "no-store",
|
|
474
|
+
"connection": "keep-alive"
|
|
475
|
+
});
|
|
476
|
+
this.musicClients.add(res);
|
|
477
|
+
res.write(": token-quota music stream\n\n");
|
|
478
|
+
req.on("close", () => {
|
|
479
|
+
this.musicClients.delete(res);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
/** Broadcast one action to every open SSE music subscriber. */
|
|
485
|
+
pushMusicAction(action) {
|
|
486
|
+
if (this.musicClients.size === 0) return;
|
|
487
|
+
const data = `event: action\ndata: ${JSON.stringify(action)}\n\n`;
|
|
488
|
+
for (const res of this.musicClients) try {
|
|
489
|
+
res.write(data);
|
|
490
|
+
} catch {
|
|
491
|
+
this.musicClients.delete(res);
|
|
492
|
+
}
|
|
442
493
|
}
|
|
443
494
|
/** Query the npm registry and rebuild the cached upgrade info (coalesced). */
|
|
444
495
|
refreshUpgrade() {
|
|
@@ -453,7 +504,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
453
504
|
return this.upgradeCheck;
|
|
454
505
|
}
|
|
455
506
|
async doRefreshUpgrade() {
|
|
456
|
-
const current = "0.1.
|
|
507
|
+
const current = "0.1.23";
|
|
457
508
|
let latest;
|
|
458
509
|
try {
|
|
459
510
|
const response = await fetch(REGISTRY_LATEST_URL, {
|
|
@@ -585,20 +636,69 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
585
636
|
return typeof limit === "number" && Number.isInteger(limit) && limit > 0 ? limit : 0;
|
|
586
637
|
}
|
|
587
638
|
onSessionEvent(session, event) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
639
|
+
const sessionId = String(session.id);
|
|
640
|
+
switch (event.type) {
|
|
641
|
+
case "turn/start":
|
|
642
|
+
this.pushMusicAction({
|
|
643
|
+
type: "turn/start",
|
|
644
|
+
sessionId
|
|
645
|
+
});
|
|
646
|
+
break;
|
|
647
|
+
case "turn/end":
|
|
648
|
+
this.pushMusicAction({
|
|
649
|
+
type: "turn/end",
|
|
650
|
+
sessionId,
|
|
651
|
+
reason: event.data.reason.kind
|
|
652
|
+
});
|
|
653
|
+
break;
|
|
654
|
+
case "step/start":
|
|
655
|
+
this.pushMusicAction({
|
|
656
|
+
type: "step/start",
|
|
657
|
+
sessionId
|
|
658
|
+
});
|
|
659
|
+
break;
|
|
660
|
+
case "request/header": {
|
|
661
|
+
const { provider, model } = event.data.header.config;
|
|
662
|
+
this.headerKeys.set(session, tokenQuotaKey(provider, model));
|
|
663
|
+
this.pushMusicAction({
|
|
664
|
+
type: "request/header",
|
|
665
|
+
sessionId,
|
|
666
|
+
provider,
|
|
667
|
+
model
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
case "tool/call":
|
|
672
|
+
this.pushMusicAction({
|
|
673
|
+
type: "tool/call",
|
|
674
|
+
sessionId,
|
|
675
|
+
name: event.data.name
|
|
676
|
+
});
|
|
677
|
+
break;
|
|
678
|
+
case "tool/result":
|
|
679
|
+
this.pushMusicAction({
|
|
680
|
+
type: "tool/result",
|
|
681
|
+
sessionId,
|
|
682
|
+
error: event.data.error !== void 0
|
|
683
|
+
});
|
|
684
|
+
break;
|
|
685
|
+
case "assistant/message":
|
|
686
|
+
this.pushMusicAction({
|
|
687
|
+
type: "assistant/message",
|
|
688
|
+
sessionId,
|
|
689
|
+
interrupted: event.data.interrupted === true
|
|
690
|
+
});
|
|
691
|
+
if (event.data.usage !== void 0) {
|
|
692
|
+
const key = this.headerKeys.get(session);
|
|
693
|
+
if (key === void 0) return;
|
|
694
|
+
if (!this.isMonitored(key)) return;
|
|
695
|
+
const tokens = usageTokens(event.data.usage);
|
|
696
|
+
if (tokens <= 0) return;
|
|
697
|
+
this.rollCycleIfNeeded();
|
|
698
|
+
this.usage[key] = (this.usage[key] ?? 0) + tokens;
|
|
699
|
+
this.scheduleWrite();
|
|
700
|
+
}
|
|
701
|
+
break;
|
|
602
702
|
}
|
|
603
703
|
}
|
|
604
704
|
/**
|
|
@@ -750,6 +850,11 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
750
850
|
if (this.disposeRouteLog !== void 0) this.disposeRouteLog();
|
|
751
851
|
if (this.disposeRouteCheck !== void 0) this.disposeRouteCheck();
|
|
752
852
|
if (this.disposeRouteClear !== void 0) this.disposeRouteClear();
|
|
853
|
+
if (this.disposeRouteEvents !== void 0) this.disposeRouteEvents();
|
|
854
|
+
for (const res of this.musicClients) try {
|
|
855
|
+
res.end();
|
|
856
|
+
} catch {}
|
|
857
|
+
this.musicClients.clear();
|
|
753
858
|
this.flush();
|
|
754
859
|
}
|
|
755
860
|
safeSplit(key) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
2
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
3
|
-
import type { TokenQuotaFullAction, TokenQuotaReset } from '@jxgame2020/dsh-token-quota/types';
|
|
3
|
+
import type { TokenQuotaFullAction, TokenQuotaMusicStyle, TokenQuotaReset } from '@jxgame2020/dsh-token-quota/types';
|
|
4
4
|
import type { createTokenQuotaPanelStore } from './store.ts';
|
|
5
5
|
/** Injected business face: data loading and mutations wired in `apply`. */
|
|
6
6
|
export interface TokenQuotaPanelInjected {
|
|
@@ -20,6 +20,12 @@ export interface TokenQuotaPanelInjected {
|
|
|
20
20
|
setCheckUpdates: (enabled: boolean) => void;
|
|
21
21
|
/** Persist whether the panel dims while the pointer is away / typing. */
|
|
22
22
|
setDimWhenIdle: (enabled: boolean) => void;
|
|
23
|
+
/** Toggle the live music output (requires a user gesture to start audio). */
|
|
24
|
+
setMusicEnabled: (enabled: boolean) => void;
|
|
25
|
+
/** Change the live-music master volume 0..1. */
|
|
26
|
+
setMusicVolume: (volume: number) => void;
|
|
27
|
+
/** Change the live-music harmonic style. */
|
|
28
|
+
setMusicStyle: (style: TokenQuotaMusicStyle) => void;
|
|
23
29
|
/** Ask the Host to check the registry right now, then refresh the snapshot. */
|
|
24
30
|
checkUpdatesNow: () => void;
|
|
25
31
|
/** Ask the Host to clear log entries before a date, then refresh the log. */
|
|
@@ -32,5 +38,5 @@ export type TokenQuotaPanelComponentProps = PropsRuntime<'shell.overlay'> & Prop
|
|
|
32
38
|
* @param props - composed slot props.
|
|
33
39
|
* @returns the panel element tree.
|
|
34
40
|
*/
|
|
35
|
-
export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
|
|
41
|
+
export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setMusicEnabled, setMusicVolume, setMusicStyle, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
|
|
36
42
|
//# sourceMappingURL=TokenQuotaPanel.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Token-quota floating panel, registered into the frame-wide `shell.overlay`
|
|
4
4
|
* seat. Renders one row per MONITORED model of the current session's
|
|
@@ -86,7 +86,7 @@ function loadPanelPos() {
|
|
|
86
86
|
* @param props - composed slot props.
|
|
87
87
|
* @returns the panel element tree.
|
|
88
88
|
*/
|
|
89
|
-
export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, useStore, actions, useSessions, }) {
|
|
89
|
+
export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setMusicEnabled, setMusicVolume, setMusicStyle, useStore, actions, useSessions, }) {
|
|
90
90
|
const [collapsed, setCollapsed] = useState(false);
|
|
91
91
|
const [drafts, setDrafts] = useState({});
|
|
92
92
|
const [editingKey, setEditingKey] = useState(null);
|
|
@@ -129,6 +129,9 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
129
129
|
const onFull = useStore(s => s.onFull);
|
|
130
130
|
const checkUpdates = useStore(s => s.checkUpdates);
|
|
131
131
|
const dimWhenIdle = useStore(s => s.dimWhenIdle);
|
|
132
|
+
const musicEnabled = useStore(s => s.musicEnabled);
|
|
133
|
+
const musicVolume = useStore(s => s.musicVolume);
|
|
134
|
+
const musicStyle = useStore(s => s.musicStyle);
|
|
132
135
|
const upgrade = useStore(s => s.upgrade);
|
|
133
136
|
const upgradeDismissed = useStore(s => s.upgradeDismissed);
|
|
134
137
|
const checkingUpdates = useStore(s => s.checkingUpdates);
|
|
@@ -294,7 +297,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
294
297
|
: '';
|
|
295
298
|
return (_jsxs("div", { ref: panelRef, className: `${css.panel}${panelDimClass !== '' ? ` ${panelDimClass}` : ''}`, style: panelPos !== null ? { left: panelPos.x, top: panelPos.y, right: 'auto' } : undefined, onPointerEnter: () => { setHovered(true); }, onPointerLeave: () => { setHovered(false); }, children: [_jsxs("div", { className: css.header, onPointerDown: (event) => { beginDrag(event, panelRef.current, applyPanelPos); }, children: [_jsxs("div", { className: css.headerText, children: [_jsxs("div", { className: css.title, children: [t('title'), __TOKEN_QUOTA_VERSION__ !== undefined && __TOKEN_QUOTA_VERSION__ !== '' && (upgrade !== null
|
|
296
299
|
? (_jsxs("span", { className: `${css.version} ${css.versionNew}`, title: t('upgradeAvailableHint'), children: ["v", __TOKEN_QUOTA_VERSION__, " \u2192 v", upgrade.latestVersion] }))
|
|
297
|
-
: (_jsxs("span", { className: css.version, children: ["v", __TOKEN_QUOTA_VERSION__] })))] }), _jsx("div", { className: css.subtitle, children: t('subtitle') })] }), _jsxs("div", { className: css.headerActions, onPointerDown: (event) => { event.stopPropagation(); }, children: [_jsx("button", { type: "button", className: css.settingsBtn, onClick: () => { actions.setLogOpen(true); }, children: t('logs') }), _jsx("button", { type: "button", className: css.settingsBtn, onClick: openSettings, children: t('settings') }), _jsx("button", { type: "button", className: css.collapse, onClick: () => { setCollapsed(true); }, children: t('collapse') })] })] }), _jsxs("div", { className: css.body, children: [loading && _jsx("div", { className: css.notice, children: t('loading') }), error !== null && _jsx("div", { className: css.noticeError, children: error }), fullNotice !== null && (_jsx("div", { className: css.fullNotice, role: "alert", children: fullNotice })), upgrade !== null && !upgradeDismissed && (_jsxs("div", { className: `${css.upgradeBanner}${upgradeFlash ? ` ${css.upgradeBannerFlash}` : ''}`, role: "alert", children: [_jsx("div", { className: css.upgradeBannerText, children: t('upgradeAvailable').replace('{version}', upgrade.latestVersion) }), _jsx("div", { className: css.upgradeCommands, children: upgrade.commands.map(cmd => (_jsxs("div", { className: css.upgradeCommandRow, children: [_jsx("code", { className: css.upgradeCommand, children: cmd }), _jsx("button", { type: "button", className: `${css.copyBtn}${copiedCmd === cmd ? ` ${css.copyBtnDone}` : ''}`, onClick: () => { copyUpgradeCommand(cmd); }, children: copiedCmd === cmd ? t('copied') : t('copy') })] }, cmd))) }), _jsx("div", { className: css.upgradeHint, children: t('upgradeHint') }), _jsx("button", { type: "button", className: css.upgradeClose, onClick: () => { actions.setUpgradeDismissed(true); }, "aria-label": t('close'), children: "\u00D7" })] })), !loading && error === null && visibleRows.length === 0 && (_jsx("div", { className: css.notice, children: monitored !== null && monitored.length === 0 ? t('noMonitored') : t('waiting') })), visibleRows.map((row) => {
|
|
300
|
+
: (_jsxs("span", { className: css.version, children: ["v", __TOKEN_QUOTA_VERSION__] })))] }), _jsx("div", { className: css.subtitle, children: t('subtitle') })] }), _jsxs("div", { className: css.headerActions, onPointerDown: (event) => { event.stopPropagation(); }, children: [_jsx("button", { type: "button", className: `${css.settingsBtn}${musicEnabled ? ` ${css.settingsBtnActive}` : ''}`, title: t('musicHint'), onClick: () => { setMusicEnabled(!musicEnabled); }, children: musicEnabled ? '🔊' : '♪' }), _jsx("button", { type: "button", className: css.settingsBtn, onClick: () => { actions.setLogOpen(true); }, children: t('logs') }), _jsx("button", { type: "button", className: css.settingsBtn, onClick: openSettings, children: t('settings') }), _jsx("button", { type: "button", className: css.collapse, onClick: () => { setCollapsed(true); }, children: t('collapse') })] })] }), _jsxs("div", { className: css.body, children: [loading && _jsx("div", { className: css.notice, children: t('loading') }), error !== null && _jsx("div", { className: css.noticeError, children: error }), fullNotice !== null && (_jsx("div", { className: css.fullNotice, role: "alert", children: fullNotice })), upgrade !== null && !upgradeDismissed && (_jsxs("div", { className: `${css.upgradeBanner}${upgradeFlash ? ` ${css.upgradeBannerFlash}` : ''}`, role: "alert", children: [_jsx("div", { className: css.upgradeBannerText, children: t('upgradeAvailable').replace('{version}', upgrade.latestVersion) }), _jsx("div", { className: css.upgradeCommands, children: upgrade.commands.map(cmd => (_jsxs("div", { className: css.upgradeCommandRow, children: [_jsx("code", { className: css.upgradeCommand, children: cmd }), _jsx("button", { type: "button", className: `${css.copyBtn}${copiedCmd === cmd ? ` ${css.copyBtnDone}` : ''}`, onClick: () => { copyUpgradeCommand(cmd); }, children: copiedCmd === cmd ? t('copied') : t('copy') })] }, cmd))) }), _jsx("div", { className: css.upgradeHint, children: t('upgradeHint') }), _jsx("button", { type: "button", className: css.upgradeClose, onClick: () => { actions.setUpgradeDismissed(true); }, "aria-label": t('close'), children: "\u00D7" })] })), !loading && error === null && visibleRows.length === 0 && (_jsx("div", { className: css.notice, children: monitored !== null && monitored.length === 0 ? t('noMonitored') : t('waiting') })), visibleRows.map((row) => {
|
|
298
301
|
const state = barStateOf(row);
|
|
299
302
|
const pct = row.limit > 0 ? Math.min(100, Math.round((row.used / row.limit) * 100)) : 0;
|
|
300
303
|
const barClass = state === 'over'
|
|
@@ -334,7 +337,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
334
337
|
hour: reset?.hour ?? 0,
|
|
335
338
|
minute: Number(event.target.value),
|
|
336
339
|
});
|
|
337
|
-
}, children: Array.from({ length: 12 }, (_, i) => i * 5).map(minute => (_jsxs("option", { value: minute, children: [String(minute).padStart(2, '0'), " \u5206"] }, minute))) })] })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: dimWhenIdle, onChange: (event) => { setDimWhenIdle(event.target.checked); } }), _jsx("span", { children: t('dimWhenIdleLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('dimWhenIdleHint') })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("div", { className: css.checkUpdatesRow, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: checkUpdates, onChange: (event) => { setCheckUpdates(event.target.checked); } }), _jsx("span", { children: t('checkUpdatesLabel') })] }), _jsx("button", { type: "button", className: css.checkBtn, disabled: checkingUpdates, onClick: checkUpdatesNow, children: checkingUpdates
|
|
340
|
+
}, children: Array.from({ length: 12 }, (_, i) => i * 5).map(minute => (_jsxs("option", { value: minute, children: [String(minute).padStart(2, '0'), " \u5206"] }, minute))) })] })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: dimWhenIdle, onChange: (event) => { setDimWhenIdle(event.target.checked); } }), _jsx("span", { children: t('dimWhenIdleLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('dimWhenIdleHint') })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: musicEnabled, onChange: (event) => { setMusicEnabled(event.target.checked); } }), _jsx("span", { children: t('musicLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('musicHint') }), musicEnabled && (_jsxs(_Fragment, { children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('musicVolume') }), _jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: musicVolume, onChange: (event) => { setMusicVolume(Number(event.target.value)); } })] }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('musicStyle') }), _jsxs("select", { value: musicStyle, onChange: (event) => { setMusicStyle(event.target.value); }, children: [_jsx("option", { value: "major", children: t('musicStyleMajor') }), _jsx("option", { value: "minor", children: t('musicStyleMinor') }), _jsx("option", { value: "pentatonic", children: t('musicStylePentatonic') })] })] })] }))] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("div", { className: css.checkUpdatesRow, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: checkUpdates, onChange: (event) => { setCheckUpdates(event.target.checked); } }), _jsx("span", { children: t('checkUpdatesLabel') })] }), _jsx("button", { type: "button", className: css.checkBtn, disabled: checkingUpdates, onClick: checkUpdatesNow, children: checkingUpdates
|
|
338
341
|
? t('checkingUpdates')
|
|
339
342
|
: lastCheckResult === 'up-to-date'
|
|
340
343
|
? t('checkUpToDate')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createTokenQuotaPanelStore } from "./store.js";
|
|
2
2
|
import { TokenQuotaPanel } from "./TokenQuotaPanel.js";
|
|
3
|
+
import { TokenQuotaMusic } from "./music.js";
|
|
3
4
|
import { en, zh } from "./locales.js";
|
|
4
5
|
export { mergeModelRows } from "./store.js";
|
|
5
6
|
/** Dictionary namespace owning the panel copy. */
|
|
@@ -35,6 +36,28 @@ export function apply(ctx) {
|
|
|
35
36
|
let lastMonitored = null;
|
|
36
37
|
let lastOnFull = 'stop';
|
|
37
38
|
let lastReset = null;
|
|
39
|
+
// Live music engine: subscribes to the plugin's SSE action stream and
|
|
40
|
+
// composes from host events. Web Audio plays by default; a detected MIDI
|
|
41
|
+
// output is driven simultaneously.
|
|
42
|
+
const music = new TokenQuotaMusic();
|
|
43
|
+
const sse = new EventSource('/token-quota/events');
|
|
44
|
+
sse.addEventListener('action', (event) => {
|
|
45
|
+
let action;
|
|
46
|
+
try {
|
|
47
|
+
action = JSON.parse(event.data);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Only play actions for the currently-visible session; a root overlay
|
|
53
|
+
// panel isn't bound to a specific session, but its display tracks
|
|
54
|
+
// useSessions(s => s.current), so keeping the soundtrack on the same
|
|
55
|
+
// session avoids a cacophony from background conversations.
|
|
56
|
+
if (lastSessionId !== undefined && action.sessionId !== String(lastSessionId))
|
|
57
|
+
return;
|
|
58
|
+
music.onAction(action);
|
|
59
|
+
});
|
|
60
|
+
ctx.effect(() => () => { sse.close(); }, 'token-quota-ui: music SSE');
|
|
38
61
|
const keyOf = (selection) => {
|
|
39
62
|
if (selection === null)
|
|
40
63
|
return undefined;
|
|
@@ -190,6 +213,13 @@ export function apply(ctx) {
|
|
|
190
213
|
bound?.setCheckUpdates(doc?.checkUpdates ?? true);
|
|
191
214
|
bound?.setDimWhenIdle(doc?.dimWhenIdle ?? false);
|
|
192
215
|
bound?.setReset(lastReset);
|
|
216
|
+
const musicSettings = { ...{ enabled: false, volume: 0.5, style: 'major' }, ...(doc?.music ?? {}) };
|
|
217
|
+
music.setEnabled(musicSettings.enabled);
|
|
218
|
+
music.setVolume(musicSettings.volume);
|
|
219
|
+
music.setStyle(musicSettings.style);
|
|
220
|
+
bound?.setMusicEnabled(musicSettings.enabled);
|
|
221
|
+
bound?.setMusicVolume(musicSettings.volume);
|
|
222
|
+
bound?.setMusicStyle(musicSettings.style);
|
|
193
223
|
void fetch('/token-quota', { headers: { accept: 'application/json' } }).then((response) => {
|
|
194
224
|
if (!response.ok) {
|
|
195
225
|
bound?.setError(`quota route: ${String(response.status)}`);
|
|
@@ -257,6 +287,29 @@ export function apply(ctx) {
|
|
|
257
287
|
bound?.setDimWhenIdle(enabled);
|
|
258
288
|
void scope.set('dimWhenIdle', enabled);
|
|
259
289
|
};
|
|
290
|
+
const setMusicEnabled = (enabled) => {
|
|
291
|
+
const doc = scope.getSnapshot().value;
|
|
292
|
+
const current = { ...{ enabled: false, volume: 0.5, style: 'major' }, ...(doc?.music ?? {}) };
|
|
293
|
+
current.enabled = enabled;
|
|
294
|
+
void scope.set('music', current);
|
|
295
|
+
// First enable: start audio inside this user gesture (the panel switch
|
|
296
|
+
// click) so the AudioContext can resume; the settings pull that follows
|
|
297
|
+
// applies enabled/volume/style to the engine.
|
|
298
|
+
if (enabled)
|
|
299
|
+
void music.ensureStarted();
|
|
300
|
+
};
|
|
301
|
+
const setMusicVolume = (volume) => {
|
|
302
|
+
const doc = scope.getSnapshot().value;
|
|
303
|
+
const current = { ...{ enabled: false, volume: 0.5, style: 'major' }, ...(doc?.music ?? {}) };
|
|
304
|
+
current.volume = Math.max(0, Math.min(1, volume));
|
|
305
|
+
void scope.set('music', current);
|
|
306
|
+
};
|
|
307
|
+
const setMusicStyle = (style) => {
|
|
308
|
+
const doc = scope.getSnapshot().value;
|
|
309
|
+
const current = { ...{ enabled: false, volume: 0.5, style: 'major' }, ...(doc?.music ?? {}) };
|
|
310
|
+
current.style = style;
|
|
311
|
+
void scope.set('music', current);
|
|
312
|
+
};
|
|
260
313
|
/** Ask the Host to check the registry right now, then refresh the snapshot. */
|
|
261
314
|
const checkUpdatesNow = () => {
|
|
262
315
|
bound?.setCheckingUpdates(true);
|
|
@@ -344,6 +397,7 @@ export function apply(ctx) {
|
|
|
344
397
|
return {
|
|
345
398
|
load, setLimit, selectModel, setMonitored, setOnFull, setReset,
|
|
346
399
|
setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle,
|
|
400
|
+
setMusicEnabled, setMusicVolume, setMusicStyle,
|
|
347
401
|
};
|
|
348
402
|
};
|
|
349
403
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
@@ -66,6 +66,13 @@ export declare const zh: {
|
|
|
66
66
|
checkUpdatesLabel: string;
|
|
67
67
|
dimWhenIdleLabel: string;
|
|
68
68
|
dimWhenIdleHint: string;
|
|
69
|
+
musicLabel: string;
|
|
70
|
+
musicHint: string;
|
|
71
|
+
musicVolume: string;
|
|
72
|
+
musicStyle: string;
|
|
73
|
+
musicStyleMajor: string;
|
|
74
|
+
musicStyleMinor: string;
|
|
75
|
+
musicStylePentatonic: string;
|
|
69
76
|
checkUpdatesNow: string;
|
|
70
77
|
checkingUpdates: string;
|
|
71
78
|
checkUpToDate: string;
|
|
@@ -135,6 +142,13 @@ export declare const en: {
|
|
|
135
142
|
checkUpdatesLabel: string;
|
|
136
143
|
dimWhenIdleLabel: string;
|
|
137
144
|
dimWhenIdleHint: string;
|
|
145
|
+
musicLabel: string;
|
|
146
|
+
musicHint: string;
|
|
147
|
+
musicVolume: string;
|
|
148
|
+
musicStyle: string;
|
|
149
|
+
musicStyleMajor: string;
|
|
150
|
+
musicStyleMinor: string;
|
|
151
|
+
musicStylePentatonic: string;
|
|
138
152
|
checkUpdatesNow: string;
|
|
139
153
|
checkingUpdates: string;
|
|
140
154
|
checkUpToDate: string;
|
|
@@ -66,6 +66,13 @@ export const zh = {
|
|
|
66
66
|
checkUpdatesLabel: '自动检查更新',
|
|
67
67
|
dimWhenIdleLabel: '失焦时窗口透明',
|
|
68
68
|
dimWhenIdleHint: '鼠标移开面板时降低不透明度,输入文字时更透明;鼠标悬停恢复',
|
|
69
|
+
musicLabel: '动作音乐',
|
|
70
|
+
musicHint: '根据 Harness 动作实时作曲:请求开始、工具调用、回合结束等(需点击开关启动音频;检测到 MIDI 输出时同步发送 MIDI)',
|
|
71
|
+
musicVolume: '音量',
|
|
72
|
+
musicStyle: '风格',
|
|
73
|
+
musicStyleMajor: '大调',
|
|
74
|
+
musicStyleMinor: '小调',
|
|
75
|
+
musicStylePentatonic: '五声',
|
|
69
76
|
checkUpdatesNow: '立即检查',
|
|
70
77
|
checkingUpdates: '检查中…',
|
|
71
78
|
checkUpToDate: '已是最新',
|
|
@@ -133,6 +140,13 @@ export const en = {
|
|
|
133
140
|
checkUpdatesLabel: 'Check for updates automatically',
|
|
134
141
|
dimWhenIdleLabel: 'Dim panel when unfocused',
|
|
135
142
|
dimWhenIdleHint: 'Fade the panel out when the pointer is away (more while typing); hover to restore',
|
|
143
|
+
musicLabel: 'Action music',
|
|
144
|
+
musicHint: 'Live-composes from Harness actions: request starts, tool calls, turn ends, etc. (click the switch to start audio; a detected MIDI output is driven as well)',
|
|
145
|
+
musicVolume: 'Volume',
|
|
146
|
+
musicStyle: 'Style',
|
|
147
|
+
musicStyleMajor: 'Major',
|
|
148
|
+
musicStyleMinor: 'Minor',
|
|
149
|
+
musicStylePentatonic: 'Pentatonic',
|
|
136
150
|
checkUpdatesNow: 'Check now',
|
|
137
151
|
checkingUpdates: 'Checking…',
|
|
138
152
|
checkUpToDate: 'Up to date',
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-composed soundtrack driven by host actions (turns, steps, tool calls,
|
|
3
|
+
* etc.). Outputs through Web Audio (built-in synthesis) by default, and also
|
|
4
|
+
* sends MIDI NoteOn/NoteOff to the first available MIDI output when one is
|
|
5
|
+
* detected (both play simultaneously when MIDI is present — use the panel
|
|
6
|
+
* volume for overall level, the MIDI output has its own gain on the receiver).
|
|
7
|
+
*
|
|
8
|
+
* The composition is intentionally simple: a fixed chord progression under a
|
|
9
|
+
* scale-constrained melody, with one short musical gesture per host action.
|
|
10
|
+
* It is meant as gentle ambience while waiting for LLM responses, not as a
|
|
11
|
+
* full piece — everything stays in key so you can never hit a wrong note.
|
|
12
|
+
*
|
|
13
|
+
* @module @jxgame2020/dsh-token-quota/music
|
|
14
|
+
*/
|
|
15
|
+
import type { TokenQuotaMusicAction, TokenQuotaMusicStyle } from '../types.ts';
|
|
16
|
+
/** Live-composed music engine driven by host actions. */
|
|
17
|
+
export declare class TokenQuotaMusic {
|
|
18
|
+
private audio;
|
|
19
|
+
private midiOutput;
|
|
20
|
+
private midiDetected;
|
|
21
|
+
private enabled;
|
|
22
|
+
private volume;
|
|
23
|
+
private style;
|
|
24
|
+
/** Current position in the chord progression (advances per step). */
|
|
25
|
+
private chordIndex;
|
|
26
|
+
/** Step counter — used for melody degree selection. */
|
|
27
|
+
private stepCount;
|
|
28
|
+
/** Tool counter — used for tool-note pitch. */
|
|
29
|
+
private toolCount;
|
|
30
|
+
/** Base MIDI note of the current key. C4 = middle C. */
|
|
31
|
+
private static readonly BASE_MIDI;
|
|
32
|
+
/** True once the audio context has been started (needs a user gesture). */
|
|
33
|
+
get started(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Start the audio context and try to find a MIDI output.
|
|
36
|
+
* Must be called inside a user gesture (the panel switch click) so the
|
|
37
|
+
* AudioContext can resume. Safe to call multiple times.
|
|
38
|
+
*/
|
|
39
|
+
ensureStarted(): Promise<boolean>;
|
|
40
|
+
setEnabled(value: boolean): void;
|
|
41
|
+
setVolume(value: number): void;
|
|
42
|
+
setStyle(value: TokenQuotaMusicStyle): void;
|
|
43
|
+
/** Handle one incoming host action and turn it into sound. */
|
|
44
|
+
onAction(action: TokenQuotaMusicAction): void;
|
|
45
|
+
private currentChordRoot;
|
|
46
|
+
private currentChordQuality;
|
|
47
|
+
/** Intervals of an ascending arpeggio from the chord root. */
|
|
48
|
+
private arpeggioDegrees;
|
|
49
|
+
/** Play one note: Web Audio + MIDI simultaneously when available. */
|
|
50
|
+
private playNote;
|
|
51
|
+
/** Play a three-note chord plus a bass octave below. */
|
|
52
|
+
private playChord;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=music.d.ts.map
|