@rayadesu/dsh-client-ui-billing 0.3.10 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +7 -3
- package/README.zh.md +7 -3
- package/lib/client.js +106 -16
- package/lib/types/client/BalancePanel.d.ts +7 -2
- package/lib/types/client/SpendCard.d.ts +41 -0
- package/lib/types/client/format.d.ts +37 -3
- package/lib/types/client/icons.d.ts +20 -0
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/locales.d.ts +12 -4
- package/lib/types/client/spendBuckets.d.ts +38 -0
- package/lib/types/client/useCardDialog.d.ts +34 -0
- package/lib/types/client/version.d.ts +16 -0
- package/package.json +4 -3
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# last confirmed-consistent state. Both languages carry equal authority; after
|
|
3
3
|
# editing either side, bring the other along and re-record both hashes with:
|
|
4
4
|
# git hash-object README.md README.zh.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: a866a9fc6e47278d35a8acf3a797a6230b8314e2
|
|
6
|
+
README.zh.md: 3a6993fc7690572fb3d30f084882c6f2e72d207d
|
package/README.md
CHANGED
|
@@ -4,18 +4,20 @@ English | [中文](README.zh.md)
|
|
|
4
4
|
|
|
5
5
|
Web billing feature owner: contributes one entry to `conversation.session.header.utilities` that mounts its own `billing` Remote and renders the DeepSeek account balance, this session's billed spend (read from the host-pushed `billingTodaySpend` projection, with `billing/getSessionSpend` as the fallback when the projection registry is absent), today's spend across every session (`billing/getTodaySpend`), today's per-session ranking (`billing/getTodaySessionsSpend`), and per-turn costs (`billing/getSessionTurnSpends`) as a top-right label box. The host half of the capability lives in [`dsh-llm-billing`](../llm-billing/README.md), which owns the `/user/balance` transport, the peak/off-peak pricing table, and the `billing` Remote namespace; this package mounts that Remote and renders what it returns.
|
|
6
6
|
|
|
7
|
-
The trigger shows two lines — the remaining balance and this conversation's billed spend ("本轮对话花费 ¥X") — and renders immediately from the last settled balance (kept per plugin instance), revalidating in the background; before the first balance ever lands it renders nothing. Clicking it opens a label box with the remaining amount, this session's billed spend (本会话花费, priced per usage sample at the official peak/off-peak rate — peak windows apply weekdays Mon–Fri only, weekends are off-peak; a forked session prices only its own post-fork work)
|
|
7
|
+
The trigger shows two lines — the remaining balance and this conversation's billed spend ("本轮对话花费 ¥X") — and renders immediately from the last settled balance (kept per plugin instance), revalidating in the background; before the first balance ever lands it renders nothing. Clicking it opens a label box with the remaining amount, then today's billed token count with today's all-session spend beside it (今日 Token / 今日花费, one row; the count sums the three billed buckets of every priced row, so a cache-hit input token and an output token each count once, and it renders in DSH's own compact notation with DSH's ` tok` unit — `517 tok`, `12.2K tok`, `1.2M tok`: below 1e3 the plain integer, below 1e6 the shared `K` unit, otherwise `M`, keeping one decimal under 100 and rounding from 100 up), then this session's billed spend on its own line under them (本会话花费, priced per usage sample at the official peak/off-peak rate — peak windows apply weekdays Mon–Fri only, weekends are off-peak; a forked session prices only its own post-fork work); a bare parenthesized amount rides right after the session amount ((¥X)) — that is **this session's share of today**, taken from the already-fetched today-session ranking and matched by the current session id, so it is neither the ranking's first row nor the day's total. It renders **only when the two amounts disagree**, which is exactly the day-crossing case; an unsettled ranking renders nothing (unknown is not a disagreement, and a session that has not crossed a Beijing day bills exactly its own total today, so the share would just repeat the amount beside it); next come one priced row per model with the cost breakdown under DSH's own bucket wording and row order ("未缓存输入 ¥X · 缓存读取 ¥Y · 输出 ¥Z" — DSH's `message.turnUsage` labels; the host prices cache writes at the miss rate, so they ride the uncached-input bucket), a manual refresh action, and a short spend disclaimer on the `?` button (only DeepSeek and MiMo models are estimated, each message priced at its own peak/off-peak rate; the next line names the parenthesized amount on the session row — this session's spend today — and the last line is a bare version like `v0.3.12`, stamped in at build time from this package's manifest, so a report can name the build; the full rate schedule lives in the host README, because the DSH tooltip bubble fits a short label only, is clipped at the viewport edge when long, and cannot be hovered — the hint is placed below the button, the side whose viewport fit can flip). The remaining-amount row (`API 剩余金额`) renders larger than the spend rows below it (今日 Token and 今日花费, and 本会话花费 with its parenthesized today share). Below the model rows the panel ends with the **today session-spend ranking** (`今日会话花费`): sessions sorted by today's cost, highest first, each row a fixed right-aligned rank column, then the same middot separator the cost breakdown uses ("·" with the same size, tone, and spacing), then the session's durable title (renames sync automatically), then its amount — the title keeps its indented rank column; at most `SESSION_RANKING_LIMIT` (10) rows render, with a "…还有 N 个会话" hint for the rest. Refreshing keeps the last values visible rather than blanking them, and a refresh failure retains the last good value. A session or day without priced usage shows a "no usage recorded" word rather than a fabricated figure. A failure — no API key configured, a rejected credential, a transport error — renders a muted "Balance unavailable" word whose tooltip carries the Remote's own error message.
|
|
8
8
|
|
|
9
9
|
The package also contributes the **turn-cost entry** (`billing-turn-cost`, order 20) to ui-chat's `conversation.chat.assistant-actions` list slot — the same strip `ui-message-feedback` uses, so entries coexist; the entry's own CSS `order: 1` sorts it after every order-0 sibling, so the amount lands at the **end** of the actions row (after the clock). The entry renders a plain static `¥X` (no icon, no label, no interaction) for each completed turn's closing assistant message, resolved from a shared per-session `messageId → cost` map (`billing/getSessionTurnSpends`, one fetch per session, coalesced across concurrently mounting rows; a message id the map does not know triggers exactly one refetch for a newly completed turn). Turns that priced to zero (no DeepSeek usage) and failed loads render nothing, so a Remote outage never clutters the row.
|
|
10
10
|
|
|
11
|
+
A third entry, the **composer spend card**, is **built but not registered** — its source (`SpendCard.tsx`, `spendBuckets.ts`, `useCardDialog.ts`, `icons.tsx`), its locale keys, and its test suite all live here, and the registration block is parked as a comment in `src/client/index.ts`. It targeted ui-conversation's `conversation.composer.dock` list slot (the row under the input box that ui-chat's own token and time pills occupy), which cannot host it: a dock entry is a list row of its own and the composer stacks those rows in a column, so the pill could only ever render as a second centred row *below* the built-in one, never beside it. Re-enabling it waits on the host offering a real seat — a child slot inside the stats row, or row grouping on the dock's list spec — at which point the parked block is uncommented as-is (plus its one import). As built, the entry was one money-bag pill (the in-house hollow-outline glyph, 14px, tertiary tone, tabular `¥X`) showing the conversation's billed spend, opening a cost card that reproduces ui-chat's token-usage dialog skin token for token (menu surface, 12px radius, elevation-prominent, 16px padding, heading row with its headline amount and a 0.5px rule, right-aligned tabular detail grid): the heading is `花费金额` / `Spend`, and its three rows are the spend's three billing buckets under the wording that token card uses — **未缓存输入 / 缓存读取 / 输出**. The amounts came from the pushed `billingTodaySpend` projection (no Remote call of its own; `spendBuckets.ts` sums the per-model rows and absorbs the four-decimal float residual into the largest bucket, so the three rows always add up to the total the heading shows) with `billing/getSessionSpend` as the projection-less fallback, and DSH keeps its own equivalent dialog seat in a private client bundle, so dismissal and placement were rebuilt from the two public primitives (`useAnchoredPosition` plus `useDismissOnOutsidePointer`).
|
|
12
|
+
|
|
11
13
|
The badge is account-level even though the slot is session-scoped: the header utilities row is simply the one always-visible title-bar seat the shell offers, and the balance it shows does not vary by session — but the spend lines do, because they price the current session's own usage (the pushed projection, or `billing/getSessionSpend`) and every session's usage through `billing/getTodaySpend`. Copy goes through the package's own `billing` locale namespace; styling uses tokens only.
|
|
12
14
|
|
|
13
15
|
## Refresh mechanics
|
|
14
16
|
|
|
15
17
|
The spends follow the conversation and the balance is cached, not polled:
|
|
16
18
|
|
|
17
|
-
- **Session spend is pushed, not fetched** — the host folds each session's spend into the client-visible `billingTodaySpend` projection and pushes every change; the badge subscribes with the framework `useProjection` seat, so the session line is live with zero Remote calls while the projection registry is composed. When the key is absent, the same line falls back to `billing/getSessionSpend
|
|
18
|
-
- **Today's spend follows settled turns, debounced** — the component subscribes to the current session's running flag through the framework `useSession` seat. When a prompt turn settles (the flag flips back to idle) it recomputes today's spend (`billing/getTodaySpend
|
|
19
|
+
- **Session spend is pushed, not fetched** — the host folds each session's spend into the client-visible `billingTodaySpend` projection and pushes every change; the badge subscribes with the framework `useProjection` seat, so the session line is live with zero Remote calls while the projection registry is composed. When the key is absent, the same line falls back to `billing/getSessionSpend` — the same face and ladder the unregistered composer spend card reads.
|
|
20
|
+
- **Today's spend follows settled turns, debounced** — the component subscribes to the current session's running flag through the framework `useSession` seat. When a prompt turn settles (the flag flips back to idle) it recomputes today's spend (`billing/getTodaySpend`; the 今日 Token figure on that same row is summed from the returned model rows in place, with no extra request); the recompute is debounced for two seconds, so a burst of turns (an agent continuing across turns) prices once instead of once per turn. The ranking (`billing/getTodaySessionsSpend`) is fetched only while the detail panel is open (and on refresh), so a badge that stays closed never pays for the all-session scan; the parenthesized today share on the 本会话花费 row reads that same fetch, so it costs no extra request and moves with the ranking (and until that ranking settles no share is rendered at all, because only a disagreement between the two amounts shows one).
|
|
19
21
|
- **Balance is TTL-cached** — the account balance is fetched on mount, on session switch, and on the explicit refresh action. The host reuses one snapshot for up to 15 seconds (`force` on the manual refresh bypasses it) and aborts a request after 5 seconds; the browser keeps the last settled value so a session switch renders the amount immediately. There is still no polling and no automatic refetch.
|
|
20
22
|
- **Refresh keeps the last values** — an in-flight refresh leaves the previous values on screen, and a failed refresh retains the last good value instead of blanking it. The refresh action calls `billing/getBalance(true)`, `billing/getTodaySpend(true)`, and `billing/getTodaySessionsSpend(true)`, which bypass the host-side caches; the turn-triggered recompute and the mount/session-switch reads use the cached path.
|
|
21
23
|
|
|
@@ -32,4 +34,6 @@ None; the package never assembles or sends provider requests, and its one RPC is
|
|
|
32
34
|
- **First balance line only** — the balance reads the primary (`balance_infos[0]`) currency line; other currency lines are not shown.
|
|
33
35
|
- **Ranking capped at 10** — the panel shows at most `SESSION_RANKING_LIMIT` sessions, with a "…N more sessions" hint.
|
|
34
36
|
- **Turn cost needs a finalized closing message** — interrupted turns have no actions row, so no turn cost is shown for them.
|
|
37
|
+
- **Today's tokens count priced rows only** — the figure sums the three billing buckets of the model rows `billing/getTodaySpend` returns (models with a rate row), so a model without one contributes no tokens; cache writes ride the cache-miss bucket.
|
|
38
|
+
- **The composer spend card has no seat yet** — its registration is parked (see above) until the host exposes a child slot in the composer stats row or row grouping on the dock's list spec; until then the input box shows only the built-in token/time pills. It prices what the host's table prices (DeepSeek and MiMo) while that token row counts every token, and cache writes ride its uncached-input row.
|
|
35
39
|
- **Balance is up to 15s stale** — the host reuses one snapshot for 15 seconds and the browser keeps the last settled value: spending from another client does not move the shown value until the TTL expires, a manual refresh, or a browser reload (there is no polling).
|
package/README.zh.md
CHANGED
|
@@ -4,18 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡献一个条目,自己挂载 `billing` Remote,并渲染 DeepSeek 账户余额、本会话的计费花费(读宿主推送的 `billingTodaySpend` 投影,投影注册表不存在时回退 `billing/getSessionSpend`)、今日所有会话的计费花费(`billing/getTodaySpend`)、今日按会话的排行(`billing/getTodaySessionsSpend`)与各回合花费(`billing/getSessionTurnSpends`),在右上角渲染成一个标签框。该能力的宿主侧在 [`dsh-llm-billing`](../llm-billing/README.md),它拥有 `/user/balance` 传输、峰谷计价表与 `billing` Remote 命名空间;本包挂载那个 Remote 并渲染它返回的内容。
|
|
6
6
|
|
|
7
|
-
触发器显示两行——剩余额度与本轮对话的计费花费(「本轮对话花费 ¥X
|
|
7
|
+
触发器显示两行——剩余额度与本轮对话的计费花费(「本轮对话花费 ¥X」)——有上次已落定的余额就立即渲染(按插件实例保留),后台校验;首次余额尚未到达时不渲染任何东西。点击后展开一个标签框:剩余金额、今日计费 token 数与今日所有会话合计(「今日 Token」与「今日花费」同一行——token 数把每个计价行的三个计费桶相加,缓存读取与输出各算一个 token,并按 DSH 自己的紧凑记数法加 ` tok` 单位渲染:`517 tok`、`12.2K tok`、`1.2M tok`——<1e3 原整数、<1e6 用共享 `K` 单位、再往上用 `M`,缩放值 <100 保留一位小数、≥100 取整)、再下一行是本会话的计费花费(「本会话花费」,按官方峰/谷单价逐条用量样本计价——高峰窗口仅周一至周五适用,周末按低谷价——分叉会话只按分叉后的自有工作计价;金额后可能紧跟一个括号括起来的纯金额(`(¥X)`),那是**本会话今日份**的花费,取自已拉取的今日会话排行、按当前会话 id 匹配,所以既不是排行首行、也不是当日合计——**只在两个金额对不上时才渲染**,也就是会话跨了北京时间自然日的情况;排行未落定则不渲染任何括号,因为「未知」不算对不上,而没跨天的会话今日份恰好等于会话总额、括号只会重复旁边的数字)、每个模型一行的花费及其分项(用 DSH 自己的桶名与行序:「未缓存输入 ¥X · 缓存读取 ¥Y · 输出 ¥Z」,即 ui-chat `message.turnUsage` 那套文案;宿主把缓存写入按未命中单价计价,所以它并入「未缓存输入」桶)、手动刷新按钮,以及「?」按钮上的一句简短花费说明(只估算 DeepSeek 与 MiMo 模型、按每条消息自身时刻的峰谷单价计价;下一行说明会话行括号里的金额就是本会话今日花费,最后一行顶格是纯版本号(如 `v0.3.12`)——构建期从本包 manifest 注入,便于反馈时指明所用版本;费率明细在宿主 README —— DSH 的悬停气泡只装得下短文案,过长会在视口边缘被裁掉且无法悬停阅读,所以说明放在按钮下方,纵向适配可翻转的那一侧)。剩余金额行(`API 剩余金额`)字号大于下方花费行(`今日 Token`/`今日花费`,以及带括号金额的 `本会话花费`)。模型行下方是**今日会话花费排行**(`今日会话花费`):按今日花费从高到低排列的会话列表,每行以右对齐的名次列开头,后接与花费分项相同的小圆点分隔符(「·」,字号、色调与间距一致),再是会话的持久标题(重命名自动同步),最后是金额——标题在名次列后缩进对齐;最多渲染 `SESSION_RANKING_LIMIT`(10)行,其余以「…还有 N 个会话」提示。刷新时旧值不消失,刷新失败保留上一次有效值。没有可计价消耗的会话或日子显示「暂无消耗记录」而不是编造数字。失败——未配置 API key、凭据被拒绝、传输错误——渲染弱化的「额度不可用」,其提示携带 Remote 自己的错误信息。
|
|
8
8
|
|
|
9
9
|
本包还贡献**本轮花费条目**(`billing-turn-cost`,order 20)到 ui-chat 的 `conversation.chat.assistant-actions` list slot——与 `ui-message-feedback` 同一条操作条,多条目共存;条目自身的 CSS `order: 1` 把它排到所有 order-0 兄弟之后,金额因此落在操作行**行尾**(时钟之后)。它为每个已完成回合的收尾助手消息渲染一个纯静态 `¥X`(无图标、无文字标签、不可点击),金额来自每会话共享的 `messageId → 金额` 映射(`billing/getSessionTurnSpends`,每会话只拉一次,并发挂载的行共享同一请求;映射里没有的消息 id 只触发一次重拉,对应当前刚完成的回合)。回合没有 DeepSeek 用量(花费为 0)或加载失败时不渲染,Remote 故障不会污染操作行。
|
|
10
10
|
|
|
11
|
+
第三个条目是**输入框花费卡片**,**已实现但未注册**——源码(`SpendCard.tsx`、`spendBuckets.ts`、`useCardDialog.ts`、`icons.tsx`)、locale 键与全套用例都在本包,注册块以注释形式停在 `src/client/index.ts` 里。它原本挂在 ui-conversation 的 `conversation.composer.dock` list slot(ui-chat 自己的 token/时间 pill 所在的输入框下方行),但那个槽放不下它:dock 条目各自独占一行、由输入框纵向堆叠,所以 pill 只能渲染成官方行**下面**第二条居中行,永远无法与官方 pill 并排。重新启用要等宿主给出真正的席位——统计行内部的子插槽,或 dock list 规格上的同行分组——届时把那段注释块原样放开即可(外加它那一行 import)。按已实现形态:这一行是一枚钱袋 pill(自制描边图标,14px、三级色调、等宽 `¥X`),显示本会话的计费花费;点击向上弹出逐 token 复刻 ui-chat token 用量对话框皮肤的花费卡片(菜单底、12px 圆角、elevation-prominent、16px 内边距、标题行带主金额与 0.5px 分隔线、右对齐等宽数字网格)——标题是「花费金额」,三行就是花费的三个计费桶,用的正是 token 卡那套文案:**未缓存输入 / 缓存读取 / 输出**。金额来自推送的 `billingTodaySpend` 投影(卡片自己不发 Remote 调用;`spendBuckets.ts` 把每模型行相加,并把四位小数的浮点残差并入最大桶,保证三行之和恒等于标题里的总额),投影缺失时回退 `billing/getSessionSpend`;而 DSH 自带的同款 dialog 席位在其私有客户端 bundle 里、无法导入,所以定位与关闭由两个公开原语重建(`useAnchoredPosition` 与 `useDismissOnOutsidePointer`)。
|
|
12
|
+
|
|
11
13
|
角标是账户级的,尽管插槽是会话作用域:顶栏工具行不过是外壳提供的那个始终可见的标题栏席位,余额并不随会话变化——但花费行会,因为它按当前会话自己的用量(推送的投影,或 `billing/getSessionSpend`)计价、通过 `billing/getTodaySpend` 按所有会话的用量计价。文案走本包自己的 `billing` locale 命名空间;样式只用 token。
|
|
12
14
|
|
|
13
15
|
## 数据更新机制
|
|
14
16
|
|
|
15
17
|
花费跟随会话、额度带缓存但不轮询:
|
|
16
18
|
|
|
17
|
-
- **本会话花费靠推送,不靠拉取** —— 宿主把每个会话的花费折进客户端可见的 `billingTodaySpend` 投影并推送每次变化;徽标用框架 `useProjection` 座位订阅,因此投影注册表装配时该行零 Remote 调用、实时更新。键不存在时同一行回退到 `billing/getSessionSpend
|
|
18
|
-
-
|
|
19
|
+
- **本会话花费靠推送,不靠拉取** —— 宿主把每个会话的花费折进客户端可见的 `billingTodaySpend` 投影并推送每次变化;徽标用框架 `useProjection` 座位订阅,因此投影注册表装配时该行零 Remote 调用、实时更新。键不存在时同一行回退到 `billing/getSessionSpend` —— 与未注册的输入框花费卡片读的是同一个面、同一条回退。
|
|
20
|
+
- **今日花费随回合结束更新(去抖)** —— 组件通过框架 `useSession` 座位订阅当前会话的「运行中」标志。每当一轮 prompt 回合结束(标志翻回空闲)就重算**今日花费**(`billing/getTodaySpend`;同一行的**今日 Token** 由这次读取的模型行原地求和,不发额外请求);重算有 2 秒去抖,回合突发(agent 连续多轮)只计价一次。**排行**(`billing/getTodaySessionsSpend`)只在详情面板打开时(以及手动刷新时)拉取,徽标一直关着就不会为全量会话扫描买单;「本会话花费」行括号里的今日份金额也来自这次排行读取,所以它不额外发请求,代价是与排行同进同退(排行未落定时不渲染括号——两个金额对不上才显示,而「未知」不算对不上)。
|
|
19
21
|
- **额度带 TTL 缓存** —— 账户余额在挂载、切换会话、点刷新按钮时拉取。宿主 15 秒内复用同一份快照(手动刷新带 `force` 绕过),单次请求 5 秒超时;浏览器保留最后一次已落定值,切会话时立即渲染。仍然**没有轮询、不会自动重拉**。
|
|
20
22
|
- **刷新保留旧值** —— 刷新在途时旧值留在屏幕上,刷新失败保留上一次有效值,不清空。刷新按钮调用 `billing/getBalance(true)`、`billing/getTodaySpend(true)` 与 `billing/getTodaySessionsSpend(true)`,绕过宿主侧缓存;回合结束触发的重算与挂载/切换会话的读取走缓存路径。
|
|
21
23
|
|
|
@@ -32,4 +34,6 @@ Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡
|
|
|
32
34
|
- **只取首条余额线** —— 余额读取主要(`balance_infos[0]`)币种行;其它币种行不显示。
|
|
33
35
|
- **排行上限 10** —— 面板最多显示 `SESSION_RANKING_LIMIT` 个会话,其余以「…还有 N 个会话」提示。
|
|
34
36
|
- **本轮花费需要已定稿的收尾消息** —— 中断的回合没有操作行,不显示本轮花费。
|
|
37
|
+
- **今日 Token 只数计价行** —— 它统计的是 `billing/getTodaySpend` 返回的那些模型行(即价目表里有费率行的模型)的三个计费桶之和;没有费率行的模型不计 token,缓存写入并入「未缓存输入」桶(与分项同一口径)。
|
|
38
|
+
- **输入框花费卡片暂无席位** —— 注册已暂停(见上),等宿主在输入框统计行里开放子插槽、或在 dock list 规格上加同行分组;在那之前输入框下方只有官方自带的 token/时间 pill。它只计价宿主费率表覆盖的模型(DeepSeek 与 MiMo),而上面那条 token 行统计的是会话全部 token,缓存写入并入它的「未缓存输入」行。
|
|
35
39
|
- **额度最多旧 15 秒** —— 宿主 15 秒内复用同一份快照、浏览器保留最后一次已落定值:其他客户端产生的消耗不会让界面数值自动变化,需等 TTL 过期、手动刷新或刷新浏览器(仍无轮询)。
|
package/lib/client.js
CHANGED
|
@@ -4364,9 +4364,58 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4364
4364
|
function formatSpend(amount) {
|
|
4365
4365
|
return `¥${amount.toFixed(4).replace(/\.?0+$/, "")}`;
|
|
4366
4366
|
}
|
|
4367
|
+
/**
|
|
4368
|
+
* A token count in DSH's own compact notation: `517` / `12.2K` / `517K` / `1.2M`.
|
|
4369
|
+
*
|
|
4370
|
+
* Mirrors ui-chat's `formatTokens` (`packages/client/ui-chat/src/client/chat/
|
|
4371
|
+
* token-format.ts`) rule for rule, so one shell never renders the same count two
|
|
4372
|
+
* ways. That rule reads: below 1e3 the plain integer (no digit grouping); below
|
|
4373
|
+
* 1e6 the shared `number.thousand` unit (`{value}K`); otherwise the shared
|
|
4374
|
+
* `number.million` unit (`{value}M`). A scaled value keeps one decimal while it
|
|
4375
|
+
* is below 100 and rounds to a whole number from 100 up (`12.2K`, but `517K`),
|
|
4376
|
+
* and there is no third unit, so 1.2e9 renders as `1200M` — exactly as DSH
|
|
4377
|
+
* renders it. The unit letters are identical in both DSH dictionaries, so they
|
|
4378
|
+
* are literals here rather than a locale lookup into the shared vocabulary.
|
|
4379
|
+
* @param count - the token count to render.
|
|
4380
|
+
* @returns the compact count, without the label's own ` tok` suffix.
|
|
4381
|
+
*/
|
|
4382
|
+
function formatTokens(count) {
|
|
4383
|
+
const scaled = (value) => value >= 100 ? String(Math.round(value)) : String(Math.round(value * 10) / 10);
|
|
4384
|
+
if (count < 1e3) return String(count);
|
|
4385
|
+
if (count < 1e6) return `${scaled(count / 1e3)}K`;
|
|
4386
|
+
return `${scaled(count / 1e6)}M`;
|
|
4387
|
+
}
|
|
4388
|
+
/**
|
|
4389
|
+
* The total billed tokens of a priced row: the three buckets summed. Tokens are
|
|
4390
|
+
* already counted per bucket when they were priced, so a cache-hit input token
|
|
4391
|
+
* and an output token are both one token here — the number answers "how many
|
|
4392
|
+
* tokens did this cost", not "how many characters were sent".
|
|
4393
|
+
* @param row - one priced model row.
|
|
4394
|
+
* @returns the row's total token count.
|
|
4395
|
+
*/
|
|
4396
|
+
function rowTokens(row) {
|
|
4397
|
+
return row.cacheHitInputTokens + row.cacheMissInputTokens + row.outputTokens;
|
|
4398
|
+
}
|
|
4399
|
+
//#endregion
|
|
4400
|
+
//#region lib/types/client/version.js
|
|
4401
|
+
/**
|
|
4402
|
+
* The plugin's own version, reported in the spend-hint bubble so a report can
|
|
4403
|
+
* name the build actually in use.
|
|
4404
|
+
*
|
|
4405
|
+
* The value is stamped in at build time rather than read at runtime: a browser
|
|
4406
|
+
* bundle has no manifest to read. `packages/tsdown.client.ts` defines
|
|
4407
|
+
* `__DSH_PLUGIN_VERSION__` from this package's own `package.json` (the three
|
|
4408
|
+
* published packages share one version), and `vitest.config.ts` defines the
|
|
4409
|
+
* same constant from the same manifest so the suites see what a release ships.
|
|
4410
|
+
* The `typeof` guard keeps a source run without either definition (a foreign
|
|
4411
|
+
* bundler, a bare `tsc` check) from throwing on the undeclared global.
|
|
4412
|
+
* @module @rayadesu/dsh-client-ui-billing/version
|
|
4413
|
+
*/
|
|
4414
|
+
/** The installed plugin version, e.g. `0.3.10`; `dev` when nothing stamped one. */
|
|
4415
|
+
const PLUGIN_VERSION = "0.3.12";
|
|
4367
4416
|
//#endregion
|
|
4368
4417
|
//#region \0dsh-css:C:\Users\admin\Desktop\me\code\DeepSeek-Harness-chat-billing\packages\ui-billing\src\client\BalanceBadge.module.css.mjs
|
|
4369
|
-
const css$1 = "._4_Wqcq_root{position:relative}._4_Wqcq_trigger{background:var(--dsw-alias-fill-l2);min-height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;border:0;border-radius:8px;align-items:center;gap:4px;padding:4px 8px;display:inline-flex}._4_Wqcq_trigger:hover,._4_Wqcq_trigger:focus-visible{color:var(--dsw-alias-label-primary)}._4_Wqcq_trigger svg{transition:transform .12s}._4_Wqcq_chevronOpen{transform:rotate(180deg)}._4_Wqcq_triggerLines{text-align:left;flex-direction:column;align-items:flex-start;gap:0;display:flex}._4_Wqcq_linePrimary{font-variant-numeric:tabular-nums;font-size:12px;line-height:16px}._4_Wqcq_lineSecondary{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px;line-height:15px}._4_Wqcq_unavailable{color:var(--dsw-alias-label-tertiary)}._4_Wqcq_inlineIcon{color:var(--dsw-alias-label-tertiary);flex:none}._4_Wqcq_spinning{color:var(--dsw-alias-label-tertiary);flex:none;animation:.8s linear infinite _4_Wqcq_spin}@keyframes _4_Wqcq_spin{to{transform:rotate(360deg)}}._4_Wqcq_panel{z-index:100;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-menu);width:336px;max-width:min(360px,100vw - 32px);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;flex-direction:column;gap:2px;padding:6px;display:flex;position:absolute;top:calc(100% + 6px);right:0}._4_Wqcq_amountRow{border-radius:8px;justify-content:space-between;align-items:center;gap:8px;padding:6px 8px;display:flex}._4_Wqcq_amountLabel{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:13px;line-height:18px}._4_Wqcq_amountRow>._4_Wqcq_amountLabel{font-size:15px;line-height:22px}._4_Wqcq_spendRow{border-top:1px solid var(--dsw-alias-border-l2);border-radius:0;justify-content:space-between;align-items:center;gap:8px;padding:6px 8px;display:flex}._4_Wqcq_costRow{align-items:center;padding:2px 8px 6px;display:flex}._4_Wqcq_costBreakdown{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px;line-height:16px}._4_Wqcq_amountActions{align-items:center;gap:2px;display:inline-flex}._4_Wqcq_infoButton,._4_Wqcq_refreshButton{width:24px;height:24px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}._4_Wqcq_infoButton:hover,._4_Wqcq_infoButton:focus-visible,._4_Wqcq_refreshButton:hover,._4_Wqcq_refreshButton:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2)}._4_Wqcq_modelRow{border-radius:8px;justify-content:space-between;align-items:baseline;gap:8px;padding:6px 8px;display:flex}._4_Wqcq_modelName{color:var(--dsw-alias-label-secondary);flex:none;font-size:12px;line-height:18px}._4_Wqcq_tasks{text-align:right;min-width:0;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;overflow-wrap:anywhere;font-size:12px;line-height:18px}._4_Wqcq_ranking{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:0;padding-top:2px;display:flex}._4_Wqcq_rankingTitle{color:var(--dsw-alias-label-secondary);padding:6px 8px 2px;font-size:12px;line-height:18px}._4_Wqcq_rankingRow{border-radius:8px;align-items:baseline;gap:0;min-width:0;padding:3px 8px;display:flex}._4_Wqcq_rankingRow:hover{background:var(--dsw-alias-fill-l2)}._4_Wqcq_rankingIndex{width:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;text-align:right;flex:none;font-size:11px;line-height:16px}._4_Wqcq_rankingDot{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:pre;flex:none;font-size:11px;line-height:16px}._4_Wqcq_rankingName{min-width:0;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:11px;line-height:16px;overflow:hidden}._4_Wqcq_rankingAmount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:8px;font-size:11px;line-height:16px}._4_Wqcq_rankingMore{color:var(--dsw-alias-label-tertiary);padding:2px 8px 4px;font-size:11px;line-height:16px}";
|
|
4418
|
+
const css$1 = "._4_Wqcq_root{position:relative}._4_Wqcq_trigger{background:var(--dsw-alias-fill-l2);min-height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;border:0;border-radius:8px;align-items:center;gap:4px;padding:4px 8px;display:inline-flex}._4_Wqcq_trigger:hover,._4_Wqcq_trigger:focus-visible{color:var(--dsw-alias-label-primary)}._4_Wqcq_trigger svg{transition:transform .12s}._4_Wqcq_chevronOpen{transform:rotate(180deg)}._4_Wqcq_triggerLines{text-align:left;flex-direction:column;align-items:flex-start;gap:0;display:flex}._4_Wqcq_linePrimary{font-variant-numeric:tabular-nums;font-size:12px;line-height:16px}._4_Wqcq_lineSecondary{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px;line-height:15px}._4_Wqcq_unavailable{color:var(--dsw-alias-label-tertiary)}._4_Wqcq_inlineIcon{color:var(--dsw-alias-label-tertiary);flex:none}._4_Wqcq_spinning{color:var(--dsw-alias-label-tertiary);flex:none;animation:.8s linear infinite _4_Wqcq_spin}@keyframes _4_Wqcq_spin{to{transform:rotate(360deg)}}._4_Wqcq_panel{z-index:100;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-menu);width:336px;max-width:min(360px,100vw - 32px);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;flex-direction:column;gap:2px;padding:6px;display:flex;position:absolute;top:calc(100% + 6px);right:0}._4_Wqcq_amountRow{border-radius:8px;justify-content:space-between;align-items:center;gap:8px;padding:6px 8px;display:flex}._4_Wqcq_amountLabel{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:13px;line-height:18px}._4_Wqcq_amountRow>._4_Wqcq_amountLabel{font-size:15px;line-height:22px}._4_Wqcq_spendToday{color:var(--dsw-alias-label-tertiary);font-size:12px}._4_Wqcq_spendRow{border-top:1px solid var(--dsw-alias-border-l2);border-radius:0;justify-content:space-between;align-items:center;gap:8px;padding:6px 8px;display:flex}._4_Wqcq_costRow{align-items:center;padding:2px 8px 6px;display:flex}._4_Wqcq_costBreakdown{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:11px;line-height:16px}._4_Wqcq_amountActions{align-items:center;gap:2px;display:inline-flex}._4_Wqcq_infoButton,._4_Wqcq_refreshButton{width:24px;height:24px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}._4_Wqcq_infoButton:hover,._4_Wqcq_infoButton:focus-visible,._4_Wqcq_refreshButton:hover,._4_Wqcq_refreshButton:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2)}._4_Wqcq_modelRow{border-radius:8px;justify-content:space-between;align-items:baseline;gap:8px;padding:6px 8px;display:flex}._4_Wqcq_modelName{color:var(--dsw-alias-label-secondary);flex:none;font-size:12px;line-height:18px}._4_Wqcq_tasks{text-align:right;min-width:0;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;overflow-wrap:anywhere;font-size:12px;line-height:18px}._4_Wqcq_ranking{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:0;padding-top:2px;display:flex}._4_Wqcq_rankingTitle{color:var(--dsw-alias-label-secondary);padding:6px 8px 2px;font-size:12px;line-height:18px}._4_Wqcq_rankingRow{border-radius:8px;align-items:baseline;gap:0;min-width:0;padding:3px 8px;display:flex}._4_Wqcq_rankingRow:hover{background:var(--dsw-alias-fill-l2)}._4_Wqcq_rankingIndex{width:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;text-align:right;flex:none;font-size:11px;line-height:16px}._4_Wqcq_rankingDot{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:pre;flex:none;font-size:11px;line-height:16px}._4_Wqcq_rankingName{min-width:0;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:11px;line-height:16px;overflow:hidden}._4_Wqcq_rankingAmount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:8px;font-size:11px;line-height:16px}._4_Wqcq_rankingMore{color:var(--dsw-alias-label-tertiary);padding:2px 8px 4px;font-size:11px;line-height:16px}";
|
|
4370
4419
|
const tagId$1 = "@rayadesu/dsh-client-ui-billing/BalanceBadge.module.css";
|
|
4371
4420
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
4372
4421
|
const tag = document.createElement("style");
|
|
@@ -4400,6 +4449,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4400
4449
|
"refreshButton": "_4_Wqcq_refreshButton",
|
|
4401
4450
|
"root": "_4_Wqcq_root",
|
|
4402
4451
|
"spendRow": "_4_Wqcq_spendRow",
|
|
4452
|
+
"spendToday": "_4_Wqcq_spendToday",
|
|
4403
4453
|
"spin": "_4_Wqcq_spin",
|
|
4404
4454
|
"spinning": "_4_Wqcq_spinning",
|
|
4405
4455
|
"tasks": "_4_Wqcq_tasks",
|
|
@@ -4407,8 +4457,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4407
4457
|
"triggerLines": "_4_Wqcq_triggerLines",
|
|
4408
4458
|
"unavailable": "_4_Wqcq_unavailable"
|
|
4409
4459
|
};
|
|
4460
|
+
/**
|
|
4461
|
+
* Tolerance for "the session total and its share of today are the same number".
|
|
4462
|
+
* Both are sums of the same priced samples, so a same-day session yields
|
|
4463
|
+
* bit-identical floats; the slack only absorbs the accumulation-order noise a
|
|
4464
|
+
* float sum can pick up on the way (a 1e-9 CNY difference is not a real one).
|
|
4465
|
+
*/
|
|
4466
|
+
const SPEND_SAME_EPSILON = 1e-9;
|
|
4410
4467
|
/** The detail box opened from the badge trigger. */
|
|
4411
|
-
function BalancePanel({ amount, spend, todaySpend, sessionsSpend, refreshing, onRefresh, t }) {
|
|
4468
|
+
function BalancePanel({ amount, sessionId, spend, todaySpend, sessionsSpend, refreshing, onRefresh, t }) {
|
|
4469
|
+
const sessionToday = sessionsSpend === null ? void 0 : sessionsSpend.sessions.find((row) => row.sessionId === sessionId)?.total ?? 0;
|
|
4470
|
+
const sessionTodayShare = spend !== null && sessionToday !== void 0 && Math.abs(spend.total - sessionToday) > SPEND_SAME_EPSILON ? sessionToday : null;
|
|
4471
|
+
const sessionAmount = spend === null ? "—" : spend.models.length === 0 ? t("stat.none") : formatSpend(spend.total);
|
|
4472
|
+
const todayTokens = todaySpend === null ? "—" : todaySpend.models.length === 0 ? t("stat.none") : t("unit.tokens", { count: formatTokens(todaySpend.models.reduce((sum, row) => sum + rowTokens(row), 0)) });
|
|
4473
|
+
const todayAmount = todaySpend === null ? "—" : todaySpend.models.length === 0 ? t("stat.none") : formatSpend(todaySpend.total);
|
|
4412
4474
|
return (0, react_jsx_runtime.jsxs)("div", {
|
|
4413
4475
|
className: BalanceBadge_module_css_default.panel,
|
|
4414
4476
|
role: "dialog",
|
|
@@ -4422,9 +4484,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4422
4484
|
}), (0, react_jsx_runtime.jsxs)("span", {
|
|
4423
4485
|
className: BalanceBadge_module_css_default.amountActions,
|
|
4424
4486
|
children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
4425
|
-
label: t("info.hint"),
|
|
4487
|
+
label: t("info.hint", { version: PLUGIN_VERSION }),
|
|
4488
|
+
side: "bottom",
|
|
4426
4489
|
delayMs: 200,
|
|
4427
|
-
maxWidth:
|
|
4490
|
+
maxWidth: 300,
|
|
4428
4491
|
children: (0, react_jsx_runtime.jsx)("button", {
|
|
4429
4492
|
type: "button",
|
|
4430
4493
|
className: BalanceBadge_module_css_default.infoButton,
|
|
@@ -4445,12 +4508,22 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4445
4508
|
className: BalanceBadge_module_css_default.spendRow,
|
|
4446
4509
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
4447
4510
|
className: BalanceBadge_module_css_default.amountLabel,
|
|
4448
|
-
children: t("label.
|
|
4511
|
+
children: t("label.todayTokens", { count: todayTokens })
|
|
4449
4512
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
4450
4513
|
className: BalanceBadge_module_css_default.amountLabel,
|
|
4451
|
-
children: t("label.todaySpend", { amount:
|
|
4514
|
+
children: t("label.todaySpend", { amount: todayAmount })
|
|
4452
4515
|
})]
|
|
4453
4516
|
}),
|
|
4517
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
4518
|
+
className: BalanceBadge_module_css_default.spendRow,
|
|
4519
|
+
children: (0, react_jsx_runtime.jsxs)("span", {
|
|
4520
|
+
className: BalanceBadge_module_css_default.amountLabel,
|
|
4521
|
+
children: [t("label.sessionSpend", { amount: sessionAmount }), sessionTodayShare !== null ? (0, react_jsx_runtime.jsx)("span", {
|
|
4522
|
+
className: BalanceBadge_module_css_default.spendToday,
|
|
4523
|
+
children: t("label.sessionSpend.today", { amount: formatSpend(sessionTodayShare) })
|
|
4524
|
+
}) : null]
|
|
4525
|
+
})
|
|
4526
|
+
}),
|
|
4454
4527
|
spend?.models.map((model) => (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
4455
4528
|
className: BalanceBadge_module_css_default.modelRow,
|
|
4456
4529
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
@@ -4465,10 +4538,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4465
4538
|
children: (0, react_jsx_runtime.jsxs)("span", {
|
|
4466
4539
|
className: BalanceBadge_module_css_default.costBreakdown,
|
|
4467
4540
|
children: [
|
|
4468
|
-
t("label.cost.hit", { amount: formatSpend(model.cacheHitInputCost) }),
|
|
4469
|
-
" · ",
|
|
4470
4541
|
t("label.cost.input", { amount: formatSpend(model.cacheMissInputCost) }),
|
|
4471
4542
|
" · ",
|
|
4543
|
+
t("label.cost.cacheRead", { amount: formatSpend(model.cacheHitInputCost) }),
|
|
4544
|
+
" · ",
|
|
4472
4545
|
t("label.cost.output", { amount: formatSpend(model.outputCost) })
|
|
4473
4546
|
]
|
|
4474
4547
|
})
|
|
@@ -4731,6 +4804,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4731
4804
|
t
|
|
4732
4805
|
}), open ? (0, react_jsx_runtime.jsx)(BalancePanel, {
|
|
4733
4806
|
amount,
|
|
4807
|
+
sessionId,
|
|
4734
4808
|
spend,
|
|
4735
4809
|
todaySpend,
|
|
4736
4810
|
sessionsSpend,
|
|
@@ -4859,16 +4933,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4859
4933
|
"trigger.conversationSpend": "本轮对话花费:{amount}",
|
|
4860
4934
|
"label.amount": "API 剩余金额:{amount}",
|
|
4861
4935
|
"label.sessionSpend": "本会话花费:{amount}",
|
|
4862
|
-
"label.
|
|
4863
|
-
"label.
|
|
4864
|
-
"label.
|
|
4936
|
+
"label.sessionSpend.today": "({amount})",
|
|
4937
|
+
"label.todaySpend": "今日花费:{amount}",
|
|
4938
|
+
"label.todayTokens": "今日 Token:{count}",
|
|
4939
|
+
"unit.tokens": "{count} tok",
|
|
4940
|
+
"label.cost.input": "未缓存输入 {amount}",
|
|
4941
|
+
"label.cost.cacheRead": "缓存读取 {amount}",
|
|
4865
4942
|
"label.cost.output": "输出 {amount}",
|
|
4943
|
+
"card.title": "花费金额",
|
|
4944
|
+
"card.input": "未缓存输入",
|
|
4945
|
+
"card.cacheRead": "缓存读取",
|
|
4946
|
+
"card.output": "输出",
|
|
4947
|
+
"card.aria": "本轮对话花费:{amount}",
|
|
4866
4948
|
"stat.none": "暂无消耗记录",
|
|
4867
4949
|
"stat.untitled": "未命名",
|
|
4868
4950
|
"state.unavailable": "额度不可用",
|
|
4869
4951
|
"action.refresh": "刷新",
|
|
4870
4952
|
"info.aria": "花费说明",
|
|
4871
|
-
"info.hint": "
|
|
4953
|
+
"info.hint": "估算:仅 DeepSeek 与 MiMo 模型,按每条消息自身时刻的峰谷官方单价计价(高峰:工作日 9:00–12:00、14:00–18:00)。\n括号内为本会话今日花费。\nv{version}",
|
|
4872
4954
|
"badge.aria": "DeepSeek 额度:{amount}",
|
|
4873
4955
|
"panel.aria": "DeepSeek 额度详情",
|
|
4874
4956
|
"label.sessionRanking": "今日会话花费",
|
|
@@ -4880,16 +4962,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4880
4962
|
"trigger.conversationSpend": "This conversation: {amount}",
|
|
4881
4963
|
"label.amount": "API balance: {amount}",
|
|
4882
4964
|
"label.sessionSpend": "This session: {amount}",
|
|
4883
|
-
"label.
|
|
4884
|
-
"label.
|
|
4885
|
-
"label.
|
|
4965
|
+
"label.sessionSpend.today": " ({amount})",
|
|
4966
|
+
"label.todaySpend": "Today spend: {amount}",
|
|
4967
|
+
"label.todayTokens": "Today tokens: {count}",
|
|
4968
|
+
"unit.tokens": "{count} tok",
|
|
4969
|
+
"label.cost.input": "Uncached input {amount}",
|
|
4970
|
+
"label.cost.cacheRead": "Cached input {amount}",
|
|
4886
4971
|
"label.cost.output": "Output {amount}",
|
|
4972
|
+
"card.title": "Spend",
|
|
4973
|
+
"card.input": "Uncached input",
|
|
4974
|
+
"card.cacheRead": "Cached input",
|
|
4975
|
+
"card.output": "Output",
|
|
4976
|
+
"card.aria": "This conversation’s spend: {amount}",
|
|
4887
4977
|
"stat.none": "No usage recorded",
|
|
4888
4978
|
"stat.untitled": "Untitled",
|
|
4889
4979
|
"state.unavailable": "Balance unavailable",
|
|
4890
4980
|
"action.refresh": "Refresh",
|
|
4891
4981
|
"info.aria": "About this spend",
|
|
4892
|
-
"info.hint": "
|
|
4982
|
+
"info.hint": "Estimate: DeepSeek and MiMo models only, each message priced at the official peak/off-peak rate of its own time (peak: weekdays 09:00–12:00, 14:00–18:00).\nThe parenthesized amount is this session's spend today.\nv{version}",
|
|
4893
4983
|
"badge.aria": "DeepSeek balance {amount}",
|
|
4894
4984
|
"panel.aria": "DeepSeek balance details",
|
|
4895
4985
|
"label.sessionRanking": "Today session spend",
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { DeepSeekSessionSpend, DeepSeekTodaySessionsSpend, DeepSeekTodaySpend } from '@rayadesu/dsh-llm-billing/types';
|
|
2
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
2
3
|
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
4
|
import { NS } from './locales.ts';
|
|
4
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* How many per-session ranking rows the panel shows before the overflow hint.
|
|
7
|
+
*/
|
|
5
8
|
export declare const SESSION_RANKING_LIMIT = 10;
|
|
6
9
|
/** Panel props: precomputed amount plus the same spend values the badge holds. */
|
|
7
10
|
export interface BalancePanelProps {
|
|
8
11
|
/** Primary balance line, e.g. `¥123.45`; `—` when the provider reports none. */
|
|
9
12
|
amount: string;
|
|
13
|
+
/** The session this panel belongs to: the row looked up in today's ranking. */
|
|
14
|
+
sessionId: SessionId;
|
|
10
15
|
spend: DeepSeekSessionSpend | null;
|
|
11
16
|
todaySpend: DeepSeekTodaySpend | null;
|
|
12
17
|
sessionsSpend: DeepSeekTodaySessionsSpend | null;
|
|
@@ -15,5 +20,5 @@ export interface BalancePanelProps {
|
|
|
15
20
|
t: PropsLocale<typeof NS>['t'];
|
|
16
21
|
}
|
|
17
22
|
/** The detail box opened from the badge trigger. */
|
|
18
|
-
export declare function BalancePanel({ amount, spend, todaySpend, sessionsSpend, refreshing, onRefresh, t }: BalancePanelProps): import("react").JSX.Element;
|
|
23
|
+
export declare function BalancePanel({ amount, sessionId, spend, todaySpend, sessionsSpend, refreshing, onRefresh, t }: BalancePanelProps): import("react").JSX.Element;
|
|
19
24
|
//# sourceMappingURL=BalancePanel.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer spend card: the money-bag pill under the input box that opens this
|
|
3
|
+
* conversation's billed-cost card. The card reproduces DSH's own token-usage
|
|
4
|
+
* dialog skin row for row (same surface, radius, elevation, title rule, and
|
|
5
|
+
* right-aligned tabular amounts) with the spend's three billing buckets where
|
|
6
|
+
* that card shows its token buckets: uncached input, cache read, output.
|
|
7
|
+
*
|
|
8
|
+
* The amounts ride the host-pushed `billingTodaySpend` projection, so the card
|
|
9
|
+
* is live with zero Remote calls; `billing/getSessionSpend` is the fallback for
|
|
10
|
+
* an assembly whose projection registry is absent (the same ladder the header
|
|
11
|
+
* badge walks in `useBillingData`).
|
|
12
|
+
* @module @rayadesu/dsh-client-ui-billing/SpendCard
|
|
13
|
+
*/
|
|
14
|
+
import type { DeepSeekSessionSpend } from '@rayadesu/dsh-llm-billing/types';
|
|
15
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
16
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
17
|
+
import { NS } from './locales.ts';
|
|
18
|
+
/** Injected face: this conversation's priced spend, for the projection-less fallback. */
|
|
19
|
+
export interface SpendCardInjected {
|
|
20
|
+
/** Read one session's billed spend across its own events (Remote fallback). */
|
|
21
|
+
getSessionSpend: (sessionId: SessionId) => Promise<DeepSeekSessionSpend>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Full props of the composer spend entry. The owner share arrives from
|
|
25
|
+
* ui-conversation's declared `conversation.composer.dock` list slot (it passes
|
|
26
|
+
* no owner values); the session runtime share adds the identity and the
|
|
27
|
+
* projection reader, as it does for every session-scoped slot.
|
|
28
|
+
*/
|
|
29
|
+
export type SpendCardProps = PropsRuntime<'conversation.composer.dock'> & InjectFace<SpendCardInjected> & PropsLocale<typeof NS>;
|
|
30
|
+
/**
|
|
31
|
+
* Render the composer spend pill, and the cost card it opens.
|
|
32
|
+
*
|
|
33
|
+
* The pill mirrors DSH's own composer stat pills (14px glyph, tertiary tone,
|
|
34
|
+
* tabular amount, hover pill) in its own row under the input box, because the
|
|
35
|
+
* composer dock is a list slot whose entries the composer stacks. A session
|
|
36
|
+
* that priced nothing renders no row at all.
|
|
37
|
+
* @param props - the session identity, the projection reader, the Remote fallback, and the locale seat.
|
|
38
|
+
* @returns the pill plus its portaled card, or null when there is nothing to show.
|
|
39
|
+
*/
|
|
40
|
+
export declare function SpendCard({ sessionId, useProjection, getSessionSpend, t }: SpendCardProps): import("react").JSX.Element | null;
|
|
41
|
+
//# sourceMappingURL=SpendCard.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared display formatting for the billing badges: the balance-line currency
|
|
3
|
-
* prefix
|
|
4
|
-
* per-turn cost label format through these, so the two
|
|
5
|
-
* apart.
|
|
3
|
+
* prefix, the CNY spend amount renderer, and the token-count renderer. Both the
|
|
4
|
+
* header badge and the per-turn cost label format through these, so the two
|
|
5
|
+
* entries cannot drift apart.
|
|
6
6
|
*/
|
|
7
7
|
import type { DeepSeekBalance } from '@rayadesu/dsh-llm-billing/types';
|
|
8
8
|
/** Currency prefix for one balance line; unknown codes render as a literal prefix. */
|
|
@@ -14,4 +14,38 @@ export declare function primaryLine(balance: DeepSeekBalance): {
|
|
|
14
14
|
} | undefined;
|
|
15
15
|
/** CNY amount, up to four decimals with trailing zeros trimmed. */
|
|
16
16
|
export declare function formatSpend(amount: number): string;
|
|
17
|
+
/**
|
|
18
|
+
* A token count in DSH's own compact notation: `517` / `12.2K` / `517K` / `1.2M`.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors ui-chat's `formatTokens` (`packages/client/ui-chat/src/client/chat/
|
|
21
|
+
* token-format.ts`) rule for rule, so one shell never renders the same count two
|
|
22
|
+
* ways. That rule reads: below 1e3 the plain integer (no digit grouping); below
|
|
23
|
+
* 1e6 the shared `number.thousand` unit (`{value}K`); otherwise the shared
|
|
24
|
+
* `number.million` unit (`{value}M`). A scaled value keeps one decimal while it
|
|
25
|
+
* is below 100 and rounds to a whole number from 100 up (`12.2K`, but `517K`),
|
|
26
|
+
* and there is no third unit, so 1.2e9 renders as `1200M` — exactly as DSH
|
|
27
|
+
* renders it. The unit letters are identical in both DSH dictionaries, so they
|
|
28
|
+
* are literals here rather than a locale lookup into the shared vocabulary.
|
|
29
|
+
* @param count - the token count to render.
|
|
30
|
+
* @returns the compact count, without the label's own ` tok` suffix.
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatTokens(count: number): string;
|
|
33
|
+
/** Every billed token bucket of one priced model row. */
|
|
34
|
+
export interface TokenBuckets {
|
|
35
|
+
/** Cache-hit input tokens. */
|
|
36
|
+
cacheHitInputTokens: number;
|
|
37
|
+
/** Cache-miss input tokens (uncached input plus cache writes). */
|
|
38
|
+
cacheMissInputTokens: number;
|
|
39
|
+
/** Output tokens (reasoning included). */
|
|
40
|
+
outputTokens: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The total billed tokens of a priced row: the three buckets summed. Tokens are
|
|
44
|
+
* already counted per bucket when they were priced, so a cache-hit input token
|
|
45
|
+
* and an output token are both one token here — the number answers "how many
|
|
46
|
+
* tokens did this cost", not "how many characters were sent".
|
|
47
|
+
* @param row - one priced model row.
|
|
48
|
+
* @returns the row's total token count.
|
|
49
|
+
*/
|
|
50
|
+
export declare function rowTokens(row: TokenBuckets): number;
|
|
17
51
|
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-house glyphs shared by this plugin's entries. Today's only member is the
|
|
3
|
+
* money bag the composer spend card and the per-turn cost label both lead with,
|
|
4
|
+
* so the two spend surfaces cannot drift apart iconographically.
|
|
5
|
+
* @module @rayadesu/dsh-client-ui-billing/icons
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* In-house money-bag glyph in the hollow-outline style (16px viewBox, stroke
|
|
9
|
+
* currentColor, fill none), referencing the 💰 emoji: a bag outline with a
|
|
10
|
+
* tie knot at the neck and a centered yuan mark on the body. The drawing
|
|
11
|
+
* fills the 16px box like the other glyphs; a host that renders it at 14px
|
|
12
|
+
* (the DSH glyph tier) keeps the same geometry and only scales down.
|
|
13
|
+
* @param props - optional size and class.
|
|
14
|
+
* @returns the money-bag icon.
|
|
15
|
+
*/
|
|
16
|
+
export declare function WalletIcon({ size, className }: {
|
|
17
|
+
size?: number;
|
|
18
|
+
className?: string;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=icons.d.ts.map
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
3
3
|
import { type BillingKey } from './locales.ts';
|
|
4
4
|
export type { BalanceBadgeInjected, BalanceBadgeProps } from './BalanceBadge.tsx';
|
|
5
|
+
export type { SpendCardInjected, SpendCardProps } from './SpendCard.tsx';
|
|
5
6
|
export type { TurnCostActionInjected, TurnCostActionProps } from './TurnCostAction.tsx';
|
|
6
7
|
export { createTurnCostStore, TURN_COST_STORE_LIMIT, type TurnCostStore } from './turnCostStore.ts';
|
|
7
8
|
export type { BillingKey } from './locales.ts';
|
|
@@ -7,16 +7,24 @@ export declare const zh: {
|
|
|
7
7
|
readonly 'trigger.conversationSpend': "本轮对话花费:{amount}";
|
|
8
8
|
readonly 'label.amount': "API 剩余金额:{amount}";
|
|
9
9
|
readonly 'label.sessionSpend': "本会话花费:{amount}";
|
|
10
|
-
readonly 'label.
|
|
11
|
-
readonly 'label.
|
|
12
|
-
readonly 'label.
|
|
10
|
+
readonly 'label.sessionSpend.today': "({amount})";
|
|
11
|
+
readonly 'label.todaySpend': "今日花费:{amount}";
|
|
12
|
+
readonly 'label.todayTokens': "今日 Token:{count}";
|
|
13
|
+
readonly 'unit.tokens': "{count} tok";
|
|
14
|
+
readonly 'label.cost.input': "未缓存输入 {amount}";
|
|
15
|
+
readonly 'label.cost.cacheRead': "缓存读取 {amount}";
|
|
13
16
|
readonly 'label.cost.output': "输出 {amount}";
|
|
17
|
+
readonly 'card.title': "花费金额";
|
|
18
|
+
readonly 'card.input': "未缓存输入";
|
|
19
|
+
readonly 'card.cacheRead': "缓存读取";
|
|
20
|
+
readonly 'card.output': "输出";
|
|
21
|
+
readonly 'card.aria': "本轮对话花费:{amount}";
|
|
14
22
|
readonly 'stat.none': "暂无消耗记录";
|
|
15
23
|
readonly 'stat.untitled': "未命名";
|
|
16
24
|
readonly 'state.unavailable': "额度不可用";
|
|
17
25
|
readonly 'action.refresh': "刷新";
|
|
18
26
|
readonly 'info.aria': "花费说明";
|
|
19
|
-
readonly 'info.hint': "
|
|
27
|
+
readonly 'info.hint': "估算:仅 DeepSeek 与 MiMo 模型,按每条消息自身时刻的峰谷官方单价计价(高峰:工作日 9:00–12:00、14:00–18:00)。\n括号内为本会话今日花费。\nv{version}";
|
|
20
28
|
readonly 'badge.aria': "DeepSeek 额度:{amount}";
|
|
21
29
|
readonly 'panel.aria': "DeepSeek 额度详情";
|
|
22
30
|
readonly 'label.sessionRanking': "今日会话花费";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composer spend card's three bucket amounts, derived from the pushed
|
|
3
|
+
* `billingTodaySpend` projection. The host fold already prices every sample
|
|
4
|
+
* into the three disjoint billing buckets (cache-hit input, cache-miss input —
|
|
5
|
+
* which includes cache writes, output including reasoning), so the card sums
|
|
6
|
+
* the session's priced model rows instead of adding a Remote call or a second
|
|
7
|
+
* fold. Keeping the derivation here (pure, no React) lets the sum and its
|
|
8
|
+
* float reconciliation be tested on their own.
|
|
9
|
+
* @module @rayadesu/dsh-client-ui-billing/spendBuckets
|
|
10
|
+
*/
|
|
11
|
+
import type { DeepSeekTodaySpend } from '@rayadesu/dsh-llm-billing/types';
|
|
12
|
+
/** One priced row's cost split across the three billing buckets, in CNY. */
|
|
13
|
+
export interface SpendBuckets {
|
|
14
|
+
/** Uncached (cache-miss) input cost, cache writes included. */
|
|
15
|
+
uncachedInput: number;
|
|
16
|
+
/** Cache-hit (cache-read) input cost. */
|
|
17
|
+
cacheRead: number;
|
|
18
|
+
/** Output cost, reasoning included. */
|
|
19
|
+
output: number;
|
|
20
|
+
/** Billed total the three rows must add up to. */
|
|
21
|
+
total: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fold one spend's model rows into the card's three bucket amounts.
|
|
25
|
+
*
|
|
26
|
+
* The rows carry float costs, so the three independently rendered values can
|
|
27
|
+
* miss the displayed total by one unit in the last rendered decimal after each
|
|
28
|
+
* is formatted to four decimals. The residual is absorbed by the largest
|
|
29
|
+
* bucket — the only one where a 1e-4 nudge cannot be seen — so the card's rows
|
|
30
|
+
* always add up to the total it shows in its header.
|
|
31
|
+
* @param spend - the session bucket of the `billingTodaySpend` projection
|
|
32
|
+
* (the session's own events, every Beijing day).
|
|
33
|
+
* @returns the three bucket totals plus the billed total.
|
|
34
|
+
*/
|
|
35
|
+
export declare function spendBucketsOf(spend: DeepSeekTodaySpend): SpendBuckets;
|
|
36
|
+
/** Whether a spend has any priced amount to show (a zero card stays hidden). */
|
|
37
|
+
export declare function hasBilledSpend(spend: DeepSeekTodaySpend): boolean;
|
|
38
|
+
//# sourceMappingURL=spendBuckets.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One trigger-anchored dialog seat for the composer spend card, mirroring the
|
|
3
|
+
* behaviour of DSH's own composer stat dialogs: the panel portals above the
|
|
4
|
+
* trigger, the position is clamped inside the viewport, an outside pointerdown
|
|
5
|
+
* or Escape closes it.
|
|
6
|
+
*
|
|
7
|
+
* DSH's `ui-chat` keeps its equivalent seat in its own private client bundle,
|
|
8
|
+
* so this plugin cannot import it; the two primitives it is built from are
|
|
9
|
+
* public (`@deepseek-ai/dsh-client-ui-primitives`), and the CSS skin they are
|
|
10
|
+
* paired with is reproduced in `SpendCard.module.css` from the same tokens.
|
|
11
|
+
* @module @rayadesu/dsh-client-ui-billing/useCardDialog
|
|
12
|
+
*/
|
|
13
|
+
import { type CSSProperties, type MutableRefObject } from 'react';
|
|
14
|
+
/**
|
|
15
|
+
* Unplaced portal panel: hidden but laid out, so the clamp's measure pass sees
|
|
16
|
+
* the panel's real dimensions instead of clamping a zero-size box.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MEASURE_STYLE: CSSProperties;
|
|
19
|
+
/** Open state, the two refs, and the clamped placement of one card dialog. */
|
|
20
|
+
export interface CardDialogSeat {
|
|
21
|
+
open: boolean;
|
|
22
|
+
setOpen: (open: boolean) => void;
|
|
23
|
+
/** Anchor: wraps the trigger pill so the clamp measures the pill, not the page. */
|
|
24
|
+
rootRef: MutableRefObject<HTMLSpanElement | null>;
|
|
25
|
+
panelRef: MutableRefObject<HTMLDivElement | null>;
|
|
26
|
+
/** Clamped placement; `null` during the measure pass (use {@link MEASURE_STYLE}). */
|
|
27
|
+
pos: CSSProperties | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Own the card's open state, placement, and dismissal.
|
|
31
|
+
* @returns the seat; spread `pos ?? MEASURE_STYLE` onto the portaled panel.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useCardDialog(): CardDialogSeat;
|
|
34
|
+
//# sourceMappingURL=useCardDialog.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin's own version, reported in the spend-hint bubble so a report can
|
|
3
|
+
* name the build actually in use.
|
|
4
|
+
*
|
|
5
|
+
* The value is stamped in at build time rather than read at runtime: a browser
|
|
6
|
+
* bundle has no manifest to read. `packages/tsdown.client.ts` defines
|
|
7
|
+
* `__DSH_PLUGIN_VERSION__` from this package's own `package.json` (the three
|
|
8
|
+
* published packages share one version), and `vitest.config.ts` defines the
|
|
9
|
+
* same constant from the same manifest so the suites see what a release ships.
|
|
10
|
+
* The `typeof` guard keeps a source run without either definition (a foreign
|
|
11
|
+
* bundler, a bare `tsc` check) from throwing on the undeclared global.
|
|
12
|
+
* @module @rayadesu/dsh-client-ui-billing/version
|
|
13
|
+
*/
|
|
14
|
+
/** The installed plugin version, e.g. `0.3.10`; `dev` when nothing stamped one. */
|
|
15
|
+
export declare const PLUGIN_VERSION: string;
|
|
16
|
+
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rayadesu/dsh-client-ui-billing",
|
|
3
3
|
"description": "Session-header DeepSeek account-balance and conversation-spend badge over the billing Remote",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.12",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@rayadesu/dsh-llm-billing": "^0.3.
|
|
50
|
+
"@rayadesu/dsh-llm-billing": "^0.3.12",
|
|
51
51
|
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.5",
|
|
52
52
|
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.5",
|
|
53
53
|
"@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.5",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@deepseek-ai/dsh-session": "^0.1.2-alpha.5"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@rayadesu/dsh-llm-billing": "^0.3.
|
|
61
|
+
"@rayadesu/dsh-llm-billing": "^0.3.12",
|
|
62
62
|
"@deepseek-ai/dsh-api-gateway": "^0.1.2-alpha.5",
|
|
63
63
|
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.5",
|
|
64
64
|
"@deepseek-ai/dsh-client-store": "^0.1.2-alpha.5",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"@testing-library/dom": "^10.4.1",
|
|
73
73
|
"@testing-library/react": "^16.3.2",
|
|
74
74
|
"@types/react": "~18.3.1",
|
|
75
|
+
"@types/react-dom": "~18.3.0",
|
|
75
76
|
"react": "^18.2.0",
|
|
76
77
|
"react-dom": "^18.2.0",
|
|
77
78
|
"use-sync-external-store": "^1.2.0",
|