@feiyang666/dsh-usage-plugin 1.11.1 → 1.12.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/CHANGELOG.md CHANGED
@@ -8,6 +8,30 @@
8
8
 
9
9
  ---
10
10
 
11
+ ## v1.12.2 (2026-08-23)
12
+
13
+ ### 修复 / Fixed
14
+
15
+ - **移动端宽表仍会逐字竖排(兜底加固)**(`lib/client.js`):v1.12.1 的修复依赖运行时向 `document.head` **注入全局样式表**(`.dsh-usage-table{min-width:720px}` + `@media (max-width:540px)` 折叠中间列)。在部分移动端运行环境(如经 mobile-remote 进出的手机页面)下该样式表因加载时序 / CSP 未生效,导致 9–11 列宽表每列被压到约 1 字符宽、标题与单元格逐字竖排、数据无法阅读。本版把 `min-width:720px` **直接写进表格的内联样式**(内联样式必然生效),窄屏时表格保持 720px、外层 `overflow-x` 容器横向滑动,彻底不再依赖样式表注入;原有注入样式表与 ≤540px 折叠逻辑保留,作为进一步优化的增强项。
16
+
17
+ ---
18
+
19
+ ## v1.12.1 (2026-08-23)
20
+
21
+ ### 修复 / Fixed
22
+
23
+ - **修复移动端「消耗表」「消耗明细」等宽表列标题逐字竖排的问题**(`lib/client.js`):窄屏下 9–11 列宽表每列被压到极限宽度,`word-break` 把「调用」「API 服务商」「输入·未命中」等表头拆成单字竖排。现为所有用量表格注入响应式 CSS:表格强制 `min-width: 720px`,外层 `overflow-x` 容器正常横向滚动、不再逐字换行;极窄屏(≤ 540px)下对宽表自动隐藏第 4 列至倒数第 2 列,只保留「模型 / 服务商 / 调用 / 总消耗」等主标识列与合计列,价格表等窄表不折叠、保留横向滚动。
24
+
25
+ ---
26
+
27
+ ## v1.12.0 (2026-08-23)
28
+
29
+ ### 新增 / New
30
+
31
+ - **SSE 实时推送端点**(`lib/index.js`):新增 `GET /usage/api/events` 事件流端点,数据变化(新调用记录写入 / 导入 / 清除等,即 `persistNow` 落盘成功)时向订阅者推送 `data: {"type":"changed","at":<时间戳>}`。桌面端数据中心等客户端订阅该流即可**即时**感知用量更新(毫秒级),不再依赖轮询。无订阅者时零开销,插件独立使用完全不受影响。
32
+
33
+ ---
34
+
11
35
  ## 开源贡献基础:合并 PR #6(@Martin-soaring-dev,2026-08-20)
12
36
 
