@rayadesu/dsh-client-ui-billing 0.2.2 → 0.2.4

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: 0856ee1b93e49f34da5f534ce189568b8157d157
6
- README.zh.md: 80a4334b4eaf79636fbdad0ce9484f07dfb1f8e0
5
+ README.md: b4ac01bab9f32a7bdad2dbd1f86f2f4a53f25af6
6
+ README.zh.md: d7719cf98df1cf5b8c8189c99901019a37f04e45
package/README.md CHANGED
@@ -2,9 +2,11 @@
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, reads the DeepSeek account balance, this session's billed spend, and today's spend across every session through `billing/getBalance`, `billing/getSessionSpend`, and `billing/getTodaySpend`, and renders them 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, reads the DeepSeek account balance, this session's billed spend, today's spend across every session, today's per-session ranking, and one turn's cost through `billing/getBalance`, `billing/getSessionSpend`, `billing/getTodaySpend`, `billing/getTodaySessionsSpend`, and `billing/getTurnSpend`, and renders them 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 nothing while the first fetch is in flight. Clicking it opens a label box with the remaining amount, this session's billed spend (本会话花费, priced per message at the official peak/off-peak rate — peak windows apply weekdays Mon–Fri only, weekends are off-peak) with today's all-session spend beside it (今日共花费), one priced row per model with the cache-hit / cache-miss-input / output cost breakdown ("缓存命中 ¥X · 未命中输入 ¥Y · 输出 ¥Z"), a manual refresh action, and a spend disclaimer. 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 ("本轮对话花费 ¥X") — and renders nothing while the first fetch is in flight. Clicking it opens a label box with the remaining amount, this session's billed spend (本会话花费, priced per message at the official peak/off-peak rate — peak windows apply weekdays Mon–Fri only, weekends are off-peak) with today's all-session spend beside it (今日共花费), one priced row per model with the cache-hit / cache-miss-input / output cost breakdown ("缓存命中 ¥X · 未命中输入 ¥Y · 输出 ¥Z"), a manual refresh action, and a spend disclaimer. Below the model rows the panel ends with the **today session-spend ranking** (`今日会话花费`): sessions sorted by today's cost, highest first, each row carrying the session's durable title (renames sync automatically) and its amount; 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
+
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. For each completed turn's closing assistant message, the entry reads `billing/getTurnSpend` (memoized per session + message, since settled turns never change) and renders a muted `本轮花费:¥X` label in the actions row, next to the turn's usage card. Turns that priced to zero (no DeepSeek usage) and failed loads render nothing, so a Remote outage never clutters the row.
8
10
 
9
11
  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 token usage through `billing/getSessionSpend` and every session's usage through `billing/getTodaySpend`. Copy goes through the package's own `billing` locale namespace; styling uses tokens only.
10
12
 
@@ -12,9 +14,9 @@ The badge is account-level even though the slot is session-scoped: the header ut
12
14
 
13
15
  The spends follow the conversation and the balance stays a manual snapshot:
14
16
 
15
- - **Spends follow 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 this session's spend (`billing/getSessionSpend`) and today's spend (`billing/getTodaySpend`), local pricing passes with no network request, so the spend lines stay live during an ongoing conversation. 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 host-side cache (see `dsh-llm-billing`) then serves the first miss for the rest of the minute.
17
+ - **Spends follow 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 this session's spend (`billing/getSessionSpend`), today's spend (`billing/getTodaySpend`), and the ranking (`billing/getTodaySessionsSpend`), local pricing passes with no network request, so the spend lines stay live during an ongoing conversation. 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 host-side cache (see `dsh-llm-billing`) then serves the first miss for the rest of the minute.
16
18
  - **Balance is manual** — the account balance is fetched on mount, on session switch, and on the explicit refresh action (`billing/getBalance`). There is no polling and no automatic refetch: the balance line only changes when one of those events happens.
17
- - **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/getTodaySpend(true)`, which bypasses the host-side cache; the turn-triggered recompute and the mount/session-switch read use the cached path.
19
+ - **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/getTodaySpend(true)` and `billing/getTodaySessionsSpend(true)`, which bypass the host-side cache; the turn-triggered recompute and the mount/session-switch read use the cached path.
18
20
 
19
21
  ## Model Experience
20
22
 
@@ -27,4 +29,6 @@ None; the package never assembles or sends provider requests, and its one RPC is
27
29
  ## Known Limitations and Deferred Work
28
30
 
29
31
  - **First balance line only** — the balance reads the primary (`balance_infos[0]`) currency line; other currency lines are not shown.
32
+ - **Ranking capped at 10** — the panel shows at most `SESSION_RANKING_LIMIT` sessions, with a "…N more sessions" hint.
33
+ - **Turn cost needs a finalized closing message** — interrupted turns have no actions row, so no turn cost is shown for them.
30
34
  - **Balance does not follow automatically** — the balance is a manual snapshot (no polling): spending from another client does not move the shown value until a refresh or browser reload.
package/README.zh.md CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡献一个条目,自己挂载 `billing` Remote,通过 `billing/getBalance`、`billing/getSessionSpend` 与 `billing/getTodaySpend` 读取 DeepSeek 账户余额、本会话的计费花费与今日所有会话的计费花费,并在右上角渲染成一个标签框。该能力的宿主侧在 [`dsh-llm-billing`](../llm-billing/README.md),它拥有 `/user/balance` 传输、峰谷计价表与 `billing` Remote 命名空间;本包挂载那个 Remote 并渲染它返回的内容。
5
+ Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡献一个条目,自己挂载 `billing` Remote,通过 `billing/getBalance`、`billing/getSessionSpend`、`billing/getTodaySpend`、`billing/getTodaySessionsSpend` 与 `billing/getTurnSpend` 读取 DeepSeek 账户余额、本会话的计费花费、今日所有会话的计费花费、今日按会话的排行与单个回合的花费,并在右上角渲染成一个标签框。该能力的宿主侧在 [`dsh-llm-billing`](../llm-billing/README.md),它拥有 `/user/balance` 传输、峰谷计价表与 `billing` Remote 命名空间;本包挂载那个 Remote 并渲染它返回的内容。
6
6
 
