@cassius0924/dsh-usage-dashboard 0.8.1 → 0.8.3

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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The peak-window breaker gate.
3
+ *
4
+ * DeepSeek's `agent/pre-step` is a waterfall: a listener receives the proposed
5
+ * step and either calls `next()` (let it proceed) or withholds it (park). This
6
+ * gate parks steps only when all conditions hold:
7
+ *
8
+ * - the master switch is on,
9
+ * - the step's agent (its session) opted in via its own switch,
10
+ * - a peak window is open,
11
+ * - the turn was started by a person (`source.kind === 'user'`).
12
+ *
13
+ * Parked steps resume when the window settles at 18:00, when the user lets them
14
+ * continue from the UI, or when the owning turn is cancelled (`signal` aborts —
15
+ * abort must let the loop unwind normally instead of throwing here).
16
+ *
17
+ * Steps after the first one in a turn can carry no messages, so the "was this a
18
+ * user turn" decision is remembered per `(session, turn)`. The same memory
19
+ * records a manual release so the user's "continue now" is not undone by the
20
+ * very next step of that turn. Both are bounded: sessions come and go, but the
21
+ * map must not grow forever on a long-lived host.
22
+ *
23
+ * Timer handling is split behind `BreakerTimerFace` so tests can drive a fake
24
+ * clock; the production face uses a real `setTimeout`, unrefs it (a parked turn
25
+ * must never keep the process alive) and re-arms long waits in chunks, because
26
+ * `setTimeout` cannot represent delays above `MAX_BREAKER_TIMER_DELAY_MS`.
27
+ */
28
+ import { type BreakerSettings } from './contract.ts';
29
+ import { type BreakerMessageFace } from './breaker.ts';
30
+ /** The timer seam; production uses `systemBreakerTimers`, tests a fake clock. */
31
+ export interface BreakerTimerFace {
32
+ schedule(callback: () => void, delayMs: number): unknown;
33
+ cancel(handle: unknown): void;
34
+ }
35
+ /** `setTimeout`'s documented maximum delay; longer waits are re-armed. */
36
+ export declare const MAX_BREAKER_TIMER_DELAY_MS = 2147483647;
37
+ /** Production timers: a real unref'd `setTimeout`. */
38
+ export declare const systemBreakerTimers: BreakerTimerFace;
39
+ /** Clamp one wait into the range a single `setTimeout` can carry. */
40
+ export declare const clampBreakerTimerDelay: (delayMs: number) => number;
41
+ export interface BreakerGateOptions {
42
+ /** Settings to start from; the host loads persisted ones asynchronously. */
43
+ initialSettings?: BreakerSettings;
44
+ /** Clock seam, epoch ms; defaults to `Date.now`. */
45
+ now?: () => number;
46
+ /** Timer seam; defaults to the real clock. */
47
+ timers?: BreakerTimerFace;
48
+ }
49
+ export declare class BreakerGate {
50
+ #private;
51
+ constructor(options?: BreakerGateOptions);
52
+ get settings(): BreakerSettings;
53
+ /** Steps parked right now, across every session. */
54
+ get held(): number;
55
+ /** Session ids with at least one parked step, sorted for the wire projection. */
56
+ get heldSessions(): string[];
57
+ /**
58
+ * Park one proposed step until it may proceed. Resolves immediately when the
59
+ * step is not subject to the breaker (session not opted in, off-peak,
60
+ * machine-started turn, turn already released by hand) or when the turn is
61
+ * aborted/disposed.
62
+ *
63
+ * The listener wrapping this must call `next()` after it resolves — this
64
+ * method never throws and never returns a decision of its own.
65
+ *
66
+ * `sessionId` is the agent id from the payload; an agent without one cannot
67
+ * be matched against the opt-in list, so its steps pass.
68
+ */
69
+ hold(sessionId: string | undefined, turn: number, messages: readonly BreakerMessageFace[], signal: AbortSignal): Promise<void>;
70
+ /**
71
+ * Let parked steps continue now. `sessionId` narrows it to one session; no
72
+ * argument releases every parked step (the UI's "continue all" button). The
73
+ * released turns are remembered, because re-parking their next step would
74
+ * contradict the user's instruction.
75
+ * @returns how many parked steps were released.
76
+ */
77
+ release(sessionId?: string): number;
78
+ /**
79
+ * Apply new settings. Turning off the master switch — or a session's own
80
+ * switch — releases the affected parked steps immediately and without
81
+ * remembering a bypass: that is the user's decision, not a request for one
82
+ * particular turn to run.
83
+ */
84
+ update(settings: BreakerSettings): void;
85
+ /** Release everything and stop the timer; called when the plugin unloads. */
86
+ dispose(): void;
87
+ /** (Re)arm the single settle timer for the current peak window. */
88
+ private armTimer;
89
+ private onTimer;
90
+ private clearTimer;
91
+ private releaseRecord;
92
+ private markReleased;
93
+ private rememberTurn;
94
+ }
@@ -0,0 +1,28 @@
1
+ import { type BreakerSettings } from './contract.ts';
2
+ /**
3
+ * Resolve the settings file path from the environment. The env parameter is
4
+ * injectable so tests can exercise the `DSH_HOME` branch without touching the
5
+ * process environment.
6
+ * @param env - environment to read `DSH_HOME` from; defaults to `process.env`.
7
+ */
8
+ export declare function breakerSettingsPath(env?: NodeJS.ProcessEnv): string;
9
+ /** Shared IO options; the defaults are what the running plugin uses. */
10
+ export interface BreakerSettingsIoOptions {
11
+ /** Explicit file path, bypassing {@link breakerSettingsPath}. */
12
+ filePath?: string;
13
+ /** Warning sink, so tests can capture instead of printing. */
14
+ warn?: (message: string) => void;
15
+ }
16
+ /**
17
+ * Read persisted settings. A missing file is the first-run case and returns
18
+ * the defaults silently; unreadable, malformed, or structurally invalid
19
+ * content warns and returns the defaults.
20
+ */
21
+ export declare function loadBreakerSettings(options?: BreakerSettingsIoOptions): Promise<BreakerSettings>;
22
+ /**
23
+ * Persist settings atomically. The temp file is created beside the target and
24
+ * renamed over it, so readers only ever see a complete document.
25
+ * @param settings - master switch and session opt-ins; normalized before writing.
26
+ * @param options - explicit path, if any.
27
+ */
28
+ export declare function saveBreakerSettings(settings: BreakerSettings, options?: Pick<BreakerSettingsIoOptions, 'filePath'>): Promise<void>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Peak-window breaker decisions — pure functions, no timers or IO.
3
+ *
4
+ * DeepSeek charges peak rates in Beijing time 09:00–12:00 and 14:00–18:00 on
5
+ * weekdays (see `pricing.ts`, which owns that definition). A session that opted
6
+ * in parks its user-started steps while a peak window is open and lets them
7
+ * continue at 18:00 — the end of the day's last peak window — so one pause
8
+ * covers both windows without a lunch-time exception.
9
+ *
10
+ * All time math is done in Beijing time (UTC+8) rather than the machine's local
11
+ * zone, matching the price table.
12
+ */
13
+ import { type BreakerData, type BreakerSettings } from './contract.ts';
14
+ /** Peak windows in Beijing time, display-only copy shared with `pricingInfo`. */
15
+ export declare const BREAKER_PEAK_WINDOWS: readonly ["09:00–12:00", "14:00–18:00"];
16
+ /**
17
+ * The one field of a message this module inspects: its producer. Only
18
+ * `kind === 'user'` means a person started the turn in a chat; scheduled
19
+ * tasks, goal continuations and subagent relays all carry another kind and are
20
+ * therefore never parked.
21
+ */
22
+ export interface BreakerMessageFace {
23
+ source?: {
24
+ kind?: string;
25
+ };
26
+ }
27
+ /**
28
+ * When the current peak window settles: 18:00 Beijing time on the same day.
29
+ * Outside peak there is nothing to wait for, so the answer is `now`.
30
+ *
31
+ * A peak window can only be open between 09:00 and 18:00 on a weekday, so the
32
+ * same-day 18:00 instant is always in the future here. Converting through
33
+ * UTC+8 schedules the same instant on a host in another timezone.
34
+ */
35
+ export declare function nextResumeAt(nowMs: number): number;
36
+ /**
37
+ * Whether these proposed-step messages belong to a turn a person started. A
38
+ * single user-produced message is enough: a step that carries it is part of
39
+ * the user's request, whatever else the batch holds.
40
+ */
41
+ export declare function isUserTurn(messages: readonly BreakerMessageFace[]): boolean;
42
+ /**
43
+ * A copy holding exactly the contract fields, with session ids deduplicated,
44
+ * filtered to the charset sessions can actually have, and sorted so persisted
45
+ * documents and API projections are stable.
46
+ */
47
+ export declare function normalizeBreakerSettings(settings: BreakerSettings): BreakerSettings;
48
+ /** Project live gate state into the wire shape the client renders. */
49
+ export declare function breakerProject(settings: BreakerSettings, nowMs: number, holding: readonly string[]): BreakerData;
@@ -1,4 +1,4 @@
1
- import type { BalanceResponse, SessionUsageResponse, UsageResponse } from '../contract.ts';
1
+ import type { BalanceResponse, BreakerReleaseRequest, BreakerResponse, BreakerSettingsRequest, SessionUsageResponse, UsageResponse } from '../contract.ts';
2
2
  /** Last cached value (possibly stale), for an instant first render. */
