balance-dsh 1.2.1 → 1.2.3
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.md +18 -12
- package/lib/client.js +362 -51
- package/lib/index.js +96 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,9 +7,10 @@ DSH 插件:在 Web UI 中直接显示 DeepSeek 账户余额和当前会话 tok
|
|
|
7
7
|
- **侧边栏余额行**:侧边栏展开时显示在底部(紧贴"设置"按钮上方),格式:钱包图标 + `余额` + 金额(带货币单位)+ 时段标签(高峰红 / 空闲绿)。金额按余额状态变色:充足绿色、偏低(<10)琥珀黄、为 0 显示红色"去充值"。
|
|
8
8
|
- **收起态 FAB**:侧边栏收起时,余额显示为左下角悬浮胶囊。
|
|
9
9
|
- **自动刷新**:每 60 秒自动刷新一次;每次对话完成(新回复落库)也会立即刷新;右键余额任意区域可手动刷新。
|
|
10
|
-
-
|
|
11
|
-
-
|
|
10
|
+
- **余额与使用统计**:单击余额打开**居中弹窗**(带半透明模糊遮罩),展示账户余额、近 90 天计费 Token、估算费用、活跃天数、按模型与高峰/空闲拆分、今日/近 7 天/近 30 天、日均与峰值日,以及按日使用热力图;点遮罩 / 按 `Esc` / 点关闭按钮均可关闭,双击余额在**应用内**打开 DeepSeek 官方用量页(官方版自带内嵌浏览器,不再弹出系统浏览器;旧版环境自动回退到浏览器)。
|
|
11
|
+
- **高峰/空闲计费**:官方高峰窗口为**工作日 09:00-12:00、14:00-18:00(北京时间)**;周末、**中国法定节假日全天**都按空闲计费,**调休上班的周末按工作日**计峰谷。节假日安排会自动从公开数据源补录(内置 2026 年表作离线兜底,无需手动更新)。消耗金额**跨时段精确计费**:高峰时段的 token 按高峰价 ×2、空闲时段按目录价,按事件时间分桶计算。
|
|
12
12
|
- **会话消耗行**:对话框下方实时显示当前会话消耗(新增 tokens + 估算金额),悬停查看完整明细(模型、时段拆分、计费口径)。
|
|
13
|
+
- **应用内查看官方用量**:双击余额、或点统计面板里的「查看官方用量」,都会在应用内嵌浏览器里打开官方用量页(余额、消费金额、请求次数、Tokens);「去充值」同理。旧版 DSH 缺少内嵌浏览器时自动回退系统浏览器。
|
|
13
14
|
- **`/balance` 命令**:命令行查询余额明细和当前会话消耗。
|
|
14
15
|
|
|
15
16
|
### 使用统计口径
|
|
@@ -17,7 +18,7 @@ DSH 插件:在 Web UI 中直接显示 DeepSeek 账户余额和当前会话 tok
|
|
|
17
18
|
- 热力图按北京时间聚合最近 90 天持久化会话中的 `assistant/message` Token usage。
|
|
18
19
|
- 费用按照每条消息的实际模型和峰谷时段估算,最终金额以 DeepSeek 官方账单为准。
|
|
19
20
|
- 为避免历史会话过多拖慢 DSH,最多扫描最近 200 个会话;服务端统计和客户端展示结果均缓存 5 分钟,重复打开菜单不会重复请求。
|
|
20
|
-
-
|
|
21
|
+
- 账户余额优先读取 **DSH 官方账号**(官方版登录后即可用,无需 API Key);未登录官方账号时回退到 API Key。两者都不可用时仍可查看本地 Token 与费用统计,只是余额会显示为不可用。
|
|
21
22
|
|
|
22
23
|
## 安装
|
|
23
24
|
|
|
@@ -53,7 +54,11 @@ dsh plugin --profile desktop add github:Rannist/balance-dsh
|
|
|
53
54
|
name: balance-dsh
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
###
|
|
57
|
+
### 余额来源
|
|
58
|
+
|
|
59
|
+
**官方 DeepSeek Harness 桌面版**:在应用内登录 DeepSeek 账号后,插件会**自动读取官方余额,无需任何配置**。
|
|
60
|
+
|
|
61
|
+
**其他环境**(或不想登录官方账号时):改用 API Key,三选一——
|
|
57
62
|
|
|
58
63
|
1. 环境变量 `DEEPSEEK_API_KEY`,或
|
|
59
64
|
2. 在注册项的 `config.apiKey` 中设置,或
|
|
@@ -77,10 +82,10 @@ dsh plugin --profile desktop add github:Rannist/balance-dsh
|
|
|
77
82
|
- {start: 14, end: 18}
|
|
78
83
|
peakWeekdays: [1, 2, 3, 4, 5] # 可选;1=周一 … 5=周五,0/6=周末
|
|
79
84
|
price: # 可选;逐项覆盖对应模型价格(元/百万 tokens)
|
|
80
|
-
inputCnyPerMillion: 1
|
|
81
|
-
cacheReadCnyPerMillion: 0.
|
|
85
|
+
inputCnyPerMillion: 1
|
|
86
|
+
cacheReadCnyPerMillion: 0.02
|
|
82
87
|
cacheWriteCnyPerMillion: 0
|
|
83
|
-
outputCnyPerMillion: 4
|
|
88
|
+
outputCnyPerMillion: 4
|
|
84
89
|
```
|
|
85
90
|
|
|
86
91
|
> 注意:插件已自动按高峰/空闲时段计费(高峰价 = 目录价 × 2),**不要再设置 `config.peak: true`**,否则会重复翻倍。
|
|
@@ -96,20 +101,21 @@ pnpm remove balance-dsh
|
|
|
96
101
|
|
|
97
102
|
## 权限 / 数据
|
|
98
103
|
|
|
99
|
-
-
|
|
100
|
-
-
|
|
104
|
+
- 已登录官方账号时,余额通过 **DSH 内置账号服务**读取(登录凭据由应用自身管理,插件不接触、不保存任何 token)。
|
|
105
|
+
- 仅当回退到 API Key 时,才会向 `api.deepseek.com` 的 `/user/balance` 端点查询余额(且只在你**主动刷新**或对话完成时)。
|
|
106
|
+
- 插件不收集、不上传任何会话内容到第三方。
|
|
101
107
|
- 消费统计完全基于本地会话事件(`request/header`、`assistant/message` 的 token usage)计算,不向 DeepSeek 之外的服务发送数据。
|
|
102
108
|
|
|
103
|
-
##
|
|
109
|
+
## 价格表(空闲时段价,元/百万 tokens)
|
|
104
110
|
|
|
105
111
|
| 模型 | 输入(缓存未命中) | 输入(缓存命中) | 输出 |
|
|
106
112
|
|---|---|---|---|
|
|
107
|
-
| deepseek-
|
|
113
|
+
| deepseek-flash | 1 | 0.02 | 4 |
|
|
108
114
|
| deepseek-v4-pro | 4.5 | 0.15 | 13.5 |
|
|
109
115
|
| deepseek-chat(旧) | 2 | 0.5 | 3 |
|
|
110
116
|
| deepseek-reasoner(旧) | 4 | 1 | 16 |
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
价格自 **2026-09-10 12:00(北京时间)** 起生效(flash 系列由 1.5 / 0.05 / 4.5 下调);**高峰时段 = 空闲价 × 2**。调价前的历史消耗仍按当时的旧价计算,不会被重算。DeepSeek 再次调价时需同步更新 `lib/index.js` 的 `MODEL_PRICING` 与 `lib/client.js` 的 `DEFAULT_PRICING`,并把被替换的旧价移入 `LEGACY_MODEL_PRICING`。
|
|
113
119
|
|
|
114
120
|
## License
|
|
115
121
|
|
package/lib/client.js
CHANGED
|
@@ -40,13 +40,34 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
.balance-fab:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
41
41
|
.balance-fab-dark{color:#fff;background:var(--dsw-alias-state-error-primary,#e5484d);border-color:var(--dsw-alias-state-error-primary,#e5484d)}
|
|
42
42
|
.balance-fab-dark:hover{background:#cf3a3f}
|
|
43
|
-
.
|
|
44
|
-
.balance-usage-
|
|
43
|
+
/* ── 消耗行 pill:规格逐条照抄 DSH 原生 StatsPills 的 .pill(_091hzG_pill)── */
|
|
44
|
+
.balance-usage-wrap{min-width:0;display:inline-flex}
|
|
45
|
+
.balance-usage-line{box-sizing:border-box;corner-shape:round;max-width:100%;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);font-variant-numeric:tabular-nums;line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));white-space:nowrap;background:0 0;border:none;border-radius:999px;align-items:center;gap:6px;padding:1px 8px;display:inline-flex;cursor:pointer;transition:background .15s var(--ds-ease-in-out,.2s)}
|
|
46
|
+
.balance-usage-line svg{flex:none;width:14px;height:14px}
|
|
47
|
+
.balance-usage-line:hover,.balance-usage-line[aria-expanded="true"]{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-secondary)}
|
|
48
|
+
.balance-usage-label{text-overflow:ellipsis;min-width:0;overflow:hidden}
|
|
49
|
+
/* ── 消耗明细浮层:规格照抄 DSH 原生「会话统计」面板(.HS2XBG_panel/.title/.titleRule/.details)── */
|
|
50
|
+
.balance-usage-panel{box-sizing:border-box;position:fixed;z-index:1100;border:0;border-radius:var(--dsw-radius-lg,12px);background:var(--dsw-specific-menu,var(--dsw-alias-bg-module-platform,#242526));width:max-content;min-width:min(300px,100vw - 24px);max-width:min(440px,100vw - 24px);backdrop-filter:var(--dsw-menu-backdrop-filter,none);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent,0 8px 24px rgba(0,0,0,.32));color:var(--dsw-alias-label-secondary);cursor:default;padding:16px;font-size:12px;line-height:18px;animation:balance-stats-panel-in .12s var(--ds-ease-in-out,ease-out) both}
|
|
51
|
+
.balance-usage-panel__title{display:flex;justify-content:space-between;gap:16px;margin-bottom:8px;color:var(--dsw-alias-label-primary);font-weight:500}
|
|
52
|
+
.balance-usage-panel__title--second{margin-top:16px}
|
|
53
|
+
.balance-usage-panel__rule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}
|
|
54
|
+
.balance-usage-panel__value{font-variant-numeric:tabular-nums}
|
|
55
|
+
.balance-usage-panel__label{display:inline-flex;align-items:center;gap:6px;min-width:0}
|
|
56
|
+
.balance-usage-panel__label svg{flex:none;width:14px;height:14px}
|
|
57
|
+
.balance-usage-panel__details{display:grid;grid-template-columns:minmax(76px,auto) minmax(0,1fr);gap:6px 16px;margin:0;color:var(--dsw-alias-label-tertiary)}
|
|
58
|
+
.balance-usage-panel__details dt,.balance-usage-panel__details dd{min-width:0;margin:0}
|
|
59
|
+
.balance-usage-panel__details dd{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}
|
|
45
60
|
/* ── 跨会话统计浮层(余额元素的子级,向右/向上展开) ── */
|
|
46
61
|
.balance-stats-anchor{position:relative;display:flex;min-width:0;overflow:visible}
|
|
47
62
|
.balance-stats-anchor--wide{width:100%}
|
|
48
63
|
.balance-stats-anchor--compact{position:fixed;left:68px;bottom:12px;z-index:10000}
|
|
49
64
|
.balance-stats-anchor--compact .balance-fab{position:static}
|
|
65
|
+
.balance-platform-layer{box-sizing:border-box;position:fixed;inset:0;z-index:2147483000;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base-platform,var(--dsw-alias-bg-base,#1b1b1c));animation:balance-stats-backdrop-in .16s var(--ds-ease-in-out,ease-out) both}
|
|
66
|
+
.balance-platform-header{flex:none;display:flex;align-items:center;gap:8px;height:44px;padding:0 12px;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.2));-webkit-app-region:drag}
|
|
67
|
+
.balance-platform-back{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary,inherit);font:inherit;font-size:13px;cursor:pointer;-webkit-app-region:no-drag;transition:background .15s var(--ds-ease-in-out,.2s)}
|
|
68
|
+
.balance-platform-back svg{flex:none;display:block}
|
|
69
|
+
.balance-platform-back:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
70
|
+
.balance-platform-viewport{flex:1;min-height:0}
|
|
50
71
|
.balance-stats-layer{box-sizing:border-box;position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.24));backdrop-filter:var(--dsw-mask-blur,blur(2px));-webkit-backdrop-filter:var(--dsw-mask-blur,blur(2px));animation:balance-stats-backdrop-in .16s var(--ds-ease-in-out,ease-out) both}
|
|
51
72
|
.balance-stats-layer--closing{pointer-events:none;animation:balance-stats-backdrop-out .14s var(--ds-ease-in-out,ease-in) both}
|
|
52
73
|
.balance-stats-panel{box-sizing:border-box;position:relative;z-index:10000;width:min(800px,100%);max-height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-gutter:stable;padding:0 0 24px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));border:0;border-radius:24px;box-shadow:var(--dsw-elevation-prominent,0 14px 36px rgba(0,0,0,.24));transform-origin:center center;will-change:transform,opacity;contain:layout style;animation:balance-stats-panel-in .26s var(--ds-ease-in-out,ease-out) .04s both;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2,rgba(127,127,127,.35));--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2,rgba(127,127,127,.55))}
|
|
@@ -205,6 +226,184 @@ window.__ModuleLoader__.load({
|
|
|
205
226
|
return String(n);
|
|
206
227
|
}
|
|
207
228
|
|
|
229
|
+
// ─── 打开 DeepSeek 平台页面(用量 / 充值) ─────────────────────────────
|
|
230
|
+
// 三级策略,逐级回退:
|
|
231
|
+
// 1) DSH 官方版的「原生平台视口」——preload 通过 contextBridge 暴露的 globalThis.dshPlatform
|
|
232
|
+
// (接口:open(page, bounds) / setBounds(bounds) / close(),page 取 "usage" | "top-up")。
|
|
233
|
+
// DSH 设置→账号与余额里的「查询用量」按钮就走它(dsh-client-ui-settings-account 的
|
|
234
|
+
// PlatformOverlay + dshPlatform.open)。这个原生子窗口自带平台凭据(官方注释:
|
|
235
|
+
// "the native child owns remote content and credentials"),所以**不需要网页登录**。
|
|
236
|
+
// 注意:那个包的 platformPages 是包内私有 store,插件无法触发 DSH 自己的 host,
|
|
237
|
+
// 因此这里自己画同款覆盖层并把视口坐标喂给 bridge。
|
|
238
|
+
// 2) 应用内侧栏浏览器(dsh-client-ui-sidebar-browser)——旧版/无原生视口时用,
|
|
239
|
+
// 按 DSH 自身 openExternalLink 的判定:sidebarRightTabs 注册了 "browser" 才可用。
|
|
240
|
+
// 该视口**没有**平台凭据,打开 platform.deepseek.com 会要求网页登录。
|
|
241
|
+
// 3) window.open 交给系统浏览器。
|
|
242
|
+
// 全程 try/catch:DSH 内部机制无兼容承诺,任一级失败都要继续降级,不能拖垮插件。
|
|
243
|
+
let pluginCtx = null;
|
|
244
|
+
const USAGE_URL = "https://platform.deepseek.com/usage";
|
|
245
|
+
const TOP_UP_URL = "https://platform.deepseek.com/top_up";
|
|
246
|
+
|
|
247
|
+
/** 当前原生平台视口的覆盖层;null 表示未打开。 */
|
|
248
|
+
let platformOverlay = null;
|
|
249
|
+
|
|
250
|
+
/** 关闭原生平台视口覆盖层(幂等;bridge.close + 摘除 DOM + 解绑监听)。 */
|
|
251
|
+
function closePlatformPage() {
|
|
252
|
+
if (platformOverlay === null) return;
|
|
253
|
+
const { layer, bridge, observer, onKey } = platformOverlay;
|
|
254
|
+
platformOverlay = null;
|
|
255
|
+
try {
|
|
256
|
+
observer.disconnect();
|
|
257
|
+
} catch {
|
|
258
|
+
/* ignore */
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
window.removeEventListener("keydown", onKey, true);
|
|
262
|
+
} catch {
|
|
263
|
+
/* ignore */
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const closing = bridge.close();
|
|
267
|
+
if (closing && typeof closing.catch === "function") closing.catch(() => {});
|
|
268
|
+
} catch {
|
|
269
|
+
/* ignore */
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
layer.remove();
|
|
273
|
+
} catch {
|
|
274
|
+
/* ignore */
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 用原生平台视口打开官方平台页面。
|
|
280
|
+
* @param page - "usage"(用量)或 "top-up"(充值)。
|
|
281
|
+
* @returns 是否成功接管(false 时调用方继续降级)。
|
|
282
|
+
*/
|
|
283
|
+
function openPlatformPage(page) {
|
|
284
|
+
const bridge = typeof globalThis === "undefined" ? void 0 : globalThis.dshPlatform;
|
|
285
|
+
if (!bridge || typeof bridge.open !== "function") return false;
|
|
286
|
+
try {
|
|
287
|
+
closePlatformPage();
|
|
288
|
+
const layer = document.createElement("div");
|
|
289
|
+
layer.className = "balance-platform-layer";
|
|
290
|
+
const header = document.createElement("header");
|
|
291
|
+
header.className = "balance-platform-header";
|
|
292
|
+
// 与 DSH 的 PlatformOverlay 一致:让标题栏可拖动窗口
|
|
293
|
+
header.setAttribute("data-window-drag", "");
|
|
294
|
+
const back = document.createElement("button");
|
|
295
|
+
back.type = "button";
|
|
296
|
+
back.className = "balance-platform-back";
|
|
297
|
+
// 与 DSH 的 PlatformOverlay 一致:chevron 图标 + 文案。DSH 用的是 primitives 的
|
|
298
|
+
// IconChevronLeftOutlineRegular 组件;这里覆盖层是纯 DOM,自绘同款 SVG,
|
|
299
|
+
// 免得为了一个图标把 React 渲染引进覆盖层。
|
|
300
|
+
const chevron = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
301
|
+
chevron.setAttribute("viewBox", "0 0 16 16");
|
|
302
|
+
chevron.setAttribute("width", "16");
|
|
303
|
+
chevron.setAttribute("height", "16");
|
|
304
|
+
chevron.setAttribute("aria-hidden", "true");
|
|
305
|
+
const chevronPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
306
|
+
chevronPath.setAttribute("d", "M10 3.5 5.5 8 10 12.5");
|
|
307
|
+
chevronPath.setAttribute("fill", "none");
|
|
308
|
+
chevronPath.setAttribute("stroke", "currentColor");
|
|
309
|
+
chevronPath.setAttribute("stroke-width", "1.6");
|
|
310
|
+
chevronPath.setAttribute("stroke-linecap", "round");
|
|
311
|
+
chevronPath.setAttribute("stroke-linejoin", "round");
|
|
312
|
+
chevron.appendChild(chevronPath);
|
|
313
|
+
back.appendChild(chevron);
|
|
314
|
+
back.appendChild(document.createTextNode("返回 DeepSeek Harness"));
|
|
315
|
+
back.addEventListener("click", closePlatformPage);
|
|
316
|
+
header.appendChild(back);
|
|
317
|
+
const viewport = document.createElement("div");
|
|
318
|
+
viewport.className = "balance-platform-viewport";
|
|
319
|
+
layer.appendChild(header);
|
|
320
|
+
layer.appendChild(viewport);
|
|
321
|
+
document.body.appendChild(layer);
|
|
322
|
+
|
|
323
|
+
// 原生视口按 CSS 像素贴合这个占位元素(DSH 也是拿 getBoundingClientRect())
|
|
324
|
+
const bounds = () => {
|
|
325
|
+
const rect = viewport.getBoundingClientRect();
|
|
326
|
+
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
327
|
+
};
|
|
328
|
+
const observer = new ResizeObserver(() => {
|
|
329
|
+
if (platformOverlay === null) return;
|
|
330
|
+
try {
|
|
331
|
+
const moved = bridge.setBounds(bounds());
|
|
332
|
+
if (moved && typeof moved.catch === "function") moved.catch(() => {});
|
|
333
|
+
} catch {
|
|
334
|
+
/* ignore */
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
const onKey = (event) => {
|
|
338
|
+
if (event.key === "Escape") closePlatformPage();
|
|
339
|
+
};
|
|
340
|
+
platformOverlay = { layer, bridge, observer, onKey };
|
|
341
|
+
observer.observe(viewport);
|
|
342
|
+
window.addEventListener("keydown", onKey, true);
|
|
343
|
+
const opened = bridge.open(page, bounds());
|
|
344
|
+
if (opened && typeof opened.catch === "function") opened.catch(() => closePlatformPage());
|
|
345
|
+
return true;
|
|
346
|
+
} catch {
|
|
347
|
+
closePlatformPage();
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** 打开应用内嵌浏览器的标签页(无原生视口时的二级回退)。 */
|
|
353
|
+
function openSidebarBrowser(url) {
|
|
354
|
+
try {
|
|
355
|
+
const get = pluginCtx && typeof pluginCtx.get === "function" ? pluginCtx.get.bind(pluginCtx) : null;
|
|
356
|
+
if (get === null) return false;
|
|
357
|
+
const right = get("sidebarRight");
|
|
358
|
+
const tabs = get("sidebarRightTabs");
|
|
359
|
+
const hasBrowser = tabs && typeof tabs.get === "function" && tabs.get("browser") !== undefined;
|
|
360
|
+
if (right && typeof right.openTab === "function" && hasBrowser) {
|
|
361
|
+
right.openTab("browser", { params: { url } });
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
// 侧栏浏览器不可用(旧版 DSH / 服务未就绪)
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** 最终回退:交给系统浏览器。 */
|
|
371
|
+
function openInSystemBrowser(url) {
|
|
372
|
+
try {
|
|
373
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
374
|
+
} catch {
|
|
375
|
+
/* ignore */
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** 打开平台页面:原生视口 → 侧栏浏览器 → 系统浏览器。 */
|
|
380
|
+
function openPlatformOrUrl(page, url) {
|
|
381
|
+
if (openPlatformPage(page)) return;
|
|
382
|
+
if (openSidebarBrowser(url)) return;
|
|
383
|
+
openInSystemBrowser(url);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 外部链接的通用属性:保留 href(可右键复制),点击时拦截默认跳转、改为应用内打开。
|
|
388
|
+
* @param url - 平台页面地址(用作 href 与最终回退目标)。
|
|
389
|
+
* @param page - 原生视口的页面标识;缺省时跳过原生视口(例如非平台链接)。
|
|
390
|
+
* @param extra - 追加的事件处理器等属性。
|
|
391
|
+
*/
|
|
392
|
+
function externalLink(url, page, extra) {
|
|
393
|
+
const options = extra || {};
|
|
394
|
+
return {
|
|
395
|
+
href: url,
|
|
396
|
+
target: "_blank",
|
|
397
|
+
rel: "noreferrer",
|
|
398
|
+
...options,
|
|
399
|
+
onClick: (event) => {
|
|
400
|
+
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
401
|
+
if (typeof options.onClick === "function") options.onClick(event);
|
|
402
|
+
openPlatformOrUrl(page, url);
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
208
407
|
// ─── 当前会话模型:按模型取价(服务端 /balance-api 按 ?model= 返回对应价格) ───
|
|
209
408
|
let sessionsFace = void 0;
|
|
210
409
|
let currentModel = "";
|
|
@@ -797,9 +996,7 @@ window.__ModuleLoader__.load({
|
|
|
797
996
|
}),
|
|
798
997
|
jsx.jsx("a", {
|
|
799
998
|
className: "balance-stats-link",
|
|
800
|
-
|
|
801
|
-
target: "_blank",
|
|
802
|
-
rel: "noreferrer",
|
|
999
|
+
...externalLink(USAGE_URL, "usage"),
|
|
803
1000
|
children: "查看官方用量",
|
|
804
1001
|
}),
|
|
805
1002
|
],
|
|
@@ -904,11 +1101,17 @@ window.__ModuleLoader__.load({
|
|
|
904
1101
|
closeTimer.current = null;
|
|
905
1102
|
setStatsClosing(false);
|
|
906
1103
|
setStatsOpen(false);
|
|
907
|
-
|
|
1104
|
+
// 双击:在应用内打开官方用量页(官方版自带内嵌浏览器;旧版回退系统浏览器)
|
|
1105
|
+
// 双击:打开官方用量页(官方版用带平台凭据的原生视口,无需网页登录)
|
|
1106
|
+
openPlatformOrUrl("usage", USAGE_URL);
|
|
908
1107
|
};
|
|
909
1108
|
// 余额元素的交互。悬停提示不用原生 title,统一走主题化浮层(见 withPanel)。
|
|
910
1109
|
const interactions = {
|
|
911
|
-
|
|
1110
|
+
// 单击打开统计面板;顺便阻止 <a href> 的默认跳转(否则会同时弹系统浏览器)
|
|
1111
|
+
onClick: (event) => {
|
|
1112
|
+
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
1113
|
+
openStats(event);
|
|
1114
|
+
},
|
|
912
1115
|
onDoubleClick: openUsagePage,
|
|
913
1116
|
onContextMenu: onBalanceContextMenu,
|
|
914
1117
|
};
|
|
@@ -946,16 +1149,13 @@ window.__ModuleLoader__.load({
|
|
|
946
1149
|
if (depleted) {
|
|
947
1150
|
return jsx.jsx("a", {
|
|
948
1151
|
className: "balance-fab balance-fab-dark",
|
|
949
|
-
|
|
950
|
-
target: "_blank",
|
|
951
|
-
rel: "noreferrer",
|
|
952
|
-
onContextMenu: onBalanceContextMenu,
|
|
1152
|
+
...externalLink(TOP_UP_URL, "top-up", { onContextMenu: onBalanceContextMenu }),
|
|
953
1153
|
children: "去充值",
|
|
954
1154
|
});
|
|
955
1155
|
}
|
|
956
1156
|
return withPanel(jsx.jsx("a", {
|
|
957
1157
|
className: "balance-fab",
|
|
958
|
-
|
|
1158
|
+
...externalLink(USAGE_URL, "usage"),
|
|
959
1159
|
...interactions,
|
|
960
1160
|
children: "¥" + b.total_balance,
|
|
961
1161
|
}));
|
|
@@ -965,7 +1165,13 @@ window.__ModuleLoader__.load({
|
|
|
965
1165
|
return withPanel(jsx.jsx("div", {
|
|
966
1166
|
className: "balance-footer",
|
|
967
1167
|
...interactions,
|
|
968
|
-
|
|
1168
|
+
// 失败原因统一走主题化浮层,避免把长错误塞进 42px 的余额行
|
|
1169
|
+
children: jsx.jsx(primitives.Tooltip, {
|
|
1170
|
+
label: state.error || "余额查询失败",
|
|
1171
|
+
side: "top",
|
|
1172
|
+
delayMs: 200,
|
|
1173
|
+
children: jsx.jsx("span", { className: "balance-footer__error", children: "余额读取失败" }),
|
|
1174
|
+
}),
|
|
969
1175
|
}));
|
|
970
1176
|
}
|
|
971
1177
|
if (!state.balance) {
|
|
@@ -986,12 +1192,12 @@ window.__ModuleLoader__.load({
|
|
|
986
1192
|
jsx.jsx("span", { className: "balance-footer__label", children: "余额" }),
|
|
987
1193
|
jsx.jsx("a", {
|
|
988
1194
|
className: "balance-footer__topup",
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1195
|
+
...externalLink(TOP_UP_URL, "top-up", {
|
|
1196
|
+
// 阻止冒泡:不要把"去充值"的点击误当成本行余额的单击/双击
|
|
1197
|
+
onClick: (event) => event.stopPropagation(),
|
|
1198
|
+
onDoubleClick: (event) => event.stopPropagation(),
|
|
1199
|
+
onContextMenu: onBalanceContextMenu,
|
|
1200
|
+
}),
|
|
995
1201
|
children: "去充值",
|
|
996
1202
|
}),
|
|
997
1203
|
],
|
|
@@ -1028,6 +1234,35 @@ function UsageCostLine({ useProjection, sessionId }) {
|
|
|
1028
1234
|
const [splitUsage, setSplitUsage] = react.useState(null);
|
|
1029
1235
|
const [subagentUsage, setSubagentUsage] = react.useState(null);
|
|
1030
1236
|
const [splitReady, setSplitReady] = react.useState(false);
|
|
1237
|
+
// 消耗明细面板:**点击展开**(对齐 DSH 原生「会话统计」的交互——点开切换 + 选中态 + portal 浮层)
|
|
1238
|
+
const [detailsOpen, setDetailsOpen] = react.useState(false);
|
|
1239
|
+
const detailsAnchorRef = react.useRef(null);
|
|
1240
|
+
const detailsPanelRef = react.useRef(null);
|
|
1241
|
+
// 定位与"点外部关闭"复用 DSH primitives 的 hook,行为/视觉都与原生浮层一致。
|
|
1242
|
+
// 运行时探测(DSH 内部 API 无兼容承诺):hook 不存在时退化为无定位(面板仍渲染),不拖垮消耗行。
|
|
1243
|
+
// 注意这里的条件调用是安全的——primitives 是模块级常量,同一组件生命周期内判定结果恒定。
|
|
1244
|
+
const anchoredPosition =
|
|
1245
|
+
typeof primitives.useAnchoredPosition === "function"
|
|
1246
|
+
? primitives.useAnchoredPosition({
|
|
1247
|
+
open: detailsOpen,
|
|
1248
|
+
anchorRef: detailsAnchorRef,
|
|
1249
|
+
panelRef: detailsPanelRef,
|
|
1250
|
+
side: "top",
|
|
1251
|
+
gap: 6,
|
|
1252
|
+
margin: 8,
|
|
1253
|
+
})
|
|
1254
|
+
: null;
|
|
1255
|
+
if (typeof primitives.useDismissOnOutsidePointer === "function") {
|
|
1256
|
+
primitives.useDismissOnOutsidePointer(detailsAnchorRef, detailsOpen, setDetailsOpen, detailsPanelRef);
|
|
1257
|
+
}
|
|
1258
|
+
react.useEffect(() => {
|
|
1259
|
+
if (!detailsOpen) return;
|
|
1260
|
+
const onKeyDown = (event) => {
|
|
1261
|
+
if (event.key === "Escape") setDetailsOpen(false);
|
|
1262
|
+
};
|
|
1263
|
+
document.addEventListener("keydown", onKeyDown);
|
|
1264
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
1265
|
+
}, [detailsOpen]);
|
|
1031
1266
|
|
|
1032
1267
|
// 会话 token 总量(未缓存输入 + 缓存读/写 + 输出);为 0 时代表还没有消耗。
|
|
1033
1268
|
const usageKey = usage
|
|
@@ -1095,46 +1330,120 @@ function UsageCostLine({ useProjection, sessionId }) {
|
|
|
1095
1330
|
: 0;
|
|
1096
1331
|
const subCount = hasSubagent ? (subagentUsage.sessions ?? 0) : 0;
|
|
1097
1332
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1333
|
+
// 缓存命中率口径与 DSH 相同:缓存读取 / (缓存读取 + 未缓存输入)
|
|
1334
|
+
const cacheHitBase = cacheRead + uncachedInput;
|
|
1335
|
+
const cacheHitPercent = cacheHitBase > 0 ? Math.round((cacheRead / cacheHitBase) * 100) : 0;
|
|
1336
|
+
|
|
1337
|
+
// pill = 图标 + 文字标签 + 金额:DSH 那颗 pill 已经在显示「{tok} · 缓存命中 {%}」,
|
|
1338
|
+
// 所以这里不再重复那些数字,但**保留文字说明**(用户要求「最早之前的文字」)。
|
|
1339
|
+
// 图标与 DSH 那颗保持一致(都用 IconDatabaseOutlineRegular)——换别的图标会「不搭配」
|
|
1340
|
+
// (2026-09-28 反馈「图标换了 不搭配」,仪表版被否掉)。
|
|
1341
|
+
// 图标缺失(DSH 改 API)时不渲染,文字照常显示,不影响点击。
|
|
1342
|
+
const children = [
|
|
1343
|
+
typeof primitives.IconDatabaseOutlineRegular === "function"
|
|
1344
|
+
? jsx.jsx(primitives.IconDatabaseOutlineRegular, { key: "icon" })
|
|
1345
|
+
: null,
|
|
1346
|
+
jsx.jsx("span", { key: "label", className: "balance-usage-label", children: "本会话消耗" }),
|
|
1347
|
+
jsx.jsx("span", { key: "cost", className: "balance-usage-label", children: "≈ ¥" + cost.toFixed(4) }),
|
|
1348
|
+
];
|
|
1349
|
+
|
|
1350
|
+
// ── 点击展开的明细浮层(结构与 DSH 原生「会话统计」一致:标题 + 分隔线 + dl 两列网格)──
|
|
1351
|
+
const usageDetails = [
|
|
1352
|
+
["缓存命中", cacheHitPercent + "%"],
|
|
1353
|
+
["未缓存输入", formatTokens(uncachedInput) + " tok"],
|
|
1354
|
+
["缓存读取", formatTokens(cacheRead) + " tok"],
|
|
1355
|
+
];
|
|
1356
|
+
if (cacheWrite > 0) usageDetails.push(["缓存写入", formatTokens(cacheWrite) + " tok"]);
|
|
1357
|
+
usageDetails.push(["输出", formatTokens(output) + " tok"]);
|
|
1358
|
+
|
|
1359
|
+
const costDetails = [
|
|
1360
|
+
["新增 tokens", formatTokens(newTokens) + " tok"],
|
|
1361
|
+
["计费 tokens", formatTokens(billedTotal) + " tok"],
|
|
1362
|
+
["模型", model || "未知(按 flash 价估算)"],
|
|
1102
1363
|
];
|
|
1103
1364
|
if (splitUsable) {
|
|
1104
|
-
|
|
1105
|
-
"
|
|
1106
|
-
"
|
|
1365
|
+
costDetails.push(
|
|
1366
|
+
["高峰 tokens", formatTokens(peakTokens) + " tok"],
|
|
1367
|
+
["空闲 tokens", formatTokens(offpeakTokens) + " tok"],
|
|
1107
1368
|
);
|
|
1108
|
-
} else {
|
|
1109
|
-
detailLines.push("估算金额:¥" + cost.toFixed(4) + "(按当前时段价估算)");
|
|
1110
1369
|
}
|
|
1111
1370
|
if (hasSubagent) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
"子代理消耗:新增 " + formatTokens(subNewTokens) + " tokens、计费 " + formatTokens(subagentUsage.totalTokens) + " tokens(" + subCount + " 个子代理)",
|
|
1116
|
-
"子代理金额:¥" + subCost.toFixed(4) + "(高峰桶×2、空闲桶按目录价)",
|
|
1371
|
+
costDetails.push(
|
|
1372
|
+
["子代理", subCount + " 个"],
|
|
1373
|
+
["子代理 tokens", "+" + formatTokens(subNewTokens) + " / 计费 " + formatTokens(subagentUsage.totalTokens)],
|
|
1117
1374
|
);
|
|
1118
1375
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1376
|
+
|
|
1377
|
+
const detailRows = (prefix, rows) =>
|
|
1378
|
+
rows.flatMap(([label, value], index) => [
|
|
1379
|
+
jsx.jsx("dt", { key: prefix + index + "t", children: label }),
|
|
1380
|
+
jsx.jsx("dd", { key: prefix + index + "d", children: value }),
|
|
1381
|
+
]);
|
|
1382
|
+
|
|
1383
|
+
const panelChildren = [
|
|
1384
|
+
jsx.jsxs("div", {
|
|
1385
|
+
className: "balance-usage-panel__title",
|
|
1386
|
+
children: [
|
|
1387
|
+
jsx.jsxs("span", {
|
|
1388
|
+
className: "balance-usage-panel__label",
|
|
1389
|
+
children: [
|
|
1390
|
+
typeof primitives.IconDatabaseOutlineRegular === "function"
|
|
1391
|
+
? jsx.jsx(primitives.IconDatabaseOutlineRegular, {})
|
|
1392
|
+
: null,
|
|
1393
|
+
"Token 用量",
|
|
1394
|
+
],
|
|
1395
|
+
}),
|
|
1396
|
+
jsx.jsx("span", { className: "balance-usage-panel__value", children: formatTokens(billedTotal) + " tok" }),
|
|
1397
|
+
],
|
|
1398
|
+
}),
|
|
1399
|
+
jsx.jsx("div", { className: "balance-usage-panel__rule" }),
|
|
1400
|
+
jsx.jsx("dl", { className: "balance-usage-panel__details", children: detailRows("u", usageDetails) }),
|
|
1401
|
+
jsx.jsxs("div", {
|
|
1402
|
+
className: "balance-usage-panel__title balance-usage-panel__title--second",
|
|
1403
|
+
children: [
|
|
1404
|
+
jsx.jsxs("span", {
|
|
1405
|
+
className: "balance-usage-panel__label",
|
|
1406
|
+
children: [
|
|
1407
|
+
typeof primitives.IconGaugeOutlineRegular === "function"
|
|
1408
|
+
? jsx.jsx(primitives.IconGaugeOutlineRegular, {})
|
|
1409
|
+
: null,
|
|
1410
|
+
"本会话金额",
|
|
1411
|
+
],
|
|
1412
|
+
}),
|
|
1413
|
+
jsx.jsx("span", { className: "balance-usage-panel__value", children: "≈ ¥" + cost.toFixed(4) }),
|
|
1414
|
+
],
|
|
1137
1415
|
}),
|
|
1416
|
+
jsx.jsx("div", { className: "balance-usage-panel__rule" }),
|
|
1417
|
+
jsx.jsx("dl", { className: "balance-usage-panel__details", children: detailRows("c", costDetails) }),
|
|
1418
|
+
];
|
|
1419
|
+
|
|
1420
|
+
return jsx.jsxs("div", {
|
|
1421
|
+
className: "balance-usage-wrap",
|
|
1422
|
+
children: [
|
|
1423
|
+
jsx.jsx("button", {
|
|
1424
|
+
type: "button",
|
|
1425
|
+
ref: detailsAnchorRef,
|
|
1426
|
+
className: "balance-usage-line",
|
|
1427
|
+
"aria-haspopup": "dialog",
|
|
1428
|
+
"aria-expanded": detailsOpen,
|
|
1429
|
+
"aria-label": "本会话 Token 用量与金额明细",
|
|
1430
|
+
onClick: () => setDetailsOpen((value) => !value),
|
|
1431
|
+
children,
|
|
1432
|
+
}),
|
|
1433
|
+
detailsOpen
|
|
1434
|
+
? reactDom.createPortal(
|
|
1435
|
+
jsx.jsx("div", {
|
|
1436
|
+
ref: detailsPanelRef,
|
|
1437
|
+
className: "balance-usage-panel",
|
|
1438
|
+
role: "dialog",
|
|
1439
|
+
"aria-label": "本会话 Token 用量",
|
|
1440
|
+
style: anchoredPosition ?? void 0,
|
|
1441
|
+
children: panelChildren,
|
|
1442
|
+
}),
|
|
1443
|
+
document.body,
|
|
1444
|
+
)
|
|
1445
|
+
: null,
|
|
1446
|
+
],
|
|
1138
1447
|
});
|
|
1139
1448
|
}
|
|
1140
1449
|
|
|
@@ -1142,6 +1451,8 @@ function UsageCostLine({ useProjection, sessionId }) {
|
|
|
1142
1451
|
const inject = ["slots", "sessions"];
|
|
1143
1452
|
|
|
1144
1453
|
function apply(ctx) {
|
|
1454
|
+
// 保存 ctx:用于探测官方版 DSH 的侧栏内嵌浏览器(ctx.sidebarRight / ctx.sidebarRightTabs)
|
|
1455
|
+
pluginCtx = ctx;
|
|
1145
1456
|
// 拿到 sessions 服务,用于解析当前会话使用的模型(按模型取价)
|
|
1146
1457
|
sessionsFace = ctx.sessions;
|
|
1147
1458
|
// 余额显示:展开态底部胶囊(order 20 排在市场 10 之后、紧挨设置);收起态悬浮 FAB
|
package/lib/index.js
CHANGED
|
@@ -314,6 +314,78 @@ function formatBalance(balance) {
|
|
|
314
314
|
return `余额(${balance.currency || "CNY"}):${balance.total_balance}\n 充值余额:${balance.topped_up_balance}\n 赠送余额:${balance.granted_balance}`;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
// ── 官方 DeepSeek Harness 账号服务(ctx.deepseekAccount)──────────────────────
|
|
318
|
+
// 官方桌面版内置账号登录(系统浏览器 PKCE),登录后可直接读到官方余额,无需 API Key。
|
|
319
|
+
// 服务由 @deepseek-ai/dsh-deepseek-account-platform 提供,已被 dsh-base 默认依赖,故官方版开箱即有。
|
|
320
|
+
// 接口:getBalance(client) → { status:"ready", value: 充值钱包[], bonusWallets: 赠送钱包[] }
|
|
321
|
+
// | null (未登录 / 凭据已变更)
|
|
322
|
+
// | { status:"failed" } (网络失败;官方明确“失败绝不变成 0 余额”)
|
|
323
|
+
// client 是 AccountClientMetadata(version / locale / timezoneOffsetSeconds),
|
|
324
|
+
// 缺字段会在平台侧组装请求头时抛错,所以三个字段都要给。
|
|
325
|
+
const PLUGIN_VERSION = "1.2.3";
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 读取官方账号余额并归一化成与 normalizeBalance 相同的形状。
|
|
329
|
+
* 服务不存在、未登录、接口报错一律返回 null,由调用方回退 API Key,绝不因此抛错。
|
|
330
|
+
*/
|
|
331
|
+
async function fetchAccountBalance(ctx) {
|
|
332
|
+
const account = typeof ctx.get === "function" ? ctx.get("deepseekAccount") : void 0;
|
|
333
|
+
if (account === void 0 || account === null || typeof account.getBalance !== "function") return null;
|
|
334
|
+
let result;
|
|
335
|
+
try {
|
|
336
|
+
result = await account.getBalance({
|
|
337
|
+
version: PLUGIN_VERSION,
|
|
338
|
+
locale: "zh-CN",
|
|
339
|
+
// 平台要求“东为正”的整秒偏移;JS 的 getTimezoneOffset() 是西为正,故取反。
|
|
340
|
+
timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
|
|
341
|
+
});
|
|
342
|
+
} catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
if (result === null || result === void 0 || result.status !== "ready") return null;
|
|
346
|
+
return normalizeAccountBalance(result);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** 官方钱包结构 → 统一余额形状。value 是充值钱包,bonusWallets 是赠送钱包(官方不计入充值余额)。 */
|
|
350
|
+
function normalizeAccountBalance(result) {
|
|
351
|
+
const pick = (list) => {
|
|
352
|
+
const arr = Array.isArray(list) ? list.filter((w) => w && typeof w.balance === "string") : [];
|
|
353
|
+
return arr.find((w) => w.currency === "CNY") ?? arr[0] ?? null;
|
|
354
|
+
};
|
|
355
|
+
const topped = pick(result.value);
|
|
356
|
+
const granted = pick(result.bonusWallets);
|
|
357
|
+
// 官方金额字符串遵循 big.js 文法(允许 0E-16 这类写法),Number() 能正确解析。
|
|
358
|
+
const toppedRaw = Number(topped?.balance ?? 0);
|
|
359
|
+
const grantedRaw = Number(granted?.balance ?? 0);
|
|
360
|
+
const toppedUp = Number.isFinite(toppedRaw) ? toppedRaw : 0;
|
|
361
|
+
const grantedAmount = Number.isFinite(grantedRaw) ? grantedRaw : 0;
|
|
362
|
+
const total = toppedUp + grantedAmount;
|
|
363
|
+
return {
|
|
364
|
+
is_available: total > 0,
|
|
365
|
+
currency: topped?.currency || granted?.currency || "CNY",
|
|
366
|
+
total_balance: total.toFixed(2),
|
|
367
|
+
granted_balance: grantedAmount.toFixed(2),
|
|
368
|
+
topped_up_balance: toppedUp.toFixed(2),
|
|
369
|
+
source: "account",
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** 取余额:官方账号优先,回退 DeepSeek API Key。返回 { balance, error },二者必有其一。 */
|
|
374
|
+
async function resolveBalance(ctx, config, signal) {
|
|
375
|
+
const fromAccount = await fetchAccountBalance(ctx);
|
|
376
|
+
if (fromAccount !== null) return { balance: fromAccount, error: null };
|
|
377
|
+
const apiKey = await resolveApiKey(ctx, config);
|
|
378
|
+
if (apiKey === "") {
|
|
379
|
+
return { balance: null, error: "未登录 DeepSeek 账号,也未设置 DEEPSEEK_API_KEY" };
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const data = await fetchBalance(apiKey, signal);
|
|
383
|
+
return { balance: { ...normalizeBalance(data), source: "apikey" }, error: null };
|
|
384
|
+
} catch (error) {
|
|
385
|
+
return { balance: null, error: error instanceof Error ? error.message : String(error) };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
317
389
|
/** 构建模型切换点列表(按时间升序)。request/header、request/context 都在模型或请求设置变化时写入,
|
|
318
390
|
* 故可用它们把会话切成若干"模型段",每条 assistant/message 归属到"它之前最近一个切换点"的模型。
|
|
319
391
|
* 这样换不同价模型时(如 flash→pro)金额能按各段模型分别计价,而不是整会话用最后一个模型。 */
|
|
@@ -811,32 +883,21 @@ export function apply(ctx, config = {}) {
|
|
|
811
883
|
name: "balance",
|
|
812
884
|
description: "查询 DeepSeek 余额与当前会话消耗",
|
|
813
885
|
handler: async ({ agent, signal }) => {
|
|
814
|
-
const apiKey = await resolveApiKey(ctx, config);
|
|
815
|
-
if (apiKey === "") {
|
|
816
|
-
return {
|
|
817
|
-
kind: "error",
|
|
818
|
-
text: "未找到 DeepSeek API Key,请设置环境变量 DEEPSEEK_API_KEY 后重试。",
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
|
|
822
886
|
const usage = sumSessionUsage(agent.session, config);
|
|
823
887
|
const model = detectModel(agent.session);
|
|
824
888
|
const pricing = resolvePricing(config, model);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const balanceText = formatBalance(normalizeBalance(data));
|
|
829
|
-
return {
|
|
830
|
-
kind: "success",
|
|
831
|
-
text: `${balanceText}\n\n${formatUsage(usage, pricing, model)}`,
|
|
832
|
-
};
|
|
833
|
-
} catch (error) {
|
|
834
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
889
|
+
// 余额来源:官方账号优先(官方版内置登录,无需 API Key),回退 API Key。
|
|
890
|
+
const { balance, error } = await resolveBalance(ctx, config, signal);
|
|
891
|
+
if (balance === null) {
|
|
835
892
|
return {
|
|
836
893
|
kind: "error",
|
|
837
|
-
text: `${
|
|
894
|
+
text: `${error}\n\n${formatUsage(usage, pricing, model)}`,
|
|
838
895
|
};
|
|
839
896
|
}
|
|
897
|
+
return {
|
|
898
|
+
kind: "success",
|
|
899
|
+
text: `${formatBalance(balance)}\n\n${formatUsage(usage, pricing, model)}`,
|
|
900
|
+
};
|
|
840
901
|
},
|
|
841
902
|
});
|
|
842
903
|
});
|
|
@@ -885,22 +946,6 @@ export function apply(ctx, config = {}) {
|
|
|
885
946
|
statsError = error instanceof Error ? error.message : String(error);
|
|
886
947
|
}
|
|
887
948
|
}
|
|
888
|
-
const apiKey = await resolveApiKey(ctx, config);
|
|
889
|
-
if (apiKey === "") {
|
|
890
|
-
if (wantStats) {
|
|
891
|
-
writeJson(res, 200, {
|
|
892
|
-
ok: true,
|
|
893
|
-
balance: null,
|
|
894
|
-
balanceError: "未设置 DEEPSEEK_API_KEY",
|
|
895
|
-
pricing: resolvePricing(config, model),
|
|
896
|
-
stats,
|
|
897
|
-
statsError,
|
|
898
|
-
});
|
|
899
|
-
} else {
|
|
900
|
-
writeJson(res, 200, { ok: false, error: "未设置 DEEPSEEK_API_KEY" });
|
|
901
|
-
}
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
949
|
// 按 session id 取会话并分时段汇总 usage(拿不到则返回 null,客户端回退用本地投影)
|
|
905
950
|
let usage = null;
|
|
906
951
|
// 子代理后代会话的 usage 汇总(与主会话分开)
|
|
@@ -953,33 +998,23 @@ export function apply(ctx, config = {}) {
|
|
|
953
998
|
if (subagentUsage && Array.isArray(subagentUsage.byModel)) {
|
|
954
999
|
subagentUsage.byModel = subagentUsage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model, b.era)) }));
|
|
955
1000
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
pricing: resolvePricing(config, effectiveModel),
|
|
962
|
-
model: effectiveModel,
|
|
963
|
-
usage,
|
|
964
|
-
subagentUsage,
|
|
965
|
-
stats,
|
|
966
|
-
statsError,
|
|
967
|
-
});
|
|
968
|
-
} catch (error) {
|
|
969
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
970
|
-
if (wantStats) {
|
|
971
|
-
writeJson(res, 200, {
|
|
972
|
-
ok: true,
|
|
973
|
-
balance: null,
|
|
974
|
-
balanceError: message,
|
|
975
|
-
pricing: resolvePricing(config, effectiveModel),
|
|
976
|
-
stats,
|
|
977
|
-
statsError,
|
|
978
|
-
});
|
|
979
|
-
} else {
|
|
980
|
-
writeJson(res, 200, { ok: false, error: message });
|
|
981
|
-
}
|
|
1001
|
+
// 余额来源:官方账号优先(官方桌面版内置登录,无需 API Key),回退 DeepSeek API Key。
|
|
1002
|
+
const { balance, error: balanceError } = await resolveBalance(ctx, config);
|
|
1003
|
+
if (balance === null && !wantStats) {
|
|
1004
|
+
writeJson(res, 200, { ok: false, error: balanceError });
|
|
1005
|
+
return;
|
|
982
1006
|
}
|
|
1007
|
+
writeJson(res, 200, {
|
|
1008
|
+
ok: true,
|
|
1009
|
+
balance,
|
|
1010
|
+
balanceError: balance === null ? balanceError : null,
|
|
1011
|
+
pricing: resolvePricing(config, effectiveModel),
|
|
1012
|
+
model: effectiveModel,
|
|
1013
|
+
usage,
|
|
1014
|
+
subagentUsage,
|
|
1015
|
+
stats,
|
|
1016
|
+
statsError,
|
|
1017
|
+
});
|
|
983
1018
|
},
|
|
984
1019
|
});
|
|
985
1020
|
if (typeof balanceRouteDisposer === "function") disposers.push(balanceRouteDisposer);
|