@feiyang666/dsh-usage-plugin 1.10.0 → 1.11.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/CHANGELOG.md CHANGED
@@ -23,6 +23,22 @@
23
23
 
24
24
  ---
25
25
 
26
+ ## v1.11.1 (2026-08-23)
27
+
28
+ - **修复:数据加载后页面空白**:加载动画三目分支误用逗号表达式,只返回最后一个表达式值(`null`),导致加载完成后各子视图全部不渲染;改为将四个子视图作为容器 `div` 的并列子元素,加载完成后正常显示。
29
+ - **计费说明改为面板级固定显示**:「高峰 / 空闲时段说明」固定在用量面板子页签下方,概览、用量日历、缓存命中列表、价格表四个页面切换时始终可见(含数据加载期间)。
30
+ - **当前时段精确显示**:用量面板顶部徽标与价格表页顶部新增「当前 · 工作日高峰时段 / 工作日空闲时段 / 周末空闲时段」动态状态并附说明文字;周末(自 2026-08-23 起)全天按空闲价计费,规则生效前仍按原规则区分。
31
+
32
+ ---
33
+
34
+ ## v1.11.0 (2026-08-23)
35
+
36
+ - **高峰 / 空闲时段说明**:概览、用量日历、缓存命中列表、价格表四个页面顶部统一新增「高峰 / 空闲时段说明」,清楚区分**工作日高峰**(周一至周五 9:00–12:00、14:00–18:00)、**工作日空闲**(其余时间)与**周末全天空闲**(自 2026-08-23 起按空闲价,此前仍按原规则)。
37
+ - **加载动画**:用量面板首次进入、数据仍在加载时显示转圈加载动画与提示文字,避免白屏无反馈;余额查询加载中同步使用加载动画。
38
+ - **表格适配**:所有表格改为宽度自适应容器(`width:100%`),单元格内容按需换行,不再因内容超宽出现横向滚动条;极窄窗口下容器保留横向滚动兜底。窗口缩放、不同分辨率下查看更舒适。
39
+
40
+ ---
41
+
26
42
  ## v1.10.0 (2026-08-23)
27
43
 
28
44
  - **新增 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,23 @@ 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
+ nowPeriod: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginTop: 10, fontSize: fs(12) },
86
+ loading: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, padding: "48px 0", color: "inherit" },
87
+ spinner: { width: 28, height: 28, borderRadius: "50%", border: "3px solid rgba(90,140,255,.22)", borderTopColor: "#5a8cff", animation: "dshUsageSpin .8s linear infinite", flexShrink: 0 },
88
+ loadText: { fontSize: fs(12), opacity: 0.65 }
82
89
  };
83
90
 
91
+ // 注入全局样式(幂等,仅一次):加载动画关键帧等
92
+ if (typeof document !== "undefined" && document.head && !document.getElementById("dsh-usage-plugin-style")) {
93
+ var styleSheetEl = document.createElement("style");
94
+ styleSheetEl.id = "dsh-usage-plugin-style";
95
+ styleSheetEl.textContent = "@keyframes dshUsageSpin{to{transform:rotate(360deg)}}";
96
+ document.head.appendChild(styleSheetEl);
97
+ }
98
+
84
99
  // ── helpers ──
85
100
  var pad2 = function (n) { return (n < 10 ? "0" : "") + n; };