13
37
  > 本期(以及后续 v1.9.2 / v1.9.3)建立在社区贡献分支 **`Martin-soaring-dev/prepare_for_contribution`** 之上。该分支由用户上传、经你合并(GitHub Pull Request #6,合并 commit `12a52be`),是插件转向对外开源贡献的形态基础。
package/lib/client.js CHANGED
@@ -40,7 +40,9 @@ window.__ModuleLoader__.load({
40
40
  scroll: { overflowX: "auto", overflowY: "hidden", width: "100%", maxWidth: "100%", minWidth: 0 },
41
41
  // 表格宽度自适应容器(width:100% + 列内容允许换行),不再用 max-content 撑出横向滚动条;
42
42
  // 极窄窗口下容器仍保留 overflow-x 兜底。
43
- tbl: { width: "100%", borderCollapse: "collapse", fontSize: fs(12) },
43
+ // minWidth:720 直接写在内联样式里(内联样式必然生效),不依赖下方注入的全局样式表:
44
+ // 窄屏时表格保持 720px、外层 st.scroll 容器横向滚动,避免 9–11 列被压到逐字竖排。
45
+ tbl: { width: "100%", minWidth: 720, borderCollapse: "collapse", fontSize: fs(12) },
44
46
  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
47
  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
48
  td: { textAlign: "right", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word" },
@@ -92,7 +94,14 @@ window.__ModuleLoader__.load({
92
94
  if (typeof document !== "undefined" && document.head && !document.getElementById("dsh-usage-plugin-style")) {
93
95
  var styleSheetEl = document.createElement("style");
94
96
  styleSheetEl.id = "dsh-usage-plugin-style";
95
- styleSheetEl.textContent = "@keyframes dshUsageSpin{to{transform:rotate(360deg)}}";
97
+ styleSheetEl.textContent = "@keyframes dshUsageSpin{to{transform:rotate(360deg)}}" +
98
+ // 所有用量表格:保证最小宽度,避免在窄屏被压缩导致标题/单元格逐字竖排;
99
+ // 外层 st.scroll 容器已设 overflow-x:auto,无法完整展示时可横向滚动。
100
+ ".dsh-usage-table{min-width:720px}" +
101
+ // 极窄屏 (≤ 540px):对带 collapse-mobile 的宽表隐藏第 4 列至倒数第 2 列,
102
+ // 只保留前 3 个主标识列 + 最后合计列(如 消耗表/明细表 保留 模型/服务商/调用/总消耗),
103
+ // 避免每列被压到极限宽度而逐字竖排。价格表等窄表不折叠,保留横向滚动。
104
+ "@media (max-width:540px){.dsh-usage-table.collapse-mobile tr>*:not(:nth-child(-n+3)):not(:last-child){display:none}.dsh-usage-table.collapse-mobile{min-width:320px}}";
96
105
  document.head.appendChild(styleSheetEl);
97
106
  }
98
107
 
@@ -635,7 +644,7 @@ window.__ModuleLoader__.load({
635
644
  ),
636
645
  el("div", { style: st.sec }, "消耗表(按模型)"),
637
646
  el("div", { style: st.scroll },
638
- el("table", { style: st.tbl },
647
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
639
648
  el("thead", null, el("tr", null,
640
649
  el("th", { style: st.thFirst }, "模型"), el("th", { style: st.thFirst }, "API 服务商"), el("th", { style: st.th }, "调用"), el("th", { style: st.th }, "高峰/空闲"),
641
650
  el("th", { style: st.th }, "输入·未命中"), el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "输出"), el("th", { style: st.th }, "推理"),
@@ -675,7 +684,7 @@ window.__ModuleLoader__.load({
675
684
  ),
676
685
  el("div", { style: st.sec, marginTop: 14 }, "消耗明细(按 API 服务商 × 模型)"),
677
686
  el("div", { style: st.scroll },
678
- el("table", { style: st.tbl },
687
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
679
688
  el("thead", null, el("tr", null,
680
689
  el("th", { style: st.thFirst }, "API 服务商"), el("th", { style: st.thFirst }, "模型"), el("th", { style: st.th }, "调用"), el("th", { style: st.th }, "高峰/空闲"),
681
690
  el("th", { style: st.th }, "输入·未命中"), el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "输出"), el("th", { style: st.th }, "推理"),
@@ -825,7 +834,7 @@ window.__ModuleLoader__.load({
825
834
  monthRows.length === 0
826
835
  ? el("div", { style: st.empty }, "本月暂无记录。")
827
836
  : el("div", { style: st.scroll },
828
- el("table", { style: st.tbl },
837
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
829
838
  el("thead", null, el("tr", null,
830
839
  el("th", { style: st.thFirst }, "日期"), el("th", { style: st.th }, "调用"), el("th", { style: st.th }, "高峰/空闲"), el("th", { style: st.th }, "输入·未命中"), el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "输出"), el("th", { style: st.th }, "高峰消耗"), el("th", { style: st.th }, "空闲消耗"), el("th", { style: st.th }, "总消耗(自动)")
831
840
  )),
@@ -856,7 +865,7 @@ window.__ModuleLoader__.load({
856
865
  selRecords.length === 0
857
866
  ? el("div", { style: st.empty }, "该日无记录。")
858
867
  : el("div", { style: st.scroll },
859
- el("table", { style: st.tbl },
868
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
860
869
  el("thead", null, el("tr", null,
861
870
  el("th", { style: st.thFirst }, "时间(北京)"), el("th", { style: st.thFirst }, "模型"), el("th", { style: st.th }, "输入·未命中"), el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "输出"), el("th", { style: st.th }, "推理"), el("th", { style: st.th }, "时段"), el("th", { style: st.th }, "结束"), el("th", { style: st.th }, "消耗")
862
871
  )),
@@ -964,7 +973,7 @@ window.__ModuleLoader__.load({
964
973
  pageBtn("next", "下一页 ›", curPage >= totalPages, function () { setPage(curPage + 1); })
965
974
  ),
966
975
  el("div", { style: st.scroll },
967
- el("table", { style: st.tbl },
976
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
968
977
  el("thead", null, el("tr", null,
969
978
  el("th", { style: st.thFirst }, "时间(北京)"), el("th", { style: st.thFirst }, "模型"), el("th", { style: st.th }, "输入·未命中"), el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "缓存写入"), el("th", { style: st.th }, "输出"), el("th", { style: st.th }, "推理"), el("th", { style: st.th }, "命中率"), el("th", { style: st.th }, "时段"), el("th", { style: st.th }, "结束"), el("th", { style: st.th }, "消耗")
970
979
  )),
@@ -1091,7 +1100,7 @@ window.__ModuleLoader__.load({
1091
1100
  : null,
1092
1101
  showRegime(regime) === "base"
1093
1102
  ? el("div", { style: st.scroll },
1094
- el("table", { style: st.tbl },
1103
+ el("table", { className: "dsh-usage-table", style: st.tbl },
1095
1104
  el("thead", null, el("tr", null,
1096
1105
  el("th", { style: st.thFirst }, "模型"), el("th", { style: st.th }, "缓存命中(输入)"), el("th", { style: st.th }, "输入 · 未命中"), el("th", { style: st.th }, "输出")
1097
1106
  )),
@@ -1099,7 +1108,7 @@ window.__ModuleLoader__.load({
1099
1108
  )
1100
1109
  )
1101
1110
  : el("div", { style: st.scroll },
1102
- el("table", { style: st.tbl },
1111
+ el("table", { className: "dsh-usage-table", style: st.tbl },
1103
1112
  el("thead", null, el("tr", null,
1104
1113
  el("th", { style: st.thFirst }, "模型"),
1105
1114
  el("th", { style: st.th }, "空闲 · 缓存命中"), el("th", { style: st.th }, "空闲 · 输入"), el("th", { style: st.th }, "空闲 · 输出"),
@@ -1114,7 +1123,7 @@ window.__ModuleLoader__.load({
1114
1123
  el("div", { style: st.sec, marginTop: 18 }, "第三方平台价格与覆盖状态(2026-08-19 核验)"),
1115
1124
  el("div", { style: st.note, marginTop: 6 }, "只把能与记录中的 provider/model 可靠匹配的价格用于自动计费;美元价格先按 USD 计算,再乘 USD/CNY 汇率统一折算成人民币。"),
1116
1125
  el("div", { style: st.scroll, marginTop: 8 },
1117
- el("table", { style: st.tbl },
1126
+ el("table", { className: "dsh-usage-table", style: st.tbl },
1118
1127
  el("thead", null, el("tr", null,
1119
1128
  el("th", { style: st.thFirst }, "API 服务商"), el("th", { style: st.thFirst }, "模型 / 状态"),
1120
1129
  el("th", { style: st.th }, "缓存命中"), el("th", { style: st.th }, "输入"), el("th", { style: st.th }, "输出"), el("th", { style: st.thFirst }, "说明")
package/lib/index.js CHANGED
@@ -286,10 +286,21 @@ export default {
286
286
  return undefined
287
287
  }
288
288
 
289
+ // SSE 实时推送:数据变化(新记录写入 / 导入 / 清除等)时通知订阅者。
290
+ // 订阅端点 /usage/api/events。无订阅者时是 no-op,插件独立使用不受影响。
291
+ const sseClients = new Set()
292
+ function notifyChanged() {
293
+ if (sseClients.size === 0) return
294
+ const payload = 'data: {"type":"changed","at":' + Date.now() + '}\n\n'
295
+ for (const res of sseClients) {
296
+ try { res.write(payload) } catch (e) { sseClients.delete(res) }
297
+ }
298
+ }
299
+
289
300
  function persistNow() {
290
301
  if (!dataPath || !persistOk) return Promise.resolve()
291
302
  const text = JSON.stringify(records)
292
- writeChain = writeChain.then(() => nfsWriteText(dataPath, text)).catch(() => {})
303
+ writeChain = writeChain.then(() => nfsWriteText(dataPath, text)).then(() => { notifyChanged() }).catch(() => {})
293
304
  return writeChain
294
305
  }
295
306
 
@@ -1182,6 +1193,27 @@ export default {
1182
1193
  } catch (e) {
1183
1194
  push('route-register-threw: ' + (e && e.stack ? e.stack : msg(e)))
1184
1195
  }
1196
+ try {
1197
+ // SSE 实时推送端点:数据变化时向订阅者推送 {"type":"changed"}。
1198
+ // 桌面端数据中心订阅该流即可即时感知用量数据更新。
1199
+ webServer.register({
1200
+ kind: 'exact',
1201
+ path: '/usage/api/events',
1202
+ handler: (req, res) => {
1203
+ res.writeHead(200, {
1204
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1205
+ 'Cache-Control': 'no-cache',
1206
+ Connection: 'keep-alive',
1207
+ 'X-Accel-Buffering': 'no',
1208
+ })
1209
+ res.write(':ok\n\n')
1210
+ sseClients.add(res)
1211
+ req.on('close', () => { sseClients.delete(res) })
1212
+ }
1213
+ })
1214
+ } catch (e) {
1215
+ push('events-route-register-threw: ' + msg(e))
1216
+ }
1185
1217
  } else {
1186
1218
  push('route-not-registered (no webServer)')
1187
1219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feiyang666/dsh-usage-plugin",
3
- "version": "1.11.1",
3
+ "version": "1.12.2",
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,8 @@ 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.11.1'
7
+ const VERSION = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')).version
8
+ const TAG = `v${VERSION}`
8
9
 
9
10
  function getCredential() {
10
11
  const input = 'protocol=https\nhost=github.com\n\n'
@@ -76,9 +77,10 @@ if (created.status !== 201) {
76
77
  const release = JSON.parse(created.body)
77
78
  console.log('release id:', release.id, 'url:', release.html_url)
78
79
 
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' })
80
+ // 打包并上传 tarball 作为附件(Windows 下 npm 为 npm.cmd 批处理脚本,经 cmd /c 执行)
81
+ const pack = process.platform === 'win32'
82
+ ? spawnSync('cmd', ['/c', 'npm', 'pack'], { encoding: 'utf8' })
83
+ : spawnSync('npm', ['pack'], { encoding: 'utf8' })
82
84
  const tgzName = (pack.stdout || '').trim().split('\n').pop()
83
85
  if (tgzName && fs.existsSync(tgzName)) {
84
86
  const buf = fs.readFileSync(tgzName)