7
- 触发器显示两行——剩余额度与本轮对话的计费花费(「本轮对话花费 ¥X」)——首个请求在途时不渲染任何东西。点击后展开一个标签框:剩余金额、本会话的计费花费(本会话花费,按官方峰/谷单价逐条消息计价——高峰窗口仅周一至周五适用,周末按低谷价——右侧并列今日所有会话的合计「今日共花费」)、每个模型一行的花费及其缓存命中/未命中输入/输出分项(「缓存命中 ¥X · 未命中输入 ¥Y · 输出 ¥Z」)、手动刷新按钮与花费说明。刷新时旧值不消失,刷新失败保留上一次有效值。没有可计价消耗的会话或日子显示「暂无消耗记录」而不是编造数字。失败——未配置 API key、凭据被拒绝、传输错误——渲染弱化的「额度不可用」,其提示携带 Remote 自己的错误信息。
7
+ 触发器显示两行——剩余额度与本轮对话的计费花费(「本轮对话花费 ¥X」)——首个请求在途时不渲染任何东西。点击后展开一个标签框:剩余金额、本会话的计费花费(本会话花费,按官方峰/谷单价逐条消息计价——高峰窗口仅周一至周五适用,周末按低谷价——右侧并列今日所有会话的合计「今日共花费」)、每个模型一行的花费及其缓存命中/未命中输入/输出分项(「缓存命中 ¥X · 未命中输入 ¥Y · 输出 ¥Z」)、手动刷新按钮与花费说明。模型行下方是**今日会话花费排行**(`今日会话花费`):按今日花费从高到低排列的会话列表,每行带会话的持久标题(重命名自动同步)与金额;最多渲染 `SESSION_RANKING_LIMIT`(10)行,其余以「…还有 N 个会话」提示。刷新时旧值不消失,刷新失败保留上一次有效值。没有可计价消耗的会话或日子显示「暂无消耗记录」而不是编造数字。失败——未配置 API key、凭据被拒绝、传输错误——渲染弱化的「额度不可用」,其提示携带 Remote 自己的错误信息。
8
+
9
+ 本包还贡献**本轮花费条目**(`billing-turn-cost`,order 20)到 ui-chat 的 `conversation.chat.assistant-actions` list slot——与 `ui-message-feedback` 同一条操作条,多条目共存。对每条已完成回合的收尾助手消息,条目通过 `billing/getTurnSpend` 读取花费(按会话 + 消息 memo,回合落定后不再变化),在操作行里(与时间、复制、分叉同一排,紧邻该回合的「本轮用量」卡片)渲染弱化的「本轮花费:¥X」。回合没有 DeepSeek 用量(花费为 0)或加载失败时不渲染,Remote 故障不会污染操作行。
8
10
 
9
11
  角标是账户级的,尽管插槽是会话作用域:顶栏工具行不过是外壳提供的那个始终可见的标题栏席位,余额并不随会话变化——但花费行会,因为它通过 `billing/getSessionSpend` 按当前会话自己的 token 用量计价、通过 `billing/getTodaySpend` 按所有会话的用量计价。文案走本包自己的 `billing` locale 命名空间;样式只用 token。
10
12
 
@@ -12,9 +14,9 @@ Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡
12
14
 
13
15
  花费跟随会话、额度保持手动快照:
14
16
 
15
- - **花费随回合结束自动更新(去抖)** —— 组件通过框架 `useSession` 座位订阅当前会话的「运行中」标志。每当一轮 prompt 回合结束(标志翻回空闲),就只重算**本会话的花费**(`billing/getSessionSpend`)与**今日共花费**(`billing/getTodaySpend`,纯本地计价、不发网络请求),连续对话时花费行实时跟随。重算有 2 秒去抖,回合突发(agent 连续多轮)只计价一次;宿主侧缓存(见 `dsh-llm-billing`)随后在一分钟内服务首次未命中。
17
+ - **花费随回合结束自动更新(去抖)** —— 组件通过框架 `useSession` 座位订阅当前会话的「运行中」标志。每当一轮 prompt 回合结束(标志翻回空闲),就只重算**本会话的花费**(`billing/getSessionSpend`)、**今日共花费**(`billing/getTodaySpend`)与**排行**(`billing/getTodaySessionsSpend`,纯本地计价、不发网络请求),连续对话时花费行实时跟随。重算有 2 秒去抖,回合突发(agent 连续多轮)只计价一次;宿主侧缓存(见 `dsh-llm-billing`)随后在一分钟内服务首次未命中。
16
18
  - **额度保持手动** —— 账户余额在挂载、切换会话、点刷新按钮时拉取(`billing/getBalance`)。**没有轮询、不会自动重拉**:余额行只在这几个时机变化。
17
- - **刷新保留旧值** —— 刷新在途时旧值留在屏幕上,刷新失败保留上一次有效值,不清空。刷新按钮调用 `billing/getTodaySpend(true)`,绕过宿主侧缓存;回合结束触发的重算与挂载/切换会话的读取走缓存路径。
19
+ - **刷新保留旧值** —— 刷新在途时旧值留在屏幕上,刷新失败保留上一次有效值,不清空。刷新按钮调用 `billing/getTodaySpend(true)` 与 `billing/getTodaySessionsSpend(true)`,绕过宿主侧缓存;回合结束触发的重算与挂载/切换会话的读取走缓存路径。
18
20
 
19
21
  ## 模型体验
20
22
 
@@ -27,4 +29,6 @@ Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡
27
29
  ## 已知限制与暂缓事项
28
30
 
