@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.
package/lib/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/contract.ts", "../src/memo.ts", "../src/trust-fence.ts", "../src/pricing.ts", "../src/usage.ts", "../src/wire.ts", "../src/index.ts"],
4
- "sourcesContent": ["/** Wire contracts shared by the host API and the client fetchers. */\n\nexport interface BalanceInfo {\n currency: string\n total: string\n granted: string\n toppedUp: string\n}\n\nexport interface BalanceData {\n isAvailable: boolean\n balances: BalanceInfo[]\n}\n\nexport interface BalanceResponse {\n ok: boolean\n error?: string\n data?: BalanceData\n}\n\nexport interface DailyUsage {\n date: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\nexport interface HourlyUsage {\n hour: number\n total: number\n cost: number\n calls: number\n}\n\nexport interface HeatmapUsage {\n date: string\n total: number\n cost: number\n calls: number\n}\n\nexport interface UsageTotals {\n input: number\n output: number\n cache: number\n reasoning: number\n total: number\n cost: number\n calls: number\n /**\n * What the cache-hit tokens would have cost at cache-miss rates, minus what\n * they actually cost \u2014 i.e. the money prefix caching saved. Folded per\n * record so it respects each record's model and time-of-day rates.\n */\n cacheSavings: number\n}\n\n/** One per-day / per-hour point of a single model's series. */\nexport interface ModelSeriesPoint {\n total: number\n cost: number\n calls: number\n}\n\n/** Per-model usage aggregation: overall totals plus 30-day and 24-hour series. */\nexport interface ModelUsage {\n provider: string\n model: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n daily: ModelSeriesPoint[]\n hourly: ModelSeriesPoint[]\n}\n\n/** Usage rolled up over one time window. */\nexport interface PeriodUsage {\n total: number\n cost: number\n calls: number\n}\n\n/**\n * Time-anchored rollups. \"Accumulated since forever\" answers no question a\n * user actually has; these do: what did today cost, what has this month cost,\n * and is that more or less than the comparable window before it.\n */\nexport interface UsageSummary {\n today: PeriodUsage\n yesterday: PeriodUsage\n /** Current calendar month, to date. */\n month: PeriodUsage\n /** Previous calendar month up to the same day-of-month, so a mid-month\n * comparison is against a same-length window rather than a full month. */\n lastMonthToDate: PeriodUsage\n}\n\n/** CNY per 1M tokens for one model tier. */\nexport interface PricingRates {\n cacheHit: number\n input: number\n output: number\n}\n\nexport interface PricingTier {\n model: string\n peak: PricingRates\n offPeak: PricingRates\n}\n\n/** What the cost column was computed with, so the estimate can be audited. */\nexport interface PricingInfo {\n currency: string\n /** Date peak/off-peak pricing took effect; earlier usage is costed at the\n * flat rates that were actually charged then. */\n switchDate: string\n inPeakNow: boolean\n peakWindows: string[]\n tiers: PricingTier[]\n}\n\n/**\n * How usage divides between DeepSeek's peak and off-peak windows, plus the\n * same usage re-priced as if everything had landed off-peak \u2014 answering\n * \"what would shifting batch work off-peak save me\" under the peak/off-peak\n * table in effect since 2026-08-17.\n */\nexport interface PeakSplit {\n peak: PeriodUsage\n offPeak: PeriodUsage\n /** Every record priced under the peak/off-peak table, as actually classified. */\n peakEraCost: number\n /** Every record priced under that table as if it had landed off-peak. */\n offPeakEraCost: number\n}\n\n/** One session's share of the bill. */\nexport interface SessionCost {\n id: string\n /** The session's title from its log; falls back to a short id. */\n title: string\n total: number\n cost: number\n calls: number\n /** Epoch ms of the last usage record in this session. */\n lastActive: number\n}\n\n/**\n * Multimodal (image) usage, folded from the same log replay: image blocks in\n * user messages and tool results are counted and sized, then estimated with\n * the official resizing rule (see `estimateImageTokens`). Each image is\n * attributed to the next usage record \u2014 the model call whose prompt carried\n * it \u2014 and priced at that call's input (cache-miss) rate, so the share answer\n * \"how much of the bill came from pictures\" has a real number.\n */\nexport interface VisionStats {\n /** Image blocks counted in the scope. */\n images: number\n /** Estimated image tokens under the official conversion rule. */\n imageTokens: number\n /** Estimated CNY cost of those image tokens at the consuming calls' rates. */\n cost: number\n /** Total image payload bytes when the attachment refs carried a size. */\n bytes: number\n}\n\n/** One day's image usage for the per-window / per-day series. */\nexport interface VisionDayPoint {\n date: string\n images: number\n imageTokens: number\n cost: number\n}\n\n/** One session's image usage, ranked for \"which run kept sending pictures\". */\nexport interface VisionSession {\n id: string\n title: string\n images: number\n imageTokens: number\n cost: number\n}\n\n/**\n * What the local log replay could actually inspect. Totals can otherwise look\n * authoritative even when one corrupt session was skipped, so this metadata\n * travels with every usage response and lets the UI disclose partial results.\n */\nexport interface UsageCoverage {\n scope: 'local-dsh-session-logs'\n /** Session headers with a usable id returned by the local persistence. */\n listedSessions: number\n /** Session logs read successfully, including logs with no usage. */\n scannedSessions: number\n /** Valid assistant usage records folded into the totals. */\n usageRecords: number\n /** Usage records rejected because their time or token data was invalid. */\n skippedRecords: number\n /** Session logs that could not be read at all. */\n failedSessions: number\n /** Epoch ms of the first/last valid usage record across all local logs. */\n earliestAt: number | null\n latestAt: number | null\n}\n\nexport const USAGE_WINDOW_DAYS = [7, 30, 90, 365] as const\nexport type UsageWindowDays = (typeof USAGE_WINDOW_DAYS)[number]\n\n/**\n * Session ids observed in this codebase (host-issued, e.g.\n * `session-484a1c14-c6fe-4d6a-abfd-a2d8d2f664d5`) are always plain\n * ASCII-safe tokens \u2014 no documented format contract exists for them outside\n * this repo (`SessionPersistenceFace` is implemented by the DSH host, not\n * here), so this is a defensive charset whitelist rather than an\n * exact-format check: letters, digits, `-`, `_` only, with a generous length\n * cap. It exists specifically to stop path-separator / `..` / null-byte\n * payloads from ever reaching the session-log read \u2014 `id` on the\n * `/session` route arrives verbatim from the browser-controlled `?id=` query\n * parameter, unlike `fetchUsage()`'s session ids, which the host itself\n * enumerates via `persistence.list()`.\n */\nexport const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\nexport const isValidSessionId = (id: string): boolean => SESSION_ID_PATTERN.test(id)\n\n/** One internally consistent view of the same local logs over a calendar window. */\nexport interface UsageWindow {\n days: UsageWindowDays\n daily: DailyUsage[]\n hourly: HourlyUsage[]\n totals: UsageTotals\n models: ModelUsage[]\n /** Most expensive sessions inside this window, capped at SESSION_TOP_N. */\n sessions: SessionCost[]\n /** All sessions that contributed usage inside this window. */\n sessionCount: number\n vision: VisionStats\n visionDaily: VisionDayPoint[]\n /** Sessions that sent the most images inside this window, capped. */\n visionSessions: VisionSession[]\n}\n\nexport interface UsageData {\n daily: DailyUsage[]\n hourly: HourlyUsage[]\n heatmap: HeatmapUsage[]\n totals: UsageTotals\n models: ModelUsage[]\n summary: UsageSummary\n pricing: PricingInfo\n peakSplit: PeakSplit\n /** Most expensive sessions first, capped \u2014 see SESSION_TOP_N. */\n sessions: SessionCost[]\n /** How many sessions contributed usage in total. */\n sessionCount: number\n coverage: UsageCoverage\n windows: UsageWindow[]\n vision: VisionStats\n visionDaily: VisionDayPoint[]\n /** Sessions that sent the most images, capped \u2014 see SESSION_TOP_N. */\n visionSessions: VisionSession[]\n}\n\nexport interface UsageResponse {\n ok: boolean\n error?: string\n data?: UsageData\n}\n\n/**\n * One model's contribution to a single session's bill \u2014 the same field\n * shape as `ModelUsage` minus the `daily`/`hourly` series, which only make\n * sense aggregated across many sessions.\n */\nexport interface SessionModelUsage {\n provider: string\n model: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\n/** One completed conversation turn's usage. `messageId` is the durable id of\n * the turn's final assistant message \u2014 the same identity the chat action-strip\n * slot exposes to client plugins. Tool-call steps inside the turn are folded\n * into this one figure rather than shown as misleading separate charges. */\nexport interface TurnUsage {\n messageId: string\n total: number\n cost: number\n calls: number\n}\n\n/**\n * Usage for exactly one session \u2014 what the \u300C\u989D\u5EA6\u300Dtab's own conversation\n * is costing, as opposed to `UsageData`'s account-wide totals. Fetched on\n * demand (see `fetchSessionUsage`), not part of the 5-minute `usage` memo:\n * a single session read is cheap and must reflect the session's live state.\n */\nexport interface SessionUsageData {\n sessionId: string\n /** The session's own title from its log; falls back to a short id. */\n title: string\n total: number\n cost: number\n calls: number\n /** Epoch ms of the first/last valid usage record in this session, or null\n * when the session has produced no usage yet. */\n firstActive: number | null\n lastActive: number | null\n /** Most expensive model first. */\n models: SessionModelUsage[]\n /** Completed turns, in log order, addressed by their final message id. */\n turns: TurnUsage[]\n}\n\nexport interface SessionUsageResponse {\n ok: boolean\n error?: string\n data?: SessionUsageData\n}\n", "/**\n * Tiny TTL memo for the /api routes.\n *\n * The usage endpoint replays every session log, so it must not run on every\n * tab click; the balance endpoint also calls the DeepSeek API. The memo:\n *\n * - `get()` serves a successful cached response while it is within `ttlMs`,\n * otherwise recomputes;\n * - `refresh()` recomputes unconditionally and reseeds the cache;\n * - concurrent recomputes (of either kind) are deduped into one upstream\n * call;\n * - `ok:false` responses are never cached, so a transient failure is retried\n * on the next request.\n *\n * No timers are held, so nothing needs disposal on plugin unload.\n */\n\ninterface Entry<T> {\n value: T\n setAt: number\n}\n\ninterface MemoState<T> {\n entry: Entry<T> | undefined\n inflight: Promise<T> | undefined\n}\n\nexport interface Memo<T> {\n get(): Promise<T>\n refresh(): Promise<T>\n}\n\nexport function memoize<T extends { ok: boolean }>(ttlMs: number, compute: () => Promise<T>): Memo<T> {\n const state: MemoState<T> = { entry: undefined, inflight: undefined }\n\n const run = (): Promise<T> => {\n if (state.inflight !== undefined) return state.inflight\n const task = compute().then(\n (value) => {\n state.inflight = undefined\n if (value.ok) state.entry = { value, setAt: Date.now() }\n return value\n },\n (err: unknown) => {\n state.inflight = undefined\n throw err\n },\n )\n state.inflight = task\n return task\n }\n\n return {\n get: () => {\n const hit = state.entry\n if (hit !== undefined && hit.value.ok && Date.now() - hit.setAt < ttlMs) {\n return Promise.resolve(hit.value)\n }\n return run()\n },\n refresh: () => run(),\n }\n}\n", "/**\n * Browser-trust fence for the plugin's own /api route \u2014 the same fences the\n * harness's /api gateway applies: the Host header must name a loopback\n * authority, a browser-marked cross-site request is refused, and an attached\n * Origin must be exactly this authority.\n */\nimport type { IncomingMessage } from 'node:http'\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]'\n}\n\nexport function isTrustedApiRequest(req: IncomingMessage): boolean {\n const host = req.headers.host\n if (host === undefined) return false\n let hostUrl: URL\n try {\n hostUrl = new URL(`http://${host}`)\n } catch {\n return false\n }\n if (!isLoopbackHostname(hostUrl.hostname)) return false\n if (req.headers['sec-fetch-site'] === 'cross-site') return false\n const origin = req.headers.origin\n if (origin === undefined) return true\n try {\n return new URL(origin).host === hostUrl.host\n } catch {\n return false\n }\n}\n", "/**\n * DeepSeek API price table and cost estimation.\n *\n * Every record is priced with the table that was in force at the moment it was\n * written \u2014 CNY per 1M tokens, from DeepSeek's official pricing page:\n *\n * - until 2026-08-17 00:00 Beijing: one flat price per model;\n * - 2026-08-17 00:00: peak/off-peak pricing \u2014 peak windows 09:00\u201312:00 and\n * 14:00\u201318:00 every day, off-peak exactly half of peak;\n * - 2026-09-10 12:00: the V4.1-Flash price cut (flash 0.04/2/8 peak), and the\n * peak windows narrow to Monday\u2013Friday;\n * - 2026-09-14 12:00: `deepseek-v4-pro` requests are served by V4.1-Flash and\n * billed at Flash rates until a V4.1 Pro ships.\n *\n * `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` are legacy names for\n * the model now published as `deepseek-flash`; both keep billing at Flash\n * rates. Images cost tokens rather than a multiplier of their own: they are\n * converted by size and priced together with the text tokens. Historical days\n * therefore keep the price that was actually charged.\n *\n * Editing this table is the one place to touch when DeepSeek changes prices.\n */\nimport type { PricingInfo, PricingRates } from './contract.ts'\n\n/** 2026-08-17 00:00 Beijing time (UTC+8), when peak/off-peak pricing took effect. */\nexport const PEAK_PRICING_FROM_MS = Date.UTC(2026, 7, 16, 16, 0, 0)\n\n/** 2026-09-10 12:00 Beijing, when the V4.1-Flash prices and the weekday-only\n * peak windows took effect. */\nexport const V41_FLASH_PRICING_FROM_MS = Date.UTC(2026, 8, 10, 4, 0, 0)\n\n/** 2026-09-14 12:00 Beijing, from when `deepseek-v4-pro` requests are routed\n * to V4.1-Flash and billed at Flash rates. */\nexport const PRO_ROUTED_TO_FLASH_FROM_MS = Date.UTC(2026, 8, 14, 4, 0, 0)\n\n/** Peak hours in Beijing time, as [startHour, endHour) pairs. */\nconst PEAK_WINDOWS: Array<[number, number]> = [[9, 12], [14, 18]]\n\nconst BEIJING_OFFSET_MS = 8 * 3_600_000\n\ntype Tier = 'pro' | 'flash' | 'vision'\n\n/** Flat rates in effect until 2026-08-17. */\nconst FLAT_RATES: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.025, input: 3, output: 6 },\n flash: { cacheHit: 0.02, input: 1, output: 2 },\n // The vision model billed at flash rates from day one, so its pre-switch\n // history is costed at the same flat prices as flash.\n vision: { cacheHit: 0.02, input: 1, output: 2 },\n}\n\n/** Peak rates from 2026-08-17; off-peak is exactly half of each. */\nconst PEAK_RATES_08_17: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.3, input: 9, output: 27 },\n flash: { cacheHit: 0.1, input: 3, output: 9 },\n // Official policy: the vision model is priced identically to flash; images\n // cost tokens, not their own multiplier.\n vision: { cacheHit: 0.1, input: 3, output: 9 },\n}\n\n/** Peak rates from 2026-09-10, the V4.1-Flash table; off-peak is half. */\nconst PEAK_RATES_09_10: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.3, input: 9, output: 27 },\n flash: { cacheHit: 0.04, input: 2, output: 8 },\n vision: { cacheHit: 0.04, input: 2, output: 8 },\n}\n\n/** One dated price table; `fromMs` is inclusive. */\ninterface PriceEra {\n readonly fromMs: number\n readonly peak: Record<Tier, PricingRates>\n /** Whether the peak windows apply Monday\u2013Friday only. */\n readonly weekdaysOnly: boolean\n}\n\n/** Oldest first \u2014 the last entry whose `fromMs` has passed is the one in force. */\nconst PRICE_ERAS: readonly PriceEra[] = [\n { fromMs: PEAK_PRICING_FROM_MS, peak: PEAK_RATES_08_17, weekdaysOnly: false },\n { fromMs: V41_FLASH_PRICING_FROM_MS, peak: PEAK_RATES_09_10, weekdaysOnly: true },\n]\n\n/** The price table in force at a moment. */\nfunction eraAt(timeMs: number): PriceEra {\n let era = PRICE_ERAS[0]\n for (const candidate of PRICE_ERAS) if (timeMs >= candidate.fromMs) era = candidate\n return era\n}\n\nconst halved = (rates: PricingRates): PricingRates => ({\n cacheHit: rates.cacheHit / 2,\n input: rates.input / 2,\n output: rates.output / 2,\n})\n\n/** Which price tier a model name falls into; anything unrecognised is billed\n * as pro, the conservative (more expensive) guess. Vision is matched first:\n * `deepseek-v4-flash-vision-exp` also contains \"flash\". */\nexport function tierOf(model: string): Tier {\n const name = model.toLowerCase()\n if (name.includes('vision')) return 'vision'\n if (name.includes('flash')) return 'flash'\n return 'pro'\n}\n\n/** The tier a model actually bills at: from 2026-09-14 `deepseek-v4-pro`\n * requests are served by V4.1-Flash and charged the Flash price. */\nfunction billedTierOf(timeMs: number, model: string): Tier {\n const tier = tierOf(model)\n return tier === 'pro' && timeMs >= PRO_ROUTED_TO_FLASH_FROM_MS ? 'flash' : tier\n}\n\n/** Beijing-time Monday\u2013Friday, which the peak windows honour from 2026-09-10. */\nfunction isBeijingWeekday(timeMs: number): boolean {\n const day = new Date(timeMs + BEIJING_OFFSET_MS).getUTCDay()\n return day >= 1 && day <= 5\n}\n\n/**\n * Image token conversion (DeepSeek official rule, Vision guide):\n * - Images below roughly 384\u00D7384 pixels are scaled UP preserving aspect\n * ratio; larger images are scaled DOWN so their pixel count is roughly that\n * of an 800\u00D7800 image.\n * - Tokens are proportional to the (resized) pixel count, capped at 384\n * tokens per image, so 2000\u00D72000 and 5000\u00D75000 cost the same.\n * - Each image is counted independently; `detail: low` caps at 512\u00D7512.\n *\n * The estimate is for the \"how much came from images\" breakdown; the exact\n * provider count is already folded into the usage record's `inputTokens`.\n * Returns tokens (possibly fractional) \u2014 0 for missing/invalid dimensions.\n */\nexport function estimateImageTokens(width: number, height: number): number {\n if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return 0\n const area = width * height\n const scaledArea = Math.min(800 * 800, Math.max(384 * 384, area))\n return 384 * (scaledArea / (800 * 800))\n}\n\n/** Whether a moment falls in a peak window, judged in Beijing time so the\n * estimate does not drift with the machine's timezone. */\nexport function isPeak(timeMs: number): boolean {\n if (eraAt(timeMs).weekdaysOnly && !isBeijingWeekday(timeMs)) return false\n const hour = Math.floor((((timeMs + BEIJING_OFFSET_MS) % 86_400_000) + 86_400_000) % 86_400_000 / 3_600_000)\n return PEAK_WINDOWS.some(([from, to]) => hour >= from && hour < to)\n}\n\n/** Rates applying to one model at one moment. */\nexport function ratesAt(timeMs: number, model: string): PricingRates {\n if (timeMs < PEAK_PRICING_FROM_MS) return FLAT_RATES[tierOf(model)]\n const peak = eraAt(timeMs).peak[billedTierOf(timeMs, model)]\n return isPeak(timeMs) ? peak : halved(peak)\n}\n\n/**\n * What prefix caching saved on one record: the cache-hit tokens re-priced at\n * the cache-miss rate, minus what they actually cost.\n */\nexport function cacheSavingOf(timeMs: number, model: string, cache: number): number {\n const rates = ratesAt(timeMs, model)\n return (cache * (rates.input - rates.cacheHit)) / 1_000_000\n}\n\nconst applyRates = (rates: PricingRates, input: number, cache: number, output: number): number =>\n (input * rates.input + cache * rates.cacheHit + output * rates.output) / 1_000_000\n\n/** Estimated CNY cost of one usage record. */\nexport function costOf(timeMs: number, model: string, input: number, cache: number, output: number): number {\n return applyRates(ratesAt(timeMs, model), input, cache, output)\n}\n\n/**\n * The same record priced as if it had landed in an idle window \u2014 used to\n * answer \"what would shifting work off-peak save me\". Prices come from the\n * newest table, the one in force now: the question is about scheduling the\n * next batch, not about re-pricing history.\n */\nexport function costUnderPeakEra(timeMs: number, model: string, input: number, cache: number, output: number, forceOffPeak = false): number {\n const peak = PRICE_ERAS[PRICE_ERAS.length - 1].peak[billedTierOf(timeMs, model)]\n const rates = !forceOffPeak && isPeak(timeMs) ? peak : halved(peak)\n return applyRates(rates, input, cache, output)\n}\n\n/** The price table to show the user: the two models the official docs list,\n * so the rows match the page they come from \u2014 legacy names, image tokens and\n * the pro routing live in the small print next to the table. */\nexport function pricingInfo(nowMs: number): PricingInfo {\n const era = eraAt(nowMs)\n const row = (model: string, tier: Tier) => ({ model, peak: era.peak[tier], offPeak: halved(era.peak[tier]) })\n return {\n currency: 'CNY',\n switchDate: '2026-08-17',\n inPeakNow: isPeak(nowMs),\n peakWindows: ['09:00\u201312:00', '14:00\u201318:00'],\n tiers: [row('deepseek-flash', 'flash'), row('deepseek-v4-pro', 'pro')],\n }\n}\n", "/**\n * Balance and usage data sources.\n *\n * - Balance: the signed-in DeepSeek account through the optional\n * `deepseekAccount` service (`getBalance`, no API key needed), falling back\n * to `GET /user/balance` with the key resolved through the credentials\n * service (`DEEPSEEK_API_KEY`).\n * - Usage: token usage folded from the DSH session logs (`sessionPersistence`),\n * one record per `assistant/message` event, bucketed by local day / hour.\n */\nimport { isValidSessionId, USAGE_WINDOW_DAYS } from './contract.ts'\nimport type { BalanceData, BalanceInfo, BalanceResponse, ModelSeriesPoint, ModelUsage, PeakSplit, PeriodUsage, SessionCost, SessionModelUsage, SessionUsageResponse, UsageCoverage, UsageData, UsageResponse, UsageSummary, UsageWindowDays, VisionDayPoint, VisionSession, VisionStats } from './contract.ts'\nimport type { AccountClientFace, AccountWalletFace, ContentBlockFace, CredentialsFace, DeepSeekAccountFace, ImageAttachmentFace, SessionEventFace, SessionHeaderFace, SessionPersistenceFace } from './context.ts'\nimport { cacheSavingOf, costOf, costUnderPeakEra, estimateImageTokens, isPeak, pricingInfo } from './pricing.ts'\n\nconst pad2 = (n: number): string => (n < 10 ? `0${n}` : String(n))\n\nfunction errorMessage(err: unknown): string {\n return (err as { message?: string } | null)?.message ?? String(err)\n}\n\n/** Identity the account service forwards to Platform as client headers. The\n * host has no browser identity; wallet totals don't depend on it, so the\n * desktop build's version (when the host environment carries one) and the\n * host's own UTC offset are enough. */\nconst ACCOUNT_CLIENT: AccountClientFace = {\n version: process.env.DSH_CLIENT_VERSION ?? '0.0.0',\n locale: 'zh-CN',\n timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,\n}\n\n/** The account service reports the recharge (`value`) and granted\n * (`bonusWallets`) wallets separately; the card shows one total per currency\n * with that same breakdown, so fold the two lists into its shape. */\nfunction accountBalances(\n toppedUpWallets: readonly AccountWalletFace[],\n grantedWallets: readonly AccountWalletFace[],\n): BalanceInfo[] {\n const byCurrency = new Map<string, { total: number; granted: number; toppedUp: number }>()\n const add = (wallets: readonly AccountWalletFace[], field: 'toppedUp' | 'granted'): void => {\n for (const wallet of wallets) {\n const currency = wallet.currency ?? ''\n const amount = Number(wallet.balance ?? '0')\n if (currency === '' || !Number.isFinite(amount)) continue\n const acc = byCurrency.get(currency) ?? { total: 0, granted: 0, toppedUp: 0 }\n acc[field] += amount\n acc.total += amount\n byCurrency.set(currency, acc)\n }\n }\n add(toppedUpWallets, 'toppedUp')\n add(grantedWallets, 'granted')\n return [...byCurrency].map(([currency, acc]) => ({\n currency,\n total: acc.total.toFixed(2),\n granted: acc.granted.toFixed(2),\n toppedUp: acc.toppedUp.toFixed(2),\n }))\n}\n\n/** Query the signed-in account's wallets. `signed-out` (the service's own\n * null) and `failed` (Platform rejected the query, or the service threw) both\n * let the caller fall back to the API key; only `failed` is worth surfacing\n * when no key exists. */\nasync function readAccountBalance(account: DeepSeekAccountFace): Promise<\n { ok: true; data: BalanceData } | { ok: false; reason: 'signed-out' | 'failed' }\n> {\n try {\n const result = await account.getBalance(ACCOUNT_CLIENT)\n if (result === null) return { ok: false, reason: 'signed-out' }\n if (result.status !== 'ready') return { ok: false, reason: 'failed' }\n const balances = accountBalances(result.value ?? [], result.bonusWallets ?? [])\n return { ok: true, data: { isAvailable: balances.length > 0, balances } }\n } catch {\n return { ok: false, reason: 'failed' }\n }\n}\n\nexport async function fetchBalance(\n credentials: CredentialsFace | undefined,\n account?: DeepSeekAccountFace,\n): Promise<BalanceResponse> {\n const fromAccount = account === undefined ? undefined : await readAccountBalance(account)\n if (fromAccount?.ok === true) return { ok: true, data: fromAccount.data }\n if (credentials === undefined) return { ok: false, error: '\u51ED\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528' }\n let cred: { value: string } | undefined\n try {\n cred = await credentials.resolve('DEEPSEEK_API_KEY')\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6 API Key \u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n if (cred === undefined || cred.value === '') {\n // Report the reason that actually applies: a failing account endpoint is\n // not the same as no credential at all, and telling a signed-in user to\n // configure an API key they don't need hides the real problem.\n return {\n ok: false,\n error: fromAccount?.reason === 'failed'\n ? '\u8D26\u53F7\u4F59\u989D\u63A5\u53E3\u4E0D\u53EF\u7528\uFF0C\u4E14\u672A\u914D\u7F6E DEEPSEEK_API_KEY'\n : '\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09',\n }\n }\n let res: Response\n try {\n res = await fetch('https://api.deepseek.com/user/balance', {\n headers: { authorization: `Bearer ${cred.value}` },\n signal: AbortSignal.timeout(20000),\n })\n } catch (err) {\n return { ok: false, error: `\u8BF7\u6C42\u4F59\u989D\u63A5\u53E3\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n if (!res.ok) return { ok: false, error: `\u4F59\u989D\u63A5\u53E3\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09` }\n let parsed: unknown\n try {\n parsed = await res.json()\n } catch {\n return { ok: false, error: '\u89E3\u6790\u4F59\u989D\u54CD\u5E94\u5931\u8D25' }\n }\n const p = parsed as {\n is_available?: boolean\n balance_infos?: Array<{ currency?: string; total_balance?: string; granted_balance?: string; topped_up_balance?: string }>\n }\n return {\n ok: true,\n data: {\n isAvailable: p.is_available === true,\n balances: (p.balance_infos ?? []).map(b => ({\n currency: b.currency ?? '',\n total: b.total_balance ?? '0',\n granted: b.granted_balance ?? '0',\n toppedUp: b.topped_up_balance ?? '0',\n })),\n },\n }\n}\n\ninterface Bucket {\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\nconst emptyBucket = (): Bucket => ({ input: 0, output: 0, cache: 0, total: 0, cost: 0, calls: 0 })\n\n/** Cost is computed once per event (it depends on the model and on whether\n * the event landed in a peak window) and folded into every bucket that\n * needs it \u2014 shared by the account-wide replay and the single-session one. */\nfunction bump(map: Map<string, Bucket>, key: string, input: number, output: number, cache: number, cost: number): void {\n const bucket = map.get(key) ?? emptyBucket()\n bucket.input += input\n bucket.output += output\n bucket.cache += cache\n // reasoningTokens is a subset of outputTokens; never double-count it.\n bucket.total += input + output + cache\n bucket.cost += cost\n bucket.calls += 1\n map.set(key, bucket)\n}\n\ntype OverallBucket = Bucket & { reasoning: number; cacheSavings: number }\nconst emptyOverall = (): OverallBucket => ({ ...emptyBucket(), reasoning: 0, cacheSavings: 0 })\n\n/** Image usage folded one record at a time; `imageTokens`/`cost` are official-\n * rule estimates, `bytes` is the counted payload size when refs carry one. */\ninterface VisionAcc {\n images: number\n imageTokens: number\n cost: number\n bytes: number\n}\n\nconst emptyVision = (): VisionAcc => ({ images: 0, imageTokens: 0, cost: 0, bytes: 0 })\n\nfunction bumpVisionInto(\n acc: VisionAcc,\n images: number,\n imageTokens: number,\n cost: number,\n bytes: number,\n): void {\n acc.images += images\n acc.imageTokens += imageTokens\n acc.cost += cost\n acc.bytes += bytes\n}\n\nfunction bumpVisionMap(\n map: Map<string, VisionAcc>,\n key: string,\n images: number,\n imageTokens: number,\n cost: number,\n bytes: number,\n): void {\n const acc = map.get(key) ?? emptyVision()\n bumpVisionInto(acc, images, imageTokens, cost, bytes)\n map.set(key, acc)\n}\n\ninterface WindowAccumulator {\n days: UsageWindowDays\n startKey: string\n endKey: string\n dayMap: Map<string, Bucket>\n hourMap: Map<string, Bucket>\n overall: OverallBucket\n modelTotals: Map<string, Bucket>\n modelDays: Map<string, Map<string, Bucket>>\n modelHours: Map<string, Map<string, Bucket>>\n sessions: SessionCost[]\n vision: VisionAcc\n visionDays: Map<string, VisionAcc>\n visionSessions: Map<string, VisionAcc & { title: string }>\n}\n\n/** How many sessions the ranking keeps; the rest are summarised by count. */\nconst SESSION_TOP_N = 6\n\n/**\n * The session's own title, folded from its log: `session/title` events are\n * append-only and the last one wins. It rides along with the usage replay, so\n * naming a session costs no extra read.\n */\nfunction titleOf(events: SessionEventFace[] | undefined, id: string): string {\n let title = ''\n if (events !== undefined) {\n for (const ev of events) {\n if (ev?.type !== 'session/title') continue\n const next = ev.data?.title\n if (typeof next === 'string' && next !== '') title = next\n }\n }\n return title !== '' ? title : `\u4F1A\u8BDD ${id.slice(0, 8)}`\n}\n\nconst dayKeyOf = (d: Date): string => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n\n/**\n * Roll the per-day buckets up into the windows the dashboard's summary card\n * shows. Everything is keyed by local date, matching how the buckets were\n * filled, so \"today\" means the user's today.\n */\nfunction summarize(dayMap: Map<string, Bucket>, now: Date): UsageSummary {\n const sum = (matches: (key: string) => boolean): PeriodUsage => {\n const acc: PeriodUsage = { total: 0, cost: 0, calls: 0 }\n for (const [key, bucket] of dayMap) {\n if (!matches(key)) continue\n acc.total += bucket.total\n acc.cost += bucket.cost\n acc.calls += bucket.calls\n }\n return acc\n }\n const todayKey = dayKeyOf(now)\n const yesterdayKey = dayKeyOf(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1))\n const monthPrefix = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-`\n const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)\n const lastMonthPrefix = `${lastMonth.getFullYear()}-${pad2(lastMonth.getMonth() + 1)}-`\n const dayOfMonth = now.getDate()\n return {\n today: sum(key => key === todayKey),\n yesterday: sum(key => key === yesterdayKey),\n month: sum(key => key.startsWith(monthPrefix)),\n lastMonthToDate: sum(key => key.startsWith(lastMonthPrefix) && Number(key.slice(8)) <= dayOfMonth),\n }\n}\n\n/**\n * Fold usage out of one session's event log. The model for every\n * `assistant/message` usage record is the one from the latest preceding\n * `request/header` event (each request logs one before dispatch), so usage\n * can be attributed per model. Events without any preceding header fall back\n * to the `''`/`''` (unknown) bucket.\n *\n * Image blocks are collected alongside: `user/message` content and tool\n * results (`tool/result`, screenshots from tools) may carry `image` blocks,\n * and every pending image is attributed to the next usage record \u2014 the model\n * call whose prompt actually carried it. Images followed by no usage record\n * (the request failed or never ran) contribute nothing to the fold, since no\n * model call was billed for them.\n */\nfunction addUsageEvent(\n events: SessionEventFace[] | undefined,\n onEvent: (\n time: number,\n input: number,\n output: number,\n cache: number,\n reasoning: number,\n provider: string,\n model: string,\n messageId: string | undefined,\n turn: number | undefined,\n images: ImageAttachmentFace[],\n ) => void,\n): Pick<UsageCoverage, 'usageRecords' | 'skippedRecords'> {\n const result = { usageRecords: 0, skippedRecords: 0 }\n if (events === undefined) return result\n let provider = ''\n let model = ''\n const pendingImages: ImageAttachmentFace[] = []\n for (const ev of events) {\n if (ev?.type === 'request/header') {\n const cfg = ev.data?.header?.config\n if (cfg?.provider !== undefined && cfg?.model !== undefined) {\n provider = cfg.provider\n model = cfg.model\n }\n continue\n }\n pendingImages.push(...imagesInEvent(ev))\n if (ev?.type !== 'assistant/message') continue\n const usage = ev.data?.usage\n if (usage === undefined) continue\n const values = [usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.reasoningTokens]\n .map(value => value === undefined ? 0 : Number(value))\n if (typeof ev.time !== 'number' || !Number.isFinite(ev.time) || values.some(value => !Number.isFinite(value) || value < 0)) {\n result.skippedRecords += 1\n continue\n }\n const [input = 0, output = 0, cache = 0, reasoning = 0] = values\n const rawMessageId = ev.data?.message?.id\n const messageId = typeof rawMessageId === 'string' && rawMessageId !== '' ? rawMessageId : undefined\n const rawTurn = ev.data?.turn\n const turn = typeof rawTurn === 'number' && Number.isFinite(rawTurn) ? rawTurn : undefined\n onEvent(ev.time, input, output, cache, reasoning, provider, model, messageId, turn, pendingImages)\n pendingImages.length = 0\n result.usageRecords += 1\n }\n return result\n}\n\n/** Every `image` attachment ref inside one content-block tree, including\n * blocks nested in tool-result blocks (`tool/result` screenshots). */\nfunction imageBlocks(blocks: ContentBlockFace[] | undefined): ImageAttachmentFace[] {\n const found: ImageAttachmentFace[] = []\n const walkAll = (items: ContentBlockFace[] | undefined): void => {\n for (const item of items ?? []) {\n if (item?.type === 'image' && item.attachment !== undefined && item.attachment !== null) {\n found.push(item.attachment)\n }\n if (Array.isArray(item?.content)) walkAll(item.content)\n }\n }\n walkAll(blocks)\n return found\n}\n\n/** Image attachment refs carried by one event: `user/message` blocks at\n * `data.content`, `tool/result` blocks at `data.message.content`. */\nfunction imagesInEvent(ev: SessionEventFace | undefined): ImageAttachmentFace[] {\n if (ev === undefined) return []\n return [\n ...imageBlocks(ev.data?.content),\n ...imageBlocks(ev.data?.message?.content),\n ]\n}\n\n/**\n * Read one session's full event log across host generations. Handle-era DSH\n * (the one without `readFrom`) exposes `open(id, 'read')` \u2192 slice reads that\n * must be looped until an empty slice, then `close()`; legacy hosts returned\n * the whole log from `readFrom(id, 0)`. One shared path so `fetchUsage` and\n * `fetchSessionUsage` close the handle on every outcome, success or failure.\n */\nasync function readSessionEvents(persistence: SessionPersistenceFace, id: string): Promise<SessionEventFace[] | undefined> {\n if (typeof persistence.open === 'function') {\n const handle = await persistence.open(id, 'read')\n try {\n const events: SessionEventFace[] = []\n for (;;) {\n const slice = await handle.read(events.length)\n const batch = slice.events ?? []\n if (batch.length === 0) return events\n // Loop, not spread: one slice can carry a whole long session and\n // `push(...batch)` would overflow the call-argument stack.\n for (const event of batch) events.push(event)\n }\n } finally {\n // A read handle has nothing durable to drain, and a close failure\n // must not mask the read's result or error.\n try { await handle.close() } catch { /* nothing to drain */ }\n }\n }\n if (persistence.readFrom === undefined) {\n throw new Error('\u5BBF\u4E3B sessionPersistence \u7F3A\u5C11 open() \u4E0E readFrom()\uFF0C\u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7')\n }\n const { events } = await persistence.readFrom(id, 0)\n return events\n}\n\nexport async function fetchUsage(persistence: SessionPersistenceFace | undefined, nowMs = Date.now()): Promise<UsageResponse> {\n if (persistence === undefined) return { ok: false, error: '\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528' }\n let headers: SessionHeaderFace[]\n try {\n headers = await persistence.list()\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n const dayMap = new Map<string, Bucket>()\n const hourMap = new Map<string, Bucket>()\n const overall = emptyOverall()\n // Per model: key `${provider}/${model}` -> totals and day/hour maps.\n const modelTotals = new Map<string, Bucket>()\n const modelDays = new Map<string, Map<string, Bucket>>()\n const modelHours = new Map<string, Map<string, Bucket>>()\n const sessions: SessionCost[] = []\n // Image usage across everything the replay sees. Day-keyed series mirrors\n // the daily bucket map; per-session tracks \"which run kept sending pictures\".\n const vision = emptyVision()\n const visionDays = new Map<string, VisionAcc>()\n const visionSessions = new Map<string, VisionAcc & { title: string }>()\n const coverage: UsageCoverage = {\n scope: 'local-dsh-session-logs',\n listedSessions: 0,\n scannedSessions: 0,\n usageRecords: 0,\n skippedRecords: 0,\n failedSessions: 0,\n earliestAt: null,\n latestAt: null,\n }\n // Peak / off-peak split, plus the same usage re-priced as if everything\n // had landed in an idle window (the shift-off-peak saving).\n const peakSplit: PeakSplit = {\n peak: { total: 0, cost: 0, calls: 0 },\n offPeak: { total: 0, cost: 0, calls: 0 },\n peakEraCost: 0,\n offPeakEraCost: 0,\n }\n\n const now = new Date(nowMs)\n const dateBefore = (days: number): Date => new Date(now.getFullYear(), now.getMonth(), now.getDate() - days)\n const windowAggregates: WindowAccumulator[] = USAGE_WINDOW_DAYS.map(days => ({\n days,\n startKey: dayKeyOf(dateBefore(days - 1)),\n endKey: dayKeyOf(now),\n dayMap: new Map(),\n hourMap: new Map(),\n overall: emptyOverall(),\n modelTotals: new Map(),\n modelDays: new Map(),\n modelHours: new Map(),\n sessions: [],\n vision: emptyVision(),\n visionDays: new Map(),\n visionSessions: new Map(),\n }))\n\n const bumpOverall = (\n bucket: OverallBucket,\n input: number,\n output: number,\n cache: number,\n reasoning: number,\n cost: number,\n cacheSavings: number,\n ): void => {\n bucket.input += input\n bucket.output += output\n bucket.cache += cache\n bucket.reasoning += reasoning\n bucket.total += input + output + cache\n bucket.cost += cost\n bucket.cacheSavings += cacheSavings\n bucket.calls += 1\n }\n\n const bumpModel = (\n totals: Map<string, Bucket>,\n days: Map<string, Map<string, Bucket>>,\n hours: Map<string, Map<string, Bucket>>,\n modelKey: string,\n dayKey: string,\n hourKey: string,\n input: number,\n output: number,\n cache: number,\n cost: number,\n ): void => {\n bump(totals, modelKey, input, output, cache, cost)\n let dayBuckets = days.get(modelKey)\n if (dayBuckets === undefined) {\n dayBuckets = new Map()\n days.set(modelKey, dayBuckets)\n }\n bump(dayBuckets, dayKey, input, output, cache, cost)\n let hourBuckets = hours.get(modelKey)\n if (hourBuckets === undefined) {\n hourBuckets = new Map()\n hours.set(modelKey, hourBuckets)\n }\n bump(hourBuckets, hourKey, input, output, cache, cost)\n }\n\n const bumpSession = (session: SessionCost, time: number, total: number, cost: number): void => {\n session.total += total\n session.cost += cost\n session.calls += 1\n if (time > session.lastActive) session.lastActive = time\n }\n\n const sessionIds = headers\n .map(header => header.id ?? header.sessionId ?? header.header?.id ?? header.header?.sessionId)\n .filter((sid): sid is string => sid !== undefined && sid !== '')\n coverage.listedSessions = sessionIds.length\n\n for (const sid of sessionIds) {\n try {\n const events = await readSessionEvents(persistence, sid)\n coverage.scannedSessions += 1\n const session: SessionCost = { id: sid, title: '', total: 0, cost: 0, calls: 0, lastActive: 0 }\n const windowSessions = windowAggregates.map((): SessionCost => ({ id: sid, title: '', total: 0, cost: 0, calls: 0, lastActive: 0 }))\n const sessionVision: VisionAcc & { title: string } = { ...emptyVision(), title: '' }\n const windowVisionSessions = windowAggregates.map((): VisionAcc & { title: string } => ({ ...emptyVision(), title: '' }))\n const scanned = addUsageEvent(events, (time, input, output, cache, reasoning, provider, model, _messageId, _turn, images) => {\n if (coverage.earliestAt === null || time < coverage.earliestAt) coverage.earliestAt = time\n if (coverage.latestAt === null || time > coverage.latestAt) coverage.latestAt = time\n const d = new Date(time)\n const dayKey = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n const hourKey = String(d.getHours())\n const modelKey = `${provider}/${model}`\n const cost = costOf(time, model, input, cache, output)\n const cacheSavings = cacheSavingOf(time, model, cache)\n const eventTotal = input + output + cache\n bump(dayMap, dayKey, input, output, cache, cost)\n bump(hourMap, hourKey, input, output, cache, cost)\n bumpModel(modelTotals, modelDays, modelHours, modelKey, dayKey, hourKey, input, output, cache, cost)\n bumpOverall(overall, input, output, cache, reasoning, cost, cacheSavings)\n bumpSession(session, time, eventTotal, cost)\n // Image blocks ride the record that consumed them: estimated tokens\n // (official resizing rule) priced at that call's cache-miss input rate.\n if (images.length > 0) {\n let imageTokensSum = 0\n let imageCostSum = 0\n let imageBytesSum = 0\n for (const image of images) {\n const width = typeof image.width === 'number' ? image.width : 0\n const height = typeof image.height === 'number' ? image.height : 0\n const tokens = estimateImageTokens(width, height)\n imageTokensSum += tokens\n imageCostSum += costOf(time, model, tokens, 0, 0)\n imageBytesSum += typeof image.bytes === 'number' ? image.bytes : 0\n }\n bumpVisionInto(vision, images.length, imageTokensSum, imageCostSum, imageBytesSum)\n bumpVisionMap(visionDays, dayKey, images.length, imageTokensSum, imageCostSum, imageBytesSum)\n bumpVisionInto(sessionVision, images.length, imageTokensSum, imageCostSum, imageBytesSum)\n for (let i = 0; i < windowAggregates.length; i++) {\n const aggregate = windowAggregates[i]\n if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue\n bumpVisionInto(aggregate.vision, images.length, imageTokensSum, imageCostSum, imageBytesSum)\n bumpVisionMap(aggregate.visionDays, dayKey, images.length, imageTokensSum, imageCostSum, imageBytesSum)\n bumpVisionInto(windowVisionSessions[i], images.length, imageTokensSum, imageCostSum, imageBytesSum)\n }\n }\n for (let i = 0; i < windowAggregates.length; i++) {\n const aggregate = windowAggregates[i]\n if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue\n bump(aggregate.dayMap, dayKey, input, output, cache, cost)\n bump(aggregate.hourMap, hourKey, input, output, cache, cost)\n bumpModel(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, modelKey, dayKey, hourKey, input, output, cache, cost)\n bumpOverall(aggregate.overall, input, output, cache, reasoning, cost, cacheSavings)\n bumpSession(windowSessions[i], time, eventTotal, cost)\n }\n const window = isPeak(time) ? peakSplit.peak : peakSplit.offPeak\n window.total += eventTotal\n window.cost += cost\n window.calls += 1\n peakSplit.peakEraCost += costUnderPeakEra(time, model, input, cache, output)\n peakSplit.offPeakEraCost += costUnderPeakEra(time, model, input, cache, output, true)\n })\n coverage.usageRecords += scanned.usageRecords\n coverage.skippedRecords += scanned.skippedRecords\n if (session.calls > 0) {\n const title = titleOf(events, sid)\n session.title = title\n sessions.push(session)\n for (let i = 0; i < windowAggregates.length; i++) {\n const windowSession = windowSessions[i]\n if (windowSession.calls === 0) continue\n windowSession.title = title\n windowAggregates[i].sessions.push(windowSession)\n }\n }\n if (sessionVision.images > 0) {\n sessionVision.title = titleOf(events, sid)\n visionSessions.set(sid, sessionVision)\n for (let i = 0; i < windowAggregates.length; i++) {\n const windowVision = windowVisionSessions[i]\n if (windowVision.images === 0) continue\n windowVision.title = sessionVision.title\n windowAggregates[i].visionSessions.set(sid, windowVision)\n }\n }\n } catch {\n // One broken session log must not sink the whole dashboard.\n coverage.failedSessions += 1\n }\n }\n\n const makeDaily = (map: Map<string, Bucket>, count: number): UsageData['daily'] => {\n const result: UsageData['daily'] = []\n for (let i = count - 1; i >= 0; i--) {\n const key = dayKeyOf(dateBefore(i))\n const b = map.get(key) ?? emptyBucket()\n result.push({ date: key, input: b.input, output: b.output, cache: b.cache, total: b.total, cost: b.cost, calls: b.calls })\n }\n return result\n }\n\n const makeHourly = (map: Map<string, Bucket>): UsageData['hourly'] => {\n const result: UsageData['hourly'] = []\n for (let i = 0; i < 24; i++) {\n const b = map.get(String(i)) ?? emptyBucket()\n result.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls })\n }\n return result\n }\n\n const makeModels = (\n totals: Map<string, Bucket>,\n daysByModel: Map<string, Map<string, Bucket>>,\n hoursByModel: Map<string, Map<string, Bucket>>,\n dayCount: number,\n ): ModelUsage[] => {\n const result: ModelUsage[] = []\n for (const [modelKey, bucket] of totals) {\n const slash = modelKey.indexOf('/')\n const provider = slash < 0 ? '' : modelKey.slice(0, slash)\n const model = slash < 0 ? modelKey : modelKey.slice(slash + 1)\n const days = daysByModel.get(modelKey)\n const hours = hoursByModel.get(modelKey)\n const daily: ModelSeriesPoint[] = makeDaily(days ?? new Map(), dayCount)\n .map(point => ({ total: point.total, cost: point.cost, calls: point.calls }))\n const hourly: ModelSeriesPoint[] = makeHourly(hours ?? new Map())\n .map(point => ({ total: point.total, cost: point.cost, calls: point.calls }))\n result.push({\n provider,\n model,\n input: bucket.input,\n output: bucket.output,\n cache: bucket.cache,\n total: bucket.total,\n cost: bucket.cost,\n calls: bucket.calls,\n daily,\n hourly,\n })\n }\n result.sort((a, b) => b.total - a.total)\n return result\n }\n\n const daily = makeDaily(dayMap, 30)\n const hourly = makeHourly(hourMap)\n\n // 52 full weeks (364 days) rather than a calendar year, so the grid is\n // always a whole number of week-columns and never carries an orphan day.\n const heatmap: UsageData['heatmap'] = []\n for (let i = 363; i >= 0; i--) {\n const key = dayKeyOf(dateBefore(i))\n const b = dayMap.get(key) ?? emptyBucket()\n heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls })\n }\n\n const makeVisionDaily = (map: Map<string, VisionAcc>, count: number): VisionDayPoint[] => {\n const result: VisionDayPoint[] = []\n for (let i = count - 1; i >= 0; i--) {\n const key = dayKeyOf(dateBefore(i))\n const b = map.get(key) ?? emptyVision()\n result.push({ date: key, images: b.images, imageTokens: b.imageTokens, cost: b.cost })\n }\n return result\n }\n\n const makeVisionSessions = (map: Map<string, VisionAcc & { title: string }>): VisionSession[] =>\n [...map.entries()]\n .map(([id, acc]) => ({\n id,\n title: acc.title !== '' ? acc.title : `\u4F1A\u8BDD ${id.slice(0, 8)}`,\n images: acc.images,\n imageTokens: acc.imageTokens,\n cost: acc.cost,\n }))\n .sort((a, b) => b.images - a.images)\n .slice(0, SESSION_TOP_N)\n\n const models = makeModels(modelTotals, modelDays, modelHours, 30)\n const windows = windowAggregates.map(aggregate => ({\n days: aggregate.days,\n daily: makeDaily(aggregate.dayMap, aggregate.days),\n hourly: makeHourly(aggregate.hourMap),\n totals: { ...aggregate.overall },\n models: makeModels(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, aggregate.days),\n sessions: [...aggregate.sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),\n sessionCount: aggregate.sessions.length,\n vision: { ...aggregate.vision },\n visionDaily: makeVisionDaily(aggregate.visionDays, aggregate.days),\n visionSessions: makeVisionSessions(aggregate.visionSessions),\n }))\n\n return {\n ok: true,\n data: {\n daily,\n hourly,\n heatmap,\n models,\n summary: summarize(dayMap, new Date(nowMs)),\n pricing: pricingInfo(nowMs),\n peakSplit,\n sessions: [...sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),\n sessionCount: sessions.length,\n coverage,\n windows,\n vision: { ...vision },\n visionDaily: makeVisionDaily(visionDays, 30),\n visionSessions: makeVisionSessions(visionSessions),\n totals: {\n input: overall.input,\n output: overall.output,\n cache: overall.cache,\n reasoning: overall.reasoning,\n total: overall.total,\n cost: overall.cost,\n calls: overall.calls,\n cacheSavings: overall.cacheSavings,\n },\n },\n }\n}\n\n/**\n * Usage for exactly one session \u2014 the \u300C\u989D\u5EA6\u300Dtab's own conversation, as\n * opposed to `fetchUsage`'s account-wide replay. Reads a single session log\n * (not every session on disk), so it is cheap enough to call on every tab\n * mount without a TTL memo; the caller is expected to re-fetch whenever the\n * session id it cares about changes.\n */\nexport async function fetchSessionUsage(persistence: SessionPersistenceFace | undefined, sessionId: string): Promise<SessionUsageResponse> {\n if (persistence === undefined) return { ok: false, error: '\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528' }\n if (typeof sessionId !== 'string' || sessionId === '') return { ok: false, error: '\u7F3A\u5C11\u4F1A\u8BDD id' }\n // Defense in depth: src/index.ts already rejects a malformed `id` before\n // this function is ever called (see SESSION_ID_PATTERN in contract.ts),\n // but this function is also unit-tested and reachable directly, so the\n // same check is repeated here rather than trusted to the one caller.\n if (!isValidSessionId(sessionId)) return { ok: false, error: '\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5' }\n\n let events: SessionEventFace[] | undefined\n try {\n events = await readSessionEvents(persistence, sessionId)\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n\n const modelTotals = new Map<string, Bucket>()\n let total = 0\n let cost = 0\n let calls = 0\n let firstActive: number | null = null\n let lastActive: number | null = null\n const turnTotals = new Map<string, { messageId: string; total: number; cost: number; calls: number }>()\n\n addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model, messageId, turn) => {\n const modelKey = `${provider}/${model}`\n const eventCost = costOf(time, model, input, cache, output)\n const eventTotal = input + output + cache\n bump(modelTotals, modelKey, input, output, cache, eventCost)\n total += eventTotal\n cost += eventCost\n calls += 1\n if (firstActive === null || time < firstActive) firstActive = time\n if (lastActive === null || time > lastActive) lastActive = time\n if (messageId !== undefined) {\n // A turn may make several model calls while tools run. The chat renders\n // one action strip beside its final assistant message, so keep replacing\n // the address while accumulating every step's usage under the turn id.\n const key = turn === undefined ? `message:${messageId}` : `turn:${turn}`\n const aggregate = turnTotals.get(key) ?? { messageId, total: 0, cost: 0, calls: 0 }\n aggregate.messageId = messageId\n aggregate.total += eventTotal\n aggregate.cost += eventCost\n aggregate.calls += 1\n turnTotals.set(key, aggregate)\n }\n })\n\n const models: SessionModelUsage[] = [...modelTotals.entries()]\n .map(([modelKey, bucket]): SessionModelUsage => {\n const slash = modelKey.indexOf('/')\n const provider = slash < 0 ? '' : modelKey.slice(0, slash)\n const model = slash < 0 ? modelKey : modelKey.slice(slash + 1)\n return {\n provider,\n model,\n input: bucket.input,\n output: bucket.output,\n cache: bucket.cache,\n total: bucket.total,\n cost: bucket.cost,\n calls: bucket.calls,\n }\n })\n .sort((a, b) => b.cost - a.cost)\n\n return {\n ok: true,\n data: {\n sessionId,\n title: titleOf(events, sessionId),\n total,\n cost,\n calls,\n firstActive,\n lastActive,\n models,\n turns: [...turnTotals.values()],\n },\n }\n}\n", "import type { ServerResponse } from 'node:http'\n\nexport function writeJson(res: ServerResponse, status: number, body: unknown, headers?: Record<string, string>): void {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...headers })\n res.end(JSON.stringify(body))\n}\n", "/**\n * dsh-usage-dashboard host plugin.\n *\n * Registers the package's own fenced JSON API under\n * `/api/dsh-usage-dashboard` (balance + usage + session), consumed by the\n * client half through same-origin fetch. `balance` and `usage` are memoized\n * with a TTL so that tab switches and the widget's polling don't replay\n * session logs or hammer the DeepSeek API; responses carry `Cache-Control`\n * so the browser's HTTP cache serves repeat requests without a network\n * round trip. `?refresh=1` bypasses the memo for an explicit force refresh.\n * `session` (usage for exactly one session, keyed by `?id=`) reads a single\n * session log and is deliberately not memoized \u2014 it is cheap and must\n * reflect that session's live state, not a 5-minute-stale snapshot.\n */\nimport type { HostContext } from './context.ts'\nimport { isValidSessionId } from './contract.ts'\nimport { memoize } from './memo.ts'\nimport { isTrustedApiRequest } from './trust-fence.ts'\nimport { fetchBalance, fetchSessionUsage, fetchUsage } from './usage.ts'\nimport { writeJson } from './wire.ts'\n\n/** Plugin identity for the cordis.patch.yml row (and the client bundle id). */\nexport const name = 'dsh-usage-dashboard'\n\n/** Services required before load: the web server plus the two data sources. */\nexport const inject = ['webServer', 'credentials', 'sessionPersistence']\n\n/** Freshness windows: balance 60s (the widget also polls every 60s), usage\n * 5min (aggregating every session log is the expensive path). */\nconst BALANCE_TTL_MS = 60_000\nconst USAGE_TTL_MS = 5 * 60_000\n\n/**\n * Mount the /api/dsh-usage-dashboard routes.\n * @param ctx - host Cordis context.\n */\nexport function apply(ctx: HostContext): void {\n // `deepseekAccount` is optional (API-key-only hosts never mount it), so it\n // is read through `get()` rather than declared in `inject` \u2014 a required\n // service would keep this plugin from loading at all on those hosts.\n const balanceMemo = memoize(BALANCE_TTL_MS, () => fetchBalance(ctx.credentials, ctx.get('deepseekAccount')))\n const usageMemo = memoize(USAGE_TTL_MS, () => fetchUsage(ctx.sessionPersistence))\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dsh-usage-dashboard',\n handler: async (req, res) => {\n if (!isTrustedApiRequest(req)) {\n writeJson(res, 403, { ok: false, error: 'forbidden' })\n return\n }\n if (req.method !== 'GET') {\n writeJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n const url = new URL(req.url ?? '/', 'http://dsh.internal')\n const pathname = url.pathname\n const force = url.searchParams.get('refresh') === '1'\n if (pathname === '/api/dsh-usage-dashboard/balance') {\n const payload = await (force ? balanceMemo.refresh() : balanceMemo.get())\n const headers = payload.ok\n ? { 'cache-control': `private, max-age=${Math.round(BALANCE_TTL_MS / 1000)}` }\n : { 'cache-control': 'no-store' }\n writeJson(res, 200, payload, headers)\n } else if (pathname === '/api/dsh-usage-dashboard/usage') {\n const payload = await (force ? usageMemo.refresh() : usageMemo.get())\n const headers = payload.ok\n ? { 'cache-control': `private, max-age=${Math.round(USAGE_TTL_MS / 1000)}` }\n : { 'cache-control': 'no-store' }\n writeJson(res, 200, payload, headers)\n } else if (pathname === '/api/dsh-usage-dashboard/session') {\n const id = url.searchParams.get('id')\n // `id` arrives verbatim from the browser-controlled `?id=` query\n // parameter (unlike `usage`'s session ids, which the host itself\n // enumerates via `persistence.list()`), so a malformed value is\n // rejected here \u2014 before it ever reaches the persistence read path \u2014\n // rather than trusted to fetchSessionUsage's own defense-in-depth\n // check. Reject as early as the route layer allows.\n if (id === null || id.trim() === '' || !isValidSessionId(id)) {\n // A missing/blank/malformed `id` is the caller's mistake (bad\n // input), so it gets a real 4xx; any other failure (e.g. the log\n // itself unreadable) keeps the same \"200 with ok:false\" shape as\n // balance/usage so the client's one error-handling path covers all\n // three endpoints.\n const error = id === null || id.trim() === '' ? '\u7F3A\u5C11\u4F1A\u8BDD id' : '\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5'\n writeJson(res, 400, { ok: false, error }, { 'cache-control': 'no-store' })\n return\n }\n const payload = await fetchSessionUsage(ctx.sessionPersistence, id)\n // id is already known-well-formed here, so any remaining failure is\n // downstream (e.g. the log itself unreadable) and keeps the same\n // \"200 with ok:false\" shape as balance/usage.\n writeJson(res, 200, payload, { 'cache-control': 'no-store' })\n } else {\n writeJson(res, 404, { ok: false, error: 'not found' })\n }\n },\n }), 'dsh-usage-dashboard: /api routes')\n}\n"],
5
- "mappings": ";AAoNO,IAAM,oBAAoB,CAAC,GAAG,IAAI,IAAI,GAAG;AAgBzC,IAAM,qBAAqB;AAE3B,IAAM,mBAAmB,CAAC,OAAwB,mBAAmB,KAAK,EAAE;;;ACtM5E,SAAS,QAAmC,OAAe,SAAoC;AACpG,QAAM,QAAsB,EAAE,OAAO,QAAW,UAAU,OAAU;AAEpE,QAAM,MAAM,MAAkB;AAC5B,QAAI,MAAM,aAAa,OAAW,QAAO,MAAM;AAC/C,UAAM,OAAO,QAAQ,EAAE;AAAA,MACrB,CAAC,UAAU;AACT,cAAM,WAAW;AACjB,YAAI,MAAM,GAAI,OAAM,QAAQ,EAAE,OAAO,OAAO,KAAK,IAAI,EAAE;AACvD,eAAO;AAAA,MACT;AAAA,MACA,CAAC,QAAiB;AAChB,cAAM,WAAW;AACjB,cAAM;AAAA,MACR;AAAA,IACF;AACA,UAAM,WAAW;AACjB,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,KAAK,MAAM;AACT,YAAM,MAAM,MAAM;AAClB,UAAI,QAAQ,UAAa,IAAI,MAAM,MAAM,KAAK,IAAI,IAAI,IAAI,QAAQ,OAAO;AACvE,eAAO,QAAQ,QAAQ,IAAI,KAAK;AAAA,MAClC;AACA,aAAO,IAAI;AAAA,IACb;AAAA,IACA,SAAS,MAAM,IAAI;AAAA,EACrB;AACF;;;ACtDA,SAAS,mBAAmB,UAA2B;AACrD,SAAO,aAAa,eAAe,aAAa,eAAe,aAAa,SAAS,aAAa;AACpG;AAEO,SAAS,oBAAoB,KAA+B;AACjE,QAAM,OAAO,IAAI,QAAQ;AACzB,MAAI,SAAS,OAAW,QAAO;AAC/B,MAAI;AACJ,MAAI;AACF,cAAU,IAAI,IAAI,UAAU,IAAI,EAAE;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,mBAAmB,QAAQ,QAAQ,EAAG,QAAO;AAClD,MAAI,IAAI,QAAQ,gBAAgB,MAAM,aAAc,QAAO;AAC3D,QAAM,SAAS,IAAI,QAAQ;AAC3B,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI;AACF,WAAO,IAAI,IAAI,MAAM,EAAE,SAAS,QAAQ;AAAA,EAC1C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACLO,IAAM,uBAAuB,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI,GAAG,CAAC;AAI3D,IAAM,4BAA4B,KAAK,IAAI,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC;AAI/D,IAAM,8BAA8B,KAAK,IAAI,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC;AAGxE,IAAM,eAAwC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;AAEhE,IAAM,oBAAoB,IAAI;AAK9B,IAAM,aAAyC;AAAA,EAC7C,KAAK,EAAE,UAAU,OAAO,OAAO,GAAG,QAAQ,EAAE;AAAA,EAC5C,OAAO,EAAE,UAAU,MAAM,OAAO,GAAG,QAAQ,EAAE;AAAA;AAAA;AAAA,EAG7C,QAAQ,EAAE,UAAU,MAAM,OAAO,GAAG,QAAQ,EAAE;AAChD;AAGA,IAAM,mBAA+C;AAAA,EACnD,KAAK,EAAE,UAAU,KAAK,OAAO,GAAG,QAAQ,GAAG;AAAA,EAC3C,OAAO,EAAE,UAAU,KAAK,OAAO,GAAG,QAAQ,EAAE;AAAA;AAAA;AAAA,EAG5C,QAAQ,EAAE,UAAU,KAAK,OAAO,GAAG,QAAQ,EAAE;AAC/C;AAGA,IAAM,mBAA+C;AAAA,EACnD,KAAK,EAAE,UAAU,KAAK,OAAO,GAAG,QAAQ,GAAG;AAAA,EAC3C,OAAO,EAAE,UAAU,MAAM,OAAO,GAAG,QAAQ,EAAE;AAAA,EAC7C,QAAQ,EAAE,UAAU,MAAM,OAAO,GAAG,QAAQ,EAAE;AAChD;AAWA,IAAM,aAAkC;AAAA,EACtC,EAAE,QAAQ,sBAAsB,MAAM,kBAAkB,cAAc,MAAM;AAAA,EAC5E,EAAE,QAAQ,2BAA2B,MAAM,kBAAkB,cAAc,KAAK;AAClF;AAGA,SAAS,MAAM,QAA0B;AACvC,MAAI,MAAM,WAAW,CAAC;AACtB,aAAW,aAAa,WAAY,KAAI,UAAU,UAAU,OAAQ,OAAM;AAC1E,SAAO;AACT;AAEA,IAAM,SAAS,CAAC,WAAuC;AAAA,EACrD,UAAU,MAAM,WAAW;AAAA,EAC3B,OAAO,MAAM,QAAQ;AAAA,EACrB,QAAQ,MAAM,SAAS;AACzB;AAKO,SAAS,OAAO,OAAqB;AAC1C,QAAMA,QAAO,MAAM,YAAY;AAC/B,MAAIA,MAAK,SAAS,QAAQ,EAAG,QAAO;AACpC,MAAIA,MAAK,SAAS,OAAO,EAAG,QAAO;AACnC,SAAO;AACT;AAIA,SAAS,aAAa,QAAgB,OAAqB;AACzD,QAAM,OAAO,OAAO,KAAK;AACzB,SAAO,SAAS,SAAS,UAAU,8BAA8B,UAAU;AAC7E;AAGA,SAAS,iBAAiB,QAAyB;AACjD,QAAM,MAAM,IAAI,KAAK,SAAS,iBAAiB,EAAE,UAAU;AAC3D,SAAO,OAAO,KAAK,OAAO;AAC5B;AAeO,SAAS,oBAAoB,OAAe,QAAwB;AACzE,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,OAAO,SAAS,MAAM,KAAK,SAAS,KAAK,UAAU,EAAG,QAAO;AAC7F,QAAM,OAAO,QAAQ;AACrB,QAAM,aAAa,KAAK,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,IAAI,CAAC;AAChE,SAAO,OAAO,cAAc,MAAM;AACpC;AAIO,SAAS,OAAO,QAAyB;AAC9C,MAAI,MAAM,MAAM,EAAE,gBAAgB,CAAC,iBAAiB,MAAM,EAAG,QAAO;AACpE,QAAM,OAAO,KAAK,QAAS,SAAS,qBAAqB,QAAc,SAAc,QAAa,IAAS;AAC3G,SAAO,aAAa,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,QAAQ,QAAQ,OAAO,EAAE;AACpE;AAGO,SAAS,QAAQ,QAAgB,OAA6B;AACnE,MAAI,SAAS,qBAAsB,QAAO,WAAW,OAAO,KAAK,CAAC;AAClE,QAAM,OAAO,MAAM,MAAM,EAAE,KAAK,aAAa,QAAQ,KAAK,CAAC;AAC3D,SAAO,OAAO,MAAM,IAAI,OAAO,OAAO,IAAI;AAC5C;AAMO,SAAS,cAAc,QAAgB,OAAe,OAAuB;AAClF,QAAM,QAAQ,QAAQ,QAAQ,KAAK;AACnC,SAAQ,SAAS,MAAM,QAAQ,MAAM,YAAa;AACpD;AAEA,IAAM,aAAa,CAAC,OAAqB,OAAe,OAAe,YACpE,QAAQ,MAAM,QAAQ,QAAQ,MAAM,WAAW,SAAS,MAAM,UAAU;AAGpE,SAAS,OAAO,QAAgB,OAAe,OAAe,OAAe,QAAwB;AAC1G,SAAO,WAAW,QAAQ,QAAQ,KAAK,GAAG,OAAO,OAAO,MAAM;AAChE;AAQO,SAAS,iBAAiB,QAAgB,OAAe,OAAe,OAAe,QAAgB,eAAe,OAAe;AAC1I,QAAM,OAAO,WAAW,WAAW,SAAS,CAAC,EAAE,KAAK,aAAa,QAAQ,KAAK,CAAC;AAC/E,QAAM,QAAQ,CAAC,gBAAgB,OAAO,MAAM,IAAI,OAAO,OAAO,IAAI;AAClE,SAAO,WAAW,OAAO,OAAO,OAAO,MAAM;AAC/C;AAKO,SAAS,YAAY,OAA4B;AACtD,QAAM,MAAM,MAAM,KAAK;AACvB,QAAM,MAAM,CAAC,OAAe,UAAgB,EAAE,OAAO,MAAM,IAAI,KAAK,IAAI,GAAG,SAAS,OAAO,IAAI,KAAK,IAAI,CAAC,EAAE;AAC3G,SAAO;AAAA,IACL,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,WAAW,OAAO,KAAK;AAAA,IACvB,aAAa,CAAC,oBAAe,kBAAa;AAAA,IAC1C,OAAO,CAAC,IAAI,kBAAkB,OAAO,GAAG,IAAI,mBAAmB,KAAK,CAAC;AAAA,EACvE;AACF;;;ACnLA,IAAM,OAAO,CAAC,MAAuB,IAAI,KAAK,IAAI,CAAC,KAAK,OAAO,CAAC;AAEhE,SAAS,aAAa,KAAsB;AAC1C,SAAQ,KAAqC,WAAW,OAAO,GAAG;AACpE;AAMA,IAAM,iBAAoC;AAAA,EACxC,SAAS,QAAQ,IAAI,sBAAsB;AAAA,EAC3C,QAAQ;AAAA,EACR,uBAAuB,EAAC,oBAAI,KAAK,GAAE,kBAAkB,IAAI;AAC3D;AAKA,SAAS,gBACP,iBACA,gBACe;AACf,QAAM,aAAa,oBAAI,IAAkE;AACzF,QAAM,MAAM,CAAC,SAAuC,UAAwC;AAC1F,eAAW,UAAU,SAAS;AAC5B,YAAM,WAAW,OAAO,YAAY;AACpC,YAAM,SAAS,OAAO,OAAO,WAAW,GAAG;AAC3C,UAAI,aAAa,MAAM,CAAC,OAAO,SAAS,MAAM,EAAG;AACjD,YAAM,MAAM,WAAW,IAAI,QAAQ,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,UAAU,EAAE;AAC5E,UAAI,KAAK,KAAK;AACd,UAAI,SAAS;AACb,iBAAW,IAAI,UAAU,GAAG;AAAA,IAC9B;AAAA,EACF;AACA,MAAI,iBAAiB,UAAU;AAC/B,MAAI,gBAAgB,SAAS;AAC7B,SAAO,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,CAAC,UAAU,GAAG,OAAO;AAAA,IAC/C;AAAA,IACA,OAAO,IAAI,MAAM,QAAQ,CAAC;AAAA,IAC1B,SAAS,IAAI,QAAQ,QAAQ,CAAC;AAAA,IAC9B,UAAU,IAAI,SAAS,QAAQ,CAAC;AAAA,EAClC,EAAE;AACJ;AAMA,eAAe,mBAAmB,SAEhC;AACA,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,WAAW,cAAc;AACtD,QAAI,WAAW,KAAM,QAAO,EAAE,IAAI,OAAO,QAAQ,aAAa;AAC9D,QAAI,OAAO,WAAW,QAAS,QAAO,EAAE,IAAI,OAAO,QAAQ,SAAS;AACpE,UAAM,WAAW,gBAAgB,OAAO,SAAS,CAAC,GAAG,OAAO,gBAAgB,CAAC,CAAC;AAC9E,WAAO,EAAE,IAAI,MAAM,MAAM,EAAE,aAAa,SAAS,SAAS,GAAG,SAAS,EAAE;AAAA,EAC1E,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,QAAQ,SAAS;AAAA,EACvC;AACF;AAEA,eAAsB,aACpB,aACA,SAC0B;AAC1B,QAAM,cAAc,YAAY,SAAY,SAAY,MAAM,mBAAmB,OAAO;AACxF,MAAI,aAAa,OAAO,KAAM,QAAO,EAAE,IAAI,MAAM,MAAM,YAAY,KAAK;AACxE,MAAI,gBAAgB,OAAW,QAAO,EAAE,IAAI,OAAO,OAAO,6CAAU;AACpE,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,YAAY,QAAQ,kBAAkB;AAAA,EACrD,SAAS,KAAK;AACZ,WAAO,EAAE,IAAI,OAAO,OAAO,0CAAiB,aAAa,GAAG,CAAC,GAAG;AAAA,EAClE;AACA,MAAI,SAAS,UAAa,KAAK,UAAU,IAAI;AAI3C,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO,aAAa,WAAW,WAC3B,0GACA;AAAA,IACN;AAAA,EACF;AACA,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,yCAAyC;AAAA,MACzD,SAAS,EAAE,eAAe,UAAU,KAAK,KAAK,GAAG;AAAA,MACjD,QAAQ,YAAY,QAAQ,GAAK;AAAA,IACnC,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,EAAE,IAAI,OAAO,OAAO,yDAAY,aAAa,GAAG,CAAC,GAAG;AAAA,EAC7D;AACA,MAAI,CAAC,IAAI,GAAI,QAAO,EAAE,IAAI,OAAO,OAAO,8DAAiB,IAAI,MAAM,SAAI;AACvE,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,IAAI,KAAK;AAAA,EAC1B,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,OAAO,mDAAW;AAAA,EACxC;AACA,QAAM,IAAI;AAIV,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,MACJ,aAAa,EAAE,iBAAiB;AAAA,MAChC,WAAW,EAAE,iBAAiB,CAAC,GAAG,IAAI,QAAM;AAAA,QAC1C,UAAU,EAAE,YAAY;AAAA,QACxB,OAAO,EAAE,iBAAiB;AAAA,QAC1B,SAAS,EAAE,mBAAmB;AAAA,QAC9B,UAAU,EAAE,qBAAqB;AAAA,MACnC,EAAE;AAAA,IACJ;AAAA,EACF;AACF;AAWA,IAAM,cAAc,OAAe,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,EAAE;AAKhG,SAAS,KAAK,KAA0B,KAAa,OAAe,QAAgB,OAAe,MAAoB;AACrH,QAAM,SAAS,IAAI,IAAI,GAAG,KAAK,YAAY;AAC3C,SAAO,SAAS;AAChB,SAAO,UAAU;AACjB,SAAO,SAAS;AAEhB,SAAO,SAAS,QAAQ,SAAS;AACjC,SAAO,QAAQ;AACf,SAAO,SAAS;AAChB,MAAI,IAAI,KAAK,MAAM;AACrB;AAGA,IAAM,eAAe,OAAsB,EAAE,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,EAAE;AAW7F,IAAM,cAAc,OAAkB,EAAE,QAAQ,GAAG,aAAa,GAAG,MAAM,GAAG,OAAO,EAAE;AAErF,SAAS,eACP,KACA,QACA,aACA,MACA,OACM;AACN,MAAI,UAAU;AACd,MAAI,eAAe;AACnB,MAAI,QAAQ;AACZ,MAAI,SAAS;AACf;AAEA,SAAS,cACP,KACA,KACA,QACA,aACA,MACA,OACM;AACN,QAAM,MAAM,IAAI,IAAI,GAAG,KAAK,YAAY;AACxC,iBAAe,KAAK,QAAQ,aAAa,MAAM,KAAK;AACpD,MAAI,IAAI,KAAK,GAAG;AAClB;AAmBA,IAAM,gBAAgB;AAOtB,SAAS,QAAQ,QAAwC,IAAoB;AAC3E,MAAI,QAAQ;AACZ,MAAI,WAAW,QAAW;AACxB,eAAW,MAAM,QAAQ;AACvB,UAAI,IAAI,SAAS,gBAAiB;AAClC,YAAM,OAAO,GAAG,MAAM;AACtB,UAAI,OAAO,SAAS,YAAY,SAAS,GAAI,SAAQ;AAAA,IACvD;AAAA,EACF;AACA,SAAO,UAAU,KAAK,QAAQ,gBAAM,GAAG,MAAM,GAAG,CAAC,CAAC;AACpD;AAEA,IAAM,WAAW,CAAC,MAAoB,GAAG,EAAE,YAAY,CAAC,IAAI,KAAK,EAAE,SAAS,IAAI,CAAC,CAAC,IAAI,KAAK,EAAE,QAAQ,CAAC,CAAC;AAOvG,SAAS,UAAU,QAA6B,KAAyB;AACvE,QAAM,MAAM,CAAC,YAAmD;AAC9D,UAAM,MAAmB,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,EAAE;AACvD,eAAW,CAAC,KAAK,MAAM,KAAK,QAAQ;AAClC,UAAI,CAAC,QAAQ,GAAG,EAAG;AACnB,UAAI,SAAS,OAAO;AACpB,UAAI,QAAQ,OAAO;AACnB,UAAI,SAAS,OAAO;AAAA,IACtB;AACA,WAAO;AAAA,EACT;AACA,QAAM,WAAW,SAAS,GAAG;AAC7B,QAAM,eAAe,SAAS,IAAI,KAAK,IAAI,YAAY,GAAG,IAAI,SAAS,GAAG,IAAI,QAAQ,IAAI,CAAC,CAAC;AAC5F,QAAM,cAAc,GAAG,IAAI,YAAY,CAAC,IAAI,KAAK,IAAI,SAAS,IAAI,CAAC,CAAC;AACpE,QAAM,YAAY,IAAI,KAAK,IAAI,YAAY,GAAG,IAAI,SAAS,IAAI,GAAG,CAAC;AACnE,QAAM,kBAAkB,GAAG,UAAU,YAAY,CAAC,IAAI,KAAK,UAAU,SAAS,IAAI,CAAC,CAAC;AACpF,QAAM,aAAa,IAAI,QAAQ;AAC/B,SAAO;AAAA,IACL,OAAO,IAAI,SAAO,QAAQ,QAAQ;AAAA,IAClC,WAAW,IAAI,SAAO,QAAQ,YAAY;AAAA,IAC1C,OAAO,IAAI,SAAO,IAAI,WAAW,WAAW,CAAC;AAAA,IAC7C,iBAAiB,IAAI,SAAO,IAAI,WAAW,eAAe,KAAK,OAAO,IAAI,MAAM,CAAC,CAAC,KAAK,UAAU;AAAA,EACnG;AACF;AAgBA,SAAS,cACP,QACA,SAYwD;AACxD,QAAM,SAAS,EAAE,cAAc,GAAG,gBAAgB,EAAE;AACpD,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI,WAAW;AACf,MAAI,QAAQ;AACZ,QAAM,gBAAuC,CAAC;AAC9C,aAAW,MAAM,QAAQ;AACvB,QAAI,IAAI,SAAS,kBAAkB;AACjC,YAAM,MAAM,GAAG,MAAM,QAAQ;AAC7B,UAAI,KAAK,aAAa,UAAa,KAAK,UAAU,QAAW;AAC3D,mBAAW,IAAI;AACf,gBAAQ,IAAI;AAAA,MACd;AACA;AAAA,IACF;AACA,kBAAc,KAAK,GAAG,cAAc,EAAE,CAAC;AACvC,QAAI,IAAI,SAAS,oBAAqB;AACtC,UAAM,QAAQ,GAAG,MAAM;AACvB,QAAI,UAAU,OAAW;AACzB,UAAM,SAAS,CAAC,MAAM,aAAa,MAAM,cAAc,MAAM,iBAAiB,MAAM,eAAe,EAChG,IAAI,WAAS,UAAU,SAAY,IAAI,OAAO,KAAK,CAAC;AACvD,QAAI,OAAO,GAAG,SAAS,YAAY,CAAC,OAAO,SAAS,GAAG,IAAI,KAAK,OAAO,KAAK,WAAS,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,CAAC,GAAG;AAC1H,aAAO,kBAAkB;AACzB;AAAA,IACF;AACA,UAAM,CAAC,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,YAAY,CAAC,IAAI;AAC1D,UAAM,eAAe,GAAG,MAAM,SAAS;AACvC,UAAM,YAAY,OAAO,iBAAiB,YAAY,iBAAiB,KAAK,eAAe;AAC3F,UAAM,UAAU,GAAG,MAAM;AACzB,UAAM,OAAO,OAAO,YAAY,YAAY,OAAO,SAAS,OAAO,IAAI,UAAU;AACjF,YAAQ,GAAG,MAAM,OAAO,QAAQ,OAAO,WAAW,UAAU,OAAO,WAAW,MAAM,aAAa;AACjG,kBAAc,SAAS;AACvB,WAAO,gBAAgB;AAAA,EACzB;AACA,SAAO;AACT;AAIA,SAAS,YAAY,QAA+D;AAClF,QAAM,QAA+B,CAAC;AACtC,QAAM,UAAU,CAAC,UAAgD;AAC/D,eAAW,QAAQ,SAAS,CAAC,GAAG;AAC9B,UAAI,MAAM,SAAS,WAAW,KAAK,eAAe,UAAa,KAAK,eAAe,MAAM;AACvF,cAAM,KAAK,KAAK,UAAU;AAAA,MAC5B;AACA,UAAI,MAAM,QAAQ,MAAM,OAAO,EAAG,SAAQ,KAAK,OAAO;AAAA,IACxD;AAAA,EACF;AACA,UAAQ,MAAM;AACd,SAAO;AACT;AAIA,SAAS,cAAc,IAAyD;AAC9E,MAAI,OAAO,OAAW,QAAO,CAAC;AAC9B,SAAO;AAAA,IACL,GAAG,YAAY,GAAG,MAAM,OAAO;AAAA,IAC/B,GAAG,YAAY,GAAG,MAAM,SAAS,OAAO;AAAA,EAC1C;AACF;AASA,eAAe,kBAAkB,aAAqC,IAAqD;AACzH,MAAI,OAAO,YAAY,SAAS,YAAY;AAC1C,UAAM,SAAS,MAAM,YAAY,KAAK,IAAI,MAAM;AAChD,QAAI;AACF,YAAMC,UAA6B,CAAC;AACpC,iBAAS;AACP,cAAM,QAAQ,MAAM,OAAO,KAAKA,QAAO,MAAM;AAC7C,cAAM,QAAQ,MAAM,UAAU,CAAC;AAC/B,YAAI,MAAM,WAAW,EAAG,QAAOA;AAG/B,mBAAW,SAAS,MAAO,CAAAA,QAAO,KAAK,KAAK;AAAA,MAC9C;AAAA,IACF,UAAE;AAGA,UAAI;AAAE,cAAM,OAAO,MAAM;AAAA,MAAE,QAAQ;AAAA,MAAyB;AAAA,IAC9D;AAAA,EACF;AACA,MAAI,YAAY,aAAa,QAAW;AACtC,UAAM,IAAI,MAAM,6HAAuD;AAAA,EACzE;AACA,QAAM,EAAE,OAAO,IAAI,MAAM,YAAY,SAAS,IAAI,CAAC;AACnD,SAAO;AACT;AAEA,eAAsB,WAAW,aAAiD,QAAQ,KAAK,IAAI,GAA2B;AAC5H,MAAI,gBAAgB,OAAW,QAAO,EAAE,IAAI,OAAO,OAAO,+DAAa;AACvE,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,YAAY,KAAK;AAAA,EACnC,SAAS,KAAK;AACZ,WAAO,EAAE,IAAI,OAAO,OAAO,yDAAY,aAAa,GAAG,CAAC,GAAG;AAAA,EAC7D;AACA,QAAM,SAAS,oBAAI,IAAoB;AACvC,QAAM,UAAU,oBAAI,IAAoB;AACxC,QAAM,UAAU,aAAa;AAE7B,QAAM,cAAc,oBAAI,IAAoB;AAC5C,QAAM,YAAY,oBAAI,IAAiC;AACvD,QAAM,aAAa,oBAAI,IAAiC;AACxD,QAAM,WAA0B,CAAC;AAGjC,QAAM,SAAS,YAAY;AAC3B,QAAM,aAAa,oBAAI,IAAuB;AAC9C,QAAM,iBAAiB,oBAAI,IAA2C;AACtE,QAAM,WAA0B;AAAA,IAC9B,OAAO;AAAA,IACP,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,YAAY;AAAA,IACZ,UAAU;AAAA,EACZ;AAGA,QAAM,YAAuB;AAAA,IAC3B,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,EAAE;AAAA,IACpC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,EAAE;AAAA,IACvC,aAAa;AAAA,IACb,gBAAgB;AAAA,EAClB;AAEA,QAAM,MAAM,IAAI,KAAK,KAAK;AAC1B,QAAM,aAAa,CAAC,SAAuB,IAAI,KAAK,IAAI,YAAY,GAAG,IAAI,SAAS,GAAG,IAAI,QAAQ,IAAI,IAAI;AAC3G,QAAM,mBAAwC,kBAAkB,IAAI,WAAS;AAAA,IAC3E;AAAA,IACA,UAAU,SAAS,WAAW,OAAO,CAAC,CAAC;AAAA,IACvC,QAAQ,SAAS,GAAG;AAAA,IACpB,QAAQ,oBAAI,IAAI;AAAA,IAChB,SAAS,oBAAI,IAAI;AAAA,IACjB,SAAS,aAAa;AAAA,IACtB,aAAa,oBAAI,IAAI;AAAA,IACrB,WAAW,oBAAI,IAAI;AAAA,IACnB,YAAY,oBAAI,IAAI;AAAA,IACpB,UAAU,CAAC;AAAA,IACX,QAAQ,YAAY;AAAA,IACpB,YAAY,oBAAI,IAAI;AAAA,IACpB,gBAAgB,oBAAI,IAAI;AAAA,EAC1B,EAAE;AAEF,QAAM,cAAc,CAClB,QACA,OACA,QACA,OACA,WACA,MACA,iBACS;AACT,WAAO,SAAS;AAChB,WAAO,UAAU;AACjB,WAAO,SAAS;AAChB,WAAO,aAAa;AACpB,WAAO,SAAS,QAAQ,SAAS;AACjC,WAAO,QAAQ;AACf,WAAO,gBAAgB;AACvB,WAAO,SAAS;AAAA,EAClB;AAEA,QAAM,YAAY,CAChB,QACA,MACA,OACA,UACA,QACA,SACA,OACA,QACA,OACA,SACS;AACT,SAAK,QAAQ,UAAU,OAAO,QAAQ,OAAO,IAAI;AACjD,QAAI,aAAa,KAAK,IAAI,QAAQ;AAClC,QAAI,eAAe,QAAW;AAC5B,mBAAa,oBAAI,IAAI;AACrB,WAAK,IAAI,UAAU,UAAU;AAAA,IAC/B;AACA,SAAK,YAAY,QAAQ,OAAO,QAAQ,OAAO,IAAI;AACnD,QAAI,cAAc,MAAM,IAAI,QAAQ;AACpC,QAAI,gBAAgB,QAAW;AAC7B,oBAAc,oBAAI,IAAI;AACtB,YAAM,IAAI,UAAU,WAAW;AAAA,IACjC;AACA,SAAK,aAAa,SAAS,OAAO,QAAQ,OAAO,IAAI;AAAA,EACvD;AAEA,QAAM,cAAc,CAAC,SAAsB,MAAc,OAAe,SAAuB;AAC7F,YAAQ,SAAS;AACjB,YAAQ,QAAQ;AAChB,YAAQ,SAAS;AACjB,QAAI,OAAO,QAAQ,WAAY,SAAQ,aAAa;AAAA,EACtD;AAEA,QAAM,aAAa,QAChB,IAAI,YAAU,OAAO,MAAM,OAAO,aAAa,OAAO,QAAQ,MAAM,OAAO,QAAQ,SAAS,EAC5F,OAAO,CAAC,QAAuB,QAAQ,UAAa,QAAQ,EAAE;AACjE,WAAS,iBAAiB,WAAW;AAErC,aAAW,OAAO,YAAY;AAC5B,QAAI;AACF,YAAM,SAAS,MAAM,kBAAkB,aAAa,GAAG;AACvD,eAAS,mBAAmB;AAC5B,YAAM,UAAuB,EAAE,IAAI,KAAK,OAAO,IAAI,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,YAAY,EAAE;AAC9F,YAAM,iBAAiB,iBAAiB,IAAI,OAAoB,EAAE,IAAI,KAAK,OAAO,IAAI,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,YAAY,EAAE,EAAE;AACnI,YAAM,gBAA+C,EAAE,GAAG,YAAY,GAAG,OAAO,GAAG;AACnF,YAAM,uBAAuB,iBAAiB,IAAI,OAAsC,EAAE,GAAG,YAAY,GAAG,OAAO,GAAG,EAAE;AACxH,YAAM,UAAU,cAAc,QAAQ,CAAC,MAAM,OAAO,QAAQ,OAAO,WAAW,UAAU,OAAO,YAAY,OAAO,WAAW;AAC3H,YAAI,SAAS,eAAe,QAAQ,OAAO,SAAS,WAAY,UAAS,aAAa;AACtF,YAAI,SAAS,aAAa,QAAQ,OAAO,SAAS,SAAU,UAAS,WAAW;AAChF,cAAM,IAAI,IAAI,KAAK,IAAI;AACvB,cAAM,SAAS,GAAG,EAAE,YAAY,CAAC,IAAI,KAAK,EAAE,SAAS,IAAI,CAAC,CAAC,IAAI,KAAK,EAAE,QAAQ,CAAC,CAAC;AAChF,cAAM,UAAU,OAAO,EAAE,SAAS,CAAC;AACnC,cAAM,WAAW,GAAG,QAAQ,IAAI,KAAK;AACrC,cAAM,OAAO,OAAO,MAAM,OAAO,OAAO,OAAO,MAAM;AACrD,cAAM,eAAe,cAAc,MAAM,OAAO,KAAK;AACrD,cAAM,aAAa,QAAQ,SAAS;AACpC,aAAK,QAAQ,QAAQ,OAAO,QAAQ,OAAO,IAAI;AAC/C,aAAK,SAAS,SAAS,OAAO,QAAQ,OAAO,IAAI;AACjD,kBAAU,aAAa,WAAW,YAAY,UAAU,QAAQ,SAAS,OAAO,QAAQ,OAAO,IAAI;AACnG,oBAAY,SAAS,OAAO,QAAQ,OAAO,WAAW,MAAM,YAAY;AACxE,oBAAY,SAAS,MAAM,YAAY,IAAI;AAG3C,YAAI,OAAO,SAAS,GAAG;AACrB,cAAI,iBAAiB;AACrB,cAAI,eAAe;AACnB,cAAI,gBAAgB;AACpB,qBAAW,SAAS,QAAQ;AAC1B,kBAAM,QAAQ,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;AAC9D,kBAAM,SAAS,OAAO,MAAM,WAAW,WAAW,MAAM,SAAS;AACjE,kBAAM,SAAS,oBAAoB,OAAO,MAAM;AAChD,8BAAkB;AAClB,4BAAgB,OAAO,MAAM,OAAO,QAAQ,GAAG,CAAC;AAChD,6BAAiB,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;AAAA,UACnE;AACA,yBAAe,QAAQ,OAAO,QAAQ,gBAAgB,cAAc,aAAa;AACjF,wBAAc,YAAY,QAAQ,OAAO,QAAQ,gBAAgB,cAAc,aAAa;AAC5F,yBAAe,eAAe,OAAO,QAAQ,gBAAgB,cAAc,aAAa;AACxF,mBAAS,IAAI,GAAG,IAAI,iBAAiB,QAAQ,KAAK;AAChD,kBAAM,YAAY,iBAAiB,CAAC;AACpC,gBAAI,SAAS,UAAU,YAAY,SAAS,UAAU,OAAQ;AAC9D,2BAAe,UAAU,QAAQ,OAAO,QAAQ,gBAAgB,cAAc,aAAa;AAC3F,0BAAc,UAAU,YAAY,QAAQ,OAAO,QAAQ,gBAAgB,cAAc,aAAa;AACtG,2BAAe,qBAAqB,CAAC,GAAG,OAAO,QAAQ,gBAAgB,cAAc,aAAa;AAAA,UACpG;AAAA,QACF;AACA,iBAAS,IAAI,GAAG,IAAI,iBAAiB,QAAQ,KAAK;AAChD,gBAAM,YAAY,iBAAiB,CAAC;AACpC,cAAI,SAAS,UAAU,YAAY,SAAS,UAAU,OAAQ;AAC9D,eAAK,UAAU,QAAQ,QAAQ,OAAO,QAAQ,OAAO,IAAI;AACzD,eAAK,UAAU,SAAS,SAAS,OAAO,QAAQ,OAAO,IAAI;AAC3D,oBAAU,UAAU,aAAa,UAAU,WAAW,UAAU,YAAY,UAAU,QAAQ,SAAS,OAAO,QAAQ,OAAO,IAAI;AACjI,sBAAY,UAAU,SAAS,OAAO,QAAQ,OAAO,WAAW,MAAM,YAAY;AAClF,sBAAY,eAAe,CAAC,GAAG,MAAM,YAAY,IAAI;AAAA,QACvD;AACA,cAAM,SAAS,OAAO,IAAI,IAAI,UAAU,OAAO,UAAU;AACzD,eAAO,SAAS;AAChB,eAAO,QAAQ;AACf,eAAO,SAAS;AAChB,kBAAU,eAAe,iBAAiB,MAAM,OAAO,OAAO,OAAO,MAAM;AAC3E,kBAAU,kBAAkB,iBAAiB,MAAM,OAAO,OAAO,OAAO,QAAQ,IAAI;AAAA,MACtF,CAAC;AACD,eAAS,gBAAgB,QAAQ;AACjC,eAAS,kBAAkB,QAAQ;AACnC,UAAI,QAAQ,QAAQ,GAAG;AACrB,cAAM,QAAQ,QAAQ,QAAQ,GAAG;AACjC,gBAAQ,QAAQ;AAChB,iBAAS,KAAK,OAAO;AACrB,iBAAS,IAAI,GAAG,IAAI,iBAAiB,QAAQ,KAAK;AAChD,gBAAM,gBAAgB,eAAe,CAAC;AACtC,cAAI,cAAc,UAAU,EAAG;AAC/B,wBAAc,QAAQ;AACtB,2BAAiB,CAAC,EAAE,SAAS,KAAK,aAAa;AAAA,QACjD;AAAA,MACF;AACA,UAAI,cAAc,SAAS,GAAG;AAC5B,sBAAc,QAAQ,QAAQ,QAAQ,GAAG;AACzC,uBAAe,IAAI,KAAK,aAAa;AACrC,iBAAS,IAAI,GAAG,IAAI,iBAAiB,QAAQ,KAAK;AAChD,gBAAM,eAAe,qBAAqB,CAAC;AAC3C,cAAI,aAAa,WAAW,EAAG;AAC/B,uBAAa,QAAQ,cAAc;AACnC,2BAAiB,CAAC,EAAE,eAAe,IAAI,KAAK,YAAY;AAAA,QAC1D;AAAA,MACF;AAAA,IACF,QAAQ;AAEN,eAAS,kBAAkB;AAAA,IAC7B;AAAA,EACF;AAEA,QAAM,YAAY,CAAC,KAA0B,UAAsC;AACjF,UAAM,SAA6B,CAAC;AACpC,aAAS,IAAI,QAAQ,GAAG,KAAK,GAAG,KAAK;AACnC,YAAM,MAAM,SAAS,WAAW,CAAC,CAAC;AAClC,YAAM,IAAI,IAAI,IAAI,GAAG,KAAK,YAAY;AACtC,aAAO,KAAK,EAAE,MAAM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,QAAQ,OAAO,EAAE,OAAO,OAAO,EAAE,OAAO,MAAM,EAAE,MAAM,OAAO,EAAE,MAAM,CAAC;AAAA,IAC3H;AACA,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,CAAC,QAAkD;AACpE,UAAM,SAA8B,CAAC;AACrC,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,YAAM,IAAI,IAAI,IAAI,OAAO,CAAC,CAAC,KAAK,YAAY;AAC5C,aAAO,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,OAAO,MAAM,EAAE,MAAM,OAAO,EAAE,MAAM,CAAC;AAAA,IACvE;AACA,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,CACjB,QACA,aACA,cACA,aACiB;AACjB,UAAM,SAAuB,CAAC;AAC9B,eAAW,CAAC,UAAU,MAAM,KAAK,QAAQ;AACvC,YAAM,QAAQ,SAAS,QAAQ,GAAG;AAClC,YAAM,WAAW,QAAQ,IAAI,KAAK,SAAS,MAAM,GAAG,KAAK;AACzD,YAAM,QAAQ,QAAQ,IAAI,WAAW,SAAS,MAAM,QAAQ,CAAC;AAC7D,YAAM,OAAO,YAAY,IAAI,QAAQ;AACrC,YAAM,QAAQ,aAAa,IAAI,QAAQ;AACvC,YAAMC,SAA4B,UAAU,QAAQ,oBAAI,IAAI,GAAG,QAAQ,EACpE,IAAI,YAAU,EAAE,OAAO,MAAM,OAAO,MAAM,MAAM,MAAM,OAAO,MAAM,MAAM,EAAE;AAC9E,YAAMC,UAA6B,WAAW,SAAS,oBAAI,IAAI,CAAC,EAC7D,IAAI,YAAU,EAAE,OAAO,MAAM,OAAO,MAAM,MAAM,MAAM,OAAO,MAAM,MAAM,EAAE;AAC9E,aAAO,KAAK;AAAA,QACV;AAAA,QACA;AAAA,QACA,OAAO,OAAO;AAAA,QACd,QAAQ,OAAO;AAAA,QACf,OAAO,OAAO;AAAA,QACd,OAAO,OAAO;AAAA,QACd,MAAM,OAAO;AAAA,QACb,OAAO,OAAO;AAAA,QACd,OAAAD;AAAA,QACA,QAAAC;AAAA,MACF,CAAC;AAAA,IACH;AACA,WAAO,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AACvC,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,UAAU,QAAQ,EAAE;AAClC,QAAM,SAAS,WAAW,OAAO;AAIjC,QAAM,UAAgC,CAAC;AACvC,WAAS,IAAI,KAAK,KAAK,GAAG,KAAK;AAC7B,UAAM,MAAM,SAAS,WAAW,CAAC,CAAC;AAClC,UAAM,IAAI,OAAO,IAAI,GAAG,KAAK,YAAY;AACzC,YAAQ,KAAK,EAAE,MAAM,KAAK,OAAO,EAAE,OAAO,MAAM,EAAE,MAAM,OAAO,EAAE,MAAM,CAAC;AAAA,EAC1E;AAEA,QAAM,kBAAkB,CAAC,KAA6B,UAAoC;AACxF,UAAM,SAA2B,CAAC;AAClC,aAAS,IAAI,QAAQ,GAAG,KAAK,GAAG,KAAK;AACnC,YAAM,MAAM,SAAS,WAAW,CAAC,CAAC;AAClC,YAAM,IAAI,IAAI,IAAI,GAAG,KAAK,YAAY;AACtC,aAAO,KAAK,EAAE,MAAM,KAAK,QAAQ,EAAE,QAAQ,aAAa,EAAE,aAAa,MAAM,EAAE,KAAK,CAAC;AAAA,IACvF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,qBAAqB,CAAC,QAC1B,CAAC,GAAG,IAAI,QAAQ,CAAC,EACd,IAAI,CAAC,CAAC,IAAI,GAAG,OAAO;AAAA,IACnB;AAAA,IACA,OAAO,IAAI,UAAU,KAAK,IAAI,QAAQ,gBAAM,GAAG,MAAM,GAAG,CAAC,CAAC;AAAA,IAC1D,QAAQ,IAAI;AAAA,IACZ,aAAa,IAAI;AAAA,IACjB,MAAM,IAAI;AAAA,EACZ,EAAE,EACD,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,EAClC,MAAM,GAAG,aAAa;AAE3B,QAAM,SAAS,WAAW,aAAa,WAAW,YAAY,EAAE;AAChE,QAAM,UAAU,iBAAiB,IAAI,gBAAc;AAAA,IACjD,MAAM,UAAU;AAAA,IAChB,OAAO,UAAU,UAAU,QAAQ,UAAU,IAAI;AAAA,IACjD,QAAQ,WAAW,UAAU,OAAO;AAAA,IACpC,QAAQ,EAAE,GAAG,UAAU,QAAQ;AAAA,IAC/B,QAAQ,WAAW,UAAU,aAAa,UAAU,WAAW,UAAU,YAAY,UAAU,IAAI;AAAA,IACnG,UAAU,CAAC,GAAG,UAAU,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa;AAAA,IACxF,cAAc,UAAU,SAAS;AAAA,IACjC,QAAQ,EAAE,GAAG,UAAU,OAAO;AAAA,IAC9B,aAAa,gBAAgB,UAAU,YAAY,UAAU,IAAI;AAAA,IACjE,gBAAgB,mBAAmB,UAAU,cAAc;AAAA,EAC7D,EAAE;AAEF,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,UAAU,QAAQ,IAAI,KAAK,KAAK,CAAC;AAAA,MAC1C,SAAS,YAAY,KAAK;AAAA,MAC1B;AAAA,MACA,UAAU,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa;AAAA,MAC9E,cAAc,SAAS;AAAA,MACvB;AAAA,MACA;AAAA,MACA,QAAQ,EAAE,GAAG,OAAO;AAAA,MACpB,aAAa,gBAAgB,YAAY,EAAE;AAAA,MAC3C,gBAAgB,mBAAmB,cAAc;AAAA,MACjD,QAAQ;AAAA,QACN,OAAO,QAAQ;AAAA,QACf,QAAQ,QAAQ;AAAA,QAChB,OAAO,QAAQ;AAAA,QACf,WAAW,QAAQ;AAAA,QACnB,OAAO,QAAQ;AAAA,QACf,MAAM,QAAQ;AAAA,QACd,OAAO,QAAQ;AAAA,QACf,cAAc,QAAQ;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AACF;AASA,eAAsB,kBAAkB,aAAiD,WAAkD;AACzI,MAAI,gBAAgB,OAAW,QAAO,EAAE,IAAI,OAAO,OAAO,+DAAa;AACvE,MAAI,OAAO,cAAc,YAAY,cAAc,GAAI,QAAO,EAAE,IAAI,OAAO,OAAO,8BAAU;AAK5F,MAAI,CAAC,iBAAiB,SAAS,EAAG,QAAO,EAAE,IAAI,OAAO,OAAO,iDAAc;AAE3E,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,kBAAkB,aAAa,SAAS;AAAA,EACzD,SAAS,KAAK;AACZ,WAAO,EAAE,IAAI,OAAO,OAAO,yDAAY,aAAa,GAAG,CAAC,GAAG;AAAA,EAC7D;AAEA,QAAM,cAAc,oBAAI,IAAoB;AAC5C,MAAI,QAAQ;AACZ,MAAI,OAAO;AACX,MAAI,QAAQ;AACZ,MAAI,cAA6B;AACjC,MAAI,aAA4B;AAChC,QAAM,aAAa,oBAAI,IAA+E;AAEtG,gBAAc,QAAQ,CAAC,MAAM,OAAO,QAAQ,OAAO,YAAY,UAAU,OAAO,WAAW,SAAS;AAClG,UAAM,WAAW,GAAG,QAAQ,IAAI,KAAK;AACrC,UAAM,YAAY,OAAO,MAAM,OAAO,OAAO,OAAO,MAAM;AAC1D,UAAM,aAAa,QAAQ,SAAS;AACpC,SAAK,aAAa,UAAU,OAAO,QAAQ,OAAO,SAAS;AAC3D,aAAS;AACT,YAAQ;AACR,aAAS;AACT,QAAI,gBAAgB,QAAQ,OAAO,YAAa,eAAc;AAC9D,QAAI,eAAe,QAAQ,OAAO,WAAY,cAAa;AAC3D,QAAI,cAAc,QAAW;AAI3B,YAAM,MAAM,SAAS,SAAY,WAAW,SAAS,KAAK,QAAQ,IAAI;AACtE,YAAM,YAAY,WAAW,IAAI,GAAG,KAAK,EAAE,WAAW,OAAO,GAAG,MAAM,GAAG,OAAO,EAAE;AAClF,gBAAU,YAAY;AACtB,gBAAU,SAAS;AACnB,gBAAU,QAAQ;AAClB,gBAAU,SAAS;AACnB,iBAAW,IAAI,KAAK,SAAS;AAAA,IAC/B;AAAA,EACF,CAAC;AAED,QAAM,SAA8B,CAAC,GAAG,YAAY,QAAQ,CAAC,EAC1D,IAAI,CAAC,CAAC,UAAU,MAAM,MAAyB;AAC9C,UAAM,QAAQ,SAAS,QAAQ,GAAG;AAClC,UAAM,WAAW,QAAQ,IAAI,KAAK,SAAS,MAAM,GAAG,KAAK;AACzD,UAAM,QAAQ,QAAQ,IAAI,WAAW,SAAS,MAAM,QAAQ,CAAC;AAC7D,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,OAAO,OAAO;AAAA,MACd,QAAQ,OAAO;AAAA,MACf,OAAO,OAAO;AAAA,MACd,OAAO,OAAO;AAAA,MACd,MAAM,OAAO;AAAA,MACb,OAAO,OAAO;AAAA,IAChB;AAAA,EACF,CAAC,EACA,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI;AAEjC,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,MACJ;AAAA,MACA,OAAO,QAAQ,QAAQ,SAAS;AAAA,MAChC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,CAAC,GAAG,WAAW,OAAO,CAAC;AAAA,IAChC;AAAA,EACF;AACF;;;ACpzBO,SAAS,UAAU,KAAqB,QAAgB,MAAe,SAAwC;AACpH,MAAI,UAAU,QAAQ,EAAE,gBAAgB,mCAAmC,GAAG,QAAQ,CAAC;AACvF,MAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;;;ACiBO,IAAM,OAAO;AAGb,IAAM,SAAS,CAAC,aAAa,eAAe,oBAAoB;AAIvE,IAAM,iBAAiB;AACvB,IAAM,eAAe,IAAI;AAMlB,SAAS,MAAM,KAAwB;AAI5C,QAAM,cAAc,QAAQ,gBAAgB,MAAM,aAAa,IAAI,aAAa,IAAI,IAAI,iBAAiB,CAAC,CAAC;AAC3G,QAAM,YAAY,QAAQ,cAAc,MAAM,WAAW,IAAI,kBAAkB,CAAC;AAEhF,MAAI,OAAO,MAAM,IAAI,UAAU,SAAS;AAAA,IACtC,MAAM;AAAA,IACN,MAAM;AAAA,IACN,SAAS,OAAO,KAAK,QAAQ;AAC3B,UAAI,CAAC,oBAAoB,GAAG,GAAG;AAC7B,kBAAU,KAAK,KAAK,EAAE,IAAI,OAAO,OAAO,YAAY,CAAC;AACrD;AAAA,MACF;AACA,UAAI,IAAI,WAAW,OAAO;AACxB,kBAAU,KAAK,KAAK,EAAE,IAAI,OAAO,OAAO,qBAAqB,CAAC;AAC9D;AAAA,MACF;AACA,YAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,qBAAqB;AACzD,YAAM,WAAW,IAAI;AACrB,YAAM,QAAQ,IAAI,aAAa,IAAI,SAAS,MAAM;AAClD,UAAI,aAAa,oCAAoC;AACnD,cAAM,UAAU,OAAO,QAAQ,YAAY,QAAQ,IAAI,YAAY,IAAI;AACvE,cAAM,UAAU,QAAQ,KACpB,EAAE,iBAAiB,oBAAoB,KAAK,MAAM,iBAAiB,GAAI,CAAC,GAAG,IAC3E,EAAE,iBAAiB,WAAW;AAClC,kBAAU,KAAK,KAAK,SAAS,OAAO;AAAA,MACtC,WAAW,aAAa,kCAAkC;AACxD,cAAM,UAAU,OAAO,QAAQ,UAAU,QAAQ,IAAI,UAAU,IAAI;AACnE,cAAM,UAAU,QAAQ,KACpB,EAAE,iBAAiB,oBAAoB,KAAK,MAAM,eAAe,GAAI,CAAC,GAAG,IACzE,EAAE,iBAAiB,WAAW;AAClC,kBAAU,KAAK,KAAK,SAAS,OAAO;AAAA,MACtC,WAAW,aAAa,oCAAoC;AAC1D,cAAM,KAAK,IAAI,aAAa,IAAI,IAAI;AAOpC,YAAI,OAAO,QAAQ,GAAG,KAAK,MAAM,MAAM,CAAC,iBAAiB,EAAE,GAAG;AAM5D,gBAAM,QAAQ,OAAO,QAAQ,GAAG,KAAK,MAAM,KAAK,gCAAY;AAC5D,oBAAU,KAAK,KAAK,EAAE,IAAI,OAAO,MAAM,GAAG,EAAE,iBAAiB,WAAW,CAAC;AACzE;AAAA,QACF;AACA,cAAM,UAAU,MAAM,kBAAkB,IAAI,oBAAoB,EAAE;AAIlE,kBAAU,KAAK,KAAK,SAAS,EAAE,iBAAiB,WAAW,CAAC;AAAA,MAC9D,OAAO;AACL,kBAAU,KAAK,KAAK,EAAE,IAAI,OAAO,OAAO,YAAY,CAAC;AAAA,MACvD;AAAA,IACF;AAAA,EACF,CAAC,GAAG,kCAAkC;AACxC;",
6
- "names": ["name", "events", "daily", "hourly"]
3
+ "sources": ["../src/contract.ts", "../src/pricing.ts", "../src/breaker.ts", "../src/breaker-gate.ts", "../src/breaker-settings.ts", "../src/memo.ts", "../src/trust-fence.ts", "../src/usage.ts", "../src/wire.ts", "../src/index.ts"],
4
+ "sourcesContent": ["/** Wire contracts shared by the host API and the client fetchers. */\n\nexport interface BalanceInfo {\n currency: string\n total: string\n granted: string\n toppedUp: string\n}\n\nexport interface BalanceData {\n isAvailable: boolean\n balances: BalanceInfo[]\n}\n\nexport interface BalanceResponse {\n ok: boolean\n error?: string\n data?: BalanceData\n}\n\nexport interface DailyUsage {\n date: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\nexport interface HourlyUsage {\n hour: number\n total: number\n cost: number\n calls: number\n}\n\nexport interface HeatmapUsage {\n date: string\n total: number\n cost: number\n calls: number\n}\n\nexport interface UsageTotals {\n input: number\n output: number\n cache: number\n reasoning: number\n total: number\n cost: number\n calls: number\n /**\n * What the cache-hit tokens would have cost at cache-miss rates, minus what\n * they actually cost \u2014 i.e. the money prefix caching saved. Folded per\n * record so it respects each record's model and time-of-day rates.\n */\n cacheSavings: number\n}\n\n/** One per-day / per-hour point of a single model's series. */\nexport interface ModelSeriesPoint {\n total: number\n cost: number\n calls: number\n}\n\n/** Per-model usage aggregation: overall totals plus 30-day and 24-hour series. */\nexport interface ModelUsage {\n provider: string\n model: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n daily: ModelSeriesPoint[]\n hourly: ModelSeriesPoint[]\n}\n\n/** Usage rolled up over one time window. */\nexport interface PeriodUsage {\n total: number\n cost: number\n calls: number\n}\n\n/**\n * Time-anchored rollups. \"Accumulated since forever\" answers no question a\n * user actually has; these do: what did today cost, what has this month cost,\n * and is that more or less than the comparable window before it.\n */\nexport interface UsageSummary {\n today: PeriodUsage\n yesterday: PeriodUsage\n /** Current calendar month, to date. */\n month: PeriodUsage\n /** Previous calendar month up to the same day-of-month, so a mid-month\n * comparison is against a same-length window rather than a full month. */\n lastMonthToDate: PeriodUsage\n}\n\n/** CNY per 1M tokens for one model tier. */\nexport interface PricingRates {\n cacheHit: number\n input: number\n output: number\n}\n\nexport interface PricingTier {\n model: string\n peak: PricingRates\n offPeak: PricingRates\n}\n\n/** What the cost column was computed with, so the estimate can be audited. */\nexport interface PricingInfo {\n currency: string\n /** Date peak/off-peak pricing took effect; earlier usage is costed at the\n * flat rates that were actually charged then. */\n switchDate: string\n inPeakNow: boolean\n peakWindows: string[]\n tiers: PricingTier[]\n}\n\n/**\n * How usage divides between DeepSeek's peak and off-peak windows, plus the\n * same usage re-priced as if everything had landed off-peak \u2014 answering\n * \"what would shifting batch work off-peak save me\" under the peak/off-peak\n * table in effect since 2026-08-17.\n */\nexport interface PeakSplit {\n peak: PeriodUsage\n offPeak: PeriodUsage\n /** Every record priced under the peak/off-peak table, as actually classified. */\n peakEraCost: number\n /** Every record priced under that table as if it had landed off-peak. */\n offPeakEraCost: number\n}\n\n/** One session's share of the bill. */\nexport interface SessionCost {\n id: string\n /** The session's title from its log; falls back to a short id. */\n title: string\n total: number\n cost: number\n calls: number\n /** Epoch ms of the last usage record in this session. */\n lastActive: number\n}\n\n/**\n * Multimodal (image) usage, folded from the same log replay: image blocks in\n * user messages and tool results are counted and sized, then estimated with\n * the official resizing rule (see `estimateImageTokens`). Each image is\n * attributed to the next usage record \u2014 the model call whose prompt carried\n * it \u2014 and priced at that call's input (cache-miss) rate, so the share answer\n * \"how much of the bill came from pictures\" has a real number.\n */\nexport interface VisionStats {\n /** Image blocks counted in the scope. */\n images: number\n /** Estimated image tokens under the official conversion rule. */\n imageTokens: number\n /** Estimated CNY cost of those image tokens at the consuming calls' rates. */\n cost: number\n /** Total image payload bytes when the attachment refs carried a size. */\n bytes: number\n}\n\n/** One day's image usage for the per-window / per-day series. */\nexport interface VisionDayPoint {\n date: string\n images: number\n imageTokens: number\n cost: number\n}\n\n/** One session's image usage, ranked for \"which run kept sending pictures\". */\nexport interface VisionSession {\n id: string\n title: string\n images: number\n imageTokens: number\n cost: number\n}\n\n/**\n * What the local log replay could actually inspect. Totals can otherwise look\n * authoritative even when one corrupt session was skipped, so this metadata\n * travels with every usage response and lets the UI disclose partial results.\n */\nexport interface UsageCoverage {\n scope: 'local-dsh-session-logs'\n /** Session headers with a usable id returned by the local persistence. */\n listedSessions: number\n /** Session logs read successfully, including logs with no usage. */\n scannedSessions: number\n /** Valid assistant usage records folded into the totals. */\n usageRecords: number\n /** Usage records rejected because their time or token data was invalid. */\n skippedRecords: number\n /** Session logs that could not be read at all. */\n failedSessions: number\n /** Epoch ms of the first/last valid usage record across all local logs. */\n earliestAt: number | null\n latestAt: number | null\n}\n\nexport const USAGE_WINDOW_DAYS = [7, 30, 90, 365] as const\nexport type UsageWindowDays = (typeof USAGE_WINDOW_DAYS)[number]\n\n/**\n * Session ids observed in this codebase (host-issued, e.g.\n * `session-484a1c14-c6fe-4d6a-abfd-a2d8d2f664d5`) are always plain\n * ASCII-safe tokens \u2014 no documented format contract exists for them outside\n * this repo (`SessionPersistenceFace` is implemented by the DSH host, not\n * here), so this is a defensive charset whitelist rather than an\n * exact-format check: letters, digits, `-`, `_` only, with a generous length\n * cap. It exists specifically to stop path-separator / `..` / null-byte\n * payloads from ever reaching the session-log read \u2014 `id` on the\n * `/session` route arrives verbatim from the browser-controlled `?id=` query\n * parameter, unlike `fetchUsage()`'s session ids, which the host itself\n * enumerates via `persistence.list()`.\n */\nexport const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\nexport const isValidSessionId = (id: string): boolean => SESSION_ID_PATTERN.test(id)\n\n/** One internally consistent view of the same local logs over a calendar window. */\nexport interface UsageWindow {\n days: UsageWindowDays\n daily: DailyUsage[]\n hourly: HourlyUsage[]\n totals: UsageTotals\n models: ModelUsage[]\n /** Most expensive sessions inside this window, capped at SESSION_TOP_N. */\n sessions: SessionCost[]\n /** All sessions that contributed usage inside this window. */\n sessionCount: number\n vision: VisionStats\n visionDaily: VisionDayPoint[]\n /** Sessions that sent the most images inside this window, capped. */\n visionSessions: VisionSession[]\n}\n\nexport interface UsageData {\n daily: DailyUsage[]\n hourly: HourlyUsage[]\n heatmap: HeatmapUsage[]\n totals: UsageTotals\n models: ModelUsage[]\n summary: UsageSummary\n pricing: PricingInfo\n peakSplit: PeakSplit\n /** Most expensive sessions first, capped \u2014 see SESSION_TOP_N. */\n sessions: SessionCost[]\n /** How many sessions contributed usage in total. */\n sessionCount: number\n coverage: UsageCoverage\n windows: UsageWindow[]\n vision: VisionStats\n visionDaily: VisionDayPoint[]\n /** Sessions that sent the most images, capped \u2014 see SESSION_TOP_N. */\n visionSessions: VisionSession[]\n}\n\nexport interface UsageResponse {\n ok: boolean\n error?: string\n data?: UsageData\n}\n\n/**\n * One model's contribution to a single session's bill \u2014 the same field\n * shape as `ModelUsage` minus the `daily`/`hourly` series, which only make\n * sense aggregated across many sessions.\n */\nexport interface SessionModelUsage {\n provider: string\n model: string\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\n/** One completed conversation turn's usage. `messageId` is the durable id of\n * the turn's final assistant message \u2014 the same identity the chat action-strip\n * slot exposes to client plugins. Tool-call steps inside the turn are folded\n * into this one figure rather than shown as misleading separate charges. */\nexport interface TurnUsage {\n messageId: string\n total: number\n cost: number\n calls: number\n}\n\n/**\n * Usage for exactly one session \u2014 what the \u300C\u989D\u5EA6\u300Dtab's own conversation\n * is costing, as opposed to `UsageData`'s account-wide totals. Fetched on\n * demand (see `fetchSessionUsage`), not part of the 5-minute `usage` memo:\n * a single session read is cheap and must reflect the session's live state.\n */\nexport interface SessionUsageData {\n sessionId: string\n /** The session's own title from its log; falls back to a short id. */\n title: string\n total: number\n cost: number\n calls: number\n /** Epoch ms of the first/last valid usage record in this session, or null\n * when the session has produced no usage yet. */\n firstActive: number | null\n lastActive: number | null\n /** Most expensive model first. */\n models: SessionModelUsage[]\n /** Completed turns, in log order, addressed by their final message id. */\n turns: TurnUsage[]\n}\n\nexport interface SessionUsageResponse {\n ok: boolean\n error?: string\n data?: SessionUsageData\n}\n\n/**\n * Per-session peak pause.\n *\n * A conversation can opt in to pausing its own user-started work while\n * DeepSeek's peak pricing is active (Beijing time, Monday\u2013Friday 09:00\u201312:00\n * and 14:00\u201318:00). The opt-in is stored per session and the switch lives in\n * that conversation's composer dock; the \u989D\u5EA6 page lists the sessions that\n * opted in and carries the master switch. Parked steps resume at 18:00.\n *\n * Deliberately not offered to scheduled automation: a reminder-triggered turn\n * runs normally even inside an opted-in session, and sessions that never opted\n * in are never parked.\n */\nexport interface BreakerSettings {\n /** Master switch; off means no session pauses, whatever its own switch says. */\n enabled: boolean\n /** Session ids whose user-started turns pause during peak pricing. */\n sessions: string[]\n}\n\n/** State used when nothing has been persisted yet: feature on, no session opted in. */\nexport const BREAKER_DEFAULT_SETTINGS: BreakerSettings = { enabled: true, sessions: [] }\n\n/**\n * One settings write. A body with `sessionId` flips that conversation's switch;\n * a body without one flips the master switch.\n */\nexport interface BreakerSettingsRequest {\n enabled: boolean\n sessionId?: string\n}\n\n/** Optional target for a manual release; omitted means every parked session. */\nexport interface BreakerReleaseRequest {\n sessionId?: string\n}\n\n/** Live peak-pause projection served by the host half. */\nexport interface BreakerData {\n /** Master switch; when off, nothing pauses regardless of per-session state. */\n enabled: boolean\n /** Sessions whose user-started turns pause during peak pricing. */\n sessions: string[]\n /** Sessions with at least one parked step right now. */\n holding: string[]\n /** Host clock at projection time, ISO 8601. */\n now: string\n /** Whether `now` is inside a Beijing-time peak window. */\n peak: boolean\n /** When parked work resumes (ISO 8601), or null outside peak. */\n resumeAt: string | null\n /** Human-readable peak windows in Beijing time, for display only. */\n peakWindows: string[]\n}\n\nexport interface BreakerResponse {\n ok: boolean\n error?: string\n data?: BreakerData\n}\n\n/** Same-origin routes the client fetches: `state` answers GET (read) and PUT\n * (either switch), `release` answers POST (let parked work go now). */\nexport const BREAKER_API = {\n state: '/api/dsh-usage-dashboard/breaker',\n release: '/api/dsh-usage-dashboard/breaker/release',\n} as const\n\n/** Structural validation for the persisted settings document. */\nexport const isBreakerSettings = (value: unknown): value is BreakerSettings => {\n if (typeof value !== 'object' || value === null) return false\n const candidate = value as { enabled?: unknown; sessions?: unknown }\n return typeof candidate.enabled === 'boolean'\n && Array.isArray(candidate.sessions)\n && candidate.sessions.every(session => typeof session === 'string' && isValidSessionId(session))\n}\n\n/** Structural validation for one settings write. */\nexport const isBreakerSettingsRequest = (value: unknown): value is BreakerSettingsRequest => {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n const candidate = value as { enabled?: unknown; sessionId?: unknown }\n if (typeof candidate.enabled !== 'boolean') return false\n if (candidate.sessionId === undefined) return true\n return typeof candidate.sessionId === 'string' && isValidSessionId(candidate.sessionId)\n}\n", "/**\n * DeepSeek API price table and cost estimation.\n *\n * Every record is priced with the table that was in force at the moment it was\n * written \u2014 CNY per 1M tokens, from DeepSeek's official pricing page:\n *\n * - until 2026-08-17 00:00 Beijing: one flat price per model;\n * - 2026-08-17 00:00: peak/off-peak pricing \u2014 peak windows 09:00\u201312:00 and\n * 14:00\u201318:00 every day, off-peak exactly half of peak;\n * - 2026-09-10 12:00: the V4.1-Flash price cut (flash 0.04/2/8 peak), and the\n * peak windows narrow to Monday\u2013Friday;\n * - 2026-09-14 12:00: `deepseek-v4-pro` requests are served by V4.1-Flash and\n * billed at Flash rates until a V4.1 Pro ships.\n *\n * `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` are legacy names for\n * the model now published as `deepseek-flash`; both keep billing at Flash\n * rates. Images cost tokens rather than a multiplier of their own: they are\n * converted by size and priced together with the text tokens. Historical days\n * therefore keep the price that was actually charged.\n *\n * Editing this table is the one place to touch when DeepSeek changes prices.\n */\nimport type { PricingInfo, PricingRates } from './contract.ts'\n\n/** 2026-08-17 00:00 Beijing time (UTC+8), when peak/off-peak pricing took effect. */\nexport const PEAK_PRICING_FROM_MS = Date.UTC(2026, 7, 16, 16, 0, 0)\n\n/** 2026-09-10 12:00 Beijing, when the V4.1-Flash prices and the weekday-only\n * peak windows took effect. */\nexport const V41_FLASH_PRICING_FROM_MS = Date.UTC(2026, 8, 10, 4, 0, 0)\n\n/** 2026-09-14 12:00 Beijing, from when `deepseek-v4-pro` requests are routed\n * to V4.1-Flash and billed at Flash rates. */\nexport const PRO_ROUTED_TO_FLASH_FROM_MS = Date.UTC(2026, 8, 14, 4, 0, 0)\n\n/** Peak hours in Beijing time, as [startHour, endHour) pairs. */\nconst PEAK_WINDOWS: Array<[number, number]> = [[9, 12], [14, 18]]\n\nconst BEIJING_OFFSET_MS = 8 * 3_600_000\n\ntype Tier = 'pro' | 'flash' | 'vision'\n\n/** Flat rates in effect until 2026-08-17. */\nconst FLAT_RATES: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.025, input: 3, output: 6 },\n flash: { cacheHit: 0.02, input: 1, output: 2 },\n // The vision model billed at flash rates from day one, so its pre-switch\n // history is costed at the same flat prices as flash.\n vision: { cacheHit: 0.02, input: 1, output: 2 },\n}\n\n/** Peak rates from 2026-08-17; off-peak is exactly half of each. */\nconst PEAK_RATES_08_17: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.3, input: 9, output: 27 },\n flash: { cacheHit: 0.1, input: 3, output: 9 },\n // Official policy: the vision model is priced identically to flash; images\n // cost tokens, not their own multiplier.\n vision: { cacheHit: 0.1, input: 3, output: 9 },\n}\n\n/** Peak rates from 2026-09-10, the V4.1-Flash table; off-peak is half. */\nconst PEAK_RATES_09_10: Record<Tier, PricingRates> = {\n pro: { cacheHit: 0.3, input: 9, output: 27 },\n flash: { cacheHit: 0.04, input: 2, output: 8 },\n vision: { cacheHit: 0.04, input: 2, output: 8 },\n}\n\n/** One dated price table; `fromMs` is inclusive. */\ninterface PriceEra {\n readonly fromMs: number\n readonly peak: Record<Tier, PricingRates>\n /** Whether the peak windows apply Monday\u2013Friday only. */\n readonly weekdaysOnly: boolean\n}\n\n/** Oldest first \u2014 the last entry whose `fromMs` has passed is the one in force. */\nconst PRICE_ERAS: readonly PriceEra[] = [\n { fromMs: PEAK_PRICING_FROM_MS, peak: PEAK_RATES_08_17, weekdaysOnly: false },\n { fromMs: V41_FLASH_PRICING_FROM_MS, peak: PEAK_RATES_09_10, weekdaysOnly: true },\n]\n\n/** The price table in force at a moment. */\nfunction eraAt(timeMs: number): PriceEra {\n let era = PRICE_ERAS[0]\n for (const candidate of PRICE_ERAS) if (timeMs >= candidate.fromMs) era = candidate\n return era\n}\n\nconst halved = (rates: PricingRates): PricingRates => ({\n cacheHit: rates.cacheHit / 2,\n input: rates.input / 2,\n output: rates.output / 2,\n})\n\n/** Which price tier a model name falls into; anything unrecognised is billed\n * as pro, the conservative (more expensive) guess. Vision is matched first:\n * `deepseek-v4-flash-vision-exp` also contains \"flash\". */\nexport function tierOf(model: string): Tier {\n const name = model.toLowerCase()\n if (name.includes('vision')) return 'vision'\n if (name.includes('flash')) return 'flash'\n return 'pro'\n}\n\n/** The tier a model actually bills at: from 2026-09-14 `deepseek-v4-pro`\n * requests are served by V4.1-Flash and charged the Flash price. */\nfunction billedTierOf(timeMs: number, model: string): Tier {\n const tier = tierOf(model)\n return tier === 'pro' && timeMs >= PRO_ROUTED_TO_FLASH_FROM_MS ? 'flash' : tier\n}\n\n/** Beijing-time Monday\u2013Friday, which the peak windows honour from 2026-09-10. */\nfunction isBeijingWeekday(timeMs: number): boolean {\n const day = new Date(timeMs + BEIJING_OFFSET_MS).getUTCDay()\n return day >= 1 && day <= 5\n}\n\n/**\n * Image token conversion (DeepSeek official rule, Vision guide):\n * - Images below roughly 384\u00D7384 pixels are scaled UP preserving aspect\n * ratio; larger images are scaled DOWN so their pixel count is roughly that\n * of an 800\u00D7800 image.\n * - Tokens are proportional to the (resized) pixel count, capped at 384\n * tokens per image, so 2000\u00D72000 and 5000\u00D75000 cost the same.\n * - Each image is counted independently; `detail: low` caps at 512\u00D7512.\n *\n * The estimate is for the \"how much came from images\" breakdown; the exact\n * provider count is already folded into the usage record's `inputTokens`.\n * Returns tokens (possibly fractional) \u2014 0 for missing/invalid dimensions.\n */\nexport function estimateImageTokens(width: number, height: number): number {\n if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return 0\n const area = width * height\n const scaledArea = Math.min(800 * 800, Math.max(384 * 384, area))\n return 384 * (scaledArea / (800 * 800))\n}\n\n/** Whether a moment falls in a peak window, judged in Beijing time so the\n * estimate does not drift with the machine's timezone. */\nexport function isPeak(timeMs: number): boolean {\n if (eraAt(timeMs).weekdaysOnly && !isBeijingWeekday(timeMs)) return false\n const hour = Math.floor((((timeMs + BEIJING_OFFSET_MS) % 86_400_000) + 86_400_000) % 86_400_000 / 3_600_000)\n return PEAK_WINDOWS.some(([from, to]) => hour >= from && hour < to)\n}\n\n/** Rates applying to one model at one moment. */\nexport function ratesAt(timeMs: number, model: string): PricingRates {\n if (timeMs < PEAK_PRICING_FROM_MS) return FLAT_RATES[tierOf(model)]\n const peak = eraAt(timeMs).peak[billedTierOf(timeMs, model)]\n return isPeak(timeMs) ? peak : halved(peak)\n}\n\n/**\n * What prefix caching saved on one record: the cache-hit tokens re-priced at\n * the cache-miss rate, minus what they actually cost.\n */\nexport function cacheSavingOf(timeMs: number, model: string, cache: number): number {\n const rates = ratesAt(timeMs, model)\n return (cache * (rates.input - rates.cacheHit)) / 1_000_000\n}\n\nconst applyRates = (rates: PricingRates, input: number, cache: number, output: number): number =>\n (input * rates.input + cache * rates.cacheHit + output * rates.output) / 1_000_000\n\n/** Estimated CNY cost of one usage record. */\nexport function costOf(timeMs: number, model: string, input: number, cache: number, output: number): number {\n return applyRates(ratesAt(timeMs, model), input, cache, output)\n}\n\n/**\n * The same record priced as if it had landed in an idle window \u2014 used to\n * answer \"what would shifting work off-peak save me\". Prices come from the\n * newest table, the one in force now: the question is about scheduling the\n * next batch, not about re-pricing history.\n */\nexport function costUnderPeakEra(timeMs: number, model: string, input: number, cache: number, output: number, forceOffPeak = false): number {\n const peak = PRICE_ERAS[PRICE_ERAS.length - 1].peak[billedTierOf(timeMs, model)]\n const rates = !forceOffPeak && isPeak(timeMs) ? peak : halved(peak)\n return applyRates(rates, input, cache, output)\n}\n\n/** The price table to show the user: the two models the official docs list,\n * so the rows match the page they come from \u2014 legacy names, image tokens and\n * the pro routing live in the small print next to the table. */\nexport function pricingInfo(nowMs: number): PricingInfo {\n const era = eraAt(nowMs)\n const row = (model: string, tier: Tier) => ({ model, peak: era.peak[tier], offPeak: halved(era.peak[tier]) })\n return {\n currency: 'CNY',\n switchDate: '2026-08-17',\n inPeakNow: isPeak(nowMs),\n peakWindows: ['09:00\u201312:00', '14:00\u201318:00'],\n tiers: [row('deepseek-flash', 'flash'), row('deepseek-v4-pro', 'pro')],\n }\n}\n", "/**\n * Peak-window breaker decisions \u2014 pure functions, no timers or IO.\n *\n * DeepSeek charges peak rates in Beijing time 09:00\u201312:00 and 14:00\u201318:00 on\n * weekdays (see `pricing.ts`, which owns that definition). A session that opted\n * in parks its user-started steps while a peak window is open and lets them\n * continue at 18:00 \u2014 the end of the day's last peak window \u2014 so one pause\n * covers both windows without a lunch-time exception.\n *\n * All time math is done in Beijing time (UTC+8) rather than the machine's local\n * zone, matching the price table.\n */\nimport { isValidSessionId, type BreakerData, type BreakerSettings } from './contract.ts'\nimport { isPeak } from './pricing.ts'\n\nconst BEIJING_OFFSET_MS = 8 * 3_600_000\nconst HOUR_MS = 3_600_000\nconst DAY_MS = 86_400_000\n\n/** Peak windows in Beijing time, display-only copy shared with `pricingInfo`. */\nexport const BREAKER_PEAK_WINDOWS = ['09:00\u201312:00', '14:00\u201318:00'] as const\n\n/**\n * The one field of a message this module inspects: its producer. Only\n * `kind === 'user'` means a person started the turn in a chat; scheduled\n * tasks, goal continuations and subagent relays all carry another kind and are\n * therefore never parked.\n */\nexport interface BreakerMessageFace {\n source?: { kind?: string }\n}\n\n/**\n * When the current peak window settles: 18:00 Beijing time on the same day.\n * Outside peak there is nothing to wait for, so the answer is `now`.\n *\n * A peak window can only be open between 09:00 and 18:00 on a weekday, so the\n * same-day 18:00 instant is always in the future here. Converting through\n * UTC+8 schedules the same instant on a host in another timezone.\n */\nexport function nextResumeAt(nowMs: number): number {\n if (!isPeak(nowMs)) return nowMs\n const beijingMs = nowMs + BEIJING_OFFSET_MS\n const dayStartMs = Math.floor(beijingMs / DAY_MS) * DAY_MS\n return dayStartMs + 18 * HOUR_MS - BEIJING_OFFSET_MS\n}\n\n/**\n * Whether these proposed-step messages belong to a turn a person started. A\n * single user-produced message is enough: a step that carries it is part of\n * the user's request, whatever else the batch holds.\n */\nexport function isUserTurn(messages: readonly BreakerMessageFace[]): boolean {\n return messages.some(message => message.source?.kind === 'user')\n}\n\n/**\n * A copy holding exactly the contract fields, with session ids deduplicated,\n * filtered to the charset sessions can actually have, and sorted so persisted\n * documents and API projections are stable.\n */\nexport function normalizeBreakerSettings(settings: BreakerSettings): BreakerSettings {\n const sessions = [...new Set(settings.sessions.filter(isValidSessionId))].sort()\n return { enabled: settings.enabled, sessions }\n}\n\n/** Project live gate state into the wire shape the client renders. */\nexport function breakerProject(\n settings: BreakerSettings,\n nowMs: number,\n holding: readonly string[],\n): BreakerData {\n const peak = isPeak(nowMs)\n const normalized = normalizeBreakerSettings(settings)\n const parkedSessions = [...new Set(holding.filter(isValidSessionId))].sort()\n return {\n enabled: normalized.enabled,\n sessions: normalized.sessions,\n holding: parkedSessions,\n now: new Date(nowMs).toISOString(),\n peak,\n resumeAt: peak ? new Date(nextResumeAt(nowMs)).toISOString() : null,\n peakWindows: [...BREAKER_PEAK_WINDOWS],\n }\n}\n", "/**\n * The peak-window breaker gate.\n *\n * DeepSeek's `agent/pre-step` is a waterfall: a listener receives the proposed\n * step and either calls `next()` (let it proceed) or withholds it (park). This\n * gate parks steps only when all conditions hold:\n *\n * - the master switch is on,\n * - the step's agent (its session) opted in via its own switch,\n * - a peak window is open,\n * - the turn was started by a person (`source.kind === 'user'`).\n *\n * Parked steps resume when the window settles at 18:00, when the user lets them\n * continue from the UI, or when the owning turn is cancelled (`signal` aborts \u2014\n * abort must let the loop unwind normally instead of throwing here).\n *\n * Steps after the first one in a turn can carry no messages, so the \"was this a\n * user turn\" decision is remembered per `(session, turn)`. The same memory\n * records a manual release so the user's \"continue now\" is not undone by the\n * very next step of that turn. Both are bounded: sessions come and go, but the\n * map must not grow forever on a long-lived host.\n *\n * Timer handling is split behind `BreakerTimerFace` so tests can drive a fake\n * clock; the production face uses a real `setTimeout`, unrefs it (a parked turn\n * must never keep the process alive) and re-arms long waits in chunks, because\n * `setTimeout` cannot represent delays above `MAX_BREAKER_TIMER_DELAY_MS`.\n */\nimport { BREAKER_DEFAULT_SETTINGS, type BreakerSettings } from './contract.ts'\nimport { isPeak } from './pricing.ts'\nimport {\n isUserTurn,\n nextResumeAt,\n normalizeBreakerSettings,\n type BreakerMessageFace,\n} from './breaker.ts'\n\n/** The timer seam; production uses `systemBreakerTimers`, tests a fake clock. */\nexport interface BreakerTimerFace {\n schedule(callback: () => void, delayMs: number): unknown\n cancel(handle: unknown): void\n}\n\n/** `setTimeout`'s documented maximum delay; longer waits are re-armed. */\nexport const MAX_BREAKER_TIMER_DELAY_MS = 2_147_483_647\n\n/** Production timers: a real unref'd `setTimeout`. */\nexport const systemBreakerTimers: BreakerTimerFace = {\n schedule: (callback, delayMs) => {\n const handle = setTimeout(callback, delayMs)\n ;(handle as { unref?: () => void }).unref?.()\n return handle\n },\n cancel: (handle) => {\n clearTimeout(handle as ReturnType<typeof setTimeout>)\n },\n}\n\n/** Clamp one wait into the range a single `setTimeout` can carry. */\nexport const clampBreakerTimerDelay = (delayMs: number): number => {\n if (!Number.isFinite(delayMs)) return 0\n return Math.max(0, Math.min(Math.trunc(delayMs), MAX_BREAKER_TIMER_DELAY_MS))\n}\n\n/**\n * Bound on the per-turn memory. Turns are short-lived, but a host can see many\n * sessions over its lifetime; a bounded FIFO keeps that memory flat.\n */\nconst MAX_TRACKED_TURNS = 512\n\n/** One parked step, kept only while it waits. */\ninterface HeldRecord {\n sessionId: string\n turn: number\n resolve: () => void\n detach: () => void\n}\n\n/** What is remembered about one turn for as long as it can still produce steps. */\ninterface TurnState {\n /** The turn was started by a person. */\n user: boolean\n /** The user manually let this turn continue, so later steps skip the gate. */\n released: boolean\n}\n\nexport interface BreakerGateOptions {\n /** Settings to start from; the host loads persisted ones asynchronously. */\n initialSettings?: BreakerSettings\n /** Clock seam, epoch ms; defaults to `Date.now`. */\n now?: () => number\n /** Timer seam; defaults to the real clock. */\n timers?: BreakerTimerFace\n}\n\nexport class BreakerGate {\n #settings: BreakerSettings\n #sessions: Set<string>\n readonly #now: () => number\n readonly #timers: BreakerTimerFace\n readonly #held = new Set<HeldRecord>()\n readonly #turns = new Map<string, TurnState>()\n #timer: { handle: unknown } | undefined\n #disposed = false\n\n constructor(options: BreakerGateOptions = {}) {\n this.#settings = normalizeBreakerSettings(options.initialSettings ?? { ...BREAKER_DEFAULT_SETTINGS })\n this.#sessions = new Set(this.#settings.sessions)\n this.#now = options.now ?? Date.now\n this.#timers = options.timers ?? systemBreakerTimers\n }\n\n get settings(): BreakerSettings {\n return normalizeBreakerSettings(this.#settings)\n }\n\n /** Steps parked right now, across every session. */\n get held(): number {\n return this.#held.size\n }\n\n /** Session ids with at least one parked step, sorted for the wire projection. */\n get heldSessions(): string[] {\n return [...new Set([...this.#held].map(record => record.sessionId))].sort()\n }\n\n /**\n * Park one proposed step until it may proceed. Resolves immediately when the\n * step is not subject to the breaker (session not opted in, off-peak,\n * machine-started turn, turn already released by hand) or when the turn is\n * aborted/disposed.\n *\n * The listener wrapping this must call `next()` after it resolves \u2014 this\n * method never throws and never returns a decision of its own.\n *\n * `sessionId` is the agent id from the payload; an agent without one cannot\n * be matched against the opt-in list, so its steps pass.\n */\n async hold(\n sessionId: string | undefined,\n turn: number,\n messages: readonly BreakerMessageFace[],\n signal: AbortSignal,\n ): Promise<void> {\n if (this.#disposed || signal.aborted || sessionId === undefined || sessionId === '') return\n\n // Classify the turn on any step that carries messages; a later step can\n // report no messages at all and must inherit the classification. A person\n // writing into a turn at any point makes it user work.\n const key = `${sessionId}:${turn}`\n let state = this.#turns.get(key)\n const user = isUserTurn(messages) || state?.user === true\n if (state === undefined) {\n state = { user, released: false }\n this.rememberTurn(key, state)\n } else {\n state.user = user\n }\n\n if (state.released || !this.#settings.enabled || !this.#sessions.has(sessionId)\n || !isPeak(this.#now()) || !state.user) return\n\n await new Promise<void>((resolve) => {\n const onAbort = (): void => {\n this.releaseRecord(record, false)\n }\n const record: HeldRecord = {\n sessionId,\n turn,\n resolve,\n detach: () => {\n signal.removeEventListener('abort', onAbort)\n },\n }\n signal.addEventListener('abort', onAbort)\n this.#held.add(record)\n this.armTimer()\n })\n }\n\n /**\n * Let parked steps continue now. `sessionId` narrows it to one session; no\n * argument releases every parked step (the UI's \"continue all\" button). The\n * released turns are remembered, because re-parking their next step would\n * contradict the user's instruction.\n * @returns how many parked steps were released.\n */\n release(sessionId?: string): number {\n const released = [...this.#held]\n .filter(record => sessionId === undefined || record.sessionId === sessionId)\n for (const record of released) this.releaseRecord(record, true)\n return released.length\n }\n\n /**\n * Apply new settings. Turning off the master switch \u2014 or a session's own\n * switch \u2014 releases the affected parked steps immediately and without\n * remembering a bypass: that is the user's decision, not a request for one\n * particular turn to run.\n */\n update(settings: BreakerSettings): void {\n this.#settings = normalizeBreakerSettings(settings)\n this.#sessions = new Set(this.#settings.sessions)\n if (this.#disposed) return\n const nowMs = this.#now()\n for (const record of [...this.#held]) {\n if (!this.#settings.enabled || !this.#sessions.has(record.sessionId) || !isPeak(nowMs)) {\n this.releaseRecord(record, false)\n }\n }\n if (this.#held.size > 0) this.armTimer()\n else this.clearTimer()\n }\n\n /** Release everything and stop the timer; called when the plugin unloads. */\n dispose(): void {\n this.#disposed = true\n this.clearTimer()\n for (const record of [...this.#held]) this.releaseRecord(record, false)\n this.#turns.clear()\n }\n\n /** (Re)arm the single settle timer for the current peak window. */\n private armTimer(): void {\n this.clearTimer()\n if (this.#disposed || this.#held.size === 0) return\n const nowMs = this.#now()\n const delayMs = nextResumeAt(nowMs) - nowMs\n if (!(delayMs > 0)) return\n const timer = { handle: undefined as unknown }\n timer.handle = this.#timers.schedule(() => {\n // A chunked/late timer re-arms from the fresh clock instead of assuming\n // the whole window elapsed.\n if (this.#timer === timer) this.#timer = undefined\n this.onTimer()\n }, clampBreakerTimerDelay(delayMs))\n this.#timer = timer\n }\n\n private onTimer(): void {\n if (this.#disposed || this.#held.size === 0) return\n if (isPeak(this.#now())) {\n this.armTimer()\n return\n }\n // The window settled on its own; these turns are not manual releases.\n for (const record of [...this.#held]) this.releaseRecord(record, false)\n }\n\n private clearTimer(): void {\n const timer = this.#timer\n if (timer === undefined) return\n this.#timer = undefined\n this.#timers.cancel(timer.handle)\n }\n\n private releaseRecord(record: HeldRecord, remember: boolean): void {\n if (!this.#held.delete(record)) return\n record.detach()\n if (remember) this.markReleased(record.sessionId, record.turn)\n record.resolve()\n if (this.#held.size === 0) this.clearTimer()\n }\n\n private markReleased(sessionId: string, turn: number): void {\n const key = `${sessionId}:${turn}`\n const state = this.#turns.get(key)\n if (state !== undefined) {\n state.released = true\n return\n }\n // The turn was never classified (it cannot be held without that), so a\n // fresh entry only needs the bypass flag.\n this.rememberTurn(key, { user: false, released: true })\n }\n\n private rememberTurn(key: string, state: TurnState): void {\n if (this.#turns.size >= MAX_TRACKED_TURNS) {\n const oldest = this.#turns.keys().next()\n if (!oldest.done) this.#turns.delete(oldest.value)\n }\n this.#turns.set(key, state)\n }\n}\n", "/**\n * Durable breaker settings.\n *\n * The file lives at `$DSH_HOME/storages/dsh-usage-dashboard/breaker.json`\n * (`DSH_HOME` defaults to `~/.dsh`, the same root DSH uses for its own\n * storages) and records the master switch plus the sessions that opted in:\n * `{ \"enabled\": true, \"sessions\": [\"session-\u2026\"] }`. Writes go through a temp\n * file + rename so a crash mid-write can never leave a truncated JSON document\n * behind; a corrupt or unreadable file falls back to the default (\"on, with no\n * session opted in\") with a warning rather than leaving the feature in an\n * unknown state.\n */\nimport { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport { BREAKER_DEFAULT_SETTINGS, type BreakerSettings } from './contract.ts'\nimport { normalizeBreakerSettings } from './breaker.ts'\n\nconst PLUGIN_LABEL = 'dsh-usage-dashboard'\n\n/** Path below the DSH home; kept as path segments so Windows joins correctly. */\nconst STORAGE_RELATIVE_PATH = join('storages', 'dsh-usage-dashboard', 'breaker.json')\n\n/**\n * Resolve the settings file path from the environment. The env parameter is\n * injectable so tests can exercise the `DSH_HOME` branch without touching the\n * process environment.\n * @param env - environment to read `DSH_HOME` from; defaults to `process.env`.\n */\nexport function breakerSettingsPath(env: NodeJS.ProcessEnv = process.env): string {\n const configured = env.DSH_HOME?.trim()\n const home = configured !== undefined && configured !== '' ? configured : join(homedir(), '.dsh')\n return join(home, STORAGE_RELATIVE_PATH)\n}\n\n/** Shared IO options; the defaults are what the running plugin uses. */\nexport interface BreakerSettingsIoOptions {\n /** Explicit file path, bypassing {@link breakerSettingsPath}. */\n filePath?: string\n /** Warning sink, so tests can capture instead of printing. */\n warn?: (message: string) => void\n}\n\nconst errorText = (error: unknown): string => (error instanceof Error ? error.message : String(error))\n\nconst defaultWarn = (message: string): void => {\n console.warn(message)\n}\n\n/**\n * Settings from a parsed document, or `undefined` when the document is not\n * shaped like settings at all. Non-string session entries are dropped \u2014 a\n * stale client or a hand-edited file must not take the whole setting down \u2014\n * and the rest is deduplicated, validated and sorted by\n * `normalizeBreakerSettings`. A document from before the master switch existed\n * has no `enabled` field and reads as enabled: that is the value the feature\n * already ran with.\n */\nfunction settingsOf(raw: unknown): BreakerSettings | undefined {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return undefined\n const candidate = raw as { enabled?: unknown; sessions?: unknown }\n if (!Array.isArray(candidate.sessions)) return undefined\n if (candidate.enabled !== undefined && typeof candidate.enabled !== 'boolean') return undefined\n const ids = candidate.sessions.filter((entry): entry is string => typeof entry === 'string')\n return normalizeBreakerSettings({\n enabled: candidate.enabled ?? true,\n sessions: ids,\n })\n}\n\n/**\n * Read persisted settings. A missing file is the first-run case and returns\n * the defaults silently; unreadable, malformed, or structurally invalid\n * content warns and returns the defaults.\n */\nexport async function loadBreakerSettings(options: BreakerSettingsIoOptions = {}): Promise<BreakerSettings> {\n const filePath = options.filePath ?? breakerSettingsPath()\n const warn = options.warn ?? defaultWarn\n let text: string\n try {\n text = await readFile(filePath, 'utf8')\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code\n if (code !== 'ENOENT') {\n warn(`[${PLUGIN_LABEL}] \u8BFB\u53D6\u9AD8\u5CF0\u6682\u505C\u8BBE\u7F6E\u5931\u8D25\uFF0C\u6309\u9ED8\u8BA4\u503C\u5904\u7406\uFF1A${errorText(error)}`)\n }\n return { ...BREAKER_DEFAULT_SETTINGS }\n }\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch (error) {\n warn(`[${PLUGIN_LABEL}] \u9AD8\u5CF0\u6682\u505C\u8BBE\u7F6E\u4E0D\u662F\u5408\u6CD5 JSON\uFF0C\u6309\u9ED8\u8BA4\u503C\u5904\u7406\uFF1A${errorText(error)}`)\n return { ...BREAKER_DEFAULT_SETTINGS }\n }\n const settings = settingsOf(raw)\n if (settings === undefined) {\n warn(`[${PLUGIN_LABEL}] \u9AD8\u5CF0\u6682\u505C\u8BBE\u7F6E\u7ED3\u6784\u4E0D\u5408\u6CD5\uFF0C\u6309\u9ED8\u8BA4\u503C\u5904\u7406`)\n return { ...BREAKER_DEFAULT_SETTINGS }\n }\n return settings\n}\n\n/**\n * Persist settings atomically. The temp file is created beside the target and\n * renamed over it, so readers only ever see a complete document.\n * @param settings - master switch and session opt-ins; normalized before writing.\n * @param options - explicit path, if any.\n */\nexport async function saveBreakerSettings(\n settings: BreakerSettings,\n options: Pick<BreakerSettingsIoOptions, 'filePath'> = {},\n): Promise<void> {\n const filePath = options.filePath ?? breakerSettingsPath()\n const normalized = normalizeBreakerSettings(settings)\n await mkdir(dirname(filePath), { recursive: true })\n const tempPath = `${filePath}.${process.pid}.${Date.now().toString(36)}.tmp`\n try {\n await writeFile(tempPath, `${JSON.stringify(normalized, null, 2)}\\n`, 'utf8')\n await rename(tempPath, filePath)\n } catch (error) {\n await rm(tempPath, { force: true }).catch(() => {})\n throw error\n }\n}\n", "/**\n * Tiny TTL memo for the /api routes.\n *\n * The usage endpoint replays every session log, so it must not run on every\n * tab click; the balance endpoint also calls the DeepSeek API. The memo:\n *\n * - `get()` serves a successful cached response while it is within `ttlMs`,\n * otherwise recomputes;\n * - `refresh()` recomputes unconditionally and reseeds the cache;\n * - concurrent recomputes (of either kind) are deduped into one upstream\n * call;\n * - `ok:false` responses are never cached, so a transient failure is retried\n * on the next request.\n *\n * No timers are held, so nothing needs disposal on plugin unload.\n */\n\ninterface Entry<T> {\n value: T\n setAt: number\n}\n\ninterface MemoState<T> {\n entry: Entry<T> | undefined\n inflight: Promise<T> | undefined\n}\n\nexport interface Memo<T> {\n get(): Promise<T>\n refresh(): Promise<T>\n}\n\nexport function memoize<T extends { ok: boolean }>(ttlMs: number, compute: () => Promise<T>): Memo<T> {\n const state: MemoState<T> = { entry: undefined, inflight: undefined }\n\n const run = (): Promise<T> => {\n if (state.inflight !== undefined) return state.inflight\n const task = compute().then(\n (value) => {\n state.inflight = undefined\n if (value.ok) state.entry = { value, setAt: Date.now() }\n return value\n },\n (err: unknown) => {\n state.inflight = undefined\n throw err\n },\n )\n state.inflight = task\n return task\n }\n\n return {\n get: () => {\n const hit = state.entry\n if (hit !== undefined && hit.value.ok && Date.now() - hit.setAt < ttlMs) {\n return Promise.resolve(hit.value)\n }\n return run()\n },\n refresh: () => run(),\n }\n}\n", "/**\n * Browser-trust fence for the plugin's own /api route \u2014 the same fences the\n * harness's /api gateway applies: the Host header must name a loopback\n * authority, a browser-marked cross-site request is refused, and an attached\n * Origin must be exactly this authority.\n */\nimport type { IncomingMessage } from 'node:http'\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '[::1]'\n}\n\nexport function isTrustedApiRequest(req: IncomingMessage): boolean {\n const host = req.headers.host\n if (host === undefined) return false\n let hostUrl: URL\n try {\n hostUrl = new URL(`http://${host}`)\n } catch {\n return false\n }\n if (!isLoopbackHostname(hostUrl.hostname)) return false\n if (req.headers['sec-fetch-site'] === 'cross-site') return false\n const origin = req.headers.origin\n if (origin === undefined) return true\n try {\n return new URL(origin).host === hostUrl.host\n } catch {\n return false\n }\n}\n", "/**\n * Balance and usage data sources.\n *\n * - Balance: the signed-in DeepSeek account through the optional\n * `deepseekAccount` service (`getBalance`, no API key needed), falling back\n * to `GET /user/balance` with the key resolved through the credentials\n * service (`DEEPSEEK_API_KEY`).\n * - Usage: token usage folded from the DSH session logs (`sessionPersistence`),\n * one record per `assistant/message` event, bucketed by local day / hour.\n */\nimport { isValidSessionId, USAGE_WINDOW_DAYS } from './contract.ts'\nimport type { BalanceData, BalanceInfo, BalanceResponse, ModelSeriesPoint, ModelUsage, PeakSplit, PeriodUsage, SessionCost, SessionModelUsage, SessionUsageResponse, UsageCoverage, UsageData, UsageResponse, UsageSummary, UsageWindowDays, VisionDayPoint, VisionSession, VisionStats } from './contract.ts'\nimport type { AccountClientFace, AccountWalletFace, ContentBlockFace, CredentialsFace, DeepSeekAccountFace, ImageAttachmentFace, SessionEventFace, SessionHeaderFace, SessionPersistenceFace } from './context.ts'\nimport { cacheSavingOf, costOf, costUnderPeakEra, estimateImageTokens, isPeak, pricingInfo } from './pricing.ts'\n\nconst pad2 = (n: number): string => (n < 10 ? `0${n}` : String(n))\n\nfunction errorMessage(err: unknown): string {\n return (err as { message?: string } | null)?.message ?? String(err)\n}\n\n/** Identity the account service forwards to Platform as client headers. The\n * host has no browser identity; wallet totals don't depend on it, so the\n * desktop build's version (when the host environment carries one) and the\n * host's own UTC offset are enough. */\nconst ACCOUNT_CLIENT: AccountClientFace = {\n version: process.env.DSH_CLIENT_VERSION ?? '0.0.0',\n locale: 'zh-CN',\n timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,\n}\n\n/** The account service reports the recharge (`value`) and granted\n * (`bonusWallets`) wallets separately; the card shows one total per currency\n * with that same breakdown, so fold the two lists into its shape. */\nfunction accountBalances(\n toppedUpWallets: readonly AccountWalletFace[],\n grantedWallets: readonly AccountWalletFace[],\n): BalanceInfo[] {\n const byCurrency = new Map<string, { total: number; granted: number; toppedUp: number }>()\n const add = (wallets: readonly AccountWalletFace[], field: 'toppedUp' | 'granted'): void => {\n for (const wallet of wallets) {\n const currency = wallet.currency ?? ''\n const amount = Number(wallet.balance ?? '0')\n if (currency === '' || !Number.isFinite(amount)) continue\n const acc = byCurrency.get(currency) ?? { total: 0, granted: 0, toppedUp: 0 }\n acc[field] += amount\n acc.total += amount\n byCurrency.set(currency, acc)\n }\n }\n add(toppedUpWallets, 'toppedUp')\n add(grantedWallets, 'granted')\n return [...byCurrency].map(([currency, acc]) => ({\n currency,\n total: acc.total.toFixed(2),\n granted: acc.granted.toFixed(2),\n toppedUp: acc.toppedUp.toFixed(2),\n }))\n}\n\n/** Query the signed-in account's wallets. `signed-out` (the service's own\n * null) and `failed` (Platform rejected the query, or the service threw) both\n * let the caller fall back to the API key; only `failed` is worth surfacing\n * when no key exists. */\nasync function readAccountBalance(account: DeepSeekAccountFace): Promise<\n { ok: true; data: BalanceData } | { ok: false; reason: 'signed-out' | 'failed' }\n> {\n try {\n const result = await account.getBalance(ACCOUNT_CLIENT)\n if (result === null) return { ok: false, reason: 'signed-out' }\n if (result.status !== 'ready') return { ok: false, reason: 'failed' }\n const balances = accountBalances(result.value ?? [], result.bonusWallets ?? [])\n return { ok: true, data: { isAvailable: balances.length > 0, balances } }\n } catch {\n return { ok: false, reason: 'failed' }\n }\n}\n\nexport async function fetchBalance(\n credentials: CredentialsFace | undefined,\n account?: DeepSeekAccountFace,\n): Promise<BalanceResponse> {\n const fromAccount = account === undefined ? undefined : await readAccountBalance(account)\n if (fromAccount?.ok === true) return { ok: true, data: fromAccount.data }\n if (credentials === undefined) return { ok: false, error: '\u51ED\u8BC1\u670D\u52A1\u4E0D\u53EF\u7528' }\n let cred: { value: string } | undefined\n try {\n cred = await credentials.resolve('DEEPSEEK_API_KEY')\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6 API Key \u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n if (cred === undefined || cred.value === '') {\n // Report the reason that actually applies: a failing account endpoint is\n // not the same as no credential at all, and telling a signed-in user to\n // configure an API key they don't need hides the real problem.\n return {\n ok: false,\n error: fromAccount?.reason === 'failed'\n ? '\u8D26\u53F7\u4F59\u989D\u63A5\u53E3\u4E0D\u53EF\u7528\uFF0C\u4E14\u672A\u914D\u7F6E DEEPSEEK_API_KEY'\n : '\u672A\u914D\u7F6E DEEPSEEK_API_KEY\uFF08\u53EF\u5728\u300C\u8BBE\u7F6E \u2192 \u6A21\u578B\u300D\u4E2D\u586B\u5199\uFF09',\n }\n }\n let res: Response\n try {\n res = await fetch('https://api.deepseek.com/user/balance', {\n headers: { authorization: `Bearer ${cred.value}` },\n signal: AbortSignal.timeout(20000),\n })\n } catch (err) {\n return { ok: false, error: `\u8BF7\u6C42\u4F59\u989D\u63A5\u53E3\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n if (!res.ok) return { ok: false, error: `\u4F59\u989D\u63A5\u53E3\u8FD4\u56DE\u9519\u8BEF\uFF08HTTP ${res.status}\uFF09` }\n let parsed: unknown\n try {\n parsed = await res.json()\n } catch {\n return { ok: false, error: '\u89E3\u6790\u4F59\u989D\u54CD\u5E94\u5931\u8D25' }\n }\n const p = parsed as {\n is_available?: boolean\n balance_infos?: Array<{ currency?: string; total_balance?: string; granted_balance?: string; topped_up_balance?: string }>\n }\n return {\n ok: true,\n data: {\n isAvailable: p.is_available === true,\n balances: (p.balance_infos ?? []).map(b => ({\n currency: b.currency ?? '',\n total: b.total_balance ?? '0',\n granted: b.granted_balance ?? '0',\n toppedUp: b.topped_up_balance ?? '0',\n })),\n },\n }\n}\n\ninterface Bucket {\n input: number\n output: number\n cache: number\n total: number\n cost: number\n calls: number\n}\n\nconst emptyBucket = (): Bucket => ({ input: 0, output: 0, cache: 0, total: 0, cost: 0, calls: 0 })\n\n/** Cost is computed once per event (it depends on the model and on whether\n * the event landed in a peak window) and folded into every bucket that\n * needs it \u2014 shared by the account-wide replay and the single-session one. */\nfunction bump(map: Map<string, Bucket>, key: string, input: number, output: number, cache: number, cost: number): void {\n const bucket = map.get(key) ?? emptyBucket()\n bucket.input += input\n bucket.output += output\n bucket.cache += cache\n // reasoningTokens is a subset of outputTokens; never double-count it.\n bucket.total += input + output + cache\n bucket.cost += cost\n bucket.calls += 1\n map.set(key, bucket)\n}\n\ntype OverallBucket = Bucket & { reasoning: number; cacheSavings: number }\nconst emptyOverall = (): OverallBucket => ({ ...emptyBucket(), reasoning: 0, cacheSavings: 0 })\n\n/** Image usage folded one record at a time; `imageTokens`/`cost` are official-\n * rule estimates, `bytes` is the counted payload size when refs carry one. */\ninterface VisionAcc {\n images: number\n imageTokens: number\n cost: number\n bytes: number\n}\n\nconst emptyVision = (): VisionAcc => ({ images: 0, imageTokens: 0, cost: 0, bytes: 0 })\n\nfunction bumpVisionInto(\n acc: VisionAcc,\n images: number,\n imageTokens: number,\n cost: number,\n bytes: number,\n): void {\n acc.images += images\n acc.imageTokens += imageTokens\n acc.cost += cost\n acc.bytes += bytes\n}\n\nfunction bumpVisionMap(\n map: Map<string, VisionAcc>,\n key: string,\n images: number,\n imageTokens: number,\n cost: number,\n bytes: number,\n): void {\n const acc = map.get(key) ?? emptyVision()\n bumpVisionInto(acc, images, imageTokens, cost, bytes)\n map.set(key, acc)\n}\n\ninterface WindowAccumulator {\n days: UsageWindowDays\n startKey: string\n endKey: string\n dayMap: Map<string, Bucket>\n hourMap: Map<string, Bucket>\n overall: OverallBucket\n modelTotals: Map<string, Bucket>\n modelDays: Map<string, Map<string, Bucket>>\n modelHours: Map<string, Map<string, Bucket>>\n sessions: SessionCost[]\n vision: VisionAcc\n visionDays: Map<string, VisionAcc>\n visionSessions: Map<string, VisionAcc & { title: string }>\n}\n\n/** How many sessions the ranking keeps; the rest are summarised by count. */\nconst SESSION_TOP_N = 6\n\n/**\n * The session's own title, folded from its log: `session/title` events are\n * append-only and the last one wins. It rides along with the usage replay, so\n * naming a session costs no extra read.\n */\nfunction titleOf(events: SessionEventFace[] | undefined, id: string): string {\n let title = ''\n if (events !== undefined) {\n for (const ev of events) {\n if (ev?.type !== 'session/title') continue\n const next = ev.data?.title\n if (typeof next === 'string' && next !== '') title = next\n }\n }\n return title !== '' ? title : `\u4F1A\u8BDD ${id.slice(0, 8)}`\n}\n\nconst dayKeyOf = (d: Date): string => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n\n/**\n * Roll the per-day buckets up into the windows the dashboard's summary card\n * shows. Everything is keyed by local date, matching how the buckets were\n * filled, so \"today\" means the user's today.\n */\nfunction summarize(dayMap: Map<string, Bucket>, now: Date): UsageSummary {\n const sum = (matches: (key: string) => boolean): PeriodUsage => {\n const acc: PeriodUsage = { total: 0, cost: 0, calls: 0 }\n for (const [key, bucket] of dayMap) {\n if (!matches(key)) continue\n acc.total += bucket.total\n acc.cost += bucket.cost\n acc.calls += bucket.calls\n }\n return acc\n }\n const todayKey = dayKeyOf(now)\n const yesterdayKey = dayKeyOf(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1))\n const monthPrefix = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-`\n const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)\n const lastMonthPrefix = `${lastMonth.getFullYear()}-${pad2(lastMonth.getMonth() + 1)}-`\n const dayOfMonth = now.getDate()\n return {\n today: sum(key => key === todayKey),\n yesterday: sum(key => key === yesterdayKey),\n month: sum(key => key.startsWith(monthPrefix)),\n lastMonthToDate: sum(key => key.startsWith(lastMonthPrefix) && Number(key.slice(8)) <= dayOfMonth),\n }\n}\n\n/**\n * Fold usage out of one session's event log. The model for every\n * `assistant/message` usage record is the one from the latest preceding\n * `request/header` event (each request logs one before dispatch), so usage\n * can be attributed per model. Events without any preceding header fall back\n * to the `''`/`''` (unknown) bucket.\n *\n * Image blocks are collected alongside: `user/message` content and tool\n * results (`tool/result`, screenshots from tools) may carry `image` blocks,\n * and every pending image is attributed to the next usage record \u2014 the model\n * call whose prompt actually carried it. Images followed by no usage record\n * (the request failed or never ran) contribute nothing to the fold, since no\n * model call was billed for them.\n */\nfunction addUsageEvent(\n events: SessionEventFace[] | undefined,\n onEvent: (\n time: number,\n input: number,\n output: number,\n cache: number,\n reasoning: number,\n provider: string,\n model: string,\n messageId: string | undefined,\n turn: number | undefined,\n images: ImageAttachmentFace[],\n ) => void,\n): Pick<UsageCoverage, 'usageRecords' | 'skippedRecords'> {\n const result = { usageRecords: 0, skippedRecords: 0 }\n if (events === undefined) return result\n let provider = ''\n let model = ''\n const pendingImages: ImageAttachmentFace[] = []\n for (const ev of events) {\n if (ev?.type === 'request/header') {\n const cfg = ev.data?.header?.config\n if (cfg?.provider !== undefined && cfg?.model !== undefined) {\n provider = cfg.provider\n model = cfg.model\n }\n continue\n }\n pendingImages.push(...imagesInEvent(ev))\n if (ev?.type !== 'assistant/message') continue\n const usage = ev.data?.usage\n if (usage === undefined) continue\n const values = [usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.reasoningTokens]\n .map(value => value === undefined ? 0 : Number(value))\n if (typeof ev.time !== 'number' || !Number.isFinite(ev.time) || values.some(value => !Number.isFinite(value) || value < 0)) {\n result.skippedRecords += 1\n continue\n }\n const [input = 0, output = 0, cache = 0, reasoning = 0] = values\n const rawMessageId = ev.data?.message?.id\n const messageId = typeof rawMessageId === 'string' && rawMessageId !== '' ? rawMessageId : undefined\n const rawTurn = ev.data?.turn\n const turn = typeof rawTurn === 'number' && Number.isFinite(rawTurn) ? rawTurn : undefined\n onEvent(ev.time, input, output, cache, reasoning, provider, model, messageId, turn, pendingImages)\n pendingImages.length = 0\n result.usageRecords += 1\n }\n return result\n}\n\n/** Every `image` attachment ref inside one content-block tree, including\n * blocks nested in tool-result blocks (`tool/result` screenshots). */\nfunction imageBlocks(blocks: ContentBlockFace[] | undefined): ImageAttachmentFace[] {\n const found: ImageAttachmentFace[] = []\n const walkAll = (items: ContentBlockFace[] | undefined): void => {\n for (const item of items ?? []) {\n if (item?.type === 'image' && item.attachment !== undefined && item.attachment !== null) {\n found.push(item.attachment)\n }\n if (Array.isArray(item?.content)) walkAll(item.content)\n }\n }\n walkAll(blocks)\n return found\n}\n\n/** Image attachment refs carried by one event: `user/message` blocks at\n * `data.content`, `tool/result` blocks at `data.message.content`. */\nfunction imagesInEvent(ev: SessionEventFace | undefined): ImageAttachmentFace[] {\n if (ev === undefined) return []\n return [\n ...imageBlocks(ev.data?.content),\n ...imageBlocks(ev.data?.message?.content),\n ]\n}\n\n/**\n * Read one session's full event log across host generations. Handle-era DSH\n * (the one without `readFrom`) exposes `open(id, 'read')` \u2192 slice reads that\n * must be looped until an empty slice, then `close()`; legacy hosts returned\n * the whole log from `readFrom(id, 0)`. One shared path so `fetchUsage` and\n * `fetchSessionUsage` close the handle on every outcome, success or failure.\n */\nasync function readSessionEvents(persistence: SessionPersistenceFace, id: string): Promise<SessionEventFace[] | undefined> {\n if (typeof persistence.open === 'function') {\n const handle = await persistence.open(id, 'read')\n try {\n const events: SessionEventFace[] = []\n for (;;) {\n const slice = await handle.read(events.length)\n const batch = slice.events ?? []\n if (batch.length === 0) return events\n // Loop, not spread: one slice can carry a whole long session and\n // `push(...batch)` would overflow the call-argument stack.\n for (const event of batch) events.push(event)\n }\n } finally {\n // A read handle has nothing durable to drain, and a close failure\n // must not mask the read's result or error.\n try { await handle.close() } catch { /* nothing to drain */ }\n }\n }\n if (persistence.readFrom === undefined) {\n throw new Error('\u5BBF\u4E3B sessionPersistence \u7F3A\u5C11 open() \u4E0E readFrom()\uFF0C\u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7')\n }\n const { events } = await persistence.readFrom(id, 0)\n return events\n}\n\nexport async function fetchUsage(persistence: SessionPersistenceFace | undefined, nowMs = Date.now()): Promise<UsageResponse> {\n if (persistence === undefined) return { ok: false, error: '\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528' }\n let headers: SessionHeaderFace[]\n try {\n headers = await persistence.list()\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n const dayMap = new Map<string, Bucket>()\n const hourMap = new Map<string, Bucket>()\n const overall = emptyOverall()\n // Per model: key `${provider}/${model}` -> totals and day/hour maps.\n const modelTotals = new Map<string, Bucket>()\n const modelDays = new Map<string, Map<string, Bucket>>()\n const modelHours = new Map<string, Map<string, Bucket>>()\n const sessions: SessionCost[] = []\n // Image usage across everything the replay sees. Day-keyed series mirrors\n // the daily bucket map; per-session tracks \"which run kept sending pictures\".\n const vision = emptyVision()\n const visionDays = new Map<string, VisionAcc>()\n const visionSessions = new Map<string, VisionAcc & { title: string }>()\n const coverage: UsageCoverage = {\n scope: 'local-dsh-session-logs',\n listedSessions: 0,\n scannedSessions: 0,\n usageRecords: 0,\n skippedRecords: 0,\n failedSessions: 0,\n earliestAt: null,\n latestAt: null,\n }\n // Peak / off-peak split, plus the same usage re-priced as if everything\n // had landed in an idle window (the shift-off-peak saving).\n const peakSplit: PeakSplit = {\n peak: { total: 0, cost: 0, calls: 0 },\n offPeak: { total: 0, cost: 0, calls: 0 },\n peakEraCost: 0,\n offPeakEraCost: 0,\n }\n\n const now = new Date(nowMs)\n const dateBefore = (days: number): Date => new Date(now.getFullYear(), now.getMonth(), now.getDate() - days)\n const windowAggregates: WindowAccumulator[] = USAGE_WINDOW_DAYS.map(days => ({\n days,\n startKey: dayKeyOf(dateBefore(days - 1)),\n endKey: dayKeyOf(now),\n dayMap: new Map(),\n hourMap: new Map(),\n overall: emptyOverall(),\n modelTotals: new Map(),\n modelDays: new Map(),\n modelHours: new Map(),\n sessions: [],\n vision: emptyVision(),\n visionDays: new Map(),\n visionSessions: new Map(),\n }))\n\n const bumpOverall = (\n bucket: OverallBucket,\n input: number,\n output: number,\n cache: number,\n reasoning: number,\n cost: number,\n cacheSavings: number,\n ): void => {\n bucket.input += input\n bucket.output += output\n bucket.cache += cache\n bucket.reasoning += reasoning\n bucket.total += input + output + cache\n bucket.cost += cost\n bucket.cacheSavings += cacheSavings\n bucket.calls += 1\n }\n\n const bumpModel = (\n totals: Map<string, Bucket>,\n days: Map<string, Map<string, Bucket>>,\n hours: Map<string, Map<string, Bucket>>,\n modelKey: string,\n dayKey: string,\n hourKey: string,\n input: number,\n output: number,\n cache: number,\n cost: number,\n ): void => {\n bump(totals, modelKey, input, output, cache, cost)\n let dayBuckets = days.get(modelKey)\n if (dayBuckets === undefined) {\n dayBuckets = new Map()\n days.set(modelKey, dayBuckets)\n }\n bump(dayBuckets, dayKey, input, output, cache, cost)\n let hourBuckets = hours.get(modelKey)\n if (hourBuckets === undefined) {\n hourBuckets = new Map()\n hours.set(modelKey, hourBuckets)\n }\n bump(hourBuckets, hourKey, input, output, cache, cost)\n }\n\n const bumpSession = (session: SessionCost, time: number, total: number, cost: number): void => {\n session.total += total\n session.cost += cost\n session.calls += 1\n if (time > session.lastActive) session.lastActive = time\n }\n\n const sessionIds = headers\n .map(header => header.id ?? header.sessionId ?? header.header?.id ?? header.header?.sessionId)\n .filter((sid): sid is string => sid !== undefined && sid !== '')\n coverage.listedSessions = sessionIds.length\n\n for (const sid of sessionIds) {\n try {\n const events = await readSessionEvents(persistence, sid)\n coverage.scannedSessions += 1\n const session: SessionCost = { id: sid, title: '', total: 0, cost: 0, calls: 0, lastActive: 0 }\n const windowSessions = windowAggregates.map((): SessionCost => ({ id: sid, title: '', total: 0, cost: 0, calls: 0, lastActive: 0 }))\n const sessionVision: VisionAcc & { title: string } = { ...emptyVision(), title: '' }\n const windowVisionSessions = windowAggregates.map((): VisionAcc & { title: string } => ({ ...emptyVision(), title: '' }))\n const scanned = addUsageEvent(events, (time, input, output, cache, reasoning, provider, model, _messageId, _turn, images) => {\n if (coverage.earliestAt === null || time < coverage.earliestAt) coverage.earliestAt = time\n if (coverage.latestAt === null || time > coverage.latestAt) coverage.latestAt = time\n const d = new Date(time)\n const dayKey = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`\n const hourKey = String(d.getHours())\n const modelKey = `${provider}/${model}`\n const cost = costOf(time, model, input, cache, output)\n const cacheSavings = cacheSavingOf(time, model, cache)\n const eventTotal = input + output + cache\n bump(dayMap, dayKey, input, output, cache, cost)\n bump(hourMap, hourKey, input, output, cache, cost)\n bumpModel(modelTotals, modelDays, modelHours, modelKey, dayKey, hourKey, input, output, cache, cost)\n bumpOverall(overall, input, output, cache, reasoning, cost, cacheSavings)\n bumpSession(session, time, eventTotal, cost)\n // Image blocks ride the record that consumed them: estimated tokens\n // (official resizing rule) priced at that call's cache-miss input rate.\n if (images.length > 0) {\n let imageTokensSum = 0\n let imageCostSum = 0\n let imageBytesSum = 0\n for (const image of images) {\n const width = typeof image.width === 'number' ? image.width : 0\n const height = typeof image.height === 'number' ? image.height : 0\n const tokens = estimateImageTokens(width, height)\n imageTokensSum += tokens\n imageCostSum += costOf(time, model, tokens, 0, 0)\n imageBytesSum += typeof image.bytes === 'number' ? image.bytes : 0\n }\n bumpVisionInto(vision, images.length, imageTokensSum, imageCostSum, imageBytesSum)\n bumpVisionMap(visionDays, dayKey, images.length, imageTokensSum, imageCostSum, imageBytesSum)\n bumpVisionInto(sessionVision, images.length, imageTokensSum, imageCostSum, imageBytesSum)\n for (let i = 0; i < windowAggregates.length; i++) {\n const aggregate = windowAggregates[i]\n if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue\n bumpVisionInto(aggregate.vision, images.length, imageTokensSum, imageCostSum, imageBytesSum)\n bumpVisionMap(aggregate.visionDays, dayKey, images.length, imageTokensSum, imageCostSum, imageBytesSum)\n bumpVisionInto(windowVisionSessions[i], images.length, imageTokensSum, imageCostSum, imageBytesSum)\n }\n }\n for (let i = 0; i < windowAggregates.length; i++) {\n const aggregate = windowAggregates[i]\n if (dayKey < aggregate.startKey || dayKey > aggregate.endKey) continue\n bump(aggregate.dayMap, dayKey, input, output, cache, cost)\n bump(aggregate.hourMap, hourKey, input, output, cache, cost)\n bumpModel(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, modelKey, dayKey, hourKey, input, output, cache, cost)\n bumpOverall(aggregate.overall, input, output, cache, reasoning, cost, cacheSavings)\n bumpSession(windowSessions[i], time, eventTotal, cost)\n }\n const window = isPeak(time) ? peakSplit.peak : peakSplit.offPeak\n window.total += eventTotal\n window.cost += cost\n window.calls += 1\n peakSplit.peakEraCost += costUnderPeakEra(time, model, input, cache, output)\n peakSplit.offPeakEraCost += costUnderPeakEra(time, model, input, cache, output, true)\n })\n coverage.usageRecords += scanned.usageRecords\n coverage.skippedRecords += scanned.skippedRecords\n if (session.calls > 0) {\n const title = titleOf(events, sid)\n session.title = title\n sessions.push(session)\n for (let i = 0; i < windowAggregates.length; i++) {\n const windowSession = windowSessions[i]\n if (windowSession.calls === 0) continue\n windowSession.title = title\n windowAggregates[i].sessions.push(windowSession)\n }\n }\n if (sessionVision.images > 0) {\n sessionVision.title = titleOf(events, sid)\n visionSessions.set(sid, sessionVision)\n for (let i = 0; i < windowAggregates.length; i++) {\n const windowVision = windowVisionSessions[i]\n if (windowVision.images === 0) continue\n windowVision.title = sessionVision.title\n windowAggregates[i].visionSessions.set(sid, windowVision)\n }\n }\n } catch {\n // One broken session log must not sink the whole dashboard.\n coverage.failedSessions += 1\n }\n }\n\n const makeDaily = (map: Map<string, Bucket>, count: number): UsageData['daily'] => {\n const result: UsageData['daily'] = []\n for (let i = count - 1; i >= 0; i--) {\n const key = dayKeyOf(dateBefore(i))\n const b = map.get(key) ?? emptyBucket()\n result.push({ date: key, input: b.input, output: b.output, cache: b.cache, total: b.total, cost: b.cost, calls: b.calls })\n }\n return result\n }\n\n const makeHourly = (map: Map<string, Bucket>): UsageData['hourly'] => {\n const result: UsageData['hourly'] = []\n for (let i = 0; i < 24; i++) {\n const b = map.get(String(i)) ?? emptyBucket()\n result.push({ hour: i, total: b.total, cost: b.cost, calls: b.calls })\n }\n return result\n }\n\n const makeModels = (\n totals: Map<string, Bucket>,\n daysByModel: Map<string, Map<string, Bucket>>,\n hoursByModel: Map<string, Map<string, Bucket>>,\n dayCount: number,\n ): ModelUsage[] => {\n const result: ModelUsage[] = []\n for (const [modelKey, bucket] of totals) {\n const slash = modelKey.indexOf('/')\n const provider = slash < 0 ? '' : modelKey.slice(0, slash)\n const model = slash < 0 ? modelKey : modelKey.slice(slash + 1)\n const days = daysByModel.get(modelKey)\n const hours = hoursByModel.get(modelKey)\n const daily: ModelSeriesPoint[] = makeDaily(days ?? new Map(), dayCount)\n .map(point => ({ total: point.total, cost: point.cost, calls: point.calls }))\n const hourly: ModelSeriesPoint[] = makeHourly(hours ?? new Map())\n .map(point => ({ total: point.total, cost: point.cost, calls: point.calls }))\n result.push({\n provider,\n model,\n input: bucket.input,\n output: bucket.output,\n cache: bucket.cache,\n total: bucket.total,\n cost: bucket.cost,\n calls: bucket.calls,\n daily,\n hourly,\n })\n }\n result.sort((a, b) => b.total - a.total)\n return result\n }\n\n const daily = makeDaily(dayMap, 30)\n const hourly = makeHourly(hourMap)\n\n // 52 full weeks (364 days) rather than a calendar year, so the grid is\n // always a whole number of week-columns and never carries an orphan day.\n const heatmap: UsageData['heatmap'] = []\n for (let i = 363; i >= 0; i--) {\n const key = dayKeyOf(dateBefore(i))\n const b = dayMap.get(key) ?? emptyBucket()\n heatmap.push({ date: key, total: b.total, cost: b.cost, calls: b.calls })\n }\n\n const makeVisionDaily = (map: Map<string, VisionAcc>, count: number): VisionDayPoint[] => {\n const result: VisionDayPoint[] = []\n for (let i = count - 1; i >= 0; i--) {\n const key = dayKeyOf(dateBefore(i))\n const b = map.get(key) ?? emptyVision()\n result.push({ date: key, images: b.images, imageTokens: b.imageTokens, cost: b.cost })\n }\n return result\n }\n\n const makeVisionSessions = (map: Map<string, VisionAcc & { title: string }>): VisionSession[] =>\n [...map.entries()]\n .map(([id, acc]) => ({\n id,\n title: acc.title !== '' ? acc.title : `\u4F1A\u8BDD ${id.slice(0, 8)}`,\n images: acc.images,\n imageTokens: acc.imageTokens,\n cost: acc.cost,\n }))\n .sort((a, b) => b.images - a.images)\n .slice(0, SESSION_TOP_N)\n\n const models = makeModels(modelTotals, modelDays, modelHours, 30)\n const windows = windowAggregates.map(aggregate => ({\n days: aggregate.days,\n daily: makeDaily(aggregate.dayMap, aggregate.days),\n hourly: makeHourly(aggregate.hourMap),\n totals: { ...aggregate.overall },\n models: makeModels(aggregate.modelTotals, aggregate.modelDays, aggregate.modelHours, aggregate.days),\n sessions: [...aggregate.sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),\n sessionCount: aggregate.sessions.length,\n vision: { ...aggregate.vision },\n visionDaily: makeVisionDaily(aggregate.visionDays, aggregate.days),\n visionSessions: makeVisionSessions(aggregate.visionSessions),\n }))\n\n return {\n ok: true,\n data: {\n daily,\n hourly,\n heatmap,\n models,\n summary: summarize(dayMap, new Date(nowMs)),\n pricing: pricingInfo(nowMs),\n peakSplit,\n sessions: [...sessions].sort((a, b) => b.cost - a.cost).slice(0, SESSION_TOP_N),\n sessionCount: sessions.length,\n coverage,\n windows,\n vision: { ...vision },\n visionDaily: makeVisionDaily(visionDays, 30),\n visionSessions: makeVisionSessions(visionSessions),\n totals: {\n input: overall.input,\n output: overall.output,\n cache: overall.cache,\n reasoning: overall.reasoning,\n total: overall.total,\n cost: overall.cost,\n calls: overall.calls,\n cacheSavings: overall.cacheSavings,\n },\n },\n }\n}\n\n/**\n * Usage for exactly one session \u2014 the \u300C\u989D\u5EA6\u300Dtab's own conversation, as\n * opposed to `fetchUsage`'s account-wide replay. Reads a single session log\n * (not every session on disk), so it is cheap enough to call on every tab\n * mount without a TTL memo; the caller is expected to re-fetch whenever the\n * session id it cares about changes.\n */\nexport async function fetchSessionUsage(persistence: SessionPersistenceFace | undefined, sessionId: string): Promise<SessionUsageResponse> {\n if (persistence === undefined) return { ok: false, error: '\u4F1A\u8BDD\u6301\u4E45\u5316\u670D\u52A1\u4E0D\u53EF\u7528' }\n if (typeof sessionId !== 'string' || sessionId === '') return { ok: false, error: '\u7F3A\u5C11\u4F1A\u8BDD id' }\n // Defense in depth: src/index.ts already rejects a malformed `id` before\n // this function is ever called (see SESSION_ID_PATTERN in contract.ts),\n // but this function is also unit-tested and reachable directly, so the\n // same check is repeated here rather than trusted to the one caller.\n if (!isValidSessionId(sessionId)) return { ok: false, error: '\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5' }\n\n let events: SessionEventFace[] | undefined\n try {\n events = await readSessionEvents(persistence, sessionId)\n } catch (err) {\n return { ok: false, error: `\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u5931\u8D25\uFF1A${errorMessage(err)}` }\n }\n\n const modelTotals = new Map<string, Bucket>()\n let total = 0\n let cost = 0\n let calls = 0\n let firstActive: number | null = null\n let lastActive: number | null = null\n const turnTotals = new Map<string, { messageId: string; total: number; cost: number; calls: number }>()\n\n addUsageEvent(events, (time, input, output, cache, _reasoning, provider, model, messageId, turn) => {\n const modelKey = `${provider}/${model}`\n const eventCost = costOf(time, model, input, cache, output)\n const eventTotal = input + output + cache\n bump(modelTotals, modelKey, input, output, cache, eventCost)\n total += eventTotal\n cost += eventCost\n calls += 1\n if (firstActive === null || time < firstActive) firstActive = time\n if (lastActive === null || time > lastActive) lastActive = time\n if (messageId !== undefined) {\n // A turn may make several model calls while tools run. The chat renders\n // one action strip beside its final assistant message, so keep replacing\n // the address while accumulating every step's usage under the turn id.\n const key = turn === undefined ? `message:${messageId}` : `turn:${turn}`\n const aggregate = turnTotals.get(key) ?? { messageId, total: 0, cost: 0, calls: 0 }\n aggregate.messageId = messageId\n aggregate.total += eventTotal\n aggregate.cost += eventCost\n aggregate.calls += 1\n turnTotals.set(key, aggregate)\n }\n })\n\n const models: SessionModelUsage[] = [...modelTotals.entries()]\n .map(([modelKey, bucket]): SessionModelUsage => {\n const slash = modelKey.indexOf('/')\n const provider = slash < 0 ? '' : modelKey.slice(0, slash)\n const model = slash < 0 ? modelKey : modelKey.slice(slash + 1)\n return {\n provider,\n model,\n input: bucket.input,\n output: bucket.output,\n cache: bucket.cache,\n total: bucket.total,\n cost: bucket.cost,\n calls: bucket.calls,\n }\n })\n .sort((a, b) => b.cost - a.cost)\n\n return {\n ok: true,\n data: {\n sessionId,\n title: titleOf(events, sessionId),\n total,\n cost,\n calls,\n firstActive,\n lastActive,\n models,\n turns: [...turnTotals.values()],\n },\n }\n}\n", "import type { ServerResponse } from 'node:http'\n\nexport function writeJson(res: ServerResponse, status: number, body: unknown, headers?: Record<string, string>): void {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...headers })\n res.end(JSON.stringify(body))\n}\n", "/**\n * dsh-usage-dashboard host plugin.\n *\n * Registers the package's own fenced JSON API under\n * `/api/dsh-usage-dashboard` (balance + usage + session), consumed by the\n * client half through same-origin fetch. `balance` and `usage` are memoized\n * with a TTL so that tab switches and the widget's polling don't replay\n * session logs or hammer the DeepSeek API; responses carry `Cache-Control`\n * so the browser's HTTP cache serves repeat requests without a network\n * round trip. `?refresh=1` bypasses the memo for an explicit force refresh.\n * `session` (usage for exactly one session, keyed by `?id=`) reads a single\n * session log and is deliberately not memoized \u2014 it is cheap and must\n * reflect that session's live state, not a 5-minute-stale snapshot.\n *\n * The host also owns the peak-pause gate: `agent/pre-step` parks a user turn\n * of an opted-in session while DeepSeek's peak pricing is active. The master\n * switch and opt-in list persist across restarts, and `/breaker` +\n * `/breaker/release` let the client read state, flip either switch, and let\n * parked work run immediately.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { BreakerData, BreakerSettings } from './contract.ts'\nimport { BREAKER_API, isBreakerSettingsRequest, isValidSessionId } from './contract.ts'\nimport type { HostContext, HostEventFace } from './context.ts'\nimport { BreakerGate, type BreakerTimerFace } from './breaker-gate.ts'\nimport { breakerProject, normalizeBreakerSettings } from './breaker.ts'\nimport { breakerSettingsPath, loadBreakerSettings, saveBreakerSettings } from './breaker-settings.ts'\nimport { memoize } from './memo.ts'\nimport { isTrustedApiRequest } from './trust-fence.ts'\nimport { fetchBalance, fetchSessionUsage, fetchUsage } from './usage.ts'\nimport { writeJson } from './wire.ts'\n\n/** Plugin identity for the cordis.patch.yml row (and the client bundle id). */\nexport const name = 'dsh-usage-dashboard'\n\n/** Services required before load: the web server plus the two data sources. */\nexport const inject = ['webServer', 'credentials', 'sessionPersistence']\n\n/** Freshness windows: balance 60s (the widget also polls every 60s), usage\n * 5min (aggregating every session log is the expensive path). */\nconst BALANCE_TTL_MS = 60_000\nconst USAGE_TTL_MS = 5 * 60_000\n\n/** Hard cap on a breaker request body; the settings document is a list of ids. */\nconst BREAKER_BODY_LIMIT_BYTES = 8 * 1024\n\nconst NO_STORE = { 'cache-control': 'no-store' } as const\n\n/**\n * Test seams for the breaker half. The host calls `apply(ctx)` (or passes its\n * plugin config, whose unknown keys are not read here), so every field falls\n * back to production behaviour: the real clock, the DSH home, console.warn.\n */\nexport interface BreakerHostOptions {\n /** Explicit settings file, bypassing `breakerSettingsPath`. */\n settingsFile?: string\n /** Clock seam, epoch ms; defaults to `Date.now`. */\n now?: () => number\n /** Timer seam; defaults to the real (unref'd) clock. */\n timers?: BreakerTimerFace\n /** Warning sink; defaults to `console.warn`. */\n warn?: (message: string) => void\n}\n\nconst errorText = (error: unknown): string => (error instanceof Error ? error.message : String(error))\n\n/** Outcome of reading a request body under the size cap. */\ntype BodyRead =\n | { kind: 'ok'; text: string }\n | { kind: 'too-large' }\n | { kind: 'unreadable' }\n\n/**\n * Read a request body, refusing to buffer more than `limit` bytes. On overflow\n * the remaining bytes are drained (not destroyed) so the client still receives\n * the 413 instead of a reset connection.\n */\nasync function readBody(req: IncomingMessage, limit: number): Promise<BodyRead> {\n const chunks: Buffer[] = []\n let size = 0\n try {\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += buffer.length\n if (size > limit) {\n req.resume()\n return { kind: 'too-large' }\n }\n chunks.push(buffer)\n }\n } catch {\n return { kind: 'unreadable' }\n }\n return { kind: 'ok', text: Buffer.concat(chunks).toString('utf8') }\n}\n\n/**\n * Mount the /api/dsh-usage-dashboard routes and the peak-window breaker gate.\n * @param ctx - host Cordis context.\n * @param options - test seams; the host passes none.\n */\nexport function apply(ctx: HostContext, options: BreakerHostOptions = {}): void {\n // `deepseekAccount` is optional (API-key-only hosts never mount it), so it\n // is read through `get()` rather than declared in `inject` \u2014 a required\n // service would keep this plugin from loading at all on those hosts.\n const balanceMemo = memoize(BALANCE_TTL_MS, () => fetchBalance(ctx.credentials, ctx.get('deepseekAccount')))\n const usageMemo = memoize(USAGE_TTL_MS, () => fetchUsage(ctx.sessionPersistence))\n\n // The host may pass its plugin config here; nothing in it is read, so an\n // absent/null config simply means \"all production defaults\".\n const seams = options ?? {}\n const warn = seams.warn ?? ((message: string): void => { console.warn(message) })\n const now = seams.now ?? Date.now\n const settingsFile = seams.settingsFile ?? breakerSettingsPath()\n const gate = new BreakerGate({ now, timers: seams.timers })\n\n // Persisted settings load asynchronously; the gate starts from the defaults\n // (on, no session opted in) and is updated once the file has been read. A\n // switch write that lands in the meantime wins \u2014 the load must not clobber a\n // fresher user choice.\n let settingsTouched = false\n void loadBreakerSettings({ filePath: settingsFile, warn })\n .then((settings) => {\n if (!settingsTouched) gate.update(settings)\n })\n .catch((error: unknown) => {\n warn(`[${name}] \u9AD8\u5CF0\u6682\u505C\u8BBE\u7F6E\u52A0\u8F7D\u5931\u8D25\uFF0C\u6309\u9ED8\u8BA4\u503C\u5904\u7406\uFF1A${errorText(error)}`)\n })\n\n const projection = (): BreakerData => breakerProject(gate.settings, now(), gate.heldSessions)\n\n const hostEvents = ctx as unknown as HostEventFace\n ctx.effect(() => {\n const dispose = hostEvents.on('agent/pre-step', async (payload, next) => {\n try {\n // `agent.id` is the session id the composer switch writes. An agent\n // without one belongs to no session the user could have opted in.\n await gate.hold(payload.agent?.id, payload.turn, payload.messages, payload.signal)\n } catch (error) {\n // A gate failure must never fail the proposal: log it and let the\n // step run as if peak pause were off.\n warn(`[${name}] \u9AD8\u5CF0\u6682\u505C\u6302\u8D77\u5931\u8D25\uFF0C\u672C\u6B21 step \u76F4\u63A5\u653E\u884C\uFF1A${errorText(error)}`)\n }\n return next()\n })\n return () => {\n dispose()\n gate.dispose()\n }\n }, 'dsh-usage-dashboard: \u9AD8\u5CF0\u6682\u505C')\n\n /** Apply the master switch or one conversation's switch and persist it. */\n const writeSettings = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const body = await readBody(req, BREAKER_BODY_LIMIT_BYTES)\n if (body.kind === 'too-large') {\n writeJson(res, 413, { ok: false, error: '\u8BF7\u6C42\u4F53\u8D85\u8FC7 8KB \u9650\u5236' }, NO_STORE)\n return\n }\n if (body.kind === 'unreadable') {\n writeJson(res, 400, { ok: false, error: '\u65E0\u6CD5\u8BFB\u53D6\u8BF7\u6C42\u4F53' }, NO_STORE)\n return\n }\n let raw: unknown\n try {\n raw = JSON.parse(body.text)\n } catch {\n writeJson(res, 400, { ok: false, error: '\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON' }, NO_STORE)\n return\n }\n if (!isBreakerSettingsRequest(raw)) {\n writeJson(res, 400, { ok: false, error: '\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F\u5408\u6CD5\u7684\u5F00\u5173\u8BF7\u6C42' }, NO_STORE)\n return\n }\n const previous = gate.settings\n let next: BreakerSettings\n if (raw.sessionId === undefined) {\n next = normalizeBreakerSettings({ enabled: raw.enabled, sessions: previous.sessions })\n } else {\n const sessions = new Set(previous.sessions)\n if (raw.enabled) sessions.add(raw.sessionId)\n else sessions.delete(raw.sessionId)\n next = normalizeBreakerSettings({ enabled: previous.enabled, sessions: [...sessions] })\n }\n settingsTouched = true\n gate.update(next)\n try {\n await saveBreakerSettings(next, { filePath: settingsFile })\n } catch (error) {\n // The API is atomic from the caller's view: a failed write rolls the\n // live settings back instead of letting them diverge.\n gate.update(previous)\n warn(`[${name}] \u9AD8\u5CF0\u6682\u505C\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25\uFF0C\u5DF2\u56DE\u9000\uFF1A${errorText(error)}`)\n writeJson(res, 500, { ok: false, error: '\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25' }, NO_STORE)\n return\n }\n writeJson(res, 200, { ok: true, data: projection() }, NO_STORE)\n }\n\n /** Let parked work continue: all sessions, or the one named in the body. */\n const releaseHeld = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const body = await readBody(req, BREAKER_BODY_LIMIT_BYTES)\n if (body.kind === 'too-large') {\n writeJson(res, 413, { ok: false, error: '\u8BF7\u6C42\u4F53\u8D85\u8FC7 8KB \u9650\u5236' }, NO_STORE)\n return\n }\n if (body.kind === 'unreadable') {\n writeJson(res, 400, { ok: false, error: '\u65E0\u6CD5\u8BFB\u53D6\u8BF7\u6C42\u4F53' }, NO_STORE)\n return\n }\n let target: string | undefined\n // An empty body is the bare \"continue everything\" request; when a body is\n // present it must be a release request naming at most one session.\n if (body.text.trim() !== '') {\n let raw: unknown\n try {\n raw = JSON.parse(body.text)\n } catch {\n writeJson(res, 400, { ok: false, error: '\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON' }, NO_STORE)\n return\n }\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n writeJson(res, 400, { ok: false, error: '\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON \u5BF9\u8C61' }, NO_STORE)\n return\n }\n const candidate = raw as { sessionId?: unknown }\n if (Object.keys(candidate).some(key => key !== 'sessionId')) {\n writeJson(res, 400, { ok: false, error: '\u8BF7\u6C42\u5B57\u6BB5\u4E0D\u5408\u6CD5' }, NO_STORE)\n return\n }\n if (candidate.sessionId !== undefined) {\n if (typeof candidate.sessionId !== 'string' || !isValidSessionId(candidate.sessionId)) {\n writeJson(res, 400, { ok: false, error: '\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5' }, NO_STORE)\n return\n }\n target = candidate.sessionId\n }\n }\n gate.release(target)\n writeJson(res, 200, { ok: true, data: projection() }, NO_STORE)\n }\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'prefix',\n path: '/api/dsh-usage-dashboard',\n handler: async (req, res) => {\n if (!isTrustedApiRequest(req)) {\n writeJson(res, 403, { ok: false, error: 'forbidden' })\n return\n }\n const url = new URL(req.url ?? '/', 'http://dsh.internal')\n const pathname = url.pathname\n const method = req.method ?? 'GET'\n\n // Peak-pause routes are method-specific: GET reads, PUT flips the master\n // switch or one conversation, and POST /breaker/release lets parked work\n // run now.\n if (pathname === BREAKER_API.state) {\n if (method === 'GET') {\n writeJson(res, 200, { ok: true, data: projection() }, NO_STORE)\n return\n }\n if (method === 'PUT') {\n await writeSettings(req, res)\n return\n }\n writeJson(res, 405, { ok: false, error: 'method not allowed' }, { ...NO_STORE, allow: 'GET, PUT' })\n return\n }\n if (pathname === BREAKER_API.release) {\n if (method === 'POST') {\n await releaseHeld(req, res)\n return\n }\n writeJson(res, 405, { ok: false, error: 'method not allowed' }, { ...NO_STORE, allow: 'POST' })\n return\n }\n\n const knownReadPath = pathname === '/api/dsh-usage-dashboard/balance'\n || pathname === '/api/dsh-usage-dashboard/usage'\n || pathname === '/api/dsh-usage-dashboard/session'\n if (!knownReadPath) {\n // Unknown routes keep their existing 404 answer regardless of method;\n // only a known route reports a wrong method.\n writeJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n if (method !== 'GET') {\n writeJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n const force = url.searchParams.get('refresh') === '1'\n if (pathname === '/api/dsh-usage-dashboard/balance') {\n const payload = await (force ? balanceMemo.refresh() : balanceMemo.get())\n const headers = payload.ok\n ? { 'cache-control': `private, max-age=${Math.round(BALANCE_TTL_MS / 1000)}` }\n : { 'cache-control': 'no-store' }\n writeJson(res, 200, payload, headers)\n } else if (pathname === '/api/dsh-usage-dashboard/usage') {\n const payload = await (force ? usageMemo.refresh() : usageMemo.get())\n const headers = payload.ok\n ? { 'cache-control': `private, max-age=${Math.round(USAGE_TTL_MS / 1000)}` }\n : { 'cache-control': 'no-store' }\n writeJson(res, 200, payload, headers)\n } else if (pathname === '/api/dsh-usage-dashboard/session') {\n const id = url.searchParams.get('id')\n // `id` arrives verbatim from the browser-controlled `?id=` query\n // parameter (unlike `usage`'s session ids, which the host itself\n // enumerates via `persistence.list()`), so a malformed value is\n // rejected here \u2014 before it ever reaches the persistence read path \u2014\n // rather than trusted to fetchSessionUsage's own defense-in-depth\n // check. Reject as early as the route layer allows.\n if (id === null || id.trim() === '' || !isValidSessionId(id)) {\n // A missing/blank/malformed `id` is the caller's mistake (bad\n // input), so it gets a real 4xx; any other failure (e.g. the log\n // itself unreadable) keeps the same \"200 with ok:false\" shape as\n // balance/usage so the client's one error-handling path covers all\n // three endpoints.\n const error = id === null || id.trim() === '' ? '\u7F3A\u5C11\u4F1A\u8BDD id' : '\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5'\n writeJson(res, 400, { ok: false, error }, { 'cache-control': 'no-store' })\n return\n }\n const payload = await fetchSessionUsage(ctx.sessionPersistence, id)\n // id is already known-well-formed here, so any remaining failure is\n // downstream (e.g. the log itself unreadable) and keeps the same\n // \"200 with ok:false\" shape as balance/usage.\n writeJson(res, 200, payload, { 'cache-control': 'no-store' })\n }\n },\n }), 'dsh-usage-dashboard: /api routes')\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["name", "BEIJING_OFFSET_MS", "events", "daily", "hourly", "errorText"]
7
7
  }