3
3
  export declare const getCachedBalance: () => BalanceResponse | null;
4
4
  export declare const getCachedUsage: () => UsageResponse | null;
@@ -10,3 +10,10 @@ export declare function fetchUsage(force?: boolean): Promise<UsageResponse>;
10
10
  export declare const getCachedSessionUsage: (sessionId: string) => SessionUsageResponse | null;
11
11
  export declare const subscribeSessionUsage: (sessionId: string, fn: () => void) => (() => void);
12
12
  export declare function fetchSessionUsage(sessionId: string, force?: boolean): Promise<SessionUsageResponse>;
13
+ /** Current projection: opted-in sessions plus the live peak/holding status. */
14
+ export declare function fetchBreaker(): Promise<BreakerResponse>;
15
+ /** Write one switch: `sessionId` targets that conversation, omitting it targets
16
+ * the master switch. The host answers with the full projection. */
17
+ export declare function updateBreaker(request: BreakerSettingsRequest): Promise<BreakerResponse>;
18
+ /** Let parked work continue now; an omitted target means every parked session. */
19
+ export declare function releaseBreaker(request?: BreakerReleaseRequest): Promise<BreakerResponse>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 高峰暂停 pill in the composer dock, beside the host's own
3
+ * 「会话统计」/「Token 用量」pills and this plugin's 「会话费用」pill. It is the
4
+ * switch for the current conversation only: on means this session's
5
+ * user-started turns pause during Beijing-time peak pricing and resume at
6
+ * 18:00. The 额度 page lists every session that opted in and carries the
7
+ * master switch; this pill is where a single conversation opts in.
8
+ *
9
+ * The three states are told apart by three channels at once, never colour
10
+ * alone: copy (「高峰暂停 · 关」/「高峰暂停 · 开」/「已暂停 · 18:00」), glyph
11
+ * (outline / solid) and colour (neutral / primary / warn).
12
+ *
13
+ * Skin and geometry mirror `./session-cost-pill.tsx` (same slot, same
14
+ * `.dq-cost-pill`), so the dock reads as one row. The switch writes
15
+ * optimistically, then takes the host's returned projection as truth; a
16
+ * failure rolls back and is surfaced through `title`/`aria-label` only.
17
+ */
18
+ import { type ReactElement } from 'react';
19
+ export declare function BreakerPill({ sessionId }: {
20
+ sessionId?: string;
21
+ }): ReactElement | null;
@@ -0,0 +1,8 @@
1
+ import type { BreakerData } from '../contract.ts';
2
+ /** Latest host projection, or null before the first read lands. */
3
+ export declare function getBreakerData(): BreakerData | null;
4
+ /** Replace the projection and notify both surfaces. */
5
+ export declare function publishBreakerData(data: BreakerData | null): void;
6
+ export declare function subscribeBreakerData(listener: () => void): () => void;
7
+ /** Subscribe the calling component to the shared projection. */
8
+ export declare function useBreakerData(): BreakerData | null;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Client-side helpers for the session-scoped peak breaker: response-shape
3
+ * validation (the host half may not be restarted yet, so an absent or
4
+ * malformed payload must degrade to「状态暂不可用」instead of crashing),
5
+ * per-session projections for optimistic switches, and pure display
6
+ * formatting that is independent of the browser timezone.
7
+ */
8
+ import { type BreakerData, type BreakerResponse } from '../contract.ts';
9
+ /**
10
+ * Narrow a response to one whose every field the UI dereferences exists.
11
+ * Mirrors `usageIsUsable` in ./api.ts: a stale host that does not serve the
12
+ * breaker route (404) or an older payload shape falls through to the neutral
13
+ * copy rather than being rendered as if it were current.
14
+ */
15
+ export declare function breakerIsUsable(res: BreakerResponse | null | undefined): res is BreakerResponse & {
16
+ data: BreakerData;
17
+ };
18
+ /**
19
+ * `HH:mm` in Beijing time for an ISO instant, or null when absent/unparseable.
20
+ *
21
+ * Computed through the fixed +08:00 offset rather than `Intl`/local Date
22
+ * getters: the resume clock is defined in Beijing time, and the browser may be
23
+ * in any timezone (same approach as ./budget.ts's month boundary).
24
+ */
25
+ export declare function beijingClock(iso: string | null | undefined): string | null;
26
+ /** Whether `sessionId` itself opted in, ignoring the master switch. */
27
+ export declare function isSessionOptedIn(data: BreakerData | null, sessionId: string | null | undefined): boolean;
28
+ /** Whether `sessionId`'s opt-in is in force: its own switch and the master switch. */
29
+ export declare function isSessionEnabled(data: BreakerData | null, sessionId: string | null | undefined): boolean;
30
+ /** Switch a session's breaker can be in; `holding` means it is parked right now. */
31
+ export type SessionBreakerState = 'off' | 'on' | 'holding';
32
+ /**
33
+ * Per-session state for the composer pill. Holding is only reported for an
34
+ * enabled session: a release can race a stale projection, and a session the
35
+ * host no longer lists must read as off regardless of the holding list. With
36
+ * the master switch off nothing is in force, so the pill reads off too.
37
+ */
38
+ export declare function sessionBreakerState(data: BreakerData | null, sessionId: string | null | undefined): SessionBreakerState;
39
+ /**
40
+ * Projection with one session switched on/off, used for the optimistic state
41
+ * before the host answers. Disabling also drops the session from `holding`:
42
+ * off means nothing is parked for it.
43
+ */
44
+ export declare function withSessionToggled(data: BreakerData | null, sessionId: string, enabled: boolean): BreakerData | null;
45
+ /**
46
+ * Projection with the master switch flipped, used for the optimistic state
47
+ * before the host answers. Off means nothing is parked — per-session switches
48
+ * are kept, so the 额度 page can still list and manage them.
49
+ */
50
+ export declare function withMasterToggled(data: BreakerData | null, enabled: boolean): BreakerData | null;
51
+ /** Title fallback for a session whose title has not been read yet. */
52
+ export declare function shortSessionId(sessionId: string): string;
53
+ /**
54
+ * Which status row the 额度 page's breaker card shows. `unavailable` means the
55
+ * host route could not be read (or answered with a shape this client cannot
56
+ * use); `masterOff` outranks the peak phases because nothing pauses then. The
57
+ * peak phases depend only on the host clock and holding list.
58
+ */
59
+ export type BreakerPhase = 'unavailable' | 'masterOff' | 'offPeak' | 'peak' | 'peakHolding';
60
+ export declare function breakerPhase(data: BreakerData | null): BreakerPhase;
@@ -58,4 +58,12 @@ export declare function Heatmap(props: {
58
58
  }): ReactElement;
