@feiyang666/dsh-usage-plugin 1.10.0 → 1.11.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 CHANGED
@@ -23,6 +23,14 @@
23
23
 
24
24
  ---
25
25
 
26
+ ## v1.11.0 (2026-08-23)
27
+
28
+ - **高峰 / 空闲时段说明**:概览、用量日历、缓存命中列表、价格表四个页面顶部统一新增「高峰 / 空闲时段说明」,清楚区分**工作日高峰**(周一至周五 9:00–12:00、14:00–18:00)、**工作日空闲**(其余时间)与**周末全天空闲**(自 2026-08-23 起按空闲价,此前仍按原规则)。
29
+ - **加载动画**:用量面板首次进入、数据仍在加载时显示转圈加载动画与提示文字,避免白屏无反馈;余额查询加载中同步使用加载动画。
30
+ - **表格适配**:所有表格改为宽度自适应容器(`width:100%`),单元格内容按需换行,不再因内容超宽出现横向滚动条;极窄窗口下容器保留横向滚动兜底。窗口缩放、不同分辨率下查看更舒适。
31
+
32
+ ---
33
+
26
34
  ## v1.10.0 (2026-08-23)
27
35
 
28
36
  - **新增 DeepSeek 官方模型 `deepseek-v4-flash-vision-exp`(DeepSeek-V4-Flash-Vision-Exp)**:官方价格表、概览「按模型」表、按 API 服务商 × 模型明细、用量日历、缓存命中列表与 PNG 报告均自动按该模型计价;「价格表」页新增该模型一行,`modelKey` 识别优先匹配 `vision`(避免被误归入 `deepseek-v4-flash`)。