29
31
  - **只取首条余额线** —— 余额读取主要(`balance_infos[0]`)币种行;其它币种行不显示。
32
+ - **排行上限 10** —— 面板最多显示 `SESSION_RANKING_LIMIT` 个会话,其余以「…还有 N 个会话」提示。
33
+ - **本轮花费需要已定稿的收尾消息** —— 中断的回合没有操作行,不显示本轮花费。
30
34
  - **额度不自动跟随** —— 余额保持手动快照(无轮询):其他客户端产生的消耗不会让界面数值自动变化,需手动刷新或刷新浏览器。
package/lib/client.js CHANGED
@@ -4120,6 +4120,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4120
4120
  "outputCost": number()
4121
4121
  }))
4122
4122
  });
4123
+ const _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_parameter_0$schema = union([
4124
+ _undefined(),
4125
+ literal(false),
4126
+ literal(true)
4127
+ ]);
4128
+ const _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_result$schema = object({ "sessions": array(object({
4129
+ "sessionId": intersection(string(), unknown()),
4130
+ "title": union([literal(null), string()]),
4131
+ "total": number()
4132
+ })) });
4123
4133
  const _rayadesu_dsh_llm_billing_billing_getTodaySpend_parameter_0$schema = union([
4124
4134
  _undefined(),
4125
4135
  literal(false),
@@ -4141,6 +4151,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4141
4151
  "outputCost": number()
4142
4152
  }))
4143
4153
  });
4154
+ const _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_0$schema = intersection(string(), unknown());
4155
+ const _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_1$schema = string();
4156
+ const _rayadesu_dsh_llm_billing_billing_getTurnSpend_result$schema = object({ "total": number() });
4144
4157
  const TYPERT_REMOTE = {
4145
4158
  package: "@rayadesu/dsh-llm-billing",
4146
4159
  descriptors: [
@@ -4158,7 +4171,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4158
4171
  },
4159
4172
  sourceLocation: {
4160
4173
  "file": "packages/llm-billing/src/balance.ts",
4161
- "line": 143,
4174
+ "line": 154,
4162
4175
  "column": 3
4163
4176
  }
4164
4177
  },
@@ -4185,7 +4198,35 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4185
4198
  },
4186
4199
  sourceLocation: {
4187
4200
  "file": "packages/llm-billing/src/balance.ts",
4188
- "line": 154,
4201
+ "line": 165,
4202
+ "column": 3
4203
+ }
4204
+ },
4205
+ {
4206
+ id: "@rayadesu/dsh-llm-billing#billing/getTodaySessionsSpend",
4207
+ service: "billing",
4208
+ namespace: "billing",
4209
+ method: "getTodaySessionsSpend",
4210
+ invocation: { kind: "direct" },
4211
+ parameters: [{
4212
+ name: "force",
4213
+ wire: "force",
4214
+ source: "json",
4215
+ acceptsUndefined: true,
4216
+ codec: {
4217
+ mode: "strict",
4218
+ typeSymbol: "@rayadesu/dsh-llm-billing#billing/getTodaySessionsSpend:force",
4219
+ schema: _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_parameter_0$schema
4220
+ }
4221
+ }],
4222
+ result: {
4223
+ mode: "strict",
4224
+ typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekTodaySessionsSpend",
4225
+ schema: _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_result$schema
4226
+ },
4227
+ sourceLocation: {
4228
+ "file": "packages/llm-billing/src/balance.ts",
4229
+ "line": 192,
4189
4230
  "column": 3
4190
4231
  }
4191
4232
  },
@@ -4213,7 +4254,43 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4213
4254
  },
4214
4255
  sourceLocation: {
4215
4256
  "file": "packages/llm-billing/src/balance.ts",
4216
- "line": 167,
4257
+ "line": 178,
4258
+ "column": 3
4259
+ }
4260
+ },
4261
+ {
4262
+ id: "@rayadesu/dsh-llm-billing#billing/getTurnSpend",
4263
+ service: "billing",
4264
+ namespace: "billing",
4265
+ method: "getTurnSpend",
4266
+ invocation: { kind: "direct" },
4267
+ parameters: [{
4268
+ name: "sessionId",
4269
+ wire: "sessionId",
4270
+ source: "json",
4271
+ codec: {
4272
+ mode: "strict",
4273
+ typeSymbol: "@rayadesu/dsh-llm-billing#billing/getTurnSpend:sessionId",
4274
+ schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_0$schema
4275
+ }
4276
+ }, {
4277
+ name: "messageId",
4278
+ wire: "messageId",
4279
+ source: "json",
4280
+ codec: {
4281
+ mode: "strict",
4282
+ typeSymbol: "@rayadesu/dsh-llm-billing#billing/getTurnSpend:messageId",
4283
+ schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_parameter_1$schema
4284
+ }
4285
+ }],
4286
+ result: {
4287
+ mode: "strict",
4288
+ typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekTurnSpend",
4289
+ schema: _rayadesu_dsh_llm_billing_billing_getTurnSpend_result$schema
4290
+ },
4291
+ sourceLocation: {
4292
+ "file": "packages/llm-billing/src/balance.ts",
4293
+ "line": 205,
4217
4294
  "column": 3
4218
4295
  }
4219
4296
  }
@@ -4221,13 +4298,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4221
4298
  };
4222
4299
  //#endregion
4223
4300
  //#region \0dsh-css:C:\Users\admin\Desktop\me\code\DeepSeek-Harness-chat-billing\packages\ui-billing\src\client\BalanceBadge.module.css.mjs