59
59
  export declare const fmt: (v: string | number) => string;
60
60
  export declare const fmtInt: (v: number | undefined) => string;
61
+ /**
62
+ * Compact token/count formatting. English delegates to `Intl` compact notation;
63
+ * Simplified Chinese keeps the 万/亿 scale words because "1.6万 tokens" reads
64
+ * faster than "16K tokens" to Chinese readers.
65
+ * @param v - raw number of tokens or calls.
66
+ * @param locale - active plugin locale; defaults to Simplified Chinese.
67
+ * @returns the compact string.
68
+ */
61
69
  export declare const fmtCompact: (v: number | undefined, locale?: LocaleId) => string;
@@ -1,12 +1,33 @@
1
1
  import { type ReactElement, type ReactNode } from 'react';
2
2
  import { type LocaleKey } from './locales.ts';
3
+ /**
4
+ * Locales this plugin ships copy for: DSH's two built-in locales — Simplified
5
+ * Chinese (`zh`, the host's own id) and English (`en`). The plugin keeps the
6
+ * host's language catalog untouched on purpose: language selection stays a DSH
7
+ * setting, and contributing a plugin-private third language would leave the
8
+ * host UI and this dashboard speaking different languages, because no other
9
+ * package ships a dictionary for it.
10
+ */
3
11
  export type LocaleId = 'zh' | 'en';
