balance-dsh 1.0.0 → 1.0.2
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/lib/client.js +23 -27
- package/lib/index.js +86 -27
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -17,10 +17,10 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
/* 完全复刻设置按钮(.VOzbGW_trigger):width calc(100%+4px)、height 42px、margin 4px -2px、
|
|
18
18
|
padding 0 10px 0 8px、gap 8px、font 14px/22px、color label-primary;
|
|
19
19
|
左缘与设置按钮严格对齐,高度/颜色一致(图标与"余额"文字也统一 label-primary,金额 600 加粗) */
|
|
20
|
-
.balance-footer{box-sizing:border-box;display:flex;align-items:center;justify-content:flex-start;gap:8px;width:
|
|
20
|
+
.balance-footer{box-sizing:border-box;display:flex;align-items:center;justify-content:flex-start;gap:8px;width:100%;min-width:0;height:42px;margin:0;padding:0 0 0 8px;color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px;text-decoration:none;cursor:pointer;white-space:nowrap;overflow:visible;-webkit-user-select:none;user-select:none}
|
|
21
21
|
.balance-footer__icon{display:inline-flex;flex:none;color:var(--dsw-alias-label-primary)}
|
|
22
22
|
.balance-footer__label{white-space:nowrap;color:var(--dsw-alias-label-primary)}
|
|
23
|
-
.balance-footer__value{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap;padding:2px 8px;border-radius:999px;margin-left:auto;text-decoration:none;cursor:pointer;transition:transform .15s var(--ds-ease-in-out,.2s)}
|
|
23
|
+
.balance-footer__value{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap;padding:2px 8px;border-radius:999px;margin-left:auto;margin-right:6px;text-decoration:none;cursor:pointer;transition:transform .15s var(--ds-ease-in-out,.2s)}
|
|
24
24
|
.balance-footer__value:hover{filter:brightness(.85);transform:scale(1.06)}
|
|
25
25
|
.balance-footer__value--healthy{color:var(--dsw-alias-state-success-primary);background:rgba(34,197,94,.15)}
|
|
26
26
|
.balance-footer__value--low{color:#d97706;background:rgba(217,119,6,.15)}
|
|
@@ -41,6 +41,7 @@ window.__ModuleLoader__.load({
|
|
|
41
41
|
.balance-fab-dark:hover{background:#cf3a3f}
|
|
42
42
|
.balance-usage-line{box-sizing:border-box;display:flex;justify-content:center;align-items:center;gap:10px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
43
43
|
.balance-usage-line strong{color:var(--dsw-alias-label-primary);font-weight:600}
|
|
44
|
+
/* ── 跨会话统计面板(单击余额弹出) ── */
|
|
44
45
|
/* ── 展开态底部一行:[设置齿轮][余额胶囊],余额在设置右边 ──
|
|
45
46
|
全部使用稳定锚点,不依赖 CSS Modules hash 类名(DSH 升级后仍有效):
|
|
46
47
|
- wide 态特征:底部存在 .balance-footer / .balance-topup(收起态渲染 position:fixed 的 .balance-fab,不影响)
|
|
@@ -104,8 +105,19 @@ window.__ModuleLoader__.load({
|
|
|
104
105
|
return input + output;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
/**
|
|
108
|
+
/** 分时段金额:优先按服务端已标注的各模型 cost 累加(byModel);
|
|
109
|
+
* 无 byModel 时回退到旧的单模型逻辑:高峰桶按目录价 ×2、空闲桶按目录价(兼容子代理/旧数据)。 */
|
|
108
110
|
function computeSplitCost(usage, p) {
|
|
111
|
+
if (Array.isArray(usage.byModel) && usage.byModel.length > 0) {
|
|
112
|
+
let total = 0;
|
|
113
|
+
for (const b of usage.byModel) {
|
|
114
|
+
total += typeof b.cost === "number" && Number.isFinite(b.cost) ? b.cost : computeSingleSplitCost(b, p);
|
|
115
|
+
}
|
|
116
|
+
return total;
|
|
117
|
+
}
|
|
118
|
+
return computeSingleSplitCost(usage, p);
|
|
119
|
+
}
|
|
120
|
+
function computeSingleSplitCost(usage, p) {
|
|
109
121
|
const costOf = (u) =>
|
|
110
122
|
((u.inputTokens ?? 0) * p.inputCnyPerMillion +
|
|
111
123
|
(u.cacheReadTokens ?? 0) * p.cacheReadCnyPerMillion +
|
|
@@ -114,7 +126,6 @@ window.__ModuleLoader__.load({
|
|
|
114
126
|
((u.outputTokens ?? 0) * p.outputCnyPerMillion) / 1_000_000;
|
|
115
127
|
return costOf(usage.peak) * 2 + costOf(usage.offpeak);
|
|
116
128
|
}
|
|
117
|
-
|
|
118
129
|
function formatTokens(n) {
|
|
119
130
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
120
131
|
if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
|
|
@@ -302,6 +313,7 @@ window.__ModuleLoader__.load({
|
|
|
302
313
|
// ─── 余额显示:展开态侧栏底部文字(¥ 图标+金额);收起态悬浮 FAB ──
|
|
303
314
|
function BalanceFooter({ wide }) {
|
|
304
315
|
const state = useBalance();
|
|
316
|
+
const openUsagePage = (e) => { e && e.preventDefault(); window.open("https://platform.deepseek.com/usage", "_blank", "noopener"); };
|
|
305
317
|
|
|
306
318
|
if (!wide) {
|
|
307
319
|
// 收起态:悬浮 FAB(原样保留)
|
|
@@ -427,13 +439,15 @@ window.__ModuleLoader__.load({
|
|
|
427
439
|
],
|
|
428
440
|
}),
|
|
429
441
|
}),
|
|
430
|
-
jsx.jsx("span", {
|
|
431
|
-
|
|
442
|
+
jsx.jsx("span", {
|
|
443
|
+
className: "balance-footer__label",
|
|
444
|
+
children: "余额",
|
|
445
|
+
}),
|
|
446
|
+
jsx.jsx("span", {
|
|
432
447
|
className: "balance-footer__value" + valueClass,
|
|
433
|
-
|
|
434
|
-
target: "_blank",
|
|
435
|
-
rel: "noreferrer",
|
|
448
|
+
onClick: openUsagePage,
|
|
436
449
|
onContextMenu: onBalanceContextMenu,
|
|
450
|
+
title: "打开 DeepSeek 用量页",
|
|
437
451
|
children: valueChildren,
|
|
438
452
|
}),
|
|
439
453
|
];
|
|
@@ -445,11 +459,6 @@ window.__ModuleLoader__.load({
|
|
|
445
459
|
}
|
|
446
460
|
|
|
447
461
|
// ─── 当前会话消耗:输入区 dock ──────────────────────────────────────────
|
|
448
|
-
// ─── 关闭按钮:侧边栏底部,设置上方 ───────────────────────────────────
|
|
449
|
-
function ShutdownButton({ wide }) {
|
|
450
|
-
return null;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
462
|
function UsageCostLine({ useProjection, sessionId }) {
|
|
454
463
|
const p = usePricing();
|
|
455
464
|
const model = useCurrentModel();
|
|
@@ -582,19 +591,6 @@ function UsageCostLine({ useProjection, sessionId }) {
|
|
|
582
591
|
),
|
|
583
592
|
);
|
|
584
593
|
|
|
585
|
-
|
|
586
|
-
// 关闭按钮:显示在设置上方、余额右侧
|
|
587
|
-
ctx.slots.inject("sidebar.footer.action", () =>
|
|
588
|
-
ctx.slots.register(
|
|
589
|
-
{
|
|
590
|
-
name: "sidebar.footer.action",
|
|
591
|
-
id: "shutdown",
|
|
592
|
-
order: 10,
|
|
593
|
-
},
|
|
594
|
-
ShutdownButton,
|
|
595
|
-
),
|
|
596
|
-
);
|
|
597
|
-
|
|
598
594
|
// 对话统计区(对话框下方)的会话消耗行
|
|
599
595
|
ctx.slots.inject("conversation.composer.dock", () =>
|
|
600
596
|
ctx.slots.register(
|
package/lib/index.js
CHANGED
|
@@ -176,17 +176,47 @@ function formatBalance(balance) {
|
|
|
176
176
|
return `余额(${balance.currency || "CNY"}):${balance.total_balance}\n 充值余额:${balance.topped_up_balance}\n 赠送余额:${balance.granted_balance}`;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/**
|
|
180
|
-
|
|
179
|
+
/** 构建模型切换点列表(按时间升序)。request/header、request/context 都在模型或请求设置变化时写入,
|
|
180
|
+
* 故可用它们把会话切成若干"模型段",每条 assistant/message 归属到"它之前最近一个切换点"的模型。
|
|
181
|
+
* 这样换不同价模型时(如 flash→pro)金额能按各段模型分别计价,而不是整会话用最后一个模型。 */
|
|
182
|
+
function collectModelSwitches(events) {
|
|
183
|
+
const switches = [];
|
|
184
|
+
for (const event of events ?? []) {
|
|
185
|
+
if (event.type === "request/header") {
|
|
186
|
+
const m = event.data?.header?.config?.model;
|
|
187
|
+
if (typeof m === "string" && m !== "") switches.push({ time: event.time ?? 0, model: m });
|
|
188
|
+
} else if (event.type === "request/context") {
|
|
189
|
+
const m = event.data?.model;
|
|
190
|
+
if (typeof m === "string" && m !== "") switches.push({ time: event.time ?? 0, model: m });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
switches.sort((a, b) => a.time - b.time);
|
|
194
|
+
return switches;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 找到 time 之前最近一次模型切换点的模型;无切换点(或早于第一个)返回 ""(调用方回退到后备模型)。 */
|
|
198
|
+
function modelAtTime(switches, time) {
|
|
199
|
+
let model = "";
|
|
200
|
+
for (const sp of switches) {
|
|
201
|
+
if (sp.time <= time) model = sp.model;
|
|
202
|
+
else break;
|
|
203
|
+
}
|
|
204
|
+
return model;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 从事件数组汇总 usage(按模型分段 + 跨时段分桶)。
|
|
208
|
+
* 供 live 会话与持久化(子代理)会话复用。返回按模型拆分的 byModel,及跨模型聚合字段。 */
|
|
209
|
+
function sumEventsUsage(events, config, fallbackModel = "") {
|
|
210
|
+
const switches = collectModelSwitches(events);
|
|
211
|
+
const fallback = fallbackModel !== "" ? fallbackModel : "deepseek-v4-flash";
|
|
181
212
|
const latestByStep = new Map();
|
|
182
|
-
for (const event of events) {
|
|
213
|
+
for (const event of events ?? []) {
|
|
183
214
|
if (event.type !== "assistant/message") continue;
|
|
184
215
|
const usage = event.data?.usage;
|
|
185
216
|
if (usage === void 0 || usage === null) continue;
|
|
186
217
|
const turn = event.data?.turn;
|
|
187
218
|
const step = event.data?.step;
|
|
188
219
|
if (turn === void 0 || step === void 0) continue;
|
|
189
|
-
// 事件带毫秒时间戳(event.time),用于按高峰/空闲时段分桶
|
|
190
220
|
latestByStep.set(`${turn}/${step}`, { usage, time: event.time ?? Date.now() });
|
|
191
221
|
}
|
|
192
222
|
const add = (bucket, usage) => {
|
|
@@ -196,10 +226,27 @@ function sumEventsUsage(events, config) {
|
|
|
196
226
|
bucket.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
|
|
197
227
|
};
|
|
198
228
|
const empty = () => ({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
|
|
229
|
+
const byModel = new Map(); // model -> { model, peak, offpeak }
|
|
230
|
+
for (const { usage, time } of latestByStep.values()) {
|
|
231
|
+
const model = modelAtTime(switches, time) || fallback;
|
|
232
|
+
let e = byModel.get(model);
|
|
233
|
+
if (!e) { e = { model, peak: empty(), offpeak: empty() }; byModel.set(model, e); }
|
|
234
|
+
add(isPeakAt(time, config) ? e.peak : e.offpeak, usage);
|
|
235
|
+
}
|
|
236
|
+
// 聚合 + 转换 byModel(按金额未在前面算,这里只分桶 + 汇总字段)
|
|
199
237
|
const peak = empty();
|
|
200
238
|
const offpeak = empty();
|
|
201
|
-
|
|
202
|
-
|
|
239
|
+
const modelList = [];
|
|
240
|
+
for (const e of byModel.values()) {
|
|
241
|
+
modelList.push({ model: e.model, peak: e.peak, offpeak: e.offpeak });
|
|
242
|
+
peak.inputTokens += e.peak.inputTokens;
|
|
243
|
+
peak.outputTokens += e.peak.outputTokens;
|
|
244
|
+
peak.cacheReadTokens += e.peak.cacheReadTokens;
|
|
245
|
+
peak.cacheWriteTokens += e.peak.cacheWriteTokens;
|
|
246
|
+
offpeak.inputTokens += e.offpeak.inputTokens;
|
|
247
|
+
offpeak.outputTokens += e.offpeak.outputTokens;
|
|
248
|
+
offpeak.cacheReadTokens += e.offpeak.cacheReadTokens;
|
|
249
|
+
offpeak.cacheWriteTokens += e.offpeak.cacheWriteTokens;
|
|
203
250
|
}
|
|
204
251
|
const inputTokens = peak.inputTokens + offpeak.inputTokens;
|
|
205
252
|
const outputTokens = peak.outputTokens + offpeak.outputTokens;
|
|
@@ -215,12 +262,14 @@ function sumEventsUsage(events, config) {
|
|
|
215
262
|
totalTokens: billedInputTokens + outputTokens,
|
|
216
263
|
peak,
|
|
217
264
|
offpeak,
|
|
265
|
+
byModel: modelList,
|
|
218
266
|
};
|
|
219
267
|
}
|
|
220
268
|
|
|
221
269
|
function sumSessionUsage(session, config) {
|
|
222
270
|
if (session == null) return null;
|
|
223
|
-
|
|
271
|
+
// fallbackModel = detectModel(session):当 message 早于所有模型切换点时兜底,避免落到默认 flash 价。
|
|
272
|
+
return sumEventsUsage(session.events ?? [], config, detectModel(session));
|
|
224
273
|
}
|
|
225
274
|
|
|
226
275
|
function estimateCost(usage, pricing) {
|
|
@@ -244,8 +293,9 @@ function estimateCost(usage, pricing) {
|
|
|
244
293
|
* 再对每个子代理用 inspectApiRemoteSession 读取持久化事件计算 usage。无子代理返回 {usage:null,debug}。 */
|
|
245
294
|
async function sumSubagentUsage(ctx, sessionId, config) {
|
|
246
295
|
if (ctx == null || sessionId === "") return { usage: null };
|
|
247
|
-
//
|
|
248
|
-
|
|
296
|
+
// 用 ctx.get("subagents") 安全获取(非注入,可空);不要直接读 ctx.subagents 属性,
|
|
297
|
+
// 否则未声明注入时会抛 "cannot get property subagents without inject"。
|
|
298
|
+
const subagentsSvc = typeof ctx.get === "function" ? ctx.get("subagents") : void 0;
|
|
249
299
|
const listDescendants = subagentsSvc != null && typeof subagentsSvc.listDescendants === "function" ? subagentsSvc.listDescendants.bind(subagentsSvc) : void 0;
|
|
250
300
|
// 1) 用 subagents.listDescendants 一次取完整后代树(含编号/层级/已结束)
|
|
251
301
|
let descendants = [];
|
|
@@ -268,6 +318,7 @@ async function sumSubagentUsage(ctx, sessionId, config) {
|
|
|
268
318
|
const empty = () => ({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
|
|
269
319
|
const peak = empty();
|
|
270
320
|
const offpeak = empty();
|
|
321
|
+
const byModelMap = new Map(); // model -> { model, peak, offpeak }
|
|
271
322
|
let count = 0;
|
|
272
323
|
for (const id of childIds) {
|
|
273
324
|
try {
|
|
@@ -284,6 +335,19 @@ async function sumSubagentUsage(ctx, sessionId, config) {
|
|
|
284
335
|
offpeak.outputTokens += u.offpeak.outputTokens;
|
|
285
336
|
offpeak.cacheReadTokens += u.offpeak.cacheReadTokens;
|
|
286
337
|
offpeak.cacheWriteTokens += u.offpeak.cacheWriteTokens;
|
|
338
|
+
// 子代理也可能换模型,按模型同样分桶聚合
|
|
339
|
+
for (const b of u.byModel ?? []) {
|
|
340
|
+
let be = byModelMap.get(b.model);
|
|
341
|
+
if (!be) { be = { model: b.model, peak: empty(), offpeak: empty() }; byModelMap.set(b.model, be); }
|
|
342
|
+
be.peak.inputTokens += b.peak.inputTokens;
|
|
343
|
+
be.peak.outputTokens += b.peak.outputTokens;
|
|
344
|
+
be.peak.cacheReadTokens += b.peak.cacheReadTokens;
|
|
345
|
+
be.peak.cacheWriteTokens += b.peak.cacheWriteTokens;
|
|
346
|
+
be.offpeak.inputTokens += b.offpeak.inputTokens;
|
|
347
|
+
be.offpeak.outputTokens += b.offpeak.outputTokens;
|
|
348
|
+
be.offpeak.cacheReadTokens += b.offpeak.cacheReadTokens;
|
|
349
|
+
be.offpeak.cacheWriteTokens += b.offpeak.cacheWriteTokens;
|
|
350
|
+
}
|
|
287
351
|
} catch {
|
|
288
352
|
// 该子代理读取失败则跳过
|
|
289
353
|
}
|
|
@@ -303,6 +367,7 @@ async function sumSubagentUsage(ctx, sessionId, config) {
|
|
|
303
367
|
totalTokens: billedInputTokens + outputTokens,
|
|
304
368
|
peak,
|
|
305
369
|
offpeak,
|
|
370
|
+
byModel: [...byModelMap.values()],
|
|
306
371
|
sessions: count,
|
|
307
372
|
};
|
|
308
373
|
return { usage };
|
|
@@ -375,7 +440,11 @@ export function apply(ctx, config = {}) {
|
|
|
375
440
|
});
|
|
376
441
|
|
|
377
442
|
// 2) /balance-api HTTP 接口:供客户端插件直接渲染余额(不经过命令,不污染会话日志)
|
|
378
|
-
|
|
443
|
+
// 注意:不要在这里声 inject ["subagents"] —— 它依赖 dsh-subagent 服务;
|
|
444
|
+
// 若 balance-dsh 加载时该服务未就绪,Cordis 会跳过整个 callback,导致
|
|
445
|
+
// /balance-api 不注册(404)。改为只声 inject ["webServer"],子代理在
|
|
446
|
+
// sumSubagentUsage 内部用可空 ctx.get("subagents") 获取。
|
|
447
|
+
ctx.inject(["webServer"], (httpCtx) => {
|
|
379
448
|
httpCtx.webServer.register({
|
|
380
449
|
kind: "exact",
|
|
381
450
|
path: "/balance-api",
|
|
@@ -425,6 +494,13 @@ export function apply(ctx, config = {}) {
|
|
|
425
494
|
}
|
|
426
495
|
// 客户端优先用自己解析的 model;为空则回退到服务端 detectModel
|
|
427
496
|
const effectiveModel = model || detectedModel;
|
|
497
|
+
// 逐模型标注 cost(高峰期×2、空闲期×1),客户端直接按各模型 cost 累加,无需自己定价。
|
|
498
|
+
if (usage && Array.isArray(usage.byModel)) {
|
|
499
|
+
usage.byModel = usage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model)) }));
|
|
500
|
+
}
|
|
501
|
+
if (subagentUsage && Array.isArray(subagentUsage.byModel)) {
|
|
502
|
+
subagentUsage.byModel = subagentUsage.byModel.map((b) => ({ ...b, cost: estimateCost(b, resolvePricing(config, b.model)) }));
|
|
503
|
+
}
|
|
428
504
|
try {
|
|
429
505
|
const data = await fetchBalance(apiKey);
|
|
430
506
|
writeJson(res, 200, {
|
|
@@ -442,22 +518,5 @@ export function apply(ctx, config = {}) {
|
|
|
442
518
|
},
|
|
443
519
|
});
|
|
444
520
|
|
|
445
|
-
|
|
446
|
-
httpCtx.webServer.register({
|
|
447
|
-
kind: "exact",
|
|
448
|
-
path: "/shutdown-api",
|
|
449
|
-
handler: async (req, res) => {
|
|
450
|
-
if (req.method !== "POST") {
|
|
451
|
-
writeJson(res, 405, { ok: false, error: "method not allowed" });
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
writeJson(res, 200, { ok: true });
|
|
455
|
-
const exit = ctx.get("appExit");
|
|
456
|
-
setTimeout(() => {
|
|
457
|
-
if (typeof exit === "function") exit(0);
|
|
458
|
-
else process.exit(0);
|
|
459
|
-
}, 150);
|
|
460
|
-
},
|
|
461
|
-
});
|
|
462
521
|
});
|
|
463
522
|
}
|