4224
- const css = "._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_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}";
4225
- const tagId = "@rayadesu/dsh-client-ui-billing/BalanceBadge.module.css";
4226
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
4301
+ 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_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-tertiary);padding:6px 8px 2px;font-size:11px;line-height:16px}._4_Wqcq_rankingRow{border-radius:8px;align-items:baseline;gap:8px;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_rankingName{min-width:0;color:var(--dsw-alias-label-secondary);text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:12px;line-height:18px;overflow:hidden}._4_Wqcq_rankingAmount{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;flex:none;font-size:12px;line-height:18px}._4_Wqcq_rankingMore{color:var(--dsw-alias-label-tertiary);padding:2px 8px 4px;font-size:11px;line-height:16px}";
4302
+ const tagId$1 = "@rayadesu/dsh-client-ui-billing/BalanceBadge.module.css";
4303
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
4227
4304
  const tag = document.createElement("style");
4228
4305
  tag.dataset.plugin = "@rayadesu/dsh-client-ui-billing";
4229
- tag.dataset.pluginCss = tagId;
4230
- tag.textContent = css;
4306
+ tag.dataset.pluginCss = tagId$1;
4307
+ tag.textContent = css$1;
4231
4308
  document.head.appendChild(tag);
4232
4309
  }
4233
4310
  var BalanceBadge_module_css_default = {
@@ -4244,6 +4321,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4244
4321
  "modelName": "_4_Wqcq_modelName",
4245
4322
  "modelRow": "_4_Wqcq_modelRow",
4246
4323
  "panel": "_4_Wqcq_panel",
4324
+ "ranking": "_4_Wqcq_ranking",
4325
+ "rankingAmount": "_4_Wqcq_rankingAmount",
4326
+ "rankingIndex": "_4_Wqcq_rankingIndex",
4327
+ "rankingMore": "_4_Wqcq_rankingMore",
4328
+ "rankingName": "_4_Wqcq_rankingName",
4329
+ "rankingRow": "_4_Wqcq_rankingRow",
4330
+ "rankingTitle": "_4_Wqcq_rankingTitle",
4247
4331
  "refreshButton": "_4_Wqcq_refreshButton",
4248
4332
  "root": "_4_Wqcq_root",
4249
4333
  "spendRow": "_4_Wqcq_spendRow",
@@ -4273,10 +4357,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4273
4357
  };
4274
4358
  }
4275
4359
  /** CNY amount, up to four decimals with trailing zeros trimmed. */
