dsh-cost-slip 0.0.0-stage → 1.4.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pktheme
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,88 @@
1
- # Temporary Holding Version
1
+ # dsh-cost-slip
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > DSH(DeepSeek Harness)插件:在输入框底部显示当前会话的估算费用(人民币),点击打开,显示费用明细和账号余额。
4
+
5
+ ![截图展示](https://raw.githubusercontent.com/pktheme/dsh-cost-slip/main/docs/Screenshot.png)
6
+
7
+ ## 功能
8
+
9
+ - **独立胶囊**:在输入框底部统计行里,和「会话统计」「Token 用量」并排;收据图标 + `费用 ¥0.44`,hover 底色和圆角与原生一致。
10
+ - **费用明细**:点开胶囊,按「缓存命中 / 未缓存输入 / 输出」列出 token 数与金额,标题右侧是合计。
11
+ - **账号余额**:明细下方显示当前登录账号的「充值余额」「赠金余额」;打开弹窗时刷新读一次,之后每 10 秒刷新。
12
+ - **按条计价**:每条回复用当时的模型和峰谷价单独计算,中途换模型、跨高峰都保持准确。
13
+ - **窄窗口自适应**:窗口变窄或打开右侧边栏时,胶囊文字像原生统计项一样省略号截断。
14
+
15
+ ## 安装
16
+
17
+ 基于 DSH `0.2.1-rc.2` 开发,更低版本安装使用尚未测试。
18
+
19
+ ### 桌面版(推荐)
20
+
21
+ 1. 打开 DSH → 左侧「插件」→「添加插件」→「安装第三方插件」。
22
+ 2. 填入包名 `dsh-cost-slip`或本地化下载的 tgz 的绝对路径。
23
+ 3. 点「立即启用」,按提示重启 DSH。
24
+
25
+ ### Web 版
26
+
27
+ ```bash
28
+ dsh plugin --profile web add dsh-cost-slip
29
+ ```
30
+
31
+ 然后二选一,让插件作为 bundle 层生效:
32
+
33
+ - 在 `~/.dsh/profiles/web/package.json` 的 `dsh.profile.bundles` 末尾加 `"dsh-cost-slip"`;
34
+ - 或者把包内 `cordis.patch.yml` 的 `- insert:` 段贴到 profile 的 `cordis.patch.yml` 末尾。
35
+
36
+ 两者只能选一个,否则同一个 id 会重复。改完刷新页面;没出现就重启。
37
+
38
+ ### 离线包
39
+
40
+ 从 [Releases](https://github.com/pktheme/dsh-cost-slip/releases) 下载 `dsh-cost-slip-1.4.0.tgz`,按桌面版第 2 步填绝对路径。
41
+
42
+ 首次安装需要联网:宿主端依赖 `zod`,由 pnpm/npm 自动安装。
43
+
44
+ ## 怎么算的
45
+
46
+ 按**每条** `assistant/message`(以及失败重试的 `assistant/attempt`)单独计价:
47
+
48
+ 1. 取这条回复的 `usage`:未命中输入 `inputTokens`、缓存命中 `cacheReadTokens`、缓存写入 `cacheWriteTokens`、输出 `outputTokens`。
49
+ 2. 取这条回复的模型(`message.source.model`,缺失时用最近一次 `request/header`)和完成时间 `event.time`。
50
+ 3. 用 `lib/pricing.js` 的价目表计算,单位换算成微元后累加。
51
+
52
+ **缓存写入按未命中单价计。** 价格来源:[DeepSeek 官方定价](https://api-docs.deepseek.com/zh-cn/quick_start/pricing)
53
+
54
+ ## 隐私
55
+
56
+ - 只通过 DSH 宿主接口读取会话的 token 用量与事件,不抓包、不联网。
57
+ - 余额通过宿主提供的账号接口读取,仅用于显示。
58
+ - 不上报任何数据,不写入额外文件。
59
+
60
+ ## 已知边界
61
+
62
+ - **估算值**:不是官方账单;赠送额度、优惠、失败请求的计费细节以 DeepSeek 账单为准。
63
+ - **子智能体**:subagent 跑在独立会话里,主会话的费用不含它们的消耗。
64
+ - **旧事件**:只对带 provider usage 的回复计费;没有 usage 的旧事件会跳过。
65
+ - **兼容性**:依赖 DSH 的 `sessionProjections` 和 `composer.dock` 插槽;DSH 大版本升级后若改名,插件需要跟着改。
66
+
67
+ ## 开发
68
+
69
+ ```bash
70
+ git clone https://github.com/pktheme/dsh-cost-slip.git
71
+ cd dsh-cost-slip
72
+ dsh plugin --profile web add "$PWD"
73
+ ```
74
+
75
+ - `lib/index.js`:宿主端会话投影,按条累计费用。
76
+ - `lib/client.js`:胶囊与弹窗 UI。
77
+ - `lib/pricing.js`:价目表与峰谷判断,调价只改这里。
78
+ - `locale/*.json`:插件名与简介。
79
+
80
+ 协议:MIT。
81
+
82
+ ## 图标
83
+
84
+ 插件页图标 `icon.svg` 与界面里的收据图标均为作者本人绘制。
85
+
86
+ ---
87
+
88
+ 非官方插件,与 DeepSeek 官方无隶属关系。
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3
+ <title>icon-cost-16</title>
4
+ <g id="icon-cost-16" stroke="none" fill="none" stroke-width="1">
5
+ <line x1="5" y1="6" x2="10" y2="6" id="路径" stroke="#000000"></line>
6
+ <polyline id="路径" stroke="#000000" points="5 9 8 9 10 9 11 9"></polyline>
7
+ <path d="M11.5863439,14.1295158 L11.5830868,14.1246301 C11.0425067,13.3137601 9.87388821,13.2513342 9.25,14 C8.59947924,14.7806249 7.40052076,14.7806249 6.75,14 C6.12611179,13.2513342 4.95749329,13.3137601 4.41691324,14.1246301 L4.41365611,14.1295158 C4.04344449,14.6848333 3.2545311,14.7552621 2.79180776,14.2743035 C2.5,13.970996 2.5,13.4910841 2.5,12.5312602 L2.5,5.1 C2.5,3.83988186 2.5,3.20982279 2.74523532,2.72852138 C2.96095041,2.30515667 3.30515667,1.96095041 3.72852138,1.74523532 C4.20982279,1.5 4.83988186,1.5 6.1,1.5 L9.9,1.5 C11.1601181,1.5 11.7901772,1.5 12.2714786,1.74523532 C12.6948433,1.96095041 13.0390496,2.30515667 13.2547647,2.72852138 C13.5,3.20982279 13.5,3.83988186 13.5,5.1 L13.5,12.5312602 C13.5,13.4910841 13.5,13.970996 13.2081922,14.2743035 C12.7454689,14.7552621 11.9565555,14.6848333 11.5863439,14.1295158 Z" id="路径" stroke="#000000"></path>
8
+ </g>
9
+ </svg>
@@ -0,0 +1,6 @@
1
+ # dsh-cost-slip —— 把本插件作为 bundle 层插入 profile。
2
+ # 装好依赖后,在本 profile 的 package.json 里把 "dsh-cost-slip" 加进 dsh.profile.bundles,
3
+ # 或者把下面这一行手工插进 profile 的 cordis.patch.yml(二选一,别同时做)。
4
+ - insert:
5
+ - id: dsh-cost-slip
6
+ name: dsh-cost-slip
package/icon.svg ADDED
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="36px" height="36px" viewBox="0 0 36 36" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3
+ <title>dsh-cost-slip</title>
4
+ <defs>
5
+ <path d="M24.4072179,8.61785298 C25.042265,8.94142562 25.5585744,9.457735 25.882147,10.0927821 C26.25,10.8147342 26.25,11.7598228 26.25,13.65 L26.25,24.7911858 C26.25,26.2321468 26.25,26.9526273 25.8119159,27.4079758 C25.1172402,28.1300277 23.932863,28.0242945 23.377073,27.1906095 C22.5637601,25.9706401 20.8108323,25.8770012 19.875,27 C18.8992189,28.1709374 17.1007811,28.1709374 16.125,27 C15.1891677,25.8770012 13.4362399,25.9706401 12.6253699,27.1869452 C12.067137,28.0242945 10.8827598,28.1300277 10.1880841,27.4079758 C9.75,26.9526273 9.75,26.2321468 9.75,24.7911858 L9.75,13.65 C9.75,11.7598228 9.75,10.8147342 10.117853,10.0927821 C10.4414256,9.457735 10.957735,8.94142562 11.5927821,8.61785298 C12.3147342,8.25 13.2598228,8.25 15.15,8.25 L20.85,8.25 C22.7401772,8.25 23.6852658,8.25 24.4072179,8.61785298 Z" id="path-1"></path>
6
+ <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
7
+ <stop stop-color="#BE84FF" offset="0%"></stop>
8
+ <stop stop-color="#4D6BFE" offset="100%"></stop>
9
+ </linearGradient>
10
+ </defs>
11
+ <g id="dsh-cost-slip" stroke="none" fill="none" xlink:href="#path-1" fill-rule="evenodd">
12
+ <path d="M23.377073,27.1906095 L23.3746301,27.1869452 C22.5637601,25.9706401 20.8108323,25.8770012 19.875,27 C18.8992189,28.1709374 17.1007811,28.1709374 16.125,27 C15.1891677,25.8770012 13.4362399,25.9706401 12.6253699,27.1869452 L12.622927,27.1906095 C12.067137,28.0242945 10.8827598,28.1300277 10.1880841,27.4079758 C9.75,26.9526273 9.75,26.2321468 9.75,24.7911858 L9.75,13.65 C9.75,11.7598228 9.75,10.8147342 10.117853,10.0927821 C10.4414256,9.457735 10.957735,8.94142562 11.5927821,8.61785298 C12.3147342,8.25 13.2598228,8.25 15.15,8.25 L20.85,8.25 C22.7401772,8.25 23.6852658,8.25 24.4072179,8.61785298 C25.042265,8.94142562 25.5585744,9.457735 25.882147,10.0927821 C26.25,10.8147342 26.25,11.7598228 26.25,13.65 L26.25,24.7911858 C26.25,26.2321468 26.25,26.9526273 25.8119159,27.4079758 C25.1172402,28.1300277 23.932863,28.0242945 23.377073,27.1906095 Z" id="路径" fill="url(#linearGradient-2)"></path>
13
+ <path d="M14.828,13.66 L19.672,13.66 C20.1368436,13.66 20.3692654,13.66 20.5468121,13.7504646 C20.8249062,13.8921606 21,14.1778877 21,14.49 C21,14.8021123 20.8249062,15.0878394 20.5468121,15.2295354 C20.3692654,15.32 20.1368436,15.32 19.672,15.32 L14.828,15.32 C14.3631564,15.32 14.1307346,15.32 13.9531879,15.2295354 C13.6750938,15.0878394 13.5,14.8021123 13.5,14.49 C13.5,14.1778877 13.6750938,13.8921606 13.9531879,13.7504646 C14.1307346,13.66 14.3631564,13.66 14.828,13.66 Z" id="矩形" fill="#FFFFFF"></path>
14
+ <path d="M14.828,19.5 L21.172,19.5 C21.6368436,19.5 21.8692654,19.5 22.0468121,19.5904646 C22.3249062,19.7321606 22.5,20.0178877 22.5,20.33 C22.5,20.6421123 22.3249062,20.9278394 22.0468121,21.0695354 C21.8692654,21.16 21.6368436,21.16 21.172,21.16 L14.828,21.16 C14.3631564,21.16 14.1307346,21.16 13.9531879,21.0695354 C13.6750938,20.9278394 13.5,20.6421123 13.5,20.33 C13.5,20.0178877 13.6750938,19.7321606 13.9531879,19.5904646 C14.1307346,19.5 14.3631564,19.5 14.828,19.5 Z" id="矩形" fill="#FFFFFF"></path>
15
+ </g>
16
+ </svg>
package/lib/client.js ADDED
@@ -0,0 +1,407 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-cost-slip",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ const { jsx, jsxs } = require("react/jsx-runtime");
8
+ const { useState, useRef, useEffect } = require("react");
9
+ const { createPortal } = require("react-dom");
10
+ const primitives = require("@deepseek-ai/dsh-client-ui-primitives");
11
+
12
+ // ── 样式 ────────────────────────────────────────────────────────────
13
+ // 胶囊部分与输入框底部现有统计胶囊同字号、同颜色;弹窗部分照搬 DSH 原生
14
+ // stat-dialog 的尺寸、圆角、阴影和栅格,保证和「Token 用量」弹窗一致。
15
+ const css = [
16
+ ".dshSessionCost_anchor{min-width:0;font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));display:inline-flex}",
17
+ ".dshSessionCost_pill{box-sizing:border-box;max-width:100%;color:var(--dsw-alias-label-tertiary);font:inherit;font-variant-numeric:tabular-nums;line-height:inherit;white-space:nowrap;background:0 0;border:none;align-items:center;gap:6px;padding:1px 8px;display:inline-flex;cursor:pointer;border-radius:var(--dsw-radius-sm)}",
18
+ ".dshSessionCost_pill:hover,.dshSessionCost_pill[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
19
+ ".dshSessionCost_pill svg{flex:none;width:14px;height:14px}",
20
+ ".dshSessionCost_label{text-overflow:ellipsis;min-width:0;overflow:hidden}",
21
+ ".dshSessionCost_panel{z-index:1100;box-sizing:border-box;border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-1,#f8f9fa);width:max-content;min-width:min(300px,100vw - 24px);max-width:min(440px,100vw - 24px);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;border:0;padding:16px;font-size:12px;line-height:18px;position:fixed}",
22
+ ".dshSessionCost_title{color:var(--dsw-alias-label-primary);justify-content:space-between;gap:16px;margin-bottom:8px;font-weight:500;display:flex}",
23
+ ".dshSessionCost_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}",
24
+ ".dshSessionCost_titleValue{font-variant-numeric:tabular-nums}",
25
+ ".dshSessionCost_titleLabel{align-items:center;gap:6px;min-width:0;display:inline-flex}",
26
+ ".dshSessionCost_details{color:var(--dsw-alias-label-tertiary);grid-template-columns:minmax(76px,auto) minmax(0,1fr);gap:6px 16px;margin:0;display:grid}",
27
+ ".dshSessionCost_details dt,.dshSessionCost_details dd{min-width:0;margin:0}",
28
+ ".dshSessionCost_details dd{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}",
29
+ ".dshSessionCost_note{color:var(--dsw-alias-label-tertiary);margin:10px 0 0;font-size:11px;line-height:16px}",
30
+ ".dshSessionCost_divider{border-top:.5px solid var(--dsw-alias-border-l2);margin:12px 0 10px}",
31
+ ".dshSessionCost_sectionTitle{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0 0 6px;font-weight:400}"
32
+ ].join("");
33
+ const tagId = "dsh-cost-slip/SessionCostPill.css";
34
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
35
+ const tag = document.createElement("style");
36
+ tag.dataset.plugin = "dsh-cost-slip";
37
+ tag.dataset.pluginCss = tagId;
38
+ tag.textContent = css;
39
+ document.head.appendChild(tag);
40
+ }
41
+ const styles = {
42
+ anchor: "dshSessionCost_anchor",
43
+ pill: "dshSessionCost_pill",
44
+ label: "dshSessionCost_label",
45
+ panel: "dshSessionCost_panel",
46
+ title: "dshSessionCost_title",
47
+ titleLabel: "dshSessionCost_titleLabel",
48
+ titleValue: "dshSessionCost_titleValue",
49
+ titleRule: "dshSessionCost_titleRule",
50
+ details: "dshSessionCost_details",
51
+ note: "dshSessionCost_note",
52
+ divider: "dshSessionCost_divider",
53
+ sectionTitle: "dshSessionCost_sectionTitle"
54
+ };
55
+
56
+ /** 弹窗未定位时的测量样式:不可见但参与布局。 */
57
+ const MEASURE_STYLE = {
58
+ visibility: "hidden",
59
+ left: 0,
60
+ top: 0
61
+ };
62
+
63
+ // ── 文案 ────────────────────────────────────────────────────────────
64
+ const zh = {
65
+ "pill.label": "费用 {cost}",
66
+ "pill.aria": "费用 {cost},查看明细",
67
+ "dialog.title": "费用",
68
+ "dialog.cacheHit": "缓存命中",
69
+ "dialog.cacheMiss": "未缓存输入",
70
+ "dialog.cacheWrite": "缓存写入",
71
+ "dialog.output": "输出",
72
+ "dialog.note": "按各条回复当时的峰谷价估算,以官方账单为准。",
73
+ "balance.title": "账户余额",
74
+ "balance.recharge": "充值余额",
75
+ "balance.bonus": "赠金余额",
76
+ "balance.loading": "读取中…",
77
+ "balance.unavailable": "暂不可用",
78
+ "balance.signedOut": "登录后查看"
79
+ };
80
+ const en = {
81
+ "pill.label": "Cost {cost}",
82
+ "pill.aria": "Cost {cost}, view details",
83
+ "dialog.title": "Cost",
84
+ "dialog.cacheHit": "Cache hit",
85
+ "dialog.cacheMiss": "Uncached input",
86
+ "dialog.cacheWrite": "Cache write",
87
+ "dialog.output": "Output",
88
+ "dialog.note": "Estimated at each reply's peak/off-peak price. Official billing prevails.",
89
+ "balance.title": "Account balance",
90
+ "balance.recharge": "Topped-up balance",
91
+ "balance.bonus": "Granted balance",
92
+ "balance.loading": "Loading…",
93
+ "balance.unavailable": "Unavailable",
94
+ "balance.signedOut": "Sign in to view"
95
+ };
96
+
97
+ // ── 格式化 ──────────────────────────────────────────────────────────
98
+ /**
99
+ * 微元转人民币文本。大于 1 分显示两位,小钱显示四位,再小给下限。
100
+ * @param {number} micros - 微元(1e-6 元)。
101
+ * @param {boolean} zeroAsZero - 为 0 时是否显示 ¥0(标题/明细用)。
102
+ * @returns {string}
103
+ */
104
+ function formatCost(micros, zeroAsZero = false) {
105
+ const value = Number(micros) / 1e6;
106
+ if (!Number.isFinite(value) || value <= 0) return zeroAsZero ? "¥0" : "¥<0.0001";
107
+ if (value >= 0.01) return "¥" + value.toFixed(2);
108
+ if (value >= 0.0001) return "¥" + value.toFixed(4);
109
+ return "¥<0.0001";
110
+ }
111
+
112
+ /**
113
+ * 明细用的精确金额:至少两位小数,常规四位(再小五位),并去掉多余的 0。
114
+ * 四位是为了让各行相加与标题合计严格对上,不像两位/三位那样出现「分项加起来多一分」。
115
+ * @param {number} micros - 微元(1e-6 元)。
116
+ * @returns {string}
117
+ */
118
+ function formatCostPrecise(micros) {
119
+ const value = Number(micros) / 1e6;
120
+ if (!Number.isFinite(value) || value <= 0) return "¥0";
121
+ if (value < 0.0001) return "¥<0.0001";
122
+ let text = (value >= 0.001 ? value.toFixed(4) : value.toFixed(5)).replace(/0+$/, "").replace(/\.$/, "");
123
+ const dot = text.indexOf(".");
124
+ if (dot === -1) text += ".00";
125
+ else if (text.length - dot - 1 < 2) text = text.padEnd(dot + 3, "0");
126
+ return "¥" + text;
127
+ }
128
+
129
+ /**
130
+ * 精确 token 数,带千分位:45,321,728。
131
+ * @param {number} value - token 数。
132
+ * @returns {string}
133
+ */
134
+ function formatTokenCount(value) {
135
+ const count = Math.round(Number(value) || 0);
136
+ return String(count).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
137
+ }
138
+
139
+ /** 最近一次读到的余额,弹窗重开时先显示旧值再刷新。 */
140
+ let balanceCache = null;
141
+
142
+ /**
143
+ * 单个钱包金额的显示规则,与账号设置页一致:0 显示 ¥0.00,
144
+ * 不足一分显示 <¥0.01,正数向下取两位并加千分位。
145
+ * @param {string} amount - 账号接口返回的十进制字符串。
146
+ * @param {string} currency - CNY / USD 等。
147
+ * @returns {string}
148
+ */
149
+ function formatBalanceAmount(amount, currency) {
150
+ const symbol = currency === "CNY" ? "¥" : currency === "USD" ? "$" : String(currency || "CNY") + " ";
151
+ const value = Number(amount);
152
+ if (!Number.isFinite(value)) return symbol + String(amount ?? "");
153
+ const fixed = (number) => {
154
+ const text = (Math.floor(number * 100) / 100).toFixed(2);
155
+ const [integer, fraction] = text.split(".");
156
+ return Number(integer).toLocaleString("en-US") + "." + fraction;
157
+ };
158
+ if (value === 0) return symbol + "0.00";
159
+ if (value < 0) {
160
+ const abs = Math.abs(value);
161
+ return "-" + symbol + (abs < 0.01 ? "0.01" : fixed(abs));
162
+ }
163
+ if (value < 0.01) return "<" + symbol + "0.01";
164
+ return symbol + fixed(value);
165
+ }
166
+
167
+ /**
168
+ * 一组钱包的显示文本;多币种并排,全为 0 时显示 ¥0.00。
169
+ * @param {Array<{balance: string, currency: string}>} wallets - 余额钱包。
170
+ * @returns {string}
171
+ */
172
+ function formatWallets(wallets) {
173
+ const list = (Array.isArray(wallets) ? wallets : []).filter((wallet) => wallet && Number(wallet.balance) > 0);
174
+ if (list.length === 0) {
175
+ const currency = Array.isArray(wallets) && wallets[0] && wallets[0].currency ? wallets[0].currency : "CNY";
176
+ return formatBalanceAmount("0", currency);
177
+ }
178
+ return list.map((wallet) => formatBalanceAmount(wallet.balance, wallet.currency)).join(" ");
179
+ }
180
+
181
+ /**
182
+ * 费用图标:用户设计的收据造型;描边跟随 currentColor,尺寸由调用处决定。
183
+ * @param {number} size - 渲染边长(像素)。
184
+ * @returns {object} 收据图标元素。
185
+ */
186
+ function receiptIcon(size) {
187
+ return jsxs("svg", {
188
+ width: size,
189
+ height: size,
190
+ viewBox: "0 0 16 16",
191
+ fill: "none",
192
+ xmlns: "http://www.w3.org/2000/svg",
193
+ "aria-hidden": true,
194
+ strokeWidth: 1,
195
+ children: [
196
+ jsx("path", { d: "M5 6H10", stroke: "currentColor" }),
197
+ jsx("path", { d: "M5 9H11", stroke: "currentColor" }),
198
+ jsx("path", {
199
+ d: "M11.5863439,14.1295158 L11.5830868,14.1246301 C11.0425067,13.3137601 9.87388821,13.2513342 9.25,14 C8.59947924,14.7806249 7.40052076,14.7806249 6.75,14 C6.12611179,13.2513342 4.95749329,13.3137601 4.41691324,14.1246301 L4.41365611,14.1295158 C4.04344449,14.6848333 3.2545311,14.7552621 2.79180776,14.2743035 C2.5,13.970996 2.5,13.4910841 2.5,12.5312602 L2.5,5.1 C2.5,3.83988186 2.5,3.20982279 2.74523532,2.72852138 C2.96095041,2.30515667 3.30515667,1.96095041 3.72852138,1.74523532 C4.20982279,1.5 4.83988186,1.5 6.1,1.5 L9.9,1.5 C11.1601181,1.5 11.7901772,1.5 12.2714786,1.74523532 C12.6948433,1.96095041 13.0390496,2.30515667 13.2547647,2.72852138 C13.5,3.20982279 13.5,3.83988186 13.5,5.1 L13.5,12.5312602 C13.5,13.4910841 13.5,13.970996 13.2081922,14.2743035 C12.7454689,14.7552621 11.9565555,14.6848333 11.5863439,14.1295158 Z",
200
+ stroke: "currentColor"
201
+ })
202
+ ]
203
+ });
204
+ }
205
+
206
+ // ── 组件 ────────────────────────────────────────────────────────────
207
+ /**
208
+ * 输入框底部统计行里的费用:默认接在缓存命中后面,点击弹出费用明细。
209
+ * @param {{useProjection: Function, t: Function}} props - 插槽框架注入的钩子与译文。
210
+ * @returns {object | null} 费用胶囊;没有可计价回复时不渲染。
211
+ */
212
+ function SessionCostPill({ useProjection, t, readBalance }) {
213
+ const view = useProjection("sessionCost");
214
+ const [open, setOpen] = useState(false);
215
+ const [balance, setBalance] = useState(() => balanceCache);
216
+ const readBalanceRef = useRef(readBalance);
217
+ readBalanceRef.current = readBalance;
218
+ const rootRef = useRef(null);
219
+ const panelRef = useRef(null);
220
+ const pos = primitives.useAnchoredPosition({
221
+ open,
222
+ anchorRef: rootRef,
223
+ panelRef,
224
+ side: "top",
225
+ gap: 8,
226
+ margin: 12
227
+ });
228
+ primitives.useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
229
+ useEffect(() => {
230
+ if (!open) return;
231
+ const onKeyDown = (event) => {
232
+ if (event.key === "Escape") setOpen(false);
233
+ };
234
+ document.addEventListener("keydown", onKeyDown);
235
+ return () => document.removeEventListener("keydown", onKeyDown);
236
+ }, [open]);
237
+ useEffect(() => {
238
+ if (!open) return;
239
+ let alive = true;
240
+ const load = () => {
241
+ const read = typeof readBalanceRef.current === "function" ? readBalanceRef.current() : null;
242
+ Promise.resolve(read).then((next) => {
243
+ const value = next && typeof next.status === "string" ? next : { status: "unavailable" };
244
+ balanceCache = value;
245
+ if (alive) setBalance(value);
246
+ }).catch(() => {
247
+ if (alive) setBalance({ status: "unavailable" });
248
+ });
249
+ };
250
+ if (balanceCache === null) setBalance({ status: "loading" });
251
+ load();
252
+ const timer = setInterval(load, 10_000);
253
+ return () => {
254
+ alive = false;
255
+ clearInterval(timer);
256
+ };
257
+ }, [open]);
258
+
259
+ if (view === void 0 || view === null || view.samples === 0 || view.totalMicros <= 0) return null;
260
+
261
+ const total = formatCostPrecise(view.totalMicros);
262
+ const label = t("pill.label", { cost: formatCost(view.totalMicros) });
263
+ const rows = [];
264
+ const pushRow = (key, name, tokens, micros) => {
265
+ if (tokens <= 0) return;
266
+ rows.push(jsx("dt", { children: name }, key + "-dt"));
267
+ rows.push(jsx("dd", {
268
+ children: `${formatTokenCount(tokens)} tok · ${formatCostPrecise(micros)}`
269
+ }, key + "-dd"));
270
+ };
271
+ pushRow("hit", t("dialog.cacheHit"), view.hitTokens, view.hitMicros);
272
+ pushRow("miss", t("dialog.cacheMiss"), view.missTokens, view.missMicros);
273
+ pushRow("write", t("dialog.cacheWrite"), view.writeTokens, view.writeMicros);
274
+ pushRow("out", t("dialog.output"), view.outTokens, view.outMicros);
275
+ const balanceFallback = balance === null || balance.status === "loading"
276
+ ? t("balance.loading")
277
+ : balance.status === "signed-out"
278
+ ? t("balance.signedOut")
279
+ : balance.status !== "ready"
280
+ ? t("balance.unavailable")
281
+ : null;
282
+ const rechargeText = balanceFallback ?? formatWallets(balance.wallets);
283
+ const bonusText = balanceFallback ?? formatWallets(balance.bonusWallets);
284
+
285
+ return jsxs("span", {
286
+ ref: rootRef,
287
+ className: styles.anchor,
288
+ "data-composer-stat": "session-cost",
289
+ children: [
290
+ jsxs("button", {
291
+ type: "button",
292
+ className: styles.pill,
293
+ "aria-haspopup": "dialog",
294
+ "aria-expanded": open,
295
+ "aria-label": t("pill.aria", { cost: formatCost(view.totalMicros) }),
296
+ onClick: () => setOpen(!open),
297
+ children: [
298
+ receiptIcon(14),
299
+ jsx("span", { className: styles.label, children: label })
300
+ ]
301
+ }),
302
+ open && createPortal(jsxs("div", {
303
+ ref: panelRef,
304
+ className: styles.panel,
305
+ role: "dialog",
306
+ "aria-label": t("dialog.title"),
307
+ style: pos ?? MEASURE_STYLE,
308
+ children: [
309
+ jsxs("div", {
310
+ className: styles.title,
311
+ children: [
312
+ jsx("span", {
313
+ className: styles.titleLabel,
314
+ children: [receiptIcon(16), t("dialog.title")]
315
+ }),
316
+ jsx("span", {
317
+ className: styles.titleValue,
318
+ children: total
319
+ })
320
+ ]
321
+ }),
322
+ jsx("div", {
323
+ className: styles.titleRule,
324
+ "aria-hidden": true
325
+ }),
326
+ jsx("dl", {
327
+ className: styles.details,
328
+ children: rows
329
+ }),
330
+ jsx("p", {
331
+ className: styles.note,
332
+ children: t("dialog.note")
333
+ }),
334
+ jsx("div", {
335
+ className: styles.divider,
336
+ "aria-hidden": true
337
+ }),
338
+ jsx("div", {
339
+ className: styles.sectionTitle,
340
+ children: t("balance.title")
341
+ }),
342
+ jsxs("dl", {
343
+ className: styles.details,
344
+ children: [
345
+ jsx("dt", { children: t("balance.recharge") }),
346
+ jsx("dd", { children: rechargeText }),
347
+ jsx("dt", { children: t("balance.bonus") }),
348
+ jsx("dd", { children: bonusText })
349
+ ]
350
+ })
351
+ ]
352
+ }), document.body)
353
+ ]
354
+ });
355
+ }
356
+
357
+ // ── 插件入口 ────────────────────────────────────────────────────────
358
+ /** 需要 slots(注册插槽)与 locale(中英文案)。 */
359
+ const inject = ["slots", "locale", "remote", "remote.account"];
360
+
361
+ /**
362
+ * 把费用胶囊注册到输入框底部的统计行,排在 token 用量(usage)后面。
363
+ * @param {object} ctx - 客户端 Cordis 上下文。
364
+ */
365
+ function apply(ctx) {
366
+ const namespace = "session-cost";
367
+ ctx.effect(() => ctx.locale.register(namespace, { zh, en }), "session-cost: dictionaries");
368
+ /** 读一次账号余额;失败/未登录都归一成弹窗可显示的状态。 */
369
+ const readBalance = async () => {
370
+ try {
371
+ const version = globalThis.__DSH_BOOT__ && globalThis.__DSH_BOOT__.release && globalThis.__DSH_BOOT__.release.version
372
+ ? globalThis.__DSH_BOOT__.release.version
373
+ : "0.2.1-rc.2";
374
+ const account = ctx.remote && ctx.remote.account;
375
+ if (!account || typeof account.getBalance !== "function") return { status: "unavailable" };
376
+ const result = await account.getBalance({
377
+ version,
378
+ locale: ctx.locale.getSnapshot().active,
379
+ timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60
380
+ });
381
+ if (!result.ok) return { status: "unavailable" };
382
+ const value = result.value;
383
+ if (value === null) return { status: "signed-out" };
384
+ if (value.status !== "ready") return { status: "unavailable" };
385
+ return {
386
+ status: "ready",
387
+ wallets: Array.isArray(value.value) ? value.value : [],
388
+ bonusWallets: Array.isArray(value.bonusWallets) ? value.bonusWallets : []
389
+ };
390
+ } catch {
391
+ return { status: "unavailable" };
392
+ }
393
+ };
394
+ ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
395
+ name: "conversation.composer.dock",
396
+ id: "session-cost",
397
+ order: 2,
398
+ locale: namespace,
399
+ inject: () => ({ readBalance })
400
+ }, SessionCostPill));
401
+ }
402
+
403
+ exports.apply = apply;
404
+ exports.inject = inject;
405
+ return module.exports;
406
+ }
407
+ });
package/lib/index.js ADDED
@@ -0,0 +1,292 @@
1
+ /**
2
+ * dsh-cost-slip —— 宿主端会话投影。
3
+ *
4
+ * 每收到一条已结算的 assistant 回复,就按它的 token 用量、模型和该条回复的时间
5
+ * (高峰 / 空闲价)累加费用,得到整个会话的估算费用(单位:微元,1e-6 元)。
6
+ * 客户端插件 dsh-cost-slip 在输入框底部统计行里读这个投影并显示“费用 ¥x.xx”。
7
+ *
8
+ * 为什么放在宿主端:
9
+ * · 会话事件里有每条回复的 usage、model、time,能精确到每一条;
10
+ * · 客户端现成的 tokenUsage 投影只有总量,没有模型和时间,算不准峰谷价;
11
+ * · 投影会随会话日志重放,刷新 / 重开页面后数字不变。
12
+ */
13
+
14
+ import { z } from "zod";
15
+ import { pricesAt } from "./pricing.js";
16
+
17
+ /** 费用分项:命中缓存输入 / 未命中输入 / 缓存写入 / 输出,单位微元。 */
18
+ const costSchema = z.object({
19
+ hit: z.number().int().nonnegative(),
20
+ miss: z.number().int().nonnegative(),
21
+ write: z.number().int().nonnegative(),
22
+ out: z.number().int().nonnegative(),
23
+ total: z.number().int().nonnegative()
24
+ }).strict();
25
+
26
+ /** token 分项,字段与 usage 对齐。 */
27
+ const tokenSchema = z.object({
28
+ hit: z.number().int().nonnegative(),
29
+ miss: z.number().int().nonnegative(),
30
+ write: z.number().int().nonnegative(),
31
+ out: z.number().int().nonnegative()
32
+ }).strict();
33
+
34
+ /** 上一条被计价样本,用于同一步骤重报时“先减后加”。 */
35
+ const lastSampleSchema = z.object({
36
+ turn: z.number().int().nonnegative(),
37
+ step: z.number().int().nonnegative(),
38
+ buckets: z.object({
39
+ uncachedInputTokens: z.number().int().nonnegative(),
40
+ cacheReadTokens: z.number().int().nonnegative(),
41
+ cacheWriteTokens: z.number().int().nonnegative(),
42
+ outputTokens: z.number().int().nonnegative()
43
+ }).strict(),
44
+ cost: costSchema,
45
+ costTokens: tokenSchema
46
+ }).strict();
47
+
48
+ /** 持久化状态(也会被投影缓存写入磁盘,读取时按此 schema 校验)。 */
49
+ const stateSchema = z.object({
50
+ cost: costSchema,
51
+ tokens: tokenSchema,
52
+ last: lastSampleSchema.nullable(),
53
+ route: z.object({
54
+ provider: z.string(),
55
+ model: z.string()
56
+ }).strict().nullable(),
57
+ lastPeak: z.boolean().nullable(),
58
+ lastTime: z.number().int().nonnegative().nullable(),
59
+ samples: z.number().int().nonnegative()
60
+ }).strict();
61
+
62
+ /** 下发到客户端的视图。 */
63
+ const viewSchema = z.object({
64
+ totalMicros: z.number().int().nonnegative(),
65
+ hitMicros: z.number().int().nonnegative(),
66
+ missMicros: z.number().int().nonnegative(),
67
+ writeMicros: z.number().int().nonnegative(),
68
+ outMicros: z.number().int().nonnegative(),
69
+ hitTokens: z.number().int().nonnegative(),
70
+ missTokens: z.number().int().nonnegative(),
71
+ writeTokens: z.number().int().nonnegative(),
72
+ outTokens: z.number().int().nonnegative(),
73
+ lastPeak: z.boolean().nullable(),
74
+ lastTime: z.number().int().nonnegative().nullable(),
75
+ samples: z.number().int().nonnegative()
76
+ }).strict();
77
+
78
+ const emptyCost = () => ({ hit: 0, miss: 0, write: 0, out: 0, total: 0 });
79
+ const emptyTokens = () => ({ hit: 0, miss: 0, write: 0, out: 0 });
80
+
81
+ const isCount = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
82
+
83
+ /**
84
+ * 取一条结算事件里的 usage。与 dsh-token-meter 的取法保持一致:
85
+ * assistant/message 用 data.usage;assistant/attempt 用 stream 里最后一个 usage 块。
86
+ * @param {object} event - 会话事件。
87
+ * @returns {object | undefined} usage 原始对象。
88
+ */
89
+ function usageOf(event) {
90
+ if (event.type === "assistant/message" && event.data?.usage !== undefined) return event.data.usage;
91
+ const stream = event.data?.stream;
92
+ if (!Array.isArray(stream)) return undefined;
93
+ for (let index = stream.length - 1; index >= 0; index -= 1) {
94
+ const record = stream[index];
95
+ if (record?.type === "chunk" && record.chunk?.type === "usage") return record.chunk.usage;
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ /**
101
+ * 把 provider usage 归一成四个计费桶;缺字段按 0,非法值整条丢弃。
102
+ * @param {unknown} usage - usage 原始对象。
103
+ * @returns {{uncachedInputTokens:number,cacheReadTokens:number,cacheWriteTokens:number,outputTokens:number} | null}
104
+ */
105
+ function normalizeUsage(usage) {
106
+ if (typeof usage !== "object" || usage === null) return null;
107
+ const input = usage.inputTokens;
108
+ const output = usage.outputTokens;
109
+ const read = usage.cacheReadTokens === undefined ? 0 : usage.cacheReadTokens;
110
+ const write = usage.cacheWriteTokens === undefined ? 0 : usage.cacheWriteTokens;
111
+ if (!isCount(input) || !isCount(output) || !isCount(read) || !isCount(write)) return null;
112
+ return {
113
+ uncachedInputTokens: input,
114
+ cacheReadTokens: read,
115
+ cacheWriteTokens: write,
116
+ outputTokens: output
117
+ };
118
+ }
119
+
120
+ const sameBuckets = (left, right) => left.uncachedInputTokens === right.uncachedInputTokens
121
+ && left.cacheReadTokens === right.cacheReadTokens
122
+ && left.cacheWriteTokens === right.cacheWriteTokens
123
+ && left.outputTokens === right.outputTokens;
124
+
125
+ /**
126
+ * 归一化后的 usage 只在这里的一处换成钱:token 数 × 元/百万 token = 微元。
127
+ * 每个分项四舍五入到 1 微元,累加误差可以忽略。
128
+ */
129
+ function costOf(buckets, prices) {
130
+ const hit = Math.round(buckets.cacheReadTokens * prices.hit);
131
+ const miss = Math.round(buckets.uncachedInputTokens * prices.miss);
132
+ // 官方未单独收取缓存写入费;若出现写入 token,按未命中价保守估算。
133
+ const write = Math.round(buckets.cacheWriteTokens * prices.miss);
134
+ const out = Math.round(buckets.outputTokens * prices.out);
135
+ return {
136
+ hit,
137
+ miss,
138
+ write,
139
+ out,
140
+ total: hit + miss + write + out
141
+ };
142
+ }
143
+
144
+ const addCost = (left, right) => ({
145
+ hit: left.hit + right.hit,
146
+ miss: left.miss + right.miss,
147
+ write: left.write + right.write,
148
+ out: left.out + right.out,
149
+ total: left.total + right.total
150
+ });
151
+
152
+ const subtractCost = (left, right) => ({
153
+ hit: left.hit - right.hit,
154
+ miss: left.miss - right.miss,
155
+ write: left.write - right.write,
156
+ out: left.out - right.out,
157
+ total: left.total - right.total
158
+ });
159
+
160
+ const addTokens = (left, right) => ({
161
+ hit: left.hit + right.hit,
162
+ miss: left.miss + right.miss,
163
+ write: left.write + right.write,
164
+ out: left.out + right.out
165
+ });
166
+
167
+ const subtractTokens = (left, right) => ({
168
+ hit: left.hit - right.hit,
169
+ miss: left.miss - right.miss,
170
+ write: left.write - right.write,
171
+ out: left.out - right.out
172
+ });
173
+
174
+ const tokensOf = (buckets) => ({
175
+ hit: buckets.cacheReadTokens,
176
+ miss: buckets.uncachedInputTokens,
177
+ write: buckets.cacheWriteTokens,
178
+ out: buckets.outputTokens
179
+ });
180
+
181
+ /**
182
+ * 每条结算事件归属的模型。
183
+ * @param {object} event - 会话事件。
184
+ * @param {object | null} route - 最近一次 request/header 的 provider/model。
185
+ * @returns {string} 模型 id;未知时退回 Flash。
186
+ */
187
+ function modelOf(event, route) {
188
+ const sourceModel = event.type === "assistant/message" ? event.data?.message?.source?.model : undefined;
189
+ if (typeof sourceModel === "string" && sourceModel.length > 0) return sourceModel;
190
+ if (route !== null && route.model.length > 0) return route.model;
191
+ return "deepseek-flash";
192
+ }
193
+
194
+ /** dsh-cost-slip 的投影单元。 */
195
+ const sessionCostProjection = {
196
+ key: "sessionCost",
197
+ stateVersion: 1,
198
+ stateSchema,
199
+ init: () => ({
200
+ cost: emptyCost(),
201
+ tokens: emptyTokens(),
202
+ last: null,
203
+ route: null,
204
+ lastPeak: null,
205
+ lastTime: null,
206
+ samples: 0
207
+ }),
208
+ apply: (state, event) => {
209
+ if (event.type === "request/header") {
210
+ const config = event.data?.header?.config;
211
+ const model = typeof config?.model === "string" ? config.model : "";
212
+ if (model.length === 0) return state;
213
+ return {
214
+ ...state,
215
+ route: {
216
+ provider: typeof config?.provider === "string" ? config.provider : "",
217
+ model
218
+ }
219
+ };
220
+ }
221
+ // 重试:上一条样本已经计过费,保留它;清掉 last,让下一次 attempt 是“新增”而不是“替换”。
222
+ if (event.type === "llm/retry-started") {
223
+ if (state.last !== null && state.last.turn === event.data?.turn && state.last.step === event.data?.step) return {
224
+ ...state,
225
+ last: null
226
+ };
227
+ return state;
228
+ }
229
+ if (event.type !== "assistant/message" && event.type !== "assistant/attempt") return state;
230
+ const turn = event.data?.turn;
231
+ const step = event.data?.step;
232
+ if (!isCount(turn) || !isCount(step)) return state;
233
+ const buckets = normalizeUsage(usageOf(event));
234
+ if (buckets === null) return state;
235
+ const previous = state.last !== null && state.last.turn === turn && state.last.step === step ? state.last : null;
236
+ if (previous !== null && sameBuckets(previous.buckets, buckets)) return state;
237
+ const model = modelOf(event, state.route);
238
+ const prices = pricesAt(model, event.time);
239
+ const cost = costOf(buckets, prices);
240
+ const tokens = tokensOf(buckets);
241
+ return {
242
+ ...state,
243
+ cost: previous === null
244
+ ? addCost(state.cost, cost)
245
+ : addCost(subtractCost(state.cost, previous.cost), cost),
246
+ tokens: previous === null
247
+ ? addTokens(state.tokens, tokens)
248
+ : addTokens(subtractTokens(state.tokens, previous.costTokens ?? tokens), tokens),
249
+ last: {
250
+ turn,
251
+ step,
252
+ buckets,
253
+ cost,
254
+ // 保存这一条样本的 token 分项,重报时才能准确减掉。
255
+ costTokens: tokens
256
+ },
257
+ lastPeak: prices.peak,
258
+ lastTime: event.time,
259
+ samples: previous === null ? state.samples + 1 : state.samples
260
+ };
261
+ },
262
+ wire: {
263
+ viewSchema,
264
+ view: (state) => ({
265
+ totalMicros: state.cost.total,
266
+ hitMicros: state.cost.hit,
267
+ missMicros: state.cost.miss,
268
+ writeMicros: state.cost.write,
269
+ outMicros: state.cost.out,
270
+ hitTokens: state.tokens.hit,
271
+ missTokens: state.tokens.miss,
272
+ writeTokens: state.tokens.write,
273
+ outTokens: state.tokens.out,
274
+ lastPeak: state.lastPeak,
275
+ lastTime: state.lastTime,
276
+ samples: state.samples
277
+ })
278
+ }
279
+ };
280
+
281
+ export const name = "session-cost";
282
+
283
+ /** 没有投影注册表就不需要挂载这个插件。 */
284
+ export const inject = ["sessionProjections"];
285
+
286
+ /**
287
+ * 注册会话费用投影。
288
+ * @param {object} ctx - 宿主 Cordis 上下文。
289
+ */
290
+ export function apply(ctx) {
291
+ ctx.sessionProjections.register(sessionCostProjection);
292
+ }
package/lib/pricing.js ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * DeepSeek 官方 API 价目(人民币,元 / 百万 token)。
3
+ *
4
+ * 数据来源与规则(2026-10-18 核对):
5
+ * · 官方定价:https://api-docs.deepseek.com/zh-cn/quick_start/pricing
6
+ * · 2026-08-17 起实行峰谷定价:北京时间周一至周五 9:00–12:00、14:00–18:00 为高峰,
7
+ * 其余时段(含周末、法定节假日全天)为空闲;高峰价 = 空闲价 × 2。
8
+ * · 2026-09-10 起 Flash 降价:缓存命中 0.05→0.02、未命中 1.5→1、输出 4.5→4。
9
+ * · V4 Pro 的单价为 Flash 的 3 倍。
10
+ *
11
+ * 要改价格,只改本文件里的 FLASH_TIERS 即可;改完重启 DSH 生效。
12
+ * 法定节假日每 11 月国务院发布次年安排后,记得把下一年的日期补进 HOLIDAY_VALLEYS。
13
+ */
14
+
15
+ /** 计价单位。 */
16
+ export const CURRENCY = "CNY";
17
+
18
+ /** 高峰时段(北京时间,整点区间,左闭右开)。 */
19
+ const PEAK_RANGES = [
20
+ [9, 12],
21
+ [14, 18]
22
+ ];
23
+
24
+ /** 峰谷机制生效时刻:北京时间 2026-08-17 00:00。此前全天同价。 */
25
+ const PEAK_FROM_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
26
+
27
+ /** “法定节假日全天谷价”生效时刻:北京时间 2026-09-19 00:00。 */
28
+ const HOLIDAY_VALLEY_FROM_MS = Date.UTC(2026, 8, 18, 16, 0, 0);
29
+
30
+ /**
31
+ * 2026 年放假日期(只列放假的日期;调休上班日都落在周末,本来就按谷价)。
32
+ * 来源:国办发明电〔2025〕7 号。
33
+ */
34
+ const HOLIDAY_VALLEYS = new Set([
35
+ "2026-01-01", "2026-01-02", "2026-01-03",
36
+ "2026-02-15", "2026-02-16", "2026-02-17", "2026-02-18", "2026-02-19",
37
+ "2026-02-20", "2026-02-21", "2026-02-22", "2026-02-23",
38
+ "2026-04-04", "2026-04-05", "2026-04-06",
39
+ "2026-05-01", "2026-05-02", "2026-05-03", "2026-05-04", "2026-05-05",
40
+ "2026-06-19", "2026-06-20", "2026-06-21",
41
+ "2026-09-25", "2026-09-26", "2026-09-27",
42
+ "2026-10-01", "2026-10-02", "2026-10-03", "2026-10-04",
43
+ "2026-10-05", "2026-10-06", "2026-10-07"
44
+ ]);
45
+
46
+ /**
47
+ * Flash 的空闲时段单价(元 / 百万 token)。
48
+ * 数组按时间升序;取第一个早于等于请求时间的档位。
49
+ */
50
+ const FLASH_TIERS = [
51
+ { fromMs: 0, hit: 0.05, miss: 1.5, out: 4.5 },
52
+ { fromMs: Date.UTC(2026, 8, 9, 16, 0, 0), hit: 0.02, miss: 1, out: 4 }
53
+ ];
54
+
55
+ /** V4 Pro 的空闲时段单价(元 / 百万 token),不随 Flash 降价。 */
56
+ const PRO_TIERS = [
57
+ { fromMs: 0, hit: 0.15, miss: 4.5, out: 13.5 }
58
+ ];
59
+
60
+ /**
61
+ * 判断某一时刻是否处于高峰时段。
62
+ * @param {number} timeMs - Unix 毫秒时间戳。
63
+ * @returns {boolean} 高峰为 true;周末、法定节假日、峰谷机制生效前均为 false。
64
+ */
65
+ export function isPeak(timeMs) {
66
+ if (!Number.isFinite(timeMs) || timeMs < PEAK_FROM_MS) return false;
67
+ // 用 UTC 读“北京时间日历日”:+8h 后直接读 UTC 字段。
68
+ const beijing = new Date(timeMs + 8 * 3600 * 1000);
69
+ const dayOfWeek = beijing.getUTCDay();
70
+ if (dayOfWeek === 0 || dayOfWeek === 6) return false;
71
+ if (timeMs >= HOLIDAY_VALLEY_FROM_MS && HOLIDAY_VALLEYS.has(beijing.toISOString().slice(0, 10))) return false;
72
+ const hour = beijing.getUTCHours();
73
+ return PEAK_RANGES.some(([start, end]) => hour >= start && hour < end);
74
+ }
75
+
76
+ /**
77
+ * 某一时刻、某一模型的生效单价。
78
+ * @param {string} model - 模型 id(如 deepseek-flash、deepseek-v4-pro)。
79
+ * @param {number} timeMs - Unix 毫秒时间戳。
80
+ * @returns {{hit:number, miss:number, out:number, peak:boolean}} 元 / 百万 token。
81
+ */
82
+ export function pricesAt(model, timeMs) {
83
+ const tiers = /pro/i.test(String(model ?? "")) ? PRO_TIERS : FLASH_TIERS;
84
+ const tier = tiers.findLast((item) => timeMs >= item.fromMs) ?? tiers[0];
85
+ const factor = isPeak(timeMs) ? 2 : 1;
86
+ return {
87
+ hit: tier.hit * factor,
88
+ miss: tier.miss * factor,
89
+ out: tier.out * factor,
90
+ peak: factor === 2
91
+ };
92
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Cost slip",
4
+ "description": "Shows the current session's estimated cost in CNY in the composer dock; open it for the cost breakdown and account balance."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "费用小票",
4
+ "description": "在输入框底部显示当前会话的估算费用(人民币),点击打开,可显示费用明细和账号余额"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "dsh-cost-slip",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.4.0",
4
+ "description": "在输入框底部显示当前会话的估算费用(人民币),点击打开,可显示费用明细和账号余额",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./locale/*.json": "./locale/*.json",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "dsh": {
14
+ "bundle": {
15
+ "patch": "./cordis.patch.yml"
16
+ },
17
+ "client": {
18
+ "platform": "web",
19
+ "inject": [
20
+ "@deepseek-ai/dsh-client-ui-chat",
21
+ "@deepseek-ai/dsh-client-locale"
22
+ ]
23
+ }
24
+ },
25
+ "icon": "./icon.svg",
26
+ "keywords": [
27
+ "dsh",
28
+ "deepseek-harness",
29
+ "plugin",
30
+ "cost",
31
+ "billing",
32
+ "pricing",
33
+ "balance"
34
+ ],
35
+ "author": "pktheme",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/pktheme/dsh-cost-slip.git"
40
+ },
41
+ "homepage": "https://github.com/pktheme/dsh-cost-slip#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/pktheme/dsh-cost-slip/issues"
44
+ },
45
+ "dependencies": {
46
+ "zod": "4.4.3"
47
+ },
48
+ "files": [
49
+ "lib",
50
+ "assets",
51
+ "icon.svg",
52
+ "locale",
53
+ "cordis.patch.yml",
54
+ "README.md"
55
+ ]
56
+ }