dsh-api-dashboard 1.4.0 → 1.4.2
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 +83 -296
- package/client/client.js +142 -32
- package/package.json +3 -3
- package/src/index.js +280 -30
package/client/client.js
CHANGED
|
@@ -191,6 +191,11 @@ window.__ModuleLoader__.load({
|
|
|
191
191
|
.dshadb_bar_logo{width:16px;height:16px;border-radius:5px;background:#eef2ff;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:800;color:#4f7cff}
|
|
192
192
|
.dshadb_bar_name{font-weight:700;color:#17181c;font-size:11px}
|
|
193
193
|
.dshadb_bar_amount{font-weight:800;font-variant-numeric:tabular-nums;color:#17181c}
|
|
194
|
+
/* C-4a (v1.4.1): .dshadb_bar_ok/warn/err 以前只有使用处、没有 CSS 规则(死类名),
|
|
195
|
+
于是"负余额/低余额金额变红"从来没生效过, 只有小圆点变色。 */
|
|
196
|
+
.dshadb_bar_ok{color:#2e9e5b}
|
|
197
|
+
.dshadb_bar_warn{color:#c8891a}
|
|
198
|
+
.dshadb_bar_err{color:#d8443f}
|
|
194
199
|
.dshadb_bar_dot{width:6px;height:6px;border-radius:50%;flex:none}
|
|
195
200
|
.dshadb_bar_dot_ok{background:#35b56b}
|
|
196
201
|
.dshadb_bar_dot_warn{background:#e6a72f}
|
|
@@ -367,9 +372,15 @@ window.__ModuleLoader__.load({
|
|
|
367
372
|
见 bindSubagentTip()。浮层必须挂在 document.body 上 —— 挂在胶囊里会被
|
|
368
373
|
.dshadb_subs 的 overflow-x:auto 裁掉。
|
|
369
374
|
⚠️ 本段在 JS 模板字符串里, 注释里**不能出现反引号**(会提前结束模板串)。 */
|
|
370
|
-
.
|
|
371
|
-
|
|
372
|
-
|
|
375
|
+
/* UI v1.4.1(按维护者反馈改):**不再把两行塞进一个容器** —— 余额条恢复成独立的药丸,
|
|
376
|
+
整块(余额条 + 子代理行)**水平居中**:wrap 用 fit-content + margin:auto,
|
|
377
|
+
子项 align-items:center,于是窄的时候两行都居中,宽的时候 wrap 撑到 100% 子代理行在内部横滑。 */
|
|
378
|
+
.dshadb_barwrap{display:flex;flex-direction:column;align-items:center;gap:0;width:fit-content;max-width:100%;min-width:0;margin:0 auto}
|
|
379
|
+
.dshadb_barrow{display:inline-flex;align-items:center;gap:2px;max-width:100%;min-width:0}
|
|
380
|
+
.dshadb_subs{display:flex;flex-wrap:nowrap;align-items:center;gap:4px;margin:3px 0 0 0;max-width:100%;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch}
|
|
381
|
+
/* UI v1.4.1: 子代理行只在**真的溢出**时右侧渐隐(由 syncSubsOverflow 打类名),
|
|
382
|
+
这样"被切一半"看起来是有意的可滑动提示, 而不是被屏幕齐口截断。 */
|
|
383
|
+
.dshadb_subs_overflow{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 20px),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 20px),transparent)}
|
|
373
384
|
.dshadb_subs::-webkit-scrollbar{display:none}
|
|
374
385
|
.dshadb_sub{flex:none;display:inline-flex;align-items:center;gap:4px;font-size:10px;line-height:1.15;padding:2px 7px;border-radius:8px;background:#f5f6f8;border:1px solid #e7e8ec;color:#777b84;white-space:nowrap;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
|
|
375
386
|
.dshadb_sub_dot{width:5px;height:5px;border-radius:50%;background:#8b93a7;flex:none}
|
|
@@ -625,6 +636,7 @@ window.__ModuleLoader__.load({
|
|
|
625
636
|
let snapshot = { status: "loading", balances: [], config: { safeThreshold: 50, warnThreshold: 10 } };
|
|
626
637
|
const listeners = new Set();
|
|
627
638
|
let timer = null, pollMs = DEFAULT_POLL_MS, inflight = null, started = false;
|
|
639
|
+
let lastForceAt = 0, seq = 0, appliedSeq = 0;
|
|
628
640
|
function notify() { for (const fn of [...listeners]) fn(); }
|
|
629
641
|
/**
|
|
630
642
|
* @param force 显式强刷 (手动刷新按钮 / 保存设置后) —— **阻塞**, 等服务端拉完最新数据
|
|
@@ -632,8 +644,36 @@ window.__ModuleLoader__.load({
|
|
|
632
644
|
* 刷新丢后台。force 那条路要等服务端跑完一次全量轮询(最慢端点可到 8s),
|
|
633
645
|
* 应用切回前台时用它就会「等一段时间」。
|
|
634
646
|
*/
|
|
647
|
+
/**
|
|
648
|
+
* C-3a (v1.4.1): fetch 必须带超时。
|
|
649
|
+
* 以前整份客户端一个 AbortController 都没有 —— 请求一旦既不 resolve 也不 reject
|
|
650
|
+
* (弱网黑洞、WebView 被冻结、服务端被最慢端点拖住), inflight 永远不为 null,
|
|
651
|
+
* schedule() 又只在 promise settle 后才续期 → 插件永久停在 4 条骨架屏, 手动刷新也救不回来。
|
|
652
|
+
*/
|
|
653
|
+
const FETCH_TIMEOUT_MS = 15000;
|
|
654
|
+
/**
|
|
655
|
+
* ⚠️ 余额端点的超时**必须远大于**其他端点。
|
|
656
|
+
* 实测(维护者真实配置:5 个中转站 + 16 个预设平台):服务端一次全量刷新要 **8~13.6 秒** ——
|
|
657
|
+
* `queryCustomRelay` 的 auto 探测是**串行**试 3 个候选端点(billing/subscription → api/user/self
|
|
658
|
+
* → credit_grants),每个都要跑到 `timeoutMs`(默认 8s) 才放弃;冷启动还要叠加 DNS/TLS 首次握手。
|
|
659
|
+
* v1.4.1 第一版把这里也写成 15s,结果**冷启动首屏必超时** → 面板直接显示「余额接口请求失败」。
|
|
660
|
+
* 超时的意义只是"别让一个彻底卡死的 socket 永远挂着",不是给正常慢启动设上限。
|
|
661
|
+
*/
|
|
662
|
+
const BALANCES_TIMEOUT_MS = 90000;
|
|
663
|
+
const fetchT = (url, opts, ms) => {
|
|
664
|
+
const timeout = typeof ms === "number" ? ms : FETCH_TIMEOUT_MS;
|
|
665
|
+
try { return fetch(url, { ...(opts || {}), signal: AbortSignal.timeout(timeout) }); }
|
|
666
|
+
catch (e) { return fetch(url, opts); }
|
|
667
|
+
};
|
|
635
668
|
async function refresh(force = false, peek = false) {
|
|
636
669
|
if (inflight !== null && !force && !peek) return inflight;
|
|
670
|
+
// C-4c (v1.4.1): 强刷节流 2s(AGENTS 里写了「强刷 2 秒节流」, 但代码里一直没有);
|
|
671
|
+
// 并且在飞时直接复用同一个请求, 避免连点刷新打出一串并发。
|
|
672
|
+
if (force) {
|
|
673
|
+
const now = Date.now();
|
|
674
|
+
if (inflight !== null && now - lastForceAt < 2000) return inflight;
|
|
675
|
+
lastForceAt = now;
|
|
676
|
+
}
|
|
637
677
|
if ((force || peek) && snapshot.isRefreshing !== true) { snapshot = { ...snapshot, isRefreshing: true }; notify(); }
|
|
638
678
|
inflight = (async () => {
|
|
639
679
|
try {
|
|
@@ -641,16 +681,26 @@ window.__ModuleLoader__.load({
|
|
|
641
681
|
: force ? "/api-dashboard/balances?force=1&_t=" + Date.now()
|
|
642
682
|
: "/api-dashboard/balances";
|
|
643
683
|
// v0.5.0: 走浏览器默认缓存 (配合服务端 ETag), 数据没变时 304 空响应; force 用独立 URL + no-store 强刷
|
|
644
|
-
const
|
|
684
|
+
const mySeq = ++seq;
|
|
685
|
+
const res = await fetchT(url, { headers: { accept: "application/json" }, cache: force ? "no-store" : "default" }, BALANCES_TIMEOUT_MS);
|
|
645
686
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
646
687
|
const data = await res.json();
|
|
688
|
+
// C-4c: 只接受最新一次请求的结果 —— 否则「新请求先回、旧请求后回」会把界面数据倒退回去
|
|
689
|
+
if (mySeq <= appliedSeq) { inflight = null; return; }
|
|
690
|
+
appliedSeq = mySeq;
|
|
647
691
|
if (data.config?.clientPollIntervalMs >= 1000) pollMs = Math.min(data.config.clientPollIntervalMs, 3600000);
|
|
648
692
|
// v0.5.0 性能适配: 内容无变化就不重建快照引用, useSyncExternalStore 判定 Object.is 相同直接跳过整棵树重渲
|
|
649
693
|
const newBalances = data.balances || [];
|
|
650
694
|
const oldBalances = Array.isArray(snapshot.balances) ? snapshot.balances : [];
|
|
651
695
|
const changed = JSON.stringify(newBalances) !== JSON.stringify(oldBalances) || snapshot.status !== "ok";
|
|
652
696
|
const nextFetchedAt = data.fetchedAt || Date.now();
|
|
653
|
-
|
|
697
|
+
// v1.4.1: 服务端冷启动不再阻塞首屏 —— 立刻回 `loading:true` + 空列表, 刷新丢后台。
|
|
698
|
+
// 这里保持「加载中」状态(骨架屏), 并把轮询临时压到 1.5 秒, 数据一到就上屏。
|
|
699
|
+
// 注意这不是假数据: 显示的是"加载中", 不是 0 元。
|
|
700
|
+
if (data.loading === true) {
|
|
701
|
+
snapshot = { ...snapshot, status: "loading", balances: [], config: data.config || snapshot.config, fetchedAt: nextFetchedAt, isRefreshing: false };
|
|
702
|
+
pollMs = Math.min(pollMs, 1500);
|
|
703
|
+
} else if (!changed) {
|
|
654
704
|
// v1.1.0: config 必须始终跟随最新响应 —— 旧写法在余额未变时保留旧 config,
|
|
655
705
|
// 导致设置里改完开关(如 whaleEnabled)后前端永远读不到新值, 表现为"开了又变回关"
|
|
656
706
|
snapshot = { ...snapshot, config: data.config || snapshot.config, fetchedAt: nextFetchedAt, isRefreshing: false };
|
|
@@ -757,6 +807,12 @@ window.__ModuleLoader__.load({
|
|
|
757
807
|
}
|
|
758
808
|
/** 给一个胶囊挂长按: 位移 > 8px(其实是在滑动这一行) 或抬手都取消; 长按抬手后的 click 吞掉。 */
|
|
759
809
|
function bindSubagentTip(node, text) {
|
|
810
|
+
// C-4b (v1.4.1): ref 是内联箭头函数 → 每次 render 都重新调用本函数,
|
|
811
|
+
// 而这里挂 7 个 addEventListener 却从不 remove → 5 秒轮询下每小时约 5000 个监听器
|
|
812
|
+
// 堆在同一个胶囊节点上。改成"同一个节点只挂一次", tip 存在节点上按需读。
|
|
813
|
+
node.__dshadbTip = text;
|
|
814
|
+
if (node.__dshadbTipBound === true) return;
|
|
815
|
+
node.__dshadbTipBound = true;
|
|
760
816
|
let timer = 0, sx = 0, sy = 0, fired = false;
|
|
761
817
|
const cancel = () => { if (timer) { clearTimeout(timer); timer = 0; } };
|
|
762
818
|
node.addEventListener("pointerdown", (e) => {
|
|
@@ -764,7 +820,7 @@ window.__ModuleLoader__.load({
|
|
|
764
820
|
fired = false;
|
|
765
821
|
sx = e.clientX; sy = e.clientY;
|
|
766
822
|
cancel();
|
|
767
|
-
timer = setTimeout(() => { timer = 0; fired = true; showSubagentTip(node, text); }, SUBTIP_LONG_PRESS_MS);
|
|
823
|
+
timer = setTimeout(() => { timer = 0; fired = true; showSubagentTip(node, node.__dshadbTip || text); }, SUBTIP_LONG_PRESS_MS);
|
|
768
824
|
});
|
|
769
825
|
node.addEventListener("pointermove", (e) => {
|
|
770
826
|
if (timer && (Math.abs(e.clientX - sx) > 8 || Math.abs(e.clientY - sy) > 8)) cancel();
|
|
@@ -789,7 +845,18 @@ window.__ModuleLoader__.load({
|
|
|
789
845
|
const list = Array.isArray(cost && cost.subagents) ? cost.subagents : [];
|
|
790
846
|
if (list.length === 0) return null;
|
|
791
847
|
const mainCur = (cost && cost.currency) || "CNY";
|
|
792
|
-
|
|
848
|
+
// UI v1.4.1: 只在真的横向溢出时才加渐隐类名, 避免内容没满也糊一条边
|
|
849
|
+
const syncSubsOverflow = (n) => {
|
|
850
|
+
if (!n) return;
|
|
851
|
+
try {
|
|
852
|
+
const over = n.scrollWidth > n.clientWidth + 1;
|
|
853
|
+
if (over) n.classList.add("dshadb_subs_overflow");
|
|
854
|
+
else n.classList.remove("dshadb_subs_overflow");
|
|
855
|
+
} catch (e) { /* 忽略 */ }
|
|
856
|
+
};
|
|
857
|
+
return react.createElement("span", { className: "dshadb_subs", key: "subs",
|
|
858
|
+
ref: (n) => { if (n) requestAnimationFrame(() => syncSubsOverflow(n)); },
|
|
859
|
+
onScroll: (e) => syncSubsOverflow(e.currentTarget) },
|
|
793
860
|
list.map((s, i) => {
|
|
794
861
|
const f = formatSessionCost({ cost: s.cost, currency: mainCur, costByCurrency: s.costByCurrency, waiting: false });
|
|
795
862
|
const amt = f !== null && f.hasValue ? "~" + f.text : "~—";
|
|
@@ -1043,6 +1110,13 @@ window.__ModuleLoader__.load({
|
|
|
1043
1110
|
const handleRefresh = () => { setRefreshing(true); store.forceRefresh().then(() => setTimeout(() => setRefreshing(false), 500)); };
|
|
1044
1111
|
const balances = data.balances || [];
|
|
1045
1112
|
const isLoading = data.status === "loading" && balances.length === 0;
|
|
1113
|
+
// v1.4.1: 加载超过 4 秒就在骨架屏下面补一句「首次加载较慢」的解释(冷启动全量刷新实测 8~30s)
|
|
1114
|
+
const [slowLoad, setSlowLoad] = react.useState(false);
|
|
1115
|
+
react.useEffect(() => {
|
|
1116
|
+
if (!isLoading) { setSlowLoad(false); return undefined; }
|
|
1117
|
+
const timer = setTimeout(() => setSlowLoad(true), 4000);
|
|
1118
|
+
return () => clearTimeout(timer);
|
|
1119
|
+
}, [isLoading]);
|
|
1046
1120
|
const okCount = balances.filter(b => b.status === "ok").length;
|
|
1047
1121
|
|
|
1048
1122
|
const groups = { domestic: [], abroad: [], relay: [], custom: [] };
|
|
@@ -1119,19 +1193,35 @@ window.__ModuleLoader__.load({
|
|
|
1119
1193
|
]);
|
|
1120
1194
|
};
|
|
1121
1195
|
|
|
1196
|
+
const slowHint = isLoading && slowLoad;
|
|
1122
1197
|
let bodyContent;
|
|
1123
1198
|
if (isLoading) {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
react.createElement("div", { className: "
|
|
1129
|
-
|
|
1130
|
-
|
|
1199
|
+
// v1.4.1: 冷启动全量刷新实测要 8~30 秒(中转站 auto 探测是串行的),
|
|
1200
|
+
// 光画骨架屏用户会以为卡死 → 超过 4 秒补一句解释。
|
|
1201
|
+
bodyContent = react.createElement("div", null, [
|
|
1202
|
+
...([0,1,2,3].map(i => react.createElement("div", { className: "dshadb_skeleton", key: i }, [
|
|
1203
|
+
react.createElement("div", { className: "dshadb_skeleton_logo", key: "logo" }),
|
|
1204
|
+
react.createElement("div", { className: "dshadb_skeleton_lines", key: "lines" }, [
|
|
1205
|
+
react.createElement("div", { className: "dshadb_skeleton_line dshadb_skeleton_line_long", key: "l1" }),
|
|
1206
|
+
react.createElement("div", { className: "dshadb_skeleton_line dshadb_skeleton_line_short", key: "l2" }),
|
|
1207
|
+
]),
|
|
1208
|
+
]))),
|
|
1209
|
+
slowHint ? react.createElement("div", { key: "slow", style: { fontSize: 11, color: "#8b91a0", textAlign: "center", lineHeight: 1.6, margin: "10px 4px 2px" } },
|
|
1210
|
+
"首次加载要逐个平台拉取余额(含中转站探测),通常 10~30 秒,请稍候…") : null,
|
|
1211
|
+
]);
|
|
1131
1212
|
} else if (balances.length === 0) {
|
|
1213
|
+
// C-3c (v1.4.1): 请求失败时以前只画"空看板" —— 用户分不清「没有平台」和「请求失败」。
|
|
1214
|
+
// 实测 snapshot.message 全文件只被赋值、从不渲染。
|
|
1215
|
+
const isErr = snapshot.status === "error";
|
|
1132
1216
|
bodyContent = react.createElement("div", { className: "dshadb_empty" }, [
|
|
1133
|
-
react.createElement("span", { key: "icon", style: { fontSize: 30, opacity: 0.5 } }, "📊"),
|
|
1134
|
-
react.createElement("span", { key: "title" }, t("title")),
|
|
1217
|
+
react.createElement("span", { key: "icon", style: { fontSize: 30, opacity: 0.5 } }, isErr ? "⚠️" : "📊"),
|
|
1218
|
+
react.createElement("span", { key: "title" }, isErr ? "余额接口请求失败" : t("title")),
|
|
1219
|
+
isErr && snapshot.message ? react.createElement("span", { key: "msg", style: { fontSize: 11, color: "#e05252", wordBreak: "break-all" } }, String(snapshot.message)) : null,
|
|
1220
|
+
isErr ? react.createElement("button", {
|
|
1221
|
+
key: "retry", type: "button", className: "dshadb_add_btn",
|
|
1222
|
+
style: { width: "auto", padding: "6px 18px", marginTop: "4px" },
|
|
1223
|
+
onClick: () => { try { store.forceRefresh(); } catch (e) { /* 忽略 */ } },
|
|
1224
|
+
}, "重试") : null,
|
|
1135
1225
|
]);
|
|
1136
1226
|
} else {
|
|
1137
1227
|
bodyContent = [
|
|
@@ -1214,7 +1304,7 @@ window.__ModuleLoader__.load({
|
|
|
1214
1304
|
react.useEffect(() => {
|
|
1215
1305
|
if (!isOpen || platformId !== "deepseek") return;
|
|
1216
1306
|
let live = true;
|
|
1217
|
-
|
|
1307
|
+
fetchT("/api-dashboard/prices", { headers: { accept: "application/json" } })
|
|
1218
1308
|
.then((r) => (r.ok ? r.json() : null))
|
|
1219
1309
|
.then((d) => { if (live && d && d.ok) setPriceData(d); })
|
|
1220
1310
|
.catch(() => {});
|
|
@@ -1398,7 +1488,7 @@ window.__ModuleLoader__.load({
|
|
|
1398
1488
|
});
|
|
1399
1489
|
const checkUpdate = react.useCallback((force) => {
|
|
1400
1490
|
setUpd(s => ({ ...s, phase: "checking" }));
|
|
1401
|
-
|
|
1491
|
+
fetchT("/api-dashboard/update" + (force ? "?force=1" : ""), { headers: { accept: "application/json" } })
|
|
1402
1492
|
.then(r => r.json())
|
|
1403
1493
|
.then(d => {
|
|
1404
1494
|
// v0.6.2: 检查完必有反馈 —— 已是最新也提示, 有新版提示版本号, 失败给固定文案
|
|
@@ -1411,7 +1501,7 @@ window.__ModuleLoader__.load({
|
|
|
1411
1501
|
react.useEffect(() => { if (isOpen) checkUpdate(false); }, [isOpen]); // 打开面板自动检查
|
|
1412
1502
|
const installUpdate = () => {
|
|
1413
1503
|
setUpd(s => ({ ...s, phase: "installing" }));
|
|
1414
|
-
|
|
1504
|
+
fetchT("/api-dashboard/update/install", { method: "POST" }, 120000)
|
|
1415
1505
|
.then(r => r.json().then(d => ({ code: r.status, body: d })))
|
|
1416
1506
|
.then(({ code, body }) => {
|
|
1417
1507
|
if (code === 200 && body.ok) {
|
|
@@ -1443,12 +1533,18 @@ window.__ModuleLoader__.load({
|
|
|
1443
1533
|
setRefreshSec(Math.round((config?.clientPollIntervalMs || 5000) / 1000) || 5);
|
|
1444
1534
|
setWhaleOn(!!(config && config.whaleEnabled));
|
|
1445
1535
|
let cancelled = false;
|
|
1446
|
-
|
|
1536
|
+
fetchT("/api-dashboard/config", { cache: "no-store" }).then(r => r.json()).then(d => {
|
|
1447
1537
|
if (cancelled) return;
|
|
1448
1538
|
if (d.ok) {
|
|
1449
1539
|
setRelays(d.customRelays || []);
|
|
1450
1540
|
setModels(d.customModels || []);
|
|
1451
1541
|
if (d.refreshIntervalSec) setRefreshSec(d.refreshIntervalSec);
|
|
1542
|
+
// C-2 (v1.4.1): 阈值/主币种也必须从 /config 补一次。
|
|
1543
|
+
// 以前只从 /balances 的 config 读, 而冷启动 /balances 要等 8~10s →
|
|
1544
|
+
// 那期间打开面板看到的是默认 50/10/CNY, 一点"保存并生效"就把用户真实配置覆盖了。
|
|
1545
|
+
if (typeof d.safeThreshold === "number") setSafe(d.safeThreshold);
|
|
1546
|
+
if (typeof d.warnThreshold === "number") setWarn(d.warnThreshold);
|
|
1547
|
+
if (typeof d.currency === "string") setCurrency(d.currency);
|
|
1452
1548
|
if (typeof d.overseasCurrency === "string") setOverseasCurrency(d.overseasCurrency);
|
|
1453
1549
|
if (typeof d.whaleEnabled === "boolean") setWhaleOn(d.whaleEnabled);
|
|
1454
1550
|
if (Array.isArray(d.officialProviders)) setOfficialText(d.officialProviders.join(", "));
|
|
@@ -1458,7 +1554,7 @@ window.__ModuleLoader__.load({
|
|
|
1458
1554
|
}
|
|
1459
1555
|
}).finally(() => { if (!cancelled) setLoading(false); });
|
|
1460
1556
|
// 大肥鱼细项与主配置并行拉取, 失败保持默认值
|
|
1461
|
-
|
|
1557
|
+
fetchT("/api-dashboard/whale/settings", { cache: "no-store" }).then(r => r.json()).then(d => {
|
|
1462
1558
|
if (cancelled || !d || !d.ok || !d.settings) return;
|
|
1463
1559
|
const s = d.settings;
|
|
1464
1560
|
setWf(prev => ({
|
|
@@ -1476,11 +1572,13 @@ window.__ModuleLoader__.load({
|
|
|
1476
1572
|
}, [isOpen]);
|
|
1477
1573
|
if (!isOpen) return null;
|
|
1478
1574
|
const save = async () => {
|
|
1479
|
-
|
|
1575
|
+
// C-1 (v1.4.1): 原来这里夹的是 5 —— 输入框允许 1 秒、服务端 clampRefreshSec 也吃 1 秒,
|
|
1576
|
+
// 只有 save() 把它悄悄改回 5 秒(用户设 1 秒保存后变 5 秒)。四处下限现在一致。
|
|
1577
|
+
const nextRefreshSec = Math.min(Math.max(Number(refreshSec) || 1, 1), 60)
|
|
1480
1578
|
setRefreshSec(nextRefreshSec)
|
|
1481
1579
|
setSaving(true);
|
|
1482
1580
|
try {
|
|
1483
|
-
await
|
|
1581
|
+
await fetchT("/api-dashboard/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({
|
|
1484
1582
|
customRelays: relays, customModels: models,
|
|
1485
1583
|
safeThreshold: Number(safe), warnThreshold: Number(warn), currency, overseasCurrency,
|
|
1486
1584
|
refreshIntervalSec: nextRefreshSec,
|
|
@@ -1514,7 +1612,7 @@ window.__ModuleLoader__.load({
|
|
|
1514
1612
|
setWhaleOn(next);
|
|
1515
1613
|
if (next) ensureWhaleWidget(); else removeWhaleWidget();
|
|
1516
1614
|
try {
|
|
1517
|
-
await
|
|
1615
|
+
await fetchT("/api-dashboard/config", {
|
|
1518
1616
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
1519
1617
|
body: JSON.stringify({ whaleEnabled: !!next }),
|
|
1520
1618
|
});
|
|
@@ -1525,7 +1623,7 @@ window.__ModuleLoader__.load({
|
|
|
1525
1623
|
const toggleBrands = async (next) => {
|
|
1526
1624
|
setShowBrands(next);
|
|
1527
1625
|
try {
|
|
1528
|
-
await
|
|
1626
|
+
await fetchT("/api-dashboard/config", {
|
|
1529
1627
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
1530
1628
|
body: JSON.stringify({ showNoBalanceBrands: !!next }),
|
|
1531
1629
|
});
|
|
@@ -1539,7 +1637,7 @@ window.__ModuleLoader__.load({
|
|
|
1539
1637
|
setWf(prev => ({ ...prev, ...patch }));
|
|
1540
1638
|
patchWhaleWidget(patch);
|
|
1541
1639
|
try {
|
|
1542
|
-
|
|
1640
|
+
fetchT("/api-dashboard/whale/settings", {
|
|
1543
1641
|
method: "PUT", headers: { "Content-Type": "application/json" },
|
|
1544
1642
|
body: JSON.stringify(patch),
|
|
1545
1643
|
}).catch(() => {});
|
|
@@ -2052,7 +2150,7 @@ window.__ModuleLoader__.load({
|
|
|
2052
2150
|
peekRatio: st.peekRatio, left: st.left, top: st.top, side: st.side,
|
|
2053
2151
|
};
|
|
2054
2152
|
try {
|
|
2055
|
-
|
|
2153
|
+
fetchT("/api-dashboard/whale/settings", {
|
|
2056
2154
|
method: "PUT", headers: { "Content-Type": "application/json" },
|
|
2057
2155
|
body: JSON.stringify(lastWhaleSettings),
|
|
2058
2156
|
}).catch(function () {});
|
|
@@ -2362,7 +2460,7 @@ window.__ModuleLoader__.load({
|
|
|
2362
2460
|
}
|
|
2363
2461
|
revealTimer = setTimeout(reveal, 2000); // 兜底: 请求挂住也不能让鱼永远不出现
|
|
2364
2462
|
applyAll();
|
|
2365
|
-
|
|
2463
|
+
fetchT("/api-dashboard/whale/settings", { cache: "no-store" })
|
|
2366
2464
|
.then(function (r) { return r.json(); })
|
|
2367
2465
|
.then(function (d) {
|
|
2368
2466
|
if (disposed) return;
|
|
@@ -2451,11 +2549,23 @@ window.__ModuleLoader__.load({
|
|
|
2451
2549
|
// v1.4.0: 看板/详情/设置任一开着 → 锁住大肥鱼挂件的触摸。
|
|
2452
2550
|
// 不这么做的话, 挂件(在 body 上, z-index 9600)会盖在设置面板上面,
|
|
2453
2551
|
// 拖「身体大小 / 露出比例」滑块时实际拖的是挂件。
|
|
2454
|
-
|
|
2552
|
+
/**
|
|
2553
|
+
* 大肥鱼挂件锁定(v1.4.1 定稿):**任何界面下都不锁**。
|
|
2554
|
+
*
|
|
2555
|
+
* 演进过程(别再走回头路):
|
|
2556
|
+
* v1.4.0 `overlayOpen = isSettingsOpen || view !== "bar"` —— 一开设置就锁死,
|
|
2557
|
+
* 维护者反馈「在设置里开启大肥鱼,图片动不了,得关掉才能动」。
|
|
2558
|
+
* v1.4.1a 改成只锁「看板 / 详情」两个整屏抽屉 —— 维护者再次要求全部放开。
|
|
2559
|
+
* v1.4.1b **本版:永不锁定**,挂件在任何界面下都能拖。
|
|
2560
|
+
*
|
|
2561
|
+
* 锁的机制本身(`setWhaleLocked` / `.dshadb-whale-locked` CSS / `onDown` 里那道兜底)
|
|
2562
|
+
* **保留着**:万一以后又出现「面板开着时拖滑块被挂件吃掉」这类反馈,
|
|
2563
|
+
* 把下面这行换成 `setWhaleLocked(view !== "bar")` 即可恢复按界面锁定。
|
|
2564
|
+
*/
|
|
2455
2565
|
react.useEffect(() => {
|
|
2456
|
-
setWhaleLocked(
|
|
2566
|
+
setWhaleLocked(false);
|
|
2457
2567
|
return () => setWhaleLocked(false);
|
|
2458
|
-
}, [
|
|
2568
|
+
}, []);
|
|
2459
2569
|
// 峰谷时段变化同步给挂件 (台词里的「当前时间段」用它, 不涉及任何金额)
|
|
2460
2570
|
react.useEffect(() => {
|
|
2461
2571
|
updateWhaleContext({ isPeak: !!(config && config.isPeak) });
|
|
@@ -2514,7 +2624,7 @@ window.__ModuleLoader__.load({
|
|
|
2514
2624
|
react.useEffect(() => {
|
|
2515
2625
|
const cachedAt = window.__dshadbUpdateCheckedAt || 0;
|
|
2516
2626
|
if (Date.now() - cachedAt < 5 * 60 * 1000) return; // 会话内查过就不重复
|
|
2517
|
-
|
|
2627
|
+
fetchT("/api-dashboard/update", { headers: { accept: "application/json" } })
|
|
2518
2628
|
.then((r) => r.json())
|
|
2519
2629
|
.then((d) => { if (d && d.ok) { window.__dshadbUpdateInfo = d; window.__dshadbUpdateCheckedAt = Date.now(); } })
|
|
2520
2630
|
.catch(() => { /* 网络异常静默, 打开设置时可见固定错误文案 */ });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-api-dashboard",
|
|
3
|
-
"version": "1.4.
|
|
4
|
-
"description": "多平台 API
|
|
3
|
+
"version": "1.4.2",
|
|
4
|
+
"description": "DeepSeek Harness 多平台 API 余额 / 用量看板:输入框下方实时显示各平台余额与本会话估算消耗",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
|
@@ -67,4 +67,4 @@
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
"license": "MIT"
|
|
70
|
-
}
|
|
70
|
+
}
|