86
101
  var fmtInt = function (n) { return String(Math.round(n || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, ","); };
@@ -127,6 +142,24 @@ window.__ModuleLoader__.load({
127
142
  var t = d.getUTCHours() * 60 + d.getUTCMinutes();
128
143
  return (t >= 9 * 60 && t < 12 * 60) || (t >= 14 * 60 && t < 18 * 60);
129
144
  };
145
+ // 返回当前计费时段 { peak, label, desc },区分:工作日高峰 / 工作日空闲 / 周末空闲(周末自 2026-08-23 起全天按空闲价)
146
+ var periodNow = function (ts) {
147
+ ts = ts || Date.now();
148
+ var d = new Date(ts + 8 * 3600 * 1000);
149
+ var wd = d.getUTCDay(); // 0=周日 1=周一 … 6=周六
150
+ var isWeekend = wd === 0 || wd === 6;
151
+ var t = d.getUTCHours() * 60 + d.getUTCMinutes();
152
+ var inPeakHours = (t >= 9 * 60 && t < 12 * 60) || (t >= 14 * 60 && t < 18 * 60);
153
+ if (isWeekend) {
154
+ if (ts >= WEEKEND_FLAT_AT) return { peak: false, label: "周末空闲时段", desc: "周末(周六、周日)全天按空闲价计费" };
155
+ return inPeakHours
156
+ ? { peak: true, label: "周末高峰时段", desc: "2026-08-23 前仍按原规则分峰谷" }
157
+ : { peak: false, label: "周末空闲时段", desc: "2026-08-23 前仍按原规则分峰谷" };
158
+ }
159
+ return inPeakHours
160
+ ? { peak: true, label: "工作日高峰时段", desc: "周一至周五 9:00–12:00、14:00–18:00" }
161
+ : { peak: false, label: "工作日空闲时段", desc: "工作日其余时间" };
162
+ };
130
163
  // 计费档位选择:auto 用 autoCost(按生效日期自动切换),base 用 baseCost,其余用 peakValleyCost
131
164
  // 计费档位显示文案
132
165
  var regimeLabel = function (regime) {
@@ -194,6 +227,27 @@ window.__ModuleLoader__.load({
194
227
  );
195
228
  }
196
229
 
230
+ // ── 计费时段说明(区分工作日高峰 / 工作日空闲 / 周末全天空闲)──
231
+ function BillingHint(props) {
232
+ return el("div", { style: st.billHint },
233
+ el("div", { style: st.billHintTitle }, "高峰 / 空闲时段说明"),
234
+ el("div", null,
235
+ el("span", { style: st.badgePeak }, "工作日高峰"), " 周一至周五 9:00–12:00、14:00–18:00"
236
+ ),
237
+ el("div", null,
238
+ el("span", { style: st.badgeValley }, "空闲时段"), " 工作日其余时间,以及周末(周六、周日)全天(自 2026-08-23 起;此前仍按原规则分峰谷)"
239
+ )
240
+ );
241
+ }
242
+
243
+ // ── 数据加载动画 ──
244
+ function LoadingView(props) {
245
+ return el("div", { style: st.loading },
246
+ el("div", { style: st.spinner }),
247
+ el("div", { style: st.loadText }, props.text || "正在加载用量数据…")
248
+ );
249
+ }
250
+
197
251
  // ── long-image report ──
198
252
  function drawReport(canvas, records) {
199
253
  records = records.slice();
@@ -1003,6 +1057,10 @@ window.__ModuleLoader__.load({
1003
1057
  });
1004
1058
 
1005
1059
  return el("div", null,
1060
+ el("div", { style: st.nowPeriod },
1061
+ el("span", { style: periodNow().peak ? st.badgePeak : st.badgeValley }, "当前 · " + periodNow().label),
1062
+ el("span", { style: st.sub }, periodNow().desc)
1063
+ ),
1006
1064
  el("div", { style: st.sec }, "今日 $ / ¥ 汇率"),
1007
1065
  el("div", { style: st.cards, marginTop: 8 },
1008
1066
  el(Card, { label: "$ → ¥", value: fx.rate ? ("$1 = ¥" + Number(fx.rate).toFixed(4)) : "暂不可用", hint: fx.date ? ("汇率日期 " + fx.date) : "等待获取" }),
@@ -1105,12 +1163,16 @@ window.__ModuleLoader__.load({
1105
1163
  var destDir = destState[0], setDestDir = destState[1];
1106
1164
  var canvasNode = null, fileInputNode = null;
1107
1165
  var timer = props.timer;
1166
+ // 初始加载中标记:首次请求完成/失败后置 false(后续定时刷新静默进行,避免闪烁)
1167
+ var loadingState = React.useState(true);
1168
+ var loading = loadingState[0], setLoading = loadingState[1];
1108
1169
 
1109
1170
  function refresh() {
1110
1171
  api({ action: "list" }).then(function (res) {
1111
1172
  setData(res || { records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [] });
1112
1173
  setError("");
1113
- }).catch(function (e) { setError(String((e && e.message) || e)); });
1174
+ setLoading(false);
1175
+ }).catch(function (e) { setError(String((e && e.message) || e)); setLoading(false); });
1114
1176
  }
1115
1177
 
1116
1178
  React.useEffect(function () {
@@ -1189,7 +1251,7 @@ window.__ModuleLoader__.load({
1189
1251
 
1190
1252
  var records = data.records || [];
1191
1253
  var dataPath = data.dataPath || "";
1192
- var nowPeak = isPeakNow(Date.now());
1254
+ var nowPeriod = periodNow(Date.now());
1193
1255
  var effectiveAt = data.effectiveAt || 0;
1194
1256
  var nowEffective = effectiveAt ? (Date.now() >= effectiveAt) : true;
1195
1257
  return el("div", { style: st.root },
@@ -1200,7 +1262,7 @@ window.__ModuleLoader__.load({
1200
1262
  el("div", { style: st.title }, "用量与消耗"),
1201
1263
  el("div", { style: st.sub }, "记录插件激活后的每一次模型调用")
1202
1264
  ),
1203
- el("span", { style: nowPeak ? st.badgePeak : st.badgeValley }, nowPeak ? "当前 · 高峰时段" : "当前 · 空闲时段"),
1265
+ el("span", { style: nowPeriod.peak ? st.badgePeak : st.badgeValley }, "当前 · " + nowPeriod.label),
1204
1266
  el("span", { style: nowEffective ? st.badgeHit : st.badgeValley }, nowEffective ? "新价格已生效" : "新价格未生效")
1205
1267
  ),
1206
1268
  el("div", { style: st.actions },
@@ -1217,13 +1279,18 @@ window.__ModuleLoader__.load({
1217
1279
  }, t.t);
1218
1280
  })
1219
1281
  ),
1282
+ el(BillingHint, null),
1220
1283
  el("div", { style: st.note, marginTop: 6 },
1221
1284
  "计价说明:DeepSeek 官方与 SiliconFlow 按人民币价格;DigitalOcean 按美元官方价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。"
1222
1285
  ),
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,
1286
+ loading
1287
+ ? el(LoadingView, null)
1288
+ : el("div", null,
1289
+ tab === "overview" ? el(OverviewView, { records: records, regime: "auto" }) : null,
1290
+ tab === "calendar" ? el(CalendarView, { records: records, regime: "auto", days: data.days || [] }) : null,
1291
+ tab === "cache" ? el(CacheListView, { records: records, regime: "auto" }) : null,
1292
+ 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
1293
+ ),
1227
1294
  el("div", { style: st.actions, marginTop: 4 },
1228
1295
  el("button", { style: st.btn, onClick: function () { doExport("csv"); } }, "导出 CSV"),
1229
1296
  el("button", { style: st.btn, onClick: function () { doExport("json"); } }, "导出 JSON"),
@@ -1372,7 +1439,7 @@ window.__ModuleLoader__.load({
1372
1439
  actionUrl ? el("a", { style: st.actionLink, href: actionUrl, target: "_blank", rel: "noreferrer" }, selected.actionLabel || "打开服务商凭据页面") : null
1373
1440
  )
1374
1441
  : null,
1375
- s.status === "loading" && !d ? el("div", { style: st.empty }, "正在查询余额…") : null,
1442
+ s.status === "loading" && !d ? el(LoadingView, { text: "正在查询余额…" }) : null,
1376
1443
  d && d.ok
1377
1444
  ? el("div", { style: st.hero },
1378
1445
  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.1",
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.1'
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 批处理脚本,需经 shell 执行)
80
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
81
+ const pack = spawnSync(npmCmd, ['pack'], { encoding: 'utf8', shell: process.platform === 'win32' })
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 = 375098880
7
7
 
8
8
  function getCredential() {
9
9
  const input = 'protocol=https\nhost=github.com\n\n'