4276
- function formatSpend(amount) {
4360
+ function formatSpend$1(amount) {
4277
4361
  return `¥${amount.toFixed(4).replace(/\.?0+$/, "")}`;
4278
4362
  }
4279
4363
  /**
4364
+ * Run one fetch line: the fetch is deferred to a microtask so the effect's
4365
+ * render commits before any state update lands; a settled value is stored
4366
+ * while `current` stays true, and a rejection keeps the previous value (a
4367
+ * no-op unless `onReject` supplies fallback handling) so a failed refetch
4368
+ * never blanks the UI.
4369
+ */
4370
+ function fetchLine(current, fetch, store, onReject) {
4371
+ return Promise.resolve().then(fetch).then((value) => {
4372
+ if (current()) store(value);
4373
+ }, (reason) => {
4374
+ if (current()) onReject?.(reason);
4375
+ });
4376
+ }
4377
+ /**
4280
4378
  * Render the billing badge in the session-header utilities row. The trigger
4281
4379
  * shows the remaining balance and this conversation's billed spend and opens
4282
4380
  * a label box with the amount, this session's spend with its cache-hit /
@@ -4292,10 +4390,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4292
4390
  * @param props - Remote face, locale, and the standard session-header runtime share.
4293
4391
  * @returns the badge, or null until the first balance fetch settles.
4294
4392
  */
4295
- function BalanceBadge({ getBalance, getSessionSpend, getTodaySpend, sessionId, useSession, t }) {
4393
+ function BalanceBadge({ getBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, sessionId, useSession, t }) {
4296
4394
  const [balance, setBalance] = (0, react.useState)(null);
4297
4395
  const [spend, setSpend] = (0, react.useState)(null);
4298
4396
  const [todaySpend, setTodaySpend] = (0, react.useState)(null);
4397
+ const [sessionsSpend, setSessionsSpend] = (0, react.useState)(null);
4299
4398
  const [error, setError] = (0, react.useState)(null);
4300
4399
  const [refreshing, setRefreshing] = (0, react.useState)(false);
4301
4400
  const [open, setOpen] = (0, react.useState)(false);
@@ -4305,28 +4404,23 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4305
4404
  const pricedRunningRef = (0, react.useRef)(running);
4306
4405
  (0, react.useEffect)(() => {
4307
4406
  let current = true;
4407
+ const isCurrent = () => current;
4308
4408
  setRefreshing(balance !== null);
4309
4409
  Promise.resolve().then(() => {
4310
- const balanceRequest = Promise.resolve().then(getBalance).then((value) => {
4311
- if (!current) return;
4410
+ const balanceRequest = fetchLine(isCurrent, getBalance, (value) => {
4312
4411
  setBalance(value);
4313
4412
  setError(null);
4314
4413
  }, (reason) => {
4315
- if (!current) return;
4316
4414
  if (balance === null) setError(reason instanceof Error ? reason.message : String(reason));
4317
4415
  });
4318
- const sessionSpendRequest = Promise.resolve().then(() => getSessionSpend(sessionId)).then((value) => {
4319
- if (!current) return;
4320
- setSpend(value);
4321
- }, () => {});
4322
- const todaySpendRequest = Promise.resolve().then(() => getTodaySpend(request > 0)).then((value) => {
4323
- if (!current) return;
4324
- setTodaySpend(value);
4325
- }, () => {});
4416
+ const sessionSpendRequest = fetchLine(isCurrent, () => getSessionSpend(sessionId), setSpend);
4417
+ const todaySpendRequest = fetchLine(isCurrent, () => getTodaySpend(request > 0), setTodaySpend);
4418
+ const sessionsSpendRequest = fetchLine(isCurrent, () => getTodaySessionsSpend(request > 0), setSessionsSpend);
4326
4419
  Promise.allSettled([
4327
4420
  balanceRequest,
4328
4421
  sessionSpendRequest,
4329
- todaySpendRequest
4422
+ todaySpendRequest,
4423
+ sessionsSpendRequest
4330
4424
  ]).then(() => {
4331
4425
  if (current) setRefreshing(false);
4332
4426
  });
@@ -4338,6 +4432,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4338
4432
  getBalance,
4339
4433
  getSessionSpend,
4340
4434
  getTodaySpend,
4435
+ getTodaySessionsSpend,
4341
4436
  sessionId,
4342
4437
  request
4343
4438
  ]);
@@ -4346,17 +4441,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4346
4441
  pricedRunningRef.current = running;
4347
4442
  if (running) return;
4348
4443
  let current = true;
4444
+ const isCurrent = () => current;
4349
4445
  const timer = setTimeout(() => {
4350
- Promise.resolve().then(() => {
4351
- Promise.resolve().then(() => getSessionSpend(sessionId)).then((value) => {
4352
- if (!current) return;
4353
- setSpend(value);
4354
- }, () => {});
4355
- Promise.resolve().then(() => getTodaySpend()).then((value) => {
4356
- if (!current) return;
4357
- setTodaySpend(value);
4358
- }, () => {});
4359
- });
4446
+ fetchLine(isCurrent, () => getSessionSpend(sessionId), setSpend);
4447
+ fetchLine(isCurrent, () => getTodaySpend(), setTodaySpend);
4448
+ fetchLine(isCurrent, () => getTodaySessionsSpend(), setSessionsSpend);
4360
4449
  }, 2e3);
4361
4450
  return () => {
4362
4451
  clearTimeout(timer);
@@ -4365,6 +4454,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4365
4454
  }, [
4366
4455
  getSessionSpend,
4367
4456
  getTodaySpend,
4457
+ getTodaySessionsSpend,
4368
4458
  sessionId,
4369
4459
  running
4370
4460
  ]);
@@ -4402,7 +4492,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4402
4492
  };
4403
4493
  const line = primaryLine(balance);
4404
4494
  const amount = line === void 0 ? "—" : `${line.symbol}${line.total}`;
4405
- const spendLine = spend !== null && spend.models.length > 0 ? t("trigger.conversationSpend", { amount: formatSpend(spend.total) }) : void 0;
4495
+ const spendLine = spend !== null && spend.models.length > 0 ? t("trigger.conversationSpend", { amount: formatSpend$1(spend.total) }) : void 0;
4406
4496
  return (0, react_jsx_runtime.jsxs)("div", {
4407
4497
  ref: rootRef,
4408
4498
  className: BalanceBadge_module_css_default.root,
@@ -4460,10 +4550,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4460
4550
  className: BalanceBadge_module_css_default.spendRow,
4461
4551
  children: [(0, react_jsx_runtime.jsx)("span", {
4462
4552
  className: BalanceBadge_module_css_default.amountLabel,
4463
- children: t("label.sessionSpend", { amount: spend === null ? "—" : spend.models.length === 0 ? t("stat.none") : formatSpend(spend.total) })
4553
+ children: t("label.sessionSpend", { amount: spend === null ? "—" : spend.models.length === 0 ? t("stat.none") : formatSpend$1(spend.total) })
4464
4554
  }), (0, react_jsx_runtime.jsx)("span", {
4465
4555
  className: BalanceBadge_module_css_default.amountLabel,
4466
- children: t("label.todaySpend", { amount: todaySpend === null ? "—" : todaySpend.models.length === 0 ? t("stat.none") : formatSpend(todaySpend.total) })
4556
+ children: t("label.todaySpend", { amount: todaySpend === null ? "—" : todaySpend.models.length === 0 ? t("stat.none") : formatSpend$1(todaySpend.total) })
4467
4557
  })]
4468
4558
  }),
4469
4559
  spend?.models.map((model) => (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
@@ -4473,26 +4563,120 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4473
4563
  children: model.displayName
4474
4564
  }), (0, react_jsx_runtime.jsx)("span", {
4475
4565
  className: BalanceBadge_module_css_default.tasks,
4476
- children: formatSpend(model.cost)
4566
+ children: formatSpend$1(model.cost)
4477
4567
  })]
4478
4568
  }), (0, react_jsx_runtime.jsx)("div", {
4479
4569
  className: BalanceBadge_module_css_default.costRow,
4480
4570
  children: (0, react_jsx_runtime.jsxs)("span", {
4481
4571
  className: BalanceBadge_module_css_default.costBreakdown,
4482
4572
  children: [
4483
- t("label.cost.hit", { amount: formatSpend(model.cacheHitInputCost) }),
4573
+ t("label.cost.hit", { amount: formatSpend$1(model.cacheHitInputCost) }),
4484
4574
  " · ",
4485
- t("label.cost.input", { amount: formatSpend(model.cacheMissInputCost) }),
4575
+ t("label.cost.input", { amount: formatSpend$1(model.cacheMissInputCost) }),
4486
4576
  " · ",
4487
- t("label.cost.output", { amount: formatSpend(model.outputCost) })
4577
+ t("label.cost.output", { amount: formatSpend$1(model.outputCost) })
4488
4578
  ]
4489
4579
  })
4490
- })] }, model.model))
4580
+ })] }, model.model)),
4581
+ sessionsSpend !== null && sessionsSpend.sessions.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
4582
+ className: BalanceBadge_module_css_default.ranking,
4583
+ children: [
4584
+ (0, react_jsx_runtime.jsx)("div", {
4585
+ className: BalanceBadge_module_css_default.rankingTitle,
4586
+ children: t("label.sessionRanking")
4587
+ }),
4588
+ sessionsSpend.sessions.slice(0, 10).map((row, index) => (0, react_jsx_runtime.jsxs)("div", {
4589
+ className: BalanceBadge_module_css_default.rankingRow,
4590
+ children: [
4591
+ (0, react_jsx_runtime.jsx)("span", {
4592
+ className: BalanceBadge_module_css_default.rankingIndex,
4593
+ children: index + 1
4594
+ }),
4595
+ (0, react_jsx_runtime.jsx)("span", {
4596
+ className: BalanceBadge_module_css_default.rankingName,
4597
+ title: row.title ?? void 0,
4598
+ children: row.title ?? t("stat.untitled")
4599
+ }),
4600
+ (0, react_jsx_runtime.jsx)("span", {
4601
+ className: BalanceBadge_module_css_default.rankingAmount,
4602
+ children: formatSpend$1(row.total)
4603
+ })
4604
+ ]
4605
+ }, row.sessionId)),
4606
+ sessionsSpend.sessions.length > 10 && (0, react_jsx_runtime.jsx)("div", {
4607
+ className: BalanceBadge_module_css_default.rankingMore,
4608
+ children: t("label.sessionRanking.more", { count: sessionsSpend.sessions.length - 10 })
4609
+ })
4610
+ ]
4611
+ })
4491
4612
  ]
