@rayadesu/dsh-client-ui-billing 0.3.8 → 0.3.10
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 +10 -9
- package/README.zh.md +10 -9
- package/lib/client.js +371 -203
- package/lib/types/client/BalanceBadge.d.ts +28 -21
- package/lib/types/client/BalancePanel.d.ts +19 -0
- package/lib/types/client/BalanceTrigger.d.ts +17 -0
- package/lib/types/client/TurnCostAction.d.ts +9 -6
- package/lib/types/client/format.d.ts +17 -0
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/turnCostStore.d.ts +30 -0
- package/lib/types/client/useBillingData.d.ts +30 -0
- package/package.json +3 -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: cd35c163925d48ff81774fefd45ea8deb06d59bd
|
|
6
|
+
README.zh.md: 7d06d48a6da7337faa1acea887b59a6624a81ea3
|
package/README.md
CHANGED
|
@@ -2,21 +2,22 @@
|
|
|
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
|
|
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
|
|
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 (今日共花费), 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. The remaining-amount row (`API 剩余金额`) renders larger than the spend rows below it. 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
|
-
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
|
|
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
|
-
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
|
|
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 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
12
|
|
|
13
13
|
## Refresh mechanics
|
|
14
14
|
|
|
15
|
-
The spends follow the conversation and the balance
|
|
15
|
+
The spends follow the conversation and the balance is cached, not polled:
|
|
16
16
|
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
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.
|
|
19
|
+
- **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.
|
|
20
21
|
|
|
21
22
|
## Model Experience
|
|
22
23
|
|
|
@@ -31,4 +32,4 @@ None; the package never assembles or sends provider requests, and its one RPC is
|
|
|
31
32
|
- **First balance line only** — the balance reads the primary (`balance_infos[0]`) currency line; other currency lines are not shown.
|
|
32
33
|
- **Ranking capped at 10** — the panel shows at most `SESSION_RANKING_LIMIT` sessions, with a "…N more sessions" hint.
|
|
33
34
|
- **Turn cost needs a finalized closing message** — interrupted turns have no actions row, so no turn cost is shown for them.
|
|
34
|
-
- **Balance
|
|
35
|
+
- **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,21 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-
Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡献一个条目,自己挂载 `billing` Remote
|
|
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」)——有上次已落定的余额就立即渲染(按插件实例保留),后台校验;首次余额尚未到达时不渲染任何东西。点击后展开一个标签框:剩余金额、本会话的计费花费(本会话花费,按官方峰/谷单价逐条用量样本计价——高峰窗口仅周一至周五适用,周末按低谷价——分叉会话只按分叉后的自有工作计价——右侧并列今日所有会话的合计「今日共花费」)、每个模型一行的花费及其缓存命中/未命中输入/输出分项(「缓存命中 ¥X · 未命中输入 ¥Y · 输出 ¥Z」)、手动刷新按钮与花费说明。剩余金额行(`API 剩余金额`)字号大于下方花费行。模型行下方是**今日会话花费排行**(`今日会话花费`):按今日花费从高到低排列的会话列表,每行以右对齐的名次列开头,后接与花费分项相同的小圆点分隔符(「·」,字号、色调与间距一致),再是会话的持久标题(重命名自动同步),最后是金额——标题在名次列后缩进对齐;最多渲染 `SESSION_RANKING_LIMIT`(10)行,其余以「…还有 N 个会话」提示。刷新时旧值不消失,刷新失败保留上一次有效值。没有可计价消耗的会话或日子显示「暂无消耗记录」而不是编造数字。失败——未配置 API key、凭据被拒绝、传输错误——渲染弱化的「额度不可用」,其提示携带 Remote 自己的错误信息。
|
|
8
8
|
|
|
9
|
-
本包还贡献**本轮花费条目**(`billing-turn-cost`,order 20)到 ui-chat 的 `conversation.chat.assistant-actions` list slot——与 `ui-message-feedback`
|
|
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
|
-
|
|
11
|
+
角标是账户级的,尽管插槽是会话作用域:顶栏工具行不过是外壳提供的那个始终可见的标题栏席位,余额并不随会话变化——但花费行会,因为它按当前会话自己的用量(推送的投影,或 `billing/getSessionSpend`)计价、通过 `billing/getTodaySpend` 按所有会话的用量计价。文案走本包自己的 `billing` locale 命名空间;样式只用 token。
|
|
12
12
|
|
|
13
13
|
## 数据更新机制
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
花费跟随会话、额度带缓存但不轮询:
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
17
|
+
- **本会话花费靠推送,不靠拉取** —— 宿主把每个会话的花费折进客户端可见的 `billingTodaySpend` 投影并推送每次变化;徽标用框架 `useProjection` 座位订阅,因此投影注册表装配时该行零 Remote 调用、实时更新。键不存在时同一行回退到 `billing/getSessionSpend`。
|
|
18
|
+
- **今日共花费随回合结束更新(去抖)** —— 组件通过框架 `useSession` 座位订阅当前会话的「运行中」标志。每当一轮 prompt 回合结束(标志翻回空闲)就重算**今日共花费**(`billing/getTodaySpend`);重算有 2 秒去抖,回合突发(agent 连续多轮)只计价一次。**排行**(`billing/getTodaySessionsSpend`)只在详情面板打开时(以及手动刷新时)拉取,徽标一直关着就不会为全量会话扫描买单。
|
|
19
|
+
- **额度带 TTL 缓存** —— 账户余额在挂载、切换会话、点刷新按钮时拉取。宿主 15 秒内复用同一份快照(手动刷新带 `force` 绕过),单次请求 5 秒超时;浏览器保留最后一次已落定值,切会话时立即渲染。仍然**没有轮询、不会自动重拉**。
|
|
20
|
+
- **刷新保留旧值** —— 刷新在途时旧值留在屏幕上,刷新失败保留上一次有效值,不清空。刷新按钮调用 `billing/getBalance(true)`、`billing/getTodaySpend(true)` 与 `billing/getTodaySessionsSpend(true)`,绕过宿主侧缓存;回合结束触发的重算与挂载/切换会话的读取走缓存路径。
|
|
20
21
|
|
|
21
22
|
## 模型体验
|
|
22
23
|
|
|
@@ -31,4 +32,4 @@ Web 额度特性的归属方:向 `conversation.session.header.utilities` 贡
|
|
|
31
32
|
- **只取首条余额线** —— 余额读取主要(`balance_infos[0]`)币种行;其它币种行不显示。
|
|
32
33
|
- **排行上限 10** —— 面板最多显示 `SESSION_RANKING_LIMIT` 个会话,其余以「…还有 N 个会话」提示。
|
|
33
34
|
- **本轮花费需要已定稿的收尾消息** —— 中断的回合没有操作行,不显示本轮花费。
|
|
34
|
-
-
|
|
35
|
+
- **额度最多旧 15 秒** —— 宿主 15 秒内复用同一份快照、浏览器保留最后一次已落定值:其他客户端产生的消耗不会让界面数值自动变化,需等 TTL 过期、手动刷新或刷新浏览器(仍无轮询)。
|
package/lib/client.js
CHANGED
|
@@ -5,8 +5,8 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
-
let react = require("react");
|
|
9
8
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react = require("react");
|
|
10
10
|
//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
|
|
11
11
|
var _a$1;
|
|
12
12
|
function $constructor(name, initializer, params) {
|
|
@@ -4094,6 +4094,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4094
4094
|
}
|
|
4095
4095
|
//#endregion
|
|
4096
4096
|
//#region ../llm-billing/lib/typert.remote-client.js
|
|
4097
|
+
const _rayadesu_dsh_llm_billing_billing_getBalance_parameter_0$schema = union([
|
|
4098
|
+
_undefined(),
|
|
4099
|
+
literal(false),
|
|
4100
|
+
literal(true)
|
|
4101
|
+
]);
|
|
4097
4102
|
const _rayadesu_dsh_llm_billing_billing_getBalance_result$schema = object({
|
|
4098
4103
|
"isAvailable": boolean(),
|
|
4099
4104
|
"lines": array(object({
|
|
@@ -4120,6 +4125,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4120
4125
|
"outputCost": number()
|
|
4121
4126
|
}))
|
|
4122
4127
|
});
|
|
4128
|
+
const _rayadesu_dsh_llm_billing_billing_getSessionTurnSpends_parameter_0$schema = intersection(string(), unknown());
|
|
4129
|
+
const _rayadesu_dsh_llm_billing_billing_getSessionTurnSpends_result$schema = object({ "turns": array(object({
|
|
4130
|
+
"messageId": string(),
|
|
4131
|
+
"total": number()
|
|
4132
|
+
})) });
|
|
4123
4133
|
const _rayadesu_dsh_llm_billing_billing_getTodaySessionsSpend_parameter_0$schema = union([
|
|
4124
4134
|
_undefined(),
|
|
4125
4135
|
literal(false),
|
|
@@ -4163,7 +4173,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4163
4173
|
namespace: "billing",
|
|
4164
4174
|
method: "getBalance",
|
|
4165
4175
|
invocation: { kind: "direct" },
|
|
4166
|
-
parameters: [
|
|
4176
|
+
parameters: [{
|
|
4177
|
+
name: "force",
|
|
4178
|
+
wire: "force",
|
|
4179
|
+
source: "json",
|
|
4180
|
+
acceptsUndefined: true,
|
|
4181
|
+
codec: {
|
|
4182
|
+
mode: "strict",
|
|
4183
|
+
typeSymbol: "@rayadesu/dsh-llm-billing#billing/getBalance:force",
|
|
4184
|
+
schema: _rayadesu_dsh_llm_billing_billing_getBalance_parameter_0$schema
|
|
4185
|
+
}
|
|
4186
|
+
}],
|
|
4167
4187
|
result: {
|
|
4168
4188
|
mode: "strict",
|
|
4169
4189
|
typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekBalance",
|
|
@@ -4171,7 +4191,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4171
4191
|
},
|
|
4172
4192
|
sourceLocation: {
|
|
4173
4193
|
"file": "packages/llm-billing/src/balance.ts",
|
|
4174
|
-
"line":
|
|
4194
|
+
"line": 165,
|
|
4175
4195
|
"column": 3
|
|
4176
4196
|
}
|
|
4177
4197
|
},
|
|
@@ -4198,7 +4218,34 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4198
4218
|
},
|
|
4199
4219
|
sourceLocation: {
|
|
4200
4220
|
"file": "packages/llm-billing/src/balance.ts",
|
|
4201
|
-
"line":
|
|
4221
|
+
"line": 176,
|
|
4222
|
+
"column": 3
|
|
4223
|
+
}
|
|
4224
|
+
},
|
|
4225
|
+
{
|
|
4226
|
+
id: "@rayadesu/dsh-llm-billing#billing/getSessionTurnSpends",
|
|
4227
|
+
service: "billing",
|
|
4228
|
+
namespace: "billing",
|
|
4229
|
+
method: "getSessionTurnSpends",
|
|
4230
|
+
invocation: { kind: "direct" },
|
|
4231
|
+
parameters: [{
|
|
4232
|
+
name: "sessionId",
|
|
4233
|
+
wire: "sessionId",
|
|
4234
|
+
source: "json",
|
|
4235
|
+
codec: {
|
|
4236
|
+
mode: "strict",
|
|
4237
|
+
typeSymbol: "@rayadesu/dsh-llm-billing#billing/getSessionTurnSpends:sessionId",
|
|
4238
|
+
schema: _rayadesu_dsh_llm_billing_billing_getSessionTurnSpends_parameter_0$schema
|
|
4239
|
+
}
|
|
4240
|
+
}],
|
|
4241
|
+
result: {
|
|
4242
|
+
mode: "strict",
|
|
4243
|
+
typeSymbol: "@rayadesu/dsh-llm-billing/types#DeepSeekSessionTurnSpends",
|
|
4244
|
+
schema: _rayadesu_dsh_llm_billing_billing_getSessionTurnSpends_result$schema
|
|
4245
|
+
},
|
|
4246
|
+
sourceLocation: {
|
|
4247
|
+
"file": "packages/llm-billing/src/balance.ts",
|
|
4248
|
+
"line": 227,
|
|
4202
4249
|
"column": 3
|
|
4203
4250
|
}
|
|
4204
4251
|
},
|
|
@@ -4226,7 +4273,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4226
4273
|
},
|
|
4227
4274
|
sourceLocation: {
|
|
4228
4275
|
"file": "packages/llm-billing/src/balance.ts",
|
|
4229
|
-
"line":
|
|
4276
|
+
"line": 202,
|
|
4230
4277
|
"column": 3
|
|
4231
4278
|
}
|
|
4232
4279
|
},
|
|
@@ -4254,7 +4301,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4254
4301
|
},
|
|
4255
4302
|
sourceLocation: {
|
|
4256
4303
|
"file": "packages/llm-billing/src/balance.ts",
|
|
4257
|
-
"line":
|
|
4304
|
+
"line": 189,
|
|
4258
4305
|
"column": 3
|
|
4259
4306
|
}
|
|
4260
4307
|
},
|
|
@@ -4290,13 +4337,34 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4290
4337
|
},
|
|
4291
4338
|
sourceLocation: {
|
|
4292
4339
|
"file": "packages/llm-billing/src/balance.ts",
|
|
4293
|
-
"line":
|
|
4340
|
+
"line": 215,
|
|
4294
4341
|
"column": 3
|
|
4295
4342
|
}
|
|
4296
4343
|
}
|
|
4297
4344
|
]
|
|
4298
4345
|
};
|
|
4299
4346
|
//#endregion
|
|
4347
|
+
//#region lib/types/client/format.js
|
|
4348
|
+
/** Currency prefix for one balance line; unknown codes render as a literal prefix. */
|
|
4349
|
+
function currencySymbol(currency) {
|
|
4350
|
+
if (currency === "CNY") return "¥";
|
|
4351
|
+
if (currency === "USD") return "$";
|
|
4352
|
+
return `${currency} `;
|
|
4353
|
+
}
|
|
4354
|
+
/** The primary balance line, or undefined when the provider reports none. */
|
|
4355
|
+
function primaryLine(balance) {
|
|
4356
|
+
const line = balance.lines[0];
|
|
4357
|
+
if (line === void 0) return void 0;
|
|
4358
|
+
return {
|
|
4359
|
+
symbol: currencySymbol(line.currency),
|
|
4360
|
+
total: line.total
|
|
4361
|
+
};
|
|
4362
|
+
}
|
|
4363
|
+
/** CNY amount, up to four decimals with trailing zeros trimmed. */
|
|
4364
|
+
function formatSpend(amount) {
|
|
4365
|
+
return `¥${amount.toFixed(4).replace(/\.?0+$/, "")}`;
|
|
4366
|
+
}
|
|
4367
|
+
//#endregion
|
|
4300
4368
|
//#region \0dsh-css:C:\Users\admin\Desktop\me\code\DeepSeek-Harness-chat-billing\packages\ui-billing\src\client\BalanceBadge.module.css.mjs
|
|
4301
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}";
|
|
4302
4370
|
const tagId$1 = "@rayadesu/dsh-client-ui-billing/BalanceBadge.module.css";
|
|
@@ -4339,28 +4407,147 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4339
4407
|
"triggerLines": "_4_Wqcq_triggerLines",
|
|
4340
4408
|
"unavailable": "_4_Wqcq_unavailable"
|
|
4341
4409
|
};
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4410
|
+
/** The detail box opened from the badge trigger. */
|
|
4411
|
+
function BalancePanel({ amount, spend, todaySpend, sessionsSpend, refreshing, onRefresh, t }) {
|
|
4412
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
4413
|
+
className: BalanceBadge_module_css_default.panel,
|
|
4414
|
+
role: "dialog",
|
|
4415
|
+
"aria-label": t("panel.aria"),
|
|
4416
|
+
children: [
|
|
4417
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
4418
|
+
className: BalanceBadge_module_css_default.amountRow,
|
|
4419
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
4420
|
+
className: BalanceBadge_module_css_default.amountLabel,
|
|
4421
|
+
children: t("label.amount", { amount })
|
|
4422
|
+
}), (0, react_jsx_runtime.jsxs)("span", {
|
|
4423
|
+
className: BalanceBadge_module_css_default.amountActions,
|
|
4424
|
+
children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
4425
|
+
label: t("info.hint"),
|
|
4426
|
+
delayMs: 200,
|
|
4427
|
+
maxWidth: 340,
|
|
4428
|
+
children: (0, react_jsx_runtime.jsx)("button", {
|
|
4429
|
+
type: "button",
|
|
4430
|
+
className: BalanceBadge_module_css_default.infoButton,
|
|
4431
|
+
"aria-label": t("info.aria"),
|
|
4432
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQuestionOutline14, { className: BalanceBadge_module_css_default.inlineIcon })
|
|
4433
|
+
})
|
|
4434
|
+
}), (0, react_jsx_runtime.jsx)("button", {
|
|
4435
|
+
type: "button",
|
|
4436
|
+
className: BalanceBadge_module_css_default.refreshButton,
|
|
4437
|
+
onClick: onRefresh,
|
|
4438
|
+
"aria-label": t("action.refresh"),
|
|
4439
|
+
"data-refreshing": refreshing || void 0,
|
|
4440
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, { className: refreshing ? BalanceBadge_module_css_default.spinning : BalanceBadge_module_css_default.inlineIcon })
|
|
4441
|
+
})]
|
|
4442
|
+
})]
|
|
4443
|
+
}),
|
|
4444
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
4445
|
+
className: BalanceBadge_module_css_default.spendRow,
|
|
4446
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
4447
|
+
className: BalanceBadge_module_css_default.amountLabel,
|
|
4448
|
+
children: t("label.sessionSpend", { amount: spend === null ? "—" : spend.models.length === 0 ? t("stat.none") : formatSpend(spend.total) })
|
|
4449
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
4450
|
+
className: BalanceBadge_module_css_default.amountLabel,
|
|
4451
|
+
children: t("label.todaySpend", { amount: todaySpend === null ? "—" : todaySpend.models.length === 0 ? t("stat.none") : formatSpend(todaySpend.total) })
|
|
4452
|
+
})]
|
|
4453
|
+
}),
|
|
4454
|
+
spend?.models.map((model) => (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
4455
|
+
className: BalanceBadge_module_css_default.modelRow,
|
|
4456
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
4457
|
+
className: BalanceBadge_module_css_default.modelName,
|
|
4458
|
+
children: model.displayName
|
|
4459
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
4460
|
+
className: BalanceBadge_module_css_default.tasks,
|
|
4461
|
+
children: formatSpend(model.cost)
|
|
4462
|
+
})]
|
|
4463
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
4464
|
+
className: BalanceBadge_module_css_default.costRow,
|
|
4465
|
+
children: (0, react_jsx_runtime.jsxs)("span", {
|
|
4466
|
+
className: BalanceBadge_module_css_default.costBreakdown,
|
|
4467
|
+
children: [
|
|
4468
|
+
t("label.cost.hit", { amount: formatSpend(model.cacheHitInputCost) }),
|
|
4469
|
+
" · ",
|
|
4470
|
+
t("label.cost.input", { amount: formatSpend(model.cacheMissInputCost) }),
|
|
4471
|
+
" · ",
|
|
4472
|
+
t("label.cost.output", { amount: formatSpend(model.outputCost) })
|
|
4473
|
+
]
|
|
4474
|
+
})
|
|
4475
|
+
})] }, model.model)),
|
|
4476
|
+
sessionsSpend !== null && sessionsSpend.sessions.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
|
|
4477
|
+
className: BalanceBadge_module_css_default.ranking,
|
|
4478
|
+
children: [
|
|
4479
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
4480
|
+
className: BalanceBadge_module_css_default.rankingTitle,
|
|
4481
|
+
children: t("label.sessionRanking")
|
|
4482
|
+
}),
|
|
4483
|
+
sessionsSpend.sessions.slice(0, 10).map((row, index) => (0, react_jsx_runtime.jsxs)("div", {
|
|
4484
|
+
className: BalanceBadge_module_css_default.rankingRow,
|
|
4485
|
+
children: [
|
|
4486
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
4487
|
+
className: BalanceBadge_module_css_default.rankingIndex,
|
|
4488
|
+
children: index + 1
|
|
4489
|
+
}),
|
|
4490
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
4491
|
+
className: BalanceBadge_module_css_default.rankingDot,
|
|
4492
|
+
"aria-hidden": true,
|
|
4493
|
+
children: " · "
|
|
4494
|
+
}),
|
|
4495
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
4496
|
+
className: BalanceBadge_module_css_default.rankingName,
|
|
4497
|
+
title: row.title ?? void 0,
|
|
4498
|
+
children: row.title ?? t("stat.untitled")
|
|
4499
|
+
}),
|
|
4500
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
4501
|
+
className: BalanceBadge_module_css_default.rankingAmount,
|
|
4502
|
+
children: formatSpend(row.total)
|
|
4503
|
+
})
|
|
4504
|
+
]
|
|
4505
|
+
}, row.sessionId)),
|
|
4506
|
+
sessionsSpend.sessions.length > 10 && (0, react_jsx_runtime.jsx)("div", {
|
|
4507
|
+
className: BalanceBadge_module_css_default.rankingMore,
|
|
4508
|
+
children: t("label.sessionRanking.more", { count: sessionsSpend.sessions.length - 10 })
|
|
4509
|
+
})
|
|
4510
|
+
]
|
|
4511
|
+
})
|
|
4512
|
+
]
|
|
4513
|
+
});
|
|
4359
4514
|
}
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4515
|
+
//#endregion
|
|
4516
|
+
//#region lib/types/client/BalanceTrigger.js
|
|
4517
|
+
/**
|
|
4518
|
+
* Session-header billing trigger: the balance plus the conversation-spend
|
|
4519
|
+
* line. Pure view over the data hook's values — no state, no effects.
|
|
4520
|
+
*/
|
|
4521
|
+
/** The badge button: balance line, spend line, and the open-state chevron. */
|
|
4522
|
+
function BalanceTrigger({ amount, spendLine, open, onToggle, t }) {
|
|
4523
|
+
return (0, react_jsx_runtime.jsxs)("button", {
|
|
4524
|
+
type: "button",
|
|
4525
|
+
className: BalanceBadge_module_css_default.trigger,
|
|
4526
|
+
"aria-expanded": open,
|
|
4527
|
+
"aria-label": t("badge.aria", { amount }),
|
|
4528
|
+
onClick: onToggle,
|
|
4529
|
+
children: [(0, react_jsx_runtime.jsxs)("span", {
|
|
4530
|
+
className: BalanceBadge_module_css_default.triggerLines,
|
|
4531
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
4532
|
+
className: BalanceBadge_module_css_default.linePrimary,
|
|
4533
|
+
children: t("trigger.balance", { amount })
|
|
4534
|
+
}), spendLine !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
4535
|
+
className: BalanceBadge_module_css_default.lineSecondary,
|
|
4536
|
+
children: spendLine
|
|
4537
|
+
})]
|
|
4538
|
+
}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: open ? BalanceBadge_module_css_default.chevronOpen : void 0 })]
|
|
4539
|
+
});
|
|
4363
4540
|
}
|
|
4541
|
+
//#endregion
|
|
4542
|
+
//#region lib/types/client/useBillingData.js
|
|
4543
|
+
/**
|
|
4544
|
+
* Billing badge data hook: owns the balance/session/today states, the
|
|
4545
|
+
* mount+refresh fetch, the turn-settled recompute, and the click-outside
|
|
4546
|
+
* close. The trigger and the panel are pure views over the returned values,
|
|
4547
|
+
* so the concurrency/race handling lives in exactly one file.
|
|
4548
|
+
*/
|
|
4549
|
+
/** Debounce for the turn-settled recompute: a burst of turns prices once. */
|
|
4550
|
+
const TURN_SETTLE_DEBOUNCE_MS = 2e3;
|
|
4364
4551
|
/**
|
|
4365
4552
|
* Run one fetch line: the fetch is deferred to a microtask so the effect's
|
|
4366
4553
|
* render commits before any state update lands; a settled value is stored
|
|
@@ -4376,23 +4563,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4376
4563
|
});
|
|
4377
4564
|
}
|
|
4378
4565
|
/**
|
|
4379
|
-
*
|
|
4380
|
-
*
|
|
4381
|
-
*
|
|
4382
|
-
*
|
|
4383
|
-
*
|
|
4384
|
-
*
|
|
4385
|
-
*
|
|
4386
|
-
* The spend follows the conversation: when a prompt turn settles in the
|
|
4387
|
-
* current session, the badge recomputes only the (local, network-free)
|
|
4388
|
-
* session spend and today's spend through `getSessionSpend` /
|
|
4389
|
-
* `getTodaySpend`; the balance stays a manual-refresh snapshot and is never
|
|
4390
|
-
* refetched on its own.
|
|
4391
|
-
* @param props - Remote face, locale, and the standard session-header runtime share.
|
|
4392
|
-
* @returns the badge, or null until the first balance fetch settles.
|
|
4566
|
+
* Start the badge's data lifecycle for one session. The spend follows the
|
|
4567
|
+
* conversation: the host-pushed `billingTodaySpend` projection drives the
|
|
4568
|
+
* session line live (zero Remote calls), with `getSessionSpend` as the
|
|
4569
|
+
* bootstrap/fallback when the projection key is absent; today's spend is
|
|
4570
|
+
* recomputed through `getTodaySpend` when a turn settles; the balance stays a
|
|
4571
|
+
* manual-refresh snapshot and is never refetched on its own.
|
|
4572
|
+
* @param props - the badge's injected face and session runtime share.
|
|
4393
4573
|
*/
|
|
4394
|
-
function
|
|
4395
|
-
const [balance, setBalance] = (0, react.useState)(
|
|
4574
|
+
function useBillingData({ getBalance, getCachedBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, sessionId, useSession, useProjection }) {
|
|
4575
|
+
const [balance, setBalance] = (0, react.useState)(getCachedBalance);
|
|
4396
4576
|
const [spend, setSpend] = (0, react.useState)(null);
|
|
4397
4577
|
const [todaySpend, setTodaySpend] = (0, react.useState)(null);
|
|
4398
4578
|
const [sessionsSpend, setSessionsSpend] = (0, react.useState)(null);
|
|
@@ -4401,22 +4581,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4401
4581
|
const [open, setOpen] = (0, react.useState)(false);
|
|
4402
4582
|
const [request, setRequest] = (0, react.useState)(0);
|
|
4403
4583
|
const rootRef = (0, react.useRef)(null);
|
|
4584
|
+
const projected = useProjection("billingTodaySpend");
|
|
4404
4585
|
const running = useSession((snapshot) => snapshot.running);
|
|
4405
4586
|
const pricedRunningRef = (0, react.useRef)(running);
|
|
4587
|
+
const balanceRef = (0, react.useRef)(balance);
|
|
4588
|
+
balanceRef.current = balance;
|
|
4589
|
+
const openRef = (0, react.useRef)(open);
|
|
4590
|
+
openRef.current = open;
|
|
4406
4591
|
(0, react.useEffect)(() => {
|
|
4407
4592
|
let current = true;
|
|
4408
4593
|
const isCurrent = () => current;
|
|
4409
|
-
setRefreshing(
|
|
4594
|
+
setRefreshing(balanceRef.current !== null);
|
|
4410
4595
|
Promise.resolve().then(() => {
|
|
4411
|
-
const balanceRequest = fetchLine(isCurrent, getBalance, (value) => {
|
|
4596
|
+
const balanceRequest = fetchLine(isCurrent, () => getBalance(request > 0), (value) => {
|
|
4412
4597
|
setBalance(value);
|
|
4413
4598
|
setError(null);
|
|
4414
4599
|
}, (reason) => {
|
|
4415
|
-
if (
|
|
4600
|
+
if (balanceRef.current === null) setError(reason instanceof Error ? reason.message : String(reason));
|
|
4416
4601
|
});
|
|
4417
4602
|
const sessionSpendRequest = fetchLine(isCurrent, () => getSessionSpend(sessionId), setSpend);
|
|
4418
4603
|
const todaySpendRequest = fetchLine(isCurrent, () => getTodaySpend(request > 0), setTodaySpend);
|
|
4419
|
-
const sessionsSpendRequest = fetchLine(isCurrent, () => getTodaySessionsSpend(request > 0), setSessionsSpend);
|
|
4604
|
+
const sessionsSpendRequest = openRef.current ? fetchLine(isCurrent, () => getTodaySessionsSpend(request > 0), setSessionsSpend) : Promise.resolve();
|
|
4420
4605
|
Promise.allSettled([
|
|
4421
4606
|
balanceRequest,
|
|
4422
4607
|
sessionSpendRequest,
|
|
@@ -4437,6 +4622,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4437
4622
|
sessionId,
|
|
4438
4623
|
request
|
|
4439
4624
|
]);
|
|
4625
|
+
(0, react.useEffect)(() => {
|
|
4626
|
+
if (!open) return;
|
|
4627
|
+
let current = true;
|
|
4628
|
+
fetchLine(() => current, () => getTodaySessionsSpend(), setSessionsSpend);
|
|
4629
|
+
return () => {
|
|
4630
|
+
current = false;
|
|
4631
|
+
};
|
|
4632
|
+
}, [getTodaySessionsSpend, open]);
|
|
4440
4633
|
(0, react.useEffect)(() => {
|
|
4441
4634
|
if (running === pricedRunningRef.current) return;
|
|
4442
4635
|
pricedRunningRef.current = running;
|
|
@@ -4446,8 +4639,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4446
4639
|
const timer = setTimeout(() => {
|
|
4447
4640
|
fetchLine(isCurrent, () => getSessionSpend(sessionId), setSpend);
|
|
4448
4641
|
fetchLine(isCurrent, () => getTodaySpend(), setTodaySpend);
|
|
4449
|
-
fetchLine(isCurrent, () => getTodaySessionsSpend(), setSessionsSpend);
|
|
4450
|
-
},
|
|
4642
|
+
if (openRef.current) fetchLine(isCurrent, () => getTodaySessionsSpend(), setSessionsSpend);
|
|
4643
|
+
}, TURN_SETTLE_DEBOUNCE_MS);
|
|
4451
4644
|
return () => {
|
|
4452
4645
|
clearTimeout(timer);
|
|
4453
4646
|
current = false;
|
|
@@ -4469,6 +4662,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4469
4662
|
document.removeEventListener("pointerdown", closeOutside);
|
|
4470
4663
|
};
|
|
4471
4664
|
}, [open]);
|
|
4665
|
+
return {
|
|
4666
|
+
balance,
|
|
4667
|
+
spend: projected === void 0 ? spend : projected.session,
|
|
4668
|
+
todaySpend,
|
|
4669
|
+
sessionsSpend,
|
|
4670
|
+
error,
|
|
4671
|
+
refreshing,
|
|
4672
|
+
open,
|
|
4673
|
+
rootRef,
|
|
4674
|
+
refresh: () => {
|
|
4675
|
+
setRequest((value) => value + 1);
|
|
4676
|
+
},
|
|
4677
|
+
toggleOpen: () => {
|
|
4678
|
+
setOpen((value) => !value);
|
|
4679
|
+
}
|
|
4680
|
+
};
|
|
4681
|
+
}
|
|
4682
|
+
//#endregion
|
|
4683
|
+
//#region lib/types/client/BalanceBadge.js
|
|
4684
|
+
/**
|
|
4685
|
+
* Render the billing badge in the session-header utilities row: the trigger
|
|
4686
|
+
* shows the remaining balance and this conversation's billed spend, and opens
|
|
4687
|
+
* the detail panel (amount, per-model spend breakdown, today's spend, ranking,
|
|
4688
|
+
* refresh, disclaimer).
|
|
4689
|
+
* @param props - Remote face, locale, and the standard session-header runtime share.
|
|
4690
|
+
* @returns the badge, or null until the first balance fetch settles.
|
|
4691
|
+
*/
|
|
4692
|
+
function BalanceBadge({ getBalance, getCachedBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, sessionId, useSession, useProjection, t }) {
|
|
4693
|
+
const { balance, spend, todaySpend, sessionsSpend, error, refreshing, open, rootRef, refresh, toggleOpen } = useBillingData({
|
|
4694
|
+
getBalance,
|
|
4695
|
+
getCachedBalance,
|
|
4696
|
+
getSessionSpend,
|
|
4697
|
+
getTodaySpend,
|
|
4698
|
+
getTodaySessionsSpend,
|
|
4699
|
+
sessionId,
|
|
4700
|
+
useSession,
|
|
4701
|
+
useProjection
|
|
4702
|
+
});
|
|
4472
4703
|
if (balance === null) {
|
|
4473
4704
|
if (error === null) return null;
|
|
4474
4705
|
return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
@@ -4477,9 +4708,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4477
4708
|
children: (0, react_jsx_runtime.jsxs)("button", {
|
|
4478
4709
|
type: "button",
|
|
4479
4710
|
className: BalanceBadge_module_css_default.trigger,
|
|
4480
|
-
onClick:
|
|
4481
|
-
setRequest((value) => value + 1);
|
|
4482
|
-
},
|
|
4711
|
+
onClick: refresh,
|
|
4483
4712
|
"aria-label": t("action.refresh"),
|
|
4484
4713
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
4485
4714
|
className: BalanceBadge_module_css_default.unavailable,
|
|
@@ -4488,134 +4717,26 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4488
4717
|
})
|
|
4489
4718
|
});
|
|
4490
4719
|
}
|
|
4491
|
-
const refresh = () => {
|
|
4492
|
-
setRequest((value) => value + 1);
|
|
4493
|
-
};
|
|
4494
4720
|
const line = primaryLine(balance);
|
|
4495
4721
|
const amount = line === void 0 ? "—" : `${line.symbol}${line.total}`;
|
|
4496
|
-
const spendLine = spend !== null && spend.models.length > 0 ? t("trigger.conversationSpend", { amount: formatSpend
|
|
4722
|
+
const spendLine = spend !== null && spend.models.length > 0 ? t("trigger.conversationSpend", { amount: formatSpend(spend.total) }) : void 0;
|
|
4497
4723
|
return (0, react_jsx_runtime.jsxs)("div", {
|
|
4498
4724
|
ref: rootRef,
|
|
4499
4725
|
className: BalanceBadge_module_css_default.root,
|
|
4500
|
-
children: [(0, react_jsx_runtime.
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
className: BalanceBadge_module_css_default.lineSecondary,
|
|
4515
|
-
children: spendLine
|
|
4516
|
-
})]
|
|
4517
|
-
}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: open ? BalanceBadge_module_css_default.chevronOpen : void 0 })]
|
|
4518
|
-
}), open ? (0, react_jsx_runtime.jsxs)("div", {
|
|
4519
|
-
className: BalanceBadge_module_css_default.panel,
|
|
4520
|
-
role: "dialog",
|
|
4521
|
-
"aria-label": t("panel.aria"),
|
|
4522
|
-
children: [
|
|
4523
|
-
(0, react_jsx_runtime.jsxs)("div", {
|
|
4524
|
-
className: BalanceBadge_module_css_default.amountRow,
|
|
4525
|
-
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
4526
|
-
className: BalanceBadge_module_css_default.amountLabel,
|
|
4527
|
-
children: t("label.amount", { amount })
|
|
4528
|
-
}), (0, react_jsx_runtime.jsxs)("span", {
|
|
4529
|
-
className: BalanceBadge_module_css_default.amountActions,
|
|
4530
|
-
children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
4531
|
-
label: t("info.hint"),
|
|
4532
|
-
delayMs: 200,
|
|
4533
|
-
maxWidth: 340,
|
|
4534
|
-
children: (0, react_jsx_runtime.jsx)("button", {
|
|
4535
|
-
type: "button",
|
|
4536
|
-
className: BalanceBadge_module_css_default.infoButton,
|
|
4537
|
-
"aria-label": t("info.aria"),
|
|
4538
|
-
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQuestionOutline14, { className: BalanceBadge_module_css_default.inlineIcon })
|
|
4539
|
-
})
|
|
4540
|
-
}), (0, react_jsx_runtime.jsx)("button", {
|
|
4541
|
-
type: "button",
|
|
4542
|
-
className: BalanceBadge_module_css_default.refreshButton,
|
|
4543
|
-
onClick: refresh,
|
|
4544
|
-
"aria-label": t("action.refresh"),
|
|
4545
|
-
"data-refreshing": refreshing || void 0,
|
|
4546
|
-
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, { className: refreshing ? BalanceBadge_module_css_default.spinning : BalanceBadge_module_css_default.inlineIcon })
|
|
4547
|
-
})]
|
|
4548
|
-
})]
|
|
4549
|
-
}),
|
|
4550
|
-
(0, react_jsx_runtime.jsxs)("div", {
|
|
4551
|
-
className: BalanceBadge_module_css_default.spendRow,
|
|
4552
|
-
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
4553
|
-
className: BalanceBadge_module_css_default.amountLabel,
|
|
4554
|
-
children: t("label.sessionSpend", { amount: spend === null ? "—" : spend.models.length === 0 ? t("stat.none") : formatSpend$1(spend.total) })
|
|
4555
|
-
}), (0, react_jsx_runtime.jsx)("span", {
|
|
4556
|
-
className: BalanceBadge_module_css_default.amountLabel,
|
|
4557
|
-
children: t("label.todaySpend", { amount: todaySpend === null ? "—" : todaySpend.models.length === 0 ? t("stat.none") : formatSpend$1(todaySpend.total) })
|
|
4558
|
-
})]
|
|
4559
|
-
}),
|
|
4560
|
-
spend?.models.map((model) => (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
4561
|
-
className: BalanceBadge_module_css_default.modelRow,
|
|
4562
|
-
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
4563
|
-
className: BalanceBadge_module_css_default.modelName,
|
|
4564
|
-
children: model.displayName
|
|
4565
|
-
}), (0, react_jsx_runtime.jsx)("span", {
|
|
4566
|
-
className: BalanceBadge_module_css_default.tasks,
|
|
4567
|
-
children: formatSpend$1(model.cost)
|
|
4568
|
-
})]
|
|
4569
|
-
}), (0, react_jsx_runtime.jsx)("div", {
|
|
4570
|
-
className: BalanceBadge_module_css_default.costRow,
|
|
4571
|
-
children: (0, react_jsx_runtime.jsxs)("span", {
|
|
4572
|
-
className: BalanceBadge_module_css_default.costBreakdown,
|
|
4573
|
-
children: [
|
|
4574
|
-
t("label.cost.hit", { amount: formatSpend$1(model.cacheHitInputCost) }),
|
|
4575
|
-
" · ",
|
|
4576
|
-
t("label.cost.input", { amount: formatSpend$1(model.cacheMissInputCost) }),
|
|
4577
|
-
" · ",
|
|
4578
|
-
t("label.cost.output", { amount: formatSpend$1(model.outputCost) })
|
|
4579
|
-
]
|
|
4580
|
-
})
|
|
4581
|
-
})] }, model.model)),
|
|
4582
|
-
sessionsSpend !== null && sessionsSpend.sessions.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
|
|
4583
|
-
className: BalanceBadge_module_css_default.ranking,
|
|
4584
|
-
children: [
|
|
4585
|
-
(0, react_jsx_runtime.jsx)("div", {
|
|
4586
|
-
className: BalanceBadge_module_css_default.rankingTitle,
|
|
4587
|
-
children: t("label.sessionRanking")
|
|
4588
|
-
}),
|
|
4589
|
-
sessionsSpend.sessions.slice(0, 10).map((row, index) => (0, react_jsx_runtime.jsxs)("div", {
|
|
4590
|
-
className: BalanceBadge_module_css_default.rankingRow,
|
|
4591
|
-
children: [
|
|
4592
|
-
(0, react_jsx_runtime.jsx)("span", {
|
|
4593
|
-
className: BalanceBadge_module_css_default.rankingIndex,
|
|
4594
|
-
children: index + 1
|
|
4595
|
-
}),
|
|
4596
|
-
(0, react_jsx_runtime.jsx)("span", {
|
|
4597
|
-
className: BalanceBadge_module_css_default.rankingDot,
|
|
4598
|
-
"aria-hidden": true,
|
|
4599
|
-
children: " · "
|
|
4600
|
-
}),
|
|
4601
|
-
(0, react_jsx_runtime.jsx)("span", {
|
|
4602
|
-
className: BalanceBadge_module_css_default.rankingName,
|
|
4603
|
-
title: row.title ?? void 0,
|
|
4604
|
-
children: row.title ?? t("stat.untitled")
|
|
4605
|
-
}),
|
|
4606
|
-
(0, react_jsx_runtime.jsx)("span", {
|
|
4607
|
-
className: BalanceBadge_module_css_default.rankingAmount,
|
|
4608
|
-
children: formatSpend$1(row.total)
|
|
4609
|
-
})
|
|
4610
|
-
]
|
|
4611
|
-
}, row.sessionId)),
|
|
4612
|
-
sessionsSpend.sessions.length > 10 && (0, react_jsx_runtime.jsx)("div", {
|
|
4613
|
-
className: BalanceBadge_module_css_default.rankingMore,
|
|
4614
|
-
children: t("label.sessionRanking.more", { count: sessionsSpend.sessions.length - 10 })
|
|
4615
|
-
})
|
|
4616
|
-
]
|
|
4617
|
-
})
|
|
4618
|
-
]
|
|
4726
|
+
children: [(0, react_jsx_runtime.jsx)(BalanceTrigger, {
|
|
4727
|
+
amount,
|
|
4728
|
+
spendLine,
|
|
4729
|
+
open,
|
|
4730
|
+
onToggle: toggleOpen,
|
|
4731
|
+
t
|
|
4732
|
+
}), open ? (0, react_jsx_runtime.jsx)(BalancePanel, {
|
|
4733
|
+
amount,
|
|
4734
|
+
spend,
|
|
4735
|
+
todaySpend,
|
|
4736
|
+
sessionsSpend,
|
|
4737
|
+
refreshing,
|
|
4738
|
+
onRefresh: refresh,
|
|
4739
|
+
t
|
|
4619
4740
|
}) : null]
|
|
4620
4741
|
});
|
|
4621
4742
|
}
|
|
@@ -4634,21 +4755,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4634
4755
|
//#endregion
|
|
4635
4756
|
//#region lib/types/client/TurnCostAction.js
|
|
4636
4757
|
/** Per-turn billed-cost label in the closing assistant message's actions row. */
|
|
4637
|
-
/** CNY amount, up to four decimals with trailing zeros trimmed. */
|
|
4638
|
-
function formatSpend(amount) {
|
|
4639
|
-
return `¥${amount.toFixed(4).replace(/\.?0+$/, "")}`;
|
|
4640
|
-
}
|
|
4641
|
-
const turnCostCache = /* @__PURE__ */ new Map();
|
|
4642
|
-
const TURN_COST_CACHE_LIMIT = 1024;
|
|
4643
|
-
async function cachedTurnCost(sessionId, messageId, getTurnSpend) {
|
|
4644
|
-
const key = `${sessionId}\0${messageId}`;
|
|
4645
|
-
const cached = turnCostCache.get(key);
|
|
4646
|
-
if (cached !== void 0) return cached;
|
|
4647
|
-
const spend = await getTurnSpend(sessionId, messageId);
|
|
4648
|
-
if (turnCostCache.size >= TURN_COST_CACHE_LIMIT) turnCostCache.clear();
|
|
4649
|
-
turnCostCache.set(key, spend);
|
|
4650
|
-
return spend;
|
|
4651
|
-
}
|
|
4652
4758
|
/**
|
|
4653
4759
|
* Render one Turn's billed cost as a plain, non-interactive `¥金额` at the
|
|
4654
4760
|
* end of the closing message's actions row. The DOM stays inside the
|
|
@@ -4658,18 +4764,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4658
4764
|
* row's 8px gap spaces it from the clock, so the amount reads as one trailing
|
|
4659
4765
|
* meta line with the time; no icon, label or hover behavior of its own (the
|
|
4660
4766
|
* row's hover reveal shows it with the clock). It appears only after the
|
|
4661
|
-
*
|
|
4767
|
+
* shared map resolves and hides again when the Turn priced to zero (no DeepSeek
|
|
4662
4768
|
* usage); a failed fetch stays hidden so a Remote outage never clutters the
|
|
4663
4769
|
* row.
|
|
4664
4770
|
* @param props - the closing message id, the session runtime share, and the injected cost reader.
|
|
4665
4771
|
* @returns the cost text, or null while loading, on failure, or for zero cost.
|
|
4666
4772
|
*/
|
|
4667
|
-
function TurnCostAction({ messageId, sessionId,
|
|
4773
|
+
function TurnCostAction({ messageId, sessionId, getTurnCost }) {
|
|
4668
4774
|
const [cost, setCost] = (0, react.useState)(null);
|
|
4669
4775
|
(0, react.useEffect)(() => {
|
|
4670
4776
|
let current = true;
|
|
4671
|
-
Promise.resolve().then(() =>
|
|
4672
|
-
if (current) setCost(value);
|
|
4777
|
+
Promise.resolve().then(() => getTurnCost(sessionId, messageId)).then((value) => {
|
|
4778
|
+
if (current) setCost(value ?? null);
|
|
4673
4779
|
}, () => {
|
|
4674
4780
|
if (current) setCost(null);
|
|
4675
4781
|
});
|
|
@@ -4677,18 +4783,72 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4677
4783
|
current = false;
|
|
4678
4784
|
};
|
|
4679
4785
|
}, [
|
|
4680
|
-
|
|
4786
|
+
getTurnCost,
|
|
4681
4787
|
messageId,
|
|
4682
4788
|
sessionId
|
|
4683
4789
|
]);
|
|
4684
|
-
if (cost === null || cost
|
|
4790
|
+
if (cost === null || cost <= 0) return null;
|
|
4685
4791
|
return (0, react_jsx_runtime.jsx)("span", {
|
|
4686
4792
|
className: TurnCostAction_module_css_default.cost,
|
|
4687
4793
|
"data-turn-cost": true,
|
|
4688
|
-
children: formatSpend(cost
|
|
4794
|
+
children: formatSpend(cost)
|
|
4689
4795
|
});
|
|
4690
4796
|
}
|
|
4691
4797
|
//#endregion
|
|
4798
|
+
//#region lib/types/client/turnCostStore.js
|
|
4799
|
+
/** Max sessions whose maps are kept before the oldest is evicted. */
|
|
4800
|
+
const TURN_COST_STORE_LIMIT = 32;
|
|
4801
|
+
/**
|
|
4802
|
+
* Build the shared store over one batch fetcher.
|
|
4803
|
+
* @param fetch - the `getSessionTurnSpends` call.
|
|
4804
|
+
* @returns the store.
|
|
4805
|
+
*/
|
|
4806
|
+
function createTurnCostStore(fetch) {
|
|
4807
|
+
const entries = /* @__PURE__ */ new Map();
|
|
4808
|
+
const entryFor = (sessionId) => {
|
|
4809
|
+
let entry = entries.get(sessionId);
|
|
4810
|
+
if (entry === void 0) {
|
|
4811
|
+
entry = {
|
|
4812
|
+
turns: /* @__PURE__ */ new Map(),
|
|
4813
|
+
inflight: void 0
|
|
4814
|
+
};
|
|
4815
|
+
entries.set(sessionId, entry);
|
|
4816
|
+
while (entries.size > 32) {
|
|
4817
|
+
const oldest = entries.keys().next().value;
|
|
4818
|
+
if (oldest === void 0) break;
|
|
4819
|
+
entries.delete(oldest);
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
return entry;
|
|
4823
|
+
};
|
|
4824
|
+
const refresh = (sessionId, entry) => {
|
|
4825
|
+
if (entry.inflight !== void 0) return entry.inflight;
|
|
4826
|
+
const run = (async () => {
|
|
4827
|
+
try {
|
|
4828
|
+
const result = await fetch(sessionId);
|
|
4829
|
+
entry.turns = new Map(result.turns.map((row) => [row.messageId, row.total]));
|
|
4830
|
+
} finally {
|
|
4831
|
+
entry.inflight = void 0;
|
|
4832
|
+
}
|
|
4833
|
+
})();
|
|
4834
|
+
entry.inflight = run;
|
|
4835
|
+
return run;
|
|
4836
|
+
};
|
|
4837
|
+
return {
|
|
4838
|
+
async get(sessionId, messageId) {
|
|
4839
|
+
const entry = entryFor(sessionId);
|
|
4840
|
+
const cached = entry.turns.get(messageId);
|
|
4841
|
+
if (cached !== void 0) return cached;
|
|
4842
|
+
await refresh(sessionId, entry).catch(() => {});
|
|
4843
|
+
return entry.turns.get(messageId);
|
|
4844
|
+
},
|
|
4845
|
+
invalidate(sessionId) {
|
|
4846
|
+
if (sessionId === void 0) entries.clear();
|
|
4847
|
+
else entries.delete(sessionId);
|
|
4848
|
+
}
|
|
4849
|
+
};
|
|
4850
|
+
}
|
|
4851
|
+
//#endregion
|
|
4692
4852
|
//#region lib/types/client/locales.js
|
|
4693
4853
|
/** `billing` namespace dictionaries. */
|
|
4694
4854
|
/** Dictionary namespace owned by this plugin. */
|
|
@@ -4708,7 +4868,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4708
4868
|
"state.unavailable": "额度不可用",
|
|
4709
4869
|
"action.refresh": "刷新",
|
|
4710
4870
|
"info.aria": "花费说明",
|
|
4711
|
-
"info.hint": "仅能预估 DeepSeek 及 MiMo
|
|
4871
|
+
"info.hint": "仅能预估 DeepSeek 及 MiMo 相关模型。本会话花费按每条消息的发生时刻(北京时间)所在峰谷时段单价计费:缓存命中输入、未命中输入(含缓存写入)、输出(含推理)分别计价。基础费率按 DeepSeek 8 月 17 日实行的标准,其中 flash 系列(V4.1 Flash / V4 Flash / Vision Exp)自 9 月 10 日 12:00 起执行新价(谷时 0.02 / 1.0 / 4.0 元每百万 token,峰时为其两倍),该时刻之前的消息仍按原价;V4 Pro 自 9 月 14 日 12:00 起改由 V4.1 Flash 服务并按其实施费率计费。高峰时段为北京时间周一至周五 9:00–12:00、14:00–18:00,周末全天按低谷价计费;MiMo 系列不区分峰谷,统一费率。",
|
|
4712
4872
|
"badge.aria": "DeepSeek 额度:{amount}",
|
|
4713
4873
|
"panel.aria": "DeepSeek 额度详情",
|
|
4714
4874
|
"label.sessionRanking": "今日会话花费",
|
|
@@ -4729,7 +4889,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4729
4889
|
"state.unavailable": "Balance unavailable",
|
|
4730
4890
|
"action.refresh": "Refresh",
|
|
4731
4891
|
"info.aria": "About this spend",
|
|
4732
|
-
"info.hint": "Only DeepSeek and MiMo 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.
|
|
4892
|
+
"info.hint": "Only DeepSeek and MiMo 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. The base rates follow DeepSeek's August 17 pricing; the flash series (V4.1 Flash, V4 Flash, Vision Exp) was re-priced from September 10, 12:00 Beijing time (off-peak 0.02 / 1.0 / 4.0 CNY per 1M tokens, peak at twice those prices) while messages from before that instant keep the earlier rates, and V4 Pro switches to V4.1 Flash and its rates from September 14, 12:00 Beijing time. 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. MiMo series uses a flat rate with no peak/off-peak distinction.",
|
|
4733
4893
|
"badge.aria": "DeepSeek balance {amount}",
|
|
4734
4894
|
"panel.aria": "DeepSeek balance details",
|
|
4735
4895
|
"label.sessionRanking": "Today session spend",
|
|
@@ -4762,12 +4922,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4762
4922
|
zh,
|
|
4763
4923
|
en
|
|
4764
4924
|
}), "ui-billing: dictionaries");
|
|
4925
|
+
let lastBalance = null;
|
|
4765
4926
|
const injected = {
|
|
4766
|
-
getBalance: async () =>
|
|
4927
|
+
getBalance: async (force) => {
|
|
4928
|
+
const value = unwrap("billing.getBalance", await billing.getBalance(force));
|
|
4929
|
+
lastBalance = value;
|
|
4930
|
+
return value;
|
|
4931
|
+
},
|
|
4932
|
+
getCachedBalance: () => lastBalance,
|
|
4767
4933
|
getSessionSpend: async (sessionId) => unwrap("billing.getSessionSpend", await billing.getSessionSpend(sessionId)),
|
|
4768
4934
|
getTodaySpend: async (force) => unwrap("billing.getTodaySpend", await billing.getTodaySpend(force)),
|
|
4769
|
-
getTodaySessionsSpend: async (force) => unwrap("billing.getTodaySessionsSpend", await billing.getTodaySessionsSpend(force))
|
|
4770
|
-
getTurnSpend: async (sessionId, messageId) => unwrap("billing.getTurnSpend", await billing.getTurnSpend(sessionId, messageId))
|
|
4935
|
+
getTodaySessionsSpend: async (force) => unwrap("billing.getTodaySessionsSpend", await billing.getTodaySessionsSpend(force))
|
|
4771
4936
|
};
|
|
4772
4937
|
ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
|
|
4773
4938
|
name: "conversation.session.header.utilities",
|
|
@@ -4776,7 +4941,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4776
4941
|
locale: NS,
|
|
4777
4942
|
inject: () => injected
|
|
4778
4943
|
}, BalanceBadge));
|
|
4779
|
-
const
|
|
4944
|
+
const turnCosts = createTurnCostStore(async (sessionId) => unwrap("billing.getSessionTurnSpends", await billing.getSessionTurnSpends(sessionId)));
|
|
4945
|
+
const turnCostInjected = { getTurnCost: (sessionId, messageId) => turnCosts.get(sessionId, messageId) };
|
|
4780
4946
|
ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
|
|
4781
4947
|
name: "conversation.chat.assistant-actions",
|
|
4782
4948
|
id: "billing-turn-cost",
|
|
@@ -4786,7 +4952,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4786
4952
|
}, TurnCostAction));
|
|
4787
4953
|
}
|
|
4788
4954
|
//#endregion
|
|
4955
|
+
exports.TURN_COST_STORE_LIMIT = TURN_COST_STORE_LIMIT;
|
|
4789
4956
|
exports.apply = apply;
|
|
4957
|
+
exports.createTurnCostStore = createTurnCostStore;
|
|
4790
4958
|
exports.inject = inject;
|
|
4791
4959
|
return module.exports;
|
|
4792
4960
|
}
|
|
@@ -1,12 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Session-header billing badge: balance plus the current conversation's billed
|
|
3
|
+
* spend. Composition root: the data lifecycle lives in {@link useBillingData},
|
|
4
|
+
* and the trigger / detail panel are pure views. The badge renders null until
|
|
5
|
+
* the first balance fetch settles, and a refresh keeps the last values
|
|
6
|
+
* visible rather than blanking them.
|
|
7
|
+
*/
|
|
8
|
+
import type { DeepSeekBalance, DeepSeekSessionSpend, DeepSeekTodaySessionsSpend, DeepSeekTodaySpend } from '@rayadesu/dsh-llm-billing/types';
|
|
3
9
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
4
10
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
11
|
import { NS } from './locales.ts';
|
|
12
|
+
export { SESSION_RANKING_LIMIT } from './BalancePanel.tsx';
|
|
13
|
+
export { TURN_SETTLE_DEBOUNCE_MS } from './useBillingData.ts';
|
|
6
14
|
/** Registration-side Remote face used by the header badge. */
|
|
7
15
|
export interface BalanceBadgeInjected {
|
|
8
|
-
/**
|
|
9
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Read the account balance; rejects with the Remote error message. `force`
|
|
18
|
+
* bypasses the host-side TTL — the manual refresh passes it, a mount does
|
|
19
|
+
* not (a snapshot younger than the TTL is reused, so switching sessions
|
|
20
|
+
* costs no provider call).
|
|
21
|
+
*/
|
|
22
|
+
getBalance: (force?: boolean) => Promise<DeepSeekBalance>;
|
|
23
|
+
/**
|
|
24
|
+
* The last balance this browser half settled, or `null` before the first
|
|
25
|
+
* one. Synchronous: a badge mount renders the amount immediately and
|
|
26
|
+
* revalidates in the background.
|
|
27
|
+
*/
|
|
28
|
+
getCachedBalance: () => DeepSeekBalance | null;
|
|
10
29
|
/** Read one session's billed spend; rejects with the Remote error message. */
|
|
11
30
|
getSessionSpend: (sessionId: SessionId) => Promise<DeepSeekSessionSpend>;
|
|
12
31
|
/**
|
|
@@ -20,28 +39,16 @@ export interface BalanceBadgeInjected {
|
|
|
20
39
|
* with the Remote error message. `force` behaves as in {@link getTodaySpend}.
|
|
21
40
|
*/
|
|
22
41
|
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>;
|
|
25
42
|
}
|
|
26
43
|
/** Full props assembled by the header utilities slot renderer. */
|
|
27
44
|
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;
|
|
30
45
|
/**
|
|
31
|
-
* Render the billing badge in the session-header utilities row
|
|
32
|
-
* shows the remaining balance and this conversation's billed spend and opens
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* every session, a refresh action, and a spend disclaimer. Refreshing keeps
|
|
36
|
-
* the last values visible rather than blanking them.
|
|
37
|
-
*
|
|
38
|
-
* The spend follows the conversation: when a prompt turn settles in the
|
|
39
|
-
* current session, the badge recomputes only the (local, network-free)
|
|
40
|
-
* session spend and today's spend through `getSessionSpend` /
|
|
41
|
-
* `getTodaySpend`; the balance stays a manual-refresh snapshot and is never
|
|
42
|
-
* refetched on its own.
|
|
46
|
+
* Render the billing badge in the session-header utilities row: the trigger
|
|
47
|
+
* shows the remaining balance and this conversation's billed spend, and opens
|
|
48
|
+
* the detail panel (amount, per-model spend breakdown, today's spend, ranking,
|
|
49
|
+
* refresh, disclaimer).
|
|
43
50
|
* @param props - Remote face, locale, and the standard session-header runtime share.
|
|
44
51
|
* @returns the badge, or null until the first balance fetch settles.
|
|
45
52
|
*/
|
|
46
|
-
export declare function BalanceBadge({ getBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, sessionId, useSession, t }: BalanceBadgeProps): import("react").JSX.Element | null;
|
|
53
|
+
export declare function BalanceBadge({ getBalance, getCachedBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, sessionId, useSession, useProjection, t }: BalanceBadgeProps): import("react").JSX.Element | null;
|
|
47
54
|
//# sourceMappingURL=BalanceBadge.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DeepSeekSessionSpend, DeepSeekTodaySessionsSpend, DeepSeekTodaySpend } from '@rayadesu/dsh-llm-billing/types';
|
|
2
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
import { NS } from './locales.ts';
|
|
4
|
+
/** How many per-session ranking rows the panel shows before the overflow hint. */
|
|
5
|
+
export declare const SESSION_RANKING_LIMIT = 10;
|
|
6
|
+
/** Panel props: precomputed amount plus the same spend values the badge holds. */
|
|
7
|
+
export interface BalancePanelProps {
|
|
8
|
+
/** Primary balance line, e.g. `¥123.45`; `—` when the provider reports none. */
|
|
9
|
+
amount: string;
|
|
10
|
+
spend: DeepSeekSessionSpend | null;
|
|
11
|
+
todaySpend: DeepSeekTodaySpend | null;
|
|
12
|
+
sessionsSpend: DeepSeekTodaySessionsSpend | null;
|
|
13
|
+
refreshing: boolean;
|
|
14
|
+
onRefresh: () => void;
|
|
15
|
+
t: PropsLocale<typeof NS>['t'];
|
|
16
|
+
}
|
|
17
|
+
/** 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;
|
|
19
|
+
//# sourceMappingURL=BalancePanel.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
import { NS } from './locales.ts';
|
|
3
|
+
/** Trigger props: the rendered strings come precomputed from the parent. */
|
|
4
|
+
export interface BalanceTriggerProps {
|
|
5
|
+
/** Primary line amount, e.g. `¥123.45`; `—` when the provider reports none. */
|
|
6
|
+
amount: string;
|
|
7
|
+
/** Secondary conversation-spend line; absent when the session has no priced usage. */
|
|
8
|
+
spendLine: string | undefined;
|
|
9
|
+
/** Whether the detail panel is open (chevron + aria-expanded). */
|
|
10
|
+
open: boolean;
|
|
11
|
+
/** Toggle the panel. */
|
|
12
|
+
onToggle: () => void;
|
|
13
|
+
t: PropsLocale<typeof NS>['t'];
|
|
14
|
+
}
|
|
15
|
+
/** The badge button: balance line, spend line, and the open-state chevron. */
|
|
16
|
+
export declare function BalanceTrigger({ amount, spendLine, open, onToggle, t }: BalanceTriggerProps): import("react").JSX.Element;
|
|
17
|
+
//# sourceMappingURL=BalanceTrigger.d.ts.map
|
|
@@ -1,12 +1,15 @@
|
|
|
1
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
2
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
4
3
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
4
|
import { NS } from './locales.ts';
|
|
6
|
-
/** Injected face: one
|
|
5
|
+
/** Injected face: one message's Turn cost, served from a shared per-session map. */
|
|
7
6
|
export interface TurnCostActionInjected {
|
|
8
|
-
/**
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Resolve one completed Turn's billed cost by its message id. The underlying
|
|
9
|
+
* `getSessionTurnSpends` fetch happens once per session (shared by every row
|
|
10
|
+
* and coalesced across concurrent mounts), not once per rendered message.
|
|
11
|
+
*/
|
|
12
|
+
getTurnCost: (sessionId: SessionId, messageId: string) => Promise<number | undefined>;
|
|
10
13
|
}
|
|
11
14
|
/**
|
|
12
15
|
* Full props of the per-turn cost entry. The slot's owner share
|
|
@@ -24,11 +27,11 @@ export type TurnCostActionProps = PropsRuntime<'conversation.chat.assistant-acti
|
|
|
24
27
|
* row's 8px gap spaces it from the clock, so the amount reads as one trailing
|
|
25
28
|
* meta line with the time; no icon, label or hover behavior of its own (the
|
|
26
29
|
* row's hover reveal shows it with the clock). It appears only after the
|
|
27
|
-
*
|
|
30
|
+
* shared map resolves and hides again when the Turn priced to zero (no DeepSeek
|
|
28
31
|
* usage); a failed fetch stays hidden so a Remote outage never clutters the
|
|
29
32
|
* row.
|
|
30
33
|
* @param props - the closing message id, the session runtime share, and the injected cost reader.
|
|
31
34
|
* @returns the cost text, or null while loading, on failure, or for zero cost.
|
|
32
35
|
*/
|
|
33
|
-
export declare function TurnCostAction({ messageId, sessionId,
|
|
36
|
+
export declare function TurnCostAction({ messageId, sessionId, getTurnCost }: TurnCostActionProps): import("react").JSX.Element | null;
|
|
34
37
|
//# sourceMappingURL=TurnCostAction.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared display formatting for the billing badges: the balance-line currency
|
|
3
|
+
* prefix and the CNY spend amount renderer. Both the header badge and the
|
|
4
|
+
* per-turn cost label format through these, so the two entries cannot drift
|
|
5
|
+
* apart.
|
|
6
|
+
*/
|
|
7
|
+
import type { DeepSeekBalance } from '@rayadesu/dsh-llm-billing/types';
|
|
8
|
+
/** Currency prefix for one balance line; unknown codes render as a literal prefix. */
|
|
9
|
+
export declare function currencySymbol(currency: string): string;
|
|
10
|
+
/** The primary balance line, or undefined when the provider reports none. */
|
|
11
|
+
export declare function primaryLine(balance: DeepSeekBalance): {
|
|
12
|
+
symbol: string;
|
|
13
|
+
total: string;
|
|
14
|
+
} | undefined;
|
|
15
|
+
/** CNY amount, up to four decimals with trailing zeros trimmed. */
|
|
16
|
+
export declare function formatSpend(amount: number): string;
|
|
17
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -3,6 +3,7 @@ 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
5
|
export type { TurnCostActionInjected, TurnCostActionProps } from './TurnCostAction.tsx';
|
|
6
|
+
export { createTurnCostStore, TURN_COST_STORE_LIMIT, type TurnCostStore } from './turnCostStore.ts';
|
|
6
7
|
export type { BillingKey } from './locales.ts';
|
|
7
8
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
8
9
|
interface LocaleNamespaceMap {
|
|
@@ -16,7 +16,7 @@ export declare const zh: {
|
|
|
16
16
|
readonly 'state.unavailable': "额度不可用";
|
|
17
17
|
readonly 'action.refresh': "刷新";
|
|
18
18
|
readonly 'info.aria': "花费说明";
|
|
19
|
-
readonly 'info.hint': "仅能预估 DeepSeek 及 MiMo
|
|
19
|
+
readonly 'info.hint': "仅能预估 DeepSeek 及 MiMo 相关模型。本会话花费按每条消息的发生时刻(北京时间)所在峰谷时段单价计费:缓存命中输入、未命中输入(含缓存写入)、输出(含推理)分别计价。基础费率按 DeepSeek 8 月 17 日实行的标准,其中 flash 系列(V4.1 Flash / V4 Flash / Vision Exp)自 9 月 10 日 12:00 起执行新价(谷时 0.02 / 1.0 / 4.0 元每百万 token,峰时为其两倍),该时刻之前的消息仍按原价;V4 Pro 自 9 月 14 日 12:00 起改由 V4.1 Flash 服务并按其实施费率计费。高峰时段为北京时间周一至周五 9:00–12:00、14:00–18:00,周末全天按低谷价计费;MiMo 系列不区分峰谷,统一费率。";
|
|
20
20
|
readonly 'badge.aria': "DeepSeek 额度:{amount}";
|
|
21
21
|
readonly 'panel.aria': "DeepSeek 额度详情";
|
|
22
22
|
readonly 'label.sessionRanking': "今日会话花费";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared per-session Turn-cost map: one `getSessionTurnSpends` fetch serves
|
|
3
|
+
* every cost row of a session, instead of one Remote call per rendered
|
|
4
|
+
* message. Concurrent row mounts join the in-flight fetch, and a message id
|
|
5
|
+
* the map does not know triggers exactly one refetch (a newly completed Turn),
|
|
6
|
+
* so a transcript of N rows costs O(1) calls plus one per new Turn.
|
|
7
|
+
*/
|
|
8
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
9
|
+
import type { DeepSeekSessionTurnSpends } from '@rayadesu/dsh-llm-billing/types';
|
|
10
|
+
/** Max sessions whose maps are kept before the oldest is evicted. */
|
|
11
|
+
export declare const TURN_COST_STORE_LIMIT = 32;
|
|
12
|
+
/** The shared store surface the cost row consumes. */
|
|
13
|
+
export interface TurnCostStore {
|
|
14
|
+
/**
|
|
15
|
+
* Resolve one message's Turn cost, fetching the session's map on a miss.
|
|
16
|
+
* @param sessionId - the session owning the message.
|
|
17
|
+
* @param messageId - the assistant message id.
|
|
18
|
+
* @returns the Turn cost in CNY, or `undefined` when the message is not in a completed Turn.
|
|
19
|
+
*/
|
|
20
|
+
get(sessionId: SessionId, messageId: string): Promise<number | undefined>;
|
|
21
|
+
/** Drop one session's map (or every map) so the next read refetches. */
|
|
22
|
+
invalidate(sessionId?: SessionId): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the shared store over one batch fetcher.
|
|
26
|
+
* @param fetch - the `getSessionTurnSpends` call.
|
|
27
|
+
* @returns the store.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createTurnCostStore(fetch: (sessionId: SessionId) => Promise<DeepSeekSessionTurnSpends>): TurnCostStore;
|
|
30
|
+
//# sourceMappingURL=turnCostStore.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
import type { DeepSeekBalance, DeepSeekSessionSpend, DeepSeekTodaySessionsSpend, DeepSeekTodaySpend } from '@rayadesu/dsh-llm-billing/types';
|
|
3
|
+
import type { BalanceBadgeProps } from './BalanceBadge.tsx';
|
|
4
|
+
/** Debounce for the turn-settled recompute: a burst of turns prices once. */
|
|
5
|
+
export declare const TURN_SETTLE_DEBOUNCE_MS = 2000;
|
|
6
|
+
/** The data surface the trigger and the panel render from. */
|
|
7
|
+
export interface BillingData {
|
|
8
|
+
balance: DeepSeekBalance | null;
|
|
9
|
+
spend: DeepSeekSessionSpend | null;
|
|
10
|
+
todaySpend: DeepSeekTodaySpend | null;
|
|
11
|
+
sessionsSpend: DeepSeekTodaySessionsSpend | null;
|
|
12
|
+
/** Balance fetch failure while no value is present yet. */
|
|
13
|
+
error: string | null;
|
|
14
|
+
refreshing: boolean;
|
|
15
|
+
open: boolean;
|
|
16
|
+
rootRef: RefObject<HTMLDivElement>;
|
|
17
|
+
refresh: () => void;
|
|
18
|
+
toggleOpen: () => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Start the badge's data lifecycle for one session. The spend follows the
|
|
22
|
+
* conversation: the host-pushed `billingTodaySpend` projection drives the
|
|
23
|
+
* session line live (zero Remote calls), with `getSessionSpend` as the
|
|
24
|
+
* bootstrap/fallback when the projection key is absent; today's spend is
|
|
25
|
+
* recomputed through `getTodaySpend` when a turn settles; the balance stays a
|
|
26
|
+
* manual-refresh snapshot and is never refetched on its own.
|
|
27
|
+
* @param props - the badge's injected face and session runtime share.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useBillingData({ getBalance, getCachedBalance, getSessionSpend, getTodaySpend, getTodaySessionsSpend, sessionId, useSession, useProjection, }: Pick<BalanceBadgeProps, 'getBalance' | 'getCachedBalance' | 'getSessionSpend' | 'getTodaySpend' | 'getTodaySessionsSpend' | 'sessionId' | 'useSession' | 'useProjection'>): BillingData;
|
|
30
|
+
//# sourceMappingURL=useBillingData.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.10",
|
|
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.10",
|
|
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.10",
|
|
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",
|