@linxin666/dsh-usage 0.3.22 → 0.3.24
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/README.i18n.yaml +2 -2
- package/README.md +6 -8
- package/README.zh.md +6 -8
- package/lib/client.js +749 -78
- package/lib/client.js.map +1 -1
- package/lib/index.js +171 -201
- package/lib/types/client/UsageSectionCard.d.ts +0 -1
- package/lib/types/client/UsageSidebarPanel.d.ts +34 -0
- package/lib/types/client/body-mutations.d.ts +45 -0
- package/lib/types/client/locales.d.ts +9 -4
- package/lib/types/client/sidebar-entry-core.d.ts +87 -0
- package/lib/types/client/sidebar-entry.d.ts +35 -0
- package/lib/types/client/sidebar-panel-mount.d.ts +40 -0
- package/lib/types/client/voucher.d.ts +2 -2
- package/lib/types/core/provider-routes.d.ts +26 -0
- package/lib/types/core/types.d.ts +14 -1
- package/lib/types/host/access.d.ts +17 -0
- package/lib/types/host/pair-access.d.ts +34 -0
- package/lib/types/host/routes.d.ts +16 -7
- package/lib/types/host/usage-service.d.ts +28 -73
- package/lib/types/index.d.ts +0 -2
- package/package.json +4 -3
- package/src/client/UsageSectionCard.tsx +1 -15
- package/src/client/UsageSidebarPanel.tsx +196 -0
- package/src/client/body-mutations.ts +133 -0
- package/src/client/index.ts +22 -1
- package/src/client/locales.ts +20 -10
- package/src/client/sidebar-entry-core.ts +321 -0
- package/src/client/sidebar-entry.ts +101 -0
- package/src/client/sidebar-panel-mount.tsx +113 -0
- package/src/client/usage.module.css +252 -0
- package/src/client/voucher.ts +2 -2
- package/src/core/adapters.ts +40 -6
- package/src/core/provider-routes.ts +37 -0
- package/src/core/types.ts +14 -1
- package/src/host/access.ts +21 -0
- package/src/host/pair-access.ts +49 -0
- package/src/host/routes.ts +19 -10
- package/src/host/usage-service.ts +65 -214
- package/src/index.ts +1 -6
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar usage panel (issue #1592): the usage body seated directly under the
|
|
3
|
+
* sidebar entry row. The row carries the panel's controls (refresh, collapse
|
|
4
|
+
* chevron), so this component renders only the content: expanded, plan
|
|
5
|
+
* providers show their quota windows and balance-only providers their
|
|
6
|
+
* remaining amount; collapsed, a one-line strip shows the current session
|
|
7
|
+
* provider's today usage (tokens, calls, and cost when priced) from the same
|
|
8
|
+
* /api/dsh-usage/overview document the settings section renders. The open
|
|
9
|
+
* state lives in the mount (persisted to localStorage) and arrives through
|
|
10
|
+
* the open face; polling runs at 10s expanded, 30s collapsed, and pauses
|
|
11
|
+
* while the page is hidden.
|
|
12
|
+
* @module @linxin666/dsh-usage/client/UsageSidebarPanel
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect, useSyncExternalStore, type ReactNode } from 'react'
|
|
16
|
+
import { t } from './locales.ts'
|
|
17
|
+
import styles from './usage.module.css'
|
|
18
|
+
import { formatTokens } from './UsageSectionCard.tsx'
|
|
19
|
+
import type { UsageStoreInstance } from './usage-store.ts'
|
|
20
|
+
import { adapterFor } from '../core/adapters.ts'
|
|
21
|
+
import { totalTokens } from '../core/ledger.ts'
|
|
22
|
+
import type { ProviderSnapshotView, UsageOverviewView, UsageTokenTotals } from '../core/types.ts'
|
|
23
|
+
import { addTotals, emptyTotals } from '../core/types.ts'
|
|
24
|
+
|
|
25
|
+
/** Expanded-state face the owning mount supplies (subscribe + current value). */
|
|
26
|
+
export interface OpenStateSource {
|
|
27
|
+
subscribe(listener: () => void): () => void
|
|
28
|
+
isOpen(): boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Props of the sidebar panel (the apply body supplies the store and poller). */
|
|
32
|
+
export interface UsageSidebarPanelProps {
|
|
33
|
+
store: UsageStoreInstance
|
|
34
|
+
/** Fetch one overview now. */
|
|
35
|
+
poll: () => void
|
|
36
|
+
/** Expanded state owned by the mount; false renders nothing and pauses polling. */
|
|
37
|
+
open: OpenStateSource
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Poll cadence while the panel is expanded and the page visible. */
|
|
41
|
+
const PANEL_POLL_MS = 10_000
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Poll cadence of the collapsed strip: the current session's usage should
|
|
45
|
+
* stay visibly live, but a collapsed surface pays a relaxed cadence instead
|
|
46
|
+
* of the expanded one.
|
|
47
|
+
*/
|
|
48
|
+
const COLLAPSED_POLL_MS = 30_000
|
|
49
|
+
|
|
50
|
+
/** Bar tone, matching the settings section's thresholds. */
|
|
51
|
+
function tone(percent: number): string {
|
|
52
|
+
if (percent >= 90) return styles.barLow
|
|
53
|
+
if (percent >= 70) return styles.barWarn
|
|
54
|
+
return styles.sidebarBarFill
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The collapsed-strip facts for the current provider: display name plus
|
|
59
|
+
* today's adapter-family totals (tokens, calls, cost). The host serves them
|
|
60
|
+
* strip-ready inside `current`; an older host document is derived against
|
|
61
|
+
* here. Undefined on a day without usage — a strip about nothing is noise.
|
|
62
|
+
*/
|
|
63
|
+
function collapsedSummary(snapshot: UsageOverviewView): { name: string; totals: UsageTokenTotals } | undefined {
|
|
64
|
+
const provider = snapshot.current.provider
|
|
65
|
+
if (provider === undefined) return undefined
|
|
66
|
+
const name = snapshot.current.displayName
|
|
67
|
+
?? snapshot.providers.find((entry) => entry.provider === provider)?.displayName
|
|
68
|
+
?? provider
|
|
69
|
+
if (snapshot.current.today !== undefined) {
|
|
70
|
+
const totals = snapshot.current.today
|
|
71
|
+
return totals.calls > 0 && totalTokens(totals) > 0 ? { name, totals } : undefined
|
|
72
|
+
}
|
|
73
|
+
const family = adapterFor(provider)
|
|
74
|
+
let merged = emptyTotals()
|
|
75
|
+
for (const row of snapshot.usage.today.providers) {
|
|
76
|
+
const same = family === undefined ? row.provider === provider : adapterFor(row.provider) === family
|
|
77
|
+
if (same) merged = addTotals(merged, row.totals)
|
|
78
|
+
}
|
|
79
|
+
return merged.calls > 0 && totalTokens(merged) > 0 ? { name, totals: merged } : undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A provider is worth listing when it is configured and carries a fact. */
|
|
83
|
+
function listable(provider: ProviderSnapshotView): boolean {
|
|
84
|
+
return provider.credential !== 'none' && (provider.plan !== undefined || provider.balance !== undefined)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One provider row: plan windows when present, otherwise the balance line. */
|
|
88
|
+
function ProviderBlock(props: { provider: ProviderSnapshotView }): ReactNode {
|
|
89
|
+
const { provider } = props
|
|
90
|
+
const windows = provider.plan?.windows ?? []
|
|
91
|
+
if (windows.length > 0) {
|
|
92
|
+
return (
|
|
93
|
+
<div className={styles.sidebarProvider} data-dsh-part="sidebar-provider">
|
|
94
|
+
<div className={styles.sidebarProviderHead}>
|
|
95
|
+
<span className={styles.sidebarProviderName}>{provider.displayName}</span>
|
|
96
|
+
</div>
|
|
97
|
+
{windows.map((window) => (
|
|
98
|
+
<div key={window.key} className={styles.sidebarWindowRow} data-dsh-part="sidebar-window">
|
|
99
|
+
<span>{window.name ?? t(`usage.plan.windows.${window.key}`)}</span>
|
|
100
|
+
{window.percent !== undefined && (
|
|
101
|
+
<>
|
|
102
|
+
<span className={styles.sidebarBar}>
|
|
103
|
+
<span className={tone(window.percent)} style={{ width: `${Math.min(100, Math.max(0, window.percent))}%` }} />
|
|
104
|
+
</span>
|
|
105
|
+
<span>{window.percent >= 10 ? Math.round(window.percent) : window.percent.toFixed(1)}%</span>
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
if (provider.balance !== undefined) {
|
|
114
|
+
return (
|
|
115
|
+
<div className={styles.sidebarProvider} data-dsh-part="sidebar-provider">
|
|
116
|
+
<div className={styles.sidebarProviderHead}>
|
|
117
|
+
<span className={styles.sidebarProviderName}>{provider.displayName}</span>
|
|
118
|
+
<span className={styles.sidebarBalance}>
|
|
119
|
+
{t('usage.sidebar.balanceLeft', { balance: `${provider.balance.currency} ${provider.balance.totalBalance}` })}
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Render the sidebar usage panel body.
|
|
130
|
+
* @param props - store, the poll callback, and the mount's open-state face.
|
|
131
|
+
* @returns the panel content, or null while collapsed.
|
|
132
|
+
*/
|
|
133
|
+
export function UsageSidebarPanel(props: UsageSidebarPanelProps): ReactNode {
|
|
134
|
+
const { store, poll, open: openState } = props
|
|
135
|
+
const ui = useSyncExternalStore(store.subscribe, store.getSnapshot)
|
|
136
|
+
const open = useSyncExternalStore(openState.subscribe, openState.isOpen)
|
|
137
|
+
|
|
138
|
+
// The strip stays live at a relaxed cadence while collapsed; the
|
|
139
|
+
// expanded panel keeps the section's own 10s cadence. A hidden tab always
|
|
140
|
+
// pauses, matching the settings section's gating.
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
poll()
|
|
143
|
+
const cadence = open ? PANEL_POLL_MS : COLLAPSED_POLL_MS
|
|
144
|
+
let timer: number | undefined
|
|
145
|
+
const start = (): void => {
|
|
146
|
+
if (timer === undefined && document.visibilityState === 'visible') timer = window.setInterval(poll, cadence)
|
|
147
|
+
}
|
|
148
|
+
const onVisibility = (): void => {
|
|
149
|
+
if (document.visibilityState === 'visible') {
|
|
150
|
+
poll()
|
|
151
|
+
start()
|
|
152
|
+
} else if (timer !== undefined) {
|
|
153
|
+
window.clearInterval(timer)
|
|
154
|
+
timer = undefined
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
start()
|
|
158
|
+
document.addEventListener('visibilitychange', onVisibility)
|
|
159
|
+
return () => {
|
|
160
|
+
if (timer !== undefined) window.clearInterval(timer)
|
|
161
|
+
document.removeEventListener('visibilitychange', onVisibility)
|
|
162
|
+
}
|
|
163
|
+
}, [poll, open])
|
|
164
|
+
|
|
165
|
+
const snapshot: UsageOverviewView | null = ui.snapshot
|
|
166
|
+
|
|
167
|
+
if (!open) {
|
|
168
|
+
const summary = snapshot === null ? undefined : collapsedSummary(snapshot)
|
|
169
|
+
if (summary === undefined) return null
|
|
170
|
+
return (
|
|
171
|
+
<div className={styles.sidebarSummary} data-dsh-plugin="usage" data-dsh-part="sidebar-summary">
|
|
172
|
+
<span className={styles.sidebarSummaryName}>{summary.name}</span>
|
|
173
|
+
<span className={styles.sidebarSummaryFacts}>
|
|
174
|
+
{t('usage.sidebar.todayUsage', { tokens: formatTokens(totalTokens(summary.totals)) })}
|
|
175
|
+
{' · '}
|
|
176
|
+
{t('usage.calls', { n: summary.totals.calls })}
|
|
177
|
+
{summary.totals.cost > 0 ? ` · ¥${summary.totals.cost.toFixed(2)}` : ''}
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const providers = snapshot === null ? [] : snapshot.providers.filter(listable)
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className={styles.sidebarPanel} data-dsh-plugin="usage" data-dsh-part="sidebar-panel">
|
|
187
|
+
{ui.status === 'error'
|
|
188
|
+
? <span className={styles.sidebarMuted}>{t('usage.sidebar.error', { error: ui.error ?? '' })}</span>
|
|
189
|
+
: snapshot === null
|
|
190
|
+
? <span className={styles.sidebarMuted}>{t('usage.sidebar.loading')}</span>
|
|
191
|
+
: providers.length === 0
|
|
192
|
+
? <span className={styles.sidebarMuted}>{t('usage.sidebar.empty')}</span>
|
|
193
|
+
: providers.map((provider) => <ProviderBlock key={provider.provider} provider={provider} />)}
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/client/body-mutations.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* One `document.body` MutationObserver per page, shared by every family
|
|
4
|
+
* plugin bundle.
|
|
5
|
+
*
|
|
6
|
+
* Family plugins that inject at the DOM level must notice when the shell
|
|
7
|
+
* re-renders around their injected node: the sidebar entry rows
|
|
8
|
+
* (sidebar-entry-core), the center-column panel containers
|
|
9
|
+
* (panel-mount-core), and the aggregate shell's column shims. Each consumer
|
|
10
|
+
* used to install its OWN `MutationObserver` on `document.body` with
|
|
11
|
+
* `{ childList: true, subtree: true }`, so a page carrying N family plugins
|
|
12
|
+
* paid N native observers and N callback invocations for EVERY mutation batch
|
|
13
|
+
* the app produced — chat token streaming alone produces many per second, and
|
|
14
|
+
* the count grows with the number of installed plugins.
|
|
15
|
+
*
|
|
16
|
+
* This module keeps exactly ONE body observer per page. The registry lives on
|
|
17
|
+
* `globalThis` under a `Symbol.for` key, so the per-package generated copies
|
|
18
|
+
* of this file (scripts/sync-shared.mjs) and separately bundled plugins all
|
|
19
|
+
* reach the same hub at runtime instead of one hub per module instance.
|
|
20
|
+
* Record subscribers receive the mutations accumulated since the last flush.
|
|
21
|
+
* Consumers that only re-check their DOM use subscribeBodyInvalidations:
|
|
22
|
+
* when all subscribers use that path, the hub retains no mutation records
|
|
23
|
+
* (or detached subtrees) while a background page's animation frames pause.
|
|
24
|
+
* Both paths run at most once per frame. The last subscriber disconnects the
|
|
25
|
+
* observer, cancels the pending frame and releases its records.
|
|
26
|
+
*
|
|
27
|
+
* Failure policy: without a DOM or `MutationObserver` the subscription is a
|
|
28
|
+
* no-op disposer (the same silence the per-consumer observers had), and when
|
|
29
|
+
* `requestAnimationFrame` is unavailable the flush runs synchronously so a
|
|
30
|
+
* subscriber is never silently dropped. A throwing subscriber cannot stop the
|
|
31
|
+
* others.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** The page-wide hub: one observer plus its subscribers and pending records. */
|
|
35
|
+
interface BodyMutationHub {
|
|
36
|
+
observer: MutationObserver
|
|
37
|
+
subscribers: Set<(records: MutationRecord[]) => void>
|
|
38
|
+
pending: MutationRecord[]
|
|
39
|
+
scheduled: boolean
|
|
40
|
+
frame?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Cross-bundle registry key; `Symbol.for` so every module copy agrees. */
|
|
44
|
+
const HUB_KEY = Symbol.for('dsh-web.body-mutation-hub')
|
|
45
|
+
const INVALIDATION_ONLY = Symbol.for('dsh-web.body-mutation-invalidation')
|
|
46
|
+
type Subscriber = ((records: MutationRecord[]) => void) & { [INVALIDATION_ONLY]?: true }
|
|
47
|
+
|
|
48
|
+
function needsRecords(subscribers: Set<(records: MutationRecord[]) => void>): boolean {
|
|
49
|
+
for (const listener of subscribers) {
|
|
50
|
+
if (!(listener as Subscriber)[INVALIDATION_ONLY]) return true
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Subscribe to a coalesced DOM re-check without retaining mutation records.
|
|
57
|
+
* The marked wrapper also works with an older hub, which delivers records
|
|
58
|
+
* that it simply ignores until a page reload picks up the updated hub.
|
|
59
|
+
*/
|
|
60
|
+
export function subscribeBodyInvalidations(subscriber: () => void): () => void {
|
|
61
|
+
const listener: Subscriber = () => { subscriber() }
|
|
62
|
+
listener[INVALIDATION_ONLY] = true
|
|
63
|
+
return subscribeBodyMutations(listener)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Subscribe to body-level childList mutations.
|
|
68
|
+
* @param subscriber - called at most once per animation frame with the records
|
|
69
|
+
* collected since the previous flush; must be safe to run repeatedly.
|
|
70
|
+
* @returns the disposer removing this subscriber (and the observer when it was
|
|
71
|
+
* the last one).
|
|
72
|
+
*/
|
|
73
|
+
export function subscribeBodyMutations(subscriber: (records: MutationRecord[]) => void): () => void {
|
|
74
|
+
if (typeof globalThis === 'undefined' || typeof document === 'undefined') return () => {}
|
|
75
|
+
if (typeof MutationObserver !== 'function') return () => {}
|
|
76
|
+
const registry = globalThis as unknown as Record<symbol, unknown>
|
|
77
|
+
let hub = registry[HUB_KEY] as BodyMutationHub | undefined
|
|
78
|
+
if (hub === undefined) {
|
|
79
|
+
const subscribers = new Set<(records: MutationRecord[]) => void>()
|
|
80
|
+
const created: BodyMutationHub = {
|
|
81
|
+
observer: undefined as unknown as MutationObserver,
|
|
82
|
+
subscribers,
|
|
83
|
+
pending: [],
|
|
84
|
+
scheduled: false,
|
|
85
|
+
}
|
|
86
|
+
const flush = (): void => {
|
|
87
|
+
created.frame = undefined
|
|
88
|
+
created.scheduled = false
|
|
89
|
+
const batch = created.pending
|
|
90
|
+
created.pending = []
|
|
91
|
+
for (const listener of [...subscribers]) {
|
|
92
|
+
if (!subscribers.has(listener)) continue
|
|
93
|
+
try {
|
|
94
|
+
listener(batch)
|
|
95
|
+
} catch {
|
|
96
|
+
// One subscriber must never break the others; the next flush retries it.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const schedule = (): void => {
|
|
101
|
+
if (created.scheduled) return
|
|
102
|
+
created.scheduled = true
|
|
103
|
+
if (typeof requestAnimationFrame === 'function') created.frame = requestAnimationFrame(flush)
|
|
104
|
+
else flush()
|
|
105
|
+
}
|
|
106
|
+
created.observer = new MutationObserver((records) => {
|
|
107
|
+
if (needsRecords(subscribers)) {
|
|
108
|
+
for (const record of records) created.pending.push(record)
|
|
109
|
+
}
|
|
110
|
+
schedule()
|
|
111
|
+
})
|
|
112
|
+
created.observer.observe(document.body ?? document.documentElement, { childList: true, subtree: true })
|
|
113
|
+
registry[HUB_KEY] = created
|
|
114
|
+
hub = created
|
|
115
|
+
}
|
|
116
|
+
const active = hub
|
|
117
|
+
active.subscribers.add(subscriber)
|
|
118
|
+
let subscribed = true
|
|
119
|
+
return () => {
|
|
120
|
+
if (!subscribed) return
|
|
121
|
+
subscribed = false
|
|
122
|
+
active.subscribers.delete(subscriber)
|
|
123
|
+
if (!needsRecords(active.subscribers)) active.pending = []
|
|
124
|
+
if (active.subscribers.size === 0 && registry[HUB_KEY] === active) {
|
|
125
|
+
active.observer.disconnect()
|
|
126
|
+
if (active.frame !== undefined && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(active.frame)
|
|
127
|
+
active.frame = undefined
|
|
128
|
+
active.pending = []
|
|
129
|
+
active.scheduled = false
|
|
130
|
+
delete registry[HUB_KEY]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -15,8 +15,9 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
|
15
15
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
16
16
|
// Type-only: pulls the ctx.slots merge (the renderer owns the slot registry since 0.1.2).
|
|
17
17
|
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
18
|
-
import { createElement } from 'react'
|
|
19
18
|
import { createUsageStore, type UsageStoreInstance } from './usage-store.ts'
|
|
19
|
+
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
20
|
+
import { mountUsagePanel } from './sidebar-panel-mount.tsx'
|
|
20
21
|
import { UsageSectionCard, type UsageSectionFace, type UsageSettings } from './UsageSectionCard.tsx'
|
|
21
22
|
import { NS, en, zh } from './locales.ts'
|
|
22
23
|
import type { UsageOverviewView } from '../core/types.ts'
|
|
@@ -123,6 +124,26 @@ export function apply(ctx: ClientContext): void {
|
|
|
123
124
|
|
|
124
125
|
const face = (): UsageSectionFace => ({ store, poll, refresh, settings: settingsScope })
|
|
125
126
|
|
|
127
|
+
// Sidebar surface (issue #1592): the entry row seats the panel's controls
|
|
128
|
+
// (refresh, collapse chevron); the collapsible panel directly under it reads
|
|
129
|
+
// the same overview document. The mount owns the persisted open state; the
|
|
130
|
+
// entry row mirrors it as highlight and chevron direction.
|
|
131
|
+
const panel = mountUsagePanel({ store, poll })
|
|
132
|
+
const disposeEntry = mountSidebarEntry({
|
|
133
|
+
onToggle: () => { panel.toggle() },
|
|
134
|
+
onRefresh: () => { refresh() },
|
|
135
|
+
isOpen: () => panel.isOpen(),
|
|
136
|
+
subscribeOpen: (listener) => panel.subscribe(listener),
|
|
137
|
+
}, ctx.locale)
|
|
138
|
+
ctx.effect(() => () => {
|
|
139
|
+
try {
|
|
140
|
+
disposeEntry()
|
|
141
|
+
} catch {
|
|
142
|
+
// Entry row already gone (teardown race).
|
|
143
|
+
}
|
|
144
|
+
panel.dispose()
|
|
145
|
+
}, 'dsh-usage: sidebar panel')
|
|
146
|
+
|
|
126
147
|
ctx.slots.inject('settings.section', () => {
|
|
127
148
|
try {
|
|
128
149
|
const unregister = ctx.slots.register({
|
package/src/client/locales.ts
CHANGED
|
@@ -48,12 +48,8 @@ export const zh = {
|
|
|
48
48
|
'usage.config.title': '设置',
|
|
49
49
|
'usage.config.enabled': '启用插件',
|
|
50
50
|
'usage.config.pollIntervalSec': '轮询间隔(秒)',
|
|
51
|
-
'usage.config.bubbleMode': '宠物气泡',
|
|
52
|
-
'usage.config.bubbleMode.always': '常驻显示',
|
|
53
|
-
'usage.config.bubbleMode.change': '仅变化时',
|
|
54
|
-
'usage.config.bubbleMode.off': '关闭',
|
|
55
51
|
'usage.bank.title': '鲸元券',
|
|
56
|
-
'usage.bank.hint': '官方 API 每消耗
|
|
52
|
+
'usage.bank.hint': '官方 API 每消耗 100 万 tokens 铸造 1 鲸元;保存或分享这张票券。',
|
|
57
53
|
'usage.bank.noUsage': '暂无 DeepSeek 官方用量数据(统计自插件启用起)',
|
|
58
54
|
'usage.bank.minted': '累计铸造 {minted} 鲸元({tokens} tokens)',
|
|
59
55
|
'usage.bank.spend.observed': '官方余额实测花费 ¥{cost}(自 {since} 起)',
|
|
@@ -62,6 +58,15 @@ export const zh = {
|
|
|
62
58
|
'usage.bank.save': '保存图片',
|
|
63
59
|
'usage.bank.share': '分享',
|
|
64
60
|
'usage.bank.drawError': '票券生成失败:{error}',
|
|
61
|
+
'usage.sidebar.entry.label': '用量',
|
|
62
|
+
'usage.sidebar.entry.tooltip': '用量面板:套餐配额与余额',
|
|
63
|
+
'usage.sidebar.toggle.collapse': '折叠',
|
|
64
|
+
'usage.sidebar.toggle.expand': '展开',
|
|
65
|
+
'usage.sidebar.empty': '没有已配置的套餐或余额数据。',
|
|
66
|
+
'usage.sidebar.error': '加载失败:{error}',
|
|
67
|
+
'usage.sidebar.loading': '正在加载用量数据…',
|
|
68
|
+
'usage.sidebar.balanceLeft': '剩余 {balance}',
|
|
69
|
+
'usage.sidebar.todayUsage': '今日 {tokens} tokens',
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/** English mirror; every zh key present. */
|
|
@@ -105,12 +110,8 @@ export const en: Record<UsageKey, string> = {
|
|
|
105
110
|
'usage.config.title': 'Settings',
|
|
106
111
|
'usage.config.enabled': 'Enable plugin',
|
|
107
112
|
'usage.config.pollIntervalSec': 'Poll interval (seconds)',
|
|
108
|
-
'usage.config.bubbleMode': 'Pet bubble',
|
|
109
|
-
'usage.config.bubbleMode.always': 'Always visible',
|
|
110
|
-
'usage.config.bubbleMode.change': 'On change',
|
|
111
|
-
'usage.config.bubbleMode.off': 'Off',
|
|
112
113
|
'usage.bank.title': 'Whale-yuan voucher',
|
|
113
|
-
'usage.bank.hint': 'Every 1,000 tokens spent on the official API mint one whale yuan; save or share the note.',
|
|
114
|
+
'usage.bank.hint': 'Every 1,000,000 tokens spent on the official API mint one whale yuan; save or share the note.',
|
|
114
115
|
'usage.bank.noUsage': 'No official DeepSeek usage yet (counting starts when the plugin is enabled)',
|
|
115
116
|
'usage.bank.minted': 'Minted {minted} whale yuan ({tokens} tokens)',
|
|
116
117
|
'usage.bank.spend.observed': 'Spent ¥{cost} observed on the official balance (watching since {since})',
|
|
@@ -119,6 +120,15 @@ export const en: Record<UsageKey, string> = {
|
|
|
119
120
|
'usage.bank.save': 'Save image',
|
|
120
121
|
'usage.bank.share': 'Share',
|
|
121
122
|
'usage.bank.drawError': 'Failed to render the voucher: {error}',
|
|
123
|
+
'usage.sidebar.entry.label': 'Usage',
|
|
124
|
+
'usage.sidebar.entry.tooltip': 'Usage panel: plan quotas and balances',
|
|
125
|
+
'usage.sidebar.toggle.collapse': 'Collapse',
|
|
126
|
+
'usage.sidebar.toggle.expand': 'Expand',
|
|
127
|
+
'usage.sidebar.empty': 'No plan or balance data configured.',
|
|
128
|
+
'usage.sidebar.error': 'Loading failed: {error}',
|
|
129
|
+
'usage.sidebar.loading': 'Loading usage data…',
|
|
130
|
+
'usage.sidebar.balanceLeft': '{balance} left',
|
|
131
|
+
'usage.sidebar.todayUsage': '{tokens} tokens today',
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
export type UsageKey = keyof typeof zh
|