balance-dsh 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +99 -20
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -1,9 +1,55 @@
|
|
|
1
1
|
// dsh 插件:余额与当前会话消耗
|
|
2
2
|
// 服务端:注册 /balance 命令 + /balance-api HTTP 接口(供 Web UI 插件直接渲染)
|
|
3
|
-
import { inspectApiRemoteSession } from "@deepseek-ai/dsh-api-remotes";
|
|
4
|
-
|
|
5
3
|
export const name = "balance-dsh";
|
|
6
4
|
|
|
5
|
+
let legacySessionInspector;
|
|
6
|
+
let legacySessionInspectorLoaded = false;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 跨 DSH 版本读取会话,且不在模块加载阶段绑定可能被删除的命名导出。
|
|
10
|
+
* 新版优先使用 sessionController;其次直接使用 sessionQuery;旧版最后回退
|
|
11
|
+
* @deepseek-ai/dsh-api-remotes.inspectApiRemoteSession。接口全部不可用时抛错,
|
|
12
|
+
* 由子代理统计调用方跳过该会话,不影响余额插件本身启动。
|
|
13
|
+
*/
|
|
14
|
+
async function inspectPersistedSession(ctx, sessionId) {
|
|
15
|
+
const getService = typeof ctx?.get === "function" ? ctx.get.bind(ctx) : null;
|
|
16
|
+
|
|
17
|
+
const sessionController = getService?.("sessionController");
|
|
18
|
+
if (typeof sessionController?.inspect === "function") {
|
|
19
|
+
return sessionController.inspect(sessionId);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sessionQuery = getService?.("sessionQuery");
|
|
23
|
+
if (typeof sessionQuery?.observeSession === "function") {
|
|
24
|
+
const observation = await sessionQuery.observeSession(sessionId, { projectionMode: "none" });
|
|
25
|
+
try {
|
|
26
|
+
return {
|
|
27
|
+
meta: observation.header,
|
|
28
|
+
events: [...(observation.events ?? [])],
|
|
29
|
+
};
|
|
30
|
+
} finally {
|
|
31
|
+
observation?.[Symbol.dispose]?.();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!legacySessionInspectorLoaded) {
|
|
36
|
+
legacySessionInspectorLoaded = true;
|
|
37
|
+
try {
|
|
38
|
+
const remotes = await import("@deepseek-ai/dsh-api-remotes");
|
|
39
|
+
if (typeof remotes.inspectApiRemoteSession === "function") {
|
|
40
|
+
legacySessionInspector = remotes.inspectApiRemoteSession;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// 新版可能不再提供旧模块或旧导出;由下方统一处理。
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (typeof legacySessionInspector === "function") {
|
|
47
|
+
return legacySessionInspector(ctx, sessionId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new Error("当前 DSH 未提供兼容的持久化会话读取接口");
|
|
51
|
+
}
|
|
52
|
+
|
|
7
53
|
const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
|
|
8
54
|
|
|
9
55
|
// DeepSeek 目录价(人民币/百万 tokens),按模型区分。默认取 2026-08-17 生效的
|
|
@@ -355,16 +401,35 @@ function recentDayKeys(days, now = Date.now()) {
|
|
|
355
401
|
}
|
|
356
402
|
|
|
357
403
|
async function buildUsageStats(ctx, config) {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
404
|
+
const getService = typeof ctx.get === "function" ? ctx.get.bind(ctx) : null;
|
|
405
|
+
const controller = getService?.("sessionController");
|
|
406
|
+
const persistence = getService?.("sessionPersistence");
|
|
362
407
|
const now = Date.now();
|
|
363
408
|
const cutoffMs = now - (STATS_DAYS - 1) * 24 * 60 * 60 * 1000;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
409
|
+
// 列举会话要跨 DSH 版本:新版 sessionController.list() 返回 { items:[{ sessionId, updatedAt }] }
|
|
410
|
+
// (已按活动降序,字段叫 sessionId);旧版 sessionPersistence.list() 返回 [{ id, createdAt }]
|
|
411
|
+
// (字段叫 id)。旧版 persistence 还提供 inspect(),新版改成 handle-based 的 open(),
|
|
412
|
+
// 所以读取一律走已做三级兼容的 inspectPersistedSession,不再直接调 persistence.inspect。
|
|
413
|
+
let sessionIds = [];
|
|
414
|
+
let availableSessions = 0;
|
|
415
|
+
if (typeof controller?.list === "function") {
|
|
416
|
+
const listed = await controller.list({});
|
|
417
|
+
const items = Array.isArray(listed) ? listed : (listed?.items ?? []);
|
|
418
|
+
sessionIds = items.map((item) => (item && typeof item === "object" ? item.sessionId ?? item.id : void 0));
|
|
419
|
+
availableSessions = sessionIds.length;
|
|
420
|
+
} else if (typeof persistence?.list === "function") {
|
|
421
|
+
const headers = await persistence.list();
|
|
422
|
+
const list = Array.isArray(headers) ? headers : [];
|
|
423
|
+
availableSessions = list.length;
|
|
424
|
+
sessionIds = [...list]
|
|
425
|
+
.filter((header) => header && typeof header.id === "string")
|
|
426
|
+
.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))
|
|
427
|
+
.map((header) => header.id);
|
|
428
|
+
} else {
|
|
429
|
+
throw new Error("会话持久化服务不可用");
|
|
430
|
+
}
|
|
431
|
+
const selected = sessionIds
|
|
432
|
+
.filter((id) => typeof id === "string" && id !== "")
|
|
368
433
|
.slice(0, STATS_MAX_SESSIONS);
|
|
369
434
|
const daily = new Map();
|
|
370
435
|
let cursor = 0;
|
|
@@ -372,16 +437,16 @@ async function buildUsageStats(ctx, config) {
|
|
|
372
437
|
let failedSessions = 0;
|
|
373
438
|
const worker = async () => {
|
|
374
439
|
while (cursor < selected.length) {
|
|
375
|
-
const
|
|
440
|
+
const sessionId = selected[cursor++];
|
|
376
441
|
try {
|
|
377
|
-
const inspected = await
|
|
442
|
+
const inspected = await inspectPersistedSession(ctx, sessionId);
|
|
378
443
|
collectDailyUsage(
|
|
379
444
|
inspected?.events ?? [],
|
|
380
445
|
config,
|
|
381
446
|
detectModel({ requestHeader: () => void 0, requestContext: () => void 0, events: inspected?.events ?? [] }),
|
|
382
447
|
cutoffMs,
|
|
383
448
|
daily,
|
|
384
|
-
|
|
449
|
+
sessionId,
|
|
385
450
|
);
|
|
386
451
|
scannedSessions++;
|
|
387
452
|
} catch {
|
|
@@ -422,19 +487,19 @@ async function buildUsageStats(ctx, config) {
|
|
|
422
487
|
rangeDays: STATS_DAYS,
|
|
423
488
|
scannedSessions,
|
|
424
489
|
failedSessions,
|
|
425
|
-
availableSessions
|
|
426
|
-
limited:
|
|
490
|
+
availableSessions,
|
|
491
|
+
limited: availableSessions > selected.length,
|
|
427
492
|
generatedAt: now,
|
|
428
493
|
};
|
|
429
494
|
}
|
|
430
495
|
|
|
431
496
|
/** 汇总某会话的所有子代理后代会话的 usage(跨时段精确计费,与主会话分开)。
|
|
432
|
-
* @param ctx DSH 上下文(用于 subagents 服务 +
|
|
497
|
+
* @param ctx DSH 上下文(用于 subagents 服务 + 跨版本持久化会话读取)
|
|
433
498
|
* @param sessionId 父会话 id
|
|
434
499
|
* @param config 插件配置
|
|
435
500
|
* 说明:子代理会话结束后会被从 live 会话 store 移除,故不能用 store.list() 枚举;
|
|
436
501
|
* 这里用 subagents 服务的 listDescendants(含已结束/嵌套子代理)取后代 id,
|
|
437
|
-
*
|
|
502
|
+
* 再对每个子代理用兼容读取器取得持久化事件并计算 usage。无子代理返回 {usage:null,debug}。 */
|
|
438
503
|
async function sumSubagentUsage(ctx, sessionId, config) {
|
|
439
504
|
if (ctx == null || sessionId === "") return { usage: null };
|
|
440
505
|
// 用 ctx.get("subagents") 安全获取(非注入,可空);不要直接读 ctx.subagents 属性,
|
|
@@ -466,7 +531,7 @@ async function sumSubagentUsage(ctx, sessionId, config) {
|
|
|
466
531
|
let count = 0;
|
|
467
532
|
for (const id of childIds) {
|
|
468
533
|
try {
|
|
469
|
-
const inspected = await
|
|
534
|
+
const inspected = await inspectPersistedSession(ctx, id);
|
|
470
535
|
const events = Array.isArray(inspected?.events) ? inspected.events : [];
|
|
471
536
|
const u = sumEventsUsage(events, config);
|
|
472
537
|
if (u === null || u === void 0) continue;
|
|
@@ -548,6 +613,11 @@ function writeJson(res, status, payload) {
|
|
|
548
613
|
|
|
549
614
|
export function apply(ctx, config = {}) {
|
|
550
615
|
let statsCache = { expiresAt: 0, value: null };
|
|
616
|
+
// DSH 热重载(reload)时会重新 apply 本插件;若旧实例注册的 /balance-api 路由
|
|
617
|
+
// 未被清理,新实例再次 register 就会抛出 duplicate route,导致整个 apply 失败、
|
|
618
|
+
// 余额不显示。这里收集每次 register 返回的 disposer,并在插件卸载(ctx.on("dispose"))
|
|
619
|
+
// 时统一清理,保证重复 apply 不冲突。
|
|
620
|
+
const disposers = [];
|
|
551
621
|
|
|
552
622
|
// 1) /balance 命令:保留命令行查询入口
|
|
553
623
|
ctx.inject(["commands"], (commandCtx) => {
|
|
@@ -591,7 +661,7 @@ export function apply(ctx, config = {}) {
|
|
|
591
661
|
// /balance-api 不注册(404)。改为只声 inject ["webServer"],子代理在
|
|
592
662
|
// sumSubagentUsage 内部用可空 ctx.get("subagents") 获取。
|
|
593
663
|
ctx.inject(["webServer"], (httpCtx) => {
|
|
594
|
-
httpCtx.webServer.register({
|
|
664
|
+
const balanceRouteDisposer = httpCtx.webServer.register({
|
|
595
665
|
kind: "exact",
|
|
596
666
|
path: "/balance-api",
|
|
597
667
|
handler: async (req, res) => {
|
|
@@ -705,6 +775,15 @@ export function apply(ctx, config = {}) {
|
|
|
705
775
|
}
|
|
706
776
|
},
|
|
707
777
|
});
|
|
708
|
-
|
|
778
|
+
if (typeof balanceRouteDisposer === "function") disposers.push(balanceRouteDisposer);
|
|
709
779
|
});
|
|
780
|
+
|
|
781
|
+
// 插件卸载/dispose 时清理本实例注册的路由,避免 DSH 热重载时旧实例残留
|
|
782
|
+
// 的路由导致新实例 register /balance-api 抛 duplicate route、插件启动失败。
|
|
783
|
+
const release = () => {
|
|
784
|
+
for (const d of disposers) {
|
|
785
|
+
try { d(); } catch { /* ignore */ }
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
if (typeof ctx.on === "function") ctx.on("dispose", release);
|
|
710
789
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "balance-dsh",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "查询 DeepSeek 余额并显示当前会话 token 消耗。DeepSeek 账户余额 + 会话 token/费用显示,含高峰/空闲峰谷计费。",
|
|
5
5
|
"author": "Rannist",
|
|
6
6
|
"type": "module",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"patch": "./cordis.patch.yml"
|
|
27
27
|
},
|
|
28
28
|
"client": {
|
|
29
|
-
"platform": "web",
|
|
30
29
|
"inject": [
|
|
31
30
|
"@deepseek-ai/dsh-client-runtime"
|
|
32
31
|
],
|
|
32
|
+
"platform": "web",
|
|
33
33
|
"immediately": true
|
|
34
34
|
}
|
|
35
35
|
},
|