dsh-deepseek-web-login 0.4.1 → 0.5.0
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/CHANGELOG.md +49 -0
- package/lib/client.js +297 -0
- package/lib/client.js.map +1 -1
- package/lib/index.js +354 -18
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/tools/panel-preview.mjs +97 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循大致语义化版本;日期为本地时间。
|
|
4
4
|
|
|
5
|
+
## 0.5.0 — 2026-09-27
|
|
6
|
+
|
|
7
|
+
> 新增「允许并行调用工具」开关,并把默认改成**一次只发一个工具调用**。
|
|
8
|
+
|
|
9
|
+
### 新增
|
|
10
|
+
|
|
11
|
+
- **「允许并行调用工具」开关**(「防风控」页 → 请求节流):关掉后模型一轮只发一个工具调用、
|
|
12
|
+
等结果回来再决定下一步;打开则允许一轮最多 3 个,由 DSH 并行执行。
|
|
13
|
+
- 它改变的是**模型看到的协议指令** —— 属于「引导」而不是「强制」,不拦截也不改写模型发出的调用。
|
|
14
|
+
- ⚠️ 切换后**第一轮会全量重发一次**(协议文本变了,投喂链要重建),之后恢复正常。
|
|
15
|
+
|
|
16
|
+
### 变更(**破坏性语义变化**)
|
|
17
|
+
|
|
18
|
+
- 🔴 **默认改为「一次只发一个工具调用」**(即开关默认不勾)。0.4.2 及以前默认允许批量。
|
|
19
|
+
- 升级后**第一个任务的协议文本即变化** ⇒ 投喂链断一次(下一轮全量重发 + 新建网页端会话)。
|
|
20
|
+
- 之后每个任务**总耗时变长**:每个工具各占一轮请求(3 个工具约从 2 轮变 4 轮)。
|
|
21
|
+
- 想保留旧行为:设置页把「允许并行调用工具」勾上即可(= 显式 `serialToolCalls: false`)。
|
|
22
|
+
|
|
23
|
+
### 说明
|
|
24
|
+
|
|
25
|
+
- 工具在 DSH 侧执行(读文件、跑命令),**不产生任何 DeepSeek 请求** ⇒ 对网页端不可见。
|
|
26
|
+
别和「允许并发生成」混淆:后者是**同时对 DeepSeek 发两条请求**(实测会触发账号级限制),
|
|
27
|
+
前者只影响模型一轮里发几个工具调用。
|
|
28
|
+
- **请求密度本身基本不变**:闸门间隔按「上一次结束」时刻计算,而工具执行发生在其后
|
|
29
|
+
⇒ 工具耗时本就盖住了那段间隔。实测 827 个相邻请求间隔里 **99% 都 > 4s**(中位 18.3s)。
|
|
30
|
+
真正变化的是**总请求数**与**总耗时**。
|
|
31
|
+
|
|
32
|
+
## 0.4.2 — 2026-09-26
|
|
33
|
+
|
|
34
|
+
> 新增「Token 统计」页:本地的用量趋势与分布。以前 token 数只在上报途中被算一次就丢了。
|
|
35
|
+
|
|
36
|
+
### 新增
|
|
37
|
+
|
|
38
|
+
- **「Token 统计」页**(标签栏在「模型」之后):把每次调用的 token 用量落到本地
|
|
39
|
+
(`<数据目录>/web-login/usage/`,按天分文件、保留 90 天),并在设置页看趋势与分布。
|
|
40
|
+
- **范围切换**:今天 / 近 7 天 / 近 30 天 / 总计
|
|
41
|
+
- **四格总览**:总 Token / 输入 Token / 输出 Token / 调用次数(副行给出服务端口径占比与成功失败数)
|
|
42
|
+
- **按天用量**:堆叠柱(输入 + 输出)+ 虚线(当天调用次数,右轴),鼠标停在柱上可看具体数字
|
|
43
|
+
- **用量分布**:按账号 / 按模型切换,横条排行带占比与次数
|
|
44
|
+
|
|
45
|
+
### 说明
|
|
46
|
+
|
|
47
|
+
- **口径**:网页端只在**部分**响应里上报总量,其余是按字符估算的 ⇒ 界面显示「服务端口径 N/M 次」,
|
|
48
|
+
不把估算值说成精确值。`server` 标记只认服务端真的给了总量那一次。
|
|
49
|
+
- **与「防风控」页的调用台账是两份存储**:台账保留 7 天、管请求密度与失败分类;
|
|
50
|
+
用量保留 90 天、只看用量。保留期差一个数量级,合并会让两边互相迁就。
|
|
51
|
+
- **懒加载**:切到这一页才读盘,不为没打开它的用户扫描历史文件。
|
|
52
|
+
- ⚠️ **历史数据是空的**:这个存储从本版才开始记,所以趋势图从升级当天开始长。
|
|
53
|
+
|
|
5
54
|
## 0.4.1 — 2026-09-26
|
|
6
55
|
|
|
7
56
|
> 被限流/封禁时能自己接下去。0.4.0 的自动换号,从这个版本起才是真正的兜底。
|
package/lib/client.js
CHANGED
|
@@ -476,6 +476,39 @@ transition:color .15s ease,border-color .15s ease}
|
|
|
476
476
|
.dsw-spark rect{fill:var(--bd2)}
|
|
477
477
|
.dsw-spark rect.on{fill:var(--accent)}
|
|
478
478
|
.dsw-spark rect.bad{fill:var(--err)}
|
|
479
|
+
/* ── Token 统计页 ────────────────────────────────────────────────────
|
|
480
|
+
图全是手写 SVG:插件不引第三方图表库(多一个依赖就多一份与宿主版本的耦合)。
|
|
481
|
+
高度写 auto 而不是固定值 —— viewBox 自带比例,固定高度会把图拉变形或留白。 */
|
|
482
|
+
.dsw-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--sp-3);
|
|
483
|
+
padding:var(--sp-3) 0;border-top:1px solid var(--bd);border-bottom:1px solid var(--bd)}
|
|
484
|
+
.dsw-stat{min-width:0}
|
|
485
|
+
.dsw-stat .k{font-size:12px;color:var(--fg3);margin:0 0 2px}
|
|
486
|
+
.dsw-stat .v{font-size:20px;font-weight:600;line-height:1.25;letter-spacing:-.01em;
|
|
487
|
+
color:var(--fg);font-variant-numeric:tabular-nums}
|
|
488
|
+
.dsw-stat .n{font-size:11px;color:var(--fg3);margin-top:2px;line-height:1.45}
|
|
489
|
+
@media (max-width:560px){.dsw-stats{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
490
|
+
.dsw-trend{display:block;width:100%;height:auto;margin:var(--sp-3) 0 0;overflow:visible}
|
|
491
|
+
.dsw-trend .grid{stroke:var(--bd);stroke-width:.5}
|
|
492
|
+
.dsw-trend .axis{fill:var(--fg3);font-size:10px}
|
|
493
|
+
.dsw-trend .bar-in{fill:var(--accent)}
|
|
494
|
+
.dsw-trend .bar-out{fill:color-mix(in srgb,var(--accent) 45%,transparent)}
|
|
495
|
+
.dsw-trend .line{fill:none;stroke:var(--fg2);stroke-width:1.2;stroke-dasharray:3 2}
|
|
496
|
+
.dsw-legend{display:flex;gap:var(--sp-3);flex-wrap:wrap;font-size:11px;color:var(--fg2);
|
|
497
|
+
margin:var(--sp-2) 0 0}
|
|
498
|
+
.dsw-legend i{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:4px;
|
|
499
|
+
vertical-align:-1px}
|
|
500
|
+
/* 分布排行:一行 = 名称 + 数值 + 占比 + 横条。条宽按**最大值**归一(不是按总量),
|
|
501
|
+
否则只有第一名看得见,其余全是一根短刺。 */
|
|
502
|
+
.dsw-ranks{display:flex;flex-direction:column;gap:var(--sp-3);margin-top:var(--sp-3)}
|
|
503
|
+
.dsw-rank{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--sp-3);
|
|
504
|
+
align-items:baseline}
|
|
505
|
+
.dsw-rank .rk-name{font-size:12px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;
|
|
506
|
+
white-space:nowrap}
|
|
507
|
+
.dsw-rank .rk-val{font-size:12px;color:var(--fg2);font-variant-numeric:tabular-nums;
|
|
508
|
+
white-space:nowrap}
|
|
509
|
+
.dsw-rank .rk-track{grid-column:1/-1;height:6px;border-radius:999px;background:var(--bd);
|
|
510
|
+
overflow:hidden}
|
|
511
|
+
.dsw-rank .rk-fill{height:100%;border-radius:999px;background:var(--accent)}
|
|
479
512
|
.dsw-path{font-family:var(--mono);font-size:11px;color:var(--fg2);word-break:break-all}
|
|
480
513
|
/* 账号库 */
|
|
481
514
|
.dsw-accounts{list-style:none;margin:var(--sp-2) 0 0;padding:0;display:flex;flex-direction:column;gap:var(--sp-2)}
|
|
@@ -588,6 +621,7 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
588
621
|
const TAB_KEYS = [
|
|
589
622
|
"account",
|
|
590
623
|
"model",
|
|
624
|
+
"token",
|
|
591
625
|
"gate",
|
|
592
626
|
"transport",
|
|
593
627
|
"context",
|
|
@@ -596,6 +630,7 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
596
630
|
const TAB_LABELS = {
|
|
597
631
|
account: "账号",
|
|
598
632
|
model: "模型",
|
|
633
|
+
token: "Token 统计",
|
|
599
634
|
gate: "防风控",
|
|
600
635
|
transport: "传输层",
|
|
601
636
|
context: "上下文",
|
|
@@ -603,6 +638,7 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
603
638
|
};
|
|
604
639
|
const accountPane = el("div", "dsw-pane");
|
|
605
640
|
const modelPane = el("div", "dsw-pane");
|
|
641
|
+
const tokenPane = el("div", "dsw-pane");
|
|
606
642
|
const gatePane = el("div", "dsw-pane");
|
|
607
643
|
const transportPane = el("div", "dsw-pane");
|
|
608
644
|
const contextPane = el("div", "dsw-pane");
|
|
@@ -610,18 +646,25 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
610
646
|
const panes = {
|
|
611
647
|
account: accountPane,
|
|
612
648
|
model: modelPane,
|
|
649
|
+
token: tokenPane,
|
|
613
650
|
gate: gatePane,
|
|
614
651
|
transport: transportPane,
|
|
615
652
|
context: contextPane,
|
|
616
653
|
about: aboutPane
|
|
617
654
|
};
|
|
618
655
|
const tabButtons = {};
|
|
656
|
+
/**
|
|
657
|
+
* 首次切到某页时才拉的数据。**不做成"启动时全拉"**:Token 统计要扫最多 90 天的
|
|
658
|
+
* jsonl,而多数人根本不打开这一页 —— 不为没看的页付代价。
|
|
659
|
+
*/
|
|
660
|
+
const paneFirstShow = {};
|
|
619
661
|
const selectTab = (key) => {
|
|
620
662
|
for (const each of TAB_KEYS) {
|
|
621
663
|
const on = each === key;
|
|
622
664
|
panes[each].hidden = !on;
|
|
623
665
|
tabButtons[each].classList.toggle("active", on);
|
|
624
666
|
tabButtons[each].setAttribute("aria-selected", String(on));
|
|
667
|
+
if (on) paneFirstShow[each]?.();
|
|
625
668
|
}
|
|
626
669
|
};
|
|
627
670
|
const tabBar = el("div", "dsw-tabs");
|
|
@@ -1599,6 +1642,26 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
1599
1642
|
paintAutoSwitch();
|
|
1600
1643
|
saveGate({ autoSwitchMinutes: currentSwitchMinutes() });
|
|
1601
1644
|
});
|
|
1645
|
+
const serialRow = el("div", "dsw-gate-row");
|
|
1646
|
+
const serialLabel = el("label", "dsw-switch");
|
|
1647
|
+
const serialInput = el("input");
|
|
1648
|
+
serialInput.type = "checkbox";
|
|
1649
|
+
const serialTrack = el("span", "dsw-switch-track");
|
|
1650
|
+
serialLabel.append(serialInput, serialTrack, el("span", void 0, "允许并行调用工具"));
|
|
1651
|
+
serialRow.append(serialLabel);
|
|
1652
|
+
gateCard.append(serialRow);
|
|
1653
|
+
const serialHint = el("p", "dsw-hint", "");
|
|
1654
|
+
gateCard.append(serialHint);
|
|
1655
|
+
const paintSerialTools = () => {
|
|
1656
|
+
const batched = serialInput.checked;
|
|
1657
|
+
serialHint.textContent = batched ? "允许并行:模型可以一次发多个工具调用(同一批最多 3 个),DSH 会并行跑它们 —— 一轮请求推进多步,任务更快。工具在 DSH 侧执行、不发 DeepSeek 请求,所以对网页端不可见。切换后的第一轮会全量重发一次。" : "默认:一次只发一个工具调用,等结果回来再决定下一步 —— 后一步能用上前一步的真实结果,逐步反应更稳。代价是每个工具各占一轮请求,任务总耗时明显变长。切换后的第一轮会全量重发一次。";
|
|
1658
|
+
};
|
|
1659
|
+
serialInput.checked = false;
|
|
1660
|
+
paintSerialTools();
|
|
1661
|
+
serialInput.addEventListener("change", () => {
|
|
1662
|
+
paintSerialTools();
|
|
1663
|
+
saveGate({ serialToolCalls: !serialInput.checked });
|
|
1664
|
+
});
|
|
1602
1665
|
const cleanupRow = el("div", "dsw-gate-row");
|
|
1603
1666
|
cleanupRow.append(el("span", "dsw-gate-label", "会话清理"));
|
|
1604
1667
|
const cleanupBtns = {};
|
|
@@ -1767,6 +1830,238 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
1767
1830
|
}
|
|
1768
1831
|
};
|
|
1769
1832
|
ledgerRefreshBtn.addEventListener("click", () => void loadLedger());
|
|
1833
|
+
const fmtTokens = (n) => {
|
|
1834
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
1835
|
+
if (n >= 1e8) return `${(n / 1e8).toFixed(2)} 亿`;
|
|
1836
|
+
if (n >= 1e4) return `${(n / 1e4).toFixed(n >= 1e6 ? 0 : 1)} 万`;
|
|
1837
|
+
return String(Math.round(n));
|
|
1838
|
+
};
|
|
1839
|
+
const fullNum = (n) => Number.isFinite(n) ? Math.round(n).toLocaleString("zh-CN") : "0";
|
|
1840
|
+
const TOKEN_RANGES = [
|
|
1841
|
+
[1, "今天"],
|
|
1842
|
+
[7, "近 7 天"],
|
|
1843
|
+
[30, "近 30 天"],
|
|
1844
|
+
[0, "总计"]
|
|
1845
|
+
];
|
|
1846
|
+
let tokenDays = 30;
|
|
1847
|
+
const tokenRangeBar = el("div", "dsw-subtabs");
|
|
1848
|
+
tokenRangeBar.setAttribute("role", "tablist");
|
|
1849
|
+
const tokenRangeBtns = [];
|
|
1850
|
+
const paintTokenRange = () => {
|
|
1851
|
+
TOKEN_RANGES.forEach(([days], index) => {
|
|
1852
|
+
const on = days === tokenDays;
|
|
1853
|
+
tokenRangeBtns[index].classList.toggle("active", on);
|
|
1854
|
+
tokenRangeBtns[index].setAttribute("aria-selected", String(on));
|
|
1855
|
+
});
|
|
1856
|
+
};
|
|
1857
|
+
for (const [days, label] of TOKEN_RANGES) {
|
|
1858
|
+
const btn = el("button", "dsw-subtab", label);
|
|
1859
|
+
btn.type = "button";
|
|
1860
|
+
btn.setAttribute("role", "tab");
|
|
1861
|
+
btn.addEventListener("click", () => {
|
|
1862
|
+
if (tokenDays === days) return;
|
|
1863
|
+
tokenDays = days;
|
|
1864
|
+
paintTokenRange();
|
|
1865
|
+
loadUsage();
|
|
1866
|
+
});
|
|
1867
|
+
tokenRangeBtns.push(btn);
|
|
1868
|
+
tokenRangeBar.append(btn);
|
|
1869
|
+
}
|
|
1870
|
+
paintTokenRange();
|
|
1871
|
+
tokenPane.append(tokenRangeBar);
|
|
1872
|
+
/** 一格总览。`set` 一次给值和注释,避免三处各写一遍 textContent。 */
|
|
1873
|
+
const statCell = (label) => {
|
|
1874
|
+
const node = el("div", "dsw-stat");
|
|
1875
|
+
const value = el("div", "v", "—");
|
|
1876
|
+
const note = el("div", "n", "");
|
|
1877
|
+
node.append(el("div", "k", label), value, note);
|
|
1878
|
+
return {
|
|
1879
|
+
node,
|
|
1880
|
+
set: (next, hint) => {
|
|
1881
|
+
value.textContent = next;
|
|
1882
|
+
note.textContent = hint ?? "";
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
};
|
|
1886
|
+
const sTotal = statCell("总 Token");
|
|
1887
|
+
const sIn = statCell("输入 Token");
|
|
1888
|
+
const sOut = statCell("输出 Token");
|
|
1889
|
+
const sCalls = statCell("调用次数");
|
|
1890
|
+
const tokenStats = el("div", "dsw-stats");
|
|
1891
|
+
tokenStats.append(sTotal.node, sIn.node, sOut.node, sCalls.node);
|
|
1892
|
+
tokenPane.append(tokenStats);
|
|
1893
|
+
const trendCard = el("div", "dsw-card");
|
|
1894
|
+
const tokenHead = el("div", "dsw-cardhead");
|
|
1895
|
+
tokenHead.append(el("span", "name", "按天用量"));
|
|
1896
|
+
const tokenBadge = el("span", "dsw-badge off", "—");
|
|
1897
|
+
tokenHead.append(tokenBadge);
|
|
1898
|
+
trendCard.append(tokenHead);
|
|
1899
|
+
trendCard.append(el("p", "dsw-hint", "柱高=当天的输入 + 输出;虚线=当天调用次数。鼠标停在柱上可看具体数字。"));
|
|
1900
|
+
const legend = el("div", "dsw-legend");
|
|
1901
|
+
legend.innerHTML = "<span><i style=\"background:var(--accent)\"></i>输入</span><span><i style=\"background:color-mix(in srgb,var(--accent) 45%,transparent)\"></i>输出</span><span><i style=\"background:var(--fg2)\"></i>调用次数</span>";
|
|
1902
|
+
const trendBox = el("div");
|
|
1903
|
+
trendCard.append(legend, trendBox);
|
|
1904
|
+
tokenPane.append(trendCard);
|
|
1905
|
+
const rankCard = el("div", "dsw-card");
|
|
1906
|
+
const rankHead = el("div", "dsw-cardhead");
|
|
1907
|
+
rankHead.append(el("span", "name", "用量分布"));
|
|
1908
|
+
const rankTabs = el("div", "dsw-subtabs");
|
|
1909
|
+
let rankKey = "account";
|
|
1910
|
+
const RANK_KEYS = [["account", "按账号"], ["model", "按模型"]];
|
|
1911
|
+
const rankBtns = [];
|
|
1912
|
+
const paintRankTabs = () => {
|
|
1913
|
+
RANK_KEYS.forEach(([key], index) => {
|
|
1914
|
+
const on = key === rankKey;
|
|
1915
|
+
rankBtns[index].classList.toggle("active", on);
|
|
1916
|
+
rankBtns[index].setAttribute("aria-selected", String(on));
|
|
1917
|
+
});
|
|
1918
|
+
};
|
|
1919
|
+
for (const [key, label] of RANK_KEYS) {
|
|
1920
|
+
const btn = el("button", "dsw-subtab", label);
|
|
1921
|
+
btn.type = "button";
|
|
1922
|
+
btn.setAttribute("role", "tab");
|
|
1923
|
+
btn.addEventListener("click", () => {
|
|
1924
|
+
if (rankKey === key) return;
|
|
1925
|
+
rankKey = key;
|
|
1926
|
+
paintRankTabs();
|
|
1927
|
+
renderRanks(rankKey === "account" ? lastUsage?.byAccount : lastUsage?.byModel, rankKey);
|
|
1928
|
+
});
|
|
1929
|
+
rankBtns.push(btn);
|
|
1930
|
+
rankTabs.append(btn);
|
|
1931
|
+
}
|
|
1932
|
+
paintRankTabs();
|
|
1933
|
+
rankHead.append(rankTabs);
|
|
1934
|
+
const ranks = el("div", "dsw-ranks");
|
|
1935
|
+
rankCard.append(rankHead, ranks);
|
|
1936
|
+
tokenPane.append(rankCard);
|
|
1937
|
+
const tokenRow = el("div", "dsw-row");
|
|
1938
|
+
const tokenRefreshBtn = el("button", "dsw-btn ghost", "刷新");
|
|
1939
|
+
tokenRefreshBtn.type = "button";
|
|
1940
|
+
tokenRefreshBtn.addEventListener("click", () => void loadUsage());
|
|
1941
|
+
tokenRow.append(tokenRefreshBtn);
|
|
1942
|
+
tokenPane.append(tokenRow);
|
|
1943
|
+
const tokenMsg = el("p", "dsw-hint dsw-gate-msg", "");
|
|
1944
|
+
tokenPane.append(tokenMsg);
|
|
1945
|
+
/** 账号 id → 给人看的名字(与调用台账同一套映射,拿不到就退回 id)。 */
|
|
1946
|
+
const accountLabel = (id) => {
|
|
1947
|
+
const hit = (Array.isArray(lastAccountsPayload?.accounts) ? lastAccountsPayload.accounts : []).find((item) => item.id === id);
|
|
1948
|
+
return hit ? String(hit.label || hit.display || hit.id) : id;
|
|
1949
|
+
};
|
|
1950
|
+
/**
|
|
1951
|
+
* 按天趋势图。手写 SVG(不引图表库)。
|
|
1952
|
+
* 左轴是 token、右轴是调用次数 —— 两把尺子只共用画布,不共用刻度(把它们画在同一
|
|
1953
|
+
* 坐标系里是最常见的图表错误:量级差几百倍时折线会贴死在底部)。
|
|
1954
|
+
*/
|
|
1955
|
+
const trendSvg = (series) => {
|
|
1956
|
+
const W = 640;
|
|
1957
|
+
const H = 170;
|
|
1958
|
+
const padL = 46;
|
|
1959
|
+
const plotW = 564;
|
|
1960
|
+
const plotH = 142;
|
|
1961
|
+
const n = Math.max(1, series.length);
|
|
1962
|
+
const tokenOf = (d) => (Number(d?.in) || 0) + (Number(d?.out) || 0);
|
|
1963
|
+
const maxTok = Math.max(1, ...series.map(tokenOf));
|
|
1964
|
+
const maxCalls = Math.max(1, ...series.map((d) => Number(d?.calls) || 0));
|
|
1965
|
+
const niceMax = (value) => {
|
|
1966
|
+
const base = Math.pow(10, Math.floor(Math.log10(value)));
|
|
1967
|
+
const norm = value / base;
|
|
1968
|
+
return (norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10) * base;
|
|
1969
|
+
};
|
|
1970
|
+
const top = niceMax(maxTok);
|
|
1971
|
+
const slot = plotW / n;
|
|
1972
|
+
const barW = Math.max(1.5, slot * .62);
|
|
1973
|
+
let out = `<svg class="dsw-trend" viewBox="0 0 ${W} ${H}" role="img" aria-label="按天用量趋势">`;
|
|
1974
|
+
for (const ratio of [
|
|
1975
|
+
0,
|
|
1976
|
+
.5,
|
|
1977
|
+
1
|
|
1978
|
+
]) {
|
|
1979
|
+
const y = 152 - plotH * ratio;
|
|
1980
|
+
out += `<line class="grid" x1="${padL}" y1="${y.toFixed(1)}" x2="610" y2="${y.toFixed(1)}"></line>`;
|
|
1981
|
+
out += `<text class="axis" x="41" y="${(y + 3).toFixed(1)}" text-anchor="end">${fmtTokens(top * ratio)}</text>`;
|
|
1982
|
+
}
|
|
1983
|
+
series.forEach((d, i) => {
|
|
1984
|
+
const x = padL + slot * i + (slot - barW) / 2;
|
|
1985
|
+
const hIn = (Number(d?.in) || 0) / top * plotH;
|
|
1986
|
+
const hOut = (Number(d?.out) || 0) / top * plotH;
|
|
1987
|
+
const yIn = 152 - hIn;
|
|
1988
|
+
out += `<g><title>${d?.date}:总 ${fullNum(tokenOf(d))} · 输入 ${fullNum(Number(d?.in) || 0)} / 输出 ${fullNum(Number(d?.out) || 0)} · 调用 ${Number(d?.calls) || 0} 次</title>`;
|
|
1989
|
+
if (hIn > 0) out += `<rect class="bar-in" x="${x.toFixed(1)}" y="${yIn.toFixed(1)}" width="${barW.toFixed(1)}" height="${hIn.toFixed(1)}" rx="1"></rect>`;
|
|
1990
|
+
if (hOut > 0) out += `<rect class="bar-out" x="${x.toFixed(1)}" y="${(yIn - hOut).toFixed(1)}" width="${barW.toFixed(1)}" height="${hOut.toFixed(1)}" rx="1"></rect>`;
|
|
1991
|
+
if (hIn + hOut <= 0 && (Number(d?.calls) || 0) > 0) out += `<rect class="bar-out" x="${x.toFixed(1)}" y="${150 .toFixed(1)}" width="${barW.toFixed(1)}" height="2" rx="0.5"></rect>`;
|
|
1992
|
+
out += "</g>";
|
|
1993
|
+
});
|
|
1994
|
+
const points = series.map((d, i) => {
|
|
1995
|
+
const x = padL + slot * i + slot / 2;
|
|
1996
|
+
const y = 152 - (Number(d?.calls) || 0) / maxCalls * plotH;
|
|
1997
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
1998
|
+
}).join(" ");
|
|
1999
|
+
out += `<polyline class="line" points="${points}"></polyline>`;
|
|
2000
|
+
out += `<text class="axis" x="614" y="${13 .toFixed(1)}" text-anchor="start">${Math.round(maxCalls)}</text>`;
|
|
2001
|
+
out += `<text class="axis" x="614" y="${155 .toFixed(1)}" text-anchor="start">0</text>`;
|
|
2002
|
+
const labelStep = Math.max(1, Math.ceil(n / 6));
|
|
2003
|
+
series.forEach((d, i) => {
|
|
2004
|
+
if (i % labelStep !== 0 && i !== n - 1) return;
|
|
2005
|
+
const x = padL + slot * i + slot / 2;
|
|
2006
|
+
const parts = String(d?.date ?? "").split("-");
|
|
2007
|
+
out += `<text class="axis" x="${x.toFixed(1)}" y="165" text-anchor="middle">${Number(parts[1])}/${Number(parts[2])}</text>`;
|
|
2008
|
+
});
|
|
2009
|
+
out += "</svg>";
|
|
2010
|
+
return out;
|
|
2011
|
+
};
|
|
2012
|
+
/** 分布排行。条宽按**最大值**归一 —— 按总量归一的话只有第一名看得见,其余全是一根短刺。 */
|
|
2013
|
+
const renderRanks = (rows, key) => {
|
|
2014
|
+
ranks.textContent = "";
|
|
2015
|
+
const items = Array.isArray(rows) ? rows.slice(0, 8) : [];
|
|
2016
|
+
if (items.length === 0) {
|
|
2017
|
+
ranks.append(el("div", "dsw-rank empty", "这段时间还没有用量记录。"));
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
const total = Number(lastUsage?.totals?.total) || 0;
|
|
2021
|
+
const max = Math.max(1, ...items.map((row) => (Number(row?.in) || 0) + (Number(row?.out) || 0)));
|
|
2022
|
+
for (const row of items) {
|
|
2023
|
+
const value = (Number(row?.in) || 0) + (Number(row?.out) || 0);
|
|
2024
|
+
const name = key === "account" ? accountLabel(String(row?.key ?? "")) : String(row?.key ?? "—");
|
|
2025
|
+
const line = el("div", "dsw-rank");
|
|
2026
|
+
line.append(el("div", "rk-name", name));
|
|
2027
|
+
line.append(el("div", "rk-val", `${fmtTokens(value)}${total > 0 ? ` · ${(value / total * 100).toFixed(1)}%` : ""} · ${Number(row?.calls) || 0} 次`));
|
|
2028
|
+
const track = el("div", "rk-track");
|
|
2029
|
+
const fill = el("div", "rk-fill");
|
|
2030
|
+
fill.style.width = `${Math.max(2, value / max * 100).toFixed(1)}%`;
|
|
2031
|
+
track.append(fill);
|
|
2032
|
+
line.append(track);
|
|
2033
|
+
ranks.append(line);
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
/** 最近一次 /usage 的响应(切换"按账号/按模型"时不必重新拉)。 */
|
|
2037
|
+
let lastUsage = null;
|
|
2038
|
+
const renderUsage = (data) => {
|
|
2039
|
+
lastUsage = data;
|
|
2040
|
+
const totals = data?.totals ?? {};
|
|
2041
|
+
const total = Number(totals.total) || 0;
|
|
2042
|
+
const calls = Number(totals.calls) || 0;
|
|
2043
|
+
const serverCalls = Number(totals.serverCalls) || 0;
|
|
2044
|
+
const share = (part) => total > 0 ? `占总量 ${(part / total * 100).toFixed(1)}%` : "";
|
|
2045
|
+
sTotal.set(fmtTokens(total), calls > 0 ? `服务端口径 ${serverCalls}/${calls} 次` : "");
|
|
2046
|
+
sIn.set(fmtTokens(Number(totals.in) || 0), share(Number(totals.in) || 0));
|
|
2047
|
+
sOut.set(fmtTokens(Number(totals.out) || 0), share(Number(totals.out) || 0));
|
|
2048
|
+
sCalls.set(String(calls), calls > 0 ? `成功 ${Number(totals.ok) || 0} · 失败 ${Number(totals.failed) || 0}` : "");
|
|
2049
|
+
tokenBadge.textContent = tokenDays === 0 ? "全部" : `近 ${tokenDays} 天`;
|
|
2050
|
+
tokenBadge.className = `dsw-badge ${total > 0 ? "on" : "off"}`;
|
|
2051
|
+
const series = Array.isArray(data?.series) ? data.series : [];
|
|
2052
|
+
trendBox.innerHTML = total > 0 ? trendSvg(series) : "";
|
|
2053
|
+
if (total <= 0) tokenMsg.textContent = calls > 0 ? `有 ${calls} 次调用,但没有可用的 token 数(失败调用或不带用量的响应不算)。` : "这段时间还没有用量记录 —— 跑一次对话后点「刷新」。";
|
|
2054
|
+
else tokenMsg.textContent = `数据覆盖 ${data?.coverage?.from ?? "—"} ~ ${data?.coverage?.to ?? "—"}(保留 ${data?.keepDays ?? 90} 天)。网页端只在部分响应里给出总量,拿不到时按字符估算,所以这两个数是估算值。`;
|
|
2055
|
+
renderRanks(rankKey === "account" ? data?.byAccount : data?.byModel, rankKey);
|
|
2056
|
+
};
|
|
2057
|
+
const loadUsage = async () => {
|
|
2058
|
+
try {
|
|
2059
|
+
renderUsage(await api(`/usage?days=${tokenDays}`));
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
tokenMsg.textContent = `用量读取失败:${error?.message ?? error}`;
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
paneFirstShow.token = () => void loadUsage();
|
|
1770
2065
|
const transportCard = el("div", "dsw-card");
|
|
1771
2066
|
transportCard.append(el("div", "dsw-cardhead", "传输层(指纹)"));
|
|
1772
2067
|
const transportRow = el("div", "dsw-gate-row");
|
|
@@ -2052,6 +2347,8 @@ background:var(--bg2);white-space:pre-wrap;font-size:12px}
|
|
|
2052
2347
|
swRange.max = String(swBounds.max);
|
|
2053
2348
|
swRange.value = String(g.autoSwitchMinutes ?? g.autoSwitchDefault ?? 0);
|
|
2054
2349
|
paintAutoSwitch();
|
|
2350
|
+
serialInput.checked = g.serialToolCalls === false;
|
|
2351
|
+
paintSerialTools();
|
|
2055
2352
|
const mode = g.cleanup?.mode ?? "deferred";
|
|
2056
2353
|
for (const key of Object.keys(cleanupBtns)) cleanupBtns[key].classList.toggle("active", key === mode);
|
|
2057
2354
|
const showRanges = mode === "deferred";
|