@feiyang666/dsh-usage-plugin 1.9.4 → 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 +17 -0
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/lib/client.js +80 -27
- package/lib/index.js +14 -1
- package/package.json +1 -1
- package/scripts/release.mjs +95 -0
- package/scripts/upload-asset.mjs +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -23,6 +23,23 @@
|
|
|
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
|
+
|
|
34
|
+
## v1.10.0 (2026-08-23)
|
|
35
|
+
|
|
36
|
+
- **新增 DeepSeek 官方模型 `deepseek-v4-flash-vision-exp`(DeepSeek-V4-Flash-Vision-Exp)**:官方价格表、概览「按模型」表、按 API 服务商 × 模型明细、用量日历、缓存命中列表与 PNG 报告均自动按该模型计价;「价格表」页新增该模型一行,`modelKey` 识别优先匹配 `vision`(避免被误归入 `deepseek-v4-flash`)。
|
|
37
|
+
- **价格**(元 / 百万 tokens,与官方最新公布一致):空闲时段 缓存命中 0.05 / 输入未命中 1.5 / 输出 4.5;高峰时段 缓存命中 0.10 / 输入未命中 3.0 / 输出 9.0(与 `deepseek-v4-flash` 同价)。
|
|
38
|
+
- **核对确认**:`deepseek-v4-flash` / `deepseek-v4-pro` 的峰谷价与官方最新公布完全一致,无价格变动;发送给该模型的图片按其尺寸换算成 token,与文本 token 一并计费。
|
|
39
|
+
- **周末统一按空闲(低谷)价计费(2026-08-23 00:00 起)**:新规则生效后,周末(周六、周日)全天不再区分峰谷时段,统一按空闲价计费;工作日仍按北京时间 9:00–12:00、14:00–18:00 高峰分段。新规则生效前的调用仍按原规则结算。插件的高峰时段判定、面板「当前时段」徽标、概览 / 日历 / 缓存列表的高峰与空闲分列统计、PNG 报告均按新规则自动判断。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
26
43
|
## v1.9.4 (2026-08-20)
|
|
27
44
|
|
|
28
45
|
- **持久化不再落到工作目录(修复「数据又进了工作区」的反馈)**:之前插件的落盘走的是模型侧沙箱化的 `fs` 服务,在 `workspace-write` 模式下会被限制只能写工作区;当系统/用户目录因沙箱不可写时,数据会兜底写进 `<工作区>/dsh-usage`,于是切换工作区又出现「数据没了」的现象(即 issue #4 的本质)。
|
package/README.md
CHANGED
|
@@ -32,10 +32,10 @@ dsh-usage-plugin is a **usage & cost tracker** plugin in the DeepSeek Harness ec
|
|
|
32
32
|
|
|
33
33
|
> Supports **Windows / macOS / Linux**: paths are handled per platform (`node:path`), and the folder picker / "reveal in file manager" use each OS's native mechanism (macOS: `osascript` / `open`; Linux: `zenity` / `xdg-open`). Balance query and export do not depend on Windows-only commands.
|
|
34
34
|
|
|
35
|
-
- **Usage & Cost**: records each model call's token usage and cache hits (input miss / cache hit / cache write / output / reasoning / finish reason), and computes cost using DeepSeek's peak/valley or base pricing (peak hours are automatically priced by Beijing time 09:00–12:00 and 14:00–18:00). Model names come from the actual request parameters, so non-DeepSeek models are shown truthfully instead of "unknown model"; models without an official price are counted as 0. The overview shows a by-model table plus a by-API-provider × model drill-down (each provider grouped with every model's calls and peak/off-peak cost split) and a grand total row. The overview also supports **date filtering** (Today / Last 7 days / Last 30 days / All, plus a custom start–end range), so the aggregate stats can be scoped to any single day or date range.
|
|
35
|
+
- **Usage & Cost**: records each model call's token usage and cache hits (input miss / cache hit / cache write / output / reasoning / finish reason), and computes cost using DeepSeek's peak/valley or base pricing (peak hours on weekdays are automatically priced by Beijing time 09:00–12:00 and 14:00–18:00; since 2026-08-23 weekends are billed entirely at the off-peak rate). Model names come from the actual request parameters, so non-DeepSeek models are shown truthfully instead of "unknown model"; models without an official price are counted as 0. The overview shows a by-model table plus a by-API-provider × model drill-down (each provider grouped with every model's calls and peak/off-peak cost split) and a grand total row. The overview also supports **date filtering** (Today / Last 7 days / Last 30 days / All, plus a custom start–end range), so the aggregate stats can be scoped to any single day or date range.
|
|
36
36
|
- **Usage Calendar**: a monthly daily-usage heatmap (colored by cost or call count), hover for details including the peak/off-peak cost split, click a day for its call list and peak/off-peak totals, plus a per-day statistics table with peak cost / off-peak cost / total columns and monthly rollups.
|
|
37
37
|
- **Cache Hit List**: newest-first, fully scrollable, with quick filters (Today / 7 days / 30 days / All) and custom date ranges; the summary line and footer total split peak vs off-peak consumption with a grand cost total. The list is paginated (100 rows per page), so it stays smooth even with large data volumes.
|
|
38
|
-
- **Price Table**: the official DeepSeek API price table — base and peak/valley unit prices shown side by side (peak vs off-peak), editable in-panel and persisted to `pricing.json`, with a reset-to-default option.
|
|
38
|
+
- **Price Table**: the official DeepSeek API price table (covering `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` / `deepseek-v4-pro`) — base and peak/valley unit prices shown side by side (peak vs off-peak), editable in-panel and persisted to `pricing.json`, with a reset-to-default option.
|
|
39
39
|
- **Balance Query**: queries your DeepSeek account balance using the configured `DEEPSEEK_API_KEY`.
|
|
40
40
|
- **Export**: CSV / JSON / **PNG long image** (newest-first, up to the latest 2000 records, warns if exceeded; the PNG report includes peak/off-peak cost columns), to any directory (native picker), auto-opens the folder after export.
|
|
41
41
|
- **Import**: merge-imports JSON / CSV files, deduplicated by time.
|
package/README.zh.md
CHANGED
|
@@ -32,10 +32,10 @@ dsh-usage-plugin 是 DeepSeek Harness 生态的**用量与消耗统计插件**
|
|
|
32
32
|
|
|
33
33
|
> 支持 **Windows / macOS / Linux**:路径按当前平台处理(`node:path`),目录选择与「打开所在目录」均调用系统原生方式(macOS 用 `osascript` / `open`,Linux 用 `zenity` / `xdg-open`),余额查询与导出不依赖 Windows 专用命令。
|
|
34
34
|
|
|
35
|
-
- **用量与消耗**:记录每次模型调用的 token 用量与缓存命中(输入·未命中 / 缓存命中 / 缓存写入 / 输出 / 推理 / 结束原因),按 DeepSeek
|
|
35
|
+
- **用量与消耗**:记录每次模型调用的 token 用量与缓存命中(输入·未命中 / 缓存命中 / 缓存写入 / 输出 / 推理 / 结束原因),按 DeepSeek 峰谷/基础价格计算消耗(工作日高峰时段按北京时间 9:00–12:00、14:00–18:00 计价;自 2026-08-23 起周末全天按空闲价计费)。模型名以请求参数为准如实显示(非 DeepSeek 模型不再显示为「未知模型」,无官方价格的模型消耗按 0 统计)。概览含「按模型」表与「按 API 服务商 × 模型」明细表(每个服务商一组,组内列出各模型的调用与高峰/空闲分列消耗),底部有总费用合计。概览还支持**日期筛选**(今天 / 近 7 天 / 近 30 天 / 全部,外加自定义起止区间),可将聚合统计限定到任意一天或日期范围。
|
|
36
36
|
- **用量日历**:按月查看每日用量热力图(按消耗或调用数着色),悬停查看详情(含高峰 / 空闲消耗拆分)、点击某天查看当日调用明细与高峰/空闲消耗统计,附本月每日统计表(高峰消耗 / 空闲消耗 / 总消耗分列)与月度汇总。
|
|
37
37
|
- **缓存命中列表**:最新记录排在最前,支持 今天 / 近7天 / 近30天 / 全部 快捷筛选与自定义起止日期区间,汇总行与表尾合计区分高峰消耗 / 空闲消耗 / 总费用合计;列表分页渲染(每页 100 条),记录量大也不卡顿。
|
|
38
|
-
- **价格表**:**DeepSeek 官方 API
|
|
38
|
+
- **价格表**:**DeepSeek 官方 API 价格表**(覆盖官方模型 `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` / `deepseek-v4-pro`),展示基础价与峰谷价(高峰/空闲)单价表,高峰价与空闲价分列展示,支持在面板内直接编辑价格并持久化(数据目录 `pricing.json`),也可一键恢复默认。
|
|
39
39
|
- **剩余余额查询**:用当前配置的 `DEEPSEEK_API_KEY` 查询 DeepSeek 账户余额。
|
|
40
40
|
- **导出**:CSV / JSON / **PNG 长图**(按最新在前展示,最多含最近 2000 条,超出会提示;PNG 报告含高峰 / 空闲消耗分列统计),可导出到任意目录(原生目录选择器),导出后自动打开所在目录。
|
|
41
41
|
- **导入**:选择文件(JSON / CSV)合并导入,按时间去重。
|
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
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,16 +79,29 @@ 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, ","); };
|
|
87
101
|
var fmtTime = function (ts) { var d = new Date(ts + 8 * 3600 * 1000); return pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate()) + " " + pad2(d.getUTCHours()) + ":" + pad2(d.getUTCMinutes()) + ":" + pad2(d.getUTCSeconds()); };
|
|
88
102
|
var fmtMoney = function (n) { if (!n) return "¥0.0000"; if (n < 0.0001) return "¥" + n.toExponential(2); if (n < 1) return "¥" + n.toFixed(4); return "¥" + n.toFixed(2); };
|
|
89
103
|
var fmtPrice = function (n) { if (n == null || isNaN(n)) return "—"; return String(parseFloat(Number(n).toFixed(4))); };
|
|
90
|
-
var modelLabel = function (mk) { if (mk === "deepseek-v4-flash") return "deepseek-v4-flash"; if (mk === "deepseek-v4-pro") return "deepseek-v4-pro"; return "未知模型"; };
|
|
104
|
+
var modelLabel = function (mk) { if (mk === "deepseek-v4-flash") return "deepseek-v4-flash"; if (mk === "deepseek-v4-flash-vision-exp") return "deepseek-v4-flash-vision-exp"; if (mk === "deepseek-v4-pro") return "deepseek-v4-pro"; return "未知模型"; };
|
|
91
105
|
// 模型显示名:以请求参数里的真实模型名为准(非 DeepSeek 模型也如实显示),
|
|
92
106
|
// 空模型名时退回已知档位名 / provider / 未知。
|
|
93
107
|
var modelName = function (r) {
|
|
@@ -118,7 +132,15 @@ window.__ModuleLoader__.load({
|
|
|
118
132
|
};
|
|
119
133
|
var finishLabel = function (f) { if (f === "stop") return "完成"; if (f === "tool-calls") return "工具调用"; if (f === "max-tokens") return "超长"; if (f === "error") return "错误"; return f || "—"; };
|
|
120
134
|
var pct = function (a, b) { return b > 0 ? (a / b * 100).toFixed(1) + "%" : "—"; };
|
|
121
|
-
|
|
135
|
+
// 周末统一按空闲价规则生效时间:北京时间 2026-08-23 00:00(周日)。此前(含周末)仍按原峰谷分段。
|
|
136
|
+
var WEEKEND_FLAT_AT = Date.parse("2026-08-23T00:00:00+08:00");
|
|
137
|
+
var isPeakNow = function (ts) {
|
|
138
|
+
var d = new Date(ts + 8 * 3600 * 1000);
|
|
139
|
+
var wd = d.getUTCDay(); // 0=周日 1=周一 … 6=周六
|
|
140
|
+
if (ts >= WEEKEND_FLAT_AT && (wd === 0 || wd === 6)) return false;
|
|
141
|
+
var t = d.getUTCHours() * 60 + d.getUTCMinutes();
|
|
142
|
+
return (t >= 9 * 60 && t < 12 * 60) || (t >= 14 * 60 && t < 18 * 60);
|
|
143
|
+
};
|
|
122
144
|
// 计费档位选择:auto 用 autoCost(按生效日期自动切换),base 用 baseCost,其余用 peakValleyCost
|
|
123
145
|
// 计费档位显示文案
|
|
124
146
|
var regimeLabel = function (regime) {
|
|
@@ -175,7 +197,7 @@ window.__ModuleLoader__.load({
|
|
|
175
197
|
var api = function (payload) {
|
|
176
198
|
return fetch("/usage/api", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).then(function (r) { return r.json(); });
|
|
177
199
|
};
|
|
178
|
-
var PRICE_MODELS = ["deepseek-v4-flash", "deepseek-v4-pro"];
|
|
200
|
+
var PRICE_MODELS = ["deepseek-v4-flash", "deepseek-v4-flash-vision-exp", "deepseek-v4-pro"];
|
|
179
201
|
|
|
180
202
|
// ── Card ──
|
|
181
203
|
function Card(props) {
|
|
@@ -186,6 +208,27 @@ window.__ModuleLoader__.load({
|
|
|
186
208
|
);
|
|
187
209
|
}
|
|
188
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
|
+
|
|
189
232
|
// ── long-image report ──
|
|
190
233
|
function drawReport(canvas, records) {
|
|
191
234
|
records = records.slice();
|
|
@@ -549,6 +592,7 @@ window.__ModuleLoader__.load({
|
|
|
549
592
|
function setCustomTo(v) { setToDate(v); setPreset("custom"); }
|
|
550
593
|
|
|
551
594
|
return el("div", null,
|
|
595
|
+
el(BillingHint, null),
|
|
552
596
|
el("div", { style: st.calBar },
|
|
553
597
|
el("div", { style: st.seg },
|
|
554
598
|
presetBtn("today", "今天"), presetBtn("7d", "近7天"), presetBtn("30d", "近30天"), presetBtn("all", "全部")
|
|
@@ -567,7 +611,7 @@ window.__ModuleLoader__.load({
|
|
|
567
611
|
el(Card, { label: "输入 · 未命中", value: fmtInt(totalMiss), hint: "token" }),
|
|
568
612
|
el(Card, { label: "输入 · 缓存命中", value: fmtInt(totalHit), hint: "命中率 " + hitRate }),
|
|
569
613
|
el(Card, { label: "输出", value: fmtInt(totalOut), hint: "token" }),
|
|
570
|
-
el(Card, { label: "高峰消耗", value: fmtMoney(peakTotal), hint: "
|
|
614
|
+
el(Card, { label: "高峰消耗", value: fmtMoney(peakTotal), hint: "工作日高峰 9:00–12:00、14:00–18:00 · 周末全天空闲价" }),
|
|
571
615
|
el(Card, { label: "空闲消耗", value: fmtMoney(offTotal), hint: "其余空闲时段" }),
|
|
572
616
|
el(Card, { label: "总消耗", value: fmtMoney(totalCost), hint: "高峰 + 空闲 · " + regimeLabel(regime) })
|
|
573
617
|
),
|
|
@@ -730,6 +774,7 @@ window.__ModuleLoader__.load({
|
|
|
730
774
|
function goToday() { setYm({ y: nowY, m: nowM }); setSelectedDay(bjKey(Date.now())); }
|
|
731
775
|
|
|
732
776
|
return el("div", null,
|
|
777
|
+
el(BillingHint, null),
|
|
733
778
|
el("div", { style: st.calBar },
|
|
734
779
|
el("button", { style: st.btn, onClick: prevMonth }, "‹ 上月"),
|
|
735
780
|
el("span", { style: { fontSize: fs(14), fontWeight: 600, minWidth: 120, textAlign: "center" } }, y + "年" + (m + 1) + "月"),
|
|
@@ -755,7 +800,7 @@ window.__ModuleLoader__.load({
|
|
|
755
800
|
el(Card, { label: "输入 · 未命中", value: fmtInt(monthMiss), hint: "token" }),
|
|
756
801
|
el(Card, { label: "输入 · 缓存命中", value: fmtInt(monthHit), hint: "token" }),
|
|
757
802
|
el(Card, { label: "输出", value: fmtInt(monthOut), hint: "token" }),
|
|
758
|
-
el(Card, { label: "高峰消耗", value: fmtMoney(monthPeak), hint: "
|
|
803
|
+
el(Card, { label: "高峰消耗", value: fmtMoney(monthPeak), hint: "工作日高峰 9:00–12:00、14:00–18:00 · 周末全天空闲价" }),
|
|
759
804
|
el(Card, { label: "空闲消耗", value: fmtMoney(monthOff), hint: "其余空闲时段" }),
|
|
760
805
|
el(Card, { label: "本月消耗", value: fmtMoney(monthCost), hint: "高峰 + 空闲 · " + costHint(regime) })
|
|
761
806
|
),
|
|
@@ -879,6 +924,7 @@ window.__ModuleLoader__.load({
|
|
|
879
924
|
var sums = splitTotals(filtered, regime);
|
|
880
925
|
|
|
881
926
|
return el("div", null,
|
|
927
|
+
el(BillingHint, null),
|
|
882
928
|
el("div", { style: st.calBar },
|
|
883
929
|
el("div", { style: st.seg },
|
|
884
930
|
presetBtn("today", "今天"), presetBtn("7d", "近7天"), presetBtn("30d", "近30天"), presetBtn("all", "全部")
|
|
@@ -995,6 +1041,7 @@ window.__ModuleLoader__.load({
|
|
|
995
1041
|
});
|
|
996
1042
|
|
|
997
1043
|
return el("div", null,
|
|
1044
|
+
el(BillingHint, null),
|
|
998
1045
|
el("div", { style: st.sec }, "今日 $ / ¥ 汇率"),
|
|
999
1046
|
el("div", { style: st.cards, marginTop: 8 },
|
|
1000
1047
|
el(Card, { label: "$ → ¥", value: fx.rate ? ("$1 = ¥" + Number(fx.rate).toFixed(4)) : "暂不可用", hint: fx.date ? ("汇率日期 " + fx.date) : "等待获取" }),
|
|
@@ -1018,7 +1065,7 @@ window.__ModuleLoader__.load({
|
|
|
1018
1065
|
el("div", { style: st.note, marginTop: 6 }, effectiveIn()),
|
|
1019
1066
|
regime === "auto"
|
|
1020
1067
|
? el("div", { style: st.note, marginTop: 6 },
|
|
1021
|
-
"
|
|
1068
|
+
"自动模式:按调用时间自动选择计费档位——新价格表生效前的调用按基础价(旧价格表),生效后的调用按峰谷价(工作日高峰时段 9:00–12:00、14:00–18:00 用高峰价,其余时间——含周末全天——用空闲价)。当前显示:",
|
|
1022
1069
|
el("strong", null, showRegime(regime) === "base" ? "基础价表(旧价格表,新价格生效前)" : "峰谷价表(新价格表)"),
|
|
1023
1070
|
"。价格按官方公布固定,不可编辑。"
|
|
1024
1071
|
)
|
|
@@ -1043,7 +1090,7 @@ window.__ModuleLoader__.load({
|
|
|
1043
1090
|
)
|
|
1044
1091
|
),
|
|
1045
1092
|
el("div", { style: st.note, marginTop: 8 },
|
|
1046
|
-
"单位:元 / 百万 tokens
|
|
1093
|
+
"单位:元 / 百万 tokens。工作日高峰时段(北京时间 9:00–12:00、14:00–18:00)用高峰价,其余时间用空闲价(空闲价 = 高峰价的一半);自 2026-08-23 起周末(周六、周日)全天按空闲价计费,新规则生效前仍按原规则。概览、用量日历、缓存命中列表中的消耗均已按高峰 / 空闲分列统计。"
|
|
1047
1094
|
),
|
|
1048
1095
|
el("div", { style: st.sec, marginTop: 18 }, "第三方平台价格与覆盖状态(2026-08-19 核验)"),
|
|
1049
1096
|
el("div", { style: st.note, marginTop: 6 }, "只把能与记录中的 provider/model 可靠匹配的价格用于自动计费;美元价格先按 USD 计算,再乘 USD/CNY 汇率统一折算成人民币。"),
|
|
@@ -1097,12 +1144,16 @@ window.__ModuleLoader__.load({
|
|
|
1097
1144
|
var destDir = destState[0], setDestDir = destState[1];
|
|
1098
1145
|
var canvasNode = null, fileInputNode = null;
|
|
1099
1146
|
var timer = props.timer;
|
|
1147
|
+
// 初始加载中标记:首次请求完成/失败后置 false(后续定时刷新静默进行,避免闪烁)
|
|
1148
|
+
var loadingState = React.useState(true);
|
|
1149
|
+
var loading = loadingState[0], setLoading = loadingState[1];
|
|
1100
1150
|
|
|
1101
1151
|
function refresh() {
|
|
1102
1152
|
api({ action: "list" }).then(function (res) {
|
|
1103
1153
|
setData(res || { records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [] });
|
|
1104
1154
|
setError("");
|
|
1105
|
-
|
|
1155
|
+
setLoading(false);
|
|
1156
|
+
}).catch(function (e) { setError(String((e && e.message) || e)); setLoading(false); });
|
|
1106
1157
|
}
|
|
1107
1158
|
|
|
1108
1159
|
React.useEffect(function () {
|
|
@@ -1212,10 +1263,12 @@ window.__ModuleLoader__.load({
|
|
|
1212
1263
|
el("div", { style: st.note, marginTop: 6 },
|
|
1213
1264
|
"计价说明:DeepSeek 官方与 SiliconFlow 按人民币价格;DigitalOcean 按美元官方价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。"
|
|
1214
1265
|
),
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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),
|
|
1219
1272
|
el("div", { style: st.actions, marginTop: 4 },
|
|
1220
1273
|
el("button", { style: st.btn, onClick: function () { doExport("csv"); } }, "导出 CSV"),
|
|
1221
1274
|
el("button", { style: st.btn, onClick: function () { doExport("json"); } }, "导出 JSON"),
|
|
@@ -1364,7 +1417,7 @@ window.__ModuleLoader__.load({
|
|
|
1364
1417
|
actionUrl ? el("a", { style: st.actionLink, href: actionUrl, target: "_blank", rel: "noreferrer" }, selected.actionLabel || "打开服务商凭据页面") : null
|
|
1365
1418
|
)
|
|
1366
1419
|
: null,
|
|
1367
|
-
s.status === "loading" && !d ? el(
|
|
1420
|
+
s.status === "loading" && !d ? el(LoadingView, { text: "正在查询余额…" }) : null,
|
|
1368
1421
|
d && d.ok
|
|
1369
1422
|
? el("div", { style: st.hero },
|
|
1370
1423
|
el("div", { style: st.heroLabel }, heroLabel),
|
package/lib/index.js
CHANGED
|
@@ -49,6 +49,7 @@ export default {
|
|
|
49
49
|
const PRICING = {
|
|
50
50
|
base: {
|
|
51
51
|
'deepseek-v4-flash': { cacheHit: 0.02, cacheMiss: 1.0, output: 2.0 },
|
|
52
|
+
'deepseek-v4-flash-vision-exp': { cacheHit: 0.02, cacheMiss: 1.0, output: 2.0 },
|
|
52
53
|
'deepseek-v4-pro': { cacheHit: 0.025, cacheMiss: 3.0, output: 6.0 }
|
|
53
54
|
},
|
|
54
55
|
peakValley: {
|
|
@@ -56,6 +57,10 @@ export default {
|
|
|
56
57
|
offPeak: { cacheHit: 0.05, cacheMiss: 1.5, output: 4.5 },
|
|
57
58
|
peak: { cacheHit: 0.1, cacheMiss: 3.0, output: 9.0 }
|
|
58
59
|
},
|
|
60
|
+
'deepseek-v4-flash-vision-exp': {
|
|
61
|
+
offPeak: { cacheHit: 0.05, cacheMiss: 1.5, output: 4.5 },
|
|
62
|
+
peak: { cacheHit: 0.1, cacheMiss: 3.0, output: 9.0 }
|
|
63
|
+
},
|
|
59
64
|
'deepseek-v4-pro': {
|
|
60
65
|
offPeak: { cacheHit: 0.15, cacheMiss: 4.5, output: 13.5 },
|
|
61
66
|
peak: { cacheHit: 0.3, cacheMiss: 9.0, output: 27.0 }
|
|
@@ -63,7 +68,7 @@ export default {
|
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
const DEFAULT_PRICING = JSON.parse(JSON.stringify(PRICING))
|
|
66
|
-
const PRICE_MODELS = ['deepseek-v4-flash', 'deepseek-v4-pro']
|
|
71
|
+
const PRICE_MODELS = ['deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro']
|
|
67
72
|
const SILICONFLOW_PRICING = {
|
|
68
73
|
'deepseek-ai/deepseek-v4-flash': { cacheHit: 0.02, cacheMiss: 1.0, output: 2.0 },
|
|
69
74
|
'deepseek-ai/deepseek-v4-pro': { cacheHit: 1.0, cacheMiss: 12.0, output: 24.0 },
|
|
@@ -80,16 +85,24 @@ export default {
|
|
|
80
85
|
// 新价格表(峰谷价)生效时间:北京时间 2026-08-17 00:00。
|
|
81
86
|
// 在此之前的调用按旧价格表(基础价 base)计费;之后按新价格表(峰谷价)计费。
|
|
82
87
|
const EFFECTIVE_AT = Date.parse('2026-08-17T00:00:00+08:00')
|
|
88
|
+
// 周末统一按低谷(空闲)价计费规则生效时间:北京时间 2026-08-23 00:00(周日)。
|
|
89
|
+
// 生效前(含周末)仍按原峰谷分段计费;生效后周末(周六、周日)全天统一按空闲价,工作日仍按峰谷分段。
|
|
90
|
+
const WEEKEND_FLAT_AT = Date.parse('2026-08-23T00:00:00+08:00')
|
|
83
91
|
|
|
84
92
|
function modelKey(model) {
|
|
85
93
|
const m = String(model || '').toLowerCase()
|
|
94
|
+
if (m.indexOf('vision') >= 0) return 'deepseek-v4-flash-vision-exp'
|
|
86
95
|
if (m.indexOf('flash') >= 0) return 'deepseek-v4-flash'
|
|
87
96
|
if (m.indexOf('pro') >= 0) return 'deepseek-v4-pro'
|
|
88
97
|
return 'unknown'
|
|
89
98
|
}
|
|
90
99
|
|
|
100
|
+
// 高峰时段判定:新规则(2026-08-23 起)周末(周六、周日)全天不再区分峰谷,统一按空闲价;
|
|
101
|
+
// 工作日仍按北京时间 9:00–12:00、14:00–18:00 为高峰;新规则生效前的调用仍按原规则判定。
|
|
91
102
|
function isPeak(ts) {
|
|
92
103
|
const d = new Date(ts + 8 * 3600 * 1000)
|
|
104
|
+
const wd = d.getUTCDay() // 0=周日 1=周一 … 6=周六
|
|
105
|
+
if (ts >= WEEKEND_FLAT_AT && (wd === 0 || wd === 6)) return false
|
|
93
106
|
const t = d.getUTCHours() * 60 + d.getUTCMinutes()
|
|
94
107
|
return (t >= 9 * 60 && t < 12 * 60) || (t >= 14 * 60 && t < 18 * 60)
|
|
95
108
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feiyang666/dsh-usage-plugin",
|
|
3
|
-
"version": "1.
|
|
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",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import https from 'node:https'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
const REPO = 'feiyang-dev/dsh-usage-plugin'
|
|
7
|
+
const TAG = 'v1.11.0'
|
|
8
|
+
|
|
9
|
+
function getCredential() {
|
|
10
|
+
const input = 'protocol=https\nhost=github.com\n\n'
|
|
11
|
+
const res = spawnSync('git', ['credential', 'fill'], { input, encoding: 'utf8' })
|
|
12
|
+
const cred = {}
|
|
13
|
+
for (const l of (res.stdout || '').split('\n')) {
|
|
14
|
+
const idx = l.indexOf('=')
|
|
15
|
+
if (idx > 0) cred[l.slice(0, idx).trim()] = l.slice(idx + 1).trim()
|
|
16
|
+
}
|
|
17
|
+
return cred
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function api(method, apiPath, body, contentType = 'application/json', host = 'api.github.com') {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const cred = getCredential()
|
|
23
|
+
const isBuffer = Buffer.isBuffer(body)
|
|
24
|
+
const req = https.request({
|
|
25
|
+
host,
|
|
26
|
+
path: apiPath,
|
|
27
|
+
method,
|
|
28
|
+
headers: {
|
|
29
|
+
'Authorization': `Bearer ${cred.password || ''}`,
|
|
30
|
+
'User-Agent': 'dsh-usage-plugin-publish',
|
|
31
|
+
'Accept': 'application/vnd.github+json',
|
|
32
|
+
'Content-Type': contentType,
|
|
33
|
+
...(body != null ? { 'Content-Length': isBuffer ? body.length : Buffer.byteLength(body) } : {}),
|
|
34
|
+
},
|
|
35
|
+
rejectUnauthorized: false,
|
|
36
|
+
}, (res) => {
|
|
37
|
+
let data = ''
|
|
38
|
+
res.on('data', (d) => data += d)
|
|
39
|
+
res.on('end', () => resolve({ status: res.statusCode, body: data }))
|
|
40
|
+
})
|
|
41
|
+
req.on('error', (e) => resolve({ status: 0, body: e.message }))
|
|
42
|
+
if (body != null) req.write(body)
|
|
43
|
+
req.end()
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractChangelog(tag) {
|
|
48
|
+
const text = fs.readFileSync(path.join(process.cwd(), 'CHANGELOG.md'), 'utf8')
|
|
49
|
+
const lines = text.split('\n')
|
|
50
|
+
const re = new RegExp('^## ' + tag.replace(/\./g, '\\.') + '\\b')
|
|
51
|
+
const start = lines.findIndex((l) => re.test(l))
|
|
52
|
+
if (start < 0) return ''
|
|
53
|
+
let end = lines.length
|
|
54
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
55
|
+
if (/^## v\d/.test(lines[i])) { end = i; break }
|
|
56
|
+
}
|
|
57
|
+
return lines.slice(start, end).join('\n').trim()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const body = extractChangelog(TAG)
|
|
61
|
+
if (!body) { console.error('changelog section not found for', TAG); process.exit(1) }
|
|
62
|
+
|
|
63
|
+
console.log('Creating release', TAG, 'for', REPO)
|
|
64
|
+
const created = await api('POST', `/repos/${REPO}/releases`, JSON.stringify({
|
|
65
|
+
tag_name: TAG,
|
|
66
|
+
name: TAG,
|
|
67
|
+
body,
|
|
68
|
+
draft: false,
|
|
69
|
+
prerelease: false,
|
|
70
|
+
}))
|
|
71
|
+
console.log('create status:', created.status)
|
|
72
|
+
if (created.status !== 201) {
|
|
73
|
+
console.error('create failed:', created.body.slice(0, 600))
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
const release = JSON.parse(created.body)
|
|
77
|
+
console.log('release id:', release.id, 'url:', release.html_url)
|
|
78
|
+
|
|
79
|
+
// 打包并上传 tarball 作为附件(Windows 下 npm 命令名为 npm.cmd,spawnSync 需要完整命令名)
|
|
80
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
81
|
+
const pack = spawnSync(npmCmd, ['pack'], { encoding: 'utf8' })
|
|
82
|
+
const tgzName = (pack.stdout || '').trim().split('\n').pop()
|
|
83
|
+
if (tgzName && fs.existsSync(tgzName)) {
|
|
84
|
+
const buf = fs.readFileSync(tgzName)
|
|
85
|
+
const assetPath = `/repos/${REPO}/releases/${release.id}/assets?name=${encodeURIComponent(tgzName)}`
|
|
86
|
+
console.log('uploading asset', tgzName, buf.length)
|
|
87
|
+
const asset = await api('POST', assetPath, buf, 'application/octet-stream', 'uploads.github.com')
|
|
88
|
+
console.log('asset status:', asset.status)
|
|
89
|
+
if (asset.status === 201) console.log('asset url:', JSON.parse(asset.body).browser_download_url)
|
|
90
|
+
else console.error('asset body:', asset.body.slice(0, 400))
|
|
91
|
+
try { fs.unlinkSync(tgzName) } catch (e) {}
|
|
92
|
+
} else {
|
|
93
|
+
console.log('no tarball produced; skip asset upload')
|
|
94
|
+
}
|
|
95
|
+
console.log('DONE')
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import https from 'node:https'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
|
|
5
|
+
const REPO = 'feiyang-dev/dsh-usage-plugin'
|
|
6
|
+
const RELEASE_ID = 375097521
|
|
7
|
+
|
|
8
|
+
function getCredential() {
|
|
9
|
+
const input = 'protocol=https\nhost=github.com\n\n'
|
|
10
|
+
const res = spawnSync('git', ['credential', 'fill'], { input, encoding: 'utf8' })
|
|
11
|
+
const cred = {}
|
|
12
|
+
for (const l of (res.stdout || '').split('\n')) {
|
|
13
|
+
const idx = l.indexOf('=')
|
|
14
|
+
if (idx > 0) cred[l.slice(0, idx).trim()] = l.slice(idx + 1).trim()
|
|
15
|
+
}
|
|
16
|
+
return cred
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function api(method, apiPath, body, contentType, host = 'api.github.com') {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const cred = getCredential()
|
|
22
|
+
const isBuffer = Buffer.isBuffer(body)
|
|
23
|
+
const req = https.request({
|
|
24
|
+
host, path: apiPath, method,
|
|
25
|
+
headers: {
|
|
26
|
+
'Authorization': `Bearer ${cred.password || ''}`,
|
|
27
|
+
'User-Agent': 'dsh-usage-plugin-publish',
|
|
28
|
+
'Accept': 'application/vnd.github+json',
|
|
29
|
+
'Content-Type': contentType,
|
|
30
|
+
...(body != null ? { 'Content-Length': isBuffer ? body.length : Buffer.byteLength(body) } : {}),
|
|
31
|
+
},
|
|
32
|
+
rejectUnauthorized: false,
|
|
33
|
+
}, (res) => { let d = ''; res.on('data', (c) => d += c); res.on('end', () => resolve({ status: res.statusCode, body: d })) })
|
|
34
|
+
req.on('error', (e) => resolve({ status: 0, body: e.message }))
|
|
35
|
+
if (body != null) req.write(body)
|
|
36
|
+
req.end()
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const tgz = fs.readdirSync('.').find((f) => f.endsWith('.tgz'))
|
|
41
|
+
if (!tgz) { console.error('no tarball found'); process.exit(1) }
|
|
42
|
+
const buf = fs.readFileSync(tgz)
|
|
43
|
+
const assetPath = `/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${encodeURIComponent(tgz)}`
|
|
44
|
+
console.log('uploading', tgz, buf.length)
|
|
45
|
+
const asset = await api('POST', assetPath, buf, 'application/octet-stream', 'uploads.github.com')
|
|
46
|
+
console.log('asset status:', asset.status)
|
|
47
|
+
if (asset.status === 201) console.log('asset url:', JSON.parse(asset.body).browser_download_url)
|
|
48
|
+
else console.error('asset body:', asset.body.slice(0, 400))
|
|
49
|
+
try { fs.unlinkSync(tgz) } catch (e) {}
|
|
50
|
+
console.log('DONE')
|