package/lib/client.js CHANGED
@@ -38,19 +38,20 @@ window.__ModuleLoader__.load({
38
38
  subtab: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "5px 14px", fontSize: fs(12), cursor: "pointer", color: "inherit" },
39
39
  subtabOn: { border: "1px solid rgba(90,140,255,.5)", background: "rgba(90,140,255,.22)", borderRadius: 6, padding: "5px 14px", fontSize: fs(12), cursor: "pointer", color: "inherit", fontWeight: 600 },
40
40
  scroll: { overflowX: "auto", overflowY: "hidden", width: "100%", maxWidth: "100%", minWidth: 0 },
41
- // 表格宽度用 max-content:内容多宽表就有多宽,容器窄时出现横向滚动条而不是裁剪右侧列
42
- tbl: { width: "max-content", minWidth: "100%", borderCollapse: "collapse", fontSize: fs(12) },
43
- th: { textAlign: "right", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap", opacity: 0.55, fontWeight: 500 },
44
- thFirst: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap", opacity: 0.55, fontWeight: 500 },
45
- td: { textAlign: "right", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap" },
46
- tdFirst: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap" },
47
- tdWrap: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word", minWidth: 120, maxWidth: 220 },
48
- tdTotal: { textAlign: "right", padding: "7px 10px", fontWeight: 600, borderTop: "1px solid rgba(128,128,128,.35)" },
49
- tdTotalFirst: { textAlign: "left", padding: "7px 10px", fontWeight: 600, borderTop: "1px solid rgba(128,128,128,.35)" },
50
- tdClick: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap", cursor: "pointer" },
51
- tdClickSel: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap", cursor: "pointer", color: "#5a8cff", fontWeight: 600 },
52
- tdGroup: { textAlign: "left", padding: "7px 10px", fontWeight: 600, background: "rgba(128,128,128,.09)", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap" },
53
- tdGroupR: { textAlign: "right", padding: "7px 10px", fontWeight: 600, background: "rgba(128,128,128,.09)", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "nowrap" },
41
+ // 表格宽度自适应容器(width:100% + 列内容允许换行),不再用 max-content 撑出横向滚动条;
42
+ // 极窄窗口下容器仍保留 overflow-x 兜底。
43
+ tbl: { width: "100%", borderCollapse: "collapse", fontSize: fs(12) },
44
+ th: { textAlign: "right", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word", opacity: 0.55, fontWeight: 500, lineHeight: 1.5 },
45
+ thFirst: { textAlign: "left", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word", opacity: 0.55, fontWeight: 500, lineHeight: 1.5 },
46
+ td: { textAlign: "right", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word" },
47
+ tdFirst: { textAlign: "left", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word" },
48
+ tdWrap: { textAlign: "left", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word" },
49
+ tdTotal: { textAlign: "right", padding: "7px 8px", fontWeight: 600, borderTop: "1px solid rgba(128,128,128,.35)", whiteSpace: "normal", wordBreak: "break-word" },
50
+ tdTotalFirst: { textAlign: "left", padding: "7px 8px", fontWeight: 600, borderTop: "1px solid rgba(128,128,128,.35)", whiteSpace: "normal", wordBreak: "break-word" },
51
+ tdClick: { textAlign: "left", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word", cursor: "pointer" },
52
+ tdClickSel: { textAlign: "left", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word", cursor: "pointer", color: "#5a8cff", fontWeight: 600 },
53
+ tdGroup: { textAlign: "left", padding: "7px 8px", fontWeight: 600, background: "rgba(128,128,128,.09)", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word" },
54
+ tdGroupR: { textAlign: "right", padding: "7px 8px", fontWeight: 600, background: "rgba(128,128,128,.09)", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word" },
54
55
  badgeHit: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(46,204,113,.18)", color: "#2ecc71", whiteSpace: "nowrap" },
55
56
  badgePeak: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(255,152,0,.18)", color: "#ff9800", whiteSpace: "nowrap" },
56
57
  badgeValley: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(90,140,255,.18)", color: "#5a8cff", whiteSpace: "nowrap" },
@@ -78,9 +79,22 @@ window.__ModuleLoader__.load({
78
79
  legend: { display: "flex", alignItems: "center", gap: 6, fontSize: fs(11), opacity: 0.65, marginTop: 8, flexWrap: "wrap" },
79
80
  legendBar: { display: "inline-block", width: 100, height: 8, borderRadius: 4, background: "linear-gradient(90deg,rgba(128,128,128,.12),rgba(46,134,222,.3),rgba(46,134,222,.85))" },
80
81
  numInput: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 4, padding: "3px 6px", fontSize: fs(12), color: "inherit", width: 78, textAlign: "right" },
81
- selDayTitle: { fontSize: fs(13), fontWeight: 600, marginTop: 14 }
82
+ selDayTitle: { fontSize: fs(13), fontWeight: 600, marginTop: 14 },
83
+ billHint: { border: "1px solid rgba(90,140,255,.35)", background: "rgba(90,140,255,.08)", borderRadius: 8, padding: "10px 14px", marginTop: 10, fontSize: fs(12), lineHeight: 1.9 },
84
+ billHintTitle: { fontWeight: 600, marginBottom: 4, color: "#5a8cff" },
85
+ loading: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, padding: "48px 0", color: "inherit" },
86
+ spinner: { width: 28, height: 28, borderRadius: "50%", border: "3px solid rgba(90,140,255,.22)", borderTopColor: "#5a8cff", animation: "dshUsageSpin .8s linear infinite", flexShrink: 0 },
87
+ loadText: { fontSize: fs(12), opacity: 0.65 }
82
88
  };
83
89
 
90
+ // 注入全局样式(幂等,仅一次):加载动画关键帧等
91
+ if (typeof document !== "undefined" && document.head && !document.getElementById("dsh-usage-plugin-style")) {
92
+ var styleSheetEl = document.createElement("style");
93
+ styleSheetEl.id = "dsh-usage-plugin-style";
94
+ styleSheetEl.textContent = "@keyframes dshUsageSpin{to{transform:rotate(360deg)}}";
95
+ document.head.appendChild(styleSheetEl);
96
+ }
97
+
84
98
  // ── helpers ──
85
99
  var pad2 = function (n) { return (n < 10 ? "0" : "") + n; };
86
100
  var fmtInt = function (n) { return String(Math.round(n || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, ","); };
@@ -194,6 +208,27 @@ window.__ModuleLoader__.load({
194
208
  );
195
209
  }
196
210
 
211
+ // ── 计费时段说明(区分工作日高峰 / 工作日空闲 / 周末全天空闲)──
212
+ function BillingHint(props) {
213
+ return el("div", { style: st.billHint },
214
+ el("div", { style: st.billHintTitle }, "高峰 / 空闲时段说明"),
215
+ el("div", null,
216
+ el("span", { style: st.badgePeak }, "工作日高峰"), " 周一至周五 9:00–12:00、14:00–18:00"
217
+ ),
218
+ el("div", null,
219
+ el("span", { style: st.badgeValley }, "空闲时段"), " 工作日其余时间,以及周末(周六、周日)全天(自 2026-08-23 起;此前仍按原规则分峰谷)"
220
+ )
221
+ );
222
+ }
223
+
224
+ // ── 数据加载动画 ──
225
+ function LoadingView(props) {
226
+ return el("div", { style: st.loading },
227
+ el("div", { style: st.spinner }),
228
+ el("div", { style: st.loadText }, props.text || "正在加载用量数据…")
229
+ );
230
+ }
231
+
197
232
  // ── long-image report ──
198
233
  function drawReport(canvas, records) {
199
234
  records = records.slice();
@@ -557,6 +592,7 @@ window.__ModuleLoader__.load({
557
592
  function setCustomTo(v) { setToDate(v); setPreset("custom"); }
558
593
 
559
594
  return el("div", null,
595
+ el(BillingHint, null),
560
596
  el("div", { style: st.calBar },
561
597
  el("div", { style: st.seg },
562
598
  presetBtn("today", "今天"), presetBtn("7d", "近7天"), presetBtn("30d", "近30天"), presetBtn("all", "全部")
@@ -738,6 +774,7 @@ window.__ModuleLoader__.load({
738
774
  function goToday() { setYm({ y: nowY, m: nowM }); setSelectedDay(bjKey(Date.now())); }
739
775
 
740
776
  return el("div", null,
777
+ el(BillingHint, null),
741
778
  el("div", { style: st.calBar },
742
779
  el("button", { style: st.btn, onClick: prevMonth }, "‹ 上月"),
743
780
  el("span", { style: { fontSize: fs(14), fontWeight: 600, minWidth: 120, textAlign: "center" } }, y + "年" + (m + 1) + "月"),
@@ -887,6 +924,7 @@ window.__ModuleLoader__.load({
887
924
  var sums = splitTotals(filtered, regime);
888
925
 
889
926
  return el("div", null,
927
+ el(BillingHint, null),
890
928
  el("div", { style: st.calBar },
891
929
  el("div", { style: st.seg },
892
930
  presetBtn("today", "今天"), presetBtn("7d", "近7天"), presetBtn("30d", "近30天"), presetBtn("all", "全部")
@@ -1003,6 +1041,7 @@ window.__ModuleLoader__.load({
1003
1041
  });
1004
1042
 
1005
1043
  return el("div", null,
1044
+ el(BillingHint, null),
1006
1045
  el("div", { style: st.sec }, "今日 $ / ¥ 汇率"),
1007
1046
  el("div", { style: st.cards, marginTop: 8 },
1008
1047
  el(Card, { label: "$ → ¥", value: fx.rate ? ("$1 = ¥" + Number(fx.rate).toFixed(4)) : "暂不可用", hint: fx.date ? ("汇率日期 " + fx.date) : "等待获取" }),
@@ -1105,12 +1144,16 @@ window.__ModuleLoader__.load({
1105
1144
  var destDir = destState[0], setDestDir = destState[1];
1106
1145
  var canvasNode = null, fileInputNode = null;
1107
1146
  var timer = props.timer;
1147
+ // 初始加载中标记:首次请求完成/失败后置 false(后续定时刷新静默进行,避免闪烁)
1148
+ var loadingState = React.useState(true);
1149
+ var loading = loadingState[0], setLoading = loadingState[1];
1108
1150
 
1109
1151
  function refresh() {
1110
1152
  api({ action: "list" }).then(function (res) {
1111
1153
  setData(res || { records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [] });
1112
1154
  setError("");
1113
- }).catch(function (e) { setError(String((e && e.message) || e)); });
1155
+ setLoading(false);
1156
+ }).catch(function (e) { setError(String((e && e.message) || e)); setLoading(false); });
1114
1157
  }
1115
1158
 
1116
1159
  React.useEffect(function () {
@@ -1220,10 +1263,12 @@ window.__ModuleLoader__.load({
1220
1263
  el("div", { style: st.note, marginTop: 6 },
1221
1264
  "计价说明:DeepSeek 官方与 SiliconFlow 按人民币价格;DigitalOcean 按美元官方价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。"
1222
1265
  ),
1223
- tab === "overview" ? el(OverviewView, { records: records, regime: "auto" }) : null,
1224
- tab === "calendar" ? el(CalendarView, { records: records, regime: "auto", days: data.days || [] }) : null,
1225
- tab === "cache" ? el(CacheListView, { records: records, regime: "auto" }) : null,
1226
- tab === "prices" ? el(PriceView, { pricing: data.pricing, effectiveAt: data.effectiveAt, fx: data.fx || {}, onFxRefresh: function () { api({ action: "fxRefresh" }).then(refresh).catch(function () {}); }, onChanged: refresh }) : null,
1266
+ loading
1267
+ ? el(LoadingView, null)
1268
+ : (tab === "overview" ? el(OverviewView, { records: records, regime: "auto" }) : null,
1269
+ tab === "calendar" ? el(CalendarView, { records: records, regime: "auto", days: data.days || [] }) : null,
1270
+ tab === "cache" ? el(CacheListView, { records: records, regime: "auto" }) : null,
1271
+ tab === "prices" ? el(PriceView, { pricing: data.pricing, effectiveAt: data.effectiveAt, fx: data.fx || {}, onFxRefresh: function () { api({ action: "fxRefresh" }).then(refresh).catch(function () {}); }, onChanged: refresh }) : null),
1227
1272
  el("div", { style: st.actions, marginTop: 4 },
1228
1273
  el("button", { style: st.btn, onClick: function () { doExport("csv"); } }, "导出 CSV"),
1229
1274
  el("button", { style: st.btn, onClick: function () { doExport("json"); } }, "导出 JSON"),
@@ -1372,7 +1417,7 @@ window.__ModuleLoader__.load({
1372
1417
  actionUrl ? el("a", { style: st.actionLink, href: actionUrl, target: "_blank", rel: "noreferrer" }, selected.actionLabel || "打开服务商凭据页面") : null
1373
1418
  )
1374
1419
  : null,
1375
- s.status === "loading" && !d ? el("div", { style: st.empty }, "正在查询余额…") : null,
1420
+ s.status === "loading" && !d ? el(LoadingView, { text: "正在查询余额…" }) : null,
1376
1421
  d && d.ok
1377
1422
  ? el("div", { style: st.hero },
1378
1423
  el("div", { style: st.heroLabel }, heroLabel),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feiyang666/dsh-usage-plugin",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "DeepSeek Harness usage & cost tracker plugin: per-call token/cache-hit stats, peak/off-peak billing, DeepSeek balance query, CSV/JSON/PNG export with custom destination, and persistent local storage. Ships a host half plus a web client half in one npm package; installs into a DSH profile as a dsh.bundle with one command (dsh plugin --profile web add @feiyang666/dsh-usage-plugin).",
5
5
  "keywords": [
6
6
  "deepseek",
@@ -4,7 +4,7 @@ import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
 
6
6
  const REPO = 'feiyang-dev/dsh-usage-plugin'
7
- const TAG = 'v1.10.0'
7
+ const TAG = 'v1.11.0'
8
8
 
9
9
  function getCredential() {
10
10
  const input = 'protocol=https\nhost=github.com\n\n'
@@ -76,8 +76,9 @@ if (created.status !== 201) {
76
76
  const release = JSON.parse(created.body)
77
77
  console.log('release id:', release.id, 'url:', release.html_url)
78
78
 
79
- // 打包并上传 tarball 作为附件
80
- const pack = spawnSync('npm', ['pack'], { encoding: 'utf8' })
79
+ // 打包并上传 tarball 作为附件(Windows 下 npm 命令名为 npm.cmd,spawnSync 需要完整命令名)
80
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
81
+ const pack = spawnSync(npmCmd, ['pack'], { encoding: 'utf8' })
81
82
  const tgzName = (pack.stdout || '').trim().split('\n').pop()
82
83
  if (tgzName && fs.existsSync(tgzName)) {
83
84
  const buf = fs.readFileSync(tgzName)
@@ -3,7 +3,7 @@ import https from 'node:https'
3
3
  import fs from 'node:fs'
4
4
 
5
5
  const REPO = 'feiyang-dev/dsh-usage-plugin'
6
- const RELEASE_ID = 373691852
6
+ const RELEASE_ID = 375097521
7
7
 
8
8
  function getCredential() {
9
9
  const input = 'protocol=https\nhost=github.com\n\n'