balance-dsh 1.0.8 → 1.0.9

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.
Files changed (2) hide show
  1. package/lib/index.js +67 -7
  2. 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 生效的
@@ -429,12 +475,12 @@ async function buildUsageStats(ctx, config) {
429
475
  }
430
476
 
431
477
  /** 汇总某会话的所有子代理后代会话的 usage(跨时段精确计费,与主会话分开)。
432
- * @param ctx DSH 上下文(用于 subagents 服务 + inspectApiRemoteSession)
478
+ * @param ctx DSH 上下文(用于 subagents 服务 + 跨版本持久化会话读取)
433
479
  * @param sessionId 父会话 id
434
480
  * @param config 插件配置
435
481
  * 说明:子代理会话结束后会被从 live 会话 store 移除,故不能用 store.list() 枚举;
436
482
  * 这里用 subagents 服务的 listDescendants(含已结束/嵌套子代理)取后代 id,
437
- * 再对每个子代理用 inspectApiRemoteSession 读取持久化事件计算 usage。无子代理返回 {usage:null,debug}。 */
483
+ * 再对每个子代理用兼容读取器取得持久化事件并计算 usage。无子代理返回 {usage:null,debug}。 */
438
484
  async function sumSubagentUsage(ctx, sessionId, config) {
439
485
  if (ctx == null || sessionId === "") return { usage: null };
440
486
  // 用 ctx.get("subagents") 安全获取(非注入,可空);不要直接读 ctx.subagents 属性,
@@ -466,7 +512,7 @@ async function sumSubagentUsage(ctx, sessionId, config) {
466
512
  let count = 0;
467
513
  for (const id of childIds) {
468
514
  try {
469
- const inspected = await inspectApiRemoteSession(ctx, id);
515
+ const inspected = await inspectPersistedSession(ctx, id);
470
516
  const events = Array.isArray(inspected?.events) ? inspected.events : [];
471
517
  const u = sumEventsUsage(events, config);
472
518
  if (u === null || u === void 0) continue;
@@ -548,6 +594,11 @@ function writeJson(res, status, payload) {
548
594
 
549
595
  export function apply(ctx, config = {}) {
550
596
  let statsCache = { expiresAt: 0, value: null };
597
+ // DSH 热重载(reload)时会重新 apply 本插件;若旧实例注册的 /balance-api 路由
598
+ // 未被清理,新实例再次 register 就会抛出 duplicate route,导致整个 apply 失败、
599
+ // 余额不显示。这里收集每次 register 返回的 disposer,并在插件卸载(ctx.on("dispose"))
600
+ // 时统一清理,保证重复 apply 不冲突。
601
+ const disposers = [];
551
602
 
552
603
  // 1) /balance 命令:保留命令行查询入口
553
604
  ctx.inject(["commands"], (commandCtx) => {
@@ -591,7 +642,7 @@ export function apply(ctx, config = {}) {
591
642
  // /balance-api 不注册(404)。改为只声 inject ["webServer"],子代理在
592
643
  // sumSubagentUsage 内部用可空 ctx.get("subagents") 获取。
593
644
  ctx.inject(["webServer"], (httpCtx) => {
594
- httpCtx.webServer.register({
645
+ const balanceRouteDisposer = httpCtx.webServer.register({
595
646
  kind: "exact",
596
647
  path: "/balance-api",
597
648
  handler: async (req, res) => {
@@ -705,6 +756,15 @@ export function apply(ctx, config = {}) {
705
756
  }
706
757
  },
707
758
  });
708
-
759
+ if (typeof balanceRouteDisposer === "function") disposers.push(balanceRouteDisposer);
709
760
  });
761
+
762
+ // 插件卸载/dispose 时清理本实例注册的路由,避免 DSH 热重载时旧实例残留
763
+ // 的路由导致新实例 register /balance-api 抛 duplicate route、插件启动失败。
764
+ const release = () => {
765
+ for (const d of disposers) {
766
+ try { d(); } catch { /* ignore */ }
767
+ }
768
+ };
769
+ if (typeof ctx.on === "function") ctx.on("dispose", release);
710
770
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "balance-dsh",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
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
  },