dsh-balance-widget 0.4.0 → 0.4.3
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.en.md +1 -1
- package/README.md +1 -1
- package/lib/client.js +164 -215
- package/lib/index.js +122 -14
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -164,7 +164,7 @@ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective
|
|
|
164
164
|
|
|
165
165
|
- Config tree: `dsh --profile web --dump-config` should show a `balance-widget` entry.
|
|
166
166
|
- Balance route: after restarting dsh web, `curl -s http://127.0.0.1:3080/api/dsh-balance/balance` should return `{ ok, balance_infos, modelId }`.
|
|
167
|
-
- Cost routes: `curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session
|
|
167
|
+
- Cost routes: `curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` and `curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` should return `{ cost, inputTokens, outputTokens, modelId }`.
|
|
168
168
|
|
|
169
169
|
## License
|
|
170
170
|
|
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ DSH 的插件配置统一放在这个文件里:
|
|
|
162
162
|
|
|
163
163
|
- 配置树:`dsh --profile web --dump-config` 应出现 `balance-widget` 条目
|
|
164
164
|
- 余额路由:重启 dsh web 后 `curl -s http://127.0.0.1:3080/api/dsh-balance/balance` 应返回 `{ ok, balance_infos, modelId }`
|
|
165
|
-
- 成本路由:`curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session
|
|
165
|
+
- 成本路由:`curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` 与 `curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` 应返回 `{ cost, inputTokens, outputTokens, modelId }`
|
|
166
166
|
|
|
167
167
|
## License
|
|
168
168
|
|
package/lib/client.js
CHANGED
|
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
let react = require("react");
|
|
9
9
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
10
|
//#region lib/types/client/styles.js
|
|
11
|
-
const css = ".dshbw_wrap{position:relative;display:inline-flex;align-items:center}.dshbw_btn{background:none;border:none;cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:6px;font-size:12px;line-height:18px;font-family:inherit}.dshbw_btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_btn[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_btn[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_pop{position:absolute;top:calc(100% + 6px);right:0;z-index:60;min-width:230px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l3);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:12px 14px;font-size:12px;line-height:20px;color:var(--dsw-alias-label-primary)}.dshbw_head{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}.dshbw_title{font-weight:600;color:var(--dsw-alias-label-primary)}.dshbw_refresh{background:none;border:1px solid var(--dsw-alias-border-l2);cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:6px;font-size:13px;line-height:1;padding:0;font-family:inherit}.dshbw_refresh:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_refresh[data-spinning=true]{animation:dshbw-spin .8s linear infinite}.dshbw_row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:2px 0}.dshbw_label{display:inline-flex;align-items:center;min-width:0}.dshbw_val{font-weight:600;color:var(--dsw-alias-label-primary)}.dshbw_val[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_val[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_err{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;margin-top:6px}.dshbw_hint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin-top:6px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:6px}.dshbw_foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px}.dshbw_link{background:none;border:none;cursor:pointer;color:var(--dsw-alias-state-business-primary);display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:6px;font-size:12px;line-height:18px;font-family:inherit;text-decoration:none}.dshbw_link:hover{color:var(--dsw-alias-state-business-primary);text-decoration:underline}@keyframes dshbw-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.dshbw_side{display:flex;flex-direction:column;gap:4px;padding:6px 8px;margin:0 8px 4px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.dshbw_side:hover{background:var(--dsw-alias-interactive-bg-hover)}.dshbw_sideTop{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;line-height:18px}.dshbw_sideAmount{font-weight:600}.dshbw_sideAmount[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_sideAmount[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_bar{height:4px;border-radius:2px;background:var(--dsw-alias-border-l2);overflow:hidden}.dshbw_barFill{height:100%;border-radius:2px;transition:width .4s ease}.dshbw_barFill[data-level=\"0\"]{background:var(--dsw-alias-state-business-primary)}.dshbw_barFill[data-level=\"1\"]{background:var(--dsw-alias-state-warn-primary)}.dshbw_barFill[data-level=\"2\"]{background:var(--dsw-alias-state-error-primary)}.dshbw_sideFoot{display:flex;justify-content:space-between;font-size:10px;line-height:14px;color:var(--dsw-alias-label-tertiary)}";
|
|
11
|
+
const css = ".dshbw_wrap{position:relative;display:inline-flex;align-items:center}.dshbw_btn{background:none;border:none;cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:6px;font-size:12px;line-height:18px;font-family:inherit}.dshbw_btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_btn[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_btn[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_pop{position:absolute;top:calc(100% + 6px);right:0;z-index:60;min-width:230px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l3);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:12px 14px;font-size:12px;line-height:20px;color:var(--dsw-alias-label-primary)}.dshbw_head{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}.dshbw_title{font-weight:600;color:var(--dsw-alias-label-primary)}.dshbw_refresh{background:none;border:1px solid var(--dsw-alias-border-l2);cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:6px;font-size:13px;line-height:1;padding:0;font-family:inherit}.dshbw_refresh:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_refresh[data-spinning=true]{animation:dshbw-spin .8s linear infinite}.dshbw_row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:2px 0}.dshbw_label{display:inline-flex;align-items:center;min-width:0}.dshbw_val{font-weight:600;color:var(--dsw-alias-label-primary)}.dshbw_val[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_val[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_err{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;margin-top:6px}.dshbw_hint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin-top:6px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:6px}.dshbw_foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px}.dshbw_link{background:none;border:none;cursor:pointer;color:var(--dsw-alias-state-business-primary);display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:6px;font-size:12px;line-height:18px;font-family:inherit;text-decoration:none}.dshbw_link:hover{color:var(--dsw-alias-state-business-primary);text-decoration:underline}@keyframes dshbw-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.dshbw_side{display:flex;flex-direction:column;gap:4px;padding:6px 8px;margin:0 8px 4px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.dshbw_side:hover{background:var(--dsw-alias-interactive-bg-hover)}.dshbw_sideTop{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;line-height:18px}.dshbw_sideAmount{font-weight:600}.dshbw_sideAmount[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_sideAmount[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_bar{height:4px;border-radius:2px;background:var(--dsw-alias-border-l2);overflow:hidden}.dshbw_barFill{height:100%;border-radius:2px;transition:width .4s ease}.dshbw_barFill[data-level=\"0\"]{background:var(--dsw-alias-state-business-primary)}.dshbw_barFill[data-level=\"1\"]{background:var(--dsw-alias-state-warn-primary)}.dshbw_barFill[data-level=\"2\"]{background:var(--dsw-alias-state-error-primary)}.dshbw_sideFoot{display:flex;justify-content:space-between;font-size:10px;line-height:14px;color:var(--dsw-alias-label-tertiary)}.dshbw_sideWrap{position:relative}.dshbw_side{display:flex;flex-direction:column;gap:4px;width:100%;padding:6px 8px;margin:0 8px 4px;border:none;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;text-align:left}.dshbw_side:hover{background:var(--dsw-alias-interactive-bg-hover)}.dshbw_sidePop{position:absolute;bottom:calc(100% + 8px);left:8px;z-index:70;min-width:240px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l3);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:12px 14px;font-size:12px;line-height:20px;color:var(--dsw-alias-label-primary)}.dshbw_sidePopHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.dshbw_sidePopTitle{font-weight:600}.dshbw_sidePopClose{background:none;border:none;cursor:pointer;color:var(--dsw-alias-label-tertiary);font-size:16px;line-height:1;padding:0 2px;font-family:inherit}.dshbw_sidePopClose:hover{color:var(--dsw-alias-label-primary)}.dshbw_sidePopFoot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}.dshbw_sidePopRefresh{background:none;border:1px solid var(--dsw-alias-border-l2);cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;padding:0;font-family:inherit}.dshbw_sidePopRefresh:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}";
|
|
12
12
|
const tagId = "dsh-balance-widget/styles.css";
|
|
13
13
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
14
14
|
const tag = document.createElement("style");
|
|
@@ -32,11 +32,18 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
"foot": "dshbw_foot",
|
|
33
33
|
"link": "dshbw_link",
|
|
34
34
|
"side": "dshbw_side",
|
|
35
|
+
"sideWrap": "dshbw_sideWrap",
|
|
35
36
|
"sideTop": "dshbw_sideTop",
|
|
36
37
|
"sideAmount": "dshbw_sideAmount",
|
|
37
38
|
"bar": "dshbw_bar",
|
|
38
39
|
"barFill": "dshbw_barFill",
|
|
39
|
-
"sideFoot": "dshbw_sideFoot"
|
|
40
|
+
"sideFoot": "dshbw_sideFoot",
|
|
41
|
+
"sidePop": "dshbw_sidePop",
|
|
42
|
+
"sidePopHead": "dshbw_sidePopHead",
|
|
43
|
+
"sidePopTitle": "dshbw_sidePopTitle",
|
|
44
|
+
"sidePopClose": "dshbw_sidePopClose",
|
|
45
|
+
"sidePopFoot": "dshbw_sidePopFoot",
|
|
46
|
+
"sidePopRefresh": "dshbw_sidePopRefresh"
|
|
40
47
|
};
|
|
41
48
|
//#endregion
|
|
42
49
|
//#region lib/types/client/pricing.js
|
|
@@ -125,9 +132,24 @@ window.__ModuleLoader__.load({
|
|
|
125
132
|
/** Same-origin cost routes. */
|
|
126
133
|
const LAST_COST_ROUTE = "/api/dsh-balance/last-cost";
|
|
127
134
|
const TODAY_COST_ROUTE = "/api/dsh-balance/today-cost";
|
|
135
|
+
const ACTIVE_COST_ROUTE = "/api/dsh-balance/active-cost";
|
|
128
136
|
/** Official DeepSeek platform top-up page (opens in a new tab). */
|
|
129
137
|
const TOP_UP_URL = "https://platform.deepseek.com/top_up";
|
|
130
138
|
/**
|
|
139
|
+
* Fetch the active session's costs (last prompt + session total).
|
|
140
|
+
* @returns { ok, lastPrompt?, sessionTotal?, error? }
|
|
141
|
+
*/
|
|
142
|
+
async function fetchActiveCost() {
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(ACTIVE_COST_ROUTE, { headers: { accept: "application/json" } });
|
|
145
|
+
const payload = await response.json().catch(() => ({}));
|
|
146
|
+
if (!response.ok) return { ok: false, error: payload.error ?? `active-cost responded ${response.status}` };
|
|
147
|
+
return { ok: true, lastPrompt: payload.lastPrompt?.cost ?? 0, sessionTotal: payload.sessionTotal?.cost ?? 0 };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
131
153
|
* Fetch the account balance from the host proxy.
|
|
132
154
|
* @returns { ok, balanceInfos?, modelId?, error? }
|
|
133
155
|
*/
|
|
@@ -208,77 +230,73 @@ window.__ModuleLoader__.load({
|
|
|
208
230
|
});
|
|
209
231
|
}
|
|
210
232
|
//#endregion
|
|
211
|
-
//#region lib/types/client/
|
|
233
|
+
//#region lib/types/client/SidebarBalanceCard.js
|
|
212
234
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
235
|
+
* Sidebar footer card: balance + remaining-ratio bar + today cost.
|
|
236
|
+
* Root-scoped (global, not per-session) — fetches balance itself and
|
|
237
|
+
* refreshes every 60s. Clicking opens a detail popover (balance, today
|
|
238
|
+
* cost, top-up, refresh) using only global data.
|
|
239
|
+
* @param {Object} props - wide (sidebar expanded), t (locale).
|
|
217
240
|
*/
|
|
218
|
-
function
|
|
241
|
+
function SidebarBalanceCard({ wide, t }) {
|
|
219
242
|
const [open, setOpen] = (0, react.useState)(false);
|
|
220
243
|
const [balance, setBalance] = (0, react.useState)(null);
|
|
221
|
-
const [
|
|
222
|
-
const [
|
|
223
|
-
const [
|
|
224
|
-
const [
|
|
225
|
-
const [loading, setLoading] = (0, react.useState)(false);
|
|
226
|
-
const [refreshing, setRefreshing] = (0, react.useState)(false);
|
|
227
|
-
const [lastUpdated, setLastUpdated] = (0, react.useState)(null);
|
|
244
|
+
const [lastPrompt, setLastPrompt] = (0, react.useState)(null);
|
|
245
|
+
const [sessionTotal, setSessionTotal] = (0, react.useState)(null);
|
|
246
|
+
const [todayWs, setTodayWs] = (0, react.useState)(null);
|
|
247
|
+
const [todayAll, setTodayAll] = (0, react.useState)(null);
|
|
228
248
|
const [lowThreshold, setLowThreshold] = (0, react.useState)(5);
|
|
229
249
|
const [criticalThreshold, setCriticalThreshold] = (0, react.useState)(1);
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
/**
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (typeof result.criticalThreshold === "number" && result.criticalThreshold >= 0) setCriticalThreshold(result.criticalThreshold);
|
|
243
|
-
}
|
|
250
|
+
const [error, setError] = (0, react.useState)(null);
|
|
251
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
252
|
+
/** Fetch today's costs (workspace + all) from the dual-value route. */
|
|
253
|
+
const fetchTodayCosts = async () => {
|
|
254
|
+
const response = await fetch(TODAY_COST_ROUTE, { headers: { accept: "application/json" } });
|
|
255
|
+
const payload = await response.json().catch(() => ({}));
|
|
256
|
+
if (!response.ok) return { ok: false, error: payload.error ?? `today-cost responded ${response.status}` };
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
workspace: payload.workspace?.cost ?? 0,
|
|
260
|
+
all: payload.all?.cost ?? 0
|
|
261
|
+
};
|
|
244
262
|
};
|
|
245
|
-
|
|
246
|
-
const refreshAll = async (showLoading) => {
|
|
263
|
+
const refresh = async (showLoading) => {
|
|
247
264
|
if (showLoading) setLoading(true);
|
|
248
265
|
setError(null);
|
|
249
|
-
const [
|
|
266
|
+
const [bResult, aResult, tResult] = await Promise.all([
|
|
250
267
|
fetchBalance(),
|
|
251
|
-
|
|
252
|
-
|
|
268
|
+
fetchActiveCost(),
|
|
269
|
+
fetchTodayCosts()
|
|
253
270
|
]);
|
|
254
|
-
// Collect every failure into one fresh message; never accumulate
|
|
255
|
-
// across refreshes (each refresh starts from a clean slate).
|
|
256
271
|
const failures = [];
|
|
257
|
-
if (
|
|
258
|
-
setBalance(displayBalance(
|
|
259
|
-
|
|
260
|
-
if (typeof
|
|
261
|
-
if (typeof balanceResult.lowThreshold === "number" && balanceResult.lowThreshold >= 0) setLowThreshold(balanceResult.lowThreshold);
|
|
262
|
-
if (typeof balanceResult.criticalThreshold === "number" && balanceResult.criticalThreshold >= 0) setCriticalThreshold(balanceResult.criticalThreshold);
|
|
272
|
+
if (bResult.ok) {
|
|
273
|
+
setBalance(displayBalance(bResult.balanceInfos));
|
|
274
|
+
if (typeof bResult.lowThreshold === "number" && bResult.lowThreshold >= 0) setLowThreshold(bResult.lowThreshold);
|
|
275
|
+
if (typeof bResult.criticalThreshold === "number" && bResult.criticalThreshold >= 0) setCriticalThreshold(bResult.criticalThreshold);
|
|
263
276
|
} else {
|
|
264
|
-
|
|
265
|
-
|
|
277
|
+
failures.push(`余额: ${bResult.error}`);
|
|
278
|
+
}
|
|
279
|
+
if (aResult.ok) {
|
|
280
|
+
setLastPrompt(aResult.lastPrompt);
|
|
281
|
+
setSessionTotal(aResult.sessionTotal);
|
|
282
|
+
} else {
|
|
283
|
+
failures.push(`会话: ${aResult.error}`);
|
|
284
|
+
}
|
|
285
|
+
if (tResult.ok) {
|
|
286
|
+
setTodayWs(tResult.workspace);
|
|
287
|
+
setTodayAll(tResult.all);
|
|
288
|
+
} else {
|
|
289
|
+
failures.push(`今日: ${tResult.error}`);
|
|
266
290
|
}
|
|
267
|
-
if (lastResult.ok) setLastCost(lastResult.cost);
|
|
268
|
-
else if (lastResult.error) failures.push(`最近提问: ${lastResult.error}`);
|
|
269
|
-
if (todayResult.ok) setTodayCost(todayResult.cost);
|
|
270
|
-
else if (todayResult.error) failures.push(`今日: ${todayResult.error}`);
|
|
271
291
|
setError(failures.length > 0 ? failures.join(";") : null);
|
|
272
292
|
if (showLoading) setLoading(false);
|
|
273
293
|
};
|
|
274
|
-
// Fetch balance on mount, then auto-refresh every 60s so the corner
|
|
275
|
-
// amount never goes stale (balance changes only on usage/top-up).
|
|
276
294
|
(0, react.useEffect)(() => {
|
|
277
|
-
|
|
278
|
-
const timer = setInterval(
|
|
295
|
+
refresh(false);
|
|
296
|
+
const timer = setInterval(() => refresh(false), 60000);
|
|
279
297
|
return () => clearInterval(timer);
|
|
280
298
|
}, []);
|
|
281
|
-
// Close the popover when clicking
|
|
299
|
+
// Close the popover when clicking outside the card.
|
|
282
300
|
const wrapRef = (0, react.useRef)(null);
|
|
283
301
|
(0, react.useEffect)(() => {
|
|
284
302
|
if (!open) return;
|
|
@@ -289,59 +307,73 @@ window.__ModuleLoader__.load({
|
|
|
289
307
|
document.addEventListener("mousedown", onDocClick);
|
|
290
308
|
return () => document.removeEventListener("mousedown", onDocClick);
|
|
291
309
|
}, [open]);
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
refreshAll(false);
|
|
299
|
-
}, 30000);
|
|
300
|
-
return () => clearInterval(timer);
|
|
301
|
-
}, [open]);
|
|
310
|
+
const level = balance === null ? 0 : balance.total < criticalThreshold ? 2 : balance.total < lowThreshold ? 1 : 0;
|
|
311
|
+
// Remaining-ratio: 100% at lowThreshold (or above), 0% at 0.
|
|
312
|
+
let percent = 100;
|
|
313
|
+
if (balance !== null && balance.total < lowThreshold) {
|
|
314
|
+
percent = lowThreshold <= 0 ? 100 : Math.max(0, Math.round(balance.total / lowThreshold * 100));
|
|
315
|
+
}
|
|
302
316
|
const toggle = async () => {
|
|
303
317
|
const next = !open;
|
|
304
318
|
setOpen(next);
|
|
305
|
-
if (next) await
|
|
306
|
-
};
|
|
307
|
-
const onRefresh = async () => {
|
|
308
|
-
setRefreshing(true);
|
|
309
|
-
await refreshAll(false);
|
|
310
|
-
setRefreshing(false);
|
|
319
|
+
if (next) await refresh(true);
|
|
311
320
|
};
|
|
312
321
|
return (0, react_jsx_runtime.jsx)("div", {
|
|
313
322
|
ref: wrapRef,
|
|
314
|
-
className: styles.
|
|
315
|
-
children: (0, react_jsx_runtime.
|
|
316
|
-
children: [(0, react_jsx_runtime.
|
|
323
|
+
className: styles.sideWrap,
|
|
324
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
325
|
+
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
317
326
|
type: "button",
|
|
318
|
-
className: styles.
|
|
319
|
-
"
|
|
320
|
-
"aria-label": t("aria"),
|
|
321
|
-
title: balanceLevel > 0 ? t(`warn.title.${balanceLevel}`) : t("title"),
|
|
327
|
+
className: styles.side,
|
|
328
|
+
title: t("title"),
|
|
322
329
|
onClick: toggle,
|
|
323
|
-
children:
|
|
324
|
-
children:
|
|
325
|
-
|
|
330
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
331
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
332
|
+
className: styles.sideTop,
|
|
333
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
334
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
335
|
+
children: "💰"
|
|
336
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
337
|
+
className: styles.sideAmount,
|
|
338
|
+
"data-level": level > 0 ? level : undefined,
|
|
339
|
+
children: balance !== null ? `${balance.symbol}${balance.total.toFixed(2)}` : "—"
|
|
340
|
+
})]
|
|
341
|
+
})
|
|
342
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
343
|
+
className: styles.bar,
|
|
344
|
+
"aria-label": t("side.bar"),
|
|
345
|
+
children: (0, react_jsx_runtime.jsx)("div", {
|
|
346
|
+
className: styles.barFill,
|
|
347
|
+
"data-level": level,
|
|
348
|
+
style: { width: `${percent}%` }
|
|
349
|
+
})
|
|
350
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
351
|
+
className: styles.sideFoot,
|
|
352
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
353
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
354
|
+
children: t("todayCostShort")
|
|
355
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
356
|
+
children: todayAll !== null ? formatCny(todayAll) : "—"
|
|
357
|
+
})]
|
|
358
|
+
})
|
|
359
|
+
})]
|
|
360
|
+
})
|
|
326
361
|
}), open && (0, react_jsx_runtime.jsx)("div", {
|
|
327
|
-
className: styles.
|
|
362
|
+
className: styles.sidePop,
|
|
328
363
|
role: "dialog",
|
|
329
364
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
330
365
|
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
331
|
-
className: styles.
|
|
366
|
+
className: styles.sidePopHead,
|
|
332
367
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
333
368
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
334
|
-
className: styles.
|
|
369
|
+
className: styles.sidePopTitle,
|
|
335
370
|
children: t("title")
|
|
336
371
|
}), (0, react_jsx_runtime.jsx)("button", {
|
|
337
372
|
type: "button",
|
|
338
|
-
className: styles.
|
|
339
|
-
"aria-label": t("
|
|
340
|
-
|
|
341
|
-
"
|
|
342
|
-
disabled: refreshing,
|
|
343
|
-
onClick: onRefresh,
|
|
344
|
-
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, {})
|
|
373
|
+
className: styles.sidePopClose,
|
|
374
|
+
"aria-label": t("close"),
|
|
375
|
+
onClick: () => setOpen(false),
|
|
376
|
+
children: "×"
|
|
345
377
|
})]
|
|
346
378
|
})
|
|
347
379
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -357,28 +389,28 @@ window.__ModuleLoader__.load({
|
|
|
357
389
|
})
|
|
358
390
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
359
391
|
className: styles.val,
|
|
360
|
-
"data-level":
|
|
392
|
+
"data-level": level > 0 ? level : undefined,
|
|
361
393
|
children: loading ? "…" : balance !== null ? `${balance.symbol}${balance.total.toFixed(2)}` : "—"
|
|
362
394
|
})]
|
|
363
395
|
})
|
|
364
|
-
}),
|
|
396
|
+
}), level > 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
365
397
|
className: styles.err,
|
|
366
398
|
role: "status",
|
|
367
|
-
children: t(`warn.text.${
|
|
399
|
+
children: t(`warn.text.${level}`)
|
|
368
400
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
369
401
|
className: styles.row,
|
|
370
402
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
371
403
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
372
404
|
className: styles.label,
|
|
373
405
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
374
|
-
children: [t("
|
|
375
|
-
label: t("explain.
|
|
406
|
+
children: [t("lastPrompt"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
407
|
+
label: t("explain.lastPrompt"),
|
|
376
408
|
t
|
|
377
409
|
})]
|
|
378
410
|
})
|
|
379
411
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
380
412
|
className: styles.val,
|
|
381
|
-
children:
|
|
413
|
+
children: loading ? "…" : lastPrompt !== null ? formatCny(lastPrompt) : "—"
|
|
382
414
|
})]
|
|
383
415
|
})
|
|
384
416
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -387,14 +419,14 @@ window.__ModuleLoader__.load({
|
|
|
387
419
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
388
420
|
className: styles.label,
|
|
389
421
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
390
|
-
children: [t("
|
|
391
|
-
label: t("explain.
|
|
422
|
+
children: [t("sessionTotal"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
423
|
+
label: t("explain.sessionTotal"),
|
|
392
424
|
t
|
|
393
425
|
})]
|
|
394
426
|
})
|
|
395
427
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
396
428
|
className: styles.val,
|
|
397
|
-
children: loading ? "…" :
|
|
429
|
+
children: loading ? "…" : sessionTotal !== null ? formatCny(sessionTotal) : "—"
|
|
398
430
|
})]
|
|
399
431
|
})
|
|
400
432
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -403,30 +435,30 @@ window.__ModuleLoader__.load({
|
|
|
403
435
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
404
436
|
className: styles.label,
|
|
405
437
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
406
|
-
children: [t("
|
|
407
|
-
label: t("explain.
|
|
438
|
+
children: [t("todayWs"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
439
|
+
label: t("explain.todayWs"),
|
|
408
440
|
t
|
|
409
441
|
})]
|
|
410
442
|
})
|
|
411
443
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
412
444
|
className: styles.val,
|
|
413
|
-
children: loading ? "…" :
|
|
445
|
+
children: loading ? "…" : todayWs !== null ? formatCny(todayWs) : "—"
|
|
414
446
|
})]
|
|
415
447
|
})
|
|
416
|
-
}),
|
|
448
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
417
449
|
className: styles.row,
|
|
418
450
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
419
451
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
420
452
|
className: styles.label,
|
|
421
453
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
422
|
-
children: [t("
|
|
423
|
-
label: t("explain.
|
|
454
|
+
children: [t("todayAll"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
455
|
+
label: t("explain.todayAll"),
|
|
424
456
|
t
|
|
425
457
|
})]
|
|
426
458
|
})
|
|
427
459
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
428
460
|
className: styles.val,
|
|
429
|
-
children:
|
|
461
|
+
children: loading ? "…" : todayAll !== null ? formatCny(todayAll) : "—"
|
|
430
462
|
})]
|
|
431
463
|
})
|
|
432
464
|
}), error !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -434,24 +466,21 @@ window.__ModuleLoader__.load({
|
|
|
434
466
|
role: "status",
|
|
435
467
|
children: error
|
|
436
468
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
437
|
-
className: styles.
|
|
438
|
-
children: lastUpdated !== null ? `${t("updatedAt")} ${formatTime(lastUpdated)} · ${t("hint")}` : t("hint")
|
|
439
|
-
}), (0, react_jsx_runtime.jsx)("div", {
|
|
440
|
-
className: styles.foot,
|
|
469
|
+
className: styles.sidePopFoot,
|
|
441
470
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
442
471
|
children: [(0, react_jsx_runtime.jsx)("a", {
|
|
443
472
|
className: styles.link,
|
|
444
473
|
href: TOP_UP_URL,
|
|
445
474
|
target: "_blank",
|
|
446
475
|
rel: "noreferrer",
|
|
447
|
-
children: (
|
|
448
|
-
children: ["⚡", t("topUp")]
|
|
449
|
-
})
|
|
476
|
+
children: t("topUp")
|
|
450
477
|
}), (0, react_jsx_runtime.jsx)("button", {
|
|
451
478
|
type: "button",
|
|
452
|
-
className: styles.
|
|
453
|
-
|
|
454
|
-
|
|
479
|
+
className: styles.sidePopRefresh,
|
|
480
|
+
"aria-label": t("refresh.aria"),
|
|
481
|
+
title: t("refresh.title"),
|
|
482
|
+
onClick: () => refresh(true),
|
|
483
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, {})
|
|
455
484
|
})]
|
|
456
485
|
})
|
|
457
486
|
})]
|
|
@@ -461,78 +490,6 @@ window.__ModuleLoader__.load({
|
|
|
461
490
|
});
|
|
462
491
|
}
|
|
463
492
|
//#endregion
|
|
464
|
-
//#region lib/types/client/SidebarBalanceCard.js
|
|
465
|
-
/**
|
|
466
|
-
* Sidebar footer card: balance + remaining-ratio bar + today cost.
|
|
467
|
-
* Root-scoped (global, not per-session) — fetches balance itself and
|
|
468
|
-
* refreshes every 60s, matching the corner widget cadence.
|
|
469
|
-
* @param {Object} props - wide (sidebar expanded), t (locale).
|
|
470
|
-
*/
|
|
471
|
-
function SidebarBalanceCard({ wide, t }) {
|
|
472
|
-
const [balance, setBalance] = (0, react.useState)(null);
|
|
473
|
-
const [todayCost, setTodayCost] = (0, react.useState)(null);
|
|
474
|
-
const [lowThreshold, setLowThreshold] = (0, react.useState)(5);
|
|
475
|
-
const [criticalThreshold, setCriticalThreshold] = (0, react.useState)(1);
|
|
476
|
-
const refresh = async () => {
|
|
477
|
-
const [bResult, tResult] = await Promise.all([
|
|
478
|
-
fetchBalance(),
|
|
479
|
-
fetchCost(TODAY_COST_ROUTE)
|
|
480
|
-
]);
|
|
481
|
-
if (bResult.ok) {
|
|
482
|
-
setBalance(displayBalance(bResult.balanceInfos));
|
|
483
|
-
if (typeof bResult.lowThreshold === "number" && bResult.lowThreshold >= 0) setLowThreshold(bResult.lowThreshold);
|
|
484
|
-
if (typeof bResult.criticalThreshold === "number" && bResult.criticalThreshold >= 0) setCriticalThreshold(bResult.criticalThreshold);
|
|
485
|
-
}
|
|
486
|
-
if (tResult.ok) setTodayCost(tResult.cost);
|
|
487
|
-
};
|
|
488
|
-
(0, react.useEffect)(() => {
|
|
489
|
-
refresh();
|
|
490
|
-
const timer = setInterval(refresh, 60000);
|
|
491
|
-
return () => clearInterval(timer);
|
|
492
|
-
}, []);
|
|
493
|
-
const level = balance === null ? 0 : balance.total < criticalThreshold ? 2 : balance.total < lowThreshold ? 1 : 0;
|
|
494
|
-
// Remaining-ratio: 100% at lowThreshold (or above), 0% at 0.
|
|
495
|
-
let percent = 100;
|
|
496
|
-
if (balance !== null && balance.total < lowThreshold) {
|
|
497
|
-
percent = lowThreshold <= 0 ? 100 : Math.max(0, Math.round(balance.total / lowThreshold * 100));
|
|
498
|
-
}
|
|
499
|
-
return (0, react_jsx_runtime.jsx)("div", {
|
|
500
|
-
className: styles.side,
|
|
501
|
-
title: t("title"),
|
|
502
|
-
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
503
|
-
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
504
|
-
className: styles.sideTop,
|
|
505
|
-
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
506
|
-
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
507
|
-
children: "💰"
|
|
508
|
-
}), (0, react_jsx_runtime.jsx)("span", {
|
|
509
|
-
className: styles.sideAmount,
|
|
510
|
-
"data-level": level > 0 ? level : undefined,
|
|
511
|
-
children: balance !== null ? `${balance.symbol}${balance.total.toFixed(2)}` : "—"
|
|
512
|
-
})]
|
|
513
|
-
})
|
|
514
|
-
}), (0, react_jsx_runtime.jsx)("div", {
|
|
515
|
-
className: styles.bar,
|
|
516
|
-
"aria-label": t("side.bar"),
|
|
517
|
-
children: (0, react_jsx_runtime.jsx)("div", {
|
|
518
|
-
className: styles.barFill,
|
|
519
|
-
"data-level": level,
|
|
520
|
-
style: { width: `${percent}%` }
|
|
521
|
-
})
|
|
522
|
-
}), (0, react_jsx_runtime.jsx)("div", {
|
|
523
|
-
className: styles.sideFoot,
|
|
524
|
-
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
525
|
-
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
526
|
-
children: t("todayCostShort")
|
|
527
|
-
}), (0, react_jsx_runtime.jsx)("span", {
|
|
528
|
-
children: todayCost !== null ? formatCny(todayCost) : "—"
|
|
529
|
-
})]
|
|
530
|
-
})
|
|
531
|
-
})]
|
|
532
|
-
})
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
//#endregion
|
|
536
493
|
//#region lib/types/client/locales.js
|
|
537
494
|
/** Dictionary namespace owned by this plugin. */
|
|
538
495
|
const NS = "balance-widget";
|
|
@@ -543,21 +500,20 @@ window.__ModuleLoader__.load({
|
|
|
543
500
|
"refresh.aria": "刷新数据",
|
|
544
501
|
"refresh.title": "刷新余额与成本",
|
|
545
502
|
"balance": "账户余额",
|
|
546
|
-
"
|
|
547
|
-
"
|
|
548
|
-
"
|
|
549
|
-
"
|
|
503
|
+
"lastPrompt": "最近一次提问",
|
|
504
|
+
"sessionTotal": "本会话成本",
|
|
505
|
+
"todayWs": "今日·本项目",
|
|
506
|
+
"todayAll": "今日·全部",
|
|
550
507
|
"explain.aria": "解释",
|
|
551
|
-
"explain.balance": "DeepSeek
|
|
552
|
-
"explain.
|
|
553
|
-
"explain.
|
|
554
|
-
"explain.
|
|
555
|
-
"explain.
|
|
508
|
+
"explain.balance": "DeepSeek 账户可用余额,实时查询官方接口。",
|
|
509
|
+
"explain.lastPrompt": "最近活跃会话的最后一个问题(含 AI 回复)的费用估算。",
|
|
510
|
+
"explain.sessionTotal": "最近活跃会话从开始到现在的累计费用估算。",
|
|
511
|
+
"explain.todayWs": "当前工作区今天所有会话的费用合计估算。",
|
|
512
|
+
"explain.todayAll": "所有工作区今天所有会话的费用合计估算(含其他项目)。",
|
|
556
513
|
"warn.title.1": "余额不足 ¥5,建议及时充值",
|
|
557
514
|
"warn.title.2": "余额低于 ¥1,即将耗尽!",
|
|
558
515
|
"warn.text.1": "⚠️ 余额不足 ¥5,建议及时充值。",
|
|
559
516
|
"warn.text.2": "⛔ 余额低于 ¥1,即将耗尽,请立即充值!",
|
|
560
|
-
"updatedAt": "更新于",
|
|
561
517
|
"side.bar": "余额剩余比例",
|
|
562
518
|
"todayCostShort": "今日",
|
|
563
519
|
"hint": "价格为估算值,实际以官方账单为准。",
|
|
@@ -571,21 +527,20 @@ window.__ModuleLoader__.load({
|
|
|
571
527
|
"refresh.aria": "Refresh data",
|
|
572
528
|
"refresh.title": "Refresh balance and costs",
|
|
573
529
|
"balance": "Account balance",
|
|
574
|
-
"
|
|
575
|
-
"
|
|
576
|
-
"
|
|
577
|
-
"
|
|
530
|
+
"lastPrompt": "Last prompt",
|
|
531
|
+
"sessionTotal": "This session",
|
|
532
|
+
"todayWs": "Today · this project",
|
|
533
|
+
"todayAll": "Today · all",
|
|
578
534
|
"explain.aria": "Explain",
|
|
579
535
|
"explain.balance": "DeepSeek account balance, fetched live from the official API.",
|
|
580
|
-
"explain.
|
|
581
|
-
"explain.
|
|
582
|
-
"explain.
|
|
583
|
-
"explain.
|
|
536
|
+
"explain.lastPrompt": "Estimated cost of the latest question (incl. the AI reply) in the most recent session.",
|
|
537
|
+
"explain.sessionTotal": "Estimated total cost of the most recent session since it started.",
|
|
538
|
+
"explain.todayWs": "Estimated total cost of today's sessions in the current workspace.",
|
|
539
|
+
"explain.todayAll": "Estimated total cost of today's sessions across all workspaces.",
|
|
584
540
|
"warn.title.1": "Balance below ¥5, consider topping up",
|
|
585
541
|
"warn.title.2": "Balance below ¥1, almost exhausted!",
|
|
586
542
|
"warn.text.1": "⚠️ Balance below ¥5, consider topping up.",
|
|
587
543
|
"warn.text.2": "⛔ Balance below ¥1, almost exhausted — top up now!",
|
|
588
|
-
"updatedAt": "Updated",
|
|
589
544
|
"side.bar": "Balance remaining ratio",
|
|
590
545
|
"todayCostShort": "Today",
|
|
591
546
|
"hint": "Prices are estimates; actual billing from the provider is authoritative.",
|
|
@@ -600,8 +555,9 @@ window.__ModuleLoader__.load({
|
|
|
600
555
|
"locale"
|
|
601
556
|
];
|
|
602
557
|
/**
|
|
603
|
-
* Client plugin body: register the
|
|
604
|
-
*
|
|
558
|
+
* Client plugin body: register the sidebar footer balance card (the
|
|
559
|
+
* single entry point since v0.4.1; the former header widget is removed
|
|
560
|
+
* to avoid duplicate always-visible balances).
|
|
605
561
|
* @param ctx - client root context.
|
|
606
562
|
*/
|
|
607
563
|
function apply(ctx) {
|
|
@@ -609,13 +565,6 @@ window.__ModuleLoader__.load({
|
|
|
609
565
|
zh,
|
|
610
566
|
en
|
|
611
567
|
}), "dsh-balance-widget: dictionaries");
|
|
612
|
-
ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
|
|
613
|
-
name: "conversation.session.header.utilities",
|
|
614
|
-
id: "balance-widget",
|
|
615
|
-
order: 90,
|
|
616
|
-
locale: NS,
|
|
617
|
-
inject: (sessionId) => ({ sessionId })
|
|
618
|
-
}, BalanceWidget));
|
|
619
568
|
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
620
569
|
name: "sidebar.footer.action",
|
|
621
570
|
id: "balance-widget",
|
package/lib/index.js
CHANGED
|
@@ -24,7 +24,8 @@ export const API = {
|
|
|
24
24
|
balance: "/api/dsh-balance/balance",
|
|
25
25
|
cost: "/api/dsh-balance/cost",
|
|
26
26
|
lastCost: "/api/dsh-balance/last-cost",
|
|
27
|
-
todayCost: "/api/dsh-balance/today-cost"
|
|
27
|
+
todayCost: "/api/dsh-balance/today-cost",
|
|
28
|
+
activeCost: "/api/dsh-balance/active-cost"
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
/** Default DeepSeek API base (mirrors dsh-llm-deepseek's PUBLIC_BASE_URL). */
|
|
@@ -459,6 +460,7 @@ async function decompressSession(path) {
|
|
|
459
460
|
function parseSessionUsages(text) {
|
|
460
461
|
const samples = [];
|
|
461
462
|
let currentModel = void 0;
|
|
463
|
+
let cwd = void 0;
|
|
462
464
|
for (const line of text.split("\n")) {
|
|
463
465
|
if (line === "") continue;
|
|
464
466
|
let event;
|
|
@@ -468,7 +470,9 @@ function parseSessionUsages(text) {
|
|
|
468
470
|
continue;
|
|
469
471
|
}
|
|
470
472
|
const type = event.type;
|
|
471
|
-
if (type === "
|
|
473
|
+
if (type === "session") {
|
|
474
|
+
cwd = event.cwd;
|
|
475
|
+
} else if (type === "request/header") {
|
|
472
476
|
currentModel = event.data?.header?.config?.model ?? currentModel;
|
|
473
477
|
} else if (type === "assistant/message") {
|
|
474
478
|
const usage = event.data?.usage;
|
|
@@ -482,7 +486,7 @@ function parseSessionUsages(text) {
|
|
|
482
486
|
}
|
|
483
487
|
}
|
|
484
488
|
}
|
|
485
|
-
return samples;
|
|
489
|
+
return { samples, cwd };
|
|
486
490
|
}
|
|
487
491
|
|
|
488
492
|
/** Cost of the last turn (the most recent user prompt) in one session file. */
|
|
@@ -502,19 +506,71 @@ function lastTurnCost(samples) {
|
|
|
502
506
|
return { cost, inputTokens: input, outputTokens: output };
|
|
503
507
|
}
|
|
504
508
|
|
|
509
|
+
/** Total cost across every sample of one session (all turns). */
|
|
510
|
+
function sessionTotalCost(samples) {
|
|
511
|
+
let cost = 0;
|
|
512
|
+
let input = 0;
|
|
513
|
+
let output = 0;
|
|
514
|
+
for (const s of samples) {
|
|
515
|
+
cost += priceUsage(s.usage, s.model, s.time);
|
|
516
|
+
input += (s.usage.inputTokens ?? s.usage.uncachedInputTokens ?? 0) + (s.usage.cacheReadTokens ?? 0);
|
|
517
|
+
output += s.usage.outputTokens ?? 0;
|
|
518
|
+
}
|
|
519
|
+
return { cost, inputTokens: input, outputTokens: output };
|
|
520
|
+
}
|
|
521
|
+
|
|
505
522
|
/**
|
|
506
|
-
*
|
|
523
|
+
* Find the session file modified most recently (the "active" session).
|
|
524
|
+
* Walks the sessions tree and picks by mtime; returns the path or undefined.
|
|
525
|
+
*/
|
|
526
|
+
async function findMostRecentSessionFile() {
|
|
527
|
+
const root = sessionsRoot();
|
|
528
|
+
try {
|
|
529
|
+
const fs = await awaitImportFs();
|
|
530
|
+
const { statSync } = fs;
|
|
531
|
+
let best = void 0;
|
|
532
|
+
let bestMtime = -1;
|
|
533
|
+
const walk = (dir) => {
|
|
534
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
535
|
+
const full = `${dir}/${entry.name}`;
|
|
536
|
+
if (entry.isDirectory()) {
|
|
537
|
+
walk(full);
|
|
538
|
+
} else if (entry.name === "session.jsonl.zstd") {
|
|
539
|
+
try {
|
|
540
|
+
const mtime = statSync(full).mtimeMs;
|
|
541
|
+
if (mtime > bestMtime) {
|
|
542
|
+
bestMtime = mtime;
|
|
543
|
+
best = full;
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
/* ignore unreadable files */
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
walk(root);
|
|
552
|
+
return best;
|
|
553
|
+
} catch {
|
|
554
|
+
return void 0;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Sum today's usage across session files. "Today" = events whose time
|
|
507
560
|
* falls on the same calendar day (local time) as now.
|
|
561
|
+
* @param {Array<{samples:Array,cwd?:string}>} sessionList - parsed sessions.
|
|
562
|
+
* @param {string} [cwdFilter] - if given, only sessions with this cwd count.
|
|
508
563
|
*/
|
|
509
|
-
function todayCost(
|
|
564
|
+
function todayCost(sessionList, cwdFilter) {
|
|
510
565
|
const now = new Date();
|
|
511
566
|
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
512
567
|
const dayEnd = dayStart + 24 * 60 * 60 * 1000;
|
|
513
568
|
let cost = 0;
|
|
514
569
|
let input = 0;
|
|
515
570
|
let output = 0;
|
|
516
|
-
for (const
|
|
517
|
-
|
|
571
|
+
for (const session of sessionList) {
|
|
572
|
+
if (cwdFilter !== void 0 && session.cwd !== cwdFilter) continue;
|
|
573
|
+
for (const s of session.samples) {
|
|
518
574
|
if (s.time < dayStart || s.time >= dayEnd) continue;
|
|
519
575
|
cost += priceUsage(s.usage, s.model, s.time);
|
|
520
576
|
input += (s.usage.inputTokens ?? s.usage.uncachedInputTokens ?? 0) + (s.usage.cacheReadTokens ?? 0);
|
|
@@ -545,7 +601,8 @@ async function collectAllSessionSamples() {
|
|
|
545
601
|
for (const file of files) {
|
|
546
602
|
const result = await decompressSession(file);
|
|
547
603
|
if (!result.ok) continue;
|
|
548
|
-
|
|
604
|
+
const parsed = parseSessionUsages(result.text);
|
|
605
|
+
perSession.push(parsed);
|
|
549
606
|
}
|
|
550
607
|
return perSession;
|
|
551
608
|
}
|
|
@@ -622,8 +679,8 @@ function makeRoutes(ctx, config) {
|
|
|
622
679
|
const compute = async () => {
|
|
623
680
|
const result = await decompressSession(file);
|
|
624
681
|
if (!result.ok) return { error: result.error };
|
|
625
|
-
const
|
|
626
|
-
return { value: lastTurnCost(samples) };
|
|
682
|
+
const parsed = parseSessionUsages(result.text);
|
|
683
|
+
return { value: lastTurnCost(parsed.samples) };
|
|
627
684
|
};
|
|
628
685
|
const outcome = await dedupe(key, () => cachedOrCompute(key, compute));
|
|
629
686
|
if (outcome.error !== void 0) {
|
|
@@ -640,12 +697,63 @@ function makeRoutes(ctx, config) {
|
|
|
640
697
|
if (!guard(req, res, "GET")) return;
|
|
641
698
|
const key = "today";
|
|
642
699
|
const compute = async () => {
|
|
643
|
-
const
|
|
644
|
-
|
|
700
|
+
const sessionList = await collectAllSessionSamples();
|
|
701
|
+
const all = todayCost(sessionList);
|
|
702
|
+
// "Current workspace": the cwd of the session with the most
|
|
703
|
+
// samples today (best proxy for where the user is working).
|
|
704
|
+
let activeCwd = void 0;
|
|
705
|
+
let activeCount = 0;
|
|
706
|
+
for (const session of sessionList) {
|
|
707
|
+
if (session.cwd === void 0) continue;
|
|
708
|
+
const todaySamples = session.samples.filter((s) => {
|
|
709
|
+
const now = new Date();
|
|
710
|
+
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
711
|
+
return s.time >= dayStart && s.time < dayStart + 24 * 60 * 60 * 1000;
|
|
712
|
+
}).length;
|
|
713
|
+
if (todaySamples > activeCount) {
|
|
714
|
+
activeCount = todaySamples;
|
|
715
|
+
activeCwd = session.cwd;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const workspace = todayCost(sessionList, activeCwd);
|
|
719
|
+
return {
|
|
720
|
+
value: {
|
|
721
|
+
workspace: { ...workspace, cwd: activeCwd },
|
|
722
|
+
all
|
|
723
|
+
}
|
|
724
|
+
};
|
|
645
725
|
};
|
|
646
726
|
const outcome = await dedupe(key, () => cachedOrCompute(key, compute));
|
|
647
727
|
writeJson(res, 200, { ...outcome.value, modelId: config.modelId, pricingSource, pricingSyncedAt });
|
|
648
728
|
}
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
kind: "exact",
|
|
732
|
+
path: API.activeCost,
|
|
733
|
+
handler: async (req, res) => {
|
|
734
|
+
if (!guard(req, res, "GET")) return;
|
|
735
|
+
const file = await findMostRecentSessionFile();
|
|
736
|
+
if (file === void 0) {
|
|
737
|
+
writeJson(res, 404, { error: "no session files found", modelId: config.modelId });
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const result = await decompressSession(file);
|
|
741
|
+
if (!result.ok) {
|
|
742
|
+
writeJson(res, 502, { error: result.error, modelId: config.modelId });
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const parsed = parseSessionUsages(result.text);
|
|
746
|
+
const last = lastTurnCost(parsed.samples);
|
|
747
|
+
const total = sessionTotalCost(parsed.samples);
|
|
748
|
+
writeJson(res, 200, {
|
|
749
|
+
lastPrompt: last,
|
|
750
|
+
sessionTotal: total,
|
|
751
|
+
cwd: parsed.cwd,
|
|
752
|
+
modelId: config.modelId,
|
|
753
|
+
pricingSource,
|
|
754
|
+
pricingSyncedAt
|
|
755
|
+
});
|
|
756
|
+
}
|
|
649
757
|
}
|
|
650
758
|
];
|
|
651
759
|
}
|
|
@@ -689,8 +797,8 @@ function buildBillingTool(ctx, config) {
|
|
|
689
797
|
}
|
|
690
798
|
if (query === "cost" || query === "both") {
|
|
691
799
|
try {
|
|
692
|
-
const
|
|
693
|
-
const today = todayCost(
|
|
800
|
+
const sessionList = await collectAllSessionSamples();
|
|
801
|
+
const today = todayCost(sessionList);
|
|
694
802
|
parts.push(`Today's estimated cost: CNY ${today.cost.toFixed(4)} (${today.inputTokens} input tokens, ${today.outputTokens} output tokens)`);
|
|
695
803
|
} catch (error) {
|
|
696
804
|
parts.push(`Cost query failed: ${error instanceof Error ? error.message : String(error)}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-balance-widget",
|
|
3
3
|
"description": "Balance & cost widget for the dsh web GUI: a corner icon in the conversation session header that shows the DeepSeek account balance and the current session's estimated spend on click. Host half proxies the official /user/balance API over ctx.webServer (loopback-only); client half renders the icon and popover. Zero external dependencies — works on Node 24.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"files": [
|