4492
4613
  }) : null]
4493
4614
  });
4494
4615
  }
4495
4616
  //#endregion
4617
+ //#region \0dsh-css:C:\Users\admin\Desktop\me\code\DeepSeek-Harness-chat-billing\packages\ui-billing\src\client\TurnCostAction.module.css.mjs
4618
+ const css = ".l2XZIa_cost{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:nowrap;flex:none;font-size:12px;line-height:18px}";
4619
+ const tagId = "@rayadesu/dsh-client-ui-billing/TurnCostAction.module.css";
4620
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
4621
+ const tag = document.createElement("style");
4622
+ tag.dataset.plugin = "@rayadesu/dsh-client-ui-billing";
4623
+ tag.dataset.pluginCss = tagId;
4624
+ tag.textContent = css;
4625
+ document.head.appendChild(tag);
4626
+ }
4627
+ var TurnCostAction_module_css_default = { "cost": "l2XZIa_cost" };
4628
+ //#endregion
4629
+ //#region lib/types/client/TurnCostAction.js
4630
+ /** Per-turn billed-cost label in the closing assistant message's actions row. */
4631
+ /** CNY amount, up to four decimals with trailing zeros trimmed. */
4632
+ function formatSpend(amount) {
4633
+ return `¥${amount.toFixed(4).replace(/\.?0+$/, "")}`;
4634
+ }
4635
+ const turnCostCache = /* @__PURE__ */ new Map();
4636
+ const TURN_COST_CACHE_LIMIT = 1024;
4637
+ async function cachedTurnCost(sessionId, messageId, getTurnSpend) {
4638
+ const key = `${sessionId}\0${messageId}`;
4639
+ const cached = turnCostCache.get(key);
4640
+ if (cached !== void 0) return cached;
4641
+ const { total } = await getTurnSpend(sessionId, messageId);
4642
+ if (turnCostCache.size >= TURN_COST_CACHE_LIMIT) turnCostCache.clear();
4643
+ turnCostCache.set(key, total);
4644
+ return total;
4645
+ }
4646
+ /**
4647
+ * Render one Turn's billed cost as a muted label inside the closing message's
4648
+ * actions row, between the built-in copy/branch controls and the message
4649
+ * clock. The label appears only after the cost settles and hides again when
4650
+ * the Turn priced to zero (no DeepSeek usage); a failed fetch stays hidden so
4651
+ * a Remote outage never clutters the row.
4652
+ * @param props - the closing message id, the session runtime share, and the injected cost reader.
4653
+ * @returns the cost label, or null while loading, on failure, or for zero cost.
4654
+ */
4655
+ function TurnCostAction({ messageId, sessionId, getTurnSpend, t }) {
4656
+ const [cost, setCost] = (0, react.useState)(null);
4657
+ (0, react.useEffect)(() => {
4658
+ let current = true;
4659
+ Promise.resolve().then(() => cachedTurnCost(sessionId, messageId, getTurnSpend)).then((total) => {
4660
+ if (current) setCost(total);
4661
+ }, () => {
4662
+ if (current) setCost(null);
4663
+ });
4664
+ return () => {
4665
+ current = false;
4666
+ };
4667
+ }, [
4668
+ getTurnSpend,
4669
+ messageId,
4670
+ sessionId
4671
+ ]);
4672
+ if (cost === null || cost <= 0) return null;
4673
+ return (0, react_jsx_runtime.jsx)("span", {
4674
+ className: TurnCostAction_module_css_default.cost,
4675
+ "data-turn-cost": true,
4676
+ children: t("turnCost", { amount: formatSpend(cost) })
4677
+ });
4678
+ }
4679
+ //#endregion
4496
4680
  //#region lib/types/client/locales.js
4497
4681
  /** `billing` namespace dictionaries. */
4498
4682
  /** Dictionary namespace owned by this plugin. */
@@ -4508,12 +4692,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4508
4692
  "label.cost.input": "未命中输入 {amount}",
4509
4693
  "label.cost.output": "输出 {amount}",
4510
4694
  "stat.none": "暂无消耗记录",
4695
+ "stat.untitled": "未命名",
4511
4696
  "state.unavailable": "额度不可用",
4512
4697
  "action.refresh": "刷新",
4513
4698
  "info.aria": "花费说明",
4514
4699
  "info.hint": "仅能预估 DeepSeek 相关模型。本会话花费按每条消息的发生时刻(北京时间)所在峰谷时段单价计费:缓存命中输入、未命中输入(含缓存写入)、输出(含推理)分别计价。费率按 8 月 17 日实行的标准,高峰时段为北京时间周一至周五 9:00–12:00、14:00–18:00,周末全天按低谷价计费。",
4515
4700
  "badge.aria": "DeepSeek 额度:{amount}",
