balance-dsh 1.3.1 → 2.0.1
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 +6 -5
- package/lib/client.js +1065 -85
- package/lib/index.js +236 -10
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -47,7 +47,7 @@ async function inspectPersistedSession(ctx, sessionId) {
|
|
|
47
47
|
return legacySessionInspector(ctx, sessionId);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
throw new Error("当前
|
|
50
|
+
throw new Error("当前 DeepSeek Harness 未提供兼容的持久化会话读取接口");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
|
|
@@ -159,8 +159,10 @@ function isPeakAt(timeMs, config) {
|
|
|
159
159
|
if (kind === "holiday") return false; // 法定节假日:全天按空闲计费
|
|
160
160
|
const hour = d.getUTCHours();
|
|
161
161
|
const weekday = d.getUTCDay(); // 0=周日 … 6=周六
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
// 官方定价页口径(2026-10 核对):周一至周五为工作日高峰区,其余时段
|
|
163
|
+
// 「包括周末及中国法定节假日全天」均为空闲。所以**调休上班的周末也按周末处理**,
|
|
164
|
+
// 不参与峰谷判定(CN_CALENDAR 里的 workday 条目只保留作数据,不再影响计费)。
|
|
165
|
+
return weekdays.includes(weekday) && windows.some((w) => hour >= w.start && hour < w.end);
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/** 把模型字符串规整到定价表键;未识别返回 ""(用 Flash 价兜底)。
|
|
@@ -343,7 +345,38 @@ function formatBalance(balance) {
|
|
|
343
345
|
// | { status:"failed" } (网络失败;官方明确“失败绝不变成 0 余额”)
|
|
344
346
|
// client 是 AccountClientMetadata(version / locale / timezoneOffsetSeconds),
|
|
345
347
|
// 缺字段会在平台侧组装请求头时抛错,所以三个字段都要给。
|
|
346
|
-
const PLUGIN_VERSION = "
|
|
348
|
+
const PLUGIN_VERSION = "2.0.1";
|
|
349
|
+
|
|
350
|
+
// ─── 检查更新 ───────────────────────────────────────────────────────────────
|
|
351
|
+
// 只查 npm registry 的 latest 标签(一个很小的 JSON),不下载任何东西。
|
|
352
|
+
// 结果缓存 10 分钟:设置页的按钮连着点也不会把 registry 打爆。
|
|
353
|
+
const UPDATE_CACHE_MS = 10 * 60 * 1000;
|
|
354
|
+
const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/balance-dsh/latest";
|
|
355
|
+
let updateCache = { at: 0, value: null };
|
|
356
|
+
|
|
357
|
+
/** 查 npm 上 balance-dsh 的最新版本。成功返回 { latest },失败返回 { error }(失败不缓存,下次可重试)。 */
|
|
358
|
+
async function fetchLatestVersion() {
|
|
359
|
+
const now = Date.now();
|
|
360
|
+
if (updateCache.value && now - updateCache.at < UPDATE_CACHE_MS) return updateCache.value;
|
|
361
|
+
if (typeof fetch !== "function") return { error: "当前运行环境没有 fetch" };
|
|
362
|
+
try {
|
|
363
|
+
let signal;
|
|
364
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
|
|
365
|
+
signal = AbortSignal.timeout(8000);
|
|
366
|
+
}
|
|
367
|
+
const res = await fetch(UPDATE_REGISTRY_URL, { headers: { accept: "application/json" }, signal });
|
|
368
|
+
if (!res || !res.ok) throw new Error("registry 返回 HTTP " + (res ? res.status : "?"));
|
|
369
|
+
const data = await res.json();
|
|
370
|
+
const latest = data && typeof data.version === "string" ? data.version.trim() : "";
|
|
371
|
+
if (latest === "") throw new Error("registry 返回里没有 version 字段");
|
|
372
|
+
updateCache = { at: now, value: { latest } };
|
|
373
|
+
return updateCache.value;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
// 超时 / 断网 / 被墙都走这里,只影响这个按钮,不影响余额与设置
|
|
376
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
347
380
|
|
|
348
381
|
/**
|
|
349
382
|
* 读取官方账号余额并归一化成与 normalizeBalance 相同的形状。
|
|
@@ -629,6 +662,30 @@ const DEFAULT_SETTINGS = {
|
|
|
629
662
|
showUsagePill: true, // 输入框下方「本会话消耗」胶囊
|
|
630
663
|
showPeriodLabel: true, // 高峰/空闲标签
|
|
631
664
|
showFab: true, // 收起态悬浮圆钮
|
|
665
|
+
// 2.0.0:余额行外观。classic = 早期那版「钱包图标 + 余额 + 金额」纯文字行;
|
|
666
|
+
// timeline = 现在的「meta 行 + 扁刻度条 + 金额」。
|
|
667
|
+
footerStyle: "timeline",
|
|
668
|
+
// 2.0.0:左侧导航栏里的插件设置入口(点一下直接打开插件配置页)
|
|
669
|
+
showSidebarEntry: true,
|
|
670
|
+
// 2.0.0:右键刷新时,余额行从左往右扫过一道泛光亮带
|
|
671
|
+
refreshWave: true,
|
|
672
|
+
// 侧栏「余额与用量」入口是否已被点开过。false = 显示红点。
|
|
673
|
+
// 与版本号无关:只有用户真正点开过这个面板才会被写成 true,之后永不回退。
|
|
674
|
+
entrySeen: false,
|
|
675
|
+
// NEW 徽标的计时起点:记录「当前版本首次启动」的时间戳与版本号。
|
|
676
|
+
// 版本一变(用户升级了)就重置计时;满 3 天后客户端自动不再显示 NEW。
|
|
677
|
+
newTagVersion: "",
|
|
678
|
+
newTagSince: 0,
|
|
679
|
+
// 升级前的版本(用户上一次跑的是哪个版本)。
|
|
680
|
+
// 客户端用它和「某项功能的引入版本」比较:只有比它新的功能才标 NEW。
|
|
681
|
+
// 这样跨几个版本一次升上来的用户能看到中间所有新功能,
|
|
682
|
+
// 已经跑过某个版本的用户不会被重复提示。
|
|
683
|
+
newTagFrom: "",
|
|
684
|
+
// 逐项计时:{ [配置项 key]: { v: 引入版本, t: 首次看到的时间戳 } }。
|
|
685
|
+
// 每个新功能各自走自己的 3 天,互不影响:
|
|
686
|
+
// 旧功能没过完 3 天时又升级,也不会被这次升级重新计时——
|
|
687
|
+
// 因为它记录的版本已经是用户见过的版本了。
|
|
688
|
+
newTagSeen: {},
|
|
632
689
|
// 收起态圆钮外观
|
|
633
690
|
fabSize: 60, // 直径(px)
|
|
634
691
|
fabLeft: 44, // 距窗口左缘(px)
|
|
@@ -655,6 +712,23 @@ function clampNumber(value, min, max, fallback) {
|
|
|
655
712
|
return Math.min(max, Math.max(min, n));
|
|
656
713
|
}
|
|
657
714
|
|
|
715
|
+
/** NEW 逐项计时表:只收 { v: 非空版本串, t: 非负时间戳 } 的条目,
|
|
716
|
+
* 其余一律丢弃(宁可丢一条记录重新计时,也不要带着脏数据跑)。 */
|
|
717
|
+
function normalizeNewTagSeen(raw) {
|
|
718
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
719
|
+
const out = {};
|
|
720
|
+
for (const key of Object.keys(raw)) {
|
|
721
|
+
if (typeof key !== "string" || key === "") continue;
|
|
722
|
+
const rec = raw[key];
|
|
723
|
+
if (rec == null || typeof rec !== "object" || Array.isArray(rec)) continue;
|
|
724
|
+
const v = typeof rec.v === "string" ? rec.v.trim() : "";
|
|
725
|
+
const t = Number(rec.t);
|
|
726
|
+
if (v === "" || !Number.isFinite(t) || t <= 0) continue;
|
|
727
|
+
out[key] = { v, t: Math.floor(t) };
|
|
728
|
+
}
|
|
729
|
+
return out;
|
|
730
|
+
}
|
|
731
|
+
|
|
658
732
|
/** 一组覆盖价:逐个字段校验(有限非负数才收),不填的字段留空表示「用默认价」。
|
|
659
733
|
* 四个字段都没填则整组丢弃(等价于不覆盖)。 */
|
|
660
734
|
function normalizePriceOverride(raw) {
|
|
@@ -683,6 +757,14 @@ function normalizeSettings(raw) {
|
|
|
683
757
|
showUsagePill: bool(src.showUsagePill, d.showUsagePill),
|
|
684
758
|
showPeriodLabel: bool(src.showPeriodLabel, d.showPeriodLabel),
|
|
685
759
|
showFab: bool(src.showFab, d.showFab),
|
|
760
|
+
footerStyle: src.footerStyle === "classic" ? "classic" : d.footerStyle,
|
|
761
|
+
showSidebarEntry: bool(src.showSidebarEntry, d.showSidebarEntry),
|
|
762
|
+
refreshWave: bool(src.refreshWave, d.refreshWave),
|
|
763
|
+
entrySeen: bool(src.entrySeen, d.entrySeen),
|
|
764
|
+
newTagVersion: typeof src.newTagVersion === "string" ? src.newTagVersion : d.newTagVersion,
|
|
765
|
+
newTagFrom: typeof src.newTagFrom === "string" ? src.newTagFrom : d.newTagFrom,
|
|
766
|
+
newTagSeen: normalizeNewTagSeen(src.newTagSeen),
|
|
767
|
+
newTagSince: clampNumber(src.newTagSince, 0, Number.MAX_SAFE_INTEGER, d.newTagSince),
|
|
686
768
|
fabSize: clampNumber(src.fabSize, 32, 120, d.fabSize),
|
|
687
769
|
fabLeft: clampNumber(src.fabLeft, 0, 600, d.fabLeft),
|
|
688
770
|
statsSessionLimit: clampNumber(src.statsSessionLimit, 10, 2000, d.statsSessionLimit),
|
|
@@ -731,21 +813,35 @@ function settingsFilePath(ctx) {
|
|
|
731
813
|
return `${home.replace(/[\\/]+$/, "")}${sep}${SETTINGS_FILENAME}`;
|
|
732
814
|
}
|
|
733
815
|
|
|
734
|
-
/** 读设置文件;不存在 / JSON 损坏 / 无权限一律返回 null(调用方保持默认值)。
|
|
816
|
+
/** 读设置文件;不存在 / JSON 损坏 / 无权限一律返回 null(调用方保持默认值)。
|
|
817
|
+
* 额外做一件事:老版本写下的文件里没有 showSidebarEntry 这个键,
|
|
818
|
+
* 读到这种「升级前」的文件时把侧边栏入口强制打开一次,让用户第一次升级后
|
|
819
|
+
* 能在左侧导航看到新入口(用户要求:第一次更新的用户强制显示在侧边栏)。
|
|
820
|
+
* 之后再关掉就按用户的选择来,不会反复弹回来。 */
|
|
735
821
|
async function readSettingsFile(file) {
|
|
736
822
|
if (file === "") return null;
|
|
737
823
|
try {
|
|
738
824
|
const { readFile } = await import("node:fs/promises");
|
|
739
825
|
const text = await readFile(file, "utf8");
|
|
740
|
-
|
|
741
|
-
|
|
826
|
+
const parsed = JSON.parse(text);
|
|
827
|
+
const settings = normalizeSettings(parsed);
|
|
828
|
+
if (parsed == null || typeof parsed !== "object" || !("showSidebarEntry" in parsed)) {
|
|
829
|
+
settings.showSidebarEntry = true;
|
|
830
|
+
}
|
|
831
|
+
// entrySeen 不按版本强制:文件里没写 true 的旧用户(升级上来的)
|
|
832
|
+
// 读出来就是未读,红点 + NEW 会提示他「这版加了新东西」。
|
|
833
|
+
return settings;
|
|
834
|
+
} catch (error) {
|
|
835
|
+
// 文件不存在 = 全新安装,这里不能当「未读」处理(否则新用户一装就看到一堆 NEW,
|
|
836
|
+
// 对他来说每一项都是新的,标 NEW 毫无信息量)。用字符串标记区分于「读失败」。
|
|
837
|
+
if (error && error.code === "ENOENT") return "missing";
|
|
742
838
|
return null;
|
|
743
839
|
}
|
|
744
840
|
}
|
|
745
841
|
|
|
746
842
|
/** 写设置文件:先写临时文件再改名,避免写一半把配置写坏。 */
|
|
747
843
|
async function writeSettingsFile(file, settings) {
|
|
748
|
-
if (file === "") throw new Error("无法定位
|
|
844
|
+
if (file === "") throw new Error("无法定位 DeepSeek Harness 配置目录");
|
|
749
845
|
const { mkdir, writeFile, rename } = await import("node:fs/promises");
|
|
750
846
|
const { dirname } = await import("node:path");
|
|
751
847
|
await mkdir(dirname(file), { recursive: true });
|
|
@@ -929,6 +1025,76 @@ function recentDayKeys(days, now = Date.now()) {
|
|
|
929
1025
|
return result;
|
|
930
1026
|
}
|
|
931
1027
|
|
|
1028
|
+
/** 只算「今天」的消耗总额,供余额行上方的 meta 行用。
|
|
1029
|
+
* 与 buildUsageStats 的区别:只扫 updatedAt 落在今天的会话,通常只有几个,
|
|
1030
|
+
* 所以可以随余额行刷新(缓存 60s),不必等 5 分钟的完整统计。
|
|
1031
|
+
* @returns {Promise<{ cost:number, totalTokens:number, sessions:number }>} */
|
|
1032
|
+
async function buildTodayCost(ctx, config) {
|
|
1033
|
+
const getService = typeof ctx.get === "function" ? ctx.get.bind(ctx) : null;
|
|
1034
|
+
const controller = getService?.("sessionController");
|
|
1035
|
+
const persistence = getService?.("sessionPersistence");
|
|
1036
|
+
const now = Date.now();
|
|
1037
|
+
// 北京当日 0 点对应的真实时间戳(用 UTC 分量读北京日期,再减回去)
|
|
1038
|
+
const bj = new Date(now + 8 * 60 * 60 * 1000);
|
|
1039
|
+
const dayStart = Date.UTC(bj.getUTCFullYear(), bj.getUTCMonth(), bj.getUTCDate()) - 8 * 60 * 60 * 1000;
|
|
1040
|
+
const todayKey = bj.getUTCFullYear() + "-" + String(bj.getUTCMonth() + 1).padStart(2, "0") + "-" + String(bj.getUTCDate()).padStart(2, "0");
|
|
1041
|
+
// 只挑今天动过的会话
|
|
1042
|
+
let candidates = [];
|
|
1043
|
+
if (typeof controller?.list === "function") {
|
|
1044
|
+
const listed = await controller.list({});
|
|
1045
|
+
const items = Array.isArray(listed) ? listed : (listed?.items ?? []);
|
|
1046
|
+
candidates = items
|
|
1047
|
+
.map((item) => (item && typeof item === "object" ? { id: item.sessionId ?? item.id, at: Number(item.updatedAt ?? item.createdAt ?? 0) } : null))
|
|
1048
|
+
.filter((x) => x && typeof x.id === "string" && x.id !== "");
|
|
1049
|
+
// list() 已按活动降序;万一没有 updatedAt 字段就保守全扫(上限 STATS_MAX_SESSIONS)
|
|
1050
|
+
if (candidates.some((x) => x.at > 0)) {
|
|
1051
|
+
candidates = candidates.filter((x) => x.at >= dayStart);
|
|
1052
|
+
}
|
|
1053
|
+
} else if (typeof persistence?.list === "function") {
|
|
1054
|
+
const headers = await persistence.list();
|
|
1055
|
+
const list = Array.isArray(headers) ? headers : [];
|
|
1056
|
+
candidates = list
|
|
1057
|
+
.map((h) => (h && typeof h.id === "string" ? { id: h.id, at: Number(h.updatedAt ?? h.createdAt ?? 0) } : null))
|
|
1058
|
+
.filter(Boolean);
|
|
1059
|
+
if (candidates.some((x) => x.at > 0)) {
|
|
1060
|
+
candidates = candidates.filter((x) => x.at >= dayStart);
|
|
1061
|
+
}
|
|
1062
|
+
} else {
|
|
1063
|
+
throw new Error("会话持久化服务不可用");
|
|
1064
|
+
}
|
|
1065
|
+
const selected = candidates
|
|
1066
|
+
.slice(0, clampNumber(config?.statsSessionLimit, 10, 2000, STATS_MAX_SESSIONS))
|
|
1067
|
+
.map((x) => x.id);
|
|
1068
|
+
const daily = new Map();
|
|
1069
|
+
let cursor = 0;
|
|
1070
|
+
const worker = async () => {
|
|
1071
|
+
while (cursor < selected.length) {
|
|
1072
|
+
const sessionId = selected[cursor++];
|
|
1073
|
+
try {
|
|
1074
|
+
const inspected = await inspectPersistedSession(ctx, sessionId);
|
|
1075
|
+
collectDailyUsage(
|
|
1076
|
+
inspected?.events ?? [],
|
|
1077
|
+
config,
|
|
1078
|
+
detectModel({ requestHeader: () => void 0, requestContext: () => void 0, events: inspected?.events ?? [] }),
|
|
1079
|
+
dayStart,
|
|
1080
|
+
daily,
|
|
1081
|
+
sessionId,
|
|
1082
|
+
);
|
|
1083
|
+
} catch {
|
|
1084
|
+
// 单个会话读不出来不影响总额
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
await Promise.all(Array.from({ length: Math.min(3, selected.length) }, () => worker()));
|
|
1089
|
+
const day = daily.get(todayKey);
|
|
1090
|
+
return {
|
|
1091
|
+
date: todayKey,
|
|
1092
|
+
cost: day?.cost ?? 0,
|
|
1093
|
+
totalTokens: day?.totalTokens ?? 0,
|
|
1094
|
+
sessions: day?.sessionIds?.size ?? 0,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
932
1098
|
async function buildUsageStats(ctx, config) {
|
|
933
1099
|
const getService = typeof ctx.get === "function" ? ctx.get.bind(ctx) : null;
|
|
934
1100
|
const controller = getService?.("sessionController");
|
|
@@ -1211,6 +1377,7 @@ function writeJson(res, status, payload) {
|
|
|
1211
1377
|
}
|
|
1212
1378
|
|
|
1213
1379
|
export function apply(ctx, config = {}) {
|
|
1380
|
+
let todayCache = { expiresAt: 0, value: null };
|
|
1214
1381
|
let statsCache = { expiresAt: 0, value: null };
|
|
1215
1382
|
// 用户设置(「插件」页配置卡片读写):先按出厂默认值跑,异步读到文件后热替换。
|
|
1216
1383
|
// 读不到文件不是错误——全新安装就是没有文件,保持默认值即可。
|
|
@@ -1219,9 +1386,35 @@ export function apply(ctx, config = {}) {
|
|
|
1219
1386
|
void (async () => {
|
|
1220
1387
|
settingsState.file = settingsFilePath(ctx);
|
|
1221
1388
|
const loaded = await readSettingsFile(settingsState.file);
|
|
1222
|
-
|
|
1389
|
+
let dirty = false;
|
|
1390
|
+
if (loaded === "missing") {
|
|
1391
|
+
// 全新安装:没有任何历史可回顾,红点不亮,
|
|
1392
|
+
// 并立即落盘固化,保证后续每次启动都走「已读」分支。
|
|
1393
|
+
settingsState.value = { ...DEFAULT_SETTINGS, entrySeen: true };
|
|
1394
|
+
dirty = true;
|
|
1395
|
+
} else if (loaded) {
|
|
1223
1396
|
settingsState.value = loaded;
|
|
1224
|
-
|
|
1397
|
+
}
|
|
1398
|
+
// NEW 徽标的计时:记下「这个版本首次启动」的时间。
|
|
1399
|
+
// 版本号变了(用户升级了)就重置,否则沿用上次记录的起点——
|
|
1400
|
+
// 否则每次启动都会把计时推到“现在”,永远过不了 3 天。
|
|
1401
|
+
const tagVersion = settingsState.value.newTagVersion || "";
|
|
1402
|
+
const tagSince = Number(settingsState.value.newTagSince) || 0;
|
|
1403
|
+
if (tagVersion !== PLUGIN_VERSION || !(tagSince > 0)) {
|
|
1404
|
+
// 升级:把「刚刚跑过的那个版本」存下来。首次安装时 tagVersion 为空串,
|
|
1405
|
+
// 客户端见空串就把所有 NEW_TAGS 都当新功能(新用户确实都没见过)。
|
|
1406
|
+
settingsState.value.newTagFrom = tagVersion || "";
|
|
1407
|
+
settingsState.value.newTagVersion = PLUGIN_VERSION;
|
|
1408
|
+
settingsState.value.newTagSince = Date.now();
|
|
1409
|
+
dirty = true;
|
|
1410
|
+
}
|
|
1411
|
+
effectiveConfig = withSettings(config, settingsState.value);
|
|
1412
|
+
if (dirty) {
|
|
1413
|
+
try {
|
|
1414
|
+
await writeSettingsFile(settingsState.file, settingsState.value);
|
|
1415
|
+
} catch {
|
|
1416
|
+
// 写盘失败(目录只读等)不影响这一次运行:内存里已经是最新值
|
|
1417
|
+
}
|
|
1225
1418
|
}
|
|
1226
1419
|
})();
|
|
1227
1420
|
// DSH 热重载(reload)时会重新 apply 本插件;若旧实例注册的 /balance-api 路由
|
|
@@ -1313,20 +1506,40 @@ export function apply(ctx, config = {}) {
|
|
|
1313
1506
|
writeJson(res, 405, { ok: false, error: "method not allowed" });
|
|
1314
1507
|
return;
|
|
1315
1508
|
}
|
|
1509
|
+
// 检查更新:GET /balance-api?update=1 → 返回 npm 上的最新版本。
|
|
1510
|
+
// 与余额无关,独立成一路,失败也不影响其它字段。
|
|
1511
|
+
let wantUpdate = false;
|
|
1512
|
+
try {
|
|
1513
|
+
wantUpdate = new URL(req.url, "http://localhost").searchParams.get("update") === "1";
|
|
1514
|
+
} catch {
|
|
1515
|
+
wantUpdate = false;
|
|
1516
|
+
}
|
|
1517
|
+
if (wantUpdate) {
|
|
1518
|
+
const found = await fetchLatestVersion();
|
|
1519
|
+
if (found && found.error) {
|
|
1520
|
+
writeJson(res, 200, { ok: false, current: PLUGIN_VERSION, error: found.error });
|
|
1521
|
+
} else {
|
|
1522
|
+
writeJson(res, 200, { ok: true, current: PLUGIN_VERSION, latest: found.latest });
|
|
1523
|
+
}
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1316
1526
|
// 客户端把当前会话使用的模型通过 ?model= 传入,服务端按模型取价;
|
|
1317
1527
|
// 可选 ?session= 传入会话 id,服务端返回该会话的分时段 usage(跨时段精确计费用)
|
|
1318
1528
|
let model = "";
|
|
1319
1529
|
let sessionParam = "";
|
|
1320
1530
|
let wantStats = false;
|
|
1531
|
+
let wantToday = false;
|
|
1321
1532
|
try {
|
|
1322
1533
|
const search = new URL(req.url, "http://localhost").searchParams;
|
|
1323
1534
|
model = search.get("model") ?? "";
|
|
1324
1535
|
sessionParam = search.get("session") ?? "";
|
|
1325
1536
|
wantStats = search.get("stats") === "1";
|
|
1537
|
+
wantToday = search.get("today") === "1";
|
|
1326
1538
|
} catch {
|
|
1327
1539
|
model = "";
|
|
1328
1540
|
sessionParam = "";
|
|
1329
1541
|
wantStats = false;
|
|
1542
|
+
wantToday = false;
|
|
1330
1543
|
}
|
|
1331
1544
|
let stats = null;
|
|
1332
1545
|
let statsError = "";
|
|
@@ -1343,6 +1556,18 @@ export function apply(ctx, config = {}) {
|
|
|
1343
1556
|
statsError = error instanceof Error ? error.message : String(error);
|
|
1344
1557
|
}
|
|
1345
1558
|
}
|
|
1559
|
+
// 今日消耗:只扫今天动过的会话,60 秒缓存,供余额行上方的 meta 行随余额一起刷新
|
|
1560
|
+
let today = null;
|
|
1561
|
+
if (wantToday) {
|
|
1562
|
+
try {
|
|
1563
|
+
if (todayCache.value == null || todayCache.expiresAt <= Date.now()) {
|
|
1564
|
+
todayCache = { value: await buildTodayCost(httpCtx, effectiveConfig), expiresAt: Date.now() + 60 * 1000 };
|
|
1565
|
+
}
|
|
1566
|
+
today = todayCache.value;
|
|
1567
|
+
} catch {
|
|
1568
|
+
today = null; // 拿不到就不显示,不阻塞余额
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1346
1571
|
// 按 session id 取会话并分时段汇总 usage(拿不到则返回 null,客户端回退用本地投影)
|
|
1347
1572
|
let usage = null;
|
|
1348
1573
|
// 子代理后代会话的 usage 汇总(与主会话分开)
|
|
@@ -1413,6 +1638,7 @@ export function apply(ctx, config = {}) {
|
|
|
1413
1638
|
subagentUsage,
|
|
1414
1639
|
stats,
|
|
1415
1640
|
statsError,
|
|
1641
|
+
today,
|
|
1416
1642
|
});
|
|
1417
1643
|
},
|
|
1418
1644
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "balance-dsh",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "在
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "在 DeepSeek Harness 中显示 DeepSeek 账户余额和当前会话 token/费用消耗(含高峰/空闲峰谷计费)。仅支持 DeepSeek 官方模型与账号,暂不支持其他大模型。",
|
|
5
5
|
"author": "Rannist",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|