balance-dsh 2.0.7 → 2.0.8

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 +85 -32
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -527,7 +527,7 @@ function formatBalance(balance) {
527
527
  // | { status:"failed" } (网络失败;官方明确“失败绝不变成 0 余额”)
528
528
  // client 是 AccountClientMetadata(version / locale / timezoneOffsetSeconds),
529
529
  // 缺字段会在平台侧组装请求头时抛错,所以三个字段都要给。
530
- const PLUGIN_VERSION = "2.0.7";
530
+ const PLUGIN_VERSION = "2.0.8";
531
531
 
532
532
  // ─── 检查更新 ───────────────────────────────────────────────────────────────
533
533
  // 只查 npm registry 的 latest 标签(一个很小的 JSON),不下载任何东西。
@@ -1940,6 +1940,39 @@ function writeJson(res, status, payload) {
1940
1940
  res.end(body);
1941
1941
  }
1942
1942
 
1943
+ /** 后台异步任务的兜底包装。
1944
+ *
1945
+ * 为什么必须有:本插件里所有「不 await 的后台任务」(读设置文件、同步节假日、拉更新)
1946
+ * 都是 fire-and-forget。只要其中任何一个抛出未被包住的异常,Node/Electron 会把它升级成
1947
+ * unhandledRejection —— **Electron 主进程遇到它会直接报错退出**。用户看到的现象是
1948
+ * 「DSH 更新完启动就崩,把插件禁用了才起得来,之后再启用又正常」(2026-10-30 反馈)。
1949
+ *
1950
+ * 那个现象的本质:启动期插件是主进程的一部分,它抛错 = bootstrap 中止;
1951
+ * 而运行时启用时 app 已经活着,同样的错只影响插件自己。
1952
+ * 所以这里把所有后台任务统一吞掉:失败就降级(设置用默认值 / 日历用内置表 / 跳过这次刷新),
1953
+ * 只写一条 warn,绝不把异常漏成 unhandledRejection。
1954
+ *
1955
+ * 注意:只包**异步/后台**那段。同步的 register 类错误该暴露还是要暴露,
1956
+ * 否则出问题时会变成「插件静默不工作」,更难查。 */
1957
+ function runQuietly(label, fn) {
1958
+ const report = (error) => {
1959
+ try {
1960
+ const detail = error && error.message ? error.message : String(error);
1961
+ console.warn("[balance-dsh] " + label + " 失败(已降级,不影响启动): " + detail);
1962
+ } catch {
1963
+ // console 本身不可用时也不能再抛
1964
+ }
1965
+ };
1966
+ try {
1967
+ const result = fn();
1968
+ if (result && typeof result.then === "function") {
1969
+ result.then(undefined, report);
1970
+ }
1971
+ } catch (error) {
1972
+ report(error);
1973
+ }
1974
+ }
1975
+
1943
1976
  export function apply(ctx, config = {}) {
1944
1977
  let todayCache = { expiresAt: 0, value: null };
1945
1978
  let statsCache = { expiresAt: 0, value: null };
@@ -1947,7 +1980,10 @@ export function apply(ctx, config = {}) {
1947
1980
  // 读不到文件不是错误——全新安装就是没有文件,保持默认值即可。
1948
1981
  const settingsState = { value: { ...DEFAULT_SETTINGS }, file: "" };
1949
1982
  let effectiveConfig = withSettings(config, settingsState.value);
1950
- void (async () => {
1983
+ // 读设置是后台任务:settingsFilePath 走 dshHomePath 服务、readSettingsFile 走 fs,
1984
+ // 两者都可能在 DSH 换版本后抛错(内部服务改名 / 目录权限变化)。
1985
+ // 用 runQuietly 包住 —— 读不到就保持出厂默认值继续跑,绝不能漏成 unhandledRejection。
1986
+ runQuietly("读取设置", async () => {
1951
1987
  settingsState.file = settingsFilePath(ctx);
1952
1988
  const loaded = await readSettingsFile(settingsState.file);
1953
1989
  let dirty = false;
@@ -1980,7 +2016,7 @@ export function apply(ctx, config = {}) {
1980
2016
  // 写盘失败(目录只读等)不影响这一次运行:内存里已经是最新值
1981
2017
  }
1982
2018
  }
1983
- })();
2019
+ });
1984
2020
  // DSH 热重载(reload)时会重新 apply 本插件;若旧实例注册的 /balance-api 路由
1985
2021
  // 未被清理,新实例再次 register 就会抛出 duplicate route,导致整个 apply 失败、
1986
2022
  // 余额不显示。这里收集每次 register 返回的 disposer,并在插件卸载(ctx.on("dispose"))
@@ -1990,39 +2026,43 @@ export function apply(ctx, config = {}) {
1990
2026
  // 节假日数据:先读本地缓存(这样断网、改过系统时间也照样按上次同步的结果算钱),
1991
2027
  // 再看缓存是不是过期(默认 30 天),过期才去拉。故意不 await —— 网络慢不能拖住启动,
1992
2028
  // isPeakAt 保持同步函数,缓存没到位时继续用内置表。
1993
- void (async () => {
1994
- try {
1995
- await loadCalendarCache(ctx);
1996
- await refreshCnCalendar(ctx, false);
1997
- } catch {
1998
- // 任何异常都只是「这次没更新」,不影响余额与计费
1999
- }
2000
- })();
2029
+ runQuietly("同步节假日数据", async () => {
2030
+ await loadCalendarCache(ctx);
2031
+ await refreshCnCalendar(ctx, false);
2032
+ });
2001
2033
 
2002
2034
  // 1) /balance 命令:保留命令行查询入口
2003
2035
  ctx.inject(["commands"], (commandCtx) => {
2004
- commandCtx.commands.register({
2005
- name: "balance",
2006
- description: "查询 DeepSeek 余额与当前会话消耗",
2007
- handler: async ({ agent, signal }) => {
2008
- const usage = sumSessionUsage(agent.session, effectiveConfig);
2009
- const model = detectModel(agent.session);
2010
- const pricing = catalogPricing(effectiveConfig, model);
2011
- const multiplier = peakMultiplierOf(effectiveConfig);
2012
- // 余额来源:官方账号优先(官方版内置登录,无需 API Key),回退 API Key。
2013
- const { balance, error } = await resolveBalance(ctx, effectiveConfig, signal);
2014
- if (balance === null) {
2036
+ // register 抛错(命令名冲突 / API 变更)只影响 /balance 命令本身,
2037
+ // 不能让它中止 bootstrap —— 余额行和消耗行都不走命令通道。
2038
+ try {
2039
+ commandCtx.commands.register({
2040
+ name: "balance",
2041
+ description: "查询 DeepSeek 余额与当前会话消耗",
2042
+ handler: async ({ agent, signal }) => {
2043
+ const usage = sumSessionUsage(agent.session, effectiveConfig);
2044
+ const model = detectModel(agent.session);
2045
+ const pricing = catalogPricing(effectiveConfig, model);
2046
+ const multiplier = peakMultiplierOf(effectiveConfig);
2047
+ // 余额来源:官方账号优先(官方版内置登录,无需 API Key),回退 API Key。
2048
+ const { balance, error } = await resolveBalance(ctx, effectiveConfig, signal);
2049
+ if (balance === null) {
2050
+ return {
2051
+ kind: "error",
2052
+ text: `${error}\n\n${formatUsage(usage, pricing, multiplier, model)}`,
2053
+ };
2054
+ }
2015
2055
  return {
2016
- kind: "error",
2017
- text: `${error}\n\n${formatUsage(usage, pricing, multiplier, model)}`,
2056
+ kind: "success",
2057
+ text: `${formatBalance(balance)}\n\n${formatUsage(usage, pricing, multiplier, model)}`,
2018
2058
  };
2019
- }
2020
- return {
2021
- kind: "success",
2022
- text: `${formatBalance(balance)}\n\n${formatUsage(usage, pricing, multiplier, model)}`,
2023
- };
2024
- },
2025
- });
2059
+ },
2060
+ });
2061
+ } catch (error) {
2062
+ runQuietly("注册 /balance 命令", () => {
2063
+ throw error;
2064
+ });
2065
+ }
2026
2066
  });
2027
2067
 
2028
2068
  // 2) /balance-api HTTP 接口:供客户端插件直接渲染余额(不经过命令,不污染会话日志)
@@ -2031,7 +2071,13 @@ export function apply(ctx, config = {}) {
2031
2071
  // /balance-api 不注册(404)。改为只声 inject ["webServer"],子代理在
2032
2072
  // sumSubagentUsage 内部用可空 ctx.get("subagents") 获取。
2033
2073
  ctx.inject(["webServer"], (httpCtx) => {
2034
- const balanceRouteDisposer = httpCtx.webServer.register({
2074
+ // register 是最可能在启动期炸的一步:DSH 更新/热重载后旧实例路由没清干净,
2075
+ // 新实例再注册就会抛 duplicate exact route。以前这个异常会一路冒到 loader,
2076
+ // 整个插件加载失败(连带余额行、消耗行、设置卡片全不显示)。
2077
+ // 现在单独兜住:注册失败只意味着余额接口这次不可用,启动照常。
2078
+ let balanceRouteDisposer = null;
2079
+ try {
2080
+ balanceRouteDisposer = httpCtx.webServer.register({
2035
2081
  kind: "exact",
2036
2082
  path: "/balance-api",
2037
2083
  handler: async (req, res) => {
@@ -2258,6 +2304,13 @@ export function apply(ctx, config = {}) {
2258
2304
  });
2259
2305
  },
2260
2306
  });
2307
+ } catch (error) {
2308
+ // duplicate exact route / API 变更:余额接口这次不可用,但插件照常加载
2309
+ runQuietly("注册 /balance-api 路由", () => {
2310
+ throw error;
2311
+ });
2312
+ return;
2313
+ }
2261
2314
  if (typeof balanceRouteDisposer === "function") disposers.push(balanceRouteDisposer);
2262
2315
  });
2263
2316
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "balance-dsh",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "在 DeepSeek Harness 中显示 DeepSeek 账户余额和当前会话 token/费用消耗(含高峰/空闲峰谷计费)。仅支持 DeepSeek 官方模型与账号,暂不支持其他大模型。",
5
5
  "author": "Rannist",
6
6
  "type": "module",