4516
- "panel.aria": "DeepSeek 额度详情"
4701
+ "panel.aria": "DeepSeek 额度详情",
4702
+ "turnCost": "本轮花费:{amount}",
4703
+ "label.sessionRanking": "今日会话花费",
4704
+ "label.sessionRanking.more": "…还有 {count} 个会话"
4517
4705
  };
4518
4706
  /** English dictionary, key-identical to the Chinese source of truth. */
4519
4707
  const en = {
@@ -4526,12 +4714,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4526
4714
  "label.cost.input": "Missed input {amount}",
4527
4715
  "label.cost.output": "Output {amount}",
4528
4716
  "stat.none": "No usage recorded",
4717
+ "stat.untitled": "Untitled",
4529
4718
  "state.unavailable": "Balance unavailable",
4530
4719
  "action.refresh": "Refresh",
4531
4720
  "info.aria": "About this spend",
4532
4721
  "info.hint": "Only DeepSeek models are estimated. This session's spend is priced per message at the rate of its Beijing-time peak/off-peak hour: cache-hit input, cache-miss input (including cache writes), and output (including reasoning) are billed separately. Rates follow the August 17 pricing; peak hours are Beijing time Monday–Friday 09:00–12:00 and 14:00–18:00, and weekends are billed at off-peak prices all day.",
4533
4722
  "badge.aria": "DeepSeek balance {amount}",
4534
- "panel.aria": "DeepSeek balance details"
4723
+ "panel.aria": "DeepSeek balance details",
4724
+ "turnCost": "This turn: {amount}",
4725
+ "label.sessionRanking": "Today session spend",
4726
+ "label.sessionRanking.more": "…{count} more sessions"
4535
4727
  };
4536
4728
  //#endregion
4537
4729
  //#region lib/types/client/index.js
@@ -4542,6 +4734,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4542
4734
  "locale",
4543
4735
  "remote"
4544
4736
  ];
4737
+ /** Unwrap one Remote result into its value, reporting the endpoint on failure. */
4738
+ function unwrap(endpoint, result) {
4739
+ if (!result.ok) throw new Error(`${endpoint} failed: ${result.error.code}: ${result.error.message}`);
4740
+ return result.value;
4741
+ }
4545
4742
  /**
4546
4743
  * Client plugin body: mount the `billing` Remote, register the dictionaries,
4547
4744
  * and contribute the header utility badge.
@@ -4555,30 +4752,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4555
4752
  zh,
4556
4753
  en
4557
4754
  }), "ui-billing: dictionaries");
4558
- const injected = () => ({
4559
- getBalance: async () => {
4560
- const result = await billing.getBalance();
4561
- if (!result.ok) throw new Error(`billing.getBalance failed: ${result.error.code}: ${result.error.message}`);
4562
- return result.value;
4563
- },
4564
- getSessionSpend: async (sessionId) => {
4565
- const result = await billing.getSessionSpend(sessionId);
4566
- if (!result.ok) throw new Error(`billing.getSessionSpend failed: ${result.error.code}: ${result.error.message}`);
4567
- return result.value;
4568
- },
4569
- getTodaySpend: async (force) => {
4570
- const result = await billing.getTodaySpend(force);
4571
- if (!result.ok) throw new Error(`billing.getTodaySpend failed: ${result.error.code}: ${result.error.message}`);
4572
- return result.value;
4573
- }
4574
- });
4755
+ const injected = {
4756
+ getBalance: async () => unwrap("billing.getBalance", await billing.getBalance()),
4757
+ getSessionSpend: async (sessionId) => unwrap("billing.getSessionSpend", await billing.getSessionSpend(sessionId)),
4758
+ getTodaySpend: async (force) => unwrap("billing.getTodaySpend", await billing.getTodaySpend(force)),
4759
+ getTodaySessionsSpend: async (force) => unwrap("billing.getTodaySessionsSpend", await billing.getTodaySessionsSpend(force)),
4760
+ getTurnSpend: async (sessionId, messageId) => unwrap("billing.getTurnSpend", await billing.getTurnSpend(sessionId, messageId))
4761
+ };
4575
4762
  ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
4576
4763
  name: "conversation.session.header.utilities",
4577
4764
  id: "billing-balance",
4578
4765
  order: 30,
4579
4766
  locale: NS,
4580
- inject: injected
4767
+ inject: () => injected
4581
4768
  }, BalanceBadge));
4769
+ const turnCostInjected = { getTurnSpend: injected.getTurnSpend };
4770
+ ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
4771
+ name: "conversation.chat.assistant-actions",
4772
+ id: "billing-turn-cost",
4773
+ order: 20,
4774
+ locale: NS,
4775
+ inject: () => turnCostInjected
4776
+ }, TurnCostAction));
4582
4777
  }
4583
4778
  //#endregion
4584
4779
  exports.apply = apply;
@@ -1,5 +1,5 @@
1
1
  /** Session-header billing badge: balance plus the current conversation's billed spend. */
2
- import type { DeepSeekBalance, DeepSeekSessionSpend, DeepSeekTodaySpend } from '@rayadesu/dsh-llm-billing/types';
2
+ import type { DeepSeekBalance, DeepSeekSessionSpend, DeepSeekTodaySessionsSpend, DeepSeekTodaySpend, DeepSeekTurnSpend } from '@rayadesu/dsh-llm-billing/types';
3
3
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
4
4
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
5
5
  import { NS } from './locales.ts';
@@ -15,9 +15,18 @@ export interface BalanceBadgeInjected {
15
15
  * passes it, the turn-triggered recompute does not.
16
16
  */
17
17
  getTodaySpend: (force?: boolean) => Promise<DeepSeekTodaySpend>;
18
+ /**
19
+ * Read today's billed spend per session, sorted by cost descending; rejects
20
+ * with the Remote error message. `force` behaves as in {@link getTodaySpend}.
21
+ */
22
+ getTodaySessionsSpend: (force?: boolean) => Promise<DeepSeekTodaySessionsSpend>;
23
+ /** Read one completed Turn's billed cost, located by its closing message id. */
24
+ getTurnSpend: (sessionId: SessionId, messageId: string) => Promise<DeepSeekTurnSpend>;
18
25
  }
