dsh-balance-widget 0.4.0 → 0.4.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/lib/client.js +93 -284
- package/package.json +1 -1
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
|
|
@@ -186,99 +193,48 @@ window.__ModuleLoader__.load({
|
|
|
186
193
|
return { symbol, total, currency: entry.currency };
|
|
187
194
|
}
|
|
188
195
|
//#endregion
|
|
189
|
-
//#region lib/types/client/
|
|
190
|
-
/**
|
|
191
|
-
* A small ⓘ marker with a hover tooltip explaining a term. Wraps the
|
|
192
|
-
* official DSH Tooltip so the popover stays visually consistent.
|
|
193
|
-
* @param {Object} props - label (tooltip text), t (locale).
|
|
194
|
-
*/
|
|
195
|
-
function ExplainIcon({ label, t }) {
|
|
196
|
-
return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
197
|
-
label: label,
|
|
198
|
-
side: "top",
|
|
199
|
-
delayMs: 300,
|
|
200
|
-
children: (0, react_jsx_runtime.jsx)("span", {
|
|
201
|
-
role: "img",
|
|
202
|
-
"aria-label": t("explain.aria"),
|
|
203
|
-
style: { cursor: "help", color: "var(--dsw-alias-label-tertiary)", marginLeft: 3, fontSize: 11, lineHeight: 1 },
|
|
204
|
-
children: (0, react_jsx_runtime.jsx)("span", {
|
|
205
|
-
children: "ⓘ"
|
|
206
|
-
})
|
|
207
|
-
})
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
//#endregion
|
|
211
|
-
//#region lib/types/client/BalanceWidget.js
|
|
196
|
+
//#region lib/types/client/SidebarBalanceCard.js
|
|
212
197
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
198
|
+
* Sidebar footer card: balance + remaining-ratio bar + today cost.
|
|
199
|
+
* Root-scoped (global, not per-session) — fetches balance itself and
|
|
200
|
+
* refreshes every 60s. Clicking opens a detail popover (balance, today
|
|
201
|
+
* cost, top-up, refresh) using only global data.
|
|
202
|
+
* @param {Object} props - wide (sidebar expanded), t (locale).
|
|
217
203
|
*/
|
|
218
|
-
function
|
|
204
|
+
function SidebarBalanceCard({ wide, t }) {
|
|
219
205
|
const [open, setOpen] = (0, react.useState)(false);
|
|
220
206
|
const [balance, setBalance] = (0, react.useState)(null);
|
|
221
|
-
const [modelId, setModelId] = (0, react.useState)("deepseek-v4-flash");
|
|
222
|
-
const [lastCost, setLastCost] = (0, react.useState)(null);
|
|
223
207
|
const [todayCost, setTodayCost] = (0, react.useState)(null);
|
|
224
|
-
const [error, setError] = (0, react.useState)(null);
|
|
225
|
-
const [loading, setLoading] = (0, react.useState)(false);
|
|
226
|
-
const [refreshing, setRefreshing] = (0, react.useState)(false);
|
|
227
|
-
const [lastUpdated, setLastUpdated] = (0, react.useState)(null);
|
|
228
208
|
const [lowThreshold, setLowThreshold] = (0, react.useState)(5);
|
|
229
209
|
const [criticalThreshold, setCriticalThreshold] = (0, react.useState)(1);
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
const balanceLevel = balance === null ? 0 : balance.total < criticalThreshold ? 2 : balance.total < lowThreshold ? 1 : 0;
|
|
234
|
-
/** Fetch balance only (used for the always-visible corner amount). */
|
|
235
|
-
const refreshBalance = async () => {
|
|
236
|
-
const result = await fetchBalance();
|
|
237
|
-
if (result.ok) {
|
|
238
|
-
setBalance(displayBalance(result.balanceInfos));
|
|
239
|
-
setLastUpdated(Date.now());
|
|
240
|
-
if (typeof result.modelId === "string" && result.modelId !== "") setModelId(result.modelId);
|
|
241
|
-
if (typeof result.lowThreshold === "number" && result.lowThreshold >= 0) setLowThreshold(result.lowThreshold);
|
|
242
|
-
if (typeof result.criticalThreshold === "number" && result.criticalThreshold >= 0) setCriticalThreshold(result.criticalThreshold);
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
/** Fetch all data; optionally keeps the popover's silent loading state. */
|
|
246
|
-
const refreshAll = async (showLoading) => {
|
|
210
|
+
const [error, setError] = (0, react.useState)(null);
|
|
211
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
212
|
+
const refresh = async (showLoading) => {
|
|
247
213
|
if (showLoading) setLoading(true);
|
|
248
214
|
setError(null);
|
|
249
|
-
const [
|
|
215
|
+
const [bResult, tResult] = await Promise.all([
|
|
250
216
|
fetchBalance(),
|
|
251
|
-
fetchCost(LAST_COST_ROUTE, sessionId),
|
|
252
217
|
fetchCost(TODAY_COST_ROUTE)
|
|
253
218
|
]);
|
|
254
|
-
// Collect every failure into one fresh message; never accumulate
|
|
255
|
-
// across refreshes (each refresh starts from a clean slate).
|
|
256
219
|
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);
|
|
220
|
+
if (bResult.ok) {
|
|
221
|
+
setBalance(displayBalance(bResult.balanceInfos));
|
|
222
|
+
if (typeof bResult.lowThreshold === "number" && bResult.lowThreshold >= 0) setLowThreshold(bResult.lowThreshold);
|
|
223
|
+
if (typeof bResult.criticalThreshold === "number" && bResult.criticalThreshold >= 0) setCriticalThreshold(bResult.criticalThreshold);
|
|
263
224
|
} else {
|
|
264
|
-
|
|
265
|
-
failures.push(`余额: ${balanceResult.error}`);
|
|
225
|
+
failures.push(`余额: ${bResult.error}`);
|
|
266
226
|
}
|
|
267
|
-
if (
|
|
268
|
-
else if (
|
|
269
|
-
if (todayResult.ok) setTodayCost(todayResult.cost);
|
|
270
|
-
else if (todayResult.error) failures.push(`今日: ${todayResult.error}`);
|
|
227
|
+
if (tResult.ok) setTodayCost(tResult.cost);
|
|
228
|
+
else if (tResult.error) failures.push(`今日: ${tResult.error}`);
|
|
271
229
|
setError(failures.length > 0 ? failures.join(";") : null);
|
|
272
230
|
if (showLoading) setLoading(false);
|
|
273
231
|
};
|
|
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
232
|
(0, react.useEffect)(() => {
|
|
277
|
-
|
|
278
|
-
const timer = setInterval(
|
|
233
|
+
refresh(false);
|
|
234
|
+
const timer = setInterval(() => refresh(false), 60000);
|
|
279
235
|
return () => clearInterval(timer);
|
|
280
236
|
}, []);
|
|
281
|
-
// Close the popover when clicking
|
|
237
|
+
// Close the popover when clicking outside the card.
|
|
282
238
|
const wrapRef = (0, react.useRef)(null);
|
|
283
239
|
(0, react.useEffect)(() => {
|
|
284
240
|
if (!open) return;
|
|
@@ -289,144 +245,98 @@ window.__ModuleLoader__.load({
|
|
|
289
245
|
document.addEventListener("mousedown", onDocClick);
|
|
290
246
|
return () => document.removeEventListener("mousedown", onDocClick);
|
|
291
247
|
}, [open]);
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
refreshAll(false);
|
|
299
|
-
}, 30000);
|
|
300
|
-
return () => clearInterval(timer);
|
|
301
|
-
}, [open]);
|
|
248
|
+
const level = balance === null ? 0 : balance.total < criticalThreshold ? 2 : balance.total < lowThreshold ? 1 : 0;
|
|
249
|
+
// Remaining-ratio: 100% at lowThreshold (or above), 0% at 0.
|
|
250
|
+
let percent = 100;
|
|
251
|
+
if (balance !== null && balance.total < lowThreshold) {
|
|
252
|
+
percent = lowThreshold <= 0 ? 100 : Math.max(0, Math.round(balance.total / lowThreshold * 100));
|
|
253
|
+
}
|
|
302
254
|
const toggle = async () => {
|
|
303
255
|
const next = !open;
|
|
304
256
|
setOpen(next);
|
|
305
|
-
if (next) await
|
|
306
|
-
};
|
|
307
|
-
const onRefresh = async () => {
|
|
308
|
-
setRefreshing(true);
|
|
309
|
-
await refreshAll(false);
|
|
310
|
-
setRefreshing(false);
|
|
257
|
+
if (next) await refresh(true);
|
|
311
258
|
};
|
|
312
259
|
return (0, react_jsx_runtime.jsx)("div", {
|
|
313
260
|
ref: wrapRef,
|
|
314
|
-
className: styles.
|
|
315
|
-
children: (0, react_jsx_runtime.
|
|
316
|
-
children: [(0, react_jsx_runtime.
|
|
261
|
+
className: styles.sideWrap,
|
|
262
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
263
|
+
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
317
264
|
type: "button",
|
|
318
|
-
className: styles.
|
|
319
|
-
"
|
|
320
|
-
"aria-label": t("aria"),
|
|
321
|
-
title: balanceLevel > 0 ? t(`warn.title.${balanceLevel}`) : t("title"),
|
|
265
|
+
className: styles.side,
|
|
266
|
+
title: t("title"),
|
|
322
267
|
onClick: toggle,
|
|
323
|
-
children: ["💰", balance !== null && (0, react_jsx_runtime.jsx)("span", {
|
|
324
|
-
children: `${balance.symbol}${balance.total.toFixed(2)}`
|
|
325
|
-
})]
|
|
326
|
-
}), open && (0, react_jsx_runtime.jsx)("div", {
|
|
327
|
-
className: styles.pop,
|
|
328
|
-
role: "dialog",
|
|
329
268
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
330
269
|
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
331
|
-
className: styles.
|
|
270
|
+
className: styles.sideTop,
|
|
332
271
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
333
272
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
"aria-label": t("refresh.aria"),
|
|
340
|
-
title: t("refresh.title"),
|
|
341
|
-
"data-spinning": refreshing || undefined,
|
|
342
|
-
disabled: refreshing,
|
|
343
|
-
onClick: onRefresh,
|
|
344
|
-
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, {})
|
|
273
|
+
children: "💰"
|
|
274
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
275
|
+
className: styles.sideAmount,
|
|
276
|
+
"data-level": level > 0 ? level : undefined,
|
|
277
|
+
children: balance !== null ? `${balance.symbol}${balance.total.toFixed(2)}` : "—"
|
|
345
278
|
})]
|
|
346
279
|
})
|
|
347
280
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
348
|
-
className: styles.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
label: t("explain.balance"),
|
|
355
|
-
t
|
|
356
|
-
})]
|
|
357
|
-
})
|
|
358
|
-
}), (0, react_jsx_runtime.jsx)("span", {
|
|
359
|
-
className: styles.val,
|
|
360
|
-
"data-level": balanceLevel > 0 ? balanceLevel : undefined,
|
|
361
|
-
children: loading ? "…" : balance !== null ? `${balance.symbol}${balance.total.toFixed(2)}` : "—"
|
|
362
|
-
})]
|
|
281
|
+
className: styles.bar,
|
|
282
|
+
"aria-label": t("side.bar"),
|
|
283
|
+
children: (0, react_jsx_runtime.jsx)("div", {
|
|
284
|
+
className: styles.barFill,
|
|
285
|
+
"data-level": level,
|
|
286
|
+
style: { width: `${percent}%` }
|
|
363
287
|
})
|
|
364
|
-
}), balanceLevel > 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
365
|
-
className: styles.err,
|
|
366
|
-
role: "status",
|
|
367
|
-
children: t(`warn.text.${balanceLevel}`)
|
|
368
288
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
369
|
-
className: styles.
|
|
289
|
+
className: styles.sideFoot,
|
|
370
290
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
371
291
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
372
|
-
|
|
373
|
-
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
374
|
-
children: [t("sessionCost"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
375
|
-
label: t("explain.sessionCost"),
|
|
376
|
-
t
|
|
377
|
-
})]
|
|
378
|
-
})
|
|
292
|
+
children: t("todayCostShort")
|
|
379
293
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
380
|
-
|
|
381
|
-
children: priced !== null ? formatCny(priced.total) : "—"
|
|
294
|
+
children: todayCost !== null ? formatCny(todayCost) : "—"
|
|
382
295
|
})]
|
|
383
296
|
})
|
|
384
|
-
})
|
|
385
|
-
|
|
297
|
+
})]
|
|
298
|
+
})
|
|
299
|
+
}), open && (0, react_jsx_runtime.jsx)("div", {
|
|
300
|
+
className: styles.sidePop,
|
|
301
|
+
role: "dialog",
|
|
302
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
303
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
304
|
+
className: styles.sidePopHead,
|
|
386
305
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
387
306
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
388
|
-
className: styles.
|
|
389
|
-
children: (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
className: styles.val,
|
|
397
|
-
children: loading ? "…" : lastCost !== null ? formatCny(lastCost) : "—"
|
|
307
|
+
className: styles.sidePopTitle,
|
|
308
|
+
children: t("title")
|
|
309
|
+
}), (0, react_jsx_runtime.jsx)("button", {
|
|
310
|
+
type: "button",
|
|
311
|
+
className: styles.sidePopClose,
|
|
312
|
+
"aria-label": t("close"),
|
|
313
|
+
onClick: () => setOpen(false),
|
|
314
|
+
children: "×"
|
|
398
315
|
})]
|
|
399
316
|
})
|
|
400
317
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
401
318
|
className: styles.row,
|
|
402
319
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
403
320
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
404
|
-
|
|
405
|
-
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
406
|
-
children: [t("todayCost"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
407
|
-
label: t("explain.todayCost"),
|
|
408
|
-
t
|
|
409
|
-
})]
|
|
410
|
-
})
|
|
321
|
+
children: t("balance")
|
|
411
322
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
412
323
|
className: styles.val,
|
|
413
|
-
|
|
324
|
+
"data-level": level > 0 ? level : undefined,
|
|
325
|
+
children: loading ? "…" : balance !== null ? `${balance.symbol}${balance.total.toFixed(2)}` : "—"
|
|
414
326
|
})]
|
|
415
327
|
})
|
|
416
|
-
}),
|
|
328
|
+
}), level > 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
329
|
+
className: styles.err,
|
|
330
|
+
role: "status",
|
|
331
|
+
children: t(`warn.text.${level}`)
|
|
332
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
417
333
|
className: styles.row,
|
|
418
334
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
419
335
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
420
|
-
|
|
421
|
-
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
422
|
-
children: [t("tokens"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
423
|
-
label: t("explain.tokens"),
|
|
424
|
-
t
|
|
425
|
-
})]
|
|
426
|
-
})
|
|
336
|
+
children: t("todayCost")
|
|
427
337
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
428
338
|
className: styles.val,
|
|
429
|
-
children:
|
|
339
|
+
children: loading ? "…" : todayCost !== null ? formatCny(todayCost) : "—"
|
|
430
340
|
})]
|
|
431
341
|
})
|
|
432
342
|
}), error !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -434,24 +344,21 @@ window.__ModuleLoader__.load({
|
|
|
434
344
|
role: "status",
|
|
435
345
|
children: error
|
|
436
346
|
}), (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,
|
|
347
|
+
className: styles.sidePopFoot,
|
|
441
348
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
442
349
|
children: [(0, react_jsx_runtime.jsx)("a", {
|
|
443
350
|
className: styles.link,
|
|
444
351
|
href: TOP_UP_URL,
|
|
445
352
|
target: "_blank",
|
|
446
353
|
rel: "noreferrer",
|
|
447
|
-
children: (
|
|
448
|
-
children: ["⚡", t("topUp")]
|
|
449
|
-
})
|
|
354
|
+
children: t("topUp")
|
|
450
355
|
}), (0, react_jsx_runtime.jsx)("button", {
|
|
451
356
|
type: "button",
|
|
452
|
-
className: styles.
|
|
453
|
-
|
|
454
|
-
|
|
357
|
+
className: styles.sidePopRefresh,
|
|
358
|
+
"aria-label": t("refresh.aria"),
|
|
359
|
+
title: t("refresh.title"),
|
|
360
|
+
onClick: () => refresh(true),
|
|
361
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, {})
|
|
455
362
|
})]
|
|
456
363
|
})
|
|
457
364
|
})]
|
|
@@ -461,78 +368,6 @@ window.__ModuleLoader__.load({
|
|
|
461
368
|
});
|
|
462
369
|
}
|
|
463
370
|
//#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
371
|
//#region lib/types/client/locales.js
|
|
537
372
|
/** Dictionary namespace owned by this plugin. */
|
|
538
373
|
const NS = "balance-widget";
|
|
@@ -543,21 +378,11 @@ window.__ModuleLoader__.load({
|
|
|
543
378
|
"refresh.aria": "刷新数据",
|
|
544
379
|
"refresh.title": "刷新余额与成本",
|
|
545
380
|
"balance": "账户余额",
|
|
546
|
-
"sessionCost": "这个对话已花费",
|
|
547
|
-
"lastPromptCost": "最近一次提问",
|
|
548
381
|
"todayCost": "今天总成本",
|
|
549
|
-
"tokens": "Token 用量",
|
|
550
|
-
"explain.aria": "解释",
|
|
551
|
-
"explain.balance": "DeepSeek 账户可用余额,实时查询官方接口(每 60 秒刷新)。",
|
|
552
|
-
"explain.sessionCost": "当前这个对话从开始到现在累计的费用,按官方峰谷定价估算。",
|
|
553
|
-
"explain.lastPromptCost": "你刚发的这条提问(含 AI 回复)的费用估算。",
|
|
554
|
-
"explain.todayCost": "今天所有对话(含其他窗口)的费用合计估算。",
|
|
555
|
-
"explain.tokens": "本会话消耗的 token 数。1 token ≈ 0.75 个英文单词 ≈ 1 个汉字。",
|
|
556
382
|
"warn.title.1": "余额不足 ¥5,建议及时充值",
|
|
557
383
|
"warn.title.2": "余额低于 ¥1,即将耗尽!",
|
|
558
384
|
"warn.text.1": "⚠️ 余额不足 ¥5,建议及时充值。",
|
|
559
385
|
"warn.text.2": "⛔ 余额低于 ¥1,即将耗尽,请立即充值!",
|
|
560
|
-
"updatedAt": "更新于",
|
|
561
386
|
"side.bar": "余额剩余比例",
|
|
562
387
|
"todayCostShort": "今日",
|
|
563
388
|
"hint": "价格为估算值,实际以官方账单为准。",
|
|
@@ -571,21 +396,11 @@ window.__ModuleLoader__.load({
|
|
|
571
396
|
"refresh.aria": "Refresh data",
|
|
572
397
|
"refresh.title": "Refresh balance and costs",
|
|
573
398
|
"balance": "Account balance",
|
|
574
|
-
"sessionCost": "This chat spent",
|
|
575
|
-
"lastPromptCost": "Last prompt",
|
|
576
399
|
"todayCost": "Today total",
|
|
577
|
-
"tokens": "Token usage",
|
|
578
|
-
"explain.aria": "Explain",
|
|
579
|
-
"explain.balance": "DeepSeek account balance, fetched live from the official API.",
|
|
580
|
-
"explain.sessionCost": "Total estimated cost of this conversation since it started, priced at official peak/off-peak rates.",
|
|
581
|
-
"explain.lastPromptCost": "Estimated cost of your latest prompt, including the AI reply.",
|
|
582
|
-
"explain.todayCost": "Estimated total cost across all conversations today.",
|
|
583
|
-
"explain.tokens": "Tokens used by this session. 1 token ≈ 0.75 English words ≈ 1 CJK character.",
|
|
584
400
|
"warn.title.1": "Balance below ¥5, consider topping up",
|
|
585
401
|
"warn.title.2": "Balance below ¥1, almost exhausted!",
|
|
586
402
|
"warn.text.1": "⚠️ Balance below ¥5, consider topping up.",
|
|
587
403
|
"warn.text.2": "⛔ Balance below ¥1, almost exhausted — top up now!",
|
|
588
|
-
"updatedAt": "Updated",
|
|
589
404
|
"side.bar": "Balance remaining ratio",
|
|
590
405
|
"todayCostShort": "Today",
|
|
591
406
|
"hint": "Prices are estimates; actual billing from the provider is authoritative.",
|
|
@@ -600,8 +415,9 @@ window.__ModuleLoader__.load({
|
|
|
600
415
|
"locale"
|
|
601
416
|
];
|
|
602
417
|
/**
|
|
603
|
-
* Client plugin body: register the
|
|
604
|
-
*
|
|
418
|
+
* Client plugin body: register the sidebar footer balance card (the
|
|
419
|
+
* single entry point since v0.4.1; the former header widget is removed
|
|
420
|
+
* to avoid duplicate always-visible balances).
|
|
605
421
|
* @param ctx - client root context.
|
|
606
422
|
*/
|
|
607
423
|
function apply(ctx) {
|
|
@@ -609,13 +425,6 @@ window.__ModuleLoader__.load({
|
|
|
609
425
|
zh,
|
|
610
426
|
en
|
|
611
427
|
}), "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
428
|
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
620
429
|
name: "sidebar.footer.action",
|
|
621
430
|
id: "balance-widget",
|
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.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"files": [
|