@rayadesu/dsh-client-ui-billing 0.3.11 → 0.3.13

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 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: aa8362e8ae5fd116cfff977101ddb4ec0cc0bd31
6
- README.zh.md: a10b50f901fea3fd26b615c41d7318eda55c43d7
5
+ README.md: 4f5111e619c2fe63ad58f31d77a1c87fe545c751
6
+ README.zh.md: 39d503d8496a482f832d56c8a784cf38b9c0c2d6
package/README.md CHANGED
@@ -2,22 +2,24 @@
2
2
 
3
3
  English | [中文](README.zh.md)
4
4
 
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.
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 conversation's billed spend (this session's own spend read from the host-pushed `billingTodaySpend` projection, with `billing/getSessionSpend` as the fallback when the projection registry is absent, plus the subagent sessions it delegated from `billing/getDelegatedSpend`), today's spend across every session (`billing/getTodaySpend`), today's per-session ranking (`billing/getTodaySessionsSpend`, one row per conversation — the host has already merged every subagent session's spend into the row of the session that delegated it), 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) with today's all-session spend beside it (今日); 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 (—) while that ranking has not settled, and (¥0) once it settled without a row for this session; next come one priced row per model with the cache-hit / cache-miss-input / output cost breakdown ("缓存命中 ¥X · 未命中输入 ¥Y · 输出 ¥Z"), 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, followed, on the next line (flush left), by a bare version like `v0.3.11` 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 (本会话花费 with its parenthesized today share, and 今日). 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.
7
+ The trigger shows two lines — the remaining balance and this conversation's billed spend — where the balance line is the panel's headline label without its `API` prefix (`剩余金额` / `Balance`, the chip being narrow) and the spend line is the panel's own `label.sessionSpend` (`本会话花费` / `This session`), word for word; it renders immediately from the last settled balance (kept per plugin instance), revalidating in the background, and 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), with the day's cache-hit share right after that figure, bare and unparenthesized — DSH's own hit-rate rule, an integer percent that grows decimals only as far as a partial hit needs to stay below 100 (`99`, `99.5`, `99.95`), and nothing at all when the day billed no prompt-side input — then today's two bucket detail lines directly under it (tokens on top, costs below, each line on its own with its natural ` · ` spacing, in the per-model breakdown line's typography but on the third section's row spacing (the today session-spend ranking's tight 6px rhythm) — no column alignment between them; DSH's bucket wording and row order; every spend amount renders at three significant digits via `formatSpendSignificant` — the day figure and its bucket costs, this session's line, each per-model row and its bucket line, and the ranking amounts — but never finer than four decimals, so an amount below ¥0.0001 reads `¥0`; the balance line alone keeps `formatSpend`'s up-to-four decimals), then this conversation's billed spend on its own line under them (本会话花费 — this session's own spend plus the subagent sessions it delegated, their model rows merged in, 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 amount rides right after it (¥X) — that is **this conversation'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 reads the row's `total`, which counts the same tree as the amount beside it (the whole conversation), so the trailing number always stays the part of that amount which fell on the day. It renders **only when the conversation did NOT start today** — the session's own creation day decides, reported as `crossedDay` by `billing/getDelegatedSpend`, never a comparison of the two amounts: the session figure is live while the ranking row is 60-second cached, so mid-turn they differ by a few cents and a comparison would flash the rider for a session created today. An unsettled ranking renders nothing (the share itself is what is conditional), a session that started today never shows one (it billed everything it ever billed today), and neither does a session that is itself a delegated subagent child — its spend rides its delegator's ranking row, so its own share is unknown rather than zero; 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 lone priced model renders no name row at all — neither its name (which says nothing new in a session that only ever billed one model) nor its amount (which repeats 本会话花费 above), leaving only its bucket line, which takes over the name row's own 2px header gap — while two or more models get a named row each), 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 states that the amounts include the subagent sessions this session delegated; the line after that names the amount trailing the session row — this conversation's spend today — and the last line is a bare version like `v0.3.13`, 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 trailing today share). Below the model rows the panel ends with the **today session-spend ranking** (`今日会话花费`): conversations sorted by today's cost, highest first — one row per conversation, because the host has already merged each subagent session's spend into the row of the session that delegated it — 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`); 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 ((—) until the first one settles).
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 same settle refreshes this session's delegated subtotal (`billing/getDelegatedSpend`), which is what keeps the session amount a whole-conversation figure as subagents finish. 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 trailing today share on the 本会话花费 row reads that same fetch (the row's `total`), 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
- - **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.
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)`, `billing/getDelegatedSpend(sessionId, 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
 
22
24
  ## Model Experience
23
25
 
@@ -30,6 +32,9 @@ None; the package never assembles or sends provider requests, and its one RPC is
30
32
  ## Known Limitations and Deferred Work
31
33
 
32
34
  - **First balance line only** — the balance reads the primary (`balance_infos[0]`) currency line; other currency lines are not shown.
33
- - **Ranking capped at 10** — the panel shows at most `SESSION_RANKING_LIMIT` sessions, with a "…N more sessions" hint.
35
+ - **Ranking capped at 10** — the panel shows at most `SESSION_RANKING_LIMIT` conversations, with a "…N more sessions" hint. The cap applies after the host's subagent roll-up, so one row is one conversation rather than one delegation.
36
+ - **The delegated subtotal moves with the turn, not per event** — the session amount's own part is live (the pushed projection moves as the turn streams), while the subagent part comes from `billing/getDelegatedSpend` on mount, on manual refresh, and when a turn settles, served from the same 60-second host cache as today's spend. A subagent that burns money mid-turn therefore lands on the parent's amount at the next read, not per event.
34
37
  - **Turn cost needs a finalized closing message** — interrupted turns have no actions row, so no turn cost is shown for them.
38
+ - **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. The hit-rate figure riding that row takes the prompt side only — cache read over cache read plus uncached input — so output tokens never enter its denominator, exactly the split DSH's own `billedInputTokens` makes. The panel types in exactly **three levels**, defined once as custom properties on `.panel` in its stylesheet with every member referencing its own level's tokens: **level one** 11px/16px tertiary tabular-nums for the bucket detail lines (`未缓存输入 ¥X · 缓存读取 ¥Y · 输出 ¥Z`), a whole today session-spend ranking row (rank, middot, title, amount), the "…N more sessions" footer, and the two trailing figures (this session's share of today, the day's cache-hit share); **level two** 12px/18px secondary for the model name with its row amount and the 今日会话花费 title — a level's text and its numbers may differ in tone: a level-two member's amount uses that level's own number tone (`--billing-type-2-number-tone`, one step below its label), and any future level-two amount follows it; **level three** 13px/18px primary tabular-nums for the 今日 Token, 今日花费 and 本会话花费 rows.
39
+ - **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
40
  - **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
@@ -2,22 +2,24 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
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 并渲染它返回的内容。
5
+ Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡献一个条目,自己挂载 `billing` Remote,并渲染 DeepSeek 账户余额、本会话的计费花费(本会话自身的花费读宿主推送的 `billingTodaySpend` 投影,投影注册表不存在时回退 `billing/getSessionSpend`;再加上它委派的子代理会话,来自 `billing/getDelegatedSpend`)、今日所有会话的计费花费(`billing/getTodaySpend`)、今日按会话的排行(`billing/getTodaySessionsSpend`,每行一个**对话**——宿主已把每个子代理会话的花费并入委派它的那个会话行里)与各回合花费(`billing/getSessionTurnSpends`),在右上角渲染成一个标签框。该能力的宿主侧在 [`dsh-llm-billing`](../llm-billing/README.md),它拥有 `/user/balance` 传输、峰谷计价表与 `billing` Remote 命名空间;本包挂载那个 Remote 并渲染它返回的内容。
6
6
 
7
- 触发器显示两行——剩余额度与本轮对话的计费花费(「本轮对话花费 ¥X」)——有上次已落定的余额就立即渲染(按插件实例保留),后台校验;首次余额尚未到达时不渲染任何东西。点击后展开一个标签框:剩余金额、本会话的计费花费(「本会话花费」,按官方峰/谷单价逐条用量样本计价——高峰窗口仅周一至周五适用,周末按低谷价——分叉会话只按分叉后的自有工作计价——同行右侧并列今日所有会话的合计「今日」;本会话花费金额后紧跟一个括号括起来的纯金额(`(¥X)`),那是**本会话今日份**的花费,取自已拉取的今日会话排行、按当前会话 id 匹配,所以既不是排行首行、也不是当日合计——排行未落定时显示 `(—)`,落定但该会话没有行时显示 `(¥0)`)、每个模型一行的花费及其缓存命中/未命中输入/输出分项(「缓存命中 ¥X · 未命中输入 ¥Y · 输出 ¥Z」)、手动刷新按钮,以及「?」按钮上的一句简短花费说明(只估算 DeepSeek 与 MiMo 模型、按每条消息自身时刻的峰谷单价计价,下一行顶格是纯版本号(如 `v0.3.11`)——构建期从本包 manifest 注入,便于反馈时指明所用版本;费率明细在宿主 README —— DSH 的悬停气泡只装得下短文案,过长会在视口边缘被裁掉且无法悬停阅读,所以说明放在按钮下方,纵向适配可翻转的那一侧)。剩余金额行(`API 剩余金额`)字号大于下方花费行(`本会话花费` 与其括号金额、`今日`)。模型行下方是**今日会话花费排行**(`今日会话花费`):按今日花费从高到低排列的会话列表,每行以右对齐的名次列开头,后接与花费分项相同的小圆点分隔符(「·」,字号、色调与间距一致),再是会话的持久标题(重命名自动同步),最后是金额——标题在名次列后缩进对齐;最多渲染 `SESSION_RANKING_LIMIT`(10)行,其余以「…还有 N 个会话」提示。刷新时旧值不消失,刷新失败保留上一次有效值。没有可计价消耗的会话或日子显示「暂无消耗记录」而不是编造数字。失败——未配置 API key、凭据被拒绝、传输错误——渲染弱化的「额度不可用」,其提示携带 Remote 自己的错误信息。
7
+ 触发器显示两行——剩余余额与本轮对话的计费花费——主行是面板标题文案去掉 `API` 前缀的短版(`剩余金额`,徽标较窄),副行用的就是面板的 `label.sessionSpend`(`本会话花费`),一字不差;有上次已落定的余额就立即渲染(按插件实例保留),后台校验;首次余额尚未到达时不渲染任何东西。点击后展开一个标签框:剩余金额、今日计费 token 数与今日所有会话合计(「今日 Token」与「今日花费」同一行——token 数把每个计价行的三个计费桶相加,缓存读取与输出各算一个 token,并按 DSH 自己的紧凑记数法加 ` tok` 单位渲染:`517 tok`、`12.2K tok`、`1.2M tok`——<1e3 原整数、<1e6 用共享 `K` 单位、再往上用 `M`,缩放值 <100 保留一位小数、≥100 取整)——紧跟在 token 数值后面的是当天的**缓存命中率**(裸数字,无括号、无文字说明),按 DSH 官方命中率规则渲染:整数百分比,只有部分命中会被四舍五入到 100% 时才逐位多留小数(`99`、`99.5`、`99.95`),当天没有 prompt 侧输入就完全不显示——紧跟该行下面是**今日两行桶明细**(token 在上、花费在下,两行各自成行、各自保持自然的 ` · ` 间距,两行之间不做列对齐;桶名与行序用 DSH 的,排版沿用下方模型分项那一套,行距用第三部分(今日会话花费排行)那一套紧凑节奏;**所有花费金额都用 `formatSpendSignificant` 按三位有效数字渲染**(今日花费与它的三个桶、本会话行、每个模型行及其分项、排行金额),但**不细于四位小数**——低于 ¥0.0001 的金额显示 `¥0`;只有余额行保持 `formatSpend` 的至多四位小数)、再下一行是本会话的计费花费(「本会话花费」——本会话自身的花费加上它委派的子代理会话,模型分项同样合并,按官方峰/谷单价逐条用量样本计价:高峰窗口仅周一至周五适用,周末按低谷价;分叉会话只按分叉后的自有工作计价;金额后可能紧跟一个纯金额(`¥X`),那是**本次对话今日份**的花费,取自已拉取的今日会话排行、按当前会话 id 匹配,所以既不是排行首行、也不是当日合计——它读的是该行的 `total`,与旁边那个金额数的是同一棵树(整次对话);**只在这段对话不是今天才创建时才渲染**:判定用会话自身的创建日(`billing/getDelegatedSpend` 返回的 `crossedDay`),不再比较两个金额——会话金额是实时的、排行行有 60 秒缓存,回合中途两者本来就会差几分钱,比较会导致今天新建的会话闪出这个数字;排行未落定则不渲染它(它本身才是那个条件量),今天创建的对话永远不显示,而当前会话自己就是被委派的子代理会话时也不显示——它的花费落在委派它的那个会话的排行行里,它自己的今日份是「未知」而不是 0)、每个模型一行的花费及其分项(用 DSH 自己的桶名与行序:「未缓存输入 ¥X · 缓存读取 ¥Y · 输出 ¥Z」,即 ui-chat `message.turnUsage` 那套文案;宿主把缓存写入按未命中单价计价,所以它并入「未缓存输入」桶);**只有一个计价模型时整行模型行都不渲染**——名字在只用一种模型的会话里说明不了什么,金额又是上一行「本会话花费」的数字,于是只剩它下面那行桶明细,信息不丢(没有名字行时该明细行接手名字行那份 2px 表头间距)、手动刷新按钮,以及「?」按钮上的一句简短花费说明(只估算 DeepSeek 与 MiMo 模型、按每条消息自身时刻的峰谷单价计价;下一行说明金额含本会话委派的子代理会话;再下一行说明会话行紧跟的金额就是本次对话今日花费,最后一行顶格是纯版本号(如 `v0.3.13`)——构建期从本包 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
- - **今日花费随回合结束更新(去抖)** —— 组件通过框架 `useSession` 座位订阅当前会话的「运行中」标志。每当一轮 prompt 回合结束(标志翻回空闲)就重算**今日**(`billing/getTodaySpend`);重算有 2 秒去抖,回合突发(agent 连续多轮)只计价一次。**排行**(`billing/getTodaySessionsSpend`)只在详情面板打开时(以及手动刷新时)拉取,徽标一直关着就不会为全量会话扫描买单;「本会话花费」行括号里的今日份金额也来自这次排行读取,所以它不额外发请求,代价是与排行同进同退(面板刚打开、排行未落定时先是 `(—)`)。
19
+ - **本会话花费靠推送,不靠拉取** —— 宿主把每个会话的花费折进客户端可见的 `billingTodaySpend` 投影并推送每次变化;徽标用框架 `useProjection` 座位订阅,因此投影注册表装配时该行零 Remote 调用、实时更新。键不存在时同一行回退到 `billing/getSessionSpend` —— 与未注册的输入框花费卡片读的是同一个面、同一条回退。
20
+ - **今日花费随回合结束更新(去抖)** —— 组件通过框架 `useSession` 座位订阅当前会话的「运行中」标志。每当一轮 prompt 回合结束(标志翻回空闲)就重算**今日花费**(`billing/getTodaySpend`;同一行的**今日 Token** 由这次读取的模型行原地求和,不发额外请求);重算有 2 秒去抖,回合突发(agent 连续多轮)只计价一次。同一次回合结束也会刷新本会话的子代理小计(`billing/getDelegatedSpend`)——这正是让会话金额在子代理跑完后仍是「整次对话」口径的来源。**排行**(`billing/getTodaySessionsSpend`)只在详情面板打开时(以及手动刷新时)拉取,徽标一直关着就不会为全量会话扫描买单;「本会话花费」行紧跟的今日份金额也来自这次排行读取(读该行的 `total`),所以它不额外发请求,代价是与排行同进同退(排行未落定时不渲染它——两个金额对不上才显示,而「未知」不算对不上)。
19
21
  - **额度带 TTL 缓存** —— 账户余额在挂载、切换会话、点刷新按钮时拉取。宿主 15 秒内复用同一份快照(手动刷新带 `force` 绕过),单次请求 5 秒超时;浏览器保留最后一次已落定值,切会话时立即渲染。仍然**没有轮询、不会自动重拉**。
20
- - **刷新保留旧值** —— 刷新在途时旧值留在屏幕上,刷新失败保留上一次有效值,不清空。刷新按钮调用 `billing/getBalance(true)`、`billing/getTodaySpend(true)` 与 `billing/getTodaySessionsSpend(true)`,绕过宿主侧缓存;回合结束触发的重算与挂载/切换会话的读取走缓存路径。
22
+ - **刷新保留旧值** —— 刷新在途时旧值留在屏幕上,刷新失败保留上一次有效值,不清空。刷新按钮调用 `billing/getBalance(true)`、`billing/getTodaySpend(true)`、`billing/getDelegatedSpend(sessionId, true)` 与 `billing/getTodaySessionsSpend(true)`,绕过宿主侧缓存;回合结束触发的重算与挂载/切换会话的读取走缓存路径。
21
23
 
22
24
  ## 模型体验
23
25
 
@@ -30,6 +32,10 @@ Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡
30
32
  ## 已知限制与暂缓事项
31
33
 
32
34
  - **只取首条余额线** —— 余额读取主要(`balance_infos[0]`)币种行;其它币种行不显示。
33
- - **排行上限 10** —— 面板最多显示 `SESSION_RANKING_LIMIT` 个会话,其余以「…还有 N 个会话」提示。
35
+ - **排行上限 10** —— 面板最多显示 `SESSION_RANKING_LIMIT` 个对话,其余以「…还有 N 个会话」提示。上限在宿主的子代理合并之后生效,所以一行就是一个对话,而不是一次委派。
36
+ - **子代理小计随回合更新,不逐事件跟进** —— 会话金额里本会话自身那部分是实时的(推送投影随回合更新),子代理那部分来自 `billing/getDelegatedSpend`:挂载、手动刷新与回合结束时各拉一次,并与今日花费共用同一个 60 秒宿主缓存。因此子代理在回合中途烧掉的钱会在下一次读取时落到父会话金额上,而不是逐事件实时跳动。
34
37
  - **本轮花费需要已定稿的收尾消息** —— 中断的回合没有操作行,不显示本轮花费。
38
+ - **今日 Token 只数计价行** —— 它统计的是 `billing/getTodaySpend` 返回的那些模型行(即价目表里有费率行的模型)的三个计费桶之和;没有费率行的模型不计 token,缓存写入并入「未缓存输入」桶(与分项同一口径)。该行紧跟的命中率只按 **prompt 侧**两个桶算(缓存读取 ÷(缓存读取 + 未缓存输入)),输出不进分母,与 DSH `billedInputTokens` 的口径一致。
39
+ - **面板排版只有三级** —— 三级字体定义在面板样式表的 `.panel` 上(每个成员只引用自己那一级的 token,改一处即整级生效):**一级** 11px/16px tertiary tabular-nums,用于桶明细行(`未缓存输入 ¥X · 缓存读取 ¥Y · 输出 ¥Z`)、今日会话排行整行(名次、分隔点、标题与金额)、「…还有 N 个会话」以及两个紧跟数字(本会话今日份金额、当天缓存命中率);**二级** 12px/18px secondary,用于模型名与其行金额、`今日会话花费` 标题——**同一级里文字与数字的色调允许不同**:二级成员的金额用该级自己的数字色调 `--billing-type-2-number-tone`(比文字暗一档),今后二级成员的金额一律照此办理;**三级** 13px/18px primary tabular-nums,用于 `今日 Token`、`今日花费`、`本会话花费` 三行(含各自的紧跟数字)。
40
+ - **输入框花费卡片暂无席位** —— 注册已暂停(见上),等宿主在输入框统计行里开放子插槽、或在 dock list 规格上加同行分组;在那之前输入框下方只有官方自带的 token/时间 pill。它只计价宿主费率表覆盖的模型(DeepSeek 与 MiMo),而上面那条 token 行统计的是会话全部 token,缓存写入并入它的「未缓存输入」行。
35
41
  - **额度最多旧 15 秒** —— 宿主 15 秒内复用同一份快照、浏览器保留最后一次已落定值:其他客户端产生的消耗不会让界面数值自动变化,需等 TTL 过期、手动刷新或刷新浏览器(仍无轮询)。