12
+ /**
13
+ * Resolve the `Intl` tag for a plugin locale.
14
+ * @param locale - active plugin locale.
15
+ * @returns the BCP 47 tag to pass to `Intl.*` / `toLocaleString`.
16
+ */
17
+ export declare function intlLocale(locale: LocaleId): string;
4
18
  export type Translate = (key: LocaleKey, params?: Record<string, unknown>) => string;
5
19
  export declare const fallbackT: Translate;
6
20
  export declare function LocaleProvider(props: {
7
21
  t: Translate;
8
22
  children: ReactNode;
9
23
  }): ReactElement;
24
+ /**
25
+ * Read the translate function plus the concrete locale it is bound to. The
26
+ * identity rides the dictionary itself (`meta.locale`), because slot components
27
+ * receive only the `t` seat, not the host's locale snapshot; every dictionary
28
+ * stores its own id there, so a language switch moves both together.
29
+ * @returns the active `t` and the plugin locale it resolves to.
30
+ */
10
31
  export declare function useI18n(): {
11
32
  t: Translate;
12
33
  locale: LocaleId;
@@ -189,6 +189,7 @@ export declare const zh: {
189
189
  readonly 'balance.granted': "赠送额度 {amount}";
190
190
  readonly 'balance.runway': "预计可用";
191
191
  readonly 'balance.runwayBreakdownTitle': "查看预计可用天数说明";
192
+ readonly 'balance.topUpTitle': "余额偏低,前往 DeepSeek 开放平台充值(新标签页打开)";
192
193
  readonly 'balance.days': "{count} 天";
193
194
  readonly 'balance.overYear': "> 365 天";
194
195
  readonly 'balance.noRunway': "近 {days} 天没有用量,无法估算";
@@ -227,6 +228,9 @@ export declare const zh: {
227
228
  readonly 'settings.widgetTabs': "展示浮窗的页面";
228
229
  readonly 'settings.widgetTabsHint': "列表会随已安装的页面自动更新。";
229
230
  readonly 'settings.widgetTabsEmpty': "当前没有可配置的会话页面。";
231
+ readonly 'settings.peakPause': "高峰暂停";
232
+ readonly 'settings.peakPauseHint': "总开关;关闭后所有会话都不暂停。每个会话的开关会保留,可随时重新打开。";
233
+ readonly 'settings.peakPauseLoading': "正在读取总开关状态…";
230
234
  readonly 'settings.lowBalance': "余额预警线";
231
235
  readonly 'settings.lowBalanceOn': "余额低于 {amount} {currency} 时,这里和悬浮窗都会转为警示色。填 0 关闭。";
232
236
  readonly 'settings.lowBalanceOff': "已关闭余额预警。填一个大于 0 的数开启。";
@@ -243,6 +247,32 @@ export declare const zh: {
243
247
  readonly 'settings.tabDragAria': "拖动「额度」调整标签位置";
244
248
  readonly 'settings.tabMoveEarlier': "前移";
245
249
  readonly 'settings.tabMoveLater': "后移";
250
+ readonly 'breaker.title': "高峰暂停";
251
+ readonly 'breaker.desc': "在对话输入框旁按会话开启;高峰时段自动暂停该会话,18:00 继续。自动化任务不受影响;总开关在「设置」卡里。";
252
+ readonly 'breaker.pillOff': "高峰暂停 · 关";
253
+ readonly 'breaker.pillOn': "高峰暂停 · 开";
254
+ readonly 'breaker.pillHolding': "已暂停 · {clock}";
255
+ readonly 'breaker.pillAriaOff': "高峰暂停:本会话关闭,点击开启";
256
+ readonly 'breaker.pillAriaOn': "高峰暂停:本会话开启,{clock} 继续,点击关闭";
257
+ readonly 'breaker.pillAriaHolding': "高峰暂停:本会话正在暂停,{clock} 继续,点击关闭";
258
+ readonly 'breaker.pillAriaMasterOff': "高峰暂停:总开关已关闭,本会话不会暂停;点击只切换本会话开关";
259
+ readonly 'breaker.pillTitleOff': "高峰时段暂停本会话:当前关闭,点击开启";
260
+ readonly 'breaker.pillTitleOn': "高峰时段暂停本会话:当前开启,{clock} 继续;点击关闭";
261
+ readonly 'breaker.pillTitleHolding': "本会话正在暂停,{clock} 继续;点击关闭";
262
+ readonly 'breaker.pillTitleMasterOff': "高峰暂停的总开关已在额度页关闭,本会话不会暂停;点击只切换本会话开关";
263
+ readonly 'breaker.actionError': "操作失败:{error}";
264
+ readonly 'breaker.loading': "载入中…";
265
+ readonly 'breaker.statusUnavailable': "状态暂不可用";
266
+ readonly 'breaker.statusMasterOff': "总开关已关闭,所有会话都不暂停;可在「设置」卡重新打开";
267
+ readonly 'breaker.statusOffPeak': "闲时 · 不暂停会话";
268
+ readonly 'breaker.statusPeak': "高峰时段 · 开启的会话会自动暂停";
269
+ readonly 'breaker.statusHolding': "已暂停 {count} 个会话 · {clock} 继续";
270
+ readonly 'breaker.statusHoldingNoClock': "已暂停 {count} 个会话";
271
+ readonly 'breaker.release': "立即全部继续";
272
+ readonly 'breaker.releaseBusy': "正在继续…";
273
+ readonly 'breaker.empty': "还没有会话开启高峰暂停。在对话输入框旁的「高峰暂停」开关里开启。";
274
+ readonly 'breaker.rowDisableAria': "关闭「{title}」的高峰暂停";
275
+ readonly 'breaker.holding': "暂停中 · {clock}";
246
276
  readonly 'panel.caption': "账户级 · 跨全部会话";
247
277
  readonly 'panel.openAria': "打开额度页";
248
278
  readonly 'widget.updatedNow': "刚刚更新";