19
26
  /** Full props assembled by the header utilities slot renderer. */
20
27
  export type BalanceBadgeProps = PropsRuntime<'conversation.session.header.utilities'> & InjectFace<BalanceBadgeInjected> & PropsLocale<typeof NS>;
28
+ /** How many per-session ranking rows the panel shows before the overflow hint. */
29
+ export declare const SESSION_RANKING_LIMIT = 10;
21
30
  /**
22
31
  * Render the billing badge in the session-header utilities row. The trigger
23
32
  * shows the remaining balance and this conversation's billed spend and opens
@@ -34,5 +43,5 @@ export type BalanceBadgeProps = PropsRuntime<'conversation.session.header.utilit
34
43
  * @param props - Remote face, locale, and the standard session-header runtime share.
35
44
  * @returns the badge, or null until the first balance fetch settles.
36
45
  */
37
- export declare function BalanceBadge({ getBalance, getSessionSpend, getTodaySpend, sessionId, useSession, t }: BalanceBadgeProps): import("react").JSX.Element | null;
46
+ export declare function BalanceBadge({ getBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, sessionId, useSession, t }: BalanceBadgeProps): import("react").JSX.Element | null;
38
47
  //# sourceMappingURL=BalanceBadge.d.ts.map
@@ -0,0 +1,28 @@
1
+ /** Per-turn billed-cost label in the closing assistant message's actions row. */
2
+ import type { DeepSeekTurnSpend } from '@rayadesu/dsh-llm-billing/types';
3
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
4
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
5
+ import { NS } from './locales.ts';
6
+ /** Injected face: one Turn's billed cost through the billing Remote. */
7
+ export interface TurnCostActionInjected {
8
+ /** Read one completed Turn's billed cost, located by its closing message id. */
9
+ getTurnSpend: (sessionId: SessionId, messageId: string) => Promise<DeepSeekTurnSpend>;
10
+ }
11
+ /**
12
+ * Full props of the per-turn cost entry. The slot's owner share
13
+ * (`AssistantActionOwnerProps.messageId`) arrives from ui-conversation's
14
+ * declared `conversation.chat.assistant-actions` slot; the session runtime
15
+ * share adds `sessionId`.
16
+ */
17
+ export type TurnCostActionProps = PropsRuntime<'conversation.chat.assistant-actions'> & InjectFace<TurnCostActionInjected> & PropsLocale<typeof NS>;
18
+ /**
19
+ * Render one Turn's billed cost as a muted label inside the closing message's
20
+ * actions row, between the built-in copy/branch controls and the message
21
+ * clock. The label appears only after the cost settles and hides again when
22
+ * the Turn priced to zero (no DeepSeek usage); a failed fetch stays hidden so
23
+ * a Remote outage never clutters the row.
24
+ * @param props - the closing message id, the session runtime share, and the injected cost reader.
25
+ * @returns the cost label, or null while loading, on failure, or for zero cost.
26
+ */
27
+ export declare function TurnCostAction({ messageId, sessionId, getTurnSpend, t }: TurnCostActionProps): import("react").JSX.Element | null;
28
+ //# sourceMappingURL=TurnCostAction.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 { TurnCostActionInjected, TurnCostActionProps } from './TurnCostAction.tsx';
5
6
  export type { BillingKey } from './locales.ts';
6
7
  declare module '@deepseek-ai/dsh-client-ui-slots' {
7
8
  interface LocaleNamespaceMap {
@@ -12,12 +12,16 @@ export declare const zh: {
12
12
  readonly 'label.cost.input': "未命中输入 {amount}";
13
13
  readonly 'label.cost.output': "输出 {amount}";
14
14
  readonly 'stat.none': "暂无消耗记录";
15
+ readonly 'stat.untitled': "未命名";
15
16
  readonly 'state.unavailable': "额度不可用";
16
17
  readonly 'action.refresh': "刷新";
17
18
  readonly 'info.aria': "花费说明";
18
19
  readonly 'info.hint': "仅能预估 DeepSeek 相关模型。本会话花费按每条消息的发生时刻(北京时间)所在峰谷时段单价计费:缓存命中输入、未命中输入(含缓存写入)、输出(含推理)分别计价。费率按 8 月 17 日实行的标准,高峰时段为北京时间周一至周五 9:00–12:00、14:00–18:00,周末全天按低谷价计费。";
19
20
  readonly 'badge.aria': "DeepSeek 额度:{amount}";
20
21
  readonly 'panel.aria': "DeepSeek 额度详情";
22
+ readonly turnCost: "本轮花费:{amount}";
23
+ readonly 'label.sessionRanking': "今日会话花费";
24
+ readonly 'label.sessionRanking.more': "…还有 {count} 个会话";
21
25
  };
22
26
  /** English dictionary, key-identical to the Chinese source of truth. */
23
27
  export declare const en: Record<BillingKey, string>;
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.2.2",
4
+ "version": "0.2.4",
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.2.2",
50
+ "@rayadesu/dsh-llm-billing": "^0.2.4",
51
51
  "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
52
52
  "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
53
53
  "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2",
@@ -58,7 +58,7 @@
58
58
  "@deepseek-ai/dsh-session": "^0.1.1-rc.2"
59
59
  },
60
60
  "devDependencies": {
61
- "@rayadesu/dsh-llm-billing": "^0.2.2",
61
+ "@rayadesu/dsh-llm-billing": "^0.2.4",
62
62
  "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
63
63
  "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
64
64
  "@deepseek-ai/dsh-client-test-runtime": "^0